diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 06fda34..cd17cd9 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -1286,26 +1286,69 @@ pub struct PlaybackInfo { #[tauri::command] pub fn playback_info(state: tauri::State<'_, AppState>, file_id: i64) -> CmdResult { let stream_available = state.tools.is_some(); - state + // (info, kind, 파일경로) — 경로는 메타 미추출 시 즉석 ffprobe용 + let (mut info, kind, path): (PlaybackInfo, i64, std::path::PathBuf) = state .db .with_read(move |conn| { 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], |r| { - Ok(PlaybackInfo { - ext: r.get(0)?, - vcodec: r.get(1)?, - acodec: r.get(2)?, - duration_ms: r.get(3)?, - width: r.get(4)?, - height: r.get(5)?, - stream_available, - }) + let dir: String = r.get(7)?; + let name: String = r.get(8)?; + Ok(( + PlaybackInfo { + ext: r.get(0)?, + vcodec: r.get(1)?, + acodec: r.get(2)?, + 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)] diff --git a/src/features/viewer/VideoPlayer.tsx b/src/features/viewer/VideoPlayer.tsx index beb699b..34af5e6 100644 --- a/src/features/viewer/VideoPlayer.tsx +++ b/src/features/viewer/VideoPlayer.tsx @@ -61,13 +61,14 @@ export const VideoPlayer: Component<{ fileId: number }> = (props) => { let streamSeekTimer: ReturnType | undefined; const isStream = () => plan()?.mode === "stream"; - // 재생 길이: 직접 모드는 요소 duration이 진실(메타 없어도 시크바 동작), - // 스트림 모드는 -ss로 잘린 요소 duration이 부정확하므로 메타데이터 우선. + // 재생 길이: ffprobe 총 길이(메타)를 항상 우선.