Cleanup and updated README

This commit is contained in:
Alex Eidt 2021-12-27 13:02:35 -08:00
parent 0cc5ffe925
commit 723de2fa8a
9 changed files with 254 additions and 130 deletions

1
.gitignore vendored
View file

@ -1,3 +1,2 @@
main.go
*.gif *.gif
*.mp4 *.mp4

192
README.md
View file

@ -2,134 +2,194 @@
A simple Video I/O library written in Go. This library relies on [FFmpeg](https://www.ffmpeg.org/), and [FFProbe](https://www.ffmpeg.org/) which must be downloaded before usage. A simple Video I/O library written in Go. This library relies on [FFmpeg](https://www.ffmpeg.org/), and [FFProbe](https://www.ffmpeg.org/) which must be downloaded before usage.
All frames are encoded and decoded in 8-bit RGB format.
```
go get github.com/AlexEidt/Vidio
```
## `Video` ## `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. 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
video := NewVideo("input.mp4") video := vidio.NewVideo("input.mp4")
for video.NextFrame() { for video.Read() {
// "frame" stores the video frame as a flattened RGB image. // "frame" stores the video frame as a flattened RGB image
frame := video.framebuffer // stored as: RGBRGBRGBRGB... frame := video.framebuffer // stored as: RGBRGBRGBRGB...
} }
``` ```
```go ```go
type Video struct { type Video struct {
filename string filename string // Video Filename
width int width int // Width of Frames
height int height int // Height of Frames
depth int depth int // Depth of Frames
bitrate int bitrate int // Bitrate for video encoding
frames int frames int // Total number of frames
duration float64 duration float64 // Duration in seconds
fps float64 fps float64 // Frames per second
codec string codec string // Codec used to encode video
pix_fmt string pix_fmt string // Pixel format video is stored in
framebuffer []byte framebuffer []byte // Raw frame data
pipe *io.ReadCloser pipe *io.ReadCloser // Stdout pipe for ffmpeg process
cmd *exec.Cmd cmd *exec.Cmd // ffmpeg command
} }
``` ```
## `Camera` ## `Camera`
The `Camera` can read from any cameras on the device running Vidio. 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.
```go ```go
type Camera struct { type Camera struct {
name string name string // Camera device name
width int width int // Camera frame width
height int height int // Camera frame height
depth int depth int // Camera frame depth
fps float64 fps float64 // Camera frames per second
codec string codec string // Camera codec
framebuffer []byte framebuffer []byte // Raw frame data
pipe *io.ReadCloser pipe *io.ReadCloser // Stdout pipe for ffmpeg process streaming webcam
cmd *exec.Cmd cmd *exec.Cmd // ffmpeg command
} }
``` ```
```go ```go
camera := NewCamera(0) // Get Webcam camera := vidio.NewCamera(0) // Get Webcam
defer camera.Close() defer camera.Close()
// Stream the webcam. // Stream the webcam
for camera.NextFrame() { for camera.Read() {
// "frame" stores the video frame as a flattened RGB image. // "frame" stores the video frame as a flattened RGB image
frame := camera.framebuffer // stored as: RGBRGBRGBRGB... frame := camera.framebuffer // stored as: RGBRGBRGBRGB...
} }
``` ```
## `VideoWriter` ## `VideoWriter`
The `VideoWriter` is used to write frames to a video file. You first need to create a `Video` struct with all the desired properties of the new video you want to create such as width, height and framerate. 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 such as width, height and framerate.
```go ```go
type Options struct { type Options struct {
width int // Width of Output Frames bitrate int // Bitrate
height int // Height of Output Frames loop int // For GIFs only. -1=no loop, 0=loop forever, >0=loop n times
bitrate int // Bitrate delay int // Delay for Final Frame of GIFs. Default -1 (Use same delay as previous frame)
loop int // For GIFs only. -1=no loop, 0=loop forever, >0=loop n times macro int // macro size for determining how to resize frames for codecs. Default 16
delay int // Delay for Final Frame of GIFs fps float64 // Frames per second. Default 25
macro int // macro size for determining how to resize frames for codecs quality float64 // If bitrate not given, use quality instead. Must be between 0 and 1. 0:best, 1:worst
fps float64 // Frames per second codec string // Codec for video. Default libx264
codec string // Codec for video
in_pix_fmt string // Pixel Format of incoming bytes
out_pix_fmt string // Pixel Format for video being written
} }
``` ```
```go ```go
type VideoWriter struct { type VideoWriter struct {
filename string filename string // Output video filename
width int width int // Frame width
height int height int // Frame height
bitrate int bitrate int // Output video bitrate for encoding
loop int loop int // Number of times for GIF to loop
delay int delay int // Delay of final frame of GIF
macro int macro int // macro size for determining how to resize frames for codecs
fps float64 fps float64 // Frames per second for output video
codec string quality float64 // Used if bitrate not given
in_pix_fmt string codec string // Codec to encode video with
out_pix_fmt string pipe *io.WriteCloser // Stdout pipe of ffmpeg process
pipe *io.WriteCloser cmd *exec.Cmd // ffmpeg command
cmd *exec.Cmd
} }
``` ```
```go ```go
w, h, c := 1920, 1080, 3 w, h, c := 1920, 1080, 3
options = Options{width: w, height: w, bitrate: 100000} options = vidio.Options{} // Will fill in defaults if empty
writer := NewVideoWriter("output.mp4", &options) writer := vidio.NewVideoWriter("output.mp4", w, h, &options)
defer writer.Close() // Make sure to close writer. 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. writer.Write(frame) // Write Frame to video
```
## Images
Vidio provides some convenience functions for reading and writing to images using an array of bytes. Currently, only `png` and `jpeg` formats are supported.
```go
// Read png image
w, h, img := vidio.Read("input.png")
// w - width of image
// h - height of image
// img - byte array in RGB format. RGBRGBRGBRGB...
vidio.Write("output.jpg", w, h, img)
``` ```
## Examples ## Examples
Copy `input` to `output`. Copy `input.mp4` to `output.mp4`.
```go ```go
video := NewVideo(input) video := vidio.NewVideo("input.mp4")
options := Options{ options := vidio.Options{
width: video.width,
height: video.height,
fps: video.fps, fps: video.fps,
bitrate: video.bitrate bitrate: video.bitrate
} }
writer := NewVideoWriter(output, &options) writer := vidio.NewVideoWriter("output.mp4", video.width, video.height, &options)
defer writer.Close() defer writer.Close()
for video.NextFrame() { for video.Read() {
writer.Write(video.framebuffer) writer.Write(video.framebuffer)
} }
``` ```
Grayscale 1000 frames of webcam stream and store in `output.mp4`.
```go
webcam := vidio.NewCamera(0)
defer webcam.Close()
options := vidio.Options{fps: webcam.fps}
writer := vidio.NewVideoWriter("output.mp4", webcam.width, webcam.height, &options)
defer writer.Close()
count := 0
for webcam.Read() {
for i := 0; i < len(webcam.framebuffer); i += 3 {
rgb := webcam.framebuffer[i : i+3]
r, g, b := int(rgb[0]), int(rgb[1]), int(rgb[2])
gray := uint8((3*r + 4*g + b) / 8)
writer.framebuffer[i] = gray
writer.framebuffer[i+1] = gray
writer.framebuffer[i+2] = gray
}
writer.Write(webcam.framebuffer)
if count > 1000 {
break
}
count++
}
```
Create a gif from a series of `png` files enumerated from 1 to 10 that loops continously with a final frame delay of 1000 centiseconds.
```go
w, h, _ := vidio.Read("1.png") // Get frame dimensions from first image
options := vidio.Options{fps: 1, loop: -1, delay: 1000}
gif := vidio.NewVideoWriter("output.gif", w, h, &options)
defer gif.Close()
for i := 1; i <= 10; i++ {
_, _, img := vidio.Read(strconv.Itoa(i)+".png")
gif.Write(img)
}
```
# Acknowledgements # Acknowledgements
* Special thanks to [Zulko](http://zulko.github.io/) and his [blog post](http://zulko.github.io/blog/2013/09/27/read-and-write-video-frames-in-python-using-ffmpeg/) about using FFmpeg to process video. * Special thanks to [Zulko](http://zulko.github.io/) and his [blog post](http://zulko.github.io/blog/2013/09/27/read-and-write-video-frames-in-python-using-ffmpeg/) about using FFmpeg to process video.

View file

@ -1,4 +1,4 @@
package main package vidio
import ( import (
"io" "io"
@ -55,7 +55,7 @@ func getDevicesWindows() []string {
func getCameraData(device string, camera *Camera) { func getCameraData(device string, camera *Camera) {
// Run command to get camera data. // Run command to get camera data.
// On windows the webcam will turn on and off again. // Webcam will turn on and then off in quick succession.
cmd := exec.Command( cmd := exec.Command(
"ffmpeg", "ffmpeg",
"-hide_banner", "-hide_banner",
@ -88,11 +88,10 @@ func getCameraData(device string, camera *Camera) {
} }
func NewCamera(stream int) *Camera { func NewCamera(stream int) *Camera {
// Check if ffmpeg is installed on the users machine.
checkExists("ffmpeg") checkExists("ffmpeg")
var device string var device string
// If OS is windows, we need to parse the listed devices to find which corresponds to the
// given "stream" index.
switch runtime.GOOS { switch runtime.GOOS {
case "linux": case "linux":
device = "/dev/video" + strconv.Itoa(stream) device = "/dev/video" + strconv.Itoa(stream)
@ -101,6 +100,8 @@ func NewCamera(stream int) *Camera {
device = strconv.Itoa(stream) device = strconv.Itoa(stream)
break break
case "windows": 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 := getDevicesWindows()
if stream >= len(devices) { if stream >= len(devices) {
panic("Could not find devices with index: " + strconv.Itoa(stream)) panic("Could not find devices with index: " + strconv.Itoa(stream))
@ -123,10 +124,10 @@ func initCamera(camera *Camera) {
cmd := exec.Command( cmd := exec.Command(
"ffmpeg", "ffmpeg",
"-hide_banner", "-hide_banner",
"-loglevel", "quiet",
"-f", webcam(), "-f", webcam(),
"-i", camera.name, "-i", camera.name,
"-f", "image2pipe", "-f", "image2pipe",
"-loglevel", "quiet",
"-pix_fmt", "rgb24", "-pix_fmt", "rgb24",
"-vcodec", "rawvideo", "-", "-vcodec", "rawvideo", "-",
) )
@ -144,7 +145,7 @@ func initCamera(camera *Camera) {
camera.framebuffer = make([]byte, camera.width*camera.height*camera.depth) camera.framebuffer = make([]byte, camera.width*camera.height*camera.depth)
} }
func (camera *Camera) NextFrame() 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) initCamera(camera)

2
go.mod
View file

@ -1,3 +1,3 @@
module github.com/AlexEidt/Video-IO module github.com/AlexEidt/Vidio
go 1.16 go 1.16

4
go.sum
View file

@ -1,4 +0,0 @@
github.com/hybridgroup/mjpeg v0.0.0-20140228234708-4680f319790e/go.mod h1:eagM805MRKrioHYuU7iKLUyFPVKqVV6um5DAvCkUtXs=
github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
gocv.io/x/gocv v0.28.0 h1:hweRS9Js60YEZPZzjhU5I+0E2ngazquLlO78zwnrFvY=
gocv.io/x/gocv v0.28.0/go.mod h1:oc6FvfYqfBp99p+yOEzs9tbYF9gOrAQSeL/dyIPefJU=

69
imageio.go Normal file
View file

@ -0,0 +1,69 @@
package vidio
import (
"image"
"os"
"strings"
"image/color"
"image/jpeg"
_ "image/jpeg"
"image/png"
_ "image/png"
)
// Reads an image from a file. Currently only supports png and jpeg.
func Read(filename string) (int, int, []byte) {
f, err := os.Open(filename)
if err != nil {
panic(err)
}
defer f.Close()
image, _, err := image.Decode(f)
if err != nil {
panic(err)
}
bounds := image.Bounds().Max
data := make([]byte, bounds.Y*bounds.X*3)
index := 0
for h := 0; h < bounds.Y; h++ {
for w := 0; w < bounds.X; w++ {
r, g, b, _ := image.At(w, h).RGBA()
r, g, b = r/256, g/256, b/256
data[index] = byte(r)
index++
data[index] = byte(g)
index++
data[index] = byte(b)
index++
}
}
return bounds.X, bounds.Y, data
}
// Writes an image to a file. Currently only supports png and jpeg.
func Write(filename string, width, height int, data []byte) {
f, err := os.Create(filename)
if err != nil {
panic(err)
}
defer f.Close()
image := image.NewRGBA(image.Rect(0, 0, width, height))
index := 0
for h := 0; h < height; h++ {
for w := 0; w < width; w++ {
r, g, b := data[index], data[index+1], data[index+2]
image.Set(w, h, color.RGBA{r, g, b, 255})
index += 3
}
}
if strings.HasSuffix(filename, ".png") {
if err := png.Encode(f, image); err != nil {
panic(err)
}
} else if strings.HasSuffix(filename, ".jpg") || strings.HasSuffix(filename, ".jpeg") {
if err := jpeg.Encode(f, image, nil); err != nil {
panic(err)
}
}
}

View file

@ -1,4 +1,4 @@
package main package vidio
import ( import (
"errors" "errors"

View file

@ -1,4 +1,4 @@
package main package vidio
import ( import (
"io" "io"
@ -28,6 +28,7 @@ func NewVideo(filename string) *Video {
if !exists(filename) { if !exists(filename) {
panic("File: " + filename + " does not exist") panic("File: " + filename + " does not exist")
} }
// Check if ffmpeg and ffprobe are installed on the users machine.
checkExists("ffmpeg") checkExists("ffmpeg")
checkExists("ffprobe") checkExists("ffprobe")
// Extract video information with ffprobe. // Extract video information with ffprobe.
@ -71,8 +72,6 @@ func initVideoStream(video *Video) {
// If user exits with Ctrl+C, stop ffmpeg process. // If user exits with Ctrl+C, stop ffmpeg process.
video.cleanup() video.cleanup()
// map = {1: "gray", 2: "gray8a", 3: "rgb24", 4: "rgba"}
cmd := exec.Command( cmd := exec.Command(
"ffmpeg", "ffmpeg",
"-i", video.filename, "-i", video.filename,
@ -94,7 +93,7 @@ func initVideoStream(video *Video) {
video.framebuffer = make([]byte, video.width*video.height*video.depth) video.framebuffer = make([]byte, video.width*video.height*video.depth)
} }
func (video *Video) NextFrame() 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 {
initVideoStream(video) initVideoStream(video)

View file

@ -1,4 +1,4 @@
package main package vidio
import ( import (
"fmt" "fmt"
@ -11,43 +11,35 @@ import (
) )
type VideoWriter struct { type VideoWriter struct {
filename string filename string
width int width int
height int height int
bitrate int bitrate int
loop int // For GIFs. -1=no loop, 0=loop forever, >0=loop n times loop int // For GIFs. -1=no loop, 0=loop forever, >0=loop n times
delay int // Delay for Final Frame of GIFs. delay int // Delay for Final Frame of GIFs.
macro int macro int
fps float64 fps float64
codec string quality float64
in_pix_fmt string codec string
out_pix_fmt string pipe *io.WriteCloser
pipe *io.WriteCloser cmd *exec.Cmd
cmd *exec.Cmd
} }
type Options struct { type Options struct {
width int bitrate int
height int loop int
bitrate int delay int
loop int macro int
delay int fps float64
macro int quality float64
fps float64 codec string
codec string
in_pix_fmt string
out_pix_fmt string
} }
func NewVideoWriter(filename string, options *Options) *VideoWriter { func NewVideoWriter(filename string, width, height int, options *Options) *VideoWriter {
writer := VideoWriter{filename: filename} writer := VideoWriter{filename: filename}
if options.width == 0 || options.height == 0 { writer.width = width
panic("width and height must be greater than 0.") writer.height = height
} else {
writer.width = options.width
writer.height = options.height
}
// Default Parameter options logic from: // 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
@ -72,6 +64,12 @@ func NewVideoWriter(filename string, options *Options) *VideoWriter {
writer.fps = options.fps writer.fps = options.fps
} }
if options.quality == 0 {
writer.quality = 0.5
} else {
writer.quality = options.quality
}
if options.codec == "" { if options.codec == "" {
if strings.HasSuffix(strings.ToLower(filename), ".wmv") { if strings.HasSuffix(strings.ToLower(filename), ".wmv") {
writer.codec = "msmpeg4" writer.codec = "msmpeg4"
@ -84,18 +82,6 @@ func NewVideoWriter(filename string, options *Options) *VideoWriter {
writer.codec = options.codec writer.codec = options.codec
} }
if options.in_pix_fmt == "" {
writer.in_pix_fmt = "rgb24"
} else {
writer.in_pix_fmt = options.in_pix_fmt
}
if options.out_pix_fmt == "" {
writer.out_pix_fmt = "yuv420p"
} else {
writer.out_pix_fmt = options.out_pix_fmt
}
return &writer return &writer
} }
@ -109,13 +95,27 @@ func initVideoWriter(writer *VideoWriter) {
"-f", "rawvideo", "-f", "rawvideo",
"-vcodec", "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", writer.in_pix_fmt, "-pix_fmt", "rgb24",
"-r", fmt.Sprintf("%.02f", writer.fps), // frames per second "-r", fmt.Sprintf("%.02f", writer.fps), // frames per second
"-i", "-", // The input comes from stdin "-i", "-", // The input comes from stdin
"-an", // Tells ffmpeg not to expect any audio "-an", // Tells ffmpeg not to expect any audio
"-vcodec", writer.codec, "-vcodec", writer.codec,
"-pix_fmt", writer.out_pix_fmt, "-pix_fmt", "yuv420p", // Output is 8-bit RGB, no alpha
"-b:v", fmt.Sprintf("%d", writer.bitrate), // bitrate }
// Code from the imageio-ffmpeg project.
// 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" {
// Quality between 0 an 51. 51 is worst.
command = append(command, "-crf", fmt.Sprintf("%d", int(writer.quality*51)))
} else {
// Quality between 1 and 31. 31 is worst.
command = append(command, "-qscale:v", fmt.Sprintf("%d", int(writer.quality*30)+1))
}
} else {
command = append(command, "-b:v", fmt.Sprintf("%d", writer.bitrate))
} }
if strings.HasSuffix(strings.ToLower(writer.filename), ".gif") { if strings.HasSuffix(strings.ToLower(writer.filename), ".gif") {