Pipe FFMPEG's output to S3 PutObject - go

I'm trying to replicate the AWS Lambda solution of the following tutorial with Go AWS SDK. My process is a little bit more complicated though because I want to be able to do the following:
Convert the video to h265
Transpose it to m3u8 (HLS)
Upload the h265 video directly from pipe (without local copy)
Upload the m3u8 and ts files (apparently impossible to do without local copy, not sure though)
I came up with the following:
func upload(video_name string) {
var compress_args = []string{
"-loglevel", "fatal",
"-i", video_name + ".mp4",
"-vcodec", "libx265",
"-x265-params", "log-level=fatal",
"-crf", "28",
"-f", "mp4",
"-movflags", "frag_keyframe+empty_moov",
"-", // pipes output to stdout
}
var transpose_args = []string{
"-loglevel", "fatal",
"-i", "-", // Read input from stdin
"-vcodec", "libx265",
"-x265-params", "log-level=fatal",
"-crf", "28",
"-start_number", "0",
"-hls_time", "1",
"-hls_list_size", "0",
"-f", "hls", "tmp/" + video_name + ".m3u8",
}
cmd := exec.Command("ffmpeg", compress_args...)
cmd2 := exec.Command("tee", "new_" + video_name + ".mp4") // how to improve that ?
cmd3 := exec.Command("ffmpeg", transpose_args...)
cmd.Stderr = os.Stderr
cmd2.Stderr = os.Stderr
cmd3.Stderr = os.Stderr
cmd2.Stdin, _ = cmd.StdoutPipe()
cmd3.Stdin, _ = cmd2.StdoutPipe()
cmd3.Stdout = os.Stdout
cmd.Start()
cmd2.Start()
cmd3.Start()
cmd.Wait()
cmd2.Wait()
cmd3.Wait()
}
For now, what this does is that it pipes the compression, the tee and the transpose commands. The tee creates a local copy of the .mp4 and relay to the compression command. Now, I would like to be able to pass tee's output to the Body parameter for s3.PutObjectInput.
Should I use a Named Pipe, let tee write on it and then somehow pass that to the PutObjectInput? is it even possible? what are the other possibilities ?
Edit
I made the compressed file upload work by using named pipes and not waiting for the commands to finish (original idea). Here is the new code:
func upload(video_name string) {
// ARGS
syscall.Mkfifo("pipe", 0644)
// CMDS and STDOUT/STDIN redirections
named_pipe, _ := os.OpenFile("pipe", os.O_RDONLY|syscall.O_NONBLOCK, 0600)
defer named_pipe.Close()
syscall.SetNonblock(int(named_pipe.Fd()), false)
sess := session.Must(session.NewSession(&aws.Config{
Region: aws.String(region),
}))
uploader := s3manager.NewUploader(sess, func(u *s3manager.Uploader) {
u.PartSize = 5 * 1024 * 1024 // 5MB
})
compress_cmd.Start()
tee_cmd.Start()
transpose_cmd.Start()
_, err := uploader.UploadWithContext(context.Background(), &s3manager.UploadInput{
Bucket: aws.String(bucket),
Key: aws.String(video_name + ".mp4"),
Body: &reader{named_pipe},
})
}
where reader is:
type reader struct {
r io.Reader
}
func (r *reader) Read(p []byte) (int, error) {
return r.r.Read(p)
}
However, is there a way to wait for the whole process to finish before uploading the HLS output?
I tried waiting for transpose_cmd to finish after the UploadWithContext but this makes the Uploader quit prematurely.

Related

How to redirect os.Stdout to io.MultiWriter() in Go?

