diff --git a/checks.gradle b/checks.gradle index 64012bd3..00854338 100644 --- a/checks.gradle +++ b/checks.gradle @@ -15,6 +15,8 @@ jacocoTestCoverageVerification { '*QueueService.migrateQueues()', '*.ShutdownHandler.*', '*FfmpegExecutor.runFfmpeg$lambda$7(java.lang.Process)', + '*FilterSettings.*', + '*.FfmpegExecutor.getProgress(java.lang.Double, java.lang.String)' ] 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 7bce78f2..b7daa6e0 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 @@ -31,7 +31,11 @@ data class AudioEncode( 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" val audioIn = job.inputs.audioInput(inputLabel) ?: return logOrThrow("Can not generate $outputName! No audio input with label '$inputLabel'.") 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 d90643b0..b9d47e91 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 @@ -22,7 +22,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" job.inputs.analyzedAudio(inputLabel) ?: return logOrThrow("Can not generate $outputName! No audio input with label '$inputLabel'.") 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 fb7f908e..30d2af74 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 @@ -29,7 +29,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 (job.segmentLength != null) { return logOrThrow("Thumbnail is not supported in segmented encode!") } 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 ed4991f5..0094c8e0 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 @@ -32,7 +32,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 (job.segmentLength != null) { return logOrThrow("Thumbnail map is not supported in segmented encode!") } 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 d7a58208..76283ac9 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 @@ -28,9 +28,9 @@ interface VideoEncode : OutputProducer { val codec: String val inputLabel: String - override fun getOutput(job: EncoreJob, encodingProperties: EncodingProperties): Output? { + override fun getOutput(job: EncoreJob, encodingProperties: EncodingProperties, filterSettings: FilterSettings): Output? { 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) ?: throw RuntimeException("No valid video input with label $inputLabel!") return Output( @@ -40,7 +40,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", @@ -66,6 +66,7 @@ interface VideoEncode : OutputProducer { debugOverlay: Boolean, encodingProperties: EncodingProperties, videoInput: VideoIn, + filterSettings: FilterSettings, ): String? { val videoFilters = mutableListOf() var scaleToWidth = width @@ -83,10 +84,28 @@ 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(":") }", + ) } 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..1e19a599 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,7 @@ class CommandBuilder( log.debug { "No video outputs for video input ${input.videoLabel}" } return@mapIndexedNotNull null } - 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 +163,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 +200,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 +215,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 80c27616..63faa0ba 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, ) } 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 6dda4b3e..585c6f32 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") @@ -88,6 +90,7 @@ class AudioEncodeTest { ), ), ), + FilterSettings(), ) assertThat(output) .hasOutput("test_aac_stereo.mp4") @@ -130,6 +133,7 @@ class AudioEncodeTest { ), ), ), + FilterSettings(), ) assertThat(output).isNull() } @@ -150,6 +154,7 @@ class AudioEncodeTest { ), ), ), + FilterSettings(), ) }.isInstanceOf(RuntimeException::class.java) .hasMessageContaining("No audio mix preset for 'de': 5.1 -> stereo") @@ -158,7 +163,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() } @@ -166,7 +171,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 4d2bbb24..f7e0c5eb 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") @@ -48,6 +49,7 @@ class ThumbnailEncodeTest { thumbnailTime = 5.0, ), encodingProperties = EncodingProperties(), + FilterSettings(), ) assertThat(output) .hasOutput("test_thumb%02d.jpg") @@ -71,6 +73,7 @@ class ThumbnailEncodeTest { val output = selectorEncode.getOutput( job = defaultEncoreJob(), encodingProperties = EncodingProperties(), + FilterSettings(), ) assertThat(output) @@ -94,6 +97,7 @@ class ThumbnailEncodeTest { duration = 4.0, ), encodingProperties = EncodingProperties(), + FilterSettings(), ) assertThat(output) .hasOutput("test_thumb%02d.jpg") @@ -124,6 +128,7 @@ class ThumbnailEncodeTest { ), ), encodingProperties = EncodingProperties(), + FilterSettings(), ) assertThat(output) .hasOutput("test_thumb%02d.jpg") @@ -143,6 +148,7 @@ class ThumbnailEncodeTest { val output = encode.copy(inputLabel = "other", optional = true).getOutput( job = defaultEncoreJob(), encodingProperties = EncodingProperties(), + FilterSettings(), ) assertThat(output).isNull() } @@ -153,6 +159,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 dcc1aae5..4e6c3085 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!") 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 e8b2b796..df8e4aa0 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 @@ -29,13 +29,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 @@ -59,6 +60,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") @@ -86,6 +88,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") @@ -101,7 +104,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 @@ -114,6 +117,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( @@ -124,7 +152,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) 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)))