Using FFprobe for video metadata
This commit is contained in:
parent
3faf4aa9c8
commit
ba32c5c493
5 changed files with 337 additions and 230 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -0,0 +1,3 @@
|
||||||
|
main.go
|
||||||
|
*.gif
|
||||||
|
*.mp4
|
37
README.md
37
README.md
|
@ -1,12 +1,6 @@
|
||||||
# Video-IO
|
# Video-IO
|
||||||
|
|
||||||
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/), 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 `videoio.go` for you to modify as you need. All functions placed in one file for portability.
|
|
||||||
|
|
||||||
## Documentation
|
|
||||||
|
|
||||||
Video-IO features `Video` and `VideoWriter` structs which can read and write videos.
|
|
||||||
|
|
||||||
## `Video`
|
## `Video`
|
||||||
|
|
||||||
|
@ -19,15 +13,23 @@ for video.NextFrame() {
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
Notice that once the `video` is initialized, you will have access to certain metadata of the video such as the
|
```
|
||||||
|
type Video struct {
|
||||||
* width (pixels)
|
filename string
|
||||||
* height (pixels)
|
width int
|
||||||
* bitrate (kB/s)
|
height int
|
||||||
* duration (seconds)
|
channels int
|
||||||
* frames per second
|
bitrate int
|
||||||
* video codec
|
frames int
|
||||||
* pixel format
|
duration float64
|
||||||
|
fps float64
|
||||||
|
codec string
|
||||||
|
pix_fmt string
|
||||||
|
framebuffer []byte
|
||||||
|
pipe *io.ReadCloser
|
||||||
|
cmd *exec.Cmd
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
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...`.
|
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...`.
|
||||||
|
|
||||||
|
@ -85,4 +87,5 @@ for video.NextFrame() {
|
||||||
|
|
||||||
# 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.
|
||||||
|
* The [ImageIO-FFMPEG](https://github.com/imageio/imageio-ffmpeg/) project on GitHub.
|
82
utils.go
Normal file
82
utils.go
Normal file
|
@ -0,0 +1,82 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// Checks if the given program is installed.
|
||||||
|
func CheckExists(program string) {
|
||||||
|
cmd := exec.Command(program, "-version")
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
panic(program + " is not installed.")
|
||||||
|
}
|
||||||
|
if err := cmd.Wait(); err != nil {
|
||||||
|
panic(program + " is not installed.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse ffprobe output to fill in video data.
|
||||||
|
func parseFFprobe(input []byte, video *Video) {
|
||||||
|
data := make(map[string]string)
|
||||||
|
for _, line := range strings.Split(string(input), "|") {
|
||||||
|
if strings.Contains(line, "=") {
|
||||||
|
keyValue := strings.Split(line, "=")
|
||||||
|
if _, ok := data[keyValue[0]]; !ok {
|
||||||
|
data[keyValue[0]] = keyValue[1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
video.width = int(parse(data["width"]))
|
||||||
|
video.height = int(parse(data["height"]))
|
||||||
|
video.duration = float64(parse(data["duration"]))
|
||||||
|
video.frames = int(parse(data["nb_frames"]))
|
||||||
|
|
||||||
|
split := strings.Split(data["r_frame_rate"], "/")
|
||||||
|
if len(split) == 2 && split[0] != "" && split[1] != "" {
|
||||||
|
video.fps = parse(split[0]) / parse(split[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
video.bitrate = int(parse(data["bit_rate"]))
|
||||||
|
video.codec = data["codec_name"]
|
||||||
|
video.pix_fmt = data["pix_fmt"]
|
||||||
|
}
|
||||||
|
|
||||||
|
func parse(data string) float64 {
|
||||||
|
n, err := strconv.ParseFloat(data, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
return n
|
||||||
|
}
|
||||||
|
|
||||||
|
// func main() {
|
||||||
|
// os := runtime.GOOS
|
||||||
|
// switch os {
|
||||||
|
// case "windows":
|
||||||
|
// fmt.Println("Windows")
|
||||||
|
// case "darwin":
|
||||||
|
// fmt.Println("MAC operating system")
|
||||||
|
// case "linux":
|
||||||
|
// fmt.Println("Linux")
|
||||||
|
// default:
|
||||||
|
// fmt.Printf("%s.\n", os)
|
||||||
|
// }
|
||||||
|
// }
|
242
videoio.go
242
videoio.go
|
@ -1,28 +1,20 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"regexp"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
)
|
)
|
||||||
|
|
||||||
// #############################################################################
|
|
||||||
// Video
|
|
||||||
// #############################################################################
|
|
||||||
|
|
||||||
type Video struct {
|
type Video struct {
|
||||||
filename string
|
filename string
|
||||||
width int
|
width int
|
||||||
height int
|
height int
|
||||||
channels int
|
channels int
|
||||||
bitrate int
|
bitrate int
|
||||||
|
frames int
|
||||||
duration float64
|
duration float64
|
||||||
fps float64
|
fps float64
|
||||||
codec string
|
codec string
|
||||||
|
@ -36,17 +28,25 @@ func NewVideo(filename string) *Video {
|
||||||
if !Exists(filename) {
|
if !Exists(filename) {
|
||||||
panic("File: " + filename + " does not exist")
|
panic("File: " + filename + " does not exist")
|
||||||
}
|
}
|
||||||
// Execute ffmpeg -i command to get video information.
|
CheckExists("ffmpeg")
|
||||||
cmd := exec.Command("ffmpeg", "-i", filename, "-")
|
CheckExists("ffprobe")
|
||||||
// ffmpeg output piped to Stderr.
|
// Extract video information with ffprobe.
|
||||||
pipe, err := cmd.StderrPipe()
|
cmd := exec.Command(
|
||||||
|
"ffprobe",
|
||||||
|
"-show_streams",
|
||||||
|
"-select_streams", "v",
|
||||||
|
"-print_format", "compact",
|
||||||
|
"-loglevel", "quiet",
|
||||||
|
filename,
|
||||||
|
)
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
buffer := make([]byte, 2<<12)
|
buffer := make([]byte, 2<<10)
|
||||||
total := 0
|
total := 0
|
||||||
for {
|
for {
|
||||||
n, err := pipe.Read(buffer[total:])
|
n, err := pipe.Read(buffer[total:])
|
||||||
|
@ -55,9 +55,11 @@ func NewVideo(filename string) *Video {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
cmd.Wait()
|
if err := cmd.Wait(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
video := &Video{filename: filename, channels: 3}
|
video := &Video{filename: filename, channels: 3}
|
||||||
parseFFMPEGHeader(video, string(buffer))
|
parseFFprobe(buffer[:total], video)
|
||||||
return video
|
return video
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,9 +69,9 @@ func (video *Video) initVideoStream() {
|
||||||
|
|
||||||
cmd := exec.Command(
|
cmd := exec.Command(
|
||||||
"ffmpeg",
|
"ffmpeg",
|
||||||
"-loglevel", "quiet",
|
|
||||||
"-i", video.filename,
|
"-i", video.filename,
|
||||||
"-f", "image2pipe",
|
"-f", "image2pipe",
|
||||||
|
"-loglevel", "quiet",
|
||||||
"-pix_fmt", "rgb24",
|
"-pix_fmt", "rgb24",
|
||||||
"-vcodec", "rawvideo", "-",
|
"-vcodec", "rawvideo", "-",
|
||||||
)
|
)
|
||||||
|
@ -94,10 +96,7 @@ func (video *Video) NextFrame() bool {
|
||||||
for total < video.width*video.height*video.channels {
|
for total < video.width*video.height*video.channels {
|
||||||
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.pipe).Close()
|
video.Close()
|
||||||
if err := video.cmd.Wait(); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
total += n
|
total += n
|
||||||
|
@ -105,6 +104,15 @@ func (video *Video) NextFrame() bool {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (video *Video) Close() {
|
||||||
|
if video.pipe != nil {
|
||||||
|
(*video.pipe).Close()
|
||||||
|
}
|
||||||
|
if err := video.cmd.Wait(); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Stops the "cmd" process running when the user presses Ctrl+C.
|
// 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() {
|
func (video *Video) cleanup() {
|
||||||
|
@ -121,195 +129,3 @@ func (video *Video) cleanup() {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
// #############################################################################
|
|
||||||
// VideoWriter
|
|
||||||
// #############################################################################
|
|
||||||
|
|
||||||
type VideoWriter struct {
|
|
||||||
filename string
|
|
||||||
width int
|
|
||||||
height 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.fps == 0 {
|
|
||||||
video.fps = 25 // Default to 25 FPS.
|
|
||||||
}
|
|
||||||
return &VideoWriter{
|
|
||||||
filename: filename,
|
|
||||||
width: video.width,
|
|
||||||
height: video.height,
|
|
||||||
bitrate: video.bitrate,
|
|
||||||
fps: video.fps,
|
|
||||||
codec: "mpeg4",
|
|
||||||
pix_fmt: "rgb24",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,
|
|
||||||
"-b:v", fmt.Sprintf("%dk", writer.bitrate), // bitrate
|
|
||||||
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 {
|
|
||||||
defer 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)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
// #############################################################################
|
|
||||||
// Utils
|
|
||||||
// #############################################################################
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
// #############################################################################
|
|
||||||
// Utils
|
|
||||||
// #############################################################################
|
|
||||||
|
|
||||||
// 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
|
|
||||||
}
|
|
||||||
|
|
203
videowriter.go
Normal file
203
videowriter.go
Normal file
|
@ -0,0 +1,203 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"os/signal"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
codec string
|
||||||
|
in_pix_fmt string
|
||||||
|
out_pix_fmt string
|
||||||
|
pipe *io.WriteCloser
|
||||||
|
cmd *exec.Cmd
|
||||||
|
}
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
width int
|
||||||
|
height int
|
||||||
|
bitrate int
|
||||||
|
loop int
|
||||||
|
macro int
|
||||||
|
delay int
|
||||||
|
fps float64
|
||||||
|
codec string
|
||||||
|
in_pix_fmt string
|
||||||
|
out_pix_fmt string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewVideoWriter(filename string, options *Options) *VideoWriter {
|
||||||
|
writer := VideoWriter{filename: filename}
|
||||||
|
|
||||||
|
if options.width == 0 || options.height == 0 {
|
||||||
|
panic("width and height must be greater than 0.")
|
||||||
|
} else {
|
||||||
|
writer.width = options.width
|
||||||
|
writer.height = options.height
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default Parameter options logic from:
|
||||||
|
// https://github.com/imageio/imageio-ffmpeg/blob/master/imageio_ffmpeg/_io.py#L268
|
||||||
|
|
||||||
|
// GIF settings
|
||||||
|
writer.loop = options.loop
|
||||||
|
if options.delay == 0 {
|
||||||
|
writer.delay = -1
|
||||||
|
} else {
|
||||||
|
writer.delay = options.delay
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.macro == 0 {
|
||||||
|
writer.macro = 16
|
||||||
|
} else {
|
||||||
|
writer.macro = options.macro
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.fps == 0 {
|
||||||
|
writer.fps = 25
|
||||||
|
} else {
|
||||||
|
writer.fps = options.fps
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.codec == "" {
|
||||||
|
if strings.HasSuffix(strings.ToLower(filename), ".wmv") {
|
||||||
|
writer.codec = "msmpeg4"
|
||||||
|
} else if strings.HasSuffix(strings.ToLower(filename), ".gif") {
|
||||||
|
writer.codec = "gif"
|
||||||
|
} else {
|
||||||
|
writer.codec = "libx264"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func (writer *VideoWriter) initVideoWriter() {
|
||||||
|
// If user exits with Ctrl+C, stop ffmpeg process.
|
||||||
|
writer.cleanup()
|
||||||
|
|
||||||
|
command := []string{
|
||||||
|
"-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
|
||||||
|
"-pix_fmt", writer.in_pix_fmt,
|
||||||
|
"-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", writer.out_pix_fmt,
|
||||||
|
"-b:v", fmt.Sprintf("%d", writer.bitrate), // bitrate
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(strings.ToLower(writer.filename), ".gif") {
|
||||||
|
command = append(
|
||||||
|
command,
|
||||||
|
"-loop", fmt.Sprintf("%d", writer.loop),
|
||||||
|
"-final_delay", fmt.Sprintf("%d", writer.delay),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Code from the imageio-ffmpeg project:
|
||||||
|
// 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 {
|
||||||
|
width := writer.width
|
||||||
|
height := writer.height
|
||||||
|
if writer.width%writer.macro > 0 {
|
||||||
|
width += writer.macro - (writer.width % writer.macro)
|
||||||
|
}
|
||||||
|
if writer.height%writer.macro > 0 {
|
||||||
|
height += writer.macro - (writer.height % writer.macro)
|
||||||
|
}
|
||||||
|
command = append(
|
||||||
|
command,
|
||||||
|
"-vf", fmt.Sprintf("scale=%d:%d", width, height),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
command = append(command, writer.filename)
|
||||||
|
cmd := exec.Command("ffmpeg", command...)
|
||||||
|
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