I am writing a Test function that testing go program interacting with a command line program. That is
os.Stdio -> cmd.Stdin
cmd.Stdin -> os.Stdin
I could use pipe to connect those io, but I would have a log of the data passing the pipe.
I tried to use io.MultiWriter but it is not a os.file object and cannot assign to os.Stdout.
I have found some sample which use a lot of pipe and io.copy. but as io.copy is not interactive. How could I connect the Stdout to a io.MultiWriter with pipe?
logfile, err := os.Create("stdout.log")
r, w, _ := os.Pipe()
mw := io.MultiWriter(os.Stdout, logfile, w)
cmd.Stdin = r
os.Stdout = mw // <- error in this line
The error message like
cannot use mw (variable of type io.Writer) as type *os.File in assignment:
As an alternative solution, you can mimic the MultiWriter using a separate func to read what has been written to the stdout, capture it, then write it to the file and also to the original stdout.
package main
import (
"bufio"
"fmt"
"os"
"time"
)
func main() {
originalStdout := os.Stdout //Backup of the original stdout
r, w, _ := os.Pipe()
os.Stdout = w
//Use a separate goroutine for non-blocking
go func() {
f, _ := os.Create("stdout.log")
defer f.Close()
scanner := bufio.NewScanner(r)
for scanner.Scan() {
s := scanner.Text() + "\r\n"
f.WriteString(s)
originalStdout.WriteString(s)
}
}()
//Test
c := time.NewTicker(time.Second)
for {
select {
case <-c.C:
fmt.Println(time.Now())
fmt.Fprintln(os.Stderr, "This is on Stderr")
}
}
}
I figure out a work around with pipe, TeeReader and MultiWriter
The setup is a test function which test the interaction of go program interact with a python program though stdin and stdout.
main.stdout -> pipe -> TeeReader--> (client.stdin, MultiWriter(log, stdout))
client.stdout -> MultiWriter(pipe --> main.stdin, logfile, stdout )
I will try to add more explanation later
func Test_Interactive(t *testing.T) {
var tests = []struct {
file string
}{
{"Test_Client.py"},
}
for tc, tt := range tests {
fmt.Println("---------------------------------")
fmt.Printf("Test %d, Test Client:%v\n", tc+1, tt.file)
fmt.Println("---------------------------------")
// Define external program
client := exec.Command("python3", tt.file)
// Define log file
logfile, err := os.Create(tt.file + ".log")
if err != nil {
panic(err)
}
defer logfile.Close()
out := os.Stdout
defer func() { os.Stdout = out }() // Restore original Stdout
in := os.Stdin
defer func() { os.Stdin = in }() // Restore original Stdin
// Create pipe connect os.Stdout to client.Stdin
gr, gw, _ := os.Pipe()
// Connect os.Stdout to writer side of pipe
os.Stdout = gw
// Create MultiWriter to write to logfile and os.Stdout at the same time
gmw := io.MultiWriter(out, logfile)
// Create a tee reader read from reader side of the pipe and flow to the MultiWriter
// Repleace the cmd.Stdin with TeeReader
client.Stdin = io.TeeReader(gr, gmw)
// Create a pipe to connect client.Stdout to os.Stdin
cr, cw, _ := os.Pipe()
// Create MultWriter to client stdout
cmw := io.MultiWriter(cw, logfile, out)
client.Stdout = cmw
// Connect os stdin to another end of the pipe
os.Stdin = cr
// Start Client
client.Start()
// Start main
main()
// Check Testing program error
if err := client.Process.Release(); err != nil {
if exiterr, ok := err.(*exec.ExitError); ok {
if status, ok := exiterr.Sys().(syscall.WaitStatus); ok {
log.Printf("Exit Status: %d", status.ExitStatus())
t.Errorf("Tester return error\n")
}
} else {
log.Fatalf("cmd.Wait: %v", err)
t.Errorf("Tester return error\n")
}
}
}
}

How can I get the file output of the ffmpeg command?

I used the following code to generate a .mp4 file:
args := []string{"-i", "rtsp://zigong.stream.xl02.cn:557/HongTranSvr?DevId=1b038d27-858c-46a1-b803-a2984af343df&Session=1b038d27-858c-46a1-b803-a2984af343df",
"-vcodec", "copy", "-t", "5", "-y", "output.mp4"}
command := exec.Command("ffmpeg", args...)
bytes, err := command.Output()
if err != nil {
fmt.Println(err)
return
}
fmt.Println(len(bytes)) //result:0
Is there a way to get the []byte format of the output.mp4?
Use the pipe:1 for the ouput instead of a file path. The example below shows you how to access to the FFmpeg command output and the generated file.
var stdOut bytes.Buffer
var stdErr bytes.Buffer
args := []string{"-i", "test.mp4", "-vcodec", "copy", "-t", "5", "-f", "mp4", "pipe:1"}
command := exec.Command("ffmpeg", args...)
command.Stdout = bufio.NewWriter(&stdOut)
command.Stderr = bufio.NewWriter(&stdErr)
if err := command.Run(); err != nil {
log.Println(err)
log.Println(string(stdErr.Bytes()))
return
}
// FFmpeg log
log.Println(string(stdErr.Bytes()))
// data from FFmpeg
log.Println(len(stdOut.Bytes()))
The problem with mp4 is that FFmpeg can not directly output it, because the metadata of the file is written at the beginning of the file but must be written at the end of the encoding. Follow this link for more information: FFMPEG: Transmux mpegts to mp4 gives error: muxer does not support non seekable output

