Added support for multiple Video streams

This commit is contained in:
Alex Eidt 2022-09-09 22:14:40 -07:00
parent 32f30850c4
commit b577495180
5 changed files with 124 additions and 73 deletions

View file

@ -20,6 +20,7 @@ Calling the `Read()` function will fill in the `Video` struct `framebuffer` with
```go ```go
vidio.NewVideo(filename string) (*vidio.Video, error) vidio.NewVideo(filename string) (*vidio.Video, error)
vidio.NewVideoStreams(filename string) ([]*vidio.Video, error)
FileName() string FileName() string
Width() int Width() int
@ -31,7 +32,9 @@ Duration() float64
FPS() float64 FPS() float64
Codec() string Codec() string
AudioCodec() string AudioCodec() string
Stream() int
FrameBuffer() []byte FrameBuffer() []byte
MetaData() map[string]string
SetFrameBuffer(buffer []byte) error SetFrameBuffer(buffer []byte) error
Read() bool Read() bool

View file

@ -1,6 +1,7 @@
package vidio package vidio
import ( import (
"bytes"
"fmt" "fmt"
"io" "io"
"os" "os"
@ -98,16 +99,15 @@ func NewCamera(stream int) (*Camera, error) {
} }
// Parses the webcam metadata (width, height, fps, codec) from ffmpeg output. // Parses the webcam metadata (width, height, fps, codec) from ffmpeg output.
func (camera *Camera) parseWebcamData(buffer []byte) { func (camera *Camera) parseWebcamData(buffer string) {
bufferstr := string(buffer) index := strings.Index(buffer, "Stream #")
index := strings.Index(bufferstr, "Stream #")
if index == -1 { if index == -1 {
index++ index++
} }
bufferstr = bufferstr[index:] buffer = buffer[index:]
// Dimensions. widthxheight. // Dimensions. widthxheight.
regex := regexp.MustCompile(`\d{2,}x\d{2,}`) regex := regexp.MustCompile(`\d{2,}x\d{2,}`)
match := regex.FindString(bufferstr) match := regex.FindString(buffer)
if len(match) > 0 { if len(match) > 0 {
split := strings.Split(match, "x") split := strings.Split(match, "x")
camera.width = int(parse(split[0])) camera.width = int(parse(split[0]))
@ -115,7 +115,7 @@ func (camera *Camera) parseWebcamData(buffer []byte) {
} }
// FPS. // FPS.
regex = regexp.MustCompile(`\d+(.\d+)? fps`) regex = regexp.MustCompile(`\d+(.\d+)? fps`)
match = regex.FindString(bufferstr) match = regex.FindString(buffer)
if len(match) > 0 { if len(match) > 0 {
index = strings.Index(match, " fps") index = strings.Index(match, " fps")
if index != -1 { if index != -1 {
@ -125,7 +125,7 @@ func (camera *Camera) parseWebcamData(buffer []byte) {
} }
// Codec. // Codec.
regex = regexp.MustCompile("Video: .+,") regex = regexp.MustCompile("Video: .+,")
match = regex.FindString(bufferstr) match = regex.FindString(buffer)
if len(match) > 0 { if len(match) > 0 {
match = match[len("Video: "):] match = match[len("Video: "):]
index = strings.Index(match, "(") index = strings.Index(match, "(")
@ -164,20 +164,22 @@ func (camera *Camera) getCameraData(device string) error {
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
return err return err
} }
// Read ffmpeg output from Stdout. // Read ffmpeg output from Stdout.
buffer := make([]byte, 2<<11) builder := bytes.Buffer{}
total := 0 buffer := make([]byte, 1024)
for { for {
n, err := pipe.Read(buffer[total:]) n, err := pipe.Read(buffer)
total += n builder.Write(buffer[:n])
if err == io.EOF { if err == io.EOF {
break break
} }
} }
// Wait for the command to finish. // Wait for the command to finish.
cmd.Wait() cmd.Wait()
camera.parseWebcamData(buffer[:total]) camera.parseWebcamData(builder.String())
return nil return nil
} }

View file

@ -1,6 +1,7 @@
package vidio package vidio
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"io" "io"
@ -39,7 +40,7 @@ func installed(program string) error {
} }
// Runs ffprobe on the given file and returns a map of the metadata. // Runs ffprobe on the given file and returns a map of the metadata.
func ffprobe(filename, stype string) (map[string]string, error) { func ffprobe(filename, stype string) ([]map[string]string, error) {
// "stype" is stream stype. "v" for video, "a" for audio. // "stype" is stream stype. "v" for video, "a" for audio.
// Extract video information with ffprobe. // Extract video information with ffprobe.
cmd := exec.Command( cmd := exec.Command(
@ -60,11 +61,11 @@ func ffprobe(filename, stype string) (map[string]string, error) {
return nil, err return nil, err
} }
// Read ffprobe output from Stdout. // Read ffprobe output from Stdout.
buffer := make([]byte, 2<<10) builder := bytes.Buffer{}
total := 0 buffer := make([]byte, 1024)
for { for {
n, err := pipe.Read(buffer[total:]) n, err := pipe.Read(buffer)
total += n builder.Write(buffer[:n])
if err == io.EOF { if err == io.EOF {
break break
} }
@ -75,8 +76,12 @@ func ffprobe(filename, stype string) (map[string]string, error) {
} }
// Parse ffprobe output to fill in video data. // Parse ffprobe output to fill in video data.
datalist := make([]map[string]string, 0)
metadata := string(builder.String())
for _, stream := range strings.Split(metadata, "\n") {
if len(strings.TrimSpace(stream)) > 0 {
data := make(map[string]string) data := make(map[string]string)
for _, line := range strings.Split(string(buffer[:total]), "|") { for _, line := range strings.Split(stream, "|") {
if strings.Contains(line, "=") { if strings.Contains(line, "=") {
keyValue := strings.Split(line, "=") keyValue := strings.Split(line, "=")
if _, ok := data[keyValue[0]]; !ok { if _, ok := data[keyValue[0]]; !ok {
@ -84,7 +89,11 @@ func ffprobe(filename, stype string) (map[string]string, error) {
} }
} }
} }
return data, nil datalist = append(datalist, data)
}
}
return datalist, nil
} }
// Parses the given data into a float64. // Parses the given data into a float64.
@ -112,17 +121,16 @@ func webcam() (string, error) {
// For webcam streaming on windows, ffmpeg requires a device name. // For webcam streaming on windows, ffmpeg requires a device name.
// All device names are parsed and returned by this function. // All device names are parsed and returned by this function.
func parseDevices(buffer []byte) []string { func parseDevices(buffer string) []string {
bufferstr := string(buffer)
index := strings.Index(strings.ToLower(bufferstr), "directshow video device") index := strings.Index(strings.ToLower(buffer), "directshow video device")
if index != -1 { if index != -1 {
bufferstr = bufferstr[index:] buffer = buffer[index:]
} }
index = strings.Index(strings.ToLower(bufferstr), "directshow audio device") index = strings.Index(strings.ToLower(buffer), "directshow audio device")
if index != -1 { if index != -1 {
bufferstr = bufferstr[:index] buffer = buffer[:index]
} }
type Pair struct { type Pair struct {
@ -135,7 +143,7 @@ func parseDevices(buffer []byte) []string {
pairs := []Pair{} pairs := []Pair{}
// Find all device names surrounded by quotes. E.g "Windows Camera Front" // Find all device names surrounded by quotes. E.g "Windows Camera Front"
regex := regexp.MustCompile("\"[^\"]+\"") regex := regexp.MustCompile("\"[^\"]+\"")
for _, line := range strings.Split(strings.ReplaceAll(bufferstr, "\r\n", "\n"), "\n") { for _, line := range strings.Split(strings.ReplaceAll(buffer, "\r\n", "\n"), "\n") {
if strings.Contains(strings.ToLower(line), "alternative name") { if strings.Contains(strings.ToLower(line), "alternative name") {
match := regex.FindString(line) match := regex.FindString(line)
if len(match) > 0 { if len(match) > 0 {
@ -188,17 +196,19 @@ func getDevicesWindows() ([]string, error) {
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
return nil, err return nil, err
} }
// Read list devices from Stdout. // Read list devices from Stdout.
buffer := make([]byte, 2<<10) builder := bytes.Buffer{}
total := 0 buffer := make([]byte, 1024)
for { for {
n, err := pipe.Read(buffer[total:]) n, err := pipe.Read(buffer)
total += n builder.Write(buffer[:n])
if err == io.EOF { if err == io.EOF {
break break
} }
} }
cmd.Wait() cmd.Wait()
devices := parseDevices(buffer) devices := parseDevices(builder.String())
return devices, nil return devices, nil
} }

View file

@ -21,7 +21,9 @@ type Video struct {
fps float64 // Frames per second. fps float64 // Frames per second.
codec string // Codec used for video encoding. codec string // Codec used for video encoding.
audioCodec string // Codec used for audio encoding. audioCodec string // Codec used for audio encoding.
stream int // Stream Index.
framebuffer []byte // Raw frame data. framebuffer []byte // Raw frame data.
metadata map[string]string // Video metadata.
pipe *io.ReadCloser // Stdout pipe for ffmpeg process. pipe *io.ReadCloser // Stdout pipe for ffmpeg process.
cmd *exec.Cmd // ffmpeg command. cmd *exec.Cmd // ffmpeg command.
} }
@ -65,27 +67,46 @@ func (video *Video) Codec() string {
return video.codec return video.codec
} }
// Returns the audio codec of the first audio track (if present).
// Can be used to check if a video has audio.
func (video *Video) AudioCodec() string { func (video *Video) AudioCodec() string {
return video.audioCodec return video.audioCodec
} }
// Returns the zero-indexed video stream index.
func (video *Video) Stream() int {
return video.stream
}
func (video *Video) FrameBuffer() []byte { func (video *Video) FrameBuffer() []byte {
return video.framebuffer return video.framebuffer
} }
// Raw Metadata from ffprobe output for the video file.
func (video *Video) MetaData() map[string]string {
return video.metadata
}
func (video *Video) SetFrameBuffer(buffer []byte) error { func (video *Video) SetFrameBuffer(buffer []byte) error {
size := video.width * video.height * video.depth size := video.width * video.height * video.depth
if len(buffer) < size { if len(buffer) < size {
return fmt.Errorf("buffer size %d is smaller than frame size %d", len(buffer), size) return fmt.Errorf("buffer size %d is smaller than frame size %d", len(buffer), size)
} }
video.framebuffer = buffer video.framebuffer = buffer
return nil return nil
} }
// Creates a new Video struct.
// Uses ffprobe to get video information and fills in the Video struct with this data.
func NewVideo(filename string) (*Video, error) { func NewVideo(filename string) (*Video, error) {
streams, err := NewVideoStreams(filename)
if streams == nil {
return nil, err
}
return streams[0], err
}
// Read all video streams from the given file.
func NewVideoStreams(filename string) ([]*Video, error) {
if !exists(filename) { if !exists(filename) {
return nil, fmt.Errorf("video file %s does not exist", filename) return nil, fmt.Errorf("video file %s does not exist", filename)
} }
@ -111,14 +132,30 @@ func NewVideo(filename string) (*Video, error) {
return nil, err return nil, err
} }
video := &Video{filename: filename, depth: 3} audioCodec := ""
if len(audioData) > 0 {
video.addVideoData(videoData) // Look at the first audio stream only.
if audioCodec, ok := audioData["codec_name"]; ok { if ac, ok := audioData[0]["codec_name"]; ok {
video.audioCodec = audioCodec audioCodec = ac
}
} }
return video, nil streams := make([]*Video, len(videoData))
for i, data := range videoData {
video := &Video{
filename: filename,
depth: 3,
audioCodec: audioCodec,
stream: i,
metadata: data,
}
video.addVideoData(data)
streams[i] = video
}
return streams, nil
} }
// Adds Video data to the video struct from the ffprobe output. // Adds Video data to the video struct from the ffprobe output.
@ -135,14 +172,12 @@ func (video *Video) addVideoData(data map[string]string) {
if frames, ok := data["nb_frames"]; ok { if frames, ok := data["nb_frames"]; ok {
video.frames = int(parse(frames)) video.frames = int(parse(frames))
} }
if fps, ok := data["r_frame_rate"]; ok { if fps, ok := data["r_frame_rate"]; ok {
split := strings.Split(fps, "/") split := strings.Split(fps, "/")
if len(split) == 2 && split[0] != "" && split[1] != "" { if len(split) == 2 && split[0] != "" && split[1] != "" {
video.fps = parse(split[0]) / parse(split[1]) video.fps = parse(split[0]) / parse(split[1])
} }
} }
if bitrate, ok := data["bit_rate"]; ok { if bitrate, ok := data["bit_rate"]; ok {
video.bitrate = int(parse(bitrate)) video.bitrate = int(parse(bitrate))
} }
@ -164,6 +199,7 @@ func (video *Video) init() error {
"-loglevel", "quiet", "-loglevel", "quiet",
"-pix_fmt", "rgb24", "-pix_fmt", "rgb24",
"-vcodec", "rawvideo", "-vcodec", "rawvideo",
"-map", fmt.Sprintf("0:v:%d", video.stream),
"-", "-",
) )

View file

@ -46,6 +46,7 @@ func TestVideoMetaData(t *testing.T) {
assertEquals(video.fps, float64(30)) assertEquals(video.fps, float64(30))
assertEquals(video.codec, "h264") assertEquals(video.codec, "h264")
assertEquals(video.audioCodec, "aac") assertEquals(video.audioCodec, "aac")
assertEquals(video.stream, 0)
assertEquals(len(video.framebuffer), 0) assertEquals(len(video.framebuffer), 0)
if video.pipe != nil { if video.pipe != nil {
@ -163,26 +164,26 @@ func TestFFprobe(t *testing.T) {
if err != nil { if err != nil {
panic(err) panic(err)
} }
assertEquals(koalaVideo["width"], "480") assertEquals(koalaVideo[0]["width"], "480")
assertEquals(koalaVideo["height"], "270") assertEquals(koalaVideo[0]["height"], "270")
assertEquals(koalaVideo["duration"], "3.366667") assertEquals(koalaVideo[0]["duration"], "3.366667")
assertEquals(koalaVideo["bit_rate"], "170549") assertEquals(koalaVideo[0]["bit_rate"], "170549")
assertEquals(koalaVideo["codec_name"], "h264") assertEquals(koalaVideo[0]["codec_name"], "h264")
koalaAudio, err := ffprobe("test/koala.mp4", "a") koalaAudio, err := ffprobe("test/koala.mp4", "a")
if err != nil { if err != nil {
panic(err) panic(err)
} }
assertEquals(koalaAudio["codec_name"], "aac") assertEquals(koalaAudio[0]["codec_name"], "aac")
koalaVideo, err = ffprobe("test/koala-noaudio.mp4", "v") koalaVideo, err = ffprobe("test/koala-noaudio.mp4", "v")
if err != nil { if err != nil {
panic(err) panic(err)
} }
assertEquals(koalaVideo["width"], "480") assertEquals(koalaVideo[0]["width"], "480")
assertEquals(koalaVideo["height"], "270") assertEquals(koalaVideo[0]["height"], "270")
assertEquals(koalaVideo["duration"], "3.366667") assertEquals(koalaVideo[0]["duration"], "3.366667")
assertEquals(koalaVideo["bit_rate"], "170549") assertEquals(koalaVideo[0]["bit_rate"], "170549")
assertEquals(koalaVideo["codec_name"], "h264") assertEquals(koalaVideo[0]["codec_name"], "h264")
koalaAudio, err = ffprobe("test/koala-noaudio.mp4", "a") koalaAudio, err = ffprobe("test/koala-noaudio.mp4", "a")
if err != nil { if err != nil {
panic(err) panic(err)
@ -197,7 +198,7 @@ func TestFFprobe(t *testing.T) {
func TestDeviceParsingWindows(t *testing.T) { func TestDeviceParsingWindows(t *testing.T) {
// Sample string taken from FFmpeg wiki: // Sample string taken from FFmpeg wiki:
data := parseDevices( data := parseDevices(
[]byte(`ffmpeg version N-45279-g6b86dd5... --enable-runtime-cpudetect `ffmpeg version N-45279-g6b86dd5... --enable-runtime-cpudetect
libavutil 51. 74.100 / 51. 74.100 libavutil 51. 74.100 / 51. 74.100
libavcodec 54. 65.100 / 54. 65.100 libavcodec 54. 65.100 / 54. 65.100
libavformat 54. 31.100 / 54. 31.100 libavformat 54. 31.100 / 54. 31.100
@ -212,7 +213,6 @@ func TestDeviceParsingWindows(t *testing.T) {
[dshow @ 03ACF580] "Internal Microphone (Conexant 2" [dshow @ 03ACF580] "Internal Microphone (Conexant 2"
[dshow @ 03ACF580] "virtual-audio-capturer" [dshow @ 03ACF580] "virtual-audio-capturer"
dummy: Immediate exit requested`, dummy: Immediate exit requested`,
),
) )
assertEquals(data[0], "Integrated Camera") assertEquals(data[0], "Integrated Camera")