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 }