#![allow(dead_code)]
use std::io::{BufRead, BufReader, Write};
use std::net::{TcpListener, TcpStream};
use std::path::PathBuf;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use ff_decode::{AudioDecoder, HardwareAccel, VideoDecoder};
pub fn assets_dir() -> PathBuf {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
PathBuf::from(format!("{}/../../assets", manifest_dir))
}
pub fn test_video_path() -> PathBuf {
assets_dir().join("video/gameplay.mp4")
}
pub fn test_audio_path() -> PathBuf {
assets_dir().join("audio/konekonoosanpo.mp3")
}
pub fn test_jpeg_path() -> PathBuf {
assets_dir().join("img/hello-triangle.jpg")
}
pub fn create_decoder() -> Result<VideoDecoder, ff_decode::DecodeError> {
VideoDecoder::open(&test_video_path())
.hardware_accel(HardwareAccel::None)
.build()
}
pub fn create_audio_decoder() -> Result<AudioDecoder, ff_decode::DecodeError> {
AudioDecoder::open(&test_audio_path()).build()
}
fn handle_request(tcp: &mut TcpStream, path: &std::path::Path) {
let mut range_start: Option<u64> = None;
let mut range_end: Option<u64> = None;
{
let mut reader = BufReader::new(&*tcp);
loop {
let mut line = String::new();
if reader.read_line(&mut line).unwrap_or(0) == 0 {
break;
}
if line == "\r\n" {
break; }
if line.to_ascii_lowercase().starts_with("range:") {
let value = line["range:".len()..].trim();
if let Some(bytes) = value.strip_prefix("bytes=") {
let mut parts = bytes.splitn(2, '-');
if let Some(start_str) = parts.next() {
range_start = start_str.trim().parse().ok();
}
if let Some(end_str) = parts.next() {
let end_str = end_str.trim();
if !end_str.is_empty() {
range_end = end_str.parse().ok();
}
}
}
}
}
}
let body = std::fs::read(path).unwrap_or_default();
let total = body.len() as u64;
if let Some(start) = range_start {
let end = range_end
.unwrap_or(total.saturating_sub(1))
.min(total.saturating_sub(1));
let start = start.min(total);
let chunk = &body[start as usize..=(end as usize).min(body.len().saturating_sub(1))];
let header = format!(
"HTTP/1.1 206 Partial Content\r\n\
Content-Type: video/mp4\r\n\
Content-Range: bytes {start}-{end}/{total}\r\n\
Content-Length: {len}\r\n\
Accept-Ranges: bytes\r\n\
Connection: close\r\n\
\r\n",
len = chunk.len(),
);
let _ = tcp.write_all(header.as_bytes());
let _ = tcp.write_all(chunk);
} else {
let header = format!(
"HTTP/1.1 200 OK\r\n\
Content-Type: video/mp4\r\n\
Content-Length: {total}\r\n\
Accept-Ranges: bytes\r\n\
Connection: close\r\n\
\r\n",
);
let _ = tcp.write_all(header.as_bytes());
let _ = tcp.write_all(&body);
}
}
pub struct TestHttpServer {
port: u16,
running: Arc<AtomicBool>,
thread: Option<std::thread::JoinHandle<()>>,
}
impl TestHttpServer {
pub fn serve(path: impl AsRef<std::path::Path>) -> Self {
let listener = TcpListener::bind("127.0.0.1:0").expect("bind test HTTP server");
let port = listener.local_addr().expect("local_addr").port();
let running = Arc::new(AtomicBool::new(true));
let running2 = Arc::clone(&running);
let path = path.as_ref().to_path_buf();
let thread = std::thread::spawn(move || {
for stream in listener.incoming() {
if !running2.load(Ordering::SeqCst) {
break;
}
match stream {
Ok(mut tcp) => handle_request(&mut tcp, &path),
Err(_) => break,
}
if !running2.load(Ordering::SeqCst) {
break;
}
}
});
Self {
port,
running,
thread: Some(thread),
}
}
pub fn url(&self) -> String {
format!("http://127.0.0.1:{}/video.mp4", self.port)
}
}
impl Drop for TestHttpServer {
fn drop(&mut self) {
self.running.store(false, Ordering::SeqCst);
let _ = TcpStream::connect(format!("127.0.0.1:{}", self.port));
if let Some(t) = self.thread.take() {
let _ = t.join();
}
}
}