Skip to content

Commit 6b18372

Browse files
committed
feat: support for specifiying custom filters for split,scale,crop,pad
When doing hardware encoding, it can be desirable to use hardware filters for split, scaling, cropping and padding. This commits add support for specifying replacements for the standard filters in a profile. It is required that the filter specified supports the parameters used by encore. Signed-off-by: Gustav Grusell <[email protected]>
1 parent 92d62cc commit 6b18372

File tree

15 files changed

+203
-21
lines changed

15 files changed

+203
-21
lines changed

checks.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ jacocoTestCoverageVerification {
1515
'*QueueService.migrateQueues()',
1616
'*.ShutdownHandler.*',
1717
'*FfmpegExecutor.runFfmpeg$lambda$7(java.lang.Process)',
18+
'*FilterSettings.*',
19+
'*.FfmpegExecutor.getProgress(java.lang.Double, java.lang.String)'
1820
]
1921
limit {
2022
counter = 'LINE'

encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/AudioEncode.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@ data class AudioEncode(
3131
val inputLabel: String = DEFAULT_AUDIO_LABEL,
3232
) : AudioEncoder() {
3333

34-
override fun getOutput(job: EncoreJob, encodingProperties: EncodingProperties): Output? {
34+
override fun getOutput(
35+
job: EncoreJob,
36+
encodingProperties: EncodingProperties,
37+
filterSettings: FilterSettings,
38+
): Output? {
3539
val outputName = "${job.baseName}$suffix.$format"
3640
val audioIn = job.inputs.audioInput(inputLabel)
3741
?: return logOrThrow("Can not generate $outputName! No audio input with label '$inputLabel'.")

encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/OutputProducer.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,5 @@ import se.svt.oss.encore.model.output.Output
2121
JsonSubTypes.Type(value = ThumbnailMapEncode::class, name = "ThumbnailMapEncode"),
2222
)
2323
interface OutputProducer {
24-
fun getOutput(job: EncoreJob, encodingProperties: EncodingProperties): Output?
24+
fun getOutput(job: EncoreJob, encodingProperties: EncodingProperties, filterSettings: FilterSettings): Output?
2525
}

encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/Profile.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,26 @@ data class Profile(
1010
val encodes: List<OutputProducer>,
1111
val scaling: String? = "bicubic",
1212
val deinterlaceFilter: String = "yadif",
13+
val filterSettings: FilterSettings = FilterSettings(),
1314
val joinSegmentParams: LinkedHashMap<String, Any?> = linkedMapOf(),
1415
)
16+
17+
data class FilterSettings(
18+
/**
19+
* The splitFilter property will be treated differently depending on if the values contains a '=' or not.
20+
* If no '=' is included, the value is treated as the name of the filter to use and something like
21+
* 'SPLITFILTERVALUE=N[ou1][out2]...' will be added to the filtergraph, where N is the number of
22+
* relevant outputs in the profile.
23+
* If an '=' is included, the value is assumed to already include the size parameters and something like
24+
* 'SPLITFILTERVALUE[ou1][out2]...' will be added to the filtergraph. Care must be taken to ensure that the
25+
* size parameters match the number of relevant outputs in the profile.
26+
* This latter form of specifying the split filter can be useful for
27+
* certain custom split filters that allow extra parameters, ie ni_quadra_split filter for netinit quadra
28+
* cards which allows access to scaled output from the decoder.
29+
*/
30+
val splitFilter: String = "split",
31+
val scaleFilter: String = "scale",
32+
val scaleFilterParams: LinkedHashMap<String, String> = linkedMapOf(),
33+
val cropFilter: String = "crop",
34+
val padFilter: String = "pad",
35+
)

encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/SimpleAudioEncode.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ data class SimpleAudioEncode(
2222
val format: String = "mp4",
2323
val inputLabel: String = DEFAULT_AUDIO_LABEL,
2424
) : AudioEncoder() {
25-
override fun getOutput(job: EncoreJob, encodingProperties: EncodingProperties): Output? {
25+
override fun getOutput(
26+
job: EncoreJob,
27+
encodingProperties: EncodingProperties,
28+
filterSettings: FilterSettings,
29+
): Output? {
2630
val outputName = "${job.baseName}$suffix.$format"
2731
job.inputs.analyzedAudio(inputLabel)
2832
?: return logOrThrow("Can not generate $outputName! No audio input with label '$inputLabel'.")

encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/ThumbnailEncode.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,11 @@ data class ThumbnailEncode(
2929
val decodeOutput: Int? = null,
3030
) : OutputProducer {
3131

32-
override fun getOutput(job: EncoreJob, encodingProperties: EncodingProperties): Output? {
32+
override fun getOutput(
33+
job: EncoreJob,
34+
encodingProperties: EncodingProperties,
35+
filterSettings: FilterSettings,
36+
): Output? {
3337
if (job.segmentLength != null) {
3438
return logOrThrow("Thumbnail is not supported in segmented encode!")
3539
}

encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/ThumbnailMapEncode.kt

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@ data class ThumbnailMapEncode(
3232
val decodeOutput: Int? = null,
3333
) : OutputProducer {
3434

35-
override fun getOutput(job: EncoreJob, encodingProperties: EncodingProperties): Output? {
35+
override fun getOutput(
36+
job: EncoreJob,
37+
encodingProperties: EncodingProperties,
38+
filterSettings: FilterSettings,
39+
): Output? {
3640
if (job.segmentLength != null) {
3741
return logOrThrow("Thumbnail map is not supported in segmented encode!")
3842
}

encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/VideoEncode.kt

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,9 +28,9 @@ interface VideoEncode : OutputProducer {
2828
val codec: String
2929
val inputLabel: String
3030

31-
override fun getOutput(job: EncoreJob, encodingProperties: EncodingProperties): Output? {
31+
override fun getOutput(job: EncoreJob, encodingProperties: EncodingProperties, filterSettings: FilterSettings): Output? {
3232
val audioEncodesToUse = audioEncodes.ifEmpty { listOfNotNull(audioEncode) }
33-
val audio = audioEncodesToUse.flatMap { it.getOutput(job, encodingProperties)?.audioStreams.orEmpty() }
33+
val audio = audioEncodesToUse.flatMap { it.getOutput(job, encodingProperties, filterSettings)?.audioStreams.orEmpty() }
3434
val videoInput = job.inputs.videoInput(inputLabel)
3535
?: throw RuntimeException("No valid video input with label $inputLabel!")
3636
return Output(
@@ -40,7 +40,7 @@ interface VideoEncode : OutputProducer {
4040
firstPassParams = firstPassParams().toParams(),
4141
inputLabels = listOf(inputLabel),
4242
twoPass = twoPass,
43-
filter = videoFilter(job.debugOverlay, encodingProperties, videoInput),
43+
filter = videoFilter(job.debugOverlay, encodingProperties, videoInput, filterSettings),
4444
),
4545
audioStreams = audio,
4646
output = "${job.baseName}$suffix.$format",
@@ -66,6 +66,7 @@ interface VideoEncode : OutputProducer {
6666
debugOverlay: Boolean,
6767
encodingProperties: EncodingProperties,
6868
videoInput: VideoIn,
69+
filterSettings: FilterSettings,
6970
): String? {
7071
val videoFilters = mutableListOf<String>()
7172
var scaleToWidth = width
@@ -83,10 +84,28 @@ interface VideoEncode : OutputProducer {
8384
scaleToHeight = width
8485
}
8586
if (scaleToWidth != null && scaleToHeight != null) {
86-
videoFilters.add("scale=$scaleToWidth:$scaleToHeight:force_original_aspect_ratio=decrease:force_divisible_by=2")
87+
val scaleParams = listOf(
88+
"$scaleToWidth",
89+
"$scaleToHeight",
90+
) + (
91+
linkedMapOf<String, String>(
92+
"force_original_aspect_ratio" to "decrease",
93+
"force_divisible_by" to "2",
94+
) + filterSettings.scaleFilterParams
95+
)
96+
.map { "${it.key}=${it.value}" }
97+
videoFilters.add(
98+
"${filterSettings.scaleFilter}=${scaleParams.joinToString(":") }",
99+
)
87100
videoFilters.add("setsar=1/1")
88101
} else if (scaleToWidth != null || scaleToHeight != null) {
89-
videoFilters.add("scale=${scaleToWidth ?: -2}:${scaleToHeight ?: -2}")
102+
val filterParams = listOf(
103+
scaleToWidth?.toString() ?: "-2",
104+
scaleToHeight?.toString() ?: "-2",
105+
) + filterSettings.scaleFilterParams.map { "${it.key}=${it.value}" }
106+
videoFilters.add(
107+
"${filterSettings.scaleFilter}=${filterParams.joinToString(":") }",
108+
)
90109
}
91110
filters?.let { videoFilters.addAll(it) }
92111
if (debugOverlay) {

encore-common/src/main/kotlin/se/svt/oss/encore/process/CommandBuilder.kt

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ class CommandBuilder(
145145
log.debug { "No video outputs for video input ${input.videoLabel}" }
146146
return@mapIndexedNotNull null
147147
}
148-
val split = "split=${splits.size}${splits.joinToString("")}"
148+
val split = splitFilter(splits)
149149
val analyzed = input.analyzedVideo
150150
val globalVideoFilters = globalVideoFilters(input, analyzed)
151151
val filters = (globalVideoFilters + split).joinToString(",")
@@ -163,6 +163,17 @@ class CommandBuilder(
163163
return videoSplits + streamFilters
164164
}
165165

166+
private fun splitFilter(splits: List<String>): String {
167+
val splitFilter = profile.filterSettings.splitFilter
168+
169+
if (splitFilter.find { it == '=' } != null) {
170+
// here we assume the size of the split is already included in the
171+
// custom split filter.
172+
return "${splitFilter}${splits.joinToString("")}"
173+
}
174+
return "$splitFilter=${splits.size}${splits.joinToString("")}"
175+
}
176+
166177
private fun VideoStreamEncode?.usesInput(input: VideoIn) =
167178
this?.inputLabels?.contains(input.videoLabel) == true
168179

@@ -189,6 +200,7 @@ class CommandBuilder(
189200

190201
private fun globalVideoFilters(input: VideoIn, videoFile: VideoFile): List<String> {
191202
val filters = mutableListOf<String>()
203+
val filterSettings = profile.filterSettings
192204
val videoStream = videoFile.highestBitrateVideoStream
193205
if (videoStream.isInterlaced) {
194206
log.debug { "Video input ${input.videoLabel} is interlaced. Applying deinterlace filter." }
@@ -203,16 +215,16 @@ class CommandBuilder(
203215
?: videoStream.displayAspectRatio?.toFractionOrNull()
204216
?: defaultAspectRatio
205217
filters.add("setdar=${dar.stringValue()}")
206-
filters.add("scale=iw*sar:ih")
218+
filters.add("${filterSettings.scaleFilter}=iw*sar:ih")
207219
} else if (videoStream.sampleAspectRatio?.toFractionOrNull() == null) {
208220
filters.add("setsar=1/1")
209221
}
210222

211223
input.cropTo?.toFraction()?.let {
212-
filters.add("crop=min(iw\\,ih*${it.stringValue()}):min(ih\\,iw/(${it.stringValue()}))")
224+
filters.add("${filterSettings.cropFilter}=min(iw\\,ih*${it.stringValue()}):min(ih\\,iw/(${it.stringValue()}))")
213225
}
214226
input.padTo?.toFraction()?.let {
215-
filters.add("pad=aspect=${it.stringValue()}:x=(ow-iw)/2:y=(oh-ih)/2")
227+
filters.add("${filterSettings.padFilter}=aspect=${it.stringValue()}:x=(ow-iw)/2:y=(oh-ih)/2")
216228
}
217229
return filters + input.videoFilters
218230
}

encore-common/src/main/kotlin/se/svt/oss/encore/service/FfmpegExecutor.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ class FfmpegExecutor(
4949
it.getOutput(
5050
encoreJob,
5151
encoreProperties.encoding,
52+
profile.filterSettings,
5253
)
5354
}
5455

0 commit comments

Comments
 (0)