fixed cb stream error
This commit is contained in:
parent
80b9f69597
commit
013f616303
114
backend/main.go
114
backend/main.go
@ -12,6 +12,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -1294,7 +1295,7 @@ func renameJobsOutputBasename(oldFile, newFile string) {
|
|||||||
|
|
||||||
// nimmt jetzt *HTTPClient entgegen
|
// nimmt jetzt *HTTPClient entgegen
|
||||||
func FetchPlaylist(ctx context.Context, hc *HTTPClient, hlsSource, httpCookie string) (*Playlist, error) {
|
func FetchPlaylist(ctx context.Context, hc *HTTPClient, hlsSource, httpCookie string) (*Playlist, error) {
|
||||||
if hlsSource == "" {
|
if strings.TrimSpace(hlsSource) == "" {
|
||||||
return nil, errors.New("HLS-URL leer")
|
return nil, errors.New("HLS-URL leer")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1309,51 +1310,122 @@ func FetchPlaylist(ctx context.Context, hc *HTTPClient, hlsSource, httpCookie st
|
|||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
return nil, fmt.Errorf("master playlist bad status: %d %s", resp.StatusCode, hlsSource)
|
||||||
|
}
|
||||||
|
|
||||||
playlist, listType, err := m3u8.DecodeFrom(resp.Body, true)
|
playlist, listType, err := m3u8.DecodeFrom(resp.Body, true)
|
||||||
if err != nil || listType != m3u8.MASTER {
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("master playlist decode failed: %w", err)
|
||||||
|
}
|
||||||
|
if listType != m3u8.MASTER {
|
||||||
return nil, errors.New("keine gültige Master-Playlist")
|
return nil, errors.New("keine gültige Master-Playlist")
|
||||||
}
|
}
|
||||||
|
|
||||||
master := playlist.(*m3u8.MasterPlaylist)
|
master, ok := playlist.(*m3u8.MasterPlaylist)
|
||||||
var bestURI string
|
if !ok || master == nil {
|
||||||
|
return nil, errors.New("master playlist parse fehlgeschlagen")
|
||||||
|
}
|
||||||
|
|
||||||
|
baseURL, err := url.Parse(hlsSource)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("master url parse failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var bestVariant *m3u8.Variant
|
||||||
var bestWidth int
|
var bestWidth int
|
||||||
var bestFramerate int
|
var bestFramerate int
|
||||||
|
|
||||||
for _, variant := range master.Variants {
|
parseFramerate := func(v *m3u8.Variant) int {
|
||||||
if variant == nil || variant.Resolution == "" {
|
if v == nil {
|
||||||
continue
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
parts := strings.Split(variant.Resolution, "x")
|
// 1) Falls BANDWIDTH/FRAME-RATE Infos im Namen kodiert sind
|
||||||
|
name := strings.ToUpper(strings.TrimSpace(v.Name))
|
||||||
|
switch {
|
||||||
|
case strings.Contains(name, "FPS:60"), strings.Contains(name, "FPS=60"), strings.Contains(name, "60FPS"):
|
||||||
|
return 60
|
||||||
|
case strings.Contains(name, "FPS:50"), strings.Contains(name, "FPS=50"), strings.Contains(name, "50FPS"):
|
||||||
|
return 50
|
||||||
|
case strings.Contains(name, "FPS:30"), strings.Contains(name, "FPS=30"), strings.Contains(name, "30FPS"):
|
||||||
|
return 30
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Fallback Standard
|
||||||
|
return 30
|
||||||
|
}
|
||||||
|
|
||||||
|
parseWidth := func(v *m3u8.Variant) int {
|
||||||
|
if v == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
res := strings.TrimSpace(v.Resolution)
|
||||||
|
if res == "" {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.Split(res, "x")
|
||||||
if len(parts) != 2 {
|
if len(parts) != 2 {
|
||||||
continue
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
width, err := strconv.Atoi(parts[0])
|
w, err := strconv.Atoi(strings.TrimSpace(parts[0]))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return w
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, variant := range master.Variants {
|
||||||
|
if variant == nil || strings.TrimSpace(variant.URI) == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
fr := 30
|
width := parseWidth(variant)
|
||||||
if strings.Contains(variant.Name, "FPS:60.0") {
|
fr := parseFramerate(variant)
|
||||||
fr = 60
|
|
||||||
}
|
|
||||||
|
|
||||||
if width > bestWidth || (width == bestWidth && fr > bestFramerate) {
|
// auch Varianten ohne Resolution zulassen, aber niedriger priorisieren
|
||||||
|
if bestVariant == nil ||
|
||||||
|
width > bestWidth ||
|
||||||
|
(width == bestWidth && fr > bestFramerate) {
|
||||||
|
bestVariant = variant
|
||||||
bestWidth = width
|
bestWidth = width
|
||||||
bestFramerate = fr
|
bestFramerate = fr
|
||||||
bestURI = variant.URI
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if bestURI == "" {
|
if bestVariant == nil {
|
||||||
return nil, errors.New("keine gültige Auflösung gefunden")
|
return nil, errors.New("keine gültige Variante gefunden")
|
||||||
|
}
|
||||||
|
|
||||||
|
variantRef, err := url.Parse(strings.TrimSpace(bestVariant.URI))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("variant uri parse failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
playlistURL := baseURL.ResolveReference(variantRef).String()
|
||||||
|
|
||||||
|
playlistParsed, err := url.Parse(playlistURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("playlist url parse failed: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rootParsed := *playlistParsed
|
||||||
|
rootParsed.RawQuery = ""
|
||||||
|
rootParsed.Fragment = ""
|
||||||
|
|
||||||
|
lastSlash := strings.LastIndex(rootParsed.Path, "/")
|
||||||
|
if lastSlash >= 0 {
|
||||||
|
rootParsed.Path = rootParsed.Path[:lastSlash+1]
|
||||||
|
} else {
|
||||||
|
rootParsed.Path = "/"
|
||||||
}
|
}
|
||||||
|
|
||||||
root := hlsSource[:strings.LastIndex(hlsSource, "/")+1]
|
|
||||||
return &Playlist{
|
return &Playlist{
|
||||||
PlaylistURL: root + bestURI,
|
PlaylistURL: playlistURL,
|
||||||
RootURL: root,
|
RootURL: rootParsed.String(),
|
||||||
Resolution: bestWidth,
|
Resolution: bestWidth,
|
||||||
Framerate: bestFramerate,
|
Framerate: bestFramerate,
|
||||||
}, nil
|
}, nil
|
||||||
|
|||||||
@ -1323,51 +1323,121 @@ func extractFirstFrameJPGScaled(path string, width int, quality int) ([]byte, er
|
|||||||
}
|
}
|
||||||
|
|
||||||
func latestPreviewSegment(previewDir string) (string, error) {
|
func latestPreviewSegment(previewDir string) (string, error) {
|
||||||
entries, err := os.ReadDir(previewDir)
|
segDir := previewSegmentsDir(previewDir)
|
||||||
|
entries, err := os.ReadDir(segDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
var best string
|
var best string
|
||||||
for _, e := range entries {
|
for _, e := range entries {
|
||||||
if e.IsDir() {
|
if e.IsDir() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
name := e.Name()
|
|
||||||
if !strings.HasPrefix(name, "seg_low_") && !strings.HasPrefix(name, "seg_hq_") {
|
name := strings.ToLower(e.Name())
|
||||||
|
if !strings.HasPrefix(name, "seg_preview_") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if best == "" || name > best {
|
if best == "" || name > best {
|
||||||
best = name
|
best = e.Name()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if best == "" {
|
if best == "" {
|
||||||
return "", fmt.Errorf("kein Preview-Segment in %s", previewDir)
|
return "", fmt.Errorf("kein Preview-Segment in %s", segDir)
|
||||||
}
|
}
|
||||||
return filepath.Join(previewDir, best), nil
|
return filepath.Join(segDir, best), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildPreviewInputFromDir(previewDir string) (string, func(), error) {
|
||||||
|
segDir := previewSegmentsDir(previewDir)
|
||||||
|
if strings.TrimSpace(segDir) == "" {
|
||||||
|
return "", nil, fmt.Errorf("segments dir fehlt für %s", previewDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
segPath, err := latestPreviewSegment(previewDir)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ext := strings.ToLower(filepath.Ext(segPath))
|
||||||
|
|
||||||
|
// TS kann direkt gelesen werden
|
||||||
|
if ext == ".ts" {
|
||||||
|
return segPath, func() {}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fMP4: init.mp4 + segment.m4s zu temp zusammensetzen
|
||||||
|
initPath := filepath.Join(segDir, "init.mp4")
|
||||||
|
if _, err := os.Stat(initPath); err != nil {
|
||||||
|
return "", nil, fmt.Errorf("init.mp4 fehlt in %s", segDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
initBytes, err := os.ReadFile(initPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, fmt.Errorf("init lesen fehlgeschlagen: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
segBytes, err := os.ReadFile(segPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, fmt.Errorf("segment lesen fehlgeschlagen: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmp, err := os.CreateTemp(segDir, "preview_input_*.mp4")
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
tmpPath := tmp.Name()
|
||||||
|
|
||||||
|
cleanup := func() {
|
||||||
|
_ = tmp.Close()
|
||||||
|
_ = os.Remove(tmpPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := tmp.Write(initBytes); err != nil {
|
||||||
|
cleanup()
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
if _, err := tmp.Write(segBytes); err != nil {
|
||||||
|
cleanup()
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
if err := tmp.Close(); err != nil {
|
||||||
|
cleanup()
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tmpPath, cleanup, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractLastFrameFromPreviewDirThumbJPG(previewDir string) ([]byte, error) {
|
func extractLastFrameFromPreviewDirThumbJPG(previewDir string) ([]byte, error) {
|
||||||
seg, err := latestPreviewSegment(previewDir)
|
inputPath, cleanup, err := buildPreviewInputFromDir(previewDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
img, err := extractLastFrameJPGScaled(seg, 320, 70)
|
defer cleanup()
|
||||||
|
|
||||||
|
img, err := extractLastFrameJPGScaled(inputPath, 320, 70)
|
||||||
if err == nil && len(img) > 0 {
|
if err == nil && len(img) > 0 {
|
||||||
return img, nil
|
return img, nil
|
||||||
}
|
}
|
||||||
return extractFirstFrameJPGScaled(seg, 320, 70)
|
return extractFirstFrameJPGScaled(inputPath, 320, 70)
|
||||||
}
|
}
|
||||||
|
|
||||||
func extractLastFrameFromPreviewDirJPG(previewDir string) ([]byte, error) {
|
func extractLastFrameFromPreviewDirJPG(previewDir string) ([]byte, error) {
|
||||||
seg, err := latestPreviewSegment(previewDir)
|
inputPath, cleanup, err := buildPreviewInputFromDir(previewDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
img, err := extractLastFrameJPG(seg)
|
defer cleanup()
|
||||||
if err != nil {
|
|
||||||
return extractFirstFrameJPGScaled(seg, 720, 75)
|
img, err := extractLastFrameJPG(inputPath)
|
||||||
|
if err == nil && len(img) > 0 {
|
||||||
|
return img, nil
|
||||||
}
|
}
|
||||||
return img, nil
|
return extractFirstFrameJPGScaled(inputPath, 720, 75)
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveLivePreviewJPGFile(w http.ResponseWriter, r *http.Request, path string) {
|
func serveLivePreviewJPGFile(w http.ResponseWriter, r *http.Request, path string) {
|
||||||
@ -1552,23 +1622,8 @@ func recordPreviewWithBase(w http.ResponseWriter, r *http.Request, basePath stri
|
|||||||
previewDir := strings.TrimSpace(j.PreviewDir)
|
previewDir := strings.TrimSpace(j.PreviewDir)
|
||||||
if previewDir != "" {
|
if previewDir != "" {
|
||||||
img, genErr = extractLastFrameFromPreviewDirJPG(previewDir)
|
img, genErr = extractLastFrameFromPreviewDirJPG(previewDir)
|
||||||
}
|
if genErr != nil {
|
||||||
|
fmt.Println("preview from previewDir failed:", previewDir, genErr)
|
||||||
if genErr != nil || len(img) == 0 {
|
|
||||||
outPath := strings.TrimSpace(j.Output)
|
|
||||||
if outPath != "" {
|
|
||||||
outPath = filepath.Clean(outPath)
|
|
||||||
if !filepath.IsAbs(outPath) {
|
|
||||||
if abs, err := resolvePathRelativeToApp(outPath); err == nil {
|
|
||||||
outPath = abs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if fi, err := os.Stat(outPath); err == nil && !fi.IsDir() && fi.Size() > 0 {
|
|
||||||
img, genErr = extractLastFrameJPG(outPath)
|
|
||||||
if genErr != nil {
|
|
||||||
img, _ = extractFirstFrameJPGScaled(outPath, 720, 75)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1602,8 +1657,7 @@ func recordPreviewWithBase(w http.ResponseWriter, r *http.Request, basePath stri
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Cache-Control", "no-store")
|
servePreviewStatusSVG(w, "Preview", http.StatusOK)
|
||||||
w.WriteHeader(http.StatusNoContent)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@ -17,6 +20,25 @@ import (
|
|||||||
"github.com/grafov/m3u8"
|
"github.com/grafov/m3u8"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func previewSegmentsDir(previewDir string) string {
|
||||||
|
previewDir = strings.TrimSpace(previewDir)
|
||||||
|
if previewDir == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return filepath.Join(previewDir, "segments")
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensurePreviewSegmentsDir(previewDir string) (string, error) {
|
||||||
|
dir := previewSegmentsDir(previewDir)
|
||||||
|
if strings.TrimSpace(dir) == "" {
|
||||||
|
return "", fmt.Errorf("previewDir leer")
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return dir, nil
|
||||||
|
}
|
||||||
|
|
||||||
// --- DVR-ähnlicher Recorder-Ablauf ---
|
// --- DVR-ähnlicher Recorder-Ablauf ---
|
||||||
// Entspricht grob dem RecordStream aus dem Channel-Snippet:
|
// Entspricht grob dem RecordStream aus dem Channel-Snippet:
|
||||||
func RecordStream(
|
func RecordStream(
|
||||||
@ -28,8 +50,6 @@ func RecordStream(
|
|||||||
httpCookie string,
|
httpCookie string,
|
||||||
job *RecordJob,
|
job *RecordJob,
|
||||||
) error {
|
) error {
|
||||||
// 1) Seite laden
|
|
||||||
// Domain sauber zusammenbauen (mit/ohne Slash)
|
|
||||||
base := strings.TrimRight(domain, "/")
|
base := strings.TrimRight(domain, "/")
|
||||||
pageURL := base + "/" + username
|
pageURL := base + "/" + username
|
||||||
|
|
||||||
@ -38,58 +58,159 @@ func RecordStream(
|
|||||||
return fmt.Errorf("seite laden: %w", err)
|
return fmt.Errorf("seite laden: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2) HLS-URL aus roomDossier extrahieren (wie DVR.ParseStream)
|
|
||||||
hlsURL, err := ParseStream(body)
|
hlsURL, err := ParseStream(body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("stream-parsing: %w", err)
|
return fmt.Errorf("stream-parsing: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Playlist holen (wie stream.GetPlaylist im DVR)
|
|
||||||
playlist, err := FetchPlaylist(ctx, hc, hlsURL, httpCookie)
|
playlist, err := FetchPlaylist(ctx, hc, hlsURL, httpCookie)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("playlist abrufen: %w", err)
|
return fmt.Errorf("playlist abrufen: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ WICHTIG: fMP4 live preview (/api/preview/live) braucht job.PreviewM3U8 als Input
|
var previewDir string
|
||||||
|
var segmentsDir string
|
||||||
|
|
||||||
if job != nil {
|
if job != nil {
|
||||||
|
assetID := assetIDForJob(job)
|
||||||
|
if assetID != "" {
|
||||||
|
if dir, derr := ensureGeneratedDir(assetID); derr == nil {
|
||||||
|
previewDir = strings.TrimSpace(dir)
|
||||||
|
|
||||||
|
if sdir, serr := ensurePreviewSegmentsDir(previewDir); serr == nil {
|
||||||
|
segmentsDir = strings.TrimSpace(sdir)
|
||||||
|
} else {
|
||||||
|
fmt.Println("preview ensurePreviewSegmentsDir failed:", assetID, serr)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Println("preview ensureGeneratedDir failed:", assetID, derr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
jobsMu.Lock()
|
jobsMu.Lock()
|
||||||
job.PreviewM3U8 = strings.TrimSpace(playlist.PlaylistURL)
|
job.PreviewM3U8 = strings.TrimSpace(playlist.PlaylistURL)
|
||||||
job.PreviewCookie = httpCookie
|
job.PreviewCookie = httpCookie
|
||||||
job.PreviewUA = hc.userAgent
|
job.PreviewUA = hc.userAgent
|
||||||
|
if previewDir != "" {
|
||||||
|
job.PreviewDir = previewDir
|
||||||
|
}
|
||||||
jobsMu.Unlock()
|
jobsMu.Unlock()
|
||||||
|
|
||||||
|
if previewDir != "" {
|
||||||
|
startLiveThumbJPGLoop(ctx, job)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if segmentsDir != "" {
|
||||||
|
defer func() {
|
||||||
|
if err := os.RemoveAll(segmentsDir); err != nil {
|
||||||
|
//fmt.Println("preview segments cleanup failed:", segmentsDir, err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4) Datei öffnen
|
|
||||||
file, err := os.Create(outputPath)
|
file, err := os.Create(outputPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("datei erstellen: %w", err)
|
return fmt.Errorf("datei erstellen: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = file.Close()
|
_ = file.Close()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
// live size tracking (für UI)
|
|
||||||
var written int64
|
var written int64
|
||||||
var lastPush time.Time
|
var lastPush time.Time
|
||||||
var lastBytes int64
|
var lastBytes int64
|
||||||
|
|
||||||
published := false
|
published := false
|
||||||
|
|
||||||
// 5) Segmente „watchen“ – analog zu WatchSegments + HandleSegment im DVR
|
trimPreviewSegments := func(dir string, keep int) {
|
||||||
err = playlist.WatchSegments(ctx, hc, httpCookie, func(b []byte, duration float64) error {
|
if strings.TrimSpace(dir) == "" || keep <= 0 {
|
||||||
// Hier wäre im DVR ch.HandleSegment – bei dir einfach in eine Datei schreiben
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
entries, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
type item struct {
|
||||||
|
name string
|
||||||
|
mod time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
items := make([]item, 0, len(entries))
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.IsDir() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
name := e.Name()
|
||||||
|
lower := strings.ToLower(name)
|
||||||
|
|
||||||
|
if lower == "init.mp4" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(lower, "seg_preview_") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
info, err := e.Info()
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
items = append(items, item{
|
||||||
|
name: name,
|
||||||
|
mod: info.ModTime(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(items) <= keep {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(items, func(i, j int) bool {
|
||||||
|
return items[i].mod.Before(items[j].mod)
|
||||||
|
})
|
||||||
|
|
||||||
|
for i := 0; i < len(items)-keep; i++ {
|
||||||
|
_ = os.Remove(filepath.Join(dir, items[i].name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = playlist.WatchSegments(ctx, hc, httpCookie, previewDir, func(segmentURL string, b []byte, duration float64) error {
|
||||||
|
if len(b) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := file.Write(b); err != nil {
|
if _, err := file.Write(b); err != nil {
|
||||||
return fmt.Errorf("schreibe segment: %w", err)
|
return fmt.Errorf("schreibe segment: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ erst sichtbar machen, wenn wirklich Bytes geschrieben wurden
|
if err := file.Sync(); err != nil {
|
||||||
|
//fmt.Println("output sync failed:", outputPath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if segmentsDir != "" {
|
||||||
|
ext := ".m4s"
|
||||||
|
if len(b) > 0 && b[0] == 0x47 {
|
||||||
|
ext = ".ts"
|
||||||
|
}
|
||||||
|
|
||||||
|
segName := fmt.Sprintf("seg_preview_%020d%s", time.Now().UnixNano(), ext)
|
||||||
|
segPath := filepath.Join(segmentsDir, segName)
|
||||||
|
|
||||||
|
if err := os.WriteFile(segPath, b, 0o644); err != nil {
|
||||||
|
fmt.Println("preview segment write failed:", segPath, err)
|
||||||
|
} else {
|
||||||
|
trimPreviewSegments(segmentsDir, 12)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if job != nil && !published {
|
if job != nil && !published {
|
||||||
published = true
|
published = true
|
||||||
_ = publishJob(job.ID)
|
_ = publishJob(job.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ live size (UI) – throttled
|
|
||||||
written += int64(len(b))
|
written += int64(len(b))
|
||||||
if job != nil {
|
if job != nil {
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
@ -103,8 +224,8 @@ func RecordStream(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Könntest hier z.B. auch Dauer/Größe loggen, wenn du möchtest
|
_ = duration
|
||||||
_ = duration // aktuell unbenutzt
|
_ = segmentURL
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -159,11 +280,75 @@ func (p *Playlist) WatchSegments(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
hc *HTTPClient,
|
hc *HTTPClient,
|
||||||
httpCookie string,
|
httpCookie string,
|
||||||
handler func([]byte, float64) error,
|
previewDir string,
|
||||||
|
handler func(segmentURL string, b []byte, duration float64) error,
|
||||||
) error {
|
) error {
|
||||||
var lastSeq int64 = -1
|
var lastSeq int64 = -1
|
||||||
emptyRounds := 0
|
emptyRounds := 0
|
||||||
const maxEmptyRounds = 60 // statt 5
|
const maxEmptyRounds = 60
|
||||||
|
|
||||||
|
playlistBaseURL, err := url.Parse(strings.TrimSpace(p.PlaylistURL))
|
||||||
|
if err != nil || playlistBaseURL == nil {
|
||||||
|
return fmt.Errorf("ungültige playlist url: %q", p.PlaylistURL)
|
||||||
|
}
|
||||||
|
|
||||||
|
var lastMapURL string
|
||||||
|
|
||||||
|
writeInitIfNeeded := func(mapURL string) {
|
||||||
|
if strings.TrimSpace(previewDir) == "" || strings.TrimSpace(mapURL) == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
segDir, err := ensurePreviewSegmentsDir(previewDir)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("ensurePreviewSegmentsDir failed:", previewDir, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
initPath := filepath.Join(segDir, "init.mp4")
|
||||||
|
|
||||||
|
// schon vorhanden und gleiches URL-Mapping -> nichts tun
|
||||||
|
if mapURL == lastMapURL {
|
||||||
|
if st, err := os.Stat(initPath); err == nil && !st.IsDir() && st.Size() > 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err := hc.NewRequest(ctx, http.MethodGet, mapURL, httpCookie)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("init request build failed:", mapURL, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := hc.client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("init download failed:", mapURL, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
fmt.Println("init bad status:", resp.StatusCode, mapURL)
|
||||||
|
_, _ = io.Copy(io.Discard, resp.Body)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
b, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("init read failed:", mapURL, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(b) == 0 {
|
||||||
|
fmt.Println("init empty:", mapURL)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(initPath, b, 0o644); err != nil {
|
||||||
|
fmt.Println("init write failed:", initPath, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
lastMapURL = mapURL
|
||||||
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@ -172,41 +357,122 @@ func (p *Playlist) WatchSegments(
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
// Playlist holen
|
if strings.TrimSpace(p.PlaylistURL) == "" {
|
||||||
|
return errors.New("playlist url fehlt")
|
||||||
|
}
|
||||||
|
|
||||||
req, err := hc.NewRequest(ctx, http.MethodGet, p.PlaylistURL, httpCookie)
|
req, err := hc.NewRequest(ctx, http.MethodGet, p.PlaylistURL, httpCookie)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("Fehler beim Erstellen der Playlist-Request: %w", err)
|
return fmt.Errorf("fehler beim erstellen der playlist-request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := hc.client.Do(req)
|
resp, err := hc.client.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
emptyRounds++
|
emptyRounds++
|
||||||
|
fmt.Println("playlist download failed:", p.PlaylistURL, err)
|
||||||
|
|
||||||
if emptyRounds >= maxEmptyRounds {
|
if emptyRounds >= maxEmptyRounds {
|
||||||
return errors.New("❌ Playlist nicht mehr erreichbar – Stream vermutlich offline")
|
return errors.New("❌ Playlist nicht mehr erreichbar – Stream vermutlich offline")
|
||||||
}
|
}
|
||||||
time.Sleep(2 * time.Second)
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||||
|
//fmt.Println("playlist bad status:", resp.StatusCode, p.PlaylistURL)
|
||||||
|
_, _ = io.Copy(io.Discard, resp.Body)
|
||||||
|
resp.Body.Close()
|
||||||
|
|
||||||
|
emptyRounds++
|
||||||
|
if emptyRounds >= maxEmptyRounds {
|
||||||
|
return fmt.Errorf("❌ playlist liefert status %d – stream vermutlich offline", resp.StatusCode)
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
playlist, listType, err := m3u8.DecodeFrom(resp.Body, true)
|
playlist, listType, err := m3u8.DecodeFrom(resp.Body, true)
|
||||||
resp.Body.Close()
|
resp.Body.Close()
|
||||||
|
if err != nil {
|
||||||
if err != nil || listType != m3u8.MEDIA {
|
|
||||||
emptyRounds++
|
emptyRounds++
|
||||||
|
fmt.Println("playlist decode failed:", p.PlaylistURL, err)
|
||||||
|
|
||||||
if emptyRounds >= maxEmptyRounds {
|
if emptyRounds >= maxEmptyRounds {
|
||||||
return errors.New("❌ Fehlerhafte Playlist – möglicherweise offline")
|
return errors.New("❌ Fehlerhafte Playlist – möglicherweise offline")
|
||||||
}
|
}
|
||||||
time.Sleep(2 * time.Second)
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
media := playlist.(*m3u8.MediaPlaylist)
|
if listType != m3u8.MEDIA {
|
||||||
|
emptyRounds++
|
||||||
|
fmt.Println("playlist is not media playlist:", p.PlaylistURL)
|
||||||
|
|
||||||
|
if emptyRounds >= maxEmptyRounds {
|
||||||
|
return errors.New("❌ Fehlerhafte Playlist – möglicherweise offline")
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
media, ok := playlist.(*m3u8.MediaPlaylist)
|
||||||
|
if !ok || media == nil {
|
||||||
|
emptyRounds++
|
||||||
|
fmt.Println("playlist cast to media failed:", p.PlaylistURL)
|
||||||
|
|
||||||
|
if emptyRounds >= maxEmptyRounds {
|
||||||
|
return errors.New("❌ Fehlerhafte Media-Playlist – möglicherweise offline")
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if media.Map != nil {
|
||||||
|
mapURI := strings.TrimSpace(media.Map.URI)
|
||||||
|
if mapURI != "" {
|
||||||
|
mapRef, err := url.Parse(mapURI)
|
||||||
|
if err != nil {
|
||||||
|
//fmt.Println("map uri parse failed:", mapURI, err)
|
||||||
|
} else {
|
||||||
|
mapURL := playlistBaseURL.ResolveReference(mapRef).String()
|
||||||
|
writeInitIfNeeded(mapURL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
newSegment := false
|
newSegment := false
|
||||||
|
successfulSegment := false
|
||||||
|
|
||||||
for _, segment := range media.Segments {
|
for _, segment := range media.Segments {
|
||||||
if segment == nil {
|
if segment == nil {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if int64(segment.SeqId) <= lastSeq {
|
if int64(segment.SeqId) <= lastSeq {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@ -214,31 +480,66 @@ func (p *Playlist) WatchSegments(
|
|||||||
lastSeq = int64(segment.SeqId)
|
lastSeq = int64(segment.SeqId)
|
||||||
newSegment = true
|
newSegment = true
|
||||||
|
|
||||||
segmentURL := p.RootURL + segment.URI
|
segURI := strings.TrimSpace(segment.URI)
|
||||||
|
if segURI == "" {
|
||||||
|
//fmt.Println("segment uri empty in playlist:", p.PlaylistURL)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
segRef, err := url.Parse(segURI)
|
||||||
|
if err != nil {
|
||||||
|
//fmt.Println("segment uri parse failed:", segURI, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
segmentURL := playlistBaseURL.ResolveReference(segRef).String()
|
||||||
|
|
||||||
segReq, err := hc.NewRequest(ctx, http.MethodGet, segmentURL, httpCookie)
|
segReq, err := hc.NewRequest(ctx, http.MethodGet, segmentURL, httpCookie)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
//fmt.Println("segment request build failed:", segmentURL, err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
segResp, err := hc.client.Do(segReq)
|
segResp, err := hc.client.Do(segReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
//fmt.Println("segment download failed:", segmentURL, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if segResp.StatusCode < 200 || segResp.StatusCode >= 300 {
|
||||||
|
//fmt.Println("segment bad status:", segResp.StatusCode, segmentURL)
|
||||||
|
_, _ = io.Copy(io.Discard, segResp.Body)
|
||||||
|
segResp.Body.Close()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := io.ReadAll(segResp.Body)
|
data, err := io.ReadAll(segResp.Body)
|
||||||
segResp.Body.Close()
|
segResp.Body.Close()
|
||||||
if err != nil || len(data) == 0 {
|
if err != nil {
|
||||||
|
//fmt.Println("segment read failed:", segmentURL, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(data) == 0 {
|
||||||
|
//fmt.Println("segment empty:", segmentURL)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := handler(data, segment.Duration); err != nil {
|
if err := handler(segmentURL, data, segment.Duration); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
successfulSegment = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if newSegment {
|
if successfulSegment {
|
||||||
emptyRounds = 0
|
emptyRounds = 0
|
||||||
|
} else if newSegment {
|
||||||
|
emptyRounds++
|
||||||
|
//fmt.Println("playlist had new segment ids but none could be downloaded:", p.PlaylistURL)
|
||||||
|
|
||||||
|
if emptyRounds >= maxEmptyRounds {
|
||||||
|
return errors.New("🛑 Neue HLS-Segmente gefunden, aber keines konnte geladen werden – Stream vermutlich geschützt, fehlerhaft oder offline.")
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
emptyRounds++
|
emptyRounds++
|
||||||
if emptyRounds >= maxEmptyRounds {
|
if emptyRounds >= maxEmptyRounds {
|
||||||
@ -246,7 +547,11 @@ func (p *Playlist) WatchSegments(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
time.Sleep(1 * time.Second)
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return ctx.Err()
|
||||||
|
case <-time.After(1 * time.Second):
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -542,6 +542,22 @@ export default function App() {
|
|||||||
const eventSourceRef = useRef<EventSource | null>(null)
|
const eventSourceRef = useRef<EventSource | null>(null)
|
||||||
const modelEventNamesRef = useRef<Set<string>>(new Set())
|
const modelEventNamesRef = useRef<Set<string>>(new Set())
|
||||||
const onModelJobEventRef = useRef<((ev: MessageEvent) => void) | null>(null)
|
const onModelJobEventRef = useRef<((ev: MessageEvent) => void) | null>(null)
|
||||||
|
|
||||||
|
|
||||||
|
const [playerModelKey, setPlayerModelKey] = useState<string | null>(null)
|
||||||
|
const [sourceUrl, setSourceUrl] = useState('')
|
||||||
|
const [jobs, setJobs] = useState<RecordJob[]>([])
|
||||||
|
const [doneJobs, setDoneJobs] = useState<RecordJob[]>([])
|
||||||
|
const [donePage, setDonePage] = useState(1)
|
||||||
|
const [doneCount, setDoneCount] = useState<number>(0)
|
||||||
|
const [modelsCount, setModelsCount] = useState(0)
|
||||||
|
|
||||||
|
const [roomStatusByModelKey, setRoomStatusByModelKey] = useState<Record<string, string>>({})
|
||||||
|
|
||||||
|
const [lastHeaderUpdateAtMs, setLastHeaderUpdateAtMs] = useState<number>(() => Date.now())
|
||||||
|
const [nowMs, setNowMs] = useState<number>(() => Date.now())
|
||||||
|
|
||||||
|
const [modelsByKey, setModelsByKey] = useState<Record<string, StoredModel>>({})
|
||||||
|
|
||||||
const selectSourceUrl = useCallback(() => {
|
const selectSourceUrl = useCallback(() => {
|
||||||
const el = sourceUrlInputRef.current
|
const el = sourceUrlInputRef.current
|
||||||
@ -753,6 +769,43 @@ export default function App() {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const loadDonePage = useCallback(async (pageToLoad = donePage, sortToLoad = doneSort) => {
|
||||||
|
try {
|
||||||
|
const key = makePrefetchKey(pageToLoad, sortToLoad)
|
||||||
|
const prefetched = donePrefetchRef.current
|
||||||
|
|
||||||
|
if (prefetched?.key === key && Array.isArray(prefetched.items)) {
|
||||||
|
setDoneJobs(prefetched.items)
|
||||||
|
setLastHeaderUpdateAtMs(Date.now())
|
||||||
|
|
||||||
|
donePrefetchRef.current = null
|
||||||
|
void prefetchDonePage(pageToLoad + 1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await fetch(
|
||||||
|
`/api/record/done?page=${pageToLoad}&pageSize=${DONE_PAGE_SIZE}&sort=${encodeURIComponent(sortToLoad)}`,
|
||||||
|
{ cache: 'no-store' as any }
|
||||||
|
)
|
||||||
|
if (!res.ok) return
|
||||||
|
|
||||||
|
const data = await res.json().catch(() => null)
|
||||||
|
|
||||||
|
const items = Array.isArray(data?.items)
|
||||||
|
? (data.items as RecordJob[])
|
||||||
|
: Array.isArray(data)
|
||||||
|
? (data as RecordJob[])
|
||||||
|
: []
|
||||||
|
|
||||||
|
setDoneJobs(items)
|
||||||
|
setLastHeaderUpdateAtMs(Date.now())
|
||||||
|
|
||||||
|
void prefetchDonePage(pageToLoad + 1)
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}, [donePage, doneSort, prefetchDonePage])
|
||||||
|
|
||||||
const requestFinishedReload = useCallback((reason = 'unknown') => {
|
const requestFinishedReload = useCallback((reason = 'unknown') => {
|
||||||
const now = Date.now()
|
const now = Date.now()
|
||||||
const cooldownMs = 700 // 👈 wichtig gegen Event-Stürme
|
const cooldownMs = 700 // 👈 wichtig gegen Event-Stürme
|
||||||
@ -838,20 +891,10 @@ export default function App() {
|
|||||||
void loadDoneCount()
|
void loadDoneCount()
|
||||||
}, [authed, loadDoneCount])
|
}, [authed, loadDoneCount])
|
||||||
|
|
||||||
const [playerModelKey, setPlayerModelKey] = useState<string | null>(null)
|
useEffect(() => {
|
||||||
const [sourceUrl, setSourceUrl] = useState('')
|
if (!authed) return
|
||||||
const [jobs, setJobs] = useState<RecordJob[]>([])
|
void loadDonePage(donePage, doneSort)
|
||||||
const [doneJobs, setDoneJobs] = useState<RecordJob[]>([])
|
}, [authed, donePage, doneSort, loadDonePage])
|
||||||
const [donePage, setDonePage] = useState(1)
|
|
||||||
const [doneCount, setDoneCount] = useState<number>(0)
|
|
||||||
const [modelsCount, setModelsCount] = useState(0)
|
|
||||||
|
|
||||||
const [roomStatusByModelKey, setRoomStatusByModelKey] = useState<Record<string, string>>({})
|
|
||||||
|
|
||||||
const [lastHeaderUpdateAtMs, setLastHeaderUpdateAtMs] = useState<number>(() => Date.now())
|
|
||||||
const [nowMs, setNowMs] = useState<number>(() => Date.now())
|
|
||||||
|
|
||||||
const [modelsByKey, setModelsByKey] = useState<Record<string, StoredModel>>({})
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setModelsCount(Object.keys(modelsByKey).length)
|
setModelsCount(Object.keys(modelsByKey).length)
|
||||||
|
|||||||
@ -215,6 +215,65 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
const data = await res.json().catch(() => null)
|
const data = await res.json().catch(() => null)
|
||||||
if (!alive || !data) return
|
if (!alive || !data) return
|
||||||
|
|
||||||
|
const assets = data.generateAssets
|
||||||
|
|
||||||
|
if (assets?.running) {
|
||||||
|
setAssetsTask((t) => ({
|
||||||
|
...t,
|
||||||
|
status: 'running',
|
||||||
|
title: 'Assets generieren',
|
||||||
|
text: shortTaskFilename(String(assets.currentFile || assets.text || '')),
|
||||||
|
done: Number(assets.done ?? 0),
|
||||||
|
total: Number(assets.total ?? 0),
|
||||||
|
err: undefined,
|
||||||
|
cancellable: true,
|
||||||
|
fading: false,
|
||||||
|
}))
|
||||||
|
} else if (assets?.error) {
|
||||||
|
const assetsError = String(assets.error || '').trim()
|
||||||
|
const cancelled =
|
||||||
|
assetsError.toLowerCase() === 'abgebrochen' ||
|
||||||
|
assetsError.toLowerCase() === 'abbruch' ||
|
||||||
|
assetsError.toLowerCase() === 'cancelled' ||
|
||||||
|
assetsError.toLowerCase() === 'canceled'
|
||||||
|
|
||||||
|
setAssetsTask((t) => ({
|
||||||
|
...t,
|
||||||
|
status: cancelled ? 'cancelled' : 'error',
|
||||||
|
title: 'Assets generieren',
|
||||||
|
text: shortTaskFilename(String(assets.currentFile || assets.text || '')),
|
||||||
|
done: Number(assets.done ?? 0),
|
||||||
|
total: Number(assets.total ?? 0),
|
||||||
|
err: cancelled ? undefined : assetsError,
|
||||||
|
cancellable: false,
|
||||||
|
fading: false,
|
||||||
|
}))
|
||||||
|
} else if (assets?.text || assets?.finishedAt) {
|
||||||
|
setAssetsTask((t) => ({
|
||||||
|
...t,
|
||||||
|
status: 'done',
|
||||||
|
title: 'Assets generieren',
|
||||||
|
text: shortTaskFilename(String(assets.currentFile || assets.text || '')),
|
||||||
|
done: Number(assets.done ?? 0),
|
||||||
|
total: Number(assets.total ?? 0),
|
||||||
|
err: undefined,
|
||||||
|
cancellable: false,
|
||||||
|
fading: false,
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
setAssetsTask((t) => ({
|
||||||
|
...t,
|
||||||
|
status: 'idle',
|
||||||
|
title: 'Assets generieren',
|
||||||
|
text: '',
|
||||||
|
done: 0,
|
||||||
|
total: 0,
|
||||||
|
err: undefined,
|
||||||
|
cancellable: true,
|
||||||
|
fading: false,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
const regen = data.regenerateAssets
|
const regen = data.regenerateAssets
|
||||||
if (regen?.running) {
|
if (regen?.running) {
|
||||||
setRegenerateAssetsTask((t) => ({
|
setRegenerateAssetsTask((t) => ({
|
||||||
@ -356,57 +415,6 @@ export default function RecorderSettings({ onAssetsGenerated }: Props) {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let alive = true
|
|
||||||
|
|
||||||
const loadTaskStatus = async () => {
|
|
||||||
try {
|
|
||||||
const res = await fetch('/api/tasks/generate-assets', { cache: 'no-store' })
|
|
||||||
if (!res.ok) return
|
|
||||||
|
|
||||||
const data = await res.json().catch(() => null)
|
|
||||||
if (!alive || !data) return
|
|
||||||
|
|
||||||
const running = Boolean(data.running)
|
|
||||||
const done = Number(data.done ?? 0)
|
|
||||||
const total = Number(data.total ?? 0)
|
|
||||||
const currentFile = shortTaskFilename(data.currentFile)
|
|
||||||
|
|
||||||
if (running) {
|
|
||||||
setAssetsTask((t) => ({
|
|
||||||
...t,
|
|
||||||
status: 'running',
|
|
||||||
title: 'Assets generieren',
|
|
||||||
text: currentFile || '',
|
|
||||||
done,
|
|
||||||
total,
|
|
||||||
err: undefined,
|
|
||||||
fading: false,
|
|
||||||
}))
|
|
||||||
} else {
|
|
||||||
setAssetsTask((t) => ({
|
|
||||||
...t,
|
|
||||||
status: 'idle',
|
|
||||||
title: 'Assets generieren',
|
|
||||||
text: '',
|
|
||||||
done: 0,
|
|
||||||
total: 0,
|
|
||||||
err: undefined,
|
|
||||||
fading: false,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignorieren
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void loadTaskStatus()
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
alive = false
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
async function browse(target: 'record' | 'done' | 'ffmpeg') {
|
async function browse(target: 'record' | 'done' | 'ffmpeg') {
|
||||||
setErr(null)
|
setErr(null)
|
||||||
setMsg(null)
|
setMsg(null)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user