Decoding bmp images from stdout using go image library

I am trying to decode a bmp image using the image and golang.org/x/image/bmp libraries. The image is output by ffmpeg into stdout. This is the code to get the frame:
cmd := exec.Command("ffmpeg", "-accurate_seek", "-ss", strconv.Itoa(index), "-i",
filename, "-frames:v", "1", "-hide_banner", "-loglevel", "0", "pipe:.bmp")
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
log.Fatal(err)
}
o := bufio.NewReader(&out)
and then I decode it using img, _, err := image.Decode(o)
However this gives an error of "image: unknown format". I have already registered the bmp format in the main method, and I have successfully decoded actual BMP files from disk previously, just not from stdout.
I have tried just using bmp.Decode instead of image.Decode but this just gives the error "EOF".
I thought maybe I was not getting the stdout in the correct way, but if I just write it straight to a file:
o := bufio.NewReader(&out)
outputfile, err := os.Create("test.bmp")
if err != nil {
log.Fatal(err)
}
defer outputfile.Close()
io.Copy(outputfile, o)
then it works fine and I can open it.
Edit: code
Update: turns out the issue was -ss takes time not frame index.
I tried to reproduce the issue but it seems to work for me. Maybe add cmd.Stderr = os.Stderr to see if ffmpeg output gives come clue. Could you also post a fully runnable example?
package main
import (
"bytes"
"image"
"log"
"os"
"os/exec"
_ "golang.org/x/image/bmp"
)
func main() {
cmd := exec.Command(
"docker", "run",
"--rm",
"mwader/static-ffmpeg",
"-ss", "5",
"-f", "lavfi",
"-i", "testsrc",
"-frames:v", "1",
"-f", "image2",
"-vcodec", "bmp",
"pipe:1",
)
var out bytes.Buffer
cmd.Stderr = os.Stderr
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
log.Fatal(err)
}
img, imgFormat, imgErr := image.Decode(&out)
log.Printf("img.Bounds(): %#+v\n", img.Bounds())
log.Printf("imgFormat: %#+v\n", imgFormat)
log.Printf("imgErr: %#+v\n", imgErr)
}

Piping raw []byte video to ffmpeg - Go

