Added support for multiple Video streams
This commit is contained in:
parent
32f30850c4
commit
b577495180
5 changed files with 124 additions and 73 deletions
|
@ -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
|
||||||
|
|
26
camera.go
26
camera.go
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
48
utils.go
48
utils.go
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
58
video.go
58
video.go
|
@ -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),
|
||||||
"-",
|
"-",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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")
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue