// ============================================================================= // llimphi-registro :: la escena de una app, escrita a un archivo // ----------------------------------------------------------------------------- // Esto NO es un formato nuevo. `llimphi-wire-escena` ya define qué es un cuadro // y `llimphi-cable` ya lo enmarca para mandarlo por un socket; acá el mismo // cuadro va a un archivo, con el tiempo al lado. El reproductor del otro lado // tampoco es nuevo: es `llimphi-reproductor`, el mismo que usa el anfitrión // vivo y el kernel de wawa. // // Para qué: un registro es un MASTER. Se rasteriza después, offline, a la // resolución que haga falta, con el tema que haga falta, y de ahí sale el mp4. // Una captura de pantalla ya está revelada — cambiar el tema pide volver a // grabar con la persona delante. Un registro se vuelve a revelar solo. // // ## El formato, entero // // MAGIA (6) VERSION u16 LE // [u32 LE n][n bytes postcard de `Cabecera`] // ── y después, uno por cuadro: ── // [u32 LE n][u32 LE t_ms][n bytes postcard de `EscenaWire`] // // El `t_ms` va AFUERA del postcard y no adentro de un struct `Marco`, por una // razón que se paga en cada cuadro: `info` y el deduplicado necesitan comparar // y medir el cuerpo **sin deserializar la escena**, que es la parte cara. Con // el tiempo adentro, cada medición sería un parseo completo del cuadro. // // ## Por qué el cuadro repetido no se escribe // // Un escritorio pasa la mayor parte del tiempo quieto, y el huésped compone // cuando pasa algo — pero un `update` que no cambia nada visible igual manda su // cuadro. Escribirlo sería pagar una escena entera por un píxel idéntico. Se // compara el cuerpo serializado contra el anterior y, si es el mismo, se cuenta // y no se escribe: el que reproduce sostiene el último hasta el `t_ms` // siguiente, así que **no se pierde nada** — es la misma película con menos // bytes. Ésa es toda la compresión que hay acá, y es la que importa. // // Lo que este crate NO hace: no dibuja (eso es `llimphi-reproductor`), no habla // por ningún socket (eso es `llimphi-cable`) y no sabe qué es ffmpeg (eso es // `shared/foreign-av`). Enmarca, ordena por tiempo y cuenta. // ============================================================================= #![forbid(unsafe_code)] use std::io::{self, Read, Write}; use llimphi_wire_escena::EscenaWire; use serde::{Deserialize, Serialize}; /// Los seis bytes del frente. Sirven para lo de siempre: que abrir el archivo /// equivocado falle **en la primera línea** y no veinte cuadros después con un /// postcard que se deserializa a cualquier cosa. pub const MAGIA: [u8; 6] = *b"LLSCN\0"; /// Versión del contenedor. Sube cuando cambia la disposición de los bloques — /// no cuando cambia `EscenaWire`, que tiene su propia compatibilidad. pub const VERSION: u16 = 1; /// Tope de un cuadro, en bytes. Mismo criterio que `llimphi_cable::MAX_MARCO`: /// no es un límite de diseño sino la guarda contra un `u32` de basura que /// pediría alocar cuatro gigas antes de darse cuenta de que el archivo está /// roto. pub const MAX_CUADRO: usize = 64 * 1024 * 1024; /// Lo que hay que saber del registro antes de mirar un solo cuadro. /// /// El tamaño va acá y no en cada cuadro porque el huésped compone en su propio /// origen `(0,0)` y no sabe dónde lo pusieron: el rectángulo es del que grabó. #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct Cabecera { /// Ancho del tablado en píxeles lógicos. pub ancho: u32, /// Alto del tablado en píxeles lógicos. pub alto: u32, /// A qué cadencia se pensó revelar esto. Es una sugerencia: los cuadros /// llevan su `t_ms` y `film` remuestrea a los fps que le pidan. pub fps_sugerido: u32, /// Qué se grabó, para que el archivo se explique solo dentro de un año. pub app: String, /// Con qué tema se compuso. Anotado y no impuesto: el mismo registro se /// puede revelar con otro, y saber cuál era el original es el dato que /// después no está en ninguna parte. pub tema: String, } impl Default for Cabecera { fn default() -> Self { Self { ancho: 1280, alto: 720, fps_sugerido: 30, app: String::new(), tema: String::new(), } } } fn invalido(e: E) -> io::Error { io::Error::new(io::ErrorKind::InvalidData, e.to_string()) } // ───────────────────────────────────────────────────────────────────────────── // Grabar // ───────────────────────────────────────────────────────────────────────────── /// Escribe un registro. Genérico sobre el destino para que los tests no toquen /// el disco: un `Vec` es un destino igual de válido que un archivo. pub struct Grabador { destino: W, cuadros: u64, repetidos: u64, bytes: u64, /// El cuerpo del último cuadro ESCRITO, para el deduplicado. `None` hasta /// el primero: un `Vec` vacío como centinela haría que una escena que /// serializa vacía se tragara el primer cuadro sin decir nada. ultimo: Option>, ultimo_t: u32, } impl Grabador { /// Abre el registro: magia, versión y cabecera. pub fn nuevo(mut destino: W, cabecera: &Cabecera) -> io::Result { destino.write_all(&MAGIA)?; destino.write_all(&VERSION.to_le_bytes())?; let cuerpo = postcard::to_stdvec(cabecera).map_err(invalido)?; destino.write_all(&(cuerpo.len() as u32).to_le_bytes())?; destino.write_all(&cuerpo)?; Ok(Self { destino, cuadros: 0, repetidos: 0, bytes: 0, ultimo: None, ultimo_t: 0, }) } /// Anota un cuadro en `t_ms`. Devuelve `false` si era idéntico al anterior /// y no se escribió — que es lo normal y no un error. pub fn cuadro(&mut self, t_ms: u32, escena: &EscenaWire) -> io::Result { let cuerpo = postcard::to_stdvec(escena).map_err(invalido)?; if cuerpo.len() > MAX_CUADRO { return Err(invalido(format!( "cuadro de {} bytes, tope {MAX_CUADRO}", cuerpo.len() ))); } if self.ultimo.as_deref() == Some(cuerpo.as_slice()) { self.repetidos += 1; return Ok(false); } self.destino.write_all(&(cuerpo.len() as u32).to_le_bytes())?; self.destino.write_all(&t_ms.to_le_bytes())?; self.destino.write_all(&cuerpo)?; self.cuadros += 1; self.bytes += cuerpo.len() as u64; self.ultimo_t = t_ms; self.ultimo = Some(cuerpo); Ok(true) } /// Cuadros efectivamente escritos. pub fn cuadros(&self) -> u64 { self.cuadros } /// Cuadros que llegaron y no se escribieron por idénticos. Es el número que /// dice cuánto ahorró el deduplicado, y sin él «grabó poco» y «no cambió /// nada» se ven igual. pub fn repetidos(&self) -> u64 { self.repetidos } /// Bytes de escena escritos (sin contar cabecera ni marcos). pub fn bytes(&self) -> u64 { self.bytes } /// El `t_ms` del último cuadro escrito: la duración útil del registro. pub fn ultimo_t_ms(&self) -> u32 { self.ultimo_t } /// Cierra y devuelve el destino, para que el llamador lo sincronice. pub fn cerrar(mut self) -> io::Result { self.destino.flush()?; Ok(self.destino) } } // ───────────────────────────────────────────────────────────────────────────── // Leer // ───────────────────────────────────────────────────────────────────────────── /// Lee un registro cuadro por cuadro. pub struct Lector { origen: R, } impl Lector { /// Abre y valida el frente. Devuelve la cabecera y el lector parado en el /// primer cuadro. pub fn abrir(mut origen: R) -> io::Result<(Cabecera, Self)> { let mut magia = [0u8; 6]; origen.read_exact(&mut magia)?; if magia != MAGIA { return Err(invalido("esto no es un registro de escena (magia)")); } let mut v = [0u8; 2]; origen.read_exact(&mut v)?; let version = u16::from_le_bytes(v); if version != VERSION { return Err(invalido(format!( "registro versión {version}, este binario lee la {VERSION}" ))); } let mut n = [0u8; 4]; origen.read_exact(&mut n)?; let n = u32::from_le_bytes(n) as usize; if n > MAX_CUADRO { return Err(invalido("cabecera absurda")); } let mut cuerpo = vec![0u8; n]; origen.read_exact(&mut cuerpo)?; let cab: Cabecera = postcard::from_bytes(&cuerpo).map_err(invalido)?; Ok((cab, Self { origen })) } /// El próximo cuadro **sin deserializar**: `(t_ms, cuerpo)`. /// /// Es el que usa `info`, y por eso existe: medir un registro de mil cuadros /// no tiene por qué costar mil parseos de escena. pub fn crudo(&mut self) -> Option)>> { let mut n = [0u8; 4]; // El final del archivo es un final, no un error: acá se distingue «no // hay más» de «se cortó en el medio», que son cosas distintas y una de // las dos hay que reportarla. match self.origen.read_exact(&mut n) { Ok(()) => {} Err(e) if e.kind() == io::ErrorKind::UnexpectedEof => return None, Err(e) => return Some(Err(e)), } let n = u32::from_le_bytes(n) as usize; if n > MAX_CUADRO { return Some(Err(invalido(format!("cuadro de {n} bytes, tope {MAX_CUADRO}")))); } let mut t = [0u8; 4]; if let Err(e) = self.origen.read_exact(&mut t) { return Some(Err(e)); } let mut cuerpo = vec![0u8; n]; if let Err(e) = self.origen.read_exact(&mut cuerpo) { return Some(Err(e)); } Some(Ok((u32::from_le_bytes(t), cuerpo))) } /// El próximo cuadro, ya escena. pub fn siguiente(&mut self) -> Option> { match self.crudo()? { Ok((t, cuerpo)) => Some( postcard::from_bytes::(&cuerpo) .map(|e| (t, e)) .map_err(invalido), ), Err(e) => Some(Err(e)), } } } // ───────────────────────────────────────────────────────────────────────────── // Medir // ───────────────────────────────────────────────────────────────────────────── /// Lo que pesa un registro, para poder compararlo con un video sin creerle a /// nadie. Es la Regla 8 aplicada a esta pregunta: «¿sale más liviano que un /// mp4?» se contesta con bytes por segundo, no con una opinión. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] pub struct Resumen { pub cuadros: u64, pub bytes: u64, pub menor: usize, pub mayor: usize, pub dur_ms: u32, } impl Resumen { /// Bytes por cuadro, promedio. pub fn por_cuadro(&self) -> u64 { if self.cuadros == 0 { 0 } else { self.bytes / self.cuadros } } /// Bytes por segundo de película. **Éste** es el número que se carea contra /// el bitrate de un mp4; los bytes por cuadro no dicen nada si el /// escritorio estuvo quieto media hora. pub fn por_segundo(&self) -> u64 { if self.dur_ms == 0 { 0 } else { self.bytes * 1000 / self.dur_ms as u64 } } } /// Recorre el registro entero contando. No deserializa ninguna escena. pub fn resumir(lector: &mut Lector) -> io::Result { let mut r = Resumen { menor: usize::MAX, ..Default::default() }; while let Some(paso) = lector.crudo() { let (t, cuerpo) = paso?; r.cuadros += 1; r.bytes += cuerpo.len() as u64; r.menor = r.menor.min(cuerpo.len()); r.mayor = r.mayor.max(cuerpo.len()); r.dur_ms = r.dur_ms.max(t); } if r.cuadros == 0 { r.menor = 0; } Ok(r) } /// Los cuadros de un registro, en memoria, listos para remuestrear. /// /// `film` necesita ir y venir por la línea de tiempo —el cuadro que toca en /// `t = i/fps` es el último cuyo `t_ms <= t`— y eso no se puede hacer con un /// lector de una sola pasada. pub fn cargar(lector: &mut Lector) -> io::Result> { let mut v = Vec::new(); while let Some(paso) = lector.siguiente() { v.push(paso?); } Ok(v) } /// Qué cuadro se ve en `t_ms`: el último que empezó antes o justo ahí. /// /// Devuelve `None` sólo si el registro está vacío o si `t_ms` cae **antes** del /// primer cuadro, que es un caso real —un registro puede empezar en 40 ms— y no /// se resuelve mostrando el cuadro 0 como si hubiera estado ahí desde el /// principio. pub fn cuadro_en(cuadros: &[(u32, EscenaWire)], t_ms: u32) -> Option { let mut elegido = None; for (i, (t, _)) in cuadros.iter().enumerate() { if *t <= t_ms { elegido = Some(i); } else { break; } } elegido } #[cfg(test)] mod pruebas { use super::*; /// Una escena con `n` órdenes distinguibles. `pop_layer` alcanza: lo que /// estos tests miran es el TAMAÑO y la identidad del cuerpo serializado, no /// el dibujo — de dibujar ya hay tests en `llimphi-reproductor`. fn escena(n: usize) -> EscenaWire { let mut e = EscenaWire::new(); for _ in 0..n { e.pop_layer(); } e } #[test] fn ida_y_vuelta_con_cabecera() { let cab = Cabecera { ancho: 800, alto: 600, fps_sugerido: 24, app: "nada".into(), tema: "oscuro".into(), }; let mut g = Grabador::nuevo(Vec::new(), &cab).unwrap(); g.cuadro(0, &escena(1)).unwrap(); g.cuadro(33, &escena(2)).unwrap(); let bytes = g.cerrar().unwrap(); let (leida, mut l) = Lector::abrir(&bytes[..]).unwrap(); assert_eq!(leida, cab, "la cabecera vuelve igual"); let (t0, _) = l.siguiente().unwrap().unwrap(); let (t1, _) = l.siguiente().unwrap().unwrap(); assert_eq!((t0, t1), (0, 33)); assert!(l.siguiente().is_none(), "y se terminó, sin error"); } #[test] fn el_cuadro_repetido_no_se_escribe() { let mut g = Grabador::nuevo(Vec::new(), &Cabecera::default()).unwrap(); assert!(g.cuadro(0, &escena(3)).unwrap(), "el primero siempre va"); assert!(!g.cuadro(16, &escena(3)).unwrap(), "idéntico: no"); assert!(!g.cuadro(33, &escena(3)).unwrap(), "idéntico: no"); assert!(g.cuadro(50, &escena(4)).unwrap(), "cambió: sí"); assert_eq!((g.cuadros(), g.repetidos()), (2, 2)); } #[test] fn el_primer_cuadro_nunca_se_deduplica() { // Brazo de control del centinela: con un `Vec` vacío en vez de `None`, // una escena que serializara a cero bytes se perdería sin decir nada. let mut g = Grabador::nuevo(Vec::new(), &Cabecera::default()).unwrap(); assert!(g.cuadro(0, &EscenaWire::default()).unwrap()); assert_eq!(g.cuadros(), 1); } #[test] fn un_archivo_ajeno_falla_en_la_primera_linea() { let basura = b"no soy un registro de nada, de veras que no".to_vec(); let r = Lector::abrir(&basura[..]); assert!(r.is_err(), "la magia no casa"); } #[test] fn un_cuadro_cortado_es_un_error_y_no_un_final() { let mut g = Grabador::nuevo(Vec::new(), &Cabecera::default()).unwrap(); g.cuadro(0, &escena(2)).unwrap(); let bytes = g.cerrar().unwrap(); let cortado = &bytes[..bytes.len() - 3]; let (_, mut l) = Lector::abrir(cortado).unwrap(); let r = l.siguiente().expect("hay un cuadro empezado"); assert!(r.is_err(), "medio cuadro no es un cuadro"); } #[test] fn resumen_cuenta_sin_deserializar() { let mut g = Grabador::nuevo(Vec::new(), &Cabecera::default()).unwrap(); g.cuadro(0, &escena(1)).unwrap(); g.cuadro(500, &escena(9)).unwrap(); let bytes = g.cerrar().unwrap(); let (_, mut l) = Lector::abrir(&bytes[..]).unwrap(); let r = resumir(&mut l).unwrap(); assert_eq!(r.cuadros, 2); assert_eq!(r.dur_ms, 500); assert!(r.menor <= r.mayor && r.menor > 0); assert_eq!(r.por_segundo(), r.bytes * 2, "500 ms → el doble por segundo"); } #[test] fn el_cuadro_que_toca_es_el_ultimo_que_empezo() { let cuadros = vec![(0, escena(1)), (100, escena(2)), (300, escena(3))]; assert_eq!(cuadro_en(&cuadros, 0), Some(0)); assert_eq!(cuadro_en(&cuadros, 99), Some(0), "se sostiene el anterior"); assert_eq!(cuadro_en(&cuadros, 100), Some(1)); assert_eq!(cuadro_en(&cuadros, 5_000), Some(2), "y el último no se acaba"); assert_eq!(cuadro_en(&[], 0), None); } }