use minifb::{Key, Window, WindowOptions};
use std::io::{BufReader, Read};
use std::process::{Child, Command, Stdio};
use std::sync::mpsc::{channel, Receiver, Sender};
use std::thread;
use std::time::Duration;
#[derive(Debug, Clone)]
pub struct StreamConfig {
pub width: usize,
pub height: usize,
pub bitrate: String,
}
impl Default for StreamConfig {
fn default() -> Self {
Self {
width: 1080,
height: 1920,
bitrate: "8M".to_string(),
}
}
}
pub enum StreamControl {
Stop,
}
#[derive(Debug)]
pub struct StreamState {
adb_process: Option<Child>,
ffmpeg_process: Option<Child>,
control_tx: Option<Sender<StreamControl>>,
stream_thread: Option<thread::JoinHandle<()>>,
}
impl Default for StreamState {
fn default() -> Self {
Self::new()
}
}
impl StreamState {
pub fn new() -> Self {
Self {
adb_process: None,
ffmpeg_process: None,
control_tx: None,
stream_thread: None,
}
}
pub fn stop(&mut self) {
if let Some(tx) = self.control_tx.take() {
let _ = tx.send(StreamControl::Stop);
}
if let Some(handle) = self.stream_thread.take() {
let _ = handle.join();
}
if let Some(mut proc) = self.adb_process.take() {
let _ = proc.kill();
}
if let Some(mut proc) = self.ffmpeg_process.take() {
let _ = proc.kill();
}
}
}
impl Drop for StreamState {
fn drop(&mut self) {
self.stop();
}
}
pub fn start_stream(config: StreamConfig) -> Result<StreamState, String> {
let (control_tx, control_rx) = channel();
let stream_thread = thread::spawn(move || {
if let Err(e) = run_stream(config, control_rx) {
eprintln!("Stream error: {}", e);
}
});
Ok(StreamState {
adb_process: None,
ffmpeg_process: None,
control_tx: Some(control_tx),
stream_thread: Some(stream_thread),
})
}
fn run_stream(config: StreamConfig, control_rx: Receiver<StreamControl>) -> Result<(), String> {
let resolution = get_device_resolution()?;
let (width, height) = resolution;
let mut window = Window::new(
"DroidTUI - Screen Stream",
width,
height,
WindowOptions {
resize: true,
scale_mode: minifb::ScaleMode::AspectRatioStretch,
..WindowOptions::default()
},
)
.map_err(|e| format!("Failed to create window: {}", e))?;
window.set_target_fps(60);
let mut adb_child = Command::new("adb")
.args([
"exec-out",
"screenrecord",
"--output-format=h264",
"--size",
&format!("{}x{}", width, height),
"--bit-rate",
&config.bitrate,
"-",
])
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.map_err(|e| format!("Failed to start screenrecord: {}", e))?;
let stdout = adb_child.stdout.take().ok_or("Failed to get stdout")?;
let mut ffmpeg_child = Command::new("ffmpeg")
.args([
"-f", "h264", "-i", "pipe:0", "-f", "rawvideo", "-pix_fmt", "rgb24", "pipe:1",
])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::null())
.spawn()
.map_err(|e| format!("Failed to start ffmpeg: {}", e))?;
let mut ffmpeg_stdin = ffmpeg_child
.stdin
.take()
.ok_or("Failed to get ffmpeg stdin")?;
let ffmpeg_stdout = ffmpeg_child
.stdout
.take()
.ok_or("Failed to get ffmpeg stdout")?;
let pipe_thread = thread::spawn(move || {
let mut reader = BufReader::new(stdout);
let mut buffer = vec![0u8; 8192];
loop {
match reader.read(&mut buffer) {
Ok(0) => break,
Ok(n) => {
if std::io::Write::write_all(&mut ffmpeg_stdin, &buffer[..n]).is_err() {
break;
}
}
Err(_) => break,
}
}
});
let frame_size = width * height * 3; let mut frame_buffer = vec![0u8; frame_size];
let mut display_buffer: Vec<u32> = vec![0; width * height];
let mut reader = BufReader::new(ffmpeg_stdout);
loop {
if let Ok(StreamControl::Stop) = control_rx.try_recv() {
break;
}
if !window.is_open() || window.is_key_down(Key::Escape) || window.is_key_down(Key::Q) {
break;
}
match reader.read_exact(&mut frame_buffer) {
Ok(_) => {
for i in 0..(width * height) {
let r = frame_buffer[i * 3] as u32;
let g = frame_buffer[i * 3 + 1] as u32;
let b = frame_buffer[i * 3 + 2] as u32;
display_buffer[i] = (r << 16) | (g << 8) | b;
}
window
.update_with_buffer(&display_buffer, width, height)
.map_err(|e| format!("Failed to update window: {}", e))?;
}
Err(_) => {
window.update();
thread::sleep(Duration::from_millis(10));
}
}
}
let _ = adb_child.kill();
let _ = ffmpeg_child.kill();
let _ = pipe_thread.join();
Ok(())
}
fn get_device_resolution() -> Result<(usize, usize), String> {
let output = Command::new("adb")
.args(["shell", "wm", "size"])
.output()
.map_err(|e| format!("Failed to get device resolution: {}", e))?;
let stdout = String::from_utf8_lossy(&output.stdout);
for line in stdout.lines() {
if line.contains("Physical size:") || line.contains("Override size:") {
if let Some(size_part) = line.split(':').nth(1) {
let size_str = size_part.trim();
if let Some((w, h)) = size_str.split_once('x') {
if let (Ok(width), Ok(height)) = (w.parse(), h.parse()) {
return Ok((width, height));
}
}
}
}
}
Ok((1080, 1920))
}
pub async fn capture_screenshot() -> Result<Vec<u8>, String> {
let output = tokio::process::Command::new("adb")
.args(["exec-out", "screencap", "-p"])
.output()
.await
.map_err(|e| format!("Failed to capture screenshot: {}", e))?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
return Err(format!("screencap failed: {}", stderr));
}
if output.stdout.is_empty() {
return Err("No screenshot data received".to_string());
}
Ok(output.stdout)
}