@@ -8,76 +8,87 @@ import mu.KotlinLogging
8
8
import se.svt.oss.encore.config.EncodingProperties
9
9
import se.svt.oss.encore.model.EncoreJob
10
10
import se.svt.oss.encore.model.input.DEFAULT_VIDEO_LABEL
11
+ import se.svt.oss.encore.model.input.VideoIn
11
12
import se.svt.oss.encore.model.input.videoInput
12
13
import se.svt.oss.encore.model.mediafile.toParams
13
14
import se.svt.oss.encore.model.output.Output
14
15
import se.svt.oss.encore.model.output.VideoStreamEncode
15
- import se.svt.oss.mediaanalyzer.file.toFractionOrNull
16
- import kotlin.math.round
17
16
18
17
data class ThumbnailEncode (
19
18
val percentages : List <Int > = listOf(25, 50, 75),
20
19
val thumbnailWidth : Int = -2 ,
21
20
val thumbnailHeight : Int = 1080 ,
22
21
val quality : Int = 5 ,
23
22
val suffix : String = " _thumb" ,
23
+ val suffixZeroPad : Int = 2 ,
24
24
val inputLabel : String = DEFAULT_VIDEO_LABEL ,
25
- val optional : Boolean = false
25
+ val optional : Boolean = false ,
26
+ val intervalSeconds : Double? = null
26
27
) : OutputProducer {
27
28
28
29
private val log = KotlinLogging .logger { }
29
30
30
31
override fun getOutput (job : EncoreJob , encodingProperties : EncodingProperties ): Output ? {
31
32
val videoInput = job.inputs.videoInput(inputLabel)
32
- val inputSeekTo = videoInput?.seekTo
33
- val videoStream = videoInput?.analyzedVideo?.highestBitrateVideoStream
34
33
? : return logOrThrow(" Can not produce thumbnail $suffix . No video input with label $inputLabel !" )
35
-
36
- val frameRate = videoStream.frameRate.toFractionOrNull()?.toDouble()
37
- ? : if (job.duration != null || job.seekTo != null || job.thumbnailTime != null || inputSeekTo != null ) {
38
- return logOrThrow(" Can not produce thumbnail $suffix ! No framerate detected in video input $inputLabel ." )
39
- } else {
40
- 0.0
41
- }
42
-
43
- val numFrames = job.duration?.let { round(it * frameRate).toInt() } ? : ((videoStream.numFrames) - (inputSeekTo?.let { round(it * frameRate).toInt() } ? : 0 ))
44
- val skipFrames = job.seekTo?.let { round(it * frameRate).toInt() } ? : 0
45
- val frames = job.thumbnailTime?.let {
46
- listOf (round((it - (inputSeekTo ? : 0.0 )) * frameRate).toInt())
47
- } ? : percentages.map {
48
- (it * numFrames) / 100 + skipFrames
34
+ val thumbnailTime = job.thumbnailTime?.let { time ->
35
+ videoInput.seekTo?.let { time - it } ? : time
36
+ }
37
+ val select = when {
38
+ thumbnailTime != null -> selectTimes(listOf (thumbnailTime))
39
+ intervalSeconds != null -> selectInterval(intervalSeconds, job.seekTo)
40
+ outputDuration(videoInput, job) <= 0 -> return logOrThrow(" Can not produce thumbnail $suffix . Could not detect duration." )
41
+ percentages.isNotEmpty() -> selectTimes(percentagesToTimes(videoInput, job))
42
+ else -> return logOrThrow(" Can not produce thumbnail $suffix . No times selected." )
49
43
}
50
44
51
- log.debug { " Thumbnail encode inputs: thumbnailTime= ${job.thumbnailTime} , framerate=$frameRate , duration= ${job.duration} , numFrames = $numFrames , skipFrames = $skipFrames . Resulting frames = $frames " }
52
-
53
- val filter = frames.joinToString(
54
- separator = " +" ,
55
- prefix = " select=" ,
56
- postfix = " ,scale=$thumbnailWidth :$thumbnailHeight "
57
- ) { " eq(n\\ ,$it )" }
58
-
59
- val fileRegex = Regex (" ${job.baseName}$suffix \\ d{2}\\ .jpg" )
45
+ val filter = " $select ,scale=w=$thumbnailWidth :h=$thumbnailHeight :out_range=jpeg"
60
46
val params = linkedMapOf(
61
- " frames:v" to " ${frames.size} " ,
62
- " vsync" to " vfr" ,
47
+ " fps_mode" to " vfr" ,
63
48
" q:v" to " $quality "
64
49
)
65
50
51
+ val fileRegex = Regex (" ${job.baseName}$suffix \\ d{$suffixZeroPad }\\ .jpg" )
52
+
66
53
return Output (
67
- id = " ${suffix} 02d .jpg" ,
54
+ id = " ${suffix} 0 ${suffixZeroPad} d .jpg" ,
68
55
video = VideoStreamEncode (
69
56
params = params.toParams(),
70
57
filter = filter,
71
58
inputLabels = listOf (inputLabel)
72
59
),
73
- output = " ${job.baseName}$suffix %02d .jpg" ,
60
+ output = " ${job.baseName}$suffix %0 ${suffixZeroPad} d .jpg" ,
74
61
postProcessor = { outputFolder ->
75
62
outputFolder.listFiles().orEmpty().filter { it.name.matches(fileRegex) }
76
- },
77
- seekable = false
63
+ }
78
64
)
79
65
}
80
66
67
+ private fun selectInterval (interval : Double , outputSeek : Double? ): String {
68
+ val select = outputSeek
69
+ ?.let { " gte(t\\ ,$it )*(isnan(prev_selected_t)+gt(floor((t-$it )/$interval )\\ ,floor((prev_selected_t-$it )/$interval )))" }
70
+ ? : " isnan(prev_selected_t)+gt(floor(t/$interval )\\ ,floor(prev_selected_t/$interval ))"
71
+ return " select=$select "
72
+ }
73
+
74
+ private fun outputDuration (videoIn : VideoIn , job : EncoreJob ): Double {
75
+ val videoStream = videoIn.analyzedVideo.highestBitrateVideoStream
76
+ var inputDuration = videoStream.duration
77
+ videoIn.seekTo?.let { inputDuration - = it }
78
+ job.seekTo?.let { inputDuration - = it }
79
+ return job.duration ? : inputDuration
80
+ }
81
+
82
+ private fun percentagesToTimes (videoIn : VideoIn , job : EncoreJob ): List <Double > {
83
+ val outputDuration = outputDuration(videoIn, job)
84
+ return percentages
85
+ .map { it * outputDuration / 100 }
86
+ .map { t -> job.seekTo?.let { t + it } ? : t }
87
+ }
88
+
89
+ private fun selectTimes (times : List <Double >) =
90
+ " select=${times.joinToString(" +" ) { " lt(prev_pts*TB\\ ,$it )*gte(pts*TB\\ ,$it )" }} "
91
+
81
92
private fun logOrThrow (message : String ): Output ? {
82
93
if (optional) {
83
94
log.info { message }
0 commit comments