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 @@ - + +
⏳ 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)