3

How do I pipe an HTTP response like in NodeJS. Here is the snippet I am using in NodeJS:

request({
  url: audio_file_url,
}).pipe(ffmpeg_process.stdin);

How can I achieve the same result in Go?

I am trying to pipe a audio stream from HTTP into an FFmpeg process so that it converts it on the fly and returns the converted file back to the client.

Just so its clear to everyone here is my source code so far:

func encodeAudio(w http.ResponseWriter, req *http.Request) {
    path, err := exec.LookPath("youtube-dl")
    if err != nil {
        log.Fatal("LookPath: ", err)
    }
    path_ff, err_ff := exec.LookPath("ffmpeg")
    if err != nil {
        log.Fatal("LookPath: ", err_ff)
    }

    streamLink := exec.Command(path,"-f", "140", "-g", "https://www.youtube.com/watch?v=VIDEOID")

    var out bytes.Buffer
    streamLink.Stdout = &out
    cmdFF := exec.Command(path_ff, "-i", "pipe:0", "-acodec", "libmp3lame", "-f", "mp3", "-")
    resp, err := http.Get(out.String())
    if err != nil {
        log.Fatal(err)
    }
    // pr, pw := io.Pipe()
    defer resp.Body.Close()
    cmdFF.Stdin = resp.Body
    cmdFF.Stdout = w
    streamLink.Run()
    //get ffmpeg running in another goroutine to receive data
    errCh := make(chan error, 1)
    go func() {
        errCh <- cmdFF.Run()
    }()

    // close the pipeline to signal the end of the stream
    // pw.Close()
    // pr.Close()

    // check for an error from ffmpeg
    if err := <-errCh; err != nil {
        // ff error
    }
}

Error: 2014/07/29 23:04:02 Get : unsupported protocol scheme ""

viperfx
  • 327
  • 5
  • 17
  • Are you certain your http client will send and receive simultaneously? Most won't do that, and you're going to block once the buffers are all filled up. – JimB Jul 29 '14 at 20:05
  • @JimB The audio is from another source. I just need to process that on the server and send the ffmpeg response back to the client. Does what you say still apply? – viperfx Jul 29 '14 at 20:20
  • ah, I see (not familiar with node, so I didn't really look at that request very closely). I thought the client was sending the source. That's fairly straightforward in go, what have to tried? – JimB Jul 29 '14 at 20:27
  • OK, I have edit my answer to show my progress so far @JimB – viperfx Jul 29 '14 at 20:33
  • ok, thanks. I apparently misunderstood again :/ – JimB Jul 29 '14 at 20:35
  • For the record, you don't need to use `exec.LookPath`, if `exec.LookPath` can find the executable, `exec.Command` will find it too. – OneOfOne Jul 29 '14 at 20:41
  • I'm not familiar with martini, and I'm not sure how to access the request struct directly in it, I'm leaving my answer until someone else who knows Martini answers. IMHO Martini is just ugly, and with the overhead from all the reflection on functions you might as well stick to nodejs. – OneOfOne Jul 29 '14 at 20:48
  • What does `out.String()` print? – OneOfOne Jul 29 '14 at 23:00
  • you're checking `out` before you run the command. There's also a lot of superfluous stuff here. A little reading of the docs might help sort out some of your confusion. – JimB Jul 30 '14 at 00:30

2 Answers2

3

Here's a possible answer using a standard http handler function. I don't have the programs to test this directly, but it does work with some simple shell commands standing in as a proxy.

func encodeAudio(w http.ResponseWriter, req *http.Request) {

    streamLink := exec.Command("youtube-dl", "-f", "140", "-g", "https://www.youtube.com/watch?v=VIDEOID")
    out, err := streamLink.Output()
    if err != nil {
        log.Fatal(err)
    }

    cmdFF := exec.Command("ffmpeg", "-i", "pipe:0", "-acodec", "libmp3lame", "-f", "mp3", "-")
    resp, err := http.Get(string(out))
    if err != nil {
        log.Fatal(err)
    }

    defer resp.Body.Close()
    cmdFF.Stdin = resp.Body

    cmdFF.Stdout = w
    if err := cmdFF.Run(); err != nil {
        log.Fatal(err)
    }
}
JimB
  • 104,193
  • 13
  • 262
  • 255
  • That wouldn't work, `youtube-dl` just prints an url, that's why he needs to use `http.Get` on the output of `steamLink`. – OneOfOne Jul 29 '14 at 21:36
  • thanks @OneOfOne ... and I've yet to decipher this question! I assumed "pipe the output" meant exactly that. – JimB Jul 29 '14 at 21:43
  • Took me a while too, I'm just familiar with `youtube-dl`. – OneOfOne Jul 29 '14 at 21:47
  • Haha I am sorry for the confusion guys. I think we are really close though. The only thing missing from that is the http.Get(). The response of this needs to be piped to the cmdFF.Stdin. – viperfx Jul 29 '14 at 21:54
  • I started doing the rest of the code and I got an error at the end. I have updated my original post. @JimB – viperfx Jul 29 '14 at 22:21
  • @viperfx: should be closer to what you're looking for now – JimB Jul 30 '14 at 00:59
  • @JimB Ah I was very close. Thank you. OK so, I have added the correct headers and now its working - its serving an audio file that plays in the browser. I am however running into an issue. The audio file is stopping around midway. Before it was stopping before there was an incorrect content length but now I have provided the correct content length of the mp3 and it still stops. I receive no errors in the server. No ideas? – viperfx Jul 30 '14 at 12:16
  • no idea. If the connection is getting closed, either the download failed, or ffmpeg exited. Try checking the headers from the stream download, or the stderr output of the ffmpeg process. – JimB Jul 30 '14 at 13:20
  • @JimB Thanks alot for your help. I have got everything sorted out now. The problem I had was actually to do with the Content Length I was setting. I also had an issue where two requests were coming through, I just had to check for the request with the `Range` header and then start piping the audio. To improve the current version, how can I go about making more scalable? Is there a way to concurrently pipe the data to ffmpeg before its fully downloaded? – viperfx Jul 30 '14 at 17:28
  • @viperfx: That's what you're doing already. `http.Response.Body` is a stream. – JimB Jul 30 '14 at 17:54
  • @JimB Ah okay. So the use of goroutines and concurrency patterns would not help much since most of the actions need to happen sequentially? It seems to work when I requested two at the same time, that is good enough for now :). – viperfx Jul 30 '14 at 19:42
  • @viperfx: It is already fully concurrent. The handler happens in a goroutine of its own, and the http client has its own goroutines for reading/writing on the connection. – JimB Jul 30 '14 at 20:31
