Options.Audio adds all streams

This commit is contained in:
Alex Eidt 2022-09-15 13:30:13 -07:00
parent c9bc141f27
commit bbbb23041a
4 changed files with 81 additions and 107 deletions

View file

@ -28,11 +28,11 @@ Height() int
Depth() int Depth() int
Bitrate() int Bitrate() int
Frames() int Frames() int
Stream() int
Duration() float64 Duration() float64
FPS() float64 FPS() float64
Codec() string Codec() string
AudioCodec() string HasAudio() bool
Stream() int
FrameBuffer() []byte FrameBuffer() []byte
MetaData() map[string]string MetaData() map[string]string
SetFrameBuffer(buffer []byte) error SetFrameBuffer(buffer []byte) error
@ -71,6 +71,7 @@ The `VideoWriter` is used to write frames to a video file. The only required par
vidio.NewVideoWriter(filename string, width, height int, options *vidio.Options) (*vidio.VideoWriter, error) vidio.NewVideoWriter(filename string, width, height int, options *vidio.Options) (*vidio.VideoWriter, error)
FileName() string FileName() string
Audio() string
Width() int Width() int
Height() int Height() int
Bitrate() int Bitrate() int
@ -81,7 +82,6 @@ FPS() float64
Quality() float64 Quality() float64
Codec() string Codec() string
Format() string Format() string
AudioCodec() string
Write(frame []byte) error Write(frame []byte) error
Close() Close()
@ -89,19 +89,22 @@ Close()
```go ```go
type Options struct { type Options struct {
Bitrate int // Bitrate. Bitrate int // Bitrate.
Loop int // For GIFs only. -1=no loop, 0=infinite loop, >0=number of loops. Loop int // For GIFs only. -1=no loop, 0=infinite loop, >0=number of loops.
Delay int // Delay for final frame of GIFs. Delay int // Delay for final frame of GIFs.
Macro int // Macroblock size for determining how to resize frames for codecs. Macro int // Macroblock size for determining how to resize frames for codecs.
FPS float64 // Frames per second for output video. 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. Quality float64 // If bitrate not given, use quality instead. Must be between 0 and 1. 0:best, 1:worst.
Codec string // Codec for video. Codec string // Codec for video.
Format string // Pixel Format for video. Default "rgb24". Format string // Pixel Format for video. Default "rgb24".
Audio string // File path for audio. If no audio, audio="". Audio string // File path for extra stream data.
AudioCodec string // Codec for audio.
} }
``` ```
The `Options.Audio` parameter is intended for users who wish to process a video stream and keep the audio. Instead of having to process the video and store in a file and then combine with the original audio later, the user can simply pass in the original file path via the `Options.Video` parameter. This will combine the video with all other streams in the given file (Audio, Subtitle, Data, and Attachments Streams) and will cut all streams to be the same length. Note that `vidio` is not a audio/video editing library.
Note that this means that adding extra stream data from a file will only work if the filename being written to is a container format.
## Images ## 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. When reading images, an optional `buffer` can be passed in to avoid array reallocation. Vidio provides some convenience functions for reading and writing to images using an array of bytes. Currently, only `png` and `jpeg` formats are supported. When reading images, an optional `buffer` can be passed in to avoid array reallocation.
@ -121,8 +124,8 @@ options := vidio.Options{
FPS: video.FPS(), FPS: video.FPS(),
Bitrate: video.Bitrate(), Bitrate: video.Bitrate(),
} }
if video.AudioCodec() != "" { if video.HasAudio() {
options.Audio = "input.mp4" options.Audio = video.FileName()
} }
writer, _ := vidio.NewVideoWriter("output.mp4", video.Width(), video.Height(), &options) writer, _ := vidio.NewVideoWriter("output.mp4", video.Width(), video.Height(), &options)

View file

