Skip to content

Instantly share code, notes, and snippets.

@dacr
Last active April 2, 2023 10:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dacr/96450a3133218767e833cd490d180dcf to your computer and use it in GitHub Desktop.
Save dacr/96450a3133218767e833cd490d180dcf to your computer and use it in GitHub Desktop.
Make videos from photos coming from several cameras, one video by camera and by day - WORK IN PROGRESS / published by https://github.com/dacr/code-examples-manager #4fc4bbfd-778c-43c1-b13d-8f8f8c837024/aad2dd56eedcae9d2c3f929a172d908f37d55649
// summary : Make videos from photos coming from several cameras, one video by camera and by day - WORK IN PROGRESS
// keywords : scala, photos, videos, zio, ziostream, TBC
// publish : gist
// authors : David Crosson
// license : Apache NON-AI License Version 2.0 (https://raw.githubusercontent.com/non-ai-licenses/non-ai-licenses/main/NON-AI-APACHE2)
// id : 4fc4bbfd-778c-43c1-b13d-8f8f8c837024
// created-on : 2022-09-02T07:26:13+02:00
//// attachments: 20220902-102506-00-raspcam1-evt.jpg, 20220902-102507-00-raspcam1-evt.jpg, 20220902-102507-01-raspcam1-evt.jpg
// managed-by : https://github.com/dacr/code-examples-manager
// attached-files
// run-with : scala-cli $file
/*
Chosen solution :
- use ffmpeg
- build ordered daily file list
- execute ffmpeg -r 2 -crf 25 -f concat -i 20220901-files-list.txt 20220901.mp4
*/
/*
Coonfiguration through those environment variables :
- PHOTOS_TO_VIDEOS_PATH
- PHOTOS_TO_VIDEOS_DELETE
- PHOTOS_TO_VIDEOS_FILE_MASK
- PHOTOS_TO_VIDEOS_TIMESTAMP_FORMAT
- PHOTOS_TO_VIDEOS_OUTPUT
*/
// ---------------------
//> using scala "3.2.1"
//> using dep "dev.zio::zio:2.0.3"
//> using dep "dev.zio::zio-streams:2.0.3"
//> using dep "dev.zio::zio-nio:2.0.0"
//> using dep "dev.zio::zio-process:0.7.1"
// ---------------------
import zio.*
import zio.stream.*
import zio.nio.file.*
import java.time.format.DateTimeFormatter
import java.time.LocalDateTime
import scala.util.matching.Regex
import zio.process.*
object Photos extends ZIOAppDefault {
case class Config(
directory: String,
delete: Boolean,
fileMask: Regex,
timestampFormat: DateTimeFormatter,
outputVideoFile: String
) {
val path = Path(directory)
}
case class PhotoGroup(origin: String, timestampGrouping: String)
case class Photo(
timestamp: LocalDateTime, // timestamp of the photo
origin: String, // photo origin (typically camera identifier)
file: String // full filename
) {
val path = Path(file)
}
def parseTimestamp(timestamp: String): ZIO[Config, Throwable, LocalDateTime] =
for {
config <- ZIO.service[Config]
parsed <- ZIO.attempt(config.timestampFormat.parse(timestamp))
dateTime <- ZIO.attempt(parsed.query(LocalDateTime.from))
} yield dateTime
def extractTimestampGroup(dateTime: LocalDateTime): String = {
DateTimeFormatter.ofPattern("yyyy-MM-dd").format(dateTime)
}
def makeVideo(group: PhotoGroup, photos: Chunk[Photo]) =
for {
config <- ZIO.service[Config]
inputPhotoListPath <- Files.createTempFileScoped(suffix = ".txt", prefix = Some(s"${group.origin}-${group.timestampGrouping}-"))
// inputPhotoListPath = Path(s"${group.origin}-${group.timestampGrouping}.txt")
_ <- Files.writeLines(inputPhotoListPath, photos.map(p => s"file '${p.path}''"))
outputFilename = config.outputVideoFile.replace("{{group}}", group.origin).replace("{{timestamp}}", group.timestampGrouping)
outputPath = Path(config.directory, outputFilename)
command = Command("/usr/bin/ffmpeg", "-hide_banner", "-y", "-f", "concat", "-safe", "0", "-i", inputPhotoListPath.toString, "-r", "2", "-crf", "25", outputPath.toString)
results <- command.lines
today <- Clock.localDateTime
todayTimeStampGrouping = extractTimestampGroup(today)
_ <- if (group.timestampGrouping != todayTimeStampGrouping && config.delete)
ZIO.foreach(photos)(photo => Files.delete(photo.path))
else ZIO.succeed(Nil)
_ <- ZIO.log(s"Command : ${command.command.mkString(" ")}\nExecution results : ${results.mkString("\n")}")
} yield ()
def makeVideos(groupedPhotos: Map[PhotoGroup, Chunk[Photo]]) =
ZIO.foreachDiscard(groupedPhotos)((group, photos) => makeVideo(group, photos))
val inventory =
for {
config <- ZIO.service[Config]
_ <- ZIO.log(s"scanning ${config.path}")
mask = config.fileMask
photos <- Files
.list(config.path)
.map(_.toString)
.filterNot(_.contains("Trash"))
// .collectZIO { case file @ mask(timestamp) => parseTimestamp(timestamp).map(ts => Photo(ts, group, file)) } // https://github.com/zio/zio/issues/7301
.mapZIO {
case file @ mask(timestamp, group) => parseTimestamp(timestamp).map(ts => Some(Photo(ts, group, file)))
case _ => ZIO.succeed(None)
}
.collect { case Some(photo) => photo }
.tap(f => ZIO.log(s"found $f"))
.runCollect
grouped = photos
.groupBy(photo => PhotoGroup(photo.origin, extractTimestampGroup(photo.timestamp)))
.map { case (group, photos) => group -> photos.sortBy(_.timestamp) }
_ <- makeVideos(grouped)
} yield ()
def run = for {
path <- System.envOrElse("PHOTOS_TO_VIDEOS_PATH", ".")
delete <- System.envOrElse("PHOTOS_TO_VIDEOS_DELETE", "true").mapAttempt(f => f.toBoolean)
mask <- System.envOrElse("PHOTOS_TO_VIDEOS_FILE_MASK", """.*/(\d{8}-\d{6})-\d{2}-(.*)-evt.jpg""").mapAttempt(m => m.r)
format <- System.envOrElse("PHOTOS_TO_VIDEOS_TIMESTAMP_FORMAT", "yyyyMMdd-HHmmss").mapAttempt(tf => DateTimeFormatter.ofPattern(tf))
output <- System.envOrElse("PHOTOS_TO_VIDEOS_OUTPUT", "{{group}}-{{timestamp}}.mp4")
config = Config(path, delete, mask, format, output)
_ <- inventory.provideSomeLayer(ZLayer.succeed(config))
} yield ()
}
Photos.main(Array.empty)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment