From 2c61650a0e0c77093271e1cace3b396877fe214d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brage=20Skj=C3=B8nborg?= Date: Thu, 11 Dec 2025 02:54:48 +0100 Subject: [PATCH] Wip --- .../converter/ConverterApplication.kt | 5 +- .../mediaprocessing/converter/TaskPoller.kt | 66 ++ .../listeners/ConvertTaskListener.kt | 6 +- apps/coordinator/build.gradle.kts | 2 + .../coordinator/CoordinatorApplication.kt | 52 ++ .../coordinator/CoordinatorEnv.kt | 11 + .../coordinator/EventPoller.kt | 30 + .../mediaprocessing/coordinator/Preference.kt | 46 ++ .../events/MediaCreateConvertTaskListener.kt | 15 + .../events/MediaCreateEncodeTaskListener.kt | 72 +++ .../events/MediaCreateExtractTaskListener.kt | 19 + ...MediaDetermineSubtitleTrackTypeListener.kt | 76 +++ .../events/MediaParseStreamsListener.kt | 75 +++ ...Listener.kt => MediaParsedInfoListener.kt} | 27 +- .../MediaReadStreamsTaskCreatedListener.kt | 32 + .../events/MediaSelectEncodeTracksListener.kt | 74 +++ .../MediaSelectExtractTracksListener.kt | 44 ++ .../listeners/events/StartedListener.kt | 34 ++ .../listeners/tasks/FfprobeTaskListener.kt | 9 + .../tasks/MediaStreamReadTaskListener.kt | 49 ++ .../no/iktdev/mediaprocessing/Resources.kt | 10 +- .../mediaprocessing/coordinator/TestData.kt | 59 -- ...Test.kt => MediaParsedInfoListenerTest.kt} | 6 +- .../reader/ParseVideoFileStreamsTest.kt | 168 ------ .../event/MetadataAndBaseInfoToFileOutTest.kt | 40 -- .../ffmpeg/EncodeArgumentCreatorTaskTest.kt | 80 --- .../listeners/ConvertWorkTaskListenerTest.kt | 77 --- .../MetadataWaitOrDefaultTaskListenerTest.kt | 52 -- .../ParseMediaFileStreamsTaskListenerTest.kt | 25 - .../mapping/EncodeWorkArgumentsMappingTest.kt | 31 - .../mapping/streams/AudioArgumentsTest.kt | 256 -------- .../mapping/streams/SubtitleArgumentsTest.kt | 566 ------------------ .../mapping/streams/VideoArgumentsTest.kt | 361 ----------- apps/processer/build.gradle.kts | 11 +- .../processer/ProcesserApplication.kt | 52 ++ .../mediaprocessing/processer/ProcesserEnv.kt | 26 + .../mediaprocessing/processer/TaskPoller.kt | 65 ++ .../iktdev/mediaprocessing/processer/Util.kt | 10 + .../processer/listeners/FfmpegTaskListener.kt | 9 + .../listeners/MuxAudioVideoTaskListener.kt | 4 + .../listeners/SubtitleTaskListener.kt | 74 +++ .../processer/listeners/VideoTaskListener.kt | 89 +++ .../processer/ffmpeg/FfmpegRunnerTest.kt | 15 - .../processer/listeners/MockFFmpeg.kt | 38 ++ .../listeners/VideoTaskListenerTest.kt | 80 +++ .../processer/services/EncodeServiceTest.kt | 4 - shared/common/build.gradle.kts | 2 +- .../event_task_contract/EventRegistry.kt | 16 +- .../event_task_contract/TaskRegistry.kt | 4 +- .../events/ConvertTaskCreatedEvent.kt | 6 + ...terEvents.kt => ConvertTaskResultEvent.kt} | 7 +- .../events/FileAddedEvent.kt | 9 + .../event_task_contract/events/FileEvents.kt | 23 - .../events/FileReadyEvent.kt | 8 + .../events/FileRemovedEvent.kt | 9 + .../events/MediaParsedInfoEvent.kt | 15 +- .../events/MediaStreamParsedEvent.kt | 2 +- .../events/MediaStreamReadTaskCreatedEvent.kt | 4 +- .../MediaTracksDetermineSubtitleTypeEvent.kt | 10 + .../events/MediaTracksEncodeSelectedEvent.kt | 9 + .../events/MediaTracksExtractSelectedEvent.kt | 9 + .../events/ProcesserEncodePerformedEvent.kt | 6 + .../events/ProcesserEncodeTaskCreatedEvent.kt | 6 + .../events/ProcesserEvents.kt | 7 +- .../event_task_contract/tasks/EncodeTask.kt | 2 +- ...{ExtractTask.kt => ExtractSubtitleTask.kt} | 6 +- .../shared/common/model/FileInfo.kt | 6 + .../shared/common/model/MediaType.kt | 6 + .../shared/common/model/SubtitleItem.kt | 5 + .../shared/common/model/SubtitleType.kt | 9 + .../shared/common/projection/Project.kt | 47 ++ shared/ffmpeg/build.gradle.kts | 4 + .../iktdev/mediaprocessing/ffmpeg/FFmpeg.kt | 13 +- .../ffmpeg/{FFinfo.kt => FFprobe.kt} | 2 +- .../ffmpeg/data}/MediaStreams.kt | 2 +- .../mediaprocessing/ffmpeg/dsl/AudioCodec.kt | 227 +++++++ .../mediaprocessing/ffmpeg/dsl/MediaPlan.kt | 73 +++ .../ffmpeg/dsl/TranscodeDecision.kt | 7 + .../mediaprocessing/ffmpeg/dsl/VideoCodec.kt | 253 ++++++++ .../ffmpeg/util/FFmpegUtils.kt | 30 + .../mediaprocessing/ffmpeg/FFmpegTest.kt | 73 +++ .../mediaprocessing/ffmpeg}/Resources.kt | 6 +- .../decoder}/FfmpegProgressDecoderTest.kt | 8 +- .../ffmpeg/dsl/MediaPlanTest.kt | 377 ++++++++++++ .../src/test/resources/encodeProgress1.txt | 12 +- 85 files changed, 2427 insertions(+), 1845 deletions(-) create mode 100644 apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/TaskPoller.kt create mode 100644 apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/CoordinatorApplication.kt create mode 100644 apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/CoordinatorEnv.kt create mode 100644 apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/EventPoller.kt create mode 100644 apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/Preference.kt create mode 100644 apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaCreateConvertTaskListener.kt create mode 100644 apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaCreateEncodeTaskListener.kt create mode 100644 apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaCreateExtractTaskListener.kt create mode 100644 apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaDetermineSubtitleTrackTypeListener.kt create mode 100644 apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaParseStreamsListener.kt rename apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/{MediaEventParsedInfoListener.kt => MediaParsedInfoListener.kt} (90%) create mode 100644 apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaReadStreamsTaskCreatedListener.kt create mode 100644 apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaSelectEncodeTracksListener.kt create mode 100644 apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaSelectExtractTracksListener.kt create mode 100644 apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/StartedListener.kt create mode 100644 apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/tasks/FfprobeTaskListener.kt create mode 100644 apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/tasks/MediaStreamReadTaskListener.kt delete mode 100644 apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/TestData.kt rename apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/events/{MediaEventParsedInfoListenerTest.kt => MediaParsedInfoListenerTest.kt} (98%) delete mode 100644 apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/reader/ParseVideoFileStreamsTest.kt delete mode 100644 apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasks/event/MetadataAndBaseInfoToFileOutTest.kt delete mode 100644 apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasks/event/ffmpeg/EncodeArgumentCreatorTaskTest.kt delete mode 100644 apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/listeners/ConvertWorkTaskListenerTest.kt delete mode 100644 apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/listeners/MetadataWaitOrDefaultTaskListenerTest.kt delete mode 100644 apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/listeners/ParseMediaFileStreamsTaskListenerTest.kt delete mode 100644 apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/mapping/EncodeWorkArgumentsMappingTest.kt delete mode 100644 apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/mapping/streams/AudioArgumentsTest.kt delete mode 100644 apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/mapping/streams/SubtitleArgumentsTest.kt delete mode 100644 apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/mapping/streams/VideoArgumentsTest.kt create mode 100644 apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/ProcesserApplication.kt create mode 100755 apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/ProcesserEnv.kt create mode 100644 apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/TaskPoller.kt create mode 100644 apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/Util.kt create mode 100644 apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/listeners/FfmpegTaskListener.kt create mode 100644 apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/listeners/MuxAudioVideoTaskListener.kt create mode 100644 apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/listeners/SubtitleTaskListener.kt create mode 100644 apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/listeners/VideoTaskListener.kt delete mode 100644 apps/processer/src/test/kotlin/no/iktdev/mediaprocessing/processer/ffmpeg/FfmpegRunnerTest.kt create mode 100644 apps/processer/src/test/kotlin/no/iktdev/mediaprocessing/processer/listeners/MockFFmpeg.kt create mode 100644 apps/processer/src/test/kotlin/no/iktdev/mediaprocessing/processer/listeners/VideoTaskListenerTest.kt delete mode 100644 apps/processer/src/test/kotlin/no/iktdev/mediaprocessing/processer/services/EncodeServiceTest.kt create mode 100644 shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ConvertTaskCreatedEvent.kt rename shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/{ConverterEvents.kt => ConvertTaskResultEvent.kt} (68%) create mode 100644 shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/FileAddedEvent.kt delete mode 100644 shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/FileEvents.kt create mode 100644 shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/FileReadyEvent.kt create mode 100644 shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/FileRemovedEvent.kt create mode 100644 shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/MediaTracksDetermineSubtitleTypeEvent.kt create mode 100644 shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/MediaTracksEncodeSelectedEvent.kt create mode 100644 shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/MediaTracksExtractSelectedEvent.kt create mode 100644 shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ProcesserEncodePerformedEvent.kt create mode 100644 shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ProcesserEncodeTaskCreatedEvent.kt rename shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/tasks/{ExtractTask.kt => ExtractSubtitleTask.kt} (72%) create mode 100644 shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/model/FileInfo.kt create mode 100644 shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/model/MediaType.kt create mode 100644 shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/model/SubtitleItem.kt create mode 100644 shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/model/SubtitleType.kt create mode 100644 shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/projection/Project.kt rename shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/{FFinfo.kt => FFprobe.kt} (98%) rename shared/{common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/model => ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/data}/MediaStreams.kt (98%) mode change 100755 => 100644 create mode 100644 shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/dsl/AudioCodec.kt create mode 100644 shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/dsl/MediaPlan.kt create mode 100644 shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/dsl/TranscodeDecision.kt create mode 100644 shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/dsl/VideoCodec.kt create mode 100644 shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/util/FFmpegUtils.kt create mode 100644 shared/ffmpeg/src/test/kotlin/no/iktdev/mediaprocessing/ffmpeg/FFmpegTest.kt rename {apps/processer/src/test/kotlin/no/iktdev/mediaprocessing/processer => shared/ffmpeg/src/test/kotlin/no/iktdev/mediaprocessing/ffmpeg}/Resources.kt (55%) mode change 100644 => 100755 rename {apps/processer/src/test/kotlin/no/iktdev/mediaprocessing/processer/ffmpeg/progress => shared/ffmpeg/src/test/kotlin/no/iktdev/mediaprocessing/ffmpeg/decoder}/FfmpegProgressDecoderTest.kt (70%) create mode 100644 shared/ffmpeg/src/test/kotlin/no/iktdev/mediaprocessing/ffmpeg/dsl/MediaPlanTest.kt rename {apps/processer => shared/ffmpeg}/src/test/resources/encodeProgress1.txt (92%) mode change 100644 => 100755 diff --git a/apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/ConverterApplication.kt b/apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/ConverterApplication.kt index 29693777..cf24cc34 100644 --- a/apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/ConverterApplication.kt +++ b/apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/ConverterApplication.kt @@ -14,10 +14,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication import org.springframework.boot.runApplication import org.springframework.context.annotation.Configuration -@SpringBootApplication(scanBasePackages = [ - "no.iktdev.converter", - "no.iktdev.mediaprocessing.shared.common" -]) + open class ConverterApplication: DatabaseApplication() { } diff --git a/apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/TaskPoller.kt b/apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/TaskPoller.kt new file mode 100644 index 00000000..a93b1087 --- /dev/null +++ b/apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/TaskPoller.kt @@ -0,0 +1,66 @@ +package no.iktdev.mediaprocessing.converter + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import no.iktdev.eventi.models.Event +import no.iktdev.eventi.tasks.AbstractTaskPoller +import no.iktdev.eventi.tasks.TaskReporter +import no.iktdev.mediaprocessing.shared.common.stores.EventStore +import no.iktdev.mediaprocessing.shared.common.stores.TaskStore +import org.springframework.boot.ApplicationArguments +import org.springframework.boot.ApplicationRunner +import org.springframework.context.annotation.Bean +import org.springframework.stereotype.Component +import org.springframework.stereotype.Service +import java.util.UUID + +@Component +class PollerAdministrator( + private val taskPoller: TaskPoller, +): ApplicationRunner { + override fun run(args: ApplicationArguments?) { + CoroutineScope(Dispatchers.Default).launch { + taskPoller.start() + } + } +} + + +@Service +class TaskPoller( + private val reporter: TaskReporter, +) : AbstractTaskPoller( + taskStore = TaskStore, + reporterFactory = { reporter } // én reporter brukes for alle tasks +) { + +} + + +@Component +class DefaultTaskReporter() : TaskReporter { + override fun markClaimed(taskId: UUID, workerId: String) { + TaskStore.claim(taskId, workerId) + } + + override fun updateLastSeen(taskId: UUID) { + TaskStore.heartbeat(taskId) + } + + override fun markConsumed(taskId: UUID) { + TaskStore.markConsumed(taskId) + } + + override fun updateProgress(taskId: UUID, progress: Int) { + // Not to be implemented for this application + } + + override fun log(taskId: UUID, message: String) { + // Not to be implemented for this application + } + + override fun publishEvent(event: Event) { + EventStore.persist(event) + } +} \ No newline at end of file diff --git a/apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/listeners/ConvertTaskListener.kt b/apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/listeners/ConvertTaskListener.kt index 5dc45d9c..d0f365bc 100644 --- a/apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/listeners/ConvertTaskListener.kt +++ b/apps/converter/src/main/kotlin/no/iktdev/mediaprocessing/converter/listeners/ConvertTaskListener.kt @@ -7,7 +7,7 @@ import no.iktdev.eventi.tasks.TaskListener import no.iktdev.eventi.tasks.TaskType import no.iktdev.mediaprocessing.converter.convert.ConvertListener import no.iktdev.mediaprocessing.converter.convert.Converter2 -import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.ConvertTaskPerformedEvent +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.ConvertTaskResultEvent import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.ConvertedData import no.iktdev.mediaprocessing.shared.common.event_task_contract.tasks.ConvertTask import org.springframework.stereotype.Component @@ -43,7 +43,7 @@ class ConvertTaskListener: TaskListener(TaskType.CPU_INTENSIVE) { return try { val result = converter.getResult() - val newEvent = ConvertTaskPerformedEvent( + val newEvent = ConvertTaskResultEvent( data = ConvertedData( language = task.data.language, outputFiles = result, @@ -54,7 +54,7 @@ class ConvertTaskListener: TaskListener(TaskType.CPU_INTENSIVE) { newEvent } catch (e: Exception) { e.printStackTrace() - val newEvent = ConvertTaskPerformedEvent( + val newEvent = ConvertTaskResultEvent( data = null, status = TaskStatus.Failed ).producedFrom(task) diff --git a/apps/coordinator/build.gradle.kts b/apps/coordinator/build.gradle.kts index e7f2f17a..b8dab861 100644 --- a/apps/coordinator/build.gradle.kts +++ b/apps/coordinator/build.gradle.kts @@ -39,6 +39,7 @@ dependencies { implementation("no.iktdev:exfl:0.0.16-SNAPSHOT") implementation("no.iktdev.streamit.library:streamit-library-db:1.0.0-alpha14") + implementation("no.iktdev:eventi:1.0-rc13") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1") @@ -73,6 +74,7 @@ dependencies { testImplementation(platform("org.junit:junit-bom:5.9.1")) testImplementation("org.junit.jupiter:junit-jupiter") + testImplementation("org.junit.jupiter:junit-jupiter-params") testImplementation("junit:junit:4.13.2") testImplementation("org.mockito:mockito-core:3.+") testImplementation("org.assertj:assertj-core:3.4.1") diff --git a/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/CoordinatorApplication.kt b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/CoordinatorApplication.kt new file mode 100644 index 00000000..6d5b14dc --- /dev/null +++ b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/CoordinatorApplication.kt @@ -0,0 +1,52 @@ +package no.iktdev.mediaprocessing.coordinator + +import mu.KotlinLogging +import no.iktdev.eventi.events.EventTypeRegistry +import no.iktdev.eventi.tasks.TaskTypeRegistry +import no.iktdev.exfl.coroutines.CoroutinesDefault +import no.iktdev.exfl.coroutines.CoroutinesIO +import no.iktdev.exfl.observable.Observables +import no.iktdev.mediaprocessing.shared.common.DatabaseApplication +import no.iktdev.mediaprocessing.shared.common.event_task_contract.EventRegistry +import no.iktdev.mediaprocessing.shared.common.event_task_contract.TaskRegistry +import no.iktdev.mediaprocessing.shared.common.getAppVersion +import org.springframework.boot.runApplication +import org.springframework.context.annotation.Configuration + +class CoordinatorApplication: DatabaseApplication() { +} + +val ioCoroutine = CoroutinesIO() +val defaultCoroutine = CoroutinesDefault() + +private val log = KotlinLogging.logger {} + +fun main(args: Array) { + ioCoroutine.addListener(listener = object: Observables.ObservableValue.ValueListener { + override fun onUpdated(value: Throwable) { + value.printStackTrace() + } + }) + defaultCoroutine.addListener(listener = object: Observables.ObservableValue.ValueListener { + override fun onUpdated(value: Throwable) { + value.printStackTrace() + } + }) + + + runApplication(*args) + log.info { "App Version: ${getAppVersion()}" } +} +//private val logger = KotlinLogging.logger {} + +@Configuration +open class ApplicationConfiguration() { + init { + EventRegistry.getEvents().let { + EventTypeRegistry.register(it) + } + TaskRegistry.getTasks().let { + TaskTypeRegistry.register(it) + } + } +} diff --git a/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/CoordinatorEnv.kt b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/CoordinatorEnv.kt new file mode 100644 index 00000000..8db7dc8d --- /dev/null +++ b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/CoordinatorEnv.kt @@ -0,0 +1,11 @@ +package no.iktdev.mediaprocessing.coordinator + +import java.io.File + +class CoordinatorEnv { + companion object { + val ffprobe: String = System.getenv("SUPPORTING_EXECUTABLE_FFPROBE") ?: "ffprobe" + + val preference: File = File("/data/config/preference.json") + } +} \ No newline at end of file diff --git a/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/EventPoller.kt b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/EventPoller.kt new file mode 100644 index 00000000..cfec6c01 --- /dev/null +++ b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/EventPoller.kt @@ -0,0 +1,30 @@ +package no.iktdev.mediaprocessing.coordinator + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import no.iktdev.eventi.events.AbstractEventPoller +import no.iktdev.eventi.events.EventDispatcher +import no.iktdev.eventi.events.SequenceDispatchQueue +import no.iktdev.mediaprocessing.shared.common.stores.EventStore +import org.springframework.boot.ApplicationArguments +import org.springframework.boot.ApplicationRunner +import org.springframework.stereotype.Component + + +@Component +class PollerAdministrator( + private val eventPoller: EventPoller, +): ApplicationRunner { + override fun run(args: ApplicationArguments?) { + CoroutineScope(Dispatchers.Default).launch { + eventPoller.start() + } + } +} + +val sequenceDispatcher = SequenceDispatchQueue(8) +val dispatcher = EventDispatcher(eventStore = EventStore) + +class EventPoller: AbstractEventPoller(eventStore = EventStore, dispatchQueue = sequenceDispatcher, dispatcher = dispatcher) { +} diff --git a/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/Preference.kt b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/Preference.kt new file mode 100644 index 00000000..e14094f9 --- /dev/null +++ b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/Preference.kt @@ -0,0 +1,46 @@ +package no.iktdev.mediaprocessing.coordinator + +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import no.iktdev.mediaprocessing.ffmpeg.dsl.AudioCodec +import no.iktdev.mediaprocessing.ffmpeg.dsl.VideoCodec +import no.iktdev.mediaprocessing.shared.common.silentTry +import java.io.File + +class ProcesserPreference { + val videoPreference: VideoPreference? = null + val audioPreference: AudioPreference? = null +} + +data class VideoPreference( + val codec: VideoCodec, + val enforceMkv: Boolean = false +) + +data class AudioPreference( + val language: String, + val codec: AudioCodec +) + + +object Preference { + fun getProcesserPreference(): ProcesserPreference { + var preference: ProcesserPreference = ProcesserPreference() + CoordinatorEnv.preference.ifExists { + val text = readText() + try { + val result = Gson().fromJson(text, ProcesserPreference::class.java) + preference = result + } catch (e: Exception) { + e.printStackTrace() + } + } + return preference + } +} + +private fun File.ifExists(block: File.() -> Unit) { + if (this.exists()) { + block() + } +} diff --git a/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaCreateConvertTaskListener.kt b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaCreateConvertTaskListener.kt new file mode 100644 index 00000000..33caee5e --- /dev/null +++ b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaCreateConvertTaskListener.kt @@ -0,0 +1,15 @@ +package no.iktdev.mediaprocessing.coordinator.listeners.events + +import no.iktdev.eventi.events.EventListener +import no.iktdev.eventi.models.Event +import org.springframework.stereotype.Component + +@Component +class MediaCreateConvertTaskListener: EventListener() { + override fun onEvent( + event: Event, + history: List + ): Event? { + return null; + } +} \ No newline at end of file diff --git a/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaCreateEncodeTaskListener.kt b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaCreateEncodeTaskListener.kt new file mode 100644 index 00000000..41791b60 --- /dev/null +++ b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaCreateEncodeTaskListener.kt @@ -0,0 +1,72 @@ +package no.iktdev.mediaprocessing.coordinator.listeners.events + +import no.iktdev.eventi.events.EventListener +import no.iktdev.eventi.models.Event +import no.iktdev.mediaprocessing.coordinator.Preference +import no.iktdev.mediaprocessing.ffmpeg.dsl.AudioCodec +import no.iktdev.mediaprocessing.ffmpeg.dsl.AudioTarget +import no.iktdev.mediaprocessing.ffmpeg.dsl.MediaPlan +import no.iktdev.mediaprocessing.ffmpeg.dsl.VideoCodec +import no.iktdev.mediaprocessing.ffmpeg.dsl.VideoTarget +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.MediaStreamParsedEvent +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.MediaTracksEncodeSelectedEvent +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.StartProcessingEvent +import no.iktdev.mediaprocessing.shared.common.event_task_contract.tasks.EncodeData +import no.iktdev.mediaprocessing.shared.common.event_task_contract.tasks.EncodeTask +import no.iktdev.mediaprocessing.shared.common.stores.TaskStore +import org.springframework.stereotype.Component +import java.io.File + +@Component +class MediaCreateEncodeTaskListener : EventListener() { + + + override fun onEvent( + event: Event, + history: List + ): Event? { + val preference = Preference.getProcesserPreference() + + val startedEvent = history.filterIsInstance().firstOrNull() ?: return null + val selectedEvent = event as? MediaTracksEncodeSelectedEvent ?: return null + val streams = history.filterIsInstance().firstOrNull()?.data ?: return null + + val videoPreference = preference.videoPreference?.codec ?: VideoCodec.Hevc() + val audioPreference = preference.audioPreference?.codec ?: AudioCodec.Aac(channels = 2) + + val audioTargets = mutableListOf( + AudioTarget( + index = selectedEvent.selectedAudioTrack, + codec = audioPreference + ) + ) + selectedEvent.selectedAudioExtendedTrack?.let { + audioTargets.add(AudioTarget( + index = it, + codec = audioPreference + )) + } + + + val plan = MediaPlan( + videoTrack = VideoTarget(index = selectedEvent.selectedVideoTrack, codec = videoPreference), + audioTracks = audioTargets + ) + val args = plan.toFfmpegArgs(streams.videoStream, streams.audioStream) + + val task = EncodeTask( + data = EncodeData( + arguments = args, + outputFileName = startedEvent.data.fileUri.let { File(it).nameWithoutExtension }, + inputFile = startedEvent.data.fileUri + ) + ).derivedOf(event) + + + TaskStore.persist(task) + return null // Create task instead of event + } + + + +} \ No newline at end of file diff --git a/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaCreateExtractTaskListener.kt b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaCreateExtractTaskListener.kt new file mode 100644 index 00000000..f06835fc --- /dev/null +++ b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaCreateExtractTaskListener.kt @@ -0,0 +1,19 @@ +package no.iktdev.mediaprocessing.coordinator.listeners.events + +import no.iktdev.eventi.events.EventListener +import no.iktdev.eventi.models.Event +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.MediaTracksExtractSelectedEvent +import org.springframework.stereotype.Component + +@Component +class MediaCreateExtractTaskListener: EventListener() { + override fun onEvent( + event: Event, + history: List + ): Event? { + val useEvent = event as? MediaTracksExtractSelectedEvent ?: return null + + + return null + } +} \ No newline at end of file diff --git a/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaDetermineSubtitleTrackTypeListener.kt b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaDetermineSubtitleTrackTypeListener.kt new file mode 100644 index 00000000..65113324 --- /dev/null +++ b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaDetermineSubtitleTrackTypeListener.kt @@ -0,0 +1,76 @@ +package no.iktdev.mediaprocessing.coordinator.listeners.events + +import no.iktdev.eventi.events.EventListener +import no.iktdev.eventi.models.Event +import no.iktdev.mediaprocessing.ffmpeg.data.SubtitleStream +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.MediaStreamParsedEvent +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.MediaTracksDetermineSubtitleTypeEvent +import no.iktdev.mediaprocessing.shared.common.model.SubtitleItem +import no.iktdev.mediaprocessing.shared.common.model.SubtitleType +import org.springframework.stereotype.Component + +@Component +class MediaDetermineSubtitleTrackTypeListener: EventListener() { + fun ignoreSHD(): Boolean = true + fun ignoreCC(): Boolean = true + fun ignoreSongs(): Boolean = true + fun ignoreCommentary(): Boolean = true + + val supportedCodecs = setOf( + "ass", "subrip", "webvtt", "vtt", "smi" + ) + + override fun onEvent( + event: Event, + history: List + ): Event? { + val useEvent = event as? MediaStreamParsedEvent ?: return null + + val collected = useEvent.data.subtitleStream + .mapToType() + .excludeTypes() + .onlySupportedCodecs() + + + return MediaTracksDetermineSubtitleTypeEvent( + subtitleTrackItems = collected + ) + } + + + fun getCommentaryFilters(): Set = setOf("commentary", "kommentar", "kommentaar") + fun getSongFilters(): Set = setOf("song", "sign") + fun getClosedCaptionFilters(): Set = setOf("closed caption", "cc", "close caption", "closed-caption", "cc.") + fun getSHDFilters(): Set = setOf("shd", "hh", "hard of hearing", "hard-of-hearing") + + private fun List.excludeTypes(): List { + return this.filter { + when (it.type) { + SubtitleType.Song -> !ignoreSongs() + SubtitleType.Commentary -> !ignoreCommentary() + SubtitleType.ClosedCaption -> !ignoreCC() + SubtitleType.SHD -> !ignoreSHD() + SubtitleType.Dialogue -> true + } + } + } + + private fun List.mapToType(): List { + return this.map { + val title = it.tags.title?.lowercase() ?: "" + val type = when { + getCommentaryFilters().any { keyword -> title.contains(keyword) } -> SubtitleType.Commentary + getSongFilters().any { keyword -> title.contains(keyword) } -> SubtitleType.Song + getClosedCaptionFilters().any { keyword -> title.contains(keyword) } -> SubtitleType.ClosedCaption + getSHDFilters().any { keyword -> title.contains(keyword) } -> SubtitleType.SHD + else -> SubtitleType.Dialogue + } + SubtitleItem(it, type) + } + } + + private fun List.onlySupportedCodecs(): List { + return this.filter { it.stream.codec_type in supportedCodecs } + } + +} \ No newline at end of file diff --git a/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaParseStreamsListener.kt b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaParseStreamsListener.kt new file mode 100644 index 00000000..c0c50658 --- /dev/null +++ b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaParseStreamsListener.kt @@ -0,0 +1,75 @@ +package no.iktdev.mediaprocessing.coordinator.listeners.events + +import com.google.gson.Gson +import com.google.gson.JsonObject +import mu.KotlinLogging +import no.iktdev.eventi.events.EventListener +import no.iktdev.eventi.models.Event +import no.iktdev.mediaprocessing.ffmpeg.data.AudioStream +import no.iktdev.mediaprocessing.ffmpeg.data.ParsedMediaStreams +import no.iktdev.mediaprocessing.ffmpeg.data.SubtitleStream +import no.iktdev.mediaprocessing.ffmpeg.data.VideoStream +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.MediaStreamParsedEvent +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.MediaStreamReadEvent +import org.springframework.core.annotation.Order +import org.springframework.stereotype.Component + +@Order(4) +@Component +class MediaParseStreamsListener: EventListener() { + val log = KotlinLogging.logger {} + + override fun onEvent( + event: Event, + history: List + ): Event? { + if (event !is MediaStreamReadEvent) return null + + val streams = parseStreams(event.data) + return MediaStreamParsedEvent( + data = streams + ).derivedOf(event) + } + + + fun parseStreams(data: JsonObject?): ParsedMediaStreams { + val ignoreCodecs = listOf("png", "mjpeg") + val gson = Gson() + return try { + val jStreams = data!!.getAsJsonArray("streams") + + val videoStreams = mutableListOf() + val audioStreams = mutableListOf() + val subtitleStreams = mutableListOf() + + for (streamJson in jStreams) { + val streamObject = streamJson.asJsonObject + if (!streamObject.has("codec_name")) continue + val codecName = streamObject.get("codec_name").asString + val codecType = streamObject.get("codec_type").asString + + if (codecName in ignoreCodecs) continue + + when (codecType) { + "video" -> videoStreams.add(gson.fromJson(streamObject, VideoStream::class.java)) + "audio" -> audioStreams.add(gson.fromJson(streamObject, AudioStream::class.java)) + "subtitle" -> subtitleStreams.add(gson.fromJson(streamObject, SubtitleStream::class.java)) + } + } + + val parsedStreams = ParsedMediaStreams( + videoStream = videoStreams, + audioStream = audioStreams, + subtitleStream = subtitleStreams + ) + parsedStreams + + } catch (e: Exception) { + "Failed to parse data, its either not a valid json structure or expected and required fields are not present.".also { + log.error { it } + } + throw e + } + + } +} \ No newline at end of file diff --git a/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaEventParsedInfoListener.kt b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaParsedInfoListener.kt similarity index 90% rename from apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaEventParsedInfoListener.kt rename to apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaParsedInfoListener.kt index 8caa21b5..610ced6f 100644 --- a/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaEventParsedInfoListener.kt +++ b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaParsedInfoListener.kt @@ -2,21 +2,36 @@ package no.iktdev.mediaprocessing.coordinator.listeners.events import no.iktdev.eventi.events.EventListener import no.iktdev.eventi.models.Event +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.MediaParsedInfoEvent import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.StartProcessingEvent +import no.iktdev.mediaprocessing.shared.common.model.MediaType +import org.springframework.core.annotation.Order import org.springframework.stereotype.Component import java.io.File +@Order(2) @Component -class MediaEventParsedInfoListener : EventListener() { +class MediaParsedInfoListener : EventListener() { override fun onEvent( event: Event, history: List ): Event? { val started = event as? StartProcessingEvent ?: return null - val fileName = File(started.data.fileUri).nameWithoutExtension - val cleanedTitle = fileName.getCleanedTitle() + val file = File(started.data.fileUri) - return null + val filename = file.guessDesiredFileName() + val collection = file.getDesiredCollection() + val searchTitles = file.guessSearchableTitle() + val mediaType = file.guessMovieOrSeries() + + return MediaParsedInfoEvent( + MediaParsedInfoEvent.ParsedData( + parsedFileName = filename, + parsedCollection = collection, + parsedSearchTitles = searchTitles, + mediaType = mediaType + ) + ).derivedOf(event) } fun String.getCleanedTitle(): String { @@ -47,10 +62,6 @@ class MediaEventParsedInfoListener : EventListener() { fun String.noExtraSpaces() = Regex("\\s{2,}").replace(this, " ") fun String.fullTrim() = this.trim('.', ',', ' ', '_', '-') - enum class MediaType { - Movie, - Serie - } fun File.guessMovieOrSeries(): MediaType { val name = this.nameWithoutExtension.lowercase() diff --git a/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaReadStreamsTaskCreatedListener.kt b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaReadStreamsTaskCreatedListener.kt new file mode 100644 index 00000000..5bbac291 --- /dev/null +++ b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaReadStreamsTaskCreatedListener.kt @@ -0,0 +1,32 @@ +package no.iktdev.mediaprocessing.coordinator.listeners.events + +import no.iktdev.eventi.events.EventListener +import no.iktdev.eventi.models.Event +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.MediaParsedInfoEvent +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.MediaStreamReadTaskCreatedEvent +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.StartProcessingEvent +import no.iktdev.mediaprocessing.shared.common.event_task_contract.tasks.MediaReadTask +import no.iktdev.mediaprocessing.shared.common.stores.TaskStore +import org.springframework.core.annotation.Order +import org.springframework.stereotype.Component + +@Order(3) +@Component +class MediaReadStreamsTaskCreatedListener: EventListener() { + override fun onEvent( + event: Event, + history: List + ): Event? { + if (event !is MediaParsedInfoEvent) return null + val startEvent = history.lastOrNull { it is StartProcessingEvent } as? StartProcessingEvent + ?: return null + + + val readTask = MediaReadTask( + fileUri = startEvent.data.fileUri + ) + + TaskStore.persist(readTask) + return null // Create task instead of event + } +} \ No newline at end of file diff --git a/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaSelectEncodeTracksListener.kt b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaSelectEncodeTracksListener.kt new file mode 100644 index 00000000..f150ea8a --- /dev/null +++ b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaSelectEncodeTracksListener.kt @@ -0,0 +1,74 @@ +package no.iktdev.mediaprocessing.coordinator.listeners.events + +import no.iktdev.eventi.events.EventListener +import no.iktdev.eventi.models.Event +import no.iktdev.mediaprocessing.ffmpeg.data.AudioStream +import no.iktdev.mediaprocessing.ffmpeg.data.VideoStream +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.MediaStreamParsedEvent +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.MediaTracksEncodeSelectedEvent +import org.springframework.stereotype.Component + +@Component +class MediaSelectEncodeTracksListener: EventListener() { + + fun getAudioLanguagePreference(): List { + return listOf("jpn") + } + + override fun onEvent( + event: Event, + history: List + ): Event? { + val useEvent = event as? MediaStreamParsedEvent ?: return null + + + val videoTrackIndex = getVideoTrackToUse(useEvent.data.videoStream) + val audioDefaultTrack = getAudioDefaultTrackToUse(useEvent.data.audioStream) + val audioExtendedTrack = getAudioExtendedTrackToUse(useEvent.data.audioStream, selectedDefaultTrack = audioDefaultTrack) + + return MediaTracksEncodeSelectedEvent( + selectedVideoTrack = videoTrackIndex, + selectedAudioTrack = audioDefaultTrack, + selectedAudioExtendedTrack = audioExtendedTrack + ).derivedOf(event) + } + + private fun getAudioExtendedTrackToUse(audioStream: List, selectedDefaultTrack: Int): Int? { + val durationFiltered = audioStream.filterOnPreferredLanguage() + .filter { (it.duration_ts ?: 0) > 0 } + .filter { it.channels > 2 } + .filter { it.index != selectedDefaultTrack } + val selected = durationFiltered.firstOrNull() ?: return null + return audioStream.indexOf(selected) + } + + /** + * Select the default audio track to use for encoding. + * If no default track is found, select the first audio track. + * If audio track with preferred language (e.g., "nor") is not found, selects "eng" or first available. + */ + private fun getAudioDefaultTrackToUse(audioStream: List): Int { + val durationFiltered = audioStream.filterOnPreferredLanguage() + .filter { (it.duration_ts ?: 0) > 0 } + + val selected = durationFiltered + .filter { it.channels == 2 }.ifEmpty { durationFiltered } + .maxByOrNull { it.index } ?: audioStream.minByOrNull { it.index } ?: durationFiltered.firstOrNull() + + return audioStream.indexOf(selected) + } + + /** + * Filters audio streams based on preferred languages. + * If no streams match the preferred languages, returns the original list. + */ + private fun List.filterOnPreferredLanguage(): List { + return this.filter { it.tags.language in getAudioLanguagePreference() }.ifEmpty { this } + } + + private fun getVideoTrackToUse(streams: List): Int { + val selectStream = streams.filter { (it.duration_ts ?: 0) > 0 } + .maxByOrNull { it.duration_ts ?: 0 } ?: streams.minByOrNull { it.index } ?: throw Exception("No video streams found") + return streams.indexOf(selectStream) + } +} \ No newline at end of file diff --git a/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaSelectExtractTracksListener.kt b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaSelectExtractTracksListener.kt new file mode 100644 index 00000000..ba7c90da --- /dev/null +++ b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaSelectExtractTracksListener.kt @@ -0,0 +1,44 @@ +package no.iktdev.mediaprocessing.coordinator.listeners.events + +import no.iktdev.eventi.events.EventListener +import no.iktdev.eventi.models.Event +import no.iktdev.mediaprocessing.ffmpeg.data.SubtitleStream +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.MediaTracksDetermineSubtitleTypeEvent +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.MediaTracksExtractSelectedEvent +import no.iktdev.mediaprocessing.shared.common.model.SubtitleType +import org.springframework.stereotype.Component + +@Component +class MediaSelectExtractTracksListener: EventListener() { + + fun limitToLanguages(): Set { + return emptySet() + } + + fun useTypes(): Set { + return setOf(SubtitleType.Dialogue) + } + + override fun onEvent( + event: Event, + history: List + ): Event? { + val useEvent = event as? MediaTracksDetermineSubtitleTypeEvent ?: return null + + val filtered = useEvent.subtitleTrackItems + .filter { it.type in useTypes() } + .map { it.stream } + .filterOnPreferredLanguage() + + return MediaTracksExtractSelectedEvent( + selectedSubtitleTracks = filtered.map { it.index } + ) + } + + + private fun List.filterOnPreferredLanguage(): List { + val languages = limitToLanguages() + if (languages.isEmpty()) return this + return this.filter { it.tags.language != null }.filter { languages.contains(it.tags.language) } + } +} \ No newline at end of file diff --git a/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/StartedListener.kt b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/StartedListener.kt new file mode 100644 index 00000000..00dde704 --- /dev/null +++ b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/StartedListener.kt @@ -0,0 +1,34 @@ +package no.iktdev.mediaprocessing.coordinator.listeners.events + +import no.iktdev.eventi.events.EventListener +import no.iktdev.eventi.models.Event +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.FileReadyEvent +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.OperationType +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.ProcessFlow +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.StartData +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.StartProcessingEvent +import org.springframework.core.annotation.Order +import org.springframework.stereotype.Component + +@Order(1) +@Component +class StartedListener : EventListener() { + override fun onEvent( + event: Event, + history: List + ): Event? { + val useEvent = event as? FileReadyEvent ?: return null + + return StartProcessingEvent( + data = StartData( + flow = ProcessFlow.Auto, + fileUri = useEvent.data.fileUri, + operation = setOf( + OperationType.Extract, + OperationType.Convert, + OperationType.Encode + ) + ) + ) + } +} \ No newline at end of file diff --git a/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/tasks/FfprobeTaskListener.kt b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/tasks/FfprobeTaskListener.kt new file mode 100644 index 00000000..a4ac39b3 --- /dev/null +++ b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/tasks/FfprobeTaskListener.kt @@ -0,0 +1,9 @@ +package no.iktdev.mediaprocessing.coordinator.listeners.tasks + +import no.iktdev.eventi.tasks.TaskListener +import no.iktdev.eventi.tasks.TaskType +import no.iktdev.mediaprocessing.ffmpeg.FFprobe + +abstract class FfprobeTaskListener(taskType: TaskType): TaskListener(taskType) { + abstract fun getFfprobe(): FFprobe +} \ No newline at end of file diff --git a/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/tasks/MediaStreamReadTaskListener.kt b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/tasks/MediaStreamReadTaskListener.kt new file mode 100644 index 00000000..f2bd4429 --- /dev/null +++ b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/tasks/MediaStreamReadTaskListener.kt @@ -0,0 +1,49 @@ +package no.iktdev.mediaprocessing.coordinator.listeners.tasks + +import mu.KotlinLogging +import no.iktdev.eventi.models.Event +import no.iktdev.eventi.models.Task +import no.iktdev.eventi.tasks.TaskType +import no.iktdev.mediaprocessing.coordinator.CoordinatorEnv +import no.iktdev.mediaprocessing.ffmpeg.FFprobe +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.MediaStreamReadEvent +import no.iktdev.mediaprocessing.shared.common.event_task_contract.tasks.MediaReadTask +import org.springframework.stereotype.Component +import java.util.UUID + +@Component +class MediaStreamReadTaskListener: FfprobeTaskListener(TaskType.CPU_INTENSIVE) { + val log = KotlinLogging.logger {} + + override fun getWorkerId(): String { + return "${this::class.java.simpleName}-${TaskType.CPU_INTENSIVE}-${UUID.randomUUID()}" + } + + override fun supports(task: Task): Boolean { + return task is MediaReadTask + } + + override suspend fun onTask(task: Task): Event? { + val pickedTask = task as? MediaReadTask ?: return null + try { + val probeResult = getFfprobe() + .readJsonStreams(pickedTask.fileUri) + + val result = probeResult.data + assert(result != null) { "No data returned from ffprobe for ${pickedTask.fileUri}" } + + return MediaStreamReadEvent(data = result!!).producedFrom(task) + + } catch (e: Exception) { + log.error(e) { "Error reading media streams for ${pickedTask.fileUri}" } + return null + } + } + + override fun getFfprobe(): FFprobe { + return JsonFfinfo(CoordinatorEnv.ffprobe) + } + + class JsonFfinfo(override val executable: String): FFprobe() { + } +} \ No newline at end of file diff --git a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/Resources.kt b/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/Resources.kt index 5ce6a1bb..20557dac 100644 --- a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/Resources.kt +++ b/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/Resources.kt @@ -1,8 +1,6 @@ package no.iktdev.mediaprocessing -import no.iktdev.mediaprocessing.shared.common.contract.Events -import no.iktdev.mediaprocessing.shared.common.contract.data.Event -import no.iktdev.mediaprocessing.shared.common.contract.jsonToEvent +import no.iktdev.eventi.models.Event import org.json.JSONArray enum class Files(val fileName: String) { @@ -28,9 +26,9 @@ fun Files.databaseJsonToEvents(): List { val obj = dataArray.getJSONObject(x) val eventType = obj.getString("event") val dataString = obj.getString("data") - dataString.jsonToEvent(eventType).also { - events.add(it) - } +// dataString.jsonToEvent(eventType).also { +// events.add(it) +// } } return events } diff --git a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/TestData.kt b/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/TestData.kt deleted file mode 100644 index 3a0eb00c..00000000 --- a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/TestData.kt +++ /dev/null @@ -1,59 +0,0 @@ -package no.iktdev.mediaprocessing.coordinator - -import no.iktdev.eventi.data.EventMetadata -import no.iktdev.eventi.data.EventStatus -import no.iktdev.mediaprocessing.coordinator.tasksV2.listeners.MetadataWaitOrDefaultTaskListener -import no.iktdev.mediaprocessing.shared.common.contract.data.* -import no.iktdev.mediaprocessing.shared.common.contract.dto.OperationEvents -import java.util.UUID - -val defaultReferenceId = UUID.randomUUID().toString() - -fun defaultStartEvent(): MediaProcessStartEvent { - return MediaProcessStartEvent( - metadata = defaultMetadata(), - data = StartEventData( - operations = listOf(OperationEvents.ENCODE, OperationEvents.EXTRACT, OperationEvents.CONVERT), - file = "DummyTestFile.mkv" - ) - ) -} - -fun defaultBaseInfoEvent(): BaseInfoEvent { - return BaseInfoEvent( - metadata = defaultMetadata(), - data = BaseInfo( - title = "Potetmos", - sanitizedName = "Potetmos mannen", - searchTitles = listOf("Potetmos mannen") - ) - ) -} - -fun metadataSearchTimedOutEvent(): MediaMetadataReceivedEvent { - return MediaMetadataReceivedEvent( - metadata = defaultMetadata() - .copy(status = EventStatus.Skipped) - .copy(source = MetadataWaitOrDefaultTaskListener::class.java.simpleName), - data = null - ) -} - -fun defaultMetadataSearchEvent(): MediaMetadataReceivedEvent { - return MediaMetadataReceivedEvent( - metadata = defaultMetadata(), - data = pyMetadata( - title = "Potetmos", - type = "movie", - ) - ) -} - - -fun defaultMetadata(): EventMetadata { - return EventMetadata( - referenceId = defaultReferenceId, - status = EventStatus.Success, - source = "TestData" - ) -} \ No newline at end of file diff --git a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/events/MediaEventParsedInfoListenerTest.kt b/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/events/MediaParsedInfoListenerTest.kt similarity index 98% rename from apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/events/MediaEventParsedInfoListenerTest.kt rename to apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/events/MediaParsedInfoListenerTest.kt index 0600ba38..70a0aaf2 100644 --- a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/events/MediaEventParsedInfoListenerTest.kt +++ b/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/events/MediaParsedInfoListenerTest.kt @@ -1,14 +1,14 @@ package no.iktdev.mediaprocessing.coordinator.events -import no.iktdev.mediaprocessing.coordinator.listeners.events.MediaEventParsedInfoListener -import no.iktdev.mediaprocessing.shared.common.parsing.FileNameParser +import no.iktdev.mediaprocessing.coordinator.listeners.events.MediaParsedInfoListener +import no.iktdev.mediaprocessing.shared.common.model.MediaType import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Named import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.MethodSource import java.io.File -class MediaEventParsedInfoListenerTest : MediaEventParsedInfoListener() { +class MediaParsedInfoListenerTest : MediaParsedInfoListener() { @MethodSource("parsedInfoTest") diff --git a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/reader/ParseVideoFileStreamsTest.kt b/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/reader/ParseVideoFileStreamsTest.kt deleted file mode 100644 index da0b05f9..00000000 --- a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/reader/ParseVideoFileStreamsTest.kt +++ /dev/null @@ -1,168 +0,0 @@ -package no.iktdev.mediaprocessing.coordinator.reader -/* -import com.google.gson.Gson -import com.google.gson.JsonObject -import no.iktdev.mediaprocessing.shared.kafka.core.CoordinatorProducer -import no.iktdev.mediaprocessing.shared.kafka.dto.events_result.ReaderPerformed -import no.iktdev.mediaprocessing.shared.kafka.dto.Status -import org.junit.jupiter.api.Test - -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.Named -import org.junit.jupiter.params.ParameterizedTest -import org.junit.jupiter.params.provider.MethodSource -import org.mockito.Mock -import org.skyscreamer.jsonassert.JSONAssert -import org.springframework.beans.factory.annotation.Autowired - -class ParseVideoFileStreamsTest { - - @Autowired - private lateinit var testBase: KafkaTestBase - - @Mock - lateinit var coordinatorProducer: CoordinatorProducer - - val parseVideoStreams = ParseVideoFileStreams(coordinatorProducer) - - @ParameterizedTest - @MethodSource("streams") - fun parseStreams(data: TestInfo) { - val gson = Gson() - val converted = gson.fromJson(data.input, JsonObject::class.java) - val result = parseVideoStreams.parseStreams(ReaderPerformed( - Status.COMPLETED, - file = "ignore", - output = converted - )) - JSONAssert.assertEquals( - data.expected, - gson.toJson(result), - false - ) - - } - - data class TestInfo( - val input: String, - val expected: String - ) - - companion object { - @JvmStatic - fun streams(): List> { - return listOf( - Named.of( - "Top Clown streams", TestInfo( - """ - { - "streams": [ - { - "index": 0, - "codec_name": "h264", - "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10", - "profile": "Main", - "codec_type": "video", - "codec_tag_string": "[0][0][0][0]", - "codec_tag": "0x0000", - "width": 1920, - "height": 1080, - "coded_width": 1920, - "coded_height": 1080, - "closed_captions": 0, - "film_grain": 0, - "has_b_frames": 0, - "sample_aspect_ratio": "1:1", - "display_aspect_ratio": "16:9", - "pix_fmt": "yuv420p", - "level": 40, - "chroma_location": "left", - "field_order": "progressive", - "refs": 1, - "is_avc": "true", - "nal_length_size": "4", - "r_frame_rate": "24000/1001", - "avg_frame_rate": "24000/1001", - "time_base": "1/1000", - "start_pts": 0, - "start_time": "0.000000", - "bits_per_raw_sample": "8", - "extradata_size": 55, - "disposition": { - "default": 1, - "dub": 0, - "original": 0, - "comment": 0, - "lyrics": 0, - "karaoke": 0, - "forced": 0, - "hearing_impaired": 0, - "visual_impaired": 0, - "clean_effects": 0, - "attached_pic": 0, - "timed_thumbnails": 0, - "non_diegetic": 0, - "captions": 0, - "descriptions": 0, - "metadata": 0, - "dependent": 0, - "still_image": 0 - } - }, - { - "index": 1, - "codec_name": "aac", - "codec_long_name": "AAC (Advanced Audio Coding)", - "profile": "HE-AAC", - "codec_type": "audio", - "codec_tag_string": "[0][0][0][0]", - "codec_tag": "0x0000", - "sample_fmt": "fltp", - "sample_rate": "48000", - "channels": 6, - "channel_layout": "5.1", - "bits_per_sample": 0, - "initial_padding": 0, - "r_frame_rate": "0/0", - "avg_frame_rate": "0/0", - "time_base": "1/1000", - "start_pts": 0, - "start_time": "0.000000", - "extradata_size": 2, - "disposition": { - "default": 1, - "dub": 0, - "original": 0, - "comment": 0, - "lyrics": 0, - "karaoke": 0, - "forced": 0, - "hearing_impaired": 0, - "visual_impaired": 0, - "clean_effects": 0, - "attached_pic": 0, - "timed_thumbnails": 0, - "non_diegetic": 0, - "captions": 0, - "descriptions": 0, - "metadata": 0, - "dependent": 0, - "still_image": 0 - }, - "tags": { - "language": "eng" - } - } - ] - } - - """.trimIndent(), - """ - - """.trimIndent() - ) - ) - ) - } - } -}*/ \ No newline at end of file diff --git a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasks/event/MetadataAndBaseInfoToFileOutTest.kt b/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasks/event/MetadataAndBaseInfoToFileOutTest.kt deleted file mode 100644 index c60971d2..00000000 --- a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasks/event/MetadataAndBaseInfoToFileOutTest.kt +++ /dev/null @@ -1,40 +0,0 @@ -package no.iktdev.mediaprocessing.coordinator.tasks.event - -import no.iktdev.mediaprocessing.coordinator.tasksV2.listeners.MediaOutInformationTaskListener -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test -/* -class MetadataAndBaseInfoToFileOutTest { - - fun testData(): String { - return """ - [ - {"type":"header","version":"5.2.1","comment":"Export to JSON plugin for PHPMyAdmin"}, - {"type":"database","name":"eventsV2"}, - {"type":"table","name":"events","database":"eventsV2","data": - [ - {"id":"9","referenceId":"f015ad8a-8210-4040-993b-bdaa5bd25d80","eventId":"3cea9a98-2e65-4e70-96bc-4b6933c06af7","event":"event:media-read-base-info:performed","data":"{\"status\":\"COMPLETED\",\"title\":\"Psycho-Pass Movie\",\"sanitizedName\":\"Psycho-Pass Movie - Providence\",\"derivedFromEventId\":\"62408248-d457-4f4d-a2c7-9b17e5701336\"}","created":"2024-04-15 22:24:07.406088"}, - {"id":"19","referenceId":"f015ad8a-8210-4040-993b-bdaa5bd25d80","eventId":"0edaa265-fc85-41bc-952a-acb21771feb9","event":"event:media-metadata-search:performed","data":"{\"status\":\"COMPLETED\",\"data\":{\"title\":\"Psycho-Pass Movie: Providence\",\"altTitle\":[],\"cover\":\"https:\/\/cdn.myanimelist.net\/images\/anime\/1244\/134653.jpg\",\"type\":\"movie\",\"summary\":[{\"summary\":\"In 2113, the Ministry of Foreign Affairs (MFA) dissolved their secret paramilitary unit known as the Peacebreakers. However, the squad disappeared, and their activities remained a mystery. Five years later, the Peacebreakers resurface when they murder Milcia Stronskaya, a scientist in possession of highly classified documents essential to the future of the Sybil System—Japan\\u0027s surveillance structure that detects potential criminals in society. To investigate the incident and prepare for a clash against the Peacebreakers in coordination with the MFA, the chief of the Public Safety Bureau decides to recruit the former Enforcer Shinya Kougami back into the force. Having defected years ago, Kougami currently works for the MFA under Frederica Hanashiro\\u0027s command. Kougami\\u0027s return creates tensions between him and his former colleagues Akane Tsunemori and Nobuchika Ginoza, but they must set aside their past grudges to focus on ensuring the security of the Sybil System. [Written by MAL Rewrite]\",\"language\":\"eng\"}],\"genres\":[\"Action\",\"Mystery\",\"Sci-Fi\",\"Suspense\"]}}","created":"2024-04-15 22:24:18.339106"} - ] - } - ] - """.trimIndent() - } - - @Test - fun testVideoData() { - val pmdj = PersistentMessageFromJsonDump(testData()) - val events = pmdj.getPersistentMessages() - - val baseInfo = events.lastOrSuccessOf(KafkaEvents.EventMediaReadBaseInfoPerformed) { it.data is BaseInfoPerformed }?.data as BaseInfoPerformed - val meta = events.lastOrSuccessOf(KafkaEvents.EventMediaMetadataSearchPerformed) { it.data is MetadataPerformed }?.data as MetadataPerformed? - - val pm = MediaOutInformationTaskListener.ProcessMediaInfoAndMetadata(baseInfo, meta) - - - val vi = pm.getVideoPayload() - - assertThat(vi).isNotNull() - } - -}*/ \ No newline at end of file diff --git a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasks/event/ffmpeg/EncodeArgumentCreatorTaskTest.kt b/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasks/event/ffmpeg/EncodeArgumentCreatorTaskTest.kt deleted file mode 100644 index 2cf883f6..00000000 --- a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasks/event/ffmpeg/EncodeArgumentCreatorTaskTest.kt +++ /dev/null @@ -1,80 +0,0 @@ -package no.iktdev.mediaprocessing.coordinator.tasks.event.ffmpeg - -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken -import no.iktdev.mediaprocessing.coordinator.tasksV2.mapping.streams.AudioArguments -import no.iktdev.mediaprocessing.shared.common.contract.ffmpeg.AudioPreference -import no.iktdev.mediaprocessing.shared.common.contract.ffmpeg.AudioStream -import no.iktdev.mediaprocessing.shared.common.contract.ffmpeg.ParsedMediaStreams -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.Test - -class EncodeArgumentCreatorTaskTest { - - @Test - fun verifyThatEacStreamGetsCorrectArguments() { - val audio = AudioArguments( - audioStream = audioStreamsEAC().first(), - allStreams = ParsedMediaStreams(listOf(), audioStreamsEAC(), listOf()), - preference = AudioPreference(preserveChannels = true, forceStereo = false, convertToEac3OnUnsupportedSurround = true) - ) - val arguments = audio.getAudioArguments() - assertThat(arguments.codecParameters).isEqualTo(listOf("-acodec", "copy")) - - } - - private fun audioStreamsEAC(): List { - //language=json - val streams = """ - - [ - { - "index": 1, - "codec_name": "eac3", - "codec_long_name": "ATSC A/52B (AC-3, E-AC-3)", - "codec_type": "audio", - "codec_tag_string": "[0][0][0][0]", - "codec_tag": "0x0000", - "r_frame_rate": "0/0", - "avg_frame_rate": "0/0", - "time_base": "1/1000", - "start_pts": 0, - "start_time": "0.000000", - "disposition": { - "default": 1, - "dub": 0, - "original": 0, - "comment": 0, - "lyrics": 0, - "karaoke": 0, - "forced": 0, - "hearing_impaired": 0, - "visual_impaired": 0, - "clean_effects": 0, - "attached_pic": 0, - "timed_thumbnails": 0 - }, - "tags": { - "BPS": "256000", - "DURATION": "01:09:55.296000000", - "NUMBER_OF_FRAMES": "131103", - "NUMBER_OF_BYTES": "134249472", - "_STATISTICS_WRITING_APP": "64-bit", - "_STATISTICS_TAGS": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES", - "language": "eng" - }, - "sample_fmt": "fltp", - "sample_rate": "48000", - "channels": 6, - "bits_per_sample": 0 - } - ] - - """.trimIndent() - val type = object : TypeToken>() {}.type - return Gson().fromJson>(streams, type) - } - - -} \ No newline at end of file diff --git a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/listeners/ConvertWorkTaskListenerTest.kt b/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/listeners/ConvertWorkTaskListenerTest.kt deleted file mode 100644 index 945a9b14..00000000 --- a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/listeners/ConvertWorkTaskListenerTest.kt +++ /dev/null @@ -1,77 +0,0 @@ -package no.iktdev.mediaprocessing.coordinator.tasksV2.listeners - -import no.iktdev.eventi.data.referenceId -import no.iktdev.mediaprocessing.Files -import no.iktdev.mediaprocessing.coordinator.defaultMetadata -import no.iktdev.mediaprocessing.coordinator.defaultStartEvent -import no.iktdev.mediaprocessing.databaseJsonToEvents -import no.iktdev.mediaprocessing.shared.common.contract.Events -import no.iktdev.mediaprocessing.shared.common.contract.data.Event -import no.iktdev.mediaprocessing.shared.common.contract.data.StartEventData -import no.iktdev.mediaprocessing.shared.common.contract.dto.OperationEvents -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.DisplayName -import org.junit.jupiter.api.Test - - -class ConvertWorkTaskListenerTest { - - @Test - @DisplayName(""" - When incoming event is of Start Event, and there is multiple operations, - Validation check should fail - """) - fun validate_shouldIProcessAndHandleEvent1() { - val listener = ConvertWorkTaskListener() - val events = listOf(defaultStartEvent()) - val result = listener.shouldIProcessAndHandleEvent(defaultStartEvent(), events) - assertThat(result).isFalse() - } - @Test - @DisplayName(""" - When incoming event is of Start Event but start is missing form list, and there is multiple operations, - Validation check should fail - """) - fun validate_shouldIProcessAndHandleEvent2() { - val listener = ConvertWorkTaskListener() - val result = listener.shouldIProcessAndHandleEvent(defaultStartEvent(), emptyList()) - assertThat(result).isFalse() - } - - @Test - @DisplayName(""" - When incoming event is of Start Event and single operation is Convert, - Validation check should succeed - """) - fun validate_shouldIProcessAndHandleEvent3() { - val listener = ConvertWorkTaskListener() - val startedEvent = defaultStartEvent().copy( - data = StartEventData( - operations = listOf(OperationEvents.CONVERT), - file = "DummyTestFile.ass" - ) - ) - val events = listOf(startedEvent) - val result = listener.shouldIProcessAndHandleEvent(startedEvent, events) - assertThat(result).isTrue() - } - - - - @Test - fun validateParsingOfEvents() { - val content = Files.MultipleLanguageBased.databaseJsonToEvents() - assertThat(content).isNotEmpty() - val referenceId = content.firstOrNull()?.referenceId() - assertThat(referenceId).isNotNull() - } - - @Test - fun validateCreationOfConvertTasks() { - val listener: ConvertWorkTaskListener = ConvertWorkTaskListener() - val content = Files.MultipleLanguageBased.databaseJsonToEvents().filter { it.eventType in listOf( Events.ExtractTaskCompleted, Events.ProcessStarted, Events.ConvertTaskCreated, Events.ConvertTaskCompleted) } - assertThat(listener).isNotNull() - val success = content.map { listener.shouldIProcessAndHandleEvent(it, content) to it } - assertThat(success.filter { it.first }.size).isGreaterThan(3) - } -} \ No newline at end of file diff --git a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/listeners/MetadataWaitOrDefaultTaskListenerTest.kt b/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/listeners/MetadataWaitOrDefaultTaskListenerTest.kt deleted file mode 100644 index f6fdb268..00000000 --- a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/listeners/MetadataWaitOrDefaultTaskListenerTest.kt +++ /dev/null @@ -1,52 +0,0 @@ -package no.iktdev.mediaprocessing.coordinator.tasksV2.listeners - -import no.iktdev.mediaprocessing.coordinator.defaultBaseInfoEvent -import no.iktdev.mediaprocessing.coordinator.defaultMetadataSearchEvent -import no.iktdev.mediaprocessing.coordinator.defaultStartEvent -import no.iktdev.mediaprocessing.coordinator.metadataSearchTimedOutEvent -import no.iktdev.mediaprocessing.shared.common.contract.data.Event -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.DisplayName -import org.junit.jupiter.api.Test - -class MetadataWaitOrDefaultTaskListenerTest { - - @Test - @DisplayName(""" - When incoming event is of base name, and there is no search performed, - Validation check should proceed - """) - fun validate_shouldIProcessAndHandleEvent1() { - val listener = MetadataWaitOrDefaultTaskListener() - val events = listOf(defaultStartEvent(), defaultBaseInfoEvent()) - val result = listener.shouldIProcessAndHandleEvent(incomingEvent = events.last(), events) - assertTrue(result) - } - - @Test - @DisplayName(""" - When incoming event is of MetadataReceivedEvent, - And timeout listener is the origin, - Then validation should abort - """) - fun validate_shouldIProcessAndHandleEvent2() { - val listener = MetadataWaitOrDefaultTaskListener() - val events = listOf(defaultStartEvent(), defaultBaseInfoEvent(), metadataSearchTimedOutEvent()) - val result = listener.shouldIProcessAndHandleEvent(incomingEvent = events.last(), events) - assertFalse(result) - } - - @Test - @DisplayName(""" - When incoming event is of MetadataReceivedEvent, - And metadata service has produced the event, - Then validation should allow, due to cleanup - """) - fun validate_shouldIProcessAndHandleEvent3() { - val listener = MetadataWaitOrDefaultTaskListener() - val events = listOf(defaultStartEvent(), defaultBaseInfoEvent(), defaultMetadataSearchEvent()) - val result = listener.shouldIProcessAndHandleEvent(incomingEvent = events.last(), events) - assertTrue(result) - } - -} \ No newline at end of file diff --git a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/listeners/ParseMediaFileStreamsTaskListenerTest.kt b/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/listeners/ParseMediaFileStreamsTaskListenerTest.kt deleted file mode 100644 index 3d944014..00000000 --- a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/listeners/ParseMediaFileStreamsTaskListenerTest.kt +++ /dev/null @@ -1,25 +0,0 @@ -package no.iktdev.mediaprocessing.coordinator.tasksV2.listeners - -import com.google.gson.JsonObject -import no.iktdev.eventi.data.dataAs -import no.iktdev.mediaprocessing.shared.common.contract.Events -import no.iktdev.mediaprocessing.shared.common.contract.jsonToEvent -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test - -class ParseMediaFileStreamsTaskListenerTest { - - @Test - fun testParse() { - val event = data.jsonToEvent(Events.StreamRead.name).dataAs() - - val parser = ParseMediaFileStreamsTaskListener() - val result = parser.parseStreams(event) - assertThat(result.videoStream).hasSize(1) - } - -} - -val data = """ - {"metadata":{"derivedFromEventId":"c2dfe881-db0d-4f1f-a6a5-5e556ebbccaf","eventId":"308267f3-6eee-4250-9dc4-0f54edb1a120","referenceId":"d8be4d8f-64c2-4df1-aed1-e66cdc7163b4","status":"Success","created":"2024-07-19T18:45:31.048489577"},"data":{"streams":[{"index":0,"codec_name":"hevc","codec_long_name":"H.265 / HEVC (High Efficiency Video Coding)","profile":"Main 10","codec_type":"video","codec_time_base":"1001/24000","codec_tag_string":"[0][0][0][0]","codec_tag":"0x0000","width":1920,"height":1080,"coded_width":1920,"coded_height":1080,"closed_captions":0,"has_b_frames":2,"sample_aspect_ratio":"1:1","display_aspect_ratio":"16:9","pix_fmt":"yuv420p10le","level":120,"color_range":"tv","color_space":"bt709","color_transfer":"bt709","color_primaries":"bt709","refs":1,"r_frame_rate":"24000/1001","avg_frame_rate":"24000/1001","time_base":"1/1000","start_pts":0,"start_time":"0.000000","disposition":{"default":1,"dub":0,"original":0,"comment":0,"lyrics":0,"karaoke":0,"forced":0,"hearing_impaired":0,"visual_impaired":0,"clean_effects":0,"attached_pic":0,"timed_thumbnails":0},"tags":{"title":"Presented By EMBER","BPS":"1288407","DURATION":"00:23:40.002000000","NUMBER_OF_FRAMES":"34046","NUMBER_OF_BYTES":"228692681","_STATISTICS_WRITING_APP":"mkvmerge v69.0.0 (\u0027Day And Age\u0027) 64-bit","_STATISTICS_WRITING_DATE_UTC":"2024-01-06 04:19:11","_STATISTICS_TAGS":"BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES"}},{"index":1,"codec_name":"eac3","codec_long_name":"ATSC A/52B (AC-3, E-AC-3)","codec_type":"audio","codec_time_base":"1/48000","codec_tag_string":"[0][0][0][0]","codec_tag":"0x0000","sample_fmt":"fltp","sample_rate":"48000","channels":2,"bits_per_sample":0,"dmix_mode":"-1","ltrt_cmixlev":"-1.000000","ltrt_surmixlev":"-1.000000","loro_cmixlev":"-1.000000","loro_surmixlev":"-1.000000","r_frame_rate":"0/0","avg_frame_rate":"0/0","time_base":"1/1000","start_pts":12,"start_time":"0.012000","disposition":{"default":1,"dub":0,"original":0,"comment":0,"lyrics":0,"karaoke":0,"forced":0,"hearing_impaired":0,"visual_impaired":0,"clean_effects":0,"attached_pic":0,"timed_thumbnails":0},"tags":{"language":"jpn","BPS":"128000","DURATION":"00:23:41.088000000","NUMBER_OF_FRAMES":"44409","NUMBER_OF_BYTES":"22737408","_STATISTICS_WRITING_APP":"mkvmerge v69.0.0 (\u0027Day And Age\u0027) 64-bit","_STATISTICS_WRITING_DATE_UTC":"2024-01-06 04:19:11","_STATISTICS_TAGS":"BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES"}},{"index":2,"codec_name":"ass","codec_long_name":"ASS (Advanced SSA) subtitle","codec_type":"subtitle","codec_time_base":"0/1","codec_tag_string":"[0][0][0][0]","codec_tag":"0x0000","r_frame_rate":"0/0","avg_frame_rate":"0/0","time_base":"1/1000","start_pts":0,"start_time":"0.000000","duration_ts":1421100,"duration":"1421.100000","disposition":{"default":1,"dub":0,"original":0,"comment":0,"lyrics":0,"karaoke":0,"forced":0,"hearing_impaired":0,"visual_impaired":0,"clean_effects":0,"attached_pic":0,"timed_thumbnails":0},"tags":{"language":"eng","BPS":"110","DURATION":"00:21:01.710000000","NUMBER_OF_FRAMES":"255","NUMBER_OF_BYTES":"17392","_STATISTICS_WRITING_APP":"mkvmerge v69.0.0 (\u0027Day And Age\u0027) 64-bit","_STATISTICS_WRITING_DATE_UTC":"2024-01-06 04:19:11","_STATISTICS_TAGS":"BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES"}},{"index":3,"codec_name":"mjpeg","codec_long_name":"Motion JPEG","profile":"Progressive","codec_type":"video","codec_time_base":"0/1","codec_tag_string":"[0][0][0][0]","codec_tag":"0x0000","width":400,"height":567,"coded_width":400,"coded_height":567,"closed_captions":0,"has_b_frames":0,"sample_aspect_ratio":"1:1","display_aspect_ratio":"400:567","pix_fmt":"yuvj444p","level":-99,"color_range":"pc","color_space":"bt470bg","chroma_location":"center","refs":1,"r_frame_rate":"90000/1","avg_frame_rate":"0/0","time_base":"1/90000","start_pts":0,"start_time":"0.000000","duration_ts":127899000,"duration":"1421.100000","bits_per_raw_sample":"8","disposition":{"default":0,"dub":0,"original":0,"comment":0,"lyrics":0,"karaoke":0,"forced":0,"hearing_impaired":0,"visual_impaired":0,"clean_effects":0,"attached_pic":1,"timed_thumbnails":0},"tags":{"filename":"cover.jpg","mimetype":"image/jpeg"}},{"index":4,"codec_name":"ttf","codec_long_name":"TrueType font","codec_type":"attachment","codec_tag_string":"[0][0][0][0]","codec_tag":"0x0000","r_frame_rate":"0/0","avg_frame_rate":"0/0","time_base":"1/90000","start_pts":0,"start_time":"0.000000","duration_ts":127899000,"duration":"1421.100000","disposition":{"default":0,"dub":0,"original":0,"comment":0,"lyrics":0,"karaoke":0,"forced":0,"hearing_impaired":0,"visual_impaired":0,"clean_effects":0,"attached_pic":0,"timed_thumbnails":0},"tags":{"filename":"Roboto-Medium.ttf","mimetype":"application/x-truetype-font"}},{"index":5,"codec_name":"ttf","codec_long_name":"TrueType font","codec_type":"attachment","codec_tag_string":"[0][0][0][0]","codec_tag":"0x0000","r_frame_rate":"0/0","avg_frame_rate":"0/0","time_base":"1/90000","start_pts":0,"start_time":"0.000000","duration_ts":127899000,"duration":"1421.100000","disposition":{"default":0,"dub":0,"original":0,"comment":0,"lyrics":0,"karaoke":0,"forced":0,"hearing_impaired":0,"visual_impaired":0,"clean_effects":0,"attached_pic":0,"timed_thumbnails":0},"tags":{"filename":"Roboto-MediumItalic.ttf","mimetype":"application/x-truetype-font"}},{"index":6,"codec_name":"ttf","codec_long_name":"TrueType font","codec_type":"attachment","codec_tag_string":"[0][0][0][0]","codec_tag":"0x0000","r_frame_rate":"0/0","avg_frame_rate":"0/0","time_base":"1/90000","start_pts":0,"start_time":"0.000000","duration_ts":127899000,"duration":"1421.100000","disposition":{"default":0,"dub":0,"original":0,"comment":0,"lyrics":0,"karaoke":0,"forced":0,"hearing_impaired":0,"visual_impaired":0,"clean_effects":0,"attached_pic":0,"timed_thumbnails":0},"tags":{"filename":"arial.ttf","mimetype":"application/x-truetype-font"}},{"index":7,"codec_name":"ttf","codec_long_name":"TrueType font","codec_type":"attachment","codec_tag_string":"[0][0][0][0]","codec_tag":"0x0000","r_frame_rate":"0/0","avg_frame_rate":"0/0","time_base":"1/90000","start_pts":0,"start_time":"0.000000","duration_ts":127899000,"duration":"1421.100000","disposition":{"default":0,"dub":0,"original":0,"comment":0,"lyrics":0,"karaoke":0,"forced":0,"hearing_impaired":0,"visual_impaired":0,"clean_effects":0,"attached_pic":0,"timed_thumbnails":0},"tags":{"filename":"arialbd.ttf","mimetype":"application/x-truetype-font"}},{"index":8,"codec_name":"ttf","codec_long_name":"TrueType font","codec_type":"attachment","codec_tag_string":"[0][0][0][0]","codec_tag":"0x0000","r_frame_rate":"0/0","avg_frame_rate":"0/0","time_base":"1/90000","start_pts":0,"start_time":"0.000000","duration_ts":127899000,"duration":"1421.100000","disposition":{"default":0,"dub":0,"original":0,"comment":0,"lyrics":0,"karaoke":0,"forced":0,"hearing_impaired":0,"visual_impaired":0,"clean_effects":0,"attached_pic":0,"timed_thumbnails":0},"tags":{"filename":"comic.ttf","mimetype":"application/x-truetype-font"}},{"index":9,"codec_name":"ttf","codec_long_name":"TrueType font","codec_type":"attachment","codec_tag_string":"[0][0][0][0]","codec_tag":"0x0000","r_frame_rate":"0/0","avg_frame_rate":"0/0","time_base":"1/90000","start_pts":0,"start_time":"0.000000","duration_ts":127899000,"duration":"1421.100000","disposition":{"default":0,"dub":0,"original":0,"comment":0,"lyrics":0,"karaoke":0,"forced":0,"hearing_impaired":0,"visual_impaired":0,"clean_effects":0,"attached_pic":0,"timed_thumbnails":0},"tags":{"filename":"comicbd.ttf","mimetype":"application/x-truetype-font"}},{"index":10,"codec_name":"ttf","codec_long_name":"TrueType font","codec_type":"attachment","codec_tag_string":"[0][0][0][0]","codec_tag":"0x0000","r_frame_rate":"0/0","avg_frame_rate":"0/0","time_base":"1/90000","start_pts":0,"start_time":"0.000000","duration_ts":127899000,"duration":"1421.100000","disposition":{"default":0,"dub":0,"original":0,"comment":0,"lyrics":0,"karaoke":0,"forced":0,"hearing_impaired":0,"visual_impaired":0,"clean_effects":0,"attached_pic":0,"timed_thumbnails":0},"tags":{"filename":"times.ttf","mimetype":"application/x-truetype-font"}},{"index":11,"codec_name":"ttf","codec_long_name":"TrueType font","codec_type":"attachment","codec_tag_string":"[0][0][0][0]","codec_tag":"0x0000","r_frame_rate":"0/0","avg_frame_rate":"0/0","time_base":"1/90000","start_pts":0,"start_time":"0.000000","duration_ts":127899000,"duration":"1421.100000","disposition":{"default":0,"dub":0,"original":0,"comment":0,"lyrics":0,"karaoke":0,"forced":0,"hearing_impaired":0,"visual_impaired":0,"clean_effects":0,"attached_pic":0,"timed_thumbnails":0},"tags":{"filename":"timesbd.ttf","mimetype":"application/x-truetype-font"}},{"index":12,"codec_name":"ttf","codec_long_name":"TrueType font","codec_type":"attachment","codec_tag_string":"[0][0][0][0]","codec_tag":"0x0000","r_frame_rate":"0/0","avg_frame_rate":"0/0","time_base":"1/90000","start_pts":0,"start_time":"0.000000","duration_ts":127899000,"duration":"1421.100000","disposition":{"default":0,"dub":0,"original":0,"comment":0,"lyrics":0,"karaoke":0,"forced":0,"hearing_impaired":0,"visual_impaired":0,"clean_effects":0,"attached_pic":0,"timed_thumbnails":0},"tags":{"filename":"trebuc.ttf","mimetype":"application/x-truetype-font"}},{"index":13,"codec_name":"ttf","codec_long_name":"TrueType font","codec_type":"attachment","codec_tag_string":"[0][0][0][0]","codec_tag":"0x0000","r_frame_rate":"0/0","avg_frame_rate":"0/0","time_base":"1/90000","start_pts":0,"start_time":"0.000000","duration_ts":127899000,"duration":"1421.100000","disposition":{"default":0,"dub":0,"original":0,"comment":0,"lyrics":0,"karaoke":0,"forced":0,"hearing_impaired":0,"visual_impaired":0,"clean_effects":0,"attached_pic":0,"timed_thumbnails":0},"tags":{"filename":"trebucbd.ttf","mimetype":"application/x-truetype-font"}},{"index":14,"codec_name":"ttf","codec_long_name":"TrueType font","codec_type":"attachment","codec_tag_string":"[0][0][0][0]","codec_tag":"0x0000","r_frame_rate":"0/0","avg_frame_rate":"0/0","time_base":"1/90000","start_pts":0,"start_time":"0.000000","duration_ts":127899000,"duration":"1421.100000","disposition":{"default":0,"dub":0,"original":0,"comment":0,"lyrics":0,"karaoke":0,"forced":0,"hearing_impaired":0,"visual_impaired":0,"clean_effects":0,"attached_pic":0,"timed_thumbnails":0},"tags":{"filename":"verdana.ttf","mimetype":"application/x-truetype-font"}},{"index":15,"codec_name":"ttf","codec_long_name":"TrueType font","codec_type":"attachment","codec_tag_string":"[0][0][0][0]","codec_tag":"0x0000","r_frame_rate":"0/0","avg_frame_rate":"0/0","time_base":"1/90000","start_pts":0,"start_time":"0.000000","duration_ts":127899000,"duration":"1421.100000","disposition":{"default":0,"dub":0,"original":0,"comment":0,"lyrics":0,"karaoke":0,"forced":0,"hearing_impaired":0,"visual_impaired":0,"clean_effects":0,"attached_pic":0,"timed_thumbnails":0},"tags":{"filename":"verdanab.ttf","mimetype":"application/x-truetype-font"}},{"index":16,"codec_name":"ttf","codec_long_name":"TrueType font","codec_type":"attachment","codec_tag_string":"[0][0][0][0]","codec_tag":"0x0000","r_frame_rate":"0/0","avg_frame_rate":"0/0","time_base":"1/90000","start_pts":0,"start_time":"0.000000","duration_ts":127899000,"duration":"1421.100000","disposition":{"default":0,"dub":0,"original":0,"comment":0,"lyrics":0,"karaoke":0,"forced":0,"hearing_impaired":0,"visual_impaired":0,"clean_effects":0,"attached_pic":0,"timed_thumbnails":0},"tags":{"filename":"CONSOLA.TTF","mimetype":"application/x-truetype-font"}},{"index":17,"codec_name":"ttf","codec_long_name":"TrueType font","codec_type":"attachment","codec_tag_string":"[0][0][0][0]","codec_tag":"0x0000","r_frame_rate":"0/0","avg_frame_rate":"0/0","time_base":"1/90000","start_pts":0,"start_time":"0.000000","duration_ts":127899000,"duration":"1421.100000","disposition":{"default":0,"dub":0,"original":0,"comment":0,"lyrics":0,"karaoke":0,"forced":0,"hearing_impaired":0,"visual_impaired":0,"clean_effects":0,"attached_pic":0,"timed_thumbnails":0},"tags":{"filename":"CONSOLAB.TTF","mimetype":"application/x-truetype-font"}},{"index":18,"codec_name":"png","codec_long_name":"PNG (Portable Network Graphics) image","codec_type":"video","codec_time_base":"0/1","codec_tag_string":"[0][0][0][0]","codec_tag":"0x0000","width":350,"height":197,"coded_width":350,"coded_height":197,"closed_captions":0,"has_b_frames":0,"sample_aspect_ratio":"1:1","display_aspect_ratio":"350:197","pix_fmt":"rgb24","level":-99,"color_range":"pc","refs":1,"r_frame_rate":"90000/1","avg_frame_rate":"0/0","time_base":"1/90000","start_pts":0,"start_time":"0.000000","duration_ts":127899000,"duration":"1421.100000","disposition":{"default":0,"dub":0,"original":0,"comment":0,"lyrics":0,"karaoke":0,"forced":0,"hearing_impaired":0,"visual_impaired":0,"clean_effects":0,"attached_pic":1,"timed_thumbnails":0},"tags":{"filename":"cover.png","mimetype":"image/png"}}]},"eventType":"EventMediaReadStreamPerformed"} -""".trimIndent() \ No newline at end of file diff --git a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/mapping/EncodeWorkArgumentsMappingTest.kt b/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/mapping/EncodeWorkArgumentsMappingTest.kt deleted file mode 100644 index e21068e2..00000000 --- a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/mapping/EncodeWorkArgumentsMappingTest.kt +++ /dev/null @@ -1,31 +0,0 @@ -package no.iktdev.mediaprocessing.coordinator.tasksV2.mapping - -import no.iktdev.mediaprocessing.shared.common.contract.Events -import no.iktdev.mediaprocessing.shared.common.contract.data.MediaFileStreamsParsedEvent -import no.iktdev.mediaprocessing.shared.common.contract.data.az -import no.iktdev.mediaprocessing.shared.common.contract.ffmpeg.AudioPreference -import no.iktdev.mediaprocessing.shared.common.contract.ffmpeg.EncodingPreference -import no.iktdev.mediaprocessing.shared.common.contract.ffmpeg.VideoPreference -import no.iktdev.mediaprocessing.shared.common.contract.jsonToEvent -import org.junit.jupiter.api.Test - -class EncodeWorkArgumentsMappingTest { - - @Test - fun parse() { - val event = data.jsonToEvent(Events.StreamParsed.name) - val parser = EncodeWorkArgumentsMapping( - "potato.mkv", - "potato.mp4", - event.az()!!.data!!, - EncodingPreference(VideoPreference(), AudioPreference()) - ) - val result = parser.getArguments() - } - - -} - -val data = """ - {"metadata":{"derivedFromEventId":"308267f3-6eee-4250-9dc4-0f54edb1a120","eventId":"51970dfb-790d-48e4-a37e-ae0915bb1b70","referenceId":"d8be4d8f-64c2-4df1-aed1-e66cdc7163b4","status":"Success","created":"2024-07-19T18:45:31.956816095"},"data":{"videoStream":[{"index":0,"codec_name":"hevc","codec_long_name":"H.265 / HEVC (High Efficiency Video Coding)","codec_type":"video","codec_tag_string":"[0][0][0][0]","codec_tag":"0x0000","r_frame_rate":"24000/1001","avg_frame_rate":"24000/1001","time_base":"1/1000","start_pts":0,"start_time":"0.000000","disposition":{"default":1,"dub":0,"original":0,"comment":0,"lyrics":0,"karaoke":0,"forced":0,"hearing_impaired":0,"visual_impaired":0,"clean_effects":0,"attached_pic":0,"timed_thumbnails":0},"tags":{"title":"Presented By EMBER","BPS":"1288407","DURATION":"00:23:40.002000000","NUMBER_OF_FRAMES":"34046","NUMBER_OF_BYTES":"228692681","_STATISTICS_WRITING_APP":"mkvmerge v69.0.0 (\u0027Day And Age\u0027) 64-bit","_STATISTICS_WRITING_DATE_UTC":"2024-01-06 04:19:11","_STATISTICS_TAGS":"BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES"},"profile":"Main 10","width":1920,"height":1080,"coded_width":1920,"coded_height":1080,"closed_captions":0,"has_b_frames":2,"sample_aspect_ratio":"1:1","display_aspect_ratio":"16:9","pix_fmt":"yuv420p10le","level":120,"color_range":"tv","color_space":"bt709","color_transfer":"bt709","color_primaries":"bt709","refs":1},{"index":18,"codec_name":"png","codec_long_name":"PNG (Portable Network Graphics) image","codec_type":"video","codec_tag_string":"[0][0][0][0]","codec_tag":"0x0000","r_frame_rate":"90000/1","avg_frame_rate":"0/0","time_base":"1/90000","start_pts":0,"start_time":"0.000000","disposition":{"default":0,"dub":0,"original":0,"comment":0,"lyrics":0,"karaoke":0,"forced":0,"hearing_impaired":0,"visual_impaired":0,"clean_effects":0,"attached_pic":1,"timed_thumbnails":0},"tags":{"filename":"cover.png","mimetype":"image/png"},"duration":"1421.100000","duration_ts":127899000,"width":350,"height":197,"coded_width":350,"coded_height":197,"closed_captions":0,"has_b_frames":0,"sample_aspect_ratio":"1:1","display_aspect_ratio":"350:197","pix_fmt":"rgb24","level":-99,"color_range":"pc","refs":1}],"audioStream":[{"index":1,"codec_name":"eac3","codec_long_name":"ATSC A/52B (AC-3, E-AC-3)","codec_type":"audio","codec_tag_string":"[0][0][0][0]","codec_tag":"0x0000","r_frame_rate":"0/0","avg_frame_rate":"0/0","time_base":"1/1000","start_pts":12,"start_time":"0.012000","disposition":{"default":1,"dub":0,"original":0,"comment":0,"lyrics":0,"karaoke":0,"forced":0,"hearing_impaired":0,"visual_impaired":0,"clean_effects":0,"attached_pic":0,"timed_thumbnails":0},"tags":{"BPS":"128000","DURATION":"00:23:41.088000000","NUMBER_OF_FRAMES":"44409","NUMBER_OF_BYTES":"22737408","_STATISTICS_WRITING_APP":"mkvmerge v69.0.0 (\u0027Day And Age\u0027) 64-bit","_STATISTICS_WRITING_DATE_UTC":"2024-01-06 04:19:11","_STATISTICS_TAGS":"BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES","language":"jpn"},"sample_fmt":"fltp","sample_rate":"48000","channels":2,"bits_per_sample":0}],"subtitleStream":[{"index":2,"codec_name":"ass","codec_long_name":"ASS (Advanced SSA) subtitle","codec_type":"subtitle","codec_tag_string":"[0][0][0][0]","codec_tag":"0x0000","r_frame_rate":"0/0","avg_frame_rate":"0/0","time_base":"1/1000","start_pts":0,"start_time":"0.000000","duration":"1421.100000","duration_ts":1421100,"disposition":{"default":1,"dub":0,"original":0,"comment":0,"lyrics":0,"karaoke":0,"forced":0,"hearing_impaired":0,"visual_impaired":0,"clean_effects":0,"attached_pic":0,"timed_thumbnails":0},"tags":{"BPS":"110","DURATION":"00:21:01.710000000","NUMBER_OF_FRAMES":"255","NUMBER_OF_BYTES":"17392","_STATISTICS_WRITING_APP":"mkvmerge v69.0.0 (\u0027Day And Age\u0027) 64-bit","_STATISTICS_WRITING_DATE_UTC":"2024-01-06 04:19:11","_STATISTICS_TAGS":"BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES","language":"eng"}}]},"eventType":"EventMediaParseStreamPerformed"} -""".trimIndent() \ No newline at end of file diff --git a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/mapping/streams/AudioArgumentsTest.kt b/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/mapping/streams/AudioArgumentsTest.kt deleted file mode 100644 index 19cef82e..00000000 --- a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/mapping/streams/AudioArgumentsTest.kt +++ /dev/null @@ -1,256 +0,0 @@ -package no.iktdev.mediaprocessing.coordinator.tasksV2.mapping.streams - -import com.google.gson.Gson -import no.iktdev.mediaprocessing.shared.common.Preference -import no.iktdev.mediaprocessing.shared.common.contract.ffmpeg.AudioPreference -import no.iktdev.mediaprocessing.shared.common.contract.ffmpeg.AudioStream -import no.iktdev.mediaprocessing.shared.common.contract.ffmpeg.ParsedMediaStreams -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.Test - -class AudioArgumentsTest { - - @Test - fun validateChecks1() { - val data = Gson().fromJson(ac3AudioStreamJson, AudioStream::class.java) - val audioArguments = AudioArguments( - allStreams = ParsedMediaStreams( - audioStream = listOf(data), videoStream = emptyList(), subtitleStream = emptyList()), - preference = Preference.getPreference().encodePreference.audio, - audioStream = data - ) - assertThat(audioArguments.isAudioCodecEqual()).isFalse() - assertThat(audioArguments.isSurround()).isTrue() - - assertThat(audioArguments.getAudioArguments().codecParameters).isEqualTo(listOf("-acodec", "copy")) - } - - @Test - fun validateChecks2() { - val data = Gson().fromJson(eac3AudioStreamJson, AudioStream::class.java) - val audioArguments = AudioArguments( - allStreams = ParsedMediaStreams( - audioStream = listOf(data), videoStream = emptyList(), subtitleStream = emptyList()), - preference = Preference.getPreference().encodePreference.audio, - audioStream = data - ) - assertThat(audioArguments.isAudioCodecEqual()).isFalse() - assertThat(audioArguments.isSurround()).isTrue() - - assertThat(audioArguments.getAudioArguments().codecParameters).isEqualTo(listOf("-acodec", "copy")) - } - - @Test - fun validateChecks3() { - val data = Gson().fromJson(aacSurroundAudioStreamJson, AudioStream::class.java) - val audioArguments = AudioArguments( - allStreams = ParsedMediaStreams( - audioStream = listOf(data), videoStream = emptyList(), subtitleStream = emptyList()), - preference = Preference.getPreference().encodePreference.audio, - audioStream = data - ) - assertThat(audioArguments.isAudioCodecEqual()).isTrue() - assertThat(audioArguments.isSurround()).isTrue() - - assertThat(audioArguments.getAudioArguments().codecParameters).isEqualTo(listOf("-c:a", "eac3")) - } - - @Test - fun validateChecks4() { - val data = Gson().fromJson(aacStereoAudioStreamJson, AudioStream::class.java) - val audioArguments = AudioArguments( - allStreams = ParsedMediaStreams( - audioStream = listOf(data), videoStream = emptyList(), subtitleStream = emptyList()), - preference = Preference.getPreference().encodePreference.audio, - audioStream = data - ) - assertThat(audioArguments.isAudioCodecEqual()).isTrue() - assertThat(audioArguments.isSurround()).isFalse() - - assertThat(audioArguments.getAudioArguments().codecParameters).isEqualTo(listOf("-acodec", "copy")) - } - - - val ac3AudioStreamJson = """ - { - "index": 1, - "codec_name": "ac3", - "codec_long_name": "ATSC A/52A (AC-3)", - "codec_type": "audio", - "codec_tag_string": "[0][0][0][0]", - "codec_tag": "0x0000", - "sample_fmt": "fltp", - "sample_rate": "48000", - "channels": 6, - "channel_layout": "5.1(side)", - "bits_per_sample": 0, - "dmix_mode": "-1", - "ltrt_cmixlev": "-1.000000", - "ltrt_surmixlev": "-1.000000", - "loro_cmixlev": "-1.000000", - "loro_surmixlev": "-1.000000", - "r_frame_rate": "0/0", - "avg_frame_rate": "0/0", - "time_base": "1/1000", - "start_pts": -5, - "start_time": "-0.005000", - "bit_rate": "448000", - "disposition": { - "default": 1, - "dub": 0, - "original": 0, - "comment": 0, - "lyrics": 0, - "karaoke": 0, - "forced": 0, - "hearing_impaired": 0, - "visual_impaired": 0, - "clean_effects": 0, - "attached_pic": 0, - "timed_thumbnails": 0 - }, - "tags": { - "language": "eng", - "ENCODER": "Lavc60.31.102 ac3", - "DURATION": "01:05:13.024000000" - } - } - """.trimIndent() - val eac3AudioStreamJson = """ - { - "index": 1, - "codec_name": "eac3", - "codec_long_name": "E-AC3", - "codec_type": "audio", - "codec_tag_string": "[0][0][0][0]", - "codec_tag": "0x0000", - "sample_fmt": "fltp", - "sample_rate": "48000", - "channels": 6, - "channel_layout": "5.1(side)", - "bits_per_sample": 0, - "dmix_mode": "-1", - "ltrt_cmixlev": "-1.000000", - "ltrt_surmixlev": "-1.000000", - "loro_cmixlev": "-1.000000", - "loro_surmixlev": "-1.000000", - "r_frame_rate": "0/0", - "avg_frame_rate": "0/0", - "time_base": "1/1000", - "start_pts": -5, - "start_time": "-0.005000", - "bit_rate": "448000", - "disposition": { - "default": 1, - "dub": 0, - "original": 0, - "comment": 0, - "lyrics": 0, - "karaoke": 0, - "forced": 0, - "hearing_impaired": 0, - "visual_impaired": 0, - "clean_effects": 0, - "attached_pic": 0, - "timed_thumbnails": 0 - }, - "tags": { - "language": "eng", - "ENCODER": "Lavc60.31.102 ac3", - "DURATION": "01:05:13.024000000" - } - } - """.trimIndent() - val aacStereoAudioStreamJson = """ - { - "index": 1, - "codec_name": "aac", - "codec_long_name": "AAC (Advanced Audio Coding)", - "profile": "LC", - "codec_type": "audio", - "codec_tag_string": "mp4a", - "codec_tag": "0x6134706d", - "sample_fmt": "fltp", - "sample_rate": "48000", - "channels": 2, - "channel_layout": "2", - "bits_per_sample": 0, - "r_frame_rate": "0/0", - "avg_frame_rate": "0/0", - "time_base": "1/48000", - "start_pts": 0, - "start_time": "0.000000", - "duration_ts": 376210896, - "duration": "7837.727000", - "bit_rate": "224000", - "nb_frames": "367396", - "disposition": { - "default": 1, - "dub": 0, - "original": 0, - "comment": 0, - "lyrics": 0, - "karaoke": 0, - "forced": 0, - "hearing_impaired": 0, - "visual_impaired": 0, - "clean_effects": 0, - "attached_pic": 0, - "timed_thumbnails": 0 - }, - "tags": { - "creation_time": "2022-02-08T20:37:35.000000Z", - "language": "eng", - "handler_name": "SoundHandler", - "vendor_id": "[0][0][0][0]" - } - } - """.trimIndent() - - val aacSurroundAudioStreamJson = """ - { - "index": 1, - "codec_name": "aac", - "codec_long_name": "AAC (Advanced Audio Coding)", - "profile": "LC", - "codec_type": "audio", - "codec_tag_string": "mp4a", - "codec_tag": "0x6134706d", - "sample_fmt": "fltp", - "sample_rate": "48000", - "channels": 6, - "channel_layout": "5.1", - "bits_per_sample": 0, - "r_frame_rate": "0/0", - "avg_frame_rate": "0/0", - "time_base": "1/48000", - "start_pts": 0, - "start_time": "0.000000", - "duration_ts": 376210896, - "duration": "7837.727000", - "bit_rate": "224000", - "nb_frames": "367396", - "disposition": { - "default": 1, - "dub": 0, - "original": 0, - "comment": 0, - "lyrics": 0, - "karaoke": 0, - "forced": 0, - "hearing_impaired": 0, - "visual_impaired": 0, - "clean_effects": 0, - "attached_pic": 0, - "timed_thumbnails": 0 - }, - "tags": { - "creation_time": "2022-02-08T20:37:35.000000Z", - "language": "eng", - "handler_name": "SoundHandler", - "vendor_id": "[0][0][0][0]" - } - } - """.trimIndent() -} \ No newline at end of file diff --git a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/mapping/streams/SubtitleArgumentsTest.kt b/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/mapping/streams/SubtitleArgumentsTest.kt deleted file mode 100644 index 4e8bb597..00000000 --- a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/mapping/streams/SubtitleArgumentsTest.kt +++ /dev/null @@ -1,566 +0,0 @@ -package no.iktdev.mediaprocessing.coordinator.tasksV2.mapping.streams - -import com.google.gson.Gson -import com.google.gson.reflect.TypeToken -import no.iktdev.mediaprocessing.shared.common.contract.ffmpeg.SubtitleStream -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Test - -class SubtitleArgumentsTest { - val type = object : TypeToken>() {}.type - - @Test - fun validate1() { - val data = Gson().fromJson>(multipleSubtitleStreamsWithSameLanguage, type) - assertThat(data.all { it is SubtitleStream }).isTrue() - assertThat(data).isNotNull() - } - - @Test - fun validate2() { - val data = Gson().fromJson>(multipleSubtitleStreamsWithSameLanguage, type) - val args = SubtitleArguments(data) - val selectable = args.excludeLowFrameCount(data) - assertThat(selectable).hasSize(3) - assertThat(selectable.find { it.index == 4 }) - assertThat(selectable.find { it.index == 5 }) - } - - @Test - fun validate3() { - val data = Gson().fromJson>(multipleSubtitleStreamsWithSameLanguage, type) - val args = SubtitleArguments(data).getSubtitleArguments() - assertThat(args).hasSize(1) - assertThat(args.firstOrNull()?.mediaIndex).isEqualTo(4) - } - - @Test - fun validate3_2() { - val data = Gson().fromJson>(multipleSubtitleStreamsWithSameLanguageWithDisposition, type) - val args = SubtitleArguments(data).getSubtitleArguments() - assertThat(args).hasSize(1) - assertThat(args.firstOrNull()?.mediaIndex).isEqualTo(4) - } - - @Test - fun assertThatCorrectTrackIsSelected() { - val data = Gson().fromJson>(selectCorrectTrack, type) - val args = SubtitleArguments(data).getSubtitleArguments() - assertThat(args).hasSize(1) - assertThat(args.firstOrNull()?.index).isEqualTo(0) - } - - @Test - fun assertThatCommentaryIsNotSelected() { - val data = Gson().fromJson>(streamsWithCommentary, type) - val args = SubtitleArguments(data).getSubtitleArguments() - assertThat(args).hasSize(1) - assertThat(args.firstOrNull()?.mediaIndex).isEqualTo(4) - } - - val multipleSubtitleStreamsWithSameLanguage = """ - [{ - "index": 3, - "codec_name": "ass", - "codec_long_name": "ASS (Advanced SSA) subtitle", - "codec_type": "subtitle", - "codec_tag_string": "[0][0][0][0]", - "codec_tag": "0x0000", - "r_frame_rate": "0/0", - "avg_frame_rate": "0/0", - "time_base": "1/1000", - "start_pts": 0, - "start_time": "0.000000", - "duration_ts": 1437083, - "duration": "1437.083000", - "extradata_size": 1967, - "tags": { - "language": "eng", - "title": "Forced", - "BPS": "5", - "DURATION": "00:21:42.640000000", - "NUMBER_OF_FRAMES": "14", - "NUMBER_OF_BYTES": "835", - "_STATISTICS_WRITING_APP": "mkvmerge v69.0.0 ('Day And Age') 64-bit", - "_STATISTICS_WRITING_DATE_UTC": "2024-10-04 08:12:59", - "_STATISTICS_TAGS": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES" - } - }, - { - "index": 4, - "codec_name": "ass", - "codec_long_name": "ASS (Advanced SSA) subtitle", - "codec_type": "subtitle", - "codec_tag_string": "[0][0][0][0]", - "codec_tag": "0x0000", - "r_frame_rate": "0/0", - "avg_frame_rate": "0/0", - "time_base": "1/1000", - "start_pts": 0, - "start_time": "0.000000", - "duration_ts": 1437083, - "duration": "1437.083000", - "extradata_size": 1791, - "tags": { - "language": "eng", - "BPS": "129", - "DURATION": "00:22:26.550000000", - "NUMBER_OF_FRAMES": "356", - "NUMBER_OF_BYTES": "21787", - "_STATISTICS_WRITING_APP": "mkvmerge v69.0.0 ('Day And Age') 64-bit", - "_STATISTICS_WRITING_DATE_UTC": "2024-10-04 08:12:59", - "_STATISTICS_TAGS": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES" - } - }, - { - "index": 5, - "codec_name": "subrip", - "codec_long_name": "SubRip subtitle", - "codec_type": "subtitle", - "codec_tag_string": "[0][0][0][0]", - "codec_tag": "0x0000", - "r_frame_rate": "0/0", - "avg_frame_rate": "0/0", - "time_base": "1/1000", - "start_pts": 790, - "start_time": "0.790000", - "tags": { - "language": "eng", - "title": "CC", - "BPS": "83", - "DURATION": "00:23:56.060000000", - "NUMBER_OF_FRAMES": "495", - "NUMBER_OF_BYTES": "14954", - "_STATISTICS_WRITING_APP": "mkvmerge v69.0.0 ('Day And Age') 64-bit", - "_STATISTICS_WRITING_DATE_UTC": "2024-10-04 08:12:59", - "_STATISTICS_TAGS": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES" - } - }] - """.trimIndent() - - //language=json - val multipleSubtitleStreamsWithSameLanguageWithDisposition = """ - [{ - "index": 3, - "codec_name": "ass", - "codec_long_name": "ASS (Advanced SSA) subtitle", - "codec_type": "subtitle", - "codec_tag_string": "[0][0][0][0]", - "codec_tag": "0x0000", - "r_frame_rate": "0/0", - "avg_frame_rate": "0/0", - "time_base": "1/1000", - "start_pts": 0, - "start_time": "0.000000", - "duration_ts": 1437083, - "duration": "1437.083000", - "extradata_size": 1967, - "disposition": { - "default": 1, - "dub": 0, - "original": 0, - "comment": 0, - "lyrics": 0, - "karaoke": 0, - "forced": 1, - "hearing_impaired": 0, - "visual_impaired": 0, - "clean_effects": 0, - "attached_pic": 0, - "timed_thumbnails": 0, - "non_diegetic": 0, - "captions": 0, - "descriptions": 0, - "metadata": 0, - "dependent": 0, - "still_image": 0 - }, - "tags": { - "language": "eng", - "title": "Forced", - "BPS": "5", - "DURATION": "00:21:42.640000000", - "NUMBER_OF_FRAMES": "14", - "NUMBER_OF_BYTES": "835", - "_STATISTICS_WRITING_APP": "mkvmerge v69.0.0 ('Day And Age') 64-bit", - "_STATISTICS_WRITING_DATE_UTC": "2024-10-04 08:12:59", - "_STATISTICS_TAGS": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES" - } - }, - { - "index": 4, - "codec_name": "ass", - "codec_long_name": "ASS (Advanced SSA) subtitle", - "codec_type": "subtitle", - "codec_tag_string": "[0][0][0][0]", - "codec_tag": "0x0000", - "r_frame_rate": "0/0", - "avg_frame_rate": "0/0", - "time_base": "1/1000", - "start_pts": 0, - "start_time": "0.000000", - "duration_ts": 1437083, - "duration": "1437.083000", - "extradata_size": 1791, - "disposition": { - "default": 0, - "dub": 0, - "original": 0, - "comment": 0, - "lyrics": 0, - "karaoke": 0, - "forced": 0, - "hearing_impaired": 0, - "visual_impaired": 0, - "clean_effects": 0, - "attached_pic": 0, - "timed_thumbnails": 0, - "non_diegetic": 0, - "captions": 0, - "descriptions": 0, - "metadata": 0, - "dependent": 0, - "still_image": 0 - }, - "tags": { - "language": "eng", - "BPS": "129", - "DURATION": "00:22:26.550000000", - "NUMBER_OF_FRAMES": "356", - "NUMBER_OF_BYTES": "21787", - "_STATISTICS_WRITING_APP": "mkvmerge v69.0.0 ('Day And Age') 64-bit", - "_STATISTICS_WRITING_DATE_UTC": "2024-10-04 08:12:59", - "_STATISTICS_TAGS": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES" - } - }, - { - "index": 5, - "codec_name": "subrip", - "codec_long_name": "SubRip subtitle", - "codec_type": "subtitle", - "codec_tag_string": "[0][0][0][0]", - "codec_tag": "0x0000", - "r_frame_rate": "0/0", - "avg_frame_rate": "0/0", - "time_base": "1/1000", - "start_pts": 790, - "start_time": "0.790000", - "disposition": { - "default": 0, - "dub": 0, - "original": 0, - "comment": 0, - "lyrics": 0, - "karaoke": 0, - "forced": 0, - "hearing_impaired": 0, - "visual_impaired": 0, - "clean_effects": 0, - "attached_pic": 0, - "timed_thumbnails": 0, - "non_diegetic": 0, - "captions": 0, - "descriptions": 0, - "metadata": 0, - "dependent": 0, - "still_image": 0 - }, - "tags": { - "language": "eng", - "title": "CC", - "BPS": "83", - "DURATION": "00:23:56.060000000", - "NUMBER_OF_FRAMES": "495", - "NUMBER_OF_BYTES": "14954", - "_STATISTICS_WRITING_APP": "mkvmerge v69.0.0 ('Day And Age') 64-bit", - "_STATISTICS_WRITING_DATE_UTC": "2024-10-04 08:12:59", - "_STATISTICS_TAGS": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES" - } - }] - """.trimIndent() - - val selectCorrectTrack = """ - [ - { - "index": 2, - "codec_name": "ass", - "codec_long_name": "ASS (Advanced SSA) subtitle", - "codec_type": "subtitle", - "codec_tag_string": "[0][0][0][0]", - "codec_tag": "0x0000", - "r_frame_rate": "0/0", - "avg_frame_rate": "0/0", - "time_base": "1/1000", - "start_pts": 0, - "start_time": "0.000000", - "duration_ts": 1430048, - "duration": "1430.048000", - "extradata_size": 2185, - "disposition": { - "default": 1, - "dub": 0, - "original": 0, - "comment": 0, - "lyrics": 0, - "karaoke": 0, - "forced": 0, - "hearing_impaired": 0, - "visual_impaired": 0, - "clean_effects": 0, - "attached_pic": 0, - "timed_thumbnails": 0, - "non_diegetic": 0, - "captions": 0, - "descriptions": 0, - "metadata": 0, - "dependent": 0, - "still_image": 0 - }, - "tags": { - "language": "eng", - "BPS": "173", - "DURATION": "00:23:43.500000000", - "NUMBER_OF_FRAMES": "436", - "NUMBER_OF_BYTES": "30896", - "_STATISTICS_WRITING_APP": "mkvmerge v69.0.0 ('Day And Age') 64-bit", - "_STATISTICS_WRITING_DATE_UTC": "2025-01-03 02:19:23", - "_STATISTICS_TAGS": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES" - } - }, - { - "index": 3, - "codec_name": "subrip", - "codec_long_name": "SubRip subtitle", - "codec_type": "subtitle", - "codec_tag_string": "[0][0][0][0]", - "codec_tag": "0x0000", - "r_frame_rate": "0/0", - "avg_frame_rate": "0/0", - "time_base": "1/1000", - "start_pts": 0, - "start_time": "0.000000", - "duration_ts": 1430048, - "duration": "1430.048000", - "disposition": { - "default": 0, - "dub": 0, - "original": 0, - "comment": 0, - "lyrics": 0, - "karaoke": 0, - "forced": 0, - "hearing_impaired": 0, - "visual_impaired": 0, - "clean_effects": 0, - "attached_pic": 0, - "timed_thumbnails": 0, - "non_diegetic": 0, - "captions": 0, - "descriptions": 0, - "metadata": 0, - "dependent": 0, - "still_image": 0 - }, - "tags": { - "language": "eng", - "BPS": "83", - "DURATION": "00:23:41.860000000", - "NUMBER_OF_FRAMES": "432", - "NUMBER_OF_BYTES": "14853", - "_STATISTICS_WRITING_APP": "mkvmerge v69.0.0 ('Day And Age') 64-bit", - "_STATISTICS_WRITING_DATE_UTC": "2025-01-03 02:19:23", - "_STATISTICS_TAGS": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES" - } - } - ] - """.trimIndent() - - val streamsWithCommentary = """ - [ - { - "index": 4, - "codec_name": "subrip", - "codec_long_name": "SubRip subtitle", - "codec_type": "subtitle", - "codec_tag_string": "[0][0][0][0]", - "codec_tag": "0x0000", - "r_frame_rate": "0/0", - "avg_frame_rate": "0/0", - "time_base": "1/1000", - "start_pts": 0, - "start_time": "0.000000", - "duration_ts": 5501856, - "duration": "5501.856000", - "disposition": { - "default": 0, - "dub": 0, - "original": 0, - "comment": 0, - "lyrics": 0, - "karaoke": 0, - "forced": 1, - "hearing_impaired": 0, - "visual_impaired": 0, - "clean_effects": 0, - "attached_pic": 0, - "timed_thumbnails": 0, - "non_diegetic": 0, - "captions": 0, - "descriptions": 0, - "metadata": 0, - "dependent": 0, - "still_image": 0 - }, - "tags": { - "language": "eng", - "title": "English (Forced)", - "BPS": "4", - "DURATION": "00:50:23.770000000", - "NUMBER_OF_FRAMES": "67", - "NUMBER_OF_BYTES": "1591", - "_STATISTICS_WRITING_APP": "mkvmerge v90.0 ('Hanging On') 64-bit", - "_STATISTICS_WRITING_DATE_UTC": "2025-03-12 18:54:52", - "_STATISTICS_TAGS": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES" - } - }, - { - "index": 5, - "codec_name": "subrip", - "codec_long_name": "SubRip subtitle", - "codec_type": "subtitle", - "codec_tag_string": "[0][0][0][0]", - "codec_tag": "0x0000", - "r_frame_rate": "0/0", - "avg_frame_rate": "0/0", - "time_base": "1/1000", - "start_pts": 0, - "start_time": "0.000000", - "duration_ts": 5501856, - "duration": "5501.856000", - "disposition": { - "default": 0, - "dub": 0, - "original": 0, - "comment": 0, - "lyrics": 0, - "karaoke": 0, - "forced": 0, - "hearing_impaired": 1, - "visual_impaired": 0, - "clean_effects": 0, - "attached_pic": 0, - "timed_thumbnails": 0, - "non_diegetic": 0, - "captions": 0, - "descriptions": 0, - "metadata": 0, - "dependent": 0, - "still_image": 0 - }, - "tags": { - "language": "eng", - "title": "English (SDH)", - "BPS": "54", - "DURATION": "01:28:15.462000000", - "NUMBER_OF_FRAMES": "1302", - "NUMBER_OF_BYTES": "35817", - "_STATISTICS_WRITING_APP": "mkvmerge v90.0 ('Hanging On') 64-bit", - "_STATISTICS_WRITING_DATE_UTC": "2025-03-12 18:54:52", - "_STATISTICS_TAGS": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES" - } - }, - { - "index": 8, - "codec_name": "subrip", - "codec_long_name": "SubRip subtitle", - "codec_type": "subtitle", - "codec_tag_string": "[0][0][0][0]", - "codec_tag": "0x0000", - "r_frame_rate": "0/0", - "avg_frame_rate": "0/0", - "time_base": "1/1000", - "start_pts": 0, - "start_time": "0.000000", - "duration_ts": 5501856, - "duration": "5501.856000", - "disposition": { - "default": 0, - "dub": 0, - "original": 0, - "comment": 0, - "lyrics": 0, - "karaoke": 0, - "forced": 0, - "hearing_impaired": 0, - "visual_impaired": 0, - "clean_effects": 0, - "attached_pic": 0, - "timed_thumbnails": 0, - "non_diegetic": 0, - "captions": 0, - "descriptions": 0, - "metadata": 0, - "dependent": 0, - "still_image": 0 - }, - "tags": { - "language": "eng", - "title": "English (Commentary #1)", - "BPS": "124", - "DURATION": "01:30:35.847000000", - "NUMBER_OF_FRAMES": "1596", - "NUMBER_OF_BYTES": "84444", - "_STATISTICS_WRITING_APP": "mkvmerge v90.0 ('Hanging On') 64-bit", - "_STATISTICS_WRITING_DATE_UTC": "2025-03-12 18:54:52", - "_STATISTICS_TAGS": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES" - } - }, - { - "index": 9, - "codec_name": "subrip", - "codec_long_name": "SubRip subtitle", - "codec_type": "subtitle", - "codec_tag_string": "[0][0][0][0]", - "codec_tag": "0x0000", - "r_frame_rate": "0/0", - "avg_frame_rate": "0/0", - "time_base": "1/1000", - "start_pts": 0, - "start_time": "0.000000", - "duration_ts": 5501856, - "duration": "5501.856000", - "disposition": { - "default": 0, - "dub": 0, - "original": 0, - "comment": 0, - "lyrics": 0, - "karaoke": 0, - "forced": 0, - "hearing_impaired": 0, - "visual_impaired": 0, - "clean_effects": 0, - "attached_pic": 0, - "timed_thumbnails": 0, - "non_diegetic": 0, - "captions": 0, - "descriptions": 0, - "metadata": 0, - "dependent": 0, - "still_image": 0 - }, - "tags": { - "language": "eng", - "title": "English (Commentary #2)", - "BPS": "134", - "DURATION": "01:31:22.561000000", - "NUMBER_OF_FRAMES": "1646", - "NUMBER_OF_BYTES": "92269", - "_STATISTICS_WRITING_APP": "mkvmerge v90.0 ('Hanging On') 64-bit", - "_STATISTICS_WRITING_DATE_UTC": "2025-03-12 18:54:52", - "_STATISTICS_TAGS": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES" - } - } - ] - """.trimIndent() -} \ No newline at end of file diff --git a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/mapping/streams/VideoArgumentsTest.kt b/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/mapping/streams/VideoArgumentsTest.kt deleted file mode 100644 index ccfbf046..00000000 --- a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/tasksV2/mapping/streams/VideoArgumentsTest.kt +++ /dev/null @@ -1,361 +0,0 @@ -package no.iktdev.mediaprocessing.coordinator.tasksV2.mapping.streams - -import com.google.gson.Gson -import no.iktdev.mediaprocessing.shared.common.Preference -import no.iktdev.mediaprocessing.shared.common.contract.ffmpeg.ParsedMediaStreams -import no.iktdev.mediaprocessing.shared.common.contract.ffmpeg.VideoPreference -import no.iktdev.mediaprocessing.shared.common.contract.ffmpeg.VideoStream -import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.DisplayName -import org.junit.jupiter.api.Test - -class VideoArgumentsTest { - - private fun getVideoArguments(videoStream: VideoStream, preference: VideoPreference): VideoArguments { - return VideoArguments( - allStreams = ParsedMediaStreams( - videoStream = listOf(videoStream), - audioStream = emptyList(), - subtitleStream = emptyList() - ), - preference = preference, - videoStream = videoStream - ) - } - - @Test - fun hevcStream1() { - val data = Gson().fromJson(hevcStream1, VideoStream::class.java) - val videoArguments = getVideoArguments(data, - Preference.getPreference().encodePreference.video - .copy(codec = "h265") - ) - assertThat(videoArguments.isVideoCodecEqual()).isTrue() - assertThat(videoArguments.getCodec()).isEqualTo("libx265") - assertThat(videoArguments.getVideoArguments().codecParameters.take(2)).isEqualTo(listOf("-c:v", "copy")) - } - - @Test - @DisplayName(""" - When a hevc encoded media file gets received, - But it has unset metadata, and re-encode for chromecast is set, - Then the parameters should not specify copy - """) - fun hevcStream2() { - val data = Gson().fromJson(hevcStream2, VideoStream::class.java) - val videoArguments = getVideoArguments(data, - Preference.getPreference().encodePreference.video - .copy(codec = "h265", reencodeOnIncorrectMetadataForChromecast = true) - ) - assertThat(videoArguments.isVideoCodecEqual()).isTrue() - assertThat(videoArguments.getCodec()).isEqualTo("libx265") - assertThat(videoArguments.getVideoArguments().codecParameters.take(2)).isEqualTo(listOf("-c:v", "libx265")) - } - - @Test - @DisplayName(""" - When a vc1 encoded media file gets received - And preference is set to hevc, - Then the parameters should be to encode in hevc - """) - fun vc1Stream1() { - val data = Gson().fromJson(vc1Stream, VideoStream::class.java) - val videoArguments = getVideoArguments(data, - Preference.getPreference().encodePreference.video - .copy(codec = "h265", reencodeOnIncorrectMetadataForChromecast = true) - ) - assertThat(videoArguments.isVideoCodecEqual()).isFalse() - assertThat(videoArguments.getCodec()).isEqualTo("vc1") - assertThat(videoArguments.getVideoArguments().codecParameters.take(2)).isEqualTo(listOf("-c:v", "libx265")) - } - - @Test - @DisplayName(""" - When a vc1 encoded media file gets received - And preference is set to h264, - Then the parameters should be to encode in h264 - """) - fun vc1Stream2() { - val data = Gson().fromJson(vc1Stream, VideoStream::class.java) - val videoArguments = getVideoArguments(data, - Preference.getPreference().encodePreference.video - .copy(codec = "h264", reencodeOnIncorrectMetadataForChromecast = true) - ) - assertThat(videoArguments.isVideoCodecEqual()).isFalse() - assertThat(videoArguments.getCodec()).isEqualTo("vc1") - assertThat(videoArguments.getVideoArguments().codecParameters.take(2)).isEqualTo(listOf("-c:v", "libx264")) - } - - @Test - fun h264Stream1() { - val data = Gson().fromJson(h264stream1, VideoStream::class.java) - val videoArguments = getVideoArguments(data, - Preference.getPreference().encodePreference.video - .copy(codec = "h265", reencodeOnIncorrectMetadataForChromecast = true) - ) - assertThat(videoArguments.isVideoCodecEqual()).isFalse() - assertThat(videoArguments.getCodec()).isEqualTo("libx264") - assertThat(videoArguments.getVideoArguments().codecParameters.take(2)).isEqualTo(listOf("-c:v", "libx265")) - } - - @Test - fun h264Stream2() { - val data = Gson().fromJson(h264stream1, VideoStream::class.java) - val videoArguments = getVideoArguments(data, - Preference.getPreference().encodePreference.video - .copy(reencodeOnIncorrectMetadataForChromecast = true) - ) - assertThat(videoArguments.isVideoCodecEqual()).isTrue() - assertThat(videoArguments.getCodec()).isEqualTo("libx264") - assertThat(videoArguments.getVideoArguments().codecParameters.take(2)).isEqualTo(listOf("-c:v", "copy")) - } - - val hevcStream1 = """ - { - "index": 0, - "codec_name": "hevc", - "codec_long_name": "H.265 / HEVC (High Efficiency Video Coding)", - "profile": "Main 10", - "codec_type": "video", - "codec_tag_string": "hev1", - "codec_tag": "0x31766568", - "width": 1920, - "height": 1080, - "coded_width": 1920, - "coded_height": 1080, - "closed_captions": 0, - "film_grain": 0, - "has_b_frames": 2, - "sample_aspect_ratio": "1:1", - "display_aspect_ratio": "16:9", - "pix_fmt": "yuv420p10le", - "level": 150, - "color_range": "tv", - "chroma_location": "left", - "refs": 1, - "id": "0x1", - "r_frame_rate": "24000/1001", - "avg_frame_rate": "34045000/1419959", - "time_base": "1/16000", - "start_pts": 0, - "start_time": "0.000000", - "duration_ts": 22719344, - "duration": "1419.959000", - "bit_rate": "2020313", - "nb_frames": "34045", - "extradata_size": 2535, - "disposition": { - "default": 1, - "dub": 0, - "original": 0, - "comment": 0, - "lyrics": 0, - "karaoke": 0, - "forced": 0, - "hearing_impaired": 0, - "visual_impaired": 0, - "clean_effects": 0, - "attached_pic": 0, - "timed_thumbnails": 0, - "non_diegetic": 0, - "captions": 0, - "descriptions": 0, - "metadata": 0, - "dependent": 0, - "still_image": 0 - }, - "tags": { - "language": "jpn", - "handler_name": "VideoHandler", - "vendor_id": "[0][0][0][0]" - } - } - """.trimIndent() - - val hevcStream2 = """ - { - "index": 0, - "codec_name": "hevc", - "codec_long_name": "H.265 / HEVC (High Efficiency Video Coding)", - "profile": "Main 10", - "codec_type": "video", - "codec_tag_string": "[0][0][0][0]", - "codec_tag": "0x0000", - "width": 1920, - "height": 1080, - "coded_width": 1920, - "coded_height": 1080, - "closed_captions": 0, - "film_grain": 0, - "has_b_frames": 2, - "sample_aspect_ratio": "1:1", - "display_aspect_ratio": "16:9", - "pix_fmt": "yuv420p10le", - "level": 150, - "color_range": "tv", - "chroma_location": "left", - "refs": 1, - "r_frame_rate": "24000/1001", - "avg_frame_rate": "24000/1001", - "time_base": "1/1000", - "start_pts": 0, - "start_time": "0.000000", - "extradata_size": 2535, - "disposition": { - "default": 1, - "dub": 0, - "original": 0, - "comment": 0, - "lyrics": 0, - "karaoke": 0, - "forced": 0, - "hearing_impaired": 0, - "visual_impaired": 0, - "clean_effects": 0, - "attached_pic": 0, - "timed_thumbnails": 0, - "non_diegetic": 0, - "captions": 0, - "descriptions": 0, - "metadata": 0, - "dependent": 0, - "still_image": 0 - }, - "tags": { - "language": "jpn", - "title": "nan", - "BPS": "2105884", - "DURATION": "00:53:27.204000000", - "NUMBER_OF_FRAMES": "76896", - "NUMBER_OF_BYTES": "844250247", - "_STATISTICS_WRITING_APP": "mkvmerge v91.0 ('Signs') 64-bit", - "_STATISTICS_WRITING_DATE_UTC": "2025-03-31 17:33:38", - "_STATISTICS_TAGS": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES" - } - } - """.trimIndent() - - - val vc1Stream = """ - { - "index": 0, - "codec_name": "vc1", - "codec_long_name": "SMPTE VC-1", - "profile": "Advanced", - "codec_type": "video", - "codec_tag_string": "WVC1", - "codec_tag": "0x31435657", - "width": 1920, - "height": 1080, - "coded_width": 1920, - "coded_height": 1080, - "closed_captions": 0, - "film_grain": 0, - "has_b_frames": 1, - "sample_aspect_ratio": "1:1", - "display_aspect_ratio": "16:9", - "pix_fmt": "yuv420p", - "level": 3, - "chroma_location": "left", - "field_order": "progressive", - "refs": 1, - "r_frame_rate": "24000/1001", - "avg_frame_rate": "24000/1001", - "time_base": "1/1000", - "start_pts": 0, - "start_time": "0.000000", - "duration_ts": 5189856, - "duration": "5189.856000", - "extradata_size": 34, - "disposition": { - "default": 1, - "dub": 0, - "original": 0, - "comment": 0, - "lyrics": 0, - "karaoke": 0, - "forced": 0, - "hearing_impaired": 0, - "visual_impaired": 0, - "clean_effects": 0, - "attached_pic": 0, - "timed_thumbnails": 0, - "non_diegetic": 0, - "captions": 0, - "descriptions": 0, - "metadata": 0, - "dependent": 0, - "still_image": 0 - }, - "tags": { - "title": "" - } - } - """.trimIndent() - - val h264stream1 = """ - { - "index": 0, - "codec_name": "h264", - "codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10", - "profile": "High", - "codec_type": "video", - "codec_tag_string": "avc1", - "codec_tag": "0x31637661", - "width": 1920, - "height": 1080, - "coded_width": 1920, - "coded_height": 1080, - "closed_captions": 0, - "film_grain": 0, - "has_b_frames": 2, - "sample_aspect_ratio": "1:1", - "display_aspect_ratio": "16:9", - "pix_fmt": "yuv420p", - "level": 40, - "chroma_location": "left", - "field_order": "progressive", - "refs": 1, - "is_avc": "true", - "nal_length_size": "4", - "id": "0x1", - "r_frame_rate": "30000/1001", - "avg_frame_rate": "30000/1001", - "time_base": "1/30000", - "start_pts": 0, - "start_time": "0.000000", - "duration_ts": 185519300, - "duration": "6183.976667", - "bit_rate": "6460534", - "bits_per_raw_sample": "8", - "nb_frames": "185334", - "extradata_size": 45, - "disposition": { - "default": 1, - "dub": 0, - "original": 0, - "comment": 0, - "lyrics": 0, - "karaoke": 0, - "forced": 0, - "hearing_impaired": 0, - "visual_impaired": 0, - "clean_effects": 0, - "attached_pic": 0, - "timed_thumbnails": 0, - "non_diegetic": 0, - "captions": 0, - "descriptions": 0, - "metadata": 0, - "dependent": 0, - "still_image": 0 - }, - "tags": { - "creation_time": "2018-07-09T06:20:13.000000Z", - "language": "und", - "handler_name": "nah", - "vendor_id": "[0][0][0][0]" - } - } - """.trimIndent() -} \ No newline at end of file diff --git a/apps/processer/build.gradle.kts b/apps/processer/build.gradle.kts index d3bcc8d8..a0697890 100644 --- a/apps/processer/build.gradle.kts +++ b/apps/processer/build.gradle.kts @@ -21,7 +21,6 @@ repositories { } } -val exposedVersion = "0.44.0" dependencies { /*Spring boot*/ @@ -29,20 +28,15 @@ dependencies { implementation("org.springframework.boot:spring-boot-starter:2.7.0") // implementation("org.springframework.kafka:spring-kafka:3.0.1") implementation("org.springframework.boot:spring-boot-starter-websocket:2.6.3") - implementation("org.springframework.kafka:spring-kafka:2.8.5") - implementation("org.jetbrains.exposed:exposed-core:$exposedVersion") - implementation("org.jetbrains.exposed:exposed-dao:$exposedVersion") - implementation("org.jetbrains.exposed:exposed-jdbc:$exposedVersion") - implementation("org.jetbrains.exposed:exposed-java-time:$exposedVersion") - implementation ("mysql:mysql-connector-java:8.0.29") - implementation("io.github.microutils:kotlin-logging-jvm:2.0.11") implementation("com.google.code.gson:gson:2.8.9") implementation("org.json:json:20210307") implementation("no.iktdev:exfl:0.0.16-SNAPSHOT") + implementation("no.iktdev:eventi:1.0-rc13") + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1") implementation("com.github.vishna:watchservice-ktx:master-SNAPSHOT") @@ -65,6 +59,7 @@ dependencies { testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.7.2") testImplementation("io.kotlintest:kotlintest-assertions:3.3.2") testImplementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.3.0") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2") implementation(kotlin("stdlib-jdk8")) } diff --git a/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/ProcesserApplication.kt b/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/ProcesserApplication.kt new file mode 100644 index 00000000..40857790 --- /dev/null +++ b/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/ProcesserApplication.kt @@ -0,0 +1,52 @@ +package no.iktdev.mediaprocessing.processer + +import mu.KotlinLogging +import no.iktdev.eventi.events.EventTypeRegistry +import no.iktdev.eventi.tasks.TaskTypeRegistry +import no.iktdev.exfl.coroutines.CoroutinesDefault +import no.iktdev.exfl.coroutines.CoroutinesIO +import no.iktdev.exfl.observable.Observables +import no.iktdev.mediaprocessing.shared.common.DatabaseApplication +import no.iktdev.mediaprocessing.shared.common.event_task_contract.EventRegistry +import no.iktdev.mediaprocessing.shared.common.event_task_contract.TaskRegistry +import no.iktdev.mediaprocessing.shared.common.getAppVersion +import org.springframework.boot.runApplication +import org.springframework.context.annotation.Configuration + +class ProcesserApplication: DatabaseApplication() { +} + +val ioCoroutine = CoroutinesIO() +val defaultCoroutine = CoroutinesDefault() + +private val log = KotlinLogging.logger {} + +fun main(args: Array) { + ioCoroutine.addListener(listener = object: Observables.ObservableValue.ValueListener { + override fun onUpdated(value: Throwable) { + value.printStackTrace() + } + }) + defaultCoroutine.addListener(listener = object: Observables.ObservableValue.ValueListener { + override fun onUpdated(value: Throwable) { + value.printStackTrace() + } + }) + + + runApplication(*args) + log.info { "App Version: ${getAppVersion()}" } +} +//private val logger = KotlinLogging.logger {} + +@Configuration +open class ApplicationConfiguration() { + init { + EventRegistry.getEvents().let { + EventTypeRegistry.register(it) + } + TaskRegistry.getTasks().let { + TaskTypeRegistry.register(it) + } + } +} diff --git a/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/ProcesserEnv.kt b/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/ProcesserEnv.kt new file mode 100755 index 00000000..3ef9d278 --- /dev/null +++ b/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/ProcesserEnv.kt @@ -0,0 +1,26 @@ +package no.iktdev.mediaprocessing.processer + +import no.iktdev.exfl.using +import java.io.File + +class ProcesserEnv { + companion object { + val wsAllowedOrigins: String = System.getenv("AllowedOriginsWebsocket")?.takeIf { it.isNotBlank() } ?: "" + + val ffmpeg: String = System.getenv("SUPPORTING_EXECUTABLE_FFMPEG") ?: "ffmpeg" + val allowOverwrite = System.getenv("ALLOW_OVERWRITE").toBoolean() ?: false + + var cachedContent: File = if (!System.getenv("DIRECTORY_CONTENT_CACHE").isNullOrBlank()) File(System.getenv("DIRECTORY_CONTENT_CACHE")) else File("/src/cache") + val outgoingContent: File = if (!System.getenv("DIRECTORY_CONTENT_OUTGOING").isNullOrBlank()) File(System.getenv("DIRECTORY_CONTENT_OUTGOING")) else File("/src/output") + + + val logDirectory = if (!System.getenv("LOG_DIR").isNullOrBlank()) File(System.getenv("LOG_DIR")) else + File("data").using("logs") + + val encodeLogDirectory = logDirectory.using("encode") + val extractLogDirectory = logDirectory.using("extract") + val subtitleExtractLogDirectory = logDirectory.using("subtitles") + + val fullLogging = System.getenv("FullLogging").toBoolean() + } +} \ No newline at end of file diff --git a/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/TaskPoller.kt b/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/TaskPoller.kt new file mode 100644 index 00000000..f9804080 --- /dev/null +++ b/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/TaskPoller.kt @@ -0,0 +1,65 @@ +package no.iktdev.mediaprocessing.processer + +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import no.iktdev.eventi.models.Event +import no.iktdev.eventi.tasks.AbstractTaskPoller +import no.iktdev.eventi.tasks.TaskReporter +import no.iktdev.mediaprocessing.shared.common.stores.EventStore +import no.iktdev.mediaprocessing.shared.common.stores.TaskStore +import org.springframework.boot.ApplicationArguments +import org.springframework.boot.ApplicationRunner +import org.springframework.stereotype.Component +import org.springframework.stereotype.Service +import java.util.UUID + +@Component +class PollerAdministrator( + private val taskPoller: TaskPoller, +): ApplicationRunner { + override fun run(args: ApplicationArguments?) { + CoroutineScope(Dispatchers.Default).launch { + taskPoller.start() + } + } +} + + +@Service +class TaskPoller( + private val reporter: TaskReporter, +) : AbstractTaskPoller( + taskStore = TaskStore, + reporterFactory = { reporter } // én reporter brukes for alle tasks +) { + +} + + +@Component +class DefaultTaskReporter() : TaskReporter { + override fun markClaimed(taskId: UUID, workerId: String) { + TaskStore.claim(taskId, workerId) + } + + override fun updateLastSeen(taskId: UUID) { + TaskStore.heartbeat(taskId) + } + + override fun markConsumed(taskId: UUID) { + TaskStore.markConsumed(taskId) + } + + override fun updateProgress(taskId: UUID, progress: Int) { + // Not to be implemented for this application + } + + override fun log(taskId: UUID, message: String) { + // Not to be implemented for this application + } + + override fun publishEvent(event: Event) { + EventStore.persist(event) + } +} \ No newline at end of file diff --git a/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/Util.kt b/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/Util.kt new file mode 100644 index 00000000..9c2e075a --- /dev/null +++ b/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/Util.kt @@ -0,0 +1,10 @@ +package no.iktdev.mediaprocessing.processer + +import no.iktdev.exfl.using +import java.io.File + +object Util { + fun getTemporaryStoreFile(fileName: String): File { + return ProcesserEnv.cachedContent.using(fileName) + } +} \ No newline at end of file diff --git a/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/listeners/FfmpegTaskListener.kt b/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/listeners/FfmpegTaskListener.kt new file mode 100644 index 00000000..81a9f448 --- /dev/null +++ b/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/listeners/FfmpegTaskListener.kt @@ -0,0 +1,9 @@ +package no.iktdev.mediaprocessing.processer.listeners + +import no.iktdev.eventi.tasks.TaskListener +import no.iktdev.eventi.tasks.TaskType +import no.iktdev.mediaprocessing.ffmpeg.FFmpeg + +abstract class FfmpegTaskListener(taskType: TaskType): TaskListener(taskType) { + abstract fun getFfmpeg(): FFmpeg +} \ No newline at end of file diff --git a/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/listeners/MuxAudioVideoTaskListener.kt b/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/listeners/MuxAudioVideoTaskListener.kt new file mode 100644 index 00000000..14431f4a --- /dev/null +++ b/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/listeners/MuxAudioVideoTaskListener.kt @@ -0,0 +1,4 @@ +package no.iktdev.mediaprocessing.processer.listeners + +class MuxAudioVideoTaskListener { +} \ No newline at end of file diff --git a/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/listeners/SubtitleTaskListener.kt b/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/listeners/SubtitleTaskListener.kt new file mode 100644 index 00000000..0313141a --- /dev/null +++ b/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/listeners/SubtitleTaskListener.kt @@ -0,0 +1,74 @@ +package no.iktdev.mediaprocessing.processer.listeners + +import no.iktdev.eventi.models.Event +import no.iktdev.eventi.models.Task +import no.iktdev.eventi.models.store.TaskStatus +import no.iktdev.eventi.tasks.TaskListener +import no.iktdev.eventi.tasks.TaskType +import no.iktdev.mediaprocessing.ffmpeg.FFmpeg +import no.iktdev.mediaprocessing.ffmpeg.arguments.MpegArgument +import no.iktdev.mediaprocessing.processer.ProcesserEnv +import no.iktdev.mediaprocessing.processer.Util +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.ExtractResult +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.ProcesserExtractEvent +import no.iktdev.mediaprocessing.shared.common.event_task_contract.tasks.ExtractSubtitleTask +import org.springframework.stereotype.Service +import java.util.UUID + +@Service +class SubtitleTaskListener: TaskListener(TaskType.CPU_INTENSIVE) { + override fun getWorkerId() = "${this::class.java.simpleName}-${taskType}-${UUID.randomUUID()}" + + override fun supports(task: Task) = task is ExtractSubtitleTask + + override suspend fun onTask(task: Task): Event? { + val taskData = task as ExtractSubtitleTask + + val cachedOutFile = Util.getTemporaryStoreFile(taskData.data.outputFileName).also { + if (!it.parentFile.exists()) { + it.parentFile.mkdirs() + } + } + + if (cachedOutFile.exists() && taskData.data.arguments.firstOrNull() != "-y") { + reporter?.publishEvent(ProcesserExtractEvent( + data = ExtractResult( + status = TaskStatus.Failed + ) + ).producedFrom(task)) + throw IllegalStateException("${cachedOutFile.absolutePath} does already exist, and arguments does not permit overwrite") + } + + val arguments = MpegArgument() + .inputFile(taskData.data.inputFile) + .outputFile(cachedOutFile.absolutePath) + .args(taskData.data.arguments) + + val result = SubtitleFFmpeg() + withHeartbeatRunner { + reporter?.updateLastSeen(task.taskId) + } + result.run(arguments) + if (result.result.resultCode != 0 ) { + return ProcesserExtractEvent(data = ExtractResult(status = TaskStatus.Failed)).producedFrom(task) + } + + return ProcesserExtractEvent( + data = ExtractResult( + status = TaskStatus.Completed, + cachedOutputFile = cachedOutFile.absolutePath + ) + ).producedFrom(task) + } + + + + class SubtitleFFmpeg(override val listener: Listener? = null): FFmpeg(executable = ProcesserEnv.ffmpeg, logDir = ProcesserEnv.subtitleExtractLogDirectory ) { + + override fun onCreate() { + if (!ProcesserEnv.subtitleExtractLogDirectory.exists()) { + ProcesserEnv.subtitleExtractLogDirectory.mkdirs() + } + } + } +} \ No newline at end of file diff --git a/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/listeners/VideoTaskListener.kt b/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/listeners/VideoTaskListener.kt new file mode 100644 index 00000000..adb5ad40 --- /dev/null +++ b/apps/processer/src/main/kotlin/no/iktdev/mediaprocessing/processer/listeners/VideoTaskListener.kt @@ -0,0 +1,89 @@ +package no.iktdev.mediaprocessing.processer.listeners + +import no.iktdev.eventi.models.Event +import no.iktdev.eventi.models.Task +import no.iktdev.eventi.models.store.TaskStatus +import no.iktdev.eventi.tasks.TaskType +import no.iktdev.mediaprocessing.ffmpeg.FFmpeg +import no.iktdev.mediaprocessing.ffmpeg.arguments.MpegArgument +import no.iktdev.mediaprocessing.ffmpeg.decoder.FfmpegDecodedProgress +import no.iktdev.mediaprocessing.processer.ProcesserEnv +import no.iktdev.mediaprocessing.processer.Util +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.EncodeResult +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.ProcesserEncodeEvent +import no.iktdev.mediaprocessing.shared.common.event_task_contract.tasks.EncodeTask +import org.springframework.stereotype.Service +import java.util.* + +@Service +class VideoTaskListener: FfmpegTaskListener(TaskType.CPU_INTENSIVE) { + override fun getWorkerId() = "${this::class.java.simpleName}-${taskType}-${UUID.randomUUID()}" + + override fun supports(task: Task) = task is EncodeTask + + override suspend fun onTask(task: Task): Event? { + val taskData = task as EncodeTask + val cachedOutFile = Util.getTemporaryStoreFile(taskData.data.outputFileName).also { + if (!it.parentFile.exists()) { + it.parentFile.mkdirs() + } + } + if (cachedOutFile.exists() && taskData.data.arguments.firstOrNull() != "-y") { + reporter?.publishEvent(ProcesserEncodeEvent( + data = EncodeResult( + status = TaskStatus.Failed + ) + ).producedFrom(task)) + throw IllegalStateException("${cachedOutFile.absolutePath} does already exist, and arguments does not permit overwrite") + } + + val arguments = MpegArgument() + .inputFile(taskData.data.inputFile) + .outputFile(cachedOutFile.absolutePath) + .args(taskData.data.arguments) + .withProgress(true) + + val result = getFfmpeg() + withHeartbeatRunner { + reporter?.updateLastSeen(task.taskId) + } + result.run(arguments) + if (result.result.resultCode != 0 ) { + return ProcesserEncodeEvent(data = EncodeResult(status = TaskStatus.Failed)).producedFrom(task) + } + + return ProcesserEncodeEvent( + data = EncodeResult( + status = TaskStatus.Completed, + cachedOutputFile = cachedOutFile.absolutePath + ) + ).producedFrom(task) + } + + override fun getFfmpeg(): FFmpeg { + return VideoFFmpeg(object : FFmpeg.Listener { + override fun onStarted(inputFile: String) { + } + + override fun onCompleted(inputFile: String, outputFile: String) { + } + + override fun onProgressChanged( + inputFile: String, + progress: FfmpegDecodedProgress + ) { + } + }) + } + + + class VideoFFmpeg(override val listener: Listener? = null): FFmpeg(executable = ProcesserEnv.ffmpeg, logDir = ProcesserEnv.encodeLogDirectory) { + + override fun onCreate() { + super.onCreate() + if (!ProcesserEnv.encodeLogDirectory.exists()) { + ProcesserEnv.encodeLogDirectory.mkdirs() + } + } + } +} \ No newline at end of file diff --git a/apps/processer/src/test/kotlin/no/iktdev/mediaprocessing/processer/ffmpeg/FfmpegRunnerTest.kt b/apps/processer/src/test/kotlin/no/iktdev/mediaprocessing/processer/ffmpeg/FfmpegRunnerTest.kt deleted file mode 100644 index 20d36b42..00000000 --- a/apps/processer/src/test/kotlin/no/iktdev/mediaprocessing/processer/ffmpeg/FfmpegRunnerTest.kt +++ /dev/null @@ -1,15 +0,0 @@ -package no.iktdev.mediaprocessing.processer.ffmpeg - -import no.iktdev.mediaprocessing.processer.Files -import no.iktdev.mediaprocessing.processer.getAsList -import org.junit.jupiter.api.Assertions.* -import org.junit.jupiter.api.DisplayName -import org.junit.jupiter.api.Test - -class FfmpegRunnerTest { - - - - - -} \ No newline at end of file diff --git a/apps/processer/src/test/kotlin/no/iktdev/mediaprocessing/processer/listeners/MockFFmpeg.kt b/apps/processer/src/test/kotlin/no/iktdev/mediaprocessing/processer/listeners/MockFFmpeg.kt new file mode 100644 index 00000000..f22deb81 --- /dev/null +++ b/apps/processer/src/test/kotlin/no/iktdev/mediaprocessing/processer/listeners/MockFFmpeg.kt @@ -0,0 +1,38 @@ +package no.iktdev.mediaprocessing.processer.listeners + +import com.github.pgreze.process.ProcessResult +import kotlinx.coroutines.delay +import no.iktdev.mediaprocessing.ffmpeg.FFmpeg +import no.iktdev.mediaprocessing.ffmpeg.arguments.MpegArgument +import no.iktdev.mediaprocessing.ffmpeg.decoder.FfmpegDecodedProgress +import java.io.File + +class MockFFmpeg(override val listener: Listener, val delayMillis: Long = 500, private val simulateSuccess: Boolean = true) : FFmpeg(executable = "", logDir = File("/null")) { + + companion object { + fun emptyListener() = object : Listener { + override fun onStarted(inputFile: String) {} + override fun onCompleted(inputFile: String, outputFile: String) {} + override fun onProgressChanged(inputFile: String, progress: FfmpegDecodedProgress) {} + override fun onError(inputFile: String, message: String) {} + } + } + + override suspend fun run(argument: MpegArgument) { + inputFile = argument.inputFile!! + listener.onStarted(argument.inputFile!!) + delay(delayMillis) + + result = ProcessResult( + resultCode = if (simulateSuccess) 0 else 1, + output = listOf("Simulated ffmpeg output") + ) + + if (simulateSuccess) { + listener.onCompleted(inputFile, argument.outputFile!!) + } else { + listener.onError(inputFile, "Simulated error") + } + } + +} \ No newline at end of file diff --git a/apps/processer/src/test/kotlin/no/iktdev/mediaprocessing/processer/listeners/VideoTaskListenerTest.kt b/apps/processer/src/test/kotlin/no/iktdev/mediaprocessing/processer/listeners/VideoTaskListenerTest.kt new file mode 100644 index 00000000..d40738a8 --- /dev/null +++ b/apps/processer/src/test/kotlin/no/iktdev/mediaprocessing/processer/listeners/VideoTaskListenerTest.kt @@ -0,0 +1,80 @@ +package no.iktdev.mediaprocessing.processer.listeners + +import kotlinx.coroutines.test.runTest +import no.iktdev.eventi.models.Event +import no.iktdev.eventi.models.Task +import no.iktdev.eventi.models.store.TaskStatus +import no.iktdev.eventi.tasks.TaskReporter +import no.iktdev.eventi.tasks.TaskTypeRegistry +import no.iktdev.mediaprocessing.ffmpeg.FFmpeg +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.ProcesserEncodeEvent +import no.iktdev.mediaprocessing.shared.common.event_task_contract.tasks.EncodeData +import no.iktdev.mediaprocessing.shared.common.event_task_contract.tasks.EncodeTask +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import java.util.UUID +import kotlin.system.measureTimeMillis + +class VideoTaskListenerTest { + + class TestListener(val delay: Long): VideoTaskListener() { + fun getJob() = currentJob + + private var _result: Event? = null + fun getResult(): Event? { + return _result + } + override fun onComplete(task: Task, result: Event?) { + this._result = result + } + + override fun getFfmpeg(): FFmpeg { + return MockFFmpeg(delayMillis = delay, listener = MockFFmpeg.emptyListener()) + } + } + + val overrideReporter = object : TaskReporter { + override fun markClaimed(taskId: UUID, workerId: String) {} + override fun updateLastSeen(taskId: UUID) {} + override fun markConsumed(taskId: UUID) {} + override fun updateProgress(taskId: UUID, progress: Int) {} + override fun log(taskId: UUID, message: String) {} + override fun publishEvent(event: Event) { + + } + } + + @BeforeEach + fun setup() { + TaskTypeRegistry.register(EncodeTask::class.java) + } + + @Test + fun `onTask waits for runner to complete`() = runTest { + val delay = 1000L + val testTask = EncodeTask( + EncodeData( + inputFile = "input.mp4", + outputFileName = "output.mp4", + arguments = listOf("-y") + ) + ).newReferenceId() + + val listener = TestListener(delay) + + val time = measureTimeMillis { + listener.accept(testTask, overrideReporter) + listener.getJob()?.join() + val event = listener.getResult() + assertTrue(event is ProcesserEncodeEvent) + assertEquals(TaskStatus.Completed, (event as ProcesserEncodeEvent).data.status) + } + + assertTrue(time >= delay, "Expected onTask to wait at least $delay ms, waited for $time ms") + assertTrue(time <= (delay*2), "Expected onTask to wait less than ${(delay*2)} ms, waited for $time ms") + + } + +} \ No newline at end of file diff --git a/apps/processer/src/test/kotlin/no/iktdev/mediaprocessing/processer/services/EncodeServiceTest.kt b/apps/processer/src/test/kotlin/no/iktdev/mediaprocessing/processer/services/EncodeServiceTest.kt deleted file mode 100644 index 22f0f943..00000000 --- a/apps/processer/src/test/kotlin/no/iktdev/mediaprocessing/processer/services/EncodeServiceTest.kt +++ /dev/null @@ -1,4 +0,0 @@ -package no.iktdev.mediaprocessing.processer.services - -class EncodeServiceTest { -} \ No newline at end of file diff --git a/shared/common/build.gradle.kts b/shared/common/build.gradle.kts index 0b38247e..b8c39da6 100644 --- a/shared/common/build.gradle.kts +++ b/shared/common/build.gradle.kts @@ -59,7 +59,7 @@ dependencies { implementation("com.zaxxer:HikariCP:7.0.2") - + implementation(project(":shared:ffmpeg")) implementation("no.iktdev:eventi:1.0-rc13") testImplementation(platform("org.junit:junit-bom:5.10.0")) diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/EventRegistry.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/EventRegistry.kt index b6363471..ea47e7fb 100644 --- a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/EventRegistry.kt +++ b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/EventRegistry.kt @@ -1,26 +1,26 @@ package no.iktdev.mediaprocessing.shared.common.event_task_contract -import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.ConvertTaskCreatedEvents -import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.ConvertTaskPerformedEvent +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.ConvertTaskResultEvent import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.FileAddedEvent import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.FileReadyEvent import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.FileRemovedEvent import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.MediaParsedInfoEvent import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.MetadataSearchTaskCreated import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.MetadataSearchTaskPerformed -import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.ProcesserEncodePerformedEvent +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.ProcesserEncodeEvent import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.ProcesserEncodeTaskCreatedEvent import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.ProcesserExtractTaskCreatedEvent -import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.ProcesserExtractedPerformedEvent +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.ProcesserExtractEvent import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.ProcesserReadTaskCreatedEvent import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.StartProcessingEvent import no.iktdev.eventi.models.Event +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.ConvertTaskCreatedEvent object EventRegistry { fun getEvents(): List> { return listOf( - ConvertTaskCreatedEvents::class.java, - ConvertTaskPerformedEvent::class.java, + ConvertTaskCreatedEvent::class.java, + ConvertTaskResultEvent::class.java, FileAddedEvent::class.java, FileReadyEvent::class.java, @@ -32,10 +32,10 @@ object EventRegistry { MetadataSearchTaskPerformed::class.java, ProcesserExtractTaskCreatedEvent::class.java, - ProcesserExtractedPerformedEvent::class.java, + ProcesserExtractEvent::class.java, ProcesserEncodeTaskCreatedEvent::class.java, - ProcesserEncodePerformedEvent::class.java, + ProcesserEncodeEvent::class.java, ProcesserReadTaskCreatedEvent::class.java, // Do i need this? diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/TaskRegistry.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/TaskRegistry.kt index 8fd07a3c..4ddfb257 100644 --- a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/TaskRegistry.kt +++ b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/TaskRegistry.kt @@ -2,7 +2,7 @@ package no.iktdev.mediaprocessing.shared.common.event_task_contract import no.iktdev.mediaprocessing.shared.common.event_task_contract.tasks.ConvertTask import no.iktdev.mediaprocessing.shared.common.event_task_contract.tasks.EncodeTask -import no.iktdev.mediaprocessing.shared.common.event_task_contract.tasks.ExtractTask +import no.iktdev.mediaprocessing.shared.common.event_task_contract.tasks.ExtractSubtitleTask import no.iktdev.mediaprocessing.shared.common.event_task_contract.tasks.MediaReadTask import no.iktdev.mediaprocessing.shared.common.event_task_contract.tasks.MetadataSearchTask import no.iktdev.eventi.models.Task @@ -12,7 +12,7 @@ object TaskRegistry { return listOf( ConvertTask::class.java, EncodeTask::class.java, - ExtractTask::class.java, + ExtractSubtitleTask::class.java, MediaReadTask::class.java, MetadataSearchTask::class.java ) diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ConvertTaskCreatedEvent.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ConvertTaskCreatedEvent.kt new file mode 100644 index 00000000..99ab3eec --- /dev/null +++ b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ConvertTaskCreatedEvent.kt @@ -0,0 +1,6 @@ +package no.iktdev.mediaprocessing.shared.common.event_task_contract.events + +import no.iktdev.eventi.models.Event + +class ConvertTaskCreatedEvent: Event() { +} \ No newline at end of file diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ConverterEvents.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ConvertTaskResultEvent.kt similarity index 68% rename from shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ConverterEvents.kt rename to shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ConvertTaskResultEvent.kt index 520494af..e08af6f5 100644 --- a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ConverterEvents.kt +++ b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ConvertTaskResultEvent.kt @@ -3,12 +3,7 @@ package no.iktdev.mediaprocessing.shared.common.event_task_contract.events import no.iktdev.eventi.models.Event import no.iktdev.eventi.models.store.TaskStatus - -// Placeholder event, so that the listener does not continue to create tasks -class ConvertTaskCreatedEvents: Event() { -} - -data class ConvertTaskPerformedEvent( +data class ConvertTaskResultEvent( val data: ConvertedData?, val status: TaskStatus, ): Event() { diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/FileAddedEvent.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/FileAddedEvent.kt new file mode 100644 index 00000000..8bf7f055 --- /dev/null +++ b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/FileAddedEvent.kt @@ -0,0 +1,9 @@ +package no.iktdev.mediaprocessing.shared.common.event_task_contract.events + +import no.iktdev.eventi.models.Event +import no.iktdev.mediaprocessing.shared.common.model.FileInfo + +data class FileAddedEvent( + val data: FileInfo +): Event() { +} \ No newline at end of file diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/FileEvents.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/FileEvents.kt deleted file mode 100644 index 74672523..00000000 --- a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/FileEvents.kt +++ /dev/null @@ -1,23 +0,0 @@ -package no.iktdev.mediaprocessing.shared.common.event_task_contract.events - -import no.iktdev.eventi.models.DeleteEvent -import no.iktdev.eventi.models.Event - -data class FileAddedEvent( - val data: FileInfo -): Event() { -} - -data class FileReadyEvent( - val data: FileInfo -): Event() {} - -class FileRemovedEvent( - val data: FileInfo -): DeleteEvent() { -} - -data class FileInfo( - val fileName: String, - val fileUri: String, -) \ No newline at end of file diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/FileReadyEvent.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/FileReadyEvent.kt new file mode 100644 index 00000000..afb6be12 --- /dev/null +++ b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/FileReadyEvent.kt @@ -0,0 +1,8 @@ +package no.iktdev.mediaprocessing.shared.common.event_task_contract.events + +import no.iktdev.eventi.models.Event +import no.iktdev.mediaprocessing.shared.common.model.FileInfo + +data class FileReadyEvent( + val data: FileInfo +): Event() {} \ No newline at end of file diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/FileRemovedEvent.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/FileRemovedEvent.kt new file mode 100644 index 00000000..82537c36 --- /dev/null +++ b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/FileRemovedEvent.kt @@ -0,0 +1,9 @@ +package no.iktdev.mediaprocessing.shared.common.event_task_contract.events + +import no.iktdev.eventi.models.DeleteEvent +import no.iktdev.mediaprocessing.shared.common.model.FileInfo + +class FileRemovedEvent( + val data: FileInfo +): DeleteEvent() { +} \ No newline at end of file diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/MediaParsedInfoEvent.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/MediaParsedInfoEvent.kt index bd698991..a4ea7d05 100644 --- a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/MediaParsedInfoEvent.kt +++ b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/MediaParsedInfoEvent.kt @@ -1,16 +1,19 @@ package no.iktdev.mediaprocessing.shared.common.event_task_contract.events import no.iktdev.eventi.models.Event +import no.iktdev.mediaprocessing.shared.common.model.MediaType class MediaParsedInfoEvent( val data: ParsedData ): Event() { + + data class ParsedData( + val parsedCollection: String, + val parsedFileName: String, + val parsedSearchTitles: List, + val mediaType: MediaType + ) { + } } -data class ParsedData( - val parsedTitle: String, - val parsedFileName: String, - val parsedSearchTitles: List -) { -} \ No newline at end of file diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/MediaStreamParsedEvent.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/MediaStreamParsedEvent.kt index a9981418..3fb3b015 100644 --- a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/MediaStreamParsedEvent.kt +++ b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/MediaStreamParsedEvent.kt @@ -1,7 +1,7 @@ package no.iktdev.mediaprocessing.shared.common.event_task_contract.events import no.iktdev.eventi.models.Event -import no.iktdev.mediaprocessing.shared.common.model.ParsedMediaStreams +import no.iktdev.mediaprocessing.ffmpeg.data.ParsedMediaStreams data class MediaStreamParsedEvent( val data: ParsedMediaStreams diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/MediaStreamReadTaskCreatedEvent.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/MediaStreamReadTaskCreatedEvent.kt index df2a3f0b..2c60982c 100644 --- a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/MediaStreamReadTaskCreatedEvent.kt +++ b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/MediaStreamReadTaskCreatedEvent.kt @@ -3,5 +3,7 @@ package no.iktdev.mediaprocessing.shared.common.event_task_contract.events import no.iktdev.eventi.models.Event -class MediaStreamReadTaskCreatedEvent(): Event() { +data class MediaStreamReadTaskCreatedEvent( + val fileUri: String +): Event() { } \ No newline at end of file diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/MediaTracksDetermineSubtitleTypeEvent.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/MediaTracksDetermineSubtitleTypeEvent.kt new file mode 100644 index 00000000..6bad893c --- /dev/null +++ b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/MediaTracksDetermineSubtitleTypeEvent.kt @@ -0,0 +1,10 @@ +package no.iktdev.mediaprocessing.shared.common.event_task_contract.events + +import no.iktdev.eventi.models.Event +import no.iktdev.mediaprocessing.shared.common.model.SubtitleItem + +data class MediaTracksDetermineSubtitleTypeEvent( + val subtitleTrackItems: List +): Event() { + +} \ No newline at end of file diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/MediaTracksEncodeSelectedEvent.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/MediaTracksEncodeSelectedEvent.kt new file mode 100644 index 00000000..9f500c6d --- /dev/null +++ b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/MediaTracksEncodeSelectedEvent.kt @@ -0,0 +1,9 @@ +package no.iktdev.mediaprocessing.shared.common.event_task_contract.events + +import no.iktdev.eventi.models.Event + +data class MediaTracksEncodeSelectedEvent( + val selectedVideoTrack: Int, + val selectedAudioTrack: Int, + val selectedAudioExtendedTrack: Int? = null // Optional extended audio track, e.g Dolby Atmos or Enhanced AAC +): Event() \ No newline at end of file diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/MediaTracksExtractSelectedEvent.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/MediaTracksExtractSelectedEvent.kt new file mode 100644 index 00000000..a9310ea3 --- /dev/null +++ b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/MediaTracksExtractSelectedEvent.kt @@ -0,0 +1,9 @@ +package no.iktdev.mediaprocessing.shared.common.event_task_contract.events + +import no.iktdev.eventi.models.Event + +data class MediaTracksExtractSelectedEvent( + val selectedSubtitleTracks: List +): Event() { + +} \ No newline at end of file diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ProcesserEncodePerformedEvent.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ProcesserEncodePerformedEvent.kt new file mode 100644 index 00000000..3de1e776 --- /dev/null +++ b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ProcesserEncodePerformedEvent.kt @@ -0,0 +1,6 @@ +package no.iktdev.mediaprocessing.shared.common.event_task_contract.events + +import no.iktdev.eventi.models.Event + +class ProcesserEncodePerformedEvent: Event() { +} \ No newline at end of file diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ProcesserEncodeTaskCreatedEvent.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ProcesserEncodeTaskCreatedEvent.kt new file mode 100644 index 00000000..a9840741 --- /dev/null +++ b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ProcesserEncodeTaskCreatedEvent.kt @@ -0,0 +1,6 @@ +package no.iktdev.mediaprocessing.shared.common.event_task_contract.events + +import no.iktdev.eventi.models.Event + +class ProcesserEncodeTaskCreatedEvent: Event() { +} \ No newline at end of file diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ProcesserEvents.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ProcesserEvents.kt index 83483747..584b4452 100644 --- a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ProcesserEvents.kt +++ b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/events/ProcesserEvents.kt @@ -7,16 +7,13 @@ import no.iktdev.eventi.models.store.TaskStatus class ProcesserExtractTaskCreatedEvent: Event() { } -// Placeholder event, so that the listener does not continue to create tasks -class ProcesserEncodeTaskCreatedEvent: Event() { -} // Placeholder event, so that the listener does not continue to create tasks class ProcesserReadTaskCreatedEvent: Event() { } -data class ProcesserEncodePerformedEvent( +data class ProcesserEncodeEvent( val data: EncodeResult ): Event() { @@ -28,7 +25,7 @@ data class EncodeResult( ) -data class ProcesserExtractedPerformedEvent( +data class ProcesserExtractEvent( val data: ExtractResult ): Event() diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/tasks/EncodeTask.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/tasks/EncodeTask.kt index 34fc1145..af095b15 100644 --- a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/tasks/EncodeTask.kt +++ b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/tasks/EncodeTask.kt @@ -9,6 +9,6 @@ data class EncodeTask( data class EncodeData( val arguments: List, - val outputFile: String, + val outputFileName: String, val inputFile: String ) \ No newline at end of file diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/tasks/ExtractTask.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/tasks/ExtractSubtitleTask.kt similarity index 72% rename from shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/tasks/ExtractTask.kt rename to shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/tasks/ExtractSubtitleTask.kt index 3a0187d2..ed8760c6 100644 --- a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/tasks/ExtractTask.kt +++ b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/event_task_contract/tasks/ExtractSubtitleTask.kt @@ -2,12 +2,12 @@ package no.iktdev.mediaprocessing.shared.common.event_task_contract.tasks import no.iktdev.eventi.models.Task -data class ExtractTask( - val data: ExtractData +data class ExtractSubtitleTask( + val data: ExtractSubtitleData ): Task() { } -data class ExtractData( +data class ExtractSubtitleData( val arguments: List, val outputFileName: String, val language: String, diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/model/FileInfo.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/model/FileInfo.kt new file mode 100644 index 00000000..ab6cdb32 --- /dev/null +++ b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/model/FileInfo.kt @@ -0,0 +1,6 @@ +package no.iktdev.mediaprocessing.shared.common.model + +data class FileInfo( + val fileName: String, + val fileUri: String, +) \ No newline at end of file diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/model/MediaType.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/model/MediaType.kt new file mode 100644 index 00000000..049e4aaf --- /dev/null +++ b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/model/MediaType.kt @@ -0,0 +1,6 @@ +package no.iktdev.mediaprocessing.shared.common.model + +enum class MediaType { + Movie, + Serie +} \ No newline at end of file diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/model/SubtitleItem.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/model/SubtitleItem.kt new file mode 100644 index 00000000..0c85bb64 --- /dev/null +++ b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/model/SubtitleItem.kt @@ -0,0 +1,5 @@ +package no.iktdev.mediaprocessing.shared.common.model + +import no.iktdev.mediaprocessing.ffmpeg.data.SubtitleStream + +data class SubtitleItem(val stream: SubtitleStream, val type: SubtitleType) \ No newline at end of file diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/model/SubtitleType.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/model/SubtitleType.kt new file mode 100644 index 00000000..e515c88b --- /dev/null +++ b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/model/SubtitleType.kt @@ -0,0 +1,9 @@ +package no.iktdev.mediaprocessing.shared.common.model + +enum class SubtitleType { + Song, + Commentary, + ClosedCaption, + SHD, + Dialogue, +} \ No newline at end of file diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/projection/Project.kt b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/projection/Project.kt new file mode 100644 index 00000000..d4dcf83d --- /dev/null +++ b/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/projection/Project.kt @@ -0,0 +1,47 @@ +package no.iktdev.mediaprocessing.shared.common.projection + +import no.iktdev.eventi.models.Event +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.OperationType +import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.ProcessFlow +import java.io.File + +class Project(val events: List) { + lateinit var startedWith: StartProjection + private set + var metadataTaskStatus: TaskStatus = TaskStatus.NotInitiated + private set + var encodeTaskStatus: TaskStatus = TaskStatus.NotInitiated + private set + var extreactTaskStatus: TaskStatus = TaskStatus.NotInitiated + private set + var convertTaskStatus: TaskStatus = TaskStatus.NotInitiated + private set + var coverDownloadTaskStatus: TaskStatus = TaskStatus.NotInitiated + private set + + + init { + + } + + + + data class StartProjection( + val inputFile: String, + val mode: ProcessFlow, + val tasks: Set + ) + + + + + enum class TaskStatus { + NotInitiated, + NotAvailable, + Pending, + Skipped, + Completed, + Failed + } + +} \ No newline at end of file diff --git a/shared/ffmpeg/build.gradle.kts b/shared/ffmpeg/build.gradle.kts index 324b6ca3..3bc44721 100644 --- a/shared/ffmpeg/build.gradle.kts +++ b/shared/ffmpeg/build.gradle.kts @@ -25,6 +25,10 @@ dependencies { implementation("no.iktdev:exfl:1.0-rc1") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.1") + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.2") + + testImplementation("org.assertj:assertj-core:3.4.1") testImplementation(kotlin("test")) } diff --git a/shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/FFmpeg.kt b/shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/FFmpeg.kt index bfa29e0f..f7238309 100644 --- a/shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/FFmpeg.kt +++ b/shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/FFmpeg.kt @@ -12,9 +12,7 @@ import java.io.FileOutputStream import java.time.LocalDateTime import java.time.format.DateTimeFormatter -abstract class FFmpeg { - abstract val executable: String - abstract val logDir: File +open class FFmpeg(val executable: String, val logDir: File) { open val listener: Listener? = null private var progress: FfmpegDecodedProgress? = null @@ -31,7 +29,14 @@ abstract class FFmpeg { lateinit var result: ProcessResult protected set - private lateinit var inputFile: String + + open fun onCreate() {} + + init { + onCreate() + } + + protected lateinit var inputFile: String open suspend fun run(argument: MpegArgument) { inputFile = if (argument.inputFile == null) throw RuntimeException("Input file is required") else argument.inputFile!! logFile = logDir.using("$formattedDateTime-${File(inputFile).nameWithoutExtension}.log") diff --git a/shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/FFinfo.kt b/shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/FFprobe.kt similarity index 98% rename from shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/FFinfo.kt rename to shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/FFprobe.kt index 0eb789ad..02045163 100644 --- a/shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/FFinfo.kt +++ b/shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/FFprobe.kt @@ -7,7 +7,7 @@ import com.google.gson.Gson import com.google.gson.JsonObject import no.iktdev.mediaprocessing.ffmpeg.data.FFinfoOutput -abstract class FFinfo { +abstract class FFprobe { open val defaultArguments: List = listOf("-v", "quiet") abstract val executable: String diff --git a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/model/MediaStreams.kt b/shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/data/MediaStreams.kt old mode 100755 new mode 100644 similarity index 98% rename from shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/model/MediaStreams.kt rename to shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/data/MediaStreams.kt index 6b1b504b..ed2cfde9 --- a/shared/common/src/main/kotlin/no/iktdev/mediaprocessing/shared/common/model/MediaStreams.kt +++ b/shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/data/MediaStreams.kt @@ -1,4 +1,4 @@ -package no.iktdev.mediaprocessing.shared.common.model +package no.iktdev.mediaprocessing.ffmpeg.data data class ParsedMediaStreams( val videoStream: List = listOf(), diff --git a/shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/dsl/AudioCodec.kt b/shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/dsl/AudioCodec.kt new file mode 100644 index 00000000..854399d7 --- /dev/null +++ b/shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/dsl/AudioCodec.kt @@ -0,0 +1,227 @@ +package no.iktdev.mediaprocessing.ffmpeg.dsl + +import no.iktdev.mediaprocessing.ffmpeg.data.AudioStream + +sealed class AudioCodec(val codec: String, open var bitrate: Int? = null, open var sampleRate: Int? = null, open var channels: Int? = null) { + + // AAC (Advanced Audio Coding) + class Aac( + // Bitrate i kbps (typisk 128–256 for stereo) + override var bitrate: Int? = null, + // Profile: LC (Low Complexity), HE (High Efficiency), HEv2 + var profile: AacProfile = AacProfile.LC, + // Antall kanaler (1 = mono, 2 = stereo) + override var channels: Int? = null, // = 2, + // Sample rate i Hz (typisk 44100 eller 48000) + override var sampleRate: Int? = null + ) : AudioCodec("aac") { + override fun determineTranscodeDecision(stream: AudioStream): TranscodeDecision { + val superDecision = super.determineTranscodeDecision(stream) + if (superDecision == TranscodeDecision.Reencode) return superDecision + if (forceCopy) return TranscodeDecision.Copy + + val profileOk = stream.profile.lowercase() == "lc" + val channelsOk = stream.channels <= 6 + val sampleRateOk = stream.sample_rate.toIntOrNull() in listOf(44100, 48000) + + return when { + !profileOk -> TranscodeDecision.Reencode // HE/HEv2 → reencode til LC + !channelsOk || !sampleRateOk -> TranscodeDecision.Reencode + else -> TranscodeDecision.Copy + } + } + + override fun buildFfmpegArgs(stream: AudioStream, trackIndex: Int?): List { + val args = super.buildFfmpegArgs(stream, trackIndex).toMutableList() + + // AAC-spesifikt: profile + if (profile != AacProfile.LC) { + args += listOf("-profile:a", profile.ffmpegName) + } + + return args + } + } + + // MP3 (MPEG Layer III) + class Mp3( + override var bitrate: Int? = null, // = 192, + override var channels: Int? = null, // = 2, + override var sampleRate: Int? = null // = 44100 + ) : AudioCodec("libmp3lame") + + // Opus (moderne, lav latency, bra for streaming) + class Opus( + override var bitrate: Int? = null, // = 128, + override var channels: Int? = null, // = 2, + override var sampleRate: Int? = null, // = 48000, + // Application mode: audio, voip, lowdelay + var application: OpusApplication = OpusApplication.Audio + ) : AudioCodec("opus") { + override fun determineTranscodeDecision(stream: AudioStream): TranscodeDecision { + val base = super.determineTranscodeDecision(stream) + if (base == TranscodeDecision.Reencode) return base + if (forceCopy) return TranscodeDecision.Copy + + // Opus må alltid være 48kHz internt, så hvis input != 48000 → reencode + val sampleRateOk = stream.sample_rate.toIntOrNull() == 48000 + val channelsOk = (channels ?: stream.channels) <= 2 // typisk stereo + + return if (sampleRateOk && channelsOk) { + TranscodeDecision.Copy + } else { + TranscodeDecision.Reencode + } + } + + override fun buildFfmpegArgs(stream: AudioStream, trackIndex: Int?): List { + val args = super.buildFfmpegArgs(stream, trackIndex).toMutableList() + // Opus-spesifikt: application mode + args += listOf("-application", application.ffmpegName) + return args + } + } + + // Vorbis (åpen kildekode, brukt i Ogg) + class Vorbis( + override var bitrate: Int? = null, // = 128, + override var channels: Int? = null, // = 2, + override var sampleRate: Int? = null, // = 44100 + ) : AudioCodec("libvorbis") + + // FLAC (lossless) + class Flac( + var compressionLevel: Int? = null, // = 5, + override var channels: Int? = null, // = 2, + override var sampleRate: Int? = null, // = 48000 + ) : AudioCodec("flac") { + override fun buildFfmpegArgs(stream: AudioStream, trackIndex: Int?): List { + val args = mutableListOf() + args += if (trackIndex != null) listOf("-c:a:$trackIndex", "flac") + else listOf("-c:a", "flac") + + compressionLevel?.let { args += listOf("-compression_level", compressionLevel.toString()) } + return args + } + } + + // AC3 (Dolby Digital) + class Ac3( + override var bitrate: Int? = null, // = 384, + override var channels: Int? = null, // = 6, + override var sampleRate: Int? = null, // = 48000 + ) : AudioCodec("ac3") + + + class Dts( + override var bitrate: Int? = null, + override var channels: Int? = null, // = 6, + override var sampleRate: Int? = null, // = 48000 + ) : AudioCodec("dts") + + class Pcm : AudioCodec("pcm_s16le") { + override fun buildFfmpegArgs(stream: AudioStream, trackIndex: Int?): List { + val idx = trackIndex?.let { ":$it" } ?: "" + return listOf("-c:a$idx", "pcm_s16le") + } + + override fun determineTranscodeDecision(stream: AudioStream) = TranscodeDecision.Reencode + } + + + // Kopier eksisterende audio uten reenkoding + object Copy : AudioCodec("copy") + + var forceCopy: Boolean = false + + open fun determineTranscodeDecision(stream: AudioStream): TranscodeDecision { + // 1) Hvis vi eksplisitt vil kopiere + if (forceCopy || this == Copy) return TranscodeDecision.Copy + + // 2) Hvis codec er identisk og ingen parametre er satt → Copy + val sameCodec = this.isSame(stream.codec_name) + val wantsBitrateChange = bitrate != null + val wantsSampleRateChange = sampleRate?.let { sr -> + val inSr = stream.sample_rate.toIntOrNull() + inSr != null && sr != inSr + } ?: false + val wantsChannelChange = channels?.let { ch -> + ch != stream.channels + } ?: false + + return when { + // samme codec og ingen endringer → Copy + sameCodec && !wantsBitrateChange && !wantsSampleRateChange && !wantsChannelChange -> + TranscodeDecision.Copy + + // ellers → Reencode + else -> TranscodeDecision.Reencode + } + } + + /** + * Felles bygging av ffmpeg-argumenter. + * - Tar hensyn til felter som er satt (bitrate, channels, sampleRate). + * - Hopper over felter som er null. + * - Validerer mot input-stream (ikke høyere enn input). + */ + open fun buildFfmpegArgs(stream: AudioStream, trackIndex: Int? = null): List { + val args = mutableListOf() + + // codec + args += if (trackIndex != null) { + listOf("-c:a:$trackIndex", codec) + } else { + listOf("-c:a", codec) + } + + // bitrate + bitrate?.let { + args += listOf("-b:a", "${it}k") + } + + // sample rate + sampleRate?.let { + val effective = it.coerceAtMost(stream.sample_rate.toInt()) + args += listOf("-ar", effective.toString()) + } + + // channels + channels?.let { + val effective = it.coerceAtMost(stream.channels) + args += listOf("-ac", effective.toString()) + } + + return args + } + +} + +fun AudioCodec.isSame(name: String): Boolean { + val codecObject = when (name.lowercase()) { + "aac", "mp4a", "libfdk_aac" -> AudioCodec.Aac() + "mp3", "mpeg3", "libmp3lame" -> AudioCodec.Mp3() + "opus", "libopus" -> AudioCodec.Opus() + "vorbis", "oggvorbis", "libvorbis" -> AudioCodec.Vorbis() + "flac" -> AudioCodec.Flac() + "ac3", "dolby", "dolbydigital" -> AudioCodec.Ac3() + "dts", "dca" -> AudioCodec.Dts() // ← lagt til her + "pcm_s16le", "pcm" -> AudioCodec.Pcm() + "copy" -> AudioCodec.Copy + else -> throw IllegalArgumentException("Unsupported audio codec: $name") + } + return (this.codec == codecObject.codec) +} + + +enum class AacProfile(val ffmpegName: String) { + LC("aac_low"), // Low Complexity – mest brukt + HE("aac_he"), // High Efficiency – bedre komprimering + HEv2("aac_he_v2") // High Efficiency v2 – enda mer komprimering +} + +enum class OpusApplication(val ffmpegName: String) { + Audio("audio"), // Vanlig musikk/lyd + Voip("voip"), // Optimalisert for tale + LowDelay("lowdelay") // Lav latency, f.eks. live streaming +} diff --git a/shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/dsl/MediaPlan.kt b/shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/dsl/MediaPlan.kt new file mode 100644 index 00000000..e126a12f --- /dev/null +++ b/shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/dsl/MediaPlan.kt @@ -0,0 +1,73 @@ +package no.iktdev.mediaprocessing.ffmpeg.dsl + +import no.iktdev.mediaprocessing.ffmpeg.data.AudioStream +import no.iktdev.mediaprocessing.ffmpeg.data.VideoStream + +// DSL entrypoint +data class MediaPlan( + val videoTrack: VideoTarget, + val audioTracks: MutableList = mutableListOf() +) { + fun toFfmpegArgs( + videoStreams: List, + audioStreams: List + ): List { + val args = mutableListOf() + + + // Video + val vStream = videoStreams[videoTrack.index] + val vDecision = videoTrack.codec.determineTranscodeDecision(vStream) + args += listOf("-map", "0:v:${videoTrack.index}") + args += when (vDecision) { + TranscodeDecision.Copy -> listOf("-c:v", "copy") + TranscodeDecision.Remux -> listOf("-c:v", videoTrack.codec.codec) + TranscodeDecision.Reencode -> videoTrack.codec.buildFfmpegArgs(vStream) + } + + // Audio + audioTracks.forEachIndexed { outIdx, target -> + val aStream = audioStreams[target.index] + val aDecision = target.codec.determineTranscodeDecision(aStream) + + if (outIdx > 0) { + val otherIsCopy = audioTracks.filter { it -> it.index == target.index && it !== target }.any { it.codec == AudioCodec.Copy } + if (otherIsCopy && aDecision == TranscodeDecision.Copy) { + // Hvis en annen audio-track med samme index er satt til copy, kan vi ikke copy denne også + return@forEachIndexed + } + } + + args += listOf("-map", "0:a:${target.index}") + when (aDecision) { + TranscodeDecision.Copy -> args += listOf("-c:a:$outIdx", "copy") + TranscodeDecision.Remux -> args += listOf("-c:a:$outIdx", target.codec.codec) + TranscodeDecision.Reencode -> { + val built = target.codec.buildFfmpegArgs(aStream).toMutableList() + // injiser output-indeks i alle audio-flagg + for (i in built.indices) { + if (built[i] == "-c:a") built[i] = "-c:a:$outIdx" + if (built[i] == "-b:a") built[i] = "-b:a:$outIdx" + if (built[i] == "-ar") built[i] = "-ar:$outIdx" + if (built[i] == "-ac") built[i] = "-ac:$outIdx" + } + args += built + } + } + } + return args + } + +} + +// Video target: index + codec +data class VideoTarget( + val index: Int, + val codec: VideoCodec +) + +// Audio target: index + codec +data class AudioTarget( + val index: Int, + val codec: AudioCodec +) diff --git a/shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/dsl/TranscodeDecision.kt b/shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/dsl/TranscodeDecision.kt new file mode 100644 index 00000000..0188c984 --- /dev/null +++ b/shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/dsl/TranscodeDecision.kt @@ -0,0 +1,7 @@ +package no.iktdev.mediaprocessing.ffmpeg.dsl + +enum class TranscodeDecision { + Copy, + Remux, + Reencode +} \ No newline at end of file diff --git a/shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/dsl/VideoCodec.kt b/shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/dsl/VideoCodec.kt new file mode 100644 index 00000000..3fef1221 --- /dev/null +++ b/shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/dsl/VideoCodec.kt @@ -0,0 +1,253 @@ +package no.iktdev.mediaprocessing.ffmpeg.dsl + +import no.iktdev.mediaprocessing.ffmpeg.data.VideoStream + +sealed class VideoCodec(val codec: String, open val crf: Int? = null, open val bitrate: Int? = null) { + + // HEVC / H.265 encoder (libx265) + class Hevc( + // Preset styrer hastighet vs komprimeringseffektivitet. + // "ultrafast" = rask, men stor fil; "veryslow" = treg, men liten fil. + var preset: Presets = Presets.Slow, + + // CRF (Constant Rate Factor) styrer kvalitet vs bitrate. + // Lavere tall = bedre kvalitet, høyere tall = lavere bitrate. + override var crf: Int = 18, + override val bitrate: Int? = null, + + // Tune kan brukes for spesifikke scenarier (film, animation, grain). + var tune: String? = null, + ) : VideoCodec("libx265") { + + override fun determineTranscodeDecision(stream: VideoStream): TranscodeDecision { + val superDecision = super.determineTranscodeDecision(stream) + if (superDecision == TranscodeDecision.Reencode) { + return superDecision + } + + val unsetTag = stream.codec_tag_string == "[0][0][0][0]" || stream.codec_tag == "0x0000" + val validTag = stream.codec_tag_string.equals("hev1", ignoreCase = true) || + stream.codec_tag_string.equals("hvc1", ignoreCase = true) + + val profileOk = stream.profile.lowercase() in listOf("main", "main10") + val levelOk = stream.level <= 153 // 5.1 ≈ 153 + + return when { + // Profil eller level utenfor Chromecast‑krav → reencode + !profileOk || !levelOk -> TranscodeDecision.Reencode + + // Tag unset eller gyldig → vi kan fortsatt copy/remux + unsetTag || validTag -> TranscodeDecision.Copy + + // Alle andre tilfeller → safe fallback til reencode + else -> TranscodeDecision.Reencode + } + } + override fun buildFfmpegArgs(stream: VideoStream): List { + val args = super.buildFfmpegArgs(stream).toMutableList() + args += listOf("-preset", preset.presetName) + tune?.let { args += listOf("-tune", it) } + return args + } + } + + + // H.264 encoder (libx264) + class H264( + // Preset: samme som for HEVC, styrer encoding speed vs compression. + var preset: Presets = Presets.Slow, + + // Profile: Baseline/Main/High etc. styrer kompatibilitet og features. + // High gir best kvalitet, Baseline brukes ofte for mobile enheter. + var profile: H264Profiles = H264Profiles.High, + + // Level: definerer maks bitrate, oppløsning og framerate. + // Eks: 4.1 passer for 1080p @ 30fps, 5.1 for 4K. + var level: Double = 4.2, + + // CRF: styrer kvalitet vs bitrate (samme som for HEVC). + override var crf: Int = 23, + override val bitrate: Int? = null + ) : VideoCodec("libx264") { + override fun determineTranscodeDecision(stream: VideoStream): TranscodeDecision { + val superDecision = super.determineTranscodeDecision(stream) + if (superDecision == TranscodeDecision.Reencode) return superDecision + + val profileOk = stream.profile.lowercase() in listOf("baseline", "main", "high", "high10") + val levelOk = stream.level <= 51 // 5.1 typisk maks for bred støtte + + return when { + !profileOk || !levelOk -> TranscodeDecision.Reencode + else -> TranscodeDecision.Copy + } + } + override fun buildFfmpegArgs(stream: VideoStream): List { + val args = super.buildFfmpegArgs(stream).toMutableList() + args += listOf("-preset", preset.presetName) + args += listOf("-profile:v", profile.profileName) + args += listOf("-level", level.toString()) + return args + } + } + + + // VP9 encoder (libvpx-vp9) + class Vp9( + // CRF: styrer kvalitet vs bitrate for VP9. + override var crf: Int = 32, + + // Bitrate: kan settes eksplisitt i kbps hvis du vil ha CBR/VBR. + override var bitrate: Int? = null, + + var cpuUsed: Int = 4 // Tradeoff mellom hastighet og komprimering. + ) : VideoCodec("libvpx-vp9") { + override fun determineTranscodeDecision(stream: VideoStream): TranscodeDecision { + val superDecision = super.determineTranscodeDecision(stream) + if (superDecision == TranscodeDecision.Reencode) return superDecision + + // MP4 har dårlig VP9-støtte, WebM er tryggere + val containerOk = stream.codec_tag_string.equals("vp09", ignoreCase = true) + + return if (containerOk) TranscodeDecision.Copy else TranscodeDecision.Remux + } + override fun buildFfmpegArgs(stream: VideoStream): List { + val args = super.buildFfmpegArgs(stream).toMutableList() + args += listOf("-cpu-used", cpuUsed.toString()) + return args + } + } + + + // VP8 encoder (libvpx) + class Vp8( + override var crf: Int = 10, + override var bitrate: Int? = null + ) : VideoCodec("libvpx") + + + // AV1 encoder (libaom-av1) + class Av1( + // CRF: styrer kvalitet vs bitrate for AV1. + override var crf: Int = 30, + + // cpuUsed: tradeoff mellom hastighet og komprimering. + // Lav verdi = treg, men effektiv; høy verdi = rask, men mindre effektiv. + var cpuUsed: Int = 4 + ) : VideoCodec("libaom-av1") { + override fun determineTranscodeDecision(stream: VideoStream): TranscodeDecision { + val superDecision = super.determineTranscodeDecision(stream) + if (superDecision == TranscodeDecision.Reencode) return superDecision + + val validTag = stream.codec_tag_string.equals("av01", ignoreCase = true) + val levelOk = stream.level <= 51 // AV1 nivåer, typisk ≤ 5.1 + + return when { + !validTag || !levelOk -> TranscodeDecision.Reencode + else -> TranscodeDecision.Copy + } + } + override fun buildFfmpegArgs(stream: VideoStream): List { + val args = super.buildFfmpegArgs(stream).toMutableList() + args += listOf("-cpu-used", cpuUsed.toString()) + return args + } + } + + + // VVC (Versatile Video Coding, H.266) + class Vvc( + // Preset: encoding speed vs compression tradeoff. + var preset: Presets = Presets.Medium, + override var crf: Int = 27, + override val bitrate: Int? = null + ) : VideoCodec("libvvc") { + override fun buildFfmpegArgs(stream: VideoStream): List { + val args = super.buildFfmpegArgs(stream).toMutableList() + args += listOf("-preset", preset.presetName) + return args + } + } + + + // Xvid (MPEG-4 Part 2) + class Vid( + // Bitrate: typisk parameter for Xvid, ofte brukt i kbps. + override var bitrate: Int? = null, + var qscale: Int? = null // Xvid bruker qscale i stedet for CRF + ) : VideoCodec("libxvid") { + override fun buildFfmpegArgs(stream: VideoStream): List { + val args = mutableListOf("-c:v", codec) + bitrate?.let { args += listOf("-b:v", "${it}k") } + qscale?.let { args += listOf("-qscale:v", it.toString()) } + return args + } + } + + // Raw video (ingen komprimering) + object Raw : VideoCodec("rawvideo") { + override fun determineTranscodeDecision(stream: VideoStream): TranscodeDecision { + return TranscodeDecision.Reencode + } + } + + // Copy: ingen reenkoding, bare remuxing av eksisterende stream. + object Copy : VideoCodec("copy") + + open fun determineTranscodeDecision(stream: VideoStream): TranscodeDecision { + return if (this.isSame(stream.codec_name)) { + TranscodeDecision.Copy + } else { + when (this) { + is Copy -> TranscodeDecision.Copy + else -> TranscodeDecision.Reencode + } + } + } + + open fun buildFfmpegArgs(stream: VideoStream): List { + val args = mutableListOf("-c:v", codec) + + crf?.let { args += listOf("-crf", it.toString()) } + bitrate?.let { args += listOf("-b:v", "${it}k") } + + return args + } +} + +fun VideoCodec.isSame(name: String): Boolean { + val codecObject = when (name.lowercase()) { + "hevc", "hevec", "h265", "h.265", "libx265" -> VideoCodec.Hevc() + "h.264", "h264", "libx264" -> VideoCodec.H264() + "vp9", "vp-9", "libvpx-vp9" -> VideoCodec.Vp9() + "av1", "libaom-av1" -> VideoCodec.Av1() + "mpeg4", "mp4", "libxvid" -> VideoCodec.Vid() + "vvc", "h.266", "libvvc" -> VideoCodec.Vvc() + "vp8", "libvpx" -> VideoCodec.Vp8() + "rawvideo" -> VideoCodec.Raw + "copy" -> VideoCodec.Copy + else -> throw IllegalArgumentException("Unsupported codec: $name") + } + return (this.codec == codecObject.codec) +} + +enum class H264Profiles(val profileName: String) { + Baseline("baseline"), + Main("main"), + High("high"), + High10("high10"), + High422("high422"), + High444("high444") +} + +enum class Presets(val presetName: String) { + Ultrafast("ultrafast"), + Superfast("superfast"), + Veryfast("veryfast"), + Faster("faster"), + Fast("fast"), + Medium("medium"), + Slow("slow"), + Slower("slower"), + Veryslow("veryslow"), + Placebo("placebo") +} \ No newline at end of file diff --git a/shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/util/FFmpegUtils.kt b/shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/util/FFmpegUtils.kt new file mode 100644 index 00000000..c35d99cf --- /dev/null +++ b/shared/ffmpeg/src/main/kotlin/no/iktdev/mediaprocessing/ffmpeg/util/FFmpegUtils.kt @@ -0,0 +1,30 @@ +package no.iktdev.mediaprocessing.ffmpeg.util + +enum class FfmpegCodecs(val ffmpegName: String) { + hevc("libx265"), + h264("libx264"), + vp9("libvpx-vp9"), + av1("libaom-av1"), + vid("libxvid"), + vvc("libvvc"), + vp8("libvpx"); + + fun getCodecs(): List { + return entries + } +} + + +fun CodecNameToFfmpegCodec(name: String): FfmpegCodecs { + return when (name.lowercase()) { + "hevc", "hevec", "h265", "h.265", "libx265" -> FfmpegCodecs.hevc + "h.264", "h264", "libx264" -> FfmpegCodecs.h264 + "vp9", "vp-9", "libvpx-vp9" -> FfmpegCodecs.vp9 + "av1", "libaom-av1" -> FfmpegCodecs.av1 + "mpeg4", "mp4", "libxvid" -> FfmpegCodecs.vid + "vvc", "h.266", "libvvc" -> FfmpegCodecs.vvc + "vp8", "libvpx" -> FfmpegCodecs.vp8 + else -> throw IllegalArgumentException("Unsupported codec: $name") + } +} + diff --git a/shared/ffmpeg/src/test/kotlin/no/iktdev/mediaprocessing/ffmpeg/FFmpegTest.kt b/shared/ffmpeg/src/test/kotlin/no/iktdev/mediaprocessing/ffmpeg/FFmpegTest.kt new file mode 100644 index 00000000..2d98151d --- /dev/null +++ b/shared/ffmpeg/src/test/kotlin/no/iktdev/mediaprocessing/ffmpeg/FFmpegTest.kt @@ -0,0 +1,73 @@ +package no.iktdev.mediaprocessing.ffmpeg + +import com.github.pgreze.process.ProcessResult +import kotlinx.coroutines.delay +import kotlinx.coroutines.runBlocking +import no.iktdev.mediaprocessing.ffmpeg.arguments.MpegArgument +import no.iktdev.mediaprocessing.ffmpeg.decoder.FfmpegDecodedProgress +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import java.io.File +import kotlin.system.measureTimeMillis +import kotlin.test.assertFalse + +class FFmpegTest { + + class MockFFmpeg(override val listener: Listener, val delayMillis: Long = 500, private val simulateSuccess: Boolean = true) : FFmpeg(executable = "", logDir = File("/null")) { + override suspend fun run(argument: MpegArgument) { + inputFile = argument.inputFile!! + listener.onStarted(argument.inputFile!!) + delay(delayMillis) + + result = ProcessResult( + resultCode = if (simulateSuccess) 0 else 1, + output = listOf("Simulated ffmpeg output") + ) + + if (simulateSuccess) { + listener.onCompleted(inputFile, argument.outputFile!!) + } else { + listener.onError(inputFile, "Simulated error") + } + } + } + + + @Test + @DisplayName("Test FFmpeg Mock Success") + fun scenarioSuccess() = runBlocking { + val arguments = MpegArgument() + .inputFile("input.mp4") + .outputFile("output.mp4") + .args(listOf("-y")) + .withProgress(true) + + val listener = object : FFmpeg.Listener { + var completed: Boolean = false + private set + override fun onStarted(inputFile: String) { + println("Started processing $inputFile") + } + + override fun onCompleted(inputFile: String, outputFile: String) { + println("Completed processing $inputFile to $outputFile") + completed = true + } + + override fun onProgressChanged(inputFile: String, progress: FfmpegDecodedProgress) { + println("Progress for $inputFile: $progress") + } + } + + val runner = MockFFmpeg(listener, delayMillis = 1000, simulateSuccess = true) + assertFalse(listener.completed, "Expected onCompleted to be false before run") + + val elapsed = measureTimeMillis { + runner.run(arguments) + assertTrue(listener.completed, "Expected onCompleted to be called") + } + + assertTrue(elapsed >= 1000, "Expected to wait at least 1000 ms, but waited for $elapsed ms") + } +} \ No newline at end of file diff --git a/apps/processer/src/test/kotlin/no/iktdev/mediaprocessing/processer/Resources.kt b/shared/ffmpeg/src/test/kotlin/no/iktdev/mediaprocessing/ffmpeg/Resources.kt old mode 100644 new mode 100755 similarity index 55% rename from apps/processer/src/test/kotlin/no/iktdev/mediaprocessing/processer/Resources.kt rename to shared/ffmpeg/src/test/kotlin/no/iktdev/mediaprocessing/ffmpeg/Resources.kt index 9a043430..393be876 --- a/apps/processer/src/test/kotlin/no/iktdev/mediaprocessing/processer/Resources.kt +++ b/shared/ffmpeg/src/test/kotlin/no/iktdev/mediaprocessing/ffmpeg/Resources.kt @@ -1,9 +1,5 @@ -package no.iktdev.mediaprocessing.processer +package no.iktdev.mediaprocessing.ffmpeg -import no.iktdev.mediaprocessing.shared.common.contract.Events -import no.iktdev.mediaprocessing.shared.common.contract.data.Event -import no.iktdev.mediaprocessing.shared.common.contract.jsonToEvent -import org.json.JSONArray enum class Files(val fileName: String) { Output1("encodeProgress1.txt") diff --git a/apps/processer/src/test/kotlin/no/iktdev/mediaprocessing/processer/ffmpeg/progress/FfmpegProgressDecoderTest.kt b/shared/ffmpeg/src/test/kotlin/no/iktdev/mediaprocessing/ffmpeg/decoder/FfmpegProgressDecoderTest.kt similarity index 70% rename from apps/processer/src/test/kotlin/no/iktdev/mediaprocessing/processer/ffmpeg/progress/FfmpegProgressDecoderTest.kt rename to shared/ffmpeg/src/test/kotlin/no/iktdev/mediaprocessing/ffmpeg/decoder/FfmpegProgressDecoderTest.kt index c77ff31b..1970ddb0 100644 --- a/apps/processer/src/test/kotlin/no/iktdev/mediaprocessing/processer/ffmpeg/progress/FfmpegProgressDecoderTest.kt +++ b/shared/ffmpeg/src/test/kotlin/no/iktdev/mediaprocessing/ffmpeg/decoder/FfmpegProgressDecoderTest.kt @@ -1,10 +1,8 @@ -package no.iktdev.mediaprocessing.processer.ffmpeg.progress +package no.iktdev.mediaprocessing.ffmpeg.decoder -import no.iktdev.mediaprocessing.processer.Files -import no.iktdev.mediaprocessing.processer.ffmpeg.FfmpegRunner -import no.iktdev.mediaprocessing.processer.getAsList +import no.iktdev.mediaprocessing.ffmpeg.Files +import no.iktdev.mediaprocessing.ffmpeg.getAsList import org.assertj.core.api.Assertions.assertThat -import org.junit.jupiter.api.Assertions.* import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test diff --git a/shared/ffmpeg/src/test/kotlin/no/iktdev/mediaprocessing/ffmpeg/dsl/MediaPlanTest.kt b/shared/ffmpeg/src/test/kotlin/no/iktdev/mediaprocessing/ffmpeg/dsl/MediaPlanTest.kt new file mode 100644 index 00000000..81d0d9ae --- /dev/null +++ b/shared/ffmpeg/src/test/kotlin/no/iktdev/mediaprocessing/ffmpeg/dsl/MediaPlanTest.kt @@ -0,0 +1,377 @@ +package no.iktdev.mediaprocessing.ffmpeg.dsl + +import no.iktdev.mediaprocessing.ffmpeg.data.AudioStream +import no.iktdev.mediaprocessing.ffmpeg.data.Disposition +import no.iktdev.mediaprocessing.ffmpeg.data.Tags +import no.iktdev.mediaprocessing.ffmpeg.data.VideoStream +import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test + +class MediaPlanTest { + + @Test + fun `video copy with one audio copy`() { + val plan = MediaPlan( + videoTrack = VideoTarget(0, VideoCodec.Copy), + audioTracks = mutableListOf(AudioTarget(0, AudioCodec.Copy)) + ) + + val args = plan.toFfmpegArgs( + videoStreams = listOf(mockVideoStream(codec = "h264", disposition = mockDisposition(), tags = mockTags())), + audioStreams = listOf(mockAudioStream(codec = "aac", disposition = mockDisposition(), tags = mockTags())) + ) + + Assertions.assertEquals( + listOf( + "-map", "0:v:0", "-c:v", "copy", + "-map", "0:a:0", "-c:a:0", "copy" + ), + args + ) + } + + @Test + fun `video reencode to hevc with crf`() { + val plan = MediaPlan( + videoTrack = VideoTarget(0, VideoCodec.Hevc(crf = 18)), + audioTracks = mutableListOf(AudioTarget(0, AudioCodec.Aac(bitrate = 192))) + ) + + val args = plan.toFfmpegArgs( + videoStreams = listOf(mockVideoStream(codec = "h264", disposition = mockDisposition(), tags = mockTags())), + audioStreams = listOf(mockAudioStream(codec = "mp3", disposition = mockDisposition(), tags = mockTags())) + ) + + Assertions.assertEquals( + listOf( + "-map", "0:v:0", "-c:v", "libx265", "-crf", "18", "-preset", "slow", + "-map", "0:a:0", "-c:a:0", "aac", "-b:a:0", "192k" + ), + args + ) + } + + @Test + fun `two audio tracks with different codecs`() { + val plan = MediaPlan( + videoTrack = VideoTarget(0, VideoCodec.Copy), + audioTracks = mutableListOf( + AudioTarget(0, AudioCodec.Aac(bitrate = 128)), + AudioTarget(1, AudioCodec.Opus(bitrate = 96)) + ) + ) + + val args = plan.toFfmpegArgs( + videoStreams = listOf(mockVideoStream(codec = "h264", disposition = mockDisposition(), tags = mockTags())), + audioStreams = listOf( + mockAudioStream(codec = "aac", channels = 6, disposition = mockDisposition(), tags = mockTags()), + mockAudioStream(index = 1, codec = "ac3", disposition = mockDisposition(), tags = mockTags()) + ) + ) + + Assertions.assertEquals( + listOf( + "-map", "0:v:0", "-c:v", "copy", + "-map", "0:a:0", "-c:a:0", "aac", "-b:a:0", "128k", + "-map", "0:a:1", "-c:a:1", "opus", "-b:a:1", "96k", "-application", "audio"), + args + ) + } + + @Test + fun `Video copy, Audio AAC reencode`() { + + val plan = MediaPlan( + videoTrack = VideoTarget(0, VideoCodec.H264()), + audioTracks = mutableListOf( + AudioTarget(0, AudioCodec.Aac(bitrate = 128, profile = AacProfile.LC)), + ) + ) + + val args = plan.toFfmpegArgs( + videoStreams = listOf(mockVideoStream(codec = "h264", disposition = mockDisposition(), tags = mockTags())), + audioStreams = listOf( + mockAudioStream(codec = "aac", channels = 6, profile = AacProfile.HE.ffmpegName, disposition = mockDisposition(), tags = mockTags()), + ) + ) + + Assertions.assertEquals( + listOf( + "-map", "0:v:0", "-c:v", "copy", + "-map", "0:a:0", "-c:a:0", "aac", "-b:a:0", "128k", + ), + args + ) + } + + @Test + @DisplayName("Video copy + Audio AAC reencode (HE→LC, bitrate 128k)") + fun videoCopyAudioAacReencode() { + val plan = MediaPlan( + videoTrack = VideoTarget(0, VideoCodec.H264()), + audioTracks = mutableListOf( + AudioTarget(0, AudioCodec.Aac(bitrate = 128, profile = AacProfile.LC)), + ) + ) + + val args = plan.toFfmpegArgs( + videoStreams = listOf(mockVideoStream(codec = "h264", disposition = mockDisposition(), tags = mockTags())), + audioStreams = listOf(mockAudioStream(codec = "aac", channels = 6, profile = AacProfile.HE.ffmpegName, disposition = mockDisposition(), tags = mockTags())) + ) + + Assertions.assertEquals( + listOf( + "-map", "0:v:0", "-c:v", "copy", + "-map", "0:a:0", "-c:a:0", "aac", "-b:a:0", "128k", + ), + args + ) + } + + @Test + @DisplayName("Video reencode to HEVC with CRF=18 and preset=slow, Audio copy") + fun videoReencodeHevcCrfPresetAudioCopy() { + val plan = MediaPlan( + videoTrack = VideoTarget(0, VideoCodec.Hevc(crf = 18, preset = Presets.Slow)), + audioTracks = mutableListOf(AudioTarget(0, AudioCodec.Copy)) + ) + + val args = plan.toFfmpegArgs( + videoStreams = listOf(mockVideoStream(codec = "h264", disposition = mockDisposition(), tags = mockTags())), + audioStreams = listOf(mockAudioStream(codec = "aac", channels = 2, profile = AacProfile.LC.ffmpegName, disposition = mockDisposition(), tags = mockTags())) + ) + + Assertions.assertEquals( + listOf( + "-map", "0:v:0", "-c:v", "libx265", "-crf", "18", "-preset", "slow", + "-map", "0:a:0", "-c:a:0", "copy", + ), + args + ) + } + + @Test + @DisplayName("Two audio tracks: AAC reencode 128k + Opus reencode 96k") + fun twoAudioTracksDifferentCodecs() { + val plan = MediaPlan( + videoTrack = VideoTarget(0, VideoCodec.Copy), + audioTracks = mutableListOf( + AudioTarget(0, AudioCodec.Aac(bitrate = 128)), + AudioTarget(1, AudioCodec.Opus(bitrate = 96)) + ) + ) + + val args = plan.toFfmpegArgs( + videoStreams = listOf(mockVideoStream(codec = "h264", disposition = mockDisposition(), tags = mockTags())), + audioStreams = listOf( + mockAudioStream(codec = "aac", channels = 2, profile = AacProfile.LC.ffmpegName, disposition = mockDisposition(), tags = mockTags()), + mockAudioStream(codec = "vorbis", channels = 2, profile = "", disposition = mockDisposition(), tags = mockTags()) + ) + ) + + Assertions.assertEquals( + listOf( + "-map", "0:v:0", "-c:v", "copy", + "-map", "0:a:0", "-c:a:0", "aac", "-b:a:0", "128k", + "-map", "0:a:1", "-c:a:1", "opus", "-b:a:1", "96k", "-application", "audio", + ), + args + ) + } + + @Test + @DisplayName("PCM input downmix to AAC stereo 192k") + fun pcmInputDownmixToAacStereo() { + val plan = MediaPlan( + videoTrack = VideoTarget(0, VideoCodec.Copy), + audioTracks = mutableListOf(AudioTarget(0, AudioCodec.Aac(bitrate = 192, channels = 2))) + ) + + val args = plan.toFfmpegArgs( + videoStreams = listOf(mockVideoStream(codec = "rawvideo", disposition = mockDisposition(), tags = mockTags())), + audioStreams = listOf(mockAudioStream(codec = "pcm_s16le", channels = 6, profile = "", disposition = mockDisposition(), tags = mockTags())) + ) + + Assertions.assertEquals( + listOf( + "-map", "0:v:0", "-c:v", "copy", + "-map", "0:a:0", "-c:a:0", "aac", "-b:a:0", "192k", "-ac:0", "2", + ), + args + ) + } + + @Test + @DisplayName("FLAC input remux to FLAC (no reencode)") + fun flacInputRemux() { + val plan = MediaPlan( + videoTrack = VideoTarget(0, VideoCodec.Copy), + audioTracks = mutableListOf(AudioTarget(0, AudioCodec.Flac())) + ) + + val args = plan.toFfmpegArgs( + videoStreams = listOf(mockVideoStream(codec = "rawvideo", disposition = mockDisposition(), tags = mockTags())), + audioStreams = listOf(mockAudioStream(codec = "flac", channels = 2, profile = "", disposition = mockDisposition(), tags = mockTags())) + ) + + Assertions.assertEquals( + listOf( + "-map", "0:v:0", "-c:v", "copy", + "-map", "0:a:0", "-c:a:0", "copy", + ), + args + ) + } + + + @Test + @DisplayName("Extended track skipped when same as default and default is copy") + fun skipExtendedIfSameAsDefaultAndDefaultIsCopy() { + // Arrange: lag en plan med default og extended som peker på samme input index + val defaultTarget = AudioTarget( + index = 0, + codec = AudioCodec.Copy // default er copy + ) + val extendedTarget = AudioTarget( + index = 0, // peker på samme input index som default + codec = AudioCodec.Copy + ) + + val plan = MediaPlan( + videoTrack = VideoTarget(0, VideoCodec.Copy), + audioTracks = mutableListOf(defaultTarget, extendedTarget) + ) + + val audioStreams = listOf( + mockAudioStream(codec = "aac", channels = 2, disposition = mockDisposition(), tags = mockTags()) + ) + val videoStreams = listOf( + mockVideoStream(codec = "h264", disposition = mockDisposition(), tags = mockTags()) + ) + + // Act: bygg ffmpeg args + val args = plan.toFfmpegArgs(videoStreams, audioStreams) + + // Assert: extended track skal være forkastet, kun ett audio map/codec skal finnes + val expected = listOf( + "-map", "0:v:0", "-c:v", "copy", + "-map", "0:a:0", "-c:a:0", "copy", + ) + assertEquals(expected, args) + } + + + fun mockVideoStream( + index: Int = 0, + codec: String = "h264", + width: Int = 1920, + height: Int = 1080, + disposition: Disposition, + tags: Tags + ) = VideoStream( + index = index, + codec_name = codec, + codec_long_name = "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10", + codec_type = "video", + codec_tag_string = "avc1", + codec_tag = "0x31637661", + r_frame_rate = "25/1", + avg_frame_rate = "25/1", + time_base = "1/90000", + start_pts = 0, + start_time = "0.000000", + disposition = disposition, + tags = tags, + duration = "60.0", + duration_ts = 54000, + profile = "High", + width = width, + height = height, + coded_width = width, + coded_height = height, + closed_captions = 0, + has_b_frames = 2, + sample_aspect_ratio = "1:1", + display_aspect_ratio = "16:9", + pix_fmt = "yuv420p", + level = 40, + color_range = "tv", + color_space = "bt709", + color_transfer = "bt709", + color_primaries = "bt709", + chroma_location = "left", + refs = 1 + ) + + fun mockAudioStream( + index: Int = 0, + codec: String = "aac", + channels: Int = 2, + profile: String = "LC", + disposition: Disposition, + tags: Tags + ) = AudioStream( + index = index, + codec_name = codec, + codec_long_name = "AAC (Advanced Audio Coding)", + codec_type = "audio", + codec_tag_string = "mp4a", + codec_tag = "0x6134706d", + r_frame_rate = "0/0", + avg_frame_rate = "0/0", + time_base = "1/48000", + start_pts = 0, + start_time = "0.000000", + duration = "60.0", + duration_ts = 2880000, + disposition = disposition, + tags = tags, + profile = profile, + sample_fmt = "fltp", + sample_rate = "48000", + channels = channels, + channel_layout = "stereo", + bits_per_sample = 0 + ) + + fun mockDisposition( + default: Int = 1, + forced: Int = 0 + ) = Disposition( + default = default, + dub = 0, + original = 0, + comment = 0, + lyrics = 0, + karaoke = 0, + forced = forced, + hearing_impaired = 0, + captions = 0, + visual_impaired = 0, + clean_effects = 0, + attached_pic = 0, + timed_thumbnails = 0 + ) + + fun mockTags( + language: String? = "eng", + title: String? = null, + filename: String? = null + ) = Tags( + title = title, + BPS = null, + DURATION = null, + NUMBER_OF_FRAMES = 0, + NUMBER_OF_BYTES = null, + _STATISTICS_WRITING_APP = null, + _STATISTICS_WRITING_DATE_UTC = null, + _STATISTICS_TAGS = null, + language = language, + filename = filename, + mimetype = null + ) + + +} \ No newline at end of file diff --git a/apps/processer/src/test/resources/encodeProgress1.txt b/shared/ffmpeg/src/test/resources/encodeProgress1.txt old mode 100644 new mode 100755 similarity index 92% rename from apps/processer/src/test/resources/encodeProgress1.txt rename to shared/ffmpeg/src/test/resources/encodeProgress1.txt index 2abf5423..7ceacd39 --- a/apps/processer/src/test/resources/encodeProgress1.txt +++ b/shared/ffmpeg/src/test/resources/encodeProgress1.txt @@ -10,7 +10,7 @@ Input #0, matroska,webm, from '/src/input/completed/standalone/Potato.mkv': DURATION-eng : 00:29:09.748000000 NUMBER_OF_FRAMES-eng: 41952 NUMBER_OF_BYTES-eng: 2020851044 - _STATISTICS_WRITING_APP-eng: mkvmerge v43.0.0 ('The Quartermaster') 64-bit + _STATISTICS_WRITING_APP-eng: mkvmerge v43.0.0 ('The Potato') 64-bit _STATISTICS_WRITING_DATE_UTC-eng: 2020-02-17 16:42:19 _STATISTICS_TAGS-eng: BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES Stream #0:1(eng): Audio: eac3, 48000 Hz, 5.1, fltp (default) @@ -19,7 +19,7 @@ Input #0, matroska,webm, from '/src/input/completed/standalone/Potato.mkv': DURATION-eng : 00:29:09.728000000 NUMBER_OF_FRAMES-eng: 54679 NUMBER_OF_BYTES-eng: 139978240 - _STATISTICS_WRITING_APP-eng: mkvmerge v43.0.0 ('The Quartermaster') 64-bit + _STATISTICS_WRITING_APP-eng: mkvmerge v43.0.0 ('The Potato') 64-bit _STATISTICS_WRITING_DATE_UTC-eng: 2020-02-17 16:42:19 _STATISTICS_TAGS-eng: BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES Stream #0:2(eng): Subtitle: subrip @@ -28,7 +28,7 @@ Input #0, matroska,webm, from '/src/input/completed/standalone/Potato.mkv': DURATION-eng : 00:28:08.917000000 NUMBER_OF_FRAMES-eng: 718 NUMBER_OF_BYTES-eng: 25445 - _STATISTICS_WRITING_APP-eng: mkvmerge v43.0.0 ('The Quartermaster') 64-bit + _STATISTICS_WRITING_APP-eng: mkvmerge v43.0.0 ('The Potato') 64-bit _STATISTICS_WRITING_DATE_UTC-eng: 2020-02-17 16:42:19 _STATISTICS_TAGS-eng: BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES Stream #0:3(eng): Subtitle: subrip @@ -38,7 +38,7 @@ Input #0, matroska,webm, from '/src/input/completed/standalone/Potato.mkv': DURATION-eng : 00:28:08.917000000 NUMBER_OF_FRAMES-eng: 718 NUMBER_OF_BYTES-eng: 25445 - _STATISTICS_WRITING_APP-eng: mkvmerge v43.0.0 ('The Quartermaster') 64-bit + _STATISTICS_WRITING_APP-eng: mkvmerge v43.0.0 ('The Potato') 64-bit _STATISTICS_WRITING_DATE_UTC-eng: 2020-02-17 16:42:19 _STATISTICS_TAGS-eng: BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES Stream mapping: @@ -112,7 +112,7 @@ Output #0, mp4, to '/src/cache/Potato.work.mp4': DURATION-eng : 00:29:09.748000000 NUMBER_OF_FRAMES-eng: 41952 NUMBER_OF_BYTES-eng: 2020851044 - _STATISTICS_WRITING_APP-eng: mkvmerge v43.0.0 ('The Quartermaster') 64-bit + _STATISTICS_WRITING_APP-eng: mkvmerge v43.0.0 ('The Potato') 64-bit _STATISTICS_WRITING_DATE_UTC-eng: 2020-02-17 16:42:19 _STATISTICS_TAGS-eng: BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES encoder : Lavc58.91.100 libx265 @@ -124,7 +124,7 @@ Output #0, mp4, to '/src/cache/Potato.work.mp4': DURATION-eng : 00:29:09.728000000 NUMBER_OF_FRAMES-eng: 54679 NUMBER_OF_BYTES-eng: 139978240 - _STATISTICS_WRITING_APP-eng: mkvmerge v43.0.0 ('The Quartermaster') 64-bit + _STATISTICS_WRITING_APP-eng: mkvmerge v43.0.0 ('The Potato') 64-bit _STATISTICS_WRITING_DATE_UTC-eng: 2020-02-17 16:42:19 _STATISTICS_TAGS-eng: BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES frame=34