Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adventures in react ui #3

Merged
merged 15 commits into from Jan 11, 2023
27 changes: 27 additions & 0 deletions .gitignore
Expand Up @@ -11,5 +11,32 @@
# Output of the go coverage tool, specifically when used with LiteIDE
*.out

.DS_Store

# Yarn:
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
ui/node_modules
ui/.pnp
ui/.pnp.js

# testing
ui/coverage

# production
ui/build

# misc
ui/.env.local
ui/.env.development.local
ui/.env.test.local
ui/.env.production.local

ui/npm-debug.log*
ui/yarn-debug.log*
ui/yarn-error.log*

# Yarn // end

pion-the-sky
boos
9 changes: 9 additions & 0 deletions README.md
Expand Up @@ -33,3 +33,12 @@ go run ./... all
2. Record a video at `http://localhost:8083/record`.
3. Hit the back button (or optionally disconnect and then hit the back button).
4. Play the videos you recorded.

# User interface

```sh
CXX=clang++ yarn global add create-react-app
yarn create react-app ui --template typescript
cd ui/
yarn add -D cra-build-watch
```
4 changes: 2 additions & 2 deletions cmd/all/cmd.go
Expand Up @@ -20,8 +20,8 @@ var Command = &cobra.Command{
}

var (
backEndConfig = configs.NewBackEndConfig()
frontEndConfig = configs.NewFrontEndConfig()
backEndConfig = configs.NewBackendConfig()
frontEndConfig = configs.NewFrontendConfig()
logConfig = configs.NewLoggingConfig()
webRTCConfig = configs.NewWebRTCConfig()
)
Expand Down
4 changes: 2 additions & 2 deletions cmd/backend/cmd.go
Expand Up @@ -20,8 +20,8 @@ var Command = &cobra.Command{
}

var (
backEndConfig = configs.NewBackEndConfig()
frontEndConfig = configs.NewFrontEndConfig()
backEndConfig = configs.NewBackendConfig()
frontEndConfig = configs.NewFrontendConfig()
logConfig = configs.NewLoggingConfig()
webRTCConfig = configs.NewWebRTCConfig()
)
Expand Down
4 changes: 2 additions & 2 deletions cmd/frontend/cmd.go
Expand Up @@ -20,8 +20,8 @@ var Command = &cobra.Command{
}

var (
backEndConfig = configs.NewBackEndConfig()
frontEndConfig = configs.NewFrontEndConfig()
backEndConfig = configs.NewBackendConfig()
frontEndConfig = configs.NewFrontendConfig()
logConfig = configs.NewLoggingConfig()
webRTCConfig = configs.NewWebRTCConfig()
)
Expand Down
14 changes: 7 additions & 7 deletions configs/backend.go
Expand Up @@ -5,27 +5,27 @@ import (
"github.com/spf13/viper"
)

