Last active
May 25, 2024 10:19
-
-
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/46c8f4d06c49278a91894aece030bf8fd3a77f6a
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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.4.2" | |
//> 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