From 3f4ae6eb231abe8462fbfaa08ac66ca29f249957 Mon Sep 17 00:00:00 2001 From: Alex Eidt Date: Sun, 17 Apr 2022 09:11:38 -0700 Subject: [PATCH] Added error returns, removed panics --- README.md | 67 +++++++++++++++++++++++++++++++++++------------ camera.go | 71 ++++++++++++++++++++++++++++++++++---------------- imageio.go | 17 ++++++------ utils.go | 32 ++++++++++++++--------- video.go | 39 +++++++++++++++++++-------- videowriter.go | 38 ++++++++++++++++++--------- vidio_test.go | 66 +++++++++++++++++++++++++++++++++++++--------- 7 files changed, 232 insertions(+), 98 deletions(-) diff --git a/README.md b/README.md index 7ffb290..2281d9f 100644 --- a/README.md +++ b/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. ```go +vidio.NewVideo() (*Video, error) // Create a new Video struct + FileName() string Width() int Height() int @@ -26,15 +28,21 @@ FPS() float64 Codec() string AudioCodec() string FrameBuffer() []byte + +Read() bool // Read a frame of video and store it in the frame buffer +Close() ``` ```go -video := vidio.NewVideo("input.mp4") +video, err := vidio.NewVideo("input.mp4") +// Error handling... for video.Read() { // "frame" stores the video frame as a flattened RGB image in row-major order frame := video.FrameBuffer() // stored as: RGBRGBRGBRGB... // 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` @@ -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. ```go +vidio.NewCamera(stream int) (*Camera, error) // Create a new Camera struct + Name() string Width() int Height() int @@ -49,10 +59,14 @@ Depth() int FPS() float64 Codec() string FrameBuffer() []byte + +Read() bool // Read a frame of video and store it in the frame buffer +Close() ``` ```go -camera := vidio.NewCamera(0) // Get Webcam +camera, err := vidio.NewCamera(0) // Get Webcam +// Error handling... defer camera.Close() // 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. ```go +vidio.NewVideoWriter() (*VideoWriter, error) // Create a new VideoWriter struct + FileName() string Width() int Height() int @@ -78,6 +94,9 @@ FPS() float64 Quality() float64 Codec() string AudioCodec() string + +Write(frame []byte) error // Write a frame to the video file +Close() ``` ```go @@ -89,7 +108,7 @@ type Options struct { 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 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 } ``` @@ -98,11 +117,13 @@ type Options struct { w, h, c := 1920, 1080, 3 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() 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 @@ -111,13 +132,15 @@ Vidio provides some convenience functions for reading and writing to images usin ```go // Read png image -w, h, img := vidio.Read("input.png") +w, h, img, err := vidio.Read("input.png") +// Error handling... // w - width of image // h - height of image // 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 @@ -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. ```go -video := vidio.NewVideo("input.mp4") +video, err := vidio.NewVideo("input.mp4") +// Error handling... options := vidio.Options{ FPS: video.FPS(), Bitrate: video.Bitrate(), 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() 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`. ```go -webcam := vidio.NewCamera(0) +webcam, err := vidio.NewCamera(0) +// Error handling... defer webcam.Close() 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() count := 0 @@ -162,7 +190,8 @@ for webcam.Read() { frame[i+1] = gray frame[i+2] = gray } - writer.Write(frame) + err := writer.Write(frame) + // Error handling... count++ if count > 1000 { 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. ```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} -gif := vidio.NewVideoWriter("output.gif", w, h, &options) +gif, err := vidio.NewVideoWriter("output.gif", w, h, &options) +// Error handling... defer gif.Close() for i := 1; i <= 10; i++ { - _, _, img := vidio.Read(strconv.Itoa(i)+".png") - gif.Write(img) + _, _, img, err := vidio.Read(strconv.Itoa(i)+".png") + // Error handling... + err := gif.Write(img) + // Error handling... } ``` diff --git a/camera.go b/camera.go index 74dcf2a..f2e4d09 100644 --- a/camera.go +++ b/camera.go @@ -1,6 +1,7 @@ package vidio import ( + "errors" "io" "os" "os/exec" @@ -52,7 +53,7 @@ func (camera *Camera) FrameBuffer() []byte { // Returns the webcam 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. cmd := exec.Command( "ffmpeg", @@ -63,10 +64,12 @@ func getDevicesWindows() []string { ) pipe, err := cmd.StderrPipe() if err != nil { - panic(err) + pipe.Close() + return nil, err } if err := cmd.Start(); err != nil { - panic(err) + cmd.Process.Kill() + return nil, err } // Read list devices from Stdout. buffer := make([]byte, 2<<10) @@ -80,28 +83,34 @@ func getDevicesWindows() []string { } cmd.Wait() devices := parseDevices(buffer) - return devices + return devices, nil } // 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. // 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", webcam(), + "-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 { - panic(err) + pipe.Close() + return err } // Start the command. if err := cmd.Start(); err != nil { - panic(err) + cmd.Process.Kill() + return err } // Read ffmpeg output from Stdout. buffer := make([]byte, 2<<11) @@ -117,51 +126,61 @@ func getCameraData(device string, camera *Camera) { cmd.Wait() parseWebcamData(buffer[:total], camera) + return nil } // 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. - checkExists("ffmpeg") + if err := checkExists("ffmpeg"); err != nil { + return nil, err + } var device string switch runtime.GOOS { case "linux": device = "/dev/video" + strconv.Itoa(stream) - break case "darwin": device = strconv.Itoa(stream) - break case "windows": // If OS is windows, we need to parse the listed devices to find which corresponds to the // given "stream" index. - devices := getDevicesWindows() + devices, err := getDevicesWindows() + if err != nil { + return nil, err + } 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] - break default: - panic("Unsupported OS: " + runtime.GOOS) + return nil, errors.New("Unsupported OS: " + runtime.GOOS) } camera := Camera{name: device, depth: 3} - getCameraData(device, &camera) - return &camera + 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) { +func initCamera(camera *Camera) 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", webcam(), + "-f", webcamDeviceName, "-i", camera.name, "-f", "image2pipe", "-pix_fmt", "rgb24", @@ -171,21 +190,27 @@ func initCamera(camera *Camera) { camera.cmd = cmd pipe, err := cmd.StdoutPipe() if err != nil { - panic(err) + pipe.Close() + return err } camera.pipe = &pipe if err := cmd.Start(); err != nil { - panic(err) + cmd.Process.Kill() + return err } + 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 { - initCamera(camera) + if err := initCamera(camera); err != nil { + return false + } } total := 0 for total < camera.width*camera.height*camera.depth { diff --git a/imageio.go b/imageio.go index cc2d45c..d7ceccd 100644 --- a/imageio.go +++ b/imageio.go @@ -13,15 +13,15 @@ import ( ) // 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) if err != nil { - panic(err) + return 0, 0, nil, err } defer f.Close() image, _, err := image.Decode(f) if err != nil { - panic(err) + return 0, 0, nil, err } bounds := image.Bounds().Max data := make([]byte, bounds.Y*bounds.X*3) @@ -38,14 +38,14 @@ func Read(filename string) (int, int, []byte) { 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. -func Write(filename string, width, height int, data []byte) { +func Write(filename string, width, height int, data []byte) error { f, err := os.Create(filename) if err != nil { - panic(err) + return err } defer f.Close() 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 err := png.Encode(f, image); err != nil { - panic(err) + return err } } else if strings.HasSuffix(filename, ".jpg") || strings.HasSuffix(filename, ".jpeg") { if err := jpeg.Encode(f, image, nil); err != nil { - panic(err) + return err } } + return nil } diff --git a/utils.go b/utils.go index 1d366dc..da4a1ab 100644 --- a/utils.go +++ b/utils.go @@ -25,18 +25,21 @@ func exists(filename string) bool { } // Checks if the given program is installed. -func checkExists(program string) { +func checkExists(program string) error { cmd := exec.Command(program, "-version") 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 { - 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. -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. // Extract video information with ffprobe. cmd := exec.Command( @@ -50,11 +53,13 @@ func ffprobe(filename, stype string) map[string]string { pipe, err := cmd.StdoutPipe() if err != nil { - panic(err) + pipe.Close() + return nil, err } if err := cmd.Start(); err != nil { - panic(err) + cmd.Process.Kill() + return nil, err } // Read ffprobe output from Stdout. buffer := make([]byte, 2<<10) @@ -68,10 +73,11 @@ func ffprobe(filename, stype string) map[string]string { } // Wait for ffprobe command to complete. 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. @@ -131,17 +137,17 @@ func parse(data string) float64 { } // Returns the webcam name used for the -f option with ffmpeg. -func webcam() string { +func webcam() (string, error) { os := runtime.GOOS switch os { case "linux": - return "v4l2" + return "v4l2", nil case "darwin": - return "avfoundation" // qtkit + return "avfoundation", nil // qtkit case "windows": - return "dshow" // vfwcap + return "dshow", nil // vfwcap default: - panic("Unsupported OS: " + os) + return "", errors.New("Unsupported OS: " + os) } } diff --git a/video.go b/video.go index 54f833c..0b9f2ed 100644 --- a/video.go +++ b/video.go @@ -1,6 +1,7 @@ package vidio import ( + "errors" "io" "os" "os/exec" @@ -74,16 +75,26 @@ func (video *Video) FrameBuffer() []byte { // Creates a new Video struct. // 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) { - 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. - checkExists("ffmpeg") - checkExists("ffprobe") + if err := checkExists("ffmpeg"); err != nil { + return nil, err + } + if err := checkExists("ffprobe"); err != nil { + return nil, err + } - videoData := ffprobe(filename, "v") - audioData := ffprobe(filename, "a") + videoData, err := ffprobe(filename, "v") + if err != nil { + return nil, err + } + audioData, err := ffprobe(filename, "a") + if err != nil { + return nil, err + } video := &Video{filename: filename, depth: 3} @@ -92,12 +103,12 @@ func NewVideo(filename string) *Video { video.audioCodec = audioCodec } - return video + return video, nil } // 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) { +func initVideo(video *Video) 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. @@ -113,13 +124,17 @@ func initVideo(video *Video) { video.cmd = cmd pipe, err := cmd.StdoutPipe() if err != nil { - panic(err) + pipe.Close() + return err } video.pipe = &pipe if err := cmd.Start(); err != nil { - panic(err) + cmd.Process.Kill() + return err } + video.framebuffer = make([]byte, video.width*video.height*video.depth) + return nil } // 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 { // If cmd is nil, video reading has not been initialized. if video.cmd == nil { - initVideo(video) + if err := initVideo(video); err != nil { + return false + } } total := 0 for total < video.width*video.height*video.depth { diff --git a/videowriter.go b/videowriter.go index d9e215a..3a4ab7b 100644 --- a/videowriter.go +++ b/videowriter.go @@ -1,6 +1,7 @@ package vidio import ( + "errors" "fmt" "io" "math" @@ -86,9 +87,11 @@ func (writer *VideoWriter) AudioCodec() string { } // 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. - checkExists("ffmpeg") + if err := checkExists("ffmpeg"); err != nil { + return nil, err + } writer := VideoWriter{filename: filename} @@ -139,11 +142,14 @@ func NewVideoWriter(filename string, width, height int, options *Options) *Video if 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 { - panic("Given \"audio\" file " + options.Audio + " has no audio.") + audioData, err := ffprobe(options.Audio, "a") + 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 @@ -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, // 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. writer.cleanup() // 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() if err != nil { - panic(err) + pipe.Close() + return err } writer.pipe = &pipe if err := cmd.Start(); err != nil { - panic(err) + cmd.Process.Kill() + return err } + + return nil } // 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 writer.cmd == nil { - initVideoWriter(writer) + if err := initVideoWriter(writer); err != nil { + return err + } } total := 0 for total < len(frame) { n, err := (*writer.pipe).Write(frame[total:]) if err != nil { - fmt.Println("Likely cause is invalid parameters to ffmpeg.") - panic(err) + return err } total += n } + return nil } // Closes the pipe and stops the ffmpeg process. diff --git a/vidio_test.go b/vidio_test.go index 71fdcdb..b01b3b1 100644 --- a/vidio_test.go +++ b/vidio_test.go @@ -13,7 +13,10 @@ func assertEquals(expected, actual interface{}) { } func TestVideoMetaData(t *testing.T) { - video := NewVideo("test/koala.mp4") + video, err := NewVideo("test/koala.mp4") + if err != nil { + panic(err) + } defer video.Close() assertEquals(video.filename, "test/koala.mp4") @@ -40,7 +43,10 @@ func TestVideoMetaData(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() video.Read() @@ -61,7 +67,10 @@ func TestVideoFrame(t *testing.T) { func TestVideoWriting(t *testing.T) { testWriting := func(input, output string, audio bool) { - video := NewVideo(input) + video, err := NewVideo(input) + if err != nil { + panic(err) + } options := Options{ FPS: video.FPS(), Bitrate: video.Bitrate(), @@ -71,9 +80,15 @@ func TestVideoWriting(t *testing.T) { 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() { - writer.Write(video.FrameBuffer()) + err := writer.Write(video.FrameBuffer()) + if err != nil { + panic(err) + } } writer.Close() @@ -87,11 +102,17 @@ func TestVideoWriting(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()} - 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 for webcam.Read() { @@ -104,7 +125,10 @@ func TestCameraIO(t *testing.T) { frame[i+1] = gray frame[i+2] = gray } - writer.Write(frame) + err := writer.Write(frame) + if err != nil { + panic(err) + } count++ if count > 100 { break @@ -119,22 +143,34 @@ func TestCameraIO(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["height"], "270") assertEquals(koalaVideo["duration"], "3.366667") assertEquals(koalaVideo["bit_rate"], "170549") 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") - 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["height"], "270") assertEquals(koalaVideo["duration"], "3.366667") assertEquals(koalaVideo["bit_rate"], "170549") 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) fmt.Println("FFprobe Test Passed") @@ -171,7 +207,7 @@ dummy: Immediate exit requested`, func TestWebcamParsing(t *testing.T) { camera := &Camera{} - getCameraData( + err := 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 @@ -179,6 +215,10 @@ At least one output file must be specified`, camera, ) + if err != nil { + panic(err) + } + assertEquals(camera.width, 1280) assertEquals(camera.height, 720) assertEquals(camera.fps, float64(30))