257 lines
5.8 KiB
Go
257 lines
5.8 KiB
Go
package vidio
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"regexp"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
// Returns true if file exists, false otherwise.
|
|
// https://stackoverflow.com/questions/12518876/how-to-check-if-a-file-exists-in-go.
|
|
func exists(filename string) bool {
|
|
_, err := os.Stat(filename)
|
|
if err == nil {
|
|
return true
|
|
}
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return false
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Checks if the given program is installed.
|
|
func installed(program string) error {
|
|
cmd := exec.Command(program, "-version")
|
|
|
|
if err := cmd.Run(); err != nil {
|
|
return fmt.Errorf("vidio: %s is not installed", program)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Runs ffprobe on the given file and returns a map of the metadata.
|
|
func ffprobe(filename, stype string) ([]map[string]string, error) {
|
|
// "stype" is stream stype. "v" for video, "a" for audio.
|
|
// Extract video information with ffprobe.
|
|
cmd := exec.Command(
|
|
"ffprobe",
|
|
"-show_streams",
|
|
"-select_streams", stype,
|
|
"-print_format", "compact",
|
|
"-loglevel", "quiet",
|
|
filename,
|
|
)
|
|
|
|
pipe, err := cmd.StdoutPipe()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Read ffprobe output from Stdout.
|
|
builder := bytes.Buffer{}
|
|
buffer := make([]byte, 1024)
|
|
for {
|
|
n, err := pipe.Read(buffer)
|
|
builder.Write(buffer[:n])
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Wait for ffprobe command to complete.
|
|
if err := cmd.Wait(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Parse ffprobe output to fill in video data.
|
|
datalist := make([]map[string]string, 0)
|
|
metadata := builder.String()
|
|
for _, stream := range strings.Split(metadata, "\n") {
|
|
if len(strings.TrimSpace(stream)) > 0 {
|
|
data := make(map[string]string)
|
|
for _, line := range strings.Split(stream, "|") {
|
|
if strings.Contains(line, "=") {
|
|
keyValue := strings.Split(line, "=")
|
|
if _, ok := data[keyValue[0]]; !ok {
|
|
data[keyValue[0]] = keyValue[1]
|
|
}
|
|
}
|
|
}
|
|
datalist = append(datalist, data)
|
|
}
|
|
}
|
|
|
|
return datalist, nil
|
|
}
|
|
|
|
// Parses the given data into a float64.
|
|
func parse(data string) float64 {
|
|
n, err := strconv.ParseFloat(data, 64)
|
|
if err != nil {
|
|
return 0
|
|
}
|
|
return n
|
|
}
|
|
|
|
// Returns the webcam name used for the -f option with ffmpeg.
|
|
func webcam() (string, error) {
|
|
switch runtime.GOOS {
|
|
case "linux":
|
|
return "v4l2", nil
|
|
case "darwin":
|
|
return "avfoundation", nil // qtkit
|
|
case "windows":
|
|
return "dshow", nil // vfwcap
|
|
default:
|
|
return "", fmt.Errorf("vidio: unsupported OS: %s", runtime.GOOS)
|
|
}
|
|
}
|
|
|
|
// For webcam streaming on windows, ffmpeg requires a device name.
|
|
// All device names are parsed and returned by this function.
|
|
func parseDevices(buffer string) []string {
|
|
index := strings.Index(strings.ToLower(buffer), "directshow video device")
|
|
if index != -1 {
|
|
buffer = buffer[index:]
|
|
}
|
|
|
|
index = strings.Index(strings.ToLower(buffer), "directshow audio device")
|
|
if index != -1 {
|
|
buffer = buffer[:index]
|
|
}
|
|
|
|
type Pair struct {
|
|
name string
|
|
alt string
|
|
}
|
|
// Parses ffmpeg output to get device names. Windows only.
|
|
// Uses parsing approach from https://github.com/imageio/imageio/blob/master/imageio/plugins/ffmpeg.py#L681.
|
|
|
|
pairs := []Pair{}
|
|
// Find all device names surrounded by quotes. E.g "Windows Camera Front"
|
|
regex := regexp.MustCompile("\"[^\"]+\"")
|
|
for _, line := range strings.Split(strings.ReplaceAll(buffer, "\r\n", "\n"), "\n") {
|
|
if strings.Contains(strings.ToLower(line), "alternative name") {
|
|
match := regex.FindString(line)
|
|
if len(match) > 0 {
|
|
pairs[len(pairs)-1].alt = match[1 : len(match)-1]
|
|
}
|
|
} else {
|
|
match := regex.FindString(line)
|
|
if len(match) > 0 {
|
|
pairs = append(pairs, Pair{name: match[1 : len(match)-1]})
|
|
}
|
|
}
|
|
}
|
|
|
|
devices := []string{}
|
|
// If two devices have the same name, use the alternate name of the later device as its name.
|
|
for _, pair := range pairs {
|
|
if contains(devices, pair.name) {
|
|
devices = append(devices, pair.alt)
|
|
} else {
|
|
devices = append(devices, pair.name)
|
|
}
|
|
}
|
|
return devices
|
|
}
|
|
|
|
func contains(list []string, item string) bool {
|
|
for _, i := range list {
|
|
if i == item {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Returns the webcam device name.
|
|
// On windows, ffmpeg output from the -list_devices command is parsed to find the device name.
|
|
func getDevicesWindows() ([]string, error) {
|
|
// Run command to get list of devices.
|
|
cmd := exec.Command(
|
|
"ffmpeg",
|
|
"-hide_banner",
|
|
"-list_devices", "true",
|
|
"-f", "dshow",
|
|
"-i", "dummy",
|
|
)
|
|
|
|
pipe, err := cmd.StderrPipe()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Read list devices from Stdout.
|
|
builder := bytes.Buffer{}
|
|
buffer := make([]byte, 1024)
|
|
for {
|
|
n, err := pipe.Read(buffer)
|
|
builder.Write(buffer[:n])
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
}
|
|
|
|
cmd.Wait()
|
|
|
|
devices := parseDevices(builder.String())
|
|
return devices, nil
|
|
}
|
|
|
|
// Error representing a strings.Builder failure in the buildSelectExpression func.
|
|
var errExpressionBuilder = fmt.Errorf("vidio: failed to write tokens to the frame select expresion")
|
|
|
|
// Helper function used to generate a "-vf select" expression that specifies which video frames should be exported.
|
|
func buildSelectExpression(n ...int) (string, error) {
|
|
sb := strings.Builder{}
|
|
if _, err := sb.WriteString("select='"); err != nil {
|
|
return "", errExpressionBuilder
|
|
}
|
|
|
|
for index, frame := range n {
|
|
if index != 0 {
|
|
if _, err := sb.WriteRune('+'); err != nil {
|
|
return "", errExpressionBuilder
|
|
}
|
|
}
|
|
|
|
if _, err := sb.WriteString("eq(n\\,"); err != nil {
|
|
|
|
return "", errExpressionBuilder
|
|
}
|
|
|
|
if _, err := sb.WriteString(strconv.Itoa(frame)); err != nil {
|
|
|
|
return "", errExpressionBuilder
|
|
}
|
|
|
|
if _, err := sb.WriteRune(')'); err != nil {
|
|
|
|
return "", errExpressionBuilder
|
|
}
|
|
}
|
|
|
|
if _, err := sb.WriteRune('\''); err != nil {
|
|
|
|
return "", errExpressionBuilder
|
|
}
|
|
|
|
return sb.String(), nil
|
|
}
|