From 95d5fcde497f10372c9ce98e8541fd8c44a26c31 Mon Sep 17 00:00:00 2001 From: Gustav Grusell Date: Fri, 27 Oct 2023 15:04:55 +0200 Subject: [PATCH] feat: add support for including complete profile in job A new field inlineProfile has been added to EncoreJob. In this field a complete profile can be included, in which case this profile is used instead of the one specified in the profile field. Exactly one of 'profile' and 'inlineProfile' should be specified. Signed-off-by: Gustav Grusell --- .../se/svt/oss/encore/model/EncoreJob.kt | 14 ++++++++----- .../oss/encore/model/profile/AudioEncoder.kt | 6 +++++- .../model/profile/GenericVideoEncode.kt | 5 ++++- .../encore/model/profile/OutputProducer.kt | 4 +++- .../encore/model/profile/ThumbnailEncode.kt | 5 ++++- .../model/profile/ThumbnailMapEncode.kt | 5 ++++- .../oss/encore/model/profile/X26XEncode.kt | 3 +++ .../svt/oss/encore/service/FfmpegExecutor.kt | 6 ++++-- .../svt/oss/encore/EncoreIntegrationTest.kt | 21 +++++++++++++++++++ 9 files changed, 57 insertions(+), 12 deletions(-) diff --git a/encore-common/src/main/kotlin/se/svt/oss/encore/model/EncoreJob.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/model/EncoreJob.kt index 1ec8f33..5d5663b 100644 --- a/encore-common/src/main/kotlin/se/svt/oss/encore/model/EncoreJob.kt +++ b/encore-common/src/main/kotlin/se/svt/oss/encore/model/EncoreJob.kt @@ -20,6 +20,7 @@ import jakarta.validation.constraints.Min import jakarta.validation.constraints.NotBlank import jakarta.validation.constraints.NotEmpty import jakarta.validation.constraints.Positive +import se.svt.oss.encore.model.profile.Profile @Validated @RedisHash("encore-jobs", timeToLive = (60 * 60 * 24 * 7).toLong()) // 1 week ttl @@ -46,10 +47,12 @@ data class EncoreJob( @Schema( description = "The name of the encoding profile to use", example = "x264-animated", - required = true + nullable = true ) - @NotBlank - val profile: String, + val profile: String? = null, + + @Schema(description = "Inline transcoding profile. If set, the profile field will be ignored", nullable = true) + val inlineProfile: Profile? = null, @Schema( description = "A directory path to where the output should be written", @@ -168,7 +171,8 @@ data class EncoreJob( val thumbnailTime: Double? = null, @NotEmpty - val inputs: List = emptyList() + val inputs: List = emptyList(), + ) { @Schema( @@ -199,6 +203,6 @@ data class EncoreJob( "id" to id.toString(), "file" to baseName, "externalId" to (externalId ?: ""), - "profile" to profile + "profile" to (profile ?: inlineProfile?.name ?: "") ) + logContext } diff --git a/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/AudioEncoder.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/AudioEncoder.kt index 5bdcd70..d9976a8 100644 --- a/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/AudioEncoder.kt +++ b/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/AudioEncoder.kt @@ -7,11 +7,15 @@ package se.svt.oss.encore.model.profile import mu.KotlinLogging import se.svt.oss.encore.model.output.Output +private val log = KotlinLogging.logger { } + abstract class AudioEncoder : OutputProducer { - private val log = KotlinLogging.logger { } abstract val optional: Boolean + override val type: String + get() = this.javaClass.simpleName + fun logOrThrow(message: String): Output? { if (optional) { log.info { message } diff --git a/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/GenericVideoEncode.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/GenericVideoEncode.kt index f99f44f..1091def 100644 --- a/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/GenericVideoEncode.kt +++ b/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/GenericVideoEncode.kt @@ -18,4 +18,7 @@ data class GenericVideoEncode( override val format: String, override val codec: String, override val inputLabel: String = DEFAULT_VIDEO_LABEL -) : VideoEncode +) : VideoEncode { + override val type: String + get() = this.javaClass.simpleName +} 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 a7df3f8..064a16a 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 @@ -10,7 +10,7 @@ import se.svt.oss.encore.config.EncodingProperties import se.svt.oss.encore.model.EncoreJob import se.svt.oss.encore.model.output.Output -@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type") +@JsonTypeInfo(use = JsonTypeInfo.Id.NAME, include = JsonTypeInfo.As.EXISTING_PROPERTY, property = "type") @JsonSubTypes( JsonSubTypes.Type(value = AudioEncode::class, name = "AudioEncode"), JsonSubTypes.Type(value = SimpleAudioEncode::class, name = "SimpleAudioEncode"), @@ -22,4 +22,6 @@ import se.svt.oss.encore.model.output.Output ) interface OutputProducer { fun getOutput(job: EncoreJob, encodingProperties: EncodingProperties): Output? + + val type: String } 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 0ef510a..5b54aa8 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 @@ -14,6 +14,8 @@ import se.svt.oss.encore.model.mediafile.toParams import se.svt.oss.encore.model.output.Output import se.svt.oss.encore.model.output.VideoStreamEncode +private val log = KotlinLogging.logger { } + data class ThumbnailEncode( val percentages: List = listOf(25, 50, 75), val thumbnailWidth: Int = -2, @@ -26,7 +28,8 @@ data class ThumbnailEncode( val intervalSeconds: Double? = null ) : OutputProducer { - private val log = KotlinLogging.logger { } + override val type: String + get() = this.javaClass.simpleName override fun getOutput(job: EncoreJob, encodingProperties: EncodingProperties): Output? { if (job.segmentLength != null) { 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 d3cd406..ff3b2f3 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 @@ -17,6 +17,8 @@ import se.svt.oss.encore.model.output.VideoStreamEncode import se.svt.oss.mediaanalyzer.file.stringValue import kotlin.io.path.createTempDirectory +private val log = KotlinLogging.logger { } + data class ThumbnailMapEncode( val tileWidth: Int = 160, val tileHeight: Int = 90, @@ -28,7 +30,8 @@ data class ThumbnailMapEncode( val inputLabel: String = DEFAULT_VIDEO_LABEL ) : OutputProducer { - private val log = KotlinLogging.logger { } + override val type: String + get() = this.javaClass.simpleName override fun getOutput(job: EncoreJob, encodingProperties: EncodingProperties): Output? { if (job.segmentLength != null) { diff --git a/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/X26XEncode.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/X26XEncode.kt index 41a16e7..6836df6 100644 --- a/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/X26XEncode.kt +++ b/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/X26XEncode.kt @@ -11,6 +11,9 @@ abstract class X26XEncode : VideoEncode { abstract val codecParams: LinkedHashMap abstract val codecParamName: String + override val type: String + get() = this.javaClass.simpleName + override val params: Map get() = ffmpegParams + if (codecParams.isNotEmpty()) { mapOf(codecParamName to codecParams.map { "${it.key}=${it.value}" }.joinToString(":")) 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 bc1742c..360fa61 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 @@ -42,7 +42,9 @@ class FfmpegExecutor( outputFolder: String, progressChannel: SendChannel? ): List { - val profile = profileService.getProfile(encoreJob.profile) + val profile = encoreJob.inlineProfile + ?: encoreJob.profile?.let { profileService.getProfile(it) } + ?: throw java.lang.RuntimeException("Job must have either profile or inlineProfile set") val outputs = profile.encodes.mapNotNull { it.getOutput( encoreJob, @@ -51,7 +53,7 @@ class FfmpegExecutor( } check(outputs.distinctBy { it.id }.size == outputs.size) { - "Profile ${encoreJob.profile} contains duplicate suffixes: ${outputs.map { it.id }}!" + "Profile ${profile.name} contains duplicate suffixes: ${outputs.map { it.id }}!" } val commands = CommandBuilder(encoreJob, profile, outputFolder, encoreProperties.encoding).buildCommands(outputs) diff --git a/encore-common/src/test/kotlin/se/svt/oss/encore/EncoreIntegrationTest.kt b/encore-common/src/test/kotlin/se/svt/oss/encore/EncoreIntegrationTest.kt index 34ee69b..772a313 100644 --- a/encore-common/src/test/kotlin/se/svt/oss/encore/EncoreIntegrationTest.kt +++ b/encore-common/src/test/kotlin/se/svt/oss/encore/EncoreIntegrationTest.kt @@ -4,6 +4,7 @@ package se.svt.oss.encore +import com.fasterxml.jackson.dataformat.yaml.YAMLMapper import org.awaitility.Awaitility.await import org.awaitility.Durations import org.junit.jupiter.api.Test @@ -15,6 +16,7 @@ import se.svt.oss.encore.model.Status import se.svt.oss.encore.model.input.AudioInput import se.svt.oss.encore.model.input.VideoInput import se.svt.oss.encore.model.profile.ChannelLayout +import se.svt.oss.encore.model.profile.Profile import se.svt.oss.encore.model.queue.QueueItem import se.svt.oss.mediaanalyzer.file.ImageFile import se.svt.oss.mediaanalyzer.file.MediaContainer @@ -161,6 +163,25 @@ class EncoreIntegrationTest : EncoreIntegrationTestBase() { awaitJob(highPriorityJob.id) { it.status.isCompleted } } + @Test + fun jobWithInlineProfileRunsSuccessfully(@TempDir outputDir: File) { + val inlineProfile: Profile = EncoreIntegrationTest::class.java.getResourceAsStream("/profile/program.yml").use { + YAMLMapper().findAndRegisterModules().readValue(it, Profile::class.java) + } + + val job = job(outputDir = outputDir, file = testFileSurround) + .copy(profile = null, inlineProfile = inlineProfile) + + successfulTest( + job, + defaultExpectedOutputFiles(outputDir, testFileSurround) + + listOf( + expectedFile(outputDir, testFileSurround, "STEREO_DE.mp4"), + expectedFile(outputDir, testFileSurround, "SURROUND.mp4") + ) + ) + } + @Test fun jobIsCancelled(@TempDir outputDir: File) { var createdJob = createAndAwaitJob(