giffer

Create .gif images from sites like youtube.com
Log | Files | Refs | LICENSE

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:
Mffmpeg.go | 197++++++++++++++++++++-----------------------------------------------------------
Apipe.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() +}