├── .editorconfig
├── .github
└── workflows
│ └── publishjar.yml
├── .gitignore
├── .gitlab-ci.yml
├── .idea
├── codeStyles
│ ├── Project.xml
│ └── codeStyleConfig.xml
├── compiler.xml
├── encodings.xml
└── vcs.xml
├── .reuse
└── dep5
├── CODE_OF_CONDUCT.adoc
├── CONTRIBUTING.adoc
├── LICENSE
├── LICENSES
├── Apache-2.0.txt
├── CC-BY-SA-4.0.txt
├── CC0-1.0.txt
└── EUPL-1.2.txt
├── README.md
├── buildSrc
├── build.gradle.kts
└── src
│ └── main
│ └── kotlin
│ ├── encore.kotlin-conventions.gradle.kts
│ └── encore.spring-boot-app-conventions.gradle.kts
├── checks.gradle
├── encore-common
├── build.gradle.kts
└── src
│ ├── main
│ ├── kotlin
│ │ └── se
│ │ │ └── svt
│ │ │ └── oss
│ │ │ └── encore
│ │ │ ├── ClientConfiguration.kt
│ │ │ ├── EncoreRuntimeHints.kt
│ │ │ ├── MediaAnalyzerConfiguration.kt
│ │ │ ├── RedisConfiguration.kt
│ │ │ ├── cancellation
│ │ │ ├── CancellationListener.kt
│ │ │ └── SegmentProgressListener.kt
│ │ │ ├── config
│ │ │ ├── AudioMixPreset.kt
│ │ │ ├── EncodingProperties.kt
│ │ │ ├── EncoreProperties.kt
│ │ │ └── ProfileProperties.kt
│ │ │ ├── model
│ │ │ ├── CancelEvent.kt
│ │ │ ├── EncoreJob.kt
│ │ │ ├── RedisEvent.kt
│ │ │ ├── SegmentProgressEvent.kt
│ │ │ ├── Status.kt
│ │ │ ├── callback
│ │ │ │ └── JobProgress.kt
│ │ │ ├── input
│ │ │ │ └── Input.kt
│ │ │ ├── mediafile
│ │ │ │ ├── AudioLayout.kt
│ │ │ │ └── Extensions.kt
│ │ │ ├── output
│ │ │ │ └── Output.kt
│ │ │ ├── profile
│ │ │ │ ├── AudioEncode.kt
│ │ │ │ ├── AudioEncoder.kt
│ │ │ │ ├── ChannelId.kt
│ │ │ │ ├── ChannelLayout.kt
│ │ │ │ ├── GenericVideoEncode.kt
│ │ │ │ ├── OutputProducer.kt
│ │ │ │ ├── Profile.kt
│ │ │ │ ├── SimpleAudioEncode.kt
│ │ │ │ ├── ThumbnailEncode.kt
│ │ │ │ ├── ThumbnailMapEncode.kt
│ │ │ │ ├── VideoEncode.kt
│ │ │ │ ├── X264Encode.kt
│ │ │ │ ├── X265Encode.kt
│ │ │ │ └── X26XEncode.kt
│ │ │ └── queue
│ │ │ │ └── QueueItem.kt
│ │ │ ├── process
│ │ │ ├── CommandBuilder.kt
│ │ │ ├── SegmentUtil.kt
│ │ │ └── TempDir.kt
│ │ │ ├── repository
│ │ │ ├── ChannelLayoutConverters.kt
│ │ │ ├── EncoreJobRepository.kt
│ │ │ └── OffsetDateTimeConverters.kt
│ │ │ └── service
│ │ │ ├── ApplicationShutdownException.kt
│ │ │ ├── EncoreService.kt
│ │ │ ├── FfmpegExecutor.kt
│ │ │ ├── ShutdownHandler.kt
│ │ │ ├── callback
│ │ │ ├── CallbackClient.kt
│ │ │ └── CallbackService.kt
│ │ │ ├── localencode
│ │ │ └── LocalEncodeService.kt
│ │ │ ├── mediaanalyzer
│ │ │ └── MediaAnalyzerService.kt
│ │ │ ├── profile
│ │ │ └── ProfileService.kt
│ │ │ └── queue
│ │ │ ├── QueueService.kt
│ │ │ └── QueueUtil.kt
│ └── resources
│ │ └── migrate_queue_script.lua
│ ├── test
│ ├── kotlin
│ │ └── se
│ │ │ └── svt
│ │ │ └── oss
│ │ │ └── encore
│ │ │ ├── EncoreClient.kt
│ │ │ ├── EncoreIntegrationTest.kt
│ │ │ ├── EncoreIntegrationTestBase.kt
│ │ │ ├── EncoreRuntimeHintsTest.kt
│ │ │ ├── LocalEncodeIntegrationTest.kt
│ │ │ ├── TestConfig.kt
│ │ │ ├── TestUtils.kt
│ │ │ ├── model
│ │ │ ├── input
│ │ │ │ └── InputTest.kt
│ │ │ ├── mediafile
│ │ │ │ └── MediaFileExtensionsTest.kt
│ │ │ └── profile
│ │ │ │ ├── AudioEncodeTest.kt
│ │ │ │ ├── GenericVideoEncodeTest.kt
│ │ │ │ ├── ThumbnailEncodeTest.kt
│ │ │ │ ├── ThumbnailMapEncodeTest.kt
│ │ │ │ ├── VideoEncodeTest.kt
│ │ │ │ ├── X264EncodeTest.kt
│ │ │ │ └── X265EncodeTest.kt
│ │ │ ├── process
│ │ │ ├── CommandBuilderTest.kt
│ │ │ └── SegmentUtilTest.kt
│ │ │ ├── repository
│ │ │ └── EncoreJobRepositoryTest.kt
│ │ │ └── service
│ │ │ ├── callback
│ │ │ └── CallbackServiceTest.kt
│ │ │ ├── profile
│ │ │ └── ProfileServiceTest.kt
│ │ │ └── queue
│ │ │ ├── QueueServiceTest.kt
│ │ │ └── QueueUtilTest.kt
│ └── resources
│ │ ├── application-test-local.yml
│ │ ├── application-test.yml
│ │ ├── input
│ │ ├── multiple-audio-file.json
│ │ ├── multiple-video-file.json
│ │ ├── multiple_audio.mp4
│ │ ├── multiple_video.mp4
│ │ ├── portrait-video-file.json
│ │ ├── rotate-to-portrait-video-file.json
│ │ ├── test.mp4
│ │ ├── test_stereo.mp4
│ │ ├── testyuv.yuv
│ │ ├── video-file-long.json
│ │ └── video-file.json
│ │ └── profile
│ │ ├── archive.yml
│ │ ├── audio-streams.yml
│ │ ├── dpb_size_failed.yml
│ │ ├── multiple_inputs.yml
│ │ ├── profiles.yml
│ │ ├── program-x265.yml
│ │ ├── program.yml
│ │ └── test_profile_invalid.yml
│ └── testFixtures
│ └── kotlin
│ └── se
│ └── svt
│ └── oss
│ └── encore
│ └── RedisExtension.kt
├── encore-web
├── Dockerfile
├── Dockerfile-jar
├── build.gradle.kts
└── src
│ ├── main
│ ├── kotlin
│ │ └── se
│ │ │ └── svt
│ │ │ └── oss
│ │ │ └── encore
│ │ │ ├── EncoreApplication.kt
│ │ │ ├── EncoreWebRuntimeHints.kt
│ │ │ ├── OpenAPIConfiguration.kt
│ │ │ ├── RepositoryConfiguration.kt
│ │ │ ├── SchedulingConfiguration.kt
│ │ │ ├── SecurityConfiguration.kt
│ │ │ ├── controller
│ │ │ └── EncoreController.kt
│ │ │ ├── handlers
│ │ │ └── EncoreJobHandler.kt
│ │ │ └── poll
│ │ │ └── JobPoller.kt
│ └── resources
│ │ ├── application.yml
│ │ ├── asciilogo.txt
│ │ └── logback-json.xml
│ └── test
│ ├── kotlin
│ └── se
│ │ └── svt
│ │ └── oss
│ │ └── encore
│ │ ├── EncoreEndpointAccessIntegrationTest.kt
│ │ ├── EncoreWebRuntimeHintsTest.kt
│ │ ├── controller
│ │ └── EncoreControllerTest.kt
│ │ ├── handlers
│ │ └── EncoreJobHandlerTest.kt
│ │ └── poll
│ │ └── JobPollerTest.kt
│ └── resources
│ ├── application-test.yml
│ └── profile
│ ├── archive.yml
│ ├── audio-streams.yml
│ ├── dpb_size_failed.yml
│ ├── multiple_inputs.yml
│ ├── profiles.yml
│ ├── program-x265.yml
│ ├── program.yml
│ └── test_profile_invalid.yml
├── encore-worker
├── Dockerfile
├── build.gradle.kts
└── src
│ ├── main
│ ├── kotlin
│ │ └── se
│ │ │ └── svt
│ │ │ └── oss
│ │ │ └── encore
│ │ │ └── EncoreWorkerApplication.kt
│ └── resources
│ │ ├── application.yml
│ │ ├── asciilogo.txt
│ │ └── logback-json.xml
│ └── test
│ └── kotlin
│ └── se
│ └── svt
│ └── oss
│ └── encore
│ └── EncoreWorkerApplicationTest.kt
├── encore_logo.png
├── gradle.properties
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
└── settings.gradle.kts
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | insert_final_newline = true
5 |
6 | [{*.kt,*.kts}]
7 | ktlint_code_style = intellij_idea
8 | ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL
9 |
10 | # Disable wildcard imports entirely
11 | ij_kotlin_name_count_to_use_star_import = 2147483647
12 | ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
13 | ij_kotlin_packages_to_use_import_on_demand = unset
--------------------------------------------------------------------------------
/.github/workflows/publishjar.yml:
--------------------------------------------------------------------------------
1 | name: Create and publish Spring Boot jars and GraalVm native images
2 |
3 | on:
4 | push:
5 | tags:
6 | - 'v*'
7 |
8 | jobs:
9 | build-artifacts:
10 | runs-on: ubuntu-latest
11 | permissions:
12 | contents: write
13 |
14 | steps:
15 | - name: Checkout repository
16 | uses: actions/checkout@v3
17 |
18 | - name: Set up GraalVm
19 | uses: graalvm/setup-graalvm@v1
20 | with:
21 | java-version: '21'
22 | distribution: 'graalvm-community'
23 | github-token: ${{ secrets.GITHUB_TOKEN }}
24 |
25 | - name: Grant execute permission for gradlew
26 | run: chmod +x gradlew
27 |
28 | - name: Build jars and native images with Gradle
29 | run: |
30 | export LC_ALL=C.UTF-8
31 | export LANG=C.UTF-8
32 | export NATIVE_IMAGE_CONFIG_FILE="$(pwd)/native-image.properties"
33 | echo 'NativeImageArgs = -J-Xmx14G -H:+AddAllCharsets -J-Dfile.encoding=UTF-8' > $NATIVE_IMAGE_CONFIG_FILE
34 | ./gradlew build nativeCompile -x test
35 |
36 | - name: Release Jars and native images
37 | uses: softprops/action-gh-release@v1
38 | if: startsWith(github.ref, 'refs/tags/')
39 | with:
40 | files: |
41 | encore-web/build/libs/encore-web*boot.jar
42 | encore-web/build/native/nativeCompile/encore-web
43 | encore-worker/build/libs/encore-worker*boot.jar
44 | encore-worker/build/native/nativeCompile/encore-worker
45 |
46 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | generated-java
2 | *.iml
3 |
4 | # Gradle 4.1 + IntelliJ build directory:
5 | out/
6 | gradlew.bat
7 |
8 | **/build
9 |
10 | *.log
11 | *.swp
12 | .gradle
13 | schema
14 |
15 | .idea/*
16 | ## Videocore shared settings:
17 | !.idea/vcs.xml
18 | !.idea/codeStyles
19 | !.idea/codeStyles/Project.xml
20 | !.idea/codeStyles/codeStyleConfig.xml
21 | !.idea/encodings.xml
22 | !.idea/sqldialects.xml
23 | !.idea/compiler.xml
24 |
25 |
--------------------------------------------------------------------------------
/.gitlab-ci.yml:
--------------------------------------------------------------------------------
1 | include:
2 | - project: 'videocore/gitlab-templates'
3 | file: 'encore.yml'
4 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/.idea/compiler.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/encodings.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/.idea/vcs.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.reuse/dep5:
--------------------------------------------------------------------------------
1 | Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
2 |
3 | Files: *.sh
4 | Copyright: 2020 Sveriges Television AB
5 | License: CC0-1.0
6 |
7 | Files: Dockerfile
8 | Copyright: 2020 Sveriges Television AB
9 | License: CC0-1.0
10 |
11 | Files: .gitignore
12 | Copyright: 2020 Sveriges Television AB
13 | License: CC0-1.0
14 |
15 | Files: *.kts
16 | Copyright: 2020 Sveriges Television AB
17 | License: CC0-1.0
18 |
19 | Files: *.xml
20 | Copyright: 2020 Sveriges Television AB
21 | License: CC0-1.0
22 |
23 | Files: *.md
24 | Copyright: 2020 Sveriges Television AB
25 | License: CC0-1.0
26 |
27 | Files: *.adoc
28 | Copyright: 2020 Sveriges Television AB
29 | License: CC0-1.0
30 |
31 | Files: *.yml
32 | Copyright: 2020 Sveriges Television AB
33 | License: CC0-1.0
34 |
35 | Files: *.json
36 | Copyright: 2020 Sveriges Television AB
37 | License: CC0-1.0
38 |
39 | Files: *.txt
40 | Copyright: 2020 Sveriges Television AB
41 | License: CC0-1.0
42 |
43 | Files: *.gradle
44 | Copyright: 2020 Sveriges Television AB
45 | License: CC0-1.0
46 |
47 | Files: *.properties
48 | Copyright: 2020 Sveriges Television AB
49 | License: CC0-1.0
50 |
51 | Files: encore_logo.png
52 | Copyright: 2021 Sveriges Television AB
53 | License: CC-BY-SA-4.0
54 |
55 | Files: *.mp4
56 | Copyright: 2021 Sveriges Television AB
57 | License: CC-BY-SA-4.0
58 |
59 | # Third-party files.
60 |
61 | Files: gradlew*
62 | Copyright: 2007-2020 The original author or authors
63 | License: Apache-2.0
64 |
65 | Files: gradle/*
66 | Copyright: 2007-2020 The original author or authors
67 | License: Apache-2.0
68 |
--------------------------------------------------------------------------------
/CODE_OF_CONDUCT.adoc:
--------------------------------------------------------------------------------
1 | Greek fabulist Aesop, c. 620 – 564 BCE, nailed it pretty good.
2 |
3 | 'No act of kindness, no matter how small, is ever wasted'.
4 |
5 | In other words, be nice to each other.
6 |
--------------------------------------------------------------------------------
/CONTRIBUTING.adoc:
--------------------------------------------------------------------------------
1 | = Introduction
2 |
3 | We gratefully accept contributions via
4 | https://help.github.com/articles/about-pull-requests/[pull requests].
5 |
6 | Use the issue tracker to suggest feature requests, report bugs, and ask questions.
7 | This is also a great way to connect with the developers of the project as well
8 | as others who are interested in this solution.
9 |
10 | == Changing the code-base
11 |
12 | Generally speaking, you should fork this repository, make changes in your
13 | own fork, and then submit a pull-request. This is often called the https://gist.github.com/Chaser324/ce0505fbed06b947d962[Fork-and-Pull model]
14 |
15 | * All contributions to this project will be released under the inbound=outbound norm, that is,
16 | they are submitted under the projects main license.
17 | * By submitting a pull request or filing a bug, issue, or
18 | feature request, you agree to comply with this waiver of copyright interest.
19 | Details can be found in the link:./LICENSE[LICENSE].
20 | * All new code should have associated unit
21 | tests that validate implemented features and the presence or lack of defects.
22 | * Additionally, the code should follow any stylistic and architectural guidelines
23 | prescribed by the project. In the absence of such guidelines, mimic the styles
24 | and patterns in the existing code-base.
25 |
26 | === Signoff and optionally Sign each Commit
27 |
28 | As part of filing a pull request you agree to the DCO.
29 | https://developercertificate.org/[Developer Certificate of Origin]
30 |
31 | A DCO is a lightweight way for a contributor to confirm that they wrote or otherwise have the right
32 | to submit code or documentation to a project.
33 |
34 | To confirm that you agree to the DCO, you need to *sign off* your commits when sending us a pull request. Technically, this is done by supplying the `-s`/`--signoff` flag to git when committing:
35 |
36 | `$ git commit -s -m "add fix for the bug"`
37 |
38 | Optionally, you can also sign the commit with `-S` which also gives your commit a nice verified button on GitHub,
39 | but, it requires that you have a GPG keypair set up.
40 | For mor information, see https://docs.github.com/en/github/authenticating-to-github/signing-commits[Sign commit on GitHub with GPG key]
41 |
42 |
43 | `$ git commit -s -S -m "add fix for the bug"`
44 |
45 | For the difference in signoff and signing, see
46 | https://medium.com/@MarkEmeis/git-commit-signoff-vs-signing-9f37ee272b14/[Git signoff vs signing]
47 |
48 | == Git History
49 |
50 | In order to maintain a high software quality standard, we strongly prefer contributions to follow these rules:
51 |
52 | * We pay more attention to the quality of commit messages. In general, we share the view on how commit messages should be written with
53 | https://github.com/git/git/blob/master/Documentation/SubmittingPatches[the Git project itself]:
54 |
55 | * https://github.com/git/git/blob/e6932248fcb41fb94a0be484050881e03c7eb298/Documentation/SubmittingPatches#L43[Make separate commits for logically separate changes.]
56 | For example, pure formatting changes that do not affect software behaviour usually do not belong in the same commit as
57 | changes to program logic.
58 |
59 | * https://github.com/git/git/blob/e6932248fcb41fb94a0be484050881e03c7eb298/Documentation/SubmittingPatches#L101[Describe your changes well.]
60 | Do not just repeat in prose what is "obvious" from the code, but provide a rationale explaining _why_ you believe
61 | your change is necessary.
62 | * https://github.com/git/git/blob/e6932248fcb41fb94a0be484050881e03c7eb298/Documentation/SubmittingPatches#L133[Describe your changes in the imperative.]
63 | Instead of writing "Fixes an issue with encoding" prefer "Fix an encoding issue". Think about it like the commit
64 | only does something _if_ it is applied. This usually results in more concise commit messages.
65 | * https://github.com/git/git/blob/e6932248fcb41fb94a0be484050881e03c7eb298/Documentation/SubmittingPatches#L95[We are picky about whitespaces.]
66 | Trailing whitespace and duplicate blank lines are simply a superfluous annoyance, and most Git tools flag them red
67 | in the diff anyway.
68 |
69 | If you have ever wondered how a "perfect" commit message is supposed to look like, just look at basically any of
70 | https://github.com/git/git/commits?author=peff[Jeff King's commits] in the Git project.
71 |
72 | Thank you for reading and happy contributing!
73 |
74 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # SVT Encore
2 | [](https://eupl.eu/)
3 | [](https://api.reuse.software/info/github.com/fsfe/reuse-tool)
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 | SVT *Encore* is a scalable video transcoding tool, built on Open Source giants like [FFmpeg](https://www.ffmpeg.org/) and [Spring Boot](https://spring.io/projects/spring-boot).
12 |
13 |
14 | *Encore* was created to scale, and abstract the transcoding _power of FFmpeg_, and to offer a simple solution for Transcoding - Transcoding-as-a-Service.
15 |
16 | *Encore* is aimed at the advanced technical user that needs a scalable video transcoding tool - for example, as a part of their VOD (Video On Demand) transcoding pipeline.
17 |
18 | ## Features
19 |
20 | - Scalable - queuing and concurrency options
21 | - Flexible profile configuration
22 | - Possibility to extend FFmpeg functionality
23 | - Tested and tried in production
24 |
25 | _Encore_ is not
26 |
27 | - A live/stream transcoder
28 | - A Video packager (see <>)
29 | - An GUI application
30 |
31 | _Built with_
32 |
33 | * Kotlin
34 | * Gradle
35 | * Spring Boot
36 | * FFmpeg
37 | * and many other great projects
38 |
39 | ## Documentation
40 |
41 | Comprehensive documentation for _Encore_ can (and should) be read:
42 |
43 | [Online](https://svt.github.io/encore-doc/)
44 |
45 | or downloaded from the:
46 |
47 | [GitHub Repository](https://github.com/svt/encore-doc)
48 |
49 | If you have a running instance, you can also view the
50 |
51 | **OpenAPI Endpoints**:
52 |
53 | ```
54 | http(s)://yourinstance/swagger-ui.html
55 |
56 | as json
57 |
58 | http(s)://yourinstance/v3/api-docs/
59 |
60 | or as yaml
61 |
62 | http(s)://yourinstance/v3/api-docs.yaml
63 | ```
64 |
65 | ### Local development
66 |
67 | Please see the [online documentation](https://svt.github.io/encore-doc/#the-user-guide)
68 |
69 | ## License
70 |
71 | Copyright 2020 Sveriges Television AB.
72 |
73 | Encore is licensed under the
74 |
75 | [EUPL-1.2-or-later](LICENSE) license
76 |
77 | ## Primary maintainer
78 |
79 | SVT Videocore Team - (videocore svt se)
--------------------------------------------------------------------------------
/buildSrc/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | `kotlin-dsl`
3 | }
4 |
5 | repositories {
6 | mavenCentral()
7 | gradlePluginPortal()
8 | }
9 | kotlin {
10 | jvmToolchain(21)
11 | }
12 |
13 | dependencies {
14 | implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.25")
15 | implementation("org.jetbrains.kotlin:kotlin-allopen:1.9.25")
16 | implementation("org.springframework.boot:spring-boot-gradle-plugin:3.4.3")
17 | implementation("io.spring.gradle:dependency-management-plugin:1.1.5")
18 | implementation("org.jmailen.gradle:kotlinter-gradle:4.4.1")
19 | implementation("pl.allegro.tech.build:axion-release-plugin:1.18.8")
20 | implementation("org.graalvm.buildtools:native-gradle-plugin:0.10.5")
21 | implementation("com.github.fhermansson:assertj-gradle-plugin:1.1.5")
22 | implementation("com.github.ben-manes:gradle-versions-plugin:0.51.0")
23 | }
24 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/encore.kotlin-conventions.gradle.kts:
--------------------------------------------------------------------------------
1 | import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask
2 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
3 |
4 | plugins {
5 | idea
6 | jacoco
7 | kotlin("jvm")
8 | kotlin("plugin.spring")
9 | id("pl.allegro.tech.build.axion-release")
10 | id("com.github.fhermansson.assertj-generator")
11 | id("org.jmailen.kotlinter")
12 | id("com.github.ben-manes.versions")
13 | id("io.spring.dependency-management")
14 | }
15 |
16 | group = "se.svt.oss"
17 | project.version = scmVersion.version
18 |
19 | tasks.withType {
20 | useJUnitPlatform()
21 | }
22 | apply { from("../checks.gradle") }
23 | repositories {
24 | mavenCentral()
25 | }
26 |
27 | kotlin {
28 | jvmToolchain(21)
29 | compilerOptions {
30 | freeCompilerArgs.addAll("-Xjsr305=strict")
31 | }
32 | }
33 |
34 | fun isNonStable(version: String): Boolean {
35 | val stableKeyword = listOf("RELEASE", "FINAL", "GA").any { version.uppercase().contains(it) }
36 | val regex = "^[0-9,.v-]+(-r)?$".toRegex()
37 | val isStable = stableKeyword || regex.matches(version)
38 | return isStable.not()
39 | }
40 |
41 | tasks.withType {
42 | rejectVersionIf {
43 | isNonStable(candidate.version)
44 | }
45 | }
46 |
47 | tasks.lintKotlinTest {
48 | source = (source - fileTree("src/test/generated-java")).asFileTree
49 | }
50 | tasks.formatKotlinTest {
51 | source = (source - fileTree("src/test/generated-java")).asFileTree
52 | }
53 |
54 | assertjGenerator {
55 | classOrPackageNames = arrayOf(
56 | "se.svt.oss.encore.model",
57 | "se.svt.oss.mediaanalyzer.file"
58 | )
59 | entryPointPackage = "se.svt.oss.encore"
60 | useJakartaAnnotations = true
61 | }
62 | tasks.test {
63 | jvmArgs("-XX:+EnableDynamicAgentLoading")
64 | }
65 | dependencyManagement {
66 | imports {
67 | mavenBom("org.springframework.boot:spring-boot-dependencies:3.4.3")
68 | mavenBom("org.springframework.cloud:spring-cloud-dependencies:2024.0.0")
69 | }
70 | }
71 | dependencies {
72 | implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
73 | implementation("io.github.oshai:kotlin-logging-jvm:7.0.5")
74 | implementation("org.springframework.boot:spring-boot-starter-data-redis")
75 | testImplementation("org.springframework.boot:spring-boot-starter-test") {
76 | exclude(group = "org.mockito")
77 | }
78 | testImplementation("org.assertj:assertj-core")
79 | testImplementation("io.mockk:mockk:1.13.17")
80 | }
81 |
82 |
83 |
84 |
--------------------------------------------------------------------------------
/buildSrc/src/main/kotlin/encore.spring-boot-app-conventions.gradle.kts:
--------------------------------------------------------------------------------
1 | import org.springframework.boot.gradle.tasks.bundling.BootJar
2 | plugins {
3 | kotlin("jvm")
4 | kotlin("plugin.spring")
5 | id("org.springframework.boot")
6 | id("org.graalvm.buildtools.native")
7 | id("io.spring.dependency-management")
8 | }
9 |
10 | graalvmNative {
11 | binaries.all {
12 | buildArgs.add("--strict-image-heap")
13 | }
14 | }
15 | tasks.named("bootJar") {
16 | archiveClassifier.set("boot")
17 | }
18 | dependencyManagement {
19 | imports {
20 | mavenBom("org.springframework.boot:spring-boot-dependencies:3.4.3")
21 | mavenBom("org.springframework.cloud:spring-cloud-dependencies:2024.0.0")
22 | }
23 | }
24 | dependencies {
25 | implementation("org.springframework.boot:spring-boot-starter")
26 | implementation("org.springframework.cloud:spring-cloud-starter-config")
27 | implementation("org.springframework.boot:spring-boot-starter-logging")
28 | implementation("net.logstash.logback:logstash-logback-encoder:8.0")
29 | }
30 |
31 |
32 |
33 |
--------------------------------------------------------------------------------
/checks.gradle:
--------------------------------------------------------------------------------
1 | jacocoTestCoverageVerification {
2 | violationRules {
3 | rule {
4 | element = 'METHOD'
5 | includes = ['se.svt.oss.encore.*']
6 | excludes = [
7 | '*.invoke()',
8 | '*.EncoreProperties*.get*()',
9 | '*.DefaultConstructorMarker*',
10 | '*ApplicationKt.main*',
11 | '*.static {...}',
12 | '*.model.*.get*',
13 | '*.service.localencode.LocalEncodeService.moveFile*',
14 | '*QueueService.getQueue*',
15 | '*QueueService.migrateQueues()',
16 | '*.ShutdownHandler.*',
17 | '*FfmpegExecutor.runFfmpeg$lambda$7(java.lang.Process)',
18 | ]
19 | limit {
20 | counter = 'LINE'
21 | minimum = 0.7
22 | }
23 | }
24 |
25 | failOnViolation = true
26 | }
27 | }
28 |
29 | jacocoTestCoverageVerification.dependsOn jacocoTestReport
30 | check.dependsOn jacocoTestCoverageVerification
31 |
--------------------------------------------------------------------------------
/encore-common/build.gradle.kts:
--------------------------------------------------------------------------------
1 | plugins {
2 | id("encore.kotlin-conventions")
3 | `java-test-fixtures`
4 | }
5 |
6 | dependencies {
7 |
8 | api("se.svt.oss:media-analyzer:2.0.7")
9 | implementation(kotlin("reflect"))
10 |
11 | compileOnly("org.springdoc:springdoc-openapi-starter-webmvc-api:2.6.0")
12 | compileOnly("org.springframework.data:spring-data-rest-core")
13 | compileOnly("org.springframework.boot:spring-boot-starter-validation")
14 | implementation("org.springframework.boot:spring-boot-starter-webflux")
15 | implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml")
16 |
17 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core")
18 | implementation("org.jetbrains.kotlinx:kotlinx-coroutines-slf4j")
19 |
20 | testImplementation(project(":encore-web"))
21 | testImplementation("org.springframework.security:spring-security-test")
22 | testImplementation("org.awaitility:awaitility")
23 | testImplementation("org.wiremock:wiremock-standalone:3.12.1")
24 | testImplementation("org.springframework.boot:spring-boot-starter-data-rest")
25 | testFixturesImplementation(platform("org.springframework.boot:spring-boot-dependencies:3.4.3"))
26 | testFixturesImplementation("com.redis:testcontainers-redis:2.2.4")
27 | testFixturesImplementation("io.github.microutils:kotlin-logging:3.0.5")
28 | testFixturesImplementation("org.junit.jupiter:junit-jupiter-api")
29 | testFixturesRuntimeOnly("org.junit.platform:junit-platform-launcher")
30 | }
31 |
32 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/ClientConfiguration.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore
6 |
7 | import org.springframework.beans.factory.annotation.Value
8 | import org.springframework.context.annotation.Bean
9 | import org.springframework.context.annotation.Configuration
10 | import org.springframework.http.HttpHeaders
11 | import org.springframework.web.reactive.function.client.WebClient
12 | import org.springframework.web.reactive.function.client.support.WebClientAdapter
13 | import org.springframework.web.service.invoker.HttpServiceProxyFactory
14 | import se.svt.oss.encore.service.callback.CallbackClient
15 |
16 | @Configuration(proxyBeanMethods = false)
17 | class ClientConfiguration {
18 |
19 | @Bean
20 | fun callbackClient(@Value("\${service.name:encore}") userAgent: String): CallbackClient {
21 | val webClient = WebClient.builder()
22 | .defaultHeader(HttpHeaders.USER_AGENT, userAgent)
23 | .build()
24 | return HttpServiceProxyFactory.builderFor(WebClientAdapter.create(webClient))
25 | .build()
26 | .createClient(CallbackClient::class.java)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/EncoreRuntimeHints.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore
6 |
7 | import org.springframework.aot.hint.MemberCategory
8 | import org.springframework.aot.hint.RuntimeHints
9 | import org.springframework.aot.hint.RuntimeHintsRegistrar
10 | import se.svt.oss.encore.config.AudioMixPreset
11 | import se.svt.oss.encore.config.EncodingProperties
12 | import se.svt.oss.encore.config.EncoreProperties
13 |
14 | class EncoreRuntimeHints : RuntimeHintsRegistrar {
15 | override fun registerHints(hints: RuntimeHints, classLoader: ClassLoader?) {
16 | hints.resources().registerPattern("migrate_queue_script.lua")
17 | hints.reflection()
18 | .registerType(
19 | EncoreProperties::class.java,
20 | MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
21 | )
22 | .registerType(
23 | EncodingProperties::class.java,
24 | MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
25 | )
26 | .registerType(
27 | AudioMixPreset::class.java,
28 | MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
29 | )
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/MediaAnalyzerConfiguration.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore
6 |
7 | import com.fasterxml.jackson.databind.ObjectMapper
8 | import org.springframework.context.annotation.Bean
9 | import org.springframework.context.annotation.Configuration
10 | import se.svt.oss.mediaanalyzer.MediaAnalyzer
11 |
12 | @Configuration(proxyBeanMethods = false)
13 | class MediaAnalyzerConfiguration {
14 |
15 | @Bean
16 | fun mediaAnalyzer(objectMapper: ObjectMapper) = MediaAnalyzer(objectMapper)
17 | }
18 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/RedisConfiguration.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore
6 |
7 | import com.fasterxml.jackson.databind.ObjectMapper
8 | import org.springframework.aot.hint.annotation.RegisterReflectionForBinding
9 | import org.springframework.context.annotation.Bean
10 | import org.springframework.context.annotation.Configuration
11 | import org.springframework.core.io.ClassPathResource
12 | import org.springframework.core.task.SimpleAsyncTaskExecutor
13 | import org.springframework.data.redis.connection.RedisConnectionFactory
14 | import org.springframework.data.redis.core.RedisKeyValueAdapter
15 | import org.springframework.data.redis.core.RedisTemplate
16 | import org.springframework.data.redis.core.convert.RedisCustomConversions
17 | import org.springframework.data.redis.core.script.RedisScript
18 | import org.springframework.data.redis.listener.PatternTopic
19 | import org.springframework.data.redis.listener.RedisMessageListenerContainer
20 | import org.springframework.data.redis.listener.adapter.MessageListenerAdapter
21 | import org.springframework.data.redis.repository.configuration.EnableRedisRepositories
22 | import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer
23 | import org.springframework.data.redis.serializer.RedisSerializer
24 | import se.svt.oss.encore.model.CancelEvent
25 | import se.svt.oss.encore.model.EncoreJob
26 | import se.svt.oss.encore.model.RedisEvent
27 | import se.svt.oss.encore.model.SegmentProgressEvent
28 | import se.svt.oss.encore.model.input.AudioInput
29 | import se.svt.oss.encore.model.input.AudioVideoInput
30 | import se.svt.oss.encore.model.input.VideoInput
31 | import se.svt.oss.encore.model.queue.QueueItem
32 | import se.svt.oss.encore.repository.ByteArrayToChannelLayoutConverter
33 | import se.svt.oss.encore.repository.ByteArrayToOffsetDateTimeConverter
34 | import se.svt.oss.encore.repository.ChannelLayoutToByteArrayConverter
35 | import se.svt.oss.encore.repository.OffsetDateTimeToByteArrayConverter
36 | import se.svt.oss.mediaanalyzer.file.AudioFile
37 | import se.svt.oss.mediaanalyzer.file.ImageFile
38 | import se.svt.oss.mediaanalyzer.file.SubtitleFile
39 | import se.svt.oss.mediaanalyzer.file.VideoFile
40 |
41 | @Configuration(proxyBeanMethods = false)
42 | @RegisterReflectionForBinding(
43 | classes = [
44 | EncoreJob::class,
45 | AudioVideoInput::class,
46 | VideoInput::class,
47 | AudioInput::class,
48 | ImageFile::class,
49 | VideoFile::class,
50 | AudioFile::class,
51 | SubtitleFile::class,
52 | CancelEvent::class,
53 | SegmentProgressEvent::class,
54 | QueueItem::class,
55 | ],
56 | classNames = ["kotlin.collections.EmptyMap", "kotlin.collections.EmptyList", "kotlin.collections.EmptySet", "java.lang.Enum.EnumDesc"],
57 | )
58 | @EnableRedisRepositories(
59 | enableKeyspaceEvents = RedisKeyValueAdapter.EnableKeyspaceEvents.ON_STARTUP,
60 | keyspaceNotificationsConfigParameter = "#{\${redis.keyspace.disable-config-notifications:false} ? '' : 'Ex'}",
61 | )
62 | class RedisConfiguration {
63 |
64 | @Bean
65 | fun redisCustomConversions() = RedisCustomConversions(
66 | listOf(
67 | OffsetDateTimeToByteArrayConverter(),
68 | ByteArrayToOffsetDateTimeConverter(),
69 | ChannelLayoutToByteArrayConverter(),
70 | ByteArrayToChannelLayoutConverter(),
71 | ),
72 | )
73 |
74 | @Bean
75 | fun redisMessageListenerContainer(connectionFactory: RedisConnectionFactory): RedisMessageListenerContainer {
76 | val container = RedisMessageListenerContainer()
77 | container.setConnectionFactory(connectionFactory)
78 | val taskExecutor = SimpleAsyncTaskExecutor("redisMessageListenerContainer-")
79 | .apply { setVirtualThreads(true) }
80 | container.setTaskExecutor(taskExecutor)
81 | // https://github.com/spring-projects/spring-data-redis/issues/2425
82 | container.addMessageListener(MessageListenerAdapter(), PatternTopic.of("Dummy"))
83 | return container
84 | }
85 |
86 | @Bean
87 | fun redisEventTemplate(
88 | connectionFactory: RedisConnectionFactory,
89 | objectMapper: ObjectMapper,
90 | ): RedisTemplate {
91 | val template = RedisTemplate()
92 | template.connectionFactory = connectionFactory
93 | template.keySerializer = RedisSerializer.string()
94 | template.valueSerializer = Jackson2JsonRedisSerializer(objectMapper, RedisEvent::class.java)
95 | return template
96 | }
97 |
98 | @Bean
99 | fun redisQueueTemplate(
100 | connectionFactory: RedisConnectionFactory,
101 | objectMapper: ObjectMapper,
102 | ): RedisTemplate {
103 | val template = RedisTemplate()
104 | template.connectionFactory = connectionFactory
105 | template.keySerializer = RedisSerializer.string()
106 | template.valueSerializer = Jackson2JsonRedisSerializer(objectMapper, QueueItem::class.java)
107 | return template
108 | }
109 |
110 | @Bean
111 | fun redisQueueMigrationScript() = RedisScript(ClassPathResource("migrate_queue_script.lua"))
112 | }
113 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/cancellation/CancellationListener.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.cancellation
6 |
7 | import com.fasterxml.jackson.databind.ObjectMapper
8 | import com.fasterxml.jackson.module.kotlin.readValue
9 | import io.github.oshai.kotlinlogging.KotlinLogging
10 | import kotlinx.coroutines.Job
11 | import org.springframework.data.redis.connection.Message
12 | import org.springframework.data.redis.connection.MessageListener
13 | import se.svt.oss.encore.model.CancelEvent
14 | import java.util.UUID
15 |
16 | private val log = KotlinLogging.logger {}
17 |
18 | class CancellationListener(
19 | private val objectMapper: ObjectMapper,
20 | private val encoreJobId: UUID,
21 | private val coroutineJob: Job,
22 | ) : MessageListener {
23 |
24 | override fun onMessage(message: Message, pattern: ByteArray?) {
25 | val jobId = objectMapper.readValue(message.body).jobId
26 | if (jobId == encoreJobId) {
27 | log.info { "Received cancel event for job $jobId" }
28 | coroutineJob.cancel()
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/cancellation/SegmentProgressListener.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.cancellation
6 |
7 | import com.fasterxml.jackson.databind.ObjectMapper
8 | import com.fasterxml.jackson.module.kotlin.readValue
9 | import kotlinx.coroutines.Job
10 | import kotlinx.coroutines.cancel
11 | import kotlinx.coroutines.channels.SendChannel
12 | import kotlinx.coroutines.channels.trySendBlocking
13 | import org.springframework.data.redis.connection.Message
14 | import org.springframework.data.redis.connection.MessageListener
15 | import se.svt.oss.encore.model.SegmentProgressEvent
16 | import java.util.UUID
17 | import java.util.concurrent.ConcurrentHashMap
18 | import java.util.concurrent.atomic.AtomicBoolean
19 |
20 | class SegmentProgressListener(
21 | private val objectMapper: ObjectMapper,
22 | private val encoreJobId: UUID,
23 | private val coroutineJob: Job,
24 | private val totalSegments: Int,
25 | private val progressChannel: SendChannel,
26 | ) : MessageListener {
27 |
28 | private val completedSegments: MutableSet = ConcurrentHashMap.newKeySet()
29 | val anyFailed = AtomicBoolean(false)
30 |
31 | fun completed() = anyFailed.get() || completedSegments.size == totalSegments
32 |
33 | fun completionCount() = completedSegments.size
34 |
35 | override fun onMessage(message: Message, pattern: ByteArray?) {
36 | val msg = objectMapper.readValue(message.body)
37 | if (msg.jobId == encoreJobId) {
38 | if (!msg.success) {
39 | progressChannel.close()
40 | anyFailed.set(true)
41 | coroutineJob.cancel("Segment ${msg.segment} failed!")
42 | } else if (completedSegments.add(msg.segment)) {
43 | val percent = (completedSegments.size * 100.0 / totalSegments).toInt()
44 | progressChannel.trySendBlocking(percent)
45 | if (percent == 100) {
46 | progressChannel.close()
47 | }
48 | }
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/config/AudioMixPreset.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 | package se.svt.oss.encore.config
5 |
6 | import org.springframework.boot.context.properties.NestedConfigurationProperty
7 | import se.svt.oss.encore.model.profile.ChannelLayout
8 |
9 | data class AudioMixPreset(
10 | val fallbackToAuto: Boolean = true,
11 | @NestedConfigurationProperty
12 | val defaultPan: Map = emptyMap(),
13 | @NestedConfigurationProperty
14 | val panMapping: Map> = emptyMap(),
15 | )
16 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/config/EncodingProperties.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.config
6 |
7 | import org.springframework.boot.context.properties.NestedConfigurationProperty
8 | import se.svt.oss.encore.model.profile.ChannelLayout
9 |
10 | data class EncodingProperties(
11 | @NestedConfigurationProperty
12 | val audioMixPresets: Map = mapOf("default" to AudioMixPreset()),
13 | @NestedConfigurationProperty
14 | val defaultChannelLayouts: Map = emptyMap(),
15 | val flipWidthHeightIfPortrait: Boolean = true,
16 | val exitOnError: Boolean = true,
17 | val globalParams: LinkedHashMap = linkedMapOf(),
18 | )
19 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/config/EncoreProperties.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.config
6 |
7 | import org.springframework.boot.context.properties.ConfigurationProperties
8 | import org.springframework.boot.context.properties.NestedConfigurationProperty
9 | import java.io.File
10 | import java.time.Duration
11 |
12 | @ConfigurationProperties("encore-settings")
13 | data class EncoreProperties(
14 | /**
15 | * transcode to local tmp dir before copying to output folder
16 | */
17 | val localTemporaryEncode: Boolean = false,
18 | /**
19 | * number of work queues and threads
20 | */
21 | val concurrency: Int = 2,
22 | /**
23 | * time to wait after application start before polling queue
24 | */
25 | val pollInitialDelay: Duration = Duration.ofSeconds(10),
26 | /**
27 | * time to wait between polls
28 | */
29 | val pollDelay: Duration = Duration.ofSeconds(5),
30 | /**
31 | * poll only the specified queue
32 | */
33 | val pollQueue: Int? = null,
34 | /**
35 | * disable polling. could be set on encore-web if all transcoding is to be done by encore-workers
36 | */
37 | val pollDisabled: Boolean = false,
38 | /**
39 | * should queues with higher prio be poller before the queue assigned to thread or worker
40 | */
41 | val pollHigherPrio: Boolean = true,
42 | /**
43 | * if true, encore-worker will poll the queue until empty before shutting down, otherwise just poll once
44 | */
45 | val workerDrainQueue: Boolean = false,
46 | val redisKeyPrefix: String = "encore",
47 | /**
48 | * optional web security settings
49 | */
50 | val security: Security = Security(),
51 | /**
52 | * open api contact information
53 | */
54 | val openApi: OpenApi = OpenApi(),
55 | /**
56 | * path to directory shared by encore instances. required for encoding in segments
57 | */
58 | val sharedWorkDir: File? = null,
59 | /**
60 | * timeout for segmented encode before failing
61 | */
62 | val segmentedEncodeTimeout: Duration = Duration.ofMinutes(120),
63 | /***
64 | * enable migration of queues from redis LIST to ZSET
65 | */
66 | val queueMigrationScriptEnabled: Boolean = true,
67 | @NestedConfigurationProperty
68 | val encoding: EncodingProperties = EncodingProperties(),
69 | ) {
70 | data class Security(
71 | val enabled: Boolean = false,
72 | val userPassword: String = "",
73 | val adminPassword: String = "",
74 | )
75 |
76 | data class OpenApi(
77 | val title: String = "Encore OpenAPI",
78 | val description: String = "Endpoints for Encore",
79 | val contactName: String = "",
80 | val contactUrl: String = "",
81 | val contactEmail: String = "",
82 | )
83 | }
84 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/config/ProfileProperties.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.config
6 |
7 | import org.springframework.boot.context.properties.ConfigurationProperties
8 | import org.springframework.core.io.Resource
9 |
10 | @ConfigurationProperties("profile")
11 | data class ProfileProperties(
12 | val location: Resource,
13 | val spelExpressionPrefix: String = "#{",
14 | val spelExpressionSuffix: String = "}",
15 | )
16 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/model/CancelEvent.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.model
6 |
7 | import java.util.UUID
8 |
9 | data class CancelEvent(val jobId: UUID) : RedisEvent
10 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/model/RedisEvent.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2025 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.model
6 |
7 | interface RedisEvent
8 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/model/SegmentProgressEvent.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.model
6 |
7 | import java.util.UUID
8 |
9 | data class SegmentProgressEvent(
10 | val jobId: UUID,
11 | val segment: Int,
12 | val success: Boolean,
13 | ) : RedisEvent
14 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/model/Status.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.model
6 |
7 | enum class Status(val isCompleted: Boolean) {
8 | NEW(false),
9 | QUEUED(false),
10 | IN_PROGRESS(false),
11 | SUCCESSFUL(true),
12 | FAILED(true),
13 | CANCELLED(true),
14 | ;
15 |
16 | val isCancelled: Boolean
17 | get() = this == CANCELLED
18 | }
19 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/model/callback/JobProgress.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.model.callback
6 |
7 | import se.svt.oss.encore.model.Status
8 | import java.util.UUID
9 |
10 | data class JobProgress(
11 | val jobId: UUID,
12 | val externalId: String?,
13 | val progress: Int,
14 | val status: Status,
15 | )
16 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/model/mediafile/AudioLayout.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.model.mediafile
6 |
7 | enum class AudioLayout {
8 | NONE,
9 | INVALID,
10 | MONO_STREAMS,
11 | MULTI_TRACK,
12 | }
13 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/model/mediafile/Extensions.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.model.mediafile
6 |
7 | import io.github.oshai.kotlinlogging.KotlinLogging
8 | import se.svt.oss.encore.model.input.AudioIn
9 | import se.svt.oss.encore.model.profile.ChannelLayout
10 | import se.svt.oss.mediaanalyzer.file.AudioFile
11 | import se.svt.oss.mediaanalyzer.file.MediaContainer
12 | import se.svt.oss.mediaanalyzer.file.VideoFile
13 |
14 | private val log = KotlinLogging.logger { }
15 |
16 | fun MediaContainer.audioLayout() = when {
17 | audioStreams.isEmpty() -> AudioLayout.NONE
18 | audioStreams.size == 1 -> AudioLayout.MULTI_TRACK
19 | audioStreams.all { it.channels == 1 } -> AudioLayout.MONO_STREAMS
20 | audioStreams.first().channels > 1 -> AudioLayout.MULTI_TRACK
21 | else -> AudioLayout.INVALID
22 | }
23 |
24 | fun MediaContainer.channelCount() = if (audioLayout() == AudioLayout.MULTI_TRACK) {
25 | audioStreams.first().channels
26 | } else {
27 | audioStreams.size
28 | }
29 |
30 | fun AudioIn.channelLayout(defaultChannelLayouts: Map): ChannelLayout = when (analyzedAudio.audioLayout()) {
31 | AudioLayout.NONE, AudioLayout.INVALID -> null
32 | AudioLayout.MONO_STREAMS -> if (analyzedAudio.channelCount() == channelLayout?.channels?.size) {
33 | channelLayout
34 | } else {
35 | defaultChannelLayouts[analyzedAudio.channelCount()]
36 | ?: ChannelLayout.defaultChannelLayout(analyzedAudio.channelCount())
37 | }
38 |
39 | AudioLayout.MULTI_TRACK -> analyzedAudio.audioStreams.first().channelLayout
40 | ?.let { ChannelLayout.getByNameOrNull(it) }
41 | ?: defaultChannelLayouts[analyzedAudio.channelCount()]
42 | ?: ChannelLayout.defaultChannelLayout(analyzedAudio.channelCount())
43 | } ?: throw RuntimeException("Could not determine channel layout for audio input '$audioLabel'!")
44 |
45 | fun VideoFile.trimAudio(keep: Int?): VideoFile = if (keep != null && keep < audioStreams.size) {
46 | log.debug { "Using first $keep audio streams of ${audioStreams.size} of ${this.file}" }
47 | copy(audioStreams = audioStreams.take(keep))
48 | } else {
49 | this
50 | }
51 |
52 | fun AudioFile.trimAudio(keep: Int?): AudioFile = if (keep != null && keep < audioStreams.size) {
53 | log.debug { "Using first $keep audio streams of ${audioStreams.size} of ${this.file}" }
54 | copy(audioStreams = audioStreams.take(keep))
55 | } else {
56 | this
57 | }
58 |
59 | fun VideoFile.selectVideoStream(index: Int?): VideoFile = index?.let {
60 | copy(videoStreams = listOf(videoStreams[it]))
61 | } ?: this
62 |
63 | fun VideoFile.selectAudioStream(index: Int?): VideoFile = index?.let {
64 | copy(audioStreams = listOf(audioStreams[it]))
65 | } ?: this
66 |
67 | fun AudioFile.selectAudioStream(index: Int?): AudioFile = index?.let {
68 | copy(audioStreams = listOf(audioStreams[it]))
69 | } ?: this
70 |
71 | fun Map.toParams(): List =
72 | flatMap { entry ->
73 | listOfNotNull("-${entry.key}", entry.value?.let { "$it" })
74 | .filterNot { it.isEmpty() }
75 | }
76 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/model/output/Output.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.model.output
6 |
7 | import java.io.File
8 |
9 | data class Output(
10 | val video: VideoStreamEncode?,
11 | val audioStreams: List = emptyList(),
12 | val output: String,
13 | val format: String = "mp4",
14 | val postProcessor: PostProcessor = PostProcessor { outputFolder -> listOf(outputFolder.resolve(output)) },
15 | val id: String,
16 | val isImage: Boolean = false,
17 | val decodeOutputStream: String? = null,
18 | )
19 |
20 | fun interface PostProcessor {
21 | fun process(outputFolder: File): List
22 | }
23 |
24 | interface StreamEncode {
25 | val params: List
26 | val filter: String?
27 | val twoPass: Boolean
28 | val inputLabels: List
29 | }
30 |
31 | data class VideoStreamEncode(
32 | override val params: List,
33 | val firstPassParams: List = emptyList(),
34 | override val filter: String? = null,
35 | override val twoPass: Boolean = false,
36 | override val inputLabels: List,
37 | ) : StreamEncode
38 |
39 | data class AudioStreamEncode(
40 | override val params: List,
41 | override val filter: String? = null,
42 | override val inputLabels: List,
43 | val preserveLayout: Boolean = false,
44 | ) : StreamEncode {
45 | override val twoPass: Boolean
46 | get() = false
47 | }
48 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/AudioEncoder.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.model.profile
6 |
7 | import io.github.oshai.kotlinlogging.KotlinLogging
8 | import se.svt.oss.encore.model.output.Output
9 |
10 | private val log = KotlinLogging.logger { }
11 |
12 | abstract class AudioEncoder : OutputProducer {
13 |
14 | abstract val optional: Boolean
15 |
16 | fun logOrThrow(message: String): Output? {
17 | if (optional) {
18 | log.info { message }
19 | return null
20 | } else {
21 | throw RuntimeException(message)
22 | }
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/ChannelId.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.model.profile
6 |
7 | enum class ChannelId {
8 | FL,
9 | FR,
10 | FC,
11 | LFE,
12 | BL,
13 | BR,
14 | FLC,
15 | FRC,
16 | BC,
17 | SL,
18 | SR,
19 | TC,
20 | TFL,
21 | TFC,
22 | TFR,
23 | TBL,
24 | TBC,
25 | TBR,
26 | DL,
27 | DR,
28 | WL,
29 | WR,
30 | SDL,
31 | SDR,
32 | LFE2,
33 | TSL,
34 | TSR,
35 | BFC,
36 | BFL,
37 | BFR,
38 | }
39 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/ChannelLayout.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.model.profile
6 |
7 | import com.fasterxml.jackson.annotation.JsonValue
8 | import se.svt.oss.encore.model.profile.ChannelId.BC
9 | import se.svt.oss.encore.model.profile.ChannelId.BFC
10 | import se.svt.oss.encore.model.profile.ChannelId.BFL
11 | import se.svt.oss.encore.model.profile.ChannelId.BFR
12 | import se.svt.oss.encore.model.profile.ChannelId.BL
13 | import se.svt.oss.encore.model.profile.ChannelId.BR
14 | import se.svt.oss.encore.model.profile.ChannelId.DL
15 | import se.svt.oss.encore.model.profile.ChannelId.DR
16 | import se.svt.oss.encore.model.profile.ChannelId.FC
17 | import se.svt.oss.encore.model.profile.ChannelId.FL
18 | import se.svt.oss.encore.model.profile.ChannelId.FLC
19 | import se.svt.oss.encore.model.profile.ChannelId.FR
20 | import se.svt.oss.encore.model.profile.ChannelId.FRC
21 | import se.svt.oss.encore.model.profile.ChannelId.LFE
22 | import se.svt.oss.encore.model.profile.ChannelId.LFE2
23 | import se.svt.oss.encore.model.profile.ChannelId.SL
24 | import se.svt.oss.encore.model.profile.ChannelId.SR
25 | import se.svt.oss.encore.model.profile.ChannelId.TBC
26 | import se.svt.oss.encore.model.profile.ChannelId.TBL
27 | import se.svt.oss.encore.model.profile.ChannelId.TBR
28 | import se.svt.oss.encore.model.profile.ChannelId.TC
29 | import se.svt.oss.encore.model.profile.ChannelId.TFC
30 | import se.svt.oss.encore.model.profile.ChannelId.TFL
31 | import se.svt.oss.encore.model.profile.ChannelId.TFR
32 | import se.svt.oss.encore.model.profile.ChannelId.TSL
33 | import se.svt.oss.encore.model.profile.ChannelId.TSR
34 | import se.svt.oss.encore.model.profile.ChannelId.WL
35 | import se.svt.oss.encore.model.profile.ChannelId.WR
36 |
37 | enum class ChannelLayout(@JsonValue val layoutName: String, val channels: List) {
38 | CH_LAYOUT_MONO("mono", listOf(FC)),
39 | CH_LAYOUT_STEREO("stereo", listOf(FL, FR)),
40 | CH_LAYOUT_2POINT1("2.1", listOf(FL, FR, LFE)),
41 | CH_LAYOUT_3POINT0("3.0", listOf(FL, FR, FC)),
42 | CH_LAYOUT_3POINT0_BACK("3.0(back)", listOf(FL, FR, BC)),
43 | CH_LAYOUT_4POINT0("4.0", listOf(FL, FR, FC, BC)),
44 | CH_LAYOUT_QUAD("quad", listOf(FL, FR, BL, BR)),
45 | CH_LAYOUT_QUAD_SIDE("quad(side)", listOf(FL, FR, SL, SR)),
46 | CH_LAYOUT_3POINT1("3.1", listOf(FL, FR, FC, LFE)),
47 | CH_LAYOUT_5POINT0("5.0", listOf(FL, FR, FC, BL, BR)),
48 | CH_LAYOUT_5POINT0_SIDE("5.0(side)", listOf(FL, FR, FC, SL, SR)),
49 | CH_LAYOUT_4POINT1("4.1", listOf(FL, FR, FC, LFE, BC)),
50 | CH_LAYOUT_5POINT1("5.1", listOf(FL, FR, FC, LFE, BL, BR)),
51 | CH_LAYOUT_5POINT1_SIDE("5.1(side)", listOf(FL, FR, FC, LFE, SL, SR)),
52 | CH_LAYOUT_6POINT0("6.0", listOf(FL, FR, FC, BC, SL, SR)),
53 | CH_LAYOUT_6POINT0_FRONT("6.0(front)", listOf(FL, FR, FLC, FRC, SL, SR)),
54 | CH_LAYOUT_3POINT1POINT2("3.1.2", listOf(FL, FR, FC, LFE, TFL, TFR)),
55 | CH_LAYOUT_HEXAGONAL("hexagonal", listOf(FL, FR, FC, BL, BR, BC)),
56 | CH_LAYOUT_6POINT1("6.1", listOf(FL, FR, FC, LFE, BC, SL, SR)),
57 | CH_LAYOUT_6POINT1_BACK("6.1(back)", listOf(FL, FR, FC, LFE, BL, BR, BC)),
58 | CH_LAYOUT_6POINT1_FRONT("6.1(front)", listOf(FL, FR, LFE, FLC, FRC, SL, SR)),
59 | CH_LAYOUT_7POINT0("7.0", listOf(FL, FR, FC, BL, BR, SL, SR)),
60 | CH_LAYOUT_7POINT0_FRONT("7.0(front)", listOf(FL, FR, FC, FLC, FRC, SL, SR)),
61 | CH_LAYOUT_7POINT1("7.1", listOf(FL, FR, FC, LFE, BL, BR, SL, SR)),
62 | CH_LAYOUT_7POINT1_WIDE("7.1(wide)", listOf(FL, FR, FC, LFE, BL, BR, FLC, FRC)),
63 | CH_LAYOUT_7POINT1_WIDE_SIDE("7.1(wide-side)", listOf(FL, FR, FC, LFE, FLC, FRC, SL, SR)),
64 | CH_LAYOUT_5POINT1POINT2("5.1.2", listOf(FL, FR, FC, LFE, BL, BR, TFL, TFR)),
65 | CH_LAYOUT_OCTAGONAL("octagonal", listOf(FL, FR, FC, BL, BR, BC, SL, SR)),
66 | CH_LAYOUT_CUBE("cube", listOf(FL, FR, BL, BR, TFL, TFR, TBL, TBR)),
67 | CH_LAYOUT_5POINT1POINT4("5.1.4", listOf(FL, FR, FC, LFE, BL, BR, TFL, TFR, TBL, TBR)),
68 | CH_LAYOUT_7POINT1POINT2("7.1.2", listOf(FL, FR, FC, LFE, BL, BR, SL, SR, TFL, TFR)),
69 | CH_LAYOUT_7POINT1POINT4("7.1.4", listOf(FL, FR, FC, LFE, BL, BR, SL, SR, TFL, TFR, TBL, TBR)),
70 | CH_LAYOUT_7POINT2POINT3("7.2.3", listOf(FL, FR, FC, LFE, BL, BR, SL, SR, TFL, TFR, TBC, LFE2)),
71 | CH_LAYOUT_9POINT1POINT4("9.1.4", listOf(FL, FR, FC, LFE, BL, BR, FLC, FRC, SL, SR, TFL, TFR, TBL, TBR)),
72 | CH_LAYOUT_HEXADECAGONAL(
73 | "hexadecagonal",
74 | listOf(
75 | FL, FR, FC, BL, BR, BC, SL, SR, TFL, TFC, TFR, TBL, TBC, TBR, WL, WR,
76 | ),
77 | ),
78 | CH_LAYOUT_DOWNMIX("downmix)", listOf(DL, DR)),
79 | CH_LAYOUT_22POINT2(
80 | "22.2",
81 | listOf(
82 | FL,
83 | FR,
84 | FC,
85 | LFE,
86 | BL,
87 | BR,
88 | FLC,
89 | FRC,
90 | BC,
91 | SL,
92 | SR,
93 | TC,
94 | TFL,
95 | TFC,
96 | TFR,
97 | TBL,
98 | TBC,
99 | TBR,
100 | LFE2,
101 | TSL,
102 | TSR,
103 | BFC,
104 | BFL,
105 | BFR,
106 | ),
107 | ),
108 | ;
109 |
110 | companion object {
111 | fun defaultChannelLayout(numChannels: Int) = entries.firstOrNull { it.channels.size == numChannels }
112 | fun getByNameOrNull(layoutName: String) = entries.firstOrNull { it.layoutName == layoutName }
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/GenericVideoEncode.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.model.profile
6 |
7 | import se.svt.oss.encore.model.input.DEFAULT_VIDEO_LABEL
8 |
9 | data class GenericVideoEncode(
10 | override val width: Int?,
11 | override val height: Int?,
12 | override val twoPass: Boolean,
13 | override val params: LinkedHashMap,
14 | override val filters: List = emptyList(),
15 | override val audioEncode: AudioEncoder?,
16 | override val audioEncodes: List = emptyList(),
17 | override val suffix: String,
18 | override val format: String,
19 | override val codec: String,
20 | override val inputLabel: String = DEFAULT_VIDEO_LABEL,
21 | ) : VideoEncode
22 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/OutputProducer.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.model.profile
6 |
7 | import com.fasterxml.jackson.annotation.JsonSubTypes
8 | import com.fasterxml.jackson.annotation.JsonTypeInfo
9 | import se.svt.oss.encore.config.EncodingProperties
10 | import se.svt.oss.encore.model.EncoreJob
11 | import se.svt.oss.encore.model.output.Output
12 |
13 | @JsonTypeInfo(use = JsonTypeInfo.Id.NAME, property = "type")
14 | @JsonSubTypes(
15 | JsonSubTypes.Type(value = AudioEncode::class, name = "AudioEncode"),
16 | JsonSubTypes.Type(value = SimpleAudioEncode::class, name = "SimpleAudioEncode"),
17 | JsonSubTypes.Type(value = X264Encode::class, name = "X264Encode"),
18 | JsonSubTypes.Type(value = X265Encode::class, name = "X265Encode"),
19 | JsonSubTypes.Type(value = GenericVideoEncode::class, name = "VideoEncode"),
20 | JsonSubTypes.Type(value = ThumbnailEncode::class, name = "ThumbnailEncode"),
21 | JsonSubTypes.Type(value = ThumbnailMapEncode::class, name = "ThumbnailMapEncode"),
22 | )
23 | interface OutputProducer {
24 | fun getOutput(job: EncoreJob, encodingProperties: EncodingProperties): Output?
25 | }
26 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/Profile.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.model.profile
6 |
7 | data class Profile(
8 | val name: String,
9 | val description: String,
10 | val encodes: List,
11 | val scaling: String? = "bicubic",
12 | val deinterlaceFilter: String = "yadif",
13 | val joinSegmentParams: LinkedHashMap = linkedMapOf(),
14 | )
15 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/SimpleAudioEncode.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.model.profile
6 |
7 | import se.svt.oss.encore.config.EncodingProperties
8 | import se.svt.oss.encore.model.EncoreJob
9 | import se.svt.oss.encore.model.input.DEFAULT_AUDIO_LABEL
10 | import se.svt.oss.encore.model.input.analyzedAudio
11 | import se.svt.oss.encore.model.mediafile.toParams
12 | import se.svt.oss.encore.model.output.AudioStreamEncode
13 | import se.svt.oss.encore.model.output.Output
14 |
15 | data class SimpleAudioEncode(
16 | val codec: String = "libfdk_aac",
17 | val bitrate: String? = null,
18 | val samplerate: Int? = null,
19 | val suffix: String = "_$codec",
20 | val params: LinkedHashMap = linkedMapOf(),
21 | override val optional: Boolean = false,
22 | val format: String = "mp4",
23 | val inputLabel: String = DEFAULT_AUDIO_LABEL,
24 | ) : AudioEncoder() {
25 | override fun getOutput(job: EncoreJob, encodingProperties: EncodingProperties): Output? {
26 | val outputName = "${job.baseName}$suffix.$format"
27 | job.inputs.analyzedAudio(inputLabel)
28 | ?: return logOrThrow("Can not generate $outputName! No audio input with label '$inputLabel'.")
29 | val outParams = linkedMapOf()
30 | outParams += params
31 | outParams["c:a"] = codec
32 | samplerate?.let { outParams["ar"] = it }
33 | bitrate?.let { outParams["b:a"] = it }
34 |
35 | return Output(
36 | id = "$suffix.$format",
37 | video = null,
38 | audioStreams = listOf(
39 | AudioStreamEncode(
40 | params = outParams.toParams(),
41 | inputLabels = listOf(inputLabel),
42 | preserveLayout = true,
43 | ),
44 | ),
45 | output = outputName,
46 | )
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/ThumbnailEncode.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.model.profile
6 |
7 | import io.github.oshai.kotlinlogging.KotlinLogging
8 | import se.svt.oss.encore.config.EncodingProperties
9 | import se.svt.oss.encore.model.EncoreJob
10 | import se.svt.oss.encore.model.input.DEFAULT_VIDEO_LABEL
11 | import se.svt.oss.encore.model.input.VideoIn
12 | import se.svt.oss.encore.model.input.videoInput
13 | import se.svt.oss.encore.model.mediafile.toParams
14 | import se.svt.oss.encore.model.output.Output
15 | import se.svt.oss.encore.model.output.VideoStreamEncode
16 |
17 | private val log = KotlinLogging.logger { }
18 |
19 | data class ThumbnailEncode(
20 | val percentages: List = listOf(25, 50, 75),
21 | val thumbnailWidth: Int = -2,
22 | val thumbnailHeight: Int = 1080,
23 | val quality: Int = 5,
24 | val suffix: String = "_thumb",
25 | val suffixZeroPad: Int = 2,
26 | val inputLabel: String = DEFAULT_VIDEO_LABEL,
27 | val optional: Boolean = false,
28 | val intervalSeconds: Double? = null,
29 | val decodeOutput: Int? = null,
30 | ) : OutputProducer {
31 |
32 | override fun getOutput(job: EncoreJob, encodingProperties: EncodingProperties): Output? {
33 | if (job.segmentLength != null) {
34 | return logOrThrow("Thumbnail is not supported in segmented encode!")
35 | }
36 | val videoInput = job.inputs.videoInput(inputLabel)
37 | ?: return logOrThrow("Can not produce thumbnail $suffix. No video input with label $inputLabel!")
38 | val thumbnailTime = job.thumbnailTime?.let { time ->
39 | videoInput.seekTo?.let { time - it } ?: time
40 | }
41 | val select = when {
42 | thumbnailTime != null -> selectTimes(listOf(thumbnailTime))
43 | intervalSeconds != null -> selectInterval(intervalSeconds, job.seekTo)
44 | outputDuration(videoInput, job) <= 0 -> return logOrThrow("Can not produce thumbnail $suffix. Could not detect duration.")
45 | percentages.isNotEmpty() -> selectTimes(percentagesToTimes(videoInput, job))
46 | else -> return logOrThrow("Can not produce thumbnail $suffix. No times selected.")
47 | }
48 |
49 | val filter = "$select,scale=w=$thumbnailWidth:h=$thumbnailHeight:out_range=jpeg"
50 | val params = linkedMapOf(
51 | "fps_mode" to "vfr",
52 | "q:v" to "$quality",
53 | )
54 |
55 | val fileRegex = Regex("${job.baseName}$suffix\\d{$suffixZeroPad}\\.jpg")
56 |
57 | return Output(
58 | id = "${suffix}0${suffixZeroPad}d.jpg",
59 | video = VideoStreamEncode(
60 | params = params.toParams(),
61 | filter = filter,
62 | inputLabels = listOf(inputLabel),
63 | ),
64 | output = "${job.baseName}$suffix%0${suffixZeroPad}d.jpg",
65 | postProcessor = { outputFolder ->
66 | outputFolder.listFiles().orEmpty().filter { it.name.matches(fileRegex) }
67 | },
68 | isImage = true,
69 | decodeOutputStream = decodeOutput?.let { "$it:v:0" },
70 | )
71 | }
72 |
73 | private fun selectInterval(interval: Double, outputSeek: Double?): String {
74 | val select = if (outputSeek != null && decodeOutput == null) {
75 | "gte(t\\,$outputSeek)*(isnan(prev_selected_t)+gt(floor((t-$outputSeek)/$interval)\\,floor((prev_selected_t-$outputSeek)/$interval)))"
76 | } else {
77 | "isnan(prev_selected_t)+gt(floor(t/$interval)\\,floor(prev_selected_t/$interval))"
78 | }
79 | return "select=$select"
80 | }
81 |
82 | private fun outputDuration(videoIn: VideoIn, job: EncoreJob): Double {
83 | val videoStream = videoIn.analyzedVideo.highestBitrateVideoStream
84 | var inputDuration = videoStream.duration
85 | videoIn.seekTo?.let { inputDuration -= it }
86 | job.seekTo?.let { inputDuration -= it }
87 | return job.duration ?: inputDuration
88 | }
89 |
90 | private fun percentagesToTimes(videoIn: VideoIn, job: EncoreJob): List {
91 | val outputDuration = outputDuration(videoIn, job)
92 | return percentages
93 | .map { it * outputDuration / 100 }
94 | .map { t ->
95 | val outputSeek = job.seekTo
96 | if (outputSeek != null && decodeOutput == null) {
97 | t + outputSeek
98 | } else {
99 | t
100 | }
101 | }
102 | }
103 |
104 | private fun selectTimes(times: List) =
105 | "select=${times.joinToString("+") { "(isnan(prev_pts)+lt(prev_pts*TB\\,$it))*gte(pts*TB\\,$it)" }}"
106 |
107 | private fun logOrThrow(message: String): Output? {
108 | if (optional) {
109 | log.info { message }
110 | return null
111 | } else {
112 | throw RuntimeException(message)
113 | }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/ThumbnailMapEncode.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.model.profile
6 |
7 | import io.github.oshai.kotlinlogging.KotlinLogging
8 | import org.apache.commons.math3.fraction.Fraction
9 | import se.svt.oss.encore.config.EncodingProperties
10 | import se.svt.oss.encore.model.EncoreJob
11 | import se.svt.oss.encore.model.input.DEFAULT_VIDEO_LABEL
12 | import se.svt.oss.encore.model.input.analyzedVideo
13 | import se.svt.oss.encore.model.input.videoInput
14 | import se.svt.oss.encore.model.mediafile.toParams
15 | import se.svt.oss.encore.model.output.Output
16 | import se.svt.oss.encore.model.output.VideoStreamEncode
17 | import se.svt.oss.encore.process.createTempDir
18 | import se.svt.oss.mediaanalyzer.file.stringValue
19 |
20 | private val log = KotlinLogging.logger { }
21 |
22 | data class ThumbnailMapEncode(
23 | val tileWidth: Int = 160,
24 | val tileHeight: Int = 90,
25 | val cols: Int = 12,
26 | val rows: Int = 20,
27 | val quality: Int = 5,
28 | val optional: Boolean = true,
29 | val suffix: String = "_${cols}x${rows}_${tileWidth}x${tileHeight}_thumbnail_map",
30 | val format: String = "jpg",
31 | val inputLabel: String = DEFAULT_VIDEO_LABEL,
32 | val decodeOutput: Int? = null,
33 | ) : OutputProducer {
34 |
35 | override fun getOutput(job: EncoreJob, encodingProperties: EncodingProperties): Output? {
36 | if (job.segmentLength != null) {
37 | return logOrThrow("Thumbnail map is not supported in segmented encode!")
38 | }
39 | val videoInput = job.inputs.videoInput(inputLabel)
40 | val inputSeekTo = videoInput?.seekTo
41 | val videoStream = job.inputs.analyzedVideo(inputLabel)?.highestBitrateVideoStream
42 | ?: return logOrThrow("No input with label $inputLabel!")
43 |
44 | var inputDuration = videoStream.duration
45 | val outputSeek = job.seekTo
46 | inputSeekTo?.let { inputDuration -= it }
47 | outputSeek?.let { inputDuration -= it }
48 | val outputDuration = job.duration ?: inputDuration
49 |
50 | if (outputDuration <= 0) {
51 | return logOrThrow("Cannot create thumbnail map $suffix! Could not detect duration.")
52 | }
53 |
54 | val interval = outputDuration / (cols * rows)
55 | val select = if (outputSeek != null && decodeOutput == null) {
56 | "gte(t\\,$outputSeek)*(isnan(prev_selected_t)+gt(floor((t-$outputSeek)/$interval)\\,floor((prev_selected_t-$outputSeek)/$interval)))"
57 | } else {
58 | "isnan(prev_selected_t)+gt(floor(t/$interval)\\,floor(prev_selected_t/$interval))"
59 | }
60 |
61 | val tempFolder = createTempDir(suffix).toFile()
62 | tempFolder.deleteOnExit()
63 |
64 | val pad = "aspect=${Fraction(tileWidth, tileHeight).stringValue()}:x=(ow-iw)/2:y=(oh-ih)/2"
65 |
66 | val scale = "-1:$tileHeight"
67 |
68 | val params = linkedMapOf(
69 | "fps_mode" to "vfr",
70 | )
71 | return Output(
72 | id = "$suffix.$format",
73 | video = VideoStreamEncode(
74 | params = params.toParams(),
75 | filter = "select=$select,pad=$pad,scale=$scale",
76 | inputLabels = listOf(inputLabel),
77 | ),
78 | output = tempFolder.resolve("${job.baseName}$suffix%04d.png").toString(),
79 | postProcessor = { outputFolder ->
80 | try {
81 | val targetFile = outputFolder.resolve("${job.baseName}$suffix.$format")
82 | val process = ProcessBuilder(
83 | "ffmpeg",
84 | "-y",
85 | "-i",
86 | "${job.baseName}$suffix%04d.png",
87 | "-vf",
88 | "tile=${cols}x$rows",
89 | "-frames:v",
90 | "1",
91 | "-q:v",
92 | "$quality",
93 | "$targetFile",
94 | )
95 | .directory(tempFolder)
96 | .start()
97 | val status = process.waitFor()
98 | tempFolder.deleteRecursively()
99 | if (status != 0) {
100 | throw RuntimeException("Ffmpeg returned status code $status")
101 | }
102 | listOf(targetFile)
103 | } catch (e: Exception) {
104 | logOrThrow("Error creating thumbnail map! ${e.message}")
105 | emptyList()
106 | }
107 | },
108 | isImage = true,
109 | decodeOutputStream = decodeOutput?.let { "$it:v:0" },
110 | )
111 | }
112 |
113 | private fun logOrThrow(message: String): Output? {
114 | if (optional) {
115 | log.info { message }
116 | return null
117 | } else {
118 | throw RuntimeException(message)
119 | }
120 | }
121 | }
122 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/VideoEncode.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.model.profile
6 |
7 | import org.apache.commons.math3.fraction.Fraction
8 | import se.svt.oss.encore.config.EncodingProperties
9 | import se.svt.oss.encore.model.EncoreJob
10 | import se.svt.oss.encore.model.input.VideoIn
11 | import se.svt.oss.encore.model.input.videoInput
12 | import se.svt.oss.encore.model.mediafile.toParams
13 | import se.svt.oss.encore.model.output.Output
14 | import se.svt.oss.encore.model.output.VideoStreamEncode
15 | import se.svt.oss.mediaanalyzer.file.toFractionOrNull
16 | import kotlin.math.absoluteValue
17 |
18 | interface VideoEncode : OutputProducer {
19 | val width: Int?
20 | val height: Int?
21 | val twoPass: Boolean
22 | val params: Map
23 | val filters: List?
24 | val audioEncode: AudioEncoder?
25 | val audioEncodes: List
26 | val suffix: String
27 | val format: String
28 | val codec: String
29 | val inputLabel: String
30 |
31 | override fun getOutput(job: EncoreJob, encodingProperties: EncodingProperties): Output? {
32 | val audioEncodesToUse = audioEncodes.ifEmpty { listOfNotNull(audioEncode) }
33 | val audio = audioEncodesToUse.flatMap { it.getOutput(job, encodingProperties)?.audioStreams.orEmpty() }
34 | val videoInput = job.inputs.videoInput(inputLabel)
35 | ?: throw RuntimeException("No valid video input with label $inputLabel!")
36 | return Output(
37 | id = "$suffix.$format",
38 | video = VideoStreamEncode(
39 | params = secondPassParams().toParams(),
40 | firstPassParams = firstPassParams().toParams(),
41 | inputLabels = listOf(inputLabel),
42 | twoPass = twoPass,
43 | filter = videoFilter(job.debugOverlay, encodingProperties, videoInput),
44 | ),
45 | audioStreams = audio,
46 | output = "${job.baseName}$suffix.$format",
47 | )
48 | }
49 |
50 | fun firstPassParams(): Map = if (!twoPass) {
51 | emptyMap()
52 | } else {
53 | params + Pair("c:v", codec) + passParams(1)
54 | }
55 |
56 | fun secondPassParams(): Map = if (!twoPass) {
57 | params + Pair("c:v", codec)
58 | } else {
59 | params + Pair("c:v", codec) + passParams(2)
60 | }
61 |
62 | fun passParams(pass: Int): Map =
63 | mapOf("pass" to pass.toString(), "passlogfile" to "log$suffix")
64 |
65 | fun videoFilter(
66 | debugOverlay: Boolean,
67 | encodingProperties: EncodingProperties,
68 | videoInput: VideoIn,
69 | ): String? {
70 | val videoFilters = mutableListOf()
71 | var scaleToWidth = width
72 | var scaleToHeight = height
73 | val videoStream = videoInput.analyzedVideo.highestBitrateVideoStream
74 | val isRotated90 = videoStream.rotation?.rem(180)?.absoluteValue == 90
75 | val outputDar = (videoInput.padTo ?: videoInput.cropTo)?.toFractionOrNull()
76 | ?: (videoStream.displayAspectRatio?.toFractionOrNull() ?: Fraction(videoStream.width, videoStream.height))
77 | .let { if (isRotated90) it.reciprocal() else it }
78 | val outputIsPortrait = outputDar < Fraction.ONE
79 | val isScalingWithinLandscape =
80 | scaleToWidth != null && scaleToHeight != null && Fraction(scaleToWidth, scaleToHeight) > Fraction.ONE
81 | if (encodingProperties.flipWidthHeightIfPortrait && outputIsPortrait && isScalingWithinLandscape) {
82 | scaleToWidth = height
83 | scaleToHeight = width
84 | }
85 | if (scaleToWidth != null && scaleToHeight != null) {
86 | videoFilters.add("scale=$scaleToWidth:$scaleToHeight:force_original_aspect_ratio=decrease:force_divisible_by=2")
87 | videoFilters.add("setsar=1/1")
88 | } else if (scaleToWidth != null || scaleToHeight != null) {
89 | videoFilters.add("scale=${scaleToWidth ?: -2}:${scaleToHeight ?: -2}")
90 | }
91 | filters?.let { videoFilters.addAll(it) }
92 | if (debugOverlay) {
93 | videoFilters.add("drawtext=text=$suffix:fontcolor=white:fontsize=50:box=1:boxcolor=black@0.75:boxborderw=5:x=10:y=10")
94 | }
95 | return if (videoFilters.isEmpty()) null else videoFilters.joinToString(",")
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/X264Encode.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.model.profile
6 |
7 | import com.fasterxml.jackson.annotation.JsonProperty
8 | import se.svt.oss.encore.model.input.DEFAULT_VIDEO_LABEL
9 |
10 | data class X264Encode(
11 | override val width: Int?,
12 | override val height: Int?,
13 | override val twoPass: Boolean,
14 | @JsonProperty("params")
15 | override val ffmpegParams: LinkedHashMap = linkedMapOf(),
16 | @JsonProperty("x264-params")
17 | override val codecParams: LinkedHashMap = linkedMapOf(),
18 | override val filters: List = emptyList(),
19 | override val audioEncode: AudioEncoder? = null,
20 | override val audioEncodes: List = emptyList(),
21 | override val suffix: String,
22 | override val format: String = "mp4",
23 | override val inputLabel: String = DEFAULT_VIDEO_LABEL,
24 | ) : X26XEncode() {
25 | override val codecParamName: String
26 | get() = "x264-params"
27 | override val codec: String
28 | get() = "libx264"
29 | }
30 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/X265Encode.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.model.profile
6 |
7 | import com.fasterxml.jackson.annotation.JsonProperty
8 | import se.svt.oss.encore.model.input.DEFAULT_VIDEO_LABEL
9 |
10 | data class X265Encode(
11 | override val width: Int?,
12 | override val height: Int?,
13 | override val twoPass: Boolean,
14 | @JsonProperty("params")
15 | override val ffmpegParams: LinkedHashMap = linkedMapOf(),
16 | @JsonProperty("x265-params")
17 | override val codecParams: LinkedHashMap = linkedMapOf(),
18 | override val filters: List = emptyList(),
19 | override val audioEncode: AudioEncoder? = null,
20 | override val audioEncodes: List = emptyList(),
21 | override val suffix: String,
22 | override val format: String = "mp4",
23 | override val inputLabel: String = DEFAULT_VIDEO_LABEL,
24 | ) : X26XEncode() {
25 | override val codecParamName: String
26 | get() = "x265-params"
27 | override val codec: String
28 | get() = "libx265"
29 | }
30 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/model/profile/X26XEncode.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.model.profile
6 |
7 | abstract class X26XEncode : VideoEncode {
8 |
9 | abstract val ffmpegParams: LinkedHashMap
10 |
11 | abstract val codecParams: LinkedHashMap
12 | abstract val codecParamName: String
13 |
14 | override val params: Map
15 | get() = ffmpegParams + if (codecParams.isNotEmpty()) {
16 | mapOf(codecParamName to codecParams.map { "${it.key}=${it.value}" }.joinToString(":"))
17 | } else {
18 | emptyMap()
19 | }
20 |
21 | override fun passParams(pass: Int): Map {
22 | val modifiedCodecParams = (codecParams + mapOf("pass" to pass.toString(), "stats" to "log$suffix"))
23 | .map { "${it.key}=${it.value}" }
24 | .joinToString(":")
25 | return mapOf(codecParamName to modifiedCodecParams)
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/model/queue/QueueItem.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.model.queue
6 |
7 | import com.fasterxml.jackson.annotation.JsonPropertyOrder
8 | import com.fasterxml.jackson.annotation.JsonTypeInfo
9 | import java.time.LocalDateTime
10 |
11 | @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, include = JsonTypeInfo.As.PROPERTY)
12 | @JsonPropertyOrder("created", "id", "segment")
13 | data class QueueItem(
14 | val id: String,
15 | val priority: Int = 0,
16 | val created: LocalDateTime = LocalDateTime.now(),
17 | val segment: Int? = null,
18 | )
19 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/process/SegmentUtil.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.process
6 |
7 | import se.svt.oss.encore.model.EncoreJob
8 | import se.svt.oss.mediaanalyzer.file.MediaContainer
9 | import kotlin.math.ceil
10 |
11 | fun EncoreJob.segmentLengthOrThrow() = segmentLength ?: throw RuntimeException("No segmentLength in job!")
12 |
13 | fun EncoreJob.numSegments(): Int {
14 | val segLen = segmentLengthOrThrow()
15 | val readDuration = duration
16 | return if (readDuration != null) {
17 | ceil(readDuration / segLen).toInt()
18 | } else {
19 | val segments =
20 | inputs.map { ceil(((it.analyzed as MediaContainer).duration - (it.seekTo ?: 0.0)) / segLen).toInt() }.toSet()
21 | if (segments.size > 1) {
22 | throw RuntimeException("Inputs differ in length")
23 | }
24 | segments.first()
25 | }
26 | }
27 |
28 | fun EncoreJob.segmentDuration(segmentNumber: Int): Double = when {
29 | duration == null -> segmentLengthOrThrow()
30 | segmentNumber < numSegments() - 1 -> segmentLengthOrThrow()
31 | else -> duration!! % segmentLengthOrThrow()
32 | }
33 |
34 | fun EncoreJob.baseName(segmentNumber: Int) = "${baseName}_%05d".format(segmentNumber)
35 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/process/TempDir.kt:
--------------------------------------------------------------------------------
1 | package se.svt.oss.encore.process
2 |
3 | import java.nio.file.Path
4 | import kotlin.io.path.createTempDirectory
5 |
6 | fun createTempDir(prefix: String): Path {
7 | val tmpdir = System.getenv("ENCORE_TMPDIR")
8 | ?: System.getProperty("java.io.tmpdir")
9 | return createTempDirectory(tmpdir?.let { Path.of(it) }, prefix)
10 | }
11 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/repository/ChannelLayoutConverters.kt:
--------------------------------------------------------------------------------
1 | package se.svt.oss.encore.repository
2 |
3 | import org.springframework.boot.context.properties.ConfigurationPropertiesBinding
4 | import org.springframework.core.convert.converter.Converter
5 | import org.springframework.data.convert.ReadingConverter
6 | import org.springframework.data.convert.WritingConverter
7 | import org.springframework.stereotype.Component
8 | import se.svt.oss.encore.model.profile.ChannelLayout
9 |
10 | @Component
11 | @ConfigurationPropertiesBinding
12 | class StringToChannelLayoutConverter : Converter {
13 | override fun convert(source: String): ChannelLayout =
14 | ChannelLayout.getByNameOrNull(source)
15 | ?: throw IllegalArgumentException("$source is not a valid channel layout. Valid values: ${ChannelLayout.entries.map { it.layoutName }}")
16 | }
17 |
18 | @ReadingConverter
19 | class ByteArrayToChannelLayoutConverter : Converter {
20 | override fun convert(source: ByteArray): ChannelLayout =
21 | ChannelLayout.getByNameOrNull(String(source))
22 | ?: throw IllegalArgumentException("${String(source)} is not a valid channel layout. Valid values: ${ChannelLayout.entries.map { it.layoutName }}")
23 | }
24 |
25 | @WritingConverter
26 | class ChannelLayoutToByteArrayConverter : Converter {
27 | override fun convert(source: ChannelLayout): ByteArray =
28 | source.layoutName.toByteArray()
29 | }
30 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/repository/EncoreJobRepository.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.repository
6 |
7 | import io.swagger.v3.oas.annotations.Operation
8 | import io.swagger.v3.oas.annotations.tags.Tag
9 | import org.springframework.data.domain.Page
10 | import org.springframework.data.domain.Pageable
11 | import org.springframework.data.repository.CrudRepository
12 | import org.springframework.data.repository.PagingAndSortingRepository
13 | import org.springframework.data.rest.core.annotation.RepositoryRestResource
14 | import se.svt.oss.encore.model.EncoreJob
15 | import se.svt.oss.encore.model.Status
16 | import java.util.UUID
17 |
18 | @RepositoryRestResource
19 | @Tag(name = "encorejob")
20 | interface EncoreJobRepository :
21 | PagingAndSortingRepository,
22 | CrudRepository {
23 |
24 | @Operation(summary = "Find EncoreJobs By Status", description = "Returns EncoreJobs according to the given Status")
25 | fun findByStatus(status: Status, pageable: Pageable): Page
26 | }
27 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/repository/OffsetDateTimeConverters.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.repository
6 |
7 | import org.springframework.core.convert.converter.Converter
8 | import org.springframework.data.convert.ReadingConverter
9 | import org.springframework.data.convert.WritingConverter
10 | import java.time.OffsetDateTime
11 |
12 | @WritingConverter
13 | class OffsetDateTimeToByteArrayConverter : Converter {
14 | override fun convert(source: OffsetDateTime): ByteArray? = source.toString().toByteArray()
15 | }
16 |
17 | @ReadingConverter
18 | class ByteArrayToOffsetDateTimeConverter : Converter {
19 | override fun convert(source: ByteArray): OffsetDateTime? = OffsetDateTime.parse(String(source))
20 | }
21 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/service/ApplicationShutdownException.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2025 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.service
6 |
7 | class ApplicationShutdownException : RuntimeException("Application was shutdown")
8 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/service/ShutdownHandler.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2025 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.service
6 |
7 | import io.github.oshai.kotlinlogging.KotlinLogging
8 | import jakarta.annotation.PostConstruct
9 | import org.springframework.context.ApplicationListener
10 | import org.springframework.context.event.ContextClosedEvent
11 | import org.springframework.stereotype.Component
12 | import java.util.concurrent.atomic.AtomicBoolean
13 |
14 | private val log = KotlinLogging.logger {}
15 |
16 | @Component
17 | class ShutdownHandler : ApplicationListener {
18 | companion object {
19 | private val isShutDown = AtomicBoolean(false)
20 | fun isShutDown() = isShutDown.get()
21 | fun checkShutdown() {
22 | if (isShutDown()) {
23 | throw ApplicationShutdownException()
24 | }
25 | }
26 | }
27 |
28 | @PostConstruct
29 | fun addHook() {
30 | Runtime.getRuntime().addShutdownHook(Thread { isShutDown.set(true) })
31 | }
32 |
33 | override fun onApplicationEvent(event: ContextClosedEvent) {
34 | if (isShutDown()) {
35 | log.info { "Delaying application shutdown" }
36 | Thread.sleep(6000)
37 | log.info { "Continue application shutdown" }
38 | }
39 | }
40 |
41 | override fun supportsAsyncExecution() = false
42 | }
43 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/service/callback/CallbackClient.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.service.callback
6 |
7 | import org.springframework.http.MediaType
8 | import org.springframework.web.bind.annotation.RequestBody
9 | import org.springframework.web.service.annotation.HttpExchange
10 | import org.springframework.web.service.annotation.PostExchange
11 | import se.svt.oss.encore.model.callback.JobProgress
12 | import java.net.URI
13 |
14 | @HttpExchange(contentType = MediaType.APPLICATION_JSON_VALUE)
15 | interface CallbackClient {
16 |
17 | @PostExchange
18 | fun sendProgressCallback(callbackUri: URI, @RequestBody progress: JobProgress)
19 | }
20 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/service/callback/CallbackService.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.service.callback
6 |
7 | import io.github.oshai.kotlinlogging.KotlinLogging
8 | import org.springframework.stereotype.Service
9 | import se.svt.oss.encore.model.EncoreJob
10 | import se.svt.oss.encore.model.callback.JobProgress
11 | import java.net.URI
12 |
13 | private val log = KotlinLogging.logger {}
14 |
15 | @Service
16 | class CallbackService(private val callbackClient: CallbackClient) {
17 |
18 | fun sendProgressCallback(encoreJob: EncoreJob) {
19 | encoreJob.progressCallbackUri?.let {
20 | try {
21 | callbackClient.sendProgressCallback(
22 | URI.create(it),
23 | JobProgress(
24 | encoreJob.id,
25 | encoreJob.externalId,
26 | encoreJob.progress,
27 | encoreJob.status,
28 | ),
29 | )
30 | } catch (e: Exception) {
31 | log.debug(e) { "Sending progress callback failed" }
32 | }
33 | }
34 | }
35 | }
36 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/service/localencode/LocalEncodeService.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.service.localencode
6 |
7 | import io.github.oshai.kotlinlogging.KotlinLogging
8 | import org.springframework.stereotype.Service
9 | import se.svt.oss.encore.config.EncoreProperties
10 | import se.svt.oss.encore.model.EncoreJob
11 | import se.svt.oss.encore.process.createTempDir
12 | import se.svt.oss.mediaanalyzer.file.AudioFile
13 | import se.svt.oss.mediaanalyzer.file.ImageFile
14 | import se.svt.oss.mediaanalyzer.file.MediaFile
15 | import se.svt.oss.mediaanalyzer.file.VideoFile
16 | import java.io.File
17 | import java.nio.file.Files
18 | import java.nio.file.Path
19 | import java.nio.file.StandardCopyOption
20 |
21 | private val log = KotlinLogging.logger {}
22 |
23 | @Service
24 | class LocalEncodeService(
25 | private val encoreProperties: EncoreProperties,
26 | ) {
27 |
28 | fun outputFolder(
29 | encoreJob: EncoreJob,
30 | ): String = if (encoreProperties.localTemporaryEncode) {
31 | createTempDir("job_${encoreJob.id}").toString()
32 | } else {
33 | encoreJob.outputFolder
34 | }
35 |
36 | fun localEncodedFilesToCorrectDir(
37 | outputFolder: String,
38 | output: List,
39 | encoreJob: EncoreJob,
40 | ): List {
41 | if (encoreProperties.localTemporaryEncode) {
42 | val destination = File(encoreJob.outputFolder)
43 | log.debug { "Moving files to correct outputFolder ${encoreJob.outputFolder}, from local temp $outputFolder" }
44 | if (!destination.exists()) destination.mkdirs()
45 | moveTempLocalFiles(destination, outputFolder)
46 | val files = resolveMovedOutputFiles(output, encoreJob)
47 | log.debug { "Locally encoded files have been successfully moved to output folder." }
48 | return files
49 | }
50 | return output
51 | }
52 |
53 | fun cleanup(tempDirectory: String?) {
54 | if (tempDirectory != null && encoreProperties.localTemporaryEncode) {
55 | File(tempDirectory).deleteRecursively()
56 | }
57 | }
58 |
59 | private fun moveTempLocalFiles(destination: File, tempDirectory: String) {
60 | File(tempDirectory).listFiles()?.forEach { moveFile(it, destination) }
61 | }
62 |
63 | private fun moveFile(file: File, destination: File) {
64 | try {
65 | executeMoveFile(file, destination)
66 | } catch (e: Exception) {
67 | log.debug { "Error when moving file ${file.path}. Trying again. message= ${e.message}" }
68 | executeMoveFile(file, destination)
69 | }
70 | }
71 |
72 | private fun executeMoveFile(file: File, destination: File) {
73 | log.debug { "Moving file ${file.path}" }
74 | Files.move(file.toPath(), destination.resolve(file.name).toPath(), StandardCopyOption.REPLACE_EXISTING)
75 | }
76 |
77 | private fun resolveMovedOutputFiles(output: List, encoreJob: EncoreJob): List = output.map { file ->
78 | when (file) {
79 | is VideoFile -> file.copy(file = resolvePath(file, encoreJob))
80 | is AudioFile -> file.copy(file = resolvePath(file, encoreJob))
81 | is ImageFile -> file.copy(file = resolvePath(file, encoreJob))
82 | else -> throw Exception("Invalid conversion")
83 | }
84 | }
85 |
86 | private fun resolvePath(file: MediaFile, encoreJob: EncoreJob): String {
87 | val path = Path.of(file.file)
88 | return path.resolveSibling("${encoreJob.outputFolder}/${path.fileName}").toString()
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/service/mediaanalyzer/MediaAnalyzerService.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.service.mediaanalyzer
6 |
7 | import io.github.oshai.kotlinlogging.KotlinLogging
8 | import org.springframework.aot.hint.annotation.RegisterReflectionForBinding
9 | import org.springframework.stereotype.Service
10 | import se.svt.oss.encore.model.input.AudioIn
11 | import se.svt.oss.encore.model.input.Input
12 | import se.svt.oss.encore.model.input.VideoIn
13 | import se.svt.oss.encore.model.mediafile.selectAudioStream
14 | import se.svt.oss.encore.model.mediafile.selectVideoStream
15 | import se.svt.oss.encore.model.mediafile.trimAudio
16 | import se.svt.oss.mediaanalyzer.MediaAnalyzer
17 | import se.svt.oss.mediaanalyzer.ffprobe.DisplayMatrix
18 | import se.svt.oss.mediaanalyzer.ffprobe.FfAudioStream
19 | import se.svt.oss.mediaanalyzer.ffprobe.FfVideoStream
20 | import se.svt.oss.mediaanalyzer.ffprobe.ProbeResult
21 | import se.svt.oss.mediaanalyzer.ffprobe.SideData
22 | import se.svt.oss.mediaanalyzer.ffprobe.UnknownSideData
23 | import se.svt.oss.mediaanalyzer.ffprobe.UnknownStream
24 | import se.svt.oss.mediaanalyzer.file.AudioFile
25 | import se.svt.oss.mediaanalyzer.file.VideoFile
26 | import se.svt.oss.mediaanalyzer.mediainfo.AudioTrack
27 | import se.svt.oss.mediaanalyzer.mediainfo.GeneralTrack
28 | import se.svt.oss.mediaanalyzer.mediainfo.ImageTrack
29 | import se.svt.oss.mediaanalyzer.mediainfo.MediaInfo
30 | import se.svt.oss.mediaanalyzer.mediainfo.OtherTrack
31 | import se.svt.oss.mediaanalyzer.mediainfo.TextTrack
32 | import se.svt.oss.mediaanalyzer.mediainfo.VideoTrack
33 |
34 | private val log = KotlinLogging.logger {}
35 |
36 | @Service
37 | @RegisterReflectionForBinding(
38 | MediaInfo::class,
39 | AudioTrack::class,
40 | GeneralTrack::class,
41 | ImageTrack::class,
42 | OtherTrack::class,
43 | TextTrack::class,
44 | VideoTrack::class,
45 | ProbeResult::class,
46 | FfAudioStream::class,
47 | FfVideoStream::class,
48 | UnknownStream::class,
49 | SideData::class,
50 | DisplayMatrix::class,
51 | UnknownSideData::class,
52 | )
53 | class MediaAnalyzerService(private val mediaAnalyzer: MediaAnalyzer) {
54 |
55 | fun analyzeInput(input: Input) {
56 | log.debug { "Analyzing input $input" }
57 | val probeInterlaced = input is VideoIn && input.probeInterlaced
58 | val useFirstAudioStreams = (input as? AudioIn)?.channelLayout?.channels?.size
59 |
60 | input.analyzed = mediaAnalyzer.analyze(
61 | file = input.uri,
62 | probeInterlaced = probeInterlaced,
63 | ffprobeInputParams = input.params,
64 | ).let {
65 | val selectedVideoStream = (input as? VideoIn)?.videoStream
66 | val selectedAudioStream = (input as? AudioIn)?.audioStream
67 | when (it) {
68 | is VideoFile -> it.selectVideoStream(selectedVideoStream)
69 | .selectAudioStream(selectedAudioStream)
70 | .trimAudio(useFirstAudioStreams)
71 | is AudioFile -> it.selectAudioStream(selectedAudioStream)
72 | .trimAudio(useFirstAudioStreams)
73 | else -> it
74 | }
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/encore-common/src/main/kotlin/se/svt/oss/encore/service/profile/ProfileService.kt:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: 2020 Sveriges Television AB
2 | //
3 | // SPDX-License-Identifier: EUPL-1.2
4 |
5 | package se.svt.oss.encore.service.profile
6 |
7 | import com.fasterxml.jackson.core.JsonProcessingException
8 | import com.fasterxml.jackson.databind.DeserializationFeature
9 | import com.fasterxml.jackson.databind.ObjectMapper
10 | import com.fasterxml.jackson.dataformat.yaml.YAMLMapper
11 | import com.fasterxml.jackson.module.kotlin.readValue
12 | import io.github.oshai.kotlinlogging.KotlinLogging
13 | import org.springframework.aot.hint.annotation.RegisterReflectionForBinding
14 | import org.springframework.boot.context.properties.EnableConfigurationProperties
15 | import org.springframework.expression.common.TemplateParserContext
16 | import org.springframework.expression.spel.SpelParserConfiguration
17 | import org.springframework.expression.spel.standard.SpelExpressionParser
18 | import org.springframework.expression.spel.support.SimpleEvaluationContext
19 | import org.springframework.stereotype.Service
20 | import se.svt.oss.encore.config.ProfileProperties
21 | import se.svt.oss.encore.model.EncoreJob
22 | import se.svt.oss.encore.model.profile.AudioEncode
23 | import se.svt.oss.encore.model.profile.ChannelLayout
24 | import se.svt.oss.encore.model.profile.GenericVideoEncode
25 | import se.svt.oss.encore.model.profile.OutputProducer
26 | import se.svt.oss.encore.model.profile.Profile
27 | import se.svt.oss.encore.model.profile.SimpleAudioEncode
28 | import se.svt.oss.encore.model.profile.ThumbnailEncode
29 | import se.svt.oss.encore.model.profile.ThumbnailMapEncode
30 | import se.svt.oss.encore.model.profile.X264Encode
31 | import se.svt.oss.encore.model.profile.X265Encode
32 | import java.io.File
33 | import java.util.Locale
34 |
35 | private val log = KotlinLogging.logger { }
36 |
37 | @Service
38 | @RegisterReflectionForBinding(
39 | Profile::class,
40 | OutputProducer::class,
41 | AudioEncode::class,
42 | SimpleAudioEncode::class,
43 | X264Encode::class,
44 | X265Encode::class,
45 | GenericVideoEncode::class,
46 | ThumbnailEncode::class,
47 | ThumbnailMapEncode::class,
48 | ChannelLayout::class,
49 | )
50 | @EnableConfigurationProperties(ProfileProperties::class)
51 | class ProfileService(
52 | private val properties: ProfileProperties,
53 | private val objectMapper: ObjectMapper,
54 | ) {
55 | private val yamlMapper: YAMLMapper =
56 | YAMLMapper()
57 | .findAndRegisterModules()
58 | .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) as YAMLMapper
59 |
60 | private val spelExpressionParser = SpelExpressionParser(
61 | SpelParserConfiguration(
62 | null,
63 | null,
64 | false,
65 | false,
66 | Int.MAX_VALUE,
67 | 100_000,
68 | ),
69 | )
70 |
71 | private val spelEvaluationContext = SimpleEvaluationContext
72 | .forReadOnlyDataBinding()
73 | .build()
74 |
75 | private val spelParserContext = TemplateParserContext(
76 | properties.spelExpressionPrefix,
77 | properties.spelExpressionSuffix,
78 | )
79 |
80 | private fun mapper() =
81 | if (properties.location.filename?.let {
82 | File(it).extension.lowercase(Locale.getDefault()) in setOf("yml", "yaml")
83 | } == true
84 | ) {
85 | yamlMapper
86 | } else {
87 | objectMapper
88 | }
89 |
90 | fun getProfile(job: EncoreJob): Profile = try {
91 | log.debug { "Get profile ${job.profile}. Reading profiles from ${properties.location}" }
92 | val profiles = mapper().readValue