Archive v0.1.0 — 사진/영상 라이브러리 관리 프로그램
Tauri v2 + SolidJS + SQLite + ffmpeg 사이드카로 구현한 크로스플랫폼 사진·영상 관리 앱. 로컬/NAS(SFTP·WebDAV·FTP) 소스, 가상 그리드, 인앱 재생, 태그/이동/삭제/undo, 중복 탐지, 포터블 배포. - archive-db: SQLite 스키마·마이그레이션·단일 writer 스레드 + FTS5 trigram - archive-vfs: VFS 4백엔드(local/sftp/ftp/webdav) + 자격증명(키체인/볼트) - archive-indexer: 스캔·해시·썸네일·중복탐지·태그·파일작업·유지보수 - archive-media: localhost HTTP 미디어 서버(Range) + ffmpeg 스트림 잡 - 프론트: 3-pane UI, justified 가상 그리드, 라이트박스, 중복 검토 패널 Rust 테스트 49개 통과. CI: win x64/arm64 포터블 zip + macOS universal dmg. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
//! 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<PathBuf> {
|
||||
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<PathBuf> {
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user