Added multiplatform webcam streaming
This commit is contained in:
parent
ba32c5c493
commit
0cc5ffe925
5 changed files with 375 additions and 61 deletions
103
README.md
103
README.md
|
@ -1,6 +1,6 @@
|
||||||
# Video-IO
|
# Vidio
|
||||||
|
|
||||||
A simple Video I/O library written in Go. This library relies on [FFmpeg](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.
|
||||||
|
|
||||||
## `Video`
|
## `Video`
|
||||||
|
|
||||||
|
@ -9,16 +9,17 @@ The `Video` struct stores data about a video file you give it. The code below sh
|
||||||
```go
|
```go
|
||||||
video := NewVideo("input.mp4")
|
video := NewVideo("input.mp4")
|
||||||
for video.NextFrame() {
|
for video.NextFrame() {
|
||||||
frame := video.framebuffer // "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...
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
```
|
```go
|
||||||
type Video struct {
|
type Video struct {
|
||||||
filename string
|
filename string
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
channels int
|
depth int
|
||||||
bitrate int
|
bitrate int
|
||||||
frames int
|
frames int
|
||||||
duration float64
|
duration float64
|
||||||
|
@ -31,43 +32,81 @@ type Video struct {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
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...`.
|
## `Camera`
|
||||||
|
|
||||||
|
The `Camera` can read from any cameras on the device running Vidio.
|
||||||
|
|
||||||
|
```go
|
||||||
|
type Camera struct {
|
||||||
|
name string
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
depth int
|
||||||
|
fps float64
|
||||||
|
codec string
|
||||||
|
framebuffer []byte
|
||||||
|
pipe *io.ReadCloser
|
||||||
|
cmd *exec.Cmd
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```go
|
||||||
|
camera := NewCamera(0) // Get Webcam
|
||||||
|
defer camera.Close()
|
||||||
|
|
||||||
|
// Stream the webcam.
|
||||||
|
for camera.NextFrame() {
|
||||||
|
// "frame" stores the video frame as a flattened RGB image.
|
||||||
|
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. 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
|
```go
|
||||||
video := Video{
|
type Options struct {
|
||||||
// width and height are required, defaults available for all other parameters.
|
width int // Width of Output Frames
|
||||||
width: 1920,
|
height int // Height of Output Frames
|
||||||
height: 1080,
|
bitrate int // Bitrate
|
||||||
... // Initialize other desired properties of the video you want to create.
|
loop int // For GIFs only. -1=no loop, 0=loop forever, >0=loop n times
|
||||||
|
delay int // Delay for Final Frame of GIFs
|
||||||
|
macro int // macro size for determining how to resize frames for codecs
|
||||||
|
fps float64 // Frames per second
|
||||||
|
codec string // Codec for video
|
||||||
|
in_pix_fmt string // Pixel Format of incoming bytes
|
||||||
|
out_pix_fmt string // Pixel Format for video being written
|
||||||
}
|
}
|
||||||
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
|
||||||
|
type VideoWriter struct {
|
||||||
|
filename string
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
bitrate int
|
||||||
|
loop int
|
||||||
|
delay int
|
||||||
|
macro int
|
||||||
|
fps float64
|
||||||
|
codec string
|
||||||
|
in_pix_fmt string
|
||||||
|
out_pix_fmt string
|
||||||
|
pipe *io.WriteCloser
|
||||||
|
cmd *exec.Cmd
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
```go
|
```go
|
||||||
writer := VideoWriter{
|
|
||||||
filename: "output.mp4",
|
|
||||||
width: 1920,
|
|
||||||
height: 1080
|
|
||||||
...
|
|
||||||
}
|
|
||||||
defer writer.Close()
|
|
||||||
|
|
||||||
w, h, c := 1920, 1080, 3
|
w, h, c := 1920, 1080, 3
|
||||||
|
options = Options{width: w, height: w, bitrate: 100000}
|
||||||
|
|
||||||
|
writer := NewVideoWriter("output.mp4", &options)
|
||||||
|
defer writer.Close() // Make sure to close writer.
|
||||||
|
|
||||||
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.
|
||||||
...
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Examples
|
## Examples
|
||||||
|
@ -76,8 +115,14 @@ Copy `input` to `output`.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
video := NewVideo(input)
|
video := NewVideo(input)
|
||||||
|
options := Options{
|
||||||
|
width: video.width,
|
||||||
|
height: video.height,
|
||||||
|
fps: video.fps,
|
||||||
|
bitrate: video.bitrate
|
||||||
|
}
|
||||||
|
|
||||||
writer := NewVideoWriter(output, video)
|
writer := NewVideoWriter(output, &options)
|
||||||
defer writer.Close()
|
defer writer.Close()
|
||||||
|
|
||||||
for video.NextFrame() {
|
for video.NextFrame() {
|
||||||
|
|
184
camera.go
Normal file
184
camera.go
Normal file
|
@ -0,0 +1,184 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Camera struct {
|
||||||
|
name string
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
depth int
|
||||||
|
fps float64
|
||||||
|
codec string
|
||||||
|
framebuffer []byte
|
||||||
|
pipe *io.ReadCloser
|
||||||
|
cmd *exec.Cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDevicesWindows() []string {
|
||||||
|
// Run command to get list of devices.
|
||||||
|
cmd := exec.Command(
|
||||||
|
"ffmpeg",
|
||||||
|
"-hide_banner",
|
||||||
|
"-list_devices", "true",
|
||||||
|
"-f", "dshow",
|
||||||
|
"-i", "dummy",
|
||||||
|
)
|
||||||
|
pipe, err := cmd.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
// Read list devices from Stdout.
|
||||||
|
buffer := make([]byte, 2<<10)
|
||||||
|
total := 0
|
||||||
|
for {
|
||||||
|
n, err := pipe.Read(buffer[total:])
|
||||||
|
total += n
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cmd.Wait()
|
||||||
|
devices := parseDevices(buffer)
|
||||||
|
return devices
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCameraData(device string, camera *Camera) {
|
||||||
|
// Run command to get camera data.
|
||||||
|
// On windows the webcam will turn on and off again.
|
||||||
|
cmd := exec.Command(
|
||||||
|
"ffmpeg",
|
||||||
|
"-hide_banner",
|
||||||
|
"-f", webcam(),
|
||||||
|
"-i", device,
|
||||||
|
)
|
||||||
|
|
||||||
|
pipe, err := cmd.StderrPipe()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
// Read ffmpeg output from Stdout.
|
||||||
|
buffer := make([]byte, 2<<11)
|
||||||
|
total := 0
|
||||||
|
for {
|
||||||
|
n, err := pipe.Read(buffer[total:])
|
||||||
|
total += n
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd.Wait()
|
||||||
|
|
||||||
|
parseWebcamData(buffer[:total], camera)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCamera(stream int) *Camera {
|
||||||
|
checkExists("ffmpeg")
|
||||||
|
|
||||||
|
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 {
|
||||||
|
case "linux":
|
||||||
|
device = "/dev/video" + strconv.Itoa(stream)
|
||||||
|
break
|
||||||
|
case "darwin":
|
||||||
|
device = strconv.Itoa(stream)
|
||||||
|
break
|
||||||
|
case "windows":
|
||||||
|
devices := getDevicesWindows()
|
||||||
|
if stream >= len(devices) {
|
||||||
|
panic("Could not find devices with index: " + strconv.Itoa(stream))
|
||||||
|
}
|
||||||
|
device = "video=" + devices[stream]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
camera := &Camera{name: device, depth: 3}
|
||||||
|
getCameraData(device, camera)
|
||||||
|
|
||||||
|
return camera
|
||||||
|
}
|
||||||
|
|
||||||
|
func initCamera(camera *Camera) {
|
||||||
|
// If user exits with Ctrl+C, stop ffmpeg process.
|
||||||
|
camera.cleanup()
|
||||||
|
|
||||||
|
// Use ffmpeg to pipe webcam to stdout.
|
||||||
|
cmd := exec.Command(
|
||||||
|
"ffmpeg",
|
||||||
|
"-hide_banner",
|
||||||
|
"-f", webcam(),
|
||||||
|
"-i", camera.name,
|
||||||
|
"-f", "image2pipe",
|
||||||
|
"-loglevel", "quiet",
|
||||||
|
"-pix_fmt", "rgb24",
|
||||||
|
"-vcodec", "rawvideo", "-",
|
||||||
|
)
|
||||||
|
|
||||||
|
camera.cmd = cmd
|
||||||
|
pipe, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
camera.pipe = &pipe
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
camera.framebuffer = make([]byte, camera.width*camera.height*camera.depth)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (camera *Camera) NextFrame() bool {
|
||||||
|
// If cmd is nil, video reading has not been initialized.
|
||||||
|
if camera.cmd == nil {
|
||||||
|
initCamera(camera)
|
||||||
|
}
|
||||||
|
total := 0
|
||||||
|
for total < camera.width*camera.height*camera.depth {
|
||||||
|
n, _ := (*camera.pipe).Read(camera.framebuffer[total:])
|
||||||
|
total += n
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (camera *Camera) Close() {
|
||||||
|
if camera.pipe != nil {
|
||||||
|
(*camera.pipe).Close()
|
||||||
|
}
|
||||||
|
if camera.cmd != nil {
|
||||||
|
camera.cmd.Process.Kill()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 (camera *Camera) cleanup() {
|
||||||
|
c := make(chan os.Signal)
|
||||||
|
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
<-c
|
||||||
|
if camera.pipe != nil {
|
||||||
|
(*camera.pipe).Close()
|
||||||
|
}
|
||||||
|
if camera.cmd != nil {
|
||||||
|
camera.cmd.Process.Kill()
|
||||||
|
}
|
||||||
|
os.Exit(1)
|
||||||
|
}()
|
||||||
|
}
|
108
utils.go
108
utils.go
|
@ -4,13 +4,15 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
|
"regexp"
|
||||||
|
"runtime"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Returns true if file exists, false otherwise.
|
// 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 {
|
func exists(filename string) bool {
|
||||||
_, err := os.Stat(filename)
|
_, err := os.Stat(filename)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
return true
|
return true
|
||||||
|
@ -22,7 +24,7 @@ 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) {
|
||||||
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.")
|
panic(program + " is not installed.")
|
||||||
|
@ -59,6 +61,7 @@ func parseFFprobe(input []byte, video *Video) {
|
||||||
video.pix_fmt = data["pix_fmt"]
|
video.pix_fmt = data["pix_fmt"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parses the given data into a float64.
|
||||||
func parse(data string) float64 {
|
func parse(data string) float64 {
|
||||||
n, err := strconv.ParseFloat(data, 64)
|
n, err := strconv.ParseFloat(data, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -67,16 +70,91 @@ func parse(data string) float64 {
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
|
|
||||||
// func main() {
|
// Returns the webcam name used for the -f option with ffmpeg.
|
||||||
// os := runtime.GOOS
|
func webcam() string {
|
||||||
// switch os {
|
os := runtime.GOOS
|
||||||
// case "windows":
|
switch os {
|
||||||
// fmt.Println("Windows")
|
case "linux":
|
||||||
// case "darwin":
|
return "v4l2"
|
||||||
// fmt.Println("MAC operating system")
|
case "darwin":
|
||||||
// case "linux":
|
return "avfoundation" // qtkit
|
||||||
// fmt.Println("Linux")
|
case "windows":
|
||||||
// default:
|
return "dshow" // vfwcap
|
||||||
// fmt.Printf("%s.\n", os)
|
default:
|
||||||
// }
|
panic("Unsupported OS: " + os)
|
||||||
// }
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For webcam streaming on windows, ffmpeg requires a device name.
|
||||||
|
// All device names are parsed and returned by this function.
|
||||||
|
func parseDevices(buffer []byte) []string {
|
||||||
|
devices := make([]string, 0)
|
||||||
|
bufferstr := string(buffer)
|
||||||
|
|
||||||
|
index := strings.Index(bufferstr, "DirectShow video devices")
|
||||||
|
if index == -1 {
|
||||||
|
return devices
|
||||||
|
}
|
||||||
|
bufferstr = bufferstr[index:]
|
||||||
|
|
||||||
|
index = strings.Index(bufferstr, "DirectShow audio devices")
|
||||||
|
if index != -1 {
|
||||||
|
bufferstr = bufferstr[:index]
|
||||||
|
}
|
||||||
|
// Find all device names surrounded by quotes. E.g "Windows Camera Front"
|
||||||
|
r := regexp.MustCompile("\"[^\"]+\"")
|
||||||
|
matches := r.FindAllStringSubmatch(bufferstr, -1)
|
||||||
|
for _, match := range matches {
|
||||||
|
device := match[0][1 : len(match[0])-1]
|
||||||
|
// Don't include Alternate Names for devices.
|
||||||
|
// Alternate names start with an '@'.
|
||||||
|
if !strings.HasPrefix(device, "@") {
|
||||||
|
devices = append(devices, device)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return devices
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parses the webcam metadata (width, height, fps, codec) from ffmpeg output.
|
||||||
|
func parseWebcamData(buffer []byte, camera *Camera) {
|
||||||
|
bufferstr := string(buffer)
|
||||||
|
index := strings.Index(bufferstr, "Stream #")
|
||||||
|
if index == -1 {
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
bufferstr = bufferstr[index:]
|
||||||
|
// Dimensions. widthxheight
|
||||||
|
regex := regexp.MustCompile("\\d{2,}x\\d{2,}")
|
||||||
|
match := regex.FindString(bufferstr)
|
||||||
|
if len(match) > 0 {
|
||||||
|
split := strings.Split(match, "x")
|
||||||
|
camera.width = int(parse(split[0]))
|
||||||
|
camera.height = int(parse(split[1]))
|
||||||
|
}
|
||||||
|
// FPS
|
||||||
|
regex = regexp.MustCompile("\\d+(.\\d+)? fps")
|
||||||
|
match = regex.FindString(bufferstr)
|
||||||
|
if len(match) > 0 {
|
||||||
|
index = strings.Index(match, " fps")
|
||||||
|
if index != -1 {
|
||||||
|
match = match[:index]
|
||||||
|
}
|
||||||
|
camera.fps = parse(match)
|
||||||
|
}
|
||||||
|
// Codec
|
||||||
|
regex = regexp.MustCompile("Video: .+,")
|
||||||
|
match = regex.FindString(bufferstr)
|
||||||
|
if len(match) > 0 {
|
||||||
|
match = match[7:]
|
||||||
|
index = strings.Index(match, "(")
|
||||||
|
if index != -1 {
|
||||||
|
match = match[:index]
|
||||||
|
}
|
||||||
|
index = strings.Index(match, ",")
|
||||||
|
if index != -1 {
|
||||||
|
match = match[:index]
|
||||||
|
}
|
||||||
|
camera.codec = strings.TrimSpace(match)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
31
videoio.go
31
videoio.go
|
@ -12,7 +12,7 @@ type Video struct {
|
||||||
filename string
|
filename string
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
channels int
|
depth int
|
||||||
bitrate int
|
bitrate int
|
||||||
frames int
|
frames int
|
||||||
duration float64
|
duration float64
|
||||||
|
@ -25,27 +25,30 @@ type Video struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewVideo(filename string) *Video {
|
func NewVideo(filename string) *Video {
|
||||||
if !Exists(filename) {
|
if !exists(filename) {
|
||||||
panic("File: " + filename + " does not exist")
|
panic("File: " + filename + " does not exist")
|
||||||
}
|
}
|
||||||
CheckExists("ffmpeg")
|
checkExists("ffmpeg")
|
||||||
CheckExists("ffprobe")
|
checkExists("ffprobe")
|
||||||
// Extract video information with ffprobe.
|
// Extract video information with ffprobe.
|
||||||
cmd := exec.Command(
|
cmd := exec.Command(
|
||||||
"ffprobe",
|
"ffprobe",
|
||||||
"-show_streams",
|
"-show_streams",
|
||||||
"-select_streams", "v",
|
"-select_streams", "v", // Only show video data
|
||||||
"-print_format", "compact",
|
"-print_format", "compact",
|
||||||
"-loglevel", "quiet",
|
"-loglevel", "quiet",
|
||||||
filename,
|
filename,
|
||||||
)
|
)
|
||||||
|
|
||||||
pipe, err := cmd.StdoutPipe()
|
pipe, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
// Read ffprobe output from Stdout.
|
||||||
buffer := make([]byte, 2<<10)
|
buffer := make([]byte, 2<<10)
|
||||||
total := 0
|
total := 0
|
||||||
for {
|
for {
|
||||||
|
@ -55,18 +58,21 @@ func NewVideo(filename string) *Video {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := cmd.Wait(); err != nil {
|
if err := cmd.Wait(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
video := &Video{filename: filename, channels: 3}
|
video := &Video{filename: filename, depth: 3}
|
||||||
parseFFprobe(buffer[:total], video)
|
parseFFprobe(buffer[:total], video)
|
||||||
return video
|
return video
|
||||||
}
|
}
|
||||||
|
|
||||||
func (video *Video) initVideoStream() {
|
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,
|
||||||
|
@ -75,6 +81,7 @@ func (video *Video) initVideoStream() {
|
||||||
"-pix_fmt", "rgb24",
|
"-pix_fmt", "rgb24",
|
||||||
"-vcodec", "rawvideo", "-",
|
"-vcodec", "rawvideo", "-",
|
||||||
)
|
)
|
||||||
|
|
||||||
video.cmd = cmd
|
video.cmd = cmd
|
||||||
pipe, err := cmd.StdoutPipe()
|
pipe, err := cmd.StdoutPipe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -84,16 +91,16 @@ func (video *Video) initVideoStream() {
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
video.framebuffer = make([]byte, video.width*video.height*video.channels)
|
video.framebuffer = make([]byte, video.width*video.height*video.depth)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (video *Video) NextFrame() bool {
|
func (video *Video) NextFrame() 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 {
|
||||||
video.initVideoStream()
|
initVideoStream(video)
|
||||||
}
|
}
|
||||||
total := 0
|
total := 0
|
||||||
for total < video.width*video.height*video.channels {
|
for total < video.width*video.height*video.depth {
|
||||||
n, err := (*video.pipe).Read(video.framebuffer[total:])
|
n, err := (*video.pipe).Read(video.framebuffer[total:])
|
||||||
if err == io.EOF {
|
if err == io.EOF {
|
||||||
video.Close()
|
video.Close()
|
||||||
|
@ -108,8 +115,8 @@ func (video *Video) Close() {
|
||||||
if video.pipe != nil {
|
if video.pipe != nil {
|
||||||
(*video.pipe).Close()
|
(*video.pipe).Close()
|
||||||
}
|
}
|
||||||
if err := video.cmd.Wait(); err != nil {
|
if video.cmd != nil {
|
||||||
panic(err)
|
video.cmd.Wait()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -31,8 +31,8 @@ type Options struct {
|
||||||
height int
|
height int
|
||||||
bitrate int
|
bitrate int
|
||||||
loop int
|
loop int
|
||||||
macro int
|
|
||||||
delay int
|
delay int
|
||||||
|
macro int
|
||||||
fps float64
|
fps float64
|
||||||
codec string
|
codec string
|
||||||
in_pix_fmt string
|
in_pix_fmt string
|
||||||
|
@ -53,9 +53,9 @@ func NewVideoWriter(filename string, options *Options) *VideoWriter {
|
||||||
// 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
|
// GIF settings
|
||||||
writer.loop = options.loop
|
writer.loop = options.loop // Default to infinite loop
|
||||||
if options.delay == 0 {
|
if options.delay == 0 {
|
||||||
writer.delay = -1
|
writer.delay = -1 // Default to frame delay of previous frame
|
||||||
} else {
|
} else {
|
||||||
writer.delay = options.delay
|
writer.delay = options.delay
|
||||||
}
|
}
|
||||||
|
@ -99,7 +99,7 @@ func NewVideoWriter(filename string, options *Options) *VideoWriter {
|
||||||
return &writer
|
return &writer
|
||||||
}
|
}
|
||||||
|
|
||||||
func (writer *VideoWriter) initVideoWriter() {
|
func initVideoWriter(writer *VideoWriter) {
|
||||||
// If user exits with Ctrl+C, stop ffmpeg process.
|
// If user exits with Ctrl+C, stop ffmpeg process.
|
||||||
writer.cleanup()
|
writer.cleanup()
|
||||||
|
|
||||||
|
@ -163,7 +163,7 @@ func (writer *VideoWriter) initVideoWriter() {
|
||||||
func (writer *VideoWriter) Write(frame []byte) {
|
func (writer *VideoWriter) Write(frame []byte) {
|
||||||
// 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 {
|
||||||
writer.initVideoWriter()
|
initVideoWriter(writer)
|
||||||
}
|
}
|
||||||
total := 0
|
total := 0
|
||||||
for total < len(frame) {
|
for total < len(frame) {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue