Two export killers fixed and the feature surfaced properly: - TicketArena::allocate() reaps finished fire-and-forget slots on every new submit, but RenderTask classified finished tickets by asking the arena for their kind/time afterwards - any ticket that completed before the next submit (all of them on the inline backend) failed with 'Render ticket reported an unexpected timestamp' and the export died after writing a header-only file. RenderTask now records each ticket's delivery key at submit time instead. - File > Export becomes 导出序列 (all 8 locales): the dialog offers a sequence picker (any sequence in the project, the current one preselected) instead of silently exporting the current one, and the project bin's sequence context menu gains 导出序列 opening the same dialog. Engines expose sequence_entries / current_sequence_id / start_export_of. - The export-project dialog drops its dead hand-typed path field (OK already asked the platform save dialog). - New end-to-end test: a one-second sequence exports to a real H.264/AAC MP4 that probes and decodes back to the clip's content.
oaktask — Oak task execution module (Rust)
Reimplements the C++ task module behind its frozen C ABI
(include/task/*.h). Every public type, trait, function and C ABI export is
implemented; the C++ code in src/task/src remains the parity source of
truth (// CPP-PARITY: markers point at the exact C++ file).
Scope
This crate replaces src/task/src (the C++ olive::Task family: the Task
base class, the TaskManager singleton, the codec submitter bridge, and the
concrete task classes ConformTask, CustomCacheTask, ExportTask,
PreCacheTask, ProxyTask, RenderTask, and the project
load/save/import/OTIO tasks). The behavior it reproduces is defined by:
- the frozen C ABI headers
include/task/*.h(authoritative), - the C++ implementation in
src/task/src(parity source of truth), - the C++ unit tests in
src/task/tests/task_test.cpp(golden source).
Architectural decisions
-
C++ inheritance → one module per class + trait objects.
olive::Taskis an abstract base with a virtualrun(). In Rust each concrete task is astructand the shared behavior lives on a commonTaskbase that exposes arun: Box<dyn FnMut() -> Result<()>>-style field (or an equivalentTaskBehaviortrait object).RenderTaskand its subclasses (ExportTask,PreCacheTask) use the same trait-object approach: the abstract base'sdownload_frame/frame_downloaded/audio_downloaded/encode_subtitlevirtuals become a single trait the subclasses implement. Rationale: Rust has no virtual inheritance, and a trait object is the minimal faithful mapping; it keeps each concrete task independent so the crate stays modular. -
Cancellation goes through the oakrender C ABI. The C++
Taskholds anolive::CancelAtom(fromrender/cancelatom.h). Until oakrender is rewritten, the Rust crate reaches it throughbridge::render(oakrender_cancelatom_*), with a borrowedOakCancelAtomstored on the base task. No oakrender type is reimplemented here. -
Event listeners are the one async return channel. The C++
Taskexposes an event listener (k_event_started/k_event_progress/k_event_finished) delivered on the task's own thread. Qt signals are gone; the Rust equivalent is a callback (std::function-shaped closure) invoked under aMutex, matching theoaktask_task_subscribeC ABI contract (event idsSTARTED/PROGRESS/FINISHED, progress in 0..1). This is the single deliberate async exception to the otherwise fully synchronous model. -
Render-driven tasks reuse oakrender tickets.
RenderTask::render()drives oakrender tickets (bridge::render,oakrender_ticket_render_frame/oakrender_ticket_render_audio). TheForceParamsstruct maps to theoakrender_video_ticket_paramsforce fields. The ticket'soakrender_ticket_finished_fncallback (fired on the render thread) is the only other async return channel. -
Project tasks borrow oaknode handles, take ownership only on take*.
ProjectSaveTaskborrows itsOakNodeProject;ProjectImportTaskborrows the folder/project and produces anOakUndoCommand(oaktask_import_take_command) and a list of importedOakNodeFootage.ProjectLoadTask/LoadOTIOTasktransfer ownership ontake_project(). This mirrors the C ABI's borrow/take split exactly. -
OTIO load/save parses with the pure-Rust
oakotiobinding.SaveOTIOTask/LoadOTIOTaskparse and serialize OpenTimelineIO JSON (.otio) and FCPXML (.fcpxml) through theoakotiocrate (crates/oakotio, a self-contained serde model of exactly the object graph the C++ loadotio/saveotio tasks use) instead of the C++ OTIO library. The format is inferred from the filename extension (case-insensitive;src/project/format.rs), so the frozen C ABI (include/task/project.h) needs no format parameter. Format handling ends at theoakotioparse/serialize call — the track/clip/footage building (load) and theserialize_*helpers (save) are shared between both formats, mirroring the C++ tasks. No OTIO or FCPXML type crosses the oaktask C ABI — the tasks build the project through the oaknode/oaktimeline C ABIs (load) and writeoakotio::Timeline/SerializableCollectiondocuments (save), mirroring the C++serialize_*helpers.ExportTask/PreCacheTaskuseoakcore_rs::{Rational, TimeRange}for their frame/audio timing (the reason this crate depends onoakcore-rs).
Layout
src/
lib.rs crate doc + module declarations
error.rs OAKTASK_* codes + Error enum (module number 08)
handle.rs owned-handle box + get/get_mut views (facade task boxes)
task.rs Task base class + TaskEvent / EventListener
manager.rs TaskManager singleton
codecbridge.rs codec task submitter registration
conform.rs ConformTask + derive_filenames
proxy.rs ProxyTask + build_arguments / parse_progress
customcache.rs CustomCacheTask
render.rs RenderTask base + ForceParams
export.rs ExportTask
precache.rs PreCacheTask
project.rs project module (declares submodules)
project/load.rs ProjectLoadBaseTask + ProjectLoadTask
project/save.rs ProjectSaveTask
project/import.rs ProjectImportTask
project/format.rs interchange-format dispatch (.otio / .fcpxml)
project/loadotio.rs LoadOTIOTask
project/saveotio.rs SaveOTIOTask
bridge/ extern "C" imports of other modules' C ABIs
mod.rs, codec.rs, common.rs, node.rs, render.rs, timeline.rs, undo.rs
ffi.rs C ABI export layer (one pub mod per include/task header)
tests/ contract + parity tests
Hard rules
- Every C ABI export checks its handle before dereferencing and maps panics
through
handle::guard*/ explicitcatch_unwind; panics never cross FFI. - Handles are opaque refcounted boxes;
ctxnever points at Rust data directly except throughRefBox. - All C++ coupling comments carry a
// CPP-PARITY: <file>marker so a C++ change that breaks parity is greppable. - Free functions are
no-opon null handles;free(NULL)is always safe. - Error codes mirror
include/task/error.hverbatim (module 08, plusOAKTASK_E_CANCELLED = -80006).
Test strategy
cargo test runs the full suite against link-time stubs of the
other-module C ABIs (tests/common/mod.rs, pulled into each integration
test binary via #[path]). The oaktask library declares its cross-module
imports as real extern "C" symbols (production builds link them against
the oak dylibs); the test binaries provide #[no_mangle] stub definitions
so plain cargo test links and runs without any oak dylib. The stubs are
faithful-in-spirit: serializer round-trips write/validate a marker file,
footage validity checks real file existence, the codec submitter routes
conform/proxy requests into the task module, and a fake ffmpeg executable
drives the proxy run end to end. The same scenarios against the real dylibs
are covered by the C++ gtest suite (src/task/tests/task_test.cpp).
Since oakrender's ticket C ABI is live, the render loop is also verified
against the real oakrender arena (no stubs): tests/ render_real_integration_test.rs links the oakrender crate into the test
binary and drives RenderTask::render through bridge::render's
extern "C" declarations, getting real frames back in timestamp order via
the CPU path (no GPU). Both rlibs expose #[no_mangle] exports that cannot
coexist with the stub-based test binaries in one link, so the test is gated
behind the real-oakrender feature and built alone:
cargo test --features real-oakrender --test render_real_integration_test
The render stubs in tests/common/mod.rs are compiled out under the same
feature (#[cfg(not(feature = "real-oakrender"))]), so plain cargo test
is unaffected.
OTIO load/save is implemented end to end: oakotio (path dep,
crates/oakotio) parses the document inside LoadOTIOTask::run and
serializes the project in SaveOTIOTask::run; the tasks keep driving the
oaknode/oaktimeline C ABIs exactly like the C++ implementations. tests/ otio_test.rs builds synthetic documents with the oakotio model, imports
them, and re-parses exports for a round-trip check (README decision #6).
The tasks are format-aware (.otio → OpenTimelineIO JSON, .fcpxml →
FCPXML, dispatched from the extension in src/project/format.rs): the
FCPXML tests build documents with oakotio's fcpxml writer, cover the
extension dispatch matrix (.otio/.fcpxml/.OTIO/.FCPXML/unknown)
and the FCPXML error paths (corrupt XML, unsupported version), and run a
save → load cycle through both tasks.
Dependency policy
Prefer mature third-party crates (MIT/Apache-2.0/BSD, GPL-compatible) over hand-rolling; register each addition (name + reason) here. Large existing C++ libraries (OTIO, OCIO, OIIO, FFmpeg) are NEVER rewritten — they are consumed through their C ABI / bridge layers.
Current dependency inventory:
oakcore-rs(path dep,Rational/TimeRangefor frame/audio timing).oakotio(path dep,crates/oakotio— pure-Rust serde model of the OpenTimelineIO JSON format used byLoadOTIOTask/SaveOTIOTask; brings inserde/serde_jsontransitively).oakrender(path dep, optional, enabled only by thereal-oakrenderfeature): links the real oakrender crate into therender_real_integration_testbinary. The library never references it — all render access stays on thebridge::renderC ABI.