feat: oakengine facade, oaknode/oakrender impls, worker+CLI, app skeleton
- oaknode Rust crate: full implementation (core engine, sequence/ track/block/footage, traverser, serializer, 43 node behaviors; 493 tests green) - oakrender Rust crate: full implementation incl. wgpu backend skeleton, ticket arena, worker pool (136 tests green; fixed lost-wakeup and ticket ordering races) - src/facade/rust (oakfacade): 222 oakengine_* exports over the module C ABIs (61 tests green); worker_main + real POSIX shm frame-slot transport (SpscRingBuffer/FrameSlotPool, wire-compatible with engine/render/ipc) - cli/rust + worker/rust binaries (29 + 29 tests green) - oakotio: FCPXML import/export (49 tests green) - oaktask: OTIO/FCPXML format dispatch (90 tests green) - app/rust: gpui app skeleton — dock panels (viewers/timeline/ explorer/inspector/node editor), transport, olive themes, i18n (en/zh), 37 tests green - gpui submodule: menu checkmarks, dock ratios, vertical meter, CPU-frame viewer surface, drop-frame timecode
This commit is contained in:
Generated
+191
@@ -0,0 +1,191 @@
|
||||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.6.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
|
||||
|
||||
[[package]]
|
||||
name = "oak-cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"oakfacade",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oakfacade"
|
||||
version = "0.1.0"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.107"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.47"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
@@ -0,0 +1,43 @@
|
||||
# Oak - Non-Linear Video Editor
|
||||
# Copyright (C) 2026 Oak Team
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
[package]
|
||||
name = "oak-cli"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
description = "Oak Video Editor headless command-line consumer of the liboakengine C ABI facade (Rust)"
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[[bin]]
|
||||
name = "oak-cli"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
|
||||
# oakfacade is where the frozen oakengine_* C ABI exports live (staticlib +
|
||||
# rlib). oak-cli is a pure consumer of that facade, so it links the rlib
|
||||
# directly. The families this CLI needs (init / project / timeline / render /
|
||||
# footage / exporter) are still deferred in the facade
|
||||
# (src/facade/rust/src/deferred.rs), so this crate references no oakengine_*
|
||||
# symbol yet: the subcommands validate their arguments and report the
|
||||
# deferral (src/deferred.rs). The extern declarations in src/ffi.rs mirror
|
||||
# the engine headers verbatim and resolve against this rlib the moment a
|
||||
# family is wrapped -- no manifest change needed.
|
||||
oakfacade = { path = "../../src/facade/rust" }
|
||||
|
||||
[profile.release]
|
||||
panic = "unwind"
|
||||
@@ -0,0 +1,80 @@
|
||||
# oak-cli (Rust)
|
||||
|
||||
Headless command-line consumer of the `liboakengine` C ABI facade — the Rust
|
||||
rewrite of `cli/main.cpp` (which stays in the tree until cutover). Same
|
||||
subcommands, same output format, same exit codes:
|
||||
|
||||
| exit | meaning |
|
||||
|---|---|
|
||||
| 0 | success |
|
||||
| 1 | general error (bad project/media file, no sequence, I/O failure) |
|
||||
| 2 | rendering unavailable or failed (e.g. no GL render backend) |
|
||||
| 64 | usage error |
|
||||
|
||||
## Build and test
|
||||
|
||||
```sh
|
||||
cargo build --release # binary: target/release/oak-cli
|
||||
cargo test # unit + integration tests (29 tests)
|
||||
```
|
||||
|
||||
The crate builds standalone: its only dependency besides `clap` is the
|
||||
`oakfacade` rlib (`../../src/facade/rust`), which has no third-party
|
||||
dependencies.
|
||||
|
||||
## Subcommands
|
||||
|
||||
Every subcommand of the C++ original is implemented:
|
||||
|
||||
```
|
||||
oak-cli info <project.ove> <start> <end> <out_dir> project name/sequences/footage
|
||||
oak-cli render <project.ove> <start_seconds> <end_seconds> <out_dir>
|
||||
oak-cli probe <mediafile>
|
||||
oak-cli transcode <input_media> <out> [width] [--format ppm|mp4]
|
||||
```
|
||||
|
||||
Argument validation is faithful to the C++ (`invalid start seconds`,
|
||||
`invalid width`, `unknown --format` … all exit 64). The output formatters
|
||||
(`src/fmt.rs`) reproduce the C++ `printf` output byte for byte and are
|
||||
golden-tested against the output captured from the C++ binary on the test
|
||||
fixtures (`tests/project_with_footage.ove`, `tests/demo.mp4`); the PPM and
|
||||
WAV writers (`src/ppm.rs`, `src/wav.rs`) are the exact ports of the C++
|
||||
`write_ppm`/`write_wav` and are unit-tested.
|
||||
|
||||
## Facade status: everything is currently deferred
|
||||
|
||||
All four subcommands depend on facade families that are still **deferred**
|
||||
in the `oakfacade` crate (`src/facade/rust/src/deferred.rs`), so today each
|
||||
subcommand validates its arguments, then prints a clear "not yet available"
|
||||
error naming the missing families and the reasons, and exits with the
|
||||
C++-compatible code — it never crashes and never fakes output:
|
||||
|
||||
| subcommand | needs | current behavior |
|
||||
|---|---|---|
|
||||
| `info` | init + node (project/footage) + timeline | "not yet available", exit 1 |
|
||||
| `probe` | init + node (footage) | "not yet available", exit 1 |
|
||||
| `render` | init + node + timeline + render | "not yet available", exit 2 |
|
||||
| `transcode` | init + node + timeline + render + exporter | "not yet available", exit 2 |
|
||||
|
||||
The deferral registry is `src/deferred.rs` (field-for-field in sync with the
|
||||
facade's own `deferred.rs`). When a family is wrapped by the facade:
|
||||
|
||||
1. remove its entry from `src/deferred.rs`,
|
||||
2. wire the call-through in `src/cmd/` using the extern declarations in
|
||||
`src/ffi.rs` (verbatim mirrors of the engine headers) and the tested
|
||||
formatters/writers — no manifest or signature change is needed, because
|
||||
the externs resolve against the already-linked `oakfacade` rlib.
|
||||
|
||||
## Layout
|
||||
|
||||
```
|
||||
src/
|
||||
main.rs clap surface, --help/-h + unknown-command handling, dispatch
|
||||
ffi.rs the oakengine_* surface oak-cli consumes (declarations only)
|
||||
deferred.rs facade-family availability registry (mirror of facade deferred.rs)
|
||||
fmt.rs golden output formatters (info/probe)
|
||||
ppm.rs P6 PPM writer (f32/u8 frames)
|
||||
wav.rs PCM s16 WAV writer (interleaved float samples)
|
||||
cmd/ per-subcommand validation + deferred gate
|
||||
tests/cli.rs binary-level tests (exit codes, messages, usage errors)
|
||||
```
|
||||
@@ -0,0 +1,44 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! `oak-cli info <project.ove>` — print the project name, its sequences and
|
||||
//! its footage (port of `cmd_info()` in cli/main.cpp).
|
||||
|
||||
use crate::cmd::{port_not_wired, require_or, EXIT_ERROR};
|
||||
|
||||
/// Run `info`. `project` is the .ove path from the command line.
|
||||
pub fn run(project: String) -> i32 {
|
||||
if let Err(code) = require_or(
|
||||
"info",
|
||||
&[
|
||||
&crate::deferred::INIT,
|
||||
&crate::deferred::NODE,
|
||||
&crate::deferred::TIMELINE,
|
||||
],
|
||||
EXIT_ERROR,
|
||||
) {
|
||||
return code;
|
||||
}
|
||||
// Facade port (unreachable while the families above are deferred):
|
||||
// oakengine_init(OAKENGINE_INIT_HEADLESS)
|
||||
// project_create + project_load(project, ...)
|
||||
// name/filename/is_modified/sequence_count/sequence_at(...) +
|
||||
// fmt::sequence() / fmt::footage_entry() for each
|
||||
// project_free + oakengine_shutdown()
|
||||
// The formatters already exist in crate::fmt and are golden-tested.
|
||||
let _ = &project;
|
||||
port_not_wired("info", EXIT_ERROR)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Subcommand implementations.
|
||||
//!
|
||||
//! Each subcommand is a faithful port of its `cli/main.cpp` counterpart:
|
||||
//! the argument validation is real (same messages, same usage-error code),
|
||||
//! and the facade work gates on [`crate::deferred::require`] — while the
|
||||
//! families a subcommand needs are deferred, it prints the "not yet
|
||||
//! available" error with the reasons and exits with the C++-compatible code
|
||||
//! (1 for info/probe, 2 for render/transcode), never crashing.
|
||||
|
||||
pub mod info;
|
||||
pub mod probe;
|
||||
pub mod render;
|
||||
pub mod transcode;
|
||||
|
||||
use crate::deferred::DeferredFamily;
|
||||
|
||||
/// 0 — success.
|
||||
pub const EXIT_OK: i32 = 0;
|
||||
/// 1 — general error (bad project/media file, no sequence, I/O failure).
|
||||
pub const EXIT_ERROR: i32 = 1;
|
||||
/// 2 — rendering unavailable or failed (e.g. no GL render backend).
|
||||
pub const EXIT_RENDER_UNAVAILABLE: i32 = 2;
|
||||
/// 64 — usage error.
|
||||
pub const EXIT_USAGE: i32 = 64;
|
||||
|
||||
/// Gate a subcommand on its facade families.
|
||||
///
|
||||
/// When every family is wrapped this returns `Ok(())` and the subcommand's
|
||||
/// port runs; when any is deferred it prints the composed "not yet
|
||||
/// available" message to stderr and returns `Err(unavailable_code)` — the
|
||||
/// code the C++ binary would exit with when that family's work is
|
||||
/// impossible (1 for info/probe, 2 for render/transcode).
|
||||
pub fn require_or(
|
||||
cmd: &str,
|
||||
families: &[&DeferredFamily],
|
||||
unavailable_code: i32,
|
||||
) -> Result<(), i32> {
|
||||
match crate::deferred::require(families) {
|
||||
Ok(()) => Ok(()),
|
||||
Err(msg) => {
|
||||
eprintln!("error: {cmd}: {msg}");
|
||||
Err(unavailable_code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Fallback for the (today unreachable) success arm of `require_or`: the
|
||||
/// gate reported the families available, but the call-through port is not
|
||||
/// wired yet. Never panics; reports an internal error and returns `code`.
|
||||
pub fn port_not_wired(cmd: &str, code: i32) -> i32 {
|
||||
eprintln!(
|
||||
"error: {cmd}: internal error: facade families reported available but no port is wired yet"
|
||||
);
|
||||
code
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! `oak-cli probe <mediafile>` — probe a media file and print its decoder,
|
||||
//! duration and video/audio/subtitle streams (port of `cmd_probe()` in
|
||||
//! cli/main.cpp).
|
||||
|
||||
use crate::cmd::{port_not_wired, require_or, EXIT_ERROR};
|
||||
|
||||
/// Run `probe`. `mediafile` is the media path from the command line.
|
||||
pub fn run(mediafile: String) -> i32 {
|
||||
if let Err(code) = require_or(
|
||||
"probe",
|
||||
&[&crate::deferred::INIT, &crate::deferred::NODE],
|
||||
EXIT_ERROR,
|
||||
) {
|
||||
return code;
|
||||
}
|
||||
// Facade port (unreachable while the families above are deferred):
|
||||
// oakengine_init(OAKENGINE_INIT_HEADLESS)
|
||||
// footage_probe(mediafile) -> decoder_name/duration/stream infos,
|
||||
// formatted with the fmt::* lines (golden-tested)
|
||||
// footage_free + oakengine_shutdown()
|
||||
let _ = &mediafile;
|
||||
port_not_wired("probe", EXIT_ERROR)
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! `oak-cli render <project.ove> <start_seconds> <end_seconds> <out_dir>` —
|
||||
//! render the first sequence to PPM frames plus a PCM s16 WAV (port of
|
||||
//! `cmd_render()` in cli/main.cpp).
|
||||
|
||||
use crate::cmd::{port_not_wired, require_or, EXIT_RENDER_UNAVAILABLE, EXIT_USAGE};
|
||||
|
||||
/// Run `render` with the validated (or rejected) seconds arguments.
|
||||
///
|
||||
/// The seconds are validated exactly like the C++ `strtod` checks before any
|
||||
/// facade work; the facade work itself (init + project + sequence + renderer,
|
||||
/// then [`crate::ppm::write_ppm`] / [`crate::wav::write_wav`] per frame) is
|
||||
/// gated on the deferred families below.
|
||||
pub fn run(project: String, start_seconds: &str, end_seconds: &str, out_dir: &str) -> i32 {
|
||||
let start: f64 = match start_seconds.parse() {
|
||||
Ok(v) => v,
|
||||
Err(_) => {
|
||||
eprintln!("error: invalid start seconds \"{start_seconds}\"");
|
||||
return EXIT_USAGE;
|
||||
}
|
||||
};
|
||||
let end: f64 = match end_seconds.parse() {
|
||||
Ok(v) => v,
|
||||
Err(_) => {
|
||||
eprintln!("error: invalid end seconds \"{end_seconds}\"");
|
||||
return EXIT_USAGE;
|
||||
}
|
||||
};
|
||||
if end <= start {
|
||||
eprintln!("error: invalid end seconds \"{end_seconds}\"");
|
||||
return EXIT_USAGE;
|
||||
}
|
||||
|
||||
if let Err(code) = require_or(
|
||||
"render",
|
||||
&[
|
||||
&crate::deferred::INIT,
|
||||
&crate::deferred::NODE,
|
||||
&crate::deferred::TIMELINE,
|
||||
&crate::deferred::RENDER,
|
||||
],
|
||||
EXIT_RENDER_UNAVAILABLE,
|
||||
) {
|
||||
return code;
|
||||
}
|
||||
// Facade port (unreachable while the families above are deferred):
|
||||
// oakengine_init(HEADLESS | RENDER), chdir to the project dir,
|
||||
// project_load, sequence 0 frame rate -> start_ts/end_ts,
|
||||
// renderer_create(f32, fr_num, fr_den), then for each timestamp
|
||||
// render_frame -> ppm::write_ppm (progress on stderr), then
|
||||
// render_audio -> wav::write_wav. Both writers are golden-tested.
|
||||
let _ = (&project, &start, &end, &out_dir);
|
||||
port_not_wired("render", EXIT_RENDER_UNAVAILABLE)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! `oak-cli transcode <input_media> <out> [width] [--format ppm|mp4]` —
|
||||
//! "media in, renders out" round trip (port of `cmd_transcode()` in
|
||||
//! cli/main.cpp).
|
||||
|
||||
use crate::cmd::{port_not_wired, require_or, EXIT_RENDER_UNAVAILABLE, EXIT_USAGE};
|
||||
|
||||
/// Run `transcode`. `width`/`format` are validated exactly like the C++ loop
|
||||
/// over `argv[4..]`; the facade work is gated on the deferred families below.
|
||||
pub fn run(
|
||||
input_media: String,
|
||||
out: String,
|
||||
width: Option<String>,
|
||||
format: Option<String>,
|
||||
) -> i32 {
|
||||
if let Some(w) = &width {
|
||||
match w.parse::<i64>() {
|
||||
Ok(n) if n > 0 => {}
|
||||
_ => {
|
||||
eprintln!("error: invalid width \"{w}\"");
|
||||
return EXIT_USAGE;
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some(f) = &format {
|
||||
if f != "ppm" && f != "mp4" {
|
||||
eprintln!("error: unknown --format \"{f}\" (ppm|mp4)");
|
||||
return EXIT_USAGE;
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(code) = require_or(
|
||||
"transcode",
|
||||
&[
|
||||
&crate::deferred::INIT,
|
||||
&crate::deferred::NODE,
|
||||
&crate::deferred::TIMELINE,
|
||||
&crate::deferred::RENDER,
|
||||
&crate::deferred::EXPORT,
|
||||
],
|
||||
EXIT_RENDER_UNAVAILABLE,
|
||||
) {
|
||||
return code;
|
||||
}
|
||||
// Facade port (unreachable while the families above are deferred):
|
||||
// probe the source for geometry/fps/duration, build a temporary
|
||||
// project (new + import_footage + sequence_new + add_track x2 +
|
||||
// add_footage_clip x2), then either the ppm path (render_frame /
|
||||
// render_audio -> ppm::write_ppm / wav::write_wav) or the mp4 path
|
||||
// (oakengine_export_render with H.264/AAC options + progress
|
||||
// callback). The C++ exits 2 when the render/export backend is
|
||||
// unavailable, which is also the code used here.
|
||||
let _ = (&input_media, &out, &width, &format);
|
||||
port_not_wired("transcode", EXIT_RENDER_UNAVAILABLE)
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Facade-family availability, mirroring `src/facade/rust/src/deferred.rs`.
|
||||
//!
|
||||
//! Every `oak-cli` subcommand depends on one or more families of the
|
||||
//! `oakengine_*` C ABI. Those families live in the `oakfacade` crate, and
|
||||
//! some of them are **deferred**: the facade does not wrap them yet, so the
|
||||
//! subcommands must report a clear "not yet available" error instead of
|
||||
//! calling into the facade (the calls would not link, and faking behavior
|
||||
//! would be worse).
|
||||
//!
|
||||
//! The entries below are kept field-for-field in sync with the facade's own
|
||||
//! deferral documentation (`src/facade/rust/src/deferred.rs`). All families
|
||||
//! this CLI consumes are currently deferred; when a family is wrapped, remove
|
||||
//! its entry here and the subcommand's call-through (see `src/cmd/`) becomes
|
||||
//! reachable.
|
||||
|
||||
/// One deferred facade family: what it covers, which engine headers define
|
||||
/// it, and why the facade does not wrap it yet.
|
||||
pub struct DeferredFamily {
|
||||
/// Short family name, as used in messages.
|
||||
pub name: &'static str,
|
||||
/// Engine headers involved.
|
||||
pub headers: &'static str,
|
||||
/// Why the family is not wrapped yet (from the facade's deferred.rs).
|
||||
pub reason: &'static str,
|
||||
}
|
||||
|
||||
/// `init.h` — engine process initialization/shutdown.
|
||||
///
|
||||
/// Not even listed in the facade's scope table yet (`src/facade/rust/README.md`):
|
||||
/// the facade currently wraps only undo/config/video_params/audio/plugin.
|
||||
pub const INIT: DeferredFamily = DeferredFamily {
|
||||
name: "init",
|
||||
headers: "init.h",
|
||||
reason: "the facade shell (oakengine_init/shutdown) is not wrapped in oakfacade yet (its scope table covers only undo/common/audio/plugin)",
|
||||
};
|
||||
|
||||
/// `project.h` + `footage.h` — the oaknode module family.
|
||||
///
|
||||
/// Facade deferred.rs "node": all 30 exports of the oaknode Rust crate are
|
||||
/// `todo!()` bodies, so the engine project/footage families have no module
|
||||
/// backing to wrap.
|
||||
pub const NODE: DeferredFamily = DeferredFamily {
|
||||
name: "node (project/footage)",
|
||||
headers: "project.h, footage.h",
|
||||
reason: "deferred: the oaknode crate is an unimplemented skeleton (every export is a todo!() body), so the project/footage families have no module backing",
|
||||
};
|
||||
|
||||
/// `timeline.h` — sequence/track/clip family.
|
||||
///
|
||||
/// Facade deferred.rs "timeline": the oaktimeline crate's exports reference
|
||||
/// ~80 oaknode C ABI symbols the skeletal oaknode crate does not define, and
|
||||
/// its test-stubs collide with the real oakundo crate in the facade test
|
||||
/// link.
|
||||
pub const TIMELINE: DeferredFamily = DeferredFamily {
|
||||
name: "timeline",
|
||||
headers: "timeline.h",
|
||||
reason: "deferred: test linkage — the oaktimeline crate's exports reference oaknode C ABI symbols the skeletal oaknode crate does not define",
|
||||
};
|
||||
|
||||
/// `renderer.h` — renderer/frame/audio-buffer family.
|
||||
///
|
||||
/// Facade deferred.rs "render": no structural blocker; the engine renderer.h
|
||||
/// family simply was not wrapped in the facade's current pass.
|
||||
pub const RENDER: DeferredFamily = DeferredFamily {
|
||||
name: "render",
|
||||
headers: "renderer.h",
|
||||
reason: "deferred for session scope: the engine renderer.h family is not wrapped in oakfacade yet (no structural blocker)",
|
||||
};
|
||||
|
||||
/// `exporter.h` — export/encode family.
|
||||
///
|
||||
/// Facade deferred.rs: exporter is a "genuinely facade-only area" (the
|
||||
/// liboakengine assembly layer) with no files in the oakfacade crate.
|
||||
pub const EXPORT: DeferredFamily = DeferredFamily {
|
||||
name: "exporter",
|
||||
headers: "exporter.h",
|
||||
reason: "deferred: the exporter family is a facade-only assembly area with no Rust backing (src/facade/rust/src/deferred.rs)",
|
||||
};
|
||||
|
||||
/// Check that every family in `families` is available in the facade.
|
||||
///
|
||||
/// Returns `Ok(())` when all are wrapped (none is today); otherwise `Err`
|
||||
/// carries the composed "not yet available" message naming each deferred
|
||||
/// family and its reason, for the subcommands to print and exit on.
|
||||
pub fn require(families: &[&DeferredFamily]) -> Result<(), String> {
|
||||
if families.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let mut detail = String::new();
|
||||
for f in families {
|
||||
detail.push_str(&format!("\n - {} ({}): {}", f.name, f.headers, f.reason));
|
||||
}
|
||||
Err(format!(
|
||||
"not yet available in the Rust facade (oakfacade): these family(ies) are still deferred \
|
||||
(see src/facade/rust/src/deferred.rs):{detail}"
|
||||
))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn empty_family_list_is_available() {
|
||||
assert!(require(&[]).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deferred_family_lists_a_reason() {
|
||||
let err = require(&[&INIT]).unwrap_err();
|
||||
assert!(err.contains("not yet available"));
|
||||
assert!(err.contains("init"));
|
||||
assert!(err.contains("oakfacade"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_cli_families_are_currently_deferred() {
|
||||
// Keeps this file honest: if any family the CLI depends on flips to
|
||||
// available, the subcommand ports in src/cmd/ become reachable and
|
||||
// the tests asserting "not yet available" must be revisited.
|
||||
let all: [&[&DeferredFamily]; 5] = [
|
||||
&[&INIT],
|
||||
&[&NODE],
|
||||
&[&TIMELINE],
|
||||
&[&RENDER],
|
||||
&[&EXPORT],
|
||||
];
|
||||
for families in all {
|
||||
assert!(require(families).is_err());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,409 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! The `oakengine_*` C ABI surface oak-cli consumes — **declared, not yet
|
||||
//! linked**.
|
||||
//!
|
||||
//! This module mirrors — verbatim — every function, opaque handle and POD
|
||||
//! struct from the engine headers that the C++ `cli/main.cpp` touches:
|
||||
//!
|
||||
//! - `engine/include/oakengine/init.h` (oakengine_init / shutdown)
|
||||
//! - `engine/include/oakengine/project.h` (project lifecycle + queries)
|
||||
//! - `engine/include/oakengine/footage.h` (probe / stream info / import)
|
||||
//! - `engine/include/oakengine/timeline.h` (sequence + track/clip editing)
|
||||
//! - `engine/include/oakengine/renderer.h` (renderer + frame + audio buffer)
|
||||
//! - `engine/include/oakengine/exporter.h` (export options + render)
|
||||
//!
|
||||
//! All of these families are **deferred** in the Rust facade crate
|
||||
//! (`oakfacade`, `src/facade/rust/src/deferred.rs`), so none of the symbols
|
||||
//! below is referenced from this crate yet — the subcommands gate on
|
||||
//! [`crate::deferred`] and report "not yet available" instead of calling
|
||||
//! them. The declarations exist so that:
|
||||
//!
|
||||
//! 1. the exact contract the CLI expects is pinned in one place (types,
|
||||
//! signatures, string conventions, error codes), and
|
||||
//! 2. when a family is wrapped by oakfacade, the call-through code in
|
||||
//! `src/cmd/` resolves against the already-linked `oakfacade` rlib
|
||||
//! without any manifest or signature churn.
|
||||
//!
|
||||
//! Nothing here is ever called today, so no symbol needs to exist in the
|
||||
//! facade yet; that keeps `cargo build` green standalone.
|
||||
//!
|
||||
//! `dead_code` is expected for this whole surface until the ports land: the
|
||||
//! declarations, the POD structs and [`facade_string`] exist precisely to be
|
||||
//! consumed by `src/cmd/` once the deferred families are wrapped.
|
||||
|
||||
#![allow(dead_code)]
|
||||
#![allow(non_camel_case_types)]
|
||||
#![allow(clippy::missing_safety_doc)]
|
||||
|
||||
use std::ffi::{c_char, c_double, c_int, c_void};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Opaque engine handle types (engine/include/oakengine/*.h).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[repr(C)]
|
||||
pub struct OakEngineProject {
|
||||
_opaque: [u8; 0],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct OakEngineSequence {
|
||||
_opaque: [u8; 0],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct OakEngineRenderer {
|
||||
_opaque: [u8; 0],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct OakEngineFrame {
|
||||
_opaque: [u8; 0],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct OakEngineAudioBuffer {
|
||||
_opaque: [u8; 0],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct OakEngineFootage {
|
||||
_opaque: [u8; 0],
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
pub struct OakEngineClip {
|
||||
_opaque: [u8; 0],
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// POD structs (footage.h / exporter.h).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// `oak_footage_video_info` (engine/include/oakengine/footage.h).
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct OakFootageVideoInfo {
|
||||
pub stream_index: c_int,
|
||||
pub width: c_int,
|
||||
pub height: c_int,
|
||||
pub frame_rate_num: c_int,
|
||||
pub frame_rate_den: c_int,
|
||||
pub duration_ts: i64,
|
||||
pub time_base_num: c_int,
|
||||
pub time_base_den: c_int,
|
||||
pub color_primaries: c_int,
|
||||
pub color_trc: c_int,
|
||||
pub interlaced: c_int,
|
||||
}
|
||||
|
||||
/// `oak_footage_audio_info` (engine/include/oakengine/footage.h).
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct OakFootageAudioInfo {
|
||||
pub stream_index: c_int,
|
||||
pub sample_rate: c_int,
|
||||
pub channel_layout: u64,
|
||||
pub channel_count: c_int,
|
||||
pub duration_ts: i64,
|
||||
pub time_base_num: c_int,
|
||||
pub time_base_den: c_int,
|
||||
}
|
||||
|
||||
/// `oak_export_options` (engine/include/oakengine/exporter.h).
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct OakExportOptions {
|
||||
pub video_codec: c_int,
|
||||
pub audio_codec: c_int,
|
||||
pub video_bit_rate: i64,
|
||||
pub audio_sample_rate: c_int,
|
||||
pub audio_channel_count: c_int,
|
||||
}
|
||||
|
||||
/// `oakengine_export_progress_fn` (exporter.h).
|
||||
pub type OakEngineExportProgressFn =
|
||||
Option<unsafe extern "C" fn(fraction: c_double, userdata: *mut c_void)>;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants (verbatim values from the engine headers).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// OAKENGINE_OK / OAKENGINE_E_* (init.h).
|
||||
pub const OAKENGINE_OK: c_int = 0;
|
||||
pub const OAKENGINE_E_INVALID: c_int = -1;
|
||||
pub const OAKENGINE_E_STATE: c_int = -2;
|
||||
pub const OAKENGINE_E_FAILED: c_int = -3;
|
||||
pub const OAKENGINE_E_NOT_FOUND: c_int = -4;
|
||||
|
||||
/// OAKENGINE_INIT_* (init.h).
|
||||
pub const OAKENGINE_INIT_HEADLESS: c_int = 0x01;
|
||||
pub const OAKENGINE_INIT_RENDER: c_int = 0x02;
|
||||
|
||||
/// olive::core::PixelFormat::f32, the renderer's frame pixel format
|
||||
/// (`k_pixel_format_f32` in cli/main.cpp).
|
||||
pub const PIXEL_FORMAT_F32: c_int = 4;
|
||||
|
||||
/// OAKENGINE_TRACK_TYPE_* (timeline.h).
|
||||
pub const OAKENGINE_TRACK_TYPE_VIDEO: c_int = 0;
|
||||
pub const OAKENGINE_TRACK_TYPE_AUDIO: c_int = 1;
|
||||
|
||||
/// OAKENGINE_EXPORT_VIDEO_* / OAKENGINE_EXPORT_AUDIO_* (exporter.h).
|
||||
pub const OAKENGINE_EXPORT_VIDEO_H264: c_int = 0;
|
||||
pub const OAKENGINE_EXPORT_AUDIO_AAC: c_int = 0;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The facade surface (declarations only — see the module docs).
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
extern "C" {
|
||||
// ---- init.h ----------------------------------------------------------
|
||||
pub fn oakengine_init(flags: c_int) -> c_int;
|
||||
pub fn oakengine_shutdown() -> c_int;
|
||||
|
||||
// ---- project.h -------------------------------------------------------
|
||||
pub fn oakengine_project_create() -> *mut OakEngineProject;
|
||||
pub fn oakengine_project_free(self_: *mut OakEngineProject);
|
||||
pub fn oakengine_project_new(self_: *mut OakEngineProject) -> c_int;
|
||||
pub fn oakengine_project_load(
|
||||
self_: *mut OakEngineProject,
|
||||
path: *const c_char,
|
||||
err: *mut c_char,
|
||||
err_size: c_int,
|
||||
) -> c_int;
|
||||
pub fn oakengine_project_name(
|
||||
self_: *const OakEngineProject,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int;
|
||||
pub fn oakengine_project_filename(
|
||||
self_: *const OakEngineProject,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int;
|
||||
pub fn oakengine_project_is_modified(self_: *const OakEngineProject) -> c_int;
|
||||
pub fn oakengine_project_sequence_count(
|
||||
self_: *const OakEngineProject,
|
||||
) -> c_int;
|
||||
pub fn oakengine_project_sequence_at(
|
||||
self_: *const OakEngineProject,
|
||||
index: c_int,
|
||||
) -> *mut OakEngineSequence;
|
||||
pub fn oakengine_project_footage_count(
|
||||
self_: *const OakEngineProject,
|
||||
) -> c_int;
|
||||
pub fn oakengine_project_footage_filename(
|
||||
self_: *const OakEngineProject,
|
||||
index: c_int,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int;
|
||||
pub fn oakengine_project_footage_is_online(
|
||||
self_: *const OakEngineProject,
|
||||
index: c_int,
|
||||
) -> c_int;
|
||||
|
||||
// ---- footage.h -------------------------------------------------------
|
||||
pub fn oakengine_project_import_footage(
|
||||
project: *mut OakEngineProject,
|
||||
path: *const c_char,
|
||||
) -> *mut OakEngineFootage;
|
||||
pub fn oakengine_footage_probe(path: *const c_char) -> *mut OakEngineFootage;
|
||||
pub fn oakengine_footage_free(self_: *mut OakEngineFootage);
|
||||
pub fn oakengine_footage_last_error(buf: *mut c_char, buf_size: c_int) -> c_int;
|
||||
pub fn oakengine_footage_get_decoder_name(
|
||||
self_: *mut OakEngineFootage,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int;
|
||||
pub fn oakengine_footage_get_duration(
|
||||
self_: *mut OakEngineFootage,
|
||||
seconds: *mut c_double,
|
||||
) -> c_int;
|
||||
pub fn oakengine_footage_get_video_stream_count(
|
||||
self_: *const OakEngineFootage,
|
||||
) -> c_int;
|
||||
pub fn oakengine_footage_get_video_stream_info(
|
||||
self_: *mut OakEngineFootage,
|
||||
index: c_int,
|
||||
out: *mut OakFootageVideoInfo,
|
||||
) -> c_int;
|
||||
pub fn oakengine_footage_get_audio_stream_count(
|
||||
self_: *const OakEngineFootage,
|
||||
) -> c_int;
|
||||
pub fn oakengine_footage_get_audio_stream_info(
|
||||
self_: *mut OakEngineFootage,
|
||||
index: c_int,
|
||||
out: *mut OakFootageAudioInfo,
|
||||
) -> c_int;
|
||||
pub fn oakengine_footage_get_subtitle_stream_count(
|
||||
self_: *const OakEngineFootage,
|
||||
) -> c_int;
|
||||
|
||||
// ---- timeline.h ------------------------------------------------------
|
||||
pub fn oakengine_sequence_new(
|
||||
project: *mut OakEngineProject,
|
||||
name: *const c_char,
|
||||
) -> *mut OakEngineSequence;
|
||||
pub fn oakengine_sequence_name(
|
||||
self_: *const OakEngineSequence,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int;
|
||||
pub fn oakengine_sequence_get_length(
|
||||
self_: *const OakEngineSequence,
|
||||
seconds: *mut c_double,
|
||||
) -> c_int;
|
||||
pub fn oakengine_sequence_get_length_rational(
|
||||
self_: *const OakEngineSequence,
|
||||
num: *mut c_int,
|
||||
den: *mut c_int,
|
||||
) -> c_int;
|
||||
pub fn oakengine_sequence_get_frame_rate(
|
||||
self_: *const OakEngineSequence,
|
||||
num: *mut c_int,
|
||||
den: *mut c_int,
|
||||
) -> c_int;
|
||||
pub fn oakengine_sequence_get_video_params(
|
||||
self_: *const OakEngineSequence,
|
||||
width: *mut c_int,
|
||||
height: *mut c_int,
|
||||
par_num: *mut c_int,
|
||||
par_den: *mut c_int,
|
||||
) -> c_int;
|
||||
pub fn oakengine_sequence_track_count(
|
||||
self_: *const OakEngineSequence,
|
||||
video: *mut c_int,
|
||||
audio: *mut c_int,
|
||||
subtitle: *mut c_int,
|
||||
) -> c_int;
|
||||
pub fn oakengine_sequence_get_playhead(
|
||||
self_: *const OakEngineSequence,
|
||||
timestamp: *mut i64,
|
||||
) -> c_int;
|
||||
pub fn oakengine_sequence_get_playhead_seconds(
|
||||
self_: *const OakEngineSequence,
|
||||
seconds: *mut c_double,
|
||||
) -> c_int;
|
||||
pub fn oakengine_sequence_add_track(
|
||||
self_: *mut OakEngineSequence,
|
||||
track_type: c_int,
|
||||
) -> c_int;
|
||||
pub fn oakengine_sequence_add_footage_clip(
|
||||
seq: *mut OakEngineSequence,
|
||||
footage: *mut OakEngineFootage,
|
||||
track_type: c_int,
|
||||
track_index: c_int,
|
||||
in_ts: i64,
|
||||
out_ts: i64,
|
||||
media_in: i64,
|
||||
) -> *mut OakEngineClip;
|
||||
pub fn oakengine_sequence_last_error(buf: *mut c_char, buf_size: c_int)
|
||||
-> c_int;
|
||||
|
||||
// ---- renderer.h ------------------------------------------------------
|
||||
pub fn oakengine_renderer_create(
|
||||
seq: *mut OakEngineSequence,
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
pixel_format: c_int,
|
||||
frame_rate_num: c_int,
|
||||
frame_rate_den: c_int,
|
||||
output_colorspace: *const c_char,
|
||||
) -> *mut OakEngineRenderer;
|
||||
pub fn oakengine_renderer_free(self_: *mut OakEngineRenderer);
|
||||
pub fn oakengine_renderer_last_error(
|
||||
self_: *const OakEngineRenderer,
|
||||
buf: *mut c_char,
|
||||
buf_size: c_int,
|
||||
) -> c_int;
|
||||
pub fn oakengine_renderer_render_frame(
|
||||
self_: *mut OakEngineRenderer,
|
||||
timestamp: i64,
|
||||
) -> *mut OakEngineFrame;
|
||||
pub fn oakengine_renderer_render_audio(
|
||||
self_: *mut OakEngineRenderer,
|
||||
start_timestamp: i64,
|
||||
length_timestamp: i64,
|
||||
) -> *mut OakEngineAudioBuffer;
|
||||
|
||||
// ---- renderer.h (OakEngineFrame) -------------------------------------
|
||||
pub fn oakengine_frame_width(self_: *const OakEngineFrame) -> c_int;
|
||||
pub fn oakengine_frame_height(self_: *const OakEngineFrame) -> c_int;
|
||||
pub fn oakengine_frame_format(self_: *const OakEngineFrame) -> c_int;
|
||||
pub fn oakengine_frame_channel_count(self_: *const OakEngineFrame) -> c_int;
|
||||
pub fn oakengine_frame_linesize_bytes(self_: *const OakEngineFrame) -> c_int;
|
||||
pub fn oakengine_frame_data(self_: *const OakEngineFrame) -> *const c_void;
|
||||
pub fn oakengine_frame_free(self_: *mut OakEngineFrame);
|
||||
|
||||
// ---- renderer.h (OakEngineAudioBuffer) --------------------------------
|
||||
pub fn oakengine_audio_sample_rate(
|
||||
self_: *const OakEngineAudioBuffer,
|
||||
) -> c_int;
|
||||
pub fn oakengine_audio_channel_count(
|
||||
self_: *const OakEngineAudioBuffer,
|
||||
) -> c_int;
|
||||
pub fn oakengine_audio_sample_count(
|
||||
self_: *const OakEngineAudioBuffer,
|
||||
) -> i64;
|
||||
pub fn oakengine_audio_data(
|
||||
self_: *const OakEngineAudioBuffer,
|
||||
channel: c_int,
|
||||
) -> *const f32;
|
||||
pub fn oakengine_audio_free(self_: *mut OakEngineAudioBuffer);
|
||||
|
||||
// ---- exporter.h ------------------------------------------------------
|
||||
pub fn oakengine_export_render(
|
||||
seq: *mut OakEngineSequence,
|
||||
path: *const c_char,
|
||||
in_ts: i64,
|
||||
out_ts: i64,
|
||||
width: c_int,
|
||||
height: c_int,
|
||||
opts: *const OakExportOptions,
|
||||
) -> c_int;
|
||||
pub fn oakengine_export_last_error(buf: *mut c_char, buf_size: c_int) -> c_int;
|
||||
pub fn oakengine_export_set_progress_callback(
|
||||
f: OakEngineExportProgressFn,
|
||||
userdata: *mut c_void,
|
||||
);
|
||||
}
|
||||
|
||||
/// Read a facade string (buf/size convention) into an owned `String`,
|
||||
/// mirroring `facade_string()` in cli/main.cpp: a negative return is an
|
||||
/// error/empty string, otherwise the getter is called twice (size query,
|
||||
/// then fill) and the trailing NUL is stripped.
|
||||
///
|
||||
/// # Safety
|
||||
/// `getter` must be one of the `oakengine_*` string getters declared above
|
||||
/// and `handle` a live handle for it.
|
||||
pub unsafe fn facade_string(
|
||||
getter: unsafe extern "C" fn(*const c_void, *mut c_char, c_int) -> c_int,
|
||||
handle: *const c_void,
|
||||
) -> String {
|
||||
unsafe {
|
||||
let size = getter(handle, std::ptr::null_mut(), 0);
|
||||
if size < 0 {
|
||||
return String::new();
|
||||
}
|
||||
let mut s = vec![0u8; size as usize + 1];
|
||||
let n = getter(handle, s.as_mut_ptr() as *mut c_char, size + 1);
|
||||
s.truncate(n.max(0) as usize);
|
||||
String::from_utf8_lossy(&s).into_owned()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,273 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! Output formatters matching the C++ `cli/main.cpp` byte for byte.
|
||||
//!
|
||||
//! The golden reference is the output of the C++ binary on the test
|
||||
//! fixtures (`tests/project_with_footage.ove`, `tests/demo.mp4`), captured
|
||||
//! before this crate existed. Each function below takes the plain data a
|
||||
//! facade call would produce and formats it exactly like the C++ `printf`
|
||||
//! call (`%.6f`, `%.3f`, `%lld`, `%d`, ...).
|
||||
//!
|
||||
//! The facade families that produce this data are still deferred
|
||||
//! (`crate::deferred`), so the formatters are exercised by unit tests
|
||||
//! against the golden text; the subcommands wire them in once the families
|
||||
//! land (`dead_code` until then).
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
/// `Project: <name>` (`cmd_info`).
|
||||
pub fn project_line(name: &str) -> String {
|
||||
format!("Project: {name}")
|
||||
}
|
||||
|
||||
/// `File: <filename>` (`cmd_info`).
|
||||
pub fn file_line(filename: &str) -> String {
|
||||
format!("File: {filename}")
|
||||
}
|
||||
|
||||
/// `Modified: yes|no` (`cmd_info`).
|
||||
pub fn modified_line(modified: bool) -> String {
|
||||
format!("Modified: {}", if modified { "yes" } else { "no" })
|
||||
}
|
||||
|
||||
/// `Sequences: <n>` (`cmd_info`).
|
||||
pub fn sequences_line(count: i64) -> String {
|
||||
format!("Sequences: {count}")
|
||||
}
|
||||
|
||||
/// `Footage: <n>` (`cmd_info`).
|
||||
pub fn footage_line(count: i64) -> String {
|
||||
format!("Footage: {count}")
|
||||
}
|
||||
|
||||
/// One sequence block (`print_sequence` in cli/main.cpp).
|
||||
///
|
||||
/// ```
|
||||
/// [0] "Fixture Sequence"
|
||||
/// length: 0.000000 s (0/1)
|
||||
/// frame rate: 30000/1001 (29.970 fps)
|
||||
/// tracks: video=0 audio=0 subtitle=0
|
||||
/// playhead: 0 (0.000000 s)
|
||||
/// ```
|
||||
pub fn sequence(
|
||||
index: i64,
|
||||
name: &str,
|
||||
length_seconds: f64,
|
||||
len_num: i64,
|
||||
len_den: i64,
|
||||
fr_num: i64,
|
||||
fr_den: i64,
|
||||
video: i64,
|
||||
audio: i64,
|
||||
subtitle: i64,
|
||||
playhead: i64,
|
||||
playhead_seconds: f64,
|
||||
) -> String {
|
||||
let fps = if fr_den != 0 {
|
||||
fr_num as f64 / fr_den as f64
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
format!(
|
||||
" [{index}] \"{name}\"\n length: {length_seconds:.6} s ({len_num}/{len_den})\n \
|
||||
frame rate: {fr_num}/{fr_den} ({fps:.3} fps)\n tracks: video={video} audio={audio} \
|
||||
subtitle={subtitle}\n playhead: {playhead} ({playhead_seconds:.6} s)"
|
||||
)
|
||||
}
|
||||
|
||||
/// One footage entry (`cmd_info`).
|
||||
///
|
||||
/// ```
|
||||
/// [0] "/abs/path/demo.mp4" online
|
||||
/// ```
|
||||
pub fn footage_entry(index: i64, filename: &str, online: bool) -> String {
|
||||
format!(
|
||||
" [{index}] \"{filename}\" {}",
|
||||
if online { "online" } else { "offline" }
|
||||
)
|
||||
}
|
||||
|
||||
/// `Decoder: <name>` (`cmd_probe`).
|
||||
pub fn decoder_line(decoder: &str) -> String {
|
||||
format!("Decoder: {decoder}")
|
||||
}
|
||||
|
||||
/// `Duration: <seconds> s` (`cmd_probe`).
|
||||
pub fn duration_line(seconds: f64) -> String {
|
||||
format!("Duration: {seconds:.6} s")
|
||||
}
|
||||
|
||||
/// `Video streams: <n>` (`cmd_probe`).
|
||||
pub fn video_streams_line(count: i64) -> String {
|
||||
format!("Video streams: {count}")
|
||||
}
|
||||
|
||||
/// One video-stream line (`cmd_probe`).
|
||||
///
|
||||
/// ```
|
||||
/// [0] stream 0: 1920x1080, 25/1 fps (25.000), duration 217600/12800 (17.000000 s), primaries=1 trc=1, progressive
|
||||
/// ```
|
||||
pub fn video_stream(
|
||||
index: i64,
|
||||
stream_index: i64,
|
||||
width: i64,
|
||||
height: i64,
|
||||
frame_rate_num: i64,
|
||||
frame_rate_den: i64,
|
||||
duration_ts: i64,
|
||||
time_base_den: i64,
|
||||
seconds: f64,
|
||||
color_primaries: i64,
|
||||
color_trc: i64,
|
||||
interlaced: bool,
|
||||
) -> String {
|
||||
let fps = if frame_rate_den != 0 {
|
||||
frame_rate_num as f64 / frame_rate_den as f64
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
let interlace = if interlaced {
|
||||
"interlaced"
|
||||
} else {
|
||||
"progressive"
|
||||
};
|
||||
format!(
|
||||
" [{index}] stream {stream_index}: {width}x{height}, {frame_rate_num}/{frame_rate_den} \
|
||||
fps ({fps:.3}), duration {duration_ts}/{time_base_den} ({seconds:.6} s), \
|
||||
primaries={color_primaries} trc={color_trc}, {interlace}"
|
||||
)
|
||||
}
|
||||
|
||||
/// `Audio streams: <n>` (`cmd_probe`).
|
||||
pub fn audio_streams_line(count: i64) -> String {
|
||||
format!("Audio streams: {count}")
|
||||
}
|
||||
|
||||
/// One audio-stream line (`cmd_probe`).
|
||||
///
|
||||
/// ```
|
||||
/// [0] stream 1: 48000 Hz, 2 channels, duration 816000/48000 (17.000000 s)
|
||||
/// ```
|
||||
pub fn audio_stream(
|
||||
index: i64,
|
||||
stream_index: i64,
|
||||
sample_rate: i64,
|
||||
channel_count: i64,
|
||||
duration_ts: i64,
|
||||
time_base_den: i64,
|
||||
seconds: f64,
|
||||
) -> String {
|
||||
format!(
|
||||
" [{index}] stream {stream_index}: {sample_rate} Hz, {channel_count} channels, \
|
||||
duration {duration_ts}/{time_base_den} ({seconds:.6} s)"
|
||||
)
|
||||
}
|
||||
|
||||
/// `Subtitle streams: <n>` (`cmd_probe`).
|
||||
pub fn subtitle_streams_line(count: i64) -> String {
|
||||
format!("Subtitle streams: {count}")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// Golden text captured from the C++ binary:
|
||||
// cmake-build-debug/cli/oak-cli info tests/project_with_footage.ove
|
||||
// cmake-build-debug/cli/oak-cli probe tests/demo.mp4
|
||||
|
||||
#[test]
|
||||
fn golden_info_output() {
|
||||
let mut out = String::new();
|
||||
out.push_str(&project_line("project_with_footage"));
|
||||
out.push('\n');
|
||||
out.push_str(&file_line("/Users/sunyu/Projects/oak/tests/project_with_footage.ove"));
|
||||
out.push('\n');
|
||||
out.push_str(&modified_line(false));
|
||||
out.push('\n');
|
||||
out.push_str(&sequences_line(1));
|
||||
out.push('\n');
|
||||
out.push_str(&sequence(
|
||||
0, "Fixture Sequence", 0.0, 0, 1, 30000, 1001, 0, 0, 0, 0, 0.0,
|
||||
));
|
||||
out.push('\n');
|
||||
out.push_str(&footage_line(1));
|
||||
out.push('\n');
|
||||
out.push_str(&footage_entry(0, "/Users/sunyu/Projects/oak/tests/demo.mp4", true));
|
||||
|
||||
const GOLDEN: &str = concat!(
|
||||
"Project: project_with_footage\n",
|
||||
"File: /Users/sunyu/Projects/oak/tests/project_with_footage.ove\n",
|
||||
"Modified: no\n",
|
||||
"Sequences: 1\n",
|
||||
" [0] \"Fixture Sequence\"\n",
|
||||
" length: 0.000000 s (0/1)\n",
|
||||
" frame rate: 30000/1001 (29.970 fps)\n",
|
||||
" tracks: video=0 audio=0 subtitle=0\n",
|
||||
" playhead: 0 (0.000000 s)\n",
|
||||
"Footage: 1\n",
|
||||
" [0] \"/Users/sunyu/Projects/oak/tests/demo.mp4\" online",
|
||||
);
|
||||
assert_eq!(out, GOLDEN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn golden_probe_output() {
|
||||
let mut out = String::new();
|
||||
out.push_str(&decoder_line("ffmpeg"));
|
||||
out.push('\n');
|
||||
out.push_str(&duration_line(17.0));
|
||||
out.push('\n');
|
||||
out.push_str(&video_streams_line(1));
|
||||
out.push('\n');
|
||||
out.push_str(&video_stream(
|
||||
0, 0, 1920, 1080, 25, 1, 217600, 12800, 17.0, 1, 1, false,
|
||||
));
|
||||
out.push('\n');
|
||||
out.push_str(&audio_streams_line(1));
|
||||
out.push('\n');
|
||||
out.push_str(&audio_stream(0, 1, 48000, 2, 816000, 48000, 17.0));
|
||||
out.push('\n');
|
||||
out.push_str(&subtitle_streams_line(0));
|
||||
|
||||
const GOLDEN: &str = concat!(
|
||||
"Decoder: ffmpeg\n",
|
||||
"Duration: 17.000000 s\n",
|
||||
"Video streams: 1\n",
|
||||
" [0] stream 0: 1920x1080, 25/1 fps (25.000), duration 217600/12800 (17.000000 s), primaries=1 trc=1, progressive\n",
|
||||
"Audio streams: 1\n",
|
||||
" [0] stream 1: 48000 Hz, 2 channels, duration 816000/48000 (17.000000 s)\n",
|
||||
"Subtitle streams: 0",
|
||||
);
|
||||
assert_eq!(out, GOLDEN);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fps_rounding_matches_printf() {
|
||||
// 30000/1001 = 29.970029... -> %.3f -> "29.970"
|
||||
let s = sequence(0, "S", 0.0, 0, 1, 30000, 1001, 0, 0, 0, 0, 0.0);
|
||||
assert!(s.contains("frame rate: 30000/1001 (29.970 fps)"), "{s}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offline_footage_prints_offline() {
|
||||
assert_eq!(
|
||||
footage_entry(2, "gone.mp4", false),
|
||||
" [2] \"gone.mp4\" offline"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,183 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! oak-cli: headless command-line consumer of the liboakengine C ABI facade.
|
||||
//!
|
||||
//! Rust rewrite of `cli/main.cpp` (which stays in the tree until cutover).
|
||||
//! Same subcommands, same output format, same exit codes:
|
||||
//!
|
||||
//! ```text
|
||||
//! oak-cli info <project.ove> <start> <end> <out_dir> (project info)
|
||||
//! oak-cli render <project.ove> <start_seconds> <end_seconds> <out_dir>
|
||||
//! oak-cli probe <mediafile>
|
||||
//! oak-cli transcode <input_media> <out> [width] [--format ppm|mp4]
|
||||
//! ```
|
||||
//!
|
||||
//! Exit codes: 0 success, 1 general error, 2 rendering unavailable,
|
||||
//! 64 usage error.
|
||||
//!
|
||||
//! The facade families every subcommand depends on (init/project/timeline/
|
||||
//! render/footage/exporter) are still **deferred** in the `oakfacade` crate
|
||||
//! (see `src/facade/rust/src/deferred.rs`), so each subcommand validates its
|
||||
//! arguments faithfully, then reports the deferral with its reason and exits
|
||||
//! with the C++-compatible code — never crashing, never faking output.
|
||||
|
||||
mod cmd;
|
||||
mod deferred;
|
||||
mod ffi;
|
||||
mod fmt;
|
||||
mod ppm;
|
||||
mod wav;
|
||||
|
||||
use std::process::exit;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
/// The exact usage text of `cli/main.cpp`'s `print_usage()` (also the
|
||||
/// `--help` output).
|
||||
const USAGE: &str = "oak-cli - headless consumer of the liboakengine C ABI\n\
|
||||
\n\
|
||||
Usage:\n\
|
||||
oak-cli info <project.ove>\n\
|
||||
Print project name, sequences and footage.\n\
|
||||
\n\
|
||||
oak-cli render <project.ove> <start_seconds> <end_seconds> <out_dir>\n\
|
||||
Render the first sequence to PPM frames (P6, 8-bit RGB) and the\n\
|
||||
audio range to a PCM s16 WAV file in <out_dir>.\n\
|
||||
\n\
|
||||
oak-cli probe <mediafile>\n\
|
||||
Probe a media file: decoder, duration, video and audio streams.\n\
|
||||
\n\
|
||||
oak-cli transcode <input_media> <out> [width] [--format ppm|mp4]\n\
|
||||
Transcode a media file end to end: import it into a temporary\n\
|
||||
project, place it as clips, and render the whole duration.\n\
|
||||
Default output is a single H.264/AAC MP4 file (encoder default\n\
|
||||
bit rate); --format ppm renders PPM frames + a WAV instead.\n\
|
||||
[width] defaults to the source width; the height follows the\n\
|
||||
source aspect ratio. <out> is the MP4 file path, or the\n\
|
||||
output directory with --format ppm.\n\
|
||||
\n\
|
||||
oak-cli --help\n\
|
||||
Show this text.\n\
|
||||
\n\
|
||||
Exit codes:\n\
|
||||
0 success\n\
|
||||
1 general error (bad project/media file, no sequence, I/O failure)\n\
|
||||
2 rendering unavailable or failed (e.g. no GL render backend)\n\
|
||||
64 usage error\n";
|
||||
|
||||
/// CLI surface. `--help`/`-h` are handled before clap so the C++ usage text
|
||||
/// is reproduced exactly; clap still enforces the argument shapes.
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(
|
||||
name = "oak-cli",
|
||||
disable_help_flag = true,
|
||||
disable_version_flag = true,
|
||||
subcommand_required = true
|
||||
)]
|
||||
struct Cli {
|
||||
#[command(subcommand)]
|
||||
command: Command,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Command {
|
||||
/// Print project name, sequences and footage.
|
||||
Info {
|
||||
/// Path to the project file (.ove).
|
||||
project: String,
|
||||
},
|
||||
/// Render the first sequence to PPM frames (P6, 8-bit RGB) and the
|
||||
/// audio range to a PCM s16 WAV file in <out_dir>.
|
||||
Render {
|
||||
/// Path to the project file (.ove).
|
||||
project: String,
|
||||
/// Start of the rendered range, in seconds.
|
||||
start_seconds: String,
|
||||
/// End of the rendered range, in seconds (must be > start).
|
||||
end_seconds: String,
|
||||
/// Directory the PPM frames and audio.wav are written into.
|
||||
out_dir: String,
|
||||
},
|
||||
/// Probe a media file: decoder, duration, video and audio streams.
|
||||
Probe {
|
||||
/// Media file to probe.
|
||||
mediafile: String,
|
||||
},
|
||||
/// Transcode a media file end to end.
|
||||
Transcode {
|
||||
/// Source media file.
|
||||
input_media: String,
|
||||
/// Output MP4 path, or the output directory with --format ppm.
|
||||
out: String,
|
||||
/// Output width (defaults to the source width).
|
||||
width: Option<String>,
|
||||
/// Output format: "mp4" (default) or "ppm".
|
||||
#[arg(long = "format")]
|
||||
format: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||
|
||||
// argv[1] handling that mirrors the C++ main() exactly.
|
||||
if let Some(first) = args.first() {
|
||||
if first == "--help" || first == "-h" {
|
||||
print!("{USAGE}");
|
||||
exit(cmd::EXIT_OK);
|
||||
}
|
||||
}
|
||||
if let Some(first) = args.first() {
|
||||
if !matches!(first.as_str(), "info" | "render" | "probe" | "transcode") {
|
||||
eprintln!("error: unknown command \"{first}\"");
|
||||
eprint_usage();
|
||||
exit(cmd::EXIT_USAGE);
|
||||
}
|
||||
}
|
||||
|
||||
let cli = match Cli::try_parse() {
|
||||
Ok(cli) => cli,
|
||||
Err(e) => {
|
||||
// clap's own arity/format message, then the C++ usage text.
|
||||
let _ = e.print();
|
||||
eprint_usage();
|
||||
exit(cmd::EXIT_USAGE);
|
||||
}
|
||||
};
|
||||
|
||||
let code = match cli.command {
|
||||
Command::Info { project } => cmd::info::run(project),
|
||||
Command::Render {
|
||||
project,
|
||||
start_seconds,
|
||||
end_seconds,
|
||||
out_dir,
|
||||
} => cmd::render::run(project, &start_seconds, &end_seconds, &out_dir),
|
||||
Command::Probe { mediafile } => cmd::probe::run(mediafile),
|
||||
Command::Transcode {
|
||||
input_media,
|
||||
out,
|
||||
width,
|
||||
format,
|
||||
} => cmd::transcode::run(input_media, out, width, format),
|
||||
};
|
||||
exit(code);
|
||||
}
|
||||
|
||||
fn eprint_usage() {
|
||||
eprint!("{USAGE}");
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! PPM (P6, 8-bit RGB) frame writer — the exact port of `write_ppm()` in
|
||||
//! `cli/main.cpp`.
|
||||
//!
|
||||
//! Takes the raw pixel data an `OakEngineFrame` facade handle would expose
|
||||
//! (linesize-strided rows of `channels` values per pixel) and writes a P6
|
||||
//! file. Pixel formats: `f32` ([`PIXEL_FORMAT_F32`], 4 bytes per channel,
|
||||
//! clamped to [0,1]) and `u8` (format 0, 1 byte per channel). Any other
|
||||
//! format is an error, mirroring the C++ throw.
|
||||
//!
|
||||
//! `dead_code` until the render/transcode ports call it (it is exercised by
|
||||
//! the unit tests below).
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::io::{self, Write};
|
||||
use std::path::Path;
|
||||
|
||||
/// olive::core::PixelFormat::f32 — the renderer's default frame format
|
||||
/// (`k_pixel_format_f32` in cli/main.cpp).
|
||||
pub const PIXEL_FORMAT_F32: i32 = 4;
|
||||
|
||||
/// Write `width` x `height` rows of pixel data as a P6 PPM file.
|
||||
///
|
||||
/// `data` must hold `linesize * height` bytes; each row starts `linesize`
|
||||
/// bytes apart (stride). `channels` is the per-pixel channel count in the
|
||||
/// source data; only the first three channels are emitted.
|
||||
pub fn write_ppm(
|
||||
path: &Path,
|
||||
width: i32,
|
||||
height: i32,
|
||||
format: i32,
|
||||
channels: i32,
|
||||
linesize: i32,
|
||||
data: &[u8],
|
||||
) -> io::Result<()> {
|
||||
let width = usize::try_from(width).map_err(|_| invalid_data("negative width"))?;
|
||||
let height = usize::try_from(height).map_err(|_| invalid_data("negative height"))?;
|
||||
let linesize = usize::try_from(linesize).unwrap_or(0);
|
||||
let channels = usize::try_from(channels).map_err(|_| invalid_data("negative channel count"))?;
|
||||
if channels < 3 {
|
||||
return Err(invalid_data("channel count below 3"));
|
||||
}
|
||||
|
||||
let mut out = Vec::with_capacity(
|
||||
format!("P6\n{width} {height}\n255\n").len() + width * height * 3,
|
||||
);
|
||||
out.extend_from_slice(format!("P6\n{width} {height}\n255\n").as_bytes());
|
||||
|
||||
let mut row = vec![0u8; width * 3];
|
||||
for y in 0..height {
|
||||
let line_start = y * linesize;
|
||||
let line_end = line_start.checked_add(linesize);
|
||||
let line = match line_end {
|
||||
Some(end) if end <= data.len() => &data[line_start..end],
|
||||
_ => {
|
||||
return Err(invalid_data("pixel data buffer is shorter than the frame geometry"));
|
||||
}
|
||||
};
|
||||
for x in 0..width {
|
||||
for c in 0..3 {
|
||||
let v = if format == PIXEL_FORMAT_F32 {
|
||||
// f32: 4 bytes per channel.
|
||||
let off = (x * channels + c) * 4;
|
||||
let px = f32::from_ne_bytes([
|
||||
line[off],
|
||||
line[off + 1],
|
||||
line[off + 2],
|
||||
line[off + 3],
|
||||
]);
|
||||
let clamped = if px < 0.0 {
|
||||
0.0
|
||||
} else if px > 1.0 {
|
||||
1.0
|
||||
} else {
|
||||
px
|
||||
};
|
||||
// static_cast<unsigned char>(clamped * 255.0f + 0.5f):
|
||||
// truncation toward zero, same as Rust `as u8`.
|
||||
(clamped * 255.0 + 0.5) as u8
|
||||
} else if format == 0 {
|
||||
// u8: 1 byte per channel.
|
||||
line[x * channels + c]
|
||||
} else {
|
||||
return Err(invalid_data(&format!(
|
||||
"unsupported frame pixel format {format}"
|
||||
)));
|
||||
};
|
||||
row[x * 3 + c] = v;
|
||||
}
|
||||
}
|
||||
out.extend_from_slice(&row);
|
||||
}
|
||||
|
||||
let mut f = std::fs::File::create(path)?;
|
||||
f.write_all(&out)
|
||||
}
|
||||
|
||||
fn invalid_data(msg: &str) -> io::Error {
|
||||
io::Error::new(io::ErrorKind::InvalidData, msg.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn bytes(hex: &str) -> Vec<u8> {
|
||||
let mut v = Vec::new();
|
||||
for pair in hex.as_bytes().chunks(2) {
|
||||
let s = std::str::from_utf8(pair).unwrap();
|
||||
v.push(u8::from_str_radix(s, 16).unwrap());
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn writes_p6_header_and_u8_rows() {
|
||||
let dir = std::env::temp_dir();
|
||||
let path = dir.join("oak_cli_test_ppm_u8.ppm");
|
||||
// 2x2, 3 channels, linesize 6, u8.
|
||||
let data = vec![
|
||||
1, 2, 3, 4, 5, 6, //
|
||||
7, 8, 9, 10, 11, 12, //
|
||||
];
|
||||
write_ppm(&path, 2, 2, 0, 3, 6, &data).unwrap();
|
||||
|
||||
let got = std::fs::read(&path).unwrap();
|
||||
let mut expected = b"P6\n2 2\n255\n".to_vec();
|
||||
expected.extend_from_slice(&data);
|
||||
assert_eq!(got, expected);
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn f32_rows_are_clamped_and_quantized() {
|
||||
let dir = std::env::temp_dir();
|
||||
let path = dir.join("oak_cli_test_ppm_f32.ppm");
|
||||
// 1x1, RGBA (4 channels), linesize 16, f32.
|
||||
let data = bytes("0000803f0000803f0000803f00000000"); // 1.0, 1.0, 1.0, 0.0
|
||||
write_ppm(&path, 1, 1, PIXEL_FORMAT_F32, 4, 16, &data).unwrap();
|
||||
|
||||
let got = std::fs::read(&path).unwrap();
|
||||
assert_eq!(&got[..11], b"P6\n1 1\n255\n");
|
||||
assert_eq!(&got[11..], &[255, 255, 255]); // 1.0 -> 255 (clamped * 255 + 0.5, truncated)
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clamps_f32_negative_and_over_one() {
|
||||
let dir = std::env::temp_dir();
|
||||
let path = dir.join("oak_cli_test_ppm_clamp.ppm");
|
||||
// 2x1 RGB f32: (-0.5, 0.25, 2.0) | (0.0, 0.5, 1.0)
|
||||
let mut data = Vec::new();
|
||||
for v in [-0.5f32, 0.25, 2.0, 0.0, 0.5, 1.0] {
|
||||
data.extend_from_slice(&v.to_ne_bytes());
|
||||
}
|
||||
write_ppm(&path, 2, 1, PIXEL_FORMAT_F32, 3, 24, &data).unwrap();
|
||||
|
||||
let got = std::fs::read(&path).unwrap();
|
||||
// 0.0 -> 0, 0.25*255+0.5=64.25 -> 64, 1.0 -> 255, 0.5*255+0.5=128.0 -> 128
|
||||
assert_eq!(&got[11..], &[0, 64, 255, 0, 128, 255]);
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_format_is_an_error() {
|
||||
let dir = std::env::temp_dir();
|
||||
let path = dir.join("oak_cli_test_ppm_bad.ppm");
|
||||
let err = write_ppm(&path, 1, 1, 7, 3, 3, &[0, 0, 0]).unwrap_err();
|
||||
assert!(err.to_string().contains("unsupported frame pixel format 7"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn short_buffer_is_an_error() {
|
||||
let dir = std::env::temp_dir();
|
||||
let path = dir.join("oak_cli_test_ppm_short.ppm");
|
||||
let err = write_ppm(&path, 4, 4, 0, 3, 12, &[0u8; 10]).unwrap_err();
|
||||
assert!(err.to_string().contains("shorter than the frame geometry"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! PCM s16 WAV writer — the exact port of `write_wav()` in `cli/main.cpp`.
|
||||
//!
|
||||
//! Takes interleaved float samples (the order `cmd_render`/`cmd_transcode`
|
||||
//! produce by interleaving the planar `OakEngineAudioBuffer` channels) and
|
||||
//! writes a classic 44-byte-header PCM WAV. Float samples are clamped to
|
||||
//! [-1, 1] and converted with `v * 32767.0` truncated toward zero, exactly
|
||||
//! like the C++ `static_cast<int16_t>(clamped * 32767.0f)`.
|
||||
//!
|
||||
//! `dead_code` until the render/transcode ports call it (it is exercised by
|
||||
//! the unit tests below).
|
||||
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::io::{self, Write};
|
||||
use std::path::Path;
|
||||
|
||||
fn write_u16_le(f: &mut impl Write, v: u16) -> io::Result<()> {
|
||||
f.write_all(&[v as u8, (v >> 8) as u8])
|
||||
}
|
||||
|
||||
fn write_u32_le(f: &mut impl Write, v: u32) -> io::Result<()> {
|
||||
f.write_all(&[
|
||||
v as u8,
|
||||
(v >> 8) as u8,
|
||||
(v >> 16) as u8,
|
||||
(v >> 24) as u8,
|
||||
])
|
||||
}
|
||||
|
||||
/// Write interleaved float samples as a PCM s16 WAV file.
|
||||
///
|
||||
/// `data` must hold `samples * channels` values in interleaved order
|
||||
/// (`[s0c0, s0c1, s1c0, s1c1, ...]`), matching what the C++ loop over
|
||||
/// `oakengine_audio_data(audio, ch)[i]` emits.
|
||||
pub fn write_wav(path: &Path, rate: i32, channels: i32, samples: i64, data: &[f32]) -> io::Result<()> {
|
||||
let rate = u32::try_from(rate).map_err(|_| invalid_data("negative sample rate"))?;
|
||||
let channels = u32::try_from(channels).map_err(|_| invalid_data("negative channel count"))?;
|
||||
let samples = u64::try_from(samples).map_err(|_| invalid_data("negative sample count"))?;
|
||||
if channels == 0 {
|
||||
return Err(invalid_data("zero channel count"));
|
||||
}
|
||||
let expected = samples
|
||||
.checked_mul(u64::from(channels))
|
||||
.ok_or_else(|| invalid_data("sample count overflow"))?;
|
||||
if data.len() as u64 != expected {
|
||||
return Err(invalid_data("sample buffer length does not match rate/channels/samples"));
|
||||
}
|
||||
|
||||
let data_size = expected
|
||||
.checked_mul(2)
|
||||
.and_then(|v| u32::try_from(v).ok())
|
||||
.ok_or_else(|| invalid_data("WAV data chunk exceeds 4 GiB"))?;
|
||||
let byte_rate = rate
|
||||
.checked_mul(channels)
|
||||
.and_then(|v| v.checked_mul(2))
|
||||
.ok_or_else(|| invalid_data("byte rate overflow"))?;
|
||||
let block_align = channels
|
||||
.checked_mul(2)
|
||||
.and_then(|v| u16::try_from(v).ok())
|
||||
.ok_or_else(|| invalid_data("block align overflow"))?;
|
||||
|
||||
let mut f = std::fs::File::create(path)?;
|
||||
f.write_all(b"RIFF")?;
|
||||
write_u32_le(&mut f, 36 + data_size)?;
|
||||
f.write_all(b"WAVE")?;
|
||||
f.write_all(b"fmt ")?;
|
||||
write_u32_le(&mut f, 16)?; // fmt chunk size
|
||||
write_u16_le(&mut f, 1)?; // PCM
|
||||
write_u16_le(&mut f, channels as u16)?;
|
||||
write_u32_le(&mut f, rate)?;
|
||||
write_u32_le(&mut f, byte_rate)?;
|
||||
write_u16_le(&mut f, block_align)?;
|
||||
write_u16_le(&mut f, 16)?; // bits per sample
|
||||
f.write_all(b"data")?;
|
||||
write_u32_le(&mut f, data_size)?;
|
||||
|
||||
for &v in data {
|
||||
let clamped = if v < -1.0 {
|
||||
-1.0
|
||||
} else if v > 1.0 {
|
||||
1.0
|
||||
} else {
|
||||
v
|
||||
};
|
||||
// static_cast<int16_t>(clamped * 32767.0f): truncation toward zero.
|
||||
let s = (clamped * 32767.0) as i16;
|
||||
write_u16_le(&mut f, s as u16)?;
|
||||
}
|
||||
f.flush()
|
||||
}
|
||||
|
||||
fn invalid_data(msg: &str) -> io::Error {
|
||||
io::Error::new(io::ErrorKind::InvalidData, msg.to_string())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn golden_mono_wav() {
|
||||
let dir = std::env::temp_dir();
|
||||
let path = dir.join("oak_cli_test_wav_mono.wav");
|
||||
// 2 samples mono at 44100 Hz: 0.0, 0.5
|
||||
write_wav(&path, 44100, 1, 2, &[0.0, 0.5]).unwrap();
|
||||
|
||||
let got = std::fs::read(&path).unwrap();
|
||||
// 44-byte header + 2 samples * 2 bytes.
|
||||
assert_eq!(got.len(), 48);
|
||||
assert_eq!(&got[0..4], b"RIFF");
|
||||
// chunk size = 36 + 4 = 40
|
||||
assert_eq!(&got[4..8], &[40, 0, 0, 0]);
|
||||
assert_eq!(&got[8..12], b"WAVE");
|
||||
assert_eq!(&got[12..16], b"fmt ");
|
||||
assert_eq!(&got[16..20], &[16, 0, 0, 0]);
|
||||
assert_eq!(&got[20..22], &[1, 0]); // PCM
|
||||
assert_eq!(&got[22..24], &[1, 0]); // mono
|
||||
assert_eq!(&got[24..28], &[0x44, 0xAC, 0, 0]); // 44100
|
||||
assert_eq!(&got[28..32], &[0x88, 0x58, 0x01, 0]); // byte rate 88200
|
||||
assert_eq!(&got[32..34], &[2, 0]); // block align
|
||||
assert_eq!(&got[34..36], &[16, 0]); // bits per sample
|
||||
assert_eq!(&got[36..40], b"data");
|
||||
assert_eq!(&got[40..44], &[4, 0, 0, 0]); // data size
|
||||
// 0.0 -> 0; 0.5 * 32767 = 16383.5 -> truncates to 16383 (0x3FFF)
|
||||
assert_eq!(&got[44..48], &[0x00, 0x00, 0xFF, 0x3F]);
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stereo_interleaving_and_clamping() {
|
||||
let dir = std::env::temp_dir();
|
||||
let path = dir.join("oak_cli_test_wav_stereo.wav");
|
||||
// 1 sample stereo at 48000: (-1.0, 1.0) interleaved.
|
||||
write_wav(&path, 48000, 2, 1, &[-1.0, 1.0]).unwrap();
|
||||
|
||||
let got = std::fs::read(&path).unwrap();
|
||||
assert_eq!(&got[22..24], &[2, 0]); // stereo
|
||||
assert_eq!(&got[32..34], &[4, 0]); // block align
|
||||
// -1.0 -> -32767 = 0x8001; 1.0 -> 32767 = 0x7FFF
|
||||
assert_eq!(&got[44..48], &[0x01, 0x80, 0xFF, 0x7F]);
|
||||
let _ = std::fs::remove_file(&path);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sample_count_mismatch_is_an_error() {
|
||||
let dir = std::env::temp_dir();
|
||||
let path = dir.join("oak_cli_test_wav_bad.wav");
|
||||
let err = write_wav(&path, 48000, 2, 10, &[0.0f32; 3]).unwrap_err();
|
||||
assert!(err.to_string().contains("does not match"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
// Oak Video Editor - Non-Linear Video Editor
|
||||
// Copyright (C) 2026 Oak Team
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License
|
||||
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
//! End-to-end tests for the built `oak-cli` binary (the C++ ctest suite
|
||||
//! `oak_cli_info`/`oak_cli_render`/`oak_cli_probe`/`oak_cli_transcode`
|
||||
//! equivalents, as far as the deferred facade allows).
|
||||
//!
|
||||
//! All four subcommands depend on facade families that are still deferred in
|
||||
//! oakfacade (see `src/deferred.rs`), so the data-producing paths assert the
|
||||
//! documented "not yet available" behavior with the C++-compatible exit
|
||||
//! codes; the argument-validation paths assert the exact C++ messages and
|
||||
//! exit code 64.
|
||||
|
||||
use std::process::Command;
|
||||
|
||||
fn bin() -> &'static str {
|
||||
env!("CARGO_BIN_EXE_oak-cli")
|
||||
}
|
||||
|
||||
fn run(args: &[&str]) -> (i32, String, String) {
|
||||
let out = Command::new(bin()).args(args).output().expect("spawn oak-cli");
|
||||
(
|
||||
out.status.code().expect("exit code"),
|
||||
String::from_utf8_lossy(&out.stdout).into_owned(),
|
||||
String::from_utf8_lossy(&out.stderr).into_owned(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_prints_the_cpp_usage_text_and_exits_zero() {
|
||||
let (code, stdout, stderr) = run(&["--help"]);
|
||||
assert_eq!(code, 0);
|
||||
assert!(stderr.is_empty());
|
||||
assert!(stdout.starts_with("oak-cli - headless consumer of the liboakengine C ABI\n"));
|
||||
assert!(stdout.contains("oak-cli transcode <input_media> <out> [width] [--format ppm|mp4]"));
|
||||
assert!(stdout.contains("Exit codes:"));
|
||||
assert!(stdout.contains("64 usage error"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn no_arguments_is_a_usage_error() {
|
||||
let (code, _stdout, stderr) = run(&[]);
|
||||
assert_eq!(code, 64);
|
||||
assert!(stderr.contains("Usage:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_command_is_a_usage_error() {
|
||||
let (code, _stdout, stderr) = run(&["frobnicate"]);
|
||||
assert_eq!(code, 64);
|
||||
assert!(stderr.contains("error: unknown command \"frobnicate\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn info_on_a_fixture_reports_not_yet_available() {
|
||||
// The fixture mirrors the ctest invocation; the deferred gate fires
|
||||
// before any file access.
|
||||
let (code, _stdout, stderr) = run(&["info", "tests/project_with_footage.ove"]);
|
||||
assert_eq!(code, 1);
|
||||
assert!(stderr.contains("error: info: not yet available"), "stderr: {stderr}");
|
||||
assert!(stderr.contains("oakfacade"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn info_with_missing_argument_is_a_usage_error() {
|
||||
let (code, _stdout, stderr) = run(&["info"]);
|
||||
assert_eq!(code, 64);
|
||||
assert!(stderr.contains("Usage:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn probe_reports_not_yet_available() {
|
||||
let (code, _stdout, stderr) = run(&["probe", "tests/demo.mp4"]);
|
||||
assert_eq!(code, 1);
|
||||
assert!(stderr.contains("error: probe: not yet available"), "stderr: {stderr}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_reports_render_unavailable() {
|
||||
let (code, _stdout, stderr) = run(&["render", "p.ove", "0", "1", "out"]);
|
||||
assert_eq!(code, 2);
|
||||
assert!(stderr.contains("error: render: not yet available"), "stderr: {stderr}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_bad_seconds_is_a_usage_error() {
|
||||
let (code, _stdout, stderr) = run(&["render", "p.ove", "abc", "1", "out"]);
|
||||
assert_eq!(code, 64);
|
||||
assert!(stderr.contains("error: invalid start seconds \"abc\""), "stderr: {stderr}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_end_not_after_start_is_a_usage_error() {
|
||||
let (code, _stdout, stderr) = run(&["render", "p.ove", "2", "1", "out"]);
|
||||
assert_eq!(code, 64);
|
||||
assert!(stderr.contains("error: invalid end seconds \"1\""), "stderr: {stderr}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transcode_reports_render_unavailable() {
|
||||
let (code, _stdout, stderr) = run(&["transcode", "in.mp4", "out.mp4", "960"]);
|
||||
assert_eq!(code, 2);
|
||||
assert!(stderr.contains("error: transcode: not yet available"), "stderr: {stderr}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transcode_bad_width_is_a_usage_error() {
|
||||
let (code, _stdout, stderr) = run(&["transcode", "in.mp4", "out.mp4", "banana"]);
|
||||
assert_eq!(code, 64);
|
||||
assert!(stderr.contains("error: invalid width \"banana\""), "stderr: {stderr}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transcode_nonpositive_width_is_a_usage_error() {
|
||||
let (code, _stdout, stderr) = run(&["transcode", "in.mp4", "out.mp4", "0"]);
|
||||
assert_eq!(code, 64);
|
||||
assert!(stderr.contains("error: invalid width \"0\""), "stderr: {stderr}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transcode_unknown_format_is_a_usage_error() {
|
||||
let (code, _stdout, stderr) = run(&["transcode", "in.mp4", "out.mp4", "--format", "webm"]);
|
||||
assert_eq!(code, 64);
|
||||
assert!(stderr.contains("error: unknown --format \"webm\" (ppm|mp4)"), "stderr: {stderr}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transcode_ppm_format_is_accepted_then_reports_not_available() {
|
||||
let (code, _stdout, stderr) = run(&["transcode", "in.mp4", "outdir", "960", "--format", "ppm"]);
|
||||
assert_eq!(code, 2);
|
||||
assert!(stderr.contains("error: transcode: not yet available"), "stderr: {stderr}");
|
||||
}
|
||||
Reference in New Issue
Block a user