39

I am trying to call shell command with os/exec in golang, that command will take some time, so I would like to retrieve the reatime output and print the processed output (a progressing ratio number).

package main

import (
    "bufio"
    "fmt"
    "io"
    "os"
    "os/exec"
    "strings"
)

func main() {
    cmdName := "ffmpeg -i t.webm  -acodec aac -vcodec libx264  cmd1.mp4"
    cmdArgs := strings.Fields(cmdName)

    cmd := exec.Command(cmdArgs[0], cmdArgs[1:len(cmdArgs)]...)
    stdout, _ := cmd.StdoutPipe()
    cmd.Start()
    go print(stdout)
    cmd.Wait()
}

// to print the processed information when stdout gets a new line
func print(stdout io.ReadCloser) {
    r := bufio.NewReader(stdout)
    line, _, err := r.ReadLine()
    fmt.Println("line: %s err %s", line, err)
}

I want to have a function where can update the screen when the command print something,

The ffmpeg command output is as follows:

frame=  101 fps=0.0 q=28.0 size=      91kB time=00:00:04.13 bitrate= 181.2kbits/
frame=  169 fps=168 q=28.0 size=     227kB time=00:00:06.82 bitrate= 272.6kbits/
frame=  231 fps=153 q=28.0 size=     348kB time=00:00:09.31 bitrate= 306.3kbits/
frame=  282 fps=140 q=28.0 size=     499kB time=00:00:11.33 bitrate= 360.8kbits/

in fact, the above 4 line is the last line of ffmpeg command output which keeps changing, I want to print that change out, like

18%
44%
69%
100%

how could I achieve this?

seaguest
  • 2,510
  • 5
  • 27
  • 45
  • 1
    The `fmt` library has set of scan functions that can parse a formatted string into values. You can use a Scanner on the stdout pipe for lines and scan each formatted line. You would need to figure out how to the ratio of complete versus total from the ffmpeg. – Ben Campbell May 07 '16 at 17:36
  • 1
    Possible duplicate of [Streaming commands output progress](http://stackoverflow.com/questions/30725751/streaming-commands-output-progress) – icza May 08 '16 at 05:14
  • @icza, could you please have a look at my posted answer, why I get nothing printed on the screen? – seaguest May 10 '16 at 10:59
  • @seaguest I added an answer here as well. – icza May 10 '16 at 11:54

5 Answers5

42

Looks like ffmpeg sends all diagnostic messages (the "console output") to stderr instead of stdout. Below code works for me.

package main

import (
    "bufio"
    "fmt"
    "os/exec"
    "strings"
)

func main() {
    args := "-i test.mp4 -acodec copy -vcodec copy -f flv rtmp://aaa/bbb"
    cmd := exec.Command("ffmpeg", strings.Split(args, " ")...)

    stderr, _ := cmd.StderrPipe()
    cmd.Start()

    scanner := bufio.NewScanner(stderr)
    scanner.Split(bufio.ScanWords)
    for scanner.Scan() {
        m := scanner.Text()
        fmt.Println(m)
    }
    cmd.Wait()
}

The version of ffmpeg is detailed as below.

ffmpeg version 3.0.2 Copyright (c) 2000-2016 the FFmpeg developers
built with Apple LLVM version 7.3.0 (clang-703.0.29)
configuration: --prefix=/usr/local/Cellar/ffmpeg/3.0.2 --enable-shared --enable-pthreads --enable-gpl --enable-version3 --enable-hardcoded-tables --enable-avresample --cc=clang --host-cflags= --host-ldflags= --enable-opencl --enable-libx264 --enable-libmp3lame --enable-libxvid --enable-vda
libavutil      55. 17.103 / 55. 17.103
libavcodec     57. 24.102 / 57. 24.102
libavformat    57. 25.100 / 57. 25.100
libavdevice    57.  0.101 / 57.  0.101
libavfilter     6. 31.100 /  6. 31.100
libavresample   3.  0.  0 /  3.  0.  0
libswscale      4.  0.100 /  4.  0.100
libswresample   2.  0.101 /  2.  0.101
libpostproc    54.  0.100 / 54.  0.100
Browny Lin
  • 2,427
  • 3
  • 28
  • 32
  • I am not sure if you saw, but there is also `CombinedOutput()` that you can call on your `Command()` object. – Vishrant Feb 11 '22 at 01:25
4

I do find icza's solution that he mentioned in that post is quite useful, however it didn't't solve my problem.

I did a little test as following:

1, I write a script which print some info every second for ten times, here is the script.sh

#!/bin/bash

for i in {1..10}
do
    echo "step " $i
    sleep 1s
done

2, read the stdout and extract the needed information from stdout and do some process to get the expected format, here is the code: package main

import (
    "fmt"
    "os/exec"
    "regexp"
    "strconv"
    "strings"
)

func getRatio(text string) float32 {
    re1, _ := regexp.Compile(`step[\s]+(\d+)`)
    result := re1.FindStringSubmatch(text)
    val, _ := strconv.Atoi(result[1])
    return float32(val) / 10
}

func main() {
    cmdName := "ffmpeg -i t.webm  -acodec aac -vcodec libx264  cmd1.mp4"
    //cmdName := "bash ./script.sh"
    cmdArgs := strings.Fields(cmdName)

    cmd := exec.Command(cmdArgs[0], cmdArgs[1:len(cmdArgs)]...)
    stdout, _ := cmd.StdoutPipe()
    cmd.Start()

    oneByte := make([]byte, 10)
    for {
        _, err := stdout.Read(oneByte)
        if err != nil {
            break
        }
        progressingRatio := getRatio(string(oneByte))
        fmt.Printf("progressing  ratio %v \n", progressingRatio)
    }
}

This does work for my script.sh test, but for the ffmpeg command it doesn't work, in ffmpeg's case, nothing get printed and the process get finished (not stuck), I guess the way of writing data to stdout for ffmpeg is a little special (maybe no newline character at all, and I tried icza's solution, but it still doesn't work).

seaguest
  • 2,510
  • 5
  • 27
  • 45
3

When you have an exec.Cmd value of an external command you started from Go, you may use its Cmd.Stdin, Cmd.Stdout and Cmd.Stderr fields to communicate with the process in some way.

Some way means you can send data to its standard input, and you can read its standard output and error streams.

The stress is on standard. If the external process is sending data on a network connection, or is writing data to a file, you will not be able to intercept those data via the above mentioned 3 streams.

Now on to ffmpeg. ffmpeg and many other console applications do not write data to standard output/error, but they use system calls or other libraries (that use system calls) to manipulate the terminal window. Of course an application may send some data to the standard output/error, and may display other data by manipulating the terminal window.

So you don't see the output of ffmpeg because you try to read its standard output/error, but ffmpeg does not display its output by writing to those streams.

In the general case if you want to capture the output of such applications, you need a library that is capable of capturing the (textual) content of the terminal window. In an easier situation the application supports dumping those output to files usually controlled by extra command line parameters, which then you can read/monitor from Go.

icza
  • 389,944
  • 63
  • 907
  • 827
  • 1
    "*you need a library that is capable of capturing the (textual) content of the terminal window*" > Do you have any suggestions for this? Is this heading in the direction of needing a PTY? – Duncan Jones Aug 12 '19 at 06:07
  • @DuncanJones No, I don't have anything in mind. I thought libs like [termbox-go](https://github.com/nsf/termbox-go) can do it, but it seems it can just return characters from its buffer. – icza Aug 13 '19 at 07:10
  • Thanks. In the interim, I've found https://github.com/creack/pty, which does an excellent job. – Duncan Jones Aug 13 '19 at 08:15
2

check the below, needs enhancements (not recommended to be used as it is) but working :)

