Arpith Siromoney đź’¬

Go: exec, goroutines, wait groups and more

I wrote a small program in go to export slides to video using ffmpeg. It creates a video for each slide then concatenates them and finally adds the audio.

For the first step, I didn’t need to create the (single image) videos one after the other, so I used goroutines to run them in separate threads.

wg.Add(1)
go img2video(imgName, imgDuration, outputName)

Running external commands is reasonably straight-forward, and so is accessing error messages

func img2video(imgName string, imgDuration string, outputName string) {
    defer wg.Done()
    var stderr bytes.Buffer
    cmd := exec.Command("ffmpeg", "-loop", "1", "-i", imgName, "-t", imgDuration, "-pix_fmt", "yuv420p", outputName)
    cmd.Stderr = &stderr
    err := cmd.Start()
    if err != nil {
        log.Fatal(err)
    } else {
        log.Printf("Creating "+outputName)
    }
    err = cmd.Wait()
    if err != nil {
		    log.Printf(fmt.Sprint(err) + ": " + stderr.String())
		    log.Fatal(err)
    } else {
        log.Printf("Created "+outputName)
    }
}

Note that defer postpones the wg.Done() call to when the function finishes, and wg is a WaitGroup.

wg.Wait()
done := make(chan bool, 1)
go concatVideos(len(lines), videoListFilename, silentFilename, done)

This way we can wait for all the (single image) videos to be created before we concatenate them using ffmpeg.

func concatVideos(numberOfVideos int, listFilename string, outputName string, done chan bool) {
    var stderr bytes.Buffer
    cmd := exec.Command("ffmpeg", "-f", "concat", "-i", listFilename, "-c", "copy", outputName)
    cmd.Stderr = &stderr
    err := cmd.Start()
    if err != nil {
        log.Fatal(err)
    } else {
        log.Printf("Creating " + outputName + " using " + listFilename)
    }
    err = cmd.Wait()
    if err != nil {
        log.Printf(fmt.Sprint(err) + ": " + stderr.String())
        log.Fatal(err)
    } else {
        log.Printf("Created " + outputName)
    }
    done <- true
}

The done channel is to know when ffmpeg has finished concatenating the files, so that we can add audio (again using ffmpeg).

<-done
go addAudio(silentFilename, *audioFilenamePtr, *outputFilenamePtr, done)

Note that we can delete the single image videos before waiting for the addAudio goroutine to pass a true value on the done channel.

for i, line := range lines {
    if line != "" {
        outputFilename := "out" + strconv.Itoa(i+1) + ".mp4"
        err = os.Remove(outputFilename)
        if err != nil {
            log.Fatal(err)
        } else {
            log.Printf("Deleted " + outputFilename)
        }
    }
}
<-done

Finally, converting an int to string requires strconv.Itoa().