ugoira-tool/conversion.go
2024-12-30 23:11:29 -03:00

115 lines
2.8 KiB
Go

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
}