commit 0400a4a2d965835f0d7a3818f0ebb99e57f68b89
parent 3cbeffacbd2da1db0a12d3f8bb91172d8d7905f0
Author: Jack Mordaunt <jackmordaunt@gmail.com>
Date: Mon, 12 Nov 2018 18:51:18 +1300
[+] Convert to gif in single command instead of multiple.
Order of magnitude faster, lighter on CPU and less code.
Diffstat:
| M | ffmpeg.go | | | 197 | ++++++++++++++++++++----------------------------------------------------------- |
| A | pipe.go | | | 67 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
2 files changed, 116 insertions(+), 148 deletions(-)
diff --git a/ffmpeg.go b/ffmpeg.go
@@ -1,166 +1,67 @@
package giffer
import (
+ "bytes"
"fmt"
- "image"
- "io/ioutil"
"log"
- "os"
- "os/exec"
- "path/filepath"
-
- "github.com/hashicorp/go-multierror"
-
- "github.com/OneOfOne/xxhash"
- "github.com/disintegration/imaging"
+ "strings"
- "github.com/pkg/errors"
+ "os/exec"
)
// FFMpeg wraps the ffmpeg binary.
type FFMpeg struct {
- Dir string
+ Debug bool
}
-// Extract the frames between start and end from the video file.
-func (f FFMpeg) Extract(video string, start, end, fps float64) ([]image.Image, error) {
- cut, err := f.Cut(video, start, end)
- if err != nil {
- return nil, errors.Wrap(err, "cutting video file")
- }
- if fps == 0 {
- fps = 24.4
- }
- hasher := xxhash.New64()
- input := fmt.Sprintf("%s_%f_%f_%f", video, start, end, fps)
- if _, err := hasher.WriteString(input); err != nil {
- return nil, errors.Wrap(err, "hashing input")
+// Convert a video into that of the specified encoding and format between start
+// and end.
+// If end is zero we convert from start until the end of the video.
+func (f FFMpeg) Convert(
+ video string,
+ start, end float64,
+ fps float64,
+ width, height int,
+ encoding, format string,
+) (*bytes.Buffer, error) {
+ var out bytes.Buffer
+ args := []string{
+ "-ss", fmt.Sprintf("%4f", start),
}
- dir := filepath.Join(f.Dir, fmt.Sprintf("%d", hasher.Sum64()))
- info, err := os.Stat(dir)
- if err != nil && !os.IsNotExist(err) {
- return nil, errors.Wrap(err, "inspecting output directory")
+ if end > 0 && end-start > 0 {
+ args = append(args, "-t", fmt.Sprintf("%2f", end-start))
}
- if os.IsNotExist(err) {
- // Wrap work in a closure so we can scope err and defer a cleanup
- // function.
- // The cleanup is necessary because we only check for existence
- // of files, not validity.
- err := func() (err error) {
- log.Printf("making frames")
- defer func() {
- if err != nil {
- if cleanup := os.RemoveAll(dir); cleanup != nil {
- err = multierror.Append(err, cleanup)
- }
- }
- }()
- if err := os.MkdirAll(dir, 0755); err != nil && !os.IsExist(err) {
- return errors.Wrap(err, "preparing directory")
- }
- if err := f.run(
- "-i", cut,
- "-vf", fmt.Sprintf("fps=%2f", fps),
- filepath.Join(dir, "$frame%03d.jpg"),
- ); err != nil {
- return errors.Wrap(err, "extracting frames")
- }
- // Since ffmpeg doesn't always return an error we need to manually check
- // for the expected output. This is naive, simply checking that
- // the directory isn't empty.
- entries, err := ioutil.ReadDir(dir)
- if err != nil {
- return errors.Wrap(err, "reading output directory")
+ args = append(args, "-i", video)
+ if width > 0 || height > 0 || fps > 0 {
+ var vfargs []string
+ if fps > 0 {
+ vfargs = append(vfargs, fmt.Sprintf("fps=%2f", fps))
+ }
+ if width > 0 || height > 0 {
+ if width <= 0 {
+ width = -1
}
- if len(entries) == 0 {
- return fmt.Errorf("no frames found (inspect ffmpeg output)")
+ if height <= 0 {
+ height = -1
}
- return nil
- }()
- if err != nil {
- return nil, err
- }
- }
- if info != nil {
- if !info.IsDir() {
- return nil, fmt.Errorf("inspecting output directory: got a file, not a directory")
- }
- }
- var frames []image.Image
- walk := func(path string, info os.FileInfo, err error) error {
- if err != nil {
- return err
+ vfargs = append(vfargs, fmt.Sprintf("scale=%d:%d", width, height))
}
- if info.IsDir() {
- return nil
- }
- frame, err := imaging.Open(path)
- if err != nil {
- return errors.Wrap(err, "decoding frame")
- }
- frames = append(frames, frame)
- return nil
- }
- if err := filepath.Walk(dir, walk); err != nil {
- return nil, errors.Wrap(err, "walking")
- }
- return frames, nil
-}
-
-// Cut the video file from start to end (in seconds).
-// The returned string is the path to the resulting file.
-func (f FFMpeg) Cut(video string, start, end float64) (string, error) {
- if start > end {
- return "", fmt.Errorf("start > end: %f > %f", start, end)
- }
- if start < 0 {
- return "", fmt.Errorf("start < 0: %f < 0", start)
- }
- if err := os.MkdirAll(f.Dir, 0755); err != nil && !os.IsExist(err) {
- return "", errors.Wrap(err, "preparing directory")
- }
- hasher := xxhash.New64()
- input := fmt.Sprintf("%s_%f_%f", video, start, end)
- if _, err := hasher.WriteString(input); err != nil {
- return "", errors.Wrap(err, "hashing input")
- }
- cut := filepath.Join(f.Dir, fmt.Sprintf("%d.mp4", hasher.Sum64()))
- info, err := os.Stat(cut)
- if err != nil && !os.IsNotExist(err) {
- return "", errors.Wrap(err, "inspecting cut file")
- }
- if info != nil {
- if info.IsDir() {
- return "", errors.Wrap(err, "expected file, got directory")
- }
- return cut, nil
- }
- log.Printf("cutting file.")
- if err := f.run(
- "-ss", fmt.Sprintf("%4f", start),
- "-t", fmt.Sprintf("%4f", end-start),
- "-i", video,
- "-c", "copy", cut,
- ); err != nil {
- return "", errors.Wrap(err, "ffmpeg")
- }
- if _, err := os.Stat(cut); os.IsNotExist(err) {
- return "", fmt.Errorf("cut failed: no output file detected (inspect ffmpeg output)")
- } else if err != nil {
- return "", errors.Wrap(err, "checking output")
- }
- return cut, nil
-}
-
-// IsInstalled checks whether FFMpeg is available on the system PATH.
-func (f FFMpeg) IsInstalled() bool {
- return f.run() == nil
-}
-
-func (f FFMpeg) run(args ...string) error {
- out, err := exec.Command("ffmpeg", args...).CombinedOutput()
- if err != nil {
- return errors.Wrap(err, string(out))
- }
- return nil
+ args = append(args, "-vf", strings.Join(vfargs, ","))
+ }
+ args = append(args,
+ "-c", "copy",
+ "-c:v", encoding,
+ "-f", format, "-",
+ )
+ if f.Debug {
+ log.Printf("ffmpeg %s", strings.Join(args, " "))
+ }
+ cmd := CmdPipe{
+ Out: &out,
+ Debug: f.Debug,
+ Stack: []*exec.Cmd{
+ exec.Command("ffmpeg", args...),
+ },
+ }
+ return &out, cmd.Run()
}
diff --git a/pipe.go b/pipe.go
@@ -0,0 +1,67 @@
+package giffer
+
+import (
+ "bytes"
+ "io"
+ "log"
+ "os/exec"
+
+ "github.com/pkg/errors"
+)
+
+// CmdPipe executes a stack of commands, piping in order.
+type CmdPipe struct {
+ In io.Reader
+ Out io.Writer
+ Stack []*exec.Cmd
+
+ Debug bool
+}
+
+// Run the commands.
+func (p CmdPipe) Run() (err error) {
+ var errBuf bytes.Buffer
+ defer func() {
+ if p.Debug {
+ log.Printf("%s", errBuf.String())
+ }
+ }()
+ pipes := make([]*io.PipeWriter, len(p.Stack)-1)
+ ii := 0
+ for ; ii < len(p.Stack)-1; ii++ {
+ if ii == 0 {
+ p.Stack[ii].Stdin = p.In
+ }
+ stdin, stdout := io.Pipe()
+ p.Stack[ii].Stdout = stdout
+ p.Stack[ii].Stderr = &errBuf
+ p.Stack[ii+1].Stdin = stdin
+ pipes[ii] = stdout
+ }
+ p.Stack[ii].Stdout = p.Out
+ p.Stack[ii].Stderr = &errBuf
+ if err := call(p.Stack, pipes); err != nil {
+ return errors.Wrap(err, string(errBuf.Bytes()))
+ }
+ return err
+}
+
+func call(stack []*exec.Cmd, pipes []*io.PipeWriter) (err error) {
+ if stack[0].Process == nil {
+ if err = stack[0].Start(); err != nil {
+ return err
+ }
+ }
+ if len(stack) > 1 {
+ if err = stack[1].Start(); err != nil {
+ return err
+ }
+ defer func() {
+ if err == nil {
+ pipes[0].Close()
+ err = call(stack[1:], pipes[1:])
+ }
+ }()
+ }
+ return stack[0].Wait()
+}