del: 移除安卓支持

This commit is contained in:
mofeng-git
2026-07-14 21:01:47 +08:00
parent 139064de35
commit 1ded7a8a66
88 changed files with 299 additions and 7107 deletions

View File

@@ -1,11 +1,7 @@
#[cfg(all(unix, not(feature = "android")))]
#[cfg(unix)]
#[path = "capture_linux.rs"]
mod imp;
#[cfg(feature = "android")]
#[path = "capture_android.rs"]
mod imp;
#[cfg(windows)]
#[path = "capture_windows.rs"]
mod imp;

View File

@@ -1,292 +0,0 @@
use alsa::pcm::{Access, Format, Frames, HwParams};
use alsa::{Direction, ValueOr, PCM};
use bytes::Bytes;
use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
use std::sync::Arc;
use std::time::Instant;
use tokio::sync::{broadcast, watch, Mutex};
use tracing::{debug, info};
use crate::audio::device::AudioDeviceInfo;
use crate::error::{AppError, Result};
use crate::utils::LogThrottler;
use crate::{error_throttled, warn_throttled};
#[derive(Debug, Clone)]
pub struct AudioConfig {
pub device_name: String,
pub sample_rate: u32,
pub channels: u32,
pub frame_size: u32,
pub buffer_frames: u32,
pub period_frames: u32,
}
impl Default for AudioConfig {
fn default() -> Self {
Self {
device_name: String::new(),
sample_rate: 48_000,
channels: 2,
frame_size: 960,
buffer_frames: 4096,
period_frames: 960,
}
}
}
impl AudioConfig {
pub fn for_device(device: &AudioDeviceInfo) -> Self {
Self {
device_name: device.name.clone(),
..Default::default()
}
}
pub fn bytes_per_sample(&self) -> u32 {
2 * self.channels
}
pub fn bytes_per_frame(&self) -> usize {
(self.frame_size * self.bytes_per_sample()) as usize
}
}
#[derive(Debug, Clone)]
pub struct AudioFrame {
pub data: Bytes,
pub sample_rate: u32,
pub channels: u32,
pub samples: u32,
pub sequence: u64,
pub timestamp: Instant,
}
impl AudioFrame {
pub fn new_interleaved(data: Bytes, channels: u32, sample_rate: u32, sequence: u64) -> Self {
let bps = 2 * channels;
Self {
samples: data.len() as u32 / bps,
data,
sample_rate,
channels,
sequence,
timestamp: Instant::now(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CaptureState {
Stopped,
Running,
Error,
}
pub struct AudioCapturer {
config: AudioConfig,
state: Arc<watch::Sender<CaptureState>>,
state_rx: watch::Receiver<CaptureState>,
frame_tx: broadcast::Sender<AudioFrame>,
stop_flag: Arc<AtomicBool>,
sequence: Arc<AtomicU64>,
capture_handle: Mutex<Option<tokio::task::JoinHandle<()>>>,
log_throttler: LogThrottler,
}
impl AudioCapturer {
pub fn new(config: AudioConfig) -> Self {
let (state_tx, state_rx) = watch::channel(CaptureState::Stopped);
let (frame_tx, _) = broadcast::channel(16);
Self {
config,
state: Arc::new(state_tx),
state_rx,
frame_tx,
stop_flag: Arc::new(AtomicBool::new(false)),
sequence: Arc::new(AtomicU64::new(0)),
capture_handle: Mutex::new(None),
log_throttler: LogThrottler::with_secs(5),
}
}
pub fn state(&self) -> CaptureState {
*self.state_rx.borrow()
}
pub fn state_watch(&self) -> watch::Receiver<CaptureState> {
self.state_rx.clone()
}
pub fn subscribe(&self) -> broadcast::Receiver<AudioFrame> {
self.frame_tx.subscribe()
}
pub async fn start(&self) -> Result<()> {
if self.state() == CaptureState::Running {
return Ok(());
}
debug!(
"Starting audio capture on {} at {}Hz {}ch",
self.config.device_name, self.config.sample_rate, self.config.channels
);
self.stop_flag.store(false, Ordering::SeqCst);
let config = self.config.clone();
let state = self.state.clone();
let frame_tx = self.frame_tx.clone();
let stop_flag = self.stop_flag.clone();
let sequence = self.sequence.clone();
let log_throttler = self.log_throttler.clone();
let handle = tokio::task::spawn_blocking(move || {
let result = run_capture(
&config,
&state,
&frame_tx,
&stop_flag,
&sequence,
&log_throttler,
);
if let Err(e) = result {
error_throttled!(log_throttler, "capture_error", "Audio capture error: {}", e);
let _ = state.send(CaptureState::Error);
} else {
let _ = state.send(CaptureState::Stopped);
}
});
*self.capture_handle.lock().await = Some(handle);
Ok(())
}
pub async fn stop(&self) -> Result<()> {
info!("Stopping audio capture");
self.stop_flag.store(true, Ordering::SeqCst);
if let Some(handle) = self.capture_handle.lock().await.take() {
let _ = handle.await;
}
let _ = self.state.send(CaptureState::Stopped);
Ok(())
}
pub fn is_running(&self) -> bool {
self.state() == CaptureState::Running
}
}
fn run_capture(
config: &AudioConfig,
state: &watch::Sender<CaptureState>,
frame_tx: &broadcast::Sender<AudioFrame>,
stop_flag: &AtomicBool,
sequence: &AtomicU64,
log_throttler: &LogThrottler,
) -> Result<()> {
let pcm = PCM::new(&config.device_name, Direction::Capture, false).map_err(|e| {
AppError::AudioError(format!(
"Failed to open audio device {}: {}",
config.device_name, e
))
})?;
{
let hwp = HwParams::any(&pcm)
.map_err(|e| AppError::AudioError(format!("Failed to get HwParams: {}", e)))?;
hwp.set_channels(config.channels)
.map_err(|e| AppError::AudioError(format!("Failed to set channels: {}", e)))?;
hwp.set_rate(config.sample_rate, ValueOr::Nearest)
.map_err(|e| AppError::AudioError(format!("Failed to set sample rate: {}", e)))?;
hwp.set_format(Format::s16())
.map_err(|e| AppError::AudioError(format!("Failed to set format: {}", e)))?;
hwp.set_access(Access::RWInterleaved)
.map_err(|e| AppError::AudioError(format!("Failed to set access: {}", e)))?;
hwp.set_buffer_size_near(config.buffer_frames as Frames)
.map_err(|e| AppError::AudioError(format!("Failed to set buffer size: {}", e)))?;
hwp.set_period_size_near(config.period_frames as Frames, ValueOr::Nearest)
.map_err(|e| AppError::AudioError(format!("Failed to set period size: {}", e)))?;
pcm.hw_params(&hwp)
.map_err(|e| AppError::AudioError(format!("Failed to apply hw params: {}", e)))?;
}
let hw_now = pcm.hw_params_current().map_err(|e| {
AppError::AudioError(format!("Failed to read hw_params after apply: {}", e))
})?;
let actual_rate = hw_now
.get_rate()
.map_err(|e| AppError::AudioError(format!("Failed to read sample rate: {}", e)))?;
let actual_ch = hw_now
.get_channels()
.map_err(|e| AppError::AudioError(format!("Failed to read channels: {}", e)))?;
if actual_rate != 48_000 {
return Err(AppError::AudioError(format!(
"Audio capture requires 48000 Hz; device is {} Hz",
actual_rate
)));
}
if actual_ch != 2 {
return Err(AppError::AudioError(format!(
"Audio capture requires 2 channels (stereo); device has {}",
actual_ch
)));
}
debug!("Audio capture: 48000 Hz, 2 ch");
pcm.prepare()
.map_err(|e| AppError::AudioError(format!("Failed to prepare PCM: {}", e)))?;
let _ = state.send(CaptureState::Running);
let period_frames = pcm
.hw_params_current()
.ok()
.and_then(|h| h.get_period_size().ok())
.map(|f| f as usize)
.unwrap_or(1024)
.max(256);
let buf_frames = period_frames.saturating_mul(4).max(2048);
let io = pcm
.io_i16()
.map_err(|e| AppError::AudioError(format!("Failed to get PCM IO: {}", e)))?;
let mut buffer = vec![0i16; buf_frames * 2];
let mut next_log = Instant::now();
while !stop_flag.load(Ordering::SeqCst) {
match io.readi(&mut buffer[..period_frames * 2]) {
Ok(frames_read) => {
if frames_read == 0 {
continue;
}
let samples = frames_read * 2;
let data = Bytes::copy_from_slice(bytemuck::cast_slice(&buffer[..samples]));
let seq = sequence.fetch_add(1, Ordering::SeqCst);
let frame = AudioFrame::new_interleaved(data, 2, 48_000, seq);
let _ = frame_tx.send(frame);
if next_log.elapsed().as_secs() >= 5 {
debug!("Captured audio frame {} ({} samples)", seq, samples / 2);
next_log = Instant::now();
}
}
Err(err) => {
warn_throttled!(
log_throttler,
"alsa_read",
"ALSA read error on {}: {}",
config.device_name,
err
);
let _ = pcm.try_recover(err, false);
}
}
}
let _ = pcm.drain();
Ok(())
}

View File

@@ -1,11 +1,7 @@
#[cfg(all(unix, not(feature = "android")))]
#[cfg(unix)]
#[path = "device_linux.rs"]
mod imp;
#[cfg(feature = "android")]
#[path = "device_android.rs"]
mod imp;
#[cfg(windows)]
#[path = "device_windows.rs"]
mod imp;

View File

@@ -1,185 +0,0 @@
use alsa::pcm::HwParams;
use alsa::{Direction, PCM};
use serde::Serialize;
use tracing::{debug, info, warn};
use crate::error::{AppError, Result};
#[derive(Debug, Clone, Serialize)]
pub struct AudioDeviceInfo {
pub name: String,
pub description: String,
pub card_index: i32,
pub device_index: i32,
pub sample_rates: Vec<u32>,
pub channels: Vec<u32>,
pub is_capture: bool,
pub is_hdmi: bool,
pub usb_bus: Option<String>,
}
fn get_usb_bus_info(card_index: i32) -> Option<String> {
if card_index < 0 {
return None;
}
let device_path = format!("/sys/class/sound/card{}/device", card_index);
let link_target = std::fs::read_link(&device_path).ok()?;
let link_str = link_target.to_string_lossy();
for component in link_str.split('/') {
if component.contains('-') && !component.contains(':') {
if component
.chars()
.next()
.map(|c| c.is_ascii_digit())
.unwrap_or(false)
{
return Some(component.to_string());
}
}
}
None
}
pub fn enumerate_audio_devices() -> Result<Vec<AudioDeviceInfo>> {
enumerate_audio_devices_with_current(None)
}
pub fn enumerate_audio_devices_with_current(
current_device: Option<&str>,
) -> Result<Vec<AudioDeviceInfo>> {
let mut devices = Vec::new();
for card_result in alsa::card::Iter::new() {
let card = match card_result {
Ok(card) => card,
Err(err) => {
debug!("Error iterating card: {}", err);
continue;
}
};
let card_index = card.get_index();
let card_name = card.get_name().unwrap_or_else(|_| "Unknown".to_string());
let card_longname = card.get_longname().unwrap_or_else(|_| card_name.clone());
debug!("Found audio card {}: {}", card_index, card_longname);
let long_lower = card_longname.to_lowercase();
let is_hdmi = long_lower.contains("hdmi")
|| long_lower.contains("capture")
|| long_lower.contains("usb");
let usb_bus = get_usb_bus_info(card_index);
for device_index in 0..8 {
let device_name = format!("hw:{},{}", card_index, device_index);
let is_current_device = current_device == Some(device_name.as_str());
let mut push_info =
|sample_rates: Vec<u32>, channels: Vec<u32>, description: String| {
devices.push(AudioDeviceInfo {
name: device_name.clone(),
description,
card_index,
device_index,
sample_rates,
channels,
is_capture: true,
is_hdmi,
usb_bus: usb_bus.clone(),
});
};
match PCM::new(&device_name, Direction::Capture, false) {
Ok(pcm) => {
let (sample_rates, channels) = query_device_caps(&pcm);
if !sample_rates.is_empty() && !channels.is_empty() {
push_info(
sample_rates,
channels,
format!("{} - Device {}", card_longname, device_index),
);
}
}
Err(_) if is_current_device => {
debug!(
"Device {} is busy (in use by us), adding with default caps",
device_name
);
push_info(
vec![44_100, 48_000],
vec![2],
format!("{} - Device {} (in use)", card_longname, device_index),
);
}
Err(_) => {}
}
}
}
info!("Found {} audio capture devices", devices.len());
Ok(devices)
}
fn query_device_caps(pcm: &PCM) -> (Vec<u32>, Vec<u32>) {
let hwp = match HwParams::any(pcm) {
Ok(h) => h,
Err(_) => return (vec![], vec![]),
};
let common_rates = [8000, 16000, 22050, 44100, 48000, 96000];
let mut supported_rates = Vec::new();
for rate in &common_rates {
if hwp.test_rate(*rate).is_ok() {
supported_rates.push(*rate);
}
}
let mut supported_channels = Vec::new();
for ch in 1..=8 {
if hwp.test_channels(ch).is_ok() {
supported_channels.push(ch);
}
}
(supported_rates, supported_channels)
}
pub fn find_best_audio_device() -> Result<AudioDeviceInfo> {
let devices = enumerate_audio_devices()?;
if devices.is_empty() {
return Err(AppError::AudioError(
"No audio capture devices found".to_string(),
));
}
let mut first_48k_stereo: Option<&AudioDeviceInfo> = None;
for device in &devices {
if !device.sample_rates.contains(&48_000) || !device.channels.contains(&2) {
continue;
}
if device.is_hdmi {
info!("Selected HDMI audio device: {}", device.description);
return Ok(device.clone());
}
if first_48k_stereo.is_none() {
first_48k_stereo = Some(device);
}
}
if let Some(device) = first_48k_stereo {
info!("Selected audio device: {}", device.description);
return Ok(device.clone());
}
let device = devices.into_iter().next().unwrap();
warn!(
"Using fallback audio device: {} (may not support optimal settings)",
device.description
);
Ok(device)
}

View File

@@ -176,18 +176,9 @@ fn get_meminfo() -> MemInfo {
}
fn get_network_addresses() -> Vec<NetworkAddress> {
#[cfg(target_os = "android")]
{
return get_network_addresses_android();
}
#[cfg(not(target_os = "android"))]
{
get_network_addresses_ifaddrs()
}
get_network_addresses_ifaddrs()
}
#[cfg(not(target_os = "android"))]
fn get_network_addresses_ifaddrs() -> Vec<NetworkAddress> {
let all_addrs = match nix::ifaddrs::getifaddrs() {
Ok(addrs) => addrs,
@@ -260,101 +251,6 @@ fn get_network_addresses_ifaddrs() -> Vec<NetworkAddress> {
addresses
}
#[cfg(target_os = "android")]
fn get_network_addresses_android() -> Vec<NetworkAddress> {
let net_dir = match std::fs::read_dir("/sys/class/net") {
Ok(dir) => dir,
Err(_) => return Vec::new(),
};
let mut addresses = Vec::new();
let mut seen = std::collections::HashSet::new();
for entry in net_dir.flatten() {
let iface_name = match entry.file_name().into_string() {
Ok(name) => name,
Err(_) => continue,
};
if iface_name == "lo" {
continue;
}
let operstate_path = entry.path().join("operstate");
let is_up = std::fs::read_to_string(&operstate_path)
.map(|s| s.trim() == "up")
.unwrap_or(false);
if !is_up {
continue;
}
let Some(ip) = android_ipv4_for_interface(&iface_name) else {
continue;
};
if ip.is_loopback() || ip.is_unspecified() {
continue;
}
let ip_str = ip.to_string();
if seen.insert((iface_name.clone(), ip_str.clone())) {
addresses.push(NetworkAddress {
interface: iface_name,
ip: ip_str,
});
}
}
addresses
}
#[cfg(target_os = "android")]
fn android_ipv4_for_interface(iface_name: &str) -> Option<std::net::Ipv4Addr> {
use std::ffi::CString;
use std::mem::{size_of, zeroed};
let name = CString::new(iface_name).ok()?;
if name.as_bytes().len() >= libc::IFNAMSIZ {
return None;
}
unsafe {
let fd = libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0);
if fd < 0 {
return None;
}
let mut request: libc::ifreq = zeroed();
std::ptr::copy_nonoverlapping(
name.as_ptr(),
request.ifr_name.as_mut_ptr(),
name.as_bytes_with_nul().len(),
);
let request_code = libc::SIOCGIFADDR.try_into().ok()?;
let result = libc::ioctl(fd, request_code, &mut request);
libc::close(fd);
if result < 0 {
return None;
}
let sockaddr = request.ifr_ifru.ifru_addr;
if sockaddr.sa_family as libc::c_int != libc::AF_INET {
return None;
}
let mut storage = [0u8; size_of::<libc::sockaddr_in>()];
std::ptr::copy_nonoverlapping(
&sockaddr as *const libc::sockaddr as *const u8,
storage.as_mut_ptr(),
size_of::<libc::sockaddr>(),
);
let sockaddr_in = &*(storage.as_ptr() as *const libc::sockaddr_in);
Some(std::net::Ipv4Addr::from(u32::from_be(
sockaddr_in.sin_addr.s_addr,
)))
}
}
#[cfg(test)]
mod tests {
use super::{parse_cpu_model_from_cpuinfo_content, parse_device_tree_model_bytes};

View File

@@ -1,68 +1,65 @@
//! Core library for One-KVM (IPKVM: capture, HID, OTG, streaming, Web UI glue).
#[cfg(not(any(feature = "android", unix, windows)))]
#[cfg(not(any(target_os = "linux", windows)))]
compile_error!("One-KVM supports Linux and Windows targets only.");
#[cfg(any(feature = "android", feature = "desktop"))]
pub mod runtime;
#[cfg(any(feature = "android", feature = "desktop"))]
#[cfg(feature = "desktop")]
pub mod atx;
#[cfg(any(feature = "android", feature = "desktop"))]
#[cfg(feature = "desktop")]
pub mod audio;
#[cfg(any(feature = "android", feature = "desktop"))]
#[cfg(feature = "desktop")]
pub mod auth;
#[cfg(any(feature = "android", feature = "desktop"))]
#[cfg(feature = "desktop")]
pub mod computer_use;
#[cfg(any(feature = "android", feature = "desktop"))]
#[cfg(feature = "desktop")]
pub mod config;
#[cfg(any(feature = "android", feature = "desktop"))]
#[cfg(feature = "desktop")]
pub mod db;
#[cfg(any(feature = "android", feature = "desktop"))]
#[cfg(feature = "desktop")]
pub mod diagnostics;
#[cfg(any(feature = "android", feature = "desktop"))]
#[cfg(feature = "desktop")]
pub mod error;
#[cfg(any(feature = "android", feature = "desktop"))]
#[cfg(feature = "desktop")]
pub mod events;
#[cfg(any(feature = "android", feature = "desktop"))]
#[cfg(feature = "desktop")]
pub mod extensions;
#[cfg(any(feature = "android", feature = "desktop"))]
#[cfg(feature = "desktop")]
pub mod hid;
#[cfg(all(unix, any(feature = "android", feature = "desktop")))]
#[cfg(all(unix, feature = "desktop"))]
pub mod msd;
#[cfg(all(unix, any(feature = "android", feature = "desktop")))]
#[cfg(all(unix, feature = "desktop"))]
pub mod otg;
#[cfg(any(feature = "android", feature = "desktop"))]
#[cfg(feature = "desktop")]
pub mod platform;
#[cfg(any(feature = "android", feature = "desktop"))]
#[cfg(feature = "desktop")]
pub mod redfish;
#[cfg(any(feature = "android", feature = "desktop"))]
#[cfg(feature = "desktop")]
pub mod rtsp;
#[cfg(any(feature = "android", feature = "desktop"))]
#[cfg(feature = "desktop")]
pub mod rustdesk;
#[cfg(any(feature = "android", feature = "desktop"))]
#[cfg(feature = "desktop")]
pub mod state;
#[cfg(any(feature = "android", feature = "desktop"))]
#[cfg(feature = "desktop")]
pub mod stream;
#[cfg(any(feature = "android", feature = "desktop"))]
#[cfg(feature = "desktop")]
pub mod stream_encoder;
#[cfg(any(feature = "android", feature = "desktop"))]
#[cfg(feature = "desktop")]
pub mod update;
#[cfg(any(feature = "android", feature = "desktop"))]
#[cfg(feature = "desktop")]
pub mod utils;
#[cfg(any(feature = "android", feature = "desktop"))]
#[cfg(feature = "desktop")]
pub mod video;
#[cfg(any(feature = "android", feature = "desktop"))]
#[cfg(feature = "desktop")]
pub mod vnc;
#[cfg(any(feature = "android", feature = "desktop"))]
#[cfg(feature = "desktop")]
pub mod web;
#[cfg(any(feature = "android", feature = "desktop"))]
#[cfg(feature = "desktop")]
pub mod webrtc;
#[cfg(any(feature = "android", feature = "desktop"))]
#[cfg(feature = "desktop")]
pub mod secrets {
include!(concat!(env!("OUT_DIR"), "/secrets_generated.rs"));
}
#[cfg(any(feature = "android", feature = "desktop"))]
#[cfg(feature = "desktop")]
pub use error::{AppError, Result};

View File

@@ -65,7 +65,12 @@ struct CliArgs {
address: Option<String>,
/// HTTP port (overrides database config)
#[arg(short = 'p', long, value_name = "PORT")]
#[arg(
short = 'p',
long = "port",
visible_alias = "http-port",
value_name = "PORT"
)]
http_port: Option<u16>,
/// HTTPS port (overrides database config)
@@ -84,7 +89,7 @@ struct CliArgs {
#[arg(long, value_name = "FILE", requires = "ssl_cert")]
ssl_key: Option<PathBuf>,
/// Data directory path (default: /etc/one-kvm, or the executable directory on Windows)
/// Data directory path
#[arg(short = 'd', long, value_name = "DIR")]
data_dir: Option<PathBuf>,
@@ -668,9 +673,11 @@ async fn main() -> anyhow::Result<()> {
let mut shutdown_rx = shutdown_tx.subscribe();
async move {
tokio::select! {
result = tokio::signal::ctrl_c() => {
result.expect("Failed to install CTRL+C handler");
tracing::info!("Shutdown signal received");
result = shutdown_signal() => {
if let Err(e) = result {
tracing::error!("Failed while waiting for shutdown signal: {}", e);
}
tracing::info!("SIGINT or SIGTERM received");
ShutdownAction::Exit
}
request = shutdown_rx.recv() => {
@@ -794,6 +801,24 @@ fn get_data_dir() -> PathBuf {
PathBuf::from("/etc/one-kvm")
}
#[cfg(unix)]
async fn shutdown_signal() -> anyhow::Result<()> {
use tokio::signal::unix::{signal, SignalKind};
let mut terminate = signal(SignalKind::terminate())?;
tokio::select! {
result = tokio::signal::ctrl_c() => result?,
_ = terminate.recv() => {},
}
Ok(())
}
#[cfg(not(unix))]
async fn shutdown_signal() -> anyhow::Result<()> {
tokio::signal::ctrl_c().await?;
Ok(())
}
async fn open_database_pool(data_dir: &Path) -> anyhow::Result<DatabasePool> {
let db_path = data_dir.join("one-kvm.db");
let db = DatabasePool::new(&db_path).await?;
@@ -1207,6 +1232,11 @@ async fn cleanup(state: &Arc<AppState>) {
}
}
#[cfg(unix)]
if let Err(e) = state.otg_service.shutdown().await {
tracing::warn!("Failed to shutdown OTG: {}", e);
}
if let Some(atx) = state.atx.write().await.as_mut() {
if let Err(e) = atx.shutdown().await {
tracing::warn!("Failed to shutdown ATX: {}", e);

View File

@@ -114,6 +114,28 @@ impl MsdController {
}
pub async fn mount_image(&self, image: &ImageInfo, cdrom: bool, read_only: bool) -> Result<()> {
self.mount_image_in_slot(image, cdrom, read_only, None)
.await
}
pub async fn mount_image_at_lun(
&self,
image: &ImageInfo,
cdrom: bool,
read_only: bool,
lun: u8,
) -> Result<()> {
self.mount_image_in_slot(image, cdrom, read_only, Some(lun))
.await
}
async fn mount_image_in_slot(
&self,
image: &ImageInfo,
cdrom: bool,
read_only: bool,
requested_lun: Option<u8>,
) -> Result<()> {
let _op_guard = self.operation_lock.write().await;
let mut state = self.state.write().await;
let previous_state = state.clone();
@@ -136,8 +158,7 @@ impl MsdController {
return Err(AppError::BadRequest("Image is already mounted".to_string()));
}
let lun = Self::lowest_free_lun(&state)
.ok_or_else(|| AppError::BadRequest("Media slots are full".to_string()))?;
let lun = Self::select_lun(&state, requested_lun)?;
let media = MountedMedia::image(lun, image, cdrom, read_only);
if let Err(e) = self.configure_media(&media).await {
@@ -250,6 +271,27 @@ impl MsdController {
.find(|lun| !state.mounted_media.iter().any(|media| media.lun == *lun))
}
fn select_lun(state: &MsdState, requested_lun: Option<u8>) -> Result<u8> {
let Some(lun) = requested_lun else {
return Self::lowest_free_lun(state)
.ok_or_else(|| AppError::BadRequest("Media slots are full".to_string()));
};
if lun >= state.disk_mode.capacity() {
return Err(AppError::BadRequest(format!(
"Media slot {} is outside the current disk mode capacity",
lun + 1
)));
}
if state.mounted_media.iter().any(|media| media.lun == lun) {
return Err(AppError::BadRequest(format!(
"Media slot {} is already occupied",
lun + 1
)));
}
Ok(lun)
}
fn reset_mounts_for_mode(state: &mut MsdState, disk_mode: DiskMode) {
state.disk_mode = disk_mode;
state.mounted_media.clear();
@@ -326,14 +368,20 @@ impl MsdController {
pub async fn unmount_image(&self, image_id: &str) -> Result<()> {
self.unmount_media(|media| media.kind == MountedMediaKind::Image && media.id == image_id)
.await
.map(|_| ())
}
pub async fn unmount_drive(&self) -> Result<()> {
self.unmount_media(|media| media.kind == MountedMediaKind::Drive)
.await
.map(|_| ())
}
async fn unmount_media<F>(&self, predicate: F) -> Result<()>
pub async fn unmount_lun(&self, lun: u8) -> Result<bool> {
self.unmount_media(|media| media.lun == lun).await
}
async fn unmount_media<F>(&self, predicate: F) -> Result<bool>
where
F: Fn(&MountedMedia) -> bool,
{
@@ -342,7 +390,7 @@ impl MsdController {
let mut state = self.state.write().await;
let Some(index) = state.mounted_media.iter().position(predicate) else {
debug!("Requested media was not mounted, skipping unmount");
return Ok(());
return Ok(false);
};
let media = state.mounted_media[index].clone();
@@ -355,7 +403,7 @@ impl MsdController {
self.mark_device_info_dirty().await;
Ok(())
Ok(true)
}
async fn configure_media(&self, media: &MountedMedia) -> Result<()> {
@@ -710,6 +758,29 @@ mod tests {
assert_eq!(MsdController::lowest_free_lun(&state), Some(2));
}
#[test]
fn explicit_lun_selection_rejects_occupied_and_out_of_range_slots() {
let temp_dir = TempDir::new().unwrap();
let image_path = temp_dir.path().join("test.img");
std::fs::write(&image_path, b"img").unwrap();
let image = ImageInfo::new("test".into(), "test.img".into(), image_path, 3);
let mut state = MsdState::default();
MsdController::reset_mounts_for_mode(&mut state, DiskMode::Multi);
state
.mounted_media
.push(MountedMedia::image(3, &image, false, true));
assert_eq!(MsdController::select_lun(&state, Some(5)).unwrap(), 5);
assert!(MsdController::select_lun(&state, Some(3))
.unwrap_err()
.to_string()
.contains("already occupied"));
assert!(MsdController::select_lun(&state, Some(8))
.unwrap_err()
.to_string()
.contains("outside"));
}
#[test]
fn multi_disk_mode_supports_eight_images_and_rejects_ninth_slot() {
let temp_dir = TempDir::new().unwrap();

View File

@@ -13,7 +13,11 @@ pub const DEFAULT_USB_BCD_DEVICE: u16 = 0x0100;
pub const USB_BCD_USB: u16 = 0x0200;
pub fn is_configfs_available() -> bool {
Path::new(CONFIGFS_PATH).exists()
configfs_path().exists()
}
pub fn configfs_path() -> &'static Path {
Path::new(CONFIGFS_PATH)
}
/// Loads `libcomposite` if needed; does not mount configfs.

View File

@@ -3,8 +3,8 @@ use std::path::PathBuf;
use tracing::{debug, error, info, warn};
use super::configfs::{
create_dir, create_symlink, find_udc, is_configfs_available, remove_dir, remove_file,
write_file, CONFIGFS_PATH, DEFAULT_GADGET_NAME, DEFAULT_USB_BCD_DEVICE, DEFAULT_USB_PRODUCT_ID,
configfs_path, create_dir, create_symlink, find_udc, is_configfs_available, remove_dir,
remove_file, write_file, DEFAULT_GADGET_NAME, DEFAULT_USB_BCD_DEVICE, DEFAULT_USB_PRODUCT_ID,
DEFAULT_USB_VENDOR_ID, USB_BCD_USB,
};
use super::endpoint::{EndpointAllocator, DEFAULT_MAX_ENDPOINTS};
@@ -65,7 +65,7 @@ impl OtgGadgetManager {
max_endpoints: u8,
descriptor: GadgetDescriptor,
) -> Self {
let gadget_path = PathBuf::from(CONFIGFS_PATH).join(gadget_name);
let gadget_path = configfs_path().join(gadget_name);
let config_path = gadget_path.join("configs/c.1");
Self {
@@ -166,9 +166,10 @@ impl OtgGadgetManager {
debug!("Setting up OTG USB Gadget: {}", self.gadget_name);
if !Self::is_available() {
return Err(AppError::Internal(
"ConfigFS not available. Is it mounted at /sys/kernel/config?".to_string(),
));
return Err(AppError::Internal(format!(
"ConfigFS not available at {}",
configfs_path().display()
)));
}
if self.gadget_exists() {

View File

@@ -286,7 +286,10 @@ pub fn run(config: &crate::config::AppConfig) -> OtgSelfCheckResponse {
);
}
let gadget_root = std::path::Path::new("/sys/kernel/config/usb_gadget");
let gadget_root = crate::otg::configfs::configfs_path();
let configfs_mount = gadget_root
.parent()
.unwrap_or_else(|| std::path::Path::new("/sys/kernel/config"));
let configfs_mounted = std::fs::read_to_string("/proc/mounts")
.ok()
.map(|mounts| {
@@ -295,7 +298,7 @@ pub fn run(config: &crate::config::AppConfig) -> OtgSelfCheckResponse {
let _src = parts.next();
let mount_point = parts.next();
let fs_type = parts.next();
mount_point == Some("/sys/kernel/config") && fs_type == Some("configfs")
mount_point == configfs_mount.to_str() && fs_type == Some("configfs")
})
})
.unwrap_or(false);
@@ -310,7 +313,7 @@ pub fn run(config: &crate::config::AppConfig) -> OtgSelfCheckResponse {
OtgSelfCheckLevel::Info,
"Check configfs mount status",
None::<String>,
Some("/sys/kernel/config"),
Some(configfs_mount.display().to_string()),
);
} else {
gadget_config_ok = false;
@@ -320,8 +323,11 @@ pub fn run(config: &crate::config::AppConfig) -> OtgSelfCheckResponse {
false,
OtgSelfCheckLevel::Error,
"Check configfs mount status",
Some("Try: mount -t configfs none /sys/kernel/config"),
Some("/sys/kernel/config"),
Some(format!(
"Try: mount -t configfs none {}",
configfs_mount.display()
)),
Some(configfs_mount.display().to_string()),
);
}
@@ -331,9 +337,9 @@ pub fn run(config: &crate::config::AppConfig) -> OtgSelfCheckResponse {
"usb_gadget_dir_exists",
true,
OtgSelfCheckLevel::Info,
"Check /sys/kernel/config/usb_gadget access",
format!("Check {} access", gadget_root.display()),
None::<String>,
Some("/sys/kernel/config/usb_gadget"),
Some(gadget_root.display().to_string()),
);
} else {
gadget_config_ok = false;
@@ -342,9 +348,9 @@ pub fn run(config: &crate::config::AppConfig) -> OtgSelfCheckResponse {
"usb_gadget_dir_exists",
false,
OtgSelfCheckLevel::Error,
"Check /sys/kernel/config/usb_gadget access",
format!("Check {} access", gadget_root.display()),
Some("Ensure configfs and USB gadget support are enabled"),
Some("/sys/kernel/config/usb_gadget"),
Some(gadget_root.display().to_string()),
);
}
@@ -426,7 +432,7 @@ pub fn run(config: &crate::config::AppConfig) -> OtgSelfCheckResponse {
OtgSelfCheckLevel::Info,
"Check for other gadget services",
None::<String>,
Some("/sys/kernel/config/usb_gadget"),
Some(gadget_root.display().to_string()),
);
} else {
push_otg_check(
@@ -436,7 +442,7 @@ pub fn run(config: &crate::config::AppConfig) -> OtgSelfCheckResponse {
OtgSelfCheckLevel::Warn,
"Check for other gadget services",
Some("Potential UDC contention with one-kvm; check other OTG services"),
Some("/sys/kernel/config/usb_gadget"),
Some(gadget_root.display().to_string()),
);
}
@@ -621,7 +627,7 @@ pub fn run(config: &crate::config::AppConfig) -> OtgSelfCheckResponse {
OtgSelfCheckLevel::Info,
"Check UDC binding conflicts",
None::<String>,
Some("/sys/kernel/config/usb_gadget/*/UDC"),
Some(format!("{}/*/UDC", gadget_root.display())),
);
} else {
push_otg_check(
@@ -631,7 +637,7 @@ pub fn run(config: &crate::config::AppConfig) -> OtgSelfCheckResponse {
OtgSelfCheckLevel::Error,
"Check UDC binding conflicts",
Some("Stop other OTG services or switch one-kvm to an idle UDC"),
Some("/sys/kernel/config/usb_gadget/*/UDC"),
Some(format!("{}/*/UDC", gadget_root.display())),
);
}
}

View File

@@ -1,44 +0,0 @@
//! Android Amlogic platform capabilities.
use super::{FeatureCapability, PlatformCapabilities, PlatformMode};
#[cfg(feature = "android")]
#[allow(dead_code)]
fn _keep_android_bionic_ifaddrs_shim_linked() {
let _ = crate::platform::android_bionic::freeifaddrs
as unsafe extern "C" fn(*mut crate::platform::android_bionic::ifaddrs);
let _ = crate::platform::android_bionic::getifaddrs
as unsafe extern "C" fn(*mut *mut crate::platform::android_bionic::ifaddrs) -> i32;
}
pub fn capabilities() -> PlatformCapabilities {
#[cfg(feature = "android")]
_keep_android_bionic_ifaddrs_shim_linked();
PlatformCapabilities {
mode: PlatformMode::AndroidAmlogic,
mode_label: PlatformMode::AndroidAmlogic.label(),
video_capture: FeatureCapability::available(["v4l2_uvc"])
.with_selected_backend(Some("v4l2_uvc".to_string())),
encoder: FeatureCapability::available(["ffmpeg_mediacodec_h264", "mjpeg"])
.with_selected_backend(Some(
if cfg!(feature = "android-mediacodec") {
"ffmpeg_mediacodec_h264"
} else {
"mjpeg"
}
.to_string(),
)),
hid: FeatureCapability::available(["otg_configfs", "ch9329", "none"]),
atx: FeatureCapability::available(["gpio", "usb_relay", "serial", "wol", "none"]),
msd: FeatureCapability::available(["otg_configfs"]),
otg: FeatureCapability::available(["configfs"]),
audio: FeatureCapability::available(["alsa", "opus"])
.with_selected_backend(Some("alsa".to_string())),
rustdesk: FeatureCapability::available(["builtin"]),
vnc: FeatureCapability::available(["builtin", "tight_jpeg", "h264"]),
diagnostics: FeatureCapability::available(["android_linux"]),
extensions: FeatureCapability::unsupported("unsupported on Android Amlogic v1"),
service_installation: FeatureCapability::available(["android_foreground_service"]),
}
}

View File

@@ -1,175 +0,0 @@
#![allow(clippy::missing_safety_doc)]
use std::ffi::CString;
use std::mem::{size_of, zeroed};
use std::os::raw::{c_char, c_int, c_uint, c_void};
#[repr(C)]
pub struct ifaddrs {
pub ifa_next: *mut ifaddrs,
pub ifa_name: *mut c_char,
pub ifa_flags: c_uint,
pub ifa_addr: *mut libc::sockaddr,
pub ifa_netmask: *mut libc::sockaddr,
pub ifa_ifu: *mut libc::sockaddr,
pub ifa_data: *mut c_void,
}
#[repr(C)]
struct AddrNode {
ifa: ifaddrs,
name: CString,
addr: libc::sockaddr_in,
next: *mut AddrNode,
}
fn sockaddr_to_ipv4(addr: libc::sockaddr) -> Option<std::net::Ipv4Addr> {
if addr.sa_family as c_int != libc::AF_INET {
return None;
}
unsafe {
let sin = &*(&addr as *const libc::sockaddr as *const libc::sockaddr_in);
Some(std::net::Ipv4Addr::from(u32::from_be(sin.sin_addr.s_addr)))
}
}
fn query_ipv4(iface_name: &str) -> Option<libc::sockaddr_in> {
let name = CString::new(iface_name).ok()?;
if name.as_bytes().len() >= libc::IFNAMSIZ {
return None;
}
unsafe {
let fd = libc::socket(libc::AF_INET, libc::SOCK_DGRAM, 0);
if fd < 0 {
return None;
}
let mut request: libc::ifreq = zeroed();
std::ptr::copy_nonoverlapping(
name.as_ptr(),
request.ifr_name.as_mut_ptr(),
name.as_bytes_with_nul().len(),
);
let request_code = libc::SIOCGIFADDR.try_into().ok()?;
let rc = libc::ioctl(fd, request_code, &mut request);
libc::close(fd);
if rc < 0 {
return None;
}
let addr = request.ifr_ifru.ifru_addr;
if addr.sa_family as c_int != libc::AF_INET {
return None;
}
let mut sin: libc::sockaddr_in = zeroed();
std::ptr::copy_nonoverlapping(
&addr as *const libc::sockaddr as *const u8,
&mut sin as *mut libc::sockaddr_in as *mut u8,
size_of::<libc::sockaddr_in>(),
);
Some(sin)
}
}
#[no_mangle]
pub unsafe extern "C" fn getifaddrs(addrs: *mut *mut ifaddrs) -> c_int {
if addrs.is_null() {
return -1;
}
*addrs = std::ptr::null_mut();
let net_dir = match std::fs::read_dir("/sys/class/net") {
Ok(dir) => dir,
Err(_) => return -1,
};
let mut head: *mut AddrNode = std::ptr::null_mut();
let mut tail: *mut AddrNode = std::ptr::null_mut();
for entry in net_dir.flatten() {
let iface_name = match entry.file_name().into_string() {
Ok(name) => name,
Err(_) => continue,
};
if iface_name == "lo" {
continue;
}
let operstate_path = entry.path().join("operstate");
let is_up = std::fs::read_to_string(&operstate_path)
.map(|s| s.trim() == "up")
.unwrap_or(false);
if !is_up {
continue;
}
let Some(addr) = query_ipv4(&iface_name) else {
continue;
};
let ip = sockaddr_to_ipv4(unsafe {
std::mem::transmute::<libc::sockaddr_in, libc::sockaddr>(addr)
});
if ip
.map(|ip| ip.is_loopback() || ip.is_unspecified())
.unwrap_or(true)
{
continue;
}
let name = match CString::new(iface_name) {
Ok(name) => name,
Err(_) => continue,
};
let mut node = Box::new(AddrNode {
ifa: ifaddrs {
ifa_next: std::ptr::null_mut(),
ifa_name: std::ptr::null_mut(),
ifa_flags: 0,
ifa_addr: std::ptr::null_mut(),
ifa_netmask: std::ptr::null_mut(),
ifa_ifu: std::ptr::null_mut(),
ifa_data: std::ptr::null_mut(),
},
name,
addr,
next: std::ptr::null_mut(),
});
node.ifa.ifa_name = node.name.as_ptr() as *mut c_char;
node.ifa.ifa_addr = &mut node.addr as *mut libc::sockaddr_in as *mut libc::sockaddr;
node.ifa.ifa_ifu = std::ptr::null_mut();
node.ifa.ifa_netmask = std::ptr::null_mut();
node.ifa.ifa_flags = (libc::IFF_UP | libc::IFF_RUNNING) as c_uint;
let raw = Box::into_raw(node);
if head.is_null() {
head = raw;
} else {
(*tail).next = raw;
(*tail).ifa.ifa_next = raw as *mut ifaddrs;
}
tail = raw;
}
*addrs = if head.is_null() {
std::ptr::null_mut()
} else {
head as *mut ifaddrs
};
0
}
#[no_mangle]
pub unsafe extern "C" fn freeifaddrs(addrs: *mut ifaddrs) {
let mut current = addrs as *mut AddrNode;
while !current.is_null() {
let next = (*current).next;
drop(Box::from_raw(current));
current = next;
}
}

View File

@@ -5,16 +5,13 @@ use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum PlatformMode {
AndroidAmlogic,
Linux,
Windows,
}
impl PlatformMode {
pub const fn current() -> Self {
if cfg!(feature = "android") {
Self::AndroidAmlogic
} else if cfg!(windows) {
if cfg!(windows) {
Self::Windows
} else {
Self::Linux
@@ -23,7 +20,6 @@ impl PlatformMode {
pub const fn label(self) -> &'static str {
match self {
Self::AndroidAmlogic => "Android Amlogic",
Self::Linux => "Linux",
Self::Windows => "Windows",
}
@@ -86,15 +82,11 @@ pub struct PlatformCapabilities {
impl PlatformCapabilities {
pub fn current() -> Self {
#[cfg(feature = "android")]
{
return crate::platform::android::capabilities();
}
#[cfg(windows)]
{
return crate::platform::windows::capabilities();
}
#[cfg(all(unix, not(feature = "android")))]
#[cfg(target_os = "linux")]
{
return crate::platform::linux::capabilities();
}

View File

@@ -1,72 +1,21 @@
use crate::config::AppConfig;
#[cfg(windows)]
use crate::config::AtxDriverType;
#[cfg(any(windows, all(unix, feature = "android")))]
#[cfg(windows)]
use crate::config::HidBackend;
pub fn apply(config: &mut AppConfig) {
#[cfg(not(any(windows, all(unix, feature = "android"))))]
#[cfg(not(windows))]
{
let _ = config;
}
#[cfg(all(unix, feature = "android"))]
{
apply_android(config);
}
#[cfg(windows)]
{
apply_windows(config);
}
}
#[cfg(all(unix, feature = "android"))]
fn apply_android(config: &mut AppConfig) {
let detected_udc = crate::otg::configfs::find_udc();
if config
.hid
.otg_udc
.as_deref()
.map(str::trim)
.unwrap_or("")
.is_empty()
{
config.hid.otg_udc = detected_udc;
}
let otg_available = config.hid.otg_udc.is_some();
if !config.initialized && otg_available {
config.hid.backend = HidBackend::Otg;
} else if config.hid.backend == HidBackend::Ch9329
&& config.hid.ch9329_port == "/dev/ttyUSB0"
&& !std::path::Path::new(&config.hid.ch9329_port).exists()
&& otg_available
{
config.hid.backend = HidBackend::Otg;
}
if !config.initialized {
config.audio.enabled = false;
config.audio.device.clear();
config.atx.enabled = false;
config.rustdesk.enabled = false;
config.rtsp.enabled = false;
config.redfish.enabled = false;
}
config
.video
.device
.get_or_insert_with(|| "auto".to_string());
config
.video
.format
.get_or_insert_with(|| "MJPEG".to_string());
config.web.bind_address = "0.0.0.0".to_string();
config.web.bind_addresses = vec!["0.0.0.0".to_string()];
}
#[cfg(windows)]
fn apply_windows(config: &mut AppConfig) {
config.msd.enabled = false;

View File

@@ -1,9 +1,5 @@
//! Platform selection and capability reporting.
#[cfg(feature = "android")]
pub mod android;
#[cfg(feature = "android")]
pub mod android_bionic;
pub mod capabilities;
pub mod defaults;
#[cfg(target_os = "linux")]

View File

@@ -4,7 +4,7 @@ mod event;
mod managers;
mod session;
mod systems;
#[cfg(all(unix, not(feature = "android")))]
#[cfg(unix)]
mod virtual_media;
use axum::{
@@ -196,7 +196,7 @@ pub fn create_redfish_router(state: Arc<AppState>) -> Router {
.merge(account::router(state.clone()))
.merge(event::router(state.clone()));
#[cfg(all(unix, not(feature = "android")))]
#[cfg(unix)]
let redfish_routes = redfish_routes.merge(virtual_media::router(state.clone()));
let redfish_routes = redfish_routes.layer(middleware::from_fn_with_state(

View File

@@ -1,789 +0,0 @@
//! Android service runtime.
//!
//! Android is treated as a packaged Linux distribution: the APK/Java layer only
//! starts and stops this runtime, while the Rust side builds the same AppState
//! and Axum router used by the desktop service.
use std::net::{IpAddr, SocketAddr};
use std::path::{Path, PathBuf};
use std::sync::{Arc, Mutex, OnceLock};
use std::thread::JoinHandle;
use std::time::{Duration, Instant};
use rustls::crypto::{ring, CryptoProvider};
use tokio::runtime::Runtime;
use tokio::sync::{broadcast, mpsc, oneshot};
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt};
use crate::atx::AtxController;
use crate::audio::{AudioController, AudioControllerConfig, AudioQuality};
use crate::auth::{SessionStore, UserStore};
use crate::computer_use::ComputerUseManager;
use crate::config::{self, AppConfig, ConfigStore};
use crate::db::DatabasePool;
use crate::events::EventBus;
use crate::extensions::ExtensionManager;
use crate::hid::{HidBackendType, HidController};
use crate::msd::MsdController;
use crate::otg::OtgService;
use crate::rtsp::RtspService;
use crate::rustdesk::RustDeskService;
use crate::state::{AppState, ShutdownAction};
use crate::stream_encoder::encoder_type_to_backend;
use crate::update::UpdateService;
use crate::utils::bind_tcp_listener;
use crate::video::codec_constraints::{
enforce_constraints_with_stream_manager, validate_third_party_codec_compatibility,
StreamCodecConstraints,
};
use crate::video::format::{PixelFormat, Resolution};
use crate::video::{Streamer, VideoStreamManager};
use crate::vnc::VncService;
use crate::web;
use crate::webrtc::{config::WebRtcConfig, WebRtcStreamer, WebRtcStreamerConfig};
#[derive(Debug, Clone)]
pub struct AndroidRuntimeConfig {
pub data_dir: String,
pub bind_address: String,
pub port: u16,
}
struct RuntimeHandle {
stop_tx: oneshot::Sender<()>,
join: JoinHandle<()>,
}
static HANDLE: OnceLock<Mutex<Option<RuntimeHandle>>> = OnceLock::new();
fn handle_slot() -> &'static Mutex<Option<RuntimeHandle>> {
HANDLE.get_or_init(|| Mutex::new(None))
}
pub fn start(config: AndroidRuntimeConfig) -> Result<String, String> {
init_logging();
let mut slot = handle_slot()
.lock()
.map_err(|_| "runtime lock poisoned".to_string())?;
if slot.is_some() {
return Ok(status());
}
let (stop_tx, stop_rx) = oneshot::channel();
let config_for_thread = config.clone();
let join = std::thread::Builder::new()
.name("one-kvm-android-runtime".to_string())
.spawn(move || {
if let Err(err) = run_runtime(config_for_thread, stop_rx) {
tracing::error!("One-KVM Android runtime exited: {}", err);
}
})
.map_err(|err| format!("failed to spawn runtime: {err}"))?;
*slot = Some(RuntimeHandle { stop_tx, join });
Ok(format!(
"One-KVM Android runtime starting on http://{}:{}",
config.bind_address, config.port
))
}
pub fn run_foreground(config: AndroidRuntimeConfig) -> Result<(), String> {
init_logging();
let (_stop_tx, stop_rx) = oneshot::channel();
run_runtime(config, stop_rx)
}
pub fn init_rustls_provider() {
ensure_rustls_provider();
}
pub fn stop() -> String {
let handle = match handle_slot().lock() {
Ok(mut slot) => slot.take(),
Err(_) => return "runtime lock poisoned".to_string(),
};
let Some(handle) = handle else {
return "One-KVM Android runtime is not running".to_string();
};
let _ = handle.stop_tx.send(());
match handle.join.join() {
Ok(()) => "One-KVM Android runtime stopped".to_string(),
Err(_) => "One-KVM Android runtime stopped after panic".to_string(),
}
}
pub fn status() -> String {
match handle_slot().lock() {
Ok(slot) if slot.is_some() => "One-KVM Android runtime running".to_string(),
Ok(_) => "One-KVM Android runtime stopped".to_string(),
Err(_) => "runtime lock poisoned".to_string(),
}
}
fn run_runtime(config: AndroidRuntimeConfig, stop_rx: oneshot::Receiver<()>) -> Result<(), String> {
ensure_rustls_provider();
let runtime = Runtime::new().map_err(|err| format!("failed to create tokio runtime: {err}"))?;
runtime.block_on(async move { run_async(config, stop_rx).await })
}
async fn run_async(
config: AndroidRuntimeConfig,
stop_rx: oneshot::Receiver<()>,
) -> Result<(), String> {
let (db, config_store, app_config) =
load_runtime_config(&PathBuf::from(&config.data_dir), &config).await?;
let (shutdown_tx, _) = broadcast::channel::<ShutdownAction>(1);
let state = build_app_state(
PathBuf::from(&config.data_dir),
db,
config_store,
app_config,
shutdown_tx.clone(),
)
.await?;
let app = web::create_router(state.clone());
let listener = bind_android_listener(&config.bind_address, config.port)?;
let local_addr = listener
.local_addr()
.map_err(|err| format!("failed to get listener address: {err}"))?;
tracing::info!(
"Starting One-KVM desktop router on Android at http://{}",
local_addr
);
let listener = tokio::net::TcpListener::from_std(listener)
.map_err(|err| format!("failed to create tokio listener: {err}"))?;
let server = axum::serve(listener, app);
let shutdown_signal = {
let mut shutdown_rx = shutdown_tx.subscribe();
async move {
tokio::select! {
_ = stop_rx => {
tracing::info!("Android stop request received");
let _ = shutdown_tx.send(ShutdownAction::Exit);
}
request = shutdown_rx.recv() => {
match request {
Ok(action) => {
tracing::info!("Android shutdown request received: {:?}", action);
}
Err(err) => {
tracing::warn!("Android shutdown request channel closed: {}", err);
}
}
}
}
}
};
tokio::select! {
result = server => {
if let Err(err) = result {
tracing::error!("Android HTTP server error: {}", err);
}
}
_ = shutdown_signal => {}
}
cleanup(&state).await;
Ok(())
}
async fn load_runtime_config(
data_dir: &Path,
runtime_config: &AndroidRuntimeConfig,
) -> Result<(DatabasePool, ConfigStore, AppConfig), String> {
tokio::fs::create_dir_all(data_dir)
.await
.map_err(|err| format!("failed to create data dir {}: {err}", data_dir.display()))?;
let db_path = data_dir.join("one-kvm.db");
let db = DatabasePool::new(&db_path)
.await
.map_err(|err| format!("failed to open database {}: {err}", db_path.display()))?;
db.init_schema()
.await
.map_err(|err| format!("failed to initialize database schema: {err}"))?;
let config_store = ConfigStore::new(db.clone_pool())
.map_err(|err| format!("failed to create config store: {err}"))?;
config_store
.load()
.await
.map_err(|err| format!("failed to load config: {err}"))?;
let mut config = (*config_store.get()).clone();
config.apply_platform_defaults();
config.web.bind_address = runtime_config.bind_address.clone();
config.web.bind_addresses = vec![runtime_config.bind_address.clone()];
config.web.http_port = runtime_config.port;
config.web.https_enabled = false;
prepare_android_runtime_dirs(data_dir, &config_store, &mut config).await?;
if let Some(device) = config.video.device.as_deref() {
if device == "auto" {
config.video.device = None;
}
}
config_store
.set(config.clone())
.await
.map_err(|err| format!("failed to persist Android runtime config: {err}"))?;
Ok((db, config_store, config))
}
async fn prepare_android_runtime_dirs(
data_dir: &Path,
config_store: &ConfigStore,
config: &mut AppConfig,
) -> Result<(), String> {
let mut updated = false;
if config.msd.msd_dir.trim().is_empty() {
config.msd.msd_dir = data_dir.join("msd").to_string_lossy().to_string();
updated = true;
} else if !PathBuf::from(&config.msd.msd_dir).is_absolute() {
config.msd.msd_dir = data_dir
.join(&config.msd.msd_dir)
.to_string_lossy()
.to_string();
updated = true;
}
let msd_dir = config.msd.msd_dir_path();
tokio::fs::create_dir_all(msd_dir.join("images"))
.await
.map_err(|err| format!("failed to create Android MSD images dir: {err}"))?;
tokio::fs::create_dir_all(msd_dir.join("ventoy"))
.await
.map_err(|err| format!("failed to create Android MSD ventoy dir: {err}"))?;
if updated {
config_store
.set(config.clone())
.await
.map_err(|err| format!("failed to persist Android MSD dir: {err}"))?;
}
Ok(())
}
#[allow(clippy::too_many_lines)]
async fn build_app_state(
data_dir: PathBuf,
db: DatabasePool,
config_store: ConfigStore,
config: AppConfig,
shutdown_tx: broadcast::Sender<ShutdownAction>,
) -> Result<Arc<AppState>, String> {
let session_store = SessionStore::new(config.auth.session_timeout_secs as i64);
let user_store = UserStore::new(db.clone_pool());
let events = Arc::new(EventBus::new());
let (video_format, video_resolution) = parse_video_config(&config);
let streamer = Streamer::new();
streamer.set_event_bus(events.clone()).await;
if let Some(ref device_path) = config.video.device {
if let Err(err) = streamer
.apply_video_config(
device_path,
video_format,
video_resolution,
config.video.fps,
)
.await
{
tracing::warn!("Android video config failed, falling back to auto: {}", err);
}
}
let webrtc_streamer = WebRtcStreamer::with_config(WebRtcStreamerConfig {
resolution: video_resolution,
input_format: video_format,
fps: config.video.fps,
bitrate_preset: config.stream.bitrate_preset,
encoder_backend: encoder_type_to_backend(config.stream.encoder.clone()),
webrtc: build_webrtc_config(&config),
..Default::default()
});
let hid_backend = match config.hid.backend {
config::HidBackend::Otg => HidBackendType::Otg,
config::HidBackend::Ch9329 => HidBackendType::Ch9329 {
port: config.hid.ch9329_port.clone(),
baud_rate: config.hid.ch9329_baudrate,
hybrid_mouse: config.hid.ch9329_hybrid_mouse,
},
config::HidBackend::None => HidBackendType::None,
};
let otg_service = Arc::new(OtgService::new());
if let Err(err) = otg_service.apply_config(&config.hid, &config.msd).await {
tracing::warn!("Failed to apply Android OTG config: {}", err);
}
let hid = Arc::new(HidController::new(hid_backend, Some(otg_service.clone())));
hid.set_event_bus(events.clone()).await;
if let Err(err) = hid.init().await {
tracing::warn!("Failed to initialize Android HID backend: {}", err);
}
let msd = if config.msd.enabled {
let ventoy_resource_dir = data_dir.join("ventoy");
if ventoy_resource_dir.exists() {
if let Err(err) = ventoy_img::init_resources(&ventoy_resource_dir) {
tracing::warn!("Failed to initialize Android Ventoy resources: {}", err);
}
}
let controller = MsdController::new(otg_service.clone(), config.msd.msd_dir_path());
if let Err(err) = controller.init().await {
tracing::warn!("Failed to initialize Android MSD controller: {}", err);
None
} else {
controller.set_event_bus(events.clone()).await;
Some(controller)
}
} else {
None
};
let atx = if config.atx.enabled {
let controller = AtxController::new(config.atx.to_controller_config());
if let Err(err) = controller.init().await {
tracing::warn!("Failed to initialize Android ATX controller: {}", err);
None
} else {
Some(controller)
}
} else {
None
};
let audio = {
let audio_config = AudioControllerConfig {
enabled: config.audio.enabled,
device: config.audio.device.clone(),
quality: config
.audio
.quality
.parse::<AudioQuality>()
.unwrap_or(AudioQuality::Balanced),
};
let controller = AudioController::new(audio_config);
controller.set_event_bus(events.clone()).await;
if config.audio.enabled {
if let Err(err) = controller.start_streaming().await {
tracing::warn!("Failed to start Android audio: {}", err);
}
}
Arc::new(controller)
};
let extensions = Arc::new(ExtensionManager::new());
webrtc_streamer.set_hid_controller(hid.clone()).await;
webrtc_streamer.set_audio_controller(audio.clone()).await;
let (device_path, actual_resolution, actual_format, actual_fps, jpeg_quality) =
streamer.current_capture_config().await;
webrtc_streamer
.update_video_config(actual_resolution, actual_format, actual_fps)
.await;
if let Some(device_path) = device_path {
let (subdev_path, bridge_kind, v4l2_driver) = streamer
.current_device()
.await
.map(|device| {
(
device.subdev_path.clone(),
device.bridge_kind.clone(),
Some(device.driver.clone()),
)
})
.unwrap_or((None, None, None));
webrtc_streamer
.set_capture_device(
device_path,
jpeg_quality,
subdev_path,
bridge_kind,
v4l2_driver,
)
.await;
}
let stream_manager = VideoStreamManager::with_webrtc_streamer(
streamer.clone(),
webrtc_streamer.clone() as Arc<dyn crate::video::traits::VideoOutput>,
);
stream_manager.set_event_bus(events.clone()).await;
stream_manager.set_config_store(config_store.clone()).await;
{
let stream_manager_weak = Arc::downgrade(&stream_manager);
audio
.set_recovered_callback(Arc::new(move || {
if let Some(stream_manager) = stream_manager_weak.upgrade() {
tokio::spawn(async move {
stream_manager.reconnect_webrtc_audio_sources().await;
});
}
}))
.await;
}
if let Err(err) = stream_manager
.init_with_mode(config.stream.mode.clone())
.await
{
tracing::warn!("Failed to initialize Android stream manager: {}", err);
}
let third_party_codec_config_valid = match validate_third_party_codec_compatibility(&config) {
Ok(()) => true,
Err(e) => {
tracing::warn!(
"Android third-party access codec configuration is invalid; RustDesk/VNC/RTSP will not start: {}",
e
);
false
}
};
let rustdesk = if third_party_codec_config_valid && config.rustdesk.is_valid() {
Some(Arc::new(RustDeskService::new(
config.rustdesk.clone(),
stream_manager.clone(),
hid.clone(),
audio.clone(),
)))
} else {
None
};
let rtsp = if third_party_codec_config_valid && config.rtsp.enabled {
Some(Arc::new(RtspService::new(
config.rtsp.clone(),
stream_manager.clone(),
)))
} else {
None
};
let vnc = if third_party_codec_config_valid && config.vnc.enabled {
Some(Arc::new(VncService::new(
config.vnc.clone(),
stream_manager.clone(),
hid.clone(),
)))
} else {
None
};
let update_service = Arc::new(UpdateService::new(data_dir.join("updates")));
let computer_use = ComputerUseManager::new(config_store.clone(), hid.clone());
let state = AppState::new(
db,
config_store.clone(),
session_store,
user_store,
otg_service,
stream_manager,
webrtc_streamer,
hid,
computer_use,
msd,
atx,
audio,
rustdesk.clone(),
vnc.clone(),
rtsp.clone(),
extensions.clone(),
events.clone(),
update_service,
shutdown_tx,
data_dir,
);
extensions.set_event_bus(events.clone()).await;
if let Some(service) = rustdesk {
if let Err(err) = service.start().await {
tracing::warn!("Failed to start Android RustDesk service: {}", err);
}
}
if let Some(service) = vnc {
if let Err(err) = service.start().await {
tracing::warn!("Failed to start Android VNC service: {}", err);
}
}
if let Some(service) = rtsp {
if let Err(err) = service.start().await {
tracing::warn!("Failed to start Android RTSP service: {}", err);
}
}
let constraints = StreamCodecConstraints::from_config(&state.config.get());
if let Err(err) =
enforce_constraints_with_stream_manager(&state.stream_manager, &constraints).await
{
tracing::warn!("Failed to enforce Android stream constraints: {}", err);
}
state.publish_device_info().await;
spawn_device_info_broadcaster(state.clone(), events);
Ok(state)
}
fn build_webrtc_config(config: &AppConfig) -> WebRtcConfig {
let mut webrtc = WebRtcConfig::default();
if let Some(stun) = config
.stream
.stun_server
.as_ref()
.filter(|value| !value.is_empty())
{
webrtc.stun_servers.push(stun.clone());
}
if let Some(turn) = config
.stream
.turn_server
.as_ref()
.filter(|value| !value.is_empty())
{
webrtc
.turn_servers
.push(crate::webrtc::config::TurnServer::new(
turn.clone(),
config.stream.turn_username.clone().unwrap_or_default(),
config.stream.turn_password.clone().unwrap_or_default(),
));
}
webrtc
}
fn parse_video_config(config: &AppConfig) -> (PixelFormat, Resolution) {
let format = config
.video
.format
.as_ref()
.and_then(|value| value.parse::<PixelFormat>().ok())
.unwrap_or(PixelFormat::Mjpeg);
(
format,
Resolution::new(config.video.width, config.video.height),
)
}
fn bind_android_listener(bind_address: &str, port: u16) -> Result<std::net::TcpListener, String> {
let ip = bind_address
.parse::<IpAddr>()
.map_err(|err| format!("invalid Android bind address {bind_address}: {err}"))?;
bind_tcp_listener(SocketAddr::new(ip, port))
.map_err(|err| format!("failed to bind Android listener {bind_address}:{port}: {err}"))
}
fn spawn_device_info_broadcaster(state: Arc<AppState>, events: Arc<EventBus>) {
enum DeviceInfoTrigger {
Event,
Lagged { topic: &'static str, count: u64 },
}
const DEVICE_INFO_TOPICS: &[&str] = &[
"stream.state_changed",
"stream.config_applied",
"stream.mode_ready",
];
const DEBOUNCE_MS: u64 = 100;
let (trigger_tx, mut trigger_rx) = mpsc::unbounded_channel();
for topic in DEVICE_INFO_TOPICS {
let Some(mut rx) = events.subscribe_topic(topic) else {
continue;
};
let trigger_tx = trigger_tx.clone();
let topic_name = *topic;
tokio::spawn(async move {
loop {
match rx.recv().await {
Ok(_) => {
if trigger_tx.send(DeviceInfoTrigger::Event).is_err() {
break;
}
}
Err(tokio::sync::broadcast::error::RecvError::Lagged(count)) => {
if trigger_tx
.send(DeviceInfoTrigger::Lagged {
topic: topic_name,
count,
})
.is_err()
{
break;
}
}
Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
}
}
});
}
{
let mut dirty_rx = events.subscribe_device_info_dirty();
let trigger_tx = trigger_tx.clone();
tokio::spawn(async move {
loop {
match dirty_rx.recv().await {
Ok(()) => {
if trigger_tx.send(DeviceInfoTrigger::Event).is_err() {
break;
}
}
Err(tokio::sync::broadcast::error::RecvError::Lagged(count)) => {
if trigger_tx
.send(DeviceInfoTrigger::Lagged {
topic: "device_info_dirty",
count,
})
.is_err()
{
break;
}
}
Err(tokio::sync::broadcast::error::RecvError::Closed) => break,
}
}
});
}
tokio::spawn(async move {
let mut last_broadcast = Instant::now() - Duration::from_millis(DEBOUNCE_MS);
let mut pending_broadcast = false;
loop {
let recv_result = if pending_broadcast {
let remaining =
DEBOUNCE_MS.saturating_sub(last_broadcast.elapsed().as_millis() as u64);
tokio::time::timeout(Duration::from_millis(remaining), trigger_rx.recv()).await
} else {
Ok(trigger_rx.recv().await)
};
match recv_result {
Ok(Some(DeviceInfoTrigger::Event)) => pending_broadcast = true,
Ok(Some(DeviceInfoTrigger::Lagged { topic, count })) => {
tracing::warn!(
"Android device info broadcaster lagged by {} events on {}",
count,
topic
);
pending_broadcast = true;
}
Ok(None) => break,
Err(_) => {}
}
if pending_broadcast && last_broadcast.elapsed() >= Duration::from_millis(DEBOUNCE_MS) {
state.publish_device_info().await;
last_broadcast = Instant::now();
pending_broadcast = false;
}
}
});
}
async fn cleanup(state: &Arc<AppState>) {
state.extensions.stop_all().await;
if let Some(service) = state.rustdesk.read().await.as_ref() {
if let Err(err) = service.stop().await {
tracing::warn!("Failed to stop Android RustDesk service: {}", err);
}
}
if let Some(service) = state.vnc.read().await.as_ref() {
if let Err(err) = service.stop().await {
tracing::warn!("Failed to stop Android VNC service: {}", err);
}
}
if let Some(service) = state.rtsp.read().await.as_ref() {
if let Err(err) = service.stop().await {
tracing::warn!("Failed to stop Android RTSP service: {}", err);
}
}
if let Err(err) = state.stream_manager.stop().await {
tracing::warn!("Failed to stop Android stream manager: {}", err);
}
if let Err(err) = state.hid.shutdown().await {
tracing::warn!("Failed to stop Android HID: {}", err);
}
if let Some(msd) = state.msd.write().await.as_mut() {
if let Err(err) = msd.shutdown().await {
tracing::warn!("Failed to stop Android MSD: {}", err);
}
}
if let Err(err) = state.otg_service.shutdown().await {
tracing::warn!("Failed to stop Android OTG: {}", err);
}
if let Some(atx) = state.atx.write().await.as_mut() {
if let Err(err) = atx.shutdown().await {
tracing::warn!("Failed to stop Android ATX: {}", err);
}
}
if let Err(err) = state.audio.shutdown().await {
tracing::warn!("Failed to stop Android audio: {}", err);
}
}
fn init_logging() {
static INIT: OnceLock<()> = OnceLock::new();
INIT.get_or_init(|| {
let _ = tracing_log::LogTracer::init();
let filter = tracing_subscriber::EnvFilter::try_from_default_env()
.unwrap_or_else(|_| "one_kvm=info,tower_http=info,webrtc_sctp=warn".into());
let fmt_layer = tracing_subscriber::fmt::layer();
if let Ok(path) = std::env::var("ONE_KVM_ANDROID_LOG_FILE") {
match std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
{
Ok(file) => {
let file_layer = tracing_subscriber::fmt::layer()
.with_ansi(false)
.with_writer(Arc::new(file));
let _ = tracing_subscriber::registry()
.with(filter)
.with(fmt_layer)
.with(file_layer)
.try_init();
}
Err(err) => {
eprintln!("failed to open Android Rust log file {path}: {err}");
let _ = tracing_subscriber::registry()
.with(filter)
.with(fmt_layer)
.try_init();
}
}
} else {
let _ = tracing_subscriber::registry()
.with(filter)
.with(fmt_layer)
.try_init();
}
});
}
fn ensure_rustls_provider() {
static INIT: OnceLock<()> = OnceLock::new();
INIT.get_or_init(|| {
let _ = CryptoProvider::install_default(ring::default_provider());
});
}

View File

@@ -1,4 +0,0 @@
//! Runtime entry points for packaged service modes.
#[cfg(feature = "android")]
pub mod android;

View File

@@ -175,9 +175,7 @@ impl MjpegStreamHandler {
}
#[cfg(not(feature = "desktop"))]
{
warn!(
"Dropping non-JPEG frame for MJPEG stream on Android; native encoder is not wired yet"
);
warn!("Dropping non-JPEG frame because this build has no JPEG encoder");
return;
}
} else {

View File

@@ -2,9 +2,9 @@
pub mod fs;
pub mod host;
#[cfg(all(unix, not(target_os = "android")))]
#[cfg(unix)]
pub mod net;
#[cfg(any(not(unix), target_os = "android"))]
#[cfg(not(unix))]
#[path = "net_disabled.rs"]
pub mod net;
pub mod serial;

View File

@@ -1,122 +0,0 @@
//! Android FFmpeg/MediaCodec encoder glue.
use bytes::Bytes;
use hwcodec::common::{Quality, RateControl};
use hwcodec::ffmpeg::{resolve_pixel_format, AVPixelFormat};
use hwcodec::ffmpeg_ram::encode::{EncodeContext, Encoder as HwEncoder};
use crate::error::{AppError, Result};
use crate::video::format::{PixelFormat, Resolution};
pub struct AndroidMediaCodecH264Encoder {
inner: HwEncoder,
resolution: Resolution,
input_format: PixelFormat,
bitrate_kbps: u32,
}
impl AndroidMediaCodecH264Encoder {
pub fn new(
resolution: Resolution,
input_format: PixelFormat,
fps: u32,
bitrate_kbps: u32,
) -> Result<Self> {
let pixfmt = match input_format {
PixelFormat::Nv12 => resolve_pixel_format("nv12", AVPixelFormat::AV_PIX_FMT_NV12),
PixelFormat::Yuv420 => {
resolve_pixel_format("yuv420p", AVPixelFormat::AV_PIX_FMT_YUV420P)
}
other => {
return Err(AppError::VideoError(format!(
"FFmpeg h264_mediacodec accepts NV12/YUV420P memory frames; {other} requires conversion first"
)))
}
};
let ctx = EncodeContext {
name: "h264_mediacodec".to_string(),
mc_name: None,
width: resolution.width as i32,
height: resolution.height as i32,
pixfmt,
align: 1,
fps: fps.max(1) as i32,
gop: fps.max(1) as i32,
rc: RateControl::RC_CBR,
quality: Quality::Quality_Low,
kbs: bitrate_kbps.max(1) as i32,
q: 23,
thread_count: 1,
};
let inner = HwEncoder::new(ctx).map_err(|_| {
AppError::VideoError("Failed to create FFmpeg h264_mediacodec encoder".to_string())
})?;
Ok(Self {
inner,
resolution,
input_format,
bitrate_kbps: bitrate_kbps.max(1),
})
}
pub fn encode_raw(&mut self, data: &[u8], pts_ms: i64) -> Result<Vec<AndroidH264Packet>> {
let min_len = self
.input_format
.frame_size(self.resolution)
.ok_or_else(|| AppError::VideoError("MediaCodec input must be raw YUV".to_string()))?;
if data.len() < min_len {
return Err(AppError::VideoError(format!(
"MediaCodec {} frame too small: {} < {}",
self.input_format,
data.len(),
min_len
)));
}
let packets = self
.inner
.encode_bytes(data, pts_ms)
.map_err(|err| AppError::VideoError(format!("h264_mediacodec encode failed: {err}")))?;
Ok(packets
.into_iter()
.map(|packet| AndroidH264Packet {
data: packet.data,
pts: packet.pts,
key_frame: packet.key == 1,
})
.collect())
}
pub fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()> {
self.inner
.set_bitrate(bitrate_kbps.max(1) as i32)
.map_err(|_| AppError::VideoError("Failed to set MediaCodec bitrate".to_string()))?;
self.bitrate_kbps = bitrate_kbps.max(1);
Ok(())
}
pub fn request_keyframe(&mut self) {
self.inner.request_keyframe();
}
pub fn codec_name(&self) -> &str {
"h264_mediacodec"
}
pub fn input_format(&self) -> PixelFormat {
self.input_format
}
}
unsafe impl Send for AndroidMediaCodecH264Encoder {}
#[derive(Debug, Clone)]
pub struct AndroidH264Packet {
pub data: Bytes,
pub pts: i64,
pub key_frame: bool,
}

View File

@@ -1,137 +0,0 @@
//! Android FFmpeg/MediaCodec MJPEG decoder glue.
use hwcodec::ffmpeg::AVPixelFormat;
use hwcodec::ffmpeg_ram::decode::{DecodeContext, Decoder};
use tracing::{info, warn};
use crate::error::{AppError, Result};
use crate::video::codec::convert::Nv12Converter;
use crate::video::format::{PixelFormat, Resolution};
pub struct AndroidMediaCodecMjpegDecoder {
decoder: Decoder,
resolution: Resolution,
nv12_converter: Option<Nv12Converter>,
last_output_format: Option<PixelFormat>,
pending_frames: u32,
}
impl AndroidMediaCodecMjpegDecoder {
pub fn new(resolution: Resolution) -> Result<Self> {
let ctx = DecodeContext {
name: "mjpeg_mediacodec".to_string(),
width: resolution.width as i32,
height: resolution.height as i32,
sw_pixfmt: AVPixelFormat::AV_PIX_FMT_NV12,
thread_count: 1,
};
let decoder = Decoder::new(ctx).map_err(|_| {
AppError::VideoError("Failed to create FFmpeg mjpeg_mediacodec decoder".to_string())
})?;
Ok(Self {
decoder,
resolution,
nv12_converter: None,
last_output_format: None,
pending_frames: 0,
})
}
pub fn decode_to_nv12(&mut self, mjpeg: &[u8]) -> Result<Vec<u8>> {
let frames = match self.decoder.decode(mjpeg) {
Ok(frames) => frames,
Err(err) if err == -11 => {
self.pending_frames += 1;
if self.pending_frames <= 3 {
return Err(AppError::VideoError(
"mjpeg_mediacodec decode needs more input".to_string(),
));
}
return Err(AppError::VideoError(
"mjpeg_mediacodec decoder did not output after 3 frames".to_string(),
));
}
Err(err) => {
return Err(AppError::VideoError(format!(
"mjpeg_mediacodec decode failed: {err}"
)));
}
};
if frames.is_empty() {
self.pending_frames += 1;
if self.pending_frames <= 3 {
return Err(AppError::VideoError(
"mjpeg_mediacodec decode needs more input".to_string(),
));
}
return Err(AppError::VideoError(
"mjpeg_mediacodec decoder did not output after 3 frames".to_string(),
));
}
self.pending_frames = 0;
if frames.len() > 1 {
warn!(
"mjpeg_mediacodec decode returned {} frames, using last",
frames.len()
);
}
let frame = frames.pop().ok_or_else(|| {
AppError::VideoError("mjpeg_mediacodec decode returned empty".to_string())
})?;
if frame.width as u32 != self.resolution.width
|| frame.height as u32 != self.resolution.height
{
warn!(
"mjpeg_mediacodec output size {}x{} differs from expected {}x{}",
frame.width, frame.height, self.resolution.width, self.resolution.height
);
}
let output_format = pixel_format_from_av(frame.pixfmt).ok_or_else(|| {
AppError::VideoError(format!(
"mjpeg_mediacodec output pixfmt {:?} is not supported",
frame.pixfmt
))
})?;
if self.last_output_format != Some(output_format) {
info!("mjpeg_mediacodec output format: {}", output_format);
self.last_output_format = Some(output_format);
}
match output_format {
PixelFormat::Nv12 => Ok(frame.data),
PixelFormat::Nv21 => {
let converter = self
.nv12_converter
.get_or_insert_with(|| Nv12Converter::nv21_to_nv12(self.resolution));
Ok(converter.convert(&frame.data)?.to_vec())
}
PixelFormat::Yuv420 => {
let converter = self
.nv12_converter
.get_or_insert_with(|| Nv12Converter::yuv420_to_nv12(self.resolution));
Ok(converter.convert(&frame.data)?.to_vec())
}
other => Err(AppError::VideoError(format!(
"mjpeg_mediacodec output {} cannot be converted to NV12",
other
))),
}
}
}
fn pixel_format_from_av(format: AVPixelFormat) -> Option<PixelFormat> {
match format {
AVPixelFormat::AV_PIX_FMT_NV12 => Some(PixelFormat::Nv12),
AVPixelFormat::AV_PIX_FMT_NV21 => Some(PixelFormat::Nv21),
AVPixelFormat::AV_PIX_FMT_YUV420P | AVPixelFormat::AV_PIX_FMT_YUVJ420P => {
Some(PixelFormat::Yuv420)
}
_ => None,
}
}
unsafe impl Send for AndroidMediaCodecMjpegDecoder {}

View File

@@ -48,8 +48,6 @@ pub enum H264EncoderType {
Rkmpp,
/// V4L2 M2M (ARM generic) - requires hwcodec extension
V4l2M2m,
/// Android MediaCodec via FFmpeg
MediaCodec,
/// Software encoding (libx264/openh264)
Software,
/// No encoder available
@@ -66,7 +64,6 @@ impl std::fmt::Display for H264EncoderType {
H264EncoderType::Vaapi => write!(f, "VAAPI"),
H264EncoderType::Rkmpp => write!(f, "RKMPP"),
H264EncoderType::V4l2M2m => write!(f, "V4L2 M2M"),
H264EncoderType::MediaCodec => write!(f, "MediaCodec"),
H264EncoderType::Software => write!(f, "Software"),
H264EncoderType::None => write!(f, "None"),
}
@@ -83,7 +80,6 @@ impl From<EncoderBackend> for H264EncoderType {
EncoderBackend::Vaapi => H264EncoderType::Vaapi,
EncoderBackend::Rkmpp => H264EncoderType::Rkmpp,
EncoderBackend::V4l2m2m => H264EncoderType::V4l2M2m,
EncoderBackend::MediaCodec => H264EncoderType::MediaCodec,
EncoderBackend::Software => H264EncoderType::Software,
}
}
@@ -196,7 +192,6 @@ pub fn get_available_encoders(width: u32, height: u32) -> Vec<CodecInfo> {
let ctx = EncodeContext {
name: String::new(),
mc_name: None,
width: width as i32,
height: height as i32,
pixfmt: resolve_pixel_format("yuv420p", AVPixelFormat::AV_PIX_FMT_YUV420P),
@@ -296,7 +291,6 @@ impl H264Encoder {
let ctx = EncodeContext {
name: codec_name.to_string(),
mc_name: None,
width: width as i32,
height: height as i32,
pixfmt,

View File

@@ -45,8 +45,6 @@ pub enum H265EncoderType {
Rkmpp,
/// V4L2 M2M (ARM generic)
V4l2M2m,
/// Android MediaCodec via FFmpeg
MediaCodec,
/// Software encoder (libx265)
Software,
/// No encoder available
@@ -63,7 +61,6 @@ impl std::fmt::Display for H265EncoderType {
H265EncoderType::Vaapi => write!(f, "VAAPI"),
H265EncoderType::Rkmpp => write!(f, "RKMPP"),
H265EncoderType::V4l2M2m => write!(f, "V4L2 M2M"),
H265EncoderType::MediaCodec => write!(f, "MediaCodec"),
H265EncoderType::Software => write!(f, "Software"),
H265EncoderType::None => write!(f, "None"),
}
@@ -79,7 +76,6 @@ impl From<EncoderBackend> for H265EncoderType {
EncoderBackend::Vaapi => H265EncoderType::Vaapi,
EncoderBackend::Rkmpp => H265EncoderType::Rkmpp,
EncoderBackend::V4l2m2m => H265EncoderType::V4l2M2m,
EncoderBackend::MediaCodec => H265EncoderType::MediaCodec,
EncoderBackend::Software => H265EncoderType::Software,
}
}
@@ -199,7 +195,6 @@ pub fn get_available_h265_encoders(width: u32, height: u32) -> Vec<CodecInfo> {
let ctx = EncodeContext {
name: String::new(),
mc_name: None,
width: width as i32,
height: height as i32,
pixfmt: resolve_pixel_format("nv12", AVPixelFormat::AV_PIX_FMT_NV12),
@@ -373,7 +368,6 @@ impl H265Encoder {
let ctx = EncodeContext {
name: codec_name.to_string(),
mc_name: None,
width: width as i32,
height: height as i32,
pixfmt,

View File

@@ -3,10 +3,6 @@
use hwcodec::common::DataFormat;
use hwcodec::ffmpeg_ram::CodecInfo;
#[cfg(feature = "android-mediacodec")]
pub mod android_mediacodec;
#[cfg(feature = "android-mediacodec")]
pub mod android_mjpeg;
pub mod convert;
pub mod h264;
@@ -23,10 +19,6 @@ pub mod vp9;
#[cfg(all(feature = "desktop", any(target_arch = "aarch64", target_arch = "arm")))]
pub mod mjpeg_rkmpp;
#[cfg(feature = "android-mediacodec")]
pub use android_mediacodec::{AndroidH264Packet, AndroidMediaCodecH264Encoder};
#[cfg(feature = "android-mediacodec")]
pub use android_mjpeg::AndroidMediaCodecMjpegDecoder;
pub use convert::{MjpegToNv12Decoder, PixelConverter, Yuv420pBuffer};
pub use h264::{H264Config, H264Encoder, H264EncoderType, H264InputFormat};
pub use h265::{H265Config, H265Encoder, H265EncoderType, H265InputFormat};

View File

@@ -96,8 +96,6 @@ pub enum EncoderBackend {
Rkmpp,
/// V4L2 Memory-to-Memory (ARM)
V4l2m2m,
/// Android MediaCodec via FFmpeg
MediaCodec,
/// Software encoding (libx264, libx265, libvpx)
Software,
}
@@ -117,8 +115,6 @@ impl EncoderBackend {
EncoderBackend::Rkmpp
} else if name.contains("v4l2m2m") {
EncoderBackend::V4l2m2m
} else if name.contains("mediacodec") {
EncoderBackend::MediaCodec
} else {
EncoderBackend::Software
}
@@ -138,7 +134,6 @@ impl EncoderBackend {
EncoderBackend::Amf => "AMF",
EncoderBackend::Rkmpp => "RKMPP",
EncoderBackend::V4l2m2m => "V4L2 M2M",
EncoderBackend::MediaCodec => "MediaCodec",
EncoderBackend::Software => "Software",
}
}
@@ -153,7 +148,6 @@ impl EncoderBackend {
"amf" => Some(EncoderBackend::Amf),
"rkmpp" => Some(EncoderBackend::Rkmpp),
"v4l2m2m" | "v4l2" => Some(EncoderBackend::V4l2m2m),
"mediacodec" | "android-mediacodec" => Some(EncoderBackend::MediaCodec),
"software" | "cpu" => Some(EncoderBackend::Software),
_ => None,
}
@@ -261,8 +255,8 @@ impl EncoderRegistry {
let codec_name = match format {
VideoEncoderType::H264 => "libx264",
VideoEncoderType::H265 => "libx265",
VideoEncoderType::VP8 => "libvpx",
VideoEncoderType::VP9 => "libvpx-vp9",
VideoEncoderType::VP8 => "libvpx_vp8",
VideoEncoderType::VP9 => "libvpx_vp9",
};
encoders.push(AvailableEncoder {
@@ -309,10 +303,9 @@ impl EncoderRegistry {
self.encoders.clear();
self.detection_resolution = (width, height);
// Create test context for encoder detection
// Create test context for encoder detection.
let ctx = EncodeContext {
name: String::new(),
mc_name: None,
width: width as i32,
height: height as i32,
pixfmt: resolve_pixel_format("nv12", AVPixelFormat::AV_PIX_FMT_NV12),
@@ -332,7 +325,6 @@ impl EncoderRegistry {
ctx.clone(),
Duration::from_millis(DETECT_TIMEOUT_MS),
);
info!("Found {} encoders from hwcodec", all_encoders.len());
for codec_info in &all_encoders {

View File

@@ -2,8 +2,6 @@ use serde::Serialize;
use std::sync::mpsc;
use std::time::{Duration, Instant};
#[cfg(feature = "android-mediacodec")]
use super::AndroidMediaCodecH264Encoder;
use super::{
EncoderRegistry, H264Config, H264Encoder, H265Config, H265Encoder, VP8Config, VP8Encoder,
VP9Config, VP9Encoder, VideoEncoderType,
@@ -237,32 +235,6 @@ fn run_smoke_test(
}
fn run_h264_smoke_test(resolution: Resolution, codec_name_ffmpeg: &str) -> Result<()> {
#[cfg(feature = "android-mediacodec")]
if codec_name_ffmpeg == "h264_mediacodec" {
let mut encoder = AndroidMediaCodecH264Encoder::new(
resolution,
PixelFormat::Nv12,
30,
bitrate_kbps_for_resolution(resolution),
)?;
encoder.request_keyframe();
let frame = build_nv12_test_frame(
resolution,
PixelFormat::Nv12.frame_size(resolution).unwrap_or(0),
);
for sequence in 0..SELF_CHECK_FRAME_ATTEMPTS {
let frames = encoder.encode_raw(&frame, pts_ms(sequence))?;
if frames.iter().any(|frame| !frame.data.is_empty()) {
return Ok(());
}
}
return Err(AppError::VideoError(
"Encoder produced no output after multiple frames".to_string(),
));
}
let mut encoder = H264Encoder::with_codec(
H264Config::low_latency(resolution, bitrate_kbps_for_resolution(resolution)),
codec_name_ffmpeg,

View File

@@ -130,7 +130,6 @@ pub fn get_available_vp8_encoders(width: u32, height: u32) -> Vec<CodecInfo> {
let ctx = EncodeContext {
name: String::new(),
mc_name: None,
width: width as i32,
height: height as i32,
pixfmt: resolve_pixel_format("nv12", AVPixelFormat::AV_PIX_FMT_NV12),
@@ -271,7 +270,6 @@ impl VP8Encoder {
let ctx = EncodeContext {
name: codec_name.to_string(),
mc_name: None,
width: width as i32,
height: height as i32,
pixfmt,

View File

@@ -130,7 +130,6 @@ pub fn get_available_vp9_encoders(width: u32, height: u32) -> Vec<CodecInfo> {
let ctx = EncodeContext {
name: String::new(),
mc_name: None,
width: width as i32,
height: height as i32,
pixfmt: resolve_pixel_format("nv12", AVPixelFormat::AV_PIX_FMT_NV12),
@@ -271,7 +270,6 @@ impl VP9Encoder {
let ctx = EncodeContext {
name: codec_name.to_string(),
mc_name: None,
width: width as i32,
height: height as i32,
pixfmt,

View File

@@ -963,19 +963,6 @@ pub fn enumerate_devices() -> Result<Vec<VideoDeviceInfo>> {
// for a single MIPI CSI pipeline. Keep only the highest-priority node per
// (driver, bus_info) group so users see one device instead of ~11.
dedup_platform_subdevices(&mut devices);
devices.retain(|device| {
let hide = should_hide_android_platform_node(device);
if hide {
debug!(
"Hiding Android platform video node: {} ({}) {}",
device.name,
device.driver,
device.path.display()
);
}
!hide
});
info!("Found {} video capture devices", devices.len());
Ok(devices)
}
@@ -1055,33 +1042,6 @@ fn dedup_platform_subdevices(devices: &mut Vec<VideoDeviceInfo>) {
});
}
fn should_hide_android_platform_node(device: &VideoDeviceInfo) -> bool {
if !cfg!(feature = "android") {
return false;
}
let driver = device.driver.to_ascii_lowercase();
let name = device.name.to_ascii_lowercase();
let card = device.card.to_ascii_lowercase();
let usb_device = driver == "uvcvideo" || device.bus_info.starts_with("usb-");
let known_bridge =
driver.contains("rkcif") || driver.contains("rk_hdmirx") || driver.contains("tc358743");
if usb_device || known_bridge {
return false;
}
matches!(
driver.as_str(),
"ionvideo" | "amlvideo" | "amlvideo2" | "videosync"
) || matches!(
name.as_str(),
"ionvideo" | "amlvideo" | "amlvideo2" | "videosync"
) || matches!(
card.as_str(),
"ionvideo" | "amlvideo" | "amlvideo2" | "videosync"
)
}
/// rkcif registers many `/dev/video*` queues; probing all in parallel can
/// contend and time out. Keep one node per board (lowest `videoN`).
fn collapse_rkcif_probe_candidates(candidates: &mut Vec<PathBuf>) {
@@ -1185,20 +1145,6 @@ fn sysfs_maybe_capture(path: &Path) -> bool {
.to_lowercase();
let driver = extract_uevent_value(&uevent, "driver");
if cfg!(feature = "android") {
let platform_skip = ["ionvideo", "amlvideo", "amlvideo2", "videosync"];
let driver_skip = driver
.as_ref()
.is_some_and(|driver| platform_skip.iter().any(|hint| driver == hint));
if driver_skip || platform_skip.iter().any(|hint| sysfs_name == *hint) {
debug!(
"Skipping Android platform video node {:?}: {}",
path, sysfs_name
);
return false;
}
}
let mut maybe_capture = false;
let capture_hints = [
"capture",

View File

@@ -8,21 +8,19 @@ pub mod codec_constraints;
pub mod device;
pub mod format;
pub mod frame;
#[cfg(any(feature = "android", feature = "desktop"))]
#[cfg(feature = "desktop")]
pub mod pipeline;
pub mod signal;
#[cfg(any(feature = "android", feature = "desktop"))]
#[cfg(feature = "desktop")]
pub mod stream_manager;
#[cfg(any(feature = "android", feature = "desktop"))]
#[cfg(feature = "desktop")]
pub mod streamer;
#[cfg(any(feature = "android", feature = "desktop"))]
#[cfg(feature = "desktop")]
pub mod traits;
#[cfg(any(feature = "android", feature = "desktop"))]
#[cfg(feature = "desktop")]
pub mod types;
pub use capture::{CaptureMeta, CaptureStream};
#[cfg(feature = "android-mediacodec")]
pub use codec::{AndroidH264Packet, AndroidMediaCodecH264Encoder};
pub use codec::{H264Encoder, H264EncoderType, JpegEncoder, PixelConverter, Yuv420pBuffer};
pub use device::{VideoDevice, VideoDeviceInfo};
pub use format::PixelFormat;

View File

@@ -6,16 +6,9 @@ use crate::video::codec::registry::{EncoderBackend, EncoderRegistry, VideoEncode
use crate::video::codec::traits::EncoderConfig;
use crate::video::codec::vp8::{VP8Config, VP8Encoder};
use crate::video::codec::vp9::{VP9Config, VP9Encoder};
#[cfg(feature = "android-mediacodec")]
use crate::video::codec::AndroidMediaCodecH264Encoder;
#[cfg(feature = "android-mediacodec")]
use crate::video::codec::AndroidMediaCodecMjpegDecoder;
use crate::video::format::{PixelFormat, Resolution};
use bytes::Bytes;
#[cfg(all(
any(target_arch = "aarch64", target_arch = "arm"),
not(target_os = "android")
))]
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
use hwcodec::ffmpeg_hw::{
last_error_message as ffmpeg_hw_last_error, HwMjpegH26xConfig, HwMjpegH26xPipeline,
};
@@ -29,15 +22,9 @@ pub(super) struct EncoderThreadState {
pub(super) nv12_converter: Option<Nv12Converter>,
pub(super) yuv420p_converter: Option<PixelConverter>,
pub(super) encoder_needs_yuv420p: bool,
#[cfg(all(
any(target_arch = "aarch64", target_arch = "arm"),
not(target_os = "android")
))]
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
pub(super) ffmpeg_hw_pipeline: Option<HwMjpegH26xPipeline>,
#[cfg(all(
any(target_arch = "aarch64", target_arch = "arm"),
not(target_os = "android")
))]
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
pub(super) ffmpeg_hw_enabled: bool,
pub(super) fps: u32,
pub(super) codec: VideoEncoderType,
@@ -129,35 +116,6 @@ impl VideoEncoderTrait for H265EncoderWrapper {
}
}
#[cfg(feature = "android-mediacodec")]
struct AndroidMediaCodecH264EncoderWrapper(AndroidMediaCodecH264Encoder);
#[cfg(feature = "android-mediacodec")]
impl VideoEncoderTrait for AndroidMediaCodecH264EncoderWrapper {
fn encode_raw(&mut self, data: &[u8], pts_ms: i64) -> Result<Vec<EncodedFrame>> {
let frames = self.0.encode_raw(data, pts_ms)?;
Ok(frames
.into_iter()
.map(|f| EncodedFrame {
data: f.data,
key: if f.key_frame { 1 } else { 0 },
})
.collect())
}
fn set_bitrate(&mut self, bitrate_kbps: u32) -> Result<()> {
self.0.set_bitrate(bitrate_kbps)
}
fn codec_name(&self) -> &str {
self.0.codec_name()
}
fn request_keyframe(&mut self) {
self.0.request_keyframe()
}
}
struct VP8EncoderWrapper(VP8Encoder);
impl VideoEncoderTrait for VP8EncoderWrapper {
@@ -209,50 +167,12 @@ impl VideoEncoderTrait for VP9EncoderWrapper {
}
pub(super) enum MjpegDecoderKind {
#[cfg(feature = "android-mediacodec")]
AndroidMediaCodec {
decoder: AndroidMediaCodecMjpegDecoder,
fallback: Box<MjpegDecoderKind>,
fallback_active: bool,
output: Vec<u8>,
},
Libyuv {
decoder: MjpegToNv12Decoder,
},
Libyuv { decoder: MjpegToNv12Decoder },
}
impl MjpegDecoderKind {
pub(super) fn decode(&mut self, data: &[u8]) -> Result<&[u8]> {
match self {
#[cfg(feature = "android-mediacodec")]
MjpegDecoderKind::AndroidMediaCodec {
decoder,
fallback,
fallback_active,
output,
} => {
if !*fallback_active {
match decoder.decode_to_nv12(data) {
Ok(decoded) => {
*output = decoded;
return Ok(output.as_slice());
}
Err(AppError::VideoError(message))
if message.contains("needs more input") =>
{
return Err(AppError::VideoError(message));
}
Err(err) => {
tracing::warn!(
"Android MediaCodec MJPEG decode failed; falling back to libyuv MJPEG->NV12: {}",
err
);
*fallback_active = true;
}
}
}
fallback.decode(data)
}
MjpegDecoderKind::Libyuv { decoder } => decoder.decode(data),
}
}
@@ -265,40 +185,6 @@ fn libyuv_mjpeg_decoder(resolution: Resolution) -> MjpegDecoderKind {
}
fn create_mjpeg_decoder(resolution: Resolution) -> Result<(MjpegDecoderKind, PixelFormat)> {
#[cfg(feature = "android-mediacodec")]
{
if std::env::var_os("ONE_KVM_ANDROID_MJPEG_MEDIACODEC").is_none() {
info!("MJPEG input detected, using libyuv decoder (MJPEG -> NV12)");
return Ok((libyuv_mjpeg_decoder(resolution), PixelFormat::Nv12));
}
info!("MJPEG input detected, trying Android MediaCodec decoder (MJPEG -> NV12)");
match AndroidMediaCodecMjpegDecoder::new(resolution) {
Ok(decoder) => {
info!("Using Android MediaCodec MJPEG decoder");
return Ok((
MjpegDecoderKind::AndroidMediaCodec {
decoder,
fallback: Box::new(libyuv_mjpeg_decoder(resolution)),
fallback_active: false,
output: Vec::with_capacity(
PixelFormat::Nv12
.frame_size(resolution)
.unwrap_or((resolution.width * resolution.height * 3 / 2) as usize),
),
},
PixelFormat::Nv12,
));
}
Err(err) => {
tracing::warn!(
"Android MediaCodec MJPEG decoder unavailable; using libyuv MJPEG->NV12: {}",
err
);
}
}
}
info!("MJPEG input detected, using libyuv decoder (MJPEG -> NV12)");
Ok((libyuv_mjpeg_decoder(resolution), PixelFormat::Nv12))
}
@@ -400,15 +286,9 @@ pub(super) fn build_encoder_state(
}
};
#[cfg(all(
any(target_arch = "aarch64", target_arch = "arm"),
not(target_os = "android")
))]
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
let is_rkmpp_encoder = selected_codec_name.contains("rkmpp");
#[cfg(all(
any(target_arch = "aarch64", target_arch = "arm"),
not(target_os = "android")
))]
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
if needs_mjpeg_decode
&& is_rkmpp_encoder
&& matches!(
@@ -448,15 +328,9 @@ pub(super) fn build_encoder_state(
nv12_converter: None,
yuv420p_converter: None,
encoder_needs_yuv420p: false,
#[cfg(all(
any(target_arch = "aarch64", target_arch = "arm"),
not(target_os = "android")
))]
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
ffmpeg_hw_pipeline: Some(pipeline),
#[cfg(all(
any(target_arch = "aarch64", target_arch = "arm"),
not(target_os = "android")
))]
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
ffmpeg_hw_enabled: true,
fps: config.fps,
codec: config.output_codec,
@@ -495,40 +369,7 @@ pub(super) fn build_encoder_state(
);
}
#[cfg(feature = "android-mediacodec")]
{
if codec_name == "h264_mediacodec" {
info!(
"Creating Android MediaCodec H264 encoder for {:?} input",
input_format
);
let pixel_format = match input_format {
H264InputFormat::Nv12 => PixelFormat::Nv12,
H264InputFormat::Yuv420p => PixelFormat::Yuv420,
other => {
return Err(AppError::VideoError(format!(
"Android MediaCodec H264 does not support {:?} direct input",
other
)));
}
};
let encoder = AndroidMediaCodecH264Encoder::new(
config.resolution,
pixel_format,
config.fps,
config.bitrate_kbps(),
)?;
info!("Created Android MediaCodec H264 encoder");
Box::new(AndroidMediaCodecH264EncoderWrapper(encoder))
} else {
create_h264_encoder(config, input_format, &codec_name)?
}
}
#[cfg(not(feature = "android-mediacodec"))]
{
create_h264_encoder(config, input_format, &codec_name)?
}
create_h264_encoder(config, input_format, &codec_name)?
}
VideoEncoderType::H265 => {
let codec_name = selected_codec_name.clone();
@@ -622,11 +463,6 @@ pub(super) fn build_encoder_state(
pipeline_input_format,
PixelFormat::Nv12 | PixelFormat::Nv16 | PixelFormat::Nv21 | PixelFormat::Yuv420
)
} else if codec_name.contains("mediacodec") {
matches!(
pipeline_input_format,
PixelFormat::Nv12 | PixelFormat::Yuv420
)
} else {
false
};
@@ -676,15 +512,9 @@ pub(super) fn build_encoder_state(
nv12_converter,
yuv420p_converter,
encoder_needs_yuv420p: needs_yuv420p,
#[cfg(all(
any(target_arch = "aarch64", target_arch = "arm"),
not(target_os = "android")
))]
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
ffmpeg_hw_pipeline: None,
#[cfg(all(
any(target_arch = "aarch64", target_arch = "arm"),
not(target_os = "android")
))]
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
ffmpeg_hw_enabled: false,
fps: config.fps,
codec: config.output_codec,
@@ -708,12 +538,6 @@ fn h264_direct_input_format(
PixelFormat::Nv24 => Some(H264InputFormat::Nv24),
_ => None,
}
} else if codec_name.contains("mediacodec") {
match input_format {
PixelFormat::Nv12 => Some(H264InputFormat::Nv12),
PixelFormat::Yuv420 => Some(H264InputFormat::Yuv420p),
_ => None,
}
} else if codec_name.contains("libx264") {
match input_format {
PixelFormat::Nv12 => Some(H264InputFormat::Nv12),

View File

@@ -61,10 +61,7 @@ use crate::video::signal::SignalStatus;
const MIN_CAPTURE_FRAME_SIZE: usize = 128;
#[cfg(all(
any(target_arch = "aarch64", target_arch = "arm"),
not(target_os = "android")
))]
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
use hwcodec::ffmpeg_hw::last_error_message as ffmpeg_hw_last_error;
/// Encoded video frame for distribution
@@ -484,15 +481,9 @@ impl SharedVideoPipeline {
fn apply_cmd(&self, state: &mut EncoderThreadState, cmd: PipelineCmd) -> Result<()> {
match cmd {
PipelineCmd::SetBitrate { bitrate_kbps, gop } => {
#[cfg(any(
not(any(target_arch = "aarch64", target_arch = "arm")),
target_os = "android"
))]
#[cfg(not(any(target_arch = "aarch64", target_arch = "arm")))]
let _ = gop;
#[cfg(all(
any(target_arch = "aarch64", target_arch = "arm"),
not(target_os = "android")
))]
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
if state.ffmpeg_hw_enabled {
if let Some(ref mut pipeline) = state.ffmpeg_hw_pipeline {
pipeline
@@ -659,7 +650,7 @@ impl SharedVideoPipeline {
*guard = Some(cmd_tx);
}
// Encoder loop uses a dedicated OS thread because FFmpeg/MediaCodec work is synchronous.
// Encoder loop uses a dedicated OS thread because FFmpeg work is synchronous.
{
let pipeline = pipeline.clone();
let latest_frame = latest_frame.clone();
@@ -1289,10 +1280,7 @@ impl SharedVideoPipeline {
current_ts_us.saturating_sub(start_ts_us) / 1000
};
#[cfg(all(
any(target_arch = "aarch64", target_arch = "arm"),
not(target_os = "android")
))]
#[cfg(any(target_arch = "aarch64", target_arch = "arm"))]
if state.ffmpeg_hw_enabled {
if input_format != PixelFormat::Mjpeg {
return Err(AppError::VideoError(

View File

@@ -9,12 +9,6 @@ pub async fn update_overview(
State(state): State<Arc<AppState>>,
axum::extract::Query(query): axum::extract::Query<UpdateOverviewQuery>,
) -> Result<Json<UpdateOverviewResponse>> {
if cfg!(feature = "android") {
return Err(AppError::BadRequest(
"Online upgrade is disabled on Android".to_string(),
));
}
let channel = query.channel.unwrap_or(UpdateChannel::Stable);
let response = state.update.overview(channel).await?;
Ok(Json(response))
@@ -24,12 +18,6 @@ pub async fn update_upgrade(
State(state): State<Arc<AppState>>,
Json(req): Json<UpgradeRequest>,
) -> Result<Json<LoginResponse>> {
if cfg!(feature = "android") {
return Err(AppError::BadRequest(
"Online upgrade is disabled on Android".to_string(),
));
}
state.update.start_upgrade(req, state.shutdown_tx.clone())?;
Ok(Json(LoginResponse {
@@ -41,11 +29,5 @@ pub async fn update_upgrade(
pub async fn update_status(
State(state): State<Arc<AppState>>,
) -> Result<Json<UpdateStatusResponse>> {
if cfg!(feature = "android") {
return Err(AppError::BadRequest(
"Online upgrade is disabled on Android".to_string(),
));
}
Ok(Json(state.update.status().await))
}