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
Bitrate() int
Frames() int
Stream() int
Duration() float64
FPS() float64
Codec() string
AudioCodec() string
Stream() int
HasAudio() bool
FrameBuffer() []byte
MetaData() map[string]string
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)
FileName() string
Audio() string
Width() int
Height() int
Bitrate() int
@ -81,7 +82,6 @@ FPS() float64
Quality() float64
Codec() string
Format() string
AudioCodec() string
Write(frame []byte) error
Close()
@ -89,19 +89,22 @@ Close()
```go
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.
Format string // Pixel Format for video. Default "rgb24".
Audio string // File path for audio. If no audio, audio="".
AudioCodec string // Codec for audio.
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.
Format string // Pixel Format for video. Default "rgb24".
Audio string // File path for extra stream data.
}
```
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
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(),
Bitrate: video.Bitrate(),
}
if video.AudioCodec() != "" {
options.Audio = "input.mp4"
if video.HasAudio() {
options.Audio = video.FileName()
}
writer, _ := vidio.NewVideoWriter("output.mp4", video.Width(), video.Height(), &options)

View file

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

View file

@ -12,41 +12,40 @@ import (
)
type VideoWriter struct {
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.
format string // Output format. Default rgb24.
audioCodec string // Codec to encode audio with. Default aac.
pipe *io.WriteCloser // Stdout pipe of ffmpeg process.
cmd *exec.Cmd // ffmpeg command.
filename string // Output filename.
audio string // Extra stream data 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.
format string // Output format. Default rgb24.
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.
Format string // Pixel Format for video. Default "rgb24".
Audio string // File path for audio. If no audio, audio="".
AudioCodec string // Codec for audio.
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.
Format string // Pixel Format for video. Default "rgb24".
Audio string // File path for extra stream data.
}
func (writer *VideoWriter) FileName() string {
return writer.filename
}
// File used to fill in extra stream data.
func (writer *VideoWriter) Audio() string {
return writer.audio
}
@ -91,10 +90,6 @@ func (writer *VideoWriter) Format() string {
return writer.format
}
func (writer *VideoWriter) AudioCodec() string {
return writer.audioCodec
}
// Creates a new VideoWriter struct with default values from the Options struct.
func NewVideoWriter(filename string, width, height int, options *Options) (*VideoWriter, error) {
// 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
}
writer := &VideoWriter{filename: filename}
if options == nil {
options = &Options{}
}
writer.width = width
writer.height = height
writer.bitrate = options.Bitrate
writer := &VideoWriter{
filename: filename,
width: width,
height: height,
bitrate: options.Bitrate,
}
// Default Parameter options logic from:
// 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 !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
if options.AudioCodec == "" {
writer.audioCodec = "aac"
} else {
writer.audioCodec = options.AudioCodec
}
}
return writer, nil
@ -202,12 +184,22 @@ func (writer *VideoWriter) init() error {
gif := strings.HasSuffix(strings.ToLower(writer.filename), ".gif")
if writer.audio == "" || gif {
command = append(command, "-an") // No audio.
} else {
// Assumes "writer.file" is a container format.
// gif check is included since they are a common format.
if writer.audio != "" && !gif {
command = append(
command,
"-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)
cmd := exec.Command("ffmpeg", command...)
writer.cmd = cmd

View file

@ -45,7 +45,6 @@ func TestVideoMetaData(t *testing.T) {
assertEquals(video.duration, 3.366667)
assertEquals(video.fps, float64(30))
assertEquals(video.codec, "h264")
assertEquals(video.audioCodec, "aac")
assertEquals(video.stream, 0)
assertEquals(len(video.framebuffer), 0)
@ -83,7 +82,7 @@ func TestVideoFrame(t *testing.T) {
}
func TestVideoWriting(t *testing.T) {
testWriting := func(input, output string, audio bool) {
testWriting := func(input, output string) {
video, err := NewVideo(input)
if err != nil {
panic(err)
@ -93,8 +92,8 @@ func TestVideoWriting(t *testing.T) {
Bitrate: video.Bitrate(),
Codec: video.Codec(),
}
if audio {
options.Audio = input
if video.HasAudio() {
options.Audio = video.FileName()
}
writer, err := NewVideoWriter(output, video.width, video.height, &options)
@ -112,9 +111,9 @@ func TestVideoWriting(t *testing.T) {
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")
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")
}