Add clip number burn-in option
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)",
|
||||
|
||||
Reference in New Issue
Block a user