diff --git a/Cargo.lock b/Cargo.lock index b82ea19..6cc7d6f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7346,6 +7346,13 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "mirada-layout" +version = "0.1.0" +dependencies = [ + "serde", +] + [[package]] name = "moka" version = "0.12.15" diff --git a/Cargo.toml b/Cargo.toml index c654f25..e9316cf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -153,6 +153,11 @@ members = [ # ============================================================ "crates/modules/charka/charka-bcd", + # ============================================================ + # modules/mirada/ — Compositor Wayland + # ============================================================ + "crates/modules/mirada/mirada-layout", + # ============================================================ # modules/nakui/ — ERP matemático (categórico) # ============================================================ diff --git a/crates/modules/mirada/SDD.md b/crates/modules/mirada/SDD.md new file mode 100644 index 0000000..8d02352 --- /dev/null +++ b/crates/modules/mirada/SDD.md @@ -0,0 +1,40 @@ +# modules/mirada/ — Compositor Wayland + +**Propósito.** Un compositor Wayland teselante con aislamiento de +clientes (sobre `arje-incarnate`) y delegación de regiones. El cerebro +espacial —cómo se reparte la pantalla— se mantiene puro y aislado de +Wayland para poder probarlo sin un servidor gráfico. + +## Crates + +| crate | tipo | rol | +| --------------- | ---- | ------------------------------------------------------------ | +| `mirada-layout` | lib | Motor de teselado: `Rect`, modos de layout, `Workspace` (ventanas, foco) | + +## mirada-layout + +- `Rect` + `split` — reparto exacto de píxeles (sin pérdidas). +- `LayoutMode` — `MasterStack`, `Monocle`, `Grid`, `Columns`; `tile` + calcula el rectángulo de cada ventana. +- `Workspace` — ventanas en orden de teselado, foco cíclico, reordenado + (`move_focused_forward/backward`) y `layout` que resuelve la geometría. +- Determinista: misma pantalla + mismas ventanas → misma distribución. + +## Dependencias + +- `mirada-layout` ← sólo `serde`. `#![forbid(unsafe_code)]`. +- Cero Wayland, cero `smithay` — ese acoplamiento vive en los crates de + integración pendientes. + +## Estado + +`mirada-layout` implementado y verde (22 tests). **Pendiente** (la capa +que toca hardware/protocolo, no verificable en modo desatendido): + +| crate pendiente | rol | +| ------------------ | ---------------------------------------------------- | +| `mirada-compositor`| integración `smithay`: superficies, buffers, salidas | +| `mirada-input` | teclado/ratón, atajos, asignación de foco | +| `mirada-sandbox` | aislamiento de clientes sobre `arje-incarnate` | + +CRIU (congelar/restaurar ventanas) queda anotado como futuro. diff --git a/crates/modules/mirada/mirada-layout/Cargo.toml b/crates/modules/mirada/mirada-layout/Cargo.toml new file mode 100644 index 0000000..e8df6d8 --- /dev/null +++ b/crates/modules/mirada/mirada-layout/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "mirada-layout" +version.workspace = true +edition.workspace = true +license.workspace = true +authors.workspace = true +publish.workspace = true +description = "mirada — motor de teselado del compositor Wayland: reparte la pantalla entre ventanas según el modo de layout. Agnóstico de Wayland y de smithay." + +[dependencies] +serde = { workspace = true } diff --git a/crates/modules/mirada/mirada-layout/src/geometry.rs b/crates/modules/mirada/mirada-layout/src/geometry.rs new file mode 100644 index 0000000..aa3a306 --- /dev/null +++ b/crates/modules/mirada/mirada-layout/src/geometry.rs @@ -0,0 +1,103 @@ +//! Geometría — el rectángulo en coordenadas de pantalla. + +use serde::{Deserialize, Serialize}; + +/// Un rectángulo en píxeles de pantalla. El origen `(0,0)` es la +/// esquina superior-izquierda; `x` crece a la derecha, `y` hacia abajo. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +pub struct Rect { + pub x: i32, + pub y: i32, + pub w: i32, + pub h: i32, +} + +impl Rect { + pub fn new(x: i32, y: i32, w: i32, h: i32) -> Self { + Self { x, y, w, h } + } + + /// Área en píxeles cuadrados. + pub fn area(&self) -> i64 { + self.w.max(0) as i64 * self.h.max(0) as i64 + } + + /// `true` si el rectángulo tiene ancho y alto positivos. + pub fn is_visible(&self) -> bool { + self.w > 0 && self.h > 0 + } + + /// Encoge el rectángulo `g` píxeles por cada lado. Si el margen se + /// come toda la dimensión, ésta queda en `0` (no negativa). + pub fn inset(&self, g: i32) -> Rect { + Rect { + x: self.x + g, + y: self.y + g, + w: (self.w - 2 * g).max(0), + h: (self.h - 2 * g).max(0), + } + } + + /// `true` si `(px, py)` cae dentro del rectángulo. + pub fn contains(&self, px: i32, py: i32) -> bool { + px >= self.x && px < self.x + self.w && py >= self.y && py < self.y + self.h + } +} + +/// Reparte `total` píxeles en `n` tramos contiguos sin perder ni un +/// píxel: las fronteras caen en `total · k / n`, así que la suma de los +/// tamaños es exactamente `total`. Devuelve `(offset, tamaño)` por tramo. +pub fn split(total: i32, n: usize) -> Vec<(i32, i32)> { + if n == 0 { + return Vec::new(); + } + let total = total.max(0) as i64; + let n64 = n as i64; + (0..n) + .map(|k| { + let start = total * k as i64 / n64; + let end = total * (k as i64 + 1) / n64; + (start as i32, (end - start) as i32) + }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn inset_shrinks_by_gap_on_every_side() { + let r = Rect::new(0, 0, 100, 80).inset(5); + assert_eq!(r, Rect::new(5, 5, 90, 70)); + } + + #[test] + fn inset_clamps_to_zero() { + let r = Rect::new(0, 0, 8, 8).inset(10); + assert_eq!((r.w, r.h), (0, 0)); + assert!(!r.is_visible()); + } + + #[test] + fn split_loses_no_pixels() { + for n in 1..=13 { + let parts = split(1000, n); + assert_eq!(parts.len(), n); + assert_eq!(parts.iter().map(|(_, s)| *s).sum::(), 1000); + // Los tramos son contiguos. + for w in parts.windows(2) { + assert_eq!(w[0].0 + w[0].1, w[1].0); + } + } + } + + #[test] + fn contains_checks_bounds() { + let r = Rect::new(10, 10, 20, 20); + assert!(r.contains(10, 10)); + assert!(r.contains(29, 29)); + assert!(!r.contains(30, 30)); + assert!(!r.contains(9, 15)); + } +} diff --git a/crates/modules/mirada/mirada-layout/src/layout.rs b/crates/modules/mirada/mirada-layout/src/layout.rs new file mode 100644 index 0000000..a3b6eb7 --- /dev/null +++ b/crates/modules/mirada/mirada-layout/src/layout.rs @@ -0,0 +1,179 @@ +//! Modos de teselado — cómo se reparte la pantalla entre ventanas. + +use serde::{Deserialize, Serialize}; + +use crate::geometry::{split, Rect}; + +/// Estrategia de teselado. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "kebab-case")] +pub enum LayoutMode { + /// Una ventana maestra a la izquierda; el resto apiladas a la derecha. + MasterStack, + /// Todas a pantalla completa, superpuestas — sólo se ve la enfocada. + Monocle, + /// Rejilla uniforme. + Grid, + /// Columnas verticales de igual ancho. + Columns, +} + +/// Parámetros del teselado. +#[derive(Debug, Clone, Copy, Serialize, Deserialize)] +pub struct LayoutParams { + pub mode: LayoutMode, + /// Fracción del ancho para la ventana maestra en `MasterStack` + /// (se acota a `0.05..=0.95`). + pub master_ratio: f32, + /// Margen en píxeles alrededor de cada ventana. + pub gap: i32, +} + +impl Default for LayoutParams { + fn default() -> Self { + Self { mode: LayoutMode::MasterStack, master_ratio: 0.6, gap: 8 } + } +} + +/// Calcula el rectángulo de cada una de las `count` ventanas dentro de +/// `screen`. El vector resultante tiene exactamente `count` elementos, +/// en el mismo orden que las ventanas. +pub fn tile(screen: Rect, count: usize, params: &LayoutParams) -> Vec { + if count == 0 { + return Vec::new(); + } + let cells = match params.mode { + LayoutMode::Monocle => vec![screen; count], + LayoutMode::Columns => columns(screen, count), + LayoutMode::Grid => grid(screen, count), + LayoutMode::MasterStack => master_stack(screen, count, params.master_ratio), + }; + // El margen se aplica al final, uniforme para todos los modos. + cells.into_iter().map(|c| c.inset(params.gap)).collect() +} + +/// Columnas verticales de igual ancho. +fn columns(screen: Rect, count: usize) -> Vec { + split(screen.w, count) + .into_iter() + .map(|(off, w)| Rect::new(screen.x + off, screen.y, w, screen.h)) + .collect() +} + +/// Rejilla `cols × rows` lo más cuadrada posible. +fn grid(screen: Rect, count: usize) -> Vec { + let cols = (count as f64).sqrt().ceil() as usize; + let rows = count.div_ceil(cols); + let col_parts = split(screen.w, cols); + let row_parts = split(screen.h, rows); + (0..count) + .map(|i| { + let (cx, cw) = col_parts[i % cols]; + let (ry, rh) = row_parts[i / cols]; + Rect::new(screen.x + cx, screen.y + ry, cw, rh) + }) + .collect() +} + +/// Ventana maestra a la izquierda + pila a la derecha. +fn master_stack(screen: Rect, count: usize, ratio: f32) -> Vec { + if count == 1 { + return vec![screen]; + } + let ratio = ratio.clamp(0.05, 0.95); + let master_w = (screen.w as f32 * ratio).round() as i32; + let master = Rect::new(screen.x, screen.y, master_w, screen.h); + + let stack_x = screen.x + master_w; + let stack_w = screen.w - master_w; + let mut out = vec![master]; + for (off, h) in split(screen.h, count - 1) { + out.push(Rect::new(stack_x, screen.y + off, stack_w, h)); + } + out +} + +#[cfg(test)] +mod tests { + use super::*; + + const SCREEN: Rect = Rect { x: 0, y: 0, w: 1920, h: 1080 }; + + fn params(mode: LayoutMode) -> LayoutParams { + LayoutParams { mode, master_ratio: 0.6, gap: 0 } + } + + #[test] + fn empty_count_yields_no_rects() { + assert!(tile(SCREEN, 0, ¶ms(LayoutMode::Grid)).is_empty()); + } + + #[test] + fn tile_count_matches_window_count() { + for mode in [ + LayoutMode::MasterStack, + LayoutMode::Monocle, + LayoutMode::Grid, + LayoutMode::Columns, + ] { + for n in 1..=9 { + assert_eq!(tile(SCREEN, n, ¶ms(mode)).len(), n); + } + } + } + + #[test] + fn monocle_gives_every_window_the_full_screen() { + for r in tile(SCREEN, 4, ¶ms(LayoutMode::Monocle)) { + assert_eq!(r, SCREEN); + } + } + + #[test] + fn columns_partition_the_width_exactly() { + let rects = tile(SCREEN, 3, ¶ms(LayoutMode::Columns)); + assert_eq!(rects.iter().map(|r| r.w).sum::(), 1920); + // Todas ocupan el alto completo. + assert!(rects.iter().all(|r| r.h == 1080)); + } + + #[test] + fn master_stack_master_takes_its_ratio() { + let rects = tile(SCREEN, 3, ¶ms(LayoutMode::MasterStack)); + // 60% de 1920 = 1152. + assert_eq!(rects[0].w, 1152); + // Las dos de la pila comparten el resto del ancho y el alto. + assert_eq!(rects[1].w, 1920 - 1152); + assert_eq!(rects[1].h + rects[2].h, 1080); + } + + #[test] + fn master_stack_single_window_fills_screen() { + let rects = tile(SCREEN, 1, ¶ms(LayoutMode::MasterStack)); + assert_eq!(rects[0], SCREEN); + } + + #[test] + fn grid_tiles_cover_the_screen_without_overlap() { + // 4 ventanas → rejilla 2×2, cada una un cuarto. + let rects = tile(SCREEN, 4, ¶ms(LayoutMode::Grid)); + let total: i64 = rects.iter().map(|r| r.area()).sum(); + assert_eq!(total, SCREEN.area()); + } + + #[test] + fn gap_shrinks_every_window() { + let p = LayoutParams { mode: LayoutMode::Columns, master_ratio: 0.6, gap: 10 }; + for r in tile(SCREEN, 2, &p) { + // Cada celda de 960 de ancho se encoge 20 (10 por lado). + assert_eq!(r.w, 960 - 20); + assert_eq!(r.h, 1080 - 20); + } + } + + #[test] + fn layout_is_deterministic() { + let p = params(LayoutMode::Grid); + assert_eq!(tile(SCREEN, 7, &p), tile(SCREEN, 7, &p)); + } +} diff --git a/crates/modules/mirada/mirada-layout/src/lib.rs b/crates/modules/mirada/mirada-layout/src/lib.rs new file mode 100644 index 0000000..7f2936f --- /dev/null +++ b/crates/modules/mirada/mirada-layout/src/lib.rs @@ -0,0 +1,23 @@ +//! `mirada-layout` — el motor de teselado del compositor Wayland. +//! +//! mirada es un compositor Wayland; este crate es su cerebro espacial, +//! aislado de Wayland y de `smithay`. Decide *dónde* va cada ventana — +//! un cálculo puro sobre rectángulos— para que el compositor sólo tenga +//! que aplicar la geometría a las superficies reales. +//! +//! - [`geometry`] — el [`Rect`] y el reparto exacto de píxeles. +//! - [`layout`] — los modos de teselado y la función [`tile`]. +//! - [`workspace`] — el [`Workspace`]: ventanas, foco y modo. +//! +//! Todo es determinista y testeable sin un servidor gráfico: la misma +//! pantalla y las mismas ventanas dan siempre la misma distribución. + +#![forbid(unsafe_code)] + +pub mod geometry; +pub mod layout; +pub mod workspace; + +pub use geometry::Rect; +pub use layout::{tile, LayoutMode, LayoutParams}; +pub use workspace::{Workspace, WindowId}; diff --git a/crates/modules/mirada/mirada-layout/src/workspace.rs b/crates/modules/mirada/mirada-layout/src/workspace.rs new file mode 100644 index 0000000..cedfe79 --- /dev/null +++ b/crates/modules/mirada/mirada-layout/src/workspace.rs @@ -0,0 +1,240 @@ +//! `Workspace` — un conjunto de ventanas, su foco y su modo de teselado. + +use serde::{Deserialize, Serialize}; + +use crate::geometry::Rect; +use crate::layout::{tile, LayoutMode, LayoutParams}; + +/// Identificador de una ventana (una superficie Wayland). +pub type WindowId = u64; + +/// Un escritorio: ventanas en orden de teselado + la enfocada + el modo. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Workspace { + /// Ventanas en orden de teselado (la 0 es la maestra en `MasterStack`). + windows: Vec, + /// Índice de la ventana enfocada en `windows`. + focus: usize, + params: LayoutParams, +} + +impl Workspace { + /// Escritorio vacío con los parámetros dados. + pub fn new(params: LayoutParams) -> Self { + Self { windows: Vec::new(), focus: 0, params } + } + + pub fn len(&self) -> usize { + self.windows.len() + } + + pub fn is_empty(&self) -> bool { + self.windows.is_empty() + } + + /// Ventanas en orden de teselado. + pub fn windows(&self) -> &[WindowId] { + &self.windows + } + + pub fn params(&self) -> &LayoutParams { + &self.params + } + + /// Cambia el modo de teselado. + pub fn set_mode(&mut self, mode: LayoutMode) { + self.params.mode = mode; + } + + /// Ajusta la fracción de la ventana maestra. + pub fn set_master_ratio(&mut self, ratio: f32) { + self.params.master_ratio = ratio; + } + + /// Añade una ventana y la enfoca. Si ya estaba, sólo la enfoca. + pub fn add(&mut self, window: WindowId) { + if let Some(i) = self.windows.iter().position(|&w| w == window) { + self.focus = i; + } else { + self.windows.push(window); + self.focus = self.windows.len() - 1; + } + } + + /// Quita una ventana. `false` si no estaba. El foco se reajusta para + /// seguir apuntando a una ventana válida. + pub fn remove(&mut self, window: WindowId) -> bool { + let Some(i) = self.windows.iter().position(|&w| w == window) else { + return false; + }; + self.windows.remove(i); + if i < self.focus { + self.focus -= 1; + } + if self.focus >= self.windows.len() { + self.focus = self.windows.len().saturating_sub(1); + } + true + } + + /// Ventana enfocada, o `None` si el escritorio está vacío. + pub fn focused(&self) -> Option { + self.windows.get(self.focus).copied() + } + + /// Mueve el foco a la ventana siguiente (cíclico). + pub fn focus_next(&mut self) { + if !self.windows.is_empty() { + self.focus = (self.focus + 1) % self.windows.len(); + } + } + + /// Mueve el foco a la ventana anterior (cíclico). + pub fn focus_prev(&mut self) { + if !self.windows.is_empty() { + self.focus = (self.focus + self.windows.len() - 1) % self.windows.len(); + } + } + + /// Enfoca una ventana por id. `false` si no está en el escritorio. + pub fn focus_window(&mut self, window: WindowId) -> bool { + match self.windows.iter().position(|&w| w == window) { + Some(i) => { + self.focus = i; + true + } + None => false, + } + } + + /// Intercambia la ventana enfocada con la siguiente en el orden de + /// teselado; el foco la acompaña. No hace nada si ya es la última. + pub fn move_focused_forward(&mut self) { + if self.focus + 1 < self.windows.len() { + self.windows.swap(self.focus, self.focus + 1); + self.focus += 1; + } + } + + /// Intercambia la ventana enfocada con la anterior. No hace nada si + /// ya es la primera. + pub fn move_focused_backward(&mut self) { + if self.focus > 0 && !self.windows.is_empty() { + self.windows.swap(self.focus, self.focus - 1); + self.focus -= 1; + } + } + + /// Resuelve la geometría: el rectángulo de cada ventana dentro de + /// `screen`, en orden de teselado. + pub fn layout(&self, screen: Rect) -> Vec<(WindowId, Rect)> { + let rects = tile(screen, self.windows.len(), &self.params); + self.windows.iter().copied().zip(rects).collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn ws() -> Workspace { + Workspace::new(LayoutParams::default()) + } + + #[test] + fn add_focuses_the_new_window() { + let mut w = ws(); + w.add(10); + w.add(20); + assert_eq!(w.focused(), Some(20)); + assert_eq!(w.len(), 2); + } + + #[test] + fn adding_an_existing_window_just_focuses_it() { + let mut w = ws(); + w.add(10); + w.add(20); + w.add(10); + assert_eq!(w.focused(), Some(10)); + assert_eq!(w.len(), 2); + } + + #[test] + fn focus_cycles_both_ways() { + let mut w = ws(); + for id in [1, 2, 3] { + w.add(id); + } + assert_eq!(w.focused(), Some(3)); + w.focus_next(); + assert_eq!(w.focused(), Some(1)); // dio la vuelta + w.focus_prev(); + assert_eq!(w.focused(), Some(3)); + } + + #[test] + fn remove_keeps_focus_valid() { + let mut w = ws(); + for id in [1, 2, 3] { + w.add(id); + } + w.focus_window(2); + w.remove(2); + // El foco se mantiene dentro de rango. + assert!(w.focused().is_some()); + assert_eq!(w.len(), 2); + } + + #[test] + fn remove_before_focus_shifts_it() { + let mut w = ws(); + for id in [1, 2, 3] { + w.add(id); + } + w.focus_window(3); // focus = 2 + w.remove(1); // quita una anterior + assert_eq!(w.focused(), Some(3)); // sigue enfocada la 3 + } + + #[test] + fn remove_last_window_empties_workspace() { + let mut w = ws(); + w.add(7); + assert!(w.remove(7)); + assert!(w.is_empty()); + assert_eq!(w.focused(), None); + } + + #[test] + fn move_focused_reorders_tiling() { + let mut w = ws(); + for id in [1, 2, 3] { + w.add(id); + } + w.focus_window(1); // primera + w.move_focused_forward(); + assert_eq!(w.windows(), &[2, 1, 3]); + assert_eq!(w.focused(), Some(1)); // el foco la acompañó + w.move_focused_backward(); + assert_eq!(w.windows(), &[1, 2, 3]); + } + + #[test] + fn layout_pairs_each_window_with_a_rect() { + let mut w = ws(); + for id in [100, 200, 300] { + w.add(id); + } + let screen = Rect::new(0, 0, 1920, 1080); + let placed = w.layout(screen); + assert_eq!(placed.len(), 3); + let ids: Vec<_> = placed.iter().map(|(id, _)| *id).collect(); + assert_eq!(ids, vec![100, 200, 300]); + } + + #[test] + fn empty_workspace_lays_out_nothing() { + assert!(ws().layout(Rect::new(0, 0, 800, 600)).is_empty()); + } +} diff --git a/nohup.out b/nohup.out index 8acf06c..3d3af54 100644 --- a/nohup.out +++ b/nohup.out @@ -189,3 +189,18 @@ Gdk-Message: 05:42:34.451: Error reading events from display: Broken pipe [Child 27081, MediaDecoderStateMachine #2] WARNING: 72495d46ca60 OpenCubeb() failed to init cubeb: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/AudioStream.cpp:279 [Child 27081, MediaDecoderStateMachine #2] WARNING: Decoder=724971e98800 [OnMediaSinkAudioError]: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/MediaDecoderStateMachine.cpp:4630 [Child 27081, MediaDecoderStateMachine #2] WARNING: Decoder=724971e98800 Decode error: NS_ERROR_DOM_MEDIA_MEDIASINK_ERR (0x806e000b) - OnMediaSinkAudioError: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/MediaDecoderStateMachineBase.cpp:168 +[Child 27081, MediaDecoderStateMachine #9] WARNING: 72496b642700 OpenCubeb() failed to init cubeb: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/AudioStream.cpp:279 +[Child 27081, MediaDecoderStateMachine #9] WARNING: Decoder=724971e98800 [OnMediaSinkAudioError]: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/MediaDecoderStateMachine.cpp:4630 +[Child 27081, MediaDecoderStateMachine #9] WARNING: Decoder=724971e98800 Decode error: NS_ERROR_DOM_MEDIA_MEDIASINK_ERR (0x806e000b) - OnMediaSinkAudioError: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/MediaDecoderStateMachineBase.cpp:168 +[Child 27081, MediaDecoderStateMachine #9] WARNING: 72495d49e4c0 OpenCubeb() failed to init cubeb: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/AudioStream.cpp:279 +[Child 27081, MediaDecoderStateMachine #9] WARNING: Decoder=724971e98800 [OnMediaSinkAudioError]: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/MediaDecoderStateMachine.cpp:4630 +[Child 27081, MediaDecoderStateMachine #9] WARNING: Decoder=724971e98800 Decode error: NS_ERROR_DOM_MEDIA_MEDIASINK_ERR (0x806e000b) - OnMediaSinkAudioError: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/MediaDecoderStateMachineBase.cpp:168 +[Child 27081, MediaDecoderStateMachine #12] WARNING: 72495eb5d3a0 OpenCubeb() failed to init cubeb: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/AudioStream.cpp:279 +[Child 27081, MediaDecoderStateMachine #12] WARNING: Decoder=724971e98800 [OnMediaSinkAudioError]: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/MediaDecoderStateMachine.cpp:4630 +[Child 27081, MediaDecoderStateMachine #12] WARNING: Decoder=724971e98800 Decode error: NS_ERROR_DOM_MEDIA_MEDIASINK_ERR (0x806e000b) - OnMediaSinkAudioError: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/MediaDecoderStateMachineBase.cpp:168 +[Child 27081, MediaDecoderStateMachine #12] WARNING: 72496b6425e0 OpenCubeb() failed to init cubeb: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/AudioStream.cpp:279 +[Child 27081, MediaDecoderStateMachine #12] WARNING: Decoder=724971e98800 [OnMediaSinkAudioError]: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/MediaDecoderStateMachine.cpp:4630 +[Child 27081, MediaDecoderStateMachine #12] WARNING: Decoder=724971e98800 Decode error: NS_ERROR_DOM_MEDIA_MEDIASINK_ERR (0x806e000b) - OnMediaSinkAudioError: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/MediaDecoderStateMachineBase.cpp:168 +[Child 27081, MediaDecoderStateMachine #12] WARNING: 72495d46cdc0 OpenCubeb() failed to init cubeb: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/AudioStream.cpp:279 +[Child 27081, MediaDecoderStateMachine #12] WARNING: Decoder=724971e98800 [OnMediaSinkAudioError]: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/MediaDecoderStateMachine.cpp:4630 +[Child 27081, MediaDecoderStateMachine #12] WARNING: Decoder=724971e98800 Decode error: NS_ERROR_DOM_MEDIA_MEDIASINK_ERR (0x806e000b) - OnMediaSinkAudioError: file /home/ubuntu/actions-runner/_work/desktop/desktop/engine/dom/media/MediaDecoderStateMachineBase.cpp:168