Initial commit

This commit is contained in:
Alexey Khit
2022-08-18 09:19:00 +03:00
commit 3e77835583
65 changed files with 6372 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
.idea/
.tmp/
go2rtc.yaml

217
README.md Normal file
View File

@@ -0,0 +1,217 @@
# go2rtc
**go2rtc** - ultimate camera streaming application with support RTSP, WebRTC, FFmpeg, RTMP, etc.
- zero-dependency and zero-config small app for all OS (Windows, macOS, Linux, ARM, etc.)
- zero-delay for all supported protocols (lowest possible streaming latency)
- zero-load on CPU for supported codecs
- on the fly transcoding for unsupported codecs via FFmpeg
- multi-source two-way [codecs negotiation](#codecs-negotiation)
- streaming from private networks via Ngrok or SSH-tunnels
## Codecs negotiation
For example, you want to watch stream from [Dahua IPC-K42](https://www.dahuasecurity.com/fr/products/All-Products/Network-Cameras/Wireless-Series/Wi-Fi-Series/4MP/IPC-K42) camera in your browser.
- this camera support codecs **H264, H265** for send video, and you select `H264` in camera settings
- this camera support codecs **AAC, PCMU, PCMA** for send audio (from mic), and you select `AAC/16000` in camera settings
- this camera support codecs **AAC, PCMU, PCMA** for receive audio (to speaker), you don't need to select them
- your browser support codecs **H264, VP8, VP9, AV1** for receive video, you don't need to select them
- your browser support codecs **OPUS, PCMU, PCMA** for send and receive audio, you don't need to select them
- you can't get camera audio directly, because their audio codecs doesn't match with your browser codecs
- so you decide to use transcoding via FFmpeg and add this setting to config YAML file
- you have chosen `OPUS/48000/2` codec, because it is higher quality than the PCMU/8000 or PCMA/8000
- now you have stream with two sources - **RTSP and FFmpeg**
`go2rtc` automatically match codecs for you browser and all your stream sources. This called **multi-source two-way codecs negotiation**. And this is one of the main features of this app.
**PS.** You can select PCMU or PCMA codec in camera setting and don't use transcoding at all. Or you can select AAC codec for main stream and PCMU codec for second stream and add both RTSP to YAML config, this also will work fine.
```yaml
streams:
dahua:
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif
- ffmpeg:rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif#audio=opus
```
![](codecs.svg)
## Configuration
Create file `go2rtc.yaml` next to the app. Modules:
- [Streams](#streams)
### Streams
**go2rtc** support different stream source types. You can setup only one link as stream source or multiple.
- [RTSP/RTSPS](#rtsp-source) - most cameras on market
- [RTMP](#rtmp-source)
- [FFmpeg/Exec](#ffmpeg-source) - FFmpeg integration
- [Hass](#hass-source) - Home Assistant integration
#### RTSP source
- Support **RTSP and RTSPS** links with multiple video and audio tracks
- Support **2 way audio** ONLY for [ONVIF Profile T](https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec.pdf) cameras (back channel connection)
**Attention:** proprietary 2 way audio standards are not supported!
```yaml
streams:
rtsp_camera: rtsp://rtsp:12345678@192.168.1.123:554/av_stream/ch0
```
If your camera support two RTSP links - you can add both of them as sources. This is useful when streams has different codecs, as example AAC audio with main stream and PCMU/PCMA audio with second stream:
**Attention:** Dahua cameras has different capabilities for different RTSP links. For example, it has support multiple codecs for two way audio with `&proto=Onvif` in link and only one coded without it.
```yaml
streams:
onvif_camera:
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif
- rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=1
```
#### RTMP source
You can get stream from RTMP server, for example [Frigate](https://docs.frigate.video/configuration/rtmp). Support ONLY `H264` video codec without audio.
```yaml
streams:
rtmp_stream: rtmp://192.168.1.123/live/camera1
```
#### FFmpeg source
You can get any stream or file or device via FFmpeg and push it to go2rtc via RTSP protocol.
Format: `ffmpeg:{input}#{params}`. Examples:
```yaml
streams:
# [FILE] all tracks will be copied without transcoding codecs
file1: ffmpeg:~/media/BigBuckBunny.mp4
# [FILE] video will be transcoded to H264, audio will be skipped
file2: ffmpeg:~/media/BigBuckBunny.mp4#video=h264
# [FILE] video will be copied, audio will be transcoded to pcmu
file3: ffmpeg:~/media/BigBuckBunny.mp4#video=copy&audio=pcmu
# [HLS] video will be copied, audio will be skipped
hls: ffmpeg:https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/gear5/prog_index.m3u8#video=copy
# [MJPEG] video will be transcoded to H264
mjpeg: ffmpeg:http://185.97.122.128/cgi-bin/faststream.jpg?stream=half&fps=15#video=h264
# [RTSP] video and audio will be copied
rtsp: ffmpeg:rtsp://rtsp:12345678@192.168.1.123:554/av_stream/ch0#video=copy&audio=copy
```
All trascoding formats has built-in templates. But you can override them via YAML config:
```yaml
ffmpeg:
bin: ffmpeg # path to ffmpeg binary
link: -hide_banner -i {input} # if input is link
file: -hide_banner -re -stream_loop -1 -i {input} # if input not link
rtsp: -hide_banner -fflags nobuffer -flags low_delay -rtsp_transport tcp -i {input} # if input is RTSP link
output: -rtsp_transport tcp -f rtsp {output} # output
h264: "-codec:v libx264 -g 30 -preset superfast -tune zerolatency -profile main -level 4.1"
h264/ultra: "-codec:v libx264 -g 30 -preset ultrafast -tune zerolatency"
h264/high: "-codec:v libx264 -g 30 -preset superfast -tune zerolatency"
h265: "-codec:v libx265 -g 30 -preset ultrafast -tune zerolatency"
opus: "-codec:a libopus -ar 48000 -ac 2"
pcmu: "-codec:a pcm_mulaw -ar 8000 -ac 1"
pcmu/16000: "-codec:a pcm_mulaw -ar 16000 -ac 1"
pcmu/48000: "-codec:a pcm_mulaw -ar 48000 -ac 1"
pcma: "-codec:a pcm_alaw -ar 8000 -ac 1"
pcma/16000: "-codec:a pcm_alaw -ar 16000 -ac 1"
pcma/48000: "-codec:a pcm_alaw -ar 48000 -ac 1"
aac/16000: "-codec:a aac -ar 16000 -ac 1"
```
#### Exec source
FFmpeg source just a shortcut to exec source. You can get any stream or file or device via FFmpeg or GStreamer and push it to go2rtc via RTSP protocol:
```yaml
streams:
stream1: exec:ffmpeg -hide_banner -re -stream_loop -1 -i ~/media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {output}
```
#### Hass source
Support import camera links from [Home Assistant](https://www.home-assistant.io/) config files.
- support ONLY [Generic Camera](https://www.home-assistant.io/integrations/generic/), setup via GUI
```yaml
hass:
config: "~/.homeassistant"
streams:
generic_camera: hass:Camera1 # Settings > Integrations > Integration Name
```
### API server
```yaml
api:
listen: ":3000" # HTTP API port
base_path: "" # API prefix for serve on suburl
static_dir: "www" # folder for static files
```
### RTSP server
```yaml
rtsp:
listen: ":554"
```
### WebRTC server
```yaml
webrtc:
listen: ":8555" # address of your local server (TCP)
candidates:
- 216.58.210.174:8555 # if you have static public IP-address
- 192.168.1.123:8555 # ip you have problems with UDP in LAN
- stun # if you have dynamic public IP-address (auto discovery via STUN)
ice_servers:
- urls: [stun:stun.l.google.com:19302]
- urls: [turn:123.123.123.123:3478]
username: your_user
credential: your_pass
```
### Ngrok
```yaml
ngrok:
command: ngrok tcp 8555 --authtoken eW91IHNoYWxsIG5vdCBwYXNzCnlvdSBzaGFsbCBub3QgcGFzcw
```
or
```yaml
ngrok:
command: ngrok start --all --config ngrok.yml
```
### Log
```yaml
log:
level: info # default level
api: trace
exec: debug
ngrok: info
rtsp: warn
streams: error
webrtc: fatal
```

4
cmd/README.md Normal file
View File

@@ -0,0 +1,4 @@
**Project layout**
- https://github.com/golang-standards/project-layout
- https://github.com/micro/micro

119
cmd/api/api.go Normal file
View File

@@ -0,0 +1,119 @@
package api
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/gorilla/websocket"
"github.com/rs/zerolog"
"net"
"net/http"
)
func Init() {
var cfg struct {
Mod struct {
Listen string `yaml:"listen"`
BasePath string `yaml:"base_path"`
StaticDir string `yaml:"static_dir"`
} `yaml:"api"`
}
// default config
cfg.Mod.Listen = ":3000"
cfg.Mod.StaticDir = "www"
// load config from YAML
app.LoadConfig(&cfg)
if cfg.Mod.Listen == "" {
return
}
basePath = cfg.Mod.BasePath
log = app.GetLogger("api")
if cfg.Mod.StaticDir != "" {
fileServer = http.FileServer(http.Dir(cfg.Mod.StaticDir))
HandleFunc("/", fileServerHandlder)
}
HandleFunc("/api/stack", stackHandler)
HandleFunc("/api/stats", statsHandler)
HandleFunc("/api/ws", apiWS)
// ensure we can listen without errors
listener, err := net.Listen("tcp", cfg.Mod.Listen)
if err != nil {
log.Fatal().Err(err).Msg("[api] listen")
}
log.Info().Str("addr", cfg.Mod.Listen).Msg("[api] listen")
go func() {
s := http.Server{}
if err = s.Serve(listener); err != nil {
log.Fatal().Err(err).Msg("[api] Serve")
}
}()
}
func HandleFunc(pattern string, handler http.HandlerFunc) {
http.HandleFunc(basePath+pattern, handler)
}
func HandleWS(msgType string, handler WSHandler) {
wsHandlers[msgType] = handler
}
var basePath string
var fileServer http.Handler
var log zerolog.Logger
var wsHandlers = make(map[string]WSHandler)
func fileServerHandlder(w http.ResponseWriter, r *http.Request) {
if basePath != "" {
r.URL.Path = r.URL.Path[len(basePath):]
}
fileServer.ServeHTTP(w, r)
}
func statsHandler(w http.ResponseWriter, _ *http.Request) {
v := map[string]interface{}{
"streams": streams.Streams,
}
data, err := json.Marshal(v)
if err != nil {
log.Error().Err(err).Msg("[api.stats] marshal")
}
if _, err = w.Write(data); err != nil {
log.Error().Err(err).Msg("[api.stats] write")
}
}
func apiWS(w http.ResponseWriter, r *http.Request) {
ctx := new(Context)
if err := ctx.Upgrade(w, r); err != nil {
log.Error().Err(err).Msg("[api.ws] upgrade")
return
}
defer ctx.Close()
for {
msg := new(streamer.Message)
if err := ctx.Conn.ReadJSON(msg); err != nil {
if websocket.IsUnexpectedCloseError(
err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure,
) {
log.Error().Err(err).Msg("[api.ws] readJSON")
}
return
}
handler := wsHandlers[msg.Type]
if handler != nil {
handler(ctx, msg)
}
}
}

52
cmd/api/stack.go Normal file
View File

@@ -0,0 +1,52 @@
package api
import (
"bytes"
"fmt"
"net/http"
"runtime"
)
var stackSkip = [][]byte{
// debug.go
[]byte("github.com/AlexxIT/go2rtc/cmd/debug.handler"),
// cmd.go
[]byte("github.com/AlexxIT/go2rtc/cmd.Run"),
[]byte("created by os/signal.Notify"),
// api.go
[]byte("created by github.com/AlexxIT/go2rtc/cmd/api.Init"),
[]byte("created by net/http.(*connReader).startBackgroundRead"),
[]byte("created by net/http.(*Server).Serve"),
[]byte("created by github.com/AlexxIT/go2rtc/cmd/rtsp.Init"),
}
func stackHandler(w http.ResponseWriter, r *http.Request) {
sep := []byte("\n\n")
buf := make([]byte, 65535)
i := 0
n := runtime.Stack(buf, true)
skipped := 0
for _, item := range bytes.Split(buf[:n], sep) {
for _, skip := range stackSkip {
if bytes.Contains(item, skip) {
item = nil
skipped++
break
}
}
if item != nil {
i += copy(buf[i:], item)
i += copy(buf[i:], sep)
}
}
i += copy(buf[i:], fmt.Sprintf(
"Total: %d, Skipped: %d", runtime.NumGoroutine(), skipped),
)
if _, err := w.Write(buf[:i]); err != nil {
panic(err)
}
}

67
cmd/api/ws.go Normal file
View File

@@ -0,0 +1,67 @@
package api
import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/gorilla/websocket"
"net/http"
"sync"
)
type WSHandler func(ctx *Context, msg *streamer.Message)
var apiWsUp = websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 512000,
}
type Context struct {
Conn *websocket.Conn
Request *http.Request
Consumer interface{} // TODO: rewrite
onClose []func()
mu sync.Mutex
}
func (ctx *Context) Upgrade(w http.ResponseWriter, r *http.Request) (err error) {
ctx.Conn, err = apiWsUp.Upgrade(w, r, nil)
ctx.Request = r
return
}
func (ctx *Context) Close() {
for _, f := range ctx.onClose {
f()
}
_ = ctx.Conn.Close()
}
func (ctx *Context) Write(msg interface{}) {
ctx.mu.Lock()
defer ctx.mu.Unlock()
var err error
switch msg := msg.(type) {
case *streamer.Message:
err = ctx.Conn.WriteJSON(msg)
case []byte:
err = ctx.Conn.WriteMessage(websocket.BinaryMessage, msg)
default:
return
}
if err != nil {
//panic(err) // TODO: fix panic
}
}
func (ctx *Context) Error(err error) {
ctx.Write(&streamer.Message{
Type: "error", Value: err.Error(),
})
}
func (ctx *Context) OnClose(f func()) {
ctx.onClose = append(ctx.onClose, f)
}

68
cmd/app/app.go Normal file
View File

@@ -0,0 +1,68 @@
package app
import (
"github.com/rs/zerolog"
"gopkg.in/yaml.v3"
"io"
"os"
"runtime"
)
func Init() {
data, _ = os.ReadFile("go2rtc.yaml")
var cfg struct {
Mod map[string]string `yaml:"log"`
}
LoadConfig(&cfg)
var writer io.Writer = os.Stdout
// styles
format := cfg.Mod["format"]
if format != "json" {
writer = zerolog.ConsoleWriter{
Out: writer, TimeFormat: "15:04:05.000",
NoColor: writer != os.Stdout || format == "text",
}
}
zerolog.TimeFieldFormat = zerolog.TimeFormatUnixMs
lvl, err := zerolog.ParseLevel(cfg.Mod["level"])
if err != nil || lvl == zerolog.NoLevel {
lvl = zerolog.InfoLevel
}
log = zerolog.New(writer).With().Timestamp().Logger().Level(lvl)
modules = cfg.Mod
log.Info().Msgf("go2rtc %s/%s", runtime.GOOS, runtime.GOARCH)
}
func LoadConfig(v interface{}) {
if data != nil {
_ = yaml.Unmarshal(data, v)
}
}
func GetLogger(module string) zerolog.Logger {
lvl, err := zerolog.ParseLevel(modules[module])
if err != nil {
return log
}
return log.Level(lvl)
}
// internal
// data - config content
var data []byte
// log - main logger
var log zerolog.Logger
// modules log levels
var modules map[string]string

41
cmd/cmd.go Normal file
View File

@@ -0,0 +1,41 @@
package cmd
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/exec"
"github.com/AlexxIT/go2rtc/cmd/ffmpeg"
"github.com/AlexxIT/go2rtc/cmd/hass"
"github.com/AlexxIT/go2rtc/cmd/mse"
"github.com/AlexxIT/go2rtc/cmd/ngrok"
"github.com/AlexxIT/go2rtc/cmd/rtmp"
"github.com/AlexxIT/go2rtc/cmd/rtsp"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/cmd/webrtc"
"os"
"os/signal"
)
func Run() {
app.Init() // init config and logs
streams.Init() // load streams list
rtsp.Init() // add support RTSP client and RTSP server
rtmp.Init() // add support RTMP client
exec.Init() // add support exec scheme (depends on RTSP server)
ffmpeg.Init() // add support ffmpeg scheme (depends on exec scheme)
hass.Init() // add support hass scheme
api.Init() // init HTTP API server
webrtc.Init()
mse.Init()
ngrok.Init()
c := make(chan os.Signal)
signal.Notify(c)
<-c
println("exit OK")
}

85
cmd/exec/exec.go Normal file
View File

@@ -0,0 +1,85 @@
package exec
import (
"crypto/md5"
"encoding/hex"
"errors"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/rtsp"
"github.com/AlexxIT/go2rtc/cmd/streams"
pkg "github.com/AlexxIT/go2rtc/pkg/rtsp"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/rs/zerolog"
"os"
"os/exec"
"strings"
"time"
)
func Init() {
// depends on RTSP server
if rtsp.Port == "" {
return
}
rtsp.OnProducer = func(prod streamer.Producer) bool {
if conn := prod.(*pkg.Conn); conn != nil {
if waiter := waiters[conn.URL.Path]; waiter != nil {
waiter <- prod
return true
}
}
return false
}
streams.HandleFunc("exec", Handle)
log = app.GetLogger("exec")
// TODO: add sync.Mutex
waiters = map[string]chan streamer.Producer{}
}
func Handle(url string) (streamer.Producer, error) {
sum := md5.Sum([]byte(url))
path := "/" + hex.EncodeToString(sum[:])
url = strings.Replace(
url, "{output}", "rtsp://localhost:"+rtsp.Port+path, 1,
)
// remove `exec:`
args := strings.Split(url[5:], " ")
cmd := exec.Command(args[0], args[1:]...)
if log.Trace().Enabled() {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
}
ch := make(chan streamer.Producer)
waiters[path] = ch
defer delete(waiters, path)
log.Debug().Str("url", url).Msg("[exec] run")
if err := cmd.Start(); err != nil {
log.Error().Err(err).Str("url", url).Msg("[exec]")
return nil, err
}
select {
case <-time.After(time.Second * 10):
_ = cmd.Process.Kill()
log.Error().Str("url", url).Msg("[exec] timeout")
return nil, errors.New("timeout")
case prod := <-ch:
return prod, nil
}
}
// internal
var log zerolog.Logger
var waiters map[string]chan streamer.Producer

6
cmd/ffmpeg/README.md Normal file
View File

@@ -0,0 +1,6 @@
## Useful links
- https://superuser.com/questions/564402/explanation-of-x264-tune
- https://stackoverflow.com/questions/33624016/why-sliced-thread-affect-so-much-on-realtime-encoding-using-ffmpeg-x264
- https://codec.fandom.com/ru/wiki/X264_-_%D0%BE%D0%BF%D0%B8%D1%81%D0%B0%D0%BD%D0%B8%D0%B5_%D0%BA%D0%BB%D1%8E%D1%87%D0%B5%D0%B9_%D0%BA%D0%BE%D0%B4%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%B8%D1%8F
- https://html5test.com/

112
cmd/ffmpeg/ffmpeg.go Normal file
View File

@@ -0,0 +1,112 @@
package ffmpeg
import (
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/exec"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"net/url"
"strings"
)
func Init() {
var cfg struct {
Mod map[string]string `yaml:"ffmpeg"`
}
// defaults
cfg.Mod = map[string]string{
"bin": "ffmpeg",
// inputs
"link": "-hide_banner -i {input}",
"rtsp": "-hide_banner -fflags nobuffer -flags low_delay -rtsp_transport tcp -i {input}",
"file": "-hide_banner -re -stream_loop -1 -i {input}",
// output
"out": "-rtsp_transport tcp -f rtsp {output}",
// `-g 30` - group of picture, GOP, keyframe interval
// `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`
// `-tune zerolatency` - for minimal latency
// `-profile main -level 4.1` - most used streaming profile
"h264": "-codec:v libx264 -g 30 -preset superfast -tune zerolatency -profile main -level 4.1",
"h264/ultra": "-codec:v libx264 -g 30 -preset ultrafast -tune zerolatency",
"h264/high": "-codec:v libx264 -g 30 -preset superfast -tune zerolatency",
"h265": "-codec:v libx265 -g 30 -preset ultrafast -tune zerolatency",
"opus": "-codec:a libopus -ar 48000 -ac 2",
"pcmu": "-codec:a pcm_mulaw -ar 8000 -ac 1",
"pcmu/16000": "-codec:a pcm_mulaw -ar 16000 -ac 1",
"pcmu/48000": "-codec:a pcm_mulaw -ar 48000 -ac 1",
"pcma": "-codec:a pcm_alaw -ar 8000 -ac 1",
"pcma/16000": "-codec:a pcm_alaw -ar 16000 -ac 1",
"pcma/48000": "-codec:a pcm_alaw -ar 48000 -ac 1",
"aac/16000": "-codec:a aac -ar 16000 -ac 1",
}
app.LoadConfig(&cfg)
tpl := cfg.Mod
streams.HandleFunc("ffmpeg", func(s string) (streamer.Producer, error) {
s = s[7:] // remove `ffmpeg:`
var query url.Values
if i := strings.IndexByte(s, '#'); i > 0 {
query, _ = url.ParseQuery(s[i+1:])
s = s[:i]
}
var template string
switch {
case strings.HasPrefix(s, "rtsp"):
template = tpl["rtsp"]
case strings.Contains(s, "://"):
template = tpl["link"]
default:
template = tpl["file"]
}
s = "exec:" + tpl["bin"] + " " +
strings.Replace(template, "{input}", s, 1)
if query != nil {
for _, raw := range query["raw"] {
s += " " + raw
}
// TODO: multiple codecs via -map
// s += fmt.Sprintf(" -map 0:v:0 -c:v:%d copy", i)
for _, video := range query["video"] {
if video == "copy" {
s += " -codec:v copy"
} else {
s += " " + tpl[video]
}
}
for _, audio := range query["audio"] {
if audio == "copy" {
s += " -codec:v copy"
} else {
s += " " + tpl[audio]
}
}
if query["video"] == nil {
s += " -vn"
}
if query["audio"] == nil {
s += " -an"
}
} else {
s += " -c copy"
}
s += " " + tpl["out"]
return exec.Handle(s)
})
}

71
cmd/hass/hass.go Normal file
View File

@@ -0,0 +1,71 @@
package hass
import (
"encoding/json"
"fmt"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"os"
"path"
)
func Init() {
var conf struct {
Mod struct {
Config string `yaml:"config"`
} `yaml:"hass"`
}
app.LoadConfig(&conf)
filename := path.Join(conf.Mod.Config, ".storage/core.config_entries")
data, err := os.ReadFile(filename)
if err != nil {
return
}
ent := new(entries)
if err = json.Unmarshal(data, ent); err != nil {
return
}
urls := map[string]string{}
for _, entrie := range ent.Data.Entries {
switch entrie.Domain {
case "generic":
if entrie.Options.StreamSource != "" {
urls[entrie.Title] = entrie.Options.StreamSource
}
}
}
streams.HandleFunc("hass", func(url string) (streamer.Producer, error) {
if hurl := urls[url[5:]]; hurl != "" {
return streams.GetProducer(hurl)
}
return nil, fmt.Errorf("can't get url: %s", url)
})
}
type entries struct {
Data struct {
Entries []struct {
Title string `json:"title"`
Domain string `json:"domain"`
Data struct {
ClientID string `json:"iOSPairingId"`
ClientPrivate string `json:"iOSDeviceLTSK"`
ClientPublic string `json:"iOSDeviceLTPK"`
DeviceID string `json:"AccessoryPairingID"`
DevicePublic string `json:"AccessoryLTPK"`
DeviceHost string `json:"AccessoryIP"`
DevicePort uint16 `json:"AccessoryPort"`
} `json:"data"`
Options struct {
StreamSource string `json:"stream_source"`
}
} `json:"entries"`
} `json:"data"`
}

42
cmd/mse/mse.go Normal file
View File

@@ -0,0 +1,42 @@
package mse
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/mse"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/rs/zerolog/log"
)
func Init() {
api.HandleWS("mse", handler)
}
func handler(ctx *api.Context, msg *streamer.Message) {
url := ctx.Request.URL.Query().Get("url")
stream := streams.Get(url)
if stream == nil {
return
}
cons := new(mse.Consumer)
cons.UserAgent = ctx.Request.UserAgent()
cons.RemoteAddr = ctx.Request.RemoteAddr
cons.Listen(func(msg interface{}) {
switch msg.(type) {
case *streamer.Message, []byte:
ctx.Write(msg)
}
})
if err := stream.AddConsumer(cons); err != nil {
log.Warn().Err(err).Msg("[api.mse] Add consumer")
ctx.Error(err)
return
}
ctx.OnClose(func() {
stream.RemoveConsumer(cons)
})
cons.Init()
}

83
cmd/ngrok/ngrok.go Normal file
View File

@@ -0,0 +1,83 @@
package ngrok
import (
"fmt"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/webrtc"
"github.com/AlexxIT/go2rtc/pkg/ngrok"
"github.com/rs/zerolog"
"net"
"strings"
)
func Init() {
var cfg struct {
Log struct {
Level string `yaml:"ngrok"`
} `yaml:"log"`
Mod struct {
Cmd string `yaml:"command"`
} `yaml:"ngrok"`
}
app.LoadConfig(&cfg)
if cfg.Mod.Cmd == "" {
return
}
log = app.GetLogger(cfg.Log.Level)
ngr, err := ngrok.NewNgrok(cfg.Mod.Cmd)
if err != nil {
log.Error().Err(err).Msg("[ngrok] start")
}
ngr.Listen(func(msg interface{}) {
if msg := msg.(*ngrok.Message); msg != nil {
if strings.HasPrefix(msg.Line, "ERROR:") {
log.Warn().Msg("[ngrok] " + msg.Line)
} else {
log.Debug().Msg("[ngrok] " + msg.Line)
}
// Addr: "//localhost:8555", URL: "tcp://1.tcp.eu.ngrok.io:12345"
if msg.Addr == "//localhost:"+webrtc.Port && strings.HasPrefix(msg.URL, "tcp://") {
// don't know if really necessary use IP
address, err := ConvertHostToIP(msg.URL[6:])
if err != nil {
log.Warn().Err(err).Msg("[ngrok] add candidate")
return
}
webrtc.AddCandidate(address)
}
}
})
go func() {
if err = ngr.Serve(); err != nil {
log.Error().Err(err).Msg("[ngrok] run")
}
}()
}
var log zerolog.Logger
func ConvertHostToIP(address string) (string, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return "", err
}
ip, err := net.LookupIP(host)
if err != nil {
return "", err
}
if len(ip) == 0 {
return "", fmt.Errorf("can't resolve: %s", host)
}
return ip[0].String() + ":" + port, nil
}

19
cmd/rtmp/rtmp.go Normal file
View File

@@ -0,0 +1,19 @@
package rtmp
import (
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/rtmp"
"github.com/AlexxIT/go2rtc/pkg/streamer"
)
func Init() {
streams.HandleFunc("rtmp", handle)
}
func handle(url string) (streamer.Producer, error) {
conn := rtmp.NewClient(url)
if err := conn.Dial(); err != nil {
return nil, err
}
return conn, nil
}

201
cmd/rtsp/rtsp.go Normal file
View File

@@ -0,0 +1,201 @@
package rtsp
import (
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/rtsp"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/rs/zerolog"
"net"
)
func Init() {
var conf struct {
Mod struct {
Listen string `yaml:"listen"`
} `yaml:"rtsp"`
}
// default config
conf.Mod.Listen = ":554"
app.LoadConfig(&conf)
log = app.GetLogger("rtsp")
// RTSP client support
streams.HandleFunc("rtsp", rtspHandler)
streams.HandleFunc("rtsps", rtspHandler)
// RTSP server support
address := conf.Mod.Listen
if address != "" {
_, Port, _ = net.SplitHostPort(address)
go worker(address)
}
}
var Port string
var OnProducer func(conn streamer.Producer) bool // TODO: maybe rewrite...
// internal
var log zerolog.Logger
func rtspHandler(url string) (streamer.Producer, error) {
conn, err := rtsp.NewClient(url)
if err != nil {
return nil, err
}
if log.Trace().Enabled() {
conn.Listen(func(msg interface{}) {
switch msg := msg.(type) {
case *tcp.Request:
log.Trace().Msgf("[rtsp] client request:\n%s", msg)
case *tcp.Response:
log.Trace().Msgf("[rtsp] client response:\n%s", msg)
}
})
}
if err = conn.Dial(); err != nil {
return nil, err
}
if err = conn.Describe(); err != nil {
return nil, err
}
return conn, nil
}
func worker(address string) {
srv, err := tcp.NewServer(address)
if err != nil {
log.Error().Err(err).Msg("[rtsp] listen")
return
}
log.Info().Str("addr", address).Msg("[rtsp] listen")
srv.Listen(func(msg interface{}) {
switch msg.(type) {
case net.Conn:
var name string
var onDisconnect func()
trace := log.Trace().Enabled()
conn := rtsp.NewServer(msg.(net.Conn))
conn.Listen(func(msg interface{}) {
if trace {
switch msg := msg.(type) {
case *tcp.Request:
log.Trace().Msgf("[rtsp] server request:\n%s", msg)
case *tcp.Response:
log.Trace().Msgf("[rtsp] server response:\n%s", msg)
}
}
switch msg {
case rtsp.MethodDescribe:
name = conn.URL.Path[1:]
log.Debug().Str("stream", name).Msg("[rtsp] new consumer")
stream := streams.Get(name) // TODO: rewrite
if stream == nil {
return
}
initMedias(conn)
if err = stream.AddConsumer(conn); err != nil {
log.Warn().Err(err).Str("stream", name).Msg("[rtsp]")
return
}
onDisconnect = func() {
stream.RemoveConsumer(conn)
}
case rtsp.MethodAnnounce:
if OnProducer != nil {
if OnProducer(conn) {
return
}
}
name = conn.URL.Path[1:]
log.Debug().Str("stream", name).Msg("[rtsp] new producer")
str := streams.Get(conn.URL.Path[1:])
if str == nil {
return
}
str.AddProducer(conn)
onDisconnect = func() {
str.RemoveProducer(conn)
}
case streamer.StatePlaying:
log.Debug().Str("stream", name).Msg("[rtsp] start")
}
})
if err = conn.Accept(); err != nil {
log.Warn().Err(err).Msg("[rtsp] accept")
return
}
if err = conn.Handle(); err != nil {
//log.Warn().Err(err).Msg("[rtsp] handle server")
}
if onDisconnect != nil {
onDisconnect()
}
log.Debug().Str("stream", name).Msg("[rtsp] disconnect")
}
})
srv.Serve()
}
func initMedias(conn *rtsp.Conn) {
// set media candidates from query list
for key, value := range conn.URL.Query() {
switch key {
case streamer.KindVideo, streamer.KindAudio:
for _, value := range value {
media := &streamer.Media{
Kind: key, Direction: streamer.DirectionRecvonly,
}
switch value {
case "", "copy": // pass empty codecs list
default:
codec := streamer.NewCodec(value)
media.Codecs = append(media.Codecs, codec)
}
conn.Medias = append(conn.Medias, media)
}
}
}
// set default media candidates if query is empty
if conn.Medias == nil {
conn.Medias = []*streamer.Media{
{Kind: streamer.KindVideo, Direction: streamer.DirectionRecvonly},
{Kind: streamer.KindAudio, Direction: streamer.DirectionRecvonly},
}
}
}

27
cmd/streams/handlers.go Normal file
View File

@@ -0,0 +1,27 @@
package streams
import (
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"strings"
)
type Handler func(url string) (streamer.Producer, error)
var handlers map[string]Handler
func HandleFunc(scheme string, handler Handler) {
if handlers == nil {
handlers = make(map[string]Handler)
}
handlers[scheme] = handler
}
func GetProducer(url string) (streamer.Producer, error) {
i := strings.IndexByte(url, ':')
handler := handlers[url[:i]]
if handler == nil {
return nil, fmt.Errorf("unsupported scheme: %s", url)
}
return handler(url)
}

89
cmd/streams/producer.go Normal file
View File

@@ -0,0 +1,89 @@
package streams
import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"strings"
)
type state byte
const (
stateNone state = iota
stateMedias
stateTracks
stateStart
)
type Producer struct {
streamer.Element
url string
element streamer.Producer
tracks []*streamer.Track
state state
}
func (p *Producer) GetMedias() []*streamer.Media {
if p.state == stateNone {
i := strings.IndexByte(p.url, ':')
handler := handlers[p.url[:i]]
if handler == nil {
log.Warn().Str("url", p.url).Msg("[streams] unsupported scheme")
return nil
}
log.Debug().Str("url", p.url).Msg("[streams] probe producer")
var err error
p.element, err = handler(p.url)
if err != nil {
log.Error().Err(err).Str("url", p.url).Msg("[streams] probe producer")
return nil
}
p.state = stateMedias
}
return p.element.GetMedias()
}
func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
if p.state == stateMedias {
p.state = stateTracks
}
track := p.element.GetTrack(media, codec)
for _, t := range p.tracks {
if track == t {
return track
}
}
p.tracks = append(p.tracks, track)
return track
}
// internals
func (p *Producer) start() {
if p.state != stateTracks {
return
}
log.Debug().Str("url", p.url).Msg("[streams] start producer")
p.state = stateStart
go p.element.Start()
}
func (p *Producer) stop() {
log.Debug().Str("url", p.url).Msg("[streams] stop producer")
_ = p.element.Stop()
p.element = nil
p.tracks = nil
p.state = stateNone
}

164
cmd/streams/stream.go Normal file
View File

@@ -0,0 +1,164 @@
package streams
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/streamer"
)
type Consumer struct {
element streamer.Consumer
tracks []*streamer.Track
}
type Stream struct {
producers []*Producer
consumers []*Consumer
}
func newStream(source interface{}) *Stream {
s := new(Stream)
switch source := source.(type) {
case string:
prod := &Producer{url: source}
s.producers = append(s.producers, prod)
case []interface{}:
for _, source := range source {
prod := &Producer{url: source.(string)}
s.producers = append(s.producers, prod)
}
case map[string]interface{}:
return newStream(source["url"])
default:
panic("wrong source type")
}
return s
}
func (s *Stream) AddConsumer(cons streamer.Consumer) (err error) {
ic := len(s.consumers)
consumer := &Consumer{element: cons}
// Step 1. Get consumer medias
for icc, consMedia := range cons.GetMedias() {
log.Trace().Stringer("media", consMedia).
Msgf("[streams] consumer:%d:%d candidate", ic, icc)
producers:
for ip, prod := range s.producers {
// Step 2. Get producer medias (not tracks yet)
for ipc, prodMedia := range prod.GetMedias() {
log.Trace().Stringer("media", prodMedia).
Msgf("[streams] producer:%d:%d candidate", ip, ipc)
// Step 3. Match consumer/producer codecs list
prodCodec := prodMedia.MatchMedia(consMedia)
if prodCodec != nil {
log.Trace().Stringer("codec", prodCodec).
Msgf("[streams] match producer:%d:%d => consumer:%d:%d", ip, ipc, ic, icc)
// Step 4. Get producer track
prodTrack := prod.GetTrack(prodMedia, prodCodec)
// Step 5. Add track to consumer and get new track
consTrack := consumer.element.AddTrack(consMedia, prodTrack)
consumer.tracks = append(consumer.tracks, consTrack)
break producers
}
}
}
}
// can't match tracks for consumer
if len(consumer.tracks) == 0 {
return nil
}
s.consumers = append(s.consumers, consumer)
for _, prod := range s.producers {
prod.start()
}
return nil
}
func (s *Stream) RemoveConsumer(cons streamer.Consumer) {
for i, consumer := range s.consumers {
if consumer.element == cons {
// remove consumer pads from all producers
for _, track := range consumer.tracks {
track.Unbind()
}
// remove consumer from slice
s.removeConsumer(i)
break
}
}
for _, producer := range s.producers {
var sink bool
for _, track := range producer.tracks {
if len(track.Sink) > 0 {
sink = true
}
}
if !sink {
producer.stop()
}
}
}
func (s *Stream) AddProducer(prod streamer.Producer) {
panic("not implemented")
}
func (s *Stream) RemoveProducer(prod streamer.Producer) {
panic("not implemented")
}
func (s *Stream) MarshalJSON() ([]byte, error) {
var v []interface{}
for _, prod := range s.producers {
if prod.element != nil {
v = append(v, prod.element)
}
}
for _, cons := range s.consumers {
// cons.element always not nil
v = append(v, cons.element)
}
if len(v) == 0 {
v = nil
}
return json.Marshal(v)
}
func (s *Stream) removeConsumer(i int) {
switch {
case len(s.consumers) == 1: // only one element
s.consumers = nil
case i == 0: // first element
s.consumers = s.consumers[1:]
case i == len(s.consumers)-1: // last element
s.consumers = s.consumers[:i]
default: // middle element
s.consumers = append(s.consumers[:i], s.consumers[i+1:]...)
}
}
func (s *Stream) removeProducer(i int) {
switch {
case len(s.producers) == 1: // only one element
s.producers = nil
case i == 0: // first element
s.producers = s.producers[1:]
case i == len(s.producers)-1: // last element
s.producers = s.producers[:i]
default: // middle element
s.producers = append(s.producers[:i], s.producers[i+1:]...)
}
}

134
cmd/streams/stream_test.go Normal file
View File

@@ -0,0 +1,134 @@
package streams
import (
"github.com/AlexxIT/go2rtc/pkg/fake"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/stretchr/testify/assert"
"testing"
"time"
)
// Google Chrome 104.0.5112.79
const chrome = `v=0
o=- 0 0 IN IP4 0.0.0.0
s=-
t=0 0
m=audio 9 UDP/TLS/RTP/SAVPF 111 63 103 104 9 0 8 110 112 113 126
a=sendrecv
a=rtpmap:111 opus/48000/2
a=rtpmap:63 red/48000/2
a=rtpmap:103 ISAC/16000
a=rtpmap:104 ISAC/32000
a=rtpmap:9 G722/8000
a=rtpmap:0 PCMU/8000
a=rtpmap:8 PCMA/8000
a=rtpmap:110 telephone-event/48000
a=rtpmap:112 telephone-event/32000
a=rtpmap:113 telephone-event/16000
a=rtpmap:126 telephone-event/8000
m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 122 127 121 125 107 108 109 124 120 123 119 35 36 37 38 39 40 41 42 114 115 116 117 118 43
a=recvonly
a=rtpmap:96 VP8/90000
a=rtpmap:97 rtx/90000
a=rtpmap:98 VP9/90000
a=rtpmap:99 rtx/90000
a=rtpmap:100 VP9/90000
a=rtpmap:101 rtx/90000
a=rtpmap:102 VP9/90000
a=rtpmap:122 rtx/90000
a=rtpmap:127 H264/90000
a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f
a=rtpmap:121 rtx/90000
a=rtpmap:125 H264/90000
a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f
a=rtpmap:107 rtx/90000
a=rtpmap:108 H264/90000
a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f
a=rtpmap:109 rtx/90000
a=rtpmap:124 H264/90000
a=fmtp:124 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f
a=rtpmap:120 rtx/90000
a=rtpmap:123 H264/90000
a=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001f
a=rtpmap:119 rtx/90000
a=rtpmap:35 H264/90000
a=fmtp:35 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=4d001f
a=rtpmap:36 rtx/90000
a=rtpmap:37 H264/90000
a=fmtp:37 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=f4001f
a=rtpmap:38 rtx/90000
a=rtpmap:39 H264/90000
a=fmtp:39 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=f4001f
a=rtpmap:40 rtx/90000
a=rtpmap:41 AV1/90000
a=rtpmap:42 rtx/90000
a=rtpmap:114 H264/90000
a=fmtp:114 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=64001f
a=rtpmap:115 rtx/90000
a=rtpmap:116 red/90000
a=rtpmap:117 rtx/90000
a=rtpmap:118 ulpfec/90000
a=rtpmap:43 flexfec-03/90000
`
const dahuaSimple = `v=0
o=- 0 0 IN IP4 0.0.0.0
s=-
t=0 0
m=video 0 RTP/AVP 96
a=control:trackID=0
a=rtpmap:96 H264/90000
a=fmtp:96 packetization-mode=1;profile-level-id=42401E;sprop-parameter-sets=Z0JAHqaAoD2QAA==,aM48gAA=
a=recvonly
m=audio 0 RTP/AVP 97
a=control:trackID=1
a=rtpmap:97 MPEG4-GENERIC/16000
a=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408
a=recvonly
m=audio 0 RTP/AVP 8
a=control:trackID=5
a=rtpmap:8 PCMA/8000
a=sendonly
`
const ffmpegPCMU48000 = `v=0
o=- 0 0 IN IP4 127.0.0.1
s=-
t=0 0
m=audio 0 RTP/AVP 96
b=AS:384
a=rtpmap:96 PCMU/48000/1
a=control:streamid=0
`
func TestRouting(t *testing.T) {
prod := &fake.Producer{}
prod.Medias, _ = streamer.UnmarshalRTSPSDP([]byte(dahuaSimple))
assert.Len(t, prod.Medias, 3)
HandleFunc("fake", func(url string) (streamer.Producer, error) {
return prod, nil
})
cons := &fake.Consumer{}
cons.Medias, _ = streamer.UnmarshalSDP([]byte(chrome))
assert.Len(t, cons.Medias, 3)
// setup stream with one producer
stream := newStream("fake:")
// main check:
err := stream.AddConsumer(cons)
assert.Nil(t, err)
assert.Len(t, prod.Tracks, 2)
assert.Len(t, cons.Tracks, 2)
time.Sleep(time.Second)
assert.Greater(t, prod.SendPackets,0)
assert.Greater(t, cons.RecvPackets,0)
assert.Greater(t, prod.RecvPackets,0)
assert.Greater(t, cons.SendPackets,0)
}

28
cmd/streams/streams.go Normal file
View File

@@ -0,0 +1,28 @@
package streams
import (
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/rs/zerolog"
)
var Streams = map[string]*Stream{}
func Init() {
var cfg struct {
Mod map[string]interface{} `yaml:"streams"`
}
app.LoadConfig(&cfg)
log = app.GetLogger("streams")
for name, item := range cfg.Mod {
Streams[name] = newStream(item)
}
}
func Get(name string) *Stream {
return Streams[name]
}
var log zerolog.Logger

265
cmd/webrtc/webrtc.go Normal file
View File

@@ -0,0 +1,265 @@
package webrtc
import (
"github.com/AlexxIT/go2rtc/cmd/api"
"github.com/AlexxIT/go2rtc/cmd/app"
"github.com/AlexxIT/go2rtc/cmd/streams"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/webrtc"
pion "github.com/pion/webrtc/v3"
"github.com/rs/zerolog"
"io/ioutil"
"net"
"net/http"
"strings"
)
func Init() {
var cfg struct {
Mod struct {
Listen string `yaml:"listen"`
Candidates []string `yaml:"candidates"`
IceServers []pion.ICEServer `yaml:"ice_servers"`
} `yaml:"webrtc"`
}
cfg.Mod.IceServers = []pion.ICEServer{
{URLs: []string{"stun:stun.l.google.com:19302"}},
}
app.LoadConfig(&cfg)
log = app.GetLogger("webrtc")
address := cfg.Mod.Listen
pionAPI, err := webrtc.NewAPI(address)
if pionAPI == nil {
log.Error().Err(err).Msg("[webrtc] Init API")
return
}
if err != nil {
log.Warn().Err(err).Msg("[webrtc] Listen")
} else if address != "" {
log.Info().Str("addr", address).Msg("[webrtc] Listen")
_, Port, _ = net.SplitHostPort(address)
}
pionConf := pion.Configuration{
ICEServers: cfg.Mod.IceServers,
SDPSemantics: pion.SDPSemanticsUnifiedPlanWithFallback,
}
NewPConn = func() (*pion.PeerConnection, error) {
return pionAPI.NewPeerConnection(pionConf)
}
candidates = cfg.Mod.Candidates
api.HandleFunc("/api/webrtc", apiHandler)
api.HandleFunc("/api/webrtc/camera", cameraHandler)
api.HandleWS(webrtc.MsgTypeOffer, offerHandler)
api.HandleWS(webrtc.MsgTypeCandidate, candidateHandler)
}
func AddCandidate(address string) {
log.Info().Str("addr", address).Msg("[webrtc] new candidate")
candidates = append(candidates, address)
}
var Port string
var log zerolog.Logger
var candidates []string
var NewPConn func() (*pion.PeerConnection, error)
func apiHandler(w http.ResponseWriter, r *http.Request) {
url := r.URL.Query().Get("url")
stream := streams.Get(url)
if stream == nil {
return
}
// get offer
offer, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Error().Err(err).Msg("[webrtc] read offer")
return
}
// create new webrtc instance
cons := new(webrtc.Conn)
cons.Conn, err = NewPConn()
if err != nil {
log.Error().Err(err).Msg("[webrtc] new conn")
return
}
cons.UserAgent = r.UserAgent()
cons.Listen(func(msg interface{}) {
if msg == streamer.StateNull {
stream.RemoveConsumer(cons)
}
})
if err = stream.AddConsumer(cons); err != nil {
log.Warn().Err(err).Msg("[api.webrtc] add consumer")
return
}
cons.Init()
// exchange sdp with waiting all candidates
answer, err := cons.ExchangeSDP(string(offer), true)
// send SDP to client
if _, err = w.Write([]byte(answer)); err != nil {
log.Error().Err(err).Msg("[api.webrtc] send answer")
}
}
func cameraHandler(w http.ResponseWriter, r *http.Request) {
url := r.URL.Query().Get("url")
stream := streams.Get(url)
if stream == nil {
return
}
// get offer
offer, err := ioutil.ReadAll(r.Body)
if err != nil {
log.Error().Err(err).Msg("[webrtc] read offer")
return
}
// create new webrtc instance
conn := new(webrtc.Conn)
conn.Conn, err = NewPConn()
if err != nil {
log.Error().Err(err).Msg("[webrtc] new conn")
return
}
conn.UserAgent = r.UserAgent()
conn.Listen(func(msg interface{}) {
switch msg.(type) {
case pion.PeerConnectionState:
if msg == pion.PeerConnectionStateDisconnected {
stream.RemoveConsumer(conn)
}
case streamer.Track:
//stream.AddProducer(conn)
}
})
conn.Init()
// exchange sdp with waiting all candidates
answer, err := conn.ExchangeSDP(string(offer), true)
// send SDP to client
if _, err = w.Write([]byte(answer)); err != nil {
log.Error().Err(err).Msg("[api.webrtc] send answer")
}
}
func offerHandler(ctx *api.Context, msg *streamer.Message) {
name := ctx.Request.URL.Query().Get("url")
stream := streams.Get(name)
if stream == nil {
return
}
log.Debug().Str("stream", name).Msg("[webrtc] new consumer")
var err error
// create new webrtc instance
conn := new(webrtc.Conn)
conn.Conn, err = NewPConn()
if err != nil {
log.Error().Err(err).Msg("[webrtc] new conn")
return
}
conn.UserAgent = ctx.Request.UserAgent()
conn.Listen(func(msg interface{}) {
switch msg := msg.(type) {
case streamer.EventType:
if msg == streamer.StateNull {
stream.RemoveConsumer(conn)
}
case *streamer.Message:
// subscribe on webrtc server candidates
log.Trace().Str("candidate", msg.Value.(string)).Msg("[webrtc] local")
ctx.Write(msg)
}
})
// 1. SetOffer, so we can get remote client codecs
offer := msg.Value.(string)
log.Trace().Msgf("[webrtc] offer:\n%s", offer)
if err = conn.SetOffer(offer); err != nil {
log.Warn().Err(err).Msg("[api.webrtc] set offer")
ctx.Error(err)
return
}
// 2. AddConsumer, so we get new tracks
if err = stream.AddConsumer(conn); err != nil {
log.Warn().Err(err).Msg("[api.webrtc] add consumer")
ctx.Error(err)
return
}
conn.Init()
// exchange sdp without waiting all candidates
//answer, err := conn.ExchangeSDP(offer, false)
answer, err := conn.GetAnswer()
log.Trace().Msgf("[webrtc] answer\n%s", answer)
if err != nil {
log.Error().Err(err).Msg("[webrtc] get answer")
ctx.Error(err)
return
}
ctx.Write(&streamer.Message{
Type: webrtc.MsgTypeAnswer, Value: answer,
})
for _, address := range candidates {
if strings.HasPrefix(address, "stun:") {
ip, err := webrtc.GetPublicIP()
if err != nil {
log.Warn().Err(err).Msg("[webrtc] public IP")
continue
}
address = ip.String() + address[4:]
}
cand, err := webrtc.NewCandidate(address)
if err != nil {
log.Warn().Err(err).Msg("[webrtc] candidate")
continue
}
conn.Fire(&streamer.Message{
Type: webrtc.MsgTypeCandidate, Value: cand,
})
}
ctx.Consumer = conn
}
func candidateHandler(ctx *api.Context, msg *streamer.Message) {
if ctx.Consumer == nil {
return
}
if conn := ctx.Consumer.(*webrtc.Conn); conn != nil {
log.Trace().Str("candidate", msg.Value.(string)).Msg("[webrtc] Remote")
conn.Push(msg)
}
}

