267 lines
6.1 KiB
Go
267 lines
6.1 KiB
Go
package vidio
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"os/exec"
|
|
"os/signal"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
"syscall"
|
|
)
|
|
|
|
type Camera struct {
|
|
name string // Camera device name.
|
|
width int // Camera frame width.
|
|
height int // Camera frame height.
|
|
depth int // Camera frame depth.
|
|
fps float64 // Camera frame rate.
|
|
codec string // Camera codec.
|
|
framebuffer []byte // Raw frame data.
|
|
pipe *io.ReadCloser // Stdout pipe for ffmpeg process streaming webcam.
|
|
cmd *exec.Cmd // ffmpeg command.
|
|
}
|
|
|
|
func (camera *Camera) Name() string {
|
|
return camera.name
|
|
}
|
|
|
|
func (camera *Camera) Width() int {
|
|
return camera.width
|
|
}
|
|
|
|
func (camera *Camera) Height() int {
|
|
return camera.height
|
|
}
|
|
|
|
func (camera *Camera) Depth() int {
|
|
return camera.depth
|
|
}
|
|
|
|
func (camera *Camera) FPS() float64 {
|
|
return camera.fps
|
|
}
|
|
|
|
func (camera *Camera) Codec() string {
|
|
return camera.codec
|
|
}
|
|
|
|
func (camera *Camera) FrameBuffer() []byte {
|
|
return camera.framebuffer
|
|
}
|
|
|
|
func (camera *Camera) SetFrameBuffer(buffer []byte) error {
|
|
size := camera.width * camera.height * camera.depth
|
|
if len(buffer) < size {
|
|
return fmt.Errorf("buffer size %d is smaller than frame size %d", len(buffer), size)
|
|
}
|
|
|
|
camera.framebuffer = buffer
|
|
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 = fmt.Sprintf("/dev/video%d", stream)
|
|
case "darwin":
|
|
device = fmt.Sprintf(`"%d"`, 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 < 0 || stream >= len(devices) {
|
|
return nil, fmt.Errorf("could not find device with index: %d", stream)
|
|
}
|
|
device = fmt.Sprintf("video=%s", 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)
|
|
}
|
|
}
|
|
|
|
// Get camera meta data such as width, height, fps and codec.
|
|
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()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
cmd := exec.Command(
|
|
"ffmpeg",
|
|
"-hide_banner",
|
|
"-f", webcamDeviceName,
|
|
"-i", device,
|
|
)
|
|
// The command will fail since we do not give a file to write to, therefore
|
|
// it will write the meta data to Stderr.
|
|
pipe, err := cmd.StderrPipe()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Start the command.
|
|
if err := cmd.Start(); err != nil {
|
|
return err
|
|
}
|
|
// Read ffmpeg output from Stdout.
|
|
buffer := make([]byte, 2<<11)
|
|
total := 0
|
|
for {
|
|
n, err := pipe.Read(buffer[total:])
|
|
total += n
|
|
if err == io.EOF {
|
|
break
|
|
}
|
|
}
|
|
// Wait for the command to finish.
|
|
cmd.Wait()
|
|
|
|
camera.parseWebcamData(buffer[:total])
|
|
return 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 (camera *Camera) init() error {
|
|
// If user exits with Ctrl+C, stop ffmpeg process.
|
|
camera.cleanup()
|
|
|
|
webcamDeviceName, err := webcam()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Use ffmpeg to pipe webcam to stdout.
|
|
cmd := exec.Command(
|
|
"ffmpeg",
|
|
"-hide_banner",
|
|
"-loglevel", "quiet",
|
|
"-f", webcamDeviceName,
|
|
"-i", camera.name,
|
|
"-f", "image2pipe",
|
|
"-pix_fmt", "rgb24",
|
|
"-vcodec", "rawvideo",
|
|
"-",
|
|
)
|
|
|
|
camera.cmd = cmd
|
|
pipe, err := cmd.StdoutPipe()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
camera.pipe = &pipe
|
|
if err := cmd.Start(); err != nil {
|
|
return err
|
|
}
|
|
|
|
if camera.framebuffer == nil {
|
|
camera.framebuffer = make([]byte, camera.width*camera.height*camera.depth)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Reads the next frame from the webcam and stores in the framebuffer.
|
|
func (camera *Camera) Read() bool {
|
|
// If cmd is nil, video reading has not been initialized.
|
|
if camera.cmd == nil {
|
|
if err := camera.init(); err != nil {
|
|
return false
|
|
}
|
|
}
|
|
total := 0
|
|
for total < camera.width*camera.height*camera.depth {
|
|
n, _ := (*camera.pipe).Read(camera.framebuffer[total:])
|
|
total += n
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Closes the pipe and stops the ffmpeg process.
|
|
func (camera *Camera) Close() {
|
|
if camera.pipe != nil {
|
|
(*camera.pipe).Close()
|
|
}
|
|
if camera.cmd != nil {
|
|
camera.cmd.Process.Kill()
|
|
}
|
|
}
|
|
|
|
// Stops the "cmd" process running when the user presses Ctrl+C.
|
|
// https://stackoverflow.com/questions/11268943/is-it-possible-to-capture-a-ctrlc-signal-and-run-a-cleanup-function-in-a-defe.
|
|
func (camera *Camera) cleanup() {
|
|
c := make(chan os.Signal, 1)
|
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
|
go func() {
|
|
<-c
|
|
if camera.pipe != nil {
|
|
(*camera.pipe).Close()
|
|
}
|
|
if camera.cmd != nil {
|
|
camera.cmd.Process.Kill()
|
|
}
|
|
os.Exit(1)
|
|
}()
|
|
}
|