package ffmpeg import ( "bufio" "bytes" "encoding/json" "errors" "fmt" "io" "os/exec" "regexp" "strconv" "strings" "time" ) const ( ffmpegCmd = "ffmpeg" updateInterval = time.Second * 5 ) // FFmpeg struct. type FFmpeg struct { Progress progress cmd *exec.Cmd isCancelled bool } type progress struct { quit chan struct{} Frame int FPS float64 Bitrate float64 TotalSize int OutTimeMS int OutTime string DupFrames int DropFrames int Speed string Progress float64 } // ffmpegOptions struct passed into Ffmpeg.Run. type ffmpegOptions struct { Input string Output string Format formatOptions `json:"format"` Video videoOptions `json:"video"` Audio audioOptions `json:"audio"` Filter filterOptions `json:"filter"` Raw []string `json:"raw"` // Raw flag options. } type formatOptions struct { Container string `json:"container"` Clip bool `json:"clip"` StartTime string `json:"startTime"` StopTime string `json:"stopTime"` } type videoOptions struct { Codec string `json:"codec"` Preset string `json:"preset"` Pass string `json:"pass"` Crf int `json:"crf"` Bitrate string `json:"bitrate"` MinRate string `json:"minrate"` MaxRate string `json:"maxrate"` BufSize string `json:"bufsize"` GopSize string `json:"gopsize"` PixelFormat string `json:"pixel_format"` FrameRate string `json:"frame_rate"` Speed string `json:"speed"` Tune string `json:"tune"` Profile string `json:"profile"` Level string `json:"level"` FastStart bool `json:"faststart"` Size string `json:"size"` Width string `json:"width"` Height string `json:"height"` Format string `json:"format"` Aspect string `json:"aspect"` Scaling string `json:"scaling"` CodecOptions string `json:"codec_options"` } type audioOptions struct { Codec string `json:"codec"` Channel string `json:"channel"` Quality string `json:"quality"` SampleRate string `json:"sample_rate"` Volume string `json:"volume"` } type filterOptions struct { Deband bool `json:"deband"` Deshake bool `json:"deshake"` Deflicker bool `json:"deflicker"` Dejudder bool `json:"dejudder"` Denoise string `json:"denoise"` Deinterlace string `json:"deinterlace"` Brightness string `json:"brightness"` Contrast string `json:"contrast"` Saturation string `json:"saturation"` Gamma string `json:"gamma"` Acontrast string `json:"acontrast"` } // Run runs the ffmpeg encoder with options. func (f *FFmpeg) Run(input, output, data string) error { // Parse options and add to args slice. args := parseOptions(input, output, data) // Execute command. f.cmd = exec.Command(ffmpegCmd, args...) // fmt.Println("generated output: ", f.cmd.String()) stdout, _ := f.cmd.StdoutPipe() // Capture stderr (if any). var stderr bytes.Buffer f.cmd.Stderr = &stderr err := f.cmd.Start() if err != nil { return err } // Send progress updates. go f.trackProgress() // Update progress struct. f.updateProgress(stdout) err = f.cmd.Wait() if err != nil { if f.isCancelled { return errors.New("cancelled") } f.finish() return errors.New(stderr.String()) } f.finish() return nil } // Cancel stops an FFmpeg job from running. func (f *FFmpeg) Cancel() { fmt.Println("killing ffmpeg process") f.isCancelled = true if err := f.cmd.Process.Kill(); err != nil { fmt.Println("failed to kill process: ", err) } fmt.Println("killed ffmpeg process") } // Version gets the ffmpeg version. func (f *FFmpeg) Version() (string, error) { out, err := exec.Command(ffmpegCmd, "-version").Output() if err != nil { return "", errors.New("ffmpeg not available on $PATH") } str := strings.Split(string(out), "\n") r, _ := regexp.Compile(`(\d+(\.\d+){2})`) version := r.FindString(str[0]) return version, nil } func (f *FFmpeg) updateProgress(stdout io.ReadCloser) { scanner := bufio.NewScanner(stdout) for scanner.Scan() { line := scanner.Text() str := strings.Replace(line, " ", "", -1) parts := strings.Split(str, " ") f.setProgressParts(parts) } } func (f *FFmpeg) setProgressParts(parts []string) { for i := 0; i < len(parts); i++ { progressSplit := strings.Split(parts[i], "=") k := progressSplit[0] v := progressSplit[1] switch k { case "frame": frame, _ := strconv.Atoi(v) f.Progress.Frame = frame case "fps": fps, _ := strconv.ParseFloat(v, 64) f.Progress.FPS = fps case "bitrate": v = strings.Replace(v, "kbits/s", "", -1) bitrate, _ := strconv.ParseFloat(v, 64) f.Progress.Bitrate = bitrate case "total_size": size, _ := strconv.Atoi(v) f.Progress.TotalSize = size case "out_time_ms": outTimeMS, _ := strconv.Atoi(v) f.Progress.OutTimeMS = outTimeMS case "out_time": f.Progress.OutTime = v case "dup_frames": frames, _ := strconv.Atoi(v) f.Progress.DupFrames = frames case "drop_frames": frames, _ := strconv.Atoi(v) f.Progress.DropFrames = frames case "speed": f.Progress.Speed = v case "progress": progress, _ := strconv.ParseFloat(v, 64) f.Progress.Progress = progress } } } func (f *FFmpeg) trackProgress() { f.Progress.quit = make(chan struct{}) ticker := time.NewTicker(updateInterval) for { select { case <-f.Progress.quit: ticker.Stop() return } } } func (f *FFmpeg) finish() { close(f.Progress.quit) } // Parse options from JSON payload. // This should match the options mapped by: // https://github.com/alfg/ffmpeg-commander/blob/master/src/ffmpeg.js func parseOptions(input, output, data string) []string { args := []string{ "-hide_banner", "-loglevel", "error", // Set loglevel to fail job on errors. "-progress", "pipe:1", "-i", input, } // Decode JSON get options list from data. options := &ffmpegOptions{} if err := json.Unmarshal([]byte(data), &options); err != nil { panic(err) } // If raw options provided, add the list of raw options from ffmpeg presets. if len(options.Raw) > 0 { for _, v := range options.Raw { args = append(args, strings.Split(v, " ")...) } args = append(args, output) return args } // Set options from struct. args = append(args, transformOptions(options)...) // Set 2 pass output if option is set. if options.Video.Pass == "2" { args = append(args, set2Pass(&args)...) } // Add output arg last. args = append(args, output) return args } func setFormatFlags(opt formatOptions) []string { args := []string{} if opt.StartTime != "" { arg := []string{"-ss", opt.StartTime} args = append(args, arg...) } if opt.StopTime != "" { arg := []string{"-to", opt.StopTime} args = append(args, arg...) } return args } func setVideoFlags(opt videoOptions) []string { args := []string{} // Video codec. if opt.Codec != "" { args = append(args, []string{"-c:v", opt.Codec}...) } // Video preset. if opt.Preset != "" && opt.Preset != "none" { args = append(args, []string{"-preset", opt.Preset}...) } // CRF. if opt.Crf != 0 && opt.Pass == "crf" { crf := strconv.Itoa(opt.Crf) args = append(args, []string{"-crf", crf}...) } // Faststart. if opt.FastStart { args = append(args, []string{"-movflags", "faststart"}...) } // Bitrate. if opt.Bitrate != "" && opt.Bitrate != "0" { args = append(args, []string{"-b:v", opt.Bitrate}...) } // Minrate. if opt.MinRate != "" && opt.MinRate != "0" { args = append(args, []string{"-minrate", opt.MinRate}...) } // Maxrate. if opt.MaxRate != "" && opt.MaxRate != "0" { args = append(args, []string{"-maxrate", opt.MaxRate}...) } // Buffer Size. if opt.BufSize != "" && opt.BufSize != "0" { args = append(args, []string{"-bufsize", opt.BufSize}...) } // GOP size. if opt.GopSize != "" && opt.GopSize != "0" { args = append(args, []string{"-g", opt.GopSize}...) } // Pixel Format. if opt.PixelFormat != "" && opt.PixelFormat != "auto" { args = append(args, []string{"-pix_fmt", opt.PixelFormat}...) } // Frame Rate. if opt.FrameRate != "" && opt.PixelFormat != "auto" { args = append(args, []string{"-r", opt.FrameRate}...) } // Tune. if opt.Tune != "" && opt.Tune != "none" { args = append(args, []string{"-tune", opt.Tune}...) } // Profile. if opt.Profile != "" && opt.Profile != "none" { args = append(args, []string{"-profile:v", opt.Profile}...) } // Level. if opt.Level != "" && opt.Level != "none" { args = append(args, []string{"-level", opt.Level}...) } // Codec params. if opt.CodecOptions != "" && (opt.Codec == "libx264" || opt.Codec == "libx265") { p := strings.Replace(opt.Codec, "lib", "", 1) args = append(args, []string{"-" + p + "-params", opt.CodecOptions}...) } return args } func setVideoFilters(vopt videoOptions, opt filterOptions) string { args := []string{} // Speed. if vopt.Speed != "" && vopt.Speed != "auto" { args = append(args, []string{"setpts=" + vopt.Speed}...) } // Scale. scaleFilters := []string{} if vopt.Size != "" && vopt.Size != "source" { var arg string if vopt.Size == "custom" { arg = "scale=" + vopt.Width + ":" + vopt.Height } else if vopt.Format == "widescreen" { arg = "scale=" + vopt.Size + ":-1" } else { arg = "scale=-1:" + vopt.Size } scaleFilters = append(scaleFilters, arg) } if vopt.Scaling != "" && vopt.Scaling != "auto" { arg := "flags=" + vopt.Scaling scaleFilters = append(scaleFilters, arg) } // Add scale filters to vf flags if provided. if len(scaleFilters) > 0 { scaleFiltersStr := strings.Join(scaleFilters, ":") args = append(args, scaleFiltersStr) } // More filters. if opt.Deband { args = append(args, "deband") } if opt.Deshake { args = append(args, "deshake") } if opt.Deflicker { args = append(args, "deflicker") } if opt.Dejudder { args = append(args, "dejudder") } if opt.Denoise != "none" { var arg string switch opt.Denoise { case "light": arg = "removegrain=22" case "medium": arg = "vaguedenoiser=threshold=3:method=soft:nsteps=5" case "heavy": arg = "vaguedenoiser=threshold=6:method=soft:nsteps=5" default: arg = "removegrain=0" } args = append(args, arg) } if opt.Deinterlace != "none" { var arg string switch opt.Deinterlace { case "frame": arg = "yadif=0:-1:0" case "field": arg = "yadif=1:-1:0" case "frame_nospatial": arg = "yadif=2:-1:0" case "field_nospatial": arg = "yadif=3:-1:0" } args = append(args, arg) } // EQ filters. eq := []string{} if opt.Contrast != "" && opt.Contrast != "1" { eq = append(eq, []string{"contrast=" + opt.Contrast}...) } if opt.Brightness != "" && opt.Brightness != "0" { eq = append(eq, []string{"brightness=" + opt.Brightness}...) } if opt.Saturation != "" && opt.Saturation != "0" { eq = append(eq, []string{"saturation=" + opt.Saturation}...) } if opt.Gamma != "" && opt.Gamma != "0" { eq = append(eq, []string{"gamma=" + opt.Gamma}...) } if len(eq) > 0 { eqStr := strings.Join(eq, ":") args = append(args, []string{"eq=" + eqStr}...) } argsStr := strings.Join(args, ",") return argsStr } func setAudioFlags(opt audioOptions) []string { args := []string{} // Audio codec. if opt.Codec != "" { args = append(args, []string{"-c:a", opt.Codec}...) } // Channel. if opt.Channel != "" && opt.Channel != "source" { args = append(args, []string{"-rematrix_maxval", "1.0", "-ac", opt.Channel}...) } // Bitrate. if opt.Quality != "" && opt.Quality != "auto" { args = append(args, []string{"-b:a", opt.Quality}...) } // Sample rate. if opt.SampleRate != "" && opt.SampleRate != "auto" { args = append(args, []string{"-ar", opt.SampleRate}...) } return args } func setAudioFilters(opt audioOptions, filter filterOptions) string { args := []string{} if opt.Volume != "" && opt.Volume != "100" { v, _ := strconv.ParseFloat(opt.Volume, 64) args = append(args, []string{"volume=" + fmt.Sprintf("%.2f", v/100)}...) } if filter.Acontrast != "" && filter.Acontrast != "33" { a, _ := strconv.ParseFloat(filter.Acontrast, 64) args = append(args, []string{"acontrast=" + fmt.Sprintf("%.2f", a/100)}...) } argsStr := strings.Join(args, ",") return argsStr } func set2Pass(args *[]string) []string { op := "NUL &&" // Windows. cpy := make([]string, len(*args)) copy(cpy, *args) *args = append(*args, []string{"-pass 1", "-f null", op}...) cpy = append([]string{"ffmpeg"}, cpy...) cpy = append(cpy, []string{"-pass 2"}...) return cpy } // transformOptions converts the ffmpegOptions{} struct and converts into // a slice of ffmpeg options to be passed to exec.Command arguments. func transformOptions(opt *ffmpegOptions) []string { args := []string{} // Set format flags if clip options are set. if opt.Format.Clip { arg := setFormatFlags(opt.Format) args = append(args, arg...) } // Video flags. args = append(args, setVideoFlags(opt.Video)...) // Video Filters. vf := []string{"-vf", setVideoFilters(opt.Video, opt.Filter)} // Only push -vf flag if there are video filter arguments. if vf[1] != "" { args = append(args, vf...) } // Audio flags. args = append(args, setAudioFlags(opt.Audio)...) // Audio filters. af := []string{"-af", setAudioFilters(opt.Audio, opt.Filter)} // Only push -af flag if there are audio filter arguments. if af[1] != "" { args = append(args, af...) } extra := []string{ "-y", } args = append(args, extra...) return args }