diff --git a/backend/nsfwapp.exe b/backend/nsfwapp.exe index 7832d96..bfe30df 100644 Binary files a/backend/nsfwapp.exe and b/backend/nsfwapp.exe differ diff --git a/backend/record_job_progress.go b/backend/record_job_progress.go index e6e581d..dab4bb6 100644 --- a/backend/record_job_progress.go +++ b/backend/record_job_progress.go @@ -1,8 +1,15 @@ -// backend\record_job_progress.go - package main +import ( + "math" + "strings" +) + func setJobProgress(job *RecordJob, phase string, pct int) { + phase = strings.TrimSpace(phase) + phaseLower := strings.ToLower(phase) + + // clamp pct 0..100 if pct < 0 { pct = 0 } @@ -10,30 +17,76 @@ func setJobProgress(job *RecordJob, phase string, pct int) { pct = 100 } + // "globale" Zielbereiche pro Phase (dein Pipeline-Modell) + // postwork wartet: 70..72 + // remuxing: 72..78 + // moving: 78..84 + // probe: 84..86 + // assets: 86..99 + type rng struct{ start, end int } + rangeFor := func(ph string) rng { + switch ph { + case "postwork": + return rng{70, 72} + case "remuxing": + return rng{72, 78} + case "moving": + return rng{78, 84} + case "probe": + return rng{84, 86} + case "assets": + return rng{86, 99} + default: + return rng{0, 100} + } + } + jobsMu.Lock() defer jobsMu.Unlock() - // ✅ Sobald Postwork/Phase läuft oder Aufnahme beendet ist: - // keine "alten" Recorder-Updates mehr akzeptieren - if job.EndedAt != nil || (job.Phase != "" && job.Phase != "recording") { - // optional: trotzdem Phase aktualisieren, aber Progress nicht senken - if phase != "" { - job.Phase = phase + // Sobald Postwork läuft oder Aufnahme beendet ist -> Recorder darf NICHTS mehr überschreiben. + inPostwork := job.EndedAt != nil || (strings.TrimSpace(job.Phase) != "" && strings.ToLower(strings.TrimSpace(job.Phase)) != "recording") + if inPostwork { + // harte Blockade: alte recording-Updates dürfen weder Phase noch Progress anfassen + if phaseLower == "" || phaseLower == "recording" { + return } - if pct < job.Progress { - pct = job.Progress - } - job.Progress = pct - return } - // Recording-Phase: + // Phase aktualisieren (aber nur wenn nicht leer) if phase != "" { job.Phase = phase } - // ✅ niemals rückwärts - if pct < job.Progress { - pct = job.Progress + + // Progress-Logik: + // - wenn wir in Postwork sind und jemand phasenlokale 0..100 liefert (z.B. remuxing 25), + // mappe das in den globalen Bereich der Phase. + // - danach: niemals rückwärts. + mapped := pct + + if inPostwork { + r := rangeFor(phaseLower) + if r.start > 0 && r.end >= r.start { + // Wenn pct kleiner ist als unser globaler Einstiegspunkt, interpretieren wir ihn als lokal (0..100) + // und mappen in [start..end]. + if pct < r.start { + width := float64(r.end - r.start) + mapped = r.start + int(math.Round((float64(pct)/100.0)*width)) + } else { + // Wenn schon "global" geliefert wird, trotzdem in den Bereich begrenzen + if mapped < r.start { + mapped = r.start + } + if mapped > r.end { + mapped = r.end + } + } + } } - job.Progress = pct + + // niemals rückwärts + if mapped < job.Progress { + mapped = job.Progress + } + job.Progress = mapped } diff --git a/backend/record_start.go b/backend/record_start.go index ff4f182..f750bab 100644 --- a/backend/record_start.go +++ b/backend/record_start.go @@ -107,12 +107,7 @@ func runJob(ctx context.Context, job *RecordJob, req RecordRequest) { } // ✅ Phase für Recording explizit setzen (damit spätere Progress-Writer das erkennen können) - jobsMu.Lock() - job.Phase = "recording" - if job.Progress < 1 { - job.Progress = 1 - } - jobsMu.Unlock() + setJobProgress(job, "recording", 1) notifyJobsChanged() // ---- Aufnahme starten (Output-Pfad sauber relativ zur EXE auflösen) ---- @@ -304,15 +299,16 @@ func runJob(ctx context.Context, job *RecordJob, req RecordRequest) { // beim Start: Queue-Status refresh (sollte jetzt "running" werden) { st := postWorkQ.StatusForKey(postKey) + jobsMu.Lock() job.PostWork = &st - // optional: wenn du "queued" Progress optisch unterscheiden willst - if job.Phase == "postwork" && job.Progress < 71 { - job.Progress = 71 - } - jobsMu.Unlock() + + // optisches "queued" bumping + setJobProgress(job, "postwork", 71) + notifyJobsChanged() + } out := strings.TrimSpace(postOut) @@ -331,18 +327,15 @@ func runJob(ctx context.Context, job *RecordJob, req RecordRequest) { // Helper: Progress nur nach oben (gegen "rückwärts") setPhase := func(phase string, pct int) { - jobsMu.Lock() - if pct < job.Progress { - pct = job.Progress - } - job.Phase = phase - job.Progress = pct + // Phase+Progress inkl. Mapping/Monotonie + setJobProgress(job, phase, pct) - // Queue-Status auch bei Phase-Wechsel aktuell halten (nice für UI) + // Queue-Status aktuell halten st := postWorkQ.StatusForKey(postKey) + jobsMu.Lock() job.PostWork = &st - jobsMu.Unlock() + notifyJobsChanged() } diff --git a/backend/serve_video.go b/backend/serve_video.go index b8368df..dc84196 100644 --- a/backend/serve_video.go +++ b/backend/serve_video.go @@ -1,9 +1,12 @@ +// backend\serve_video.go + package main import ( "bytes" "context" "fmt" + "math" "net/http" "os" "path/filepath" @@ -104,9 +107,9 @@ func maybeRemuxTSForJob(job *RecordJob, path string) (string, error) { mp4 := strings.TrimSuffix(path, filepath.Ext(path)) + ".mp4" - // input size für fallback + // input size für fallback (optional für progress/ffmpeg) var inSize int64 - if fi, err := os.Stat(path); err == nil && !fi.IsDir() { + if fi, err := os.Stat(path); err == nil && fi != nil && !fi.IsDir() { inSize = fi.Size() } @@ -118,10 +121,8 @@ func maybeRemuxTSForJob(job *RecordJob, path string) (string, error) { cancel() } - const base = 10 - const span = 60 // 10..69 (70 startet "moving") - - lastProgress := base + // Throttle + monoton (lokal), globale Monotonie macht setJobProgress + lastProgress := -1 lastTick := time.Now().Add(-time.Second) onRatio := func(r float64) { @@ -131,21 +132,30 @@ func maybeRemuxTSForJob(job *RecordJob, path string) (string, error) { if r > 1 { r = 1 } - p := base + int(r*float64(span)) - if p >= 70 { - p = 69 + + p := int(math.Round(r * 100)) + if p < 0 { + p = 0 + } + if p > 100 { + p = 100 } + // nur steigen lassen if p <= lastProgress { return } - // leicht throttlen - if time.Since(lastTick) < 150*time.Millisecond && p < 79 { + + // leicht throttlen (außer kurz vor Schluss) + if time.Since(lastTick) < 150*time.Millisecond && p < 99 { return } + lastProgress = p lastTick = time.Now() - setJobPhase(job, "remuxing", p) + + // ✅ wichtig: 0..100 übergeben (Mapping macht setJobProgress) + setJobProgress(job, "remuxing", p) } remuxCtx, cancel := context.WithTimeout(context.Background(), 30*time.Minute) @@ -155,10 +165,12 @@ func maybeRemuxTSForJob(job *RecordJob, path string) (string, error) { return "", err } - _ = os.Remove(path) // TS entfernen, wenn MP4 ok - setJobPhase(job, "remuxing", 69) // ✅ Remux finished (nie rückwärts) - return mp4, nil + _ = os.Remove(path) // TS entfernen, wenn MP4 ok + // ✅ Remux finished + setJobProgress(job, "remuxing", 100) + + return mp4, nil } func moveToDoneDir(src string) (string, error) { diff --git a/backend/transcode.go b/backend/transcode.go index e228389..b09a5c6 100644 --- a/backend/transcode.go +++ b/backend/transcode.go @@ -411,17 +411,20 @@ func buildFFmpegStreamArgs(inPath string, prof TranscodeProfile) []string { gop := "60" vf := fmt.Sprintf("scale=-2:%d", prof.Height) - - // ✅ Fragmented MP4: spielbar bevor der File “fertig” ist - // empty_moov + moof fragments => Browser kann früh starten movflags := "frag_keyframe+empty_moov+default_base_moof" return []string{ "-hide_banner", "-loglevel", "error", + "-nostdin", "-y", "-i", inPath, + // ✅ robust (wie im File-Transcode) + "-map", "0:v:0?", + "-map", "0:a:0?", + "-sn", + "-vf", vf, "-c:v", "libx264", @@ -440,7 +443,6 @@ func buildFFmpegStreamArgs(inPath string, prof TranscodeProfile) []string { "-movflags", movflags, - // ✅ wichtig: Format explizit + Ausgabe in stdout "-f", "mp4", "pipe:1", }