commit cc3b3f7a61760721bd7114ab184beafec1b15504 Author: Ondřej Slabý Date: Wed Nov 23 16:20:06 2022 +0100 Initial code commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..584e031 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,274 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "artvidnet-linky" +version = "0.1.0" +dependencies = [ + "ffmpeg-rs", + "getopts", +] + +[[package]] +name = "bindgen" +version = "0.59.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bd2a9a458e8f4304c52c43ebb0cfbd520289f8379a52e329a38afda99bf8eb8" +dependencies = [ + "bitflags", + "cexpr", + "clang-sys", + "lazy_static", + "lazycell", + "peeking_take_while", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cc" +version = "1.0.74" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581f5dba903aac52ea3feb5ec4810848460ee833876f1f9b0fdeab1f19091574" + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "clang-sys" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa2e27ae6ab525c3d369ded447057bca5438d86dc3a68f6faafb8269ba82ebf3" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "ffmpeg-rs" +version = "5.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0e17f67b535f0917a6790db6046293c1f15b7800702dd092bdd8271d023d586" +dependencies = [ + "bitflags", + "ffmpeg-sys-next", + "libc", +] + +[[package]] +name = "ffmpeg-sys-next" +version = "5.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d780b36e092254367e2f1f21191992735c8e23f31a5a5a8678db3a79f775021f" +dependencies = [ + "bindgen", + "cc", + "libc", + "num_cpus", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "getopts" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" +dependencies = [ + "unicode-width", +] + +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.137" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" + +[[package]] +name = "libloading" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f" +dependencies = [ + "cfg-if", + "winapi", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "nom" +version = "7.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num_cpus" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "peeking_take_while" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b17cddbe7ec3f8bc800887bab5e717348c95ea2ca0b1bf0837fb964dc67099" + +[[package]] +name = "pkg-config" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" + +[[package]] +name = "proc-macro2" +version = "1.0.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "shlex" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43b2853a4d09f215c24cc5489c992ce46052d359b5109343cbafbf26bc62f8a3" + +[[package]] +name = "unicode-ident" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3" + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..ce407b8 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "artvidnet-linky" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +ffmpeg-rs = "5.2.1" +getopts = "0.2.21" \ No newline at end of file diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..8896513 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,432 @@ +use std::{ + cmp, env, + net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket}, + path::Path, + thread::{self}, + time, fs::File, io::Write, +}; + +use ffmpeg_rs::{ + format::{input, Pixel}, + frame::{Video}, + media::Type, + software::scaling::Flags, + Error, +}; +use getopts::Options; + +extern crate getopts; + +struct LinkyFrame { + data: [u8; 4 * 204 * 5], +} + +impl LinkyFrame { + fn new() -> LinkyFrame { + LinkyFrame { + data: [0; 4 * 204 * 5], + } + } + + fn cols() -> usize { + 5 + } + + fn rows() -> usize { + 204 + } + + fn universes() -> u16 { + 5 * 2 + } + + fn set_pixel(&mut self, index: usize, rgb: (u8, u8, u8), brightness: f32) { + let r: f32 = f32::from(rgb.0) * brightness; + let g: f32 = f32::from(rgb.1) * brightness; + let b: f32 = f32::from(rgb.2) * brightness; + let rgb = unsafe { + ( + r.to_int_unchecked(), + g.to_int_unchecked(), + b.to_int_unchecked(), + ) + }; + + let white = cmp::min(cmp::min(rgb.0, rgb.1), rgb.2); + self.data[index * 4] = rgb.0 - white; + self.data[index * 4 + 1] = rgb.1 - white; + self.data[index * 4 + 2] = rgb.2 - white; + self.data[index * 4 + 3] = white; + } + + fn get_universe_data(&self, uni: u16) -> Vec { + if uni > LinkyFrame::universes() { + () + } + let mut data = Vec::new(); + + let start = usize::from(uni / 2) * 204 * 4; + let middle = usize::from(uni / 2) * 204 * 4 + 120 * 4; + let stop = usize::from(uni / 2 + 1) * 204 * 4; + + let (begin, end) = if uni < 8 { + if uni % 2 == 0 { + (start, middle) + } else { + (middle, stop) + } + } else { + if uni % 2 == 0 { + (middle, stop) + } else { + (start, middle) + } + }; + + for i in 0..((end - begin) / 48) { + // Elation RGBW led light structure, each 12 lights (a single strip) begins with this sequence. + data.extend_from_slice(&[0, 255, 0]); + // Then fill RGBW data. + data.extend_from_slice(&self.data[begin + i * 48..begin + (i + 1) * 48]); + } + + data + } +} + +struct ArtNetDMX { + data: Vec, + dmx_length: usize, +} + +impl ArtNetDMX { + fn new() -> ArtNetDMX { + let mut packet: ArtNetDMX = ArtNetDMX { + data: Vec::new(), + dmx_length: 0, + }; + packet.data.resize(18, 0); + + // ArtNet packet identifier + packet.data[0..8].copy_from_slice(b"Art-Net\0"); + + // OpCode 0x5000 (OpDmx) + packet.data[8] = 0x00; + packet.data[9] = 0x50; + + // ProtVer - always has to be 14, high byte first + packet.data[10] = 0x00; + packet.data[11] = 14; + + // Sequence - should be set after creation + packet.data[12] = 0; + + // Physical port - doesn't matter + packet.data[13] = 0; + + // Universe - should be set after creation + packet.data[14] = 0; + packet.data[15] = 0; + + // Data length - should be set after creation + packet.data[16] = 0; + packet.data[17] = 0; + + packet + } + + #[allow(dead_code)] + fn set_seq(&mut self, seq: u8) -> u8 { + let seq = if seq > 0 { seq } else { 1 }; + self.data[12] = seq; + if seq < 255 { + seq + 1 + } else { + 1 + } + } + + #[allow(dead_code)] + fn unset_seq(&mut self) { + self.data[12] = 0; + } + + fn set_universe(&mut self, uni: u16) { + let uni = uni & 0x7fff; + self.data[14..16].copy_from_slice(&uni.to_le_bytes()); + } + + fn set_length(&mut self, length: u16) { + let length = cmp::min(512, length); + self.dmx_length = usize::from(length); + self.data.resize(18 + self.dmx_length, 0); + self.data[16..18].copy_from_slice(&length.to_be_bytes()); + } + + #[allow(dead_code)] + fn set_values(&mut self, values: &[u8]) { + self.set_length(u16::try_from(values.len()).unwrap()); + self.data[18..(18 + self.dmx_length)].copy_from_slice(values); + } + + #[allow(dead_code)] + fn set_value_full(&mut self, values: &[u8]) { + self.set_length(512); + self.data[18..18 + values.len()].copy_from_slice(values); + self.data[18 + values.len()..530].fill(0); + } +} + +fn process_frames>( + file_path: P, + mut frame_callback: F, + play_realtime: bool, +) -> Result<(), Error> { + if let Ok(mut ictx) = input(&file_path) { + let video_stream = ictx + .streams() + .best(Type::Video) + .ok_or(ffmpeg_rs::Error::StreamNotFound)?; + let video_stream_index = video_stream.index(); + let video_stream_timebase = video_stream.time_base(); + + let context_decoder = + ffmpeg_rs::codec::context::Context::from_parameters(video_stream.parameters())?; + let mut decoder = context_decoder.decoder().video()?; + + let mut scaler = ffmpeg_rs::software::scaling::Context::get( + decoder.format(), + decoder.width(), + decoder.height(), + Pixel::RGB24, + decoder.width(), + decoder.height(), + Flags::BILINEAR, + )?; + + let mut frame_index = 0; + let mut last_pts = 0; + let mut process_and_receive_frames = + |decoder: &mut ffmpeg_rs::decoder::Video| -> Result<(), Error> { + let mut decoded = Video::empty(); + let mut rgb_frame = Video::empty(); + while decoder.receive_frame(&mut decoded).is_ok() { + scaler.run(&decoded, &mut rgb_frame)?; + frame_callback(&rgb_frame, frame_index); + + let frametime = match decoded.pts() { + Some(pts) => { + let res = ((pts - last_pts) as f64) * f64::from(video_stream_timebase); + last_pts = pts; + res + } + None => 1.0 / 24.0, + }; + if play_realtime { + thread::sleep(time::Duration::from_secs_f64(frametime)); + } + frame_index += 1; + } + + Ok(()) + }; + + for (stream, packet) in ictx.packets() { + if stream.index() == video_stream_index { + decoder.send_packet(&packet)?; + process_and_receive_frames(&mut decoder)?; + } + } + decoder.send_eof()?; + process_and_receive_frames(&mut decoder)?; + } + + Ok(()) +} + +fn blurred_pixel(data: &[(u8, u8, u8)], stride: usize, pix_index: usize, blur_radius: u32) -> (u8, u8, u8) { + let blur_size: usize = usize::try_from(blur_radius).unwrap(); + let box_start: usize = pix_index - blur_size * stride - blur_size; + let mut r: u32 = 0; + let mut g: u32 = 0; + let mut b: u32 = 0; + + for i in 0..blur_size * 2 + 1 { + for j in 0..blur_size * 2 + 1 { + let pixel: usize = box_start + i * stride + j; + r += u32::from(data[pixel].0); + g += u32::from(data[pixel].1); + b += u32::from(data[pixel].2); + } + } + + // Include the center pixel/line + let blur_radius = blur_radius + 1; + r /= blur_radius * blur_radius; + g /= blur_radius * blur_radius; + b /= blur_radius * blur_radius; + + ( + u8::try_from(r).unwrap_or(255), + u8::try_from(g).unwrap_or(255), + u8::try_from(b).unwrap_or(255), + ) +} + +fn gen_linky_frame(frame: &Video, index: u64, blur_size: u32, brightness: f32) -> LinkyFrame { + let mut linky_frame = LinkyFrame::new(); + println!( + "Processing frame {}: {} x {}", + index, + frame.width(), + frame.height() + ); + + let usable_width = frame.width() - 2 * blur_size; + let usable_height = frame.height() - 2 * blur_size; + + let base_index = usize::try_from(blur_size * frame.width() + blur_size).unwrap(); + let row_step = usize::try_from(usable_height - 1).unwrap() / (LinkyFrame::rows() - 1); + let col_step = usize::try_from(usable_width - 1).unwrap() / (LinkyFrame::cols() - 1); + let pix_stride = frame.stride(0) / 3; + + let video_plane: &[(u8, u8, u8)] = frame.plane(0); + + for i in 0..LinkyFrame::cols() { + for j in 0..LinkyFrame::rows() { + let pix_index = base_index + + row_step * j * pix_stride + + col_step * i; + let rgb = if blur_size > 0 { + blurred_pixel( + video_plane, + pix_stride, + pix_index, + blur_size, + ) + } else { + video_plane[pix_index] + }; + + linky_frame.set_pixel(i * LinkyFrame::rows() + j, rgb, brightness); + } + } + linky_frame +} + +fn main() { + let mut options = Options::new(); + options.optopt("f", "file", "Video file to parse", "FILE"); + options.optopt( + "s", + "send", + "Send the resulting ArtNet packets to this address", + "ADDRESS:PORT", + ); + options.optopt( + "w", + "write", + "Write the ArtNet packets as a single byte stream to this file", + "FILE", + ); + options.optopt( + "r", + "radius", + "Blurring radius to use when processing frames - 0 means no blur", + "NUMBER", + ); + options.optopt( + "b", + "brightness", + "Normalized float scaling the brightness of each pixel (0.0-1.0 clamped)", + "FLOAT", + ); + + let args: Vec = env::args().collect(); + let program = args[0].clone(); + + let matches = match options.parse(args) { + Ok(m) => m, + Err(_) => { + print_usage(program, options); + return; + } + }; + + if !matches.opt_present("f") { + print_usage(program, options); + return; + } + if !matches.opt_present("s") && !matches.opt_present("w") { + return; + } + + let blur_size = matches.opt_get::("r").unwrap_or_default().unwrap_or(0); + let mut brightness = matches + .opt_get::("b") + .unwrap_or_default() + .unwrap_or(1.0); + brightness = brightness.clamp(0.0, 1.0); + + if matches.opt_present("s") { + let remote = matches.opt_str("s").unwrap(); + + let socket = match UdpSocket::bind(SocketAddr::new(IpAddr::V4(Ipv4Addr::UNSPECIFIED), 0)) { + Ok(sock) => sock, + Err(e) => panic!("{}", e), + }; + + // let mut seq = 0; + let frame_callback = |frame: &Video, index: u64| { + let linky_frame = gen_linky_frame(frame, index, blur_size, brightness); + for u in 0..LinkyFrame::universes() { + let mut packet = ArtNetDMX::new(); + packet.set_universe(u); + packet.unset_seq(); + // seq = packet.set_seq(seq); + // println!("Sequence: {}", seq); + + let linky_data = linky_frame.get_universe_data(u); + // println!("Universe {} size: {}", u, linky_data.len()); + // packet.set_values(&linky_data); + packet.set_value_full(&linky_data); + + // for i in 0..packet.data.len() { + // print!("{:02X} ", packet.data[i]); + // if i % 24 == 23 { + // print!("\n"); + // } + // } + // print!("\n"); + + socket.send_to(&packet.data, remote.clone()).unwrap(); + } + }; + + process_frames(&matches.opt_str("f").unwrap(), frame_callback, true).unwrap(); + } else if matches.opt_present("w") { + let mut file = File::create(matches.opt_str("w").unwrap()).unwrap(); + let frame_callback = |frame: &Video, index: u64| { + let linky_frame = gen_linky_frame(frame, index, blur_size, brightness); + for u in 0..LinkyFrame::universes() { + let mut packet = ArtNetDMX::new(); + packet.set_universe(u); + packet.unset_seq(); + + let linky_data = linky_frame.get_universe_data(u); + packet.set_value_full(&linky_data); + + file.write(&packet.data).unwrap(); + } + }; + + process_frames(&matches.opt_str("f").unwrap(), frame_callback, false).unwrap(); + } else { + print_usage(program, options); + } +} + +fn print_usage(program: String, options: Options) { + let brief = format!("Usage: {} -f FILE [options]", program); + print!("{}", options.usage(&brief)); +}