4
codecs.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 27 KiB

58
examples/rtsp_client.go Normal file
View File

@@ -0,0 +1,58 @@
package main
import (
"fmt"
"github.com/AlexxIT/go2rtc/pkg/rtsp"
"github.com/pion/rtp"
"os"
"time"
)
func main() {
client, err := rtsp.NewClient(os.Args[1])
if err != nil {
panic(err)
}
if err = client.Dial(); err != nil {
panic(err)
}
if err = client.Describe(); err != nil {
panic(err)
}
for _, media := range client.GetMedias() {
fmt.Printf("Media: %v\n", media)
if media.AV() {
track := client.GetTrack(media, media.Codecs[0])
fmt.Printf("Track: %v, %v\n", track, track.Codec)
track.Bind(func(packet *rtp.Packet) error {
nalUnitType := packet.Payload[0] & 0x1F
fmt.Printf(
"[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d\n",
track.Codec.Name, nalUnitType, len(packet.Payload), packet.Timestamp,
packet.PayloadType, packet.SSRC,
)
return nil
})
}
}
if err = client.Play(); err != nil {
panic(err)
}
time.AfterFunc(time.Second*5, func() {
if err = client.Close(); err != nil {
panic(err)
}
})
if err = client.Handle(); err != nil {
panic(err)
}
fmt.Println("The End")
}

41
go.mod Normal file
View File

@@ -0,0 +1,41 @@
module github.com/AlexxIT/go2rtc
go 1.17
require (
github.com/deepch/vdk v0.0.19
github.com/gorilla/websocket v1.5.0
github.com/pion/ice/v2 v2.2.6
github.com/pion/interceptor v0.1.11
github.com/pion/logging v0.2.2
github.com/pion/rtcp v1.2.9
github.com/pion/rtp v1.7.13
github.com/pion/sdp/v3 v3.0.5
github.com/pion/stun v0.3.5
github.com/pion/webrtc/v3 v3.1.43
github.com/rs/zerolog v1.27.0
github.com/stretchr/testify v1.7.1
gopkg.in/yaml.v3 v3.0.1
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/pion/datachannel v1.5.2 // indirect
github.com/pion/dtls/v2 v2.1.5 // indirect
github.com/pion/mdns v0.0.5 // indirect
github.com/pion/randutil v0.1.0 // indirect
github.com/pion/sctp v1.8.2 // indirect
github.com/pion/srtp/v2 v2.0.10 // indirect
github.com/pion/transport v0.13.1 // indirect
github.com/pion/turn/v2 v2.0.8 // indirect
github.com/pion/udp v0.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8 // indirect
golang.org/x/net v0.0.0-20220630215102-69896b714898 // indirect
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 // indirect
)
replace github.com/deepch/vdk v0.0.19 => github.com/AlexxIT/vdk v0.0.18-0.20220616041030-b0d122807b2e

221
go.sum Normal file
View File

@@ -0,0 +1,221 @@
github.com/AlexxIT/vdk v0.0.18-0.20220616041030-b0d122807b2e h1:NAgHHZB+JUN3/J4/yq1q1EAc8xwJ8bb/Qp0AcjkfzAA=
github.com/AlexxIT/vdk v0.0.18-0.20220616041030-b0d122807b2e/go.mod h1:KqQ/KU3hOc4a62l/jPRH5Hiz5fhTq5cGCl8IqeCxWQI=
github.com/cheekybits/genny v1.0.0/go.mod h1:+tQajlRqAUrPI7DOSpB0XAqZYtQakVtB7wXkRAgjxjQ=
github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ=
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
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/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/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw=
github.com/marten-seemann/qtls v0.2.3/go.mod h1:xzjG7avBwGGbdZ8dTGxlBnLArsVKLvwmjgmPuiQEcYk=
github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40=
github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4=
github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
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/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk=
github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0=
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
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.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY=
github.com/pion/datachannel v1.4.21/go.mod h1:oiNyP4gHx2DIwRzX/MFyH0Rz/Gz05OgBlayAI2hAWjg=
github.com/pion/datachannel v1.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E=
github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ=
github.com/pion/dtls/v2 v2.0.1/go.mod h1:uMQkz2W0cSqY00xav7WByQ4Hb+18xeQh2oH2fRezr5U=
github.com/pion/dtls/v2 v2.0.2/go.mod h1:27PEO3MDdaCfo21heT59/vsdmZc0zMt9wQPcSlLu/1I=
github.com/pion/dtls/v2 v2.1.3/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus=
github.com/pion/dtls/v2 v2.1.5 h1:jlh2vtIyUBShchoTDqpCCqiYCyRFJ/lvf/gQ8TALs+c=
github.com/pion/dtls/v2 v2.1.5/go.mod h1:BqCE7xPZbPSubGasRoDFJeTsyJtdD1FanJYL0JGheqY=
github.com/pion/ice v0.7.18 h1:KbAWlzWRUdX9SmehBh3gYpIFsirjhSQsCw6K2MjYMK0=
github.com/pion/ice v0.7.18/go.mod h1:+Bvnm3nYC6Nnp7VV6glUkuOfToB/AtMRZpOU8ihuf4c=
github.com/pion/ice/v2 v2.2.6 h1:R/vaLlI1J2gCx141L5PEwtuGAGcyS6e7E0hDeJFq5Ig=
github.com/pion/ice/v2 v2.2.6/go.mod h1:SWuHiOGP17lGromHTFadUe1EuPgFh/oCU6FCMZHooVE=
github.com/pion/interceptor v0.1.11 h1:00U6OlqxA3FFB50HSg25J/8cWi7P6FbSzw4eFn24Bvs=
github.com/pion/interceptor v0.1.11/go.mod h1:tbtKjZY14awXd7Bq0mmWvgtHB5MDaRN7HV3OZ/uy7s8=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
github.com/pion/mdns v0.0.4/go.mod h1:R1sL0p50l42S5lJs91oNdUL58nm0QHrhxnSegr++qC0=
github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw=
github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g=
github.com/pion/quic v0.1.1/go.mod h1:zEU51v7ru8Mp4AUBJvj6psrSth5eEFNnVQK5K48oV3k=
github.com/pion/randutil v0.0.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
github.com/pion/rtcp v1.2.3/go.mod h1:zGhIv0RPRF0Z1Wiij22pUt5W/c9fevqSzT4jje/oK7I=
github.com/pion/rtcp v1.2.9 h1:1ujStwg++IOLIEoOiIQ2s+qBuJ1VN81KW+9pMPsif+U=
github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo=
github.com/pion/rtp v1.6.0/go.mod h1:QgfogHsMBVE/RFNno467U/KBqfUywEH+HK+0rtnwsdI=
github.com/pion/rtp v1.7.13 h1:qcHwlmtiI50t1XivvoawdCGTP4Uiypzfrsap+bijcoA=
github.com/pion/rtp v1.7.13/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko=
github.com/pion/sctp v1.7.10/go.mod h1:EhpTUQu1/lcK3xI+eriS6/96fWetHGCvBi9MSsnaBN0=
github.com/pion/sctp v1.8.0/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
github.com/pion/sctp v1.8.2 h1:yBBCIrUMJ4yFICL3RIvR4eh/H2BTTvlligmSTy+3kiA=
github.com/pion/sctp v1.8.2/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s=
github.com/pion/sdp/v2 v2.4.0/go.mod h1:L2LxrOpSTJbAns244vfPChbciR/ReU1KWfG04OpkR7E=
github.com/pion/sdp/v3 v3.0.5 h1:ouvI7IgGl+V4CrqskVtr3AaTrPvPisEOxwgpdktctkU=
github.com/pion/sdp/v3 v3.0.5/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw=
github.com/pion/srtp v1.5.1 h1:9Q3jAfslYZBt+C69SI/ZcONJh9049JUHZWYRRf5KEKw=
github.com/pion/srtp v1.5.1/go.mod h1:B+QgX5xPeQTNc1CJStJPHzOlHK66ViMDWTT0HZTCkcA=
github.com/pion/srtp/v2 v2.0.9/go.mod h1:5TtM9yw6lsH0ppNCehB/EjEUli7VkUgKSPJqWVqbhQ4=
github.com/pion/srtp/v2 v2.0.10 h1:b8ZvEuI+mrL8hbr/f1YiJFB34UMrOac3R3N1yq2UN0w=
github.com/pion/srtp/v2 v2.0.10/go.mod h1:XEeSWaK9PfuMs7zxXyiN252AHPbH12NX5q/CFDWtUuA=
github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg=
github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA=
github.com/pion/transport v0.6.0/go.mod h1:iWZ07doqOosSLMhZ+FXUTq+TamDoXSllxpbGcfkCmbE=
github.com/pion/transport v0.8.10/go.mod h1:tBmha/UCjpum5hqTWhfAEs3CO4/tHSg0MYRhSzR+CZ8=
github.com/pion/transport v0.10.0/go.mod h1:BnHnUipd0rZQyTVB2SBGojFHT9CBt5C5TcsJSQGkvSE=
github.com/pion/transport v0.10.1/go.mod h1:PBis1stIILMiis0PewDw91WJeLJkyIMcEk+DwKOzf4A=
github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q=
github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A=
github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g=
github.com/pion/transport v0.13.1 h1:/UH5yLeQtwm2VZIPjxwnNFxjS4DFhyLfS4GlfuKUzfA=
github.com/pion/transport v0.13.1/go.mod h1:EBxbqzyv+ZrmDb82XswEE0BjfQFtuw1Nu6sjnjWCsGg=
github.com/pion/turn/v2 v2.0.4/go.mod h1:1812p4DcGVbYVBTiraUmP50XoKye++AMkbfp+N27mog=
github.com/pion/turn/v2 v2.0.8 h1:KEstL92OUN3k5k8qxsXHpr7WWfrdp7iJZHx99ud8muw=
github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw=
github.com/pion/udp v0.1.0/go.mod h1:BPELIjbwE9PRbd/zxI/KYBnbo7B6+oA6YuEaNE8lths=
github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o=
github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M=
github.com/pion/webrtc/v2 v2.2.26/go.mod h1:XMZbZRNHyPDe1gzTIHFcQu02283YO45CbiwFgKvXnmc=
github.com/pion/webrtc/v3 v3.1.41/go.mod h1:sUcW9SFPEWerDqGOBmdYEMfRvbdd7rgwo4bNzfsXww4=
github.com/pion/webrtc/v3 v3.1.43 h1:YT3ZTO94UT4kSBvZnRAH82+0jJPUruiKr9CEstdlQzk=
github.com/pion/webrtc/v3 v3.1.43/go.mod h1:G/J8k0+grVsjC/rjCZ24AKoCCxcFFODgh7zThNZGs0M=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/rs/zerolog v1.27.0 h1:1T7qCieN22GVc8S4Q2yuexzBb1EqjbgjSH9RohbMjKs=
github.com/rs/zerolog v1.27.0/go.mod h1:7frBqO0oezxmnO7GF86FY++uy8I0Tk/If5ni1G9Qc0U=
github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1 h1:5TQK59W5E3v0r2duFAb7P95B6hEeOyEnHRa8MjYSMTY=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
golang.org/x/crypto v0.0.0-20190228161510-8dd112bcdc25/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200602180216-279210d13fed/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20200709230013-948cd5f35899/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220427172511-eb4f295cb31f/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8 h1:y+mHpWoQJNAHt26Nhh6JP7hvM71IRZureyvZhoVALIs=
golang.org/x/crypto v0.0.0-20220516162934-403b01795ae8/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200602114024-627f9648deb9/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk=
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220401154927-543a649e0bdd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
golang.org/x/net v0.0.0-20220531201128-c960675eff93/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.0.0-20220630215102-69896b714898 h1:K7wO6V1IrczY9QOQ2WkVpw4JQSwCd52UsxVEirZUfiw=
golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200724161237-0e2f3a69832c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220608164250-635b8c9b7f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664 h1:wEZYwx+kK+KlZ0hpvP2Ls1Xr4+RWnlzGFwPP0aiDjIU=
golang.org/x/sys v0.0.0-20220622161953-175b2fd9d664/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
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=
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/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/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

9
main.go Normal file
View File

@@ -0,0 +1,9 @@
package main
import (
"github.com/AlexxIT/go2rtc/cmd"
)
func main() {
cmd.Run()
}

47
pkg/fake/consumer.go Normal file
View File

@@ -0,0 +1,47 @@
package fake
import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
"time"
)
type Consumer struct {
streamer.Element
Medias []*streamer.Media
Tracks []*streamer.Track
RecvPackets int
SendPackets int
}
func (c *Consumer) GetMedias() []*streamer.Media {
return c.Medias
}
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
switch track.Direction {
case streamer.DirectionSendonly:
track = track.Bind(func(packet *rtp.Packet) error {
if track.Codec.PayloadType != packet.PayloadType {
panic("wrong payload type")
}
c.RecvPackets++
return nil
})
case streamer.DirectionRecvonly:
go func() {
for {
pkt := &rtp.Packet{}
pkt.PayloadType = track.Codec.PayloadType
if err := track.WriteRTP(pkt); err != nil {
return
}
c.SendPackets++
time.Sleep(time.Second)
}
}()
}
c.Tracks = append(c.Tracks, track)
return track
}

62
pkg/fake/producer.go Normal file
View File

@@ -0,0 +1,62 @@
package fake
import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
"time"
)
type Producer struct {
streamer.Element
Medias []*streamer.Media
Tracks []*streamer.Track
RecvPackets int
SendPackets int
}
func (p *Producer) GetMedias() []*streamer.Media {
return p.Medias
}
func (p *Producer) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
if !streamer.Contains(p.Medias, media, codec) {
panic("you shall not pass!")
}
track := &streamer.Track{Codec: codec, Direction: media.Direction}
switch media.Direction {
case streamer.DirectionSendonly:
track2 := track.Bind(func(packet *rtp.Packet) error {
p.RecvPackets++
return nil
})
p.Tracks = append(p.Tracks, track2)
case streamer.DirectionRecvonly:
p.Tracks = append(p.Tracks, track)
}
return track
}
func (p *Producer) Start() error {
for {
for _, track := range p.Tracks {
if track.Direction != streamer.DirectionSendonly {
continue
}
pkt := &rtp.Packet{}
pkt.PayloadType = track.Codec.PayloadType
if err := track.WriteRTP(pkt); err != nil {
return err
}
p.SendPackets++
}
time.Sleep(time.Second)
}
}
func (p *Producer) Stop() error {
panic("not implemented")
}

27
pkg/h264/README.md Normal file
View File