1

http.Request.Body is an io.ReadCloser, so you could pipe it into exec.Cmd.Stdin:

func Handler(rw http.ResponseWriter, req *http.Request) {
    cmd := exec.Command("ffmpeg", other, args, ...)
    cmd.Stdin = req.Body
    go func() {
        defer req.Body.Close()

        if err := cmd.Run(); err != nil {
            // do something
        }
    }()
    //redirect the user and check for progress?
}

//edit I misunderstood the question, however the answer still stands, the http.Get version:

http.Response.Body is an io.ReadCloser just like http.Request.Body.

func EncodeUrl(url, fn string) error {
    resp, err := http.Get(url)
    if err != nil {
        return err
    }
    defer resp.Body.Close()
    cmd := exec.Command("ffmpeg", ......, fn)
    cmd.Stdin = resp.Body
    return cmd.Run()
}

//edit2:

this should work, according to the martini documentation, but again, I highly recommend learning to use ServeMux or at least use Gorilla.

m := martini.Classic()
m.Get("/stream/:ytid", func(params martini.Params, rw http.ResponseWriter,
                            req *http.Request) string {
    ytid := params["ytid"]
    stream_link := exec.Command("youtube-dl","-f", "140", "-g", "https://www.youtube.com/watch?v=" + ytid)
    var out bytes.Buffer
    stream_link.Stdout = &out
    errr := stream_link.Run()
    if err != nil {
        log.Fatal(err)
    }
    log.Println("Link", out.String())

    cmd_ff := exec.Command("ffmpeg", "-i", "pipe:0", "-acodec", "libmp3lame", "-f", "mp3", "-")
    resp, err := http.Get(url)
    if err != nil {
        log.Fatal(err)
    }
    cmd_ff.Stdin = resp.Body
    go func() {
        defer resp.Body.Close()
        if err := cmd_ff.Run(); err != nil {
            log.Fatal(err)
        }
    }()
    return "Youtube ID: " + ytid
})
m.Run()
OneOfOne
  • 95,033
  • 20
  • 184
  • 185
  • Is the Handler not used to serve HTTP, and used as a server? Would I not need to use net/http to request a URL and then pipe the body of that into the cmd.Stdin? – viperfx Jul 29 '14 at 20:25
  • I misread the question, I thought you wanted to do it in an http handler on your server, however the same thing stands, using `http.Get` returns an `http.Response` that also has `.Body`, I'll update the example. – OneOfOne Jul 29 '14 at 20:31
  • if you have the time, could you briefly add your recommended way of doing this. – viperfx Jul 29 '14 at 21:10