This commit is contained in:
Linrador 2026-02-09 14:42:56 +01:00
parent cb4ecfb889
commit 76ea79a1a9
5 changed files with 116 additions and 56 deletions

Binary file not shown.

View File

@ -1,8 +1,15 @@
// backend\record_job_progress.go
package main package main
import (
"math"
"strings"
)
func setJobProgress(job *RecordJob, phase string, pct int) { func setJobProgress(job *RecordJob, phase string, pct int) {
phase = strings.TrimSpace(phase)
phaseLower := strings.ToLower(phase)
// clamp pct 0..100
if pct < 0 { if pct < 0 {
pct = 0 pct = 0
} }
@ -10,30 +17,76 @@ func setJobProgress(job *RecordJob, phase string, pct int) {
pct = 100 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() jobsMu.Lock()
defer jobsMu.Unlock() defer jobsMu.Unlock()
// ✅ Sobald Postwork/Phase läuft oder Aufnahme beendet ist: // Sobald Postwork läuft oder Aufnahme beendet ist -> Recorder darf NICHTS mehr überschreiben.
// keine "alten" Recorder-Updates mehr akzeptieren inPostwork := job.EndedAt != nil || (strings.TrimSpace(job.Phase) != "" && strings.ToLower(strings.TrimSpace(job.Phase)) != "recording")
if job.EndedAt != nil || (job.Phase != "" && job.Phase != "recording") { if inPostwork {
// optional: trotzdem Phase aktualisieren, aber Progress nicht senken // harte Blockade: alte recording-Updates dürfen weder Phase noch Progress anfassen
if phase != "" { if phaseLower == "" || phaseLower == "recording" {
job.Phase = phase return
} }
if pct < job.Progress {
pct = job.Progress
}
job.Progress = pct
return
} }
// Recording-Phase: // Phase aktualisieren (aber nur wenn nicht leer)
if phase != "" { if phase != "" {
job.Phase = phase job.Phase = phase
} }
// ✅ niemals rückwärts
if pct < job.Progress { // Progress-Logik:
pct = job.Progress // - 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
} }

View File

@ -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) // ✅ Phase für Recording explizit setzen (damit spätere Progress-Writer das erkennen können)
jobsMu.Lock() setJobProgress(job, "recording", 1)
job.Phase = "recording"
if job.Progress < 1 {
job.Progress = 1
}
jobsMu.Unlock()
notifyJobsChanged() notifyJobsChanged()
// ---- Aufnahme starten (Output-Pfad sauber relativ zur EXE auflösen) ---- // ---- 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) // beim Start: Queue-Status refresh (sollte jetzt "running" werden)
{ {
st := postWorkQ.StatusForKey(postKey) st := postWorkQ.StatusForKey(postKey)
jobsMu.Lock() jobsMu.Lock()
job.PostWork = &st job.PostWork = &st
// optional: wenn du "queued" Progress optisch unterscheiden willst
if job.Phase == "postwork" && job.Progress < 71 {
job.Progress = 71
}
jobsMu.Unlock() jobsMu.Unlock()
// optisches "queued" bumping
setJobProgress(job, "postwork", 71)
notifyJobsChanged() notifyJobsChanged()
} }
out := strings.TrimSpace(postOut) 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") // Helper: Progress nur nach oben (gegen "rückwärts")
setPhase := func(phase string, pct int) { setPhase := func(phase string, pct int) {
jobsMu.Lock() // Phase+Progress inkl. Mapping/Monotonie
if pct < job.Progress { setJobProgress(job, phase, pct)
pct = job.Progress
}
job.Phase = phase
job.Progress = pct
// Queue-Status auch bei Phase-Wechsel aktuell halten (nice für UI) // Queue-Status aktuell halten
st := postWorkQ.StatusForKey(postKey) st := postWorkQ.StatusForKey(postKey)
jobsMu.Lock()
job.PostWork = &st job.PostWork = &st
jobsMu.Unlock() jobsMu.Unlock()
notifyJobsChanged() notifyJobsChanged()
} }

View File

@ -1,9 +1,12 @@
// backend\serve_video.go
package main package main
import ( import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"math"
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
@ -104,9 +107,9 @@ func maybeRemuxTSForJob(job *RecordJob, path string) (string, error) {
mp4 := strings.TrimSuffix(path, filepath.Ext(path)) + ".mp4" 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 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() inSize = fi.Size()
} }
@ -118,10 +121,8 @@ func maybeRemuxTSForJob(job *RecordJob, path string) (string, error) {
cancel() cancel()
} }
const base = 10 // Throttle + monoton (lokal), globale Monotonie macht setJobProgress
const span = 60 // 10..69 (70 startet "moving") lastProgress := -1
lastProgress := base
lastTick := time.Now().Add(-time.Second) lastTick := time.Now().Add(-time.Second)
onRatio := func(r float64) { onRatio := func(r float64) {
@ -131,21 +132,30 @@ func maybeRemuxTSForJob(job *RecordJob, path string) (string, error) {
if r > 1 { if r > 1 {
r = 1 r = 1
} }
p := base + int(r*float64(span))
if p >= 70 { p := int(math.Round(r * 100))
p = 69 if p < 0 {
p = 0
}
if p > 100 {
p = 100
} }
// nur steigen lassen
if p <= lastProgress { if p <= lastProgress {
return 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 return
} }
lastProgress = p lastProgress = p
lastTick = time.Now() 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) remuxCtx, cancel := context.WithTimeout(context.Background(), 30*time.Minute)
@ -155,10 +165,12 @@ func maybeRemuxTSForJob(job *RecordJob, path string) (string, error) {
return "", err return "", err
} }
_ = os.Remove(path) // TS entfernen, wenn MP4 ok _ = os.Remove(path) // TS entfernen, wenn MP4 ok
setJobPhase(job, "remuxing", 69) // ✅ Remux finished (nie rückwärts)
return mp4, nil
// ✅ Remux finished
setJobProgress(job, "remuxing", 100)
return mp4, nil
} }
func moveToDoneDir(src string) (string, error) { func moveToDoneDir(src string) (string, error) {

View File

@ -411,17 +411,20 @@ func buildFFmpegStreamArgs(inPath string, prof TranscodeProfile) []string {
gop := "60" gop := "60"
vf := fmt.Sprintf("scale=-2:%d", prof.Height) 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" movflags := "frag_keyframe+empty_moov+default_base_moof"
return []string{ return []string{
"-hide_banner", "-hide_banner",
"-loglevel", "error", "-loglevel", "error",
"-nostdin",
"-y", "-y",
"-i", inPath, "-i", inPath,
// ✅ robust (wie im File-Transcode)
"-map", "0:v:0?",
"-map", "0:a:0?",
"-sn",
"-vf", vf, "-vf", vf,
"-c:v", "libx264", "-c:v", "libx264",
@ -440,7 +443,6 @@ func buildFFmpegStreamArgs(inPath string, prof TranscodeProfile) []string {
"-movflags", movflags, "-movflags", movflags,
// ✅ wichtig: Format explizit + Ausgabe in stdout
"-f", "mp4", "-f", "mp4",
"pipe:1", "pipe:1",
} }