From 866f7f228cdcc0d9341d82a14baea005a3cc1005 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Brage=20Skj=C3=B8nborg?= Date: Mon, 2 Feb 2026 21:43:11 +0100 Subject: [PATCH] Track selection --- .../mediaprocessing/coordinator/Preference.kt | 171 ++++++++-- .../controller/PreferenceController.kt | 34 ++ .../events/MediaCreateExtractTaskListener.kt | 2 +- .../events/MediaSelectEncodeTracksListener.kt | 160 ++++++--- .../MediaSelectExtractTracksListener.kt | 124 +++++-- .../mediaprocessing/FakeCoordinatorEnv.kt | 22 ++ .../no/iktdev/mediaprocessing/MockData.kt | 111 ++++++ .../no/iktdev/mediaprocessing/Resources.kt | 3 +- .../no/iktdev/mediaprocessing/TestBase.kt | 2 + .../MediaSelectEncodeTracksListenerTest.kt | 315 +++++++----------- .../MediaSelectExtractTracksListenerTest.kt | 205 +++++++----- 11 files changed, 783 insertions(+), 366 deletions(-) create mode 100644 apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/controller/PreferenceController.kt create mode 100644 apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/FakeCoordinatorEnv.kt 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 index 7662c7fa..9b6aefa3 100644 --- a/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/Preference.kt +++ b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/Preference.kt @@ -1,16 +1,55 @@ package no.iktdev.mediaprocessing.coordinator import com.google.gson.Gson +import com.google.gson.JsonObject +import com.google.gson.JsonParser import no.iktdev.mediaprocessing.ffmpeg.dsl.AudioCodec import no.iktdev.mediaprocessing.ffmpeg.dsl.VideoCodec import org.springframework.stereotype.Component import java.io.File +import java.io.IOException +// ------------------------------------------------------------ +// MODELLER +// ------------------------------------------------------------ data class PeferenceConfig( - val processer: ProcesserPreference + val processer: ProcesserPreference, + val language: LanguagePreference ) +data class LanguagePreference( + val preferredAudio: List, + val preferredSubtitles: List, + + val preferOriginal: Boolean = true, + val avoidDub: Boolean = true, + + // NEW: Prioritet for hvilket subtitle-format som skal brukes som master + val subtitleFormatPriority: List = listOf("ass", "srt", "vtt", "smi"), + + // NEW: Hvordan subtitles skal velges + val subtitleSelectionMode: SubtitleSelectionMode = SubtitleSelectionMode.DialogueOnly +) { + companion object { + fun default() = LanguagePreference( + preferredAudio = listOf("eng", "nor", "jpn"), + preferredSubtitles = listOf("eng", "nor"), + preferOriginal = true, + avoidDub = true, + subtitleFormatPriority = listOf("ass", "srt", "vtt", "smi"), + subtitleSelectionMode = SubtitleSelectionMode.DialogueOnly + ) + } +} + +enum class SubtitleSelectionMode { + DialogueOnly, // Kun dialog + DialogueAndForced, // Dialog + forced + All // Alle typer +} + + data class ProcesserPreference( val videoPreference: VideoPreference? = null, val audioPreference: AudioPreference? = null @@ -19,7 +58,7 @@ data class ProcesserPreference( fun default(): ProcesserPreference { return ProcesserPreference( videoPreference = VideoPreference(VideoCodec.Hevc(), false), - audioPreference = AudioPreference("jpn", AudioCodec.Aac()) + audioPreference = AudioPreference(AudioCodec.Aac()) ) } } @@ -31,57 +70,121 @@ data class VideoPreference( ) data class AudioPreference( - val language: String? = null, val codec: AudioCodec ) +// ------------------------------------------------------------ +// PREFERENCE COMPONENT +// ------------------------------------------------------------ + @Component class Preference(private val coordinatorEnv: CoordinatorEnv) { - fun getProcesserPreference(): ProcesserPreference { - val default = ProcesserPreference.default() + private val gson = Gson() + private val lock = Any() + /** + * Leser hele configen, men bevarer ukjente felter. + */ + fun getFullConfig(): PeferenceConfig { val file = coordinatorEnv.preference + if (!file.exists()) { - // Opprett fil med default - file.writeText(Gson().toJson(PeferenceConfig(default))) + val default = PeferenceConfig( + processer = ProcesserPreference.default(), + language = LanguagePreference.default() + ) + writeJsonObject(JsonObject().apply { + add("processer", gson.toJsonTree(default.processer)) + add("language", gson.toJsonTree(default.language)) + }, file) return default } - val text = try { - file.readText() + val root = try { + JsonParser.parseString(file.readText()).asJsonObject } catch (e: Exception) { - return default + return PeferenceConfig( + processer = ProcesserPreference.default(), + language = LanguagePreference.default() + ) } - val parsed = try { - Gson().fromJson(text, PeferenceConfig::class.java) - } catch (e: Exception) { - return default + val processer = try { + gson.fromJson(root.get("processer"), ProcesserPreference::class.java) + ?: ProcesserPreference.default() + } catch (_: Exception) { + ProcesserPreference.default() } - // Hvis hele configen er null → default - val cfg = parsed ?: return default + val language = try { + gson.fromJson(root.get("language"), LanguagePreference::class.java) + ?: LanguagePreference.default() + } catch (_: Exception) { + LanguagePreference.default() + } - // Hvis processer er null → default - val p = cfg.processer ?: return default + return PeferenceConfig(processer, language) + } - // Hvis underfelter er null → fyll inn default - val safeVideo = p.videoPreference ?: default.videoPreference - val safeAudio = p.audioPreference ?: default.audioPreference + fun getProcesserPreference(): ProcesserPreference = + getFullConfig().processer - return ProcesserPreference( - videoPreference = safeVideo, - audioPreference = safeAudio - ) - } -} - - -private fun File.ifExists(block: File.() -> Unit, orElse: () -> Unit = {}) { - if (this.exists()) { - block() - } else { - orElse() + fun getLanguagePreference(): LanguagePreference = + getFullConfig().language + + /** + * Oppdaterer kun processer-delen og bevarer resten av JSON. + */ + fun saveProcesserPreference(pref: ProcesserPreference) { + val file = coordinatorEnv.preference + synchronized(lock) { + val root = readOrEmpty(file) + root.add("processer", gson.toJsonTree(pref)) + writeJsonObject(root, file) + } + } + + /** + * Oppdaterer kun language-delen og bevarer resten av JSON. + */ + fun saveLanguagePreference(pref: LanguagePreference) { + val file = coordinatorEnv.preference + synchronized(lock) { + val root = readOrEmpty(file) + root.add("language", gson.toJsonTree(pref)) + writeJsonObject(root, file) + } + } + + /** + * Overskriver hele configen (brukes hvis FE sender alt). + */ + fun saveFullConfig(cfg: PeferenceConfig) { + val file = coordinatorEnv.preference + synchronized(lock) { + val root = JsonObject() + root.add("processer", gson.toJsonTree(cfg.processer)) + root.add("language", gson.toJsonTree(cfg.language)) + writeJsonObject(root, file) + } + } + + private fun readOrEmpty(file: File): JsonObject = + if (file.exists()) { + try { + JsonParser.parseString(file.readText()).asJsonObject + } catch (_: Exception) { + JsonObject() + } + } else JsonObject() + + private fun writeJsonObject(obj: JsonObject, file: File) { + try { + file.parentFile?.mkdirs() + file.writeText(gson.toJson(obj)) + } catch (e: IOException) { + throw RuntimeException("Failed to write preference file", e) + } } } diff --git a/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/controller/PreferenceController.kt b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/controller/PreferenceController.kt new file mode 100644 index 00000000..c38fdff6 --- /dev/null +++ b/apps/coordinator/src/main/kotlin/no/iktdev/mediaprocessing/coordinator/controller/PreferenceController.kt @@ -0,0 +1,34 @@ +package no.iktdev.mediaprocessing.coordinator.controller + +import no.iktdev.mediaprocessing.coordinator.PeferenceConfig +import no.iktdev.mediaprocessing.coordinator.Preference +import no.iktdev.mediaprocessing.coordinator.ProcesserPreference +import org.springframework.http.ResponseEntity +import org.springframework.web.bind.annotation.* + +@RestController +@RequestMapping("/preference") +class PreferenceController( + private val preference: Preference +) { + + @GetMapping + fun getFull(): ResponseEntity = + ResponseEntity.ok(preference.getFullConfig()) + + @PutMapping + fun putFull(@RequestBody body: PeferenceConfig): ResponseEntity { + preference.saveFullConfig(body) + return ResponseEntity.ok(preference.getFullConfig()) + } + + @GetMapping("/processer") + fun getProcesser(): ResponseEntity = + ResponseEntity.ok(preference.getProcesserPreference()) + + @PutMapping("/processer") + fun putProcesser(@RequestBody body: ProcesserPreference): ResponseEntity { + preference.saveProcesserPreference(body) + return ResponseEntity.ok(preference.getProcesserPreference()) + } +} 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 index 9629e1f7..993dfba9 100644 --- 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 @@ -13,7 +13,7 @@ import java.io.File import java.util.* @Component -class MediaCreateExtractTaskListener: EventListener() { +class MediaCreateExtractTaskListener(): EventListener() { override fun onEvent( event: Event, history: List 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 index 24df9c96..344db552 100644 --- 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 @@ -2,6 +2,7 @@ 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.data.AudioStream import no.iktdev.mediaprocessing.ffmpeg.data.VideoStream import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.MediaStreamParsedEvent @@ -9,11 +10,9 @@ import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.MediaT import org.springframework.stereotype.Component @Component -class MediaSelectEncodeTracksListener: EventListener() { - - fun getAudioLanguagePreference(): List { - return listOf("jpn") - } +class MediaSelectEncodeTracksListener( + private val preference: Preference +) : EventListener() { override fun onEvent( event: Event, @@ -21,10 +20,12 @@ class MediaSelectEncodeTracksListener: EventListener() { ): 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) + val audioExtendedTrack = getAudioExtendedTrackToUse( + useEvent.data.audioStream, + selectedDefaultTrack = audioDefaultTrack + ) return MediaTracksEncodeSelectedEvent( selectedVideoTrack = videoTrackIndex, @@ -33,42 +34,125 @@ class MediaSelectEncodeTracksListener: EventListener() { ).derivedOf(event) } - protected fun getAudioExtendedTrackToUse(audioStream: List, selectedDefaultTrack: Int): Int? { - val durationFiltered = audioStream.filterOnPreferredLanguage() + // ------------------------------------------------------------ + // AUDIO SELECTION + // ------------------------------------------------------------ + + + + private fun getAudioDefaultTrackToUse(audioStreams: List): Int { + val pref = preference.getLanguagePreference() + + val selected = selectBestAudioStream( + streams = audioStreams, + preferredLanguages = pref.preferredAudio, + preferOriginal = pref.preferOriginal, + avoidDub = pref.avoidDub, + mode = AudioSelectMode.DEFAULT + ) ?: audioStreams.firstOrNull() + + return audioStreams.indexOf(selected) + } + + private fun getAudioExtendedTrackToUse( + audioStreams: List, + selectedDefaultTrack: Int + ): Int? { + val pref = preference.getLanguagePreference() + + val candidates = audioStreams + .filter { it.index != selectedDefaultTrack } .filter { (it.duration_ts ?: 0) > 0 } .filter { it.channels > 2 } - .filter { it.index != selectedDefaultTrack } - val selected = durationFiltered.firstOrNull() ?: return null - return audioStream.indexOf(selected) + + val selected = selectBestAudioStream( + streams = candidates, + preferredLanguages = pref.preferredAudio, + preferOriginal = pref.preferOriginal, + avoidDub = pref.avoidDub, + mode = AudioSelectMode.EXTENDED + ) ?: return null + + return audioStreams.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. - */ - protected fun getAudioDefaultTrackToUse(audioStream: List): Int { - val durationFiltered = audioStream.filterOnPreferredLanguage() + + // ------------------------------------------------------------ + // CORE AUDIO SELECTION LOGIC + // ------------------------------------------------------------ + + private enum class AudioSelectMode { DEFAULT, EXTENDED } + + private fun selectBestAudioStream( + streams: List, + preferredLanguages: List, + preferOriginal: Boolean, + avoidDub: Boolean, + mode: AudioSelectMode + ): AudioStream? { + if (streams.isEmpty()) return null + + // 1. Originalspråk + if (preferOriginal) { + val originals = streams.filter { it.disposition.original == 1 } + if (originals.isNotEmpty()) { + return when (mode) { + AudioSelectMode.DEFAULT -> originals.minByOrNull { it.channels } + AudioSelectMode.EXTENDED -> originals.maxByOrNull { it.channels } + } + } + } + + // 2. Filtrer bort dub + val filtered = if (avoidDub) { + streams.filter { it.disposition.dub != 1 } + } else streams + + // 3. Foretrukne språk + for (lang in preferredLanguages) { + val match = filtered.filter { + it.tags.language?.equals(lang, ignoreCase = true) == true + } + if (match.isNotEmpty()) { + return when (mode) { + AudioSelectMode.DEFAULT -> match.minByOrNull { it.channels } + AudioSelectMode.EXTENDED -> match.maxByOrNull { it.channels } + } + } + } + + // 4. Default-flagget + val default = filtered.firstOrNull { it.disposition.default == 1 } + if (default != null) return default + + // 5. Fallback + return filtered.firstOrNull() + } + + + // ------------------------------------------------------------ + // QUALITY SCORE (no bitrate) + // ------------------------------------------------------------ + + private fun qualityScore(s: AudioStream): Int { + val channelsScore = s.channels * 10 + val bitsScore = s.bits_per_sample + val sampleRateScore = s.sample_rate.toIntOrNull()?.div(1000) ?: 0 + + return channelsScore + bitsScore + sampleRateScore + } + + // ------------------------------------------------------------ + // VIDEO SELECTION + // ------------------------------------------------------------ + + 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") - val selected = durationFiltered - .filter { it.channels == 2 }.ifEmpty { durationFiltered } - .minByOrNull { 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. - */ - protected fun List.filterOnPreferredLanguage(): List { - return this.filter { it.tags.language in getAudioLanguagePreference() }.ifEmpty { this } - } - - protected 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 index c0b7087f..fdbcb931 100644 --- 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 @@ -2,22 +2,19 @@ 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.coordinator.SubtitleSelectionMode 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.SubtitleItem import no.iktdev.mediaprocessing.shared.common.model.SubtitleType import org.springframework.stereotype.Component @Component -class MediaSelectExtractTracksListener: EventListener() { - - open fun limitToLanguages(): Set { - return emptySet() - } - - fun useTypes(): Set { - return setOf(SubtitleType.Dialogue) - } +class MediaSelectExtractTracksListener( + private val preference: Preference +) : EventListener() { override fun onEvent( event: Event, @@ -25,20 +22,109 @@ class MediaSelectExtractTracksListener: EventListener() { ): Event? { val useEvent = event as? MediaTracksDetermineSubtitleTypeEvent ?: return null - val filtered = useEvent.subtitleTrackItems - .filter { it.type in useTypes() } - .map { it.stream } - .filterOnPreferredLanguage() + val pref = preference.getLanguagePreference() + + // 1. Filter by subtitle type (dialogue, forced, etc.) + val filteredByType = filterBySelectionMode( + items = useEvent.subtitleTrackItems, + mode = pref.subtitleSelectionMode + ) + + // 2. Extract streams + val streams = filteredByType.map { it.stream } + + // 3. Select subtitles based on language + format priority + val selected = selectSubtitleStreams( + streams = streams, + preferredLanguages = pref.preferredSubtitles, + preferOriginal = pref.preferOriginal, + formatPriority = pref.subtitleFormatPriority + ) return MediaTracksExtractSelectedEvent( - selectedSubtitleTracks = filtered.map { it.index } + selectedSubtitleTracks = selected.map { it.index } ).derivedOf(event) } + // ------------------------------------------------------------ + // TYPE FILTERING + // ------------------------------------------------------------ - protected 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) } + private fun filterBySelectionMode( + items: List, + mode: SubtitleSelectionMode + ): List { + return when (mode) { + SubtitleSelectionMode.DialogueOnly -> + items.filter { it.type == SubtitleType.Dialogue } + + SubtitleSelectionMode.DialogueAndForced -> + items.filter { + it.type == SubtitleType.Dialogue || it.stream.disposition?.forced == 1 + } + + SubtitleSelectionMode.All -> + items + } } -} \ No newline at end of file + + // ------------------------------------------------------------ + // SUBTITLE SELECTION LOGIC + // ------------------------------------------------------------ + + private fun selectSubtitleStreams( + streams: List, + preferredLanguages: List, + preferOriginal: Boolean, + formatPriority: List + ): List { + if (streams.isEmpty()) return emptyList() + + // 1. Originalspråk + if (preferOriginal) { + val originals = streams.filter { it.disposition?.original == 1 } + if (originals.isNotEmpty()) { + return originals.uniquePerLanguageBestFormat(formatPriority) + } + } + + // 2. Preferred languages + for (lang in preferredLanguages) { + val match = streams.filter { + it.tags.language?.equals(lang, ignoreCase = true) == true || + it.subtitle_tags.language?.equals(lang, ignoreCase = true) == true + } + if (match.isNotEmpty()) { + return match.uniquePerLanguageBestFormat(formatPriority) + } + } + + // 3. Default subtitles + val defaults = streams.filter { it.disposition?.default == 1 } + if (defaults.isNotEmpty()) { + return defaults.uniquePerLanguageBestFormat(formatPriority) + } + + // 4. Fallback: all subtitles + return streams.uniquePerLanguageBestFormat(formatPriority) + } + + // ------------------------------------------------------------ + // UNIQUE PER LANGUAGE + FORMAT PRIORITY + // ------------------------------------------------------------ + + private fun List.uniquePerLanguageBestFormat( + formatPriority: List + ): List { + return this + .groupBy { it.tags.language ?: it.subtitle_tags.language ?: "unknown" } + .mapNotNull { (_, langGroup) -> + langGroup + .sortedBy { s -> + val idx = formatPriority.indexOf(s.codec_name.lowercase()) + if (idx == -1) Int.MAX_VALUE else idx + } + .firstOrNull() + } + } +} diff --git a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/FakeCoordinatorEnv.kt b/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/FakeCoordinatorEnv.kt new file mode 100644 index 00000000..1f149471 --- /dev/null +++ b/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/FakeCoordinatorEnv.kt @@ -0,0 +1,22 @@ +package no.iktdev.mediaprocessing + +import no.iktdev.mediaprocessing.coordinator.CoordinatorEnv +import no.iktdev.mediaprocessing.coordinator.config.ExecutablesConfig +import no.iktdev.mediaprocessing.shared.common.configs.MediaPaths +import no.iktdev.mediaprocessing.shared.common.configs.StreamItConfig +import java.io.File + +// ------------------------------------------------------------ +// Fake CoordinatorEnv for testing +// ------------------------------------------------------------ +class FakeCoordinatorEnv(prefFile: File) : CoordinatorEnv( + streamIt = StreamItConfig(address = "http://localhost"), + exec = ExecutablesConfig(ffprobe = "/usr/bin/ffprobe"), + media = MediaPaths( + cache = "/tmp/cache", + outgoing = "/tmp/outgoing", + incoming = "/tmp/incoming" + ) +) { + override val preference: File = prefFile +} \ No newline at end of file diff --git a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/MockData.kt b/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/MockData.kt index 68e8d6d6..9ccfed27 100644 --- a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/MockData.kt +++ b/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/MockData.kt @@ -3,8 +3,11 @@ package no.iktdev.mediaprocessing import no.iktdev.eventi.models.Event import no.iktdev.eventi.models.store.TaskStatus import no.iktdev.mediaprocessing.TestBase.DummyTask +import no.iktdev.mediaprocessing.ffmpeg.data.* import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.* import no.iktdev.mediaprocessing.shared.common.model.MediaType +import no.iktdev.mediaprocessing.shared.common.model.SubtitleItem +import no.iktdev.mediaprocessing.shared.common.model.SubtitleType import java.util.* object MockData { @@ -119,4 +122,112 @@ object MockData { return listOf(start, result) } + fun dummyAudioStream( + index: Int, + language: String, + channels: Int, + durationTs: Long = 1000 + ): AudioStream { + return AudioStream( + index = index, + codec_name = "aac", + codec_long_name = "AAC", + codec_type = "audio", + codec_tag_string = "", + codec_tag = "", + r_frame_rate = "0/0", + avg_frame_rate = "0/0", + time_base = "1/1000", + start_pts = 0, + start_time = "0", + duration = null, + duration_ts = durationTs, + disposition = Disposition(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + tags = Tags( + title = null, 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 = null, mimetype = null + ), + profile = "LC", + sample_fmt = "fltp", + sample_rate = "48000", + channels = channels, + channel_layout = "stereo", + bits_per_sample = 16 + ) + } + + fun dummyVideoStream(index: Int, durationTs: Long = 1000): VideoStream { + return VideoStream( + index = index, + codec_name = "h264", + codec_long_name = "H.264", + codec_type = "video", + codec_tag_string = "", + codec_tag = "", + r_frame_rate = "25/1", + avg_frame_rate = "25/1", + time_base = "1/1000", + start_pts = 0, + start_time = "0", + duration = null, + duration_ts = durationTs, + disposition = Disposition(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), + tags = Tags( + title = null, 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 = "eng", filename = null, mimetype = null + ), + profile = "main", + width = 1920, + height = 1080, + coded_width = 1920, + coded_height = 1080, + closed_captions = 0, + has_b_frames = 0, + sample_aspect_ratio = "1:1", + display_aspect_ratio = "16:9", + pix_fmt = "yuv420p", + level = 30, + color_range = "tv", + color_space = "bt709", + color_transfer = "bt709", + color_primaries = "bt709", + chroma_location = "left", + refs = 1 + ) + } + + fun dummySubtitleStream(index: Int, language: String?): SubtitleStream { + return SubtitleStream( + index = index, + codec_name = "ass", + codec_long_name = "ASS", + codec_type = "subtitle", + codec_tag_string = "", + codec_tag = "", + r_frame_rate = "0/0", + avg_frame_rate = "0/0", + time_base = "1/1000", + start_pts = 0, + start_time = "0", + duration = null, + duration_ts = 1000, + disposition = null, + tags = Tags( + title = null, 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 = null, mimetype = null + ), + subtitle_tags = SubtitleTags(language = language, filename = null, mimetype = null) + ) + } + + fun dummySubtitleItem(index: Int, language: String?, type: SubtitleType): SubtitleItem { + val stream = dummySubtitleStream(index, language) + return SubtitleItem(stream = stream, type = type) + } + + + } \ 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 20557dac..f77ae31c 100644 --- a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/Resources.kt +++ b/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/Resources.kt @@ -4,7 +4,8 @@ import no.iktdev.eventi.models.Event import org.json.JSONArray enum class Files(val fileName: String) { - MultipleLanguageBased("Events.json") + MultipleLanguageBased("Events.json"), + MediaStreamParsedEvent("MediaStreamParsedEvent.json") } fun Files.getContent(): String? { diff --git a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/TestBase.kt b/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/TestBase.kt index ab86bc68..51f40731 100644 --- a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/TestBase.kt +++ b/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/TestBase.kt @@ -59,4 +59,6 @@ open class TestBase { return start } + + } \ No newline at end of file diff --git a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaSelectEncodeTracksListenerTest.kt b/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaSelectEncodeTracksListenerTest.kt index 4dc3e2b6..5ad71911 100644 --- a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaSelectEncodeTracksListenerTest.kt +++ b/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaSelectEncodeTracksListenerTest.kt @@ -1,212 +1,151 @@ package no.iktdev.mediaprocessing.coordinator.listeners.events import no.iktdev.eventi.models.Event -import no.iktdev.mediaprocessing.ffmpeg.data.* +import no.iktdev.mediaprocessing.FakeCoordinatorEnv +import no.iktdev.mediaprocessing.MockData +import no.iktdev.mediaprocessing.coordinator.Preference +import no.iktdev.mediaprocessing.ffmpeg.data.ParsedMediaStreams import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.MediaStreamParsedEvent import no.iktdev.mediaprocessing.shared.common.event_task_contract.events.MediaTracksEncodeSelectedEvent -import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +import java.io.File -class MediaTracksEncodeSelectorTest: MediaSelectEncodeTracksListener() { +class MediaTracksEncodeSelectorTest { - private fun dummyAudioStream( - index: Int, - language: String, - channels: Int, - durationTs: Long = 1000 - ): AudioStream { - return AudioStream( - index = index, - codec_name = "aac", - codec_long_name = "AAC", - codec_type = "audio", - codec_tag_string = "", - codec_tag = "", - r_frame_rate = "0/0", - avg_frame_rate = "0/0", - time_base = "1/1000", - start_pts = 0, - start_time = "0", - duration = null, - duration_ts = durationTs, - disposition = Disposition(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), - tags = Tags( - title = null, 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 = null, mimetype = null - ), - profile = "LC", - sample_fmt = "fltp", - sample_rate = "48000", - channels = channels, - channel_layout = "stereo", - bits_per_sample = 0 + private fun testPreference(): Preference { + val tmp = File.createTempFile("pref", ".json") + tmp.writeText( + """ + { + "processer": {}, + "language": { + "preferredAudio": ["jpn", "eng"], + "preferredSubtitles": ["eng"], + "preferOriginal": true, + "avoidDub": true, + "subtitleFormatPriority": ["ass","srt","vtt","smi"], + "subtitleSelectionMode": "DialogueOnly" + } + } + """.trimIndent() ) + return Preference(FakeCoordinatorEnv(tmp)) } - private fun dummyVideoStream(index: Int, durationTs: Long = 1000): VideoStream { - return VideoStream( - index = index, - codec_name = "h264", - codec_long_name = "H.264", - codec_type = "video", - codec_tag_string = "", - codec_tag = "", - r_frame_rate = "25/1", - avg_frame_rate = "25/1", - time_base = "1/1000", - start_pts = 0, - start_time = "0", - duration = null, - duration_ts = durationTs, - disposition = Disposition(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0), - tags = Tags( - title = null, 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 = "eng", filename = null, mimetype = null - ), - profile = "main", - width = 1920, - height = 1080, - coded_width = 1920, - coded_height = 1080, - closed_captions = 0, - has_b_frames = 0, - sample_aspect_ratio = "1:1", - display_aspect_ratio = "16:9", - pix_fmt = "yuv420p", - level = 30, - color_range = "tv", - color_space = "bt709", - color_transfer = "bt709", - color_primaries = "bt709", - chroma_location = "left", - refs = 1 - ) - } + private val listener = MediaSelectEncodeTracksListener(testPreference()) + + // ------------------------------------------------------------ + // TESTS + // ------------------------------------------------------------ @Test @DisplayName(""" - Hvis video streams har ulik varighet - Når getVideoTrackToUse kalles + Når video streams er tilgjengelige + Hvis en stream har lengre varighet enn de andre Så: - Returneres index til stream med lengst varighet - """) + Velges video-sporet med lengst varighet + """) fun testVideoTrackSelection() { - val streams = listOf(dummyVideoStream(0, 1000), dummyVideoStream(1, 5000)) - val index = getVideoTrackToUse(streams) - assertEquals(1, index) - } - - @Test - @DisplayName(""" - Hvis audio streams inneholder foretrukket språk jpn med 2 kanaler - Når getAudioDefaultTrackToUse kalles - Så: - Returneres index til jpn stereo track - """) - fun testAudioDefaultTrackSelectionPreferredLanguageStereo() { val streams = listOf( - dummyAudioStream(0, "eng", 2), - dummyAudioStream(1, "jpn", 2), - dummyAudioStream(2, "jpn", 6) - ) - val index = getAudioDefaultTrackToUse(streams) - assertEquals(1, index) - } - - @Test - @DisplayName(""" - Hvis audio streams inneholder foretrukket språk jpn med 6 kanaler - Når getAudioExtendedTrackToUse kalles - Så: - Returneres index til jpn 6-kanals track - """) - fun testAudioExtendedTrackSelectionPreferredLanguageSurround() { - val streams = listOf( - dummyAudioStream(0, "jpn", 2), - dummyAudioStream(1, "jpn", 6) - ) - val defaultIndex = getAudioDefaultTrackToUse(streams) - val extendedIndex = getAudioExtendedTrackToUse(streams, defaultIndex) - assertEquals(0, defaultIndex) - assertEquals(1, extendedIndex) - } - - @Test - @DisplayName(""" - Hvis audio streams ikke matcher foretrukket språk - Når filterOnPreferredLanguage kalles - Så: - Returneres original liste uten filtrering - """) - fun testFilterOnPreferredLanguageFallback() { - val streams = listOf( - dummyAudioStream(0, "eng", 2), - dummyAudioStream(1, "fra", 2) - ) - val filtered = streams.filterOnPreferredLanguage() - assertEquals(streams.size, filtered.size) - } - - @Test - @DisplayName(""" - Hvis audio streams ikke matcher foretrukket språk - Når getAudioDefaultTrackToUse kalles - Så: - Velges et spor (fallback) selv om ingen matcher - """) - fun testAudioDefaultTrackFallbackSelection() { - val streams = listOf( - dummyAudioStream(0, "eng", 2), - dummyAudioStream(1, "fra", 2) + MockData.dummyVideoStream(0, 1000), + MockData.dummyVideoStream(1, 5000) ) - // filterOnPreferredLanguage skal returnere original listen - val filtered = streams.filterOnPreferredLanguage() - assertEquals(streams.size, filtered.size) - - // getAudioDefaultTrackToUse skal likevel velge et spor - val selectedIndex = getAudioDefaultTrackToUse(streams) - - // Sjekk at det faktisk er en gyldig index - assertTrue(selectedIndex in streams.indices) - - // I dette tilfellet velges siste med høyest index (1) - assertEquals(0, selectedIndex) - } - - - - class DummyEvent: Event() - - @Test - @DisplayName(""" - Hvis event ikke er MediaStreamParsedEvent - Når onEvent kalles - Så: - Returneres null - """) - fun testOnEventNonParsedEvent() { - val result = onEvent(DummyEvent(), emptyList()) - assertNull(result) - } - - @Test - @DisplayName(""" - Hvis event er MediaStreamParsedEvent med video og audio - Når onEvent kalles - Så: - Returneres MediaTracksEncodeSelectedEvent med riktige spor - """) - fun testOnEventParsedEvent() { - val videoStreams = listOf(dummyVideoStream(0, 1000)) - val audioStreams = listOf(dummyAudioStream(0, "jpn", 2), dummyAudioStream(1, "jpn", 6)) - val parsedEvent = MediaStreamParsedEvent( - ParsedMediaStreams(videoStream = videoStreams, audioStream = audioStreams, subtitleStream = emptyList()) + val event = MediaStreamParsedEvent( + ParsedMediaStreams(videoStream = streams, audioStream = emptyList(), subtitleStream = emptyList()) ).newReferenceId() - val result = onEvent(parsedEvent, emptyList()) as MediaTracksEncodeSelectedEvent + + val result = listener.onEvent(event, emptyList()) as MediaTracksEncodeSelectedEvent + assertEquals(1, result.selectedVideoTrack) + } + + @Test@DisplayName(""" + Når audio streams inneholder flere språk og kanaloppsett + Hvis foretrukket språk finnes i stereo + Så: + Velges jpn stereo som default audio + """) + fun testAudioDefaultTrackSelectionPreferredLanguageStereo() { + val audio = listOf( + MockData.dummyAudioStream(0, "eng", 2), + MockData.dummyAudioStream(1, "jpn", 2), + MockData.dummyAudioStream(2, "jpn", 6) + ) + + val event = MediaStreamParsedEvent( + ParsedMediaStreams( + videoStream = listOf(MockData.dummyVideoStream(0)), + audioStream = audio, + subtitleStream = emptyList() + ) + ).newReferenceId() + + val result = listener.onEvent(event, emptyList()) as MediaTracksEncodeSelectedEvent + assertEquals(1, result.selectedAudioTrack) + } + + @Test + @DisplayName(""" + Når audio streams inneholder både stereo og surround + Hvis foretrukket språk finnes i begge + Så: + Velges jpn 6-kanals som extended audio + """) + fun testAudioExtendedTrackSelectionPreferredLanguageSurround() { + val audio = listOf( + MockData.dummyAudioStream(0, "jpn", 2), + MockData.dummyAudioStream(1, "jpn", 6) + ) + + val event = MediaStreamParsedEvent( + ParsedMediaStreams( + videoStream = listOf(MockData.dummyVideoStream(0)), + audioStream = audio, + subtitleStream = emptyList() + ) + ).newReferenceId() + + val result = listener.onEvent(event, emptyList()) as MediaTracksEncodeSelectedEvent + assertEquals(0, result.selectedAudioTrack) + assertEquals(1, result.selectedAudioExtendedTrack) + } + + class DummyEvent : Event() + + @Test + @DisplayName(""" + Når event ikke er av typen MediaStreamParsedEvent + Hvis onEvent kalles + Så: + Returneres null + """) + fun testOnEventNonParsedEvent() { + assertNull(listener.onEvent(DummyEvent(), emptyList())) + } + + @Test + @DisplayName(""" + Når MediaStreamParsedEvent mottas med video og audio streams + Hvis sporene analyseres etter preferanser + Så: + Velges riktige video-, default audio- og extended audio-spor + """) + fun testOnEventParsedEvent() { + val video = listOf(MockData.dummyVideoStream(0, 1000)) + val audio = listOf( + MockData.dummyAudioStream(0, "jpn", 2), + MockData.dummyAudioStream(1, "jpn", 6) + ) + + val parsed = MediaStreamParsedEvent( + ParsedMediaStreams(videoStream = video, audioStream = audio, subtitleStream = emptyList()) + ).newReferenceId() + + val result = listener.onEvent(parsed, emptyList()) as MediaTracksEncodeSelectedEvent + assertEquals(0, result.selectedVideoTrack) assertEquals(0, result.selectedAudioTrack) assertEquals(1, result.selectedAudioExtendedTrack) diff --git a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaSelectExtractTracksListenerTest.kt b/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaSelectExtractTracksListenerTest.kt index 0252728b..d8f705e3 100644 --- a/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaSelectExtractTracksListenerTest.kt +++ b/apps/coordinator/src/test/kotlin/no/iktdev/mediaprocessing/coordinator/listeners/events/MediaSelectExtractTracksListenerTest.kt @@ -1,9 +1,10 @@ package no.iktdev.mediaprocessing.coordinator.listeners.events -import no.iktdev.mediaprocessing.TestBase -import no.iktdev.mediaprocessing.ffmpeg.data.SubtitleStream -import no.iktdev.mediaprocessing.ffmpeg.data.SubtitleTags -import no.iktdev.mediaprocessing.ffmpeg.data.Tags +import no.iktdev.eventi.models.Event +import no.iktdev.mediaprocessing.FakeCoordinatorEnv +import no.iktdev.mediaprocessing.MockData +import no.iktdev.mediaprocessing.coordinator.Preference +import no.iktdev.mediaprocessing.coordinator.SubtitleSelectionMode 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.SubtitleItem @@ -12,111 +13,145 @@ import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.Assertions.assertNull import org.junit.jupiter.api.DisplayName import org.junit.jupiter.api.Test +import java.io.File -class MediaSelectExtractTracksListenerTest: TestBase() { +class MediaSelectExtractTracksListenerTest { - - // Vi lager en subclass som gir oss tilgang til alt og lar oss overstyre språkpreferanser - class TestableMediaSelectExtractTracksListener( - private val preferredLanguages: Set = emptySet() - ) : MediaSelectExtractTracksListener() { - override fun limitToLanguages(): Set = preferredLanguages - // gjør private extension tilgjengelig via wrapper - fun callFilterOnPreferredLanguage(streams: List): List { - return streams.filterOnPreferredLanguage() - } - } - - private fun dummySubtitleStream(index: Int, language: String?, type: SubtitleType): SubtitleItem { - val stream = SubtitleStream( - index = index, - codec_name = "ass", - codec_long_name = "ASS", - codec_type = "subtitle", - codec_tag_string = "", - codec_tag = "", - r_frame_rate = "0/0", - avg_frame_rate = "0/0", - time_base = "1/1000", - start_pts = 0, - start_time = "0", - duration = null, - duration_ts = 1000, - disposition = null, - tags = Tags( - title = null, 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 = null, mimetype = null - ), - subtitle_tags = SubtitleTags(language = language, filename = null, mimetype = null) + // ------------------------------------------------------------ + // Helper: create a Preference with custom subtitle settings + // ------------------------------------------------------------ + private fun testPreference( + preferredSubtitles: List = listOf("eng"), + formatPriority: List = listOf("ass", "srt", "vtt", "smi"), + mode: SubtitleSelectionMode = SubtitleSelectionMode.DialogueOnly + ): Preference { + val tmp = File.createTempFile("pref", ".json") + tmp.writeText( + """ + { + "processer": {}, + "language": { + "preferredAudio": ["eng"], + "preferredSubtitles": ${preferredSubtitles}, + "preferOriginal": true, + "avoidDub": true, + "subtitleFormatPriority": ${formatPriority}, + "subtitleSelectionMode": "$mode" + } + } + """.trimIndent() ) - return SubtitleItem(stream = stream, type = type) + return Preference(FakeCoordinatorEnv(tmp)) } + private fun listener( + preferredSubtitles: List = listOf("eng"), + formatPriority: List = listOf("ass", "srt", "vtt", "smi"), + mode: SubtitleSelectionMode = SubtitleSelectionMode.DialogueOnly + ) = MediaSelectExtractTracksListener( + testPreference(preferredSubtitles, formatPriority, mode) + ) + + class DummyEvent : Event() + + // ------------------------------------------------------------ + // TESTS + // ------------------------------------------------------------ + @Test @DisplayName(""" - Hvis event ikke er MediaTracksDetermineSubtitleTypeEvent - Når onEvent kalles + Når event ikke er av typen MediaTracksDetermineSubtitleTypeEvent + Hvis onEvent kalles Så: - Returneres null - """) + Returneres null + """) fun testOnEventNonSubtitleEvent() { - val listener = TestableMediaSelectExtractTracksListener() - val result = listener.onEvent(DummyEvent(), emptyList()) + val result = listener().onEvent(DummyEvent(), emptyList()) assertNull(result) } + @Test @DisplayName(""" - Hvis event inneholder Dialogue subtitles - Når onEvent kalles + Når subtitles inneholder Dialogue og Commentary + Hvis modus er DialogueOnly Så: - Returneres MediaTracksExtractSelectedEvent med index til Dialogue tracks - """) - fun testOnEventDialogueTracksSelected() { - val listener = TestableMediaSelectExtractTracksListener() + Velges kun Dialogue subtitles + """) + fun testDialogueSelection() { val items = listOf( - dummySubtitleStream(0, "eng", SubtitleType.Dialogue), - dummySubtitleStream(1, "eng", SubtitleType.Commentary) + SubtitleItem(MockData.dummySubtitleStream(0, "eng"), SubtitleType.Dialogue), + SubtitleItem(MockData.dummySubtitleStream(1, "eng"), SubtitleType.Commentary) ) - val event = MediaTracksDetermineSubtitleTypeEvent(subtitleTrackItems = items).newReferenceId() - val result = listener.onEvent(event, emptyList()) as MediaTracksExtractSelectedEvent + + val event = MediaTracksDetermineSubtitleTypeEvent(items).newReferenceId() + val result = listener().onEvent(event, emptyList()) as MediaTracksExtractSelectedEvent + assertEquals(listOf(0), result.selectedSubtitleTracks) } - @Test - @DisplayName(""" - Hvis limitToLanguages returnerer jpn - Når filterOnPreferredLanguage kalles - Så: - Returneres kun spor med språk jpn - """) - fun testFilterOnPreferredLanguageWithLimit() { - val listener = TestableMediaSelectExtractTracksListener(setOf("jpn")) - val streams = listOf( - dummySubtitleStream(0, "eng", SubtitleType.Dialogue).stream, - dummySubtitleStream(1, "jpn", SubtitleType.Dialogue).stream - ) - val filtered = listener.callFilterOnPreferredLanguage(streams) - assertEquals(1, filtered.size) - assertEquals("jpn", filtered[0].tags.language) - } @Test @DisplayName(""" - Hvis limitToLanguages er tom - Når filterOnPreferredLanguage kalles + Når subtitles finnes i flere språk + Hvis foretrukket språk er jpn Så: - Returneres original liste uten filtrering - """) - fun testFilterOnPreferredLanguageNoLimit() { - val listener = TestableMediaSelectExtractTracksListener() - val streams = listOf( - dummySubtitleStream(0, "eng", SubtitleType.Dialogue).stream, - dummySubtitleStream(1, "fra", SubtitleType.Dialogue).stream + Velges kun subtitles i jpn + """) + fun testPreferredLanguageSelection() { + val items = listOf( + SubtitleItem(MockData.dummySubtitleStream(0, "eng"), SubtitleType.Dialogue), + SubtitleItem(MockData.dummySubtitleStream(1, "jpn"), SubtitleType.Dialogue) ) - val filtered = listener.callFilterOnPreferredLanguage(streams) - assertEquals(streams.size, filtered.size) + + val event = MediaTracksDetermineSubtitleTypeEvent(items).newReferenceId() + val result = listener(preferredSubtitles = listOf("jpn")) + .onEvent(event, emptyList()) as MediaTracksExtractSelectedEvent + + assertEquals(listOf(1), result.selectedSubtitleTracks) } -} \ No newline at end of file + + @Test + @DisplayName(""" + Når flere subtitles i samme språk finnes + Hvis format-prioritet er ass > srt + Så: + Velges subtitle med codec ass + """) + fun testFormatPrioritySelection() { + val items = listOf( + SubtitleItem(MockData.dummySubtitleStream(0, "eng").copy(codec_name = "srt"), SubtitleType.Dialogue), + SubtitleItem(MockData.dummySubtitleStream(1, "eng").copy(codec_name = "ass"), SubtitleType.Dialogue) + ) + + val event = MediaTracksDetermineSubtitleTypeEvent(items).newReferenceId() + val result = listener( + preferredSubtitles = listOf("eng"), + formatPriority = listOf("ass", "srt") + ).onEvent(event, emptyList()) as MediaTracksExtractSelectedEvent + + assertEquals(listOf(1), result.selectedSubtitleTracks) + } + + + @Test + @DisplayName(""" + Når flere subtitles i samme språk finnes + Hvis kun én subtitle per språk skal velges + Så: + Returneres kun ett spor + """) + fun testUniquePerLanguage() { + val items = listOf( + SubtitleItem(MockData.dummySubtitleStream(0, "eng").copy(codec_name = "srt"), SubtitleType.Dialogue), + SubtitleItem(MockData.dummySubtitleStream(1, "eng").copy(codec_name = "vtt"), SubtitleType.Dialogue) + ) + + val event = MediaTracksDetermineSubtitleTypeEvent(items).newReferenceId() + val result = listener().onEvent(event, emptyList()) as MediaTracksExtractSelectedEvent + + assertEquals(1, result.selectedSubtitleTracks.size) + } + +}