@@ -0,0 +1,27 @@
## WebRTC
Video codec | Media string | Device
----------------|--------------|-------
H.264/baseline! | avc1.42E0xx | Chromecast
H.264/baseline! | avc1.42E0xx | Chrome/Safari WebRTC
H.264/baseline! | avc1.42C0xx | FFmpeg ultrafast
H.264/baseline! | avc1.4240xx | Dahua H264B
H.264/baseline | avc1.4200xx | Chrome WebRTC
H.264/main! | avc1.4D40xx | Chromecast
H.264/main! | avc1.4D40xx | FFmpeg superfast main
H.264/main! | avc1.4D40xx | Dahua H264
H.264/main | avc1.4D00xx | Chrome WebRTC
H.264/high! | avc1.640Cxx | Safari WebRTC
H.264/high | avc1.6400xx | Chromecast
H.264/high | avc1.6400xx | FFmpeg superfast
## Useful Links
- [RTP Payload Format for H.264 Video](https://datatracker.ietf.org/doc/html/rfc6184)
- [The H264 Sequence parameter set](https://www.cardinalpeak.com/blog/the-h-264-sequence-parameter-set)
- [H.264 Video Types (Microsoft)](https://docs.microsoft.com/en-us/windows/win32/directshow/h-264-video-types)
- [Automatic Generation of H.264 Parameter Sets to Recover Video File Fragments](https://arxiv.org/pdf/2104.14522.pdf)
- [Chromium sources](https://chromium.googlesource.com/external/webrtc/+/HEAD/common_video/h264)
- [AVC levels](https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels)
- [AVC profiles table](https://developer.mozilla.org/ru/docs/Web/Media/Formats/codecs_parameter)
- [Supported Media for Google Cast](https://developers.google.com/cast/docs/media)

View File

@@ -0,0 +1,87 @@
package golomb
import "bytes"
type Reader struct {
r *bytes.Reader
b byte
shift byte
}
func NewReader(b []byte) *Reader {
return &Reader{
r: bytes.NewReader(b),
}
}
func (g *Reader) ReadBit() (b byte, err error) {
if g.shift == 0 {
if g.b, err = g.r.ReadByte(); err != nil {
return 0, err
}
g.shift = 7
} else {
g.shift--
}
b = (g.b >> g.shift) & 0b1
return
}
func (g *Reader) ReadBits(n byte) (res uint, err error) {
var b byte
for i := n - 1; i != 255; i-- {
if b, err = g.ReadBit(); err != nil {
return
}
res |= uint(b) << i
}
return
}
func (g *Reader) ReadUEGolomb() (res uint, err error) {
var b uint
var i byte
for i = 0; i < 32; i++ {
if b, err = g.ReadBits(1); err != nil {
return
}
if b != 0 {
break
}
}
if res, err = g.ReadBits(i); err != nil {
return
}
res += (1 << i) - 1
return
}
func (g *Reader) ReadSEGolomb() (res int, err error) {
var b uint
if b, err = g.ReadUEGolomb(); err != nil {
return
}
if b%2 == 0 {
res = -int(b >> 1)
} else {
res = int(b>>1)
}
return
}
func (g *Reader) ReadByte() (byte, error) {
return g.r.ReadByte()
}
func (g *Reader) End() bool {
// if only one bit in next byte left
if g.shift == 0 && g.r.Len() == 1 {
b, _ := g.r.ReadByte()
_ = g.r.UnreadByte()
return b == 0x80
}
if g.r.Len() == 0 {
//panic("not implemented")
}
return false
}

View File

@@ -0,0 +1,56 @@
package golomb
import "math/bits"
type Writer struct {
buf []byte
b byte // last byte
i int // last byte index
shift byte
}
func NewWriter() *Writer {
return &Writer{i: -1}
}
func (g *Writer) WriteBit(b byte) {
if g.shift == 0 {
g.buf = append(g.buf, 0)
g.b = 0
g.i++
g.shift = 7
} else {
g.shift--
}
g.b |= b << g.shift
g.buf[g.i] = g.b
}
func (g *Writer) WriteBits(b, n byte) {
for i := n - 1; i != 255; i-- {
g.WriteBit((b >> i) & 0b1)
}
}
func (g *Writer) WriteByte(b byte) {
g.buf = append(g.buf, b)
g.i++
}
func (g *Writer) WriteUEGolomb(b byte) {
b++
n := uint8(bits.Len8(b))*2 - 1
g.WriteBits(b, n)
}
func (g *Writer) WriteSEGolomb(b int8) {
if b > 0 {
g.WriteUEGolomb(byte(b)*2 - 1)
} else {
g.WriteUEGolomb(byte(-b) * 2)
}
}
func (g *Writer) Bytes() []byte {
return g.buf
}

53
pkg/h264/helper.go Normal file
View File

@@ -0,0 +1,53 @@
package h264
import (
"encoding/base64"
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"strings"
)
const (
NALUTypePFrame = 1
NALUTypeIFrame = 5
NALUTypeSPS = 7
NALUTypePPS = 8
PayloadTypeAVC = 255
)
func NALUType(b []byte) byte {
return b[4] & 0x1F
}
func EncodeAVC(raw []byte) (avc []byte) {
avc = make([]byte, len(raw)+4)
binary.BigEndian.PutUint32(avc, uint32(len(raw)))
copy(avc[4:], raw)
return
}
func IsAVC(codec *streamer.Codec) bool {
return codec.PayloadType == PayloadTypeAVC
}
func GetParameterSet(fmtp string) (sps, pps []byte) {
if fmtp == "" {
return
}
s := streamer.Between(fmtp, "sprop-parameter-sets=", ";")
if s == "" {
return
}
i := strings.IndexByte(s, ',')
if i < 0 {
return
}
sps, _ = base64.StdEncoding.DecodeString(s[:i])
pps, _ = base64.StdEncoding.DecodeString(s[i+1:])
return
}

202
pkg/h264/payloader.go Normal file
View File

@@ -0,0 +1,202 @@
package h264
import "encoding/binary"
// Payloader payloads H264 packets
type Payloader struct {
IsAVC bool
spsNalu, ppsNalu []byte
}
const (
stapaNALUType = 24
fuaNALUType = 28
fubNALUType = 29
spsNALUType = 7
ppsNALUType = 8
audNALUType = 9
fillerNALUType = 12
fuaHeaderSize = 2
//stapaHeaderSize = 1
//stapaNALULengthSize = 2
naluTypeBitmask = 0x1F
naluRefIdcBitmask = 0x60
//fuStartBitmask = 0x80
//fuEndBitmask = 0x40
outputStapAHeader = 0x78
)
//func annexbNALUStartCode() []byte { return []byte{0x00, 0x00, 0x00, 0x01} }
func emitNalus(nals []byte, isAVC bool, emit func([]byte)) {
if !isAVC {
nextInd := func(nalu []byte, start int) (indStart int, indLen int) {
zeroCount := 0
for i, b := range nalu[start:] {
if b == 0 {
zeroCount++
continue
} else if b == 1 {
if zeroCount >= 2 {
return start + i - zeroCount, zeroCount + 1
}
}
zeroCount = 0
}
return -1, -1
}
nextIndStart, nextIndLen := nextInd(nals, 0)
if nextIndStart == -1 {
emit(nals)
} else {
for nextIndStart != -1 {
prevStart := nextIndStart + nextIndLen
nextIndStart, nextIndLen = nextInd(nals, prevStart)
if nextIndStart != -1 {
emit(nals[prevStart:nextIndStart])
} else {
// Emit until end of stream, no end indicator found
emit(nals[prevStart:])
}
}
}
} else {
for {
end := 4 + binary.BigEndian.Uint32(nals)
emit(nals[4:end])
if int(end) >= len(nals) {
break
}
nals = nals[end:]
}
}
}
// Payload fragments a H264 packet across one or more byte arrays
func (p *Payloader) Payload(mtu uint16, payload []byte) [][]byte {
var payloads [][]byte
if len(payload) == 0 {
return payloads
}
emitNalus(payload, p.IsAVC, func(nalu []byte) {
if len(nalu) == 0 {
return
}
naluType := nalu[0] & naluTypeBitmask
naluRefIdc := nalu[0] & naluRefIdcBitmask
switch {
case naluType == audNALUType || naluType == fillerNALUType:
return
case naluType == spsNALUType:
p.spsNalu = nalu
return
case naluType == ppsNALUType:
p.ppsNalu = nalu
return
case p.spsNalu != nil && p.ppsNalu != nil:
// Pack current NALU with SPS and PPS as STAP-A
spsLen := make([]byte, 2)
binary.BigEndian.PutUint16(spsLen, uint16(len(p.spsNalu)))
ppsLen := make([]byte, 2)
binary.BigEndian.PutUint16(ppsLen, uint16(len(p.ppsNalu)))
stapANalu := []byte{outputStapAHeader}
stapANalu = append(stapANalu, spsLen...)
stapANalu = append(stapANalu, p.spsNalu...)
stapANalu = append(stapANalu, ppsLen...)
stapANalu = append(stapANalu, p.ppsNalu...)
if len(stapANalu) <= int(mtu) {
out := make([]byte, len(stapANalu))
copy(out, stapANalu)
payloads = append(payloads, out)
}
p.spsNalu = nil
p.ppsNalu = nil
}
// Single NALU
if len(nalu) <= int(mtu) {
out := make([]byte, len(nalu))
copy(out, nalu)
payloads = append(payloads, out)
return
}
// FU-A
maxFragmentSize := int(mtu) - fuaHeaderSize
// The FU payload consists of fragments of the payload of the fragmented
// NAL unit so that if the fragmentation unit payloads of consecutive
// FUs are sequentially concatenated, the payload of the fragmented NAL
// unit can be reconstructed. The NAL unit type octet of the fragmented
// NAL unit is not included as such in the fragmentation unit payload,
// but rather the information of the NAL unit type octet of the
// fragmented NAL unit is conveyed in the F and NRI fields of the FU
// indicator octet of the fragmentation unit and in the type field of
// the FU header. An FU payload MAY have any number of octets and MAY
// be empty.
naluData := nalu
// According to the RFC, the first octet is skipped due to redundant information
naluDataIndex := 1
naluDataLength := len(nalu) - naluDataIndex
naluDataRemaining := naluDataLength
if min(maxFragmentSize, naluDataRemaining) <= 0 {
return
}
for naluDataRemaining > 0 {
currentFragmentSize := min(maxFragmentSize, naluDataRemaining)
out := make([]byte, fuaHeaderSize+currentFragmentSize)
// +---------------+
// |0|1|2|3|4|5|6|7|
// +-+-+-+-+-+-+-+-+
// |F|NRI| Type |
// +---------------+
out[0] = fuaNALUType
out[0] |= naluRefIdc
// +---------------+
// |0|1|2|3|4|5|6|7|
// +-+-+-+-+-+-+-+-+
// |S|E|R| Type |
// +---------------+
out[1] = naluType
if naluDataRemaining == naluDataLength {
// Set start bit
out[1] |= 1 << 7
} else if naluDataRemaining-currentFragmentSize == 0 {
// Set end bit
out[1] |= 1 << 6
}
copy(out[fuaHeaderSize:], naluData[naluDataIndex:naluDataIndex+currentFragmentSize])
payloads = append(payloads, out)
naluDataRemaining -= currentFragmentSize
naluDataIndex += currentFragmentSize
}
})
return payloads
}
func min(a, b int) int {
if a < b {
return a
}
return b
}

127
pkg/h264/ps/pps.go Normal file
View File

@@ -0,0 +1,127 @@
package ps
import (
"errors"
"github.com/AlexxIT/go2rtc/pkg/h264/golomb"
)
const PPSHeader = 0x68
// https://www.itu.int/rec/T-REC-H.264
// 7.3.2.2 Picture parameter set RBSP syntax
type PPS struct{}
func (p *PPS) Marshal() []byte {
w := golomb.NewWriter()
// this is typical PPS for most H264 cameras
w.WriteByte(PPSHeader)
w.WriteUEGolomb(0) // pic_parameter_set_id
w.WriteUEGolomb(0) // seq_parameter_set_id
w.WriteBit(1) // entropy_coding_mode_flag
w.WriteBit(0) // bottom_field_pic_order_in_frame_present_flag
w.WriteUEGolomb(0) // num_slice_groups_minus1
w.WriteUEGolomb(0) // num_ref_idx_l0_default_active_minus1
w.WriteUEGolomb(0) // num_ref_idx_l1_default_active_minus1
w.WriteBit(0) // weighted_pred_flag
w.WriteBits(0, 2) // weighted_bipred_idc
w.WriteSEGolomb(0) // pic_init_qp_minus26
w.WriteSEGolomb(0) // pic_init_qs_minus26
w.WriteSEGolomb(0) // chroma_qp_index_offset
w.WriteBit(1) // deblocking_filter_control_present_flag
w.WriteBit(0) // constrained_intra_pred_flag
w.WriteBit(0) // redundant_pic_cnt_present_flag
w.WriteBit(1) // rbsp_trailing_bits()
return w.Bytes()
}
func (p *PPS) Unmarshal(data []byte) (err error) {
r := golomb.NewReader(data)
var b byte
var u uint
if b, err = r.ReadByte(); err != nil {
return
}
if b&0x1F != 8 {
err = errors.New("not PPS data")
return
}
// pic_parameter_set_id
if u, err = r.ReadUEGolomb(); err != nil {
return
}
// seq_parameter_set_id
if u, err = r.ReadUEGolomb(); err != nil {
return
}
// entropy_coding_mode_flag
if b, err = r.ReadBit(); err != nil {
return
}
// bottom_field_pic_order_in_frame_present_flag
if b, err = r.ReadBit(); err != nil {
return
}
// num_slice_groups_minus1
if u, err = r.ReadUEGolomb(); err != nil {
return
}
if u > 0 {
//panic("not implemented")
return nil
}
// num_ref_idx_l0_default_active_minus1
if _, err = r.ReadUEGolomb(); err != nil {
return
}
// num_ref_idx_l1_default_active_minus1
if _, err = r.ReadUEGolomb(); err != nil {
return
}
// weighted_pred_flag
if _, err = r.ReadBit(); err != nil {
return
}
// weighted_bipred_idc
if _, err = r.ReadBits(2); err != nil {
return
}
// pic_init_qp_minus26
if _, err = r.ReadSEGolomb(); err != nil {
return
}
// pic_init_qs_minus26
if _, err = r.ReadSEGolomb(); err != nil {
return
}
// chroma_qp_index_offset
if _, err = r.ReadSEGolomb(); err != nil {
return
}
// deblocking_filter_control_present_flag
if _, err = r.ReadBit(); err != nil {
return
}
// constrained_intra_pred_flag
if _, err = r.ReadBit(); err != nil {
return
}
// redundant_pic_cnt_present_flag
if _, err = r.ReadBit(); err != nil {
return
}
if !r.End() {
//panic("not implemented")
}
return
}

279
pkg/h264/ps/sps.go Normal file
View File

@@ -0,0 +1,279 @@
package ps
import (
"errors"
"github.com/AlexxIT/go2rtc/pkg/h264/golomb"
)
const firstByte = 0x67
// Google to "h264 specification pdf"
// https://www.itu.int/rec/dologin_pub.asp?lang=e&id=T-REC-H.264-201602-S!!PDF-E&type=items
type SPS struct {
Profile string
ProfileIDC uint8
ProfileIOP uint8
LevelIDC uint8
Width uint16
Height uint16
}
func NewSPS(profile string, level uint8, width uint16, height uint16) *SPS {
s := &SPS{
Profile: profile, LevelIDC: level, Width: width, Height: height,
}
s.ProfileIDC, s.ProfileIOP = DecodeProfile(profile)
return s
}
// https://www.cardinalpeak.com/blog/the-h-264-sequence-parameter-set
func (s *SPS) Marshal() []byte {
w := golomb.NewWriter()
// this is typical SPS for most H264 cameras
w.WriteByte(firstByte)
w.WriteByte(s.ProfileIDC)
w.WriteByte(s.ProfileIOP)
w.WriteByte(s.LevelIDC)
w.WriteUEGolomb(0) // seq_parameter_set_id (0)
w.WriteUEGolomb(0) // log2_max_frame_num_minus4 (depends)
w.WriteUEGolomb(0) // pic_order_cnt_type (0 or 2)
w.WriteUEGolomb(0) // log2_max_pic_order_cnt_lsb_minus4 (depends)
w.WriteUEGolomb(1) // num_ref_frames (1)
w.WriteBit(0) // gaps_in_frame_num_value_allowed_flag (0)
w.WriteUEGolomb(uint8(s.Width>>4) - 1) // pic_width_in_mbs_minus_1
w.WriteUEGolomb(uint8(s.Height>>4) - 1) // pic_height_in_map_units_minus_1
w.WriteBit(1) // frame_mbs_only_flag (1)
w.WriteBit(1) // direct_8x8_inference_flag (1)
w.WriteBit(0) // frame_cropping_flag (0 is OK)
w.WriteBit(0) // vui_prameters_present_flag (0 is OK)
w.WriteBit(1) // rbsp_stop_one_bit
return w.Bytes()
}
func (s *SPS) Unmarshal(data []byte) (err error) {
r := golomb.NewReader(data)
var b byte
var u uint
if b, err = r.ReadByte(); err != nil {
return
}
if b&0x1F != 7 {
err = errors.New("not SPS data")
return
}
if s.ProfileIDC, err = r.ReadByte(); err != nil {
return
}
if s.ProfileIOP, err = r.ReadByte(); err != nil {
return
}
if s.LevelIDC, err = r.ReadByte(); err != nil {
return
}
s.Profile = EncodeProfile(s.ProfileIDC, s.ProfileIOP)
u, err = r.ReadUEGolomb() // seq_parameter_set_id
if s.ProfileIDC == 100 || s.ProfileIDC == 110 || s.ProfileIDC == 122 ||
s.ProfileIDC == 244 || s.ProfileIDC == 44 || s.ProfileIDC == 83 ||
s.ProfileIDC == 86 || s.ProfileIDC == 118 || s.ProfileIDC == 128 ||
s.ProfileIDC == 138 || s.ProfileIDC == 139 || s.ProfileIDC == 134 ||
s.ProfileIDC == 135 {
var n byte
u, err = r.ReadUEGolomb() // chroma_format_idc
if u == 3 {
b, err = r.ReadBit() // separate_colour_plane_flag
n = 12
} else {
n = 8
}
u, err = r.ReadUEGolomb() // bit_depth_luma_minus8
u, err = r.ReadUEGolomb() // bit_depth_chroma_minus8
b, err = r.ReadBit() // qpprime_y_zero_transform_bypass_flag
b, err = r.ReadBit() // seq_scaling_matrix_present_flag
if b > 0 {
for i := byte(0); i < n; i++ {
b, err = r.ReadBit() // seq_scaling_list_present_flag[i]
if b > 0 {
panic("not implemented")
}
}
}
}
u, err = r.ReadUEGolomb() // log2_max_frame_num_minus4
u, err = r.ReadUEGolomb() // pic_order_cnt_type
switch u {
case 0:
u, err = r.ReadUEGolomb() // log2_max_pic_order_cnt_lsb_minus4
case 1:
b, err = r.ReadBit() // delta_pic_order_always_zero_flag
_, err = r.ReadSEGolomb() // offset_for_non_ref_pic
_, err = r.ReadSEGolomb() // offset_for_top_to_bottom_field
u, err = r.ReadUEGolomb() // num_ref_frames_in_pic_order_cnt_cycle
for i := byte(0); i < b; i++ {
_, err = r.ReadSEGolomb() // offset_for_ref_frame[i]
}
}
u, err = r.ReadUEGolomb() // num_ref_frames
b, err = r.ReadBit() // gaps_in_frame_num_value_allowed_flag
u, err = r.ReadUEGolomb() // pic_width_in_mbs_minus_1
s.Width = uint16(u+1) << 4
u, err = r.ReadUEGolomb() // pic_height_in_map_units_minus_1
s.Height = uint16(u+1) << 4
b, err = r.ReadBit() // frame_mbs_only_flag
if b == 0 {
_, err = r.ReadBit()
}
b, err = r.ReadBit() // direct_8x8_inference_flag
b, err = r.ReadBit() // frame_cropping_flag
if b > 0 {
u, err = r.ReadUEGolomb() // frame_crop_left_offset
s.Width -= uint16(u) << 1
u, err = r.ReadUEGolomb() // frame_crop_right_offset
s.Width -= uint16(u) << 1
u, err = r.ReadUEGolomb() // frame_crop_top_offset
s.Height -= uint16(u) << 1
u, err = r.ReadUEGolomb() // frame_crop_bottom_offset
s.Height -= uint16(u) << 1
}
b, err = r.ReadBit() // vui_prameters_present_flag
if b > 0 {
b, err = r.ReadBit() // vui_prameters_present_flag
if b > 0 {
u, err = r.ReadBits(8) // aspect_ratio_idc
if b == 255 {
u, err = r.ReadBits(16) // sar_width
u, err = r.ReadBits(16) // sar_height
}
}
b, err = r.ReadBit() // overscan_info_present_flag
if b > 0 {
b, err = r.ReadBit() // overscan_appropriate_flag
}
b, err = r.ReadBit() // video_signal_type_present_flag
if b > 0 {
u, err = r.ReadBits(3) // video_format
b, err = r.ReadBit() // video_full_range_flag
b, err = r.ReadBit() // colour_description_present_flag
if b > 0 {
u, err = r.ReadBits(8) // colour_primaries
u, err = r.ReadBits(8) // transfer_characteristics
u, err = r.ReadBits(8) // matrix_coefficients
}
}
b, err = r.ReadBit() // chroma_loc_info_present_flag
if b > 0 {
u, err = r.ReadUEGolomb() // chroma_sample_loc_type_top_field
u, err = r.ReadUEGolomb() // chroma_sample_loc_type_bottom_field
}
b, err = r.ReadBit() // timing_info_present_flag
if b > 0 {
u, err = r.ReadBits(32) // num_units_in_tick
u, err = r.ReadBits(32) // time_scale
b, err = r.ReadBit() // fixed_frame_rate_flag
}
b, err = r.ReadBit() // nal_hrd_parameters_present_flag
if b > 0 {
//panic("not implemented")
return nil
}
b, err = r.ReadBit() // vcl_hrd_parameters_present_flag
if b > 0 {
//panic("not implemented")
return nil
}
// if (nal_hrd_parameters_present_flag || vcl_hrd_parameters_present_flag)
// b, err = r.ReadBit() // low_delay_hrd_flag
b, err = r.ReadBit() // pic_struct_present_flag
b, err = r.ReadBit() // bitstream_restriction_flag
if b > 0 {
b, err = r.ReadBit() // motion_vectors_over_pic_boundaries_flag
u, err = r.ReadUEGolomb() // max_bytes_per_pic_denom
u, err = r.ReadUEGolomb() // max_bits_per_mb_denom
u, err = r.ReadUEGolomb() // log2_max_mv_length_horizontal
u, err = r.ReadUEGolomb() // log2_max_mv_length_vertical
u, err = r.ReadUEGolomb() // max_num_reorder_frames
u, err = r.ReadUEGolomb() // max_dec_frame_buffering
}
}
b, err = r.ReadBit() // rbsp_stop_one_bit
return
}
func EncodeProfile(idc, iop byte) string {
// https://datatracker.ietf.org/doc/html/rfc6184#page-41
switch {
// 4240xx 42C0xx 42E0xx
case idc == 0x42 && iop&0b01001111 == 0b01000000:
return "CB"
case idc == 0x4D && iop&0b10001111 == 0b10000000:
return "CB"
case idc == 0x58 && iop&0b11001111 == 0b11000000:
return "CB"
// 4200xx
case idc == 0x42 && iop&0b01001111 == 0:
return "B"
case idc == 0x58 && iop&0b11001111 == 0b10000000:
return "B"
// 4d40xx
case idc == 0x4D && iop&0b10101111 == 0:
return "M"
case idc == 0x58 && iop&0b11001111 == 0:
return "E"
case idc == 0x64 && iop == 0:
return "H"
case idc == 0x6E && iop == 0:
return "H10"
}
return ""
}
func DecodeProfile(profile string) (idc, iop byte) {
switch profile {
case "CB":
return 0x42, 0b01000000
case "B":
return 0x42, 0 // 66
case "M":
return 0x4D, 0 // 77
case "E":
return 0x58, 0 // 88
case "H":
return 0x64, 0
}
return 0, 0
}

View File

@@ -0,0 +1,56 @@
package ps
import (
"bytes"
"testing"
)
func TestUnmarshalSPS(t *testing.T) {
raw := []byte{0x67, 0x42, 0x00, 0x0a, 0xf8, 0x41, 0xa2}
s := SPS{}
if err := s.Unmarshal(raw); err != nil {
t.Fatal(err)
}
raw2 := s.Marshal()
if bytes.Compare(raw, raw2) != 0 {
t.Fatal()
}
}
func TestUnmarshalPPS(t *testing.T) {
raw := []byte{0x68, 0xce, 0x38, 0x80}
p := PPS{}
if err := p.Unmarshal(raw); err != nil {
t.Fatal(err)
}
raw2 := p.Marshal()
if bytes.Compare(raw, raw2) != 0 {
t.Fatal()
}
}
func TestUnmarshalPPS2(t *testing.T) {
raw := []byte{72, 238, 60, 128}
p := PPS{}
if err := p.Unmarshal(raw); err != nil {
t.Fatal(err)
}
raw2 := p.Marshal()
if bytes.Compare(raw, raw2) != 0 {
t.Fatal()
}
}
func TestSafari(t *testing.T) {
// CB66, L3.1: chrome, edge, safari, android chrome
s := EncodeProfile(0x42, 0xE0)
t.Logf("Profile: %s, Level: %d", s, 0x1F)
// B66, L3.1: chrome, edge
s = EncodeProfile(0x42, 0x00)
t.Logf("Profile: %s, Level: %d", s, 0x1F)
// M77, L3.1: chrome, edge
s = EncodeProfile(0x4D, 0x00)
t.Logf("Profile: %s, Level: %d", s, 0x1F)
}

113
pkg/h264/rtp.go Normal file
View File

@@ -0,0 +1,113 @@
package h264
import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
"github.com/pion/rtp/codecs"
)
const RTPPacketVersionAVC = 0
func RTPDepay(track *streamer.Track) streamer.WrapperFunc {
depack := &codecs.H264Packet{IsAVC: true}
sps, pps := GetParameterSet(track.Codec.FmtpLine)
sps = EncodeAVC(sps)
pps = EncodeAVC(pps)
var buffer []byte
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error {
//println(packet.SequenceNumber, packet.Payload[0]&0x1F, packet.Payload[0], packet.Payload[1], packet.Marker, packet.Timestamp)
data, err := depack.Unmarshal(packet.Payload)
if len(data) == 0 || err != nil {
return nil
}
naluType := NALUType(data)
//println(naluType, len(data))
switch naluType {
case NALUTypeSPS:
//println("new SPS")
sps = data
return nil
case NALUTypePPS:
//println("new PPS")
pps = data
return nil
}
// ffmpeg with `-tune zerolatency` enable option `-x264opts sliced-threads=1`
// and every NALU will be sliced to multiple NALUs
if !packet.Marker {
buffer = append(buffer, data...)
return nil
}
if buffer != nil {
buffer = append(buffer, data...)
data = buffer
buffer = nil
}
var clone rtp.Packet
if naluType == NALUTypeIFrame {
clone = *packet
clone.Version = RTPPacketVersionAVC
clone.Payload = sps
if err = push(&clone); err != nil {
return err
}
clone = *packet
clone.Version = RTPPacketVersionAVC
clone.Payload = pps
if err = push(&clone); err != nil {
return err
}
}
clone = *packet
clone.Version = RTPPacketVersionAVC
clone.Payload = data
return push(&clone)
}
}
}
func RTPPay(mtu uint16) streamer.WrapperFunc {
payloader := &Payloader{IsAVC: true}
sequencer := rtp.NewRandomSequencer()
mtu -= 12 // rtp.Header size
return func(push streamer.WriterFunc) streamer.WriterFunc {
return func(packet *rtp.Packet) error {
if packet.Version == RTPPacketVersionAVC {
payloads := payloader.Payload(mtu, packet.Payload)
for i, payload := range payloads {
clone := rtp.Packet{
Header: rtp.Header{
Version: 2,
Marker: i == len(payloads)-1,
//PayloadType: packet.PayloadType,
SequenceNumber: sequencer.NextSequenceNumber(),
Timestamp: packet.Timestamp,
//SSRC: packet.SSRC,
},
Payload: payload,
}
if err := push(&clone); err != nil {
return err
}
}
return nil
}
return push(packet)
}
}
}

131
pkg/mse/consumer.go Normal file
View File

@@ -0,0 +1,131 @@
package mse
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/deepch/vdk/av"
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/format/mp4f"
"github.com/pion/rtp"
"time"
)
const MsgTypeMSE = "mse"
type Consumer struct {
streamer.Element
UserAgent string
RemoteAddr string
muxer *mp4f.Muxer
streams []av.CodecData
start bool
send int
}
func (c *Consumer) GetMedias() []*streamer.Media {
return []*streamer.Media{
{
Kind: streamer.KindVideo,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecH264, ClockRate: 90000},
},
}, {
Kind: streamer.KindAudio,
Direction: streamer.DirectionRecvonly,
Codecs: []*streamer.Codec{
{Name: streamer.CodecAAC, ClockRate: 16000},
},
},
}
}
func (c *Consumer) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
codec := track.Codec
switch codec.Name {
case streamer.CodecH264:
idx := int8(len(c.streams))
sps, pps := h264.GetParameterSet(codec.FmtpLine)
stream, err := h264parser.NewCodecDataFromSPSAndPPS(sps, pps)
if err != nil {
return nil
}
c.streams = append(c.streams, stream)
pkt := av.Packet{Idx: idx, CompositionTime: time.Millisecond}
ts2time := time.Second / time.Duration(codec.ClockRate)
push := func(packet *rtp.Packet) error {
if packet.Version != h264.RTPPacketVersionAVC {
return nil
}
switch h264.NALUType(packet.Payload) {
case h264.NALUTypeIFrame:
c.start = true
pkt.IsKeyFrame = true
case h264.NALUTypePFrame:
if !c.start {
return nil
}
default:
return nil
}
pkt.Data = packet.Payload
newTime := time.Duration(packet.Timestamp) * ts2time
if pkt.Time > 0 {
pkt.Duration = newTime - pkt.Time
}
pkt.Time = newTime
for _, buf := range c.muxer.WritePacketV5(pkt) {
c.send += len(buf)
c.Fire(buf)
}
return nil
}
if !h264.IsAVC(codec) {
wrapper := h264.RTPDepay(track)
push = wrapper(push)
}
return track.Bind(push)
}
panic("unsupported codec")
}
func (c *Consumer) Init() {
c.muxer = mp4f.NewMuxer(nil)
if err := c.muxer.WriteHeader(c.streams); err != nil {
return
}
codecs, buf := c.muxer.GetInit(c.streams)
c.Fire(&streamer.Message{Type: MsgTypeMSE, Value: codecs})
c.send += len(buf)
c.Fire(buf)
}
//
func (c *Consumer) MarshalJSON() ([]byte, error) {
v := map[string]interface{}{
"type": "MSE server consumer",
"send": c.send,
"remote_addr": c.RemoteAddr,
"user_agent": c.UserAgent,
}
return json.Marshal(v)
}

79
pkg/ngrok/ngrok.go Normal file
View File

@@ -0,0 +1,79 @@
package ngrok
import (
"bufio"
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"io"
"os/exec"
"strings"
)
type Ngrok struct {
streamer.Element
Tunnels map[string]string
reader *bufio.Reader
}
type Message struct {
Msg string `json:"msg"`
Addr string `json:"addr"`
URL string `json:"url"`
Line string
}
func NewNgrok(command interface{}) (*Ngrok, error) {
var arg []string
switch command.(type) {
case string:
arg = strings.Split(command.(string), " ")
case []string:
arg = command.([]string)
}
arg = append(arg, "--log", "stdout", "--log-format", "json")
cmd := exec.Command(arg[0], arg[1:]...)
r, err := cmd.StdoutPipe()
if err != nil {
return nil, err
}
cmd.Stderr = cmd.Stdout
n := &Ngrok{
Tunnels: map[string]string{},
reader: bufio.NewReader(r),
}
if err = cmd.Start(); err != nil {
return nil, err
}
return n, nil
}
func (n *Ngrok) Serve() error {
for {
line, _, err := n.reader.ReadLine()
if err != nil {
if err != io.EOF {
return err
}
return nil
}
msg := new(Message)
_ = json.Unmarshal(line, msg)
if msg.Msg == "started tunnel" {
n.Tunnels[msg.Addr] = msg.URL
}
msg.Line = string(line)
n.Fire(msg)
}
}

142
pkg/rtmp/client.go Normal file
View File

@@ -0,0 +1,142 @@
package rtmp
import (
"encoding/base64"
"encoding/binary"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/deepch/vdk/av"
"github.com/deepch/vdk/codec/h264parser"
"github.com/deepch/vdk/format/rtmp"
"github.com/pion/rtp"
"time"
)
type Client struct {
streamer.Element
URI string
medias []*streamer.Media
tracks []*streamer.Track
conn *rtmp.Conn
closed bool
}
func NewClient(uri string) *Client {
return &Client{URI: uri}
}
func (c *Client) Dial() (err error) {
c.conn, err = rtmp.Dial(c.URI)
if err != nil {
return
}
// important to get SPS/PPS
streams, err := c.conn.Streams()
if err != nil {
return
}
for _, stream := range streams {
switch stream.Type() {
case av.H264:
cd := stream.(h264parser.CodecData)
fmtp := "sprop-parameter-sets=" +
base64.StdEncoding.EncodeToString(cd.RecordInfo.SPS[0]) + "," +
base64.StdEncoding.EncodeToString(cd.RecordInfo.PPS[0])
codec := &streamer.Codec{
Name: streamer.CodecH264,
ClockRate: 90000,
FmtpLine: fmtp,
PayloadType: h264.PayloadTypeAVC,
}
media := &streamer.Media{
Kind: streamer.KindVideo,
Direction: streamer.DirectionSendonly,
Codecs: []*streamer.Codec{codec},
}
c.medias = append(c.medias, media)
track := &streamer.Track{
Codec: codec, Direction: media.Direction,
}
c.tracks = append(c.tracks, track)
case av.AAC:
panic("not implemented")
default:
panic("unsupported codec")
}
}
c.Fire(streamer.StateReady)
return
}
func (c *Client) Handle() (err error) {
defer c.Fire(streamer.StateNull)
c.Fire(streamer.StatePlaying)
for {
var pkt av.Packet
pkt, err = c.conn.ReadPacket()
if err != nil {
if c.closed {
return nil
}
return
}
track := c.tracks[int(pkt.Idx)]
timestamp := uint32(pkt.Time / time.Duration(track.Codec.ClockRate))
var payloads [][]byte
if track.Codec.Name == streamer.CodecH264 {
payloads = splitAVC(pkt.Data)
} else {
payloads = [][]byte{pkt.Data}
}
for _, payload := range payloads {
packet := &rtp.Packet{
Header: rtp.Header{Timestamp: timestamp},
Payload: payload,
}
_ = track.WriteRTP(packet)
}
}
}
func (c *Client) Close() error {
if c.conn == nil {
return nil
}
c.closed = true
return c.conn.Close()
}
func splitAVC(data []byte) [][]byte {
var nals [][]byte
for {
// get AVC length
size := int(binary.BigEndian.Uint32(data))
// check if multiple items in one packet
if size+4 < len(data) {
nals = append(nals, data[:size+4])
data = data[size+4:]
} else {
nals = append(nals, data)
break
}
}
return nals
}

26
pkg/rtmp/streamer.go Normal file
View File

@@ -0,0 +1,26 @@
package rtmp
import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
)
func (c *Client) GetMedias() []*streamer.Media {
return c.medias
}
func (c *Client) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
for _, track := range c.tracks {
if track.Codec == codec {
return track
}
}
panic("wrong codec")
}
func (c *Client) Start() error {
return c.Handle()
}
func (c *Client) Stop() error {
return c.Close()
}

3
pkg/rtsp/README.md Normal file
View File

@@ -0,0 +1,3 @@
## Useful links
- https://www.kurento.org/blog/rtp-i-intro-rtp-and-sdp

696
pkg/rtsp/conn.go Normal file
View File

