diff --git a/app/main.py b/app/main.py
index ee25d8f..83fa29b 100644
--- a/app/main.py
+++ b/app/main.py
@@ -1233,14 +1233,58 @@ async def delete_job(job_id: str, user: str = Depends(check_auth)):
# ─── EDIT FEATURE ────────────────────────────────────────────────
@app.get("/api/source-video/{job_id}")
-async def source_video(job_id: str, user: str = Depends(check_auth)):
- """Vrne 16:9 original video za predogled v editorju."""
+async def source_video(job_id: str, quality: str = "high", user: str = Depends(check_auth)):
+ """Vrne 16:9 original video za predogled v editorju.
+
+ quality='high' (default): originalni file
+ quality='low': 480p re-encoded version (cached) — za hitro scrubbanje v editorju
+ """
job = load_job(job_id)
if not job:
raise HTTPException(404, "Ne obstaja")
src = job.get("input_path")
if not src or not Path(src).exists():
raise HTTPException(404, "Original video ne obstaja")
+
+ if quality == "low":
+ # 480p cached za hitro scrubbanje
+ cache_path = OUTPUT_DIR / f"{job_id}_source_low.mp4"
+ cache_valid = cache_path.exists() and cache_path.stat().st_size > 1024
+
+ if not cache_valid:
+ if cache_path.exists():
+ cache_path.unlink()
+ cmd = [
+ "ffmpeg", "-y",
+ "-i", str(src),
+ "-vf", "scale=854:480:force_original_aspect_ratio=decrease,scale=trunc(iw/2)*2:trunc(ih/2)*2",
+ "-c:v", "libx264",
+ "-preset", "veryfast", # malo boljša kvaliteta od ultrafast
+ "-crf", "28",
+ "-c:a", "aac",
+ "-b:a", "96k",
+ "-movflags", "+faststart",
+ "-loglevel", "error",
+ str(cache_path),
+ ]
+ try:
+ proc = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
+ if proc.returncode != 0 or not cache_path.exists() or cache_path.stat().st_size < 1024:
+ if cache_path.exists():
+ cache_path.unlink()
+ raise HTTPException(500, f"FFmpeg failed: {(proc.stderr or 'unknown')[-300:]}")
+ except subprocess.TimeoutExpired:
+ if cache_path.exists():
+ cache_path.unlink()
+ raise HTTPException(500, "Low-q source render timeout (>120s)")
+
+ return FileResponse(
+ path=cache_path,
+ media_type="video/mp4",
+ filename=f"source_low_{job_id}.mp4",
+ headers={"Accept-Ranges": "bytes", "Cache-Control": "max-age=3600"},
+ )
+
return FileResponse(
path=src,
media_type="video/mp4",
diff --git a/templates/index.html b/templates/index.html
index a992e30..c9c6458 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -1027,7 +1027,8 @@
✏️ Edit: ${escapeHtml(title)}
-
+
+ ⏳ Pripravljam predogled (~5s prvič, potem instant)…
@@ -1222,72 +1223,36 @@
}
};
- // ─── LIVE PREVIEW: naloži low-q clip označenega dela ───
- window.previewSelection = async function() {
- const status = document.getElementById("preview-status");
- const playBtn = document.getElementById("preview-btn");
- const start = trimStart;
- const end = trimEnd;
-
- if (end - start < 1) {
- alert("Trajanje vsaj 1s");
- return;
- }
-
- playBtn.disabled = true;
- playBtn.textContent = "⏳ Renderiram preview...";
- status.textContent = `🎬 Renderiram odsek ${formatTime(start)} - ${formatTime(end)} (~3s)...`;
-
- const url = `/api/preview-clip/${jobId}?start=${start.toFixed(1)}&end=${end.toFixed(1)}`;
- const t0 = Date.now();
-
- try {
- // Preverim ali je preview že cached (HEAD request)
- const headRes = await fetch(url, { method: "HEAD" });
- if (!headRes.ok) {
- // Cache miss → trigger render z GET
- const getRes = await fetch(url);
- if (!getRes.ok) {
- const err = await getRes.json().catch(() => ({}));
- status.textContent = "❌ " + (err.detail || "Render failed");
- playBtn.disabled = false;
- playBtn.textContent = "▶ Predvajaj odsek";
- return;
- }
- }
-
- const elapsed = ((Date.now() - t0) / 1000).toFixed(1);
- status.textContent = `✅ Pripravljen v ${elapsed}s. Predvajam...`;
-
- // Set video src to preview clip
- video.src = url;
- video.load();
- video.addEventListener("loadeddata", function loadedOnce() {
- video.removeEventListener("loadeddata", loadedOnce);
- video.currentTime = 0;
- video.play();
- }, { once: true });
-
- // Auto-pojavi success
- setTimeout(() => { status.textContent = ""; }, 3000);
- } catch (e) {
- status.textContent = "❌ Napaka: " + e.message;
- } finally {
- playBtn.disabled = false;
- playBtn.textContent = "▶ Predvajaj odsek";
- }
+ // ─── INSTANT PREVIEW: predvajaj označen del (od trimStart, auto-stop pri trimEnd) ───
+ window.previewSelection = function() {
+ if (!video) return;
+ // Skoči na trim start in predvajaj
+ video.currentTime = trimStart;
+ video.play().catch(err => {
+ console.warn("Play failed:", err);
+ });
};
- // Auto-stop predvajanje ko doseže trimEnd (samo če gledamo full video)
+ // Auto-stop ko doseže trimEnd (brez render-a, brez preview clip URL)
+ // Samo če NI v aktivnem dragu (ker drag = naročiteljsko seekanje)
if (video) {
video.addEventListener("timeupdate", () => {
- // Če gledamo preview clip (src vsebuje '/api/preview-clip/'), ne ustavi
- if (video.src && video.src.indexOf("/api/preview-clip/") !== -1) return;
+ if (dragging) return; // Ne ustavi med dragom
if (!video.paused && video.currentTime >= trimEnd) {
video.pause();
video.currentTime = trimStart;
}
});
+
+ // Source-status: skrij ko se naloži
+ const sourceStatus = document.getElementById("source-status");
+ video.addEventListener("loadeddata", () => {
+ if (sourceStatus) sourceStatus.textContent = "✅ Predogled pripravljen — drag ročajev za fine-tune";
+ setTimeout(() => { if (sourceStatus) sourceStatus.style.display = "none"; }, 2000);
+ });
+ video.addEventListener("error", () => {
+ if (sourceStatus) sourceStatus.textContent = "❌ Napaka pri nalaganju predogleda";
+ });
}
// Initial render — počakaj da DOM ima dimenzije (modal je bil pravkar dodan)