Refactoring and Cleanup

This commit is contained in:
Alex Eidt 2022-08-05 18:40:55 -07:00
parent af983f09e4
commit 54cfa7783a
5 changed files with 144 additions and 142 deletions

141
camera.go
View file

@ -6,8 +6,10 @@ import (
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
"regexp"
"runtime" "runtime"
"strconv" "strconv"
"strings"
"syscall" "syscall"
) )
@ -61,41 +63,86 @@ func (camera *Camera) SetFrameBuffer(buffer []byte) error {
return nil return nil
} }
// Returns the webcam device name. // Creates a new camera struct that can read from the device with the given stream index.
// On windows, ffmpeg output from the -list_devices command is parsed to find the device name. func NewCamera(stream int) (*Camera, error) {
func getDevicesWindows() ([]string, error) { // Check if ffmpeg is installed on the users machine.
// Run command to get list of devices. if err := checkExists("ffmpeg"); err != nil {
cmd := exec.Command( return nil, err
"ffmpeg", }
"-hide_banner",
"-list_devices", "true", var device string
"-f", "dshow", switch runtime.GOOS {
"-i", "dummy", case "linux":
) device = "/dev/video" + strconv.Itoa(stream)
pipe, err := cmd.StderrPipe() case "darwin":
device = strconv.Itoa(stream)
case "windows":
// If OS is windows, we need to parse the listed devices to find which corresponds to the
// given "stream" index.
devices, err := getDevicesWindows()
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := cmd.Start(); err != nil { if stream >= len(devices) {
return nil, fmt.Errorf("could not find device with index: %d", stream)
}
device = "video=" + devices[stream]
default:
return nil, fmt.Errorf("unsupported OS: %s", runtime.GOOS)
}
camera := &Camera{name: device, depth: 3}
if err := camera.getCameraData(device); err != nil {
return nil, err return nil, err
} }
// Read list devices from Stdout. return camera, nil
buffer := make([]byte, 2<<10)
total := 0
for {
n, err := pipe.Read(buffer[total:])
total += n
if err == io.EOF {
break
} }
// 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 #")
if index == -1 {
index++
}
bufferstr = bufferstr[index:]
// Dimensions. widthxheight.
regex := regexp.MustCompile(`\d{2,}x\d{2,}`)
match := regex.FindString(bufferstr)
if len(match) > 0 {
split := strings.Split(match, "x")
camera.width = int(parse(split[0]))
camera.height = int(parse(split[1]))
}
// FPS.
regex = regexp.MustCompile(`\d+(.\d+)? fps`)
match = regex.FindString(bufferstr)
if len(match) > 0 {
index = strings.Index(match, " fps")
if index != -1 {
match = match[:index]
}
camera.fps = parse(match)
}
// Codec.
regex = regexp.MustCompile("Video: .+,")
match = regex.FindString(bufferstr)
if len(match) > 0 {
match = match[len("Video: "):]
index = strings.Index(match, "(")
if index != -1 {
match = match[:index]
}
index = strings.Index(match, ",")
if index != -1 {
match = match[:index]
}
camera.codec = strings.TrimSpace(match)
} }
cmd.Wait()
devices := parseDevices(buffer)
return devices, nil
} }
// Get camera meta data such as width, height, fps and codec. // Get camera meta data such as width, height, fps and codec.
func getCameraData(device string, camera *Camera) error { func (camera *Camera) getCameraData(device string) error {
// Run command to get camera data. // Run command to get camera data.
// Webcam will turn on and then off in quick succession. // Webcam will turn on and then off in quick succession.
webcamDeviceName, err := webcam() webcamDeviceName, err := webcam()
@ -131,48 +178,13 @@ func getCameraData(device string, camera *Camera) error {
// Wait for the command to finish. // Wait for the command to finish.
cmd.Wait() cmd.Wait()
parseWebcamData(buffer[:total], camera) camera.parseWebcamData(buffer[:total])
return nil return nil
} }
// Creates a new camera struct that can read from the device with the given stream index.
func NewCamera(stream int) (*Camera, error) {
// Check if ffmpeg is installed on the users machine.
if err := checkExists("ffmpeg"); err != nil {
return nil, err
}
var device string
switch runtime.GOOS {
case "linux":
device = "/dev/video" + strconv.Itoa(stream)
case "darwin":
device = strconv.Itoa(stream)
case "windows":
// If OS is windows, we need to parse the listed devices to find which corresponds to the
// given "stream" index.
devices, err := getDevicesWindows()
if err != nil {
return nil, err
}
if stream >= len(devices) {
return nil, fmt.Errorf("could not find device with index: %d", stream)
}
device = "video=" + devices[stream]
default:
return nil, fmt.Errorf("unsupported OS: %s", runtime.GOOS)
}
camera := Camera{name: device, depth: 3}
if err := getCameraData(device, &camera); err != nil {
return nil, err
}
return &camera, nil
}
// Once the user calls Read() for the first time on a Camera struct, // Once the user calls Read() for the first time on a Camera struct,
// the ffmpeg command which is used to read the camera device is started. // the ffmpeg command which is used to read the camera device is started.
func initCamera(camera *Camera) error { func (camera *Camera) init() error {
// If user exits with Ctrl+C, stop ffmpeg process. // If user exits with Ctrl+C, stop ffmpeg process.
camera.cleanup() camera.cleanup()
@ -190,7 +202,8 @@ func initCamera(camera *Camera) error {
"-i", camera.name, "-i", camera.name,
"-f", "image2pipe", "-f", "image2pipe",
"-pix_fmt", "rgb24", "-pix_fmt", "rgb24",
"-vcodec", "rawvideo", "-", "-vcodec", "rawvideo",
"-",
) )
camera.cmd = cmd camera.cmd = cmd
@ -215,7 +228,7 @@ func initCamera(camera *Camera) error {
func (camera *Camera) Read() bool { func (camera *Camera) Read() bool {
// If cmd is nil, video reading has not been initialized. // If cmd is nil, video reading has not been initialized.
if camera.cmd == nil { if camera.cmd == nil {
if err := initCamera(camera); err != nil { if err := camera.init(); err != nil {
return false return false
} }
} }

View file

@ -91,36 +91,6 @@ func parseFFprobe(input []byte) map[string]string {
return data return data
} }
// Adds Video data to the video struct from the ffprobe output.
func addVideoData(data map[string]string, video *Video) {
if width, ok := data["width"]; ok {
video.width = int(parse(width))
}
if height, ok := data["height"]; ok {
video.height = int(parse(height))
}
if duration, ok := data["duration"]; ok {
video.duration = float64(parse(duration))
}
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))
}
if codec, ok := data["codec_name"]; ok {
video.codec = codec
}
}
// Parses the given data into a float64. // Parses the given data into a float64.
func parse(data string) float64 { func parse(data string) float64 {
n, err := strconv.ParseFloat(data, 64) n, err := strconv.ParseFloat(data, 64)
@ -195,7 +165,6 @@ func parseDevices(buffer []byte) []string {
return devices return devices
} }
// Helper function. Array contains function.
func contains(list []string, item string) bool { func contains(list []string, item string) bool {
for _, i := range list { for _, i := range list {
if i == item { if i == item {
@ -205,45 +174,35 @@ func contains(list []string, item string) bool {
return false return false
} }
// Parses the webcam metadata (width, height, fps, codec) from ffmpeg output. // Returns the webcam device name.
func parseWebcamData(buffer []byte, camera *Camera) { // On windows, ffmpeg output from the -list_devices command is parsed to find the device name.
bufferstr := string(buffer) func getDevicesWindows() ([]string, error) {
index := strings.Index(bufferstr, "Stream #") // Run command to get list of devices.
if index == -1 { cmd := exec.Command(
index++ "ffmpeg",
"-hide_banner",
"-list_devices", "true",
"-f", "dshow",
"-i", "dummy",
)
pipe, err := cmd.StderrPipe()
if err != nil {
return nil, err
} }
bufferstr = bufferstr[index:] if err := cmd.Start(); err != nil {
// Dimensions. widthxheight. return nil, err
regex := regexp.MustCompile(`\d{2,}x\d{2,}`)
match := regex.FindString(bufferstr)
if len(match) > 0 {
split := strings.Split(match, "x")
camera.width = int(parse(split[0]))
camera.height = int(parse(split[1]))
} }
// FPS. // Read list devices from Stdout.
regex = regexp.MustCompile(`\d+(.\d+)? fps`) buffer := make([]byte, 2<<10)
match = regex.FindString(bufferstr) total := 0
if len(match) > 0 { for {
index = strings.Index(match, " fps") n, err := pipe.Read(buffer[total:])
if index != -1 { total += n
match = match[:index] if err == io.EOF {
} break
camera.fps = parse(match)
}
// Codec.
regex = regexp.MustCompile("Video: .+,")
match = regex.FindString(bufferstr)
if len(match) > 0 {
match = match[len("Video: "):]
index = strings.Index(match, "(")
if index != -1 {
match = match[:index]
}
index = strings.Index(match, ",")
if index != -1 {
match = match[:index]
}
camera.codec = strings.TrimSpace(match)
} }
} }
cmd.Wait()
devices := parseDevices(buffer)
return devices, nil
}

View file

@ -6,6 +6,7 @@ import (
"os" "os"
"os/exec" "os/exec"
"os/signal" "os/signal"
"strings"
"syscall" "syscall"
) )
@ -107,7 +108,7 @@ func NewVideo(filename string) (*Video, error) {
video := &Video{filename: filename, depth: 3} video := &Video{filename: filename, depth: 3}
addVideoData(videoData, video) video.addVideoData(videoData)
if audioCodec, ok := audioData["codec_name"]; ok { if audioCodec, ok := audioData["codec_name"]; ok {
video.audioCodec = audioCodec video.audioCodec = audioCodec
} }
@ -115,9 +116,39 @@ func NewVideo(filename string) (*Video, error) {
return video, nil return video, nil
} }
// Adds Video data to the video struct from the ffprobe output.
func (video *Video) addVideoData(data map[string]string) {
if width, ok := data["width"]; ok {
video.width = int(parse(width))
}
if height, ok := data["height"]; ok {
video.height = int(parse(height))
}
if duration, ok := data["duration"]; ok {
video.duration = float64(parse(duration))
}
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))
}
if codec, ok := data["codec_name"]; ok {
video.codec = codec
}
}
// Once the user calls Read() for the first time on a Video struct, // Once the user calls Read() for the first time on a Video struct,
// the ffmpeg command which is used to read the video is started. // the ffmpeg command which is used to read the video is started.
func initVideo(video *Video) error { func (video *Video) init() error {
// If user exits with Ctrl+C, stop ffmpeg process. // If user exits with Ctrl+C, stop ffmpeg process.
video.cleanup() video.cleanup()
// ffmpeg command to pipe video data to stdout in 8-bit RGB format. // ffmpeg command to pipe video data to stdout in 8-bit RGB format.
@ -153,7 +184,7 @@ func initVideo(video *Video) error {
func (video *Video) Read() bool { func (video *Video) Read() bool {
// If cmd is nil, video reading has not been initialized. // If cmd is nil, video reading has not been initialized.
if video.cmd == nil { if video.cmd == nil {
if err := initVideo(video); err != nil { if err := video.init(); err != nil {
return false return false
} }
} }

View file

@ -185,7 +185,7 @@ func NewVideoWriter(filename string, width, height int, options *Options) (*Vide
// Once the user calls Write() for the first time on a VideoWriter struct, // Once the user calls Write() for the first time on a VideoWriter struct,
// the ffmpeg command which is used to write to the video file is started. // the ffmpeg command which is used to write to the video file is started.
func initVideoWriter(writer *VideoWriter) error { func (writer *VideoWriter) init() error {
// If user exits with Ctrl+C, stop ffmpeg process. // If user exits with Ctrl+C, stop ffmpeg process.
writer.cleanup() writer.cleanup()
// ffmpeg command to write to video file. Takes in bytes from Stdin and encodes them. // ffmpeg command to write to video file. Takes in bytes from Stdin and encodes them.
@ -291,7 +291,7 @@ func initVideoWriter(writer *VideoWriter) error {
func (writer *VideoWriter) Write(frame []byte) error { func (writer *VideoWriter) Write(frame []byte) error {
// If cmd is nil, video writing has not been set up. // If cmd is nil, video writing has not been set up.
if writer.cmd == nil { if writer.cmd == nil {
if err := initVideoWriter(writer); err != nil { if err := writer.init(); err != nil {
return err return err
} }
} }

View file

@ -223,12 +223,11 @@ dummy: Immediate exit requested`,
func TestWebcamParsing(t *testing.T) { func TestWebcamParsing(t *testing.T) {
camera := &Camera{} camera := &Camera{}
err := getCameraData( err := camera.getCameraData(
`Input #0, dshow, from 'video=Integrated Camera': `Input #0, dshow, from 'video=Integrated Camera':
Duration: N/A, start: 1367309.442000, bitrate: N/A Duration: N/A, start: 1367309.442000, bitrate: N/A
Stream #0:0: Video: mjpeg (Baseline) (MJPG / 0x47504A4D), yuvj422p(pc, bt470bg/unknown/unknown), 1280x720, 30 fps, 30 tbr, 10000k tbn Stream #0:0: Video: mjpeg (Baseline) (MJPG / 0x47504A4D), yuvj422p(pc, bt470bg/unknown/unknown), 1280x720, 30 fps, 30 tbr, 10000k tbn
At least one output file must be specified`, At least one output file must be specified`,
camera,
) )
if err != nil { if err != nil {