// BackEndConfig represents backend configuration.
type BackEndConfig struct {
// BackendConfig represents backend configuration.
type BackendConfig struct {
flagBase

BindAddress string
ExternalAddress string
}

// InitFromViper initializes this configuration from viper.
func (c *BackEndConfig) InitFromViper(v *viper.Viper) {
func (c *BackendConfig) InitFromViper(v *viper.Viper) {
c.BindAddress = v.GetString("backend-bind-address")
c.ExternalAddress = v.GetString("backend-external-address")
}

// NewBackEndConfig returns a new backend configuration.
func NewBackEndConfig() *BackEndConfig {
return &BackEndConfig{}
// NewBackendConfig returns a new backend configuration.
func NewBackendConfig() *BackendConfig {
return &BackendConfig{}
}

// FlagSet returns an instance of the flag set for the configuration.
func (c *BackEndConfig) FlagSet() *pflag.FlagSet {
func (c *BackendConfig) FlagSet() *pflag.FlagSet {
if c.initFlagSet() {
c.flagSet.StringVar(&c.BindAddress, "backend-bind-address", "127.0.0.1:8082", "Host-port to bind the backend server on")
c.flagSet.StringVar(&c.ExternalAddress, "backend-external-address", "http://localhost:8082", "External address this backend server is reachable at")
Expand Down
21 changes: 14 additions & 7 deletions configs/frontend.go
Expand Up @@ -5,30 +5,37 @@ import (
"github.com/spf13/viper"
)

// FrontEndConfig represents frontend configuration.
type FrontEndConfig struct {
// FrontendConfig represents frontend configuration.
type FrontendConfig struct {
flagBase

BindAddress string
ExternalAddress string

StaticDirectoryPath string
StaticDirectoryRootDocument string
}

// InitFromViper initializes this configuration from viper.
func (c *FrontEndConfig) InitFromViper(v *viper.Viper) {
func (c *FrontendConfig) InitFromViper(v *viper.Viper) {
c.BindAddress = v.GetString("frontend-bind-address")
c.ExternalAddress = v.GetString("frontend-external-address")
c.StaticDirectoryPath = v.GetString("static-directory-path")
c.StaticDirectoryRootDocument = v.GetString("static-directory-root-document")
}

// NewFrontEndConfig returns a new frontend configuration.
func NewFrontEndConfig() *FrontEndConfig {
return &FrontEndConfig{}
// NewFrontendConfig returns a new frontend configuration.
func NewFrontendConfig() *FrontendConfig {
return &FrontendConfig{}
}

// FlagSet returns an instance of the flag set for the configuration.
func (c *FrontEndConfig) FlagSet() *pflag.FlagSet {
func (c *FrontendConfig) FlagSet() *pflag.FlagSet {
if c.initFlagSet() {
c.flagSet.StringVar(&c.BindAddress, "frontend-bind-address", "127.0.0.1:8083", "Host-port to bind the frontend server on")
c.flagSet.StringVar(&c.ExternalAddress, "frontend-external-address", "http://localhost:8083", "External address this frontend server is available on")
c.flagSet.StringVar(&c.StaticDirectoryPath, "static-directory-path", "./ui/build", "Frontend static directory path")
c.flagSet.StringVar(&c.StaticDirectoryRootDocument, "static-directory-root-document", "index.html", "Frontend static directory root document")
}
return c.flagSet
}
3 changes: 2 additions & 1 deletion pkg/backend/client.go
Expand Up @@ -11,8 +11,9 @@ import (
"github.com/pion/sdp/v2"
"github.com/pion/webrtc/v3"
"github.com/pion/webrtc/v3/pkg/media"
"github.com/pion/webrtc/v3/pkg/media/ivfwriter"
"github.com/pion/webrtc/v3/pkg/media/oggwriter"

"github.com/radekg/boos/pkg/media/ivfwriter"
)

// PeerClientType represents the types of signal messages
Expand Down
6 changes: 3 additions & 3 deletions pkg/backend/signaling.go
Expand Up @@ -12,15 +12,15 @@ import (
// Server implements a WebRTC signal server used to locate and connect up with peers.
// In this simple case the peer is the server.
type Server struct {
frontEndConfig *configs.FrontEndConfig
frontEndConfig *configs.FrontendConfig
webRTCConfig *configs.WebRTCConfig
logger hclog.Logger
services *WebRTCService
}

// ServeListen creates a new frontend server and attempts to listen.
func ServeListen(backEndConfig *configs.BackEndConfig,
frontEndConfig *configs.FrontEndConfig,
func ServeListen(backEndConfig *configs.BackendConfig,
frontEndConfig *configs.FrontendConfig,
webRTCConfig *configs.WebRTCConfig,
logger hclog.Logger) error {

Expand Down
86 changes: 73 additions & 13 deletions pkg/backend/webrtc.go
Expand Up @@ -65,8 +65,42 @@ func CreateNewWebRTCService(webRTCConfig *configs.WebRTCConfig, logger hclog.Log
return nil, err
}

if err := svc.mediaEngine.RegisterDefaultCodecs(); err != nil {
svc.logger.Error("Failed registering default codecs", "reason", err)
// Select the exact codecs we're prepared to handle. Otherwise peers may
// negotiate something that we're not prepared to handle.
// Preferably, we should be able to simply register the default set of codecs
// but we need more logic around recording handling.
// For example, Safari will negotiate H264.

// if err := svc.mediaEngine.RegisterDefaultCodecs(); err != nil {
// svc.logger.Error("Failed registering default codecs", "reason", err)
// return nil, err
// }

if err := svc.mediaEngine.RegisterCodec(webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeOpus,
ClockRate: 48000,
Channels: 2,
SDPFmtpLine: "minptime=10;useinbandfec=1",
RTCPFeedback: nil,
},
PayloadType: 111,
}, webrtc.RTPCodecTypeAudio); err != nil {
svc.logger.Error("Failed registering audio codec", "reason", err)
return nil, err
}

if err := svc.mediaEngine.RegisterCodec(webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeVP8,
ClockRate: 90000,
Channels: 0,
SDPFmtpLine: "max-fs=12288;max-fr=30",
RTCPFeedback: nil,
},
PayloadType: 96,
}, webrtc.RTPCodecTypeVideo); err != nil {
svc.logger.Error("Failed registering audio codec", "reason", err)
return nil, err
}

Expand Down Expand Up @@ -197,27 +231,19 @@ func (svc *WebRTCService) CreatePlaybackConnection(client *PeerClient) error {
}()

go func() {
ivf, header, ivfErr := ivfreader.NewWith(bytes.NewReader(avr.video.Bytes()))
ivf, _, ivfErr := ivfreader.NewWith(bytes.NewReader(avr.video.Bytes()))
if ivfErr != nil {
panic(ivfErr)
}

// Wait for connection established
<-iceConnectedCtx.Done()

// Send our video file frame at a time. Pace our sending so we send it at the same speed it should be played back as.
// This isn't required since the video is timestamped, but we will such much higher loss if we send all at once.
//
// It is important to use a time.Ticker instead of time.Sleep because
// * avoids accumulating skew, just calling time.Sleep didn't compensate for the time spent parsing the data
// * works around latency issues with Sleep (see https://github.com/golang/go/issues/44343)
duration := time.Millisecond * time.Duration((float32(header.TimebaseNumerator)/float32(header.TimebaseDenominator))*2300)
lastTs := uint64(0)
for {
ctx, ctxCancelFunc := context.WithTimeout(context.Background(), duration)
frame, header, ivfErr := ivf.ParseNextFrame()
if errors.Is(ivfErr, io.EOF) {
fmt.Printf("All video frames parsed and sent")
ctxCancelFunc()
break
}

Expand All @@ -229,7 +255,23 @@ func (svc *WebRTCService) CreatePlaybackConnection(client *PeerClient) error {
panic(ivfErr)
}

<-ctx.Done()
var diffMs int64
if lastTs > 0 {
diff := header.Timestamp - lastTs
diffMs = int64(diff)
}
lastTs = header.Timestamp

if diffMs > 0 {
// Okay, what I'm seeing ts that various input devices send various frame rates.
// What's even more interesting, for an Apple Screen camera, I see variable number of frames
// in every second of a stream.
// Because of that, I assume that the only correct way to recreate the pace of the video
// is to calculate the exact difference between frames in milliseconds.
// Basically, there's no such thing as fps, it's maximum fps.
d := time.Duration(diffMs/100) * time.Millisecond
<-time.After(d)
}

}
}()
Expand Down Expand Up @@ -403,6 +445,24 @@ func VP8FrameHeaderToString(fh *vp8.FrameHeader) string {

}

// VP8FrameHeaderToString compiles a vp8 video frame header fields into a string for logging.
func RTPHeaderToString(fh rtp.Header) string {
return fmt.Sprintf("VP8:{Timestamp:%d SequenceNumber:%d, CSRC: %v, Extension: %v, ExtensionProfile: %v, Extensions: %v, Marker: %v, Padding: %v, PayloadType: %v, SSRC: %v, Version: %v}",
fh.Timestamp,
fh.SequenceNumber,
fh.CSRC,
fh.Extension,
fh.ExtensionProfile,
fh.Extensions,
fh.Marker,
fh.Padding,
fh.PayloadType,
fh.SSRC,
fh.Version,
)

}

/*
// SaveAsPNG saves the specified image as a png file.
func SaveAsPNG(img *image.YCbCr, fn string) error {
Expand Down
40 changes: 33 additions & 7 deletions pkg/frontend/server.go
Expand Up @@ -4,6 +4,8 @@ import (
"encoding/json"
"fmt"
"net/http"
"os"
"path/filepath"
"time"

"github.com/hashicorp/go-hclog"
Expand All @@ -12,14 +14,14 @@ import (

// Server is a basic implementation of a frontend server.
type Server struct {
backEndConfig *configs.BackEndConfig
backEndConfig *configs.BackendConfig
webRTCConfig *configs.WebRTCConfig
logger hclog.Logger
}

// ServeListen creates a new frontend server and attempts to listen.
func ServeListen(backEndConfig *configs.BackEndConfig,
frontEndConfig *configs.FrontEndConfig,
func ServeListen(backEndConfig *configs.BackendConfig,
frontEndConfig *configs.FrontendConfig,
webRTCConfig *configs.WebRTCConfig,
logger hclog.Logger) error {

Expand All @@ -29,14 +31,38 @@ func ServeListen(backEndConfig *configs.BackEndConfig,
webRTCConfig: webRTCConfig,
}

fs := http.FileServer(http.Dir("./public"))
http.HandleFunc("/backend", srv.backendHandler)
http.Handle("/", fs)
fileServer := http.FileServer(http.Dir(frontEndConfig.StaticDirectoryPath))

chanErr := make(chan error, 1)

go func() {
err := http.ListenAndServe(frontEndConfig.BindAddress, nil)
err := http.ListenAndServe(frontEndConfig.BindAddress, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

if r.URL.Path == "/backend" {
srv.backendHandler(w, r)
return
}

path, err := filepath.Abs(r.URL.Path)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}

path = filepath.Join(frontEndConfig.StaticDirectoryPath, path)

_, err = os.Stat(path)
if os.IsNotExist(err) {
// file does not exist, serve index.html
http.ServeFile(w, r, filepath.Join(frontEndConfig.StaticDirectoryPath, frontEndConfig.StaticDirectoryRootDocument))
return
} else if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

fileServer.ServeHTTP(w, r)
}))
if err != nil {
chanErr <- err
}
Expand Down