Added error returns, removed panics
This commit is contained in:
parent
5de0d207f5
commit
3f4ae6eb23
7 changed files with 232 additions and 98 deletions
67
README.md
67
README.md
|
@ -15,6 +15,8 @@ go get github.com/AlexEidt/Vidio
|
||||||
The `Video` struct stores data about a video file you give it. The code below shows an example of sequentially reading the frames of the given video.
|
The `Video` struct stores data about a video file you give it. The code below shows an example of sequentially reading the frames of the given video.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
vidio.NewVideo() (*Video, error) // Create a new Video struct
|
||||||
|
|
||||||
FileName() string
|
FileName() string
|
||||||
Width() int
|
Width() int
|
||||||
Height() int
|
Height() int
|
||||||
|
@ -26,15 +28,21 @@ FPS() float64
|
||||||
Codec() string
|
Codec() string
|
||||||
AudioCodec() string
|
AudioCodec() string
|
||||||
FrameBuffer() []byte
|
FrameBuffer() []byte
|
||||||
|
|
||||||
|
Read() bool // Read a frame of video and store it in the frame buffer
|
||||||
|
Close()
|
||||||
```
|
```
|
||||||
|
|
||||||
```go
|
```go
|
||||||
video := vidio.NewVideo("input.mp4")
|
video, err := vidio.NewVideo("input.mp4")
|
||||||
|
// Error handling...
|
||||||
for video.Read() {
|
for video.Read() {
|
||||||
// "frame" stores the video frame as a flattened RGB image in row-major order
|
// "frame" stores the video frame as a flattened RGB image in row-major order
|
||||||
frame := video.FrameBuffer() // stored as: RGBRGBRGBRGB...
|
frame := video.FrameBuffer() // stored as: RGBRGBRGBRGB...
|
||||||
// Video processing here...
|
// Video processing here...
|
||||||
}
|
}
|
||||||
|
// If all frames have been read, "video" will be closed automatically.
|
||||||
|
// If not all frames are read, call "video.Close()" to close the video.
|
||||||
```
|
```
|
||||||
|
|
||||||
## `Camera`
|
## `Camera`
|
||||||
|
@ -42,6 +50,8 @@ for video.Read() {
|
||||||
The `Camera` can read from any cameras on the device running Vidio. It takes in the stream index. On most machines the webcam device has index 0. Note that audio retrieval from the microphone is not yet supported.
|
The `Camera` can read from any cameras on the device running Vidio. It takes in the stream index. On most machines the webcam device has index 0. Note that audio retrieval from the microphone is not yet supported.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
vidio.NewCamera(stream int) (*Camera, error) // Create a new Camera struct
|
||||||
|
|
||||||
Name() string
|
Name() string
|
||||||
Width() int
|
Width() int
|
||||||
Height() int
|
Height() int
|
||||||
|
@ -49,10 +59,14 @@ Depth() int
|
||||||
FPS() float64
|
FPS() float64
|
||||||
Codec() string
|
Codec() string
|
||||||
FrameBuffer() []byte
|
FrameBuffer() []byte
|
||||||
|
|
||||||
|
Read() bool // Read a frame of video and store it in the frame buffer
|
||||||
|
Close()
|
||||||
```
|
```
|
||||||
|
|
||||||
```go
|
```go
|
||||||
camera := vidio.NewCamera(0) // Get Webcam
|
camera, err := vidio.NewCamera(0) // Get Webcam
|
||||||
|
// Error handling...
|
||||||
defer camera.Close()
|
defer camera.Close()
|
||||||
|
|
||||||
// Stream the webcam
|
// Stream the webcam
|
||||||
|
@ -67,6 +81,8 @@ for camera.Read() {
|
||||||
The `VideoWriter` is used to write frames to a video file. The only required parameters are the output file name, the width and height of the frames being written, and an `Options` struct. This contains all the desired properties of the new video you want to create.
|
The `VideoWriter` is used to write frames to a video file. The only required parameters are the output file name, the width and height of the frames being written, and an `Options` struct. This contains all the desired properties of the new video you want to create.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
vidio.NewVideoWriter() (*VideoWriter, error) // Create a new VideoWriter struct
|
||||||
|
|
||||||
FileName() string
|
FileName() string
|
||||||
Width() int
|
Width() int
|
||||||
Height() int
|
Height() int
|
||||||
|
@ -78,6 +94,9 @@ FPS() float64
|
||||||
Quality() float64
|
Quality() float64
|
||||||
Codec() string
|
Codec() string
|
||||||
AudioCodec() string
|
AudioCodec() string
|
||||||
|
|
||||||
|
Write(frame []byte) error // Write a frame to the video file
|
||||||
|
Close()
|
||||||
```
|
```
|
||||||
|
|
||||||
```go
|
```go
|
||||||
|
@ -89,7 +108,7 @@ type Options struct {
|
||||||
FPS float64 // Frames per second. Default 25
|
FPS float64 // Frames per second. Default 25
|
||||||
Quality float64 // If bitrate not given, use quality instead. Must be between 0 and 1. 0:best, 1:worst
|
Quality float64 // If bitrate not given, use quality instead. Must be between 0 and 1. 0:best, 1:worst
|
||||||
Codec string // Codec for video. Default libx264
|
Codec string // Codec for video. Default libx264
|
||||||
Audio string // File path for audio for the video. If no audio, audio="".
|
Audio string // File path for audio for the video. If no audio, audio=""
|
||||||
AudioCodec string // Codec for audio. Default aac
|
AudioCodec string // Codec for audio. Default aac
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
@ -98,11 +117,13 @@ type Options struct {
|
||||||
w, h, c := 1920, 1080, 3
|
w, h, c := 1920, 1080, 3
|
||||||
options := vidio.Options{} // Will fill in defaults if empty
|
options := vidio.Options{} // Will fill in defaults if empty
|
||||||
|
|
||||||
writer := vidio.NewVideoWriter("output.mp4", w, h, &options)
|
writer, err := vidio.NewVideoWriter("output.mp4", w, h, &options)
|
||||||
|
// Error handling...
|
||||||
defer writer.Close()
|
defer writer.Close()
|
||||||
|
|
||||||
frame := make([]byte, w*h*c) // Create Frame as RGB Image and modify
|
frame := make([]byte, w*h*c) // Create Frame as RGB Image and modify
|
||||||
writer.Write(frame) // Write Frame to video
|
err := writer.Write(frame) // Write Frame to video
|
||||||
|
// Error handling...
|
||||||
```
|
```
|
||||||
|
|
||||||
## Images
|
## Images
|
||||||
|
@ -111,13 +132,15 @@ Vidio provides some convenience functions for reading and writing to images usin
|
||||||
|
|
||||||
```go
|
```go
|
||||||
// Read png image
|
// Read png image
|
||||||
w, h, img := vidio.Read("input.png")
|
w, h, img, err := vidio.Read("input.png")
|
||||||
|
// Error handling...
|
||||||
|
|
||||||
// w - width of image
|
// w - width of image
|
||||||
// h - height of image
|
// h - height of image
|
||||||
// img - byte array in RGB format. RGBRGBRGBRGB...
|
// img - byte array in RGB format. RGBRGBRGBRGB...
|
||||||
|
|
||||||
vidio.Write("output.jpg", w, h, img)
|
err := vidio.Write("output.jpg", w, h, img)
|
||||||
|
// Error handling...
|
||||||
```
|
```
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
@ -125,30 +148,35 @@ vidio.Write("output.jpg", w, h, img)
|
||||||
Copy `input.mp4` to `output.mp4`. Copy the audio from `input.mp4` to `output.mp4` as well.
|
Copy `input.mp4` to `output.mp4`. Copy the audio from `input.mp4` to `output.mp4` as well.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
video := vidio.NewVideo("input.mp4")
|
video, err := vidio.NewVideo("input.mp4")
|
||||||
|
// Error handling...
|
||||||
options := vidio.Options{
|
options := vidio.Options{
|
||||||
FPS: video.FPS(),
|
FPS: video.FPS(),
|
||||||
Bitrate: video.Bitrate(),
|
Bitrate: video.Bitrate(),
|
||||||
Audio: "input.mp4",
|
Audio: "input.mp4",
|
||||||
}
|
}
|
||||||
|
|
||||||
writer := vidio.NewVideoWriter("output.mp4", video.Width(), video.Height(), &options)
|
writer, err := vidio.NewVideoWriter("output.mp4", video.Width(), video.Height(), &options)
|
||||||
|
// Error handling...
|
||||||
defer writer.Close()
|
defer writer.Close()
|
||||||
|
|
||||||
for video.Read() {
|
for video.Read() {
|
||||||
writer.Write(video.FrameBuffer())
|
err := writer.Write(video.FrameBuffer())
|
||||||
|
// Error handling...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Grayscale 1000 frames of webcam stream and store in `output.mp4`.
|
Grayscale 1000 frames of webcam stream and store in `output.mp4`.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
webcam := vidio.NewCamera(0)
|
webcam, err := vidio.NewCamera(0)
|
||||||
|
// Error handling...
|
||||||
defer webcam.Close()
|
defer webcam.Close()
|
||||||
|
|
||||||
options := vidio.Options{FPS: webcam.FPS()}
|
options := vidio.Options{FPS: webcam.FPS()}
|
||||||
|
|
||||||
writer := vidio.NewVideoWriter("output.mp4", webcam.Width(), webcam.Height(), &options)
|
writer, err := vidio.NewVideoWriter("output.mp4", webcam.Width(), webcam.Height(), &options)
|
||||||
|
// Error handling...
|
||||||
defer writer.Close()
|
defer writer.Close()
|
||||||
|
|
||||||
count := 0
|
count := 0
|
||||||
|
@ -162,7 +190,8 @@ for webcam.Read() {
|
||||||
frame[i+1] = gray
|
frame[i+1] = gray
|
||||||
frame[i+2] = gray
|
frame[i+2] = gray
|
||||||
}
|
}
|
||||||
writer.Write(frame)
|
err := writer.Write(frame)
|
||||||
|
// Error handling...
|
||||||
count++
|
count++
|
||||||
if count > 1000 {
|
if count > 1000 {
|
||||||
break
|
break
|
||||||
|
@ -173,16 +202,20 @@ for webcam.Read() {
|
||||||
Create a gif from a series of `png` files enumerated from 1 to 10 that loops continuously with a final frame delay of 1000 centiseconds.
|
Create a gif from a series of `png` files enumerated from 1 to 10 that loops continuously with a final frame delay of 1000 centiseconds.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
w, h, _ := vidio.Read("1.png") // Get frame dimensions from first image
|
w, h, _, err := vidio.Read("1.png") // Get frame dimensions from first image
|
||||||
|
// Error handling...
|
||||||
|
|
||||||
options := vidio.Options{FPS: 1, Loop: 0, Delay: 1000}
|
options := vidio.Options{FPS: 1, Loop: 0, Delay: 1000}
|
||||||
|
|
||||||
gif := vidio.NewVideoWriter("output.gif", w, h, &options)
|
gif, err := vidio.NewVideoWriter("output.gif", w, h, &options)
|
||||||
|
// Error handling...
|
||||||
defer gif.Close()
|
defer gif.Close()
|
||||||
|
|
||||||
for i := 1; i <= 10; i++ {
|
for i := 1; i <= 10; i++ {
|
||||||
_, _, img := vidio.Read(strconv.Itoa(i)+".png")
|
_, _, img, err := vidio.Read(strconv.Itoa(i)+".png")
|
||||||
gif.Write(img)
|
// Error handling...
|
||||||
|
err := gif.Write(img)
|
||||||
|
// Error handling...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
71
camera.go
71
camera.go
|
@ -1,6 +1,7 @@
|
||||||
package vidio
|
package vidio
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
@ -52,7 +53,7 @@ func (camera *Camera) FrameBuffer() []byte {
|
||||||
|
|
||||||
// Returns the webcam device name.
|
// Returns the webcam device name.
|
||||||
// On windows, ffmpeg output from the -list_devices command is parsed to find the device name.
|
// On windows, ffmpeg output from the -list_devices command is parsed to find the device name.
|
||||||
func getDevicesWindows() []string {
|
func getDevicesWindows() ([]string, error) {
|
||||||
// Run command to get list of devices.
|
// Run command to get list of devices.
|
||||||
cmd := exec.Command(
|
cmd := exec.Command(
|
||||||
"ffmpeg",
|
"ffmpeg",
|
||||||
|
@ -63,10 +64,12 @@ func getDevicesWindows() []string {
|
||||||
)
|
)
|
||||||
pipe, err := cmd.StderrPipe()
|
pipe, err := cmd.StderrPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
pipe.Close()
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
panic(err)
|
cmd.Process.Kill()
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
// Read list devices from Stdout.
|
// Read list devices from Stdout.
|
||||||
buffer := make([]byte, 2<<10)
|
buffer := make([]byte, 2<<10)
|
||||||
|
@ -80,28 +83,34 @@ func getDevicesWindows() []string {
|
||||||
}
|
}
|
||||||
cmd.Wait()
|
cmd.Wait()
|
||||||
devices := parseDevices(buffer)
|
devices := parseDevices(buffer)
|
||||||
return devices
|
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) {
|
func getCameraData(device string, camera *Camera) 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()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
cmd := exec.Command(
|
cmd := exec.Command(
|
||||||
"ffmpeg",
|
"ffmpeg",
|
||||||
"-hide_banner",
|
"-hide_banner",
|
||||||
"-f", webcam(),
|
"-f", webcamDeviceName,
|
||||||
"-i", device,
|
"-i", device,
|
||||||
)
|
)
|
||||||
// The command will fail since we do not give a file to write to, therefore
|
// The command will fail since we do not give a file to write to, therefore
|
||||||
// it will write the meta data to Stderr.
|
// it will write the meta data to Stderr.
|
||||||
pipe, err := cmd.StderrPipe()
|
pipe, err := cmd.StderrPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
pipe.Close()
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
// Start the command.
|
// Start the command.
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
panic(err)
|
cmd.Process.Kill()
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
// Read ffmpeg output from Stdout.
|
// Read ffmpeg output from Stdout.
|
||||||
buffer := make([]byte, 2<<11)
|
buffer := make([]byte, 2<<11)
|
||||||
|
@ -117,51 +126,61 @@ func getCameraData(device string, camera *Camera) {
|
||||||
cmd.Wait()
|
cmd.Wait()
|
||||||
|
|
||||||
parseWebcamData(buffer[:total], camera)
|
parseWebcamData(buffer[:total], camera)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new camera struct that can read from the device with the given stream index.
|
// Creates a new camera struct that can read from the device with the given stream index.
|
||||||
func NewCamera(stream int) *Camera {
|
func NewCamera(stream int) (*Camera, error) {
|
||||||
// Check if ffmpeg is installed on the users machine.
|
// Check if ffmpeg is installed on the users machine.
|
||||||
checkExists("ffmpeg")
|
if err := checkExists("ffmpeg"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
var device string
|
var device string
|
||||||
switch runtime.GOOS {
|
switch runtime.GOOS {
|
||||||
case "linux":
|
case "linux":
|
||||||
device = "/dev/video" + strconv.Itoa(stream)
|
device = "/dev/video" + strconv.Itoa(stream)
|
||||||
break
|
|
||||||
case "darwin":
|
case "darwin":
|
||||||
device = strconv.Itoa(stream)
|
device = strconv.Itoa(stream)
|
||||||
break
|
|
||||||
case "windows":
|
case "windows":
|
||||||
// If OS is windows, we need to parse the listed devices to find which corresponds to the
|
// If OS is windows, we need to parse the listed devices to find which corresponds to the
|
||||||
// given "stream" index.
|
// given "stream" index.
|
||||||
devices := getDevicesWindows()
|
devices, err := getDevicesWindows()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
if stream >= len(devices) {
|
if stream >= len(devices) {
|
||||||
panic("Could not find devices with index: " + strconv.Itoa(stream))
|
return nil, errors.New("Could not find device with index: " + strconv.Itoa(stream))
|
||||||
}
|
}
|
||||||
device = "video=" + devices[stream]
|
device = "video=" + devices[stream]
|
||||||
break
|
|
||||||
default:
|
default:
|
||||||
panic("Unsupported OS: " + runtime.GOOS)
|
return nil, errors.New("Unsupported OS: " + runtime.GOOS)
|
||||||
}
|
}
|
||||||
|
|
||||||
camera := Camera{name: device, depth: 3}
|
camera := Camera{name: device, depth: 3}
|
||||||
getCameraData(device, &camera)
|
if err := getCameraData(device, &camera); err != nil {
|
||||||
return &camera
|
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) {
|
func initCamera(camera *Camera) error {
|
||||||
// If user exits with Ctrl+C, stop ffmpeg process.
|
// If user exits with Ctrl+C, stop ffmpeg process.
|
||||||
camera.cleanup()
|
camera.cleanup()
|
||||||
|
|
||||||
|
webcamDeviceName, err := webcam()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Use ffmpeg to pipe webcam to stdout.
|
// Use ffmpeg to pipe webcam to stdout.
|
||||||
cmd := exec.Command(
|
cmd := exec.Command(
|
||||||
"ffmpeg",
|
"ffmpeg",
|
||||||
"-hide_banner",
|
"-hide_banner",
|
||||||
"-loglevel", "quiet",
|
"-loglevel", "quiet",
|
||||||
"-f", webcam(),
|
"-f", webcamDeviceName,
|
||||||
"-i", camera.name,
|
"-i", camera.name,
|
||||||
"-f", "image2pipe",
|
"-f", "image2pipe",
|
||||||
"-pix_fmt", "rgb24",
|
"-pix_fmt", "rgb24",
|
||||||
|
@ -171,21 +190,27 @@ func initCamera(camera *Camera) {
|
||||||
camera.cmd = cmd
|
camera.cmd = cmd
|
||||||
pipe, err := cmd.StdoutPipe()
|
pipe, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
pipe.Close()
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
camera.pipe = &pipe
|
camera.pipe = &pipe
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
panic(err)
|
cmd.Process.Kill()
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
camera.framebuffer = make([]byte, camera.width*camera.height*camera.depth)
|
camera.framebuffer = make([]byte, camera.width*camera.height*camera.depth)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reads the next frame from the webcam and stores in the framebuffer.
|
// Reads the next frame from the webcam and stores in the framebuffer.
|
||||||
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 {
|
||||||
initCamera(camera)
|
if err := initCamera(camera); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
total := 0
|
total := 0
|
||||||
for total < camera.width*camera.height*camera.depth {
|
for total < camera.width*camera.height*camera.depth {
|
||||||
|
|
17
imageio.go
17
imageio.go
|
@ -13,15 +13,15 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
// Reads an image from a file. Currently only supports png and jpeg.
|
// Reads an image from a file. Currently only supports png and jpeg.
|
||||||
func Read(filename string) (int, int, []byte) {
|
func Read(filename string) (int, int, []byte, error) {
|
||||||
f, err := os.Open(filename)
|
f, err := os.Open(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return 0, 0, nil, err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
image, _, err := image.Decode(f)
|
image, _, err := image.Decode(f)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return 0, 0, nil, err
|
||||||
}
|
}
|
||||||
bounds := image.Bounds().Max
|
bounds := image.Bounds().Max
|
||||||
data := make([]byte, bounds.Y*bounds.X*3)
|
data := make([]byte, bounds.Y*bounds.X*3)
|
||||||
|
@ -38,14 +38,14 @@ func Read(filename string) (int, int, []byte) {
|
||||||
index++
|
index++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return bounds.X, bounds.Y, data
|
return bounds.X, bounds.Y, data, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Writes an image to a file. Currently only supports png and jpeg.
|
// Writes an image to a file. Currently only supports png and jpeg.
|
||||||
func Write(filename string, width, height int, data []byte) {
|
func Write(filename string, width, height int, data []byte) error {
|
||||||
f, err := os.Create(filename)
|
f, err := os.Create(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
return err
|
||||||
}
|
}
|
||||||
defer f.Close()
|
defer f.Close()
|
||||||
image := image.NewRGBA(image.Rect(0, 0, width, height))
|
image := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||||
|
@ -59,11 +59,12 @@ func Write(filename string, width, height int, data []byte) {
|
||||||
}
|
}
|
||||||
if strings.HasSuffix(filename, ".png") {
|
if strings.HasSuffix(filename, ".png") {
|
||||||
if err := png.Encode(f, image); err != nil {
|
if err := png.Encode(f, image); err != nil {
|
||||||
panic(err)
|
return err
|
||||||
}
|
}
|
||||||
} else if strings.HasSuffix(filename, ".jpg") || strings.HasSuffix(filename, ".jpeg") {
|
} else if strings.HasSuffix(filename, ".jpg") || strings.HasSuffix(filename, ".jpeg") {
|
||||||
if err := jpeg.Encode(f, image, nil); err != nil {
|
if err := jpeg.Encode(f, image, nil); err != nil {
|
||||||
panic(err)
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
32
utils.go
32
utils.go
|
@ -25,18 +25,21 @@ func exists(filename string) bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks if the given program is installed.
|
// Checks if the given program is installed.
|
||||||
func checkExists(program string) {
|
func checkExists(program string) error {
|
||||||
cmd := exec.Command(program, "-version")
|
cmd := exec.Command(program, "-version")
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
panic(program + " is not installed.")
|
cmd.Process.Kill()
|
||||||
|
return errors.New(program + " is not installed.")
|
||||||
}
|
}
|
||||||
if err := cmd.Wait(); err != nil {
|
if err := cmd.Wait(); err != nil {
|
||||||
panic(program + " is not installed.")
|
cmd.Process.Kill()
|
||||||
|
return errors.New(program + " is not installed.")
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
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(
|
||||||
|
@ -50,11 +53,13 @@ func ffprobe(filename, stype string) map[string]string {
|
||||||
|
|
||||||
pipe, err := cmd.StdoutPipe()
|
pipe, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
pipe.Close()
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
panic(err)
|
cmd.Process.Kill()
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
// Read ffprobe output from Stdout.
|
// Read ffprobe output from Stdout.
|
||||||
buffer := make([]byte, 2<<10)
|
buffer := make([]byte, 2<<10)
|
||||||
|
@ -68,10 +73,11 @@ func ffprobe(filename, stype string) map[string]string {
|
||||||
}
|
}
|
||||||
// Wait for ffprobe command to complete.
|
// Wait for ffprobe command to complete.
|
||||||
if err := cmd.Wait(); err != nil {
|
if err := cmd.Wait(); err != nil {
|
||||||
panic(err)
|
cmd.Process.Kill()
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseFFprobe(buffer[:total])
|
return parseFFprobe(buffer[:total]), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse ffprobe output to fill in video data.
|
// Parse ffprobe output to fill in video data.
|
||||||
|
@ -131,17 +137,17 @@ func parse(data string) float64 {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Returns the webcam name used for the -f option with ffmpeg.
|
// Returns the webcam name used for the -f option with ffmpeg.
|
||||||
func webcam() string {
|
func webcam() (string, error) {
|
||||||
os := runtime.GOOS
|
os := runtime.GOOS
|
||||||
switch os {
|
switch os {
|
||||||
case "linux":
|
case "linux":
|
||||||
return "v4l2"
|
return "v4l2", nil
|
||||||
case "darwin":
|
case "darwin":
|
||||||
return "avfoundation" // qtkit
|
return "avfoundation", nil // qtkit
|
||||||
case "windows":
|
case "windows":
|
||||||
return "dshow" // vfwcap
|
return "dshow", nil // vfwcap
|
||||||
default:
|
default:
|
||||||
panic("Unsupported OS: " + os)
|
return "", errors.New("Unsupported OS: " + os)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
39
video.go
39
video.go
|
@ -1,6 +1,7 @@
|
||||||
package vidio
|
package vidio
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
@ -74,16 +75,26 @@ func (video *Video) FrameBuffer() []byte {
|
||||||
|
|
||||||
// Creates a new Video struct.
|
// Creates a new Video struct.
|
||||||
// Uses ffprobe to get video information and fills in the Video struct with this data.
|
// Uses ffprobe to get video information and fills in the Video struct with this data.
|
||||||
func NewVideo(filename string) *Video {
|
func NewVideo(filename string) (*Video, error) {
|
||||||
if !exists(filename) {
|
if !exists(filename) {
|
||||||
panic("Video file " + filename + " does not exist")
|
return nil, errors.New("Video file " + filename + " does not exist")
|
||||||
}
|
}
|
||||||
// Check if ffmpeg and ffprobe are installed on the users machine.
|
// Check if ffmpeg and ffprobe are installed on the users machine.
|
||||||
checkExists("ffmpeg")
|
if err := checkExists("ffmpeg"); err != nil {
|
||||||
checkExists("ffprobe")
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := checkExists("ffprobe"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
videoData := ffprobe(filename, "v")
|
videoData, err := ffprobe(filename, "v")
|
||||||
audioData := ffprobe(filename, "a")
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
audioData, err := ffprobe(filename, "a")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
video := &Video{filename: filename, depth: 3}
|
video := &Video{filename: filename, depth: 3}
|
||||||
|
|
||||||
|
@ -92,12 +103,12 @@ func NewVideo(filename string) *Video {
|
||||||
video.audioCodec = audioCodec
|
video.audioCodec = audioCodec
|
||||||
}
|
}
|
||||||
|
|
||||||
return video
|
return video, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
func initVideo(video *Video) 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.
|
||||||
|
@ -113,13 +124,17 @@ func initVideo(video *Video) {
|
||||||
video.cmd = cmd
|
video.cmd = cmd
|
||||||
pipe, err := cmd.StdoutPipe()
|
pipe, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
pipe.Close()
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
video.pipe = &pipe
|
video.pipe = &pipe
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
panic(err)
|
cmd.Process.Kill()
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
video.framebuffer = make([]byte, video.width*video.height*video.depth)
|
video.framebuffer = make([]byte, video.width*video.height*video.depth)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reads the next frame from the video and stores in the framebuffer.
|
// Reads the next frame from the video and stores in the framebuffer.
|
||||||
|
@ -127,7 +142,9 @@ func initVideo(video *Video) {
|
||||||
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 {
|
||||||
initVideo(video)
|
if err := initVideo(video); err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
total := 0
|
total := 0
|
||||||
for total < video.width*video.height*video.depth {
|
for total < video.width*video.height*video.depth {
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package vidio
|
package vidio
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
|
@ -86,9 +87,11 @@ func (writer *VideoWriter) AudioCodec() string {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creates a new VideoWriter struct with default values from the Options struct.
|
// Creates a new VideoWriter struct with default values from the Options struct.
|
||||||
func NewVideoWriter(filename string, width, height int, options *Options) *VideoWriter {
|
func NewVideoWriter(filename string, width, height int, options *Options) (*VideoWriter, error) {
|
||||||
// Check if ffmpeg is installed on the users machine.
|
// Check if ffmpeg is installed on the users machine.
|
||||||
checkExists("ffmpeg")
|
if err := checkExists("ffmpeg"); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
writer := VideoWriter{filename: filename}
|
writer := VideoWriter{filename: filename}
|
||||||
|
|
||||||
|
@ -139,11 +142,14 @@ func NewVideoWriter(filename string, width, height int, options *Options) *Video
|
||||||
|
|
||||||
if options.Audio != "" {
|
if options.Audio != "" {
|
||||||
if !exists(options.Audio) {
|
if !exists(options.Audio) {
|
||||||
panic("Audio file " + options.Audio + " does not exist.")
|
return nil, errors.New("Audio file " + options.Audio + " does not exist.")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(ffprobe(options.Audio, "a")) == 0 {
|
audioData, err := ffprobe(options.Audio, "a")
|
||||||
panic("Given \"audio\" file " + options.Audio + " has no audio.")
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if len(audioData) == 0 {
|
||||||
|
return nil, errors.New("Given \"audio\" file " + options.Audio + " has no audio.")
|
||||||
}
|
}
|
||||||
|
|
||||||
writer.audio = options.Audio
|
writer.audio = options.Audio
|
||||||
|
@ -155,12 +161,12 @@ func NewVideoWriter(filename string, width, height int, options *Options) *Video
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &writer
|
return &writer, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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) {
|
func initVideoWriter(writer *VideoWriter) 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.
|
||||||
|
@ -252,29 +258,35 @@ func initVideoWriter(writer *VideoWriter) {
|
||||||
|
|
||||||
pipe, err := cmd.StdinPipe()
|
pipe, err := cmd.StdinPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
pipe.Close()
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
writer.pipe = &pipe
|
writer.pipe = &pipe
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
panic(err)
|
cmd.Process.Kill()
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Writes the given frame to the video file.
|
// Writes the given frame to the video file.
|
||||||
func (writer *VideoWriter) Write(frame []byte) {
|
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 {
|
||||||
initVideoWriter(writer)
|
if err := initVideoWriter(writer); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
total := 0
|
total := 0
|
||||||
for total < len(frame) {
|
for total < len(frame) {
|
||||||
n, err := (*writer.pipe).Write(frame[total:])
|
n, err := (*writer.pipe).Write(frame[total:])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Likely cause is invalid parameters to ffmpeg.")
|
return err
|
||||||
panic(err)
|
|
||||||
}
|
}
|
||||||
total += n
|
total += n
|
||||||
}
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Closes the pipe and stops the ffmpeg process.
|
// Closes the pipe and stops the ffmpeg process.
|
||||||
|
|
|
@ -13,7 +13,10 @@ func assertEquals(expected, actual interface{}) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestVideoMetaData(t *testing.T) {
|
func TestVideoMetaData(t *testing.T) {
|
||||||
video := NewVideo("test/koala.mp4")
|
video, err := NewVideo("test/koala.mp4")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
defer video.Close()
|
defer video.Close()
|
||||||
|
|
||||||
assertEquals(video.filename, "test/koala.mp4")
|
assertEquals(video.filename, "test/koala.mp4")
|
||||||
|
@ -40,7 +43,10 @@ func TestVideoMetaData(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestVideoFrame(t *testing.T) {
|
func TestVideoFrame(t *testing.T) {
|
||||||
video := NewVideo("test/koala.mp4")
|
video, err := NewVideo("test/koala.mp4")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
defer video.Close()
|
defer video.Close()
|
||||||
|
|
||||||
video.Read()
|
video.Read()
|
||||||
|
@ -61,7 +67,10 @@ func TestVideoFrame(t *testing.T) {
|
||||||
|
|
||||||
func TestVideoWriting(t *testing.T) {
|
func TestVideoWriting(t *testing.T) {
|
||||||
testWriting := func(input, output string, audio bool) {
|
testWriting := func(input, output string, audio bool) {
|
||||||
video := NewVideo(input)
|
video, err := NewVideo(input)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
options := Options{
|
options := Options{
|
||||||
FPS: video.FPS(),
|
FPS: video.FPS(),
|
||||||
Bitrate: video.Bitrate(),
|
Bitrate: video.Bitrate(),
|
||||||
|
@ -71,9 +80,15 @@ func TestVideoWriting(t *testing.T) {
|
||||||
options.Audio = input
|
options.Audio = input
|
||||||
}
|
}
|
||||||
|
|
||||||
writer := NewVideoWriter(output, video.width, video.height, &options)
|
writer, err := NewVideoWriter(output, video.width, video.height, &options)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
for video.Read() {
|
for video.Read() {
|
||||||
writer.Write(video.FrameBuffer())
|
err := writer.Write(video.FrameBuffer())
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
writer.Close()
|
writer.Close()
|
||||||
|
|
||||||
|
@ -87,11 +102,17 @@ func TestVideoWriting(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCameraIO(t *testing.T) {
|
func TestCameraIO(t *testing.T) {
|
||||||
webcam := NewCamera(0)
|
webcam, err := NewCamera(0)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
options := Options{FPS: webcam.FPS()}
|
options := Options{FPS: webcam.FPS()}
|
||||||
|
|
||||||
writer := NewVideoWriter("test/camera.mp4", webcam.width, webcam.height, &options)
|
writer, err := NewVideoWriter("test/camera.mp4", webcam.width, webcam.height, &options)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
count := 0
|
count := 0
|
||||||
for webcam.Read() {
|
for webcam.Read() {
|
||||||
|
@ -104,7 +125,10 @@ func TestCameraIO(t *testing.T) {
|
||||||
frame[i+1] = gray
|
frame[i+1] = gray
|
||||||
frame[i+2] = gray
|
frame[i+2] = gray
|
||||||
}
|
}
|
||||||
writer.Write(frame)
|
err := writer.Write(frame)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
count++
|
count++
|
||||||
if count > 100 {
|
if count > 100 {
|
||||||
break
|
break
|
||||||
|
@ -119,22 +143,34 @@ func TestCameraIO(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFFprobe(t *testing.T) {
|
func TestFFprobe(t *testing.T) {
|
||||||
koalaVideo := ffprobe("test/koala.mp4", "v")
|
koalaVideo, err := ffprobe("test/koala.mp4", "v")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
assertEquals(koalaVideo["width"], "480")
|
assertEquals(koalaVideo["width"], "480")
|
||||||
assertEquals(koalaVideo["height"], "270")
|
assertEquals(koalaVideo["height"], "270")
|
||||||
assertEquals(koalaVideo["duration"], "3.366667")
|
assertEquals(koalaVideo["duration"], "3.366667")
|
||||||
assertEquals(koalaVideo["bit_rate"], "170549")
|
assertEquals(koalaVideo["bit_rate"], "170549")
|
||||||
assertEquals(koalaVideo["codec_name"], "h264")
|
assertEquals(koalaVideo["codec_name"], "h264")
|
||||||
koalaAudio := ffprobe("test/koala.mp4", "a")
|
koalaAudio, err := ffprobe("test/koala.mp4", "a")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
assertEquals(koalaAudio["codec_name"], "aac")
|
assertEquals(koalaAudio["codec_name"], "aac")
|
||||||
|
|
||||||
koalaVideo = ffprobe("test/koala-noaudio.mp4", "v")
|
koalaVideo, err = ffprobe("test/koala-noaudio.mp4", "v")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
assertEquals(koalaVideo["width"], "480")
|
assertEquals(koalaVideo["width"], "480")
|
||||||
assertEquals(koalaVideo["height"], "270")
|
assertEquals(koalaVideo["height"], "270")
|
||||||
assertEquals(koalaVideo["duration"], "3.366667")
|
assertEquals(koalaVideo["duration"], "3.366667")
|
||||||
assertEquals(koalaVideo["bit_rate"], "170549")
|
assertEquals(koalaVideo["bit_rate"], "170549")
|
||||||
assertEquals(koalaVideo["codec_name"], "h264")
|
assertEquals(koalaVideo["codec_name"], "h264")
|
||||||
koalaAudio = ffprobe("test/koala-noaudio.mp4", "a")
|
koalaAudio, err = ffprobe("test/koala-noaudio.mp4", "a")
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
assertEquals(len(koalaAudio), 0)
|
assertEquals(len(koalaAudio), 0)
|
||||||
|
|
||||||
fmt.Println("FFprobe Test Passed")
|
fmt.Println("FFprobe Test Passed")
|
||||||
|
@ -171,7 +207,7 @@ dummy: Immediate exit requested`,
|
||||||
|
|
||||||
func TestWebcamParsing(t *testing.T) {
|
func TestWebcamParsing(t *testing.T) {
|
||||||
camera := &Camera{}
|
camera := &Camera{}
|
||||||
getCameraData(
|
err := 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
|
||||||
|
@ -179,6 +215,10 @@ At least one output file must be specified`,
|
||||||
camera,
|
camera,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
assertEquals(camera.width, 1280)
|
assertEquals(camera.width, 1280)
|
||||||
assertEquals(camera.height, 720)
|
assertEquals(camera.height, 720)
|
||||||
assertEquals(camera.fps, float64(30))
|
assertEquals(camera.fps, float64(30))
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue