diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml
deleted file mode 100644
index 33102c8a..00000000
--- a/.github/workflows/publish.yaml
+++ /dev/null
@@ -1,100 +0,0 @@
-name: Create and publish a Docker image
-
-on:
- push:
- branches: ['master','main']
- tags:
- - 'v*'
-
-env:
- REGISTRY: ghcr.io
- IMAGE_NAME: encore-debian
- DOCKER_BASE_IMAGE: ghcr.io/svt/avtools-osadl-jre-debian:latest
-
-jobs:
- build-artifact:
- runs-on: ubuntu-latest
- container:
- image: ghcr.io/svt/avtools-osadl-debian:latest
- options: --user root
- permissions:
- contents: read
- packages: write
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v2
-
- - name: Set up JDK 11
- uses: actions/setup-java@v2
- with:
- java-version: '11'
- distribution: 'adopt'
-
- - name: Grant execute permission for gradlew
- run: chmod +x gradlew
-
- - name: Build with Gradle
- run: ./gradlew build -x test
-
- - name: cache build
- uses: actions/cache@v2
- id: restore-build
- with:
- path: ./build
- key: ${{ github.sha }}
-
- build-and-push-image:
- runs-on: ubuntu-latest
- needs: build-artifact
- permissions:
- contents: read
- packages: write
-
- steps:
- - name: Checkout repository
- uses: actions/checkout@v2
-
- - name: Get Cache Build
- uses: actions/cache@v2
- id: restore-build
- with:
- path: ./build
- key: ${{ github.sha }}
- - run: ls ./build
-
- - name: 'Echo download path'
- run: echo ${{steps.download.outputs.download-path}}
-
- - name: Log in to the Container registry
- uses: docker/login-action@v1
- with:
- registry: ${{ env.REGISTRY }}
- username: ${{ github.actor }}
- password: ${{ secrets.GITHUB_TOKEN }}
-
- - name: Extract metadata (tags, labels) for Docker
- id: meta
- uses: docker/metadata-action@v3
- with:
- images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME }}
- flavor: |
- latest=true
- tags: |
- type=raw,value={{branch}},priority=1,enable=${{ !startsWith(github.ref, 'refs/tags/v') }}
- type=ref,event=tag,priority=2
- type=raw,value=${{ env.IMAGE_NAME }}-{{branch}}-{{date 'YYYYMMDD'}}-{{sha}},priority=31,enable=${{ !startsWith(github.ref, 'refs/tags/v') }}
- type=raw,value=${{ env.IMAGE_NAME }}-{{tag}}-{{date 'YYYYMMDD'}}-{{sha}},priority=32, enable=${{ startsWith(github.ref, 'refs/tags/v') }}
-
- - name: Build and push Docker image
- uses: docker/build-push-action@v2
- with:
- context: .
- build-args: |
- DOCKER_BASE_IMAGE=${{ env.DOCKER_BASE_IMAGE }}
- USR=avtools
- platforms: x86_64
- push: true
- tags: ${{ steps.meta.outputs.tags }}
- labels: ${{ steps.meta.outputs.labels }}
-
diff --git a/.github/workflows/publishjar.yml b/.github/workflows/publishjar.yml
index 7bd155e1..8b22d914 100644
--- a/.github/workflows/publishjar.yml
+++ b/.github/workflows/publishjar.yml
@@ -1,4 +1,4 @@
-name: Create and publish a Spring Boot jar/Spring Boot LaunchScript Jar
+name: Create and publish Spring Boot jars and GraalVm native images
on:
push:
@@ -13,25 +13,28 @@ jobs:
steps:
- name: Checkout repository
- uses: actions/checkout@v2
+ uses: actions/checkout@v3
- - name: Set up JDK 11
- uses: actions/setup-java@v2
+ - name: Set up GraalVm
+ uses: graalvm/setup-graalvm@v1
with:
- java-version: '11'
- distribution: 'adopt'
+ java-version: '17'
+ distribution: 'graalvm-community'
+ github-token: ${{ secrets.GITHUB_TOKEN }}
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- - name: Build jars with Gradle
- run: |
- ./gradlew build -x test
+ - name: Build jars and native images with Gradle
+ run: gradlew build nativeCompile -x test
- - name: Release Jars
+ - name: Release Jars and native images
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
files: |
- build/libs/encore*.jar
+ encore-web/build/libs/encore-web*boot.jar
+ encore-web/build/native/nativeCompile/encore-web
+ encore-worker/build/libs/encore-worker*boot.jar
+ encore-worker/build/native/nativeCompile/encore-worker
diff --git a/.idea/compiler.xml b/.idea/compiler.xml
index 6b4e05a5..bcb93c49 100644
--- a/.idea/compiler.xml
+++ b/.idea/compiler.xml
@@ -4,14 +4,6 @@
-
-
-
-
-
-
+
-
-
-
-
+
\ No newline at end of file
diff --git a/build.gradle.kts b/build.gradle.kts
deleted file mode 100644
index d946db2c..00000000
--- a/build.gradle.kts
+++ /dev/null
@@ -1,152 +0,0 @@
-import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
-
-plugins {
- idea
- jacoco
- id("org.springframework.boot") version "2.7.6"
- id("se.ascp.gradle.gradle-versions-filter") version "0.1.16"
- kotlin("jvm") version "1.7.21"
- kotlin("plugin.spring") version "1.7.21"
- id("com.github.fhermansson.assertj-generator") version "1.1.4"
- id("org.jmailen.kotlinter") version "3.12.0"
- id("io.spring.dependency-management") version "1.1.0"
- id("pl.allegro.tech.build.axion-release") version "1.14.2"
-
- //openapi generation
- id("com.github.johnrengelman.processes") version "0.5.0"
- id("org.springdoc.openapi-gradle-plugin") version "1.5.0"
-}
-
-
-project.version = scmVersion.version
-
-apply(from = "checks.gradle")
-
-group = "se.svt.oss"
-
-assertjGenerator {
- classOrPackageNames = arrayOf(
- "se.svt.oss.encore.model",
- "se.svt.oss.mediaanalyzer.file"
- )
- entryPointPackage = "se.svt.oss.encore"
-}
-
-kotlinter {
- disabledRules = arrayOf(
- "import-ordering",
- "trailing-comma-on-declaration-site",
- "trailing-comma-on-call-site"
- )
-}
-
-tasks.lintKotlinTest {
- source = (source - fileTree("src/test/generated-java")).asFileTree
-}
-
-tasks.test {
- useJUnitPlatform()
-}
-
-openApi {
- customBootRun {
- args.set(listOf("--spring.profiles.active=local"))
- }
-}
-
-tasks.withType {
- kotlinOptions.jvmTarget = "11"
-}
-
-repositories {
- mavenCentral()
-}
-
-//don't build the extra plain jars that was auto-added in Spring Boot 2.5.0,
-//https://docs.spring.io/spring-boot/docs/current/gradle-plugin/reference/htmlsingle/#packaging-executable.and-plain-archives
-tasks.getByName("jar") {
- enabled = false
-}
-
-configurations {
- implementation {
- exclude(module = "spring-boot-starter-logging")
- exclude(module = "lombok")
- }
-}
-
-dependencyManagement {
- imports {
- mavenBom("org.springframework.cloud:spring-cloud-dependencies:2021.0.5")
- }
-}
-
-val redissonVersion = "3.18.1"
-
-dependencies {
- implementation("se.svt.oss:media-analyzer:2.0.1")
- implementation(kotlin("reflect"))
-
- implementation("org.springframework.boot:spring-boot-starter-web")
- implementation("org.springframework.boot:spring-boot-starter-log4j2")
- implementation("org.springframework.boot:spring-boot-starter-actuator")
- implementation("org.springframework.boot:spring-boot-starter-data-rest")
- implementation("org.springframework.boot:spring-boot-starter-data-redis")
- implementation("org.springframework.boot:spring-boot-starter-validation")
- implementation("org.springframework.cloud:spring-cloud-starter-config")
- implementation("org.springframework.boot:spring-boot-starter-security")
-
- implementation("org.redisson:redisson-spring-boot-starter:$redissonVersion")
- implementation("org.redisson:redisson-spring-data-27:$redissonVersion") // match boot version
- implementation("io.github.microutils:kotlin-logging:3.0.2")
- implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
- implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4")
- implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.6.4")
- implementation("org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:1.6.4")
-
- implementation("org.springframework.cloud:spring-cloud-starter-openfeign")
- implementation("io.github.openfeign:feign-okhttp")
- implementation("org.springframework.retry:spring-retry")
- implementation("org.springframework.boot:spring-boot-starter-aop")
-
- //openapi generation
- implementation("org.springdoc:springdoc-openapi-ui:1.6.12")
- implementation("org.springdoc:springdoc-openapi-kotlin:1.6.12")
- implementation("org.springdoc:springdoc-openapi-data-rest:1.6.12")
- implementation("org.springdoc:springdoc-openapi-hateoas:1.6.12")
-
- testImplementation("se.svt.oss:junit5-redis-extension:3.0.0")
- testImplementation("se.svt.oss:random-port-initializer:1.0.5")
- testImplementation("org.awaitility:awaitility")
- testImplementation("org.springframework.boot:spring-boot-starter-test")
- testImplementation("org.springframework.security:spring-security-test")
- testImplementation("org.assertj:assertj-core")
- testImplementation("io.mockk:mockk:1.13.2")
- testImplementation("com.github.tomakehurst:wiremock-jre8:2.35.0")
- testImplementation("com.ninja-squad:springmockk:3.1.1")
- testImplementation("org.junit.jupiter:junit-jupiter-api")
- testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine")
-}
-
-
-tasks.wrapper {
- distributionType = Wrapper.DistributionType.ALL
- gradleVersion = "7.5.1"
-}
-
-val integrationTestsPreReq = setOf("mediainfo", "ffmpeg", "ffprobe").map {
-
- tasks.create("Verify $it is in path, required for integration tests", Exec::class.java) {
- isIgnoreExitValue = true
- executable = it
-
- if (!it.equals("mediainfo")) {
- args("-hide_banner")
- }
- }
-}
-
-tasks.test {
- dependsOn(integrationTestsPreReq)
-}
-
diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts
new file mode 100644
index 00000000..1fe42474
--- /dev/null
+++ b/buildSrc/build.gradle.kts
@@ -0,0 +1,24 @@
+plugins {
+ `kotlin-dsl`
+}
+
+repositories {
+ mavenCentral()
+ gradlePluginPortal()
+}
+kotlin {
+ jvmToolchain(17)
+}
+dependencies {
+ implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.9.10"))
+ implementation(platform("org.springframework.boot:spring-boot-dependencies:3.1.3"))
+ implementation(platform("org.springframework.cloud:spring-cloud-dependencies:2022.0.4"))
+ implementation("org.jetbrains.kotlin:kotlin-gradle-plugin")
+ implementation("org.jetbrains.kotlin:kotlin-allopen")
+ implementation("org.springframework.boot:spring-boot-gradle-plugin:3.1.3")
+ implementation("org.jmailen.gradle:kotlinter-gradle:3.13.0")
+ implementation("pl.allegro.tech.build:axion-release-plugin:1.14.3")
+ implementation("org.graalvm.buildtools:native-gradle-plugin:0.9.25")
+ implementation("com.github.fhermansson:assertj-gradle-plugin:1.1.5")
+ implementation("se.ascp.gradle:gradle-versions-filter:0.1.16")
+}
diff --git a/buildSrc/src/main/kotlin/encore.kotlin-conventions.gradle.kts b/buildSrc/src/main/kotlin/encore.kotlin-conventions.gradle.kts
new file mode 100644
index 00000000..1f7cb68c
--- /dev/null
+++ b/buildSrc/src/main/kotlin/encore.kotlin-conventions.gradle.kts
@@ -0,0 +1,64 @@
+plugins {
+ idea
+ jacoco
+ kotlin("jvm")
+ kotlin("plugin.spring")
+ id("pl.allegro.tech.build.axion-release")
+ id("com.github.fhermansson.assertj-generator")
+ id("org.jmailen.kotlinter")
+ id("se.ascp.gradle.gradle-versions-filter")
+}
+
+group = "se.svt.oss"
+project.version = scmVersion.version
+
+tasks.withType {
+ useJUnitPlatform()
+}
+apply { from("../checks.gradle") }
+repositories {
+ mavenCentral()
+}
+
+kotlin {
+ jvmToolchain(17)
+}
+tasks.lintKotlinTest {
+ source = (source - fileTree("src/test/generated-java")).asFileTree
+}
+tasks.formatKotlinTest {
+ source = (source - fileTree("src/test/generated-java")).asFileTree
+}
+kotlinter {
+ disabledRules = arrayOf(
+ "import-ordering",
+ "trailing-comma-on-declaration-site",
+ "trailing-comma-on-call-site"
+ )
+}
+assertjGenerator {
+ classOrPackageNames = arrayOf(
+ "se.svt.oss.encore.model",
+ "se.svt.oss.mediaanalyzer.file"
+ )
+ entryPointPackage = "se.svt.oss.encore"
+ useJakartaAnnotations = true
+}
+
+val redissonVersion = "3.23.2"
+
+dependencies {
+ implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.9.10"))
+ implementation(platform("org.springframework.boot:spring-boot-dependencies:3.1.3"))
+ implementation(platform("org.springframework.cloud:spring-cloud-dependencies:2022.0.4"))
+ implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
+ implementation("io.github.microutils:kotlin-logging:3.0.5")
+ implementation("org.redisson:redisson-spring-boot-starter:$redissonVersion")
+ implementation("org.redisson:redisson-spring-data-31:$redissonVersion") // match boot version
+ testImplementation("org.springframework.boot:spring-boot-starter-test")
+ testImplementation("org.assertj:assertj-core")
+ testImplementation("io.mockk:mockk:1.13.7")
+}
+
+
+
diff --git a/buildSrc/src/main/kotlin/encore.spring-boot-app-conventions.gradle.kts b/buildSrc/src/main/kotlin/encore.spring-boot-app-conventions.gradle.kts
new file mode 100644
index 00000000..530c692f
--- /dev/null
+++ b/buildSrc/src/main/kotlin/encore.spring-boot-app-conventions.gradle.kts
@@ -0,0 +1,21 @@
+import org.springframework.boot.gradle.tasks.bundling.BootJar
+plugins {
+ kotlin("jvm")
+ kotlin("plugin.spring")
+ id("org.springframework.boot")
+ id("org.graalvm.buildtools.native")
+}
+
+tasks.named("bootJar") {
+ archiveClassifier.set("boot")
+}
+
+dependencies {
+ implementation("org.springframework.boot:spring-boot-starter")
+ implementation("org.springframework.cloud:spring-cloud-starter-config")
+ implementation("org.springframework.boot:spring-boot-starter-logging")
+ implementation("net.logstash.logback:logstash-logback-encoder:7.4")
+}
+
+
+
diff --git a/checks.gradle b/checks.gradle
index afc1d2a3..1d1e2b12 100644
--- a/checks.gradle
+++ b/checks.gradle
@@ -5,12 +5,12 @@ jacocoTestCoverageVerification {
includes = ['se.svt.oss.encore.*']
excludes = [
'*.invoke()',
- '*.Security.getEnabled()',
+ '*.EncoreProperties*.get*()',
'*.DefaultConstructorMarker*',
- '*.EncoreApplicationKt.*',
+ '*ApplicationKt.main*',
'*.static {...}',
- '*.model.*.*.get*',
- '*.service.localencode.LocalEncodeService.moveFile*'
+ '*.model.*.get*',
+ '*.service.localencode.LocalEncodeService.moveFile*',
]
limit {
counter = 'LINE'
@@ -21,8 +21,6 @@ jacocoTestCoverageVerification {
failOnViolation = true
}
}
-jacoco {
- toolVersion = '0.8.7'
-}
+
jacocoTestCoverageVerification.dependsOn jacocoTestReport
check.dependsOn jacocoTestCoverageVerification
diff --git a/encore-common/build.gradle.kts b/encore-common/build.gradle.kts
new file mode 100644
index 00000000..4075f0d2
--- /dev/null
+++ b/encore-common/build.gradle.kts
@@ -0,0 +1,43 @@
+plugins {
+ id("encore.kotlin-conventions")
+}
+
+dependencies {
+
+ api("se.svt.oss:media-analyzer:2.0.3")
+ implementation(kotlin("reflect"))
+
+ compileOnly("org.springdoc:springdoc-openapi-starter-webmvc-api:2.2.0")
+ compileOnly("org.springframework.data:spring-data-rest-core")
+ implementation("org.springframework.boot:spring-boot-starter-webflux")
+ implementation("org.springframework.boot:spring-boot-starter-validation")
+
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
+ implementation("org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:1.7.3")
+
+ testImplementation(project(":encore-web"))
+ testImplementation("se.svt.oss:junit5-redis-extension:3.0.0")
+ testImplementation("org.springframework.security:spring-security-test")
+ testImplementation("org.awaitility:awaitility")
+ testImplementation("com.github.tomakehurst:wiremock-jre8-standalone:2.35.0")
+ testImplementation("org.springframework.boot:spring-boot-starter-webflux")
+ testImplementation("org.springframework.boot:spring-boot-starter-data-rest")
+}
+
+
+val integrationTestsPreReq = setOf("mediainfo", "ffmpeg", "ffprobe").map {
+
+ tasks.create("Verify $it is in path, required for integration tests", Exec::class.java) {
+ isIgnoreExitValue = true
+ executable = it
+
+ if (!it.equals("mediainfo")) {
+ args("-hide_banner")
+ }
+ }
+}
+
+tasks.test {
+ dependsOn(integrationTestsPreReq)
+}
+
diff --git a/encore-common/src/main/kotlin/se/svt/oss/encore/ClientConfiguration.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/ClientConfiguration.kt
new file mode 100644
index 00000000..a99e75bd
--- /dev/null
+++ b/encore-common/src/main/kotlin/se/svt/oss/encore/ClientConfiguration.kt
@@ -0,0 +1,28 @@
+// SPDX-FileCopyrightText: 2020 Sveriges Television AB
+//
+// SPDX-License-Identifier: EUPL-1.2
+
+package se.svt.oss.encore
+
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.context.annotation.Bean
+import org.springframework.context.annotation.Configuration
+import org.springframework.http.HttpHeaders
+import org.springframework.web.reactive.function.client.WebClient
+import org.springframework.web.reactive.function.client.support.WebClientAdapter
+import org.springframework.web.service.invoker.HttpServiceProxyFactory
+import se.svt.oss.encore.service.callback.CallbackClient
+
+@Configuration
+class ClientConfiguration {
+
+ @Bean
+ fun callbackClient(@Value("\${service.name:encore}") userAgent: String): CallbackClient {
+ val webClient = WebClient.builder()
+ .defaultHeader(HttpHeaders.USER_AGENT, userAgent)
+ .build()
+ return HttpServiceProxyFactory.builder(WebClientAdapter.forClient(webClient))
+ .build()
+ .createClient(CallbackClient::class.java)
+ }
+}
diff --git a/encore-common/src/main/kotlin/se/svt/oss/encore/EncoreRuntimeHints.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/EncoreRuntimeHints.kt
new file mode 100644
index 00000000..c69d664e
--- /dev/null
+++ b/encore-common/src/main/kotlin/se/svt/oss/encore/EncoreRuntimeHints.kt
@@ -0,0 +1,30 @@
+// SPDX-FileCopyrightText: 2020 Sveriges Television AB
+//
+// SPDX-License-Identifier: EUPL-1.2
+
+package se.svt.oss.encore
+
+import org.springframework.aot.hint.MemberCategory
+import org.springframework.aot.hint.RuntimeHints
+import org.springframework.aot.hint.RuntimeHintsRegistrar
+import se.svt.oss.encore.config.AudioMixPreset
+import se.svt.oss.encore.config.EncodingProperties
+import se.svt.oss.encore.config.EncoreProperties
+
+class EncoreRuntimeHints : RuntimeHintsRegistrar {
+ override fun registerHints(hints: RuntimeHints, classLoader: ClassLoader?) {
+ hints.reflection()
+ .registerType(
+ EncoreProperties::class.java,
+ MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
+ )
+ .registerType(
+ EncodingProperties::class.java,
+ MemberCategory.INVOKE_DECLARED_CONSTRUCTORS
+ )
+ .registerType(
+ AudioMixPreset::class.java,
+ MemberCategory.INVOKE_DECLARED_CONSTRUCTORS
+ )
+ }
+}
diff --git a/src/main/kotlin/se/svt/oss/encore/MediaAnalyzerConfiguration.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/MediaAnalyzerConfiguration.kt
similarity index 100%
rename from src/main/kotlin/se/svt/oss/encore/MediaAnalyzerConfiguration.kt
rename to encore-common/src/main/kotlin/se/svt/oss/encore/MediaAnalyzerConfiguration.kt
diff --git a/src/main/kotlin/se/svt/oss/encore/RedisConfiguration.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/RedisConfiguration.kt
similarity index 64%
rename from src/main/kotlin/se/svt/oss/encore/RedisConfiguration.kt
rename to encore-common/src/main/kotlin/se/svt/oss/encore/RedisConfiguration.kt
index c51453a8..816a9396 100644
--- a/src/main/kotlin/se/svt/oss/encore/RedisConfiguration.kt
+++ b/encore-common/src/main/kotlin/se/svt/oss/encore/RedisConfiguration.kt
@@ -7,21 +7,42 @@ package se.svt.oss.encore
import com.fasterxml.jackson.databind.ObjectMapper
import org.redisson.codec.JsonJacksonCodec
import org.redisson.spring.starter.RedissonAutoConfigurationCustomizer
+import org.springframework.aot.hint.annotation.RegisterReflectionForBinding
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.data.redis.core.RedisKeyValueAdapter
import org.springframework.data.redis.core.convert.RedisCustomConversions
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories
+import se.svt.oss.encore.model.CancelEvent
+import se.svt.oss.encore.model.EncoreJob
+import se.svt.oss.encore.model.SegmentProgressEvent
+import se.svt.oss.encore.model.input.AudioInput
+import se.svt.oss.encore.model.input.AudioVideoInput
+import se.svt.oss.encore.model.input.VideoInput
+import se.svt.oss.encore.model.queue.QueueItem
import se.svt.oss.encore.repository.ByteArrayToChannelLayoutConverter
import se.svt.oss.encore.repository.ByteArrayToOffsetDateTimeConverter
-import se.svt.oss.encore.repository.ByteArrayToURIConverter
-import se.svt.oss.encore.repository.ByteArrayToUUIDConverter
import se.svt.oss.encore.repository.ChannelLayoutToByteArrayConverter
import se.svt.oss.encore.repository.OffsetDateTimeToByteArrayConverter
-import se.svt.oss.encore.repository.URIToByteArrayConverter
-import se.svt.oss.encore.repository.UUIDToByteArrayConverter
+import se.svt.oss.mediaanalyzer.file.AudioFile
+import se.svt.oss.mediaanalyzer.file.ImageFile
+import se.svt.oss.mediaanalyzer.file.SubtitleFile
+import se.svt.oss.mediaanalyzer.file.VideoFile
@Configuration
+@RegisterReflectionForBinding(
+ EncoreJob::class,
+ AudioVideoInput::class,
+ VideoInput::class,
+ AudioInput::class,
+ ImageFile::class,
+ VideoFile::class,
+ AudioFile::class,
+ SubtitleFile::class,
+ CancelEvent::class,
+ SegmentProgressEvent::class,
+ QueueItem::class
+)
@EnableRedisRepositories(enableKeyspaceEvents = RedisKeyValueAdapter.EnableKeyspaceEvents.ON_STARTUP)
class RedisConfiguration {
@@ -30,10 +51,6 @@ class RedisConfiguration {
listOf(
OffsetDateTimeToByteArrayConverter(),
ByteArrayToOffsetDateTimeConverter(),
- UUIDToByteArrayConverter(),
- ByteArrayToUUIDConverter(),
- URIToByteArrayConverter(),
- ByteArrayToURIConverter(),
ChannelLayoutToByteArrayConverter(),
ByteArrayToChannelLayoutConverter()
)
diff --git a/src/main/kotlin/se/svt/oss/encore/cancellation/CancellationListener.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/cancellation/CancellationListener.kt
similarity index 100%
rename from src/main/kotlin/se/svt/oss/encore/cancellation/CancellationListener.kt
rename to encore-common/src/main/kotlin/se/svt/oss/encore/cancellation/CancellationListener.kt
diff --git a/encore-common/src/main/kotlin/se/svt/oss/encore/cancellation/SegmentProgressListener.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/cancellation/SegmentProgressListener.kt
new file mode 100644
index 00000000..f3f81db5
--- /dev/null
+++ b/encore-common/src/main/kotlin/se/svt/oss/encore/cancellation/SegmentProgressListener.kt
@@ -0,0 +1,46 @@
+// SPDX-FileCopyrightText: 2020 Sveriges Television AB
+//
+// SPDX-License-Identifier: EUPL-1.2
+
+package se.svt.oss.encore.cancellation
+
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.channels.SendChannel
+import kotlinx.coroutines.channels.trySendBlocking
+import org.redisson.api.listener.MessageListener
+import se.svt.oss.encore.model.SegmentProgressEvent
+import java.util.UUID
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.atomic.AtomicBoolean
+
+class SegmentProgressListener(
+ private val encoreJobId: UUID,
+ private val coroutineJob: Job,
+ private val totalSegments: Int,
+ private val progressChannel: SendChannel
+) : MessageListener {
+
+ private val completedSegments: MutableSet = ConcurrentHashMap.newKeySet()
+ val anyFailed = AtomicBoolean(false)
+
+ fun completed() = anyFailed.get() || completedSegments.size == totalSegments
+
+ fun completionCount() = completedSegments.size
+
+ override fun onMessage(channel: CharSequence?, msg: SegmentProgressEvent?) {
+ if (msg?.jobId == encoreJobId) {
+ if (!msg.success) {
+ progressChannel.close()
+ anyFailed.set(true)
+ coroutineJob.cancel("Segment ${msg.segment} failed!")
+ } else if (completedSegments.add(msg.segment)) {
+ val percent = (completedSegments.size * 100.0 / totalSegments).toInt()
+ progressChannel.trySendBlocking(percent)
+ if (percent == 100) {
+ progressChannel.close()
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/se/svt/oss/encore/config/AudioMixPreset.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/config/AudioMixPreset.kt
similarity index 73%
rename from src/main/kotlin/se/svt/oss/encore/config/AudioMixPreset.kt
rename to encore-common/src/main/kotlin/se/svt/oss/encore/config/AudioMixPreset.kt
index e8b8a3cd..f8e1d063 100644
--- a/src/main/kotlin/se/svt/oss/encore/config/AudioMixPreset.kt
+++ b/encore-common/src/main/kotlin/se/svt/oss/encore/config/AudioMixPreset.kt
@@ -3,10 +3,13 @@
// SPDX-License-Identifier: EUPL-1.2
package se.svt.oss.encore.config
+import org.springframework.boot.context.properties.NestedConfigurationProperty
import se.svt.oss.encore.model.profile.ChannelLayout
data class AudioMixPreset(
val fallbackToAuto: Boolean = true,
+ @NestedConfigurationProperty
val defaultPan: Map = emptyMap(),
+ @NestedConfigurationProperty
val panMapping: Map> = emptyMap(),
)
diff --git a/src/main/kotlin/se/svt/oss/encore/config/EncodingProperties.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/config/EncodingProperties.kt
similarity index 58%
rename from src/main/kotlin/se/svt/oss/encore/config/EncodingProperties.kt
rename to encore-common/src/main/kotlin/se/svt/oss/encore/config/EncodingProperties.kt
index bb044237..af78ad46 100644
--- a/src/main/kotlin/se/svt/oss/encore/config/EncodingProperties.kt
+++ b/encore-common/src/main/kotlin/se/svt/oss/encore/config/EncodingProperties.kt
@@ -1,9 +1,16 @@
+// SPDX-FileCopyrightText: 2020 Sveriges Television AB
+//
+// SPDX-License-Identifier: EUPL-1.2
+
package se.svt.oss.encore.config
+import org.springframework.boot.context.properties.NestedConfigurationProperty
import se.svt.oss.encore.model.profile.ChannelLayout
data class EncodingProperties(
+ @NestedConfigurationProperty
val audioMixPresets: Map = mapOf("default" to AudioMixPreset()),
+ @NestedConfigurationProperty
val defaultChannelLayouts: Map = emptyMap(),
val flipWidthHeightIfPortrait: Boolean = true
)
diff --git a/encore-common/src/main/kotlin/se/svt/oss/encore/config/EncoreProperties.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/config/EncoreProperties.kt
new file mode 100644
index 00000000..f76b7c63
--- /dev/null
+++ b/encore-common/src/main/kotlin/se/svt/oss/encore/config/EncoreProperties.kt
@@ -0,0 +1,79 @@
+// SPDX-FileCopyrightText: 2020 Sveriges Television AB
+//
+// SPDX-License-Identifier: EUPL-1.2
+
+package se.svt.oss.encore.config
+
+import org.springframework.boot.context.properties.ConfigurationProperties
+import org.springframework.boot.context.properties.NestedConfigurationProperty
+import java.io.File
+import java.time.Duration
+
+@ConfigurationProperties("encore-settings")
+data class EncoreProperties(
+ /**
+ * transcode to local tmp dir before copying to output folder
+ */
+ val localTemporaryEncode: Boolean = false,
+ /**
+ * number of work queues and threads
+ */
+ val concurrency: Int = 2,
+ /**
+ * time to wait after application start before polling queue
+ */
+ val pollInitialDelay: Duration = Duration.ofSeconds(10),
+ /**
+ * time to wait between polls
+ */
+ val pollDelay: Duration = Duration.ofSeconds(5),
+ /**
+ * poll only the specified queue
+ */
+ val pollQueue: Int? = null,
+ /**
+ * disable polling. could be set on encore-web if all transcoding is to be done by encore-workers
+ */
+ val pollDisabled: Boolean = false,
+ /**
+ * should queues with higher prio be poller before the queue assigned to thread or worker
+ */
+ val pollHigherPrio: Boolean = true,
+ /**
+ * if true, encore-worker will poll the queue until empty before shutting down, otherwise just poll once
+ */
+ val workerDrainQueue: Boolean = false,
+ val redisKeyPrefix: String = "encore",
+ /**
+ * optional web security settings
+ */
+ val security: Security = Security(),
+ /**
+ * open api contact information
+ */
+ val openApi: OpenApi = OpenApi(),
+ /**
+ * path to directory shared by encore instances. required for encoding in segments
+ */
+ val sharedWorkDir: File? = null,
+ /**
+ * timeout for segemnted encode before failing
+ */
+ val segmentedEncodeTimeout: Duration = Duration.ofMinutes(120),
+ @NestedConfigurationProperty
+ val encoding: EncodingProperties = EncodingProperties()
+) {
+ data class Security(
+ val enabled: Boolean = false,
+ val userPassword: String = "",
+ val adminPassword: String = ""
+ )
+
+ data class OpenApi(
+ val title: String = "Encore OpenAPI",
+ val description: String = "Endpoints for Encore",
+ val contactName: String = "",
+ val contactUrl: String = "",
+ val contactEmail: String = ""
+ )
+}
diff --git a/src/main/kotlin/se/svt/oss/encore/model/CancelEvent.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/model/CancelEvent.kt
similarity index 100%
rename from src/main/kotlin/se/svt/oss/encore/model/CancelEvent.kt
rename to encore-common/src/main/kotlin/se/svt/oss/encore/model/CancelEvent.kt
diff --git a/src/main/kotlin/se/svt/oss/encore/model/EncoreJob.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/model/EncoreJob.kt
similarity index 91%
rename from src/main/kotlin/se/svt/oss/encore/model/EncoreJob.kt
rename to encore-common/src/main/kotlin/se/svt/oss/encore/model/EncoreJob.kt
index 37ab924d..1ec8f339 100644
--- a/src/main/kotlin/se/svt/oss/encore/model/EncoreJob.kt
+++ b/encore-common/src/main/kotlin/se/svt/oss/encore/model/EncoreJob.kt
@@ -13,14 +13,13 @@ import org.springframework.data.redis.core.index.Indexed
import org.springframework.validation.annotation.Validated
import se.svt.oss.encore.model.input.Input
import se.svt.oss.mediaanalyzer.file.MediaFile
-import java.net.URI
import java.time.OffsetDateTime
import java.util.UUID
-import javax.validation.constraints.Max
-import javax.validation.constraints.Min
-import javax.validation.constraints.NotBlank
-import javax.validation.constraints.NotEmpty
-import javax.validation.constraints.Positive
+import jakarta.validation.constraints.Max
+import jakarta.validation.constraints.Min
+import jakarta.validation.constraints.NotBlank
+import jakarta.validation.constraints.NotEmpty
+import jakarta.validation.constraints.Positive
@Validated
@RedisHash("encore-jobs", timeToLive = (60 * 60 * 24 * 7).toLong()) // 1 week ttl
@@ -82,7 +81,7 @@ data class EncoreJob(
example = "http://projectx/encorecallback",
nullable = true
)
- val progressCallbackUri: URI? = null,
+ val progressCallbackUri: String? = null,
@Schema(
description = "The queue priority of the EncoreJob",
@@ -94,6 +93,14 @@ data class EncoreJob(
@Max(100)
val priority: Int = 0,
+ @Schema(
+ description = "Transcode segments of specified length in seconds in parallell. Should be a multiple of target GOP.",
+ example = "19.2",
+ nullable = true
+ )
+ @Positive
+ val segmentLength: Double? = null,
+
@Schema(
description = "The exception message, if the EncoreJob failed",
example = "input/output error",
diff --git a/encore-common/src/main/kotlin/se/svt/oss/encore/model/SegmentProgressEvent.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/model/SegmentProgressEvent.kt
new file mode 100644
index 00000000..b521b8f7
--- /dev/null
+++ b/encore-common/src/main/kotlin/se/svt/oss/encore/model/SegmentProgressEvent.kt
@@ -0,0 +1,11 @@
+// SPDX-FileCopyrightText: 2020 Sveriges Television AB
+//
+// SPDX-License-Identifier: EUPL-1.2
+
+package se.svt.oss.encore.model
+
+import com.fasterxml.jackson.annotation.JsonTypeInfo
+import java.util.UUID
+
+@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS)
+data class SegmentProgressEvent(val jobId: UUID, val segment: Int, val success: Boolean)
diff --git a/src/main/kotlin/se/svt/oss/encore/model/Status.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/model/Status.kt
similarity index 100%
rename from src/main/kotlin/se/svt/oss/encore/model/Status.kt
rename to encore-common/src/main/kotlin/se/svt/oss/encore/model/Status.kt
diff --git a/src/main/kotlin/se/svt/oss/encore/model/callback/JobProgress.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/model/callback/JobProgress.kt
similarity index 100%
rename from src/main/kotlin/se/svt/oss/encore/model/callback/JobProgress.kt
rename to encore-common/src/main/kotlin/se/svt/oss/encore/model/callback/JobProgress.kt
diff --git a/src/main/kotlin/se/svt/oss/encore/model/input/Input.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/model/input/Input.kt
similarity index 92%
rename from src/main/kotlin/se/svt/oss/encore/model/input/Input.kt
rename to encore-common/src/main/kotlin/se/svt/oss/encore/model/input/Input.kt
index 95bc460b..44ce36dd 100644
--- a/src/main/kotlin/se/svt/oss/encore/model/input/Input.kt
+++ b/encore-common/src/main/kotlin/se/svt/oss/encore/model/input/Input.kt
@@ -14,8 +14,8 @@ import se.svt.oss.mediaanalyzer.file.FractionString
import se.svt.oss.mediaanalyzer.file.MediaContainer
import se.svt.oss.mediaanalyzer.file.MediaFile
import se.svt.oss.mediaanalyzer.file.VideoFile
-import javax.validation.constraints.Pattern
-import javax.validation.constraints.PositiveOrZero
+import jakarta.validation.constraints.Pattern
+import jakarta.validation.constraints.PositiveOrZero
const val TYPE_AUDIO_VIDEO = "AudioVideo"
const val TYPE_AUDIO = "Audio"
@@ -57,6 +57,10 @@ sealed interface Input {
nullable = true
)
val seekTo: Double?
+
+ val copyTs: Boolean
+
+ fun withSeekTo(seekTo: Double): Input
}
sealed interface AudioIn : Input {
@@ -153,7 +157,8 @@ data class AudioInput(
override var analyzed: MediaFile? = null,
override val audioStream: Int? = null,
override val channelLayout: ChannelLayout? = null,
- override val seekTo: Double? = null
+ override val seekTo: Double? = null,
+ override val copyTs: Boolean = false
) : AudioIn {
override val analyzedAudio: MediaContainer
@JsonIgnore
@@ -162,6 +167,8 @@ data class AudioInput(
override val type: String
get() = TYPE_AUDIO
+ override fun withSeekTo(seekTo: Double) = copy(seekTo = seekTo)
+
val duration: Double
@JsonIgnore
get() = analyzedAudio.duration
@@ -178,7 +185,8 @@ data class VideoInput(
override var analyzed: MediaFile? = null,
override val videoStream: Int? = null,
override val probeInterlaced: Boolean = true,
- override val seekTo: Double? = null
+ override val seekTo: Double? = null,
+ override val copyTs: Boolean = false
) : VideoIn {
override val analyzedVideo: VideoFile
@JsonIgnore
@@ -187,6 +195,8 @@ data class VideoInput(
override val type: String
get() = TYPE_VIDEO
+ override fun withSeekTo(seekTo: Double) = copy(seekTo = seekTo)
+
val duration: Double
@JsonIgnore
get() = analyzedVideo.duration
@@ -207,7 +217,8 @@ data class AudioVideoInput(
override val audioStream: Int? = null,
override val probeInterlaced: Boolean = true,
override val channelLayout: ChannelLayout? = null,
- override val seekTo: Double? = null
+ override val seekTo: Double? = null,
+ override val copyTs: Boolean = false
) : VideoIn, AudioIn {
override val analyzedVideo: VideoFile
@JsonIgnore
@@ -220,6 +231,8 @@ data class AudioVideoInput(
override val type: String
get() = TYPE_AUDIO_VIDEO
+ override fun withSeekTo(seekTo: Double) = copy(seekTo = seekTo)
+
val duration: Double
@JsonIgnore
get() = analyzedVideo.duration
@@ -230,6 +243,7 @@ fun List.inputParams(readDuration: Double?): List =
input.params.toParams() +
(readDuration?.let { listOf("-t", "$it") } ?: emptyList()) +
(input.seekTo?.let { listOf("-ss", "$it") } ?: emptyList()) +
+ (if (input.copyTs) listOf("-copyts") else emptyList()) +
listOf("-i", input.uri)
}
diff --git a/src/main/kotlin/se/svt/oss/encore/model/mediafile/AudioLayout.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/model/mediafile/AudioLayout.kt
similarity index 100%
rename from src/main/kotlin/se/svt/oss/encore/model/mediafile/AudioLayout.kt
rename to encore-common/src/main/kotlin/se/svt/oss/encore/model/mediafile/AudioLayout.kt
diff --git a/src/main/kotlin/se/svt/oss/encore/model/mediafile/Extensions.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/model/mediafile/Extensions.kt
similarity index 100%
rename from src/main/kotlin/se/svt/oss/encore/model/mediafile/Extensions.kt
rename to encore-common/src/main/kotlin/se/svt/oss/encore/model/mediafile/Extensions.kt
diff --git a/src/main/kotlin/se/svt/oss/encore/model/output/Output.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/model/output/Output.kt
similarity index 100%
rename from src/main/kotlin/se/svt/oss/encore/model/output/Output.kt
rename to encore-common/src/main/kotlin/se/svt/oss/encore/model/output/Output.kt
diff --git a/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
similarity index 100%
rename from src/main/kotlin/se/svt/oss/encore/model/profile/AudioEncode.kt
rename to encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/AudioEncode.kt
diff --git a/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
similarity index 100%
rename from src/main/kotlin/se/svt/oss/encore/model/profile/AudioEncoder.kt
rename to encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/AudioEncoder.kt
diff --git a/src/main/kotlin/se/svt/oss/encore/model/profile/ChannelId.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/ChannelId.kt
similarity index 100%
rename from src/main/kotlin/se/svt/oss/encore/model/profile/ChannelId.kt
rename to encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/ChannelId.kt
diff --git a/src/main/kotlin/se/svt/oss/encore/model/profile/ChannelLayout.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/ChannelLayout.kt
similarity index 100%
rename from src/main/kotlin/se/svt/oss/encore/model/profile/ChannelLayout.kt
rename to encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/ChannelLayout.kt
diff --git a/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
similarity index 100%
rename from src/main/kotlin/se/svt/oss/encore/model/profile/GenericVideoEncode.kt
rename to encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/GenericVideoEncode.kt
diff --git a/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
similarity index 100%
rename from src/main/kotlin/se/svt/oss/encore/model/profile/OutputProducer.kt
rename to encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/OutputProducer.kt
diff --git a/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
similarity index 100%
rename from src/main/kotlin/se/svt/oss/encore/model/profile/Profile.kt
rename to encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/Profile.kt
diff --git a/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
similarity index 100%
rename from src/main/kotlin/se/svt/oss/encore/model/profile/SimpleAudioEncode.kt
rename to encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/SimpleAudioEncode.kt
diff --git a/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
similarity index 96%
rename from src/main/kotlin/se/svt/oss/encore/model/profile/ThumbnailEncode.kt
rename to encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/ThumbnailEncode.kt
index 011de7fa..0ef510a9 100644
--- a/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,6 +29,9 @@ data class ThumbnailEncode(
private val log = KotlinLogging.logger { }
override fun getOutput(job: EncoreJob, encodingProperties: EncodingProperties): Output? {
+ if (job.segmentLength != null) {
+ return logOrThrow("Thumbnail is not supported in segmented encode!")
+ }
val videoInput = job.inputs.videoInput(inputLabel)
?: return logOrThrow("Can not produce thumbnail $suffix. No video input with label $inputLabel!")
val thumbnailTime = job.thumbnailTime?.let { time ->
diff --git a/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
similarity index 96%
rename from src/main/kotlin/se/svt/oss/encore/model/profile/ThumbnailMapEncode.kt
rename to encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/ThumbnailMapEncode.kt
index 9f0576ff..d3cd406d 100644
--- a/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
@@ -31,6 +31,9 @@ data class ThumbnailMapEncode(
private val log = KotlinLogging.logger { }
override fun getOutput(job: EncoreJob, encodingProperties: EncodingProperties): Output? {
+ if (job.segmentLength != null) {
+ return logOrThrow("Thumbnail map is not supported in segmented encode!")
+ }
val videoInput = job.inputs.videoInput(inputLabel)
val inputSeekTo = videoInput?.seekTo
val videoStream = job.inputs.analyzedVideo(inputLabel)?.highestBitrateVideoStream
diff --git a/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
similarity index 100%
rename from src/main/kotlin/se/svt/oss/encore/model/profile/VideoEncode.kt
rename to encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/VideoEncode.kt
diff --git a/src/main/kotlin/se/svt/oss/encore/model/profile/X264Encode.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/X264Encode.kt
similarity index 100%
rename from src/main/kotlin/se/svt/oss/encore/model/profile/X264Encode.kt
rename to encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/X264Encode.kt
diff --git a/src/main/kotlin/se/svt/oss/encore/model/profile/X265Encode.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/X265Encode.kt
similarity index 100%
rename from src/main/kotlin/se/svt/oss/encore/model/profile/X265Encode.kt
rename to encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/X265Encode.kt
diff --git a/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
similarity index 100%
rename from src/main/kotlin/se/svt/oss/encore/model/profile/X26XEncode.kt
rename to encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/X26XEncode.kt
diff --git a/src/main/kotlin/se/svt/oss/encore/model/queue/QueueItem.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/model/queue/QueueItem.kt
similarity index 65%
rename from src/main/kotlin/se/svt/oss/encore/model/queue/QueueItem.kt
rename to encore-common/src/main/kotlin/se/svt/oss/encore/model/queue/QueueItem.kt
index 25dc4ba5..966acb24 100644
--- a/src/main/kotlin/se/svt/oss/encore/model/queue/QueueItem.kt
+++ b/encore-common/src/main/kotlin/se/svt/oss/encore/model/queue/QueueItem.kt
@@ -4,7 +4,12 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo
import java.time.LocalDateTime
@JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
-data class QueueItem(val id: String, val priority: Int = 0, val created: LocalDateTime = LocalDateTime.now()) :
+data class QueueItem(
+ val id: String,
+ val priority: Int = 0,
+ val created: LocalDateTime = LocalDateTime.now(),
+ val segment: Int? = null
+) :
Comparable {
override fun compareTo(other: QueueItem): Int {
if (this == other) {
@@ -18,6 +23,10 @@ data class QueueItem(val id: String, val priority: Int = 0, val created: LocalDa
if (createdCompare != 0) {
return createdCompare
}
- return id.compareTo(other.id)
+ val idCompare = id.compareTo(other.id)
+ if (idCompare != 0) {
+ return idCompare
+ }
+ return (segment ?: 0).compareTo((other.segment ?: 0))
}
}
diff --git a/src/main/kotlin/se/svt/oss/encore/process/CommandBuilder.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/process/CommandBuilder.kt
similarity index 96%
rename from src/main/kotlin/se/svt/oss/encore/process/CommandBuilder.kt
rename to encore-common/src/main/kotlin/se/svt/oss/encore/process/CommandBuilder.kt
index e5251f11..0e2d7f27 100644
--- a/src/main/kotlin/se/svt/oss/encore/process/CommandBuilder.kt
+++ b/encore-common/src/main/kotlin/se/svt/oss/encore/process/CommandBuilder.kt
@@ -267,8 +267,18 @@ class CommandBuilder(
File(outputFolder).resolve(output.output).toString()
}
- private fun seekParams(): List =
- encoreJob.seekTo?.let { listOf("-ss", "$it") } ?: emptyList()
+ private fun seekParams(): List {
+ val copyTsAdjustment = encoreJob.inputs
+ .filter { it.copyTs }
+ .mapNotNull { it.seekTo }
+ .maxOrNull()
+ val seekTo = listOfNotNull(copyTsAdjustment, encoreJob.seekTo).sum()
+ return if (seekTo > 0) {
+ listOf("-ss", "$seekTo")
+ } else {
+ emptyList()
+ }
+ }
private fun durationParams(): List =
encoreJob.duration?.let { listOf("-t", "$it") } ?: emptyList()
diff --git a/encore-common/src/main/kotlin/se/svt/oss/encore/process/SegmentUtil.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/process/SegmentUtil.kt
new file mode 100644
index 00000000..c0c8ec05
--- /dev/null
+++ b/encore-common/src/main/kotlin/se/svt/oss/encore/process/SegmentUtil.kt
@@ -0,0 +1,34 @@
+// SPDX-FileCopyrightText: 2020 Sveriges Television AB
+//
+// SPDX-License-Identifier: EUPL-1.2
+
+package se.svt.oss.encore.process
+
+import se.svt.oss.encore.model.EncoreJob
+import se.svt.oss.mediaanalyzer.file.MediaContainer
+import kotlin.math.ceil
+
+fun EncoreJob.segmentLengthOrThrow() = segmentLength ?: throw RuntimeException("No segmentLength in job!")
+
+fun EncoreJob.numSegments(): Int {
+ val segLen = segmentLengthOrThrow()
+ val readDuration = duration
+ return if (readDuration != null) {
+ ceil(readDuration / segLen).toInt()
+ } else {
+ val segments =
+ inputs.map { ceil(((it.analyzed as MediaContainer).duration - (it.seekTo ?: 0.0)) / segLen).toInt() }.toSet()
+ if (segments.size > 1) {
+ throw RuntimeException("Inputs differ in length")
+ }
+ segments.first()
+ }
+}
+
+fun EncoreJob.segmentDuration(segmentNumber: Int): Double = when {
+ duration == null -> segmentLengthOrThrow()
+ segmentNumber < numSegments() - 1 -> segmentLengthOrThrow()
+ else -> duration!! % segmentLengthOrThrow()
+}
+
+fun EncoreJob.baseName(segmentNumber: Int) = "${baseName}_%05d".format(segmentNumber)
diff --git a/src/main/kotlin/se/svt/oss/encore/repository/ChannelLayoutConverters.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/repository/ChannelLayoutConverters.kt
similarity index 100%
rename from src/main/kotlin/se/svt/oss/encore/repository/ChannelLayoutConverters.kt
rename to encore-common/src/main/kotlin/se/svt/oss/encore/repository/ChannelLayoutConverters.kt
diff --git a/src/main/kotlin/se/svt/oss/encore/repository/EncoreJobRepository.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/repository/EncoreJobRepository.kt
similarity index 89%
rename from src/main/kotlin/se/svt/oss/encore/repository/EncoreJobRepository.kt
rename to encore-common/src/main/kotlin/se/svt/oss/encore/repository/EncoreJobRepository.kt
index fce56fb8..2bb9a760 100644
--- a/src/main/kotlin/se/svt/oss/encore/repository/EncoreJobRepository.kt
+++ b/encore-common/src/main/kotlin/se/svt/oss/encore/repository/EncoreJobRepository.kt
@@ -6,17 +6,18 @@ package se.svt.oss.encore.repository
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.tags.Tag
-import java.util.UUID
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
+import org.springframework.data.repository.CrudRepository
import org.springframework.data.repository.PagingAndSortingRepository
import org.springframework.data.rest.core.annotation.RepositoryRestResource
import se.svt.oss.encore.model.EncoreJob
import se.svt.oss.encore.model.Status
+import java.util.UUID
@RepositoryRestResource
@Tag(name = "encorejob")
-interface EncoreJobRepository : PagingAndSortingRepository {
+interface EncoreJobRepository : PagingAndSortingRepository, CrudRepository {
@Operation(summary = "Find EncoreJobs By Status", description = "Returns EncoreJobs according to the given Status")
fun findByStatus(status: Status, pageable: Pageable): Page
diff --git a/src/main/kotlin/se/svt/oss/encore/repository/OffsetDateTimeConverters.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/repository/OffsetDateTimeConverters.kt
similarity index 100%
rename from src/main/kotlin/se/svt/oss/encore/repository/OffsetDateTimeConverters.kt
rename to encore-common/src/main/kotlin/se/svt/oss/encore/repository/OffsetDateTimeConverters.kt
diff --git a/encore-common/src/main/kotlin/se/svt/oss/encore/service/EncoreService.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/service/EncoreService.kt
new file mode 100644
index 00000000..7bbfe527
--- /dev/null
+++ b/encore-common/src/main/kotlin/se/svt/oss/encore/service/EncoreService.kt
@@ -0,0 +1,278 @@
+// SPDX-FileCopyrightText: 2020 Sveriges Television AB
+//
+// SPDX-License-Identifier: EUPL-1.2
+
+package se.svt.oss.encore.service
+
+import kotlinx.coroutines.CancellationException
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.FlowPreview
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.channels.Channel
+import kotlinx.coroutines.channels.ReceiveChannel
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.consumeAsFlow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.sample
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.slf4j.MDCContext
+import kotlinx.coroutines.time.withTimeout
+import mu.KotlinLogging
+import org.redisson.api.RTopic
+import org.redisson.api.RedissonClient
+import org.springframework.data.redis.core.PartialUpdate
+import org.springframework.data.redis.core.RedisKeyValueTemplate
+import org.springframework.stereotype.Service
+import se.svt.oss.encore.cancellation.CancellationListener
+import se.svt.oss.encore.cancellation.SegmentProgressListener
+import se.svt.oss.encore.config.EncoreProperties
+import se.svt.oss.encore.model.CancelEvent
+import se.svt.oss.encore.model.EncoreJob
+import se.svt.oss.encore.model.SegmentProgressEvent
+import se.svt.oss.encore.model.Status
+import se.svt.oss.encore.model.queue.QueueItem
+import se.svt.oss.encore.process.baseName
+import se.svt.oss.encore.process.numSegments
+import se.svt.oss.encore.process.segmentDuration
+import se.svt.oss.encore.process.segmentLengthOrThrow
+import se.svt.oss.encore.repository.EncoreJobRepository
+import se.svt.oss.encore.service.callback.CallbackService
+import se.svt.oss.encore.service.localencode.LocalEncodeService
+import se.svt.oss.encore.service.mediaanalyzer.MediaAnalyzerService
+import se.svt.oss.encore.service.queue.QueueService
+import se.svt.oss.mediaanalyzer.file.MediaContainer
+import se.svt.oss.mediaanalyzer.file.MediaFile
+import java.io.File
+import java.util.Locale
+import kotlin.time.TimedValue
+import kotlin.time.measureTimedValue
+
+@Service
+class EncoreService(
+ private val callbackService: CallbackService,
+ private val repository: EncoreJobRepository,
+ private val ffmpegExecutor: FfmpegExecutor,
+ private val redissonClient: RedissonClient,
+ private val redisKeyValueTemplate: RedisKeyValueTemplate,
+ private val mediaAnalyzerService: MediaAnalyzerService,
+ private val localEncodeService: LocalEncodeService,
+ private val encoreProperties: EncoreProperties,
+ private val queueService: QueueService,
+) {
+
+ private val log = KotlinLogging.logger {}
+
+ private val cancelTopicName = "cancel"
+
+ private fun sharedWorkDirOrNull(encoreJob: EncoreJob): File? =
+ encoreProperties.sharedWorkDir?.resolve(encoreJob.id.toString())
+
+ private fun sharedWorkDir(encoreJob: EncoreJob): File =
+ sharedWorkDirOrNull(encoreJob)
+ ?: throw IllegalStateException("Shared work dir has not been configured")
+
+ fun encode(queueItem: QueueItem, job: EncoreJob) {
+ when {
+ queueItem.segment != null -> encodeSegment(job, queueItem.segment)
+ job.segmentLength != null -> encodeSegmented(job)
+ else -> encode(job)
+ }
+ }
+
+ private fun encodeSegmented(encoreJob: EncoreJob) {
+ val coroutineJob = Job()
+ val cancelListener = CancellationListener(encoreJob.id, coroutineJob)
+ var progressListener: SegmentProgressListener? = null
+ var cancelTopic: RTopic? = null
+ var progressTopic: RTopic? = null
+ try {
+ initJob(encoreJob)
+ val numSegments = encoreJob.numSegments()
+ log.debug { "Encoding using $numSegments segments" }
+ cancelTopic = redissonClient.getTopic(cancelTopicName)
+ cancelTopic.addListener(CancelEvent::class.java, cancelListener)
+ progressTopic = redissonClient.getTopic("segment-progress")
+ val progressChannel = Channel()
+ progressListener = SegmentProgressListener(encoreJob.id, coroutineJob, numSegments, progressChannel)
+ progressTopic.addListener(SegmentProgressEvent::class.java, progressListener)
+ val timedOutput = measureTimedValue {
+ sharedWorkDir(encoreJob).mkdirs()
+ repeat(numSegments) {
+ queueService.enqueue(
+ QueueItem(
+ id = encoreJob.id.toString(),
+ priority = encoreJob.priority,
+ segment = it
+ )
+ )
+ }
+
+ runBlocking(coroutineJob + MDCContext()) {
+ withTimeout(encoreProperties.segmentedEncodeTimeout) {
+ handleProgress(progressChannel, encoreJob)
+ while (!progressListener.completed()) {
+ log.info { "Awaiting completion ${progressListener.completionCount()}/$numSegments..." }
+ delay(1000)
+ }
+ }
+ }
+ if (progressListener.anyFailed.get()) {
+ throw RuntimeException("Some segments failed")
+ }
+ log.info { "All segments completed" }
+ val outWorkDir = sharedWorkDir(encoreJob)
+ val suffixes = mutableSetOf()
+ repeat(numSegments) { segmentNum ->
+ val segmentBaseName = encoreJob.baseName(segmentNum)
+ outWorkDir.list()?.filter { it.startsWith(segmentBaseName) }
+ ?.forEach {
+ val suffix = it.replaceFirst(segmentBaseName, "")
+ suffixes.add(suffix)
+ outWorkDir.resolve("$suffix.txt").appendText("file $it\n")
+ }
+ }
+ val outputFolder = File(encoreJob.outputFolder)
+ outputFolder.mkdirs()
+ val outputFiles = suffixes.map {
+ val targetName = encoreJob.baseName + it
+ log.info { "Joining segments for $targetName" }
+ val targetFile = outputFolder.resolve(targetName)
+ ffmpegExecutor.joinSegments(outWorkDir.resolve("$it.txt"), targetFile)
+ }
+ outputFiles
+ }
+ updateSuccessfulJob(encoreJob, timedOutput)
+ } catch (e: CancellationException) {
+ log.error(e) { "Job execution cancelled: ${e.message}" }
+ encoreJob.status = Status.CANCELLED
+ encoreJob.message = e.message
+ } catch (e: Exception) {
+ log.error(e) { "Job execution failed: ${e.message}" }
+ encoreJob.status = Status.FAILED
+ encoreJob.message = e.message
+ } finally {
+ repository.save(encoreJob)
+ sharedWorkDirOrNull(encoreJob)?.deleteRecursively()
+ cancelTopic?.removeListener(cancelListener)
+ progressListener?.let { progressTopic?.removeListener(it) }
+ callbackService.sendProgressCallback(encoreJob)
+ }
+ }
+
+ private fun encodeSegment(encoreJob: EncoreJob, segmentNumber: Int) {
+ try {
+ log.info { "Start encoding ${encoreJob.baseName} segment $segmentNumber/${encoreJob.numSegments()} " }
+ val outputFolder = sharedWorkDir(encoreJob).absolutePath
+ val job = encoreJob.copy(
+ baseName = encoreJob.baseName(segmentNumber),
+ duration = encoreJob.segmentDuration(segmentNumber),
+ inputs = encoreJob.inputs.map {
+ it.withSeekTo((it.seekTo ?: 0.0) + encoreJob.segmentLengthOrThrow() * segmentNumber)
+ }
+ )
+ ffmpegExecutor.run(job, outputFolder, null)
+ redissonClient.getTopic("segment-progress").publish(SegmentProgressEvent(encoreJob.id, segmentNumber, true))
+ log.info { "Completed ${encoreJob.baseName} segment $segmentNumber/${encoreJob.numSegments()} " }
+ } catch (e: Exception) {
+ log.error(e) { "Error encoding segment $segmentNumber: ${e.message}" }
+ redissonClient.getTopic("segment-progress")
+ .publish(SegmentProgressEvent(encoreJob.id, segmentNumber, false))
+ }
+ }
+
+ private fun encode(encoreJob: EncoreJob) {
+ val coroutineJob = Job()
+ val cancelListener = CancellationListener(encoreJob.id, coroutineJob)
+ var cancelTopic: RTopic? = null
+ var outputFolder: String? = null
+
+ try {
+ cancelTopic = redissonClient.getTopic(cancelTopicName)
+ cancelTopic.addListener(CancelEvent::class.java, cancelListener)
+ outputFolder = localEncodeService.outputFolder(encoreJob)
+
+ val timedOutput = measureTimedValue {
+ initJob(encoreJob)
+
+ val outputFiles = runBlocking(coroutineJob + MDCContext()) {
+ val progressChannel = Channel()
+ handleProgress(progressChannel, encoreJob)
+ ffmpegExecutor.run(encoreJob, outputFolder, progressChannel)
+ }
+
+ localEncodeService.localEncodedFilesToCorrectDir(outputFolder, outputFiles, encoreJob)
+ }
+
+ updateSuccessfulJob(encoreJob, timedOutput)
+ log.info { "Done with $encoreJob" }
+ } catch (e: InterruptedException) {
+ val message = "Job execution interrupted"
+ log.error(e) { message }
+ encoreJob.status = Status.QUEUED
+ encoreJob.message = message
+ throw e
+ } catch (e: CancellationException) {
+ log.error(e) { "Job execution cancelled: ${e.message}" }
+ encoreJob.status = Status.CANCELLED
+ encoreJob.message = e.message
+ } catch (e: Exception) {
+ log.error(e) { "Job execution failed: ${e.message}" }
+ encoreJob.status = Status.FAILED
+ encoreJob.message = e.message
+ } finally {
+ repository.save(encoreJob)
+ cancelTopic?.removeListener(cancelListener)
+ callbackService.sendProgressCallback(encoreJob)
+ localEncodeService.cleanup(outputFolder)
+ }
+ }
+
+ @OptIn(FlowPreview::class)
+ private fun CoroutineScope.handleProgress(
+ progressChannel: ReceiveChannel,
+ encoreJob: EncoreJob
+ ) {
+ launch {
+ progressChannel.consumeAsFlow()
+ .conflate()
+ .distinctUntilChanged()
+ .sample(10_000)
+ .collect {
+ log.info { "Received progress $it" }
+ try {
+ encoreJob.progress = it
+ val partialUpdate = PartialUpdate(encoreJob.id, EncoreJob::class.java)
+ .set(encoreJob::progress.name, encoreJob.progress)
+ redisKeyValueTemplate.update(partialUpdate)
+ callbackService.sendProgressCallback(encoreJob)
+ } catch (e: Exception) {
+ log.warn(e) { "Error updating progress!" }
+ }
+ }
+ }
+ }
+
+ private fun updateSuccessfulJob(encoreJob: EncoreJob, timedOutput: TimedValue>) {
+ val outputFiles = timedOutput.value
+ val timeInSeconds = timedOutput.duration.inWholeSeconds
+ val speed = outputFiles.filterIsInstance().firstOrNull()?.let {
+ "%.3f".format(Locale.US, it.duration / timeInSeconds).toDouble()
+ } ?: 0.0
+ log.info { "Done encoding, time: ${timeInSeconds}s, speed: ${speed}X" }
+ encoreJob.output = outputFiles
+ encoreJob.status = Status.SUCCESSFUL
+ encoreJob.progress = 100
+ encoreJob.speed = speed
+ }
+
+ private fun initJob(encoreJob: EncoreJob) {
+ encoreJob.inputs.forEach { input ->
+ mediaAnalyzerService.analyzeInput(input)
+ }
+ log.info { "Start encoding" }
+ encoreJob.status = Status.IN_PROGRESS
+ repository.save(encoreJob)
+ }
+}
diff --git a/src/main/kotlin/se/svt/oss/encore/service/FfmpegExecutor.kt b/encore-common/src/main/kotlin/se/svt/oss/encore/service/FfmpegExecutor.kt
similarity index 80%
rename from src/main/kotlin/se/svt/oss/encore/service/FfmpegExecutor.kt
rename to encore-common/src/main/kotlin/se/svt/oss/encore/service/FfmpegExecutor.kt
index fd988938..bc1742cb 100644
--- a/src/main/kotlin/se/svt/oss/encore/service/FfmpegExecutor.kt
+++ b/encore-common/src/main/kotlin/se/svt/oss/encore/service/FfmpegExecutor.kt
@@ -12,9 +12,8 @@ import org.springframework.stereotype.Service
import se.svt.oss.encore.config.EncoreProperties
import se.svt.oss.encore.model.EncoreJob
import se.svt.oss.encore.model.input.maxDuration
-import se.svt.oss.encore.model.output.Output
-import se.svt.oss.encore.model.profile.Profile
import se.svt.oss.encore.process.CommandBuilder
+import se.svt.oss.encore.service.profile.ProfileService
import se.svt.oss.mediaanalyzer.MediaAnalyzer
import se.svt.oss.mediaanalyzer.file.MediaFile
import java.io.File
@@ -26,6 +25,7 @@ import kotlin.math.round
@Service
class FfmpegExecutor(
private val mediaAnalyzer: MediaAnalyzer,
+ private val profileService: ProfileService,
private val encoreProperties: EncoreProperties
) {
@@ -39,11 +39,20 @@ class FfmpegExecutor(
fun run(
encoreJob: EncoreJob,
- profile: Profile,
- outputs: List