@@ -0,0 +1,696 @@
package rtsp
import (
"bufio"
"bytes"
"crypto/tls"
"encoding/binary"
"errors"
"fmt"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/AlexxIT/go2rtc/pkg/tcp"
"github.com/pion/rtcp"
"github.com/pion/rtp"
"io"
"net"
"net/http"
"net/url"
"strconv"
"strings"
"time"
)
const (
ProtoRTSP = "RTSP/1.0"
MethodOptions = "OPTIONS"
MethodSetup = "SETUP"
MethodTeardown = "TEARDOWN"
MethodDescribe = "DESCRIBE"
MethodPlay = "PLAY"
MethodPause = "PAUSE"
MethodAnnounce = "ANNOUNCE"
MethodRecord = "RECORD"
)
type Mode byte
const (
ModeUnknown Mode = iota
ModeClientProducer
ModeServerUnknown
ModeServerProducer
ModeServerConsumer
)
type Conn struct {
streamer.Element
// public
Medias []*streamer.Media
Session string
UserAgent string
URL *url.URL
// internal
auth *tcp.Auth
conn net.Conn
reader *bufio.Reader
sequence int
mode Mode
tracks []*streamer.Track
channels map[byte]*streamer.Track
// stats
receive int
send int
}
func NewClient(uri string) (*Conn, error) {
var err error
c := new(Conn)
c.URL, err = url.Parse(uri)
if err != nil {
return nil, err
}
if strings.IndexByte(c.URL.Host, ':') < 0 {
c.URL.Host += ":554"
}
// remove UserInfo from URL
c.auth = tcp.NewAuth(c.URL.User)
c.mode = ModeClientProducer
c.URL.User = nil
return c, nil
}
func NewServer(conn net.Conn) *Conn {
c := new(Conn)
c.conn = conn
c.mode = ModeServerUnknown
c.reader = bufio.NewReader(conn)
return c
}
func (c *Conn) Dial() (err error) {
//if c.state != StateClientInit {
// panic("wrong state")
//}
c.conn, err = net.DialTimeout(
"tcp", c.URL.Host, 10*time.Second,
)
if err != nil {
return
}
var tlsConf *tls.Config
switch c.URL.Scheme {
case "rtsps":
tlsConf = &tls.Config{ServerName: c.URL.Hostname()}
case "rtspx":
c.URL.Scheme = "rtsps"
tlsConf = &tls.Config{InsecureSkipVerify: true}
}
if tlsConf != nil {
tlsConn := tls.Client(c.conn, tlsConf)
if err = tlsConn.Handshake(); err != nil {
return err
}
c.conn = tlsConn
}
c.reader = bufio.NewReader(c.conn)
return nil
}
// Request sends only Request
func (c *Conn) Request(req *tcp.Request) error {
if req.Proto == "" {
req.Proto = ProtoRTSP
}
if req.Header == nil {
req.Header = make(map[string][]string)
}
c.sequence++
req.Header.Set("CSeq", strconv.Itoa(c.sequence))
c.auth.Write(req)
if c.Session != "" {
req.Header.Set("Session", c.Session)
}
if req.Body != nil {
val := strconv.Itoa(len(req.Body))
req.Header.Set("Content-Length", val)
}
c.Fire(req)
return req.Write(c.conn)
}
// Do send Request and receive and process Response
func (c *Conn) Do(req *tcp.Request) (*tcp.Response, error) {
if err := c.Request(req); err != nil {
return nil, err
}
res, err := tcp.ReadResponse(c.reader)
if err != nil {
return nil, err
}
c.Fire(res)
if res.StatusCode == http.StatusUnauthorized {
switch c.auth.Method {
case tcp.AuthNone:
return nil, errors.New("user/pass not provided")
case tcp.AuthUnknown:
if c.auth.Read(res) {
return c.Do(req)
}
case tcp.AuthBasic, tcp.AuthDigest:
return nil, errors.New("wrong user/pass")
}
}
if res.StatusCode != http.StatusOK {
return nil, fmt.Errorf("wrong response on %s", req.Method)
}
return res, nil
}
func (c *Conn) Response(res *tcp.Response) error {
if res.Proto == "" {
res.Proto = ProtoRTSP
}
if res.Status == "" {
res.Status = "200 OK"
}
if res.Header == nil {
res.Header = make(map[string][]string)
}
if res.Request != nil && res.Request.Header != nil {
seq := res.Request.Header.Get("CSeq")
if seq != "" {
res.Header.Set("CSeq", seq)
}
}
if c.Session != "" {
res.Header.Set("Session", c.Session)
}
if res.Body != nil {
val := strconv.Itoa(len(res.Body))
res.Header.Set("Content-Length", val)
}
c.Fire(res)
return res.Write(c.conn)
}
func (c *Conn) Options() error {
req := &tcp.Request{Method: MethodOptions, URL: c.URL}
res, err := c.Do(req)
if err != nil {
return err
}
if val := res.Header.Get("Content-Base"); val != "" {
c.URL, err = url.Parse(val)
if err != nil {
return err
}
}
return nil
}
func (c *Conn) Describe() error {
// 5.3 Back channel connection
// https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec.pdf
req := &tcp.Request{
Method: MethodDescribe,
URL: c.URL,
Header: map[string][]string{
"Accept": {"application/sdp"},
"Require": {"www.onvif.org/ver20/backchannel"},
},
}
res, err := c.Do(req)
if err != nil {
return err
}
// fix bug in Sonoff camera SDP "o=- 1 1 IN IP4 rom t_rtsplin"
// TODO: make some universal fix
if i := bytes.Index(res.Body, []byte("rom t_rtsplin")); i > 0 {
res.Body[i+3] = '_'
}
c.Medias, err = streamer.UnmarshalRTSPSDP(res.Body)
if err != nil {
return err
}
c.mode = ModeClientProducer
return nil
}
//func (c *Conn) Announce() (err error) {
// req := &tcp.Request{
// Method: MethodAnnounce,
// URL: c.URL,
// Header: map[string][]string{
// "Content-Type": {"application/sdp"},
// },
// }
//
// //req.Body, err = c.sdp.Marshal()
// if err != nil {
// return
// }
//
// _, err = c.Do(req)
//
// return
//}
func (c *Conn) Setup() error {
for _, media := range c.Medias {
_, err := c.SetupMedia(media, media.Codecs[0])
if err != nil {
return err
}
}
return nil
}
func (c *Conn) SetupMedia(
media *streamer.Media, codec *streamer.Codec,
) (*streamer.Track, error) {
ch := c.GetChannel(media)
if ch < 0 {
return nil, fmt.Errorf("wrong media: %v", media)
}
trackURL, err := url.Parse(media.Control)
if err != nil {
return nil, err
}
trackURL = c.URL.ResolveReference(trackURL)
req := &tcp.Request{
Method: MethodSetup,
URL: trackURL,
Header: map[string][]string{
"Transport": {fmt.Sprintf(
// i - RTP (data channel)
// i+1 - RTCP (control channel)
"RTP/AVP/TCP;unicast;interleaved=%d-%d", ch*2, ch*2+1,
)},
},
}
var res *tcp.Response
res, err = c.Do(req)
if err != nil {
return nil, err
}
if c.Session == "" {
// Session: 216525287999;timeout=60
if s := res.Header.Get("Session"); s != "" {
if j := strings.IndexByte(s, ';'); j > 0 {
s = s[:j]
}
c.Session = s
}
}
// we send our `interleaved`, but camera can answer with another
// Transport: RTP/AVP/TCP;unicast;interleaved=10-11;ssrc=10117CB7
s := res.Header.Get("Transport")
s, ok1, ok2 := between(s, "RTP/AVP/TCP;unicast;interleaved=", "-")
if !ok1 || !ok2 {
panic("wrong response")
}
ch, err = strconv.Atoi(s)
if err != nil {
return nil, err
}
track := &streamer.Track{
Codec: codec, Direction: media.Direction,
}
switch track.Direction {
case streamer.DirectionSendonly:
if c.channels == nil {
c.channels = make(map[byte]*streamer.Track)
}
c.channels[byte(ch)] = track
case streamer.DirectionRecvonly:
track = c.bindTrack(track, byte(ch), codec.PayloadType)
}
c.tracks = append(c.tracks, track)
return track, nil
}
func (c *Conn) Play() (err error) {
req := &tcp.Request{Method: MethodPlay, URL: c.URL}
return c.Request(req)
}
func (c *Conn) Teardown() (err error) {
//if c.state != StateClientPlay {
// panic("wrong state")
//}
req := &tcp.Request{Method: MethodTeardown, URL: c.URL}
return c.Request(req)
}
func (c *Conn) Close() error {
if c.conn == nil {
return nil
}
if err := c.Teardown(); err != nil {
return err
}
conn := c.conn
c.conn = nil
return conn.Close()
}
const transport = "RTP/AVP/TCP;unicast;interleaved="
func (c *Conn) Accept() error {
//if c.state != StateServerInit {
// panic("wrong state")
//}
for {
req, err := tcp.ReadRequest(c.reader)
if err != nil {
return err
}
c.Fire(req)
// Receiver: OPTIONS > DESCRIBE > SETUP... > PLAY > TEARDOWN
// Sender: OPTIONS > ANNOUNCE > SETUP... > RECORD > TEARDOWN
switch req.Method {
case MethodOptions:
c.URL = req.URL
c.UserAgent = req.Header.Get("User-Agent")
res := &tcp.Response{
Header: map[string][]string{
"Public": {"OPTIONS, SETUP, TEARDOWN, DESCRIBE, PLAY, PAUSE, ANNOUNCE, RECORD"},
},
Request: req,
}
if err = c.Response(res); err != nil {
return err
}
case MethodAnnounce:
if req.Header.Get("Content-Type") != "application/sdp" {
return errors.New("wrong content type")
}
c.Medias, err = streamer.UnmarshalRTSPSDP(req.Body)
if err != nil {
return err
}
// TODO: fix someday...
c.channels = map[byte]*streamer.Track{}
for i, media := range c.Medias {
track := &streamer.Track{
Codec: media.Codecs[0], Direction: media.Direction,
}
c.tracks = append(c.tracks, track)
c.channels[byte(i<<1)] = track
}
c.mode = ModeServerProducer
c.Fire(MethodAnnounce)
res := &tcp.Response{Request: req}
if err = c.Response(res); err != nil {
return err
}
case MethodDescribe:
c.mode = ModeServerConsumer
c.Fire(MethodDescribe)
if c.tracks == nil {
res := &tcp.Response{
Status: "404 Not Found",
Request: req,
}
return c.Response(res)
}
res := &tcp.Response{
Header: map[string][]string{
"Content-Type": {"application/sdp"},
},
Request: req,
}
// convert tracks to real output medias medias
var medias []*streamer.Media
for _, track := range c.tracks {
media := &streamer.Media{
Kind: streamer.GetKind(track.Codec.Name),
Direction: streamer.DirectionSendonly,
Codecs: []*streamer.Codec{track.Codec},
}
medias = append(medias, media)
}
res.Body, err = streamer.MarshalSDP(medias)
if err != nil {
return err
}
if err = c.Response(res); err != nil {
return err
}
case MethodSetup:
tr := req.Header.Get("Transport")
res := &tcp.Response{
Header: map[string][]string{},
Request: req,
}
if tr[:len(transport)] == transport {
c.Session = "1" // TODO: fixme
res.Header.Set("Transport", tr[:len(transport)+3])
} else {
res.Status = "461 Unsupported transport"
}
if err = c.Response(res); err != nil {
return err
}
case MethodRecord, MethodPlay:
res := &tcp.Response{Request: req}
return c.Response(res)
default:
return fmt.Errorf("unsupported method: %s", req.Method)
}
}
}
func (c *Conn) Handle() (err error) {
defer func() {
if c.conn == nil {
err = nil
}
//c.Fire(streamer.StateNull)
}()
//c.Fire(streamer.StatePlaying)
for {
// we can read:
// 1. RTP interleaved: `$` + 1B channel number + 2B size
// 2. RTSP response: RTSP/1.0 200 OK
// 3. RTSP request: OPTIONS ...
var buf4 []byte // `$` + 1B channel number + 2B size
buf4, err = c.reader.Peek(4)
if err != nil {
return
}
if buf4[0] != '$' {
if string(buf4) == "RTSP" {
var res *tcp.Response
res, err = tcp.ReadResponse(c.reader)
if err != nil {
return
}
c.Fire(res)
} else {
var req *tcp.Request
req, err = tcp.ReadRequest(c.reader)
if err != nil {
return
}
c.Fire(req)
}
continue
}
// hope that the odd channels are always RTCP
channelID := buf4[1]
// get data size
size := int(binary.BigEndian.Uint16(buf4[2:]))
if _, err = c.reader.Discard(4); err != nil {
return
}
// init memory for data
buf := make([]byte, size)
if _, err = io.ReadFull(c.reader, buf); err != nil {
return
}
c.receive += size
if channelID&1 == 0 {
packet := &rtp.Packet{}
if err = packet.Unmarshal(buf); err != nil {
return errors.New("wrong RTP data")
}
track := c.channels[channelID]
if track != nil {
_ = track.WriteRTP(packet)
//return fmt.Errorf("wrong channelID: %d", channelID)
} else {
panic("wrong channelID")
}
} else {
msg := &RTCP{Channel: channelID}
if err = msg.Header.Unmarshal(buf); err != nil {
return errors.New("wrong RTCP data")
}
msg.Packets, err = rtcp.Unmarshal(buf)
if err != nil {
return errors.New("wrong RTCP data")
}
c.Fire(msg)
}
}
}
func (c *Conn) GetChannel(media *streamer.Media) int {
for i, m := range c.Medias {
if m == media {
return i
}
}
return -1
}
func (c *Conn) bindTrack(
track *streamer.Track, channel uint8, payloadType uint8,
) *streamer.Track {
push := func(packet *rtp.Packet) error {
if c.conn == nil {
return nil
}
packet.Header.PayloadType = payloadType
//packet.Header.PayloadType = 100
//packet.Header.PayloadType = 8
//packet.Header.PayloadType = 106
size := packet.MarshalSize()
data := make([]byte, 4+size)
data[0] = '$'
data[1] = channel
//data[1] = 10
binary.BigEndian.PutUint16(data[2:], uint16(size))
if _, err := packet.MarshalTo(data[4:]); err != nil {
return nil
}
if _, err := c.conn.Write(data); err != nil {
return err
}
c.send += size
return nil
}
return track.Bind(push)
}
type RTCP struct {
Channel byte
Header rtcp.Header
Packets []rtcp.Packet
}
func between(s, sub1, sub2 string) (res string, ok1 bool, ok2 bool) {
i := strings.Index(s, sub1)
if i >= 0 {
ok1 = true
s = s[i+len(sub1):]
}
i = strings.Index(s, sub2)
if i >= 0 {
return s[:i], ok1, true
}
return s, ok1, false
}

123
pkg/rtsp/streamer.go Normal file
View File

@@ -0,0 +1,123 @@
package rtsp
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"strconv"
)
// Element Producer
func (c *Conn) GetMedias() []*streamer.Media {
return c.Medias
}
func (c *Conn) GetTrack(media *streamer.Media, codec *streamer.Codec) *streamer.Track {
for _, track := range c.tracks {
if track.Codec == codec {
return track
}
}
track, err := c.SetupMedia(media, codec)
if err != nil {
return nil
}
return track
}
func (c *Conn) Start() error {
if c.mode == ModeServerProducer {
return nil
}
if err := c.Play(); err != nil {
return err
}
return c.Handle()
}
func (c *Conn) Stop() error {
return c.Close()
}
// Consumer
func (c *Conn) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
switch track.Direction {
// send our track to RTSP consumer (ex. FFmpeg)
case streamer.DirectionSendonly:
i := len(c.tracks)
channelID := byte(i << 1)
codec := track.Codec.Clone()
codec.PayloadType = uint8(96 + i)
for i, m := range c.Medias {
if m == media {
media.Codecs = []*streamer.Codec{codec}
c.Medias[i] = media
break
}
}
track = c.bindTrack(track, channelID, codec.PayloadType)
track.Codec = codec
c.tracks = append(c.tracks, track)
return track
case streamer.DirectionRecvonly:
panic("not implemented")
}
panic("wrong direction")
}
//
func (c *Conn) MarshalJSON() ([]byte, error) {
v := map[string]interface{}{
streamer.JSONReceive: c.receive,
streamer.JSONSend: c.send,
}
switch c.mode {
case ModeUnknown:
v[streamer.JSONType] = "RTSP unknown"
case ModeClientProducer:
v[streamer.JSONType] = "RTSP client producer"
case ModeServerProducer:
v[streamer.JSONType] = "RTSP server producer"
case ModeServerConsumer:
v[streamer.JSONType] = "RTSP server consumer"
}
//if c.URI != "" {
// v["uri"] = c.URI
//}
if c.URL != nil {
v["url"] = c.URL.String()
}
if c.conn != nil {
v[streamer.JSONRemoteAddr] = c.conn.RemoteAddr().String()
}
if c.UserAgent != "" {
v[streamer.JSONUserAgent] = c.UserAgent
}
for i, media := range c.Medias {
k := "media:" + strconv.Itoa(i)
v[k] = media.String()
}
for i, track := range c.tracks {
k := "track:" + strconv.Itoa(int(i>>1))
v[k] = track.String()
}
//for i, track := range c.tracks {
// k := "track:" + strconv.Itoa(i+1)
// if track.MimeType() == streamer.MimeTypeH264 {
// v[k] = h264.Describe(track.Caps())
// } else {
// v[k] = track.MimeType()
// }
//}
return json.Marshal(v)
}

57
pkg/streamer/helpers.go Normal file
View File

@@ -0,0 +1,57 @@
package streamer
import (
"strings"
)
const (
JSONType = "type"
JSONRemoteAddr = "remote_addr"
JSONUserAgent = "user_agent"
JSONReceive = "receive"
JSONSend = "send"
)
// Message - struct for data exchange in Web API
type Message struct {
Type string `json:"type"`
Value interface{} `json:"value,omitempty"`
}
// other
func Between(s, sub1, sub2 string) string {
i := strings.Index(s, sub1)
if i < 0 {
return ""
}
s = s[i+len(sub1):]
if len(sub2) == 1 {
i = strings.IndexByte(s, sub2[0])
} else {
i = strings.Index(s, sub2)
}
if i >= 0 {
return s[:i]
}
return s
}
func Contains(medias []*Media, media *Media, codec *Codec) bool {
var ok1, ok2 bool
for _, m := range medias {
if m == media {
ok1 = true
break
}
}
for _, c := range media.Codecs {
if c == codec {
ok2 = true
break
}
}
return ok1 && ok2
}

294
pkg/streamer/media.go Normal file
View File

@@ -0,0 +1,294 @@
package streamer
import (
"fmt"
"github.com/pion/sdp/v3"
"strconv"
"strings"
)
const (
DirectionRecvonly = "recvonly"
DirectionSendonly = "sendonly"
DirectionSendRecv = "sendrecv"
)
const (
KindVideo = "video"
KindAudio = "audio"
)
const (
CodecH264 = "H264" // payloadType: 96
CodecH265 = "H265"
CodecVP8 = "VP8"
CodecVP9 = "VP9"
CodecAV1 = "AV1"
CodecPCMU = "PCMU" // payloadType: 0
CodecPCMA = "PCMA" // payloadType: 8
CodecAAC = "MPEG4-GENERIC"
CodecOpus = "OPUS" // payloadType: 111
CodecG722 = "G722"
)
func GetKind(name string) string {
switch name {
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1:
return KindVideo
case CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722:
return KindAudio
}
return ""
}
// Media take best from:
// - deepch/vdk/format/rtsp/sdp.Media
// - pion/sdp.MediaDescription
type Media struct {
Kind string // video, audio
Direction string
Codecs []*Codec
MID string // TODO: fixme?
Control string // TODO: fixme?
}
func (m *Media) String() string {
s := fmt.Sprintf("%s, %s", m.Kind, m.Direction)
for _, codec := range m.Codecs {
s += ", " + codec.String()
}
return s
}
func (m *Media) Clone() *Media {
clone := *m
return &clone
}
func (m *Media) AV() bool {
return m.Kind == KindVideo || m.Kind == KindAudio
}
func (m *Media) MatchCodec(codec *Codec) bool {
for _, c := range m.Codecs {
if c.Match(codec) {
return true
}
}
return false
}
func (m *Media) MatchMedia(media *Media) *Codec {
if m.Kind != media.Kind {
return nil
}
switch m.Direction {
case DirectionSendonly:
if media.Direction != DirectionRecvonly {
return nil
}
case DirectionRecvonly:
if media.Direction != DirectionSendonly {
return nil
}
default:
panic("wrong direction")
}
for _, localCodec := range m.Codecs {
if media.Codecs == nil {
return localCodec
}
for _, remoteCodec := range media.Codecs {
if localCodec.Match(remoteCodec) {
return localCodec
}
}
}
return nil
}
// Codec take best from:
// - deepch/vdk/av.CodecData
// - pion/webrtc.RTPCodecCapability
type Codec struct {
Name string // H264, PCMU, PCMA, opus...
ClockRate uint32 // 90000, 8000, 16000...
Channels uint16 // 0, 1, 2
FmtpLine string
PayloadType uint8
}
func NewCodec(name string) *Codec {
name = strings.ToUpper(name)
switch name {
case CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1:
return &Codec{Name: name, ClockRate: 90000}
case CodecPCMU, CodecPCMA:
return &Codec{Name: name, ClockRate: 8000}
case CodecOpus:
return &Codec{Name: name, ClockRate: 48000, Channels: 2}
}
panic(fmt.Sprintf("unsupported codec: %s", name))
}
func (c *Codec) String() string {
s := fmt.Sprintf("%d %s/%d", c.PayloadType, c.Name, c.ClockRate)
if c.Channels > 0 {
s = fmt.Sprintf("%s/%d", s, c.Channels)
}
return s
}
func (c *Codec) Clone() *Codec {
clone := *c
return &clone
}
func (c *Codec) Match(codec *Codec) bool {
return c.Name == codec.Name &&
c.ClockRate == codec.ClockRate &&
c.Channels == codec.Channels
}
func UnmarshalSDP(rawSDP []byte) ([]*Media, error) {
sd := &sdp.SessionDescription{}
if err := sd.Unmarshal(rawSDP); err != nil {
return nil, err
}
var medias []*Media
for _, md := range sd.MediaDescriptions {
media := UnmarshalMedia(md)
if media.Direction == DirectionSendRecv {
media.Direction = DirectionRecvonly
medias = append(medias, media)
media = media.Clone()
media.Direction = DirectionSendonly
}
medias = append(medias, media)
}
return medias, nil
}
func UnmarshalRTSPSDP(rawSDP []byte) ([]*Media, error) {
medias, err := UnmarshalSDP(rawSDP)
if err != nil {
return nil, err
}
// fix bug in ONVIF spec
// https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec-v241.pdf
for _, media := range medias {
switch media.Direction {
case DirectionRecvonly, "":
media.Direction = DirectionSendonly
case DirectionSendonly:
media.Direction = DirectionRecvonly
}
}
return medias, nil
}
func MarshalSDP(medias []*Media) ([]byte, error) {
sd := &sdp.SessionDescription{}
payloadType := uint8(96)
for _, media := range medias {
if media.Codecs == nil {
continue
}
codec := media.Codecs[0]
md := &sdp.MediaDescription{
MediaName: sdp.MediaName{
Media: media.Kind,
Protos: []string{"RTP", "AVP"},
},
}
md.WithCodec(payloadType, codec.Name, codec.ClockRate, codec.Channels, codec.FmtpLine)
sd.MediaDescriptions = append(sd.MediaDescriptions, md)
payloadType++
}
return sd.Marshal()
}
func UnmarshalMedia(md *sdp.MediaDescription) *Media {
m := &Media{
Kind: md.MediaName.Media,
}
for _, attr := range md.Attributes {
switch attr.Key {
case DirectionSendonly, DirectionRecvonly, DirectionSendRecv:
m.Direction = attr.Key
case "control":
m.Control = attr.Value
case "mid":
m.MID = attr.Value
}
}
for _, format := range md.MediaName.Formats {
m.Codecs = append(m.Codecs, UnmarshalCodec(md, format))
}
return m
}
func UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {
c := &Codec{PayloadType: byte(atoi(payloadType))}
for _, attr := range md.Attributes {
switch {
case c.Name == "" && attr.Key == "rtpmap" && strings.HasPrefix(attr.Value, payloadType):
i := strings.IndexByte(attr.Value, ' ')
ss := strings.Split(attr.Value[i+1:], "/")
c.Name = strings.ToUpper(ss[0])
c.ClockRate = uint32(atoi(ss[1]))
if len(ss) == 3 && ss[2] == "2" {
c.Channels = 2
}
case c.FmtpLine == "" && attr.Key == "fmtp" && strings.HasPrefix(attr.Value, payloadType):
if i := strings.IndexByte(attr.Value, ' '); i > 0 {
c.FmtpLine = attr.Value[i+1:]
}
}
}
if c.Name == "" {
switch payloadType {
case "0":
c.Name = "PCMU"
c.ClockRate = 8000
case "8":
c.Name = "PCMA"
c.ClockRate = 8000
default:
panic("unknown codec")
}
}
return c
}
func atoi(s string) (i int) {
i, _ = strconv.Atoi(s)
return
}

48
pkg/streamer/streamer.go Normal file
View File

@@ -0,0 +1,48 @@
package streamer
// States, Queries and Events
type EventType byte
const (
StateNull EventType = iota
StateReady
StatePaused
StatePlaying
)
// Element base struct for all classes with support feedback
type Element struct {
events []EventFunc
}
type EventFunc func(msg interface{})
func (e *Element) Listen(f EventFunc) {
e.events = append(e.events, f)
}
func (e *Element) Fire(msg interface{}) {
for _, f := range e.events {
f(msg)
}
}
func (e *Element) Push(msg interface{}) {
}
// Producer and Consumer interfaces
type Producer interface {
Listen(f EventFunc)
GetMedias() []*Media
GetTrack(media *Media, codec *Codec) *Track
Start() error
Stop() error
}
type Consumer interface {
Listen(f EventFunc)
GetMedias() []*Media
AddTrack(media *Media, track *Track) *Track
}

44
pkg/streamer/track.go Normal file
View File

@@ -0,0 +1,44 @@
package streamer
import (
"fmt"
"github.com/pion/rtp"
)
type WriterFunc func(packet *rtp.Packet) error
type WrapperFunc func(push WriterFunc) WriterFunc
type Track struct {
Codec *Codec
Direction string
Sink map[*Track]WriterFunc
}
func (t *Track) String() string {
s := t.Codec.String()
s += fmt.Sprintf(", sinks=%d", len(t.Sink))
return s
}
func (t *Track) WriteRTP(p *rtp.Packet) error {
for _, f := range t.Sink {
_ = f(p)
}
return nil
}
func (t *Track) Bind(w WriterFunc) *Track {
if t.Sink == nil {
t.Sink = map[*Track]WriterFunc{}
}
clone := &Track{
Codec: t.Codec, Direction: t.Direction, Sink: t.Sink,
}
t.Sink[clone] = w
return clone
}
func (t *Track) Unbind() {
delete(t.Sink, t)
}

104
pkg/tcp/auth.go Normal file
View File

@@ -0,0 +1,104 @@
package tcp
import (
"crypto/md5"
"encoding/base64"
"encoding/hex"
"fmt"
"net/url"
"strings"
)
type Auth struct {
Method byte
user string
pass string
header string
h1nonce string
}
const (
AuthNone byte = iota
AuthUnknown
AuthBasic
AuthDigest
)
func NewAuth(user *url.Userinfo) *Auth {
a := new(Auth)
a.user = user.Username()
a.pass, _ = user.Password()
if a.user != "" {
a.Method = AuthUnknown
}
return a
}
func (a *Auth) Read(res *Response) bool {
auth := res.Header.Get("WWW-Authenticate")
if len(auth) < 6 {
return false
}
switch auth[:6] {
case "Basic ":
a.header = "Basic " + B64(a.user, a.pass)
a.Method = AuthBasic
return true
case "Digest":
realm := Between(auth, `realm="`, `"`)
nonce := Between(auth, `nonce="`, `"`)
a.h1nonce = HexMD5(a.user, realm, a.pass) + ":" + nonce
a.header = fmt.Sprintf(
`Digest username="%s", realm="%s", nonce="%s"`,
a.user, realm, nonce,
)
a.Method = AuthDigest
return true
default:
return false
}
}
func (a *Auth) Write(req *Request) {
if a == nil {
return
}
switch a.Method {
case AuthBasic:
req.Header.Set("Authorization", a.header)
case AuthDigest:
uri := req.URL.RequestURI()
h2 := HexMD5(req.Method, uri)
response := HexMD5(a.h1nonce, h2)
header := a.header + fmt.Sprintf(
`, uri="%s", response="%s"`, uri, response,
)
req.Header.Set("Authorization", header)
}
}
func Between(s, sub1, sub2 string) string {
i := strings.Index(s, sub1)
if i < 0 {
return ""
}
s = s[i+len(sub1):]
i = strings.Index(s, sub2)
if i < 0 {
return ""
}
return s[:i]
}
func HexMD5(s ...string) string {
b := md5.Sum([]byte(strings.Join(s, ":")))
return hex.EncodeToString(b[:])
}
func B64(s ...string) string {
b := []byte(strings.Join(s, ":"))
return base64.StdEncoding.EncodeToString(b)
}

36
pkg/tcp/server.go Normal file
View File

@@ -0,0 +1,36 @@
package tcp
import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"net"
)
type Server struct {
streamer.Element
listener net.Listener
closed bool
}
func NewServer(address string) (srv *Server, err error) {
srv = &Server{}
srv.listener, err = net.Listen("tcp", address)
return
}
func (s *Server) Serve() {
for {
conn, err := s.listener.Accept()
if err != nil {
return
}
go func() {
s.Fire(conn)
_ = conn.Close()
}()
}
}
func (s *Server) Close() error {
return s.listener.Close()
}

147
pkg/tcp/textproto.go Normal file
View File

@@ -0,0 +1,147 @@
package tcp
import (
"bufio"
"errors"
"fmt"
"io"
"net/textproto"
"net/url"
"strconv"
"strings"
)
const EndLine = "\r\n"
// Response like http.Response, but with any proto
type Response struct {
Status string
StatusCode int
Proto string
Header textproto.MIMEHeader
Body []byte
Request *Request
}
func (r Response) String() string {
s := r.Proto + " " + r.Status + EndLine
for k, v := range r.Header {
s += k + ": " + v[0] + EndLine
}
s += EndLine
if r.Body != nil {
s += string(r.Body)
}
return s
}
func (r *Response) Write(w io.Writer) (err error) {
_, err = w.Write([]byte(r.String()))
return
}
func ReadResponse(r *bufio.Reader) (*Response, error) {
tp := textproto.NewReader(r)
line, err := tp.ReadLine()
if err != nil {
return nil, err
}
ss := strings.SplitN(line, " ", 3)
if len(ss) != 3 {
return nil, errors.New("malformed response")
}
res := &Response{
Status: ss[1] + " " + ss[2],
Proto: ss[0],
}
res.StatusCode, err = strconv.Atoi(ss[1])
if err != nil {
return nil, err
}
res.Header, err = tp.ReadMIMEHeader()
if err != nil {
return nil, err
}
if val := res.Header.Get("Content-Length"); val != "" {
var i int
i, err = strconv.Atoi(val)
res.Body = make([]byte, i)
if _, err = io.ReadAtLeast(r, res.Body, i); err != nil {
return nil, err
}
}
return res, nil
}
// Request like http.Request, but with any proto
type Request struct {
Method string
URL *url.URL
Proto string
Header textproto.MIMEHeader
Body []byte
}
func (r *Request) String() string {
s := r.Method + " " + r.URL.String() + " " + r.Proto + EndLine
for k, v := range r.Header {
s += k + ": " + v[0] + EndLine
}
s += EndLine
if r.Body != nil {
s += string(r.Body)
}
return s
}
func (r *Request) Write(w io.Writer) (err error) {
_, err = w.Write([]byte(r.String()))
return
}
func ReadRequest(r *bufio.Reader) (*Request, error) {
tp := textproto.NewReader(r)
line, err := tp.ReadLine()
if err != nil {
return nil, err
}
ss := strings.SplitN(line, " ", 3)
if len(ss) != 3 {
return nil, fmt.Errorf("wrong request: %s", line)
}
req := &Request{
Method: ss[0],
Proto: ss[2],
}
req.URL, err = url.Parse(ss[1])
if err != nil {
return nil, err
}
req.Header, err = tp.ReadMIMEHeader()
if err != nil {
return nil, err
}
if val := req.Header.Get("Content-Length"); val != "" {
var i int
i, err = strconv.Atoi(val)
req.Body = make([]byte, i)
if _, err = io.ReadAtLeast(r, req.Body, i); err != nil {
return nil, err
}
}
return req, nil
}

30
pkg/tcp/textproto_test.go Normal file
View File

@@ -0,0 +1,30 @@
package tcp
import (
"bufio"
"bytes"
"net/http"
"testing"
)
func assert(t *testing.T, one, two interface{}) {
if one != two {
t.FailNow()
}
}
func TestName(t *testing.T) {
data := []byte(`RTSP/1.0 401 Unauthorized
WWW-Authenticate: Digest realm="testrealm@host.com",
nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",
`)
buf := bytes.NewBuffer(data)
r := bufio.NewReader(buf)
res, err := ReadResponse(r)
assert(t, err, nil)
assert(t, res.StatusCode, http.StatusUnauthorized)
}

104
pkg/webrtc/api.go Normal file
View File

@@ -0,0 +1,104 @@
package webrtc
import (
"github.com/pion/interceptor"
"github.com/pion/webrtc/v3"
"net"
)
func NewAPI(address string) (*webrtc.API, error) {
// for debug logs add to env: `PION_LOG_DEBUG=all`
m := &webrtc.MediaEngine{}
//if err := m.RegisterDefaultCodecs(); err != nil {
// return nil, err
//}
if err := RegisterDefaultCodecs(m); err != nil {
return nil, err
}
i := &interceptor.Registry{}
if err := webrtc.RegisterDefaultInterceptors(m, i); err != nil {
return nil, err
}
if address == "" {
return webrtc.NewAPI(
webrtc.WithMediaEngine(m),
webrtc.WithInterceptorRegistry(i),
), nil
}
ln, err := net.Listen("tcp", address)
if err != nil {
return webrtc.NewAPI(
webrtc.WithMediaEngine(m),
webrtc.WithInterceptorRegistry(i),
), err
}
s := webrtc.SettingEngine{
//LoggerFactory: customLoggerFactory{},
}
s.SetNetworkTypes([]webrtc.NetworkType{
webrtc.NetworkTypeUDP4, webrtc.NetworkTypeUDP6,
webrtc.NetworkTypeTCP4, webrtc.NetworkTypeTCP6,
})
tcpMux := webrtc.NewICETCPMux(nil, ln, 8)
s.SetICETCPMux(tcpMux)
return webrtc.NewAPI(
webrtc.WithMediaEngine(m),
webrtc.WithInterceptorRegistry(i),
webrtc.WithSettingEngine(s),
), nil
}
func RegisterDefaultCodecs(m *webrtc.MediaEngine) error {
for _, codec := range []webrtc.RTPCodecParameters{
{
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypeOpus, 48000, 2, "minptime=10;useinbandfec=1", nil},
PayloadType: 101, //111,
}, {
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypePCMU, 8000, 0, "", nil},
PayloadType: 0,
}, {
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypePCMA, 8000, 0, "", nil},
PayloadType: 8,
},
} {
if err := m.RegisterCodec(codec, webrtc.RTPCodecTypeAudio); err != nil {
return err
}
}
videoRTCPFeedback := []webrtc.RTCPFeedback{{"goog-remb", ""}, {"ccm", "fir"}, {"nack", ""}, {"nack", "pli"}}
for _, codec := range []webrtc.RTPCodecParameters{
// macOS Google Chrome 103.0.5060.134
{
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f", videoRTCPFeedback},
PayloadType: 96, //102,
}, {
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", videoRTCPFeedback},
PayloadType: 97, //125,
}, {
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032", videoRTCPFeedback},
PayloadType: 98, //123,
},
// macOS Safari 15.1
{
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypeH264, 90000, 0, "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640c1f", videoRTCPFeedback},
PayloadType: 99,
},
{
RTPCodecCapability: webrtc.RTPCodecCapability{webrtc.MimeTypeH265, 90000, 0, "", videoRTCPFeedback},
PayloadType: 100,
},
} {
if err := m.RegisterCodec(codec, webrtc.RTPCodecTypeVideo); err != nil {
return err
}
}
return nil
}

195
pkg/webrtc/conn.go Normal file
View File

@@ -0,0 +1,195 @@
package webrtc
import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/webrtc/v3"
)
const (
MsgTypeOffer = "webrtc/offer"
MsgTypeOfferComplete = "webrtc/offer-complete"
MsgTypeAnswer = "webrtc/answer"
MsgTypeCandidate = "webrtc/candidate"
)
type Conn struct {
streamer.Element
UserAgent string
Conn *webrtc.PeerConnection
medias []*streamer.Media
tracks []*streamer.Track
receive int
send int
}
func (c *Conn) Init() {
c.Conn.OnICECandidate(func(candidate *webrtc.ICECandidate) {
if candidate != nil {
c.Fire(&streamer.Message{
Type: MsgTypeCandidate, Value: candidate.ToJSON().Candidate,
})
}
})
c.Conn.OnTrack(func(remote *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {
for _, track := range c.tracks {
if track.Direction != streamer.DirectionRecvonly {
continue
}
if track.Codec.PayloadType != uint8(remote.PayloadType()) {
continue
}
for {
packet, _, err := remote.ReadRTP()
if err != nil {
return
}
if len(packet.Payload) == 0 {
continue
}
c.receive += len(packet.Payload)
_ = track.WriteRTP(packet)
}
}
panic("something wrong")
})
c.Conn.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {
c.Fire(state)
// TODO: remove
switch state {
case webrtc.PeerConnectionStateConnected:
c.Fire(streamer.StatePlaying)
case webrtc.PeerConnectionStateDisconnected:
c.Fire(streamer.StateNull)
case webrtc.PeerConnectionStateFailed:
_ = c.Conn.Close()
}
})
}
func (c *Conn) ExchangeSDP(offer string, complete bool) (answer string, err error) {
sdOffer := webrtc.SessionDescription{
Type: webrtc.SDPTypeOffer, SDP: offer,
}
if err = c.Conn.SetRemoteDescription(sdOffer); err != nil {
return
}
//for _, tr := range c.Conn.GetTransceivers() {
// switch tr.Direction() {
// case webrtc.RTPTransceiverDirectionSendonly:
// // disable transceivers if we don't have track
// // make direction=inactive
// // don't really necessary, but anyway
// if tr.Sender() == nil {
// if err = tr.Stop(); err != nil {
// return
// }
// }
// case webrtc.RTPTransceiverDirectionRecvonly:
// // TODO: change codecs list
// caps := webrtc.RTPCodecCapability{
// MimeType: webrtc.MimeTypePCMU,
// ClockRate: 8000,
// }
// codecs := []webrtc.RTPCodecParameters{
// {RTPCodecCapability: caps},
// }
// if err = tr.SetCodecPreferences(codecs); err != nil {
// return
// }
// }
//}
var sdAnswer webrtc.SessionDescription
sdAnswer, err = c.Conn.CreateAnswer(nil)
if err != nil {
return
}
//var sd *sdp.SessionDescription
//sd, err = sdAnswer.Unmarshal()
//for _, media := range sd.MediaDescriptions {
// if media.MediaName.Media != "audio" {
// continue
// }
// for i, attr := range media.Attributes {
// if attr.Key == "sendonly" {
// attr.Key = "inactive"
// media.Attributes[i] = attr
// break
// }
// }
//}
//var b []byte
//b, err = sd.Marshal()
//sdAnswer.SDP = string(b)
if err = c.Conn.SetLocalDescription(sdAnswer); err != nil {
return
}
if complete {
<-webrtc.GatheringCompletePromise(c.Conn)
return c.Conn.LocalDescription().SDP, nil
}
return sdAnswer.SDP, nil
}
func (c *Conn) SetOffer(offer string) (err error) {
sdOffer := webrtc.SessionDescription{
Type: webrtc.SDPTypeOffer, SDP: offer,
}
if err = c.Conn.SetRemoteDescription(sdOffer); err != nil {
return
}
rawSDP := []byte(c.Conn.RemoteDescription().SDP)
c.medias, err = streamer.UnmarshalSDP(rawSDP)
return
}
func (c *Conn) GetAnswer() (answer string, err error) {
for _, tr := range c.Conn.GetTransceivers() {
if tr.Direction() != webrtc.RTPTransceiverDirectionSendonly {
continue
}
// disable transceivers if we don't have track
// make direction=inactive
// don't really necessary, but anyway
if tr.Sender() == nil {
if err = tr.Stop(); err != nil {
return
}
}
}
var sdAnswer webrtc.SessionDescription
sdAnswer, err = c.Conn.CreateAnswer(nil)
if err != nil {
return
}
if err = c.Conn.SetLocalDescription(sdAnswer); err != nil {
return
}
return sdAnswer.SDP, nil
}
func (c *Conn) remote() string {
for _, trans := range c.Conn.GetTransceivers() {
pair, _ := trans.Receiver().Transport().ICETransport().GetSelectedCandidatePair()
return pair.Remote.String()
}
return ""
}

