GoLang http webserver provide video (mp4)
Asked Answered
R

4

12

I developed a webserver using golang. Pretty plain stuff, it just provides html/js/css and images which works perfectly fine:

func main() {
    http.Handle("/", new(viewHandler))
    http.ListenAndServe(":8080", nil)
}

func (vh *viewHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    path := r.URL.Path[1:]
    log.Println(path)
    data, err := ioutil.ReadFile(string(path))

    if err == nil {

        var contentType string

        if strings.HasSuffix(path, ".html") {
            contentType = "text/html"
        } else if strings.HasSuffix(path, ".css") {
            contentType = "text/css"
        } else if strings.HasSuffix(path, ".js") {
            contentType = "application/javascript"
        } else if strings.HasSuffix(path, ".png") {
            contentType = "image/png"
        } else if strings.HasSuffix(path, ".jpg") {
            contentType = "image/jpeg"
        } 

        w.Header().Add("Content-Type", contentType)
        w.Write(data)
    } else {
        log.Println("ERROR!")
        w.WriteHeader(404)
        w.Write([]byte("404 - " + http.StatusText(404)))
    }
}

I tried to add a video in mp4 format to my website in the same way:

<video width="685" height="525" autoplay loop style="margin-top: 20px">
   <source src="../public/video/marketing.mp4" type="video/mp4">Your browser does not support the video tag.</video>

and enhanced the content type part of my go application with:

else if strings.HasSuffix(path, ".mp4") {
            contentType = "video/mp4"
        }

When I open the html file directly in Chrome the Video is playing correctly, but if I open the website by calling the Web Server at http://localhost... it can not be loaded and therefor not played.

There is no video loaded, if I try to hit it directly the browser just shows some video contols without loading the file:

enter image description here

media loaded twice

media details

Any ideas on this? thanks in advance.

UPDATE:

As suggested I also tried another video which is working perfectly fine! but I have no clue why, I compared both videos:

enter image description here enter image description here

Could it be the size??

Cheers and thanks!

UPDATE2:

icza is right and I marked his post as correct answer to my question. But let me explain what I finally did to get it working:

I tried to solve my problem without using the http.FileServe method. Therefore I implemented like so:

} else if strings.HasSuffix(path, ".mp4") {
            contentType = "video/mp4"
            size := binary.Size(data)
            if size > 0 {
                requestedBytes := r.Header.Get("Range")
                w.Header().Add("Accept-Ranges", "bytes")
                w.Header().Add("Content-Length", strconv.Itoa(size))
                w.Header().Add("Content-Range", "bytes "+requestedBytes[6:len(requestedBytes)]+strconv.Itoa(size-1)+"/"+strconv.Itoa(size))
                w.WriteHeader(206)
            }
        }

        w.Header().Add("Content-Type", contentType)
        w.Write(data)

That worked for the first time I played the video. But since I want it to loop it should directly start from the beginning but it stuck at the very last frame of the video.

So I implemented the ServeFile() method like:

if contentType == "video/mp4" {
            http.ServeFile(w, r, path)
        } else {
            w.Header().Add("Content-Type", contentType)
            w.Write(data)
        }

The video is also playing in a loop correctly now. But I still have no idea why ServeFile() is working and the other method not though the response is exactly the same for both methods. Nevertheless the content is looking like that now:

enter image description here

Cheers and thanks to all of you who helped me.

Rubberize answered 28/6, 2016 at 7:21 Comment(13)
The code you have seems to work fine for me. You may have to elaborate on the specific issue you're seeing. (You say "it can not be loaded and therefor not played," but I'm not sure what you mean by that. Please be specific about the problem you're having.)Expectation
You still haven't described the actual issue you're seeing. Is there an error? If so, where do you see that error, and what does the error say?Expectation
Sorry. No I don't receive any errors, there is just no video. If I try to load it diectly like localhost:8080/public/video/marketing.mp4 it shows me empty video controls and the video is not loaded. I updated the Issue again with a screenshot of the result.Rubberize
Is there any chance you can share the exact video that's problematic?Expectation
You should serve your files using http.ServeFile() which supports serving Range requests (required for videos to be able to seek). See related question: How to serve http partial content with Go?Indefeasible
unfortunately I can't share it sorry, but thanks for offering me to investigate deeper. Regarding http.ServeFile: I don't wan't it to be searched it is a video running in a loop without the needs of any controls. It just makes me curious why one video with the same codec is working and the other one is not. The only thing i could think of is the size. Maybe there is a size limit?Rubberize
@icz - correct, I already tried to state that:) "Another video" in this case is no proof, improper range handling is more likely the case because the code pasted here obviously does not deal with that.Lysis
@MichaelB simply run wireshark to see what exaclty is going on;) If chrome decides to dispatch another request with a Range header other than 0- then of course that is the issue.Lysis
@MichaelB also open chrome://media-internals/ in a new tab - that shows the internal info for all the opened media sinks. You will most probably be able to see what exactly went wrong.Lysis
Thank you guys! I will do so and update the Issue as soon as I have more information. You were very helpful thanks again.Rubberize
To your last update: "without using the http.FileServe method" you claim you don't use FileServe() yet in your code you call it. Another problem here is that you call w.WriteHeader() which commits the headers (so you can't change response headers after that) and you call http.ServeFile() after this - but it won't be able to change headers.Indefeasible
@icza: sorry forget to remove the line... corrected it! thanksRubberize
Don't use a bunch of if else as much as possible. Unfortunately, the suggested edit queue is full, so I put it on go-playgroundPulsatile
I
16

Your "issue" is related to the video length / size. I don't know the default buffer Chrome uses (it may depend on multiple things like free memory, free disk space etc.), but you shouldn't rely on it for your video to play!

One of your video is a few seconds shorter and almost 3rd the size of the other. Chrome buffers completely the smaller video (likely in memory), while does not do the same with the other. And since it's bigger than the buffer, it needs the server to support Range requests (partial content serving). The way you serve the content of your video, you are not supporting partial content serving (which Chrome expects in case of "large" videos), so Chrome simply denies to handle / play the video.

You should serve your files using http.ServeFile() which supports serving Range requests (required for videos that are bigger than a certain size). See related question: How to serve http partial content with Go?

Once you do this, both your video files will play fine.

Going forward, there is no practical reason not to use http.ServeFile(). It won't load the complete file into memory as you did which is a really bad idea in case of big (video) files. http.ServeFile() handles Range requests if asked so, and also properly sets response headers including the Content-Type (and knows that .mp4 files require video/mp4 MIME type).

Analyzing requests of a big video file

Testing this simple application:

func fileh(w http.ResponseWriter, r *http.Request) {
    http.ServeFile(w, r, "/path/to/a/big/video.mp4")    
}

func main() {
    http.HandleFunc("/", fileh)
    panic(http.ListenAndServe("localhost:8080", nil))
}

And opening http://localhost:8080 from Chrome, the browser makes 2 requests (I intentionally filtered out unimportant / unrelated request-response headers):

Request #1

GET / HTTP/1.1

To get the requested (root) path.

Response #1

HTTP/1.1 200 OK
Accept-Ranges: bytes
Content-Length: 104715956
Content-Type: video/mp4

As you can see, the Go app reports the video size is around 100 MB. http.ServeFile() also includes the response header Accept-Ranges: bytes which lets clients know prior that we support Range requests.

How does Chrome respond to this? Closes the connection after receiving 32 KB (32.2 KB including headers; which is enough to look into the stream and tell what it can do with it), then fires another request (which also happens even if the Accept-Ranges response header is not sent - your example code does not send it):

Request #2

GET / HTTP/1.1
Referer: http://localhost:8080/
Range: bytes=0-

Chrome fires a Range request (asking everything from the first byte), so if your Go app does not support this, you're in trouble. See response #2.

Response #2

HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Length: 104715956
Content-Range: bytes 0-104715955/104715956
Content-Type: video/mp4

http.ServeFile() behaves well, and responds properly to the Range request (HTTP 206 Partial Content status code and Content-Range header), and sends the range that was requested (everything in this case).

Your example code does not respond with an HTTP 206 Partial Content but with a simple HTTP 200 OK. When Chrome receives the unexpected HTTP 200 OK, that's the point when it gives up on playing your video. If the video file is small, Chrome will not make a big deal out of it as it will just accept the HTTP 200 OK response too (and likely just store the small video in memory or in a cache file).

Indefeasible answered 28/6, 2016 at 12:14 Comment(1)
thanks! that was very helpful. I marked your statement as the correct answer.Rubberize
E
3

Your code seems to work fine for me. Here's the complete working example I tested, using Chrome on OS X.

main.go:

package main

import "io/ioutil"
import "log"
import "net/http"
import "strings"

type viewHandler struct{}

func (vh *viewHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    path := r.URL.Path[1:]

    data, err := ioutil.ReadFile(string(path))

    if err != nil {
        log.Printf("Error with path %s: %v", path, err)
        w.WriteHeader(404)
        w.Write([]byte("404"))
    }

    if strings.HasSuffix(path, ".html") {
        w.Header().Add("Content-Type", "text/html")
    } else if strings.HasSuffix(path, ".mp4") {
        w.Header().Add("Content-Type", "video/mp4")
    }

    w.Write(data)
}

func main() {
    http.Handle("/", new(viewHandler))
    http.ListenAndServe(":8080", nil)
}

index.html:

<!doctype html>
<html>
<body>
    <video autoplay loop>
        <source src="big_buck_bunny.mp4" type="video/mp4">
    </video>
</body>
</html>

Video downloaded from http://www.quirksmode.org/html5/videos/big_buck_bunny.mp4.

Expectation answered 28/6, 2016 at 9:16 Comment(1)
You are absolutely right! I never tried another video, but it is indeed working. The strange thing is, that I have no clue why... I updated my post and added two new images. Thanks for your helpRubberize
L
1

This is an example that supports all file types. contentType := http.DetectContentType(buffer) https://play.golang.org/p/XuVgmsCHUZB

Locomotor answered 26/1, 2018 at 2:58 Comment(0)
L
0

Well, since it's not a live stream (in terms that the size and duration are known since the source is an mp4 file) I think your server should honor the Range header in the request and respond with proper Content-Length, Content-Range and Accept-Range values (otherwise seeking would be impossible and I guess you don't want that). That should make Chrome happy. Take care when implementing the handling of the Range header value in the request - if it is not 0- then you must actually seek to the requested point and return the data from there. Also I don't think videos should be transferred with Transfer-Encoding set to chunked.

Lysis answered 28/6, 2016 at 8:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.