// 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 . //! Background task submission hook (`include/codec/task.h`). //! //! The codec module needs occasional background work (audio conforms, //! proxy transcodes). The task system itself splits out at milestone M8; //! until then oakcodec exposes a single global submit callback. A host //! (M8: oaktask) registers with [`set_task_submit_cb`]; the conform/proxy //! managers call it whenever they need a task. With no callback, managers //! report work as unavailable — they never crash and never block. use std::ffi::{c_void, CString}; use std::sync::Mutex; use crate::error::{Error, OAKCODEC_OK}; /// Kinds of background tasks oakcodec can request. #[derive(Clone, Copy, Debug, PartialEq, Eq)] #[repr(i32)] pub enum TaskKind { /// Audio conform to pcm cache files. Conform = 0, /// Video proxy transcode. Proxy = 1, } /// Description of one background task request. /// /// All strings are borrowed and only valid for the duration of the submit /// call; the callback must copy anything it retains. #[repr(C)] pub struct TaskRequest<'a> { /// `TaskKind`. pub kind: TaskKind, /// Source media filename. pub input_filename: &'a str, /// Final destination path (see field docs in include/codec/task.h). pub output_filename: &'a str, /// Stream inside the source media. pub stream_index: i32, /// Conform: target sample rate. pub sample_rate: i32, /// Conform: target channel layout mask. pub channel_layout: u64, /// Conform: target sample format (enum as int). pub sample_format: i32, /// Proxy: target width (0 = unspecified/divider-based). pub proxy_width: i32, /// Proxy: target height (0 = unspecified/divider-based). pub proxy_height: i32, } /// Task submit callback signature. /// /// Returns `Ok(())` if the task was accepted (completed synchronously or /// queued), `Err` if the request was rejected. pub type TaskSubmitFn = dyn Fn(&TaskRequest, *mut std::ffi::c_void) -> crate::error::Result<()>; /// `OakCodecTaskRequest` — C ABI mirror of [`TaskRequest`] for the submit /// callback; see `include/codec/task.h`. Strings are borrowed C pointers, /// valid only for the duration of the call. #[repr(C)] pub struct OakCodecTaskRequest { /// `TaskKind` value. pub kind: i32, /// Source media filename. pub input_filename: *const std::ffi::c_char, /// Final destination path. pub output_filename: *const std::ffi::c_char, /// Stream inside the source media. pub stream_index: i32, /// Conform: target sample rate. pub sample_rate: i32, /// Conform: target channel-layout mask. pub channel_layout: u64, /// Conform: target sample format (enum as int). pub sample_format: i32, /// Proxy: target width (0 = unspecified/divider-based). pub proxy_width: i32, /// Proxy: target height (0 = unspecified/divider-based). pub proxy_height: i32, } /// `oakcodec_task_submit_fn` — the extern-C submit callback typedef; see /// `include/codec/task.h`. Returns `OAKCODEC_OK` on accept, else a /// negative `OAKCODEC_E_*` code. pub type OakCodecTaskSubmitFn = unsafe extern "C" fn(req: *const OakCodecTaskRequest, userdata: *mut std::ffi::c_void) -> i32; /// One registered submit callback (extern-C from the host, or a crate /// Rust closure). Mirrors the C++ `g_task_cb`/`g_task_cb_userdata` pair. enum SubmitCb { /// No callback registered. None, /// Extern-C callback registered via `oakcodec_set_task_submit_cb`. Extern { /// The C function pointer. cb: OakCodecTaskSubmitFn, /// Opaque userdata passed back on each call. userdata: *mut c_void, }, /// Crate-internal Rust closure registered via [`set_task_submit_cb`]. Rust { /// Raw fat-pointer to the `&'static TaskSubmitFn` (kept `*const` so /// the registry is `Send`). cb: *const TaskSubmitFn, /// Opaque userdata passed back on each call. userdata: *mut c_void, }, } // # Safety: the stored pointers (extern-C fn pointer, fat pointer to a // 'static closure, userdata) are only dereferenced/called while holding the // registry mutex; the Rust closure is 'static and the extern-C fn outlives // registration by contract. Moving the enum between threads under the lock // therefore cannot alias. unsafe impl Send for SubmitCb {} /// The global task submit callback registry. Only one callback is held at /// a time; registering replaces it, `None` clears it. Thread-safe. static TASK_SUBMIT: Mutex = Mutex::new(SubmitCb::None); /// Registers (or replaces) the global task submit callback. Pass `None` to /// unregister. Thread-safe. Interim state (pre-M8): nobody registers and all /// task-dependent work reports unavailable. pub fn set_task_submit_cb(cb: Option<&'static TaskSubmitFn>, userdata: *mut std::ffi::c_void) { let mut g = TASK_SUBMIT.lock().unwrap(); *g = match cb { Some(cb) => SubmitCb::Rust { cb: cb as *const TaskSubmitFn, userdata, }, None => SubmitCb::None, }; } /// Register an extern-C submit callback (used by `ffi::task`). /// /// Mirrors `oakcodec_set_task_submit_cb`: a `None` pointer clears it. pub(crate) fn set_task_submit_cb_extern( cb: Option, userdata: *mut std::ffi::c_void, ) { let mut g = TASK_SUBMIT.lock().unwrap(); *g = match cb { Some(cb) => SubmitCb::Extern { cb, userdata }, None => SubmitCb::None, }; } /// Returns 1 if a submit callback is currently registered, else 0. /// Thread-safe. pub fn task_submit_is_registered() -> bool { let g = TASK_SUBMIT.lock().unwrap(); !matches!(&*g, SubmitCb::None) } /// Submit a task through the registered callback, if any. /// /// Returns `Ok(false)` when no callback is registered (nothing submitted), /// `Ok(true)` when accepted, or `Err` when the callback rejected it. pub fn submit_task(req: &TaskRequest) -> crate::error::Result { let g = TASK_SUBMIT.lock().unwrap(); match &*g { SubmitCb::None => Ok(false), SubmitCb::Extern { cb, userdata } => { // Bind the C strings to locals so the temporaries outlive the // callback call (their pointers feed the request struct). let in_c = cstring_or_empty(req.input_filename); let out_c = cstring_or_empty(req.output_filename); let creq = OakCodecTaskRequest { kind: req.kind as i32, input_filename: in_c.as_ptr(), output_filename: out_c.as_ptr(), stream_index: req.stream_index, sample_rate: req.sample_rate, channel_layout: req.channel_layout, sample_format: req.sample_format, proxy_width: req.proxy_width, proxy_height: req.proxy_height, }; // # Safety: the callback is a C function we registered; passing a // request whose string pointers are alive for the call duration. let ret = unsafe { cb(&creq, *userdata) }; if ret == OAKCODEC_OK { Ok(true) } else { Err(Error::Failed(format!( "task submit rejected (code {})", ret ))) } } SubmitCb::Rust { cb, userdata } => { // # Safety: the fat pointer was stored by set_task_submit_cb and // points to a 'static closure that outlives this call. let cb = unsafe { &**cb }; match cb(req, *userdata) { Ok(()) => Ok(true), Err(e) => Err(e), } } } } /// Build a NUL-terminated C string from a Rust string; empty on embedded /// NUL (callers pass sane filenames, so this is defensive only). fn cstring_or_empty(s: &str) -> CString { CString::new(s).unwrap_or_else(|_| CString::new("").unwrap()) } #[cfg(test)] mod tests { use super::*; // Same registry lock the conform/proxy/ffi tests use: the submit // callback is process-global and every test that mutates it must // serialize on the same mutex. use crate::conformmanager::test_util::REG_LOCK; unsafe extern "C" fn reject_cb( _req: *const OakCodecTaskRequest, _ud: *mut std::ffi::c_void, ) -> i32 { -1 // rejected } #[test] fn submit_via_rust_closure_and_clear() { let _g = REG_LOCK.lock().unwrap(); // A Rust closure that accepts and records the request. let accepted = std::sync::Arc::new(std::sync::Mutex::new(false)); let recorded = std::sync::Arc::new(std::sync::Mutex::new(None::)); let acc = accepted.clone(); let rec = recorded.clone(); let cb: &'static TaskSubmitFn = Box::leak(Box::new( move |req: &TaskRequest, _ud: *mut std::ffi::c_void| { *acc.lock().unwrap() = true; *rec.lock().unwrap() = Some(req.output_filename.to_string()); Ok(()) }, )); set_task_submit_cb(Some(cb), std::ptr::null_mut()); assert!(task_submit_is_registered()); let req = TaskRequest { kind: TaskKind::Conform, input_filename: "in.mp4", output_filename: "out.pcm", stream_index: 1, sample_rate: 48000, channel_layout: 0x3, sample_format: 10, proxy_width: 0, proxy_height: 0, }; assert!(submit_task(&req).unwrap()); assert!(*accepted.lock().unwrap()); assert_eq!(recorded.lock().unwrap().as_deref(), Some("out.pcm")); // Clearing the callback: nothing submitted. set_task_submit_cb(None, std::ptr::null_mut()); assert!(!task_submit_is_registered()); assert!(!submit_task(&req).unwrap()); } #[test] fn extern_cb_reject_maps_to_err() { let _g = REG_LOCK.lock().unwrap(); set_task_submit_cb_extern(Some(reject_cb), std::ptr::null_mut()); let req = TaskRequest { kind: TaskKind::Proxy, input_filename: "in.mp4", output_filename: "out.mp4", stream_index: 0, sample_rate: 0, channel_layout: 0, sample_format: 0, proxy_width: 1280, proxy_height: 720, }; assert!(submit_task(&req).is_err()); set_task_submit_cb_extern(None, std::ptr::null_mut()); } #[test] fn extern_cb_accept_returns_ok() { let _g = REG_LOCK.lock().unwrap(); set_task_submit_cb_extern( Some(crate::conformmanager::test_util::accept_cb), std::ptr::null_mut(), ); let req = TaskRequest { kind: TaskKind::Conform, input_filename: "in.mp4", output_filename: "out.pcm", stream_index: 0, sample_rate: 0, channel_layout: 0, sample_format: 0, proxy_width: 0, proxy_height: 0, }; assert!(submit_task(&req).unwrap()); set_task_submit_cb_extern(None, std::ptr::null_mut()); } #[test] fn cstring_or_empty_handles_embedded_nul() { // Embedded NUL -> empty string (defensive). let c = cstring_or_empty("a\0b"); assert_eq!(c.as_c_str().to_bytes(), b""); assert_eq!(cstring_or_empty("ok").as_c_str().to_bytes(), b"ok"); } #[test] fn task_kind_values_match_abi() { assert_eq!(TaskKind::Conform as i32, OAKCODEC_TASK_CONFORM as i32); assert_eq!(TaskKind::Proxy as i32, OAKCODEC_TASK_PROXY as i32); } // ABI constants mirrored from include/codec/task.h. const OAKCODEC_TASK_CONFORM: i32 = 0; const OAKCODEC_TASK_PROXY: i32 = 1; }