Compare commits

..

28 Commits

Author SHA1 Message Date
567fb71ca7 Release as version v0.1.2
All checks were successful
Run go tests / test (push) Successful in 1m41s
release-nightly / release-image (push) Successful in 4m11s
release-tag / release-image (push) Successful in 2m57s
2023-12-07 00:08:33 +01:00
cb33bc3555 Change push secret for workflow
All checks were successful
release-nightly / release-image (push) Successful in 2m14s
2023-12-07 00:00:25 +01:00
94def554c3 Fix tags
Some checks failed
release-nightly / release-image (push) Failing after 2m4s
2023-12-06 23:46:59 +01:00
5d76154a25 Update image used for docker workflow
Some checks failed
release-nightly / release-image (push) Failing after 59s
2023-12-06 23:39:40 +01:00
381523a2be Update test to only run on changed go files
Some checks failed
release-nightly / release-image (push) Failing after 20s
2023-12-06 23:33:34 +01:00
5ec7086843 Update release workflow 2023-12-06 23:32:08 +01:00
504d7bc16c Use https for submodule
Some checks failed
release-nightly / release-image (push) Failing after 49s
Run go tests / test (push) Successful in 2m10s
2023-12-06 23:26:08 +01:00
836c5a3bbf Try removing host key
Some checks failed
Run go tests / test (push) Successful in 1m52s
release-nightly / release-image (push) Failing after 9s
2023-12-06 16:21:32 +01:00
6ac0efb057 Fix branch name in nightly workflow
Some checks failed
Run go tests / test (push) Waiting to run
release-nightly / release-image (push) Failing after 45s
2023-12-06 03:57:25 +01:00
565de48636 Add release-nightly workflow
Some checks are pending
Run go tests / test (push) Waiting to run
2023-12-06 03:54:46 +01:00
ea0f73b516 Update frontend
Some checks failed
Run go tests / test (push) Failing after 10m44s
2023-12-06 03:30:17 +01:00
c0a3cadda4 Update frontend
Some checks failed
Run go tests / test (push) Failing after 11m53s
2023-12-06 00:18:39 +01:00
634bad14d9 Add viewcount to stream list endpoint
Some checks are pending
Run go tests / test (push) Waiting to run
2023-12-06 00:08:14 +01:00
7fe24673ed Fix cache action
All checks were successful
Run go tests / test (push) Successful in 55s
2023-12-05 22:41:57 +01:00
cd209a3c74 Fix typo in workflow
All checks were successful
Run go tests / test (push) Successful in 52s
2023-12-05 22:38:21 +01:00
f33a81d1f4 Merge pull request 'Add go test workflow' (#4) from chore/workflow into master
Reviewed-on: #4
2023-12-05 21:32:49 +00:00
d346944e20 Publish coverage report
All checks were successful
Run go tests / test (pull_request) Successful in 50s
2023-12-05 22:30:12 +01:00
555ce22479 Go mod tidy
All checks were successful
Run go tests / test (pull_request) Successful in 1m21s
2023-12-05 21:58:28 +01:00
644edc33bd Add go test workflow
Some checks failed
Run go tests / test (pull_request) Failing after 1m22s
2023-12-05 21:51:15 +01:00
5cbfc14eab Fix env override for config 2023-12-05 21:32:31 +01:00
9fe5e68551 Update frontend 2023-12-05 03:24:40 +01:00
912cbfe8dd Merge pull request 'Add config' (#3) from feature/2/config into master
Reviewed-on: #3
2023-12-05 00:35:38 +00:00
4b171b849f Make ephemeral udp ports configurable 2023-12-05 01:30:04 +01:00
fd18ba6527 Change http config to sub element 2023-12-05 01:15:09 +01:00
8cb0ede570 Use config in server 2023-12-05 00:46:04 +01:00
e1e96e1e19 Add minimal config 2023-12-05 00:45:43 +01:00
af0f583aff Update frontend 2023-12-04 23:40:02 +01:00
20da2e5d30 Update frontend 2023-12-04 02:18:48 +01:00
13 changed files with 462 additions and 27 deletions

View File

@@ -0,0 +1,74 @@
name: Run go tests
on:
push:
paths:
- '**.go'
pull_request:
paths:
- '**.go'
env:
GOPATH: /go_path
GOCACHE: /go_cache
jobs:
test:
runs-on: ubuntu-latest
timeout-minutes: 5
env:
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: https://github.com/actions/setup-go@v4
with:
go-version: 1.21
use-latest: true
- name: Hash go.mod and go.sum
uses: https://gitea.com/actions/go-hashfiles@v0.0.1
id: hash-go
with:
patterns: |
go.mod
go.sum
- name: Cache go
uses: actions/cache@v3
id: cache-go
with:
path: |
/go_path
/go_cache
key: cache-go-${{ steps.hash-go.outputs.hash }}
- name: Go mod download
run: go mod download
- name: Go mod verify
run: go mod verify
- name: Go mod tidy
run: go mod tidy && git diff --exit-code
- name: Check gofmt
run: gofmt -s -w . && git diff --exit-code
- name: Check go vet
run: go vet ./...
- name: Build
run: go build -v ./...
- name: Test
run: go test -cover -coverprofile=/tmp/cov.out -v ./...
- name: Make coverage report
run: go tool cover -html=/tmp/cov.out -o /tmp/cov.html
- name: Publish coverage report
uses: actions/upload-artifact@v3
with:
name: coverage-report
path: /tmp/cov.html

View File

@@ -0,0 +1,56 @@
name: release-nightly
on:
push:
branches: [ master ]
env:
REGISTRY: git.t-juice.club
IMAGE_NAME: ${{ gitea.repository }}
DOCKER_TAG: nightly
jobs:
release-image:
runs-on: ubuntu-latest
container:
image: ghcr.io/catthehacker/ubuntu:act-latest
env:
DOCKER_ORG: torjus
DOCKER_TAG: nightly
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # all history for all branches and tags
submodules: true
github-server-url: 'https://git.t-juice.club'
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker BuildX
uses: docker/setup-buildx-action@v3
- name: Login to git.t-juice.club registry
uses: docker/login-action@v3
with:
registry: git.t-juice.club
username: ${{ gitea.actor }}
password: ${{ secrets.DOCKER_PUSH }}
- name: Get Meta
id: meta
run: |
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
echo REPO_VERSION=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: |
linux/amd64
push: true
tags: |
${{ env.REGISTRY }}/${{ gitea.repository }}:${{ env.DOCKER_TAG }}

View File

@@ -0,0 +1,58 @@
name: release-tag
on:
push:
tags:
- '*'
env:
REGISTRY: git.t-juice.club
IMAGE_NAME: ${{ gitea.repository }}
DOCKER_TAG: latest
jobs:
release-image:
runs-on: ubuntu-latest
container:
image: ghcr.io/catthehacker/ubuntu:act-latest
env:
DOCKER_ORG: torjus
DOCKER_TAG: nightly
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # all history for all branches and tags
submodules: true
github-server-url: 'https://git.t-juice.club'
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker BuildX
uses: docker/setup-buildx-action@v3
- name: Login to git.t-juice.club registry
uses: docker/login-action@v3
with:
registry: git.t-juice.club
username: ${{ gitea.actor }}
password: ${{ secrets.DOCKER_PUSH }}
- name: Get Meta
id: meta
run: |
echo REPO_NAME=$(echo ${GITHUB_REPOSITORY} | awk -F"/" '{print $2}') >> $GITHUB_OUTPUT
echo REPO_VERSION=$(git describe --tags --always | sed 's/^v//') >> $GITHUB_OUTPUT
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: ./Dockerfile
platforms: |
linux/amd64
push: true
tags: |
${{ env.REGISTRY }}/${{ gitea.repository }}:${{ steps.meta.outputs.REPO_VERSION }}
${{ env.REGISTRY }}/${{ gitea.repository }}:${{ env.DOCKER_TAG }}

2
.gitmodules vendored
View File

@@ -1,3 +1,3 @@
[submodule "ministream-frontend"] [submodule "ministream-frontend"]
path = ministream-frontend path = ministream-frontend
url = ssh://git@git.t-juice.club:222/torjus/ministream-frontend.git url = https://git.t-juice.club/torjus/ministream-frontend.git

9
go.mod
View File

@@ -4,6 +4,11 @@ go 1.21.3
require ( require (
github.com/go-chi/chi/v5 v5.0.10 github.com/go-chi/chi/v5 v5.0.10
github.com/google/uuid v1.4.0
github.com/pelletier/go-toml/v2 v2.1.0
github.com/pion/interceptor v0.1.25
github.com/pion/sdp/v3 v3.0.6
github.com/pion/webrtc/v4 v4.0.0-beta.7
github.com/urfave/cli/v2 v2.25.7 github.com/urfave/cli/v2 v2.25.7
golang.org/x/crypto v0.15.0 golang.org/x/crypto v0.15.0
) )
@@ -11,24 +16,20 @@ require (
require ( require (
github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.3 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/uuid v1.4.0 // indirect
github.com/pion/datachannel v1.5.5 // indirect github.com/pion/datachannel v1.5.5 // indirect
github.com/pion/dtls/v2 v2.2.8 // indirect github.com/pion/dtls/v2 v2.2.8 // indirect
github.com/pion/ice/v3 v3.0.2 // indirect github.com/pion/ice/v3 v3.0.2 // indirect
github.com/pion/interceptor v0.1.25 // indirect
github.com/pion/logging v0.2.2 // indirect github.com/pion/logging v0.2.2 // indirect
github.com/pion/mdns v0.0.9 // indirect github.com/pion/mdns v0.0.9 // indirect
github.com/pion/randutil v0.1.0 // indirect github.com/pion/randutil v0.1.0 // indirect
github.com/pion/rtcp v1.2.12 // indirect github.com/pion/rtcp v1.2.12 // indirect
github.com/pion/rtp v1.8.3 // indirect github.com/pion/rtp v1.8.3 // indirect
github.com/pion/sctp v1.8.9 // indirect github.com/pion/sctp v1.8.9 // indirect
github.com/pion/sdp/v3 v3.0.6 // indirect
github.com/pion/srtp/v3 v3.0.1 // indirect github.com/pion/srtp/v3 v3.0.1 // indirect
github.com/pion/stun/v2 v2.0.0 // indirect github.com/pion/stun/v2 v2.0.0 // indirect
github.com/pion/transport/v2 v2.2.4 // indirect github.com/pion/transport/v2 v2.2.4 // indirect
github.com/pion/transport/v3 v3.0.1 // indirect github.com/pion/transport/v3 v3.0.1 // indirect
github.com/pion/turn/v3 v3.0.1 // indirect github.com/pion/turn/v3 v3.0.1 // indirect
github.com/pion/webrtc/v4 v4.0.0-beta.7 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/stretchr/testify v1.8.4 // indirect github.com/stretchr/testify v1.8.4 // indirect

5
go.sum
View File

@@ -25,8 +25,10 @@ github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
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/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
@@ -37,6 +39,8 @@ github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042
github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY=
github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo=
github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4=
github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8= github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew8=
github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0= github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0=
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
@@ -194,6 +198,7 @@ google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=

53
main.go
View File

@@ -2,14 +2,23 @@ package main
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"log/slog"
"net/http"
"os" "os"
"git.t-juice.club/torjus/ministream/server" "git.t-juice.club/torjus/ministream/server"
"github.com/urfave/cli/v2" "github.com/urfave/cli/v2"
) )
const Version = "v0.1.1" const Version = "v0.1.2"
type ctxKey string
const (
ctxKeyConfig ctxKey = "config"
)
func main() { func main() {
app := cli.App{ app := cli.App{
@@ -17,14 +26,18 @@ func main() {
Usage: "Small livestreaming platform.", Usage: "Small livestreaming platform.",
Commands: []*cli.Command{ Commands: []*cli.Command{
{ {
Name: "serve", Name: "serve",
Usage: "Start livestreaming server.", Usage: "Start livestreaming server.",
Action: func(ctx *cli.Context) error { Before: loadConfig,
store := server.NewUserStore() Action: func(c *cli.Context) error {
cfg := configFromCtx(c.Context)
srv := server.NewServer(store) srv := server.NewServer(cfg)
srv.Addr = ":8080" srv.Addr = cfg.HTTP.ListenAddr
srv.ListenAndServe() slog.Info("Starting HTTP-server", "addr", srv.Addr, "cfg", cfg)
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil return nil
}, },
}, },
@@ -37,3 +50,27 @@ func main() {
fmt.Println(err) fmt.Println(err)
} }
} }
func loadConfig(c *cli.Context) error {
cfg, err := server.ConfigFromDefault()
if err != nil {
return err
}
c.Context = context.WithValue(c.Context, ctxKeyConfig, cfg)
return nil
}
func configFromCtx(ctx context.Context) *server.Config {
value := ctx.Value(ctxKeyConfig)
if value == nil {
panic("unable to load config")
}
config, ok := value.(*server.Config)
if !ok {
panic("config type assertion failed")
}
return config
}

21
ministream.toml Normal file
View File

@@ -0,0 +1,21 @@
# SiteName is used for displaying title of site on frontend.
# Default: "ministream"
# Env: MINISTREAM_SITENAME
SiteName = "stream.example.org"
[HTTP]
# HTTPListenAddr is which port the HTTP server will listen on.
# Default: ":8080"
# Env: MINISTREAM_HTTP_LISTENADDR
ListenAddr = ":8080"
[WebRTC]
# UDPMin is the minimum used for ephemeral ports.
# Default: 50000
# Env: MINISTREAM_WEBRTC_UDPMIN
UDPMin = 50000
# UDPMax is the maximum used for ephemeral ports.
# Default: 50050
# Env: MINISTREAM_WEBRTC_UDPMAX
UDPMAX = 50050

107
server/config.go Normal file
View File

@@ -0,0 +1,107 @@
package server
import (
"io"
"os"
"strconv"
"github.com/pelletier/go-toml/v2"
)
type Config struct {
SiteName string `toml:"siteName"`
HTTP ConfigHTTP `toml:"http"`
WebRTC ConfigWebRTC `toml:"WebRTC"`
}
type ConfigHTTP struct {
ListenAddr string `json:"ListenAddr" toml:"ListenAddr"`
}
type ConfigWebRTC struct {
UDPMin int `toml:"UDPMin"`
UDPMax int `toml:"UDPMax"`
}
func DefaultConfig() *Config {
return &Config{
SiteName: "ministream",
HTTP: ConfigHTTP{
ListenAddr: ":8080",
},
WebRTC: ConfigWebRTC{
UDPMin: 50000,
UDPMax: 50050,
},
}
}
func (c *Config) OverrideFromEnv() {
if siteName, ok := os.LookupEnv("MINISTREAM_SITENAME"); ok {
c.SiteName = siteName
}
if httpAddr, ok := os.LookupEnv("MINISTREAM_HTTP_LISTENADDR"); ok {
c.HTTP.ListenAddr = httpAddr
}
if value, ok := os.LookupEnv("MINISTREAM_WEBRTC_UDPMIN"); ok {
min, err := strconv.Atoi(value)
if err != nil {
panic("MINISTREAM_WEBRTC_UDPMIN is invalid")
}
c.WebRTC.UDPMin = min
}
if value, ok := os.LookupEnv("MINISTREAM_WEBRTC_UDPMAX"); ok {
max, err := strconv.Atoi(value)
if err != nil {
panic("MINISTREAM_WEBRTC_UDPMAX is invalid")
}
c.WebRTC.UDPMin = max
}
}
func ConfigFromReader(r io.Reader) (*Config, error) {
var c Config
err := toml.NewDecoder(r).Decode(&c)
if err != nil {
return nil, err
}
return &c, nil
}
func ConfigFromFile(path string) (*Config, error) {
f, err := os.Open(path)
if err != nil {
return nil, err
}
defer f.Close()
return ConfigFromReader(f)
}
func ConfigFromDefault() (*Config, error) {
var config *Config
defaultPaths := []string{
"ministream.toml",
}
for _, p := range defaultPaths {
c, err := ConfigFromFile(p)
if err != nil {
continue
}
config = c
break
}
if config == nil {
config = DefaultConfig()
}
config.OverrideFromEnv()
return config, nil
}

37
server/config_test.go Normal file
View File

@@ -0,0 +1,37 @@
package server_test
import (
"strings"
"testing"
"git.t-juice.club/torjus/ministream/server"
)
func TestConfig(t *testing.T) {
t.Run("FromReader", func(t *testing.T) {
configString := `SiteName = "ministream.example.org"`
expectedSiteName := "ministream.example.org"
r := strings.NewReader(configString)
c, err := server.ConfigFromReader(r)
if err != nil {
t.Fatalf("Error reading config: %s", err)
}
if c.SiteName != expectedSiteName {
t.Errorf("SiteName incorrect. Got %s want %s", c.SiteName, expectedSiteName)
}
})
t.Run("OverrideFromEnv", func(t *testing.T) {
c := server.DefaultConfig()
expectedSiteName := "ms.example.org"
t.Setenv("MINISTREAM_SITENAME", expectedSiteName)
c.OverrideFromEnv()
if c.SiteName != expectedSiteName {
t.Errorf("SiteName incorrect. Got %s want %s", c.SiteName, expectedSiteName)
}
})
}

View File

@@ -19,15 +19,15 @@ import (
var static embed.FS var static embed.FS
type Server struct { type Server struct {
users *UserStore
streams *StreamStore streams *StreamStore
config *Config
http.Server http.Server
} }
func NewServer(store *UserStore) *Server { func NewServer(config *Config) *Server {
srv := &Server{ srv := &Server{
users: store, streams: NewStreamStore(config),
streams: NewStreamStore(), config: config,
} }
r := chi.NewRouter() r := chi.NewRouter()
@@ -41,6 +41,7 @@ func NewServer(store *UserStore) *Server {
r.Patch("/whip/{streamKey}", srv.PatchHandler) r.Patch("/whip/{streamKey}", srv.PatchHandler)
r.Post("/whip/{streamKey}", srv.PostOfferHandler) r.Post("/whip/{streamKey}", srv.PostOfferHandler)
r.Get("/stats", srv.streams.StatsHandler) r.Get("/stats", srv.streams.StatsHandler)
r.Get("/api/siteinfo", srv.InfoHandler)
srv.Handler = r srv.Handler = r
@@ -55,6 +56,17 @@ func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(fn) return http.HandlerFunc(fn)
} }
func (s *Server) InfoHandler(w http.ResponseWriter, r *http.Request) {
var infoResponse struct {
SiteName string `json:"siteName"`
}
infoResponse.SiteName = s.config.SiteName
if err := json.NewEncoder(w).Encode(&infoResponse); err != nil {
slog.Warn("Error writing info response")
}
}
func (s *Server) OptionsHandler(w http.ResponseWriter, r *http.Request) { func (s *Server) OptionsHandler(w http.ResponseWriter, r *http.Request) {
slog.Info("Got OPTIONS") slog.Info("Got OPTIONS")
} }
@@ -124,9 +136,18 @@ func (s *Server) DeleteHandler(w http.ResponseWriter, r *http.Request) {
} }
func (s *Server) ListHandler(w http.ResponseWriter, r *http.Request) { func (s *Server) ListHandler(w http.ResponseWriter, r *http.Request) {
streams := s.streams.List() type StreamInfo struct {
StreamKey string `json:"streamKey"`
ViewCount int `json:"viewCount"`
}
infos := []StreamInfo{}
for key, stream := range s.streams.Streams {
infos = append(infos, StreamInfo{StreamKey: key, ViewCount: len(stream.viewers)})
}
enc := json.NewEncoder(w) enc := json.NewEncoder(w)
enc.Encode(&streams) enc.Encode(&infos)
} }
func (s *Server) WhipHandler(w http.ResponseWriter, r *http.Request) { func (s *Server) WhipHandler(w http.ResponseWriter, r *http.Request) {

View File

@@ -10,6 +10,7 @@ import (
"sync" "sync"
"time" "time"
"github.com/google/uuid"
"github.com/pion/interceptor" "github.com/pion/interceptor"
"github.com/pion/interceptor/pkg/nack" "github.com/pion/interceptor/pkg/nack"
"github.com/pion/interceptor/pkg/stats" "github.com/pion/interceptor/pkg/stats"
@@ -19,26 +20,30 @@ import (
var ErrNoSuchStream error = fmt.Errorf("no such stream") var ErrNoSuchStream error = fmt.Errorf("no such stream")
type StreamStore struct { type StreamStore struct {
Streams map[string]*Stream Streams map[string]*Stream
config *Config
webRTCConfig webrtc.Configuration webRTCConfig webrtc.Configuration
mu sync.Mutex mu sync.Mutex
} }
func NewStreamStore() *StreamStore { func NewStreamStore(config *Config) *StreamStore {
s := &StreamStore{ s := &StreamStore{
Streams: make(map[string]*Stream), Streams: make(map[string]*Stream),
config: config,
} }
return s return s
} }
type Stream struct { type Stream struct {
store *StreamStore
peerConnection *webrtc.PeerConnection peerConnection *webrtc.PeerConnection
peerConnectionStats map[string]*stats.Stats peerConnectionStats map[string]*stats.Stats
peerConnectionStatsMu sync.Mutex peerConnectionStatsMu sync.Mutex
lastUpdate time.Time lastUpdate time.Time
localTracks []*webrtc.TrackLocalStaticRTP localTracks []*webrtc.TrackLocalStaticRTP
peers []*webrtc.PeerConnection viewers map[string]*webrtc.PeerConnection
mu sync.Mutex mu sync.Mutex
} }
@@ -68,6 +73,7 @@ func (s *StreamStore) StatsHandler(w http.ResponseWriter, r *http.Request) {
} }
func (s *Stream) AddListener(sd *webrtc.SessionDescription) (*webrtc.SessionDescription, error) { func (s *Stream) AddListener(sd *webrtc.SessionDescription) (*webrtc.SessionDescription, error) {
viewerID := uuid.New().String()
m := &webrtc.MediaEngine{} m := &webrtc.MediaEngine{}
if err := m.RegisterDefaultCodecs(); err != nil { if err := m.RegisterDefaultCodecs(); err != nil {
panic(err) panic(err)
@@ -96,7 +102,10 @@ func (s *Stream) AddListener(sd *webrtc.SessionDescription) (*webrtc.SessionDesc
//i.Add(statsInterceptorFactory) //i.Add(statsInterceptorFactory)
se := webrtc.SettingEngine{} se := webrtc.SettingEngine{}
_ = se.SetEphemeralUDPPortRange(50000, 50050) _ = se.SetEphemeralUDPPortRange(
uint16(s.store.config.WebRTC.UDPMin),
uint16(s.store.config.WebRTC.UDPMax),
)
webRTCConfig := webrtc.Configuration{ webRTCConfig := webrtc.Configuration{
ICEServers: []webrtc.ICEServer{ ICEServers: []webrtc.ICEServer{
@@ -117,6 +126,10 @@ func (s *Stream) AddListener(sd *webrtc.SessionDescription) (*webrtc.SessionDesc
rtpSender, err := peerConnection.AddTrack(ltrack) rtpSender, err := peerConnection.AddTrack(ltrack)
if err != nil { if err != nil {
// TODO, stop peerconn // TODO, stop peerconn
peerConnection.Close()
s.mu.Lock()
delete(s.viewers, viewerID)
s.mu.Unlock()
return nil, err return nil, err
} }
go func() { go func() {
@@ -124,6 +137,9 @@ func (s *Stream) AddListener(sd *webrtc.SessionDescription) (*webrtc.SessionDesc
for { for {
if _, _, err := rtpSender.Read(rtcpBuf); err != nil { if _, _, err := rtpSender.Read(rtcpBuf); err != nil {
peerConnection.Close() peerConnection.Close()
s.mu.Lock()
delete(s.viewers, viewerID)
s.mu.Unlock()
return return
} }
} }
@@ -166,7 +182,7 @@ func (s *Stream) AddListener(sd *webrtc.SessionDescription) (*webrtc.SessionDesc
// in a production application you should exchange ICE Candidates via OnICECandidate // in a production application you should exchange ICE Candidates via OnICECandidate
<-gatherComplete <-gatherComplete
s.mu.Lock() s.mu.Lock()
s.peers = append(s.peers, peerConnection) s.viewers[viewerID] = peerConnection
defer s.mu.Unlock() defer s.mu.Unlock()
@@ -179,8 +195,10 @@ func (s *StreamStore) Add(streamKey string, sd *webrtc.SessionDescription) (*web
go func() { go func() {
stream := &Stream{ stream := &Stream{
store: s,
lastUpdate: time.Now(), lastUpdate: time.Now(),
peerConnectionStats: make(map[string]*stats.Stats), peerConnectionStats: make(map[string]*stats.Stats),
viewers: make(map[string]*webrtc.PeerConnection),
} }
m := &webrtc.MediaEngine{} m := &webrtc.MediaEngine{}
if err := m.RegisterDefaultCodecs(); err != nil { if err := m.RegisterDefaultCodecs(); err != nil {
@@ -226,7 +244,7 @@ func (s *StreamStore) Add(streamKey string, sd *webrtc.SessionDescription) (*web
}, },
} }
se := webrtc.SettingEngine{} se := webrtc.SettingEngine{}
_ = se.SetEphemeralUDPPortRange(50000, 50050) _ = se.SetEphemeralUDPPortRange(uint16(s.config.WebRTC.UDPMin), uint16(s.config.WebRTC.UDPMax))
// se.BufferFactory = func(packetType packetio.BufferPacketType, ssrc uint32) io.ReadWriteCloser { // se.BufferFactory = func(packetType packetio.BufferPacketType, ssrc uint32) io.ReadWriteCloser {
// buf := packetio.NewBuffer() // buf := packetio.NewBuffer()
@@ -383,7 +401,7 @@ func (s *StreamStore) Delete(streamKey string) error {
defer s.mu.Unlock() defer s.mu.Unlock()
delete(s.Streams, streamKey) delete(s.Streams, streamKey)
for _, peer := range stream.peers { for _, peer := range stream.viewers {
if err := peer.Close(); err != nil { if err := peer.Close(); err != nil {
slog.Warn("Error closing peer.", "error", err) slog.Warn("Error closing peer.", "error", err)
} }