85
pkg/webrtc/helper.go Normal file
View File

@@ -0,0 +1,85 @@
package webrtc
import (
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/ice/v2"
"github.com/pion/stun"
"github.com/pion/webrtc/v3"
"net"
"strconv"
)
func NewCandidate(address string) (string, error) {
host, port, err := net.SplitHostPort(address)
if err != nil {
return "", err
}
i, err := strconv.Atoi(port)
if err != nil {
return "", err
}
cand, err := ice.NewCandidateHost(&ice.CandidateHostConfig{
Network: "tcp",
Address: host,
Port: i,
Component: ice.ComponentRTP,
TCPType: ice.TCPTypePassive,
})
if err != nil {
return "", err
}
return "candidate:" + cand.Marshal(), nil
}
// GetPublicIP example from https://github.com/pion/stun
func GetPublicIP() (net.IP, error) {
c, err := stun.Dial("udp", "stun.l.google.com:19302")
if err != nil {
return nil, err
}
var res stun.Event
message := stun.MustBuild(stun.TransactionID, stun.BindingRequest)
if err = c.Do(message, func(e stun.Event) { res = e }); err != nil {
return nil, err
}
if res.Error != nil {
return nil, res.Error
}
var xorAddr stun.XORMappedAddress
if err = xorAddr.GetFrom(res.Message); err != nil {
return nil, err
}
return xorAddr.IP, nil
}
func MimeType(codec *streamer.Codec) string {
switch codec.Name {
case streamer.CodecH264:
return webrtc.MimeTypeH264
case streamer.CodecH265:
return webrtc.MimeTypeH265
case streamer.CodecVP8:
return webrtc.MimeTypeVP8
case streamer.CodecVP9:
return webrtc.MimeTypeVP9
case streamer.CodecAV1:
return webrtc.MimeTypeAV1
case streamer.CodecPCMU:
return webrtc.MimeTypePCMU
case streamer.CodecPCMA:
return webrtc.MimeTypePCMA
case streamer.CodecOpus:
return webrtc.MimeTypeOpus
case streamer.CodecG722:
return webrtc.MimeTypeG722
}
panic("not implemented")
}

125
pkg/webrtc/streamer.go Normal file
View File

@@ -0,0 +1,125 @@
package webrtc
import (
"encoding/json"
"github.com/AlexxIT/go2rtc/pkg/h264"
"github.com/AlexxIT/go2rtc/pkg/streamer"
"github.com/pion/rtp"
"github.com/pion/webrtc/v3"
)
// Consumer
func (c *Conn) GetMedias() []*streamer.Media {
return c.medias
}
func (c *Conn) AddTrack(media *streamer.Media, track *streamer.Track) *streamer.Track {
switch track.Direction {
// send our track to WebRTC consumer
case streamer.DirectionSendonly:
codec := track.Codec
// webrtc.codecParametersFuzzySearch
caps := webrtc.RTPCodecCapability{
MimeType: MimeType(codec),
Channels: codec.Channels,
ClockRate: codec.ClockRate,
}
if codec.Name == streamer.CodecH264 {
// don't know if this really neccessary
// I have tested multiple browsers and H264 profile has no effect on anything
caps.SDPFmtpLine = "packetization-mode=1;profile-level-id=42e01f"
}
// important to use same streamID so JS will automatically
// join two tracks as one source/stream
trackLocal, err := webrtc.NewTrackLocalStaticRTP(
caps, caps.MimeType[:5], "go2rtc",
)
if err != nil {
return nil
}
if _, err = c.Conn.AddTrack(trackLocal); err != nil {
return nil
}
push := func(packet *rtp.Packet) error {
c.send += packet.MarshalSize()
return trackLocal.WriteRTP(packet)
}
if codec.Name == streamer.CodecH264 {
wrapper := h264.RTPPay(1200)
push = wrapper(push)
if codec.PayloadType != 255 {
wrapper = h264.RTPDepay(track)
push = wrapper(push)
}
}
track = track.Bind(push)
c.tracks = append(c.tracks, track)
return track
// receive track from WebRTC consumer (microphone, backchannel, two way audio)
case streamer.DirectionRecvonly:
for _, tr := range c.Conn.GetTransceivers() {
if tr.Mid() != media.MID {
continue
}
codec := track.Codec
caps := webrtc.RTPCodecCapability{
MimeType: MimeType(codec),
ClockRate: codec.ClockRate,
Channels: codec.Channels,
}
codecs := []webrtc.RTPCodecParameters{
{RTPCodecCapability: caps},
}
if err := tr.SetCodecPreferences(codecs); err != nil {
return nil
}
c.tracks = append(c.tracks, track)
return track
}
}
panic("wrong direction")
}
//
func (c *Conn) Push(msg interface{}) {
if msg := msg.(*streamer.Message); msg != nil {
if msg.Type == MsgTypeCandidate {
_ = c.Conn.AddICECandidate(webrtc.ICECandidateInit{
Candidate: msg.Value.(string),
})
}
}
}
func (c *Conn) MarshalJSON() ([]byte, error) {
v := map[string]interface{}{
streamer.JSONType: "WebRTC server consumer",
streamer.JSONRemoteAddr: c.remote(),
}
if c.receive > 0 {
v[streamer.JSONReceive] = c.receive
}
if c.send > 0 {
v[streamer.JSONSend] = c.send
}
if c.UserAgent != "" {
v[streamer.JSONUserAgent] = c.UserAgent
}
return json.Marshal(v)
}

56
pkg/webrtc/webrtc_test.go Normal file
View File

@@ -0,0 +1,56 @@
package webrtc
import (
"github.com/pion/ice/v2"
"github.com/pion/sdp/v3"
"github.com/pion/webrtc/v3"
"github.com/stretchr/testify/assert"
"testing"
)
func TestName(t *testing.T) {
i, _ := ice.NewCandidateHost(&ice.CandidateHostConfig{
Network: "tcp",
Address: "192.168.1.123",
Port: 8555,
Component: ice.ComponentRTP,
TCPType: ice.TCPTypePassive,
})
a := i.Marshal()
println(a)
}
func TestPublicIP(t *testing.T) {
ip, err := GetPublicIP()
assert.Nil(t, err)
assert.NotNil(t, ip)
t.Logf("your public IP: %s", ip.String())
}
func TestMedia(t *testing.T) {
codec := webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeH264,
ClockRate: 90000,
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f",
},
PayloadType: 96,
}
md := &sdp.MediaDescription{
MediaName: sdp.MediaName{
Media: "video", Protos: []string{"RTP", "AVP"},
},
}
md.WithCodec(
uint8(codec.PayloadType), codec.MimeType[6:], codec.ClockRate,
codec.Channels, codec.SDPFmtpLine,
)
sd := &sdp.SessionDescription{
MediaDescriptions: []*sdp.MediaDescription{md},
}
data, err := sd.Marshal()
assert.Nil(t, err)
assert.NotNil(t, data)
}

47
scripts/build.cmd Normal file
View File

@@ -0,0 +1,47 @@
@ECHO OFF
@SET GOOS=windows
@SET GOARCH=amd64
@SET FILENAME=go2rtc_win64.exe
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME%
@SET GOOS=windows
@SET GOARCH=386
@SET FILENAME=go2rtc_win32.exe
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME%
@SET GOOS=linux
@SET GOARCH=amd64
@SET FILENAME=go2rtc_linux_amd64
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME%
@SET GOOS=linux
@SET GOARCH=386
@SET FILENAME=go2rtc_linux_i386
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME%
@SET GOOS=linux
@SET GOARCH=arm64
@SET FILENAME=go2rtc_linux_arm64
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME%
@SET GOOS=linux
@SET GOARCH=arm
@SET GOARM=7
@SET FILENAME=go2rtc_linux_arm
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME%
@SET GOOS=linux
@SET GOARCH=mipsle
@SET FILENAME=go2rtc_linux_mipsel
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME%
@SET GOOS=darwin
@SET GOARCH=amd64
@SET FILENAME=go2rtc_mac_amd64
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME%
@SET GOOS=darwin
@SET GOARCH=arm64
@SET FILENAME=go2rtc_mac_arm64
go build -ldflags "-s -w" -trimpath -o %FILENAME% && upx %FILENAME%

49
www/README.md Normal file
View File

@@ -0,0 +1,49 @@
# HTML5
**1. Autoplay video tag**
[Video auto play is not working](https://stackoverflow.com/questions/17994666/video-auto-play-is-not-working-in-safari-and-chrome-desktop-browser)
> Recently many browsers can only autoplay the videos with sound off, so you'll need to add muted attribute to the video tag too
```html
<video id="video" autoplay controls playsinline muted></video>
```
**2. [Safari] pc.createOffer**
Don't work in Desktop Safari:
```js
pc.createOffer({offerToReceiveAudio: true, offerToReceiveVideo: true})
```
Should be replaced with:
```js
pc.addTransceiver('video', {direction: 'recvonly'});
pc.addTransceiver('audio', {direction: 'recvonly'});
pc.createOffer();
```
**3. pc.ontrack**
TODO
```js
pc.ontrack = ev => {
const video = document.getElementById('video');
// when audio track not exist in Chrome
if (ev.streams.length === 0) return;
// when audio track not exist in Firefox
if (ev.streams[0].id[0] === '{') return;
// when stream already init
if (video.srcObject !== null) return;
video.srcObject = ev.streams[0];
}
```

59
www/codecs.html Normal file
View File

@@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>go2rtc - WebRTC</title>
<style>
body {
margin: 0;
padding: 0;
}
html, body {
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<div id="out"></div>
<script>
const out = document.getElementById("out");
const print = (name, caps) => {
out.innerText += name + "\n";
caps.codecs.forEach((codec) => {
out.innerText += [codec.mimeType, codec.channels, codec.clockRate, codec.sdpFmtpLine] + "\n";
});
out.innerText += "\n";
}
if (RTCRtpReceiver.getCapabilities) {
print("receiver video", RTCRtpReceiver.getCapabilities("video"));
print("receiver audio", RTCRtpReceiver.getCapabilities("audio"));
print("sender video", RTCRtpSender.getCapabilities("video"));
print("sender audio", RTCRtpSender.getCapabilities("audio"));
}
const types = [
'video/mp4; codecs="avc1.42401E"',
'video/mp4; codecs="avc1.42C01E"',
'video/mp4; codecs="avc1.42E01E"',
'video/mp4; codecs="avc1.42001E"',
'video/mp4; codecs="avc1.4D401E"',
'video/mp4; codecs="avc1.4D001E"',
'video/mp4; codecs="avc1.640032"',
'video/mp4; codecs="avc1.640C32"',
'video/mp4; codecs="avc1.F4001F"',
'video/mp4; codecs="hvc1.016000"',
];
const video = document.createElement("video");
out.innerText += "video.canPlayType\n";
types.forEach(type => {
out.innerText += type + "=" + (video.canPlayType(type) ? "true" : "false") + "\n";
})
</script>
</body>
</html>

43
www/index.html Normal file
View File

@@ -0,0 +1,43 @@
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport"
content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>go2rtc</title>
</head>
<body>
<div id="header"></div>
<table id="items"></table>
<script>
const baseUrl = location.origin + location.pathname.substr(
0, location.pathname.lastIndexOf("/")
);
const header = document.getElementById('header');
header.innerHTML = `<a href="api/stats">stats</a>` +
`<a href="webcam.html?url=webcam">webcam</a>`;
const links = [
'<a href="webrtc-async.html?url={name}">webrtc-async</a>',
'<a href="webrtc-sync.html?url={name}">webrtc-sync</a>',
'<a href="mse.html?url={name}">mse</a>',
];
fetch(`${baseUrl}/api/stats`).then(r => {
r.json().then(data => {
const content = document.getElementById('items');
for (let name in data.streams) {
let html = `<tr><td>${name || 'default'}</td>`;
links.forEach(link => {
html += `<td>${link.replace('{name}', name)}</td>`
})
html += `</tr>`;
content.innerHTML += html
}
});
})
</script>
</body>
</html>

119
www/mse.html Normal file
View File

@@ -0,0 +1,119 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>go2rtc - MSE</title>
<style>
body {
margin: 0;
padding: 0;
}
html, body {
height: 100%;
width: 100%;
}
#video {
width: 100%;
height: 100%;
background: black;
}
</style>
</head>
<body>
<!-- muted is important for autoplay -->
<video id="video" autoplay controls playsinline muted></video>
<script>
const video = document.querySelector('#video');
// support api_path
const baseUrl = location.origin + location.pathname.substr(
0, location.pathname.lastIndexOf("/")
);
const ws = new WebSocket(`ws${baseUrl.substr(4)}/api/ws${location.search}`);
ws.binaryType = "arraybuffer";
let mediaSource;
ws.onopen = () => {
console.log("Start WS");
// https://web.dev/i18n/en/fast-playback-with-preload/#manual_buffering
// https://developer.mozilla.org/en-US/docs/Web/API/Media_Source_Extensions_API
mediaSource = new MediaSource();
video.src = URL.createObjectURL(mediaSource);
mediaSource.onsourceopen = () => {
console.debug("mediaSource.onsourceopen");
mediaSource.onsourceopen = null;
URL.revokeObjectURL(video.src);
ws.send(JSON.stringify({"type": "mse"}));
};
};
let sourceBuffer, queueBuffer = [];
ws.onmessage = ev => {
if (typeof ev.data === 'string') {
const data = JSON.parse(ev.data);
console.debug("ws.onmessage", data);
if (data.type === "mse") {
sourceBuffer = mediaSource.addSourceBuffer(
`video/mp4; codecs="${data.value}"`
);
// important: segments supports TrackFragDecodeTime
// sequence supports only TrackFragRunEntry Duration
sourceBuffer.mode = "segments";
sourceBuffer.onupdateend = () => {
if (!sourceBuffer.updating && queueBuffer.length > 0) {
sourceBuffer.appendBuffer(queueBuffer.shift());
}
}
}
} else {
if (sourceBuffer.updating) {
queueBuffer.push(ev.data)
} else {
sourceBuffer.appendBuffer(ev.data);
}
}
}
let offsetTime = 1, noWaiting = 0;
setInterval(() => {
if (video.paused || video.seekable.length === 0) return;
if (noWaiting < 0) {
offsetTime = Math.min(offsetTime * 1.1, 5);
console.debug("offset time up:", offsetTime);
} else if (noWaiting >= 30) {
noWaiting = 0;
offsetTime = Math.max(offsetTime * 0.9, 0.5);
console.debug("offset time down:", offsetTime);
}
noWaiting += 1;
const endTime = video.seekable.end(video.seekable.length - 1);
let playbackRate = (endTime - video.currentTime) / offsetTime;
if (playbackRate < 0.1) {
// video.currentTime = endTime - offsetTime;
playbackRate = 0.1;
} else if (playbackRate > 10) {
// video.currentTime = endTime - offsetTime;
playbackRate = 10;
}
// https://github.com/GoogleChrome/developer.chrome.com/issues/135
video.playbackRate = playbackRate;
}, 1000);
video.onwaiting = () => {
const endTime = video.seekable.end(video.seekable.length - 1);
video.currentTime = endTime - offsetTime;
noWaiting = -1;
}
</script>
</body>
</html>

124
www/webrtc-async.html Normal file
View File

@@ -0,0 +1,124 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>go2rtc - WebRTC</title>
<style>
body {
margin: 0;
padding: 0;
}
html, body {
height: 100%;
width: 100%;
}
#video {
/* video "container" size */
width: 100%;
height: 100%;
background: black;
}
</style>
<!-- Fix bugs for example with Safari... -->
<!-- <script src="https://webrtc.github.io/adapter/adapter-latest.js"></script>-->
</head>
<body>
<!-- muted is important for autoplay -->
<video id="video" autoplay controls playsinline muted></video>
<script>
function init(stream) {
// support api_path
const baseUrl = location.origin + location.pathname.substr(
0, location.pathname.lastIndexOf("/")
);
const ws = new WebSocket(`ws${baseUrl.substr(4)}/api/ws${location.search}`);
ws.onopen = () => {
console.debug('ws.onopen');
pc.createOffer({
// this is adds two media to SDP with recvonly direction
// offerToReceiveAudio: true,
// offerToReceiveVideo: true,
}).then(offer => {
pc.setLocalDescription(offer).then(() => {
console.log(offer.sdp);
const msg = {type: 'webrtc/offer', value: pc.localDescription.sdp};
ws.send(JSON.stringify(msg));
});
});
}
ws.onmessage = ev => {
const msg = JSON.parse(ev.data);
console.debug('ws.onmessage', msg);
if (msg.type === 'webrtc/candidate') {
pc.addIceCandidate({candidate: msg.value, sdpMid: ''});
} else if (msg.type === 'webrtc/answer') {
pc.setRemoteDescription({type: 'answer', sdp: msg.value});
pc.getTransceivers().forEach(t => {
if (t.receiver.track.kind === 'audio') {
t.currentDirection
}
})
}
}
const pc = new RTCPeerConnection({
iceServers: [{urls: 'stun:stun.l.google.com:19302'}]
});
pc.onicecandidate = ev => {
console.debug("pc.onicecandidate", ev.candidate);
if (ev.candidate !== null) {
ws.send(JSON.stringify({
type: 'webrtc/candidate', value: ev.candidate.toJSON().candidate
}));
}
}
pc.ontrack = ev => {
const video = document.getElementById('video');
console.debug('pc.ontrack', video.srcObject !== null);
// when audio track not exist in Chrome
if (ev.streams.length === 0) return;
// when audio track not exist in Firefox
if (ev.streams[0].id[0] === '{') return;
// when stream already init
if (video.srcObject !== null) return;
video.srcObject = ev.streams[0];
}
// Safari don't support "offerToReceiveVideo"
// so need to create transeivers manually
pc.addTransceiver('video', {direction: 'recvonly'});
pc.addTransceiver('audio', {direction: 'recvonly'});
if (stream) {
stream.getTracks().forEach(track => {
const sender = pc.addTrack(track, stream)
// track.stop();
// setTimeout(() => {
// navigator.mediaDevices.getUserMedia({audio: true}).then(stream => {
// stream.getTracks().forEach(track => {
// sender.replaceTrack(track);
// });
// });
// }, 10000);
});
}
}
if (navigator.mediaDevices) {
navigator.mediaDevices.getUserMedia({audio: true}).then(init).catch(() => init());
} else {
init();
}
</script>
</body>
</html>

64
www/webrtc-sync.html Normal file
View File

@@ -0,0 +1,64 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>go2rtc - WebRTC</title>
<style>
body {
margin: 0;
padding: 0;
}
html, body {
height: 100%;
width: 100%;
}
#video {
/* video "container" size */
width: 100%;
height: 100%;
background: black;
}
</style>
</head>
<body>
<!-- muted is important for autoplay -->
<video id="video" autoplay controls playsinline muted></video>
<script>
// support api_path
let baseUrl = location.origin + location.pathname.substr(
0, location.pathname.lastIndexOf("/")
);
let pc = new RTCPeerConnection({
iceServers: [{urls: 'stun:stun.l.google.com:19302'}]
});
pc.onicegatheringstatechange = async () => {
if (pc.iceGatheringState !== 'complete') return;
let r = await fetch(`${baseUrl}/api/webrtc${location.search}`, {
method: 'POST', body: pc.localDescription.sdp,
});
await pc.setRemoteDescription({
type: 'answer', sdp: await r.text()
});
}
pc.ontrack = ev => {
let video = document.getElementById('video');
if (video.srcObject === null) {
video.srcObject = ev.streams[0];
}
}
pc.addTransceiver('video');
pc.addTransceiver('audio');
pc.createOffer({
offerToReceiveVideo: true, offerToReceiveAudio: true
}).then(offer => {
pc.setLocalDescription(offer);
});
</script>
</body>
</html>