我试图在golang中用os / exec调用shell命令,该命令将花费一些时间,因此我想检索reatime输出并打印处理后的输出(进度比率编号)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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)
}

我想拥有一个功能,可以在命令打印某些内容时更新屏幕,

ffmpeg命令输出如下:

1
2
3
4
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/

实际上,上面的4行是ffmpeg命令输出的最后一行,它不断变化,我想将变化打印出来,就像

1
2
3
4
18%
44%
69%
100%

我怎样才能做到这一点?

  • fmt库具有一组扫描功能,可以将格式化的字符串解析为值。 您可以在标准输出管道上使用"扫描仪"来扫描行并扫描每条格式化的行。 您需要弄清楚如何从ffmpeg中获得完整与总计的比率。
  • 流命令输出进度可能重复
  • @icza,您能看看我发布的答案吗,为什么我的屏幕上什么都没打印出来?
  • @seaguest我也在这里添加了答案。

看起来ffmpeg发送所有诊断消息("控制台输出")
到stderr而不是stdout。下面的代码对我有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
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()
}

ffmpeg的版本如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
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

检查以下内容,需要增强(不建议直接使用),但是可以使用:)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
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)
    }
}

我确实发现他在那篇文章中提到的icza解决方案非常有用,但是并不能解决我的问题。

我做了如下测试:

1,我写了一个脚本,每秒打印一些信息十次,这是script.sh

1
2
3
4
5
6
7
#!/bin/bash

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

2,读取标准输出并从标准输出中提取所需的信息,并执行一些过程以获取所需的格式,这是代码:
包主

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
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
", progressingRatio)
    }
}

这确实适用于我的script.sh测试,但对于ffmpeg命令却不起作用,在ffmpeg的情况下,什么都没有打印,并且进程完成了(不卡住),我想将数据写入ffmpeg的stdout的方式是有点特殊(也许根本没有换行符,我尝试了icza的解决方案,但仍然不起作用)。

  • 您可以在伪终端中运行ffmpeg,这可能会起作用(?)

当您具有从Go开始的外部命令的exec.Cmd值时,可以使用其Cmd.StdinCmd.StdoutCmd.Stderr字段以某种方式与进程通信。

某种方式意味着您可以将数据发送到其标准输入,并且可以读取其标准输出和错误流。

压力是标准的。如果外部进程正在通过网络连接发送数据,或者正在将数据写入文件,则将无法通过上述3个流截取这些数据。

现在进入ffmpegffmpeg和许多其他控制台应用程序不将数据写入标准输出/错误,但是它们使用系统调用或其他库(使用系统调用)来操纵终端窗口。当然,应用程序可以将一些数据发送到标准输出/错误,并可以通过操纵终端窗口来显示其他数据。

因此,您看不到ffmpeg的输出,因为您尝试读取其标准输出/错误,但是ffmpeg不会通过写入这些流来显示其输出。

通常,如果要捕获此类应用程序的输出,则需要一个能够捕获终端窗口(文本)内容的库。在较容易的情况下,该应用程序支持将这些输出转储到通常由额外的命令行参数控制的文件中,然后您可以从Go中读取/监视这些文件。

  • "您需要一个能够捕获终端窗口(文本)内容的库">您对此有何建议? 这是朝着需要PTY的方向发展吗?
  • @DuncanJones不,我没有任何想法。 我以为像termbox-go这样的库可以做到这一点,但似乎它可以从其缓冲区中返回字符。
  • 谢谢。 在此期间,Ive找到了github.com/creack/pty,这做得很好。