115 lines
2.8 KiB
Go
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
|
|
}
|