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
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
}

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)
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()
}

View File

@ -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) {

View File

@ -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",
}