Added changes to encode or remux

This commit is contained in:
bskjon 2025-04-20 23:42:49 +02:00
parent b6fa3977b1
commit 6ce7094119
3 changed files with 447 additions and 22 deletions

View File

@ -1,5 +1,6 @@
package no.iktdev.mediaprocessing.coordinator.tasksV2.mapping.streams package no.iktdev.mediaprocessing.coordinator.tasksV2.mapping.streams
import no.iktdev.mediaprocessing.coordinator.log
import no.iktdev.mediaprocessing.shared.common.contract.ffmpeg.ParsedMediaStreams import no.iktdev.mediaprocessing.shared.common.contract.ffmpeg.ParsedMediaStreams
import no.iktdev.mediaprocessing.shared.common.contract.ffmpeg.VideoArgumentsDto import no.iktdev.mediaprocessing.shared.common.contract.ffmpeg.VideoArgumentsDto
import no.iktdev.mediaprocessing.shared.common.contract.ffmpeg.VideoPreference import no.iktdev.mediaprocessing.shared.common.contract.ffmpeg.VideoPreference
@ -11,39 +12,99 @@ class VideoArguments(
val preference: VideoPreference val preference: VideoPreference
) { ) {
fun isVideoCodecEqual() = getCodec(videoStream.codec_name) == getCodec(preference.codec.lowercase()) fun isVideoCodecEqual() = getCodec(videoStream.codec_name) == getCodec(preference.codec.lowercase())
protected fun getCodec(name: String): String { fun getCodec(name: String): String {
return when (name) { return when (name.lowercase()) {
"hevc", "hevec", "h265", "h.265", "libx265" "hevc", "hevec", "h265", "h.265", "libx265" -> "libx265"
-> "libx265" "h.264", "h264", "libx264" -> "libx264"
"vp9", "vp-9", "libvpx-vp9" -> "libvpx-vp9"
"h.264", "h264", "libx264" "av1", "libaom-av1" -> "libaom-av1"
-> "libx264" "mpeg4", "mp4", "libxvid" -> "libxvid"
"vvc", "h.266", "libvvc" -> "libvvc"
"vp8", "libvpx" -> "libvpx"
else -> name else -> name
} }
} }
fun getCodec() = getCodec(videoStream.codec_name)
fun getVideoArguments(): VideoArgumentsDto { fun getVideoArguments(): VideoArgumentsDto {
val optionalParams = mutableListOf<String>()
if (preference.pixelFormatPassthrough.none { it == videoStream.pix_fmt }) {
optionalParams.addAll(listOf("-pix_fmt", preference.pixelFormat))
}
val codecParams = if (isVideoCodecEqual()) { val codecParams = if (isVideoCodecEqual()) {
val default = mutableListOf("-c:v", "copy") if (getCodec() == "libx265") {
if (getCodec(videoStream.codec_name) == "libx265") { composeHevcArguments(getCodec())
default.addAll(listOf("-vbsf", "hevc_mp4toannexb")) } else {
mutableListOf("-c:v", "copy")
}
} else {
when (getCodec(preference.codec.lowercase())) {
"libx265" -> composeHevcArguments(getCodec())
"libx264" -> composeH264Arguments(getCodec())
else -> run {
val codec = getCodec(preference.codec.lowercase())
log.info { "Unsupported codec found ${codec}, making best effort..." }
listOf("-c:v", codec)
}
} }
default
}
else {
optionalParams.addAll(listOf("-crf", preference.threshold.toString()))
listOf("-c:v", getCodec(preference.codec.lowercase()))
} }
return VideoArgumentsDto( return VideoArgumentsDto(
index = allStreams.videoStream.indexOf(videoStream), index = allStreams.videoStream.indexOf(videoStream),
codecParameters = codecParams, codecParameters = codecParams,
optionalParameters = optionalParams optionalParameters = composeOptionalArguments()
) )
} }
private fun composeOptionalArguments(): List<String> {
val pixelFormat: List<String> = if (preference.pixelFormatPassthrough.none { it == videoStream.pix_fmt }) {
listOf("-pix_fmt", preference.pixelFormat)
} else emptyList()
val crfParam = if (pixelFormat.isNotEmpty() || !isVideoCodecEqual()) {
listOf("-crf", preference.threshold.toString())
} else emptyList()
val defaultCodecParams = listOf("-movflags", "+faststart")
return pixelFormat + crfParam + defaultCodecParams
}
private fun composeH264Arguments(codec: String): List<String> {
return listOf(
"-c:v", "libx264",
"-profile:v", "high",
"-level:v", preference.h264Level.toString(),
"preset", "slow",
)
}
private fun composeHevcArguments(codec: String): List<String> {
val targetProfile = if (videoStream.pix_fmt.contains("10")) "main10" else "main"
val unsetCodecMetadata = videoStream.codec_tag_string == "[0][0][0][0]" || videoStream.codec_tag == "0x0000"
// Map level til en streng her forenklet
val targetLevel = when (videoStream.level) {
150 -> "5.0"
153 -> "5.1"
else -> "5.0" // Default hvis vi ikke har en eksplisitt mapping
}
return if (codec != "libx265" || (unsetCodecMetadata && preference.reencodeOnIncorrectMetadataForChromecast)) {
// Konverter (eller reenkode) til HEVC med x265 med riktige parametere
listOf(
"-c:v", "libx265", "-preset", "slow",
"-x265-params", "\"profile=$targetProfile:level=$targetLevel\"",
"-tag:v", "hev1"
)
} else {
// Dersom vi mener at vi kun trenger å remuxe, kan vi gjøre
listOf(
"-c:v", "copy", "-tag:v", "hev1"
)
}
}
} }

