diff --git a/checks.gradle b/checks.gradle index 7a689d62..b62fecc3 100644 --- a/checks.gradle +++ b/checks.gradle @@ -15,6 +15,7 @@ jacocoTestCoverageVerification { '*QueueService.migrateQueues()', '*.ShutdownHandler.*', '*FfmpegExecutor.runFfmpeg$lambda$?(java.lang.Process)', + '*FilterSettings.*', ] limit { counter = 'LINE' diff --git a/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/AudioEncode.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/AudioEncode.kt index bbb5bf3c..23254d34 100644 --- a/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/AudioEncode.kt +++ b/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/AudioEncode.kt @@ -32,7 +32,11 @@ data class AudioEncode( override val enabled: Boolean = true, ) : AudioEncoder() { - override fun getOutput(job: EncoreJob, encodingProperties: EncodingProperties): Output? { + override fun getOutput( + job: EncoreJob, + encodingProperties: EncodingProperties, + filterSettings: FilterSettings, + ): Output? { val outputName = "${job.baseName}$suffix.$format" if (!enabled) { return logOrThrow("$outputName is disabled. Skipping...") diff --git a/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/OutputProducer.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/OutputProducer.kt index 3c91f345..6fe3f166 100644 --- a/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/OutputProducer.kt +++ b/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/OutputProducer.kt @@ -21,5 +21,5 @@ import se.svt.oss.encore.model.output.Output JsonSubTypes.Type(value = ThumbnailMapEncode::class, name = "ThumbnailMapEncode"), ) interface OutputProducer { - fun getOutput(job: EncoreJob, encodingProperties: EncodingProperties): Output? + fun getOutput(job: EncoreJob, encodingProperties: EncodingProperties, filterSettings: FilterSettings): Output? } diff --git a/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/Profile.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/Profile.kt index 019fee80..90692fc8 100644 --- a/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/Profile.kt +++ b/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/Profile.kt @@ -10,5 +10,26 @@ data class Profile( val encodes: List, val scaling: String? = "bicubic", val deinterlaceFilter: String = "yadif", + val filterSettings: FilterSettings = FilterSettings(), val joinSegmentParams: LinkedHashMap = linkedMapOf(), ) + +data class FilterSettings( + /** + * The splitFilter property will be treated differently depending on if the values contains a '=' or not. + * If no '=' is included, the value is treated as the name of the filter to use and something like + * 'SPLITFILTERVALUE=N[ou1][out2]...' will be added to the filtergraph, where N is the number of + * relevant outputs in the profile. + * If an '=' is included, the value is assumed to already include the size parameters and something like + * 'SPLITFILTERVALUE[ou1][out2]...' will be added to the filtergraph. Care must be taken to ensure that the + * size parameters match the number of relevant outputs in the profile. + * This latter form of specifying the split filter can be useful for + * certain custom split filters that allow extra parameters, ie ni_quadra_split filter for netinit quadra + * cards which allows access to scaled output from the decoder. + */ + val splitFilter: String = "split", + val scaleFilter: String = "scale", + val scaleFilterParams: LinkedHashMap = linkedMapOf(), + val cropFilter: String = "crop", + val padFilter: String = "pad", +) diff --git a/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/SimpleAudioEncode.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/SimpleAudioEncode.kt index de41224e..52fc1a98 100644 --- a/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/SimpleAudioEncode.kt +++ b/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/SimpleAudioEncode.kt @@ -23,7 +23,11 @@ data class SimpleAudioEncode( val format: String = "mp4", val inputLabel: String = DEFAULT_AUDIO_LABEL, ) : AudioEncoder() { - override fun getOutput(job: EncoreJob, encodingProperties: EncodingProperties): Output? { + override fun getOutput( + job: EncoreJob, + encodingProperties: EncodingProperties, + filterSettings: FilterSettings, + ): Output? { val outputName = "${job.baseName}$suffix.$format" if (!enabled) { return logOrThrow("$outputName is disabled. Skipping...") diff --git a/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/ThumbnailEncode.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/ThumbnailEncode.kt index ddc64c74..9442c46e 100644 --- a/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/ThumbnailEncode.kt +++ b/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/ThumbnailEncode.kt @@ -30,7 +30,11 @@ data class ThumbnailEncode( val decodeOutput: Int? = null, ) : OutputProducer { - override fun getOutput(job: EncoreJob, encodingProperties: EncodingProperties): Output? { + override fun getOutput( + job: EncoreJob, + encodingProperties: EncodingProperties, + filterSettings: FilterSettings, + ): Output? { if (!enabled) { return logOrThrow("Thumbnail with suffix $suffix is disabled. Skipping...") } diff --git a/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/ThumbnailMapEncode.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/ThumbnailMapEncode.kt index 1c840180..91524720 100644 --- a/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/ThumbnailMapEncode.kt +++ b/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/ThumbnailMapEncode.kt @@ -33,7 +33,11 @@ data class ThumbnailMapEncode( val decodeOutput: Int? = null, ) : OutputProducer { - override fun getOutput(job: EncoreJob, encodingProperties: EncodingProperties): Output? { + override fun getOutput( + job: EncoreJob, + encodingProperties: EncodingProperties, + filterSettings: FilterSettings, + ): Output? { if (!enabled) { return logOrThrow("Thumbnail map with suffix $suffix is disabled. Skipping...") } diff --git a/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/VideoEncode.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/VideoEncode.kt index f31693a0..08026b00 100644 --- a/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/VideoEncode.kt +++ b/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/VideoEncode.kt @@ -38,13 +38,13 @@ interface VideoEncode : OutputProducer { val cropTo: FractionString? val padTo: FractionString? - override fun getOutput(job: EncoreJob, encodingProperties: EncodingProperties): Output? { + override fun getOutput(job: EncoreJob, encodingProperties: EncodingProperties, filterSettings: FilterSettings): Output? { if (!enabled) { log.info { "Encode $suffix is not enabled. Skipping." } return null } val audioEncodesToUse = audioEncodes.ifEmpty { listOfNotNull(audioEncode) } - val audio = audioEncodesToUse.flatMap { it.getOutput(job, encodingProperties)?.audioStreams.orEmpty() } + val audio = audioEncodesToUse.flatMap { it.getOutput(job, encodingProperties, filterSettings)?.audioStreams.orEmpty() } val videoInput = job.inputs.videoInput(inputLabel) ?: return logOrThrow("No valid video input with label $inputLabel!") return Output( @@ -54,7 +54,7 @@ interface VideoEncode : OutputProducer { firstPassParams = firstPassParams().toParams(), inputLabels = listOf(inputLabel), twoPass = twoPass, - filter = videoFilter(job.debugOverlay, encodingProperties, videoInput), + filter = videoFilter(job.debugOverlay, encodingProperties, videoInput, filterSettings), ), audioStreams = audio, output = "${job.baseName}$suffix.$format", @@ -80,10 +80,11 @@ interface VideoEncode : OutputProducer { debugOverlay: Boolean, encodingProperties: EncodingProperties, videoInput: VideoIn, + filterSettings: FilterSettings, ): String? { val videoFilters = mutableListOf() cropTo?.toFraction()?.let { - videoFilters.add("crop=min(iw\\,ih*${it.stringValue()}):min(ih\\,iw/(${it.stringValue()}))") + videoFilters.add("${filterSettings.cropFilter}=min(iw\\,ih*${it.stringValue()}):min(ih\\,iw/(${it.stringValue()}))") } var scaleToWidth = width var scaleToHeight = height @@ -100,13 +101,31 @@ interface VideoEncode : OutputProducer { scaleToHeight = width } if (scaleToWidth != null && scaleToHeight != null) { - videoFilters.add("scale=$scaleToWidth:$scaleToHeight:force_original_aspect_ratio=decrease:force_divisible_by=2") + val scaleParams = listOf( + "$scaleToWidth", + "$scaleToHeight", + ) + ( + linkedMapOf( + "force_original_aspect_ratio" to "decrease", + "force_divisible_by" to "2", + ) + filterSettings.scaleFilterParams + ) + .map { "${it.key}=${it.value}" } + videoFilters.add( + "${filterSettings.scaleFilter}=${scaleParams.joinToString(":") }", + ) videoFilters.add("setsar=1/1") } else if (scaleToWidth != null || scaleToHeight != null) { - videoFilters.add("scale=${scaleToWidth ?: -2}:${scaleToHeight ?: -2}") + val filterParams = listOf( + scaleToWidth?.toString() ?: "-2", + scaleToHeight?.toString() ?: "-2", + ) + filterSettings.scaleFilterParams.map { "${it.key}=${it.value}" } + videoFilters.add( + "${filterSettings.scaleFilter}=${filterParams.joinToString(":") }", + ) } padTo?.toFraction()?.let { - videoFilters.add("pad=aspect=${it.stringValue()}:x=(ow-iw)/2:y=(oh-ih)/2") + videoFilters.add("${filterSettings.padFilter}=aspect=${it.stringValue()}:x=(ow-iw)/2:y=(oh-ih)/2") } filters?.let { videoFilters.addAll(it) } if (debugOverlay) { diff --git a/encore-common/src/main/kotlin/se/svt/oss/encore/process/CommandBuilder.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/process/CommandBuilder.kt index 899023c5..11cbd6f4 100644 --- a/encore-common/src/main/kotlin/se/svt/oss/encore/process/CommandBuilder.kt +++ b/encore-common/src/main/kotlin/se/svt/oss/encore/process/CommandBuilder.kt @@ -145,7 +145,8 @@ class CommandBuilder( log.debug { "No video outputs for video input ${input.videoLabel}" } return@mapIndexedNotNull null } - val split = "split=${splits.size}${splits.joinToString("")}" + // val split = "split=${splits.size}${splits.joinToString("")}" + val split = splitFilter(splits) val analyzed = input.analyzedVideo val globalVideoFilters = globalVideoFilters(input, analyzed) val filters = (globalVideoFilters + split).joinToString(",") @@ -163,6 +164,17 @@ class CommandBuilder( return videoSplits + streamFilters } + private fun splitFilter(splits: List): String { + val splitFilter = profile.filterSettings.splitFilter + + if (splitFilter.find { it == '=' } != null) { + // here we assume the size of the split is already included in the + // custom split filter. + return "${splitFilter}${splits.joinToString("")}" + } + return "$splitFilter=${splits.size}${splits.joinToString("")}" + } + private fun VideoStreamEncode?.usesInput(input: VideoIn) = this?.inputLabels?.contains(input.videoLabel) == true @@ -189,6 +201,7 @@ class CommandBuilder( private fun globalVideoFilters(input: VideoIn, videoFile: VideoFile): List { val filters = mutableListOf() + val filterSettings = profile.filterSettings val videoStream = videoFile.highestBitrateVideoStream if (videoStream.isInterlaced) { log.debug { "Video input ${input.videoLabel} is interlaced. Applying deinterlace filter." } @@ -203,16 +216,16 @@ class CommandBuilder( ?: videoStream.displayAspectRatio?.toFractionOrNull() ?: defaultAspectRatio filters.add("setdar=${dar.stringValue()}") - filters.add("scale=iw*sar:ih") + filters.add("${filterSettings.scaleFilter}=iw*sar:ih") } else if (videoStream.sampleAspectRatio?.toFractionOrNull() == null) { filters.add("setsar=1/1") } input.cropTo?.toFraction()?.let { - filters.add("crop=min(iw\\,ih*${it.stringValue()}):min(ih\\,iw/(${it.stringValue()}))") + filters.add("${filterSettings.cropFilter}=min(iw\\,ih*${it.stringValue()}):min(ih\\,iw/(${it.stringValue()}))") } input.padTo?.toFraction()?.let { - filters.add("pad=aspect=${it.stringValue()}:x=(ow-iw)/2:y=(oh-ih)/2") + filters.add("${filterSettings.padFilter}=aspect=${it.stringValue()}:x=(ow-iw)/2:y=(oh-ih)/2") } return filters + input.videoFilters } diff --git a/encore-common/src/main/kotlin/se/svt/oss/encore/service/FfmpegExecutor.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/service/FfmpegExecutor.kt index 737f40e6..a42eb75d 100644 --- a/encore-common/src/main/kotlin/se/svt/oss/encore/service/FfmpegExecutor.kt +++ b/encore-common/src/main/kotlin/se/svt/oss/encore/service/FfmpegExecutor.kt @@ -49,6 +49,7 @@ class FfmpegExecutor( it.getOutput( encoreJob, encoreProperties.encoding, + profile.filterSettings, ) } check(outputs.isNotEmpty()) { diff --git a/encore-common/src/test/kotlin/se/svt/oss/encore/model/profile/AudioEncodeTest.kt b/encore-common/src/test/kotlin/se/svt/oss/encore/model/profile/AudioEncodeTest.kt index 479b2e3a..de0c0356 100644 --- a/encore-common/src/test/kotlin/se/svt/oss/encore/model/profile/AudioEncodeTest.kt +++ b/encore-common/src/test/kotlin/se/svt/oss/encore/model/profile/AudioEncodeTest.kt @@ -30,7 +30,7 @@ class AudioEncodeTest { @Test fun `no audio streams throws exception`() { assertThatThrownBy { - audioEncode.getOutput(job(), EncodingProperties()) + audioEncode.getOutput(job(), EncodingProperties(), FilterSettings()) }.isInstanceOf(RuntimeException::class.java) .hasMessageContaining("No audio streams in input") } @@ -42,6 +42,7 @@ class AudioEncodeTest { audioEncode.getOutput( job, EncodingProperties(audioMixPresets = mapOf("default" to AudioMixPreset(fallbackToAuto = false))), + FilterSettings(), ) }.isInstanceOf(RuntimeException::class.java) .hasMessage("Audio layout of audio input 'main' is not supported!") @@ -52,6 +53,7 @@ class AudioEncodeTest { val output = audioEncode.getOutput( job(getAudioStream(6)), EncodingProperties(), + FilterSettings(), ) assertThat(output) .hasOutput("test_aac_stereo.mp4") @@ -69,7 +71,7 @@ class AudioEncodeTest { @Test fun `returns null when not enabled`() { val output = audioEncode.copy(enabled = false) - .getOutput(job(getAudioStream(6)), EncodingProperties()) + .getOutput(job(getAudioStream(6)), EncodingProperties(), FilterSettings()) assertThat(output).isNull() } @@ -95,6 +97,7 @@ class AudioEncodeTest { ), ), ), + FilterSettings(), ) assertThat(output) .hasOutput("test_aac_stereo.mp4") @@ -137,6 +140,7 @@ class AudioEncodeTest { ), ), ), + FilterSettings(), ) assertThat(output).isNull() } @@ -157,6 +161,7 @@ class AudioEncodeTest { ), ), ), + FilterSettings(), ) }.isInstanceOf(RuntimeException::class.java) .hasMessageContaining("No audio mix preset for 'de': 5.1 -> stereo") @@ -165,7 +170,7 @@ class AudioEncodeTest { @Test fun `unmapped input optional returns null`() { val audioEncodeLocal = audioEncode.copy(inputLabel = "other", optional = true) - val output = audioEncodeLocal.getOutput(job(getAudioStream(6)), EncodingProperties()) + val output = audioEncodeLocal.getOutput(job(getAudioStream(6)), EncodingProperties(), FilterSettings()) assertThat(output).isNull() } @@ -173,7 +178,7 @@ class AudioEncodeTest { fun `unmapped input not optional throws`() { val audioEncodeLocal = audioEncode.copy(inputLabel = "other") assertThatThrownBy { - audioEncodeLocal.getOutput(job(getAudioStream(6)), EncodingProperties()) + audioEncodeLocal.getOutput(job(getAudioStream(6)), EncodingProperties(), FilterSettings()) }.isInstanceOf(RuntimeException::class.java) .hasMessage("Can not generate test_aac_stereo.mp4! No audio input with label 'other'.") } diff --git a/encore-common/src/test/kotlin/se/svt/oss/encore/model/profile/ThumbnailEncodeTest.kt b/encore-common/src/test/kotlin/se/svt/oss/encore/model/profile/ThumbnailEncodeTest.kt index 97b518ae..496dca8f 100644 --- a/encore-common/src/test/kotlin/se/svt/oss/encore/model/profile/ThumbnailEncodeTest.kt +++ b/encore-common/src/test/kotlin/se/svt/oss/encore/model/profile/ThumbnailEncodeTest.kt @@ -27,6 +27,7 @@ class ThumbnailEncodeTest { val output = encode.getOutput( job = defaultEncoreJob(), encodingProperties = EncodingProperties(), + FilterSettings(), ) assertThat(output) .hasOutput("test_thumb%02d.jpg") @@ -46,6 +47,7 @@ class ThumbnailEncodeTest { val output = encode.copy(enabled = false).getOutput( job = defaultEncoreJob(), encodingProperties = EncodingProperties(), + FilterSettings(), ) assertThat(output).isNull() } @@ -57,6 +59,7 @@ class ThumbnailEncodeTest { thumbnailTime = 5.0, ), encodingProperties = EncodingProperties(), + FilterSettings(), ) assertThat(output) .hasOutput("test_thumb%02d.jpg") @@ -80,6 +83,7 @@ class ThumbnailEncodeTest { val output = selectorEncode.getOutput( job = defaultEncoreJob(), encodingProperties = EncodingProperties(), + FilterSettings(), ) assertThat(output) @@ -103,6 +107,7 @@ class ThumbnailEncodeTest { duration = 4.0, ), encodingProperties = EncodingProperties(), + FilterSettings(), ) assertThat(output) .hasOutput("test_thumb%02d.jpg") @@ -133,6 +138,7 @@ class ThumbnailEncodeTest { ), ), encodingProperties = EncodingProperties(), + FilterSettings(), ) assertThat(output) .hasOutput("test_thumb%02d.jpg") @@ -152,6 +158,7 @@ class ThumbnailEncodeTest { val output = encode.copy(inputLabel = "other", optional = true).getOutput( job = defaultEncoreJob(), encodingProperties = EncodingProperties(), + FilterSettings(), ) assertThat(output).isNull() } @@ -162,6 +169,7 @@ class ThumbnailEncodeTest { encode.copy(inputLabel = "other", optional = false).getOutput( job = defaultEncoreJob(), encodingProperties = EncodingProperties(), + FilterSettings(), ) }.isInstanceOf(RuntimeException::class.java) .hasMessageContaining("No video input with label other!") diff --git a/encore-common/src/test/kotlin/se/svt/oss/encore/model/profile/ThumbnailMapEncodeTest.kt b/encore-common/src/test/kotlin/se/svt/oss/encore/model/profile/ThumbnailMapEncodeTest.kt index 07651e42..b00cf966 100644 --- a/encore-common/src/test/kotlin/se/svt/oss/encore/model/profile/ThumbnailMapEncodeTest.kt +++ b/encore-common/src/test/kotlin/se/svt/oss/encore/model/profile/ThumbnailMapEncodeTest.kt @@ -23,7 +23,7 @@ class ThumbnailMapEncodeTest { @Test fun `correct output`() { - val output = encode.getOutput(defaultEncoreJob(), EncodingProperties()) + val output = encode.getOutput(defaultEncoreJob(), EncodingProperties(), FilterSettings()) assertThat(output) .hasNoAudioStreams() .hasId("_12x20_160x90_thumbnail_map.jpg") @@ -44,6 +44,7 @@ class ThumbnailMapEncodeTest { defaultEncoreJob() .copy(seekTo = 1.0, duration = 5.0), EncodingProperties(), + FilterSettings(), ) assertThat(output) .hasNoAudioStreams() @@ -63,6 +64,7 @@ class ThumbnailMapEncodeTest { val output = encode.copy(inputLabel = "other", optional = true).getOutput( job = defaultEncoreJob(), encodingProperties = EncodingProperties(), + FilterSettings(), ) assertThat(output).isNull() } @@ -73,6 +75,7 @@ class ThumbnailMapEncodeTest { encode.copy(inputLabel = "other", optional = false).getOutput( job = defaultEncoreJob(), encodingProperties = EncodingProperties(), + FilterSettings(), ) }.isInstanceOf(RuntimeException::class.java) .hasMessageContaining("No input with label other!") @@ -83,6 +86,7 @@ class ThumbnailMapEncodeTest { val output = encode.copy(enabled = false).getOutput( job = defaultEncoreJob(), encodingProperties = EncodingProperties(), + filterSettings = FilterSettings(), ) assertThat(output).isNull() } diff --git a/encore-common/src/test/kotlin/se/svt/oss/encore/model/profile/VideoEncodeTest.kt b/encore-common/src/test/kotlin/se/svt/oss/encore/model/profile/VideoEncodeTest.kt index e1a61764..7a4d1a94 100644 --- a/encore-common/src/test/kotlin/se/svt/oss/encore/model/profile/VideoEncodeTest.kt +++ b/encore-common/src/test/kotlin/se/svt/oss/encore/model/profile/VideoEncodeTest.kt @@ -35,13 +35,14 @@ abstract class VideoEncodeTest { ): T private val encodingProperties = EncodingProperties() + private val filterSettings = FilterSettings() private val audioEncode = mockk() private val audioStreamEncode = mockk() private val defaultParams = linkedMapOf("a" to "b") @BeforeEach internal fun setUp() { - every { audioEncode.getOutput(any(), encodingProperties)?.audioStreams } returns listOf(audioStreamEncode) + every { audioEncode.getOutput(any(), encodingProperties, filterSettings)?.audioStreams } returns listOf(audioStreamEncode) } @Test @@ -65,6 +66,7 @@ abstract class VideoEncodeTest { ), ), encodingProperties, + filterSettings, ) assertThat(output?.video).hasFilter("scale=1080:1920:force_original_aspect_ratio=decrease:force_divisible_by=2,setsar=1/1") @@ -92,6 +94,7 @@ abstract class VideoEncodeTest { ), ), encodingProperties, + filterSettings, ) assertThat(output?.video).hasFilter("scale=1080:1920:force_original_aspect_ratio=decrease:force_divisible_by=2,setsar=1/1") @@ -107,7 +110,7 @@ abstract class VideoEncodeTest { filters = listOf("afilter"), audioEncode = audioEncode, ) - val output = encode.getOutput(defaultEncoreJob(), encodingProperties) + val output = encode.getOutput(defaultEncoreJob(), encodingProperties, filterSettings) assertThat(output) .hasOnlyAudioStreams(audioStreamEncode) val videoStreamEncode = output!!.video @@ -120,6 +123,31 @@ abstract class VideoEncodeTest { verifySecondPassParams(encode, videoStreamEncode.params) } + @Test + fun `single pass scale to height with custom scale filter`() { + val filterSettings = FilterSettings(scaleFilter = "myscale") + every { audioEncode.getOutput(any(), encodingProperties, filterSettings)?.audioStreams } returns listOf(audioStreamEncode) + val encode = createEncode( + width = null, + height = 1080, + twoPass = false, + params = defaultParams, + filters = listOf("afilter"), + audioEncode = audioEncode, + ) + val output = encode.getOutput(defaultEncoreJob(), encodingProperties, filterSettings) + assertThat(output) + .hasOnlyAudioStreams(audioStreamEncode) + val videoStreamEncode = output!!.video + assertThat(videoStreamEncode) + .isNotNull + .hasNoFirstPassParams() + .hasTwoPass(false) + .hasFilter("myscale=-2:1080,afilter") + verifyFirstPassParams(encode, videoStreamEncode!!.firstPassParams) + verifySecondPassParams(encode, videoStreamEncode.params) + } + @Test fun `two-pass encode`() { val encode = createEncode( @@ -130,7 +158,7 @@ abstract class VideoEncodeTest { filters = listOf("afilter"), audioEncode = audioEncode, ) - val output = encode.getOutput(defaultEncoreJob(), encodingProperties) + val output = encode.getOutput(defaultEncoreJob(), encodingProperties, filterSettings) assertThat(output).isNotNull val videoStreamEncode = output!!.video assertThat(videoStreamEncode) @@ -162,6 +190,7 @@ abstract class VideoEncodeTest { ), ), encodingProperties, + FilterSettings(), ) assertThat(output).isNull() } @@ -190,6 +219,7 @@ abstract class VideoEncodeTest { ), ), encodingProperties, + FilterSettings(), ) }.hasMessage("No valid video input with label main!") } @@ -205,7 +235,7 @@ abstract class VideoEncodeTest { audioEncode = audioEncode, enabled = false, ) - val output = encode.getOutput(defaultEncoreJob(), encodingProperties) + val output = encode.getOutput(defaultEncoreJob(), encodingProperties, FilterSettings()) assertThat(output).isNull() } @@ -221,7 +251,7 @@ abstract class VideoEncodeTest { cropTo = "9:16", padTo = "16:9", ) - val output = encode.getOutput(defaultEncoreJob(), encodingProperties) + val output = encode.getOutput(defaultEncoreJob(), encodingProperties, FilterSettings()) assertThat(output?.video).hasFilter("crop=min(iw\\,ih*9/16):min(ih\\,iw/(9/16)),scale=1920:1080:force_original_aspect_ratio=decrease:force_divisible_by=2,setsar=1/1,pad=aspect=16/9:x=(ow-iw)/2:y=(oh-ih)/2,afilter") } diff --git a/encore-common/src/test/kotlin/se/svt/oss/encore/process/CommandBuilderTest.kt b/encore-common/src/test/kotlin/se/svt/oss/encore/process/CommandBuilderTest.kt index 0908fde7..86284591 100644 --- a/encore-common/src/test/kotlin/se/svt/oss/encore/process/CommandBuilderTest.kt +++ b/encore-common/src/test/kotlin/se/svt/oss/encore/process/CommandBuilderTest.kt @@ -22,6 +22,7 @@ import se.svt.oss.encore.model.output.AudioStreamEncode import se.svt.oss.encore.model.output.Output import se.svt.oss.encore.model.output.VideoStreamEncode import se.svt.oss.encore.model.profile.ChannelLayout +import se.svt.oss.encore.model.profile.FilterSettings import se.svt.oss.encore.model.profile.Profile import se.svt.oss.mediaanalyzer.file.AudioFile @@ -40,6 +41,7 @@ internal class CommandBuilderTest { commandBuilder = CommandBuilder(encoreJob, profile, encoreJob.outputFolder, encodingProperties) every { profile.scaling } returns "scaling" every { profile.deinterlaceFilter } returns "yadif" + every { profile.filterSettings } returns FilterSettings() } @Test @@ -90,6 +92,72 @@ internal class CommandBuilderTest { assertThat(command).isEqualTo("ffmpeg -xerror -hide_banner -loglevel +level -y -i /input/test.mp4 -filter_complex sws_flags=scaling;[0:a]join=inputs=3:channel_layout=3.0:map=0.0-FL|1.0-FR|2.0-FC,asplit=1[AUDIO-main-test-out-0];[AUDIO-main-test-out-0]aformat=channel_layouts=stereo[AUDIO-test-out-0] -map [AUDIO-test-out-0] -vn -c:a:0 aac -metadata comment=Transcoded using Encore /output/path/out.mp4") } + @Test + fun `custom splitFilter no size param`() { + every { profile.filterSettings } returns FilterSettings(splitFilter = "custom-split-filter") + val buildCommands = commandBuilder.buildCommands(listOf(output(false))) + + assertThat(buildCommands).hasSize(1) + + val command = buildCommands.first().joinToString(" ") + assertThat(command).isEqualTo("ffmpeg -xerror -hide_banner -loglevel +level -y -i /input/test.mp4 -filter_complex sws_flags=scaling;[0:v]custom-split-filter=1[VIDEO-main-test-out];[VIDEO-main-test-out]video-filter[VIDEO-test-out];[0:a]join=inputs=8:channel_layout=7.1:map=0.0-FL|1.0-FR|2.0-FC|3.0-LFE|4.0-BL|5.0-BR|6.0-SL|7.0-SR,asplit=1[AUDIO-main-test-out-0];[AUDIO-main-test-out-0]audio-filter[AUDIO-test-out-0] -map [VIDEO-test-out] -map [AUDIO-test-out-0] video params audio params -metadata comment=Transcoded using Encore /output/path/out.mp4") + } + + @Test + fun `custom splitFilter with size params`() { + every { profile.filterSettings } returns FilterSettings(splitFilter = "custom-split-filter=1:2:3") + val buildCommands = commandBuilder.buildCommands(listOf(output(false))) + + assertThat(buildCommands).hasSize(1) + + val command = buildCommands.first().joinToString(" ") + assertThat(command).isEqualTo("ffmpeg -xerror -hide_banner -loglevel +level -y -i /input/test.mp4 -filter_complex sws_flags=scaling;[0:v]custom-split-filter=1:2:3[VIDEO-main-test-out];[VIDEO-main-test-out]video-filter[VIDEO-test-out];[0:a]join=inputs=8:channel_layout=7.1:map=0.0-FL|1.0-FR|2.0-FC|3.0-LFE|4.0-BL|5.0-BR|6.0-SL|7.0-SR,asplit=1[AUDIO-main-test-out-0];[AUDIO-main-test-out-0]audio-filter[AUDIO-test-out-0] -map [VIDEO-test-out] -map [AUDIO-test-out-0] video params audio params -metadata comment=Transcoded using Encore /output/path/out.mp4") + } + + @Test + fun `custom crop filter`() { + val job = defaultEncoreJob().copy( + inputs = listOf( + AudioVideoInput( + uri = "/input/test.mp4", + analyzed = defaultVideoFile, + cropTo = "1:1", + ), + ), + ) + every { profile.filterSettings } returns FilterSettings(cropFilter = "hw_crop") + commandBuilder = CommandBuilder(job, profile, encoreJob.outputFolder, encodingProperties) + + val buildCommands = commandBuilder.buildCommands(listOf(output(false))) + + assertThat(buildCommands).hasSize(1) + + val command = buildCommands.first().joinToString(" ") + assertThat(command).isEqualTo("ffmpeg -xerror -hide_banner -loglevel +level -y -i /input/test.mp4 -filter_complex sws_flags=scaling;[0:v]hw_crop=min(iw\\,ih*1/1):min(ih\\,iw/(1/1)),split=1[VIDEO-main-test-out];[VIDEO-main-test-out]video-filter[VIDEO-test-out];[0:a]join=inputs=8:channel_layout=7.1:map=0.0-FL|1.0-FR|2.0-FC|3.0-LFE|4.0-BL|5.0-BR|6.0-SL|7.0-SR,asplit=1[AUDIO-main-test-out-0];[AUDIO-main-test-out-0]audio-filter[AUDIO-test-out-0] -map [VIDEO-test-out] -map [AUDIO-test-out-0] video params audio params -metadata comment=Transcoded using Encore /output/path/out.mp4") + } + + @Test + fun `custom pad filter`() { + val job = defaultEncoreJob().copy( + inputs = listOf( + AudioVideoInput( + uri = "/input/test.mp4", + analyzed = defaultVideoFile, + padTo = "1:1", + ), + ), + ) + every { profile.filterSettings } returns FilterSettings(padFilter = "hw_pad") + commandBuilder = CommandBuilder(job, profile, encoreJob.outputFolder, encodingProperties) + + val buildCommands = commandBuilder.buildCommands(listOf(output(false))) + + assertThat(buildCommands).hasSize(1) + + val command = buildCommands.first().joinToString(" ") + assertThat(command).isEqualTo("ffmpeg -xerror -hide_banner -loglevel +level -y -i /input/test.mp4 -filter_complex sws_flags=scaling;[0:v]hw_pad=aspect=1/1:x=(ow-iw)/2:y=(oh-ih)/2,split=1[VIDEO-main-test-out];[VIDEO-main-test-out]video-filter[VIDEO-test-out];[0:a]join=inputs=8:channel_layout=7.1:map=0.0-FL|1.0-FR|2.0-FC|3.0-LFE|4.0-BL|5.0-BR|6.0-SL|7.0-SR,asplit=1[AUDIO-main-test-out-0];[AUDIO-main-test-out-0]audio-filter[AUDIO-test-out-0] -map [VIDEO-test-out] -map [AUDIO-test-out-0] video params audio params -metadata comment=Transcoded using Encore /output/path/out.mp4") + } + @Test fun `one pass encode`() { val buildCommands = commandBuilder.buildCommands(listOf(output(false)))