Added error returns, removed panics

This commit is contained in:
Alex Eidt 2022-04-17 09:11:38 -07:00
parent 5de0d207f5
commit 3f4ae6eb23
7 changed files with 232 additions and 98 deletions

View file

@ -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...
} }
``` ```

View file

@ -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 {

View file

@ -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
} }

View file

@ -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)
} }
} }

View file

@ -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 {

View file

@ -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.

View file

@ -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))