View File

@ -0,0 +1,361 @@
package no.iktdev.mediaprocessing.coordinator.tasksV2.mapping.streams
import com.google.gson.Gson
import no.iktdev.mediaprocessing.shared.common.Preference
import no.iktdev.mediaprocessing.shared.common.contract.ffmpeg.ParsedMediaStreams
import no.iktdev.mediaprocessing.shared.common.contract.ffmpeg.VideoPreference
import no.iktdev.mediaprocessing.shared.common.contract.ffmpeg.VideoStream
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.DisplayName
import org.junit.jupiter.api.Test
class VideoArgumentsTest {
private fun getVideoArguments(videoStream: VideoStream, preference: VideoPreference): VideoArguments {
return VideoArguments(
allStreams = ParsedMediaStreams(
videoStream = listOf(videoStream),
audioStream = emptyList(),
subtitleStream = emptyList()
),
preference = preference,
videoStream = videoStream
)
}
@Test
fun hevcStream1() {
val data = Gson().fromJson(hevcStream1, VideoStream::class.java)
val videoArguments = getVideoArguments(data,
Preference.getPreference().encodePreference.video
.copy(codec = "h265")
)
assertThat(videoArguments.isVideoCodecEqual()).isTrue()
assertThat(videoArguments.getCodec()).isEqualTo("libx265")
assertThat(videoArguments.getVideoArguments().codecParameters.take(2)).isEqualTo(listOf("-c:v", "copy"))
}
@Test
@DisplayName("""
When a hevc encoded media file gets received,
But it has unset metadata, and re-encode for chromecast is set,
Then the parameters should not specify copy
""")
fun hevcStream2() {
val data = Gson().fromJson(hevcStream2, VideoStream::class.java)
val videoArguments = getVideoArguments(data,
Preference.getPreference().encodePreference.video
.copy(codec = "h265", reencodeOnIncorrectMetadataForChromecast = true)
)
assertThat(videoArguments.isVideoCodecEqual()).isTrue()
assertThat(videoArguments.getCodec()).isEqualTo("libx265")
assertThat(videoArguments.getVideoArguments().codecParameters.take(2)).isEqualTo(listOf("-c:v", "libx265"))
}
@Test
@DisplayName("""
When a vc1 encoded media file gets received
And preference is set to hevc,
Then the parameters should be to encode in hevc
""")
fun vc1Stream1() {
val data = Gson().fromJson(vc1Stream, VideoStream::class.java)
val videoArguments = getVideoArguments(data,
Preference.getPreference().encodePreference.video
.copy(codec = "h265", reencodeOnIncorrectMetadataForChromecast = true)
)
assertThat(videoArguments.isVideoCodecEqual()).isFalse()
assertThat(videoArguments.getCodec()).isEqualTo("vc1")
assertThat(videoArguments.getVideoArguments().codecParameters.take(2)).isEqualTo(listOf("-c:v", "libx265"))
}
@Test
@DisplayName("""
When a vc1 encoded media file gets received
And preference is set to h264,
Then the parameters should be to encode in h264
""")
fun vc1Stream2() {
val data = Gson().fromJson(vc1Stream, VideoStream::class.java)
val videoArguments = getVideoArguments(data,
Preference.getPreference().encodePreference.video
.copy(codec = "h264", reencodeOnIncorrectMetadataForChromecast = true)
)
assertThat(videoArguments.isVideoCodecEqual()).isFalse()
assertThat(videoArguments.getCodec()).isEqualTo("vc1")
assertThat(videoArguments.getVideoArguments().codecParameters.take(2)).isEqualTo(listOf("-c:v", "libx264"))
}
@Test
fun h264Stream1() {
val data = Gson().fromJson(h264stream1, VideoStream::class.java)
val videoArguments = getVideoArguments(data,
Preference.getPreference().encodePreference.video
.copy(codec = "h265", reencodeOnIncorrectMetadataForChromecast = true)
)
assertThat(videoArguments.isVideoCodecEqual()).isFalse()
assertThat(videoArguments.getCodec()).isEqualTo("libx264")
assertThat(videoArguments.getVideoArguments().codecParameters.take(2)).isEqualTo(listOf("-c:v", "libx265"))
}
@Test
fun h264Stream2() {
val data = Gson().fromJson(h264stream1, VideoStream::class.java)
val videoArguments = getVideoArguments(data,
Preference.getPreference().encodePreference.video
.copy(reencodeOnIncorrectMetadataForChromecast = true)
)
assertThat(videoArguments.isVideoCodecEqual()).isTrue()
assertThat(videoArguments.getCodec()).isEqualTo("libx264")
assertThat(videoArguments.getVideoArguments().codecParameters.take(2)).isEqualTo(listOf("-c:v", "copy"))
}
val hevcStream1 = """
{
"index": 0,
"codec_name": "hevc",
"codec_long_name": "H.265 / HEVC (High Efficiency Video Coding)",
"profile": "Main 10",
"codec_type": "video",
"codec_tag_string": "hev1",
"codec_tag": "0x31766568",
"width": 1920,
"height": 1080,
"coded_width": 1920,
"coded_height": 1080,
"closed_captions": 0,
"film_grain": 0,
"has_b_frames": 2,
"sample_aspect_ratio": "1:1",
"display_aspect_ratio": "16:9",
"pix_fmt": "yuv420p10le",
"level": 150,
"color_range": "tv",
"chroma_location": "left",
"refs": 1,
"id": "0x1",
"r_frame_rate": "24000/1001",
"avg_frame_rate": "34045000/1419959",
"time_base": "1/16000",
"start_pts": 0,
"start_time": "0.000000",
"duration_ts": 22719344,
"duration": "1419.959000",
"bit_rate": "2020313",
"nb_frames": "34045",
"extradata_size": 2535,
"disposition": {
"default": 1,
"dub": 0,
"original": 0,
"comment": 0,
"lyrics": 0,
"karaoke": 0,
"forced": 0,
"hearing_impaired": 0,
"visual_impaired": 0,
"clean_effects": 0,
"attached_pic": 0,
"timed_thumbnails": 0,
"non_diegetic": 0,
"captions": 0,
"descriptions": 0,
"metadata": 0,
"dependent": 0,
"still_image": 0
},
"tags": {
"language": "jpn",
"handler_name": "VideoHandler",
"vendor_id": "[0][0][0][0]"
}
}
""".trimIndent()
val hevcStream2 = """
{
"index": 0,
"codec_name": "hevc",
"codec_long_name": "H.265 / HEVC (High Efficiency Video Coding)",
"profile": "Main 10",
"codec_type": "video",
"codec_tag_string": "[0][0][0][0]",
"codec_tag": "0x0000",
"width": 1920,
"height": 1080,
"coded_width": 1920,
"coded_height": 1080,
"closed_captions": 0,
"film_grain": 0,
"has_b_frames": 2,
"sample_aspect_ratio": "1:1",
"display_aspect_ratio": "16:9",
"pix_fmt": "yuv420p10le",
"level": 150,
"color_range": "tv",
"chroma_location": "left",
"refs": 1,
"r_frame_rate": "24000/1001",
"avg_frame_rate": "24000/1001",
"time_base": "1/1000",
"start_pts": 0,
"start_time": "0.000000",
"extradata_size": 2535,
"disposition": {
"default": 1,
"dub": 0,
"original": 0,
"comment": 0,
"lyrics": 0,
"karaoke": 0,
"forced": 0,
"hearing_impaired": 0,
"visual_impaired": 0,
"clean_effects": 0,
"attached_pic": 0,
"timed_thumbnails": 0,
"non_diegetic": 0,
"captions": 0,
"descriptions": 0,
"metadata": 0,
"dependent": 0,
"still_image": 0
},
"tags": {
"language": "jpn",
"title": "nan",
"BPS": "2105884",
"DURATION": "00:53:27.204000000",
"NUMBER_OF_FRAMES": "76896",
"NUMBER_OF_BYTES": "844250247",
"_STATISTICS_WRITING_APP": "mkvmerge v91.0 ('Signs') 64-bit",
"_STATISTICS_WRITING_DATE_UTC": "2025-03-31 17:33:38",
"_STATISTICS_TAGS": "BPS DURATION NUMBER_OF_FRAMES NUMBER_OF_BYTES"
}
}
""".trimIndent()
val vc1Stream = """
{
"index": 0,
"codec_name": "vc1",
"codec_long_name": "SMPTE VC-1",
"profile": "Advanced",
"codec_type": "video",
"codec_tag_string": "WVC1",
"codec_tag": "0x31435657",
"width": 1920,
"height": 1080,
"coded_width": 1920,
"coded_height": 1080,
"closed_captions": 0,
"film_grain": 0,
"has_b_frames": 1,
"sample_aspect_ratio": "1:1",
"display_aspect_ratio": "16:9",
"pix_fmt": "yuv420p",
"level": 3,
"chroma_location": "left",
"field_order": "progressive",
"refs": 1,
"r_frame_rate": "24000/1001",
"avg_frame_rate": "24000/1001",
"time_base": "1/1000",
"start_pts": 0,
"start_time": "0.000000",
"duration_ts": 5189856,
"duration": "5189.856000",
"extradata_size": 34,
"disposition": {
"default": 1,
"dub": 0,
"original": 0,
"comment": 0,
"lyrics": 0,
"karaoke": 0,
"forced": 0,
"hearing_impaired": 0,
"visual_impaired": 0,
"clean_effects": 0,
"attached_pic": 0,
"timed_thumbnails": 0,
"non_diegetic": 0,
"captions": 0,
"descriptions": 0,
"metadata": 0,
"dependent": 0,
"still_image": 0
},
"tags": {
"title": ""
}
}
""".trimIndent()
val h264stream1 = """
{
"index": 0,
"codec_name": "h264",
"codec_long_name": "H.264 / AVC / MPEG-4 AVC / MPEG-4 part 10",
"profile": "High",
"codec_type": "video",
"codec_tag_string": "avc1",
"codec_tag": "0x31637661",
"width": 1920,
"height": 1080,
"coded_width": 1920,
"coded_height": 1080,
"closed_captions": 0,
"film_grain": 0,
"has_b_frames": 2,
"sample_aspect_ratio": "1:1",
"display_aspect_ratio": "16:9",
"pix_fmt": "yuv420p",
"level": 40,
"chroma_location": "left",
"field_order": "progressive",
"refs": 1,
"is_avc": "true",
"nal_length_size": "4",
"id": "0x1",
"r_frame_rate": "30000/1001",
"avg_frame_rate": "30000/1001",
"time_base": "1/30000",
"start_pts": 0,
"start_time": "0.000000",
"duration_ts": 185519300,
"duration": "6183.976667",
"bit_rate": "6460534",
"bits_per_raw_sample": "8",
"nb_frames": "185334",
"extradata_size": 45,
"disposition": {
"default": 1,
"dub": 0,
"original": 0,
"comment": 0,
"lyrics": 0,
"karaoke": 0,
"forced": 0,
"hearing_impaired": 0,
"visual_impaired": 0,
"clean_effects": 0,
"attached_pic": 0,
"timed_thumbnails": 0,
"non_diegetic": 0,
"captions": 0,
"descriptions": 0,
"metadata": 0,
"dependent": 0,
"still_image": 0
},
"tags": {
"creation_time": "2018-07-09T06:20:13.000000Z",
"language": "und",
"handler_name": "nah",
"vendor_id": "[0][0][0][0]"
}
}
""".trimIndent()
}

View File

@ -29,7 +29,10 @@ data class VideoPreference(
val codec: String = "h264", val codec: String = "h264",
val pixelFormat: String = "yuv420p", val pixelFormat: String = "yuv420p",
val pixelFormatPassthrough: List<String> = listOf<String>("yuv420p", "yuv420p10le"), val pixelFormatPassthrough: List<String> = listOf<String>("yuv420p", "yuv420p10le"),
val threshold: Int = 16 val threshold: Int = 16,
val useAnnexB: Boolean = false,
val reencodeOnIncorrectMetadataForChromecast: Boolean = false,
val h264Level: Double = 4.2
) )
data class AudioPreference( data class AudioPreference(