diff --git a/encore-common/src/main/kotlin/se/svt/oss/encore/config/EncodingProperties.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/config/EncodingProperties.kt index 3dd7fb0e..e6456669 100644 --- a/encore-common/src/main/kotlin/se/svt/oss/encore/config/EncodingProperties.kt +++ b/encore-common/src/main/kotlin/se/svt/oss/encore/config/EncodingProperties.kt @@ -5,9 +5,11 @@ package se.svt.oss.encore.config import org.springframework.boot.context.properties.NestedConfigurationProperty +import org.springframework.core.io.Resource import se.svt.oss.encore.model.profile.ChannelLayout data class EncodingProperties( + val audioMixPresetLocation: Resource? = null, @NestedConfigurationProperty val audioMixPresets: Map = mapOf("default" to AudioMixPreset()), @NestedConfigurationProperty 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..d86b828b 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 @@ -14,6 +14,7 @@ import se.svt.oss.encore.model.input.maxDuration import se.svt.oss.encore.model.mediafile.toParams import se.svt.oss.encore.process.CommandBuilder import se.svt.oss.encore.process.createTempDir +import se.svt.oss.encore.service.audiomix.AudiomixPresetService import se.svt.oss.encore.service.profile.ProfileService import se.svt.oss.mediaanalyzer.MediaAnalyzer import se.svt.oss.mediaanalyzer.file.MediaFile @@ -28,6 +29,7 @@ private val log = KotlinLogging.logger { } class FfmpegExecutor( private val mediaAnalyzer: MediaAnalyzer, private val profileService: ProfileService, + private val audioMixService: AudiomixPresetService, private val encoreProperties: EncoreProperties, ) { @@ -45,10 +47,14 @@ class FfmpegExecutor( ): List { ShutdownHandler.checkShutdown() val profile = profileService.getProfile(encoreJob) + val audioMixPresets = audioMixService.getAudioMixPresets() + val encodingProperties = encoreProperties.encoding.copy( + audioMixPresets = audioMixPresets, + ) val outputs = profile.encodes.mapNotNull { it.getOutput( encoreJob, - encoreProperties.encoding, + encodingProperties, ) } diff --git a/encore-common/src/main/kotlin/se/svt/oss/encore/service/audiomix/AudiomixPresetService.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/service/audiomix/AudiomixPresetService.kt new file mode 100644 index 00000000..669eed9c --- /dev/null +++ b/encore-common/src/main/kotlin/se/svt/oss/encore/service/audiomix/AudiomixPresetService.kt @@ -0,0 +1,46 @@ +package se.svt.oss.encore.service.audiomix + +import com.fasterxml.jackson.core.JsonProcessingException +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.dataformat.yaml.YAMLMapper +import com.fasterxml.jackson.module.kotlin.readValue +import io.github.oshai.kotlinlogging.KotlinLogging +import org.springframework.aot.hint.annotation.RegisterReflectionForBinding +import org.springframework.stereotype.Service +import se.svt.oss.encore.config.AudioMixPreset +import se.svt.oss.encore.config.EncoreProperties +import java.io.File +import java.util.Locale + +private val log = KotlinLogging.logger { } + +@Service +@RegisterReflectionForBinding(AudioMixPreset::class) +class AudiomixPresetService( + private val objectMapper: ObjectMapper, + private val encoreProperties: EncoreProperties, +) { + private val yamlMapper: YAMLMapper = + YAMLMapper().findAndRegisterModules() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) as YAMLMapper + + private fun mapper() = + if (encoreProperties.encoding.audioMixPresetLocation?.filename?.let { + File(it).extension.lowercase(Locale.getDefault()) in setOf("yml", "yaml") + } == true + ) { + yamlMapper + } else { + objectMapper + } + + fun getAudioMixPresets(): Map = try { + log.debug { "Reading presets from ${encoreProperties.encoding.audioMixPresetLocation}" } + encoreProperties.encoding.audioMixPresetLocation?.let { location -> + mapper().readValue>(location.inputStream) + } ?: encoreProperties.encoding.audioMixPresets + } catch (e: JsonProcessingException) { + throw RuntimeException("Error parsing audio mix presets ${e.message}") + } +} diff --git a/encore-common/src/test/kotlin/se/svt/oss/encore/service/audiomix/AudioMixPresetServiceTest.kt b/encore-common/src/test/kotlin/se/svt/oss/encore/service/audiomix/AudioMixPresetServiceTest.kt new file mode 100644 index 00000000..a56486ce --- /dev/null +++ b/encore-common/src/test/kotlin/se/svt/oss/encore/service/audiomix/AudioMixPresetServiceTest.kt @@ -0,0 +1,54 @@ +package se.svt.oss.encore.service.audiomix + +import com.fasterxml.jackson.databind.ObjectMapper +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import org.springframework.core.io.ClassPathResource +import se.svt.oss.encore.config.EncodingProperties +import se.svt.oss.encore.config.EncoreProperties +import java.io.IOException + +class AudioMixPresetServiceTest { + + private lateinit var mixService: AudiomixPresetService + private val objectMapper = ObjectMapper().findAndRegisterModules() + private val encoreProperties = + EncoreProperties(encoding = EncodingProperties(ClassPathResource("audiomixpreset/audio-mix-presets.yml"))) + + @BeforeEach + internal fun setUp() { + mixService = AudiomixPresetService( + objectMapper, + encoreProperties, + ) + } + + @Test + fun `successfully parses existing and valid presets`() { + val presets = mixService.getAudioMixPresets() + assertThat(presets).hasSize(2) + assertThat(presets["default"]).isNotNull + assertThat(presets["de"]).isNotNull + } + + @Test + fun `nonexistent preset throws error`() { + mixService = AudiomixPresetService( + objectMapper, + encoreProperties.copy( + encoding = encoreProperties.encoding.copy( + audioMixPresetLocation = ClassPathResource( + "i-dont-exist", + ), + ), + ), + ) + assertThatThrownBy { + mixService.getAudioMixPresets() + } + .isInstanceOf(IOException::class.java) + .hasMessageStartingWith("class path resource [i-dont-exist] cannot be opened because it does not exist") + } +} diff --git a/encore-common/src/test/resources/application-test.yml b/encore-common/src/test/resources/application-test.yml index 49303114..938affaa 100644 --- a/encore-common/src/test/resources/application-test.yml +++ b/encore-common/src/test/resources/application-test.yml @@ -19,6 +19,7 @@ encore-settings: poll-delay: 1s shared-work-dir: ${java.io.tmpdir}/encore-shared encoding: + audio-mix-preset-location: classpath:audiomixpreset/audio-mix-presets.yml default-channel-layouts: 3: "3.0" audio-mix-presets: diff --git a/encore-common/src/test/resources/audiomixpreset/audio-mix-presets.yml b/encore-common/src/test/resources/audiomixpreset/audio-mix-presets.yml new file mode 100644 index 00000000..482c280f --- /dev/null +++ b/encore-common/src/test/resources/audiomixpreset/audio-mix-presets.yml @@ -0,0 +1,15 @@ +default: + default-pan: + stereo: FL=FL+0.707107*FC+0.707107*BL+0.707107*SL|FR=FR+0.707107*FC+0.707107*BR+0.707107*SR + pan-mapping: + mono: + stereo: FL=0.707*FC|FR=0.707*FC +de: + fallback-to-auto: false + default-pan: + stereo: FL