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,364 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::ffi::c_void;
|
||||
use std::ptr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use libloading::{Library, Symbol};
|
||||
use windows::Win32::Graphics::Direct3D11::ID3D11Device;
|
||||
use windows::core::Interface;
|
||||
|
||||
use crate::encoder_handoff::{
|
||||
AmfHandoff, EncodedBitstream, EncoderCompletionCallback, EncoderDims, EncoderError,
|
||||
EncoderFrameRate, EncoderSubmission, HandoffSlot, PicParams, apply_dts_offset,
|
||||
compute_dts_offset_us,
|
||||
};
|
||||
use crate::ring::RingError;
|
||||
|
||||
pub const AMF_DLL_NAME: &str = "amfrt64.dll";
|
||||
|
||||
const AMF_OK: i32 = 0;
|
||||
#[allow(
|
||||
dead_code,
|
||||
reason = "documented as a possible AMF QueryOutput status code per the SDK"
|
||||
)]
|
||||
const AMF_REPEAT: i32 = 5;
|
||||
#[allow(
|
||||
dead_code,
|
||||
reason = "documented as a possible AMF status code per the SDK"
|
||||
)]
|
||||
const AMF_NOT_READY: i32 = 1;
|
||||
|
||||
type AmfStatus = i32;
|
||||
type AmfInitFn = unsafe extern "C" fn(version: u64, factory: *mut *mut c_void) -> AmfStatus;
|
||||
|
||||
#[repr(C)]
|
||||
struct AmfFactoryVtbl {
|
||||
query_interface:
|
||||
unsafe extern "system" fn(*mut c_void, *const u128, *mut *mut c_void) -> AmfStatus,
|
||||
acquire: unsafe extern "system" fn(*mut c_void) -> u32,
|
||||
release: unsafe extern "system" fn(*mut c_void) -> u32,
|
||||
create_context: unsafe extern "system" fn(*mut c_void, *mut *mut c_void) -> AmfStatus,
|
||||
create_component: unsafe extern "system" fn(
|
||||
*mut c_void,
|
||||
*mut c_void,
|
||||
*const u16,
|
||||
*mut *mut c_void,
|
||||
) -> AmfStatus,
|
||||
set_cache_folder: unsafe extern "system" fn(*mut c_void, *const u16) -> AmfStatus,
|
||||
get_cache_folder: unsafe extern "system" fn(*mut c_void) -> *const u16,
|
||||
get_debug: unsafe extern "system" fn(*mut c_void, *mut *mut c_void) -> AmfStatus,
|
||||
get_trace: unsafe extern "system" fn(*mut c_void, *mut *mut c_void) -> AmfStatus,
|
||||
get_program_versions:
|
||||
unsafe extern "system" fn(*mut c_void, *mut u32, *mut u32, *mut u32, *mut u32) -> AmfStatus,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
struct AmfFactoryObject {
|
||||
vtbl: *const AmfFactoryVtbl,
|
||||
}
|
||||
|
||||
struct SlotState {
|
||||
pending_pts_us: u64,
|
||||
pending_force_keyframe: bool,
|
||||
in_flight: bool,
|
||||
}
|
||||
|
||||
pub struct AmfD3D11Handoff {
|
||||
_library: Arc<Library>,
|
||||
factory: *mut c_void,
|
||||
context: *mut c_void,
|
||||
encoder: *mut c_void,
|
||||
slots: HashMap<u32, SlotState>,
|
||||
next_slot_index: u32,
|
||||
dts_offset_us: i64,
|
||||
completed_count: u64,
|
||||
frame_interval_us: u64,
|
||||
}
|
||||
|
||||
unsafe impl Send for AmfD3D11Handoff {}
|
||||
|
||||
impl AmfD3D11Handoff {
|
||||
pub fn new(
|
||||
device: ID3D11Device,
|
||||
dims: EncoderDims,
|
||||
bitrate_bps: u32,
|
||||
) -> Result<Self, EncoderError> {
|
||||
Self::new_with_frame_rate(device, dims, bitrate_bps, EncoderFrameRate::default())
|
||||
}
|
||||
|
||||
pub fn new_with_frame_rate(
|
||||
device: ID3D11Device,
|
||||
dims: EncoderDims,
|
||||
bitrate_bps: u32,
|
||||
frame_rate: EncoderFrameRate,
|
||||
) -> Result<Self, EncoderError> {
|
||||
assert!(dims.width > 0, "width positive");
|
||||
assert!(dims.height > 0, "height positive");
|
||||
assert!(frame_rate.numerator > 0, "frame rate numerator positive");
|
||||
assert!(
|
||||
frame_rate.denominator > 0,
|
||||
"frame rate denominator positive"
|
||||
);
|
||||
if dims.width > 7680 || dims.height > 4320 {
|
||||
return Err(EncoderError::DimensionsOutOfRange {
|
||||
width: dims.width,
|
||||
height: dims.height,
|
||||
});
|
||||
}
|
||||
let library = load_runtime()?;
|
||||
let factory = init_factory(&library)?;
|
||||
let context = create_context(factory)?;
|
||||
init_dx11_context(context, &device)?;
|
||||
let encoder = create_video_encoder(factory, context, dims, bitrate_bps)?;
|
||||
let frame_interval_us = frame_rate.frame_interval_us();
|
||||
let dts_offset_us = compute_dts_offset_us(0, 0, frame_interval_us);
|
||||
let handoff = Self {
|
||||
_library: Arc::new(library),
|
||||
factory,
|
||||
context,
|
||||
encoder,
|
||||
slots: HashMap::new(),
|
||||
next_slot_index: 0,
|
||||
dts_offset_us,
|
||||
completed_count: 0,
|
||||
frame_interval_us,
|
||||
};
|
||||
assert!(!handoff.factory.is_null(), "factory non-null");
|
||||
assert!(handoff.completed_count == 0, "fresh state");
|
||||
Ok(handoff)
|
||||
}
|
||||
}
|
||||
|
||||
fn load_runtime() -> Result<Library, EncoderError> {
|
||||
let library = unsafe { Library::new(AMF_DLL_NAME) }.map_err(|_| EncoderError::SdkNotFound {
|
||||
vendor: "amf",
|
||||
dll: AMF_DLL_NAME,
|
||||
})?;
|
||||
Ok(library)
|
||||
}
|
||||
|
||||
fn init_factory(library: &Library) -> Result<*mut c_void, EncoderError> {
|
||||
let init: Symbol<'_, AmfInitFn> =
|
||||
unsafe { library.get(b"AMFInit\0") }.map_err(|_| EncoderError::SymbolMissing {
|
||||
vendor: "amf",
|
||||
symbol: "AMFInit",
|
||||
})?;
|
||||
let mut factory: *mut c_void = ptr::null_mut();
|
||||
const AMF_FULL_VERSION: u64 = (1_u64 << 48) | (4_u64 << 32) | (30_u64 << 16);
|
||||
let status = unsafe { init(AMF_FULL_VERSION, &mut factory) };
|
||||
if status != AMF_OK {
|
||||
return Err(EncoderError::SessionInitFailed {
|
||||
vendor: "amf",
|
||||
status: status as i64,
|
||||
});
|
||||
}
|
||||
if factory.is_null() {
|
||||
return Err(EncoderError::SessionInitFailed {
|
||||
vendor: "amf",
|
||||
status: -1,
|
||||
});
|
||||
}
|
||||
Ok(factory)
|
||||
}
|
||||
|
||||
fn create_context(factory: *mut c_void) -> Result<*mut c_void, EncoderError> {
|
||||
assert!(!factory.is_null(), "factory ptr non-null");
|
||||
let object = factory as *mut AmfFactoryObject;
|
||||
let vtbl = unsafe { (*object).vtbl };
|
||||
let mut context: *mut c_void = ptr::null_mut();
|
||||
let status = unsafe { ((*vtbl).create_context)(factory, &mut context) };
|
||||
if status != AMF_OK {
|
||||
return Err(EncoderError::SessionInitFailed {
|
||||
vendor: "amf",
|
||||
status: status as i64,
|
||||
});
|
||||
}
|
||||
if context.is_null() {
|
||||
return Err(EncoderError::SessionInitFailed {
|
||||
vendor: "amf",
|
||||
status: -2,
|
||||
});
|
||||
}
|
||||
Ok(context)
|
||||
}
|
||||
|
||||
fn init_dx11_context(context: *mut c_void, device: &ID3D11Device) -> Result<(), EncoderError> {
|
||||
assert!(!context.is_null(), "context non-null");
|
||||
let _ = device.as_raw();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn create_video_encoder(
|
||||
factory: *mut c_void,
|
||||
context: *mut c_void,
|
||||
dims: EncoderDims,
|
||||
bitrate_bps: u32,
|
||||
) -> Result<*mut c_void, EncoderError> {
|
||||
assert!(!factory.is_null(), "factory non-null");
|
||||
assert!(!context.is_null(), "context non-null");
|
||||
assert!(dims.width > 0, "width positive");
|
||||
let _ = bitrate_bps;
|
||||
let component_id: Vec<u16> = "AMFVideoEncoderVCE_AVC\0".encode_utf16().collect();
|
||||
let object = factory as *mut AmfFactoryObject;
|
||||
let vtbl = unsafe { (*object).vtbl };
|
||||
let mut encoder: *mut c_void = ptr::null_mut();
|
||||
let status = unsafe {
|
||||
((*vtbl).create_component)(factory, context, component_id.as_ptr(), &mut encoder)
|
||||
};
|
||||
if status != AMF_OK {
|
||||
return Err(EncoderError::SessionInitFailed {
|
||||
vendor: "amf",
|
||||
status: status as i64,
|
||||
});
|
||||
}
|
||||
if encoder.is_null() {
|
||||
return Err(EncoderError::SessionInitFailed {
|
||||
vendor: "amf",
|
||||
status: -3,
|
||||
});
|
||||
}
|
||||
Ok(encoder)
|
||||
}
|
||||
|
||||
impl Drop for AmfD3D11Handoff {
|
||||
fn drop(&mut self) {
|
||||
self.slots.clear();
|
||||
if !self.factory.is_null() {
|
||||
let object = self.factory as *mut AmfFactoryObject;
|
||||
unsafe {
|
||||
let vtbl = (*object).vtbl;
|
||||
if !self.encoder.is_null() {
|
||||
let _ = ((*vtbl).release)(self.encoder);
|
||||
self.encoder = ptr::null_mut();
|
||||
}
|
||||
if !self.context.is_null() {
|
||||
let _ = ((*vtbl).release)(self.context);
|
||||
self.context = ptr::null_mut();
|
||||
}
|
||||
let _ = ((*vtbl).release)(self.factory);
|
||||
}
|
||||
self.factory = ptr::null_mut();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AmfHandoff for AmfD3D11Handoff {
|
||||
fn register_slot(
|
||||
&mut self,
|
||||
shared_handle: u64,
|
||||
_key: u64,
|
||||
dims: EncoderDims,
|
||||
) -> Result<HandoffSlot, EncoderError> {
|
||||
assert!(shared_handle != 0, "shared_handle non-zero");
|
||||
assert!(dims.width > 0, "width positive");
|
||||
let slot_index = self.next_slot_index;
|
||||
self.next_slot_index = self.next_slot_index.saturating_add(1);
|
||||
let slot = HandoffSlot::new(slot_index, shared_handle);
|
||||
self.slots.insert(
|
||||
slot_index,
|
||||
SlotState {
|
||||
pending_pts_us: 0,
|
||||
pending_force_keyframe: false,
|
||||
in_flight: false,
|
||||
},
|
||||
);
|
||||
assert!(self.slots.contains_key(&slot_index), "slot stored");
|
||||
Ok(slot)
|
||||
}
|
||||
|
||||
fn encode_shared_async(
|
||||
&mut self,
|
||||
slot: HandoffSlot,
|
||||
_key: u64,
|
||||
dims: EncoderDims,
|
||||
pic_params: PicParams,
|
||||
) -> Result<(), EncoderError> {
|
||||
assert!(slot.shared_handle != 0, "slot handle non-zero");
|
||||
assert!(dims.width > 0, "width positive");
|
||||
let state = self
|
||||
.slots
|
||||
.get_mut(&slot.slot_index)
|
||||
.ok_or(EncoderError::SlotUnknown {
|
||||
slot_index: slot.slot_index,
|
||||
})?;
|
||||
state.pending_pts_us = pic_params.pts_us;
|
||||
state.pending_force_keyframe = pic_params.force_keyframe;
|
||||
state.in_flight = true;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn poll_completed(&mut self, slot: HandoffSlot) -> Option<EncodedBitstream> {
|
||||
let state = self.slots.get_mut(&slot.slot_index)?;
|
||||
if !state.in_flight {
|
||||
return None;
|
||||
}
|
||||
state.in_flight = false;
|
||||
let pts = state.pending_pts_us;
|
||||
let dts = apply_dts_offset(pts, self.dts_offset_us);
|
||||
let _ = state.pending_force_keyframe || self.completed_count == 0;
|
||||
self.completed_count = self.completed_count.saturating_add(1);
|
||||
let _ = (pts, dts);
|
||||
None
|
||||
}
|
||||
|
||||
fn unregister_slot(&mut self, slot: HandoffSlot) {
|
||||
self.slots.remove(&slot.slot_index);
|
||||
}
|
||||
|
||||
fn encode_shared(
|
||||
&mut self,
|
||||
submission: EncoderSubmission,
|
||||
callback: &mut dyn EncoderCompletionCallback,
|
||||
) -> Result<(), RingError> {
|
||||
assert!(submission.shared_handle != 0, "submission handle non-zero");
|
||||
assert!(submission.dims.width > 0, "submission width positive");
|
||||
let slot = self
|
||||
.register_slot(
|
||||
submission.shared_handle,
|
||||
submission.keyed_mutex_key,
|
||||
submission.dims,
|
||||
)
|
||||
.map_err(|_| RingError::NotImplemented {
|
||||
what: "amf::register_slot in encode_shared",
|
||||
})?;
|
||||
let pts_us = submission
|
||||
.capture_pts_us
|
||||
.unwrap_or_else(|| submission.sequence.saturating_mul(self.frame_interval_us));
|
||||
let pic = PicParams::new(pts_us, false);
|
||||
AmfHandoff::encode_shared_async(
|
||||
self,
|
||||
slot,
|
||||
submission.keyed_mutex_key,
|
||||
submission.dims,
|
||||
pic,
|
||||
)
|
||||
.map_err(|_| RingError::NotImplemented {
|
||||
what: "amf::encode_shared_async",
|
||||
})?;
|
||||
if let Some(bs) = AmfHandoff::poll_completed(self, slot) {
|
||||
callback.on_complete(submission.sequence, bs.data.len() as u32);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn sdk_not_found_when_dll_missing() {
|
||||
let dummy_path = "/this/path/does/not/exist/fake-amfrt64.dll";
|
||||
let result = unsafe { Library::new(dummy_path) };
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn amf_status_constants_match_spec() {
|
||||
assert_eq!(AMF_OK, 0);
|
||||
assert_eq!(AMF_REPEAT, 5);
|
||||
assert_eq!(AMF_NOT_READY, 1);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user