Skip to content

Commit

Permalink
* Use input params when performing analysis.
Browse files Browse the repository at this point in the history
* Prevent thumbnails from in some cases being detected as video.
* Add joinSegmentParams to Profile to be applied when joining segments.
* When running tests, redis is started in a docker container.
  • Loading branch information
fhermansson committed Mar 20, 2024
2 parents 1897aaa + 5d7ebda commit 47f9295
Show file tree
Hide file tree
Showing 29 changed files with 123 additions and 60 deletions.
25 changes: 6 additions & 19 deletions encore-common/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
plugins {
id("encore.kotlin-conventions")
`java-test-fixtures`
}

dependencies {

api("se.svt.oss:media-analyzer:2.0.3")
api("se.svt.oss:media-analyzer:2.0.4")
implementation(kotlin("reflect"))

compileOnly("org.springdoc:springdoc-openapi-starter-webmvc-api:2.2.0")
Expand All @@ -16,28 +17,14 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-slf4j:1.7.3")

testImplementation(project(":encore-web"))
testImplementation("se.svt.oss:junit5-redis-extension:3.0.0")
testImplementation("org.springframework.security:spring-security-test")
testImplementation("org.awaitility:awaitility")
testImplementation("com.github.tomakehurst:wiremock-jre8-standalone:2.35.0")
testImplementation("org.springframework.boot:spring-boot-starter-webflux")
testImplementation("org.springframework.boot:spring-boot-starter-data-rest")
}


val integrationTestsPreReq = setOf("mediainfo", "ffmpeg", "ffprobe").map {

tasks.create("Verify $it is in path, required for integration tests", Exec::class.java) {
isIgnoreExitValue = true
executable = it

if (!it.equals("mediainfo")) {
args("-hide_banner")
}
}
}

tasks.test {
dependsOn(integrationTestsPreReq)
testFixturesImplementation(platform("org.springframework.boot:spring-boot-dependencies:3.1.3"))
testFixturesImplementation("com.redis:testcontainers-redis:2.2.0")
testFixturesImplementation("io.github.microutils:kotlin-logging:3.0.5")
testFixturesImplementation("org.junit.jupiter:junit-jupiter-api")
}

Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import org.springframework.web.reactive.function.client.support.WebClientAdapter
import org.springframework.web.service.invoker.HttpServiceProxyFactory
import se.svt.oss.encore.service.callback.CallbackClient

@Configuration
@Configuration(proxyBeanMethods = false)
class ClientConfiguration {

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import se.svt.oss.mediaanalyzer.MediaAnalyzer

@Configuration
@Configuration(proxyBeanMethods = false)
class MediaAnalyzerConfiguration {

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ import se.svt.oss.mediaanalyzer.file.ImageFile
import se.svt.oss.mediaanalyzer.file.SubtitleFile
import se.svt.oss.mediaanalyzer.file.VideoFile

@Configuration
@Configuration(proxyBeanMethods = false)
@RegisterReflectionForBinding(
EncoreJob::class,
AudioVideoInput::class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ sealed interface Input {
val uri: String

@get:Schema(description = "Input params required to properly decode input", example = """{ "ac": "2" }""")
val params: LinkedHashMap<String, String>
val params: LinkedHashMap<String, String?>

@get:Schema(
description = "Type of input",
Expand Down Expand Up @@ -152,7 +152,7 @@ sealed interface VideoIn : Input {
data class AudioInput(
override val uri: String,
override val audioLabel: String = DEFAULT_AUDIO_LABEL,
override val params: LinkedHashMap<String, String> = linkedMapOf(),
override val params: LinkedHashMap<String, String?> = linkedMapOf(),
override val audioFilters: List<String> = emptyList(),
override var analyzed: MediaFile? = null,
override val audioStream: Int? = null,
Expand All @@ -177,7 +177,7 @@ data class AudioInput(
data class VideoInput(
override val uri: String,
override val videoLabel: String = DEFAULT_VIDEO_LABEL,
override val params: LinkedHashMap<String, String> = linkedMapOf(),
override val params: LinkedHashMap<String, String?> = linkedMapOf(),
override val dar: FractionString? = null,
override val cropTo: FractionString? = null,
override val padTo: FractionString? = null,
Expand Down Expand Up @@ -206,7 +206,7 @@ data class AudioVideoInput(
override val uri: String,
override val videoLabel: String = DEFAULT_VIDEO_LABEL,
override val audioLabel: String = DEFAULT_AUDIO_LABEL,
override val params: LinkedHashMap<String, String> = linkedMapOf(),
override val params: LinkedHashMap<String, String?> = linkedMapOf(),
override val dar: FractionString? = null,
override val cropTo: FractionString? = null,
override val padTo: FractionString? = null,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ data class Output(
val format: String = "mp4",
val postProcessor: PostProcessor = PostProcessor { outputFolder -> listOf(outputFolder.resolve(output)) },
val id: String,
val isImage: Boolean = false
)

fun interface PostProcessor {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,6 @@ data class Profile(
val description: String,
val encodes: List<OutputProducer>,
val scaling: String? = "bicubic",
val deinterlaceFilter: String = "yadif"
val deinterlaceFilter: String = "yadif",
val joinSegmentParams: LinkedHashMap<String, Any?> = linkedMapOf()
)
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ data class ThumbnailEncode(
output = "${job.baseName}$suffix%0${suffixZeroPad}d.jpg",
postProcessor = { outputFolder ->
outputFolder.listFiles().orEmpty().filter { it.name.matches(fileRegex) }
}
},
isImage = true
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ data class ThumbnailMapEncode(
val targetFile = outputFolder.resolve("${job.baseName}$suffix.$format")
val process = ProcessBuilder(
"ffmpeg",
"-y",
"-i",
"${job.baseName}$suffix%04d.$format",
"-vf",
Expand All @@ -100,7 +101,8 @@ data class ThumbnailMapEncode(
logOrThrow("Error creating thumbnail map! ${e.message}")
emptyList()
}
}
},
isImage = true
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ interface VideoEncode : OutputProducer {
fun passParams(pass: Int): Map<String, String> =
mapOf("pass" to pass.toString(), "passlogfile" to "log$suffix")

private fun videoFilter(
fun videoFilter(
debugOverlay: Boolean,
encodingProperties: EncodingProperties,
videoInput: VideoIn
Expand All @@ -75,7 +75,8 @@ interface VideoEncode : OutputProducer {
var scaleToHeight = height
val videoStream = videoInput.analyzedVideo.highestBitrateVideoStream
val outputDar = (videoInput.padTo ?: videoInput.cropTo ?: videoStream.displayAspectRatio)?.toFractionOrNull()
val outputIsPortrait = outputDar != null && outputDar < Fraction.ONE
?: Fraction(videoStream.width, videoStream.height)
val outputIsPortrait = outputDar < Fraction.ONE
val isScalingWithinLandscape =
scaleToWidth != null && scaleToHeight != null && Fraction(scaleToWidth, scaleToHeight) > Fraction.ONE
if (encodingProperties.flipWidthHeightIfPortrait && outputIsPortrait && isScalingWithinLandscape) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ class EncoreService(
val targetName = encoreJob.baseName + it
log.info { "Joining segments for $targetName" }
val targetFile = outputFolder.resolve(targetName)
ffmpegExecutor.joinSegments(outWorkDir.resolve("$it.txt"), targetFile)
ffmpegExecutor.joinSegments(encoreJob, outWorkDir.resolve("$it.txt"), targetFile)
}
outputFiles
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import org.springframework.stereotype.Service
import se.svt.oss.encore.config.EncoreProperties
import se.svt.oss.encore.model.EncoreJob
import se.svt.oss.encore.model.input.maxDuration
import se.svt.oss.encore.model.mediafile.toParams
import se.svt.oss.encore.process.CommandBuilder
import se.svt.oss.encore.service.profile.ProfileService
import se.svt.oss.mediaanalyzer.MediaAnalyzer
Expand Down Expand Up @@ -69,7 +70,7 @@ class FfmpegExecutor(
progressChannel?.close()
outputs.flatMap { out ->
out.postProcessor.process(File(outputFolder))
.map { mediaAnalyzer.analyze(it.toString()) }
.map { mediaAnalyzer.analyze(it.toString(), disableImageSequenceDetection = out.isImage) }
}
} catch (e: CancellationException) {
log.info { "Job was cancelled" }
Expand Down Expand Up @@ -167,7 +168,8 @@ class FfmpegExecutor(
}
}

fun joinSegments(segmentList: File, targetFile: File): MediaFile {
fun joinSegments(encoreJob: EncoreJob, segmentList: File, targetFile: File): MediaFile {
val joinParams = profileService.getProfile(encoreJob).joinSegmentParams.toParams()
val command = listOf(
"ffmpeg",
"-hide_banner",
Expand All @@ -185,8 +187,9 @@ class FfmpegExecutor(
"-ignore_unknown",
"-c",
"copy",
"$targetFile"
)
"-metadata",
"comment=Transcoded using Encore",
) + joinParams + "$targetFile"
runFfmpeg(command, segmentList.parentFile, null) {}
return mediaAnalyzer.analyze(targetFile.absolutePath)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,11 @@ class MediaAnalyzerService(private val mediaAnalyzer: MediaAnalyzer) {
val probeInterlaced = input is VideoIn && input.probeInterlaced
val useFirstAudioStreams = (input as? AudioIn)?.channelLayout?.channels?.size

input.analyzed = mediaAnalyzer.analyze(input.uri, probeInterlaced).let {
input.analyzed = mediaAnalyzer.analyze(
file = input.uri,
probeInterlaced = probeInterlaced,
ffprobeInputParams = input.params
).let {
val selectedVideoStream = (input as? VideoIn)?.videoStream
val selectedAudioStream = (input as? AudioIn)?.audioStream
when (it) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import org.awaitility.Awaitility.await
import org.awaitility.Durations
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.io.TempDir
import org.springframework.core.io.ClassPathResource
import org.springframework.test.annotation.DirtiesContext
import org.springframework.test.context.ActiveProfiles
import se.svt.oss.encore.Assertions.assertThat
Expand Down Expand Up @@ -136,6 +137,32 @@ class EncoreIntegrationTest : EncoreIntegrationTestBase() {
)
}

@Test
fun jobWithInputParamsForRawVideo(@TempDir outputDir: File) {
val inputFile = ClassPathResource("input/testyuv.yuv")
val encoreJob = job(outputDir = outputDir, file = inputFile)

successfulTest(
encoreJob.copy(
profile = "archive",
profileParams = linkedMapOf("suffix" to "_raw", "height" to 1080),
inputs = listOf(
VideoInput(
uri = inputFile.file.absolutePath,
params = linkedMapOf(
"f" to "rawvideo",
"video_size" to "640x360",
"framerate" to "25",
"pixel_format" to "yuv420p",
)
)
)
),

listOf(outputDir.resolve("testyuv_raw.mxf").absolutePath)
)
}

@Test
fun highPriorityJobIsRunInParallel(@TempDir outputDir1: File, @TempDir outputDir2: File) {
val standardPriorityJob = createAndAwaitJob(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,12 @@ import se.svt.oss.encore.model.Status
import se.svt.oss.encore.model.callback.JobProgress
import se.svt.oss.encore.model.input.AudioVideoInput
import se.svt.oss.encore.model.profile.ChannelLayout
import se.svt.oss.junit5.redis.EmbeddedRedisExtension
import java.io.File
import java.time.Duration
import java.util.UUID

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ExtendWith(EmbeddedRedisExtension::class)
@ExtendWith(RedisExtension::class)
@DirtiesContext
@Import(TestConfig::class)
class EncoreIntegrationTestBase {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,14 @@ import org.springframework.boot.test.context.SpringBootTest
import org.springframework.data.domain.PageRequest
import org.springframework.test.context.ActiveProfiles
import se.svt.oss.encore.Assertions.assertThat
import se.svt.oss.encore.RedisExtension
import se.svt.oss.encore.model.EncoreJob
import se.svt.oss.encore.model.Status
import se.svt.oss.junit5.redis.EmbeddedRedisExtension
import java.time.OffsetDateTime
import java.util.UUID

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@ExtendWith(EmbeddedRedisExtension::class)
@ExtendWith(RedisExtension::class)
@ActiveProfiles("test")
class EncoreJobRepositoryTest {

Expand Down
3 changes: 0 additions & 3 deletions encore-common/src/test/resources/application-test-local.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
spring:
data:
redis:
port: ${embedded-redis.port}
main:
allow-bean-definition-overriding: true

Expand Down
5 changes: 1 addition & 4 deletions encore-common/src/test/resources/application-test.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
spring:
data:
redis:
port: ${embedded-redis.port}
main:
allow-bean-definition-overriding: true

Expand All @@ -25,7 +22,7 @@ encore-settings:
concurrency: 3
local-temporary-encode: false
poll-initial-delay: 1s
poll-delaly: 1s
poll-delay: 1s
shared-work-dir: ${java.io.tmpdir}/encore-shared
encoding:
default-channel-layouts:
Expand Down
6 changes: 6 additions & 0 deletions encore-common/src/test/resources/input/testyuv.yuv

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions encore-common/src/test/resources/profile/archive.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ encodes:
format: mxf
twoPass: false
audioEncode:
optional: true
type: SimpleAudioEncode
codec: pcm_s24le
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package se.svt.oss.encore

import com.redis.testcontainers.RedisContainer
import mu.KotlinLogging
import org.junit.jupiter.api.extension.BeforeAllCallback
import org.junit.jupiter.api.extension.ExtensionContext
import org.testcontainers.DockerClientFactory
import org.testcontainers.utility.DockerImageName

private const val DEFAULT_REDIS_DOCKER_IMAGE = "redis:6.2.13"

class RedisExtension : BeforeAllCallback {
private val log = KotlinLogging.logger { }
override fun beforeAll(context: ExtensionContext?) {
if (isDockerAvailable()) {
val dockerImageName = System.getenv("ENCORE_REDIS_DOCKER_IMAGE") ?: DEFAULT_REDIS_DOCKER_IMAGE
val redisContainer = RedisContainer(DockerImageName.parse(dockerImageName))
.withKeyspaceNotifications()
redisContainer.start()
val host = redisContainer.redisHost
val port = redisContainer.redisPort.toString()
log.info { "Setting spring.data.redis.host=$host" }
log.info { "Setting spring.data.redis.port=$port" }
System.setProperty("spring.data.redis.host", host)
System.setProperty("spring.data.redis.port", port)
}
}

private fun isDockerAvailable(): Boolean {
return try {
log.info { "Checking for docker..." }
DockerClientFactory.instance().client()
log.info { "Docker is available" }
true
} catch (ex: Throwable) {
log.warn { "Docker is not available! Make sure redis is available as configured by spring.data.redis (default localhost:6379)" }
false
}
}
}
2 changes: 1 addition & 1 deletion encore-web/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-security")
implementation("org.springframework.boot:spring-boot-starter-actuator")

testImplementation(testFixtures(project(":encore-common")))
testImplementation("com.ninja-squad:springmockk:4.0.2")
testImplementation("se.svt.oss:junit5-redis-extension:3.0.0")
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import se.svt.oss.encore.config.EncoreProperties

@Configuration
@Configuration(proxyBeanMethods = false)
class OpenAPIConfiguration {

@Bean
Expand Down
Loading

0 comments on commit 47f9295

Please sign in to comment.