Skip to content

Commit

Permalink
Initial code dump
Browse files Browse the repository at this point in the history
  • Loading branch information
shimberger committed Jul 19, 2015
0 parents commit fab3621
Show file tree
Hide file tree
Showing 22 changed files with 846 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.sass-cache
node_modules
videos
29 changes: 29 additions & 0 deletions LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
BSD License

Copyright (c) 2015, Sebastian Himberger
All rights reserved.

Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:

* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

* Neither the name Sebastian Himberger nor the names of its contributors may be used to
endorse or promote products derived from this software without specific
prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Golang HLS Streamer
===================

Simple server that exposes a directory for video streaming via HTTP Live Streaming (HLS).
Uses ffmpeg for transcoding.

This project is cobbled together from all kinds of code I has lying around so it's pretty crapy all around.

Running it
----------

# Place ffmpeg and ffprobe binaries in "tools" dir
# Run go run *.go <path to videos> in project root (e.g. go run *.go ~/Documents/)
# Access http://localhost:8080/ui/show/

License
-------
See LICENSE.txt

15 changes: 15 additions & 0 deletions debug.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package main

import (
"log"
)

var debug Debugger = false

type Debugger bool

func (d Debugger) Printf(format string, args ...interface{}) {
if d {
log.Printf("DEBUG: "+format, args...)
}
}
18 changes: 18 additions & 0 deletions http_debug.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package main

import (
"net/http"
)

type DebugHandlerWrapper struct {
handler http.Handler
}

func NewDebugHandlerWrapper(handler http.Handler) *DebugHandlerWrapper {
return &DebugHandlerWrapper{handler}
}

func (s *DebugHandlerWrapper) ServeHTTP(w http.ResponseWriter, r *http.Request) {
debug.Printf("%v %v", r.Method, r.URL.Path)
s.handler.ServeHTTP(w, r)
}
25 changes: 25 additions & 0 deletions http_frame.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package main

import (
_ "bytes"
"log"
"net/http"
"os/exec"
"path"
)

type FrameHandler struct {
root string
}

func NewFrameHandler(root string) *FrameHandler {
return &FrameHandler{root}
}

func (s *FrameHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
path := path.Join(s.root, r.URL.Path)
cmd := exec.Command("tools/ffmpeg", "-loglevel", "error", "-ss", "00:00:30", "-i", path, "-vf", "scale=320:-1", "-frames:v", "1", "-f", "image2", "-")
if err := ServeCommand(cmd, w); err != nil {
log.Printf("Error serving screenshot: %v", err)
}
}
68 changes: 68 additions & 0 deletions http_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package main

import (
"fmt"
"io/ioutil"
"log"
"net/http"
"path"
"strings"
)

type ListResponseVideo struct {
Name string `json:"name"`
Path string `json:"path"`
Info *VideoInfo `json:"info"`
}

type ListResponseFolder struct {
Name string `json:"name"`
Path string `json:"path"`
}

type ListResponse struct {
Error error `json:"error"`
Folders []*ListResponseFolder `json:"folders"`
Videos []*ListResponseVideo `json:"videos"`
}

type ListHandler struct {
path string
}

func NewListHandler(path string) *ListHandler {
return &ListHandler{path}
}

func (s *ListHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
videos := make([]*ListResponseVideo, 0)
folders := make([]*ListResponseFolder, 0)
response := &ListResponse{nil, folders, videos}
files, rerr := ioutil.ReadDir(path.Join(s.path, r.URL.Path))
if rerr != nil {
response.Error = fmt.Errorf("Error reading path: %v", r.URL.Path)
ServeJson(500, response, w)
return
}
for _, f := range files {
filePath := path.Join(s.path, r.URL.Path, f.Name())
if strings.HasPrefix(f.Name(), ".") || strings.HasPrefix(f.Name(), "$") {
continue
}
if FilenameLooksLikeVideo(filePath) {
vinfo, err := GetVideoInformation(filePath)
if err != nil {
log.Printf("Could not read video information of %v: %v", filePath, err)
}
video := &ListResponseVideo{f.Name(), path.Join(r.URL.Path, f.Name()), vinfo}
videos = append(videos, video)
}
if f.IsDir() {
folder := &ListResponseFolder{f.Name(), path.Join(r.URL.Path, f.Name())}
folders = append(folders, folder)
}
}
response.Videos = videos
response.Folders = folders
ServeJson(200, response, w)
}
70 changes: 70 additions & 0 deletions http_playlists.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package main

import (
"fmt"
"net/http"
"net/url"
"path"
//"strings"
)

// UrlEncoded encodes a string like Javascript's encodeURIComponent()
func UrlEncoded(str string) (string, error) {
u, err := url.Parse(str)
if err != nil {
return "", err
}
return u.String(), nil
//return strings.Replace(u.String(), "/", "%2F", -1), nil
}

const hlsSegmentLength = 5.0 // 5 Seconds

type PlaylistHandler struct {
root string
}

func NewPlaylistHandler(root string) *PlaylistHandler {
return &PlaylistHandler{root}
}

func (s *PlaylistHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
filePath := path.Join(s.root, r.URL.Path)
vinfo, err := GetVideoInformation(filePath)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
duration := vinfo.Duration
baseurl := fmt.Sprintf("http://%v", r.Host)

id, err := UrlEncoded(r.URL.Path)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

w.Header()["Content-Type"] = []string{"application/vnd.apple.mpegurl"}

fmt.Fprint(w, "#EXTM3U\n")
fmt.Fprint(w, "#EXT-X-VERSION:3\n")
fmt.Fprint(w, "#EXT-X-MEDIA-SEQUENCE:0\n")
fmt.Fprint(w, "#EXT-X-ALLOW-CACHE:YES\n")
fmt.Fprint(w, "#EXT-X-TARGETDURATION:5\n")
fmt.Fprint(w, "#EXT-X-PLAYLIST-TYPE:VOD\n")

leftover := duration
segmentIndex := 0

for leftover > 0 {
if leftover > hlsSegmentLength {
fmt.Fprintf(w, "#EXTINF: %f,\n", hlsSegmentLength)
} else {
fmt.Fprintf(w, "#EXTINF: %f,\n", leftover)
}
fmt.Fprintf(w, baseurl+"/segments/%v/%v.ts\n", id, segmentIndex)
segmentIndex++
leftover = leftover - hlsSegmentLength
}
fmt.Fprint(w, "#EXT-X-ENDLIST\n")
}
15 changes: 15 additions & 0 deletions http_singlefile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package main

import "net/http"

type SingleFileServer struct {
path string
}

func NewSingleFileServer(path string) *SingleFileServer {
return &SingleFileServer{path}
}

func (s *SingleFileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
http.ServeFile(w, r, s.path)
}
27 changes: 27 additions & 0 deletions http_stream.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package main

import (
"fmt"
"net/http"
"os/exec"
"path"
"strconv"
"strings"
)

type StreamHandler struct {
root string
}

func NewStreamHandler(root string) *StreamHandler {
return &StreamHandler{root}
}

func (s *StreamHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
filePath := path.Join(s.root, r.URL.Path[0:strings.LastIndex(r.URL.Path, "/")])
idx, _ := strconv.ParseInt(r.URL.Path[strings.LastIndex(r.URL.Path, "/")+1:strings.LastIndex(r.URL.Path, ".")], 0, 64)
startTime := idx * hlsSegmentLength
debug.Printf("Streaming second %v of %v", startTime, filePath)
cmd := exec.Command("tools/ffmpeg", "-ss", fmt.Sprintf("%v", startTime), "-t", "5", "-i", filePath, "-vcodec", "libx264", "-strict", "experimental", "-acodec", "aac", "-pix_fmt", "yuv420p", "-r", "25", "-profile:v", "baseline", "-b:v", "2000k", "-maxrate", "2500k", "-f", "mpegts", "-")
ServeCommand(cmd, w)
}
44 changes: 44 additions & 0 deletions http_util.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package main

import (
"encoding/json"
"io"
"log"
"net/http"
"os/exec"
"syscall"
)

func ServeCommand(cmd *exec.Cmd, w io.Writer) error {
stdout, err := cmd.StdoutPipe()
defer stdout.Close()
if err != nil {
log.Printf("Error opening stdout of command: %v", err)
return err
}
err = cmd.Start()
if err != nil {
log.Printf("Error starting command: %v", err)
return err
}
_, err = io.Copy(w, stdout)
if err != nil {
log.Printf("Error copying data to client: %v", err)
// Ask the process to exit
cmd.Process.Signal(syscall.SIGKILL)
cmd.Process.Wait()
return err
}
cmd.Wait()
return nil
}

func ServeJson(status int, data interface{}, w http.ResponseWriter) {
js, err := json.Marshal(data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.Write(js)
}
25 changes: 25 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package main

import (
"flag"
"net/http"
"path"
)

func main() {
flag.Parse()
uiDirectory := path.Join(".", "ui")
indexHtml := path.Join(uiDirectory, "index.html")
contentDir := path.Join(".", "videos")
if flag.NArg() > 0 {
contentDir = flag.Arg(0)
}
http.Handle("/ui/assets/", http.StripPrefix("/ui/", http.FileServer(http.Dir(uiDirectory))))
http.Handle("/ui/", NewDebugHandlerWrapper(http.StripPrefix("/ui/", NewSingleFileServer(indexHtml))))
http.Handle("/list/", NewDebugHandlerWrapper(http.StripPrefix("/list/", NewListHandler(contentDir))))
http.Handle("/frame/", NewDebugHandlerWrapper(http.StripPrefix("/frame/", NewFrameHandler(contentDir))))
http.Handle("/playlist/", NewDebugHandlerWrapper(http.StripPrefix("/playlist/", NewPlaylistHandler(contentDir))))
http.Handle("/segments/", NewDebugHandlerWrapper(http.StripPrefix("/segments/", NewStreamHandler(contentDir))))
http.ListenAndServe(":8080", nil)

}
5 changes: 5 additions & 0 deletions tools/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Ignore everything in this directory
*
# Except this file
!.gitignore
!README.md
10 changes: 10 additions & 0 deletions tools/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
This directory should contain the two programs:

- ffmpeg
- ffprobe

You can download statically linked versions from:

http://www.ffmpeg.org/download.html

or symlink/copy your custom versions.
Loading

0 comments on commit fab3621

Please sign in to comment.