install go2rtc on bob

This commit is contained in:
2026-04-04 19:36:14 +02:00
parent f0b56e63d1
commit ccf88187b8
537 changed files with 69213 additions and 0 deletions
@@ -0,0 +1,45 @@
# WebTorrent
> [!NOTE]
> This section needs some improvement.
## WebTorrent Client
[`new in v1.3.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)
This source can get a stream from another go2rtc via [WebTorrent](https://en.wikipedia.org/wiki/WebTorrent) protocol.
### Client Configuration
```yaml
streams:
webtorrent1: webtorrent:?share=huofssuxaty00izc&pwd=k3l2j9djeg8v8r7e
```
## WebTorrent Server
[`new in v1.3.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)
This module supports:
- Share any local stream via [WebTorrent](https://webtorrent.io/) technology
- Get any [incoming stream](../webrtc/README.md#ingest-browser) from PC or mobile via [WebTorrent](https://webtorrent.io/) technology
- Get any remote go2rtc source via [WebTorrent](https://webtorrent.io/) technology
Securely and freely. You do not need to open a public access to the go2rtc server. But in some cases (Symmetric NAT), you may need to set up external access to [WebRTC module](../webrtc/README.md).
To generate a sharing link or incoming link, go to the go2rtc WebUI (stream links page). This link is **temporary** and will stop working after go2rtc is restarted!
### Server Configuration
You can create permanent external links in the go2rtc config:
```yaml
webtorrent:
shares:
super-secret-share: # share name, should be unique among all go2rtc users!
pwd: super-secret-password
src: rtsp-dahua1 # stream name from streams section
```
Link example: `https://go2rtc.org/webtorrent/#share=02SNtgjKXY&pwd=wznEQqznxW&media=video+audio`
@@ -0,0 +1,176 @@
package webtorrent
import (
"errors"
"fmt"
"net/http"
"net/url"
"github.com/AlexxIT/go2rtc/internal/api"
"github.com/AlexxIT/go2rtc/internal/app"
"github.com/AlexxIT/go2rtc/internal/streams"
"github.com/AlexxIT/go2rtc/internal/webrtc"
"github.com/AlexxIT/go2rtc/pkg/core"
"github.com/AlexxIT/go2rtc/pkg/webtorrent"
"github.com/rs/zerolog"
)
func Init() {
var cfg struct {
Mod struct {
Trackers []string `yaml:"trackers"`
Shares map[string]struct {
Pwd string `yaml:"pwd"`
Src string `yaml:"src"`
} `yaml:"shares"`
} `yaml:"webtorrent"`
}
cfg.Mod.Trackers = []string{"wss://tracker.openwebtorrent.com"}
app.LoadConfig(&cfg)
if len(cfg.Mod.Trackers) == 0 {
return
}
log = app.GetLogger("webtorrent")
streams.HandleFunc("webtorrent", streamHandle)
api.HandleFunc("api/webtorrent", apiHandle)
srv = &webtorrent.Server{
URL: cfg.Mod.Trackers[0],
Exchange: func(src, offer string) (answer string, err error) {
stream := streams.Get(src)
if stream == nil {
return "", errors.New(api.StreamNotFound)
}
return webrtc.ExchangeSDP(stream, offer, "webtorrent", "")
},
}
if log.Debug().Enabled() {
srv.Listen(func(msg any) {
switch msg.(type) {
case string, error:
log.Debug().Msgf("[webtorrent] %s", msg)
case *webtorrent.Message:
log.Trace().Any("msg", msg).Msgf("[webtorrent]")
}
})
}
for name, share := range cfg.Mod.Shares {
if len(name) < 8 {
log.Warn().Str("name", name).Msgf("min share name len - 8 symbols")
continue
}
if len(share.Pwd) < 4 {
log.Warn().Str("name", name).Str("pwd", share.Pwd).Msgf("min share pwd len - 4 symbols")
continue
}
if streams.Get(share.Src) == nil {
log.Warn().Str("stream", share.Src).Msgf("stream not exists")
continue
}
srv.AddShare(name, share.Pwd, share.Src)
// adds to GET /api/webtorrent
shares[name] = name
}
}
var log zerolog.Logger
var shares = map[string]string{}
var srv *webtorrent.Server
func apiHandle(w http.ResponseWriter, r *http.Request) {
src := r.URL.Query().Get("src")
share, ok := shares[src]
switch r.Method {
case "GET":
// support act as WebTorrent tracker (for testing purposes)
if r.Header.Get("Connection") == "Upgrade" {
tracker(w, r)
return
}
if src != "" {
// response one share
if ok {
pwd := srv.GetSharePwd(share)
data := fmt.Sprintf(`{"share":%q,"pwd":%q}`, share, pwd)
_, _ = w.Write([]byte(data))
} else {
http.Error(w, "", http.StatusNotFound)
}
} else {
// response all shares
var items []*api.Source
for src, share := range shares {
pwd := srv.GetSharePwd(share)
source := fmt.Sprintf("webtorrent:?share=%s&pwd=%s", share, pwd)
items = append(items, &api.Source{ID: src, URL: source})
}
api.ResponseSources(w, items)
}
case "POST":
// check if share already exist
if ok {
http.Error(w, "", http.StatusBadRequest)
return
}
// check if stream exists
if stream := streams.Get(src); stream == nil {
http.Error(w, "", http.StatusNotFound)
return
}
// create new random share
share = core.RandString(10, 62)
pwd := core.RandString(10, 62)
srv.AddShare(share, pwd, src)
shares[src] = share
w.WriteHeader(http.StatusCreated)
data := fmt.Sprintf(`{"share":%q,"pwd":%q}`, share, pwd)
api.Response(w, data, api.MimeJSON)
case "DELETE":
if ok {
srv.RemoveShare(share)
delete(shares, src)
} else {
http.Error(w, "", http.StatusNotFound)
}
}
}
func streamHandle(rawURL string) (core.Producer, error) {
u, err := url.Parse(rawURL)
if err != nil {
return nil, err
}
query := u.Query()
share := query.Get("share")
pwd := query.Get("pwd")
if len(share) < 8 || len(pwd) < 4 {
return nil, errors.New("wrong URL: " + rawURL)
}
pc, err := webrtc.PeerConnection(true)
if err != nil {
return nil, err
}
return webtorrent.NewClient(srv.URL, share, pwd, pc)
}
@@ -0,0 +1,108 @@
package webtorrent
import (
"fmt"
"net/http"
"github.com/AlexxIT/go2rtc/pkg/webtorrent"
"github.com/gorilla/websocket"
)
var upgrader *websocket.Upgrader
var hashes map[string]map[string]*websocket.Conn
func tracker(w http.ResponseWriter, r *http.Request) {
if upgrader == nil {
upgrader = &websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 2028,
}
upgrader.CheckOrigin = func(r *http.Request) bool {
return true
}
}
ws, err := upgrader.Upgrade(w, r, nil)
if err != nil {
log.Warn().Err(err).Send()
return
}
defer ws.Close()
for {
var msg webtorrent.Message
if err = ws.ReadJSON(&msg); err != nil {
return
}
//log.Trace().Msgf("[webtorrent] message=%v", msg)
if msg.InfoHash == "" || msg.PeerId == "" {
continue
}
if hashes == nil {
hashes = map[string]map[string]*websocket.Conn{}
}
// new or old client with offers
clients := hashes[msg.InfoHash]
if clients == nil {
clients = map[string]*websocket.Conn{
msg.PeerId: ws,
}
hashes[msg.InfoHash] = clients
} else {
clients[msg.PeerId] = ws
}
switch {
case msg.Offers != nil:
// ask for ping
raw := fmt.Sprintf(
`{"action":"announce","interval":120,"info_hash":"%s","complete":0,"incomplete":1}`,
msg.InfoHash,
)
if err = ws.WriteMessage(websocket.TextMessage, []byte(raw)); err != nil {
log.Warn().Err(err).Send()
return
}
// skip if no offers (server)
if len(msg.Offers) == 0 {
continue
}
// get and check only first offer
offer := msg.Offers[0]
if offer.OfferId == "" || offer.Offer.Type != "offer" || offer.Offer.SDP == "" {
continue
}
// send offer to all clients (one of them - server)
raw = fmt.Sprintf(
`{"action":"announce","info_hash":"%s","peer_id":"%s","offer_id":"%s","offer":{"type":"offer","sdp":"%s"}}`,
msg.InfoHash, msg.PeerId, offer.OfferId, offer.Offer.SDP,
)
for _, server := range clients {
if server != ws {
_ = server.WriteMessage(websocket.TextMessage, []byte(raw))
}
}
case msg.OfferId != "" && msg.ToPeerId != "" && msg.Answer != nil:
ws1, ok := clients[msg.ToPeerId]
if !ok {
continue
}
raw := fmt.Sprintf(
`{"action":"announce","info_hash":"%s","peer_id":"%s","offer_id":"%s","answer":{"type":"answer","sdp":"%s"}}`,
msg.InfoHash, msg.PeerId, msg.OfferId, msg.Answer.SDP,
)
_ = ws1.WriteMessage(websocket.TextMessage, []byte(raw))
}
}
}