Skip to content

Commit

Permalink
Logging was being a pain so I hardcoded it.
Browse files Browse the repository at this point in the history
  • Loading branch information
salamanders committed Jan 14, 2020
1 parent 748ad1d commit c208934
Show file tree
Hide file tree
Showing 13 changed files with 111 additions and 87 deletions.
7 changes: 4 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
out.mp4
.idea/*

# Created by https://www.gitignore.io/api/osx,java,linux,kotlin,windows,eclipse,intellij,sublimetext
# Edit at https://www.gitignore.io/?templates=osx,java,linux,kotlin,windows,eclipse,intellij,sublimetext
Expand Down Expand Up @@ -293,4 +291,7 @@ $RECYCLE.BIN/
# Windows shortcuts
*.lnk

# End of https://www.gitignore.io/api/osx,java,linux,kotlin,windows,eclipse,intellij,sublimetext
# End of https://www.gitignore.io/api/osx,java,linux,kotlin,windows,eclipse,intellij,sublimetext

.idea/*
/makeslideshow.iml
17 changes: 0 additions & 17 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,23 +26,6 @@
<version>${kotlin.version}</version>
</dependency>

<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.8.0-beta4</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-simple</artifactId>
<version>1.8.0-beta4</version>
</dependency>
<dependency>
<groupId>io.github.microutils</groupId>
<artifactId>kotlin-logging</artifactId>
<version>1.6.25</version>
</dependency>

<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
<dependency>
<groupId>com.google.code.gson</groupId>
Expand Down
18 changes: 9 additions & 9 deletions src/main/kotlin/info/benjaminhill/slideshow/Clip.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ package info.benjaminhill.slideshow

import com.drew.imaging.ImageMetadataReader
import com.drew.metadata.exif.ExifSubIFDDirectory
import mu.KotlinLogging
import net.coobird.thumbnailator.Thumbnails
import java.awt.Dimension
import java.awt.image.BufferedImage
import java.io.File
import java.util.*

/**
* Abstract media wrapper that produces frames that are correctly oriented.
*/
abstract class Clip(val file: File) : Comparable<Clip> {

protected var orientation: Int
Expand Down Expand Up @@ -48,11 +50,13 @@ abstract class Clip(val file: File) : Comparable<Clip> {
abstract fun getNumberOfFrames(): Int

/** Repeat frames as necessary to reach the approx length */
fun getCorrectedFrames(
frameLengthMin: Int,
open fun getCorrectedFrames(
maxRes: Dimension,
frameLengthMin: Int = getNumberOfFrames(),
frameLengthMax: Int = 2 * frameLengthMin
): Sequence<BufferedImage> = sequence {

// Duplicate frames evenly to stretch out the video
val duplicator = if (getNumberOfFrames() < frameLengthMin) {
val duplicateCount = frameLengthMin / getNumberOfFrames()
if (duplicateCount > 1 && getNumberOfFrames() > 1) {
Expand All @@ -62,11 +66,12 @@ abstract class Clip(val file: File) : Comparable<Clip> {
} else {
1
}
// Frames to drop off the beginning if the clip is too long
val trimFromBeginning = if (getNumberOfFrames() * duplicator > frameLengthMax) {
val trim = (getNumberOfFrames() * duplicator) - frameLengthMax
LOG.debug { "${file.name} trimming $trim frames from beginning of clip (after duplication)" }
if (trim > frameLengthMax * 2) {
LOG.info { "${file.name} trimming more than 2x." }
LOG.info { "${file.name} trimming more than 2x ($trim of ${getNumberOfFrames()} removed from beginning, duplicator:$duplicator)" }
}
trim
} else {
Expand Down Expand Up @@ -94,9 +99,4 @@ abstract class Clip(val file: File) : Comparable<Clip> {
else -> this.file.nameWithoutExtension.toLowerCase().compareTo(other.file.nameWithoutExtension.toLowerCase())
}

companion object {
val LOG = KotlinLogging.logger {}
}


}
34 changes: 8 additions & 26 deletions src/main/kotlin/info/benjaminhill/slideshow/ClipFactory.kt
Original file line number Diff line number Diff line change
@@ -1,33 +1,15 @@
package info.benjaminhill.slideshow

import mu.KotlinLogging
import java.io.File

/**
* Loads the correct Clip subclass based on media type (with some fallbacks)
*/
object ClipFactory {
private val LOG = KotlinLogging.logger {}

fun fileToClip(
file: File
): Clip {
if (listOf("mp4", "mpeg", "mov").contains(file.extension.toLowerCase())) {
return ClipMovie(file)
}

if (file.extension.toLowerCase() == "gif") {
return ClipGif(file)
}

if (listOf("jpg", "jpeg", "png").contains(file.extension.toLowerCase())) {
if (file.name.startsWith("mvimg", true) || file.nameWithoutExtension.endsWith("_mp", true)) {
try {
return ClipMotionPhoto(file)
} catch (e: Exception) {
LOG.info { "${file.name} unable to extract motion photo video, ${e.message}" }
}
}

return ClipStill(file)
}
error("${file.name} Don't know how to load file")
fun fileToClip(file: File): Clip = when {
listOf("mp4", "mpeg", "mov").contains(file.extension.toLowerCase()) -> ClipMovie(file)
listOf("gif").contains(file.extension.toLowerCase()) -> ClipGif(file)
listOf("jpg", "jpeg").contains(file.extension.toLowerCase()) && ClipMotionPhoto.isVideoEmbedded(file) -> ClipMotionPhoto(file)
else -> ClipStill(file)
}
}
5 changes: 4 additions & 1 deletion src/main/kotlin/info/benjaminhill/slideshow/ClipGif.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import java.io.File
import javax.imageio.ImageIO
import javax.imageio.ImageReader

/** StackOverflow's advice on how to read an animated GIF file */
/**
* StackOverflow's advice on how to read an animated GIF file
* Ignores frame duration
*/
class ClipGif(file: File) : Clip(file) {

private val reader: ImageReader by lazy {
Expand Down
26 changes: 18 additions & 8 deletions src/main/kotlin/info/benjaminhill/slideshow/ClipMotionPhoto.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,15 @@ import com.google.common.primitives.Bytes
import org.bytedeco.javacv.FFmpegFrameGrabber
import java.io.File

/**
* Special type of movie hidden in a Motion Photo.
*
*/
class ClipMotionPhoto(file: File) : ClipMovie(file) {

init {
val content = file.readBytes()
val target = "ftypmp42".toByteArray()
require(Bytes.indexOf(content, target) > -1) { "${file.name} 'ftypmp42' not found in file." }
}

override fun createGrabber(): FFmpegFrameGrabber {
val content = file.readBytes()
val target = "ftypmp42".toByteArray()
val idx = Bytes.indexOf(content, target)
val idx = Bytes.indexOf(content, TARGET)
require(idx > -1)
LOG.debug { "${file.name} successful motion photo." }
val actualIdx = idx - 4
Expand All @@ -24,4 +21,17 @@ class ClipMotionPhoto(file: File) : ClipMovie(file) {
}
}

companion object {
private val TARGET = "ftypmp42".toByteArray()
fun isVideoEmbedded(file: File): Boolean = file.canRead() &&
(file.name.startsWith("mvimg", true) || file.nameWithoutExtension.endsWith("_mp", true)) &&
file.readBytes().let {
val exists = Bytes.indexOf(it, TARGET) > -1
if (!exists) {
LOG.warn { "${file.name} doesn't contain a MP4 marker." }
}
exists
}
}

}
3 changes: 2 additions & 1 deletion src/main/kotlin/info/benjaminhill/slideshow/ClipMovie.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ import java.awt.image.BufferedImage
import java.io.File

/**
* With any FFmpegFrameGrabber (direct from file or parsed from Live/Motion Photo)
* With any FFmpegFrameGrabber (direct from file or parsed from Live/Motion Photo).
* Apple's Live Photos will already be split into standalone video files from Google Photos album download.
*/
open class ClipMovie(file: File) : Clip(file) {

Expand Down
8 changes: 5 additions & 3 deletions src/main/kotlin/info/benjaminhill/slideshow/ClipStill.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,17 @@ import java.io.File
import javax.imageio.ImageIO

class ClipStill(file: File) : Clip(file) {
// TODO: maxRes: Dimension

override fun getFrames() = sequence<Thumbnails.Builder<BufferedImage>> {
LOG.debug { "Still: ${file.name}" }
LOG.debug { "Still: ${file.name}, zooming over ${getNumberOfFrames()} frames." }
val originalBi = ImageIO.read(file)!!
val width = originalBi.width
val widthPct = width * SCALE_PCT
val height = originalBi.height
val heightPct = height * SCALE_PCT

for (i in 0 until getNumberOfFrames()) {
// Gradual zoom towards center by trimming off the edges equally
yield(Thumbnails.of(originalBi)
.sourceRegion(
(i * widthPct).toInt(), (i * heightPct).toInt(),
Expand All @@ -24,7 +25,8 @@ class ClipStill(file: File) : Clip(file) {
}
}

override fun getNumberOfFrames(): Int = 30
// Reasonable 2 second clip
override fun getNumberOfFrames(): Int = 60

companion object {
const val SCALE_PCT = 0.002
Expand Down
11 changes: 4 additions & 7 deletions src/main/kotlin/info/benjaminhill/slideshow/Main.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package info.benjaminhill.slideshow

import info.benjaminhill.util.BasicLogger

const val FPS = 30
val LOG = BasicLogger()

/**
* @author Benjamin Hill benjaminhill@gmail.com
Expand All @@ -13,14 +16,8 @@ const val FPS = 30
* Also `mogrify -auto-orient -path ../rotated *.jpg` for the stills
*/
fun main() {
LOG.level = BasicLogger.Companion.LEVEL.WARN
val ss = SlideShow()
ss.record()
}

internal fun <T : Any> MutableList<T>.removeOrNull(): T? {
return if (this.isNotEmpty()) {
this.removeAt(0)
} else {
null
}
}
8 changes: 1 addition & 7 deletions src/main/kotlin/info/benjaminhill/slideshow/NOTES
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
TODO: stretch frames if not long enough. Trim if too long
/**
* If a movie then direct to small images
* If an image then copy to maxFrames of images (maybe throw in some nice zoom)
* If a Live Motion or Motion Photo, extract the motion
* Resulting images should be oriented AND sized
*/


val skip = max(0, g.lengthInVideoFrames - maxFrames)
LOG.info { "${file.name}: frames:${g.lengthInVideoFrames}, rotation:$orientation" }
Expand Down
9 changes: 4 additions & 5 deletions src/main/kotlin/info/benjaminhill/slideshow/SlideShow.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package info.benjaminhill.slideshow

import mu.KotlinLogging
import info.benjaminhill.util.removeOrNull
import org.bytedeco.javacpp.avutil
import org.bytedeco.javacv.FFmpegFrameRecorder
import org.bytedeco.javacv.Java2DFrameConverter
Expand Down Expand Up @@ -52,7 +52,7 @@ class SlideShow(
if (creditsFile.canRead()) {
LOG.debug { "FRAME $frameCount: clip into fullscreen" }
ClipStill(creditsFile)
.getCorrectedFrames(frameLengthMin = minClipFrames, maxRes = outputRes)
.getCorrectedFrames(maxRes = outputRes)
.forEach { frame ->
drawFullScreen(frame)
frameCount++
Expand All @@ -73,8 +73,8 @@ class SlideShow(
.forEach { halfScreenClip ->
// drop the halfScreenClip into the first free slot
when {
leftSlot.isEmpty() -> leftSlot.addAll(halfScreenClip.getCorrectedFrames(minClipFrames, maxRes = halfSize))
rightSlot.isEmpty() -> rightSlot.addAll(halfScreenClip.getCorrectedFrames(minClipFrames, maxRes = halfSize))
leftSlot.isEmpty() -> leftSlot.addAll(halfScreenClip.getCorrectedFrames(maxRes = halfSize, frameLengthMin = minClipFrames))
rightSlot.isEmpty() -> rightSlot.addAll(halfScreenClip.getCorrectedFrames(maxRes = halfSize, frameLengthMin = minClipFrames))
else -> LOG.warn { "Why were no slots empty?" }
}
halfScreenClipCount++
Expand Down Expand Up @@ -154,7 +154,6 @@ class SlideShow(


companion object {
private val LOG = KotlinLogging.logger {}
private val converter = Java2DFrameConverter()
}

Expand Down
43 changes: 43 additions & 0 deletions src/main/kotlin/info/benjaminhill/util/BasicLogger.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package info.benjaminhill.util

import java.lang.management.ManagementFactory


class BasicLogger {
var level = LEVEL.DEBUG
private val jvmStartTime = ManagementFactory.getRuntimeMXBean().startTime
private fun ts() = String.format("%09d", System.currentTimeMillis() - jvmStartTime)

fun debug(message: () -> String) {
if (level <= LEVEL.DEBUG) {
println("${ts()} DEBUG: ${message()}")
}
}

fun info(message: () -> String) {
if (level <= LEVEL.INFO) {
println("${ts()} INFO: ${message()}")
}
}

fun warn(message: () -> String) {
if (level <= LEVEL.WARN) {
System.err.println("${ts()} WARN: ${message()}")
}
}

fun error(message: () -> String) {
if (level <= LEVEL.ERROR) {
System.err.println("${ts()} ERROR: ${message()}")
}
}

companion object {
enum class LEVEL {
DEBUG,
INFO,
WARN,
ERROR
}
}
}
9 changes: 9 additions & 0 deletions src/main/kotlin/info/benjaminhill/util/CollectionHelpers.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package info.benjaminhill.util

fun <T : Any> MutableList<T>.removeOrNull(): T? {
return if (this.isNotEmpty()) {
this.removeAt(0)
} else {
null
}
}

0 comments on commit c208934

Please sign in to comment.