I have a video directly from the http body in a [] byte format:
//Parsing video
videoData, err := ioutil.ReadAll(r.Body)
if err != nil {
w.WriteHeader(UPLOAD_ERROR)
w.Write([]byte("Error uploading the file"))
return
}
and I need a single frame of the video and convert it to a png. This is how someone would do it with a static and encoded file using ffmpeg:
filename := "test.mp4"
width := 640
height := 360
cmd := exec.Command("ffmpeg", "-i", filename, "-vframes", "1", "-s", fmt.Sprintf("%dx%d", width, height), "-f", "singlejpeg", "-")
var buffer bytes.Buffer
cmd.Stdout = &buffer
if cmd.Run() != nil {
panic("could not generate frame")
}
How can I achieve the same with a raw video?
A user from reddit told me that I might achieve this with https://ffmpeg.org/ffmpeg-protocols.html#pipe but I was unable to find any resources.
Any help is appreciated, thanks.
(EDIT: I tried to pipe the []byte array to ffmpeg now, but ffmpeg does not fill in my buffer:
width := 640
height := 360
log.Print("Size of the video: ", len(videoData))
cmd := exec.Command("ffmpeg", "-i", "pipe:0", "-vframes", "1", "-s", fmt.Sprintf("%dx%d", width, height), "-f", "singlejpeg", "-")
cmd.Stdin = bytes.NewReader(videoData)
var imageBuffer bytes.Buffer
cmd.Stdout = &imageBuffer
err := cmd.Run()
if err != nil {
log.Panic("ERROR")
}
imageBytes := imageBuffer.Bytes()
log.Print("Size of the image: ", len(imageBytes))
But I get following error:
[mov,mp4,m4a,3gp,3g2,mj2 # 0x7ff05d002600]stream 0, offset 0x5ded: partial file
pipe:0: Invalid data found when processing input
Finishing stream 0:0 without any data written to it.
frame= 0 fps=0.0 q=0.0
Lsize= 0kB time=-577014:32:22.77 bitrate= -0.0kbits/s speed=N/A
video:0kB audio:0kB subtitle:0kB other streams:0kB global headers:0kB
muxing overhead: unknown
Output file is empty, nothing was encoded (check -ss / -t / -frames parameters if used)
I need a single frame of the video and convert it to a png. This is
how someone would do it with ffmpeg.
There is a popular go library that is exactly made for what you search for:
https://github.com/bakape/thumbnailer
thumbnailDimensions := thumbnailer.Dims{Width: 320, Height: 130}
thumbnailOptions := thumbnailer.Options{JPEGQuality:100, MaxSourceDims:thumbnailer.Dims{}, ThumbDims:thumbnailDimensions, AcceptedMimeTypes: nil}
sourceData, thumbnail, err := thumbnailer.ProcessBuffer(videoData, thumbnailOptions)
imageBytes := thumbnail.Image.Data
They use ffmpeg under the hood, but removes the abstraction for you.
Please check this out, I wrote this to down sample mp3 files to 128k bitrate and it should work with you. Please change the command which suits you:
package main
import (
"bytes"
"io/ioutil"
"log"
"os"
"os/exec"
)
func check(err error) {
if err != nil {
log.Fatalln(err)
}
}
func main() {
file, err := os.Open("test.mp3") // open file
check(err)
defer file.Close()
buf, err := ioutil.ReadAll(file)
check(err)
cmd := exec.Command("ffmpeg", "-y", // Yes to all
//"-hide_banner", "-loglevel", "panic", // Hide all logs
"-i", "pipe:0", // take stdin as input
"-map_metadata", "-1", // strip out all (mostly) metadata
"-c:a", "libmp3lame", // use mp3 lame codec
"-vsync", "2", // suppress "Frame rate very high for a muxer not efficiently supporting it"
"-b:a", "128k", // Down sample audio birate to 128k
"-f", "mp3", // using mp3 muxer (IMPORTANT, output data to pipe require manual muxer selecting)
"pipe:1", // output to stdout
)
resultBuffer := bytes.NewBuffer(make([]byte, 5*1024*1024)) // pre allocate 5MiB buffer
cmd.Stderr = os.Stderr // bind log stream to stderr
cmd.Stdout = resultBuffer // stdout result will be written here
stdin, err := cmd.StdinPipe() // Open stdin pipe
check(err)
err = cmd.Start() // Start a process on another goroutine
check(err)
_, err = stdin.Write(buf) // pump audio data to stdin pipe
check(err)
err = stdin.Close() // close the stdin, or ffmpeg will wait forever
check(err)
err = cmd.Wait() // wait until ffmpeg finish
check(err)
outputFile, err := os.Create("out.mp3") // create new file
check(err)
defer outputFile.Close()
_, err = outputFile.Write(resultBuffer.Bytes()) // write result buffer to file
check(err)
}
Reference: https://gist.github.com/aperture147/ad0f5b965912537d03b0e851bb95bd38

stream stdout from other program without for loop

I have a program that compile and run another program and pipe the stdout to itself for printing, since that program doesn't terminate so I need to stream it's stdout
// boilerplate ommited
func stream(stdoutPipe io.ReadCloser) {
buffer := make([]byte, 100, 1000)
for ;; {
n, err := stdoutPipe.Read(buffer)
if err == io.EOF {
stdoutPipe.Close()
break
}
buffer = buffer[0:n]
os.Stdout.Write(buffer)
}
}
func main() {
command := exec.Command("go", "run", "my-program.go")
stdoutPipe, _ := command.StdoutPipe()
_ = command.Start()
go stream(stdoutPipe)
do_my_own_thing()
command.Wait()
}
It works, but how do I do the same without repeatedly checking with a for loop, is there a library function that does the same thing?
You can give the exec.Cmd an io.Writer to use as stdout. The variable your own program uses for stdout (os.Stdout) is also an io.Writer.
command := exec.Command("go", "run", "my-program.go")
command.Stdout = os.Stdout
command.Start()
command.Wait()

Resources