Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added options disableImageSequenceDetection and ffprobeInputParams #12

Merged
merged 2 commits into from
Dec 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .reuse/dep5
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ Files: *.mp4
Copyright: 2021 Sveriges Television AB
License: CC-BY-SA-4.0

Files: *.tts
Copyright: 2023 Sveriges Television AB
License: CC-BY-SA-4.0

Files: *.json
Copyright: 2020 Sveriges Television AB
License: CC0-1.0
Expand Down
24 changes: 16 additions & 8 deletions src/main/kotlin/se/svt/oss/mediaanalyzer/MediaAnalyzer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,14 @@ class MediaAnalyzer

private val log = KotlinLogging.logger { }

fun analyze(file: String, probeInterlaced: Boolean = false): MediaFile {
fun analyze(
file: String,
probeInterlaced: Boolean = false,
disableImageSequenceDetection: Boolean = false,
ffprobeInputParams: LinkedHashMap<String, String?> = linkedMapOf()
): MediaFile {
val mediaInfo = try {
mediaInfoAnalyzer.analyze(file)
mediaInfoAnalyzer.analyze(file, disableImageSequenceDetection)
} catch (e: Exception) {
log.warn(e) { "Error running mediainfo on $file!" }
null
Expand All @@ -57,14 +62,15 @@ class MediaAnalyzer
)
}

val probeResult = ffprobeAnalyzer.analyze(file)
return mergeResults(probeResult, mediaInfo, probeInterlaced)
val probeResult = ffprobeAnalyzer.analyze(file, ffprobeInputParams)
return mergeResults(probeResult, mediaInfo, probeInterlaced, ffprobeInputParams)
}

private fun mergeResults(
probeResult: ProbeResult,
mediaInfo: MediaInfo?,
probeInterlaced: Boolean
probeInterlaced: Boolean,
ffprobeInputParams: LinkedHashMap<String, String?>
): MediaFile {
val format = probeResult.format ?: throw IllegalStateException("No format detected in ffprobe result!")
val formatName = mediaInfo?.generalTrack?.format ?: format.format_name
Expand Down Expand Up @@ -93,15 +99,16 @@ class MediaAnalyzer
format = formatName,
overallBitrate = overallBitrate,
duration = duration,
videoStreams = videoStreams(probeResult, mediaInfo, probeInterlaced),
videoStreams = videoStreams(probeResult, mediaInfo, probeInterlaced, ffprobeInputParams),
audioStreams = audioStreams(probeResult, mediaInfo)
)
}

