Added audio support for videos
This commit is contained in:
parent
204ed5b1b5
commit
b1fd41b75f
5 changed files with 132 additions and 65 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -1,2 +1,4 @@
|
|||
*.gif
|
||||
*.mp4
|
||||
|
||||
vidio_test.go
|
26
README.md
26
README.md
|
@ -1,6 +1,6 @@
|
|||
# Vidio
|
||||
|
||||
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 and added to the system path.
|
||||
|
||||
All frames are encoded and decoded in 8-bit RGB format.
|
||||
|
||||
|
@ -25,6 +25,7 @@ type Video struct {
|
|||
duration float64 // Duration in seconds
|
||||
fps float64 // Frames per second
|
||||
codec string // Codec used to encode video
|
||||
audio_codec string // Codec used for audio encoding
|
||||
pix_fmt string // Pixel format video is stored in
|
||||
framebuffer []byte // Raw frame data
|
||||
pipe *io.ReadCloser // Stdout pipe for ffmpeg process
|
||||
|
@ -43,7 +44,7 @@ for video.Read() {
|
|||
|
||||
## `Camera`
|
||||
|
||||
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.
|
||||
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. Note that audio retrieval from the microphone is not yet supported.
|
||||
|
||||
```go
|
||||
type Camera struct {
|
||||
|
@ -84,21 +85,25 @@ type Options struct {
|
|||
fps float64 // Frames per second. Default 25
|
||||
quality float64 // If bitrate not given, use quality instead. Must be between 0 and 1. 0:best, 1:worst
|
||||
codec string // Codec for video. Default libx264
|
||||
audio string // File path for audio for the video. If no audio, audio=nil.
|
||||
audio_codec string // Codec for audio. Default aac
|
||||
}
|
||||
```
|
||||
|
||||
```go
|
||||
type VideoWriter struct {
|
||||
filename string // Output video filename
|
||||
filename string // Output filename
|
||||
audio string // Audio filename.
|
||||
width int // Frame width
|
||||
height int // Frame height
|
||||
bitrate int // Output video bitrate for encoding
|
||||
bitrate int // Output video bitrate
|
||||
loop int // Number of times for GIF to loop
|
||||
delay int // Delay of final frame of GIF
|
||||
macro int // macro size for determining how to resize frames for codecs
|
||||
fps float64 // Frames per second for output video
|
||||
quality float64 // Used if bitrate not given
|
||||
codec string // Codec to encode video with
|
||||
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
|
||||
audio_codec string // Codec to encode audio with. Default aac
|
||||
pipe *io.WriteCloser // Stdout pipe of ffmpeg process
|
||||
cmd *exec.Cmd // ffmpeg command
|
||||
}
|
||||
|
@ -132,13 +137,14 @@ vidio.Write("output.jpg", w, h, img)
|
|||
|
||||
## Examples
|
||||
|
||||
Copy `input.mp4` to `output.mp4`.
|
||||
Copy `input.mp4` to `output.mp4`. Copy the audio from `input.mp4` to `output.mp4` as well.
|
||||
|
||||
```go
|
||||
video := vidio.NewVideo("input.mp4")
|
||||
options := vidio.Options{
|
||||
fps: video.fps,
|
||||
bitrate: video.bitrate
|
||||
audio: "input.mp4",
|
||||
}
|
||||
|
||||
writer := vidio.NewVideoWriter("output.mp4", video.width, video.height, &options)
|
||||
|
|
7
utils.go
7
utils.go
|
@ -35,7 +35,7 @@ func checkExists(program string) {
|
|||
}
|
||||
|
||||
// Parse ffprobe output to fill in video data.
|
||||
func parseFFprobe(input []byte, video *Video) {
|
||||
func parseFFprobe(input []byte) map[string]string {
|
||||
data := make(map[string]string)
|
||||
for _, line := range strings.Split(string(input), "|") {
|
||||
if strings.Contains(line, "=") {
|
||||
|
@ -45,7 +45,11 @@ func parseFFprobe(input []byte, video *Video) {
|
|||
}
|
||||
}
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
// Adds Video data to the video struct from the ffprobe output.
|
||||
func addVideoData(data map[string]string, video *Video) {
|
||||
video.width = int(parse(data["width"]))
|
||||
video.height = int(parse(data["height"]))
|
||||
video.duration = float64(parse(data["duration"]))
|
||||
|
@ -133,7 +137,6 @@ func parseDevices(buffer []byte) []string {
|
|||
devices = append(devices, pair.name)
|
||||
}
|
||||
}
|
||||
|
||||
return devices
|
||||
}
|
||||
|
||||
|
|
72
video.go
72
video.go
|
@ -18,6 +18,7 @@ type Video struct {
|
|||
duration float64 // Duration of video in seconds.
|
||||
fps float64 // Frames per second.
|
||||
codec string // Codec used for video encoding.
|
||||
audio_codec string // Codec used for audio encoding.
|
||||
pix_fmt string // Pixel format video is stored in.
|
||||
framebuffer []byte // Raw frame data.
|
||||
pipe *io.ReadCloser // Stdout pipe for ffmpeg process.
|
||||
|
@ -33,40 +34,53 @@ func NewVideo(filename string) *Video {
|
|||
// Check if ffmpeg and ffprobe are installed on the users machine.
|
||||
checkExists("ffmpeg")
|
||||
checkExists("ffprobe")
|
||||
// Extract video information with ffprobe.
|
||||
cmd := exec.Command(
|
||||
"ffprobe",
|
||||
"-show_streams",
|
||||
"-select_streams", "v", // Only show video data
|
||||
"-print_format", "compact",
|
||||
"-loglevel", "quiet",
|
||||
filename,
|
||||
)
|
||||
|
||||
pipe, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
ffprobe := func(stype string) map[string]string {
|
||||
// "stype" is stream stype. "v" for video, "a" for audio.
|
||||
// Extract video information with ffprobe.
|
||||
cmd := exec.Command(
|
||||
"ffprobe",
|
||||
"-show_streams",
|
||||
"-select_streams", stype, // Only show video data
|
||||
"-print_format", "compact",
|
||||
"-loglevel", "quiet",
|
||||
filename,
|
||||
)
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Read ffprobe output from Stdout.
|
||||
buffer := make([]byte, 2<<10)
|
||||
total := 0
|
||||
for {
|
||||
n, err := pipe.Read(buffer[total:])
|
||||
total += n
|
||||
if err == io.EOF {
|
||||
break
|
||||
pipe, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
// Read ffprobe output from Stdout.
|
||||
buffer := make([]byte, 2<<10)
|
||||
total := 0
|
||||
for {
|
||||
n, err := pipe.Read(buffer[total:])
|
||||
total += n
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
}
|
||||
// Wait for ffprobe command to complete.
|
||||
if err := cmd.Wait(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return parseFFprobe(buffer[:total])
|
||||
}
|
||||
// Wait for ffprobe command to complete.
|
||||
if err := cmd.Wait(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
videoData := ffprobe("v")
|
||||
audioData := ffprobe("a")
|
||||
|
||||
video := &Video{filename: filename, depth: 3}
|
||||
parseFFprobe(buffer[:total], video)
|
||||
|
||||
addVideoData(videoData, video)
|
||||
video.audio_codec = audioData["codec_name"]
|
||||
|
||||
return video
|
||||
}
|
||||
|
||||
|
|
|
@ -11,29 +11,33 @@ import (
|
|||
)
|
||||
|
||||
type VideoWriter struct {
|
||||
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.
|
||||
filename string // Output filename.
|
||||
audio string // Audio 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.
|
||||
audio_codec string // Codec to encode audio with. Default aac.
|
||||
pipe *io.WriteCloser // Stdout pipe of ffmpeg process.
|
||||
cmd *exec.Cmd // ffmpeg command.
|
||||
}
|
||||
|
||||
// Optional parameters for VideoWriter.
|
||||
type Options struct {
|
||||
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.
|
||||
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.
|
||||
audio string // File path for audio. If no audio, audio=nil.
|
||||
audio_codec string // Codec for audio.
|
||||
}
|
||||
|
||||
// Creates a new VideoWriter struct with default values from the Options struct.
|
||||
|
@ -86,6 +90,20 @@ func NewVideoWriter(filename string, width, height int, options *Options) *Video
|
|||
} else {
|
||||
writer.codec = options.codec
|
||||
}
|
||||
|
||||
if options.audio != "" {
|
||||
if !exists(options.audio) {
|
||||
panic("Audio file does not exist.")
|
||||
}
|
||||
writer.audio = options.audio
|
||||
|
||||
if options.audio_codec == "" {
|
||||
writer.audio_codec = "aac"
|
||||
} else {
|
||||
writer.audio_codec = options.audio_codec
|
||||
}
|
||||
}
|
||||
|
||||
return &writer
|
||||
}
|
||||
|
||||
|
@ -104,11 +122,25 @@ func initVideoWriter(writer *VideoWriter) {
|
|||
"-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.
|
||||
"-vcodec", writer.codec,
|
||||
"-pix_fmt", "yuv420p", // Output is 8-bit RGB, no alpha.
|
||||
}
|
||||
|
||||
gif := strings.HasSuffix(strings.ToLower(writer.filename), ".gif")
|
||||
|
||||
if writer.audio == "" || gif {
|
||||
command = append(command, "-an") // No audio.
|
||||
} else {
|
||||
command = append(
|
||||
command,
|
||||
"-i", writer.audio,
|
||||
)
|
||||
}
|
||||
|
||||
command = append(
|
||||
command,
|
||||
"-vcodec", writer.codec,
|
||||
"-pix_fmt", "yuv420p", // Output is 8-but RGB, no alpha.
|
||||
)
|
||||
|
||||
// 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.
|
||||
|
@ -124,7 +156,7 @@ func initVideoWriter(writer *VideoWriter) {
|
|||
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") {
|
||||
if gif {
|
||||
command = append(
|
||||
command,
|
||||
"-loop", fmt.Sprintf("%d", writer.loop),
|
||||
|
@ -152,6 +184,16 @@ func initVideoWriter(writer *VideoWriter) {
|
|||
}
|
||||
}
|
||||
|
||||
// If audio was included, then specify video and audio channels.
|
||||
if writer.audio != "" {
|
||||
command = append(
|
||||
command,
|
||||
"-acodec", writer.audio_codec,
|
||||
"-map", "0:v:0",
|
||||
"-map", "1:a:0",
|
||||
)
|
||||
}
|
||||
|
||||
command = append(command, writer.filename)
|
||||
cmd := exec.Command("ffmpeg", command...)
|
||||
writer.cmd = cmd
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue