Initial commit
This commit is contained in:
commit
059f46f9ed
10 changed files with 511 additions and 0 deletions
0
.gitignore
vendored
Normal file
0
.gitignore
vendored
Normal file
90
README.md
Normal file
90
README.md
Normal file
|
@ -0,0 +1,90 @@
|
|||
# ImageIO
|
||||
|
||||
A simple Image and Video I/O library written in Go. This library relies on [FFMPEG](https://www.ffmpeg.org/), which must be downloaded before usage.
|
||||
|
||||
One of the key features of this library is it's simplicity: The FFMPEG commands used to read and write video are readily available in `video.go` and `videowriter.go` for you to modify as you need.
|
||||
|
||||
For portability, all the library code can be found in the `videoio.go` file.
|
||||
|
||||
## Documentation
|
||||
|
||||
ImageIO features `Video` and `VideoWriter` structs which can read and write videos.
|
||||
|
||||
## `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
|
||||
video := NewVideo("input.mp4")
|
||||
for video.NextFrame() {
|
||||
frame := video.framebuffer // "frame" stores the video frame as a flattened RGB image.
|
||||
}
|
||||
```
|
||||
|
||||
Notice that once the `video` is initialized, you will have access to certain metadata of the video such as the
|
||||
|
||||
* width (pixels)
|
||||
* height (pixels)
|
||||
* bitrate (kB/s)
|
||||
* duration (seconds)
|
||||
* frames per second
|
||||
* video codec
|
||||
* pixel format
|
||||
|
||||
Once the frame is read by calling the `NextFrame()` function, the resulting frame is stored in the `framebuffer` as shown above. The frame buffer is an array of bytes representing the most recently read frame as an RGB image. The framebuffer is flattened and contains image data in the form: `RGBRGBRGBRGB...`.
|
||||
|
||||
## `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.
|
||||
|
||||
```go
|
||||
video := Video{
|
||||
// width and height are required, defaults available for all other parameters.
|
||||
width: 1920
|
||||
height: 1080
|
||||
... // Initialize other desired properties of the video you want to create.
|
||||
}
|
||||
writer := NewVideoWriter("output.mp4", video)
|
||||
defer writer.Close() // Make sure to close writer.
|
||||
|
||||
w, h, c := 1920, 1080, 3
|
||||
frame = make([]byte, w*h*c) // Create Frame as RGB Image and modify.
|
||||
writer.Write(frame) // Write Frame to video.
|
||||
...
|
||||
```
|
||||
|
||||
Alternatively, you could manually create a `VideoWriter` struct and fill it in yourself.
|
||||
|
||||
```go
|
||||
writer := VideoWriter{
|
||||
filename: "output.mp4"
|
||||
width: 1920
|
||||
height: 1080
|
||||
...
|
||||
}
|
||||
defer writer.Close() // Make sure to close writer.
|
||||
|
||||
w, h, c := 1920, 1080, 3
|
||||
frame = make([]byte, w*h*c) // Create Frame as RGB Image and modify.
|
||||
writer.Write(frame) // Write Frame to video.
|
||||
...
|
||||
```
|
||||
|
||||
## Examples
|
||||
|
||||
Copy `input` to `output`.
|
||||
|
||||
```go
|
||||
video := NewVideo(input)
|
||||
|
||||
writer := NewVideoWriter(output, video)
|
||||
defer writer.Close()
|
||||
|
||||
for video.NextFrame() {
|
||||
writer.Write(video.framebuffer)
|
||||
}
|
||||
```
|
||||
|
||||
# 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.
|
24
demo.go
Normal file
24
demo.go
Normal file
|
@ -0,0 +1,24 @@
|
|||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
// Try it yourself!
|
||||
// Update "filename" to a video file on your system and
|
||||
// create and output file you'd like to copy this video to.
|
||||
filename := "input.mp4"
|
||||
output := "output.mp4"
|
||||
video := NewVideo(filename)
|
||||
|
||||
fmt.Println(video.bitrate)
|
||||
|
||||
writer := NewVideoWriter(output, video)
|
||||
defer writer.Close()
|
||||
|
||||
count := 0
|
||||
for video.NextFrame() {
|
||||
writer.Write(video.framebuffer)
|
||||
count += 1
|
||||
fmt.Println(count)
|
||||
}
|
||||
}
|
5
go.mod
Normal file
5
go.mod
Normal file
|
@ -0,0 +1,5 @@
|
|||
module github.com/AlexEidt/test
|
||||
|
||||
go 1.16
|
||||
|
||||
require gocv.io/x/gocv v0.28.0
|
4
go.sum
Normal file
4
go.sum
Normal file
|
@ -0,0 +1,4 @@
|
|||
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=
|
61
imageio.go
Normal file
61
imageio.go
Normal file
|
@ -0,0 +1,61 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type Image struct {
|
||||
width int
|
||||
height int
|
||||
channels int
|
||||
data []byte
|
||||
}
|
||||
|
||||
func ReadImage(filename string) *Image {
|
||||
// Read image from "filename".
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
fmt.Printf("%s not found.", filename)
|
||||
return nil
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var im image.Image
|
||||
if strings.HasSuffix(filename, "jpg") {
|
||||
im, err = jpeg.Decode(file)
|
||||
} else if strings.HasSuffix(filename, "png") {
|
||||
im, err = png.Decode(file)
|
||||
} else {
|
||||
im, _, err = image.Decode(file)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("%s is an invalid image format. Could not parse.\n", filename)
|
||||
return nil
|
||||
}
|
||||
bounds := im.Bounds().Max
|
||||
|
||||
data := make([]byte, bounds.Y*bounds.X*4)
|
||||
// Fill in "data" with colors of the image.
|
||||
index := 0
|
||||
for y := 0; y < bounds.Y; y++ {
|
||||
for x := 0; x < bounds.X; x++ {
|
||||
r, g, b, a := im.At(x, y).RGBA()
|
||||
data[index] = byte(r)
|
||||
data[index+1] = byte(g)
|
||||
data[index+2] = byte(b)
|
||||
data[index+3] = byte(a)
|
||||
index += 4
|
||||
}
|
||||
}
|
||||
return &Image{width: bounds.X, height: bounds.Y, channels: 4, data: data}
|
||||
}
|
||||
|
||||
func WriteImage(filename string, img *Image) {
|
||||
|
||||
}
|
72
parsing.go
Normal file
72
parsing.go
Normal file
|
@ -0,0 +1,72 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Parses the duration of the video from the ffmpeg header.
|
||||
func parseDurationBitrate(video *Video, data []string) {
|
||||
videoData := ""
|
||||
for _, line := range data {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "Duration: ") {
|
||||
videoData = line
|
||||
break
|
||||
}
|
||||
}
|
||||
if videoData == "" {
|
||||
panic("Could not find duration in ffmpeg header.")
|
||||
}
|
||||
// Duration
|
||||
duration := strings.Split(strings.SplitN(strings.SplitN(videoData, ",", 2)[0], "Duration:", 2)[1], ":")
|
||||
seconds, _ := strconv.ParseFloat(duration[len(duration)-1], 64)
|
||||
minutes, _ := strconv.ParseFloat(duration[len(duration)-2], 64)
|
||||
hours, _ := strconv.ParseFloat(duration[len(duration)-3], 64)
|
||||
video.duration = seconds + minutes*60 + hours*3600
|
||||
|
||||
// Bitrate
|
||||
bitrate := strings.SplitN(strings.TrimSpace(strings.SplitN(videoData, "bitrate:", 2)[1]), " ", 2)[0]
|
||||
video.bitrate, _ = strconv.Atoi(bitrate)
|
||||
}
|
||||
|
||||
func parseVideoData(video *Video, data []string) {
|
||||
videoData := ""
|
||||
// Get string containing video data.
|
||||
for _, line := range data {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "Stream") && strings.Contains(line, "Video:") {
|
||||
videoData = strings.TrimSpace(strings.SplitN(line, "Video:", 2)[1])
|
||||
break
|
||||
}
|
||||
}
|
||||
if videoData == "" {
|
||||
panic("No video data found in ffmpeg header.")
|
||||
}
|
||||
// Video Codec
|
||||
video.codec = strings.TrimSpace(strings.SplitN(videoData, " ", 2)[0])
|
||||
// FPS
|
||||
fpsstr := strings.SplitN(videoData, "fps", 2)[0]
|
||||
fps, _ := strconv.Atoi(strings.TrimSpace(fpsstr[strings.LastIndex(fpsstr, ",")+1:]))
|
||||
video.fps = float64(fps)
|
||||
// Pixel Format
|
||||
video.pix_fmt = strings.TrimSpace(strings.Split(videoData, ",")[1])
|
||||
// Width and Height
|
||||
r, _ := regexp.Compile("\\d+x\\d+")
|
||||
wh := r.FindAllString(videoData, -1)
|
||||
dims := strings.SplitN(wh[len(wh)-1], "x", 2)
|
||||
width, _ := strconv.Atoi(dims[0])
|
||||
height, _ := strconv.Atoi(dims[1])
|
||||
video.width = width
|
||||
video.height = height
|
||||
}
|
||||
|
||||
// Parses the ffmpeg header.
|
||||
// Code inspired by the imageio-ffmpeg project.
|
||||
// GitHub: https://github.com/imageio/imageio-ffmpeg/blob/master/imageio_ffmpeg/_parsing.py#L113
|
||||
func parseFFMPEGHeader(video *Video, header string) {
|
||||
data := strings.Split(strings.ReplaceAll(header, "\r\n", "\n"), "\n")
|
||||
parseDurationBitrate(video, data)
|
||||
parseVideoData(video, data)
|
||||
}
|
19
utils.go
Normal file
19
utils.go
Normal file
|
@ -0,0 +1,19 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Returns true if file exists, false otherwise.
|
||||
// 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 {
|
||||
return true
|
||||
}
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return false
|
||||
}
|
||||
return false
|
||||
}
|
114
video.go
Normal file
114
video.go
Normal file
|
@ -0,0 +1,114 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
type Video struct {
|
||||
filename string
|
||||
width int
|
||||
height int
|
||||
channels int
|
||||
bitrate int
|
||||
duration float64
|
||||
fps float64
|
||||
codec string
|
||||
pix_fmt string
|
||||
framebuffer []byte
|
||||
pipe *io.ReadCloser
|
||||
cmd *exec.Cmd
|
||||
}
|
||||
|
||||
func NewVideo(filename string) *Video {
|
||||
if !Exists(filename) {
|
||||
panic("File: " + filename + " does not exist")
|
||||
}
|
||||
// Execute ffmpeg -i command to get video information.
|
||||
cmd := exec.Command("ffmpeg", "-i", filename, "-")
|
||||
// FFMPEG output piped to Stderr.
|
||||
pipe, err := cmd.StderrPipe()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err := cmd.Start(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
buffer := make([]byte, 2<<12)
|
||||
total := 0
|
||||
for {
|
||||
n, err := pipe.Read(buffer[total:])
|
||||
total += n
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
cmd.Wait()
|
||||
video := &Video{filename: filename, channels: 3, pipe: nil, framebuffer: nil}
|
||||
parseFFMPEGHeader(video, string(buffer))
|
||||
return video
|
||||
}
|
||||
|
||||
func (video *Video) initVideoStream() {
|
||||
// If user exits with Ctrl+C, stop ffmpeg process.
|
||||
video.cleanup()
|
||||
|
||||
cmd := exec.Command(
|
||||
"ffmpeg",
|
||||
"-loglevel", "quiet",
|
||||
"-i", video.filename,
|
||||
"-f", "image2pipe",
|
||||
"-pix_fmt", "rgb24",
|
||||
"-vcodec", "rawvideo", "-",
|
||||
)
|
||||
video.cmd = cmd
|
||||
pipe, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
video.pipe = &pipe
|
||||
if err := cmd.Start(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
video.framebuffer = make([]byte, video.width*video.height*video.channels)
|
||||
}
|
||||
|
||||
func (video *Video) NextFrame() bool {
|
||||
// If cmd is nil, video reading has not been initialized.
|
||||
if video.cmd == nil {
|
||||
video.initVideoStream()
|
||||
}
|
||||
total := 0
|
||||
for total < video.width*video.height*video.channels {
|
||||
n, err := (*video.pipe).Read(video.framebuffer[total:])
|
||||
if err == io.EOF {
|
||||
(*video.pipe).Close()
|
||||
if err := video.cmd.Wait(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return false
|
||||
}
|
||||
total += n
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// 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
|
||||
func (video *Video) cleanup() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-c
|
||||
if video.pipe != nil {
|
||||
(*video.pipe).Close()
|
||||
}
|
||||
if video.cmd != nil {
|
||||
video.cmd.Process.Kill()
|
||||
}
|
||||
os.Exit(1)
|
||||
}()
|
||||
}
|
122
videowriter.go
Normal file
122
videowriter.go
Normal file
|
@ -0,0 +1,122 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
type VideoWriter struct {
|
||||
filename string
|
||||
width int
|
||||
height int
|
||||
channels int
|
||||
bitrate int
|
||||
fps float64
|
||||
codec string
|
||||
pix_fmt string
|
||||
pipe *io.WriteCloser
|
||||
cmd *exec.Cmd
|
||||
}
|
||||
|
||||
func NewVideoWriter(filename string, video *Video) *VideoWriter {
|
||||
if video.width == 0 || video.height == 0 {
|
||||
panic("Video width and height must be set.")
|
||||
}
|
||||
if video.channels == 0 {
|
||||
video.channels = 3 // Default to RGB frames.
|
||||
}
|
||||
if video.fps == 0 {
|
||||
video.fps = 25 // Default to 25 FPS.
|
||||
}
|
||||
if video.codec == "" {
|
||||
video.codec = "mpeg4" // Default to MPEG4.
|
||||
}
|
||||
if video.pix_fmt == "" {
|
||||
video.pix_fmt = "rgb24" // Default to RGB24.
|
||||
}
|
||||
return &VideoWriter{
|
||||
filename: filename,
|
||||
width: video.width,
|
||||
height: video.height,
|
||||
channels: video.channels,
|
||||
bitrate: video.bitrate,
|
||||
fps: video.fps,
|
||||
codec: video.codec,
|
||||
pix_fmt: video.pix_fmt,
|
||||
}
|
||||
}
|
||||
|
||||
func (writer *VideoWriter) initVideoWriter() {
|
||||
// If user exits with Ctrl+C, stop ffmpeg process.
|
||||
writer.cleanup()
|
||||
|
||||
cmd := exec.Command(
|
||||
"ffmpeg",
|
||||
"-y", // overwrite output file if it exists
|
||||
"-f", "rawvideo",
|
||||
"-vcodec", "rawvideo",
|
||||
"-s", fmt.Sprintf("%dx%d", writer.width, writer.height), // frame w x h
|
||||
"-pix_fmt", writer.pix_fmt,
|
||||
"-r", fmt.Sprintf("%f", writer.fps), // frames per second
|
||||
"-i", "-", // The imput comes from stdin
|
||||
"-an", // Tells FFMPEG not to expect any audio
|
||||
"-vcodec", writer.codec,
|
||||
writer.filename,
|
||||
)
|
||||
writer.cmd = cmd
|
||||
|
||||
pipe, err := cmd.StdinPipe()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
writer.pipe = &pipe
|
||||
if err := cmd.Start(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (writer *VideoWriter) Write(frame []byte) {
|
||||
// If cmd is nil, video writing has not been set up.
|
||||
if writer.cmd == nil {
|
||||
writer.initVideoWriter()
|
||||
}
|
||||
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)
|
||||
}
|
||||
total += n
|
||||
}
|
||||
}
|
||||
|
||||
func (writer *VideoWriter) Close() {
|
||||
if writer.pipe != nil {
|
||||
(*writer.pipe).Close()
|
||||
}
|
||||
if writer.cmd != nil {
|
||||
writer.cmd.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
func (writer *VideoWriter) cleanup() {
|
||||
c := make(chan os.Signal)
|
||||
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||
go func() {
|
||||
<-c
|
||||
if writer.pipe != nil {
|
||||
(*writer.pipe).Close()
|
||||
}
|
||||
if writer.cmd != nil {
|
||||
writer.cmd.Process.Kill()
|
||||
}
|
||||
os.Exit(1)
|
||||
}()
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue