재생바 총길이 안정화 — ffprobe 메타 우선 + 없으면 즉석 ffprobe

증상: 영상 재생 시 재생바가 실시간으로 늘어나며 요동침. 총 재생시간이
고정되지 않음.
원인: durationSec가 직접 모드에서 <video>.duration을 우선 사용했는데, 스트림/
일부 컨테이너(리먹스 fMP4 등)에선 재생 중 요소 duration이 계속 커져 재생바가
current/duration 비율로 요동쳤다. 또 메타(duration_ms)가 아직 추출 안 된 영상은
playback_info가 null을 반환해 요소 duration에 의존.

수정:
- VideoPlayer.durationSec: ffprobe 메타 총길이를 항상 우선, 없을 때만 요소 duration
  폴백. (고정 총길이라 재생바 안정)
- playback_info: 영상인데 duration_ms가 null이면 즉석 ffprobe로 채우고 DB에 캐시
  (SMB 등 메타 미추출 상태에서도 정확한 총길이/코덱 보장, 다음 재생부터 재프로브 없음).

검증(VP9 20s): playback_info durationMs=20000, 재생 중 표시 0:00→ 진행하며 총길이는
0:20 고정, 진행바 0%→4%→8% 단조 증가(요동 없음).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
강 한
2026-07-19 23:17:28 +09:00
co-authored by Claude Fable 5
parent ffa737be98
commit 2072f9f27e
2 changed files with 60 additions and 16 deletions
+55 -12
View File
@@ -1286,26 +1286,69 @@ pub struct PlaybackInfo {
#[tauri::command] #[tauri::command]
pub fn playback_info(state: tauri::State<'_, AppState>, file_id: i64) -> CmdResult<PlaybackInfo> { pub fn playback_info(state: tauri::State<'_, AppState>, file_id: i64) -> CmdResult<PlaybackInfo> {
let stream_available = state.tools.is_some(); let stream_available = state.tools.is_some();
state // (info, kind, 파일경로) — 경로는 메타 미추출 시 즉석 ffprobe용
let (mut info, kind, path): (PlaybackInfo, i64, std::path::PathBuf) = state
.db .db
.with_read(move |conn| { .with_read(move |conn| {
conn.query_row( conn.query_row(
"SELECT ext, vcodec, acodec, duration_ms, width, height FROM files WHERE id = ?1", "SELECT f.ext, f.vcodec, f.acodec, f.duration_ms, f.width, f.height, f.kind, fo.path, f.name
FROM files f JOIN folders fo ON fo.id = f.folder_id WHERE f.id = ?1",
[file_id], [file_id],
|r| { |r| {
Ok(PlaybackInfo { let dir: String = r.get(7)?;
ext: r.get(0)?, let name: String = r.get(8)?;
vcodec: r.get(1)?, Ok((
acodec: r.get(2)?, PlaybackInfo {
duration_ms: r.get(3)?, ext: r.get(0)?,
width: r.get(4)?, vcodec: r.get(1)?,
height: r.get(5)?, acodec: r.get(2)?,
stream_available, duration_ms: r.get(3)?,
}) width: r.get(4)?,
height: r.get(5)?,
stream_available,
},
r.get::<_, i64>(6)?,
std::path::Path::new(&dir).join(name),
))
}, },
) )
}) })
.map_err(err_str) .map_err(err_str)?;
// 영상인데 메타(재생시간)가 아직 없으면 즉석 ffprobe로 채운다.
// 재생바 총길이가 요소 duration(스트림 중 증가)에 의존하지 않도록 정확한 총길이를 보장.
if info.duration_ms.is_none() && kind == 1 {
if let Some(tools) = &state.tools {
if let Ok(mi) = archive_indexer::ffmpeg::probe(tools, &path) {
info.duration_ms = mi.duration_ms;
if info.vcodec.is_none() {
info.vcodec = mi.vcodec.clone();
}
if info.acodec.is_none() {
info.acodec = mi.acodec.clone();
}
if info.width.is_none() {
info.width = mi.width;
}
if info.height.is_none() {
info.height = mi.height;
}
// DB에 캐시 (같은 영상 재생 시 SMB 재프로브 방지). meta_state 미완이면 완료 표시.
let (d, vc, ac, w, h) = (mi.duration_ms, mi.vcodec, mi.acodec, mi.width, mi.height);
let _ = state.db.with_write(move |conn| {
conn.execute(
"UPDATE files SET duration_ms=COALESCE(duration_ms,?2),
vcodec=COALESCE(vcodec,?3), acodec=COALESCE(acodec,?4),
width=COALESCE(width,?5), height=COALESCE(height,?6),
meta_state=CASE WHEN meta_state=0 THEN 1 ELSE meta_state END
WHERE id=?1",
rusqlite::params![file_id, d, vc, ac, w, h],
)
});
}
}
}
Ok(info)
} }
#[derive(serde::Serialize)] #[derive(serde::Serialize)]
+5 -4
View File
@@ -61,13 +61,14 @@ export const VideoPlayer: Component<{ fileId: number }> = (props) => {
let streamSeekTimer: ReturnType<typeof setTimeout> | undefined; let streamSeekTimer: ReturnType<typeof setTimeout> | undefined;
const isStream = () => plan()?.mode === "stream"; const isStream = () => plan()?.mode === "stream";
// 재생 길이: 직접 모드는 요소 duration이 진실(메타 없어도 시크바 동작), // 재생 길이: ffprobe 총 길이(메타)를 항상 우선. <video> 요소 duration은 스트림·일부
// 스트림 모드는 -ss로 잘린 요소 duration이 부정확하므로 메타데이터 우선. // 컨테이너(리먹스 fMP4 등)에서 재생 중 실시간으로 늘어나 재생바가 요동치므로,
// 메타가 있으면 그 고정 총길이를 쓰고, 없을 때만 요소 duration으로 폴백한다.
const durationSec = () => { const durationSec = () => {
const meta = (pb()?.durationMs ?? 0) / 1000; const meta = (pb()?.durationMs ?? 0) / 1000;
if (meta > 0) return meta;
const ed = elDuration(); const ed = elDuration();
if (isStream()) return meta > 0 ? meta : ed; return ed > 0 ? ed : 0;
return ed > 0 ? ed : meta;
}; };
const cleanupJob = () => { const cleanupJob = () => {