Add playlist-generation
This commit is contained in:
@@ -3,6 +3,7 @@ package server
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"regexp"
|
||||
@@ -16,6 +17,8 @@ import (
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var ErrNoSuchItem error = fmt.Errorf("no such stream")
|
||||
|
||||
type RTMPClient struct {
|
||||
c *rtmp.Conn
|
||||
nc net.Conn
|
||||
@@ -238,6 +241,7 @@ func (client *RTMPClient) handleClient() {
|
||||
|
||||
type RTMPServer struct {
|
||||
ListenAddr string
|
||||
Hostname string
|
||||
Logger *zap.SugaredLogger
|
||||
streamsLock sync.Mutex
|
||||
streams map[string]*Stream
|
||||
@@ -248,6 +252,7 @@ type RTMPServer struct {
|
||||
func NewRTMPServer(ctx context.Context, addr string) *RTMPServer {
|
||||
serverCtx, cancel := context.WithCancel(ctx)
|
||||
rs := &RTMPServer{
|
||||
Hostname: "localhost",
|
||||
ListenAddr: addr,
|
||||
Logger: zap.NewNop().Sugar(),
|
||||
streams: make(map[string]*Stream),
|
||||
@@ -358,3 +363,31 @@ func (rs *RTMPServer) handleConn(c *rtmp.Conn, nc net.Conn) {
|
||||
// Stream URL is invalid, disconnect
|
||||
nc.Close()
|
||||
}
|
||||
|
||||
type StreamInfo struct {
|
||||
Name string
|
||||
Path string
|
||||
}
|
||||
|
||||
func (s *RTMPServer) List() []*StreamInfo {
|
||||
var results []*StreamInfo
|
||||
_, port, _ := net.SplitHostPort(s.ListenAddr)
|
||||
|
||||
for _, stream := range s.streams {
|
||||
results = append(results, &StreamInfo{Name: stream.Name, Path: fmt.Sprintf("rtmp://%s:%s/view/%s", s.Hostname, port, stream.Name)})
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func (s *RTMPServer) GetInfo(name string) (*StreamInfo, error) {
|
||||
stream, ok := s.streams[name]
|
||||
if !ok {
|
||||
return nil, ErrNoSuchItem
|
||||
}
|
||||
_, port, _ := net.SplitHostPort(s.ListenAddr)
|
||||
|
||||
return &StreamInfo{
|
||||
Name: stream.Name,
|
||||
Path: fmt.Sprintf("rtmp://%s:%s/view/%s", s.Hostname, port, stream.Name),
|
||||
}, nil
|
||||
}
|
||||
|
@@ -1,8 +1,10 @@
|
||||
<DOCTYPE html>
|
||||
<html>
|
||||
<html>
|
||||
<ol>
|
||||
{{ range .Streams }}
|
||||
<li>{{ .URL }}</li>
|
||||
{{ end }}
|
||||
{{ range .Streams }}
|
||||
<li>
|
||||
<a href="{{ .Path }}">{{ .Name }}</a>
|
||||
</li>
|
||||
{{ end }}
|
||||
</ol>
|
||||
</html>
|
@@ -7,6 +7,8 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.uio.no/torjus/dogtamer/m3u"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
@@ -28,9 +30,12 @@ func NewWebServer(ctx context.Context, rs *RTMPServer) *WebServer {
|
||||
}
|
||||
|
||||
func (ws *WebServer) Serve() error {
|
||||
r := chi.NewRouter()
|
||||
r.Get("/", ws.IndexHandler)
|
||||
r.Get("/playlist/{name}", ws.PlaylistHandler)
|
||||
ws.httpServer = &http.Server{
|
||||
Addr: ws.ListenAddr,
|
||||
Handler: http.HandlerFunc(ws.IndexHandler),
|
||||
Handler: r,
|
||||
}
|
||||
|
||||
go func() {
|
||||
@@ -52,22 +57,19 @@ func (ws *WebServer) Serve() error {
|
||||
}
|
||||
|
||||
func (ws *WebServer) IndexHandler(w http.ResponseWriter, r *http.Request) {
|
||||
data := struct {
|
||||
|
||||
var data struct {
|
||||
Streams []struct {
|
||||
Name string
|
||||
URL template.URL
|
||||
Path template.URL
|
||||
}
|
||||
}{}
|
||||
|
||||
for _, s := range ws.rtmpServer.streams {
|
||||
stream := struct {
|
||||
}
|
||||
streams := ws.rtmpServer.List()
|
||||
for _, stream := range streams {
|
||||
data.Streams = append(data.Streams, struct {
|
||||
Name string
|
||||
URL template.URL
|
||||
}{
|
||||
Name: s.Name,
|
||||
URL: template.URL(fmt.Sprintf("rtmp://localhost:5566/view/%s", s.Name)),
|
||||
}
|
||||
data.Streams = append(data.Streams, stream)
|
||||
Path template.URL
|
||||
}{Name: stream.Name, Path: template.URL(fmt.Sprintf("/playlist/%s", stream.Name))})
|
||||
}
|
||||
|
||||
tmpl := template.Must(template.ParseFiles("server/templates/index.html"))
|
||||
@@ -76,3 +78,34 @@ func (ws *WebServer) IndexHandler(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
tmpl.Execute(w, data)
|
||||
}
|
||||
|
||||
func (ws *WebServer) PlaylistHandler(w http.ResponseWriter, r *http.Request) {
|
||||
streamName := chi.URLParam(r, "name")
|
||||
if streamName == "" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
streamInfo, err := ws.rtmpServer.GetInfo(streamName)
|
||||
if err != nil {
|
||||
if err == ErrNoSuchItem {
|
||||
ws.Logger.Debugw("Client requested non-existing playlist", "stream_name", streamName, "error", err)
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
return
|
||||
}
|
||||
ws.Logger.Warnw("Error getting stream info", "stream_name", streamName, "error", err)
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
var p m3u.Playlist
|
||||
p.Add(&m3u.PlaylistItem{Name: streamInfo.Name, Time: -1, Path: streamInfo.Path})
|
||||
|
||||
w.Header().Add("Content-Type", "application/mpegurl")
|
||||
w.Header().Add("Content-Disposition", "attachment; filename=stream.m3u")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
_, err = p.WriteTo(w)
|
||||
if err != nil {
|
||||
ws.Logger.Warnw("error generating playlist", "stream_name", streamName, "error", err)
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user