private fun videoStreams(
probeResult: ProbeResult,
mediaInfo: MediaInfo?,
probeInterlaced: Boolean
probeInterlaced: Boolean,
ffprobeInputParams: LinkedHashMap<String, String?>
): List<VideoStream> {
val videoStreams = probeResult.videoStreams
val mediaInfoStreams = mediaInfo?.videoTracks?.let {
Expand All @@ -118,7 +125,8 @@ class MediaAnalyzer
?: (ffVideoStream.r_frame_rate.toFraction().toDouble() * duration).toInt()
val interlaced = if (probeInterlaced) ffprobeAnalyzer.isInterlaced(
probeResult.format!!.filename,
index
index,
ffprobeInputParams
) else videoTrack?.isInterlaced ?: false

VideoStream(
Expand Down
59 changes: 38 additions & 21 deletions src/main/kotlin/se/svt/oss/mediaanalyzer/ffprobe/FfprobeAnalyzer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,18 +10,25 @@ import se.svt.oss.mediaanalyzer.util.ProcessUtil
class FfprobeAnalyzer
@JvmOverloads constructor(private val objectMapper: ObjectMapper = ObjectMapper().findAndRegisterModules()) {

fun analyze(file: String): ProbeResult {
fun analyze(file: String, ffprobeInputParams: LinkedHashMap<String, String?>): ProbeResult {
val command = buildList<String> {
add("ffprobe")
add("-v")
add("quiet")
add("-of")
add("json")
add("-show_streams")
add("-show_format")
add("-show_error")
ffprobeInputParams.forEach { (key, value) ->
add("-$key")
value?.let { add(it) }
}
add(file)
}
val (exitCode, probeResult) = ProcessUtil.runAndParse<ProbeResult>(
objectMapper,
"ffprobe",
"-v",
"quiet",
"-of",
"json",
"-show_streams",
"-show_format",
"-show_error",
file
*command.toTypedArray()
)
if (exitCode != 0 || probeResult.error != null) {
val message = probeResult.error?.string ?: "exitcode: $exitCode"
Expand All @@ -31,19 +38,29 @@ class FfprobeAnalyzer
}

@JvmOverloads
fun isInterlaced(file: String, videoIndex: Int = 0): Boolean {
fun isInterlaced(file: String, videoIndex: Int = 0, ffprobeInputParams: LinkedHashMap<String, String?>): Boolean {
val command = buildList<String> {
add("ffprobe")
add("-v")
add("quiet")
add("-of")
add("json")
add("-read_intervals")
add("%+5")
add("-select_streams")
add("v:$videoIndex")
add("-show_entries")
add("frame=interlaced_frame")
add("-show_error")
ffprobeInputParams.forEach { (key, value) ->
add("-$key")
value?.let { add(it) }
}
add(file)
}
val (exitCode, probeResult) = ProcessUtil.runAndParse<ProbeResult>(
objectMapper,
"ffprobe",
"-v",
"quiet",
"-of",
"json",
"-read_intervals", "%+5",
"-select_streams", "v:$videoIndex",
"-show_entries", "frame=interlaced_frame",
"-show_error",
file
*command.toTypedArray()
)
if (exitCode != 0 || probeResult.error != null) {
val message = probeResult.error?.string ?: "exitcode: $exitCode"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,18 @@ import se.svt.oss.mediaanalyzer.util.ProcessUtil
class MediaInfoAnalyzer
@JvmOverloads constructor(private val objectMapper: ObjectMapper = ObjectMapper().findAndRegisterModules()) {

fun analyze(file: String): MediaInfo {
fun analyze(file: String, disableImageSequenceDetection: Boolean): MediaInfo {
val args = buildList {
add("mediainfo")
add("--Output=JSON")
if (disableImageSequenceDetection) {
add("--File_TestContinuousFileNames=0")
}
add(file)
}
val (exitCode, mediaInfo) = ProcessUtil.runAndParse<MediaInfo>(
objectMapper,
"mediainfo",
"--Output=JSON",
file
*args.toTypedArray()
)
if (exitCode != 0) {
throw RuntimeException("mediainfo returned exit code: $exitCode")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,10 @@ class MediaAnalyzerIntegrationTest {
fun testVideoFile() {
val file = javaClass.getResource("/test.mp4").file
val videoFile = MediaAnalyzer().analyze(file, true) as VideoFile
println("videoFile is: $videoFile")
assertThat(videoFile)
.hasFormat("MPEG-4")
.hasDuration(10.016)
.hasOverallBitrate(2424115)
.hasDuration(10.01)
.hasOverallBitrate(2425568)
.hasFileSize(3034992)

assertThat(videoFile.file).endsWith("/test.mp4")
Expand Down Expand Up @@ -80,8 +79,8 @@ class MediaAnalyzerIntegrationTest {

assertThat(audioFile)
.hasFormat("MPEG-4")
.hasOverallBitrate(132016)
.hasDuration(2.643)
.hasOverallBitrate(133124)
.hasDuration(2.621)

assertThat(audioFile.audioStreams).hasSize(1)
assertThat(audioFile.audioStreams.first())
Expand All @@ -93,6 +92,7 @@ class MediaAnalyzerIntegrationTest {
.hasBitrate(128104)
.hasProfile("LC")
}

@Test
fun testAudioHeAac() {
val file = javaClass.getResource("/he-aac_test.mp4").file
Expand All @@ -115,4 +115,22 @@ class MediaAnalyzerIntegrationTest {
.hasBitrate(64183)
.hasProfile("HE-AAC")
}

@Test
fun ffprobeInputParams() {
val file = javaClass.getResource("/rawaudio.tts").file

val audioFile = MediaAnalyzer()
.analyze(
file,
ffprobeInputParams = linkedMapOf(
"f" to "s16le",
"ac" to "1",
"ar" to "22050",
)
)
assertThat(audioFile).isInstanceOf(AudioFile::class.java)
assertThat(audioFile as AudioFile)
.hasDuration(5.0)
}
}
8 changes: 4 additions & 4 deletions src/test/kotlin/se/svt/oss/mediaanalyzer/MediaAnalyzerTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -230,19 +230,19 @@ internal class MediaAnalyzerTest {
}

private fun mockFfprobe(jsonPath: String) {
every { anyConstructed<FfprobeAnalyzer>().analyze(file) } returns parse(jsonPath)
every { anyConstructed<FfprobeAnalyzer>().analyze(file, any()) } returns parse(jsonPath)
}

private fun mockFfprobeInterlaced(interlaced: Boolean) {
every { anyConstructed<FfprobeAnalyzer>().isInterlaced(any(), any()) } returns interlaced
every { anyConstructed<FfprobeAnalyzer>().isInterlaced(any(), any(), any()) } returns interlaced
}

private fun mockMediaInfoFails() {
every { anyConstructed<MediaInfoAnalyzer>().analyze(file) } throws RuntimeException("mediainfo failed!")
every { anyConstructed<MediaInfoAnalyzer>().analyze(file, any()) } throws RuntimeException("mediainfo failed!")
}

private fun mockMediaInfo(jsonPath: String) {
every { anyConstructed<MediaInfoAnalyzer>().analyze(file) } returns parse(jsonPath)
every { anyConstructed<MediaInfoAnalyzer>().analyze(file, any()) } returns parse(jsonPath)
}

private inline fun <reified T> parse(file: String): T = objectMapper.readValue<T>(javaClass.getResource(file))
Expand Down
Binary file added src/test/resources/rawaudio.tts
Binary file not shown.
Loading