ugoira-tool/conversion.go
tocariimaa 01c7151dd3 remove redundant os.Remove
The created temporal file is already within another temporary directory
that already gets removed (along with its contents) with `os.RemoveAll`.
2025-01-04 13:03:44 -03:00

126 lines
3.1 KiB
Go

package main
import (
"archive/zip"
"bytes"
"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
fmt.Fprintf(frameDescFile, "file '%s'\n", frame.File)
fmt.Fprintf(frameDescFile, "duration %f\n", frameDur)
}
// repeat last frame (see: https://trac.ffmpeg.org/wiki/Slideshow#Concatdemuxer
lastFrame := frames[len(frames)-1]
fmt.Fprintf(frameDescFile, "file '%s'\n", lastFrame.File)
}
func callFFmpeg(frameDescPath, outputFilePath, workDir, ffmpegArgs string) error {
const ffmpegBaseArgs = `-hide_banner
-loglevel warning
-f concat -i %s
-fps_mode vfr`
// Default codec arguments, for a VP9 WebM
const ffmpegDefaultArgs = `-an
-q:v 10
-c:v libvpx-vp9
-b:v 1M
-crf 5 %s`
var ffmpegArgsTemp string
if len(ffmpegArgs) > 0 {
ffmpegArgsTemp = ffmpegBaseArgs + " " + ffmpegArgs
} else {
ffmpegArgsTemp = ffmpegBaseArgs + " " + ffmpegDefaultArgs
}
cmdArgs := fmt.Sprintf(ffmpegArgsTemp, frameDescPath, outputFilePath)
cmdArgsSplit := strings.Fields(cmdArgs)
ffmpegProc := exec.Command("ffmpeg", cmdArgsSplit...)
var stderr bytes.Buffer
ffmpegProc.Stderr = &stderr
ffmpegProc.Dir = workDir
if err := ffmpegProc.Run(); err != nil {
fmt.Fprintf(os.Stderr, "FFmpeg stderr:\n%s\n", stderr.String())
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, ffmpegArgs 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 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, ffmpegArgs); err != nil {
return err
}
return nil
}