Archived
1
0
This repository has been archived on 2022-06-28. You can view files and clone it, but cannot push or open issues or pull requests.
ffmpeger/converter/task.go

218 lines
5.9 KiB
Go

package converter
import (
// stdlib
"bufio"
"log"
"os"
"os/exec"
"strconv"
"strings"
"time"
)
// Task represents a single task received via NATS.
type Task struct {
Name string
InputFile string
OutputFile string
// Filed in conversion.
totalFrames int
// Initial calculation state information.
previousOutput string
gotInput bool
gotDuration bool
gotTimeOrFPSParsingError bool
// After totalFrames will be filled we will use these variables
// to work with output.
gotFrame bool
// File info.
duration string
fps string
}
// Convert launches conversion procedure. Should be launched in separate
// goroutine.
func (t *Task) Convert() {
log.Printf("Starting conversion task: %+v\n", t)
currentlyRunningMutex.Lock()
currentlyRunning++
currentlyRunningMutex.Unlock()
defer func() {
currentlyRunningMutex.Lock()
currentlyRunning--
currentlyRunningMutex.Unlock()
}()
ffmpegCmd := exec.Command(ffmpegPath, "-i", t.InputFile, "-c:v", "libx264", "-b:v", "1000k", "-c:a", "aac", "-f", "mp4", t.OutputFile, "-y")
stderr, err := ffmpegCmd.StderrPipe()
if err != nil {
log.Fatalln("Error while preparing to redirect ffmpeg's stderr:", err.Error())
}
stderrScanner := bufio.NewScanner(stderr)
stderrScanner.Split(bufio.ScanWords)
// We will check state every 500ms.
go func() {
checkTick := time.NewTicker(time.Millisecond * 500)
err1 := ffmpegCmd.Start()
if err1 != nil {
log.Fatalln("Failed to start ffmpeg:", err1.Error())
}
for range checkTick.C {
// Should we shutdown immediately?
shouldShutdownMutex.Lock()
shouldWeStop := shouldShutdown
shouldShutdownMutex.Unlock()
if shouldWeStop {
log.Println("Killing converter goroutine...")
err := ffmpegCmd.Process.Kill()
if err != nil {
log.Println("ERROR: failed to kill ffmpeg process:", err.Error())
}
ffmpegCmd.Process.Release()
break
}
}
log.Println("Child ffmpeg process killed")
}()
// Read output.
for {
// Should we shutdown immediately?
shouldShutdownMutex.Lock()
shouldWeStop := shouldShutdown
shouldShutdownMutex.Unlock()
if shouldWeStop {
break
}
proceeding := stderrScanner.Scan()
if proceeding == false {
break
}
//log.Println(stderrScanner.Text())
t.workWithOutput(stderrScanner.Text())
}
log.Println("Stopped reading ffmpeg output")
}
// Printing progress for this task.
func (t *Task) workWithOutput(output string) {
// Do nothing if we have empty output string or if we're not ready.
if output == "" || t.gotTimeOrFPSParsingError {
return
}
// If we have totalFrames defined, which is the very final state
// for calculations below, we should work with output in different
// manner :)
if t.totalFrames != 0 {
// We should look for current frame count.
// If we have "frame=" there - then output for next function
// call will be current frame count.
if strings.Contains(output, "frame=") {
t.gotFrame = true
// If we have only "frame" here - then actual current frame
// will be in next output. Otherwise we should fix output
// to contain only actual current frame.
// We have ASCII here, not runes, len() is fine.
if len(output) == 6 {
// Current frame will be in next output.
return
} else {
output = strings.Split(output, "frame=")[1]
}
}
// ... which we should properly use.
if t.gotFrame {
currentFrame, err := strconv.Atoi(output)
if err != nil {
log.Println("Failed to convert current frame value to int ("+output+"):", err.Error())
t.gotFrame = false
return
}
percentage := currentFrame / int(t.totalFrames/100)
// What if... we mistaken with totalFrames prediction?
if percentage > 100 {
percentage = 100
}
os.Stdout.Write([]byte("\rConverting " + t.InputFile + ": " + strconv.Itoa(percentage) + "% done (" + output + " frame of " + strconv.Itoa(t.totalFrames) + ")"))
// ... and reset it's state so next "frame=" will be the
// next stop.
t.gotFrame = false
}
return
}
// We got input keyword. Next function runs will look for duration.
if output == "Input" {
t.gotInput = true
return
}
if t.gotInput && output == "Duration:" {
t.gotDuration = true
return
}
if t.gotDuration && t.duration == "" {
t.duration = output
log.Println("File duration:", t.duration)
return
}
if t.duration != "" && output == "fps," {
t.fps = t.previousOutput
log.Println("Got FPS value:", t.fps)
// Calculate total frames approximately, because even if ffmpeg
// writes that there is 29.97 fps, it actually might be something
// like 29.971872638217638216.
// BTW, this is a duration, not a time, and to avoid all
// kind of type pr0n we will just fix gathered duration to
// be parsable by time.ParseDuration()
fileDuration := strings.Replace(t.duration, ":", "h", 1)
fileDuration = strings.Replace(fileDuration, ":", "m", 1)
fileDuration = strings.Replace(fileDuration, ".", "s", 1)
fileDuration = strings.Replace(fileDuration, ",", "", 1)
fileDuration += "ms"
totalTime, err := time.ParseDuration(fileDuration)
log.Println("Got file duration parsed:", totalTime)
seconds := totalTime.Seconds()
if err != nil {
log.Println("ERROR: failed to parse video file total time value. No progress output will be produced!")
t.gotTimeOrFPSParsingError = true
}
log.Println("Got file duration in seconds:", seconds)
fps, err1 := strconv.ParseFloat(t.fps, 64)
if err1 != nil {
log.Println("ERROR: failed to parse frames per second value: '" + t.fps + "', no progress output will be produced!")
t.gotTimeOrFPSParsingError = true
}
// We don't mind to loose 1 or 2 fps from total fps counter,
// yea? :)
t.totalFrames = int(float64(seconds) * fps)
log.Println("Total frames calculated:", t.totalFrames)
}
// Save previous output for good unless we have everything we need
// to print progress.
if t.totalFrames == 0 {
t.previousOutput = output
}
}