Add minimal web interface
This commit is contained in:
parent
72c84dff1e
commit
1d6e4270ba
@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
|
||||||
@ -57,10 +58,21 @@ func main() {
|
|||||||
func ActionServe(c *cli.Context) error {
|
func ActionServe(c *cli.Context) error {
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
// Setup rtmp server
|
||||||
s := server.NewRTMPServer(ctx, ":5566")
|
s := server.NewRTMPServer(ctx, ":5566")
|
||||||
logger := setupServerLogger()
|
logger := setupServerLogger()
|
||||||
s.Logger = logger
|
s.Logger = logger
|
||||||
|
|
||||||
|
// Setup web server
|
||||||
|
ws := server.NewWebServer(ctx, s)
|
||||||
|
go func() {
|
||||||
|
s.Logger.Info("Starting web server")
|
||||||
|
err := ws.Serve()
|
||||||
|
if err != nil && err != http.ErrServerClosed {
|
||||||
|
s.Logger.Infow("Web server shut down with error", "err", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Listen for SIGINT
|
// Listen for SIGINT
|
||||||
sigChan := make(chan os.Signal, 1)
|
sigChan := make(chan os.Signal, 1)
|
||||||
signal.Notify(sigChan, os.Interrupt)
|
signal.Notify(sigChan, os.Interrupt)
|
||||||
|
98
config/config.go
Normal file
98
config/config.go
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/pelletier/go-toml"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrNotFound = errors.New("no config file found")
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
ListenAddr string `toml:"ListenAddr"`
|
||||||
|
LogLevel string `toml:"LogLevel"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type InvalidValueError struct {
|
||||||
|
Key string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ive *InvalidValueError) Error() string {
|
||||||
|
return fmt.Sprintf("invalid value: config key %s has invalid value", ive.Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromReader(r io.Reader) (*Config, error) {
|
||||||
|
var c Config
|
||||||
|
// Set some defaults
|
||||||
|
c.ListenAddr = ":5566"
|
||||||
|
c.LogLevel = "INFO"
|
||||||
|
|
||||||
|
decoder := toml.NewDecoder(r)
|
||||||
|
if err := decoder.Decode(&c); err != nil {
|
||||||
|
return nil, fmt.Errorf("error parsing config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &c, c.Verify()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) Verify() error {
|
||||||
|
// Check that LogLevel is valid
|
||||||
|
switch c.LogLevel {
|
||||||
|
case "DEBUG", "INFO", "WARN", "ERROR":
|
||||||
|
default:
|
||||||
|
return &InvalidValueError{Key: "LogLevel"}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) UpdateFromEnv() error {
|
||||||
|
if loglevel, found := os.LookupEnv("DOGTAMER_LOGLEVEL"); found {
|
||||||
|
c.LogLevel = loglevel
|
||||||
|
}
|
||||||
|
if listenAddr, found := os.LookupEnv("DOGTAMER_LISTENADDR"); found {
|
||||||
|
c.ListenAddr = listenAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Verify()
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromFile(path string) (*Config, error) {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Error opening config file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return FromReader(f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func FromDefaultLocations(path string) (*Config, error) {
|
||||||
|
defaultLocations := []string{
|
||||||
|
"dogtamer.toml",
|
||||||
|
"/etc/dogtamer.toml",
|
||||||
|
}
|
||||||
|
baseConfigDir, err := os.UserConfigDir()
|
||||||
|
if err == nil {
|
||||||
|
userConfigFile := filepath.Join(baseConfigDir, "dogtamer", "dogtamer.toml")
|
||||||
|
defaultLocations = append(defaultLocations, userConfigFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, fname := range defaultLocations {
|
||||||
|
if _, err := os.Stat(fname); os.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := FromFile(fname)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg, cfg.UpdateFromEnv()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, ErrNotFound
|
||||||
|
}
|
126
config/config_test.go
Normal file
126
config/config_test.go
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
package config_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.uio.no/torjus/dogtamer/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfig(t *testing.T) {
|
||||||
|
t.Run("TestEmpty", func(t *testing.T) {
|
||||||
|
emptyConfig := ""
|
||||||
|
sr := strings.NewReader(emptyConfig)
|
||||||
|
|
||||||
|
c, err := config.FromReader(sr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error reading empty config: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure proper defaults are set
|
||||||
|
if c.ListenAddr != ":5566" {
|
||||||
|
t.Errorf("Unexpected ListenAddr: %s", c.ListenAddr)
|
||||||
|
}
|
||||||
|
if c.LogLevel != "INFO" {
|
||||||
|
t.Errorf("Unexpected LogLevel: %s", c.LogLevel)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("TestWithValidValues", func(t *testing.T) {
|
||||||
|
configString := `
|
||||||
|
# Random comment
|
||||||
|
LogLevel = "DEBUG"
|
||||||
|
|
||||||
|
ListenAddr = ":5555"
|
||||||
|
`
|
||||||
|
sr := strings.NewReader(configString)
|
||||||
|
|
||||||
|
c, err := config.FromReader(sr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error reading config: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.ListenAddr != ":5555" {
|
||||||
|
t.Errorf("Unexpected ListenAddr: %s", c.ListenAddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.LogLevel != "DEBUG" {
|
||||||
|
t.Errorf("Unexpected LogLevel: %s", c.LogLevel)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("TestWithInValidValues", func(t *testing.T) {
|
||||||
|
configString := `
|
||||||
|
# Random comment
|
||||||
|
LogLevel = "INVALID"
|
||||||
|
|
||||||
|
ListenAddr = ":5555"
|
||||||
|
`
|
||||||
|
sr := strings.NewReader(configString)
|
||||||
|
|
||||||
|
_, err := config.FromReader(sr)
|
||||||
|
ive, ok := err.(*config.InvalidValueError)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("Error is of wrong type: %T", ive)
|
||||||
|
}
|
||||||
|
if ive.Key != "LogLevel" {
|
||||||
|
t.Errorf("Error has wrong key: %s", ive.Key)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("TestFromEnv", func(t *testing.T) {
|
||||||
|
os.Clearenv()
|
||||||
|
|
||||||
|
envValues := map[string]string{
|
||||||
|
"DOGTAMER_LOGLEVEL": "DEBUG",
|
||||||
|
"DOGTAMER_LISTENADDR": ":3333",
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range envValues {
|
||||||
|
if err := os.Setenv(k, v); err != nil {
|
||||||
|
t.Fatalf("Unable to set env values: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyCfg := ""
|
||||||
|
sr := strings.NewReader(emptyCfg)
|
||||||
|
|
||||||
|
c, err := config.FromReader(sr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error parsing config string: %s", err)
|
||||||
|
}
|
||||||
|
if err := c.UpdateFromEnv(); err != nil {
|
||||||
|
t.Errorf("Error updating config with environment")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.ListenAddr != envValues["DOGTAMER_LISTENADDR"] {
|
||||||
|
t.Errorf("ListenAddr has wrong value: %s", c.ListenAddr)
|
||||||
|
}
|
||||||
|
if c.LogLevel != envValues["DOGTAMER_LOGLEVEL"] {
|
||||||
|
t.Errorf("LogLevel has wrong value: %s", c.LogLevel)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
t.Run("TestFromEnvInvalid", func(t *testing.T) {
|
||||||
|
os.Clearenv()
|
||||||
|
|
||||||
|
envValues := map[string]string{
|
||||||
|
"DOGTAMER_LOGLEVEL": "TEST",
|
||||||
|
"DOGTAMER_LISTENADDR": ":3333",
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range envValues {
|
||||||
|
if err := os.Setenv(k, v); err != nil {
|
||||||
|
t.Fatalf("Unable to set env values: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emptyCfg := ""
|
||||||
|
sr := strings.NewReader(emptyCfg)
|
||||||
|
|
||||||
|
c, err := config.FromReader(sr)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error parsing config string: %s", err)
|
||||||
|
}
|
||||||
|
if err := c.UpdateFromEnv(); err == nil {
|
||||||
|
t.Errorf("No error when parsing invalid env values")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
10
dogtamer.example.toml
Normal file
10
dogtamer.example.toml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
# dogtamer example config file
|
||||||
|
|
||||||
|
# Address to listen to for incoming connections
|
||||||
|
# Default: ":5566"
|
||||||
|
ListenAddr = ":5566"
|
||||||
|
|
||||||
|
# Log level
|
||||||
|
# Default: INFO
|
||||||
|
# Possible values: "DEBUG", "INFO", "WARN", "ERROR"
|
||||||
|
LogLevel = "INFO"
|
1
go.mod
1
go.mod
@ -7,6 +7,7 @@ require github.com/urfave/cli/v2 v2.3.0
|
|||||||
require (
|
require (
|
||||||
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
|
github.com/cpuguy83/go-md2man/v2 v2.0.1 // indirect
|
||||||
github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590 // indirect
|
github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590 // indirect
|
||||||
|
github.com/pelletier/go-toml v1.9.3 // indirect
|
||||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||||
go.uber.org/atomic v1.9.0 // indirect
|
go.uber.org/atomic v1.9.0 // indirect
|
||||||
go.uber.org/multierr v1.7.0 // indirect
|
go.uber.org/multierr v1.7.0 // indirect
|
||||||
|
2
go.sum
2
go.sum
@ -10,6 +10,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590 h1:PnxRU8L8Y2q82vFC2QdNw23Dm2u6WrjecIdpXjiYbXM=
|
github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590 h1:PnxRU8L8Y2q82vFC2QdNw23Dm2u6WrjecIdpXjiYbXM=
|
||||||
github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590/go.mod h1:XmAOs6UJXpNXRwKk+KY/nv5kL6xXYXyellk+A1pTlko=
|
github.com/nareix/joy5 v0.0.0-20210317075623-2c912ca30590/go.mod h1:XmAOs6UJXpNXRwKk+KY/nv5kL6xXYXyellk+A1pTlko=
|
||||||
|
github.com/pelletier/go-toml v1.9.3 h1:zeC5b1GviRUyKYd6OJPvBU/mcVDVoL1OhT17FCt5dSQ=
|
||||||
|
github.com/pelletier/go-toml v1.9.3/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
|
8
server/templates/index.html
Normal file
8
server/templates/index.html
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<ol>
|
||||||
|
{{ range .Streams }}
|
||||||
|
<li>{{ .URL }}</li>
|
||||||
|
{{ end }}
|
||||||
|
</ol>
|
||||||
|
</html>
|
68
server/web.go
Normal file
68
server/web.go
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"html/template"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WebServer struct {
|
||||||
|
Logger *zap.SugaredLogger
|
||||||
|
ctx context.Context
|
||||||
|
rtmpServer *RTMPServer
|
||||||
|
httpServer *http.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewWebServer(ctx context.Context, rs *RTMPServer) *WebServer {
|
||||||
|
return &WebServer{
|
||||||
|
ctx: ctx,
|
||||||
|
rtmpServer: rs,
|
||||||
|
Logger: zap.NewNop().Sugar(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WebServer) Serve() error {
|
||||||
|
ws.httpServer = &http.Server{
|
||||||
|
Addr: ":8077",
|
||||||
|
Handler: http.HandlerFunc(ws.IndexHandler),
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-ws.ctx.Done()
|
||||||
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
_ = ws.httpServer.Shutdown(shutdownCtx)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return ws.httpServer.ListenAndServe()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ws *WebServer) IndexHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
|
data := struct {
|
||||||
|
Streams []struct {
|
||||||
|
Name string
|
||||||
|
URL template.URL
|
||||||
|
}
|
||||||
|
}{}
|
||||||
|
|
||||||
|
for _, s := range ws.rtmpServer.streams {
|
||||||
|
stream := 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
tmpl := template.Must(template.ParseFiles("server/templates/index.html"))
|
||||||
|
|
||||||
|
w.Header().Add("Content-Type", "text/html")
|
||||||
|
|
||||||
|
tmpl.Execute(w, data)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user