From 19faed7a403ffdd310f1c5fd9a7e9ab51b1d3eab Mon Sep 17 00:00:00 2001 From: Linrador <68631622+Linrador@users.noreply.github.com> Date: Sat, 2 May 2026 21:19:15 +0200 Subject: [PATCH] bugfixes --- backend/record_stream_hls.go | 93 +++++++++++++++++----- frontend/src/components/ui/TrainingTab.tsx | 33 +++++--- 2 files changed, 96 insertions(+), 30 deletions(-) diff --git a/backend/record_stream_hls.go b/backend/record_stream_hls.go index 4fdbf25..36bbe5b 100644 --- a/backend/record_stream_hls.go +++ b/backend/record_stream_hls.go @@ -368,7 +368,7 @@ func appendFileAndRemove(dstPath, srcPath string) error { return err } if sfi == nil || sfi.IsDir() || sfi.Size() <= 0 { - _ = os.Remove(srcPath) + _ = removeWithRetry(srcPath) return nil } @@ -376,19 +376,55 @@ func appendFileAndRemove(dstPath, srcPath string) error { if err != nil { return err } - defer src.Close() dst, err := os.OpenFile(dstPath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644) if err != nil { - return err - } - defer dst.Close() - - if _, err := io.Copy(dst, src); err != nil { + _ = src.Close() return err } - return os.Remove(srcPath) + _, copyErr := io.Copy(dst, src) + + closeSrcErr := src.Close() + closeDstErr := dst.Close() + + if copyErr != nil { + return copyErr + } + if closeSrcErr != nil { + return closeSrcErr + } + if closeDstErr != nil { + return closeDstErr + } + + if err := removeWithRetry(srcPath); err != nil && !os.IsNotExist(err) { + return err + } + + return nil +} + +func cleanupHLSRetryChunks(outFile string) { + dir := filepath.Dir(outFile) + ext := filepath.Ext(outFile) + base := strings.TrimSuffix(filepath.Base(outFile), ext) + + canonical := canonicalAssetIDFromName(base) + if canonical == "" { + canonical = base + } + + pattern := filepath.Join(dir, fmt.Sprintf(".%s.retry.*%s", canonical, ext)) + + matches, err := filepath.Glob(pattern) + if err != nil { + return + } + + for _, p := range matches { + _ = removeWithRetry(p) + } } func handleM3U8Mode( @@ -419,10 +455,26 @@ func handleM3U8Mode( } } - if strings.TrimSpace(outFile) == "" { + outFile = strings.TrimSpace(outFile) + if outFile == "" { return errors.New("output file path leer") } + cleanupHLSRetryChunks(outFile) + defer cleanupHLSRetryChunks(outFile) + + ext := strings.ToLower(filepath.Ext(outFile)) + if ext != ".ts" && ext != ".mp4" { + return fmt.Errorf("nicht unterstützte output-endung: %q", ext) + } + + // Wichtig: ffmpeg schreibt in temporäre Retry-Datei. + // Danach wird sie an outFile angehängt und gelöscht. + retryOut := hlsRetryChunkPath(outFile, 1) + defer func() { + _ = removeWithRetry(retryOut) + }() + args := []string{ "-y", "-hide_banner", @@ -452,8 +504,6 @@ func handleM3U8Mode( return args } - ext := strings.ToLower(filepath.Ext(strings.TrimSpace(outFile))) - if audioURL != "" { args = appendInputOptions(args) args = append(args, "-i", videoURL) @@ -484,21 +534,19 @@ func handleM3U8Mode( case ".ts": args = append(args, "-f", "mpegts", - outFile, + retryOut, ) case ".mp4": args = append(args, "-movflags", "+faststart", - outFile, + retryOut, ) - default: - return fmt.Errorf("nicht unterstützte output-endung: %q", ext) } cmd := exec.CommandContext(ctx, ffmpegPath, args...) cmd.SysProcAttr = &syscall.SysProcAttr{ HideWindow: true, - CreationFlags: 0x08000000, // CREATE_NO_WINDOW + CreationFlags: 0x08000000, } var stderr bytes.Buffer @@ -520,16 +568,15 @@ func handleM3U8Mode( case <-stopStat: return case <-t.C: - fi, err := os.Stat(outFile) - if err != nil || fi == nil || fi.IsDir() { - continue - } + mainSize := safeFileSize(outFile) + retrySize := safeFileSize(retryOut) + sz := mainSize + retrySize - sz := fi.Size() if sz > 0 && sz != last { jobsMu.Lock() job.SizeBytes = sz jobsMu.Unlock() + last = sz _ = publishJob(job.ID) } @@ -549,6 +596,10 @@ func handleM3U8Mode( return fmt.Errorf("ffmpeg m3u8 failed: %w", err) } + if err := appendFileAndRemove(outFile, retryOut); err != nil { + return fmt.Errorf("retry chunk übernehmen fehlgeschlagen: %w", err) + } + if job != nil { if fi, statErr := os.Stat(outFile); statErr == nil && fi != nil && !fi.IsDir() { jobsMu.Lock() diff --git a/frontend/src/components/ui/TrainingTab.tsx b/frontend/src/components/ui/TrainingTab.tsx index 2c67020..4018841 100644 --- a/frontend/src/components/ui/TrainingTab.tsx +++ b/frontend/src/components/ui/TrainingTab.tsx @@ -2043,9 +2043,9 @@ export default function TrainingTab(props: { progressTimer = window.setInterval(() => { setAnalysisProgress((value) => { - if (value < 35) return value + 4 - if (value < 65) return value + 2 - if (value < 88) return value + 1 + if (value < 35) return Math.min(35, value + 4) + if (value < 65) return Math.min(65, value + 2) + if (value < 95) return Math.min(95, value + 1) return value }) }, 350) @@ -2117,7 +2117,7 @@ export default function TrainingTab(props: { setLoading(false) setAnalysisProgress(0) setAnalysisStep('') - }, 150) + }, 500) } }, []) @@ -2159,6 +2159,8 @@ export default function TrainingTab(props: { setTraining(Boolean(job?.running)) if (job?.message && !job.running) { + setTrainingProgress(100) + setTrainingStep('Training abgeschlossen.') setMessage((prev) => prev || String(job.message)) } @@ -2311,9 +2313,12 @@ export default function TrainingTab(props: { useEffect(() => { if (!trainingRunning) { - setTrainingProgress(0) - setTrainingStep('') - return + const timer = window.setTimeout(() => { + setTrainingProgress(0) + setTrainingStep('') + }, 800) + + return () => window.clearTimeout(timer) } const serverProgress = Number(trainingStatus?.training?.progress ?? 0) @@ -3208,8 +3213,18 @@ export default function TrainingTab(props: { disabled={uiLocked} className={[ isActiveBox - ? 'pointer-events-auto absolute z-20 flex h-7 w-7 touch-none items-center justify-center rounded-full bg-transparent p-0 opacity-100 disabled:opacity-50 sm:h-5 sm:w-5' - : 'pointer-events-none absolute z-20 hidden h-7 w-7 touch-none items-center justify-center rounded-full bg-transparent p-0 opacity-100 disabled:opacity-50 sm:h-5 sm:w-5', + ? [ + 'pointer-events-auto absolute z-20 flex h-7 w-7 touch-none items-center justify-center', + 'appearance-none rounded-none border-0 p-0 opacity-100 shadow-none ring-0', + '!bg-transparent hover:!bg-transparent active:!bg-transparent focus:!bg-transparent', + 'focus:outline-none disabled:opacity-50 sm:h-5 sm:w-5', + ].join(' ') + : [ + 'pointer-events-none absolute z-20 hidden h-7 w-7 touch-none items-center justify-center', + 'appearance-none rounded-none border-0 p-0 opacity-100 shadow-none ring-0', + '!bg-transparent hover:!bg-transparent active:!bg-transparent focus:!bg-transparent', + 'focus:outline-none disabled:opacity-50 sm:h-5 sm:w-5', + ].join(' '), handle === 'nw' ? '-left-3.5 -top-3.5 cursor-nwse-resize sm:-left-2.5 sm:-top-2.5' : '', handle === 'ne' ? '-right-3.5 -top-3.5 cursor-nesw-resize sm:-right-2.5 sm:-top-2.5' : '', handle === 'sw' ? '-bottom-3.5 -left-3.5 cursor-nesw-resize sm:-bottom-2.5 sm:-left-2.5' : '',