From 6682b51f76e41c822dbd6cfde4180f45698003d6 Mon Sep 17 00:00:00 2001 From: Linrador <68631622+Linrador@users.noreply.github.com> Date: Mon, 16 Mar 2026 15:11:45 +0100 Subject: [PATCH] updated finisheddownloads --- backend/live.go | 11 ++- backend/record_stream_cb.go | 17 ----- backend/record_stream_mfc.go | 17 ----- backend/recorder_settings.json | 2 +- backend/settings.go | 8 +-- .../src/components/ui/FinishedDownloads.tsx | 32 ++++----- .../ui/FinishedDownloadsCardsView.tsx | 41 ++++++----- .../ui/FinishedDownloadsGalleryView.tsx | 41 ++++++----- .../ui/FinishedDownloadsTableView.tsx | 31 +++++---- frontend/src/components/ui/teaserPlayback.ts | 69 +++++++++++++++++++ 10 files changed, 154 insertions(+), 115 deletions(-) create mode 100644 frontend/src/components/ui/teaserPlayback.ts diff --git a/backend/live.go b/backend/live.go index f49c9f2..7195c51 100644 --- a/backend/live.go +++ b/backend/live.go @@ -695,7 +695,7 @@ func recordPreviewLiveFMP4(w http.ResponseWriter, r *http.Request) { } // ffmpeg args: input = m3u8, output = fragmented mp4 to stdout - // ✅ nur Video (kein Audio), damit MSE codecs stabil sind + // ✅ Video + Audio für Browser-Playback args := []string{"-hide_banner", "-loglevel", "error"} if ua != "" { args = append(args, "-user_agent", ua) @@ -707,9 +707,10 @@ func recordPreviewLiveFMP4(w http.ResponseWriter, r *http.Request) { // Input args = append(args, "-i", m3u8) - // Video encode (low-latency-ish) + // Video + Audio encode (low-latency-ish) args = append(args, - "-an", + "-map", "0:v:0", + "-map", "0:a:0?", "-vf", "scale=480:-2", "-c:v", "libx264", "-preset", "veryfast", @@ -720,6 +721,10 @@ func recordPreviewLiveFMP4(w http.ResponseWriter, r *http.Request) { "-g", "48", "-keyint_min", "48", "-sc_threshold", "0", + "-c:a", "aac", + "-b:a", "128k", + "-ac", "2", + "-ar", "48000", ) // Output: fMP4 fragmented to stdout (single HTTP response) diff --git a/backend/record_stream_cb.go b/backend/record_stream_cb.go index f197ce7..172645b 100644 --- a/backend/record_stream_cb.go +++ b/backend/record_stream_cb.go @@ -10,7 +10,6 @@ import ( "io" "net/http" "os" - "path/filepath" "strconv" "strings" "time" @@ -60,22 +59,6 @@ func RecordStream( jobsMu.Unlock() } - if job != nil && strings.TrimSpace(job.PreviewDir) == "" { - assetID := assetIDForJob(job) - if strings.TrimSpace(assetID) == "" { - assetID = job.ID - } - previewDir := filepath.Join(os.TempDir(), "rec_preview", assetID) - - jobsMu.Lock() - job.PreviewDir = previewDir - jobsMu.Unlock() - - if err := startPreviewHLS(ctx, job, playlist.PlaylistURL, previewDir, httpCookie, hc.userAgent); err != nil { - fmt.Println("⚠️ preview start fehlgeschlagen:", err) - } - } - // 4) Datei öffnen file, err := os.Create(outputPath) if err != nil { diff --git a/backend/record_stream_mfc.go b/backend/record_stream_mfc.go index cc11113..2ca87b3 100644 --- a/backend/record_stream_mfc.go +++ b/backend/record_stream_mfc.go @@ -12,7 +12,6 @@ import ( "net/url" "os" "os/exec" - "path/filepath" "strconv" "strings" "time" @@ -89,22 +88,6 @@ func RecordStreamMFC( _ = publishJob(job.ID) } - // ✅ Preview starten - if job != nil && job.PreviewDir == "" { - assetID := assetIDForJob(job) - if strings.TrimSpace(assetID) == "" { - assetID = job.ID - } - previewDir := filepath.Join(os.TempDir(), "rec_preview", assetID) - - job.PreviewDir = previewDir - - if err := startPreviewHLS(ctx, job, m3u8URL, previewDir, "", hc.userAgent); err != nil { - fmt.Println("⚠️ preview start fehlgeschlagen:", err) - job.PreviewDir = "" // rollback - } - } - // Aufnahme starten return handleM3U8Mode(ctx, m3u8URL, outputPath, job) } diff --git a/backend/recorder_settings.json b/backend/recorder_settings.json index 736a221..21e23cf 100644 --- a/backend/recorder_settings.json +++ b/backend/recorder_settings.json @@ -13,7 +13,7 @@ "lowDiskPauseBelowGB": 5, "blurPreviews": false, "teaserPlayback": "hover", - "teaserAudio": false, + "teaserAudio": true, "enableNotifications": true, "encryptedCookies": "" } diff --git a/backend/settings.go b/backend/settings.go index a585c7d..54cb0f8 100644 --- a/backend/settings.go +++ b/backend/settings.go @@ -171,10 +171,10 @@ func loadSettings() { // ffmpeg-Pfad anhand Settings/Env/PATH bestimmen ffmpegPath = detectFFmpegPath() - //fmt.Println("🔍 ffmpegPath:", ffmpegPath) + fmt.Println("🔍 ffmpegPath:", ffmpegPath) ffprobePath = detectFFprobePath() - //fmt.Println("🔍 ffprobePath:", ffprobePath) + fmt.Println("🔍 ffprobePath:", ffprobePath) } @@ -403,10 +403,10 @@ func recordSettingsHandler(w http.ResponseWriter, r *http.Request) { } else { ffmpegPath = detectFFmpegPath() } - fmt.Println("🔍 ffmpegPath:", ffmpegPath) + //fmt.Println("🔍 ffmpegPath:", ffmpegPath) ffprobePath = detectFFprobePath() - fmt.Println("🔍 ffprobePath:", ffprobePath) + //fmt.Println("🔍 ffprobePath:", ffprobePath) w.Header().Set("Content-Type", "application/json") w.Header().Set("Cache-Control", "no-store") diff --git a/frontend/src/components/ui/FinishedDownloads.tsx b/frontend/src/components/ui/FinishedDownloads.tsx index be401b9..babd51f 100644 --- a/frontend/src/components/ui/FinishedDownloads.tsx +++ b/frontend/src/components/ui/FinishedDownloads.tsx @@ -27,6 +27,7 @@ import { isHotName, stripHotPrefix } from './hotName' import LabeledSwitch from './LabeledSwitch' import Switch from './Switch' import LoadingSpinner from './LoadingSpinner' +import type { FinishedDownloadsTeaserState, TeaserPlaybackMode } from './teaserPlayback' type SortMode = | 'completed_desc' @@ -38,8 +39,6 @@ type SortMode = | 'size_desc' | 'size_asc' -type TeaserPlaybackMode = 'still' | 'hover' | 'all' - type Props = { jobs: RecordJob[] doneJobs: RecordJob[] @@ -357,6 +356,16 @@ export default function FinishedDownloads({ // ✅ schützt gegen alte Effect-Instanzen / StrictMode-Cleanup / Race Conditions const refillSessionRef = React.useRef(0) + const teaserState = React.useMemo( + () => ({ + mode: teaserPlaybackMode, + activeKey: teaserKey, + hoverKey: hoverTeaserKey, + audioEnabled: Boolean(teaserAudio), + }), + [teaserPlaybackMode, teaserKey, hoverTeaserKey, teaserAudio] + ) + type UndoAction = | { kind: 'delete'; undoToken: string; originalFile: string; rowKey?: string; from?: 'done' | 'keep' } | { kind: 'keep'; keptFile: string; originalFile: string; rowKey?: string } @@ -1928,7 +1937,7 @@ export default function FinishedDownloads({ // 🖱️ Desktop: Teaser nur bei Hover (Settings: 'hover' = Standard). Mobile: weiterhin Viewport-Fokus (Effect darunter) useEffect(() => { const active = - teaserPlaybackMode === 'hover' && + teaserState.mode === 'hover' && !canHover && (view === 'cards' || view === 'gallery' || view === 'table') @@ -1985,7 +1994,7 @@ export default function FinishedDownloads({ io.disconnect() if (teaserIORef.current === io) teaserIORef.current = null } - }, [view, teaserPlaybackMode, canHover, inlinePlay?.key]) + }, [view, teaserState.mode, canHover, inlinePlay?.key]) // 🧠 Laufzeit-Anzeige: bevorzugt Videodauer, sonst Fallback auf startedAt/endedAt const runtimeOf = (job: RecordJob): string => { @@ -2456,10 +2465,7 @@ export default function FinishedDownloads({ isLoading={isLoading} blurPreviews={blurPreviews} durations={durations} - teaserKey={teaserKey} - teaserPlayback={teaserPlaybackMode} - teaserAudio={teaserAudio} - hoverTeaserKey={hoverTeaserKey} + teaserState={teaserState} inlinePlay={inlinePlay} deletingKeys={deletingKeys} keepingKeys={keepingKeys} @@ -2515,11 +2521,8 @@ export default function FinishedDownloads({ resolutions={resolutions} durations={durations} canHover={canHover} - teaserAudio={teaserAudio} - hoverTeaserKey={hoverTeaserKey} + teaserState={teaserState} setHoverTeaserKey={setHoverTeaserKey} - teaserPlayback={teaserPlaybackMode} - teaserKey={teaserKey} registerTeaserHost={registerTeaserHost} handleDuration={handleDuration} handleResolution={handleResolution} @@ -2556,10 +2559,7 @@ export default function FinishedDownloads({ blurPreviews={blurPreviews} durations={durations} handleDuration={handleDuration} - teaserKey={teaserKey} - teaserPlayback={teaserPlaybackMode} - teaserAudio={teaserAudio} - hoverTeaserKey={hoverTeaserKey} + teaserState={teaserState} keyFor={keyFor} baseName={baseName} modelNameFromOutput={modelNameFromOutput} diff --git a/frontend/src/components/ui/FinishedDownloadsCardsView.tsx b/frontend/src/components/ui/FinishedDownloadsCardsView.tsx index ba9de13..6301a72 100644 --- a/frontend/src/components/ui/FinishedDownloadsCardsView.tsx +++ b/frontend/src/components/ui/FinishedDownloadsCardsView.tsx @@ -11,6 +11,11 @@ import TagOverflowRow from './TagOverflowRow' import PreviewScrubber from './PreviewScrubber' import { isHotName, stripHotPrefix } from './hotName' import { formatResolution } from './formatters' +import type { FinishedDownloadsTeaserState } from './teaserPlayback' +import { + shouldAnimateTeaser, + shouldEnableTeaserAudio, +} from './teaserPlayback' type InlinePlayState = { key: string; nonce: number } | null @@ -18,14 +23,11 @@ type Props = { rows: RecordJob[] isLoading?: boolean isSmall: boolean - teaserPlayback: 'still' | 'hover' | 'all' - teaserAudio?: boolean - hoverTeaserKey?: string | null blurPreviews?: boolean durations: Record - teaserKey: string | null inlinePlay: InlinePlayState + teaserState: FinishedDownloadsTeaserState deletingKeys: Set keepingKeys: Set @@ -524,12 +526,9 @@ export default function FinishedDownloadsCardsView({ rows, isSmall, isLoading, - teaserPlayback, - teaserAudio, - hoverTeaserKey, blurPreviews, durations, - teaserKey, + teaserState, inlinePlay, deletingKeys, keepingKeys, @@ -704,10 +703,12 @@ export default function FinishedDownloadsCardsView({ const realInlineActive = inlinePlay?.key === k const inlineActive = opts?.disableInline ? false : realInlineActive - const allowSound = - !opts?.forceStill && - Boolean(teaserAudio) && - (inlineActive || hoverTeaserKey === k) + const allowSound = shouldEnableTeaserAudio({ + state: teaserState, + itemKey: k, + inlineActive, + disabled: Boolean(opts?.forceStill), + }) const previewMuted = !allowSound const inlineNonce = inlineActive ? inlinePlay?.nonce ?? 0 : 0 @@ -717,15 +718,13 @@ export default function FinishedDownloadsCardsView({ // ✅ Im Mobile-Stack soll nur die Top-Card Teaser-Video bekommen. // Untere Karten zeigen immer nur Bild (Still), selbst wenn teaserKey mal matcht. const allowTeaserAnimation = - opts?.forceStill - ? false - : opts?.mobileStackTopOnlyVideo - ? (teaserPlayback === 'still' ? false : true) - : (teaserPlayback === 'all' - ? true - : teaserPlayback === 'hover' - ? teaserKey === k - : false) + opts?.mobileStackTopOnlyVideo + ? teaserState.mode !== 'still' + : shouldAnimateTeaser({ + state: teaserState, + itemKey: k, + disabled: Boolean(opts?.forceStill), + }) const busy = deletingKeys.has(k) || keepingKeys.has(k) || removingKeys.has(k) diff --git a/frontend/src/components/ui/FinishedDownloadsGalleryView.tsx b/frontend/src/components/ui/FinishedDownloadsGalleryView.tsx index d645ce0..9f48971 100644 --- a/frontend/src/components/ui/FinishedDownloadsGalleryView.tsx +++ b/frontend/src/components/ui/FinishedDownloadsGalleryView.tsx @@ -14,6 +14,12 @@ import TagOverflowRow from './TagOverflowRow' import { isHotName, stripHotPrefix } from './hotName' import { formatResolution } from './formatters' import PreviewScrubber from './PreviewScrubber' +import type { FinishedDownloadsTeaserState } from './teaserPlayback' +import { + shouldAnimateTeaser, + shouldEnableTeaserAudio, + shouldObserveTeasers, +} from './teaserPlayback' type ModelFlags = { favorite?: boolean @@ -35,10 +41,7 @@ type Props = { isLoading?: boolean blurPreviews?: boolean durations: Record - teaserPlayback: 'still' | 'hover' | 'all' - teaserAudio?: boolean - hoverTeaserKey?: string | null - teaserKey: string | null + teaserState: FinishedDownloadsTeaserState handleDuration: (job: RecordJob, seconds: number) => void @@ -150,10 +153,7 @@ export default function FinishedDownloadsGalleryView({ isLoading, blurPreviews, durations, - teaserPlayback, - teaserAudio, - hoverTeaserKey, - teaserKey, + teaserState, handleDuration, handleScrubberClickIndex, @@ -189,18 +189,18 @@ export default function FinishedDownloadsGalleryView({ enqueueToggleHot, }: Props) { // ✅ Teaser-Observer nur aktiv, wenn Preview überhaupt "laufen" soll - const shouldObserveTeasers = teaserPlayback === 'hover' || teaserPlayback === 'all' + const observeTeasers = shouldObserveTeasers(teaserState.mode) // ✅ Wrapper: bei still unregistrieren / nicht registrieren const registerTeaserHostIfNeeded = React.useCallback( (key: string) => (el: HTMLDivElement | null) => { - if (!shouldObserveTeasers) { + if (!observeTeasers) { registerTeaserHost(key)(null) return } registerTeaserHost(key)(el) }, - [registerTeaserHost, shouldObserveTeasers] + [registerTeaserHost, observeTeasers] ) const parseTags = (raw?: string): string[] => { @@ -276,7 +276,10 @@ export default function FinishedDownloadsGalleryView({ const k = keyFor(j) // Sound nur bei Hover auf genau diesem Teaser - const allowSound = Boolean(teaserAudio) && hoverTeaserKey === k + const allowSound = shouldEnableTeaserAudio({ + state: teaserState, + itemKey: k, + }) const previewMuted = !allowSound const model = modelNameFromOutput(j.output) @@ -490,15 +493,11 @@ export default function FinishedDownloadsGalleryView({ variant="fill" showPopover={false} blur={blurPreviews} - animated={ - hideTeaserUnderOverlay - ? false - : teaserPlayback === 'all' - ? true - : teaserPlayback === 'hover' - ? teaserKey === k - : false - } + animated={shouldAnimateTeaser({ + state: teaserState, + itemKey: k, + disabled: hideTeaserUnderOverlay, + })} animatedMode="teaser" animatedTrigger="always" muted={previewMuted} diff --git a/frontend/src/components/ui/FinishedDownloadsTableView.tsx b/frontend/src/components/ui/FinishedDownloadsTableView.tsx index 9089e1a..9851a6c 100644 --- a/frontend/src/components/ui/FinishedDownloadsTableView.tsx +++ b/frontend/src/components/ui/FinishedDownloadsTableView.tsx @@ -5,12 +5,16 @@ import * as React from 'react' import Table, { type Column, type SortState } from './Table' import type { RecordJob } from '../../types' - import FinishedVideoPreview from './FinishedVideoPreview' import RecordJobActions from './RecordJobActions' import TagOverflowRow from './TagOverflowRow' import { isHotName, stripHotPrefix } from './hotName' import { formatResolution } from './formatters' +import type { FinishedDownloadsTeaserState } from './teaserPlayback' +import { + shouldAnimateTeaser, + shouldEnableTeaserAudio, +} from './teaserPlayback' type SortMode = | 'completed_desc' @@ -39,11 +43,8 @@ type Props = { // teaser/preview canHover: boolean - teaserAudio?: boolean - hoverTeaserKey: string | null + teaserState: FinishedDownloadsTeaserState setHoverTeaserKey: React.Dispatch> - teaserPlayback?: 'still' | 'hover' | 'all' - teaserKey: string | null registerTeaserHost: (key: string) => (el: HTMLDivElement | null) => void handleDuration: (job: RecordJob, seconds: number) => void handleResolution: (job: RecordJob, w: number, h: number) => void @@ -95,11 +96,8 @@ export default function FinishedDownloadsTableView({ durations, canHover, - teaserAudio, - hoverTeaserKey, + teaserState, setHoverTeaserKey, - teaserPlayback = 'hover', - teaserKey, registerTeaserHost, handleDuration, handleResolution, @@ -225,7 +223,10 @@ export default function FinishedDownloadsTableView({ widthClassName: 'w-[140px]', cell: (j) => { const k = keyFor(j) - const allowSound = Boolean(teaserAudio) && hoverTeaserKey === k + const allowSound = shouldEnableTeaserAudio({ + state: teaserState, + itemKey: k, + }) const previewMuted = !allowSound const spriteInfo = previewSpriteInfoOf(j) const scrubActiveIndex = scrubActiveByKey[k] @@ -268,7 +269,10 @@ export default function FinishedDownloadsTableView({ className="w-28 h-16 rounded-md ring-1 ring-black/5 dark:ring-white/10" showPopover={false} blur={blurPreviews} - animated={teaserPlayback === 'all' ? true : teaserPlayback === 'hover' ? teaserKey === k : false} + animated={shouldAnimateTeaser({ + state: teaserState, + itemKey: k, + })} animatedMode="teaser" animatedTrigger="always" assetNonce={assetNonce} @@ -474,16 +478,13 @@ export default function FinishedDownloadsTableView({ keyFor, baseName, durations, - teaserAudio, - hoverTeaserKey, registerTeaserHost, canHover, setHoverTeaserKey, handleDuration, handleResolution, blurPreviews, - teaserPlayback, - teaserKey, + teaserState, assetNonce, lower, modelNameFromOutput, diff --git a/frontend/src/components/ui/teaserPlayback.ts b/frontend/src/components/ui/teaserPlayback.ts new file mode 100644 index 0000000..03dc0c6 --- /dev/null +++ b/frontend/src/components/ui/teaserPlayback.ts @@ -0,0 +1,69 @@ +// frontend\src\components\ui\teaserPlayback.ts + +export type TeaserPlaybackMode = 'still' | 'hover' | 'all' + +export type FinishedDownloadsTeaserState = { + mode: TeaserPlaybackMode + activeKey: string | null + hoverKey: string | null + audioEnabled: boolean +} + +type TeaserAnimationOptions = { + state: FinishedDownloadsTeaserState + itemKey: string + disabled?: boolean + forceAll?: boolean +} + +type TeaserAudioOptions = { + state: FinishedDownloadsTeaserState + itemKey: string + inlineActive?: boolean + disabled?: boolean +} + +export function shouldObserveTeasers(mode: TeaserPlaybackMode): boolean { + return mode === 'hover' || mode === 'all' +} + +export function shouldAnimateTeaser({ + state, + itemKey, + disabled = false, + forceAll = false, +}: TeaserAnimationOptions): boolean { + if (disabled) return false + if (forceAll) return true + + switch (state.mode) { + case 'all': + return true + case 'hover': + return state.hoverKey === itemKey + case 'still': + default: + return false + } +} + +export function shouldEnableTeaserAudio({ + state, + itemKey, + inlineActive = false, + disabled = false, +}: TeaserAudioOptions): boolean { + if (disabled) return false + if (!state.audioEnabled) return false + + // Inline immer bevorzugen + if (inlineActive) return true + + // Desktop-Hover + if (state.hoverKey === itemKey) return true + + // Mobile / aktiver Teaser im Viewport + if (state.activeKey === itemKey) return true + + return false +} \ No newline at end of file