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.")
|
.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)
|
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.")
|
.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)
|
.disabled(processor.state.isRunning)
|
||||||
|
|
||||||
|
|||||||
@@ -155,7 +155,8 @@ actor FFmpegService {
|
|||||||
return FFmpegFilterAvailability(
|
return FFmpegFilterAvailability(
|
||||||
hasVidstabDetect: result.output.contains("vidstabdetect"),
|
hasVidstabDetect: result.output.contains("vidstabdetect"),
|
||||||
hasVidstabTransform: result.output.contains("vidstabtransform"),
|
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 hasVidstabDetect: Bool
|
||||||
let hasVidstabTransform: Bool
|
let hasVidstabTransform: Bool
|
||||||
let hasDeshake: Bool
|
let hasDeshake: Bool
|
||||||
|
let hasDrawtext: Bool
|
||||||
|
|
||||||
var hasVidstab: Bool {
|
var hasVidstab: Bool {
|
||||||
hasVidstabDetect && hasVidstabTransform
|
hasVidstabDetect && hasVidstabTransform
|
||||||
@@ -135,6 +136,7 @@ struct RenderSettings: Equatable {
|
|||||||
var startOffset: Double = 0
|
var startOffset: Double = 0
|
||||||
var keepIntermediateClips: Bool = false
|
var keepIntermediateClips: Bool = false
|
||||||
var keepRenderCacheForReview: Bool = true
|
var keepRenderCacheForReview: Bool = true
|
||||||
|
var burnInClipNumbers: Bool = false
|
||||||
var videoEncoder: VideoEncoder = .videoToolbox
|
var videoEncoder: VideoEncoder = .videoToolbox
|
||||||
var crf: Int = 20
|
var crf: Int = 20
|
||||||
var hardwareBitrateMbps: Int = 12
|
var hardwareBitrateMbps: Int = 12
|
||||||
@@ -168,6 +170,7 @@ struct PersistedRenderSettings: Codable {
|
|||||||
var startOffset: Double
|
var startOffset: Double
|
||||||
var keepIntermediateClips: Bool
|
var keepIntermediateClips: Bool
|
||||||
var keepRenderCacheForReview: Bool?
|
var keepRenderCacheForReview: Bool?
|
||||||
|
var burnInClipNumbers: Bool?
|
||||||
var videoEncoder: VideoEncoder
|
var videoEncoder: VideoEncoder
|
||||||
var crf: Int
|
var crf: Int
|
||||||
var hardwareBitrateMbps: Int
|
var hardwareBitrateMbps: Int
|
||||||
@@ -200,6 +203,7 @@ struct PersistedRenderSettings: Codable {
|
|||||||
startOffset = settings.startOffset
|
startOffset = settings.startOffset
|
||||||
keepIntermediateClips = settings.keepIntermediateClips
|
keepIntermediateClips = settings.keepIntermediateClips
|
||||||
keepRenderCacheForReview = settings.keepRenderCacheForReview
|
keepRenderCacheForReview = settings.keepRenderCacheForReview
|
||||||
|
burnInClipNumbers = settings.burnInClipNumbers
|
||||||
videoEncoder = settings.videoEncoder
|
videoEncoder = settings.videoEncoder
|
||||||
crf = settings.crf
|
crf = settings.crf
|
||||||
hardwareBitrateMbps = settings.hardwareBitrateMbps
|
hardwareBitrateMbps = settings.hardwareBitrateMbps
|
||||||
@@ -234,6 +238,7 @@ struct PersistedRenderSettings: Codable {
|
|||||||
startOffset: startOffset,
|
startOffset: startOffset,
|
||||||
keepIntermediateClips: keepIntermediateClips,
|
keepIntermediateClips: keepIntermediateClips,
|
||||||
keepRenderCacheForReview: keepRenderCacheForReview ?? true,
|
keepRenderCacheForReview: keepRenderCacheForReview ?? true,
|
||||||
|
burnInClipNumbers: burnInClipNumbers ?? false,
|
||||||
videoEncoder: videoEncoder,
|
videoEncoder: videoEncoder,
|
||||||
crf: crf,
|
crf: crf,
|
||||||
hardwareBitrateMbps: hardwareBitrateMbps,
|
hardwareBitrateMbps: hardwareBitrateMbps,
|
||||||
|
|||||||
@@ -174,14 +174,22 @@ final class VideoProcessor: ObservableObject {
|
|||||||
return try await renderPhotoMorph(settings: effectiveSettings, ffmpeg: tools.ffmpeg, ffprobe: tools.ffprobe)
|
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)
|
let filters = try await service.availableFilters(ffmpeg: tools.ffmpeg)
|
||||||
|
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(
|
effectiveSettings.stabilizationMethod = resolveStabilizationMethod(
|
||||||
requested: effectiveSettings.stabilizationMethod,
|
requested: effectiveSettings.stabilizationMethod,
|
||||||
availability: filters
|
availability: filters
|
||||||
)
|
)
|
||||||
appendLog("Stabilisierung: \(effectiveSettings.stabilizationMethod.rawValue), \(effectiveSettings.stabilizationStrength.rawValue)")
|
appendLog("Stabilisierung: \(effectiveSettings.stabilizationMethod.rawValue), \(effectiveSettings.stabilizationStrength.rawValue)")
|
||||||
}
|
}
|
||||||
|
if effectiveSettings.burnInClipNumbers {
|
||||||
|
appendLog("Clipnummern: Einblendung unten links aktiv.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let inputFolder = settings.inputFolder!
|
let inputFolder = settings.inputFolder!
|
||||||
let outputFile = settings.outputFile!
|
let outputFile = settings.outputFile!
|
||||||
@@ -857,6 +865,7 @@ final class VideoProcessor: ObservableObject {
|
|||||||
faceCrop: faceCrop,
|
faceCrop: faceCrop,
|
||||||
settings: settings,
|
settings: settings,
|
||||||
targetDimensions: targetDimensions,
|
targetDimensions: targetDimensions,
|
||||||
|
clipIndex: projectClip.index,
|
||||||
ffmpeg: ffmpeg,
|
ffmpeg: ffmpeg,
|
||||||
clipNumber: clipNumber,
|
clipNumber: clipNumber,
|
||||||
totalClips: videos.count
|
totalClips: videos.count
|
||||||
@@ -876,6 +885,7 @@ final class VideoProcessor: ObservableObject {
|
|||||||
faceCrop: faceCrop,
|
faceCrop: faceCrop,
|
||||||
settings: settings,
|
settings: settings,
|
||||||
targetDimensions: targetDimensions,
|
targetDimensions: targetDimensions,
|
||||||
|
clipIndex: projectClip.index,
|
||||||
ffmpeg: ffmpeg,
|
ffmpeg: ffmpeg,
|
||||||
clipNumber: clipNumber,
|
clipNumber: clipNumber,
|
||||||
totalClips: videos.count,
|
totalClips: videos.count,
|
||||||
@@ -1082,6 +1092,7 @@ final class VideoProcessor: ObservableObject {
|
|||||||
faceCrop: FaceCrop?,
|
faceCrop: FaceCrop?,
|
||||||
settings: RenderSettings,
|
settings: RenderSettings,
|
||||||
targetDimensions: VideoDimensions,
|
targetDimensions: VideoDimensions,
|
||||||
|
clipIndex: Int,
|
||||||
ffmpeg: URL,
|
ffmpeg: URL,
|
||||||
clipNumber: Int,
|
clipNumber: Int,
|
||||||
totalClips: Int
|
totalClips: Int
|
||||||
@@ -1093,7 +1104,7 @@ final class VideoProcessor: ObservableObject {
|
|||||||
"-nostdin",
|
"-nostdin",
|
||||||
"-t", decimal(actualSegmentLength),
|
"-t", decimal(actualSegmentLength),
|
||||||
"-i", source.path,
|
"-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),
|
"-t", decimal(settings.targetClipLength),
|
||||||
"-an"
|
"-an"
|
||||||
]
|
]
|
||||||
@@ -1115,6 +1126,7 @@ final class VideoProcessor: ObservableObject {
|
|||||||
faceCrop: FaceCrop?,
|
faceCrop: FaceCrop?,
|
||||||
settings: RenderSettings,
|
settings: RenderSettings,
|
||||||
targetDimensions: VideoDimensions,
|
targetDimensions: VideoDimensions,
|
||||||
|
clipIndex: Int,
|
||||||
ffmpeg: URL,
|
ffmpeg: URL,
|
||||||
clipNumber: Int,
|
clipNumber: Int,
|
||||||
totalClips: Int,
|
totalClips: Int,
|
||||||
@@ -1128,7 +1140,7 @@ final class VideoProcessor: ObservableObject {
|
|||||||
"-ss", decimal(start),
|
"-ss", decimal(start),
|
||||||
"-t", decimal(duration),
|
"-t", decimal(duration),
|
||||||
"-i", source.path,
|
"-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),
|
"-t", decimal(settings.targetClipLength),
|
||||||
]
|
]
|
||||||
arguments.append(contentsOf: videoEncoderArguments(settings))
|
arguments.append(contentsOf: videoEncoderArguments(settings))
|
||||||
@@ -1452,7 +1464,14 @@ final class VideoProcessor: ObservableObject {
|
|||||||
Swift.max(minimum, Swift.min(maximum, value))
|
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] = []
|
var filters: [String] = []
|
||||||
if let faceCrop {
|
if let faceCrop {
|
||||||
filters.append("crop=\(faceCrop.width):\(faceCrop.height):\(faceCrop.x):\(faceCrop.y)")
|
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("pad=\(targetDimensions.width):\(targetDimensions.height):(ow-iw)/2:(oh-ih)/2")
|
||||||
filters.append("setsar=1")
|
filters.append("setsar=1")
|
||||||
filters.append(contentsOf: sdrFilters())
|
filters.append(contentsOf: sdrFilters())
|
||||||
|
if burnInClipNumber {
|
||||||
|
filters.append(clipNumberOverlayFilter(clipIndex))
|
||||||
|
}
|
||||||
return filters.joined(separator: ",")
|
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(
|
private func detectFaceCropIfNeeded(
|
||||||
source: URL,
|
source: URL,
|
||||||
start: Double,
|
start: Double,
|
||||||
@@ -1558,6 +1594,7 @@ final class VideoProcessor: ObservableObject {
|
|||||||
"Übergangslänge: \(settings.transitionLength)s",
|
"Übergangslänge: \(settings.transitionLength)s",
|
||||||
"Audio entfernen: \(settings.removeAudio ? "ja" : "nein")",
|
"Audio entfernen: \(settings.removeAudio ? "ja" : "nein")",
|
||||||
"Segment aus Mitte: \(settings.takeMiddleSegment ? "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")",
|
"Bestes Segment automatisch: \(settings.bestSegmentDetectionEnabled ? "an, Gesicht/Augen sichtbar" : "aus")",
|
||||||
"Stabilisierung: \(settings.stabilizationEnabled ? settings.stabilizationMethod.rawValue : "aus")",
|
"Stabilisierung: \(settings.stabilizationEnabled ? settings.stabilizationMethod.rawValue : "aus")",
|
||||||
"Stabilisierungsstärke: \(settings.stabilizationStrength.rawValue)",
|
"Stabilisierungsstärke: \(settings.stabilizationStrength.rawValue)",
|
||||||
|
|||||||
Reference in New Issue
Block a user