From b57749518082c3f77c2efc77151e213ee37fccf1 Mon Sep 17 00:00:00 2001 From: Alex Eidt Date: Fri, 9 Sep 2022 22:14:40 -0700 Subject: [PATCH] Added support for multiple Video streams --- README.md | 3 ++ camera.go | 26 ++++++++-------- utils.go | 58 ++++++++++++++++++++--------------- video.go | 84 ++++++++++++++++++++++++++++++++++++--------------- vidio_test.go | 26 ++++++++-------- 5 files changed, 124 insertions(+), 73 deletions(-) diff --git a/README.md b/README.md index f0e7f39..65ec3be 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ Calling the `Read()` function will fill in the `Video` struct `framebuffer` with ```go vidio.NewVideo(filename string) (*vidio.Video, error) +vidio.NewVideoStreams(filename string) ([]*vidio.Video, error) FileName() string Width() int @@ -31,7 +32,9 @@ Duration() float64 FPS() float64 Codec() string AudioCodec() string +Stream() int FrameBuffer() []byte +MetaData() map[string]string SetFrameBuffer(buffer []byte) error Read() bool diff --git a/camera.go b/camera.go index 0605875..642b35a 100644 --- a/camera.go +++ b/camera.go @@ -1,6 +1,7 @@ package vidio import ( + "bytes" "fmt" "io" "os" @@ -98,16 +99,15 @@ func NewCamera(stream int) (*Camera, error) { } // Parses the webcam metadata (width, height, fps, codec) from ffmpeg output. -func (camera *Camera) parseWebcamData(buffer []byte) { - bufferstr := string(buffer) - index := strings.Index(bufferstr, "Stream #") +func (camera *Camera) parseWebcamData(buffer string) { + index := strings.Index(buffer, "Stream #") if index == -1 { index++ } - bufferstr = bufferstr[index:] + buffer = buffer[index:] // Dimensions. widthxheight. regex := regexp.MustCompile(`\d{2,}x\d{2,}`) - match := regex.FindString(bufferstr) + match := regex.FindString(buffer) if len(match) > 0 { split := strings.Split(match, "x") camera.width = int(parse(split[0])) @@ -115,7 +115,7 @@ func (camera *Camera) parseWebcamData(buffer []byte) { } // FPS. regex = regexp.MustCompile(`\d+(.\d+)? fps`) - match = regex.FindString(bufferstr) + match = regex.FindString(buffer) if len(match) > 0 { index = strings.Index(match, " fps") if index != -1 { @@ -125,7 +125,7 @@ func (camera *Camera) parseWebcamData(buffer []byte) { } // Codec. regex = regexp.MustCompile("Video: .+,") - match = regex.FindString(bufferstr) + match = regex.FindString(buffer) if len(match) > 0 { match = match[len("Video: "):] index = strings.Index(match, "(") @@ -164,20 +164,22 @@ func (camera *Camera) getCameraData(device string) error { if err := cmd.Start(); err != nil { return err } + // Read ffmpeg output from Stdout. - buffer := make([]byte, 2<<11) - total := 0 + builder := bytes.Buffer{} + buffer := make([]byte, 1024) for { - n, err := pipe.Read(buffer[total:]) - total += n + n, err := pipe.Read(buffer) + builder.Write(buffer[:n]) if err == io.EOF { break } } + // Wait for the command to finish. cmd.Wait() - camera.parseWebcamData(buffer[:total]) + camera.parseWebcamData(builder.String()) return nil } diff --git a/utils.go b/utils.go index ffaacc5..0b59550 100644 --- a/utils.go +++ b/utils.go @@ -1,6 +1,7 @@ package vidio import ( + "bytes" "errors" "fmt" "io" @@ -39,7 +40,7 @@ func installed(program string) error { } // 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. // Extract video information with ffprobe. cmd := exec.Command( @@ -60,11 +61,11 @@ func ffprobe(filename, stype string) (map[string]string, error) { return nil, err } // Read ffprobe output from Stdout. - buffer := make([]byte, 2<<10) - total := 0 + builder := bytes.Buffer{} + buffer := make([]byte, 1024) for { - n, err := pipe.Read(buffer[total:]) - total += n + n, err := pipe.Read(buffer) + builder.Write(buffer[:n]) if err == io.EOF { break } @@ -75,16 +76,24 @@ func ffprobe(filename, stype string) (map[string]string, error) { } // Parse ffprobe output to fill in video data. - data := make(map[string]string) - for _, line := range strings.Split(string(buffer[:total]), "|") { - if strings.Contains(line, "=") { - keyValue := strings.Split(line, "=") - if _, ok := data[keyValue[0]]; !ok { - data[keyValue[0]] = keyValue[1] + 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) + 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 data, nil + + return datalist, nil } // 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. // All device names are parsed and returned by this function. -func parseDevices(buffer []byte) []string { - bufferstr := string(buffer) +func parseDevices(buffer string) []string { - index := strings.Index(strings.ToLower(bufferstr), "directshow video device") + index := strings.Index(strings.ToLower(buffer), "directshow video device") 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 { - bufferstr = bufferstr[:index] + buffer = buffer[:index] } type Pair struct { @@ -135,7 +143,7 @@ func parseDevices(buffer []byte) []string { pairs := []Pair{} // Find all device names surrounded by quotes. E.g "Windows Camera Front" 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") { match := regex.FindString(line) if len(match) > 0 { @@ -188,17 +196,19 @@ func getDevicesWindows() ([]string, error) { if err := cmd.Start(); err != nil { return nil, err } + // Read list devices from Stdout. - buffer := make([]byte, 2<<10) - total := 0 + builder := bytes.Buffer{} + buffer := make([]byte, 1024) for { - n, err := pipe.Read(buffer[total:]) - total += n + n, err := pipe.Read(buffer) + builder.Write(buffer[:n]) if err == io.EOF { break } } + cmd.Wait() - devices := parseDevices(buffer) + devices := parseDevices(builder.String()) return devices, nil } diff --git a/video.go b/video.go index c7e58c1..a0738cc 100644 --- a/video.go +++ b/video.go @@ -11,19 +11,21 @@ import ( ) type Video struct { - filename string // Video Filename. - width int // Width of frames. - height int // Height of frames. - depth int // Depth of frames. - bitrate int // Bitrate for video encoding. - frames int // Total number of frames. - duration float64 // Duration of video in seconds. - fps float64 // Frames per second. - codec string // Codec used for video encoding. - audioCodec string // Codec used for audio encoding. - framebuffer []byte // Raw frame data. - pipe *io.ReadCloser // Stdout pipe for ffmpeg process. - cmd *exec.Cmd // ffmpeg command. + filename string // Video Filename. + width int // Width of frames. + height int // Height of frames. + depth int // Depth of frames. + bitrate int // Bitrate for video encoding. + frames int // Total number of frames. + duration float64 // Duration of video in seconds. + fps float64 // Frames per second. + codec string // Codec used for video encoding. + audioCodec string // Codec used for audio encoding. + stream int // Stream Index. + framebuffer []byte // Raw frame data. + metadata map[string]string // Video metadata. + pipe *io.ReadCloser // Stdout pipe for ffmpeg process. + cmd *exec.Cmd // ffmpeg command. } func (video *Video) FileName() string { @@ -65,27 +67,46 @@ func (video *Video) Codec() string { 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 { return video.audioCodec } +// Returns the zero-indexed video stream index. +func (video *Video) Stream() int { + return video.stream +} + func (video *Video) FrameBuffer() []byte { 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 { size := video.width * video.height * video.depth if len(buffer) < size { return fmt.Errorf("buffer size %d is smaller than frame size %d", len(buffer), size) } - video.framebuffer = buffer 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) { + 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) { return nil, fmt.Errorf("video file %s does not exist", filename) } @@ -111,14 +132,30 @@ func NewVideo(filename string) (*Video, error) { return nil, err } - video := &Video{filename: filename, depth: 3} - - video.addVideoData(videoData) - if audioCodec, ok := audioData["codec_name"]; ok { - video.audioCodec = audioCodec + audioCodec := "" + if len(audioData) > 0 { + // Look at the first audio stream only. + if ac, ok := audioData[0]["codec_name"]; ok { + 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. @@ -135,14 +172,12 @@ func (video *Video) addVideoData(data map[string]string) { if frames, ok := data["nb_frames"]; ok { video.frames = int(parse(frames)) } - if fps, ok := data["r_frame_rate"]; ok { split := strings.Split(fps, "/") if len(split) == 2 && split[0] != "" && split[1] != "" { video.fps = parse(split[0]) / parse(split[1]) } } - if bitrate, ok := data["bit_rate"]; ok { video.bitrate = int(parse(bitrate)) } @@ -164,6 +199,7 @@ func (video *Video) init() error { "-loglevel", "quiet", "-pix_fmt", "rgb24", "-vcodec", "rawvideo", + "-map", fmt.Sprintf("0:v:%d", video.stream), "-", ) diff --git a/vidio_test.go b/vidio_test.go index d9091d8..5305826 100644 --- a/vidio_test.go +++ b/vidio_test.go @@ -46,6 +46,7 @@ func TestVideoMetaData(t *testing.T) { assertEquals(video.fps, float64(30)) assertEquals(video.codec, "h264") assertEquals(video.audioCodec, "aac") + assertEquals(video.stream, 0) assertEquals(len(video.framebuffer), 0) if video.pipe != nil { @@ -163,26 +164,26 @@ func TestFFprobe(t *testing.T) { if err != nil { panic(err) } - assertEquals(koalaVideo["width"], "480") - assertEquals(koalaVideo["height"], "270") - assertEquals(koalaVideo["duration"], "3.366667") - assertEquals(koalaVideo["bit_rate"], "170549") - assertEquals(koalaVideo["codec_name"], "h264") + assertEquals(koalaVideo[0]["width"], "480") + assertEquals(koalaVideo[0]["height"], "270") + assertEquals(koalaVideo[0]["duration"], "3.366667") + assertEquals(koalaVideo[0]["bit_rate"], "170549") + assertEquals(koalaVideo[0]["codec_name"], "h264") koalaAudio, err := ffprobe("test/koala.mp4", "a") if err != nil { panic(err) } - assertEquals(koalaAudio["codec_name"], "aac") + assertEquals(koalaAudio[0]["codec_name"], "aac") koalaVideo, err = ffprobe("test/koala-noaudio.mp4", "v") if err != nil { panic(err) } - assertEquals(koalaVideo["width"], "480") - assertEquals(koalaVideo["height"], "270") - assertEquals(koalaVideo["duration"], "3.366667") - assertEquals(koalaVideo["bit_rate"], "170549") - assertEquals(koalaVideo["codec_name"], "h264") + assertEquals(koalaVideo[0]["width"], "480") + assertEquals(koalaVideo[0]["height"], "270") + assertEquals(koalaVideo[0]["duration"], "3.366667") + assertEquals(koalaVideo[0]["bit_rate"], "170549") + assertEquals(koalaVideo[0]["codec_name"], "h264") koalaAudio, err = ffprobe("test/koala-noaudio.mp4", "a") if err != nil { panic(err) @@ -197,7 +198,7 @@ func TestFFprobe(t *testing.T) { func TestDeviceParsingWindows(t *testing.T) { // Sample string taken from FFmpeg wiki: 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 libavcodec 54. 65.100 / 54. 65.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] "virtual-audio-capturer" dummy: Immediate exit requested`, - ), ) assertEquals(data[0], "Integrated Camera")