Add clip number burn-in option

This commit is contained in:
Mikei386
2026-06-04 19:59:01 +02:00
parent 1c61c8d998
commit c7ae3fcc59
4 changed files with 55 additions and 10 deletions
+2
View File
@@ -152,6 +152,8 @@ struct ContentView: View {
.help("Behält segment_*, stabilized_*, clip_* und xfade_* Dateien zur Kontrolle. Sonst wird der Temp-Ordner nach Erfolg gelöscht.")
Toggle("Render-Cache für Nachbearbeitung behalten", isOn: $settings.keepRenderCacheForReview)
.help("Behält die normalisierten clip_*.mp4 Dateien und eine Projektdatei neben dem Output. So können einzelne Segmentfenster später angepasst und schneller neu gerendert werden.")
Toggle("Clipnummer einblenden", isOn: $settings.burnInClipNumbers)
.help("Brennt die laufende Clipnummer unten links ins Video ein. Praktisch, um im fertigen Film problematische Clips schnell in der Nachbearbeitung zu finden.")
}
.disabled(processor.state.isRunning)
+2 -1
View File
@@ -155,7 +155,8 @@ actor FFmpegService {
return FFmpegFilterAvailability(
hasVidstabDetect: result.output.contains("vidstabdetect"),
hasVidstabTransform: result.output.contains("vidstabtransform"),
hasDeshake: result.output.contains("deshake")
hasDeshake: result.output.contains("deshake"),
hasDrawtext: result.output.contains("drawtext")
)
}
+5
View File
@@ -111,6 +111,7 @@ struct FFmpegFilterAvailability: Equatable {
let hasVidstabDetect: Bool
let hasVidstabTransform: Bool
let hasDeshake: Bool
let hasDrawtext: Bool
var hasVidstab: Bool {
hasVidstabDetect && hasVidstabTransform
@@ -135,6 +136,7 @@ struct RenderSettings: Equatable {
var startOffset: Double = 0
var keepIntermediateClips: Bool = false
var keepRenderCacheForReview: Bool = true
var burnInClipNumbers: Bool = false
var videoEncoder: VideoEncoder = .videoToolbox
var crf: Int = 20
var hardwareBitrateMbps: Int = 12
@@ -168,6 +170,7 @@ struct PersistedRenderSettings: Codable {
var startOffset: Double
var keepIntermediateClips: Bool
var keepRenderCacheForReview: Bool?
var burnInClipNumbers: Bool?
var videoEncoder: VideoEncoder
var crf: Int
var hardwareBitrateMbps: Int
@@ -200,6 +203,7 @@ struct PersistedRenderSettings: Codable {
startOffset = settings.startOffset
keepIntermediateClips = settings.keepIntermediateClips
keepRenderCacheForReview = settings.keepRenderCacheForReview
burnInClipNumbers = settings.burnInClipNumbers
videoEncoder = settings.videoEncoder
crf = settings.crf
hardwareBitrateMbps = settings.hardwareBitrateMbps
@@ -234,6 +238,7 @@ struct PersistedRenderSettings: Codable {
startOffset: startOffset,
keepIntermediateClips: keepIntermediateClips,
keepRenderCacheForReview: keepRenderCacheForReview ?? true,
burnInClipNumbers: burnInClipNumbers ?? false,
videoEncoder: videoEncoder,
crf: crf,
hardwareBitrateMbps: hardwareBitrateMbps,
+46 -9
View File
@@ -174,13 +174,21 @@ final class VideoProcessor: ObservableObject {
return try await renderPhotoMorph(settings: effectiveSettings, ffmpeg: tools.ffmpeg, ffprobe: tools.ffprobe)
}
if effectiveSettings.stabilizationEnabled {
if effectiveSettings.stabilizationEnabled || effectiveSettings.burnInClipNumbers {
let filters = try await service.availableFilters(ffmpeg: tools.ffmpeg)
effectiveSettings.stabilizationMethod = resolveStabilizationMethod(
requested: effectiveSettings.stabilizationMethod,
availability: filters
)
appendLog("Stabilisierung: \(effectiveSettings.stabilizationMethod.rawValue), \(effectiveSettings.stabilizationStrength.rawValue)")
if effectiveSettings.burnInClipNumbers, !filters.hasDrawtext {
throw VideoProcessingError.invalidSettings("Clipnummern können nicht eingeblendet werden, weil dieser ffmpeg-Build den Filter drawtext nicht enthält.")
}
if effectiveSettings.stabilizationEnabled {
effectiveSettings.stabilizationMethod = resolveStabilizationMethod(
requested: effectiveSettings.stabilizationMethod,
availability: filters
)
appendLog("Stabilisierung: \(effectiveSettings.stabilizationMethod.rawValue), \(effectiveSettings.stabilizationStrength.rawValue)")
}
if effectiveSettings.burnInClipNumbers {
appendLog("Clipnummern: Einblendung unten links aktiv.")
}
}
let inputFolder = settings.inputFolder!
@@ -857,6 +865,7 @@ final class VideoProcessor: ObservableObject {
faceCrop: faceCrop,
settings: settings,
targetDimensions: targetDimensions,
clipIndex: projectClip.index,
ffmpeg: ffmpeg,
clipNumber: clipNumber,
totalClips: videos.count
@@ -876,6 +885,7 @@ final class VideoProcessor: ObservableObject {
faceCrop: faceCrop,
settings: settings,
targetDimensions: targetDimensions,
clipIndex: projectClip.index,
ffmpeg: ffmpeg,
clipNumber: clipNumber,
totalClips: videos.count,
@@ -1082,6 +1092,7 @@ final class VideoProcessor: ObservableObject {
faceCrop: FaceCrop?,
settings: RenderSettings,
targetDimensions: VideoDimensions,
clipIndex: Int,
ffmpeg: URL,
clipNumber: Int,
totalClips: Int
@@ -1093,7 +1104,7 @@ final class VideoProcessor: ObservableObject {
"-nostdin",
"-t", decimal(actualSegmentLength),
"-i", source.path,
"-vf", normalizationFilter(speedFactor: speedFactor, fps: settings.fps, targetDimensions: targetDimensions, faceCrop: faceCrop),
"-vf", normalizationFilter(speedFactor: speedFactor, fps: settings.fps, targetDimensions: targetDimensions, faceCrop: faceCrop, clipIndex: clipIndex, burnInClipNumber: settings.burnInClipNumbers),
"-t", decimal(settings.targetClipLength),
"-an"
]
@@ -1115,6 +1126,7 @@ final class VideoProcessor: ObservableObject {
faceCrop: FaceCrop?,
settings: RenderSettings,
targetDimensions: VideoDimensions,
clipIndex: Int,
ffmpeg: URL,
clipNumber: Int,
totalClips: Int,
@@ -1128,7 +1140,7 @@ final class VideoProcessor: ObservableObject {
"-ss", decimal(start),
"-t", decimal(duration),
"-i", source.path,
"-vf", normalizationFilter(speedFactor: speedFactor, fps: settings.fps, targetDimensions: targetDimensions, faceCrop: faceCrop),
"-vf", normalizationFilter(speedFactor: speedFactor, fps: settings.fps, targetDimensions: targetDimensions, faceCrop: faceCrop, clipIndex: clipIndex, burnInClipNumber: settings.burnInClipNumbers),
"-t", decimal(settings.targetClipLength),
]
arguments.append(contentsOf: videoEncoderArguments(settings))
@@ -1452,7 +1464,14 @@ final class VideoProcessor: ObservableObject {
Swift.max(minimum, Swift.min(maximum, value))
}
private func normalizationFilter(speedFactor: Double, fps: Int, targetDimensions: VideoDimensions, faceCrop: FaceCrop?) -> String {
private func normalizationFilter(
speedFactor: Double,
fps: Int,
targetDimensions: VideoDimensions,
faceCrop: FaceCrop?,
clipIndex: Int,
burnInClipNumber: Bool
) -> String {
var filters: [String] = []
if let faceCrop {
filters.append("crop=\(faceCrop.width):\(faceCrop.height):\(faceCrop.x):\(faceCrop.y)")
@@ -1463,9 +1482,26 @@ final class VideoProcessor: ObservableObject {
filters.append("pad=\(targetDimensions.width):\(targetDimensions.height):(ow-iw)/2:(oh-ih)/2")
filters.append("setsar=1")
filters.append(contentsOf: sdrFilters())
if burnInClipNumber {
filters.append(clipNumberOverlayFilter(clipIndex))
}
return filters.joined(separator: ",")
}
private func clipNumberOverlayFilter(_ clipIndex: Int) -> String {
let label = String(format: "%03d", clipIndex)
return [
"drawtext=text='\(label)'",
"x=36",
"y=h-th-32",
"fontsize=54",
"fontcolor=white",
"box=1",
"boxcolor=black@0.55",
"boxborderw=16"
].joined(separator: ":")
}
private func detectFaceCropIfNeeded(
source: URL,
start: Double,
@@ -1558,6 +1594,7 @@ final class VideoProcessor: ObservableObject {
"Übergangslänge: \(settings.transitionLength)s",
"Audio entfernen: \(settings.removeAudio ? "ja" : "nein")",
"Segment aus Mitte: \(settings.takeMiddleSegment ? "ja" : "nein")",
"Clipnummern eingeblendet: \(settings.burnInClipNumbers ? "ja" : "nein")",
"Bestes Segment automatisch: \(settings.bestSegmentDetectionEnabled ? "an, Gesicht/Augen sichtbar" : "aus")",
"Stabilisierung: \(settings.stabilizationEnabled ? settings.stabilizationMethod.rawValue : "aus")",
"Stabilisierungsstärke: \(settings.stabilizationStrength.rawValue)",