12 files changed
+166
-38
No files matched your search
@@ -25,7 +25,10 @@ jobs:
|
|||||||
node-version: 18
|
node-version: 18
|
||||||
cache-dependency-path: static
|
cache-dependency-path: static
|
||||||
cache: npm
|
cache: npm
|
||||||
- run: npm --prefix static ci && npm --prefix static run build
|
- run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install --yes --no-install-recommends make
|
||||||
|
- run: make build-static
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: static
|
name: static
|
||||||
@@ -71,7 +74,7 @@ jobs:
|
|||||||
echo "GOARCH=${{ matrix.target.goarch }}" >> $GITHUB_ENV
|
echo "GOARCH=${{ matrix.target.goarch }}" >> $GITHUB_ENV
|
||||||
echo "PKG_CONFIG_PATH=/usr/lib/${{ matrix.target.prefix }}/pkgconfig" >> $GITHUB_ENV
|
echo "PKG_CONFIG_PATH=/usr/lib/${{ matrix.target.prefix }}/pkgconfig" >> $GITHUB_ENV
|
||||||
echo "CC=${PREFIX:+$PREFIX-}gcc" >> $GITHUB_ENV
|
echo "CC=${PREFIX:+$PREFIX-}gcc" >> $GITHUB_ENV
|
||||||
- run: make build-static
|
- run: make build
|
||||||
- uses: actions/upload-artifact@v3
|
- uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: service-${{ matrix.target.os }}-${{ matrix.target.goarch }}
|
name: service-${{ matrix.target.os }}-${{ matrix.target.goarch }}
|
||||||
|
|||||||
Vendored
+2
-2
@@ -12,8 +12,8 @@
|
|||||||
"program": "${workspaceFolder}/main.go",
|
"program": "${workspaceFolder}/main.go",
|
||||||
"env": {
|
"env": {
|
||||||
"HTTP_STATIC": "${workspaceFolder}/static/dist",
|
"HTTP_STATIC": "${workspaceFolder}/static/dist",
|
||||||
"VIDEO_DEVICE": "/dev/video4",
|
"VIDEO_OUT_DEVICE": "/dev/video4",
|
||||||
"AUDIO_DEVICE": "plughw:0,1,0"
|
"AUDIO_OUT_DEVICE": "plughw:0,1,0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
|
|
||||||
build:
|
build:
|
||||||
CGO_ENABLED=1 go build -mod=vendor -o service main.go
|
CGO_ENABLED=1 go build -mod=vendor -tags=embed -o service main.go
|
||||||
|
|
||||||
build-static:
|
build-static:
|
||||||
CGO_ENABLED=1 go build -mod=vendor -tags=embed -o service main.go
|
npm --prefix static ci && npm --prefix static run build
|
||||||
|
|
||||||
build-armhf:
|
build-armhf:
|
||||||
GOARCH=arm \
|
GOARCH=arm \
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Webrtc streamer based on golang (pion) and gstreamer
|
||||||
|
|
||||||
|
This project demonstrates who to stream any source (thanks to gstreamer) in a webrtc session using golang pion to a webrtc client (angular).
|
||||||
|
|
||||||
|
## build locally
|
||||||
|
|
||||||
|
### Build web client
|
||||||
|
To build the web client static sources make sure you have `node` and `npm` installed.
|
||||||
|
```
|
||||||
|
make build-static
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build service
|
||||||
|
To build the service make sure you have build the `static` content first.
|
||||||
|
```
|
||||||
|
make build
|
||||||
|
```
|
||||||
|
|
||||||
|
## build cross
|
||||||
|
To build for `arm` or `arm64` just use the sections in the `Makefile`. Make sure you have `podman` installed as it will be used to create the tmp build container.
|
||||||
|
```
|
||||||
|
make build-armhf
|
||||||
|
```
|
||||||
@@ -26,11 +26,17 @@ func main() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
http := server.NewHttpServer(lg.With(zap.String("context", "server")), &cfg)
|
http := server.NewHttpServer(lg.With(zap.String("context", "server")), &cfg.Http)
|
||||||
|
|
||||||
webrtc.NewWebrtcHandler(ctx, lg.With(zap.String("context", "webrtc")), cfg.Stream(), http.Hndl)
|
err = webrtc.NewWebrtcHandler(ctx, lg.With(zap.String("context", "webrtc")), cfg.Stream(), http.Hndl)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
http.ListenAndServe(ctx, cfg.Http.Address())
|
err = http.ListenAndServe(ctx)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
// Listen for the interrupt signal.
|
// Listen for the interrupt signal.
|
||||||
<-ctx.Done()
|
<-ctx.Done()
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateSelfSigned() (*tls.Certificate, error) {
|
||||||
|
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
serialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)
|
||||||
|
serialNumber, err := rand.Int(rand.Reader, serialNumberLimit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to generate serial number: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
template := x509.Certificate{
|
||||||
|
SerialNumber: serialNumber,
|
||||||
|
Subject: pkix.Name{Organization: []string{"PHI"}},
|
||||||
|
NotBefore: time.Now(),
|
||||||
|
NotAfter: time.Now().Add(time.Hour * 24 * 180),
|
||||||
|
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||||
|
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||||
|
BasicConstraintsValid: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
cert, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &tls.Certificate{
|
||||||
|
PrivateKey: priv,
|
||||||
|
Certificate: [][]byte{cert},
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
+18
-15
@@ -28,33 +28,36 @@ type ConfigLogging struct {
|
|||||||
type ConfigHTTP struct {
|
type ConfigHTTP struct {
|
||||||
Host string `arg:"--http-host,env:HTTP_HOST" default:"0.0.0.0"`
|
Host string `arg:"--http-host,env:HTTP_HOST" default:"0.0.0.0"`
|
||||||
Port uint `arg:"--http-port,env:HTTP_PORT" default:"8080"`
|
Port uint `arg:"--http-port,env:HTTP_PORT" default:"8080"`
|
||||||
|
Tls bool `arg:"--http-tls,env:HTTP_TLS" default:"true"`
|
||||||
|
TlsKey *string `arg:"--http-tls-key,env:HTTP_TLS_KEY"`
|
||||||
|
TlsCert *string `arg:"--http-tls-cert,env:HTTP_TLS_CERT"`
|
||||||
PathGetLiveness string `arg:"env:HTTP_PATH_LIVENESS" default:"/healthz"`
|
PathGetLiveness string `arg:"env:HTTP_PATH_LIVENESS" default:"/healthz"`
|
||||||
PathGetReadiness string `arg:"env:HTTP_PATH_READINESS" default:"/readyz"`
|
PathGetReadiness string `arg:"env:HTTP_PATH_READINESS" default:"/readyz"`
|
||||||
StaticPath *string `arg:"--http-static,env:HTTP_STATIC"`
|
StaticPath *string `arg:"--http-static,env:HTTP_STATIC"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfigVideoOutputStream struct {
|
type ConfigVideoOutputStream struct {
|
||||||
Source string `arg:"--video-src,env:VIDEO_SRC" default:"v4l2src"`
|
Source string `arg:"--video-out-src,env:VIDEO_OUT_SRC" default:"v4l2src"`
|
||||||
Device string `arg:"--video-device,env:VIDEO_DEVICE" default:"/dev/video0"`
|
Device string `arg:"--video-out-device,env:VIDEO_OUT_DEVICE" default:"/dev/video0"`
|
||||||
Codec string `arg:"--video-codec,env:VIDEO_CODEC" default:"vp8"`
|
Codec string `arg:"--video-out-codec,env:VIDEO_OUT_CODEC" default:"vp8"`
|
||||||
Height uint `arg:"--video-height,env:VIDEO_HEIGHT" default:"480"`
|
Height uint `arg:"--video-out-height,env:VIDEO_OUT_HEIGHT" default:"480"`
|
||||||
Width uint `arg:"--video-width,env:VIDEO_WIDTH" default:"640"`
|
Width uint `arg:"--video-out-width,env:VIDEO_OUT_WIDTH" default:"640"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfigAudioOutputStream struct {
|
type ConfigAudioOutputStream struct {
|
||||||
Source string `arg:"--audio-src,env:AUDIO_SRC" default:"alsasrc"`
|
Source string `arg:"--audio-out-src,env:AUDIO_OUT_SRC" default:"alsasrc"`
|
||||||
DeviceName string `arg:"--audio-device-name,env:AUDIO_DEVICE" default:"default"`
|
DeviceName string `arg:"--audio-out-device-name,env:AUDIO_OUT_DEVICE" default:"default"`
|
||||||
Device *string `arg:"--audio-device,env:AUDIO_DEVICE"`
|
Device *string `arg:"--audio-out-device,env:AUDIO_OUT_DEVICE"`
|
||||||
Codec string `arg:"--audio-codec,env:AUDIO_CODEC" default:"opus"`
|
Codec string `arg:"--audio-out-codec,env:AUDIO_OUT_CODEC" default:"opus"`
|
||||||
Channels uint `arg:"--audio-channels,env:AUDIO_CHANNELS" default:"1"`
|
Channels uint `arg:"--audio-out-channels,env:AUDIO_OUT_CHANNELS" default:"1"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ConfigAudioInputStream struct {
|
type ConfigAudioInputStream struct {
|
||||||
Sink string `arg:"--audio-src,env:AUDIO_SINK" default:"alsasink"`
|
Sink string `arg:"--audio-in-src,env:AUDIO_IN_SINK" default:"alsasink"`
|
||||||
DeviceName string `arg:"--audio-device-name,env:AUDIO_DEVICE" default:"default"`
|
DeviceName string `arg:"--audio-in-device-name,env:AUDIO_IN_DEVICE" default:"default"`
|
||||||
Device *string `arg:"--audio-device,env:AUDIO_DEVICE"`
|
Device *string `arg:"--audio-in-device,env:AUDIO_IN_DEVICE"`
|
||||||
Codec string `arg:"--audio-codec,env:AUDIO_CODEC" default:"opus"`
|
Codec string `arg:"--audio-in-codec,env:AUDIO_IN_CODEC" default:"opus"`
|
||||||
Channels uint `arg:"--audio-channels,env:AUDIO_CHANNELS" default:"1"`
|
Channels uint `arg:"--audio-in-channels,env:AUDIO_IN_CHANNELS" default:"1"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Config) Stream() *ConfigStream {
|
func (c *Config) Stream() *ConfigStream {
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
func Time[T any](runnable func() (T, error)) (time.Duration, T, error) {
|
||||||
|
t := time.Now()
|
||||||
|
res, err := runnable()
|
||||||
|
return time.Since(t), res, err
|
||||||
|
}
|
||||||
+50
-10
@@ -2,6 +2,7 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
@@ -24,6 +25,7 @@ type SignalingHandle struct {
|
|||||||
type HttpServer struct {
|
type HttpServer struct {
|
||||||
http.Server
|
http.Server
|
||||||
lg *zap.Logger
|
lg *zap.Logger
|
||||||
|
cfg *common.ConfigHTTP
|
||||||
Hndl chan *SignalingHandle
|
Hndl chan *SignalingHandle
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -35,9 +37,10 @@ func NewSignalingHandle(id string) SignalingHandle {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHttpServer(lg *zap.Logger, cfg *common.Config) *HttpServer {
|
func NewHttpServer(lg *zap.Logger, cfg *common.ConfigHTTP) *HttpServer {
|
||||||
h := HttpServer{
|
h := HttpServer{
|
||||||
Hndl: make(chan *SignalingHandle, 10),
|
Hndl: make(chan *SignalingHandle, 10),
|
||||||
|
cfg: cfg,
|
||||||
lg: lg,
|
lg: lg,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -45,7 +48,7 @@ func NewHttpServer(lg *zap.Logger, cfg *common.Config) *HttpServer {
|
|||||||
engine.GET("/signaling/:id", h.signalingHandler)
|
engine.GET("/signaling/:id", h.signalingHandler)
|
||||||
|
|
||||||
// static handler
|
// static handler
|
||||||
static.SetupHandler(engine, cfg.Http)
|
static.SetupHandler(engine, cfg)
|
||||||
|
|
||||||
// set out handler
|
// set out handler
|
||||||
h.Handler = engine
|
h.Handler = engine
|
||||||
@@ -53,18 +56,55 @@ func NewHttpServer(lg *zap.Logger, cfg *common.Config) *HttpServer {
|
|||||||
return &h
|
return &h
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HttpServer) ListenAndServe(ctx context.Context, addr string) {
|
func (h *HttpServer) ListenAndServe(ctx context.Context) error {
|
||||||
go func() {
|
if h.cfg.Tls {
|
||||||
|
|
||||||
// set the configured address
|
// set the configured address
|
||||||
h.Addr = addr
|
h.Addr = h.cfg.Address()
|
||||||
|
|
||||||
// and listen
|
if h.cfg.TlsCert != nil && h.cfg.TlsKey != nil {
|
||||||
if err := h.Server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
cert, err := tls.LoadX509KeyPair(*h.cfg.TlsCert, *h.cfg.TlsKey)
|
||||||
h.lg.Fatal("listen failed", zap.Error(err))
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
h.Server.TLSConfig = &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{cert},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
h.lg.Info("creating self signed certificate")
|
||||||
|
var cert *tls.Certificate
|
||||||
|
duration, cert, err := common.Time(func() (*tls.Certificate, error) {
|
||||||
|
return common.GenerateSelfSigned()
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.lg.Info("certificate created", zap.Duration("elapsed", duration))
|
||||||
|
|
||||||
|
h.Server.TLSConfig = &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{*cert},
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}()
|
go func() {
|
||||||
|
// and listen
|
||||||
|
if err := h.Server.ListenAndServeTLS("", ""); err != nil && err != http.ErrServerClosed {
|
||||||
|
h.lg.Fatal("listen failed", zap.Error(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
} else {
|
||||||
|
// set the configured address
|
||||||
|
h.Addr = h.cfg.Address()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// and listen
|
||||||
|
if err := h.Server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
h.lg.Fatal("listen failed", zap.Error(err))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HttpServer) TearDown() error {
|
func (h *HttpServer) TearDown() error {
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ func (wh *WebrtcHandler) handleAudioSamples(ctx context.Context, cfg *common.Con
|
|||||||
if cfg.Device != nil {
|
if cfg.Device != nil {
|
||||||
properties["device"] = *cfg.Device
|
properties["device"] = *cfg.Device
|
||||||
} else {
|
} else {
|
||||||
properties["device-name"] = cfg.DeviceName
|
properties["device"] = cfg.DeviceName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -38,7 +38,7 @@ func (s staticDiskSource) Open(p string) (common.StaticSourceFile, error) {
|
|||||||
return &staticSourceFile{t}, nil
|
return &staticSourceFile{t}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetupHandler(e *gin.Engine, cfg common.ConfigHTTP) {
|
func SetupHandler(e *gin.Engine, cfg *common.ConfigHTTP) {
|
||||||
if cfg.StaticPath != nil {
|
if cfg.StaticPath != nil {
|
||||||
handler := common.NewStaticHandler(staticDiskSource{*cfg.StaticPath}, "index.html")
|
handler := common.NewStaticHandler(staticDiskSource{*cfg.StaticPath}, "index.html")
|
||||||
e.NoRoute(func(c *gin.Context) {
|
e.NoRoute(func(c *gin.Context) {
|
||||||
|
|||||||
+1
-1
@@ -41,7 +41,7 @@ func (s staticSource) Open(p string) (common.StaticSourceFile, error) {
|
|||||||
return &staticSourceFile{t}, nil
|
return &staticSourceFile{t}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetupHandler(e *gin.Engine, _ common.ConfigHTTP) {
|
func SetupHandler(e *gin.Engine, _ *common.ConfigHTTP) {
|
||||||
handler := common.NewStaticHandler(staticSource{"dist"}, "index.html")
|
handler := common.NewStaticHandler(staticSource{"dist"}, "index.html")
|
||||||
e.NoRoute(func(c *gin.Context) {
|
e.NoRoute(func(c *gin.Context) {
|
||||||
encoding := c.Request.Header.Get("Accept-Encoding")
|
encoding := c.Request.Header.Get("Accept-Encoding")
|
||||||
|
|||||||
Reference in new issue
Block a user