//! ffmpeg 스트림(리먹스) 실기 테스트 — 사이드카가 있을 때만 실행. use archive_media::stream::{AudioMode, StreamSpec, VideoMode}; use archive_media::PathResolver; use std::path::{Path, PathBuf}; use std::sync::Arc; fn ffmpeg_path() -> Option { let bin = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../binaries"); let triple = if cfg!(all(windows, target_arch = "x86_64")) { "x86_64-pc-windows-msvc" } else if cfg!(all(windows, target_arch = "aarch64")) { "aarch64-pc-windows-msvc" } else if cfg!(all(target_os = "macos", target_arch = "aarch64")) { "aarch64-apple-darwin" } else { "x86_64-apple-darwin" }; let ext = if cfg!(windows) { ".exe" } else { "" }; let p = bin.join(format!("ffmpeg-{triple}{ext}")); p.is_file().then_some(p) } struct NoResolver; #[async_trait::async_trait] impl PathResolver for NoResolver { fn resolve(&self, _: i64) -> Option { None } } fn make_mkv(ffmpeg: &Path, dir: &Path) -> PathBuf { let out = dir.join("입력.mkv"); let vcodec = if cfg!(windows) { "h264_mf" } else { "h264_videotoolbox" }; let status = std::process::Command::new(ffmpeg) .args([ "-y", "-v", "error", "-f", "lavfi", "-i", "testsrc2=duration=3:size=320x240:rate=25", "-f", "lavfi", "-i", "sine=frequency=440:duration=3", "-c:v", vcodec, "-b:v", "300k", "-c:a", "ac3", "-shortest", ]) .arg(&out) .status() .unwrap(); assert!(status.success()); out } #[tokio::test] async fn remux_mkv_to_fmp4_stream() { let Some(ffmpeg) = ffmpeg_path() else { eprintln!("사이드카 없음 — 건너뜀"); return; }; let dir = tempfile::tempdir().unwrap(); let mkv = make_mkv(&ffmpeg, dir.path()); let server = archive_media::start(Arc::new(NoResolver), Some(ffmpeg)).await.unwrap(); // MKV(h264 + ac3) → 영상 카피 + 오디오 AAC 변환 (모드 3) let job = server.streams.create(StreamSpec { input: mkv, start_seconds: 0.0, video: VideoMode::Copy, audio: AudioMode::Aac, }); let url = server.stream_url(job); let res = reqwest::get(&url).await.unwrap(); assert_eq!(res.status(), 200); assert_eq!(res.headers()["content-type"], "video/mp4"); let body = res.bytes().await.unwrap(); assert!(body.len() > 10_000, "스트림이 너무 짧음: {} bytes", body.len()); // fMP4 시그니처: 4번째 바이트부터 'ftyp' assert_eq!(&body[4..8], b"ftyp", "fMP4 헤더가 아님"); // fragmented mp4는 moof 박스를 포함 assert!( body.windows(4).any(|w| w == b"moof"), "fragmented MP4가 아님 (moof 없음)" ); server.streams.stop(job); // 시킹 시나리오: 1.5초 지점에서 재기동 let job2 = server.streams.create(StreamSpec { input: dir.path().join("입력.mkv"), start_seconds: 1.5, video: VideoMode::Copy, audio: AudioMode::Aac, }); let res2 = reqwest::get(server.stream_url(job2)).await.unwrap(); assert_eq!(res2.status(), 200); let body2 = res2.bytes().await.unwrap(); assert!(body2.len() > 5_000); assert!(body2.len() < body.len(), "1.5초 이후 스트림이 전체보다 짧아야 함"); server.streams.stop(job2); }