Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions checks.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ jacocoTestCoverageVerification {
'*QueueService.migrateQueues()',
'*.ShutdownHandler.*',
'*FfmpegExecutor.runFfmpeg$lambda$?(java.lang.Process)',
'*FfmpegExecutorKt.getProgressRegex()',
'*FilterSettings.*',
'se.svt.oss.encore.service.EncoreService.handleProgress.1.1.emit(int, kotlin.coroutines.Continuation)',
]
limit {
counter = 'LINE'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@
package se.svt.oss.encore.config

import org.springframework.boot.context.properties.NestedConfigurationProperty
import se.svt.oss.encore.model.AudioEncodingMode
import se.svt.oss.encore.model.profile.ChannelLayout

data class SegmentedEncodingProperties(
val audioEncodingMode: AudioEncodingMode = AudioEncodingMode.ENCODE_WITH_VIDEO,
)

data class EncodingProperties(
@NestedConfigurationProperty
val audioMixPresets: Map<String, AudioMixPreset> = mapOf("default" to AudioMixPreset()),
Expand All @@ -15,4 +20,6 @@ data class EncodingProperties(
val flipWidthHeightIfPortrait: Boolean = true,
val exitOnError: Boolean = true,
val globalParams: LinkedHashMap<String, Any?> = linkedMapOf(),
@NestedConfigurationProperty
val segmentedEncoding: SegmentedEncodingProperties = SegmentedEncodingProperties(),
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// SPDX-FileCopyrightText: 2025 Eyevinn Technology AB
//
// SPDX-License-Identifier: EUPL-1.2

package se.svt.oss.encore.model

/**
* Defines how audio should be encoded when using segmented encoding.
*/
enum class AudioEncodingMode {
/**
* Encode audio and video together in the same segments.
* Creates N tasks of type AUDIOVIDEOSEGMENT.
*/
ENCODE_WITH_VIDEO,

/**
* Encode audio separately from video as a single full-length file (not segmented).
* Creates 1 AUDIOFULL task + N VIDEOSEGMENT tasks.
*/
ENCODE_SEPARATELY_FULL,

/**
* Encode audio separately from video, with both audio and video segmented.
* Creates N AUDIOSEGMENT tasks + N VIDEOSEGMENT tasks (2N total tasks).
*/
ENCODE_SEPARATELY_SEGMENTED,
}
72 changes: 72 additions & 0 deletions encore-common/src/main/kotlin/se/svt/oss/encore/model/EncoreJob.kt
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,55 @@ import se.svt.oss.mediaanalyzer.file.MediaFile
import java.time.OffsetDateTime
import java.util.UUID

data class SegmentedEncodingInfo(
@field:Schema(
description = "Length of each segment in seconds. Should be a multiple of target GOP.",
example = "19.2",
readOnly = true,
nullable = false,
)
val segmentLength: Double,
@field:Schema(
description = "Number of video segments",
nullable = false,
readOnly = true,
)
val numSegments: Int,
@field:Schema(
description = "Number of encoding tasks used for this job. This will be equal to numSegments plus numAudioSegments",
nullable = false,
readOnly = true,
)
val numTasks: Int,
@field:Schema(
description = "The audio encoding mode used for this job.",
example = "ENCODE_WITH_VIDEO",
nullable = false,
readOnly = true,
)
val audioEncodingMode: AudioEncodingMode,
@field:Schema(
description = "Audio segment padding in seconds (added at start/end of segments to avoid artifacts). Only relevant in ENCODE_SEPARATELY_SEGMENTED mode.",
example = "0.04267",
nullable = false,
readOnly = true,
)
val audioSegmentPadding: Double = 0.0,
@field:Schema(
description = "Length of each audio segment in seconds. Only relevant in ENCODE_SEPARATELY_SEGMENTED mode.",
example = "256.0",
nullable = false,
readOnly = true,
)
val audioSegmentLength: Double = 0.0,
@field:Schema(
description = "Number of audio segments",
nullable = false,
readOnly = true,
)
val numAudioSegments: Int,
)

@Validated
@RedisHash("encore-jobs", timeToLive = (60 * 60 * 24 * 7).toLong()) // 1 week ttl
@Tag(name = "encorejob")
Expand Down Expand Up @@ -107,6 +156,29 @@ data class EncoreJob(
@field:Positive
val segmentLength: Double? = null,

@field:Schema(
description = "Defines how audio should be encoded when using segmented encoding. ENCODE_WITH_VIDEO: audio and video together in segments; ENCODE_SEPARATELY_FULL: audio separately as full file; ENCODE_SEPARATELY_SEGMENTED: audio separately in segments.",
example = "ENCODE_WITH_VIDEO",
defaultValue = "ENCODE_WITH_VIDEO",
nullable = true,
)
val audioEncodingMode: AudioEncodingMode? = null,

@field:Schema(
description = "Length of audio segments in seconds when using ENCODE_SEPARATELY_SEGMENTED mode. If not specified, a value close to 256s will be calculated that is a multiple of the audio frame size.",
example = "256.0",
nullable = true,
)
@field:Positive
val audioSegmentLength: Double? = null,

@field:Schema(
description = "Properties for segmented encoding, or null if not used",
nullable = true,
readOnly = true,
)
var segmentedEncodingInfo: SegmentedEncodingInfo? = null,

@field:Schema(
description = "The exception message, if the EncoreJob failed",
example = "input/output error",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import se.svt.oss.encore.model.output.AudioStreamEncode
import se.svt.oss.encore.model.output.Output

data class AudioEncode(
val codec: String = "libfdk_aac",
override val codec: String = "libfdk_aac",
val bitrate: String? = null,
val samplerate: Int = 48000,
val channelLayout: ChannelLayout = ChannelLayout.CH_LAYOUT_STEREO,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ private val log = KotlinLogging.logger { }
abstract class AudioEncoder : OutputProducer {

abstract val optional: Boolean
abstract val enabled: Boolean
abstract val codec: String
abstract override val enabled: Boolean

fun logOrThrow(message: String): Output? {
if (optional || !enabled) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,6 @@ import se.svt.oss.encore.model.output.Output
JsonSubTypes.Type(value = ThumbnailMapEncode::class, name = "ThumbnailMapEncode"),
)
interface OutputProducer {
val enabled: Boolean
fun getOutput(job: EncoreJob, encodingProperties: EncodingProperties): Output?
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import se.svt.oss.encore.model.output.AudioStreamEncode
import se.svt.oss.encore.model.output.Output

data class SimpleAudioEncode(
val codec: String = "libfdk_aac",
override val codec: String = "libfdk_aac",
val bitrate: String? = null,
val samplerate: Int? = null,
val suffix: String = "_$codec",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ data class ThumbnailEncode(
val suffixZeroPad: Int = 2,
val inputLabel: String = DEFAULT_VIDEO_LABEL,
val optional: Boolean = false,
val enabled: Boolean = true,
override val enabled: Boolean = true,
val intervalSeconds: Double? = null,
val decodeOutput: Int? = null,
) : OutputProducer {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ data class ThumbnailMapEncode(
val rows: Int = 20,
val quality: Int = 5,
val optional: Boolean = true,
val enabled: Boolean = true,
override val enabled: Boolean = true,
val suffix: String = "_${cols}x${rows}_${tileWidth}x${tileHeight}_thumbnail_map",
val format: String = "jpg",
val inputLabel: String = DEFAULT_VIDEO_LABEL,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ interface VideoEncode : OutputProducer {
val codec: String
val inputLabel: String
val optional: Boolean
val enabled: Boolean
override val enabled: Boolean
val cropTo: FractionString?
val padTo: FractionString?

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,18 @@ data class QueueItem(
val id: String,
val priority: Int = 0,
val created: LocalDateTime = LocalDateTime.now(),
val segment: Int? = null,
val task: Task? = null,
)

enum class TaskType {
AUDIOVIDEOSEGMENT,
VIDEOSEGMENT,
AUDIOFULL,
AUDIOSEGMENT,
}

data class Task(
val type: TaskType,
val taskNo: Int,
val segment: Int,
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,30 @@
package se.svt.oss.encore.process

import se.svt.oss.encore.model.EncoreJob
import se.svt.oss.mediaanalyzer.file.MediaContainer
import kotlin.math.ceil

fun EncoreJob.segmentLengthOrThrow() = segmentLength ?: throw RuntimeException("No segmentLength in job!")

fun EncoreJob.numSegments(): Int {
val segLen = segmentLengthOrThrow()
val readDuration = duration
return if (readDuration != null) {
ceil(readDuration / segLen).toInt()
} else {
val segments =
inputs.map { ceil(((it.analyzed as MediaContainer).duration - (it.seekTo ?: 0.0)) / segLen).toInt() }.toSet()
if (segments.size > 1) {
throw RuntimeException("Inputs differ in length")
}
segments.first()

fun EncoreJob.segmentLengthOrThrow() = segmentedEncodingInfoOrThrow().segmentLength

fun EncoreJob.segmentedEncodingInfoOrThrow() = segmentedEncodingInfo ?: throw RuntimeException("No segmentedEncodingInfo in job!")

fun EncoreJob.segmentDuration(segmentNumber: Int): Double {
val numSegments = segmentedEncodingInfoOrThrow().numSegments
return when {
duration == null -> segmentLengthOrThrow()
segmentNumber < numSegments - 1 -> segmentLengthOrThrow()
segmentNumber == numSegments - 1 ->
// This correctly handles the case where the duration is an exact multiple of the segment length
duration!! - segmentLengthOrThrow() * (numSegments - 1)
else -> throw IllegalArgumentException("segmentNumber $segmentNumber is out of range for job with $numSegments segments")
}
}

fun EncoreJob.segmentDuration(segmentNumber: Int): Double = when {
duration == null -> segmentLengthOrThrow()
segmentNumber < numSegments() - 1 -> segmentLengthOrThrow()
else -> duration!! % segmentLengthOrThrow()
fun EncoreJob.baseName(segmentNumber: Int) = "${baseName}_%05d".format(segmentNumber)

fun EncoreJob.segmentSuffixFromFilename(file: String): String {
val regex = Regex("${baseName}_\\d{5}(.*)")
val match = regex.find(file) ?: throw RuntimeException("Could not find segment suffix for file $file")
return match.groupValues[1]
}

fun EncoreJob.baseName(segmentNumber: Int) = "${baseName}_%05d".format(segmentNumber)
fun EncoreJob.targetFilenameFromSegmentFilename(segmentFile: String) =
segmentFile.replace(Regex("^${baseName}_\\d{5}"), baseName)
Loading