diff --git a/README.md b/README.md index 65ec3be..c4f37fa 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/video.go b/video.go index b5715b4..46e2d58 100644 --- a/video.go +++ b/video.go @@ -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) diff --git a/videowriter.go b/videowriter.go index 68edf6c..617323e 100644 --- a/videowriter.go +++ b/videowriter.go @@ -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 diff --git a/vidio_test.go b/vidio_test.go index 5305826..343e766 100644 --- a/vidio_test.go +++ b/vidio_test.go @@ -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") }