Browse Source

webrtc: allow using special characters in ICE server credentials (#1953) (#2000)

pull/2006/head
Alessandro Ros 2 years ago committed by GitHub
parent
commit
1a748bb971
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 18
      README.md
  2. 11
      apidocs/openapi.yaml
  3. 0
      internal/conf/auth_method.go
  4. 48
      internal/conf/conf.go
  5. 27
      internal/conf/env/env.go
  6. 50
      internal/conf/env/env_test.go
  7. 0
      internal/conf/hls_variant.go
  8. 0
      internal/conf/ips_or_cidrs.go
  9. 0
      internal/conf/log_destination.go
  10. 0
      internal/conf/log_level.go
  11. 0
      internal/conf/rtsp_range_type.go
  12. 0
      internal/conf/source_protocol.go
  13. 0
      internal/conf/string_duration.go
  14. 0
      internal/conf/string_size.go
  15. 8
      internal/conf/webrtc_ice_server.go
  16. 4
      internal/core/core.go
  17. 61
      internal/core/webrtc_http_server.go
  18. 95
      internal/core/webrtc_manager.go
  19. 8
      internal/core/webrtc_publish_index.html
  20. 8
      internal/core/webrtc_read_index.html
  21. 11
      internal/core/webrtc_session.go
  22. 21
      mediamtx.yml

18
README.md

@ -1051,9 +1051,7 @@ vlc --network-caching=50 rtsp://...
### General usage ### General usage
RTMP is a protocol that allows to read and publish streams, but is less versatile and less efficient than RTSP (doesn't support UDP, encryption, doesn't support most RTSP codecs, doesn't support feedback mechanism). It is used when there's need of publishing or reading streams from a software that supports only RTMP (for instance, OBS Studio and DJI drones). RTMP is a protocol that allows to read and publish streams, but is less versatile and less efficient than RTSP (doesn't support UDP, encryption, doesn't support most RTSP codecs, doesn't support feedback mechanism). It is used when there's need of publishing or reading streams from a software that supports RTMP only (for instance, OBS Studio and DJI drones).
At the moment, only the H264 and AAC codecs can be used with the RTMP protocol.
Streams can be published or read with the RTMP protocol, for instance with _FFmpeg_: Streams can be published or read with the RTMP protocol, for instance with _FFmpeg_:
@ -1213,7 +1211,7 @@ http://localhost:8889/mystream
### WHIP and WHEP ### WHIP and WHEP
WHIP and WHEP are two WebRTC extensions that allows to publish and read streams with WebRTC without passing through a web page. This allows to use WebRTC as a general purpose streaming protocol. WHIP and WHEP are two WebRTC extensions that allow to publish and read streams with WebRTC without passing through a web page. This allows to use WebRTC as a general purpose streaming protocol.
If you are using a software that supports WHIP, you can publish a stream to the server by using this WHIP URL: If you are using a software that supports WHIP, you can publish a stream to the server by using this WHIP URL:
@ -1270,15 +1268,21 @@ bluenviron/mediamtx
Finally, if none of these methods work, you can force all WebRTC/ICE connections to pass through a TURN server, like [coturn](https://github.com/coturn/coturn), that must be configured externally. The server address and credentials must be set in the configuration file: Finally, if none of these methods work, you can force all WebRTC/ICE connections to pass through a TURN server, like [coturn](https://github.com/coturn/coturn), that must be configured externally. The server address and credentials must be set in the configuration file:
```yml ```yml
webrtcICEServers: [turn:user:pass:host:port] webrtcICEServers2:
- url: turn:host:port
username: user
password: password
``` ```
Where `user` and `pass` are the username and password of the server. Note that `port` is not optional. Where `user` and `pass` are the username and password of the server. Note that `port` is not optional.
If the server uses a secret-based authentication (for instance, coturn with the `use-auth-secret` option), it must be configured in this way: If the server uses a secret-based authentication (for instance, coturn with the `use-auth-secret` option), it must be configured by using `AUTH_SECRET` as username, and the secret as password:
```yml ```yml
webrtcICEServers: [turn:AUTH_SECRET:secret:host:port] webrtcICEServers2:
- url: turn:host:port
username: AUTH_SECRET
password: secret
``` ```
where `secret` is the secret of the TURN server. _MediaMTX_ will generate a set of credentials by using the secret, and credentials will be sent to clients before the WebRTC/ICE connection is established. where `secret` is the secret of the TURN server. _MediaMTX_ will generate a set of credentials by using the secret, and credentials will be sent to clients before the WebRTC/ICE connection is established.

11
apidocs/openapi.yaml

@ -149,10 +149,17 @@ components:
type: array type: array
items: items:
type: string type: string
webrtcICEServers: webrtcICEServers2:
type: array type: array
items: items:
type: string type: object
properties:
url:
type: string
username:
type: string
password:
type: string
webrtcICEHostNAT1To1IPs: webrtcICEHostNAT1To1IPs:
type: array type: array
items: items:

0
internal/conf/authmethod.go → internal/conf/auth_method.go

48
internal/conf/conf.go

@ -137,17 +137,18 @@ type Conf struct {
HLSDirectory string `json:"hlsDirectory"` HLSDirectory string `json:"hlsDirectory"`
// WebRTC // WebRTC
WebRTCDisable bool `json:"webrtcDisable"` WebRTCDisable bool `json:"webrtcDisable"`
WebRTCAddress string `json:"webrtcAddress"` WebRTCAddress string `json:"webrtcAddress"`
WebRTCEncryption bool `json:"webrtcEncryption"` WebRTCEncryption bool `json:"webrtcEncryption"`
WebRTCServerKey string `json:"webrtcServerKey"` WebRTCServerKey string `json:"webrtcServerKey"`
WebRTCServerCert string `json:"webrtcServerCert"` WebRTCServerCert string `json:"webrtcServerCert"`
WebRTCAllowOrigin string `json:"webrtcAllowOrigin"` WebRTCAllowOrigin string `json:"webrtcAllowOrigin"`
WebRTCTrustedProxies IPsOrCIDRs `json:"webrtcTrustedProxies"` WebRTCTrustedProxies IPsOrCIDRs `json:"webrtcTrustedProxies"`
WebRTCICEServers []string `json:"webrtcICEServers"` WebRTCICEServers []string `json:"webrtcICEServers"` // deprecated
WebRTCICEHostNAT1To1IPs []string `json:"webrtcICEHostNAT1To1IPs"` WebRTCICEServers2 []WebRTCICEServer `json:"webrtcICEServers2"`
WebRTCICEUDPMuxAddress string `json:"webrtcICEUDPMuxAddress"` WebRTCICEHostNAT1To1IPs []string `json:"webrtcICEHostNAT1To1IPs"`
WebRTCICETCPMuxAddress string `json:"webrtcICETCPMuxAddress"` WebRTCICEUDPMuxAddress string `json:"webrtcICEUDPMuxAddress"`
WebRTCICETCPMuxAddress string `json:"webrtcICETCPMuxAddress"`
// paths // paths
Paths map[string]*PathConf `json:"paths"` Paths map[string]*PathConf `json:"paths"`
@ -237,10 +238,25 @@ func (conf *Conf) Check() error {
// WebRTC // WebRTC
for _, server := range conf.WebRTCICEServers { for _, server := range conf.WebRTCICEServers {
if !strings.HasPrefix(server, "stun:") && parts := strings.Split(server, ":")
!strings.HasPrefix(server, "turn:") && if len(parts) == 5 {
!strings.HasPrefix(server, "turns:") { conf.WebRTCICEServers2 = append(conf.WebRTCICEServers2, WebRTCICEServer{
return fmt.Errorf("invalid ICE server: '%s'", server) URL: parts[0] + ":" + parts[3] + ":" + parts[4],
Username: parts[1],
Password: parts[2],
})
} else {
conf.WebRTCICEServers2 = append(conf.WebRTCICEServers2, WebRTCICEServer{
URL: server,
})
}
}
conf.WebRTCICEServers = nil
for _, server := range conf.WebRTCICEServers2 {
if !strings.HasPrefix(server.URL, "stun:") &&
!strings.HasPrefix(server.URL, "turn:") &&
!strings.HasPrefix(server.URL, "turns:") {
return fmt.Errorf("invalid ICE server: '%s'", server.URL)
} }
} }
@ -324,7 +340,7 @@ func (conf *Conf) UnmarshalJSON(b []byte) error {
conf.WebRTCServerKey = "server.key" conf.WebRTCServerKey = "server.key"
conf.WebRTCServerCert = "server.crt" conf.WebRTCServerCert = "server.crt"
conf.WebRTCAllowOrigin = "*" conf.WebRTCAllowOrigin = "*"
conf.WebRTCICEServers = []string{"stun:stun.l.google.com:19302"} conf.WebRTCICEServers2 = []WebRTCICEServer{{URL: "stun:stun.l.google.com:19302"}}
type alias Conf type alias Conf
d := json.NewDecoder(bytes.NewReader(b)) d := json.NewDecoder(bytes.NewReader(b))

27
internal/conf/env/env.go vendored

@ -14,6 +14,15 @@ type envUnmarshaler interface {
UnmarshalEnv(string) error UnmarshalEnv(string) error
} }
func envHasAtLeastAKeyWithPrefix(env map[string]string, prefix string) bool {
for key := range env {
if strings.HasPrefix(key, prefix) {
return true
}
}
return false
}
func loadEnvInternal(env map[string]string, prefix string, rv reflect.Value) error { func loadEnvInternal(env map[string]string, prefix string, rv reflect.Value) error {
rt := rv.Type() rt := rv.Type()
@ -148,6 +157,24 @@ func loadEnvInternal(env map[string]string, prefix string, rv reflect.Value) err
} }
return nil return nil
} }
if rt.Elem().Kind() == reflect.Struct {
for i := 0; ; i++ {
itemPrefix := prefix + "_" + strconv.FormatInt(int64(i), 10)
if !envHasAtLeastAKeyWithPrefix(env, itemPrefix) {
break
}
elem := reflect.New(rt.Elem())
err := loadEnvInternal(env, itemPrefix, elem.Elem())
if err != nil {
return err
}
rv.Set(reflect.Append(rv, elem.Elem()))
}
return nil
}
} }
return fmt.Errorf("unsupported type: %v", rt) return fmt.Errorf("unsupported type: %v", rt)

50
internal/conf/env/env_test.go vendored

@ -40,15 +40,22 @@ func (d *myDuration) UnmarshalEnv(s string) error {
return d.UnmarshalJSON([]byte(`"` + s + `"`)) return d.UnmarshalJSON([]byte(`"` + s + `"`))
} }
type mySubStruct struct {
URL string
Username string
Password string
}
type testStruct struct { type testStruct struct {
MyString string MyString string
MyInt int MyInt int
MyFloat float64 MyFloat float64
MyBool bool MyBool bool
MyDuration myDuration MyDuration myDuration
MyMap map[string]*mapEntry MyMap map[string]*mapEntry
MySlice []string MySlice []string
MySliceEmpty []string MySliceEmpty []string
MySliceSubStruct []mySubStruct
} }
func TestLoad(t *testing.T) { func TestLoad(t *testing.T) {
@ -82,6 +89,21 @@ func TestLoad(t *testing.T) {
os.Setenv("MYPREFIX_MYSLICEEMPTY", "") os.Setenv("MYPREFIX_MYSLICEEMPTY", "")
defer os.Unsetenv("MYPREFIX_MYSLICEEMPTY") defer os.Unsetenv("MYPREFIX_MYSLICEEMPTY")
os.Setenv("MYPREFIX_MYSLICESUBSTRUCT_0_URL", "url1")
defer os.Unsetenv("MYPREFIX_MYSLICESUBSTRUCT_0_URL")
os.Setenv("MYPREFIX_MYSLICESUBSTRUCT_0_USERNAME", "user1")
defer os.Unsetenv("MYPREFIX_MYSLICESUBSTRUCT_0_USERNAME")
os.Setenv("MYPREFIX_MYSLICESUBSTRUCT_0_PASSWORD", "pass1")
defer os.Unsetenv("MYPREFIX_MYSLICESUBSTRUCT_0_PASSWORD")
os.Setenv("MYPREFIX_MYSLICESUBSTRUCT_1_URL", "url2")
defer os.Unsetenv("MYPREFIX_MYSLICESUBSTRUCT_1_URL")
os.Setenv("MYPREFIX_MYSLICESUBSTRUCT_1_PASSWORD", "pass2")
defer os.Unsetenv("MYPREFIX_MYSLICESUBSTRUCT_1_PASSWORD")
var s testStruct var s testStruct
err := Load("MYPREFIX", &s) err := Load("MYPREFIX", &s)
require.NoError(t, err) require.NoError(t, err)
@ -102,4 +124,16 @@ func TestLoad(t *testing.T) {
require.Equal(t, []string{"val1", "val2"}, s.MySlice) require.Equal(t, []string{"val1", "val2"}, s.MySlice)
require.Equal(t, []string{}, s.MySliceEmpty) require.Equal(t, []string{}, s.MySliceEmpty)
require.Equal(t, []mySubStruct{
{
URL: "url1",
Username: "user1",
Password: "pass1",
},
{
URL: "url2",
Password: "pass2",
},
}, s.MySliceSubStruct)
} }

0
internal/conf/hlsvariant.go → internal/conf/hls_variant.go

0
internal/conf/ipsorcidrs.go → internal/conf/ips_or_cidrs.go

0
internal/conf/logdestination.go → internal/conf/log_destination.go

0
internal/conf/loglevel.go → internal/conf/log_level.go

0
internal/conf/rtsprangetype.go → internal/conf/rtsp_range_type.go

0
internal/conf/sourceprotocol.go → internal/conf/source_protocol.go

0
internal/conf/stringduration.go → internal/conf/string_duration.go

0
internal/conf/stringsize.go → internal/conf/string_size.go

8
internal/conf/webrtc_ice_server.go

@ -0,0 +1,8 @@
package conf
// WebRTCICEServer is a WebRTC ICE Server.
type WebRTCICEServer struct {
URL string `json:"url"`
Username string `json:"username"`
Password string `json:"password"`
}

4
internal/core/core.go

@ -424,7 +424,7 @@ func (p *Core) createResources(initial bool) error {
p.conf.WebRTCServerCert, p.conf.WebRTCServerCert,
p.conf.WebRTCAllowOrigin, p.conf.WebRTCAllowOrigin,
p.conf.WebRTCTrustedProxies, p.conf.WebRTCTrustedProxies,
p.conf.WebRTCICEServers, p.conf.WebRTCICEServers2,
p.conf.ReadTimeout, p.conf.ReadTimeout,
p.conf.ReadBufferCount, p.conf.ReadBufferCount,
p.pathManager, p.pathManager,
@ -594,7 +594,7 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {
newConf.WebRTCServerCert != p.conf.WebRTCServerCert || newConf.WebRTCServerCert != p.conf.WebRTCServerCert ||
newConf.WebRTCAllowOrigin != p.conf.WebRTCAllowOrigin || newConf.WebRTCAllowOrigin != p.conf.WebRTCAllowOrigin ||
!reflect.DeepEqual(newConf.WebRTCTrustedProxies, p.conf.WebRTCTrustedProxies) || !reflect.DeepEqual(newConf.WebRTCTrustedProxies, p.conf.WebRTCTrustedProxies) ||
!reflect.DeepEqual(newConf.WebRTCICEServers, p.conf.WebRTCICEServers) || !reflect.DeepEqual(newConf.WebRTCICEServers2, p.conf.WebRTCICEServers2) ||
newConf.ReadTimeout != p.conf.ReadTimeout || newConf.ReadTimeout != p.conf.ReadTimeout ||
newConf.ReadBufferCount != p.conf.ReadBufferCount || newConf.ReadBufferCount != p.conf.ReadBufferCount ||
closeMetrics || closeMetrics ||

61
internal/core/webrtc_http_server.go

@ -2,10 +2,12 @@ package core
import ( import (
_ "embed" _ "embed"
"encoding/json"
"fmt" "fmt"
"io" "io"
"net" "net"
"net/http" "net/http"
"regexp"
"strconv" "strconv"
"strings" "strings"
@ -24,6 +26,59 @@ var webrtcPublishIndex []byte
//go:embed webrtc_read_index.html //go:embed webrtc_read_index.html
var webrtcReadIndex []byte var webrtcReadIndex []byte
func quoteCredential(v string) string {
b, _ := json.Marshal(v)
s := string(b)
return s[1 : len(s)-1]
}
func unquoteCredential(v string) string {
var s string
json.Unmarshal([]byte("\""+v+"\""), &s)
return s
}
func iceServersToLinkHeader(iceServers []webrtc.ICEServer) []string {
ret := make([]string, len(iceServers))
for i, server := range iceServers {
link := "<" + server.URLs[0] + ">; rel=\"ice-server\""
if server.Username != "" {
link += "; username=\"" + quoteCredential(server.Username) + "\"" +
"; credential=\"" + quoteCredential(server.Credential.(string)) + "\"; credential-type=\"password\""
}
ret[i] = link
}
return ret
}
var reLink = regexp.MustCompile(`^<(.+?)>; rel="ice-server"(; username="(.+?)"` +
`; credential="(.+?)"; credential-type="password")?`)
func linkHeaderToIceServers(link []string) []webrtc.ICEServer {
var ret []webrtc.ICEServer
for _, li := range link {
m := reLink.FindStringSubmatch(li)
if m != nil {
s := webrtc.ICEServer{
URLs: []string{m[1]},
}
if m[3] != "" {
s.Username = unquoteCredential(m[3])
s.Credential = unquoteCredential(m[4])
s.CredentialType = webrtc.ICECredentialTypePassword
}
ret = append(ret, s)
}
}
return ret
}
func unmarshalICEFragment(buf []byte) ([]*webrtc.ICECandidateInit, error) { func unmarshalICEFragment(buf []byte) ([]*webrtc.ICECandidateInit, error) {
buf = append([]byte("v=0\r\no=- 0 0 IN IP4 0.0.0.0\r\ns=-\r\nt=0 0\r\n"), buf...) buf = append([]byte("v=0\r\no=- 0 0 IN IP4 0.0.0.0\r\ns=-\r\nt=0 0\r\n"), buf...)
@ -104,7 +159,7 @@ func marshalICEFragment(offer *webrtc.SessionDescription, candidates []*webrtc.I
type webRTCHTTPServerParent interface { type webRTCHTTPServerParent interface {
logger.Writer logger.Writer
genICEServers() []webrtc.ICEServer generateICEServers() []webrtc.ICEServer
sessionNew(req webRTCSessionNewReq) webRTCSessionNewRes sessionNew(req webRTCSessionNewReq) webRTCSessionNewRes
sessionAddCandidates(req webRTCSessionAddCandidatesReq) webRTCSessionAddCandidatesRes sessionAddCandidates(req webRTCSessionAddCandidatesReq) webRTCSessionAddCandidatesRes
} }
@ -282,7 +337,7 @@ func (s *webRTCHTTPServer) onRequest(ctx *gin.Context) {
case http.MethodOptions: case http.MethodOptions:
ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PATCH") ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PATCH")
ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, If-Match") ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, If-Match")
ctx.Writer.Header()["Link"] = iceServersToLinkHeader(s.parent.genICEServers()) ctx.Writer.Header()["Link"] = iceServersToLinkHeader(s.parent.generateICEServers())
ctx.Writer.WriteHeader(http.StatusOK) ctx.Writer.WriteHeader(http.StatusOK)
case http.MethodPost: case http.MethodPost:
@ -314,7 +369,7 @@ func (s *webRTCHTTPServer) onRequest(ctx *gin.Context) {
ctx.Writer.Header().Set("E-Tag", res.sx.secret.String()) ctx.Writer.Header().Set("E-Tag", res.sx.secret.String())
ctx.Writer.Header().Set("ID", res.sx.uuid.String()) ctx.Writer.Header().Set("ID", res.sx.uuid.String())
ctx.Writer.Header().Set("Accept-Patch", "application/trickle-ice-sdpfrag") ctx.Writer.Header().Set("Accept-Patch", "application/trickle-ice-sdpfrag")
ctx.Writer.Header()["Link"] = iceServersToLinkHeader(s.parent.genICEServers()) ctx.Writer.Header()["Link"] = iceServersToLinkHeader(s.parent.generateICEServers())
ctx.Writer.Header().Set("Location", ctx.Request.URL.String()) ctx.Writer.Header().Set("Location", ctx.Request.URL.String())
ctx.Writer.WriteHeader(http.StatusCreated) ctx.Writer.WriteHeader(http.StatusCreated)
ctx.Writer.Write(res.answer) ctx.Writer.Write(res.answer)

95
internal/core/webrtc_manager.go

@ -9,10 +9,8 @@ import (
"fmt" "fmt"
"net" "net"
"net/http" "net/http"
"regexp"
"sort" "sort"
"strconv" "strconv"
"strings"
"sync" "sync"
"time" "time"
@ -24,46 +22,13 @@ import (
"github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/logger"
) )
func iceServersToLinkHeader(iceServers []webrtc.ICEServer) []string { const (
ret := make([]string, len(iceServers)) webrtcHandshakeTimeout = 10 * time.Second
webrtcTrackGatherTimeout = 2 * time.Second
for i, server := range iceServers { webrtcPayloadMaxSize = 1188 // 1200 - 12 (RTP header)
link := "<" + server.URLs[0] + ">; rel=\"ice-server\"" webrtcStreamID = "mediamtx"
if server.Username != "" { webrtcTurnSecretExpiration = 24 * 3600 * time.Second
link += "; username=\"" + server.Username + "\"" + )
"; credential=\"" + server.Credential.(string) + "\"; credential-type=\"password\""
}
ret[i] = link
}
return ret
}
var reLink = regexp.MustCompile(`^<(.+?)>; rel="ice-server"(; username="(.+?)"` +
`; credential="(.+?)"; credential-type="password")?`)
func linkHeaderToIceServers(link []string) []webrtc.ICEServer {
var ret []webrtc.ICEServer
for _, li := range link {
m := reLink.FindStringSubmatch(li)
if m != nil {
s := webrtc.ICEServer{
URLs: []string{m[1]},
}
if m[3] != "" {
s.Username = m[3]
s.Credential = m[4]
s.CredentialType = webrtc.ICECredentialTypePassword
}
ret = append(ret, s)
}
}
return ret
}
func randInt63() int64 { func randInt63() int64 {
var b [8]byte var b [8]byte
@ -155,7 +120,7 @@ type webRTCManagerParent interface {
type webRTCManager struct { type webRTCManager struct {
allowOrigin string allowOrigin string
trustedProxies conf.IPsOrCIDRs trustedProxies conf.IPsOrCIDRs
iceServers []string iceServers []conf.WebRTCICEServer
readBufferCount int readBufferCount int
pathManager *pathManager pathManager *pathManager
metrics *metrics metrics *metrics
@ -192,7 +157,7 @@ func newWebRTCManager(
serverCert string, serverCert string,
allowOrigin string, allowOrigin string,
trustedProxies conf.IPsOrCIDRs, trustedProxies conf.IPsOrCIDRs,
iceServers []string, iceServers []conf.WebRTCICEServer,
readTimeout conf.StringDuration, readTimeout conf.StringDuration,
readBufferCount int, readBufferCount int,
pathManager *pathManager, pathManager *pathManager,
@ -395,35 +360,21 @@ func (m *webRTCManager) findSessionByUUID(uuid uuid.UUID) *webRTCSession {
return nil return nil
} }
func (m *webRTCManager) genICEServers() []webrtc.ICEServer { func (m *webRTCManager) generateICEServers() []webrtc.ICEServer {
ret := make([]webrtc.ICEServer, len(m.iceServers)) ret := make([]webrtc.ICEServer, len(m.iceServers))
for i, s := range m.iceServers { for i, server := range m.iceServers {
parts := strings.Split(s, ":") if server.Username == "AUTH_SECRET" {
if len(parts) == 5 { expireDate := time.Now().Add(webrtcTurnSecretExpiration).Unix()
if parts[1] == "AUTH_SECRET" { server.Username = strconv.FormatInt(expireDate, 10) + ":" + randomTurnUser()
s := webrtc.ICEServer{ h := hmac.New(sha1.New, []byte(server.Password))
URLs: []string{parts[0] + ":" + parts[3] + ":" + parts[4]}, h.Write([]byte(server.Username))
} server.Password = base64.StdEncoding.EncodeToString(h.Sum(nil))
}
expireDate := time.Now().Add(24 * 3600 * time.Second).Unix()
s.Username = strconv.FormatInt(expireDate, 10) + ":" + randomTurnUser() ret[i] = webrtc.ICEServer{
URLs: []string{server.URL},
h := hmac.New(sha1.New, []byte(parts[2])) Username: server.Username,
h.Write([]byte(s.Username)) Credential: server.Password,
s.Credential = base64.StdEncoding.EncodeToString(h.Sum(nil))
ret[i] = s
} else {
ret[i] = webrtc.ICEServer{
URLs: []string{parts[0] + ":" + parts[3] + ":" + parts[4]},
Username: parts[1],
Credential: parts[2],
}
}
} else {
ret[i] = webrtc.ICEServer{
URLs: []string{s},
}
} }
} }
return ret return ret

8
internal/core/webrtc_publish_index.html

@ -118,6 +118,10 @@ const setState = (newState) => {
const restartPause = 2000; const restartPause = 2000;
const unquoteCredential = (v) => (
JSON.parse(`"${v}"`)
);
const linkToIceServers = (links) => ( const linkToIceServers = (links) => (
(links !== null) ? links.split(', ').map((link) => { (links !== null) ? links.split(', ').map((link) => {
const m = link.match(/^<(.+?)>; rel="ice-server"(; username="(.*?)"; credential="(.*?)"; credential-type="password")?/i); const m = link.match(/^<(.+?)>; rel="ice-server"(; username="(.*?)"; credential="(.*?)"; credential-type="password")?/i);
@ -126,8 +130,8 @@ const linkToIceServers = (links) => (
}; };
if (m[3] !== undefined) { if (m[3] !== undefined) {
ret.username = m[3]; ret.username = unquoteCredential(m[3]);
ret.credential = m[4]; ret.credential = unquoteCredential(m[4]);
ret.credentialType = "password"; ret.credentialType = "password";
} }

8
internal/core/webrtc_read_index.html

@ -25,6 +25,10 @@ html, body {
const restartPause = 2000; const restartPause = 2000;
const unquoteCredential = (v) => (
JSON.parse(`"${v}"`)
);
const linkToIceServers = (links) => ( const linkToIceServers = (links) => (
(links !== null) ? links.split(', ').map((link) => { (links !== null) ? links.split(', ').map((link) => {
const m = link.match(/^<(.+?)>; rel="ice-server"(; username="(.*?)"; credential="(.*?)"; credential-type="password")?/i); const m = link.match(/^<(.+?)>; rel="ice-server"(; username="(.*?)"; credential="(.*?)"; credential-type="password")?/i);
@ -33,8 +37,8 @@ const linkToIceServers = (links) => (
}; };
if (m[3] !== undefined) { if (m[3] !== undefined) {
ret.username = m[3]; ret.username = unquoteCredential(m[3]);
ret.credential = m[4]; ret.credential = unquoteCredential(m[4]);
ret.credentialType = "password"; ret.credentialType = "password";
} }

11
internal/core/webrtc_session.go

@ -18,13 +18,6 @@ import (
"github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/logger"
) )
const (
webrtcHandshakeTimeout = 10 * time.Second
webrtcTrackGatherTimeout = 2 * time.Second
webrtcPayloadMaxSize = 1188 // 1200 - 12 (RTP header)
webrtcStreamID = "mediamtx"
)
type trackRecvPair struct { type trackRecvPair struct {
track *webrtc.TrackRemote track *webrtc.TrackRemote
receiver *webrtc.RTPReceiver receiver *webrtc.RTPReceiver
@ -235,7 +228,7 @@ func (s *webRTCSession) runPublish() (int, error) {
defer res.path.publisherRemove(pathPublisherRemoveReq{author: s}) defer res.path.publisherRemove(pathPublisherRemoveReq{author: s})
pc, err := newPeerConnection( pc, err := newPeerConnection(
s.parent.genICEServers(), s.parent.generateICEServers(),
s.iceHostNAT1To1IPs, s.iceHostNAT1To1IPs,
s.iceUDPMux, s.iceUDPMux,
s.iceTCPMux, s.iceTCPMux,
@ -357,7 +350,7 @@ func (s *webRTCSession) runRead() (int, error) {
} }
pc, err := newPeerConnection( pc, err := newPeerConnection(
s.parent.genICEServers(), s.parent.generateICEServers(),
s.iceHostNAT1To1IPs, s.iceHostNAT1To1IPs,
s.iceUDPMux, s.iceUDPMux,
s.iceTCPMux, s.iceTCPMux,

21
mediamtx.yml

@ -200,15 +200,18 @@ webrtcAllowOrigin: '*'
# If the server receives a request from one of these entries, IP in logs # If the server receives a request from one of these entries, IP in logs
# will be taken from the X-Forwarded-For header. # will be taken from the X-Forwarded-For header.
webrtcTrustedProxies: [] webrtcTrustedProxies: []
# List of ICE servers, in format type:user:password:host:port or type:host:port. # List of ICE servers.
# type can be "stun", "turn" or "turns". webrtcICEServers2:
# STUN servers are used to obtain the public IP of server and clients. They are # URL can point to a STUN, TURN or TURNS server.
# needed when server and clients are on different LANs. # STUN servers are used to obtain the public IP of server and clients. They are
# TURN servers are needed when a direct connection between server and clients # needed when server and clients are on different LANs.
# is not possible. All traffic is routed through the chosen TURN server. # TURN/TURNS servers are needed when a direct connection between server and
# if user is "AUTH_SECRET", then authentication is secret based. # clients is not possible. All traffic is routed through them.
# the secret must be inserted into the password field. - url: stun:stun.l.google.com:19302
webrtcICEServers: [stun:stun.l.google.com:19302] # if user is "AUTH_SECRET", then authentication is secret based.
# the secret must be inserted into the password field.
username: ''
password: ''
# List of public IP addresses that are to be used as a host. # List of public IP addresses that are to be used as a host.
# This is used typically for servers that are behind 1:1 D-NAT. # This is used typically for servers that are behind 1:1 D-NAT.
webrtcICEHostNAT1To1IPs: [] webrtcICEHostNAT1To1IPs: []

Loading…
Cancel
Save