package main

import (
    "fmt"
    "os"
    "os/exec"
    "strconv"
    "strings"
)

var duration = 0
var allRes = ""
var lastPer = -1

func durToSec(dur string) (sec int) {
    durAry := strings.Split(dur, ":")
    if len(durAry) != 3 {
        return
    }
    hr, _ := strconv.Atoi(durAry[0])
    sec = hr * (60 * 60)
    min, _ := strconv.Atoi(durAry[1])
    sec += min * (60)
    second, _ := strconv.Atoi(durAry[2])
    sec += second
    return
}
func getRatio(res string) {
    i := strings.Index(res, "Duration")
    if i >= 0 {

        dur := res[i+10:]
        if len(dur) > 8 {
            dur = dur[0:8]

            duration = durToSec(dur)
            fmt.Println("duration:", duration)
            allRes = ""
        }
    }
    if duration == 0 {
        return
    }
    i = strings.Index(res, "time=")
    if i >= 0 {

        time := res[i+5:]
        if len(time) > 8 {
            time = time[0:8]
            sec := durToSec(time)
            per := (sec * 100) / duration
            if lastPer != per {
                lastPer = per
                fmt.Println("Percentage:", per)
            }

            allRes = ""
        }
    }
}

func main() {
    os.Remove("cmd1.mp4")
    cmdName := "ffmpeg -i 1.mp4  -acodec aac -vcodec libx264  cmd1.mp4 2>&1"
    cmd := exec.Command("sh", "-c", cmdName)
    stdout, _ := cmd.StdoutPipe()
    cmd.Start()
    oneByte := make([]byte, 8)
    for {
        _, err := stdout.Read(oneByte)
        if err != nil {
            fmt.Printf(err.Error())
            break
        }
        allRes += string(oneByte)
        getRatio(allRes)
    }
}
aissa
  • 13
  • 6
2

cmd.StderrPipe() will not work, use cmd.StdoutPipe() instead:

package main

import (
    "bufio"
    "fmt"
    "os/exec"
)

func main() {
    cmd :=  exec.Command("ping","-c","5","192.168.0.1")

    stdout, _ := cmd.StdoutPipe()
    cmd.Start()

    scanner := bufio.NewScanner(stdout)
    scanner.Split(bufio.ScanWords)
    for scanner.Scan() {
        m := scanner.Text()
        fmt.Println(m)
    }
    cmd.Wait()
}
xikan
  • 21
  • 3