mirror of
https://github.com/darkrenaissance/darkfi.git
synced 2026-01-09 14:48:08 -05:00
app: add new video module, which contains IVF demuxer and YUV -> RGB color conversion. add rav1d branch with rust API to cargo.toml
This commit is contained in:
114
bin/app/Cargo.lock
generated
114
bin/app/Cargo.lock
generated
@@ -638,6 +638,26 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "628d228f918ac3b82fe590352cc719d30664a0c13ca3a60266fe02c7132d480a"
|
||||
|
||||
[[package]]
|
||||
name = "atomig"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd0f41f4bb89f5c6450325e283fb78c4a3d042181b54f3855ee2f872919f9863"
|
||||
dependencies = [
|
||||
"atomig-macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomig-macro"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49c98dba06b920588de7d63f6acc23f1e6a9fade5fd6198e564506334fb5a4f5"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atty"
|
||||
version = "0.2.14"
|
||||
@@ -655,6 +675,19 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
|
||||
|
||||
[[package]]
|
||||
name = "av-data"
|
||||
version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fca67ba5d317924c02180c576157afd54babe48a76ebc66ce6d34bb8ba08308e"
|
||||
dependencies = [
|
||||
"byte-slice-cast",
|
||||
"bytes",
|
||||
"num-derive",
|
||||
"num-rational",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "av-scenechange"
|
||||
version = "0.14.1"
|
||||
@@ -880,6 +913,12 @@ version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06"
|
||||
|
||||
[[package]]
|
||||
name = "byte-slice-cast"
|
||||
version = "1.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d"
|
||||
|
||||
[[package]]
|
||||
name = "bytecount"
|
||||
version = "0.6.9"
|
||||
@@ -1538,6 +1577,7 @@ dependencies = [
|
||||
"peniko",
|
||||
"qoi",
|
||||
"rand 0.8.5",
|
||||
"rav1d",
|
||||
"regex",
|
||||
"semver",
|
||||
"sled-overlay",
|
||||
@@ -2788,7 +2828,7 @@ checksum = "6ea2d84b969582b4b1864a92dc5d27cd2b77b622a8d79306834f1be5ba20d84b"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crunchy",
|
||||
"zerocopy",
|
||||
"zerocopy 0.8.30",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3673,6 +3713,16 @@ dependencies = [
|
||||
"pxfm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nasm-rs"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34f676553b60ccbb76f41f9ae8f2428dac3f259ff8f1c2468a174778d06a1af9"
|
||||
dependencies = [
|
||||
"jobserver",
|
||||
"log",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ndk-sys"
|
||||
version = "0.2.2"
|
||||
@@ -4399,7 +4449,7 @@ version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
|
||||
dependencies = [
|
||||
"zerocopy",
|
||||
"zerocopy 0.8.30",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4649,6 +4699,28 @@ dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rav1d"
|
||||
version = "1.1.0"
|
||||
source = "git+https://github.com/leo030303/rav1d?branch=add-rust-api#3ef268229621b863fd88a20cea226944d764dcd0"
|
||||
dependencies = [
|
||||
"assert_matches",
|
||||
"atomig",
|
||||
"av-data",
|
||||
"bitflags 2.10.0",
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"nasm-rs",
|
||||
"parking_lot 0.12.5",
|
||||
"paste",
|
||||
"raw-cpuid",
|
||||
"static_assertions",
|
||||
"strum",
|
||||
"to_method",
|
||||
"zerocopy 0.7.35",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rav1e"
|
||||
version = "0.8.1"
|
||||
@@ -4699,6 +4771,15 @@ dependencies = [
|
||||
"rgb",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "raw-cpuid"
|
||||
version = "11.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "498cd0dc59d73224351ee52a95fee0f1a617a2eae0e7d9d720cc622c73a54186"
|
||||
dependencies = [
|
||||
"bitflags 2.10.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rayon"
|
||||
version = "1.11.0"
|
||||
@@ -5865,6 +5946,12 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
||||
|
||||
[[package]]
|
||||
name = "to_method"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c7c4ceeeca15c8384bbc3e011dbd8fccb7f068a440b752b7d9b32ceb0ca0e2e8"
|
||||
|
||||
[[package]]
|
||||
name = "toml"
|
||||
version = "0.5.11"
|
||||
@@ -8066,13 +8153,34 @@ version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524"
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
|
||||
dependencies = [
|
||||
"byteorder",
|
||||
"zerocopy-derive 0.7.35",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ea879c944afe8a2b25fef16bb4ba234f47c694565e97383b36f3a878219065c"
|
||||
dependencies = [
|
||||
"zerocopy-derive",
|
||||
"zerocopy-derive 0.8.30",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy-derive"
|
||||
version = "0.7.35"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.111",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -19,6 +19,11 @@ freetype-rs = { version = "0.37.0", features = ["bundled"] }
|
||||
|
||||
image = "0.25.5"
|
||||
qoi = "0.4.1"
|
||||
|
||||
# AV1 video decoding - rav1d with Rust API from leo030303 fork
|
||||
# Disable default features to avoid NASM dependency (asm)
|
||||
rav1d = { git = "https://github.com/leo030303/rav1d", branch = "add-rust-api", default-features = false, features = ["bitdepth_8"] }
|
||||
|
||||
tracing = "0.1.41"
|
||||
glam = "0.29.2"
|
||||
#zmq = "0.10.0"
|
||||
|
||||
@@ -59,6 +59,7 @@ mod text;
|
||||
mod text2;
|
||||
mod ui;
|
||||
mod util;
|
||||
mod video;
|
||||
|
||||
use crate::{
|
||||
app::{App, AppPtr},
|
||||
|
||||
@@ -20,12 +20,16 @@ use sled_overlay::sled;
|
||||
use std::{array::TryFromSliceError, string::FromUtf8Error, sync::Arc};
|
||||
|
||||
pub mod darkirc;
|
||||
#[cfg(feature = "enable-plugins")]
|
||||
pub use darkirc::DarkIrc;
|
||||
pub use darkirc::DarkIrcPtr;
|
||||
|
||||
pub mod fud;
|
||||
pub use fud::{FudPlugin as Fud, FudPluginPtr as FudPtr};
|
||||
pub use fud::FudPluginPtr as FudPtr;
|
||||
|
||||
#[cfg(feature = "enable-plugins")]
|
||||
pub use {
|
||||
darkirc::DarkIrc,
|
||||
fud::FudPlugin
|
||||
};
|
||||
|
||||
use darkfi::net::Settings as NetSettings;
|
||||
|
||||
|
||||
272
bin/app/src/video/ivf.rs
Normal file
272
bin/app/src/video/ivf.rs
Normal file
@@ -0,0 +1,272 @@
|
||||
/* This file is part of DarkFi (https://dark.fi)
|
||||
*
|
||||
* Copyright (C) 2020-2025 Dyne.org foundation
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! IVF (Indeo Video File) container format demuxer for AV1 video.
|
||||
//!
|
||||
//! IVF is a simple container format with a 32-byte header followed by
|
||||
//! frame headers (12 bytes each) and frame data.
|
||||
|
||||
use darkfi_serial::Decodable;
|
||||
use std::io::{Cursor, Read};
|
||||
use thiserror::Error;
|
||||
|
||||
macro_rules! t { ($($arg:tt)*) => { trace!(target: "video::ivf", $($arg)*); } }
|
||||
macro_rules! d { ($($arg:tt)*) => { debug!(target: "video::ivf", $($arg)*); } }
|
||||
|
||||
/// Errors that can occur during IVF demuxing
|
||||
#[derive(Debug, Error)]
|
||||
pub enum IvfError {
|
||||
#[error("Invalid IVF signature: expected 'DKIF', got '{0:?}'")]
|
||||
InvalidSignature([u8; 4]),
|
||||
|
||||
#[error("Unsupported codec: expected 'AV01', got '{0:?}'")]
|
||||
UnsupportedCodec([u8; 4]),
|
||||
|
||||
#[error("Unexpected end of file")]
|
||||
UnexpectedEof,
|
||||
|
||||
#[error("Invalid frame size: {0}")]
|
||||
InvalidFrameSize(u32),
|
||||
}
|
||||
|
||||
impl From<std::io::Error> for IvfError {
|
||||
fn from(_: std::io::Error) -> Self {
|
||||
IvfError::UnexpectedEof
|
||||
}
|
||||
}
|
||||
|
||||
pub type IvfResult<T> = Result<T, IvfError>;
|
||||
|
||||
/// IVF file header (32 bytes)
|
||||
///
|
||||
/// ```
|
||||
/// DKIF - signature (4 bytes)
|
||||
/// version (u16) - version (2 bytes)
|
||||
/// header_len (u16) - header length (2 bytes)
|
||||
/// codec_fourcc - codec fourcc (4 bytes), e.g., "AV01" for AV1
|
||||
/// width (u16) - width (2 bytes)
|
||||
/// height (u16) - height (2 bytes)
|
||||
/// timebase_den (u32) - timebase denominator (4 bytes)
|
||||
/// timebase_num (u32) - timebase numerator (4 bytes)
|
||||
/// num_frames (u32) - number of frames (4 bytes)
|
||||
/// unused (u32) - unused (4 bytes)
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
struct IvfHeader {
|
||||
signature: [u8; 4],
|
||||
version: u16,
|
||||
header_len: u16,
|
||||
codec_fourcc: [u8; 4],
|
||||
pub width: u16,
|
||||
pub height: u16,
|
||||
timebase_den: u32,
|
||||
timebase_num: u32,
|
||||
num_frames: u32,
|
||||
unused: u32,
|
||||
}
|
||||
|
||||
/// IVF demuxer for AV1 video files
|
||||
pub struct IvfDemuxer {
|
||||
cur: Cursor<Vec<u8>>,
|
||||
pub header: IvfHeader,
|
||||
current_frame: u32,
|
||||
}
|
||||
|
||||
impl IvfDemuxer {
|
||||
/// Create a new IVF demuxer from raw bytes
|
||||
pub fn from_bytes(data: Vec<u8>) -> IvfResult<Self> {
|
||||
if data.len() < 32 {
|
||||
return Err(IvfError::UnexpectedEof);
|
||||
}
|
||||
|
||||
let mut self_ = Self {
|
||||
cur: Cursor::new(data),
|
||||
header: unsafe { std::mem::zeroed() },
|
||||
current_frame: 0,
|
||||
};
|
||||
|
||||
self_.parse_header()?;
|
||||
|
||||
// Validate signature (bytes 0-3 should be "DKIF")
|
||||
if &self_.header.signature != b"DKIF" {
|
||||
return Err(IvfError::InvalidSignature(self_.header.signature));
|
||||
}
|
||||
|
||||
// Validate codec (bytes 8-11 should be "AV01" for AV1)
|
||||
if &self_.header.codec_fourcc != b"AV01" {
|
||||
return Err(IvfError::UnsupportedCodec(self_.header.codec_fourcc));
|
||||
}
|
||||
|
||||
d!(
|
||||
"IVF header: {}x{} frames={}",
|
||||
self_.header.width,
|
||||
self_.header.height,
|
||||
self_.header.num_frames
|
||||
);
|
||||
|
||||
Ok(self_)
|
||||
}
|
||||
|
||||
/// Parse IVF header from bytes
|
||||
///
|
||||
/// # IVF Header Structure (32 bytes, little-endian)
|
||||
///
|
||||
/// | Offset | Size | Field | Value |
|
||||
/// |--------|------|-----------------|----------------------------|
|
||||
/// | 0 | 4 | signature | "DKIF" |
|
||||
/// | 4 | 2 | version | 0 |
|
||||
/// | 6 | 2 | header_len | 32 |
|
||||
/// | 8 | 4 | codec_fourcc | "AV01" for AV1 |
|
||||
/// | 12 | 2 | width | Frame width in pixels |
|
||||
/// | 14 | 2 | height | Frame height in pixels |
|
||||
/// | 16 | 4 | timebase_den | FPS denominator |
|
||||
/// | 20 | 4 | timebase_num | FPS numerator |
|
||||
/// | 24 | 4 | num_frames | Total frames |
|
||||
/// | 28 | 4 | unused | Reserved |
|
||||
fn parse_header(&mut self) -> Result<(), std::io::Error> {
|
||||
// Offset 0-3: Signature "DKIF" (raw bytes)
|
||||
let mut signature = [0u8; 4];
|
||||
self.cur.read_exact(&mut signature)?;
|
||||
|
||||
// Offset 4-5: Version (usually 0)
|
||||
let version = u16::decode(&mut self.cur)?;
|
||||
// Offset 6-7: Header length (usually 32)
|
||||
let header_len = u16::decode(&mut self.cur)?;
|
||||
|
||||
// Offset 8-11: Codec FourCC ("AV01" for AV1, "VP80" for VP8) (raw bytes)
|
||||
let mut codec_fourcc = [0u8; 4];
|
||||
self.cur.read_exact(&mut codec_fourcc)?;
|
||||
|
||||
// Offset 12-13: Frame width
|
||||
let width = u16::decode(&mut self.cur)?;
|
||||
// Offset 14-15: Frame height
|
||||
let height = u16::decode(&mut self.cur)?;
|
||||
|
||||
// Offset 16-19: Timebase denominator (FPS numerator)
|
||||
let timebase_den = u32::decode(&mut self.cur)?;
|
||||
// Offset 20-23: Timebase numerator (FPS denominator)
|
||||
let timebase_num = u32::decode(&mut self.cur)?;
|
||||
|
||||
// Offset 24-27: Total number of frames
|
||||
let num_frames = u32::decode(&mut self.cur)?;
|
||||
// Offset 28-31: Unused/reserved
|
||||
let unused = u32::decode(&mut self.cur)?;
|
||||
|
||||
self.header = IvfHeader {
|
||||
signature,
|
||||
version,
|
||||
header_len,
|
||||
codec_fourcc,
|
||||
width,
|
||||
height,
|
||||
timebase_den,
|
||||
timebase_num,
|
||||
num_frames,
|
||||
unused,
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get the next frame's AV1 bitstream data
|
||||
///
|
||||
/// # IVF Frame Header Structure (12 bytes, little-endian)
|
||||
///
|
||||
/// | Offset | Size | Field | Description |
|
||||
/// |--------|------|-------------|---------------------------------------|
|
||||
/// | 0 | 4 | frame_size | Size of frame data in bytes |
|
||||
/// | 4 | 8 | timestamp | Presentation timestamp |
|
||||
///
|
||||
/// The frame data immediately follows the 12-byte header.
|
||||
pub fn next_frame(&mut self) -> Result<Vec<u8>, std::io::Error> {
|
||||
// Offset 0-3: Frame size in bytes
|
||||
let frame_size = u32::decode(&mut self.cur)?;
|
||||
// Offset 4-11: Timestamp (8 bytes) - not used for linear playback
|
||||
let _timestamp = u64::decode(&mut self.cur)?;
|
||||
|
||||
let mut frame_data = vec![0u8; frame_size as usize];
|
||||
self.cur.read_exact(&mut frame_data)?;
|
||||
// Read the frame
|
||||
self.current_frame += 1;
|
||||
Ok(frame_data)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_header_parsing() {
|
||||
// Create a minimal valid IVF header
|
||||
let mut header_data = vec![0u8; 32];
|
||||
|
||||
// Signature
|
||||
header_data[0..4].copy_from_slice(b"DKIF");
|
||||
|
||||
// Version
|
||||
header_data[4..6].copy_from_slice(&0u16.to_le_bytes());
|
||||
|
||||
// Header length
|
||||
header_data[6..8].copy_from_slice(&32u16.to_le_bytes());
|
||||
|
||||
// Codec
|
||||
header_data[8..12].copy_from_slice(b"AV01");
|
||||
|
||||
// Dimensions
|
||||
header_data[12..14].copy_from_slice(&1920u16.to_le_bytes());
|
||||
header_data[14..16].copy_from_slice(&1080u16.to_le_bytes());
|
||||
|
||||
// Timebase: 25 FPS = 25/1
|
||||
header_data[16..20].copy_from_slice(&25u32.to_le_bytes()); // denominator
|
||||
header_data[20..24].copy_from_slice(&1u32.to_le_bytes()); // numerator
|
||||
|
||||
// Frame count
|
||||
header_data[24..28].copy_from_slice(&100u32.to_le_bytes());
|
||||
|
||||
let header = IvfDemuxer::parse_header(&header_data).unwrap();
|
||||
|
||||
assert_eq!(&header.signature, b"DKIF");
|
||||
assert_eq!(&header.codec_fourcc, b"AV01");
|
||||
assert_eq!(header.width, 1920);
|
||||
assert_eq!(header.height, 1080);
|
||||
assert_eq!(header.timebase_den, 25);
|
||||
assert_eq!(header.timebase_num, 1);
|
||||
assert_eq!(header.num_frames, 100);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_signature() {
|
||||
let mut header_data = vec![0u8; 32];
|
||||
header_data[0..4].copy_from_slice(b"BAD!");
|
||||
|
||||
let result = IvfDemuxer::from_bytes(header_data);
|
||||
assert!(matches!(result, Err(IvfError::InvalidSignature(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unsupported_codec() {
|
||||
let mut header_data = vec![0u8; 32];
|
||||
header_data[0..4].copy_from_slice(b"DKIF");
|
||||
// VP8 instead of AV1
|
||||
header_data[8..12].copy_from_slice(b"VP80");
|
||||
|
||||
let result = IvfDemuxer::from_bytes(header_data);
|
||||
assert!(matches!(result, Err(IvfError::UnsupportedCodec(_))));
|
||||
}
|
||||
}
|
||||
24
bin/app/src/video/mod.rs
Normal file
24
bin/app/src/video/mod.rs
Normal file
@@ -0,0 +1,24 @@
|
||||
/* This file is part of DarkFi (https://dark.fi)
|
||||
*
|
||||
* Copyright (C) 2020-2025 Dyne.org foundation
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
pub mod ivf;
|
||||
pub mod yuv_conv;
|
||||
|
||||
pub use ivf::{IvfDemuxer, IvfError, IvfResult};
|
||||
pub use yuv_conv::yuv420p_to_rgba;
|
||||
|
||||
186
bin/app/src/video/yuv_conv.rs
Normal file
186
bin/app/src/video/yuv_conv.rs
Normal file
@@ -0,0 +1,186 @@
|
||||
/* This file is part of DarkFi (https://dark.fi)
|
||||
*
|
||||
* Copyright (C) 2020-2025 Dyne.org foundation
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
//! YUV to RGBA color space conversion
|
||||
//!
|
||||
//! This module provides functions to convert YUV420P planar format
|
||||
//! to RGBA format for GPU rendering.
|
||||
|
||||
macro_rules! t { ($($arg:tt)*) => { trace!(target: "video::yuv_conv", $($arg)*); } }
|
||||
|
||||
/// Convert YUV420P planar format to RGBA
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `y_plane` - Y (luminance) plane data
|
||||
/// * `u_plane` - U (chrominance) plane data
|
||||
/// * `v_plane` - V (chrominance) plane data
|
||||
/// * `width` - Frame width in pixels
|
||||
/// * `height` - Frame height in pixels
|
||||
/// * `y_stride` - Y plane stride (bytes per row)
|
||||
/// * `u_stride` - U plane stride (bytes per row)
|
||||
/// * `v_stride` - V plane stride (bytes per row)
|
||||
///
|
||||
/// # Returns
|
||||
/// A Vec<u8> containing RGBA data (width * height * 4 bytes)
|
||||
///
|
||||
/// # Color Conversion
|
||||
/// Uses BT.601 standard for YUV to RGB conversion:
|
||||
/// ```text
|
||||
/// R = Y + 1.402 * (V - 128)
|
||||
/// G = Y - 0.344 * (U - 128) - 0.714 * (V - 128)
|
||||
/// B = Y + 1.772 * (U - 128)
|
||||
/// ```
|
||||
pub fn yuv420p_to_rgba(
|
||||
y_plane: &[u8],
|
||||
u_plane: &[u8],
|
||||
v_plane: &[u8],
|
||||
width: usize,
|
||||
height: usize,
|
||||
y_stride: usize,
|
||||
u_stride: usize,
|
||||
v_stride: usize,
|
||||
) -> Vec<u8> {
|
||||
//t!("yuv420p_to_rgba() {}x{} strides: y={} u={} v={}", width, height, y_stride, u_stride, v_stride);
|
||||
|
||||
let mut rgba = vec![0u8; width * height * 4];
|
||||
|
||||
for y in 0..height {
|
||||
for x in 0..width {
|
||||
let y_idx = y * y_stride + x;
|
||||
let u_idx = (y / 2) * u_stride + (x / 2);
|
||||
let v_idx = (y / 2) * v_stride + (x / 2);
|
||||
|
||||
let y_val = y_plane[y_idx] as i32;
|
||||
let u_val = u_plane[u_idx] as i32 - 128;
|
||||
let v_val = v_plane[v_idx] as i32 - 128;
|
||||
|
||||
// BT.601 YUV to RGB conversion
|
||||
let r = clamp((y_val as f32) + 1.402 * (v_val as f32));
|
||||
let g = clamp((y_val as f32) - 0.344136 * (u_val as f32) - 0.714136 * (v_val as f32));
|
||||
let b = clamp((y_val as f32) + 1.772 * (u_val as f32));
|
||||
|
||||
let out_idx = (y * width + x) * 4;
|
||||
rgba[out_idx] = r;
|
||||
rgba[out_idx + 1] = g;
|
||||
rgba[out_idx + 2] = b;
|
||||
// Alpha
|
||||
rgba[out_idx + 3] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
rgba
|
||||
}
|
||||
|
||||
/// Clamp a floating point value to 0-255 range and convert to u8
|
||||
#[inline]
|
||||
fn clamp(value: f32) -> u8 {
|
||||
value.round().clamp(0.0, 255.0) as u8
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_yuv420p_to_rgba_basic() {
|
||||
// Create a small test frame (4x4)
|
||||
let width = 4;
|
||||
let height = 4;
|
||||
|
||||
// Y plane - full resolution
|
||||
let y_plane: Vec<u8> = (0..16).map(|i| (i * 16) as u8).collect();
|
||||
|
||||
// U and V planes - half resolution (2x2)
|
||||
let u_plane: Vec<u8> = vec![128, 128, 128, 128];
|
||||
let v_plane: Vec<u8> = vec![128, 128, 128, 128];
|
||||
|
||||
let y_stride = width;
|
||||
let u_stride = width / 2;
|
||||
let v_stride = width / 2;
|
||||
|
||||
let rgba = yuv420p_to_rgba(
|
||||
&y_plane,
|
||||
&u_plane,
|
||||
&v_plane,
|
||||
width,
|
||||
height,
|
||||
y_stride,
|
||||
u_stride,
|
||||
v_stride,
|
||||
);
|
||||
|
||||
// Check output size
|
||||
assert_eq!(rgba.len(), width * height * 4);
|
||||
|
||||
// With U=V=128 (neutral chroma), RGB should equal Y
|
||||
for i in 0..16 {
|
||||
let expected_y = (i * 16) as u8;
|
||||
// R
|
||||
assert_eq!(rgba[i * 4], expected_y);
|
||||
// G
|
||||
assert_eq!(rgba[i * 4 + 1], expected_y);
|
||||
// B
|
||||
assert_eq!(rgba[i * 4 + 2], expected_y);
|
||||
// A
|
||||
assert_eq!(rgba[i * 4 + 3], 255);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clamp() {
|
||||
assert_eq!(clamp(-10.0), 0);
|
||||
assert_eq!(clamp(0.0), 0);
|
||||
assert_eq!(clamp(127.5), 128);
|
||||
assert_eq!(clamp(255.0), 255);
|
||||
assert_eq!(clamp(300.0), 255);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_yuv_to_rgb_conversion() {
|
||||
// Test specific color conversions
|
||||
// Black: Y=0, U=128, V=128
|
||||
let black = yuv_to_rgb(0, 128, 128);
|
||||
assert_eq!(black, [0, 0, 0]);
|
||||
|
||||
// White: Y=255, U=128, V=128
|
||||
let white = yuv_to_rgb(255, 128, 128);
|
||||
assert_eq!(white, [255, 255, 255]);
|
||||
|
||||
// Red: Y=76, U=85, V=255 (approximate pure red in BT.601)
|
||||
let red = yuv_to_rgb(76, 85, 255);
|
||||
// R should be high
|
||||
assert!(red[0] > 200);
|
||||
// G should be low
|
||||
assert!(red[1] < 50);
|
||||
// B should be low
|
||||
assert!(red[2] < 50);
|
||||
}
|
||||
|
||||
/// Helper function to convert a single YUV pixel to RGB
|
||||
fn yuv_to_rgb(y: u8, u: u8, v: u8) -> [u8; 3] {
|
||||
let y_val = y as i32;
|
||||
let u_val = u as i32 - 128;
|
||||
let v_val = v as i32 - 128;
|
||||
|
||||
let r = clamp((y_val as f32) + 1.402 * (v_val as f32));
|
||||
let g = clamp((y_val as f32) - 0.344136 * (u_val as f32) - 0.714136 * (v_val as f32));
|
||||
let b = clamp((y_val as f32) + 1.772 * (u_val as f32));
|
||||
|
||||
[r, g, b]
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user