diff --git a/bin/app/Cargo.lock b/bin/app/Cargo.lock index e23d6c5f1..a54dd517e 100644 --- a/bin/app/Cargo.lock +++ b/bin/app/Cargo.lock @@ -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]] diff --git a/bin/app/Cargo.toml b/bin/app/Cargo.toml index fbda76248..2aa33c545 100644 --- a/bin/app/Cargo.toml +++ b/bin/app/Cargo.toml @@ -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" diff --git a/bin/app/src/main.rs b/bin/app/src/main.rs index c0630e8c3..bd47ba448 100644 --- a/bin/app/src/main.rs +++ b/bin/app/src/main.rs @@ -59,6 +59,7 @@ mod text; mod text2; mod ui; mod util; +mod video; use crate::{ app::{App, AppPtr}, diff --git a/bin/app/src/plugin/mod.rs b/bin/app/src/plugin/mod.rs index 822d3d84f..638f39d0c 100644 --- a/bin/app/src/plugin/mod.rs +++ b/bin/app/src/plugin/mod.rs @@ -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; diff --git a/bin/app/src/video/ivf.rs b/bin/app/src/video/ivf.rs new file mode 100644 index 000000000..90900b046 --- /dev/null +++ b/bin/app/src/video/ivf.rs @@ -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 . + */ + +//! 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 for IvfError { + fn from(_: std::io::Error) -> Self { + IvfError::UnexpectedEof + } +} + +pub type IvfResult = Result; + +/// 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>, + pub header: IvfHeader, + current_frame: u32, +} + +impl IvfDemuxer { + /// Create a new IVF demuxer from raw bytes + pub fn from_bytes(data: Vec) -> IvfResult { + 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, 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(_)))); + } +} diff --git a/bin/app/src/video/mod.rs b/bin/app/src/video/mod.rs new file mode 100644 index 000000000..12eea7844 --- /dev/null +++ b/bin/app/src/video/mod.rs @@ -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 . + */ + +pub mod ivf; +pub mod yuv_conv; + +pub use ivf::{IvfDemuxer, IvfError, IvfResult}; +pub use yuv_conv::yuv420p_to_rgba; + diff --git a/bin/app/src/video/yuv_conv.rs b/bin/app/src/video/yuv_conv.rs new file mode 100644 index 000000000..be4f1c234 --- /dev/null +++ b/bin/app/src/video/yuv_conv.rs @@ -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 . + */ + +//! 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 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 { + //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 = (0..16).map(|i| (i * 16) as u8).collect(); + + // U and V planes - half resolution (2x2) + let u_plane: Vec = vec![128, 128, 128, 128]; + let v_plane: Vec = 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] + } +}