diff --git a/camera.go b/camera.go index 4e585bd..383b7ed 100644 --- a/camera.go +++ b/camera.go @@ -6,8 +6,10 @@ import ( "os" "os/exec" "os/signal" + "regexp" "runtime" "strconv" + "strings" "syscall" ) @@ -61,41 +63,86 @@ func (camera *Camera) SetFrameBuffer(buffer []byte) error { return nil } -// 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 { +// 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 } - if err := cmd.Start(); err != nil { - return nil, err - } - // Read list devices from Stdout. - buffer := make([]byte, 2<<10) - total := 0 - for { - n, err := pipe.Read(buffer[total:]) - total += n - if err == io.EOF { - break + + 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 := camera.getCameraData(device); err != nil { + return nil, err + } + return camera, nil +} + +// 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. -func getCameraData(device string, camera *Camera) error { +func (camera *Camera) getCameraData(device string) error { // Run command to get camera data. // Webcam will turn on and then off in quick succession. webcamDeviceName, err := webcam() @@ -131,48 +178,13 @@ func getCameraData(device string, camera *Camera) error { // Wait for the command to finish. cmd.Wait() - parseWebcamData(buffer[:total], camera) + camera.parseWebcamData(buffer[:total]) 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, // 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. camera.cleanup() @@ -190,7 +202,8 @@ func initCamera(camera *Camera) error { "-i", camera.name, "-f", "image2pipe", "-pix_fmt", "rgb24", - "-vcodec", "rawvideo", "-", + "-vcodec", "rawvideo", + "-", ) camera.cmd = cmd @@ -215,7 +228,7 @@ func initCamera(camera *Camera) error { func (camera *Camera) Read() bool { // If cmd is nil, video reading has not been initialized. if camera.cmd == nil { - if err := initCamera(camera); err != nil { + if err := camera.init(); err != nil { return false } } diff --git a/utils.go b/utils.go index 0edd6ae..dbcff7e 100644 --- a/utils.go +++ b/utils.go @@ -91,36 +91,6 @@ func parseFFprobe(input []byte) map[string]string { 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. func parse(data string) float64 { n, err := strconv.ParseFloat(data, 64) @@ -195,7 +165,6 @@ func parseDevices(buffer []byte) []string { return devices } -// Helper function. Array contains function. func contains(list []string, item string) bool { for _, i := range list { if i == item { @@ -205,45 +174,35 @@ func contains(list []string, item string) bool { return false } -// Parses the webcam metadata (width, height, fps, codec) from ffmpeg output. -func parseWebcamData(buffer []byte, camera *Camera) { - bufferstr := string(buffer) - index := strings.Index(bufferstr, "Stream #") - if index == -1 { - index++ +// 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 } - 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])) + if err := cmd.Start(); err != nil { + return nil, err } - // 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] + // Read list devices from Stdout. + buffer := make([]byte, 2<<10) + total := 0 + for { + n, err := pipe.Read(buffer[total:]) + total += n + 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 } diff --git a/video.go b/video.go index d4664f1..a3fac5b 100644 --- a/video.go +++ b/video.go @@ -6,6 +6,7 @@ import ( "os" "os/exec" "os/signal" + "strings" "syscall" ) @@ -107,7 +108,7 @@ func NewVideo(filename string) (*Video, error) { video := &Video{filename: filename, depth: 3} - addVideoData(videoData, video) + video.addVideoData(videoData) if audioCodec, ok := audioData["codec_name"]; ok { video.audioCodec = audioCodec } @@ -115,9 +116,39 @@ func NewVideo(filename string) (*Video, error) { 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, // 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. video.cleanup() // 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 { // If cmd is nil, video reading has not been initialized. if video.cmd == nil { - if err := initVideo(video); err != nil { + if err := video.init(); err != nil { return false } } diff --git a/videowriter.go b/videowriter.go index f776bc5..404fbff 100644 --- a/videowriter.go +++ b/videowriter.go @@ -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, // 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. writer.cleanup() // 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 { // If cmd is nil, video writing has not been set up. if writer.cmd == nil { - if err := initVideoWriter(writer); err != nil { + if err := writer.init(); err != nil { return err } } diff --git a/vidio_test.go b/vidio_test.go index a2c5c26..d9091d8 100644 --- a/vidio_test.go +++ b/vidio_test.go @@ -223,12 +223,11 @@ dummy: Immediate exit requested`, func TestWebcamParsing(t *testing.T) { camera := &Camera{} - err := getCameraData( + err := camera.getCameraData( `Input #0, dshow, from 'video=Integrated Camera': 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 At least one output file must be specified`, - camera, ) if err != nil {