From 9bab1e8413a26447b97e8ae2af9a270ca5e209fd Mon Sep 17 00:00:00 2001 From: tocariimaa Date: Mon, 30 Dec 2024 23:11:29 -0300 Subject: [PATCH] add conversion.go --- conversion.go | 115 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 conversion.go diff --git a/conversion.go b/conversion.go new file mode 100644 index 0000000..b7eac66 --- /dev/null +++ b/conversion.go @@ -0,0 +1,115 @@ +package main + +import ( + "archive/zip" + "fmt" + "io" + "os" + "os/exec" + "path/filepath" + "strings" +) + +// Create a file describing frames and their durations for FFmpeg's concat demuxer +// https://ffmpeg.org/ffmpeg-formats.html#concat +func makeConcatDemuxFile(manifest UgoiraManifest, frameDescFile *os.File) { + frames := manifest.Body.Frames + for _, frame := range frames { + frameDur := float32(frame.Delay) / 1000 // convert to seconds + frameDescFile.WriteString(fmt.Sprintf("file '%s'\n", frame.File)) + frameDescFile.WriteString(fmt.Sprintf("duration %f\n", frameDur)) + } + // repeat last frame (see: https://trac.ffmpeg.org/wiki/Slideshow#Concatdemuxer + lastFrame := frames[len(frames)-1] + frameDescFile.WriteString(fmt.Sprintf("file '%s'\n", lastFrame.File)) +} + +func callFFmpeg(frameDescPath, outputFilePath, workDir string) error { + // TODO: allow custom ffmpeg flags + const ffmpegArgs = `-hide_banner + -loglevel warning + -f concat -i %s + -an + -fps_mode vfr + -q:v 10 + -c:v libvpx-vp9 + -b:v 1M + -lossless 1 %s` + + cmdArgs := fmt.Sprintf(ffmpegArgs, frameDescPath, outputFilePath) + cmdArgsSplit := strings.Fields(cmdArgs) + ffmpegProc := exec.Command("ffmpeg", cmdArgsSplit...) + ffmpegProc.Dir = workDir + + if err := ffmpegProc.Run(); err != nil { + if exitErr, ok := err.(*exec.ExitError); ok { + return fmt.Errorf("ffmpeg exited with non zero code: %d", exitErr.ExitCode()) + } + return fmt.Errorf("Could not run ffmpeg: %w", err) + } + return nil +} + +// Reads and uncompress a frame from the archive and saves it +func uncompressFrame(file *zip.File, framePath string) error { + fileFp, err := file.Open() + if err != nil { + return err + } + defer fileFp.Close() + + outFp, err := os.Create(framePath) + if err != nil { + return err + } + defer outFp.Close() + + if _, err := io.Copy(outFp, fileFp); err != nil { + return err + } + return nil +} + +func unpackUgoira(ugoiraPath, outDir string) error { + reader, err := zip.OpenReader(ugoiraPath) + if err != nil { + return err + } + defer reader.Close() + + for _, file := range reader.File { + path := filepath.Join(outDir, file.Name) + if err := uncompressFrame(file, path); err != nil { + return err + } + } + return nil +} + +func ugoira2video(manifest UgoiraManifest, ugoiraFileName, vidOut string) error { + tdir, err := os.MkdirTemp("", "u2vwd-") + if err != nil { + return err + } + defer os.RemoveAll(tdir) + + fraFp, err := os.CreateTemp(tdir, "frames*.txt") + if err != nil { + return err + } + defer os.Remove(fraFp.Name()) + defer fraFp.Close() + + vidOutAbs, err := filepath.Abs(vidOut) + if err != nil { + return err + } + if err := unpackUgoira(ugoiraFileName, tdir); err != nil { + return fmt.Errorf("Could not unpack Ugoira: %w", err) + } + makeConcatDemuxFile(manifest, fraFp) + if err := callFFmpeg(fraFp.Name(), vidOutAbs, tdir); err != nil { + return err + } + return nil +}