Initial code commit

This commit is contained in:
2022-11-23 16:20:06 +01:00
commit cc3b3f7a61
4 changed files with 717 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/target

274
Cargo.lock generated Normal file
View File

@@ -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"

10
Cargo.toml Normal file
View File

@@ -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"

432
src/main.rs Normal file
View File

@@ -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<u8> {
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<u8>,
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<F: FnMut(&Video, u64), P: AsRef<Path>>(
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<String> = 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::<u32>("r").unwrap_or_default().unwrap_or(0);
let mut brightness = matches
.opt_get::<f32>("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));
}