Track selection
This commit is contained in:
parent
5babd17d18
commit
866f7f228c
@ -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<String>,
|
||||
val preferredSubtitles: List<String>,
|
||||
|
||||
val preferOriginal: Boolean = true,
|
||||
val avoidDub: Boolean = true,
|
||||
|
||||
// NEW: Prioritet for hvilket subtitle-format som skal brukes som master
|
||||
val subtitleFormatPriority: List<String> = 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
|
||||
}
|
||||
|
||||
val parsed = try {
|
||||
Gson().fromJson(text, PeferenceConfig::class.java)
|
||||
} catch (e: Exception) {
|
||||
return default
|
||||
}
|
||||
|
||||
// Hvis hele configen er null → default
|
||||
val cfg = parsed ?: return default
|
||||
|
||||
// Hvis processer er null → default
|
||||
val p = cfg.processer ?: return default
|
||||
|
||||
// Hvis underfelter er null → fyll inn default
|
||||
val safeVideo = p.videoPreference ?: default.videoPreference
|
||||
val safeAudio = p.audioPreference ?: default.audioPreference
|
||||
|
||||
return ProcesserPreference(
|
||||
videoPreference = safeVideo,
|
||||
audioPreference = safeAudio
|
||||
return PeferenceConfig(
|
||||
processer = ProcesserPreference.default(),
|
||||
language = LanguagePreference.default()
|
||||
)
|
||||
}
|
||||
|
||||
val processer = try {
|
||||
gson.fromJson(root.get("processer"), ProcesserPreference::class.java)
|
||||
?: ProcesserPreference.default()
|
||||
} catch (_: Exception) {
|
||||
ProcesserPreference.default()
|
||||
}
|
||||
|
||||
val language = try {
|
||||
gson.fromJson(root.get("language"), LanguagePreference::class.java)
|
||||
?: LanguagePreference.default()
|
||||
} catch (_: Exception) {
|
||||
LanguagePreference.default()
|
||||
}
|
||||
|
||||
private fun File.ifExists(block: File.() -> Unit, orElse: () -> Unit = {}) {
|
||||
if (this.exists()) {
|
||||
block()
|
||||
} else {
|
||||
orElse()
|
||||
return PeferenceConfig(processer, language)
|
||||
}
|
||||
|
||||
fun getProcesserPreference(): ProcesserPreference =
|
||||
getFullConfig().processer
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<PeferenceConfig> =
|
||||
ResponseEntity.ok(preference.getFullConfig())
|
||||
|
||||
@PutMapping
|
||||
fun putFull(@RequestBody body: PeferenceConfig): ResponseEntity<PeferenceConfig> {
|
||||
preference.saveFullConfig(body)
|
||||
return ResponseEntity.ok(preference.getFullConfig())
|
||||
}
|
||||
|
||||
@GetMapping("/processer")
|
||||
fun getProcesser(): ResponseEntity<ProcesserPreference> =
|
||||
ResponseEntity.ok(preference.getProcesserPreference())
|
||||
|
||||
@PutMapping("/processer")
|
||||
fun putProcesser(@RequestBody body: ProcesserPreference): ResponseEntity<ProcesserPreference> {
|
||||
preference.saveProcesserPreference(body)
|
||||
return ResponseEntity.ok(preference.getProcesserPreference())
|
||||
}
|
||||
}
|
||||
@ -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<Event>
|
||||
|
||||
@ -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<String> {
|
||||
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<AudioStream>, selectedDefaultTrack: Int): Int? {
|
||||
val durationFiltered = audioStream.filterOnPreferredLanguage()
|
||||
// ------------------------------------------------------------
|
||||
// AUDIO SELECTION
|
||||
// ------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
private fun getAudioDefaultTrackToUse(audioStreams: List<AudioStream>): 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<AudioStream>,
|
||||
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<AudioStream>): Int {
|
||||
val durationFiltered = audioStream.filterOnPreferredLanguage()
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// CORE AUDIO SELECTION LOGIC
|
||||
// ------------------------------------------------------------
|
||||
|
||||
private enum class AudioSelectMode { DEFAULT, EXTENDED }
|
||||
|
||||
private fun selectBestAudioStream(
|
||||
streams: List<AudioStream>,
|
||||
preferredLanguages: List<String>,
|
||||
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<VideoStream>): 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<AudioStream>.filterOnPreferredLanguage(): List<AudioStream> {
|
||||
return this.filter { it.tags.language in getAudioLanguagePreference() }.ifEmpty { this }
|
||||
}
|
||||
|
||||
protected fun getVideoTrackToUse(streams: List<VideoStream>): 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)
|
||||
}
|
||||
}
|
||||
@ -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<String> {
|
||||
return emptySet()
|
||||
}
|
||||
|
||||
fun useTypes(): Set<SubtitleType> {
|
||||
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<SubtitleStream>.filterOnPreferredLanguage(): List<SubtitleStream> {
|
||||
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<SubtitleItem>,
|
||||
mode: SubtitleSelectionMode
|
||||
): List<SubtitleItem> {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------
|
||||
// SUBTITLE SELECTION LOGIC
|
||||
// ------------------------------------------------------------
|
||||
|
||||
private fun selectSubtitleStreams(
|
||||
streams: List<SubtitleStream>,
|
||||
preferredLanguages: List<String>,
|
||||
preferOriginal: Boolean,
|
||||
formatPriority: List<String>
|
||||
): List<SubtitleStream> {
|
||||
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<SubtitleStream>.uniquePerLanguageBestFormat(
|
||||
formatPriority: List<String>
|
||||
): List<SubtitleStream> {
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
@ -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? {
|
||||
|
||||
@ -59,4 +59,6 @@ open class TestBase {
|
||||
return start
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@ -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)
|
||||
val streams = listOf(
|
||||
MockData.dummyVideoStream(0, 1000),
|
||||
MockData.dummyVideoStream(1, 5000)
|
||||
)
|
||||
|
||||
val event = MediaStreamParsedEvent(
|
||||
ParsedMediaStreams(videoStream = streams, audioStream = emptyList(), subtitleStream = emptyList())
|
||||
).newReferenceId()
|
||||
|
||||
val result = listener.onEvent(event, emptyList()) as MediaTracksEncodeSelectedEvent
|
||||
assertEquals(1, result.selectedVideoTrack)
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("""
|
||||
Hvis audio streams inneholder foretrukket språk jpn med 2 kanaler
|
||||
Når getAudioDefaultTrackToUse kalles
|
||||
@Test@DisplayName("""
|
||||
Når audio streams inneholder flere språk og kanaloppsett
|
||||
Hvis foretrukket språk finnes i stereo
|
||||
Så:
|
||||
Returneres index til jpn stereo track
|
||||
Velges jpn stereo som default audio
|
||||
""")
|
||||
fun testAudioDefaultTrackSelectionPreferredLanguageStereo() {
|
||||
val streams = listOf(
|
||||
dummyAudioStream(0, "eng", 2),
|
||||
dummyAudioStream(1, "jpn", 2),
|
||||
dummyAudioStream(2, "jpn", 6)
|
||||
val audio = listOf(
|
||||
MockData.dummyAudioStream(0, "eng", 2),
|
||||
MockData.dummyAudioStream(1, "jpn", 2),
|
||||
MockData.dummyAudioStream(2, "jpn", 6)
|
||||
)
|
||||
val index = getAudioDefaultTrackToUse(streams)
|
||||
assertEquals(1, index)
|
||||
|
||||
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("""
|
||||
Hvis audio streams inneholder foretrukket språk jpn med 6 kanaler
|
||||
Når getAudioExtendedTrackToUse kalles
|
||||
Når audio streams inneholder både stereo og surround
|
||||
Hvis foretrukket språk finnes i begge
|
||||
Så:
|
||||
Returneres index til jpn 6-kanals track
|
||||
Velges jpn 6-kanals som extended audio
|
||||
""")
|
||||
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)
|
||||
val audio = listOf(
|
||||
MockData.dummyAudioStream(0, "jpn", 2),
|
||||
MockData.dummyAudioStream(1, "jpn", 6)
|
||||
)
|
||||
|
||||
// filterOnPreferredLanguage skal returnere original listen
|
||||
val filtered = streams.filterOnPreferredLanguage()
|
||||
assertEquals(streams.size, filtered.size)
|
||||
val event = MediaStreamParsedEvent(
|
||||
ParsedMediaStreams(
|
||||
videoStream = listOf(MockData.dummyVideoStream(0)),
|
||||
audioStream = audio,
|
||||
subtitleStream = emptyList()
|
||||
)
|
||||
).newReferenceId()
|
||||
|
||||
// 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)
|
||||
val result = listener.onEvent(event, emptyList()) as MediaTracksEncodeSelectedEvent
|
||||
assertEquals(0, result.selectedAudioTrack)
|
||||
assertEquals(1, result.selectedAudioExtendedTrack)
|
||||
}
|
||||
|
||||
|
||||
|
||||
class DummyEvent : Event()
|
||||
|
||||
@Test
|
||||
@DisplayName("""
|
||||
Hvis event ikke er MediaStreamParsedEvent
|
||||
Når onEvent kalles
|
||||
Når event ikke er av typen MediaStreamParsedEvent
|
||||
Hvis onEvent kalles
|
||||
Så:
|
||||
Returneres null
|
||||
""")
|
||||
fun testOnEventNonParsedEvent() {
|
||||
val result = onEvent(DummyEvent(), emptyList())
|
||||
assertNull(result)
|
||||
assertNull(listener.onEvent(DummyEvent(), emptyList()))
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("""
|
||||
Hvis event er MediaStreamParsedEvent med video og audio
|
||||
Når onEvent kalles
|
||||
Når MediaStreamParsedEvent mottas med video og audio streams
|
||||
Hvis sporene analyseres etter preferanser
|
||||
Så:
|
||||
Returneres MediaTracksEncodeSelectedEvent med riktige spor
|
||||
Velges riktige video-, default audio- og extended audio-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 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 = onEvent(parsedEvent, emptyList()) as MediaTracksEncodeSelectedEvent
|
||||
|
||||
val result = listener.onEvent(parsed, emptyList()) as MediaTracksEncodeSelectedEvent
|
||||
|
||||
assertEquals(0, result.selectedVideoTrack)
|
||||
assertEquals(0, result.selectedAudioTrack)
|
||||
assertEquals(1, result.selectedAudioExtendedTrack)
|
||||
|
||||
@ -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<String> = emptySet()
|
||||
) : MediaSelectExtractTracksListener() {
|
||||
override fun limitToLanguages(): Set<String> = preferredLanguages
|
||||
// gjør private extension tilgjengelig via wrapper
|
||||
fun callFilterOnPreferredLanguage(streams: List<SubtitleStream>): List<SubtitleStream> {
|
||||
return streams.filterOnPreferredLanguage()
|
||||
// ------------------------------------------------------------
|
||||
// Helper: create a Preference with custom subtitle settings
|
||||
// ------------------------------------------------------------
|
||||
private fun testPreference(
|
||||
preferredSubtitles: List<String> = listOf("eng"),
|
||||
formatPriority: List<String> = 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"
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
""".trimIndent()
|
||||
)
|
||||
return SubtitleItem(stream = stream, type = type)
|
||||
return Preference(FakeCoordinatorEnv(tmp))
|
||||
}
|
||||
|
||||
private fun listener(
|
||||
preferredSubtitles: List<String> = listOf("eng"),
|
||||
formatPriority: List<String> = 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
|
||||
""")
|
||||
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
|
||||
Velges kun Dialogue subtitles
|
||||
""")
|
||||
fun testOnEventDialogueTracksSelected() {
|
||||
val listener = TestableMediaSelectExtractTracksListener()
|
||||
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
|
||||
Velges kun subtitles i jpn
|
||||
""")
|
||||
fun testFilterOnPreferredLanguageNoLimit() {
|
||||
val listener = TestableMediaSelectExtractTracksListener()
|
||||
val streams = listOf(
|
||||
dummySubtitleStream(0, "eng", SubtitleType.Dialogue).stream,
|
||||
dummySubtitleStream(1, "fra", SubtitleType.Dialogue).stream
|
||||
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)
|
||||
}
|
||||
|
||||
|
||||
@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)
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user