@ -17,11 +17,11 @@ type Video struct {
depth int // Depth of frames. depth int // Depth of frames.
bitrate int // Bitrate for video encoding. bitrate int // Bitrate for video encoding.
frames int // Total number of frames. frames int // Total number of frames.
stream int // Stream Index.
duration float64 // Duration of video in seconds. duration float64 // Duration of video in seconds.
fps float64 // Frames per second. fps float64 // Frames per second.
codec string // Codec used for video encoding. codec string // Codec used for video encoding.
audioCodec string // Codec used for audio encoding. hasaudio bool // Flag storing whether file has Audio.
stream int // Stream Index.
framebuffer []byte // Raw frame data. framebuffer []byte // Raw frame data.
metadata map[string]string // Video metadata. metadata map[string]string // Video metadata.
pipe *io.ReadCloser // Stdout pipe for ffmpeg process. pipe *io.ReadCloser // Stdout pipe for ffmpeg process.
@ -55,6 +55,11 @@ func (video *Video) Frames() int {
return video.frames return video.frames
} }
// Returns the zero-indexed video stream index.
func (video *Video) Stream() int {
return video.stream
}
func (video *Video) Duration() float64 { func (video *Video) Duration() float64 {
return video.duration return video.duration
} }
@ -67,15 +72,8 @@ func (video *Video) Codec() string {
return video.codec return video.codec
} }
// Returns the audio codec of the first audio track (if present). func (video *Video) HasAudio() bool {
// Can be used to check if a video has audio. return video.hasaudio
func (video *Video) AudioCodec() string {
return video.audioCodec
}
// Returns the zero-indexed video stream index.
func (video *Video) Stream() int {
return video.stream
} }
func (video *Video) FrameBuffer() []byte { func (video *Video) FrameBuffer() []byte {
@ -132,22 +130,14 @@ func NewVideoStreams(filename string) ([]*Video, error) {
return nil, err return nil, err
} }
audioCodec := ""
if len(audioData) > 0 {
// Look at the first audio stream only.
if ac, ok := audioData[0]["codec_name"]; ok {
audioCodec = ac
}
}
streams := make([]*Video, len(videoData)) streams := make([]*Video, len(videoData))
for i, data := range videoData { for i, data := range videoData {
video := &Video{ video := &Video{
filename: filename, filename: filename,
depth: 3, depth: 3,
audioCodec: audioCodec, stream: i,
stream: i, hasaudio: len(audioData) > 0,
metadata: data, metadata: data,
} }
video.addVideoData(data) video.addVideoData(data)

View file

@ -12,41 +12,40 @@ import (
) )
type VideoWriter struct { type VideoWriter struct {
filename string // Output filename. filename string // Output filename.
audio string // Audio filename. audio string // Extra stream data filename.
width int // Frame width. width int // Frame width.
height int // Frame height. height int // Frame height.
bitrate int // Output video bitrate. bitrate int // Output video bitrate.
loop int // Number of times for GIF to loop. loop int // Number of times for GIF to loop.
delay int // Delay of final frame of GIF. Default -1 (same delay as previous frame). 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. macro int // Macroblock size for determining how to resize frames for codecs.
fps float64 // Frames per second for output video. Default 25. fps float64 // Frames per second for output video. Default 25.
quality float64 // Used if bitrate not given. Default 0.5. quality float64 // Used if bitrate not given. Default 0.5.
codec string // Codec to encode video with. Default libx264. codec string // Codec to encode video with. Default libx264.
format string // Output format. Default rgb24. format string // Output format. Default rgb24.
audioCodec string // Codec to encode audio with. Default aac. pipe *io.WriteCloser // Stdout pipe of ffmpeg process.
pipe *io.WriteCloser // Stdout pipe of ffmpeg process. cmd *exec.Cmd // ffmpeg command.
cmd *exec.Cmd // ffmpeg command.
} }
// Optional parameters for VideoWriter. // Optional parameters for VideoWriter.
type Options struct { type Options struct {
Bitrate int // Bitrate. Bitrate int // Bitrate.
Loop int // For GIFs only. -1=no loop, 0=infinite loop, >0=number of loops. Loop int // For GIFs only. -1=no loop, 0=infinite loop, >0=number of loops.
Delay int // Delay for final frame of GIFs. Delay int // Delay for final frame of GIFs.
Macro int // Macroblock size for determining how to resize frames for codecs. Macro int // Macroblock size for determining how to resize frames for codecs.
FPS float64 // Frames per second for output video. 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. Quality float64 // If bitrate not given, use quality instead. Must be between 0 and 1. 0:best, 1:worst.
Codec string // Codec for video. Codec string // Codec for video.
Format string // Pixel Format for video. Default "rgb24". Format string // Pixel Format for video. Default "rgb24".
Audio string // File path for audio. If no audio, audio="". Audio string // File path for extra stream data.
AudioCodec string // Codec for audio.
} }
func (writer *VideoWriter) FileName() string { func (writer *VideoWriter) FileName() string {
return writer.filename return writer.filename
} }
// File used to fill in extra stream data.
func (writer *VideoWriter) Audio() string { func (writer *VideoWriter) Audio() string {
return writer.audio return writer.audio
} }
@ -91,10 +90,6 @@ func (writer *VideoWriter) Format() string {
return writer.format return writer.format
} }
func (writer *VideoWriter) AudioCodec() string {
return writer.audioCodec
}
// Creates a new VideoWriter struct with default values from the Options struct. // Creates a new VideoWriter struct with default values from the Options struct.
func NewVideoWriter(filename string, width, height int, options *Options) (*VideoWriter, error) { func NewVideoWriter(filename string, width, height int, options *Options) (*VideoWriter, error) {
// Check if ffmpeg is installed on the users machine. // Check if ffmpeg is installed on the users machine.
@ -102,15 +97,16 @@ func NewVideoWriter(filename string, width, height int, options *Options) (*Vide
return nil, err return nil, err
} }
writer := &VideoWriter{filename: filename}
if options == nil { if options == nil {
options = &Options{} options = &Options{}
} }
writer.width = width writer := &VideoWriter{
writer.height = height filename: filename,
writer.bitrate = options.Bitrate width: width,
height: height,
bitrate: options.Bitrate,
}
// 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.
@ -161,23 +157,9 @@ func NewVideoWriter(filename string, width, height int, options *Options) (*Vide
if options.Audio != "" { if options.Audio != "" {
if !exists(options.Audio) { if !exists(options.Audio) {
return nil, fmt.Errorf("audio file %s does not exist", options.Audio) return nil, fmt.Errorf("file %s does not exist", options.Audio)
} }
audioData, err := ffprobe(options.Audio, "a")
if err != nil {
return nil, err
} else if len(audioData) == 0 {
return nil, fmt.Errorf("given audio file %s has no audio", options.Audio)
}
writer.audio = options.Audio writer.audio = options.Audio
if options.AudioCodec == "" {
writer.audioCodec = "aac"
} else {
writer.audioCodec = options.AudioCodec
}
} }
return writer, nil return writer, nil
@ -202,12 +184,22 @@ func (writer *VideoWriter) init() error {
gif := strings.HasSuffix(strings.ToLower(writer.filename), ".gif") gif := strings.HasSuffix(strings.ToLower(writer.filename), ".gif")
if writer.audio == "" || gif { // Assumes "writer.file" is a container format.
command = append(command, "-an") // No audio. // gif check is included since they are a common format.
} else { if writer.audio != "" && !gif {
command = append( command = append(
command, command,
"-i", writer.audio, "-i", writer.audio,
"-map", "0:v:0",
"-map", "1:a?", // Add Audio streams if present.
"-c:a", "copy",
"-map", "1:s?", // Add Subtitle streams if present.
"-c:s", "copy",
"-map", "1:d?", // Add Data streams if present.
"-c:d", "copy",
"-map", "1:t?", // Add Attachments streams if present.
"-c:t", "copy",
"-shortest", // Cut longest streams to match audio duration.
) )
} }
@ -261,16 +253,6 @@ func (writer *VideoWriter) init() error {
} }
} }
// If audio was included, then specify video and audio channels.
if writer.audio != "" && !gif {
command = append(
command,
"-acodec", writer.audioCodec,
"-map", "0:v:0",
"-map", "1:a:0",
)
}
command = append(command, writer.filename) command = append(command, writer.filename)
cmd := exec.Command("ffmpeg", command...) cmd := exec.Command("ffmpeg", command...)
writer.cmd = cmd writer.cmd = cmd

View file

@ -45,7 +45,6 @@ func TestVideoMetaData(t *testing.T) {
assertEquals(video.duration, 3.366667) assertEquals(video.duration, 3.366667)
assertEquals(video.fps, float64(30)) assertEquals(video.fps, float64(30))
assertEquals(video.codec, "h264") assertEquals(video.codec, "h264")
assertEquals(video.audioCodec, "aac")
assertEquals(video.stream, 0) assertEquals(video.stream, 0)
assertEquals(len(video.framebuffer), 0) assertEquals(len(video.framebuffer), 0)
@ -83,7 +82,7 @@ func TestVideoFrame(t *testing.T) {
} }
func TestVideoWriting(t *testing.T) { func TestVideoWriting(t *testing.T) {
testWriting := func(input, output string, audio bool) { testWriting := func(input, output string) {
video, err := NewVideo(input) video, err := NewVideo(input)
if err != nil { if err != nil {
panic(err) panic(err)
@ -93,8 +92,8 @@ func TestVideoWriting(t *testing.T) {
Bitrate: video.Bitrate(), Bitrate: video.Bitrate(),
Codec: video.Codec(), Codec: video.Codec(),
} }
if audio { if video.HasAudio() {
options.Audio = input options.Audio = video.FileName()
} }
writer, err := NewVideoWriter(output, video.width, video.height, &options) writer, err := NewVideoWriter(output, video.width, video.height, &options)
@ -112,9 +111,9 @@ func TestVideoWriting(t *testing.T) {
os.Remove(output) os.Remove(output)
} }
testWriting("test/koala.mp4", "test/koala-out.mp4", true) testWriting("test/koala.mp4", "test/koala-out.mp4")
fmt.Println("Video Writing (with Audio) Test Passed") fmt.Println("Video Writing (with Audio) Test Passed")
testWriting("test/koala-noaudio.mp4", "test/koala-noaudio-out.mp4", false) testWriting("test/koala-noaudio.mp4", "test/koala-noaudio-out.mp4")
fmt.Println("Video Writing (without Audio) Test Passed") fmt.Println("Video Writing (without Audio) Test Passed")
} }