live video streaming server in golang
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

162 lines
3.8 KiB

package hls
import (
"fmt"
"github.com/gwuhaolin/livego/configure"
"net"
"net/http"
"path"
"strconv"
"strings"
"time"
"github.com/gwuhaolin/livego/av"
cmap "github.com/orcaman/concurrent-map"
log "github.com/sirupsen/logrus"
)
const (
duration = 3000
)
var (
ErrNoPublisher = fmt.Errorf("no publisher")
ErrInvalidReq = fmt.Errorf("invalid req url path")
ErrNoSupportVideoCodec = fmt.Errorf("no support video codec")
ErrNoSupportAudioCodec = fmt.Errorf("no support audio codec")
)
var crossdomainxml = []byte(`<?xml version="1.0" ?>
<cross-domain-policy>
<allow-access-from domain="*" />
<allow-http-request-headers-from domain="*" headers="*"/>
</cross-domain-policy>`)
type Server struct {
listener net.Listener
conns cmap.ConcurrentMap
}
func NewServer() *Server {
ret := &Server{
conns: cmap.New(),
}
go ret.checkStop()
return ret
}
func (server *Server) Serve(listener net.Listener) error {
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
server.handle(w, r)
})
server.listener = listener
http.Serve(listener, mux)
return nil
}
func (server *Server) GetWriter(info av.Info) av.WriteCloser {
var s *Source
ok := server.conns.Has(info.Key)
if !ok {
log.Debug("new hls source")
s = NewSource(info)
server.conns.Set(info.Key, s)
} else {
v, _ := server.conns.Get(info.Key)
s = v.(*Source)
}
return s
}
func (server *Server) getConn(key string) *Source {
v, ok := server.conns.Get(key)
if !ok {
return nil
}
return v.(*Source)
}
func (server *Server) checkStop() {
for {
<-time.After(5 * time.Second)
for item := range server.conns.IterBuffered() {
v := item.Val.(*Source)
if !v.Alive() && !configure.Config.GetBool("hls_keep_after_end") {
log.Debug("check stop and remove: ", v.Info())
server.conns.Remove(item.Key)
}
}
}
}
func (server *Server) handle(w http.ResponseWriter, r *http.Request) {
if path.Base(r.URL.Path) == "crossdomain.xml" {
w.Header().Set("Content-Type", "application/xml")
w.Write(crossdomainxml)
return
}
switch path.Ext(r.URL.Path) {
case ".m3u8":
key, _ := server.parseM3u8(r.URL.Path)
conn := server.getConn(key)
if conn == nil {
http.Error(w, ErrNoPublisher.Error(), http.StatusForbidden)
return
}
tsCache := conn.GetCacheInc()
if tsCache == nil {
http.Error(w, ErrNoPublisher.Error(), http.StatusForbidden)
return
}
body, err := tsCache.GenM3U8PlayList()
if err != nil {
log.Debug("GenM3U8PlayList error: ", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Content-Type", "application/x-mpegURL")
w.Header().Set("Content-Length", strconv.Itoa(len(body)))
w.Write(body)
case ".ts":
key, _ := server.parseTs(r.URL.Path)
conn := server.getConn(key)
if conn == nil {
http.Error(w, ErrNoPublisher.Error(), http.StatusForbidden)
return
}
tsCache := conn.GetCacheInc()
item, err := tsCache.GetItem(r.URL.Path)
if err != nil {
log.Debug("GetItem error: ", err)
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Content-Type", "video/mp2ts")
w.Header().Set("Content-Length", strconv.Itoa(len(item.Data)))
w.Write(item.Data)
}
}
func (server *Server) parseM3u8(pathstr string) (key string, err error) {
pathstr = strings.TrimLeft(pathstr, "/")
key = strings.Split(pathstr, path.Ext(pathstr))[0]
return
}
func (server *Server) parseTs(pathstr string) (key string, err error) {
pathstr = strings.TrimLeft(pathstr, "/")
paths := strings.SplitN(pathstr, "/", 3)
if len(paths) != 3 {
err = fmt.Errorf("invalid path=%s", pathstr)
return
}
key = paths[0] + "/" + paths[1]
return
}