...instead of creating a new string with `fmt.Sprintf` and writing it with to the file separately.
127 lines
3.1 KiB
Go
127 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 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, ffmpegArgs); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|