Files
oak-editor/crates/oak-codec/src/oiioframebridge.rs
T
Mike-Solar 244d5e860f
CI / Build & test (Windows) (push) Failing after 7s
workspace: kebab-case crates, app under crates/oak-app, shared versions
All crates take the oak-* kebab-case naming (oak-audio, oak-codec,
oak-common, oak-core, oak-ffmpeg-link, oak-node, oak-otio, oak-plugin,
oak-render, oak-storage, oak-task, oak-timeline, oak-undo), with the
lib identifiers rewritten (oakrender:: -> oak_render::, oakcore_rs:: ->
oak_core::, ...) across all 226 referencing files.

The GUI application moves from the workspace root into
crates/oak-app/: src/, build.rs (paths fixed for the new location) and
tests/ travel with it, the root Cargo.toml becomes workspace-only
([workspace] + workspace.package + profiles), and the app package
inherits the workspace version. The screenshots example becomes a
standalone crate examples/simple_player/ with its own Cargo.toml.

Every crate now inherits the single workspace version
(version.workspace = true), and the workflows' crate paths and the
build docs follow the renames.

Validated with a clean cargo check --workspace.
2026-08-22 16:58:37 +08:00

421 lines
13 KiB
Rust

// 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/>.
//! `oiio_frame_to_buffer` / `oiio_buffer_to_frame` — codec-internal OIIO
//! frame <-> pixel-buffer conversion.
//!
//! Mirrors `src/codec/src/oiioframebridge.{h,cpp}`. These are internal C++
//! functions that moved into codec from oakcommon (NOTES.md §oakcommon侧修复);
//! oakcommon keeps its OIIO mapping functions; the frame conversion itself
//! lives here.
//!
//! The C++ bridge copies pixels through the live OpenImageIO `ImageBuf`
//! (`oiio_frame_to_buffer`/`oiio_buffer_to_frame`). OIIO is not linked into
//! this build, so the port serializes the frame into a self-describing byte
//! buffer instead. The layout is stable (documented in
//! [`OiioBufferHeader`]); it carries the frame's geometry, pixel format,
//! timestamp and time base alongside the raw pixel rows, so a buffer can be
//! turned back into an equivalent [`Frame`] without any external state.
use oak_common::ocioutils::PixelFormat as OakPixelFormat;
use oak_common::videoparams::VideoParams;
use crate::frame::Frame;
use oak_core::Rational;
use std::ffi::c_int;
/// Fixed header size, in bytes, of an OIIO frame buffer.
///
/// The layout is a 64-byte little-endian header followed by the raw pixel
/// data. `OiioBufferHeader::to_bytes` / `from_bytes` are the single writer /
/// reader of this header, so the exact offsets only ever exist in one place.
const HEADER_LEN: usize = 64;
/// Magic bytes identifying an OIIO frame buffer (`"OFMB"`).
const MAGIC: &[u8; 4] = b"OFMB";
/// Current serialization version.
const VERSION: u32 = 1;
/// Decoded OIIO frame-buffer header.
///
/// # Byte layout (little-endian, `HEADER_LEN` = 64 bytes)
///
/// | Offset | Size | Field |
/// |--------|------|-------|
/// | 0 | 4 | Magic bytes `"OFMB"` |
/// | 4 | 4 | Serialization `version` (`u32`, currently 1) |
/// | 8 | 4 | `width` (`i32`) |
/// | 12 | 4 | `height` (`i32`) |
/// | 16 | 4 | `format` (`i32`, an `OakPixelFormat` value) |
/// | 20 | 4 | `linesize_bytes` (`i32`, distance between pixel rows) |
/// | 24 | 8 | `timestamp_num` (`i64`) |
/// | 32 | 8 | `timestamp_den` (`i64`) |
/// | 40 | 8 | `time_base_num` (`i64`) |
/// | 48 | 8 | `time_base_den` (`i64`) |
/// | 56 | 8 | `pixel_len` (`u64`, pixel-data length in bytes) |
/// | 64 | … | raw pixel data (`pixel_len` bytes) |
///
/// `pixel_len` must equal `linesize_bytes * height`; the frame geometry in the
/// header must match the params the frame is reconstructed with.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
struct OiioBufferHeader {
version: u32,
width: i32,
height: i32,
format: i32,
linesize_bytes: i32,
timestamp_num: i64,
timestamp_den: i64,
time_base_num: i64,
time_base_den: i64,
pixel_len: u64,
}
impl OiioBufferHeader {
/// Serialize the header into exactly `HEADER_LEN` bytes (little-endian).
fn to_bytes(&self) -> [u8; HEADER_LEN] {
let mut b = [0u8; HEADER_LEN];
b[0..4].copy_from_slice(MAGIC);
b[4..8].copy_from_slice(&self.version.to_le_bytes());
b[8..12].copy_from_slice(&self.width.to_le_bytes());
b[12..16].copy_from_slice(&self.height.to_le_bytes());
b[16..20].copy_from_slice(&self.format.to_le_bytes());
b[20..24].copy_from_slice(&self.linesize_bytes.to_le_bytes());
b[24..32].copy_from_slice(&self.timestamp_num.to_le_bytes());
b[32..40].copy_from_slice(&self.timestamp_den.to_le_bytes());
b[40..48].copy_from_slice(&self.time_base_num.to_le_bytes());
b[48..56].copy_from_slice(&self.time_base_den.to_le_bytes());
b[56..64].copy_from_slice(&self.pixel_len.to_le_bytes());
b
}
/// Parse a header from the start of `bytes`, validating the magic and
/// version and requiring at least `HEADER_LEN` bytes. Returns the decoded
/// header and the number of bytes consumed (`HEADER_LEN`).
fn from_bytes(bytes: &[u8]) -> crate::error::Result<(Self, usize)> {
if bytes.len() < HEADER_LEN {
return Err(crate::error::Error::Invalid);
}
if &bytes[0..4] != MAGIC {
return Err(crate::error::Error::Invalid);
}
let version = u32::from_le_bytes(bytes[4..8].try_into().unwrap());
if version != VERSION {
return Err(crate::error::Error::Invalid);
}
let header = OiioBufferHeader {
version,
width: i32::from_le_bytes(bytes[8..12].try_into().unwrap()),
height: i32::from_le_bytes(bytes[12..16].try_into().unwrap()),
format: i32::from_le_bytes(bytes[16..20].try_into().unwrap()),
linesize_bytes: i32::from_le_bytes(bytes[20..24].try_into().unwrap()),
timestamp_num: i64::from_le_bytes(bytes[24..32].try_into().unwrap()),
timestamp_den: i64::from_le_bytes(bytes[32..40].try_into().unwrap()),
time_base_num: i64::from_le_bytes(bytes[40..48].try_into().unwrap()),
time_base_den: i64::from_le_bytes(bytes[48..56].try_into().unwrap()),
pixel_len: u64::from_le_bytes(bytes[56..64].try_into().unwrap()),
};
Ok((header, HEADER_LEN))
}
}
/// Convert an OIIO-backed `Frame` into a raw pixel buffer.
///
/// # CPP-PARITY
/// `src/codec/src/oiioframebridge.cpp` `oiio_frame_to_buffer` —
/// allocates the destination and copies the OIIO pixel data out.
///
/// The buffer is a 64-byte [`OiioBufferHeader`] followed by the frame's pixel
/// rows (see the header docs for the exact layout). The frame must already be
/// allocated ([`Frame::allocate`]); otherwise this returns
/// [`crate::error::Error::State`].
pub fn oiio_frame_to_buffer(frame: &Frame) -> crate::error::Result<Vec<u8>> {
let data = frame.data().ok_or(crate::error::Error::State)?;
let width = frame.width();
let height = frame.height();
let format = frame.format() as i32;
let linesize_bytes = frame.linesize_bytes();
let timestamp = frame.timestamp();
let (time_base_num, time_base_den) = match frame.params() {
Some(p) => {
let (num, den) = p.time_base();
(i64::from(num), i64::from(den))
}
None => (0, 0),
};
let header = OiioBufferHeader {
version: VERSION,
width,
height,
format,
linesize_bytes,
timestamp_num: timestamp.numerator(),
timestamp_den: timestamp.denominator(),
time_base_num,
time_base_den,
pixel_len: data.len() as u64,
};
let mut out = Vec::with_capacity(HEADER_LEN + data.len());
out.extend_from_slice(&header.to_bytes());
out.extend_from_slice(data);
Ok(out)
}
/// Convert a raw pixel buffer back into a `Frame`.
///
/// # CPP-PARITY
/// `src/codec/src/oiioframebridge.cpp` `oiio_buffer_to_frame` —
/// wraps the buffer bytes in a `Frame` for encoder consumption.
///
/// Parses a [`OiioBufferHeader`] from the front of `buffer`, reconstructs the
/// frame's params (geometry, time base, pixel format) and timestamp, then
/// copies the pixel rows into an allocated frame. The buffer is rejected with
/// [`crate::error::Error::Invalid`] when the magic/version is wrong, the
/// header is truncated, or `pixel_len` is inconsistent with the declared
/// `linesize_bytes * height` / the bytes actually present.
pub fn oiio_buffer_to_frame(buffer: &[u8]) -> crate::error::Result<Frame> {
let (header, consumed) = OiioBufferHeader::from_bytes(buffer)?;
let pixels = &buffer[consumed..];
// `pixel_len` must be consistent with the header geometry and the bytes
// actually present (checked, so a corrupt header can't panic later).
if header.linesize_bytes < 0 || header.height < 0 {
return Err(crate::error::Error::Invalid);
}
let expected = (header.linesize_bytes as u64).checked_mul(header.height as u64);
if expected != Some(header.pixel_len) {
return Err(crate::error::Error::Invalid);
}
if pixels.len() != header.pixel_len as usize {
return Err(crate::error::Error::Invalid);
}
// Build the params from the header, then hand ownership to the frame.
let mut params = VideoParams::new_with_time_base(
header.width,
header.height,
header.time_base_num as c_int,
header.time_base_den as c_int,
OakPixelFormat::from_code(0),
4,
1,
1,
0,
1,
);
params.set_format(OakPixelFormat::from_code(header.format));
let mut frame = Frame::with_params(params);
frame.set_timestamp(Rational::new(header.timestamp_num, header.timestamp_den));
frame.allocate()?;
match frame.data_mut() {
Some(dst) if dst.len() == pixels.len() => {
dst.copy_from_slice(pixels);
Ok(frame)
}
// Reconstructed params should produce exactly `pixel_len` bytes; if
// the geometry in the header disagreed with the format's line size,
// refuse rather than copy a mismatched slice.
_ => Err(crate::error::Error::Invalid),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn header_round_trip() {
let header = OiioBufferHeader {
version: VERSION,
width: 100,
height: 50,
format: 0, // U8
linesize_bytes: 512,
timestamp_num: 5,
timestamp_den: 2,
time_base_num: 1,
time_base_den: 30,
pixel_len: 25600,
};
let bytes = header.to_bytes();
assert_eq!(bytes.len(), HEADER_LEN);
let (decoded, consumed) = OiioBufferHeader::from_bytes(&bytes).unwrap();
assert_eq!(consumed, HEADER_LEN);
assert_eq!(decoded, header);
}
#[test]
fn header_is_little_endian_magic_and_version() {
let header = OiioBufferHeader {
version: VERSION,
width: 1,
height: 1,
format: 0,
linesize_bytes: 4,
timestamp_num: 0,
timestamp_den: 1,
time_base_num: 0,
time_base_den: 0,
pixel_len: 4,
};
let bytes = header.to_bytes();
assert_eq!(&bytes[0..4], b"OFMB");
assert_eq!(u32::from_le_bytes(bytes[4..8].try_into().unwrap()), 1);
// width = 1 -> little-endian 01 00 00 00.
assert_eq!(&bytes[8..12], &[1, 0, 0, 0]);
}
#[test]
fn from_bytes_rejects_truncated() {
let header = OiioBufferHeader {
version: VERSION,
width: 1,
height: 1,
format: 0,
linesize_bytes: 4,
timestamp_num: 0,
timestamp_den: 1,
time_base_num: 0,
time_base_den: 0,
pixel_len: 4,
};
let bytes = header.to_bytes();
assert!(OiioBufferHeader::from_bytes(&bytes[..HEADER_LEN - 1]).is_err());
}
#[test]
fn from_bytes_rejects_bad_magic_and_version() {
let mut bytes = OiioBufferHeader {
version: VERSION,
width: 1,
height: 1,
format: 0,
linesize_bytes: 4,
timestamp_num: 0,
timestamp_den: 1,
time_base_num: 0,
time_base_den: 0,
pixel_len: 4,
}
.to_bytes();
bytes[0] = b'X';
assert!(OiioBufferHeader::from_bytes(&bytes).is_err());
let mut bytes = OiioBufferHeader {
version: VERSION,
width: 1,
height: 1,
format: 0,
linesize_bytes: 4,
timestamp_num: 0,
timestamp_den: 1,
time_base_num: 0,
time_base_den: 0,
pixel_len: 4,
}
.to_bytes();
bytes[4..8].copy_from_slice(&99u32.to_le_bytes());
assert!(OiioBufferHeader::from_bytes(&bytes).is_err());
}
/// A small helper to build a fully allocated, filled frame using the same
/// params pattern as `frame.rs`.
fn make_frame() -> Frame {
let mut params = VideoParams::new_with_time_base(
100,
50,
1,
30,
OakPixelFormat::from_code(0),
4,
1,
1,
0,
1,
);
params.set_format(OakPixelFormat::from_code(0)); // U8
let mut frame = Frame::with_params(params);
frame.set_timestamp(Rational::new(5, 2));
frame.allocate().unwrap();
frame
}
#[test]
fn frame_to_buffer_round_trip() {
let mut frame = make_frame();
if let Some(d) = frame.data_mut() {
d.fill(0xAB);
}
let buffer = oiio_frame_to_buffer(&frame).unwrap();
assert_eq!(buffer.len(), HEADER_LEN + frame.allocated_size());
let (header, consumed) = OiioBufferHeader::from_bytes(&buffer).unwrap();
assert_eq!(consumed, HEADER_LEN);
assert_eq!(header.width, 100);
assert_eq!(header.height, 50);
assert_eq!(header.format, 0);
assert_eq!(header.linesize_bytes, 512);
assert_eq!(header.timestamp_num, 5);
assert_eq!(header.timestamp_den, 2);
assert_eq!(header.time_base_num, 1);
assert_eq!(header.time_base_den, 30);
assert_eq!(header.pixel_len, frame.allocated_size() as u64);
}
#[test]
fn buffer_to_frame_round_trip() {
let mut frame = make_frame();
let expected_len = frame.allocated_size();
if let Some(d) = frame.data_mut() {
for (i, b) in d.iter_mut().enumerate() {
*b = (i % 256) as u8;
}
}
let buffer = oiio_frame_to_buffer(&frame).unwrap();
let out = oiio_buffer_to_frame(&buffer).unwrap();
assert_eq!(out.width(), 100);
assert_eq!(out.height(), 50);
assert_eq!(out.format() as i32, 0);
assert_eq!(out.allocated_size(), expected_len);
assert_eq!(out.timestamp().numerator(), 5);
assert_eq!(out.timestamp().denominator(), 2);
assert_eq!(out.data().unwrap(), frame.data().unwrap());
}
#[test]
fn buffer_to_frame_rejects_corrupt() {
let frame = make_frame();
let buffer = oiio_frame_to_buffer(&frame).unwrap();
// Truncated pixel data.
assert!(oiio_buffer_to_frame(&buffer[..HEADER_LEN + 1]).is_err());
// Bad magic.
let mut bad = buffer.clone();
bad[0] = 0;
assert!(oiio_buffer_to_frame(&bad).is_err());
// pixel_len inconsistent with linesize * height.
let mut bad = buffer.clone();
bad[56..64].copy_from_slice(&(999u64).to_le_bytes());
assert!(oiio_buffer_to_frame(&bad).is_err());
}
}