Add native self-hosted instance connection to fluxer_desktop
Trimmed monorepo checkout (fluxer_desktop + packages/voice_engine_v2 + tools/ci) with a "Connect to a Different Server" menu item and popout that lets the desktop app switch to any self-hosted Fluxer instance, plus fixes for well-known discovery on single-domain self-hosted deployments and a false-positive ERR_ABORTED on same-origin client redirects during the switch. Defaults to chat.fluxr.chat and uses an isolated userData directory from the official build.
This commit is contained in:
@@ -0,0 +1,491 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Nv12Layout {
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub stride_y: u32,
|
||||
pub stride_uv: u32,
|
||||
}
|
||||
|
||||
impl Nv12Layout {
|
||||
pub fn packed_size(&self) -> Option<usize> {
|
||||
if self.width == 0 || self.height == 0 || self.height % 2 != 0 {
|
||||
return None;
|
||||
}
|
||||
let w = self.width as usize;
|
||||
let h = self.height as usize;
|
||||
let y_bytes = w.checked_mul(h)?;
|
||||
let uv_bytes = w.checked_mul(h / 2)?;
|
||||
y_bytes.checked_add(uv_bytes)
|
||||
}
|
||||
|
||||
pub fn packed_stride_y(&self) -> u32 {
|
||||
self.width
|
||||
}
|
||||
|
||||
pub fn packed_stride_uv(&self) -> u32 {
|
||||
self.width
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pack_nv12(layout: Nv12Layout, y_plane: &[u8], uv_plane: &[u8], dst: &mut [u8]) -> bool {
|
||||
let Some(total) = layout.packed_size() else {
|
||||
return false;
|
||||
};
|
||||
if dst.len() != total {
|
||||
return false;
|
||||
}
|
||||
let w = layout.width as usize;
|
||||
let h = layout.height as usize;
|
||||
let stride_y = layout.stride_y as usize;
|
||||
let stride_uv = layout.stride_uv as usize;
|
||||
if stride_y < w || stride_uv < w {
|
||||
return false;
|
||||
}
|
||||
if y_plane.len() < stride_y * h {
|
||||
return false;
|
||||
}
|
||||
if uv_plane.len() < stride_uv * (h / 2) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let y_bytes = w * h;
|
||||
let uv_bytes = w * (h / 2);
|
||||
if stride_y == w && stride_uv == w {
|
||||
dst[..y_bytes].copy_from_slice(&y_plane[..y_bytes]);
|
||||
dst[y_bytes..y_bytes + uv_bytes].copy_from_slice(&uv_plane[..uv_bytes]);
|
||||
} else {
|
||||
let (dst_y, dst_uv) = dst.split_at_mut(y_bytes);
|
||||
for row in 0..h {
|
||||
let src_off = row * stride_y;
|
||||
let dst_off = row * w;
|
||||
dst_y[dst_off..dst_off + w].copy_from_slice(&y_plane[src_off..src_off + w]);
|
||||
}
|
||||
for row in 0..h / 2 {
|
||||
let src_off = row * stride_uv;
|
||||
let dst_off = row * w;
|
||||
dst_uv[dst_off..dst_off + w].copy_from_slice(&uv_plane[src_off..src_off + w]);
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
fn bgra_to_nv12_dcp(w: u32, h: u32, bgra: &[u8], bgra_stride: usize, dst: &mut [u8]) -> bool {
|
||||
use dcv_color_primitives as dcp;
|
||||
let src_format = dcp::ImageFormat {
|
||||
pixel_format: dcp::PixelFormat::Bgra,
|
||||
color_space: dcp::ColorSpace::Rgb,
|
||||
num_planes: 1,
|
||||
};
|
||||
let dst_format = dcp::ImageFormat {
|
||||
pixel_format: dcp::PixelFormat::Nv12,
|
||||
color_space: dcp::ColorSpace::Bt601,
|
||||
num_planes: 2,
|
||||
};
|
||||
let y_bytes = (w as usize) * (h as usize);
|
||||
let (dst_y, dst_uv) = dst.split_at_mut(y_bytes);
|
||||
dcp::convert_image(
|
||||
w,
|
||||
h,
|
||||
&src_format,
|
||||
Some(&[bgra_stride]),
|
||||
&[bgra],
|
||||
&dst_format,
|
||||
Some(&[w as usize, w as usize]),
|
||||
&mut [dst_y, dst_uv],
|
||||
)
|
||||
.is_ok()
|
||||
}
|
||||
|
||||
fn flip_nv12_vertical(w: usize, h: usize, dst: &mut [u8]) {
|
||||
assert!(w > 0);
|
||||
assert!(h % 2 == 0);
|
||||
let y_bytes = w * h;
|
||||
let uv_bytes = w * (h / 2);
|
||||
assert!(dst.len() >= y_bytes + uv_bytes);
|
||||
let (y_plane, rest) = dst.split_at_mut(y_bytes);
|
||||
flip_plane_rows(y_plane, w, h);
|
||||
flip_plane_rows(&mut rest[..uv_bytes], w, h / 2);
|
||||
}
|
||||
|
||||
fn flip_plane_rows(plane: &mut [u8], row_bytes: usize, rows: usize) {
|
||||
assert!(row_bytes > 0);
|
||||
assert!(plane.len() >= row_bytes * rows);
|
||||
for row in 0..rows / 2 {
|
||||
let top_start = row * row_bytes;
|
||||
let bottom_start = (rows - 1 - row) * row_bytes;
|
||||
let (head, tail) = plane.split_at_mut(bottom_start);
|
||||
head[top_start..top_start + row_bytes].swap_with_slice(&mut tail[..row_bytes]);
|
||||
}
|
||||
}
|
||||
|
||||
fn bgra_to_nv12_scalar(
|
||||
w: usize,
|
||||
h: usize,
|
||||
bgra: &[u8],
|
||||
bgra_row: usize,
|
||||
dst: &mut [u8],
|
||||
flip: bool,
|
||||
) -> bool {
|
||||
let y_bytes = w * h;
|
||||
let (dst_y, dst_uv) = dst.split_at_mut(y_bytes);
|
||||
for row in 0..h {
|
||||
let dst_row_index = if flip { h - 1 - row } else { row };
|
||||
let src_row = &bgra[row * bgra_row..row * bgra_row + w * 4];
|
||||
let dst_row = &mut dst_y[dst_row_index * w..dst_row_index * w + w];
|
||||
for col in 0..w {
|
||||
let b = src_row[col * 4] as i32;
|
||||
let g = src_row[col * 4 + 1] as i32;
|
||||
let r = src_row[col * 4 + 2] as i32;
|
||||
let y = (66 * r + 129 * g + 25 * b + 128) >> 8;
|
||||
dst_row[col] = (y + 16).clamp(0, 255) as u8;
|
||||
}
|
||||
}
|
||||
for row in 0..h / 2 {
|
||||
let dst_row_index = if flip { h / 2 - 1 - row } else { row };
|
||||
let r0 = &bgra[(row * 2) * bgra_row..(row * 2) * bgra_row + w * 4];
|
||||
let r1 = &bgra[(row * 2 + 1) * bgra_row..(row * 2 + 1) * bgra_row + w * 4];
|
||||
let dst_row = &mut dst_uv[dst_row_index * w..dst_row_index * w + w];
|
||||
for col in 0..w / 2 {
|
||||
let cx0 = col * 2 * 4;
|
||||
let cx1 = (col * 2 + 1) * 4;
|
||||
let b = (r0[cx0] as i32 + r0[cx1] as i32 + r1[cx0] as i32 + r1[cx1] as i32) >> 2;
|
||||
let g =
|
||||
(r0[cx0 + 1] as i32 + r0[cx1 + 1] as i32 + r1[cx0 + 1] as i32 + r1[cx1 + 1] as i32)
|
||||
>> 2;
|
||||
let r =
|
||||
(r0[cx0 + 2] as i32 + r0[cx1 + 2] as i32 + r1[cx0 + 2] as i32 + r1[cx1 + 2] as i32)
|
||||
>> 2;
|
||||
let u = ((-38 * r - 74 * g + 112 * b + 128) >> 8) + 128;
|
||||
let v = ((112 * r - 94 * g - 18 * b + 128) >> 8) + 128;
|
||||
dst_row[col * 2] = u.clamp(0, 255) as u8;
|
||||
dst_row[col * 2 + 1] = v.clamp(0, 255) as u8;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
pub fn bgra_to_nv12(
|
||||
layout: Nv12Layout,
|
||||
bgra: &[u8],
|
||||
bgra_stride: u32,
|
||||
dst: &mut [u8],
|
||||
flip: bool,
|
||||
) -> bool {
|
||||
let Some(total) = layout.packed_size() else {
|
||||
return false;
|
||||
};
|
||||
if dst.len() < total {
|
||||
return false;
|
||||
}
|
||||
let w = layout.width as usize;
|
||||
let h = layout.height as usize;
|
||||
let bgra_row = bgra_stride as usize;
|
||||
if bgra_row < w.checked_mul(4).unwrap_or(usize::MAX) {
|
||||
return false;
|
||||
}
|
||||
if bgra.len() < bgra_row.checked_mul(h).unwrap_or(usize::MAX) {
|
||||
return false;
|
||||
}
|
||||
#[cfg(target_os = "linux")]
|
||||
if bgra_to_nv12_dcp(layout.width, layout.height, bgra, bgra_row, dst) {
|
||||
if flip {
|
||||
flip_nv12_vertical(w, h, dst);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bgra_to_nv12_scalar(w, h, bgra, bgra_row, dst, flip)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn make_y(width: usize, height: usize, stride: usize) -> Vec<u8> {
|
||||
let mut v = vec![0u8; stride * height];
|
||||
for row in 0..height {
|
||||
for col in 0..width {
|
||||
v[row * stride + col] = ((row * width + col) % 251) as u8;
|
||||
}
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
fn make_uv(width: usize, height_half: usize, stride: usize) -> Vec<u8> {
|
||||
let mut v = vec![0u8; stride * height_half];
|
||||
for row in 0..height_half {
|
||||
for col in 0..width {
|
||||
v[row * stride + col] = ((row * width + col + 7) % 241) as u8;
|
||||
}
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packed_size_rejects_odd_height() {
|
||||
let layout = Nv12Layout {
|
||||
width: 16,
|
||||
height: 15,
|
||||
stride_y: 16,
|
||||
stride_uv: 16,
|
||||
};
|
||||
assert!(layout.packed_size().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packed_size_rejects_zero_dims() {
|
||||
let layout = Nv12Layout {
|
||||
width: 0,
|
||||
height: 4,
|
||||
stride_y: 0,
|
||||
stride_uv: 0,
|
||||
};
|
||||
assert!(layout.packed_size().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packed_size_matches_yuv420_layout() {
|
||||
let layout = Nv12Layout {
|
||||
width: 1920,
|
||||
height: 1080,
|
||||
stride_y: 1920,
|
||||
stride_uv: 1920,
|
||||
};
|
||||
assert_eq!(Some(1920 * 1080 * 3 / 2), layout.packed_size());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pack_nv12_strips_row_padding() {
|
||||
let layout = Nv12Layout {
|
||||
width: 8,
|
||||
height: 4,
|
||||
stride_y: 16,
|
||||
stride_uv: 16,
|
||||
};
|
||||
let y = make_y(8, 4, 16);
|
||||
let uv = make_uv(8, 2, 16);
|
||||
let mut dst = vec![0u8; layout.packed_size().unwrap()];
|
||||
assert!(pack_nv12(layout, &y, &uv, &mut dst));
|
||||
for row in 0..4 {
|
||||
for col in 0..8 {
|
||||
assert_eq!(dst[row * 8 + col], ((row * 8 + col) % 251) as u8);
|
||||
}
|
||||
}
|
||||
for row in 0..2 {
|
||||
for col in 0..8 {
|
||||
let off = 8 * 4 + row * 8 + col;
|
||||
assert_eq!(dst[off], ((row * 8 + col + 7) % 241) as u8);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pack_nv12_zero_padding_is_identity() {
|
||||
let layout = Nv12Layout {
|
||||
width: 4,
|
||||
height: 2,
|
||||
stride_y: 4,
|
||||
stride_uv: 4,
|
||||
};
|
||||
let y = vec![1, 2, 3, 4, 5, 6, 7, 8];
|
||||
let uv = vec![10, 11, 12, 13];
|
||||
let mut dst = vec![0u8; layout.packed_size().unwrap()];
|
||||
assert!(pack_nv12(layout, &y, &uv, &mut dst));
|
||||
assert_eq!(dst, vec![1, 2, 3, 4, 5, 6, 7, 8, 10, 11, 12, 13]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pack_nv12_rejects_short_source() {
|
||||
let layout = Nv12Layout {
|
||||
width: 4,
|
||||
height: 2,
|
||||
stride_y: 4,
|
||||
stride_uv: 4,
|
||||
};
|
||||
let y = vec![1, 2, 3];
|
||||
let uv = vec![10, 11, 12, 13];
|
||||
let mut dst = vec![0u8; layout.packed_size().unwrap()];
|
||||
assert!(!pack_nv12(layout, &y, &uv, &mut dst));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pack_nv12_rejects_undersized_stride() {
|
||||
let layout = Nv12Layout {
|
||||
width: 8,
|
||||
height: 2,
|
||||
stride_y: 4,
|
||||
stride_uv: 4,
|
||||
};
|
||||
let y = vec![0; 8];
|
||||
let uv = vec![0; 4];
|
||||
let mut dst = vec![0u8; 24];
|
||||
assert!(!pack_nv12(layout, &y, &uv, &mut dst));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pack_nv12_rejects_wrong_dst_size() {
|
||||
let layout = Nv12Layout {
|
||||
width: 4,
|
||||
height: 2,
|
||||
stride_y: 4,
|
||||
stride_uv: 4,
|
||||
};
|
||||
let y = vec![0; 8];
|
||||
let uv = vec![0; 4];
|
||||
let mut dst = vec![0u8; 11];
|
||||
assert!(!pack_nv12(layout, &y, &uv, &mut dst));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bgra_to_nv12_solid_black() {
|
||||
let layout = Nv12Layout {
|
||||
width: 4,
|
||||
height: 2,
|
||||
stride_y: 4,
|
||||
stride_uv: 4,
|
||||
};
|
||||
let bgra = vec![0u8; 4 * 4 * 2];
|
||||
let mut dst = vec![0u8; layout.packed_size().unwrap()];
|
||||
assert!(bgra_to_nv12(layout, &bgra, 16, &mut dst, false));
|
||||
for byte in &dst[..8] {
|
||||
assert_eq!(*byte, 16);
|
||||
}
|
||||
for chunk in dst[8..].chunks_exact(2) {
|
||||
assert_eq!(chunk[0], 128);
|
||||
assert_eq!(chunk[1], 128);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bgra_to_nv12_solid_white_is_in_range() {
|
||||
let layout = Nv12Layout {
|
||||
width: 4,
|
||||
height: 2,
|
||||
stride_y: 4,
|
||||
stride_uv: 4,
|
||||
};
|
||||
let bgra = vec![255u8; 4 * 4 * 2];
|
||||
let mut dst = vec![0u8; layout.packed_size().unwrap()];
|
||||
assert!(bgra_to_nv12(layout, &bgra, 16, &mut dst, false));
|
||||
for byte in &dst[..8] {
|
||||
assert!(*byte >= 230 && *byte <= 240, "luma out of range: {byte}");
|
||||
}
|
||||
for chunk in dst[8..].chunks_exact(2) {
|
||||
assert!(
|
||||
chunk[0] >= 124 && chunk[0] <= 132,
|
||||
"U out of range: {}",
|
||||
chunk[0]
|
||||
);
|
||||
assert!(
|
||||
chunk[1] >= 124 && chunk[1] <= 132,
|
||||
"V out of range: {}",
|
||||
chunk[1]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bgra_to_nv12_rejects_short_stride() {
|
||||
let layout = Nv12Layout {
|
||||
width: 4,
|
||||
height: 2,
|
||||
stride_y: 4,
|
||||
stride_uv: 4,
|
||||
};
|
||||
let bgra = vec![0u8; 8];
|
||||
let mut dst = vec![0u8; layout.packed_size().unwrap()];
|
||||
assert!(!bgra_to_nv12(layout, &bgra, 4, &mut dst, false));
|
||||
}
|
||||
|
||||
fn deterministic_bgra_frame(w: usize, h: usize, seed: u64) -> Vec<u8> {
|
||||
let mut state = seed;
|
||||
let mut v = vec![0u8; w * h * 4];
|
||||
for byte in v.iter_mut() {
|
||||
state = state
|
||||
.wrapping_mul(6364136223846793005)
|
||||
.wrapping_add(1442695040888963407);
|
||||
*byte = (state >> 56) as u8;
|
||||
}
|
||||
v
|
||||
}
|
||||
|
||||
fn row_reversed(bgra: &[u8], w: usize, h: usize) -> Vec<u8> {
|
||||
let row_bytes = w * 4;
|
||||
assert_eq!(bgra.len(), row_bytes * h);
|
||||
let mut reversed = vec![0u8; bgra.len()];
|
||||
for row in 0..h {
|
||||
let src = (h - 1 - row) * row_bytes;
|
||||
reversed[row * row_bytes..(row + 1) * row_bytes]
|
||||
.copy_from_slice(&bgra[src..src + row_bytes]);
|
||||
}
|
||||
reversed
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bgra_to_nv12_flip_matches_pre_reversed_rows() {
|
||||
for (w, h, seed) in [(8usize, 4usize, 1u64), (16, 8, 2), (64, 32, 3), (12, 6, 4)] {
|
||||
let layout = Nv12Layout {
|
||||
width: w as u32,
|
||||
height: h as u32,
|
||||
stride_y: w as u32,
|
||||
stride_uv: w as u32,
|
||||
};
|
||||
let bgra = deterministic_bgra_frame(w, h, seed);
|
||||
let reversed = row_reversed(&bgra, w, h);
|
||||
let total = layout.packed_size().expect("even dims");
|
||||
let mut flipped = vec![0u8; total];
|
||||
let mut reference = vec![0u8; total];
|
||||
assert!(bgra_to_nv12(
|
||||
layout,
|
||||
&bgra,
|
||||
(w * 4) as u32,
|
||||
&mut flipped,
|
||||
true
|
||||
));
|
||||
assert!(bgra_to_nv12(
|
||||
layout,
|
||||
&reversed,
|
||||
(w * 4) as u32,
|
||||
&mut reference,
|
||||
false
|
||||
));
|
||||
assert_eq!(flipped, reference, "mismatch at {w}x{h} seed {seed}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bgra_to_nv12_flip_moves_top_row_luma_to_bottom() {
|
||||
let layout = Nv12Layout {
|
||||
width: 4,
|
||||
height: 4,
|
||||
stride_y: 4,
|
||||
stride_uv: 4,
|
||||
};
|
||||
let mut bgra = vec![0u8; 4 * 4 * 4];
|
||||
bgra[..16].fill(255);
|
||||
let mut dst = vec![0u8; layout.packed_size().unwrap()];
|
||||
assert!(bgra_to_nv12(layout, &bgra, 16, &mut dst, true));
|
||||
let y = &dst[..16];
|
||||
assert!(y[12] > 200, "bottom row should hold the white luma: {y:?}");
|
||||
assert!(y[0] < 32, "top row should hold the black luma: {y:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bgra_to_nv12_unflipped_path_is_unchanged_by_flip_support() {
|
||||
let layout = Nv12Layout {
|
||||
width: 8,
|
||||
height: 4,
|
||||
stride_y: 8,
|
||||
stride_uv: 8,
|
||||
};
|
||||
let bgra = deterministic_bgra_frame(8, 4, 9);
|
||||
let total = layout.packed_size().unwrap();
|
||||
let mut first = vec![0u8; total];
|
||||
let mut second = vec![0u8; total];
|
||||
assert!(bgra_to_nv12(layout, &bgra, 32, &mut first, false));
|
||||
assert!(bgra_to_nv12(layout, &bgra, 32, &mut second, false));
|
||||
assert_eq!(first, second);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user