From 1062e2f4f86130e5c6d1629fd493b87e658a34a0 Mon Sep 17 00:00:00 2001 From: Alex Eidt Date: Tue, 28 Dec 2021 22:11:25 -0800 Subject: [PATCH] Better documentation --- camera.go | 35 +++++++++++++++--------- utils.go | 12 ++++----- video.go | 43 +++++++++++++++++------------- videowriter.go | 72 +++++++++++++++++++++++++++----------------------- 4 files changed, 92 insertions(+), 70 deletions(-) diff --git a/camera.go b/camera.go index aef93ea..84ed820 100644 --- a/camera.go +++ b/camera.go @@ -11,17 +11,19 @@ import ( ) type Camera struct { - name string - width int - height int - depth int - fps float64 - codec string - framebuffer []byte - pipe *io.ReadCloser - cmd *exec.Cmd + name string // Camera device name. + width int // Camera frame width. + height int // Camera frame height. + depth int // Camera frame depth. + fps float64 // Camera frame rate. + codec string // Camera codec. + framebuffer []byte // Raw frame data. + pipe *io.ReadCloser // Stdout pipe for ffmpeg process streaming webcam. + cmd *exec.Cmd // ffmpeg command. } +// Returns the webcam device name. +// On windows, ffmpeg output from the -list_devices command is parsed to find the device name. func getDevicesWindows() []string { // Run command to get list of devices. cmd := exec.Command( @@ -53,6 +55,7 @@ func getDevicesWindows() []string { return devices } +// Get camera meta data such as width, height, fps and codec. func getCameraData(device string, camera *Camera) { // Run command to get camera data. // Webcam will turn on and then off in quick succession. @@ -62,12 +65,13 @@ func getCameraData(device string, camera *Camera) { "-f", webcam(), "-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) } - + // Start the command. if err := cmd.Start(); err != nil { panic(err) } @@ -81,12 +85,13 @@ func getCameraData(device string, camera *Camera) { break } } - + // Wait for the command to finish. cmd.Wait() parseWebcamData(buffer[:total], camera) } +// Creates a new camera struct that can read from the device with the given stream index. func NewCamera(stream int) *Camera { // Check if ffmpeg is installed on the users machine. checkExists("ffmpeg") @@ -117,6 +122,8 @@ func NewCamera(stream int) *Camera { return &camera } +// 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) { // If user exits with Ctrl+C, stop ffmpeg process. camera.cleanup() @@ -146,6 +153,7 @@ func initCamera(camera *Camera) { camera.framebuffer = make([]byte, camera.width*camera.height*camera.depth) } +// 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 { @@ -159,6 +167,7 @@ func (camera *Camera) Read() bool { return true } +// Closes the pipe and stops the ffmpeg process. func (camera *Camera) Close() { if camera.pipe != nil { (*camera.pipe).Close() @@ -169,7 +178,7 @@ func (camera *Camera) Close() { } // Stops the "cmd" process running when the user presses Ctrl+C. -// https://stackoverflow.com/questions/11268943/is-it-possible-to-capture-a-ctrlc-signal-and-run-a-cleanup-function-in-a-defe +// https://stackoverflow.com/questions/11268943/is-it-possible-to-capture-a-ctrlc-signal-and-run-a-cleanup-function-in-a-defe. func (camera *Camera) cleanup() { c := make(chan os.Signal) signal.Notify(c, os.Interrupt, syscall.SIGTERM) diff --git a/utils.go b/utils.go index c5c2d66..c0ada31 100644 --- a/utils.go +++ b/utils.go @@ -11,7 +11,7 @@ import ( ) // Returns true if file exists, false otherwise. -// https://stackoverflow.com/questions/12518876/how-to-check-if-a-file-exists-in-go +// https://stackoverflow.com/questions/12518876/how-to-check-if-a-file-exists-in-go. func exists(filename string) bool { _, err := os.Stat(filename) if err == nil { @@ -105,7 +105,7 @@ func parseDevices(buffer []byte) []string { alt string } // Parses ffmpeg output to get device names. Windows only. - // Uses parsing approach from https://github.com/imageio/imageio/blob/master/imageio/plugins/ffmpeg.py#L681 + // Uses parsing approach from https://github.com/imageio/imageio/blob/master/imageio/plugins/ffmpeg.py#L681. pairs := []Pair{} // Find all device names surrounded by quotes. E.g "Windows Camera Front" @@ -125,7 +125,7 @@ func parseDevices(buffer []byte) []string { } devices := []string{} - // If two devices have the same name, use the alternate name of the later device as its name + // If two devices have the same name, use the alternate name of the later device as its name. for _, pair := range pairs { if contains(devices, pair.name) { devices = append(devices, pair.alt) @@ -155,7 +155,7 @@ func parseWebcamData(buffer []byte, camera *Camera) { index++ } bufferstr = bufferstr[index:] - // Dimensions. widthxheight + // Dimensions. widthxheight. regex := regexp.MustCompile("\\d{2,}x\\d{2,}") match := regex.FindString(bufferstr) if len(match) > 0 { @@ -163,7 +163,7 @@ func parseWebcamData(buffer []byte, camera *Camera) { camera.width = int(parse(split[0])) camera.height = int(parse(split[1])) } - // FPS + // FPS. regex = regexp.MustCompile("\\d+(.\\d+)? fps") match = regex.FindString(bufferstr) if len(match) > 0 { @@ -173,7 +173,7 @@ func parseWebcamData(buffer []byte, camera *Camera) { } camera.fps = parse(match) } - // Codec + // Codec. regex = regexp.MustCompile("Video: .+,") match = regex.FindString(bufferstr) if len(match) > 0 { diff --git a/video.go b/video.go index 5dcb74b..63ee53a 100644 --- a/video.go +++ b/video.go @@ -9,21 +9,23 @@ import ( ) type Video struct { - filename string - width int - height int - depth int - bitrate int - frames int - duration float64 - fps float64 - codec string - pix_fmt string - framebuffer []byte - pipe *io.ReadCloser - cmd *exec.Cmd + filename string // Video Filename. + width int // Width of frames. + height int // Height of frames. + depth int // Depth of frames. + bitrate int // Bitrate for video encoding. + frames int // Total number of frames. + duration float64 // Duration of video in seconds. + fps float64 // Frames per second. + codec string // Codec used for video encoding. + pix_fmt string // Pixel format video is stored in. + framebuffer []byte // Raw frame data. + pipe *io.ReadCloser // Stdout pipe for ffmpeg process. + cmd *exec.Cmd // ffmpeg command. } +// 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 { if !exists(filename) { panic("File: " + filename + " does not exist") @@ -59,7 +61,7 @@ func NewVideo(filename string) *Video { break } } - + // Wait for ffprobe command to complete. if err := cmd.Wait(); err != nil { panic(err) } @@ -68,10 +70,12 @@ func NewVideo(filename string) *Video { return video } -func initVideoStream(video *Video) { +// 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) { // If user exits with Ctrl+C, stop ffmpeg process. video.cleanup() - + // ffmpeg command to pipe video data to stdout in 8-bit RGB format. cmd := exec.Command( "ffmpeg", "-i", video.filename, @@ -93,10 +97,12 @@ func initVideoStream(video *Video) { video.framebuffer = make([]byte, video.width*video.height*video.depth) } +// Reads the next frame from the video and stores in the framebuffer. +// If thelast frame has been read, returns false, otherwise true. func (video *Video) Read() bool { // If cmd is nil, video reading has not been initialized. if video.cmd == nil { - initVideoStream(video) + initVideo(video) } total := 0 for total < video.width*video.height*video.depth { @@ -110,6 +116,7 @@ func (video *Video) Read() bool { return true } +// Closes the pipe and stops the ffmpeg process. func (video *Video) Close() { if video.pipe != nil { (*video.pipe).Close() @@ -120,7 +127,7 @@ func (video *Video) Close() { } // Stops the "cmd" process running when the user presses Ctrl+C. -// https://stackoverflow.com/questions/11268943/is-it-possible-to-capture-a-ctrlc-signal-and-run-a-cleanup-function-in-a-defe +// https://stackoverflow.com/questions/11268943/is-it-possible-to-capture-a-ctrlc-signal-and-run-a-cleanup-function-in-a-defe. func (video *Video) cleanup() { c := make(chan os.Signal) signal.Notify(c, os.Interrupt, syscall.SIGTERM) diff --git a/videowriter.go b/videowriter.go index 051e00b..8e14513 100644 --- a/videowriter.go +++ b/videowriter.go @@ -11,30 +11,32 @@ import ( ) type VideoWriter struct { - filename string - width int - height int - bitrate int - loop int // For GIFs. -1=no loop, 0=loop forever, >0=loop n times - delay int // Delay for Final Frame of GIFs. - macro int - fps float64 - quality float64 - codec string - pipe *io.WriteCloser - cmd *exec.Cmd + filename string // Output filename. + width int // Frame width. + height int // Frame height. + bitrate int // Output video bitrate. + loop int // Number of times for GIF to loop. + delay int // Delay of final frame of GIF. Default -1 (same delay as previous frame). + macro int // Macroblock size for determining how to resize frames for codecs. + fps float64 // Frames per second for output video. Default 25. + quality float64 // Used if bitrate not given. Default 0.5. + codec string // Codec to encode video with. Default libx264. + pipe *io.WriteCloser // Stdout pipe of ffmpeg process. + cmd *exec.Cmd // ffmpeg command. } +// Optional parameters for VideoWriter. type Options struct { - bitrate int - loop int - delay int - macro int - fps float64 - quality float64 - codec string + bitrate int // Bitrate. + loop int // For GIFs only. -1=no loop, 0=infinite loop, >0=number of loops. + delay int // Delay for final frame of GIFs. + macro int // Macroblock size for determining how to resize frames for codecs. + fps float64 // Frames per second for output video. + quality float64 // If bitrate not given, use quality instead. Must be between 0 and 1. 0:best, 1:worst. + codec string // Codec for video. } +// Creates a new VideoWriter struct with default values from the Options struct. func NewVideoWriter(filename string, width, height int, options *Options) *VideoWriter { // Check if ffmpeg is installed on the users machine. checkExists("ffmpeg") @@ -45,12 +47,12 @@ func NewVideoWriter(filename string, width, height int, options *Options) *Video writer.height = height // Default Parameter options logic from: - // https://github.com/imageio/imageio-ffmpeg/blob/master/imageio_ffmpeg/_io.py#L268 + // https://github.com/imageio/imageio-ffmpeg/blob/master/imageio_ffmpeg/_io.py#L268. // GIF settings - writer.loop = options.loop // Default to infinite loop + writer.loop = options.loop // Default to infinite loop. if options.delay == 0 { - writer.delay = -1 // Default to frame delay of previous frame + writer.delay = -1 // Default to frame delay of previous frame. } else { writer.delay = options.delay } @@ -87,26 +89,28 @@ func NewVideoWriter(filename string, width, height int, options *Options) *Video return &writer } +// 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) { // 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. command := []string{ - "-y", // overwrite output file if it exists + "-y", // overwrite output file if it exists. "-loglevel", "quiet", "-f", "rawvideo", "-vcodec", "rawvideo", - "-s", fmt.Sprintf("%dx%d", writer.width, writer.height), // frame w x h + "-s", fmt.Sprintf("%dx%d", writer.width, writer.height), // frame w x h. "-pix_fmt", "rgb24", - "-r", fmt.Sprintf("%.02f", writer.fps), // frames per second - "-i", "-", // The input comes from stdin - "-an", // Tells ffmpeg not to expect any audio + "-r", fmt.Sprintf("%.02f", writer.fps), // frames per second. + "-i", "-", // The input comes from stdin. + "-an", // Tells ffmpeg not to expect any audio. "-vcodec", writer.codec, - "-pix_fmt", "yuv420p", // Output is 8-bit RGB, no alpha + "-pix_fmt", "yuv420p", // Output is 8-bit RGB, no alpha. } // Code from the imageio-ffmpeg project. - // https://github.com/imageio/imageio-ffmpeg/blob/master/imageio_ffmpeg/_io.py#L399 + // https://github.com/imageio/imageio-ffmpeg/blob/master/imageio_ffmpeg/_io.py#L399. // If bitrate not given, use a default. if writer.bitrate == 0 { if writer.codec == "libx264" { @@ -119,7 +123,7 @@ func initVideoWriter(writer *VideoWriter) { } else { command = append(command, "-b:v", fmt.Sprintf("%d", writer.bitrate)) } - + // For GIFs, add looping and delay parameters. if strings.HasSuffix(strings.ToLower(writer.filename), ".gif") { command = append( command, @@ -129,7 +133,7 @@ func initVideoWriter(writer *VideoWriter) { } // Code from the imageio-ffmpeg project: - // https://github.com/imageio/imageio-ffmpeg/blob/master/imageio_ffmpeg/_io.py#L415 + // https://github.com/imageio/imageio-ffmpeg/blob/master/imageio_ffmpeg/_io.py#L415. // Resizes the video frames to a size that works with most codecs. if writer.macro > 1 { if writer.width%writer.macro > 0 || writer.height%writer.macro > 0 { @@ -162,6 +166,7 @@ func initVideoWriter(writer *VideoWriter) { } } +// Writes the given frame to the video file. func (writer *VideoWriter) Write(frame []byte) { // If cmd is nil, video writing has not been set up. if writer.cmd == nil { @@ -178,6 +183,7 @@ func (writer *VideoWriter) Write(frame []byte) { } } +// Closes the pipe and stops the ffmpeg process. func (writer *VideoWriter) Close() { if writer.pipe != nil { (*writer.pipe).Close() @@ -188,7 +194,7 @@ func (writer *VideoWriter) Close() { } // Stops the "cmd" process running when the user presses Ctrl+C. -// https://stackoverflow.com/questions/11268943/is-it-possible-to-capture-a-ctrlc-signal-and-run-a-cleanup-function-in-a-defe +// https://stackoverflow.com/questions/11268943/is-it-possible-to-capture-a-ctrlc-signal-and-run-a-cleanup-function-in-a-defe. func (writer *VideoWriter) cleanup() { c := make(chan os.Signal) signal.Notify(c, os.Interrupt, syscall.SIGTERM)