diff --git a/src/main/kotlin/pw/binom/mirror/worker/convert/FileInfo.kt b/src/main/kotlin/pw/binom/mirror/worker/convert/FileInfo.kt index 7796897..0e1d6f6 100644 --- a/src/main/kotlin/pw/binom/mirror/worker/convert/FileInfo.kt +++ b/src/main/kotlin/pw/binom/mirror/worker/convert/FileInfo.kt @@ -37,7 +37,7 @@ data class FileInfo( override val duration: Duration? = null, @SerialName("start_time") @Serializable(DurationAsSecondsString::class) - override val startTime: Duration?, + override val startTime: Duration? = null, override val tags: Map = emptyMap(), ) : Stream @@ -95,6 +95,18 @@ data class FileInfo( get() = tags["language"] ?: tags["lang"] } + @Serializable + @SerialName("data") + data class Data( + override val index: Int, + @Serializable(DurationAsSecondsString::class) + override val duration: Duration? = null, + @SerialName("start_time") + @Serializable(DurationAsSecondsString::class) + override val startTime: Duration? = null, + override val tags: Map = emptyMap(), + ) : Stream + @Serializable data class Format( @SerialName("format_name") diff --git a/src/test/kotlin/pw/binom/mirror/worker/TestMedia.kt b/src/test/kotlin/pw/binom/mirror/worker/TestMedia.kt index ef0012a..74917c9 100644 --- a/src/test/kotlin/pw/binom/mirror/worker/TestMedia.kt +++ b/src/test/kotlin/pw/binom/mirror/worker/TestMedia.kt @@ -33,6 +33,23 @@ object TestMedia { return out } + fun generateSourceWithDataStream(dir: Path, durationSeconds: Int = 2): Path { + val base = generateSource(dir, durationSeconds) + val out = dir.resolve("source-with-data.mov") + val command = listOf( + FFMPEG, + "-y", + "-i", base.toString(), + "-map", "0:v", + "-map", "0:a", + "-c", "copy", + "-timecode", "00:00:00:00", + out.toString(), + ) + run(command) + return out + } + fun generateBrokenFile(dir: Path): Path { Files.createDirectories(dir) val out = dir.resolve("broken.bin") diff --git a/src/test/kotlin/pw/binom/mirror/worker/convert/FfprobeServiceTest.kt b/src/test/kotlin/pw/binom/mirror/worker/convert/FfprobeServiceTest.kt index a328ed9..1e3b62e 100644 --- a/src/test/kotlin/pw/binom/mirror/worker/convert/FfprobeServiceTest.kt +++ b/src/test/kotlin/pw/binom/mirror/worker/convert/FfprobeServiceTest.kt @@ -43,6 +43,16 @@ class FfprobeServiceTest { assertTrue(audio.channels >= 1) } + @Test + fun `getInfo ignores data streams and keeps audio and video`() { + val source = TestMedia.generateSourceWithDataStream(tempDir) + val info = service().getInfo(source) + + assertTrue(info.streams.any { it is FileInfo.Data }) + assertEquals(1, info.videoStreams.size) + assertEquals(2, info.audioStreams.size) + } + @Test fun `getInfo fails on broken file`() { val broken = TestMedia.generateBrokenFile(tempDir) diff --git a/src/test/kotlin/pw/binom/mirror/worker/convert/FileInfoTest.kt b/src/test/kotlin/pw/binom/mirror/worker/convert/FileInfoTest.kt index 4efbc70..af26728 100644 --- a/src/test/kotlin/pw/binom/mirror/worker/convert/FileInfoTest.kt +++ b/src/test/kotlin/pw/binom/mirror/worker/convert/FileInfoTest.kt @@ -4,6 +4,7 @@ import kotlinx.serialization.json.Json import kotlin.time.Duration.Companion.seconds import kotlin.test.Test import kotlin.test.assertEquals +import kotlin.test.assertIs import kotlin.test.assertNotNull import kotlin.test.assertTrue @@ -92,6 +93,91 @@ class FileInfoTest { assertEquals(3, info.videoStreams[0].index) } + @Test + fun `parses ffprobe json with data stream`() { + val fixture = """ + { + "streams": [ + { + "index": 0, + "codec_name": "h264", + "codec_type": "video", + "width": 1920, + "height": 1080, + "duration": "10.000000", + "start_time": "0.000000", + "tags": {"title": "Main"} + }, + { + "index": 1, + "codec_name": "aac", + "codec_type": "audio", + "channels": 2, + "channel_layout": "stereo", + "duration": "10.000000", + "start_time": "0.000000", + "tags": {"language": "eng", "title": "English"} + }, + { + "index": 2, + "codec_type": "data", + "codec_tag_string": "tmcd", + "tags": {"language": "eng", "handler_name": "SubtitleHandler"} + }, + { + "index": 3, + "codec_name": "subrip", + "codec_type": "subtitle", + "start_time": "0.000000", + "tags": {"language": "rus"} + } + ], + "format": { + "format_name": "matroska,webm", + "format_long_name": "Matroska / WebM", + "duration": "10.000000", + "size": "1000", + "bit_rate": "2000", + "tags": {"encoder": "x"} + } + } + """.trimIndent() + + val info = json.decodeFromString(FileInfo.serializer(), fixture) + + assertEquals(4, info.streams.size) + assertEquals(1, info.videoStreams.size) + assertEquals(1, info.audioStreams.size) + assertIs(info.streams[0]) + assertIs(info.streams[1]) + assertIs(info.streams[2]) + assertIs(info.streams[3]) + assertEquals(1920, info.videoStreams.single().width) + assertEquals(2, info.audioStreams.single().channels) + assertEquals(10.seconds, info.format.duration) + } + + @Test + fun `data stream round trip`() { + val info = FileInfo( + streams = listOf( + FileInfo.VideoStream(index = 0, codecName = "vp9", width = 1920, height = 1080, startTime = 0.seconds), + FileInfo.Data(index = 1, tags = mapOf("language" to "eng")), + ), + format = FileInfo.Format( + formatName = "matroska", + formatLongName = "M", + duration = 5.seconds, + size = 1, + bitRate = 1, + ), + ) + val decoded = json.decodeFromString(FileInfo.serializer(), json.encodeToString(FileInfo.serializer(), info)) + assertEquals(1, decoded.videoStreams.size) + assertEquals(0, decoded.audioStreams.size) + assertIs(decoded.streams[1]) + } + @Test fun `audio stream title and language fallbacks`() { val a1 = FileInfo.AudioStream(