From 500c5ef3504d817a8a8d1f57326b4b4282456184 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 30 May 2024 10:35:03 +0200 Subject: [PATCH 01/52] bump hls.js to v1.5.9 (#3407) Co-authored-by: mediamtx-bot --- internal/servers/hls/hlsjsdownloader/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/servers/hls/hlsjsdownloader/VERSION b/internal/servers/hls/hlsjsdownloader/VERSION index 1097babb..0fa909eb 100644 --- a/internal/servers/hls/hlsjsdownloader/VERSION +++ b/internal/servers/hls/hlsjsdownloader/VERSION @@ -1 +1 @@ -v1.5.8 +v1.5.9 From f6b0a72453da798e79c605fba1389784f802b2fb Mon Sep 17 00:00:00 2001 From: aler9 <46489434+aler9@users.noreply.github.com> Date: Thu, 30 May 2024 13:28:59 +0200 Subject: [PATCH 02/52] set server version when compiling from source too --- .dockerignore | 1 + .github/workflows/code_lint.yml | 2 +- .gitignore | 1 + internal/core/core.go | 2 +- internal/core/versiongetter/main.go | 52 +++++++++++++++++++++++++++++ scripts/binaries.mk | 15 ++++----- scripts/lint.mk | 2 +- 7 files changed, 64 insertions(+), 11 deletions(-) create mode 100644 internal/core/versiongetter/main.go diff --git a/.dockerignore b/.dockerignore index f327c60a..71d9f635 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,6 +3,7 @@ /binaries /coverage*.txt /apidocs/*.html +/internal/core/version.go /internal/servers/hls/hls.min.js /internal/protocols/rpicamera/exe/text_font.h /internal/protocols/rpicamera/exe/exe diff --git a/.github/workflows/code_lint.yml b/.github/workflows/code_lint.yml index d9cafd22..ea7e7bd0 100644 --- a/.github/workflows/code_lint.yml +++ b/.github/workflows/code_lint.yml @@ -17,7 +17,7 @@ jobs: with: go-version: "1.22" - - run: touch internal/servers/hls/hls.min.js + - run: go generate ./... - uses: golangci/golangci-lint-action@v4 with: diff --git a/.gitignore b/.gitignore index 66f36121..73ffd9a1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ /binaries /coverage*.txt /apidocs/*.html +/internal/core/version.go /internal/servers/hls/hls.min.js /internal/protocols/rpicamera/exe/text_font.h /internal/protocols/rpicamera/exe/exe diff --git a/internal/core/core.go b/internal/core/core.go index 9f5f6135..2dd81949 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -34,7 +34,7 @@ import ( "github.com/bluenviron/mediamtx/internal/servers/webrtc" ) -var version = "v0.0.0" +//go:generate go run ./versiongetter var defaultConfPaths = []string{ "rtsp-simple-server.yml", diff --git a/internal/core/versiongetter/main.go b/internal/core/versiongetter/main.go new file mode 100644 index 00000000..c406edbc --- /dev/null +++ b/internal/core/versiongetter/main.go @@ -0,0 +1,52 @@ +// Package main contains an utility to get the server version +package main + +import ( + "bytes" + "html/template" + "log" + "os" + "os/exec" +) + +var tpl = template.Must(template.New("").Parse( + `// autogenerated:yes +package core + +const version = "{{ .Version }}" +`)) + +func do() error { + log.Println("getting version...") + + stdout, err := exec.Command("git", "describe", "--tags").Output() + if err != nil { + return err + } + + version := string(stdout[:len(stdout)-1]) + + var buf bytes.Buffer + err = tpl.Execute(&buf, map[string]interface{}{ + "Version": version, + }) + if err != nil { + return err + } + + err = os.WriteFile("version.go", buf.Bytes(), 0o644) + if err != nil { + return err + } + + log.Println("ok") + return nil +} + +func main() { + err := do() + if err != nil { + log.Printf("ERR: %v", err) + os.Exit(1) + } +} diff --git a/scripts/binaries.mk b/scripts/binaries.mk index c9693fc3..d3252766 100644 --- a/scripts/binaries.mk +++ b/scripts/binaries.mk @@ -29,36 +29,36 @@ RUN cp mediamtx.yml LICENSE tmp/ RUN go generate ./... FROM build-base AS build-windows-amd64 -RUN GOOS=windows GOARCH=amd64 go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME).exe +RUN GOOS=windows GOARCH=amd64 go build -o tmp/$(BINARY_NAME).exe RUN cd tmp && zip -q ../binaries/$(BINARY_NAME)_$${VERSION}_windows_amd64.zip $(BINARY_NAME).exe mediamtx.yml LICENSE FROM build-base AS build-linux-amd64 -RUN GOOS=linux GOARCH=amd64 go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME) +RUN GOOS=linux GOARCH=amd64 go build -o tmp/$(BINARY_NAME) RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_linux_amd64.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE FROM build-base AS build-darwin-amd64 -RUN GOOS=darwin GOARCH=amd64 go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME) +RUN GOOS=darwin GOARCH=amd64 go build -o tmp/$(BINARY_NAME) RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_darwin_amd64.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE FROM build-base AS build-darwin-arm64 -RUN GOOS=darwin GOARCH=arm64 go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME) +RUN GOOS=darwin GOARCH=arm64 go build -o tmp/$(BINARY_NAME) RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_darwin_arm64.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE FROM build-base AS build-linux-armv6 COPY --from=rpicamera32 /s/internal/protocols/rpicamera/exe/exe internal/protocols/rpicamera/exe/ -RUN GOOS=linux GOARCH=arm GOARM=6 go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME) -tags rpicamera +RUN GOOS=linux GOARCH=arm GOARM=6 go build -o tmp/$(BINARY_NAME) -tags rpicamera RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_linux_armv6.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE RUN rm internal/protocols/rpicamera/exe/exe FROM build-base AS build-linux-armv7 COPY --from=rpicamera32 /s/internal/protocols/rpicamera/exe/exe internal/protocols/rpicamera/exe/ -RUN GOOS=linux GOARCH=arm GOARM=7 go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME) -tags rpicamera +RUN GOOS=linux GOARCH=arm GOARM=7 go build -o tmp/$(BINARY_NAME) -tags rpicamera RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_linux_armv7.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE RUN rm internal/protocols/rpicamera/exe/exe FROM build-base AS build-linux-arm64 COPY --from=rpicamera64 /s/internal/protocols/rpicamera/exe/exe internal/protocols/rpicamera/exe/ -RUN GOOS=linux GOARCH=arm64 go build -ldflags "-X github.com/bluenviron/mediamtx/internal/core.version=$$VERSION" -o tmp/$(BINARY_NAME) -tags rpicamera +RUN GOOS=linux GOARCH=arm64 go build -o tmp/$(BINARY_NAME) -tags rpicamera RUN tar -C tmp -czf binaries/$(BINARY_NAME)_$${VERSION}_linux_arm64v8.tar.gz --owner=0 --group=0 $(BINARY_NAME) mediamtx.yml LICENSE RUN rm internal/protocols/rpicamera/exe/exe @@ -75,7 +75,6 @@ export DOCKERFILE_BINARIES binaries: echo "$$DOCKERFILE_BINARIES" | DOCKER_BUILDKIT=1 docker build . -f - \ - --build-arg VERSION=$$(git describe --tags) \ -t temp docker run --rm -v $(PWD):/out \ temp sh -c "rm -rf /out/binaries && cp -r /s/binaries /out/" diff --git a/scripts/lint.mk b/scripts/lint.mk index 5245ac8c..5b03fcea 100644 --- a/scripts/lint.mk +++ b/scripts/lint.mk @@ -1,5 +1,5 @@ lint: - touch internal/servers/hls/hls.min.js + go generate ./... docker run --rm -v $(PWD):/app -w /app \ $(LINT_IMAGE) \ golangci-lint run -v From 1c2f95f6096a975ebc275e43e11c5a0189d6cba0 Mon Sep 17 00:00:00 2001 From: Jason Walton Date: Thu, 30 May 2024 07:36:58 -0400 Subject: [PATCH 03/52] webrtc: allow configuring timeouts (#3404) (#3406) * webrtc: allow configuring timeouts (#3404) * fix from code inspect --- apidocs/openapi.yaml | 4 +++ internal/conf/conf.go | 4 +++ internal/core/core.go | 4 +++ internal/protocols/webrtc/peer_connection.go | 11 ++++--- .../protocols/webrtc/peer_connection_test.go | 11 ++++--- internal/protocols/webrtc/whip_client.go | 32 ++++++++++++------- internal/servers/webrtc/server.go | 2 ++ internal/servers/webrtc/server_test.go | 14 ++++++-- internal/servers/webrtc/session.go | 4 +++ internal/staticsources/webrtc/source_test.go | 12 ++++--- mediamtx.yml | 4 +++ 11 files changed, 74 insertions(+), 28 deletions(-) diff --git a/apidocs/openapi.yaml b/apidocs/openapi.yaml index d4f6e9f0..93802e49 100644 --- a/apidocs/openapi.yaml +++ b/apidocs/openapi.yaml @@ -246,6 +246,10 @@ components: type: string clientOnly: type: boolean + webrtcHandshakeTimeout: + type: string + webrtcTrackGatherTimeout: + type: string # SRT server srt: diff --git a/internal/conf/conf.go b/internal/conf/conf.go index de97c202..e70da0dc 100644 --- a/internal/conf/conf.go +++ b/internal/conf/conf.go @@ -238,6 +238,8 @@ type Conf struct { WebRTCIPsFromInterfacesList []string `json:"webrtcIPsFromInterfacesList"` WebRTCAdditionalHosts []string `json:"webrtcAdditionalHosts"` WebRTCICEServers2 WebRTCICEServers `json:"webrtcICEServers2"` + WebRTCHandshakeTimeout StringDuration `json:"webrtcHandshakeTimeout"` + WebRTCTrackGatherTimeout StringDuration `json:"webrtcTrackGatherTimeout"` WebRTCICEUDPMuxAddress *string `json:"webrtcICEUDPMuxAddress,omitempty"` // deprecated WebRTCICETCPMuxAddress *string `json:"webrtcICETCPMuxAddress,omitempty"` // deprecated WebRTCICEHostNAT1To1IPs *[]string `json:"webrtcICEHostNAT1To1IPs,omitempty"` // deprecated @@ -392,6 +394,8 @@ func (conf *Conf) setDefaults() { conf.WebRTCIPsFromInterfacesList = []string{} conf.WebRTCAdditionalHosts = []string{} conf.WebRTCICEServers2 = []WebRTCICEServer{} + conf.WebRTCHandshakeTimeout = 10 * StringDuration(time.Second) + conf.WebRTCTrackGatherTimeout = 2 * StringDuration(time.Second) // SRT server conf.SRT = true diff --git a/internal/core/core.go b/internal/core/core.go index 9f5f6135..2eeadcdb 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -580,6 +580,8 @@ func (p *Core) createResources(initial bool) error { IPsFromInterfacesList: p.conf.WebRTCIPsFromInterfacesList, AdditionalHosts: p.conf.WebRTCAdditionalHosts, ICEServers: p.conf.WebRTCICEServers2, + HandshakeTimeout: p.conf.WebRTCHandshakeTimeout, + TrackGatherTimeout: p.conf.WebRTCTrackGatherTimeout, ExternalCmdPool: p.externalCmdPool, PathManager: p.pathManager, Parent: p, @@ -848,6 +850,8 @@ func (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) { !reflect.DeepEqual(newConf.WebRTCIPsFromInterfacesList, p.conf.WebRTCIPsFromInterfacesList) || !reflect.DeepEqual(newConf.WebRTCAdditionalHosts, p.conf.WebRTCAdditionalHosts) || !reflect.DeepEqual(newConf.WebRTCICEServers2, p.conf.WebRTCICEServers2) || + newConf.WebRTCHandshakeTimeout != p.conf.WebRTCHandshakeTimeout || + newConf.WebRTCTrackGatherTimeout != p.conf.WebRTCTrackGatherTimeout || closeMetrics || closePathManager || closeLogger diff --git a/internal/protocols/webrtc/peer_connection.go b/internal/protocols/webrtc/peer_connection.go index ca8b3165..bb984fd7 100644 --- a/internal/protocols/webrtc/peer_connection.go +++ b/internal/protocols/webrtc/peer_connection.go @@ -11,13 +11,12 @@ import ( "github.com/pion/interceptor" "github.com/pion/webrtc/v3" + "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/logger" ) const ( - webrtcHandshakeTimeout = 10 * time.Second - webrtcTrackGatherTimeout = 2 * time.Second - webrtcStreamID = "mediamtx" + webrtcStreamID = "mediamtx" ) func stringInSlice(a string, list []string) bool { @@ -39,6 +38,8 @@ type PeerConnection struct { ICEServers []webrtc.ICEServer ICEUDPMux ice.UDPMux ICETCPMux ice.TCPMux + HandshakeTimeout conf.StringDuration + TrackGatherTimeout conf.StringDuration LocalRandomUDP bool IPsFromInterfaces bool IPsFromInterfacesList []string @@ -312,7 +313,7 @@ func (co *PeerConnection) WaitGatheringDone(ctx context.Context) error { func (co *PeerConnection) WaitUntilConnected( ctx context.Context, ) error { - t := time.NewTimer(webrtcHandshakeTimeout) + t := time.NewTimer(time.Duration(co.HandshakeTimeout)) defer t.Stop() outer: @@ -339,7 +340,7 @@ func (co *PeerConnection) GatherIncomingTracks( ) ([]*IncomingTrack, error) { var tracks []*IncomingTrack - t := time.NewTimer(webrtcTrackGatherTimeout) + t := time.NewTimer(time.Duration(co.TrackGatherTimeout)) defer t.Stop() for { diff --git a/internal/protocols/webrtc/peer_connection_test.go b/internal/protocols/webrtc/peer_connection_test.go index d4d0fa7c..b9ed8ba8 100644 --- a/internal/protocols/webrtc/peer_connection_test.go +++ b/internal/protocols/webrtc/peer_connection_test.go @@ -4,16 +4,19 @@ import ( "testing" "time" + "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/test" "github.com/stretchr/testify/require" ) func TestPeerConnectionCloseAfterError(t *testing.T) { pc := &PeerConnection{ - LocalRandomUDP: true, - IPsFromInterfaces: true, - Publish: false, - Log: test.NilLogger, + HandshakeTimeout: conf.StringDuration(10 * time.Second), + TrackGatherTimeout: conf.StringDuration(2 * time.Second), + LocalRandomUDP: true, + IPsFromInterfaces: true, + Publish: false, + Log: test.NilLogger, } err := pc.Start() require.NoError(t, err) diff --git a/internal/protocols/webrtc/whip_client.go b/internal/protocols/webrtc/whip_client.go index 40511200..28c8cd0f 100644 --- a/internal/protocols/webrtc/whip_client.go +++ b/internal/protocols/webrtc/whip_client.go @@ -13,10 +13,16 @@ import ( "github.com/pion/sdp/v3" "github.com/pion/webrtc/v3" + "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/protocols/httpp" ) +const ( + webrtcHandshakeTimeout = 10 * time.Second + webrtcTrackGatherTimeout = 2 * time.Second +) + // WHIPClient is a WHIP client. type WHIPClient struct { HTTPClient *http.Client @@ -48,12 +54,14 @@ func (c *WHIPClient) Publish( } c.pc = &PeerConnection{ - ICEServers: iceServers, - LocalRandomUDP: true, - IPsFromInterfaces: true, - Publish: true, - OutgoingTracks: outgoingTracks, - Log: c.Log, + ICEServers: iceServers, + HandshakeTimeout: conf.StringDuration(10 * time.Second), + TrackGatherTimeout: conf.StringDuration(2 * time.Second), + LocalRandomUDP: true, + IPsFromInterfaces: true, + Publish: true, + OutgoingTracks: outgoingTracks, + Log: c.Log, } err = c.pc.Start() if err != nil { @@ -122,11 +130,13 @@ func (c *WHIPClient) Read(ctx context.Context) ([]*IncomingTrack, error) { } c.pc = &PeerConnection{ - ICEServers: iceServers, - LocalRandomUDP: true, - IPsFromInterfaces: true, - Publish: false, - Log: c.Log, + ICEServers: iceServers, + HandshakeTimeout: conf.StringDuration(10 * time.Second), + TrackGatherTimeout: conf.StringDuration(2 * time.Second), + LocalRandomUDP: true, + IPsFromInterfaces: true, + Publish: false, + Log: c.Log, } err = c.pc.Start() if err != nil { diff --git a/internal/servers/webrtc/server.go b/internal/servers/webrtc/server.go index ba855eb6..a5392032 100644 --- a/internal/servers/webrtc/server.go +++ b/internal/servers/webrtc/server.go @@ -192,6 +192,8 @@ type Server struct { IPsFromInterfacesList []string AdditionalHosts []string ICEServers []conf.WebRTCICEServer + HandshakeTimeout conf.StringDuration + TrackGatherTimeout conf.StringDuration ExternalCmdPool *externalcmd.Pool PathManager serverPathManager Parent serverParent diff --git a/internal/servers/webrtc/server_test.go b/internal/servers/webrtc/server_test.go index 197917db..38727792 100644 --- a/internal/servers/webrtc/server_test.go +++ b/internal/servers/webrtc/server_test.go @@ -117,6 +117,8 @@ func initializeTestServer(t *testing.T) *Server { IPsFromInterfacesList: []string{}, AdditionalHosts: []string{}, ICEServers: []conf.WebRTCICEServer{}, + HandshakeTimeout: conf.StringDuration(10 * time.Second), + TrackGatherTimeout: conf.StringDuration(2 * time.Second), ExternalCmdPool: nil, PathManager: pathManager, Parent: test.NilLogger, @@ -195,9 +197,11 @@ func TestServerOptionsICEServer(t *testing.T) { Username: "myuser", Password: "mypass", }}, - ExternalCmdPool: nil, - PathManager: pathManager, - Parent: test.NilLogger, + HandshakeTimeout: conf.StringDuration(10 * time.Second), + TrackGatherTimeout: conf.StringDuration(2 * time.Second), + ExternalCmdPool: nil, + PathManager: pathManager, + Parent: test.NilLogger, } err := s.Initialize() require.NoError(t, err) @@ -249,6 +253,8 @@ func TestServerPublish(t *testing.T) { IPsFromInterfacesList: []string{}, AdditionalHosts: []string{}, ICEServers: []conf.WebRTCICEServer{}, + HandshakeTimeout: conf.StringDuration(10 * time.Second), + TrackGatherTimeout: conf.StringDuration(2 * time.Second), ExternalCmdPool: nil, PathManager: pathManager, Parent: test.NilLogger, @@ -359,6 +365,8 @@ func TestServerRead(t *testing.T) { IPsFromInterfacesList: []string{}, AdditionalHosts: []string{}, ICEServers: []conf.WebRTCICEServer{}, + HandshakeTimeout: conf.StringDuration(10 * time.Second), + TrackGatherTimeout: conf.StringDuration(2 * time.Second), ExternalCmdPool: nil, PathManager: pathManager, Parent: test.NilLogger, diff --git a/internal/servers/webrtc/session.go b/internal/servers/webrtc/session.go index cb98580d..e921a4b0 100644 --- a/internal/servers/webrtc/session.go +++ b/internal/servers/webrtc/session.go @@ -409,6 +409,8 @@ func (s *session) runPublish() (int, error) { pc := &webrtc.PeerConnection{ ICEServers: iceServers, + HandshakeTimeout: s.parent.HandshakeTimeout, + TrackGatherTimeout: s.parent.TrackGatherTimeout, IPsFromInterfaces: s.ipsFromInterfaces, IPsFromInterfacesList: s.ipsFromInterfacesList, AdditionalHosts: s.additionalHosts, @@ -566,6 +568,8 @@ func (s *session) runRead() (int, error) { pc := &webrtc.PeerConnection{ ICEServers: iceServers, + HandshakeTimeout: s.parent.HandshakeTimeout, + TrackGatherTimeout: s.parent.TrackGatherTimeout, IPsFromInterfaces: s.ipsFromInterfaces, IPsFromInterfacesList: s.ipsFromInterfacesList, AdditionalHosts: s.additionalHosts, diff --git a/internal/staticsources/webrtc/source_test.go b/internal/staticsources/webrtc/source_test.go index ef809dd3..20edf3e7 100644 --- a/internal/staticsources/webrtc/source_test.go +++ b/internal/staticsources/webrtc/source_test.go @@ -32,11 +32,13 @@ func TestSource(t *testing.T) { ChannelCount: 2, }}} pc := &webrtc.PeerConnection{ - LocalRandomUDP: true, - IPsFromInterfaces: true, - Publish: true, - OutgoingTracks: outgoingTracks, - Log: test.NilLogger, + LocalRandomUDP: true, + IPsFromInterfaces: true, + Publish: true, + HandshakeTimeout: conf.StringDuration(10 * time.Second), + TrackGatherTimeout: conf.StringDuration(2 * time.Second), + OutgoingTracks: outgoingTracks, + Log: test.NilLogger, } err := pc.Start() require.NoError(t, err) diff --git a/mediamtx.yml b/mediamtx.yml index cdc45cb0..29468381 100644 --- a/mediamtx.yml +++ b/mediamtx.yml @@ -381,6 +381,10 @@ webrtcICEServers2: [] # username: '' # password: '' # clientOnly: false +# Time to wait for the WebRTC handshake to complete. +webrtcHandshakeTimeout: 10s +# Maximum time to gather video tracks. +webrtcTrackGatherTimeout: 2s ############################################### # Global settings -> SRT server From f4b7f147a52f15d13ba00dde5a0335b3031c583f Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Thu, 30 May 2024 14:29:05 +0200 Subject: [PATCH 04/52] api, playback: add CORS headers on non-existing pages too (#1792) (#3410) --- internal/api/api.go | 1 + internal/playback/server.go | 3 +++ 2 files changed, 4 insertions(+) diff --git a/internal/api/api.go b/internal/api/api.go index 642b99cb..c86dc778 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -190,6 +190,7 @@ func (a *API) Initialize() error { router := gin.New() router.SetTrustedProxies(a.TrustedProxies.ToTrustedProxies()) //nolint:errcheck + router.NoRoute(a.middlewareOrigin, a.middlewareAuth) group := router.Group("/", a.middlewareOrigin, a.middlewareAuth) group.GET("/v3/config/global/get", a.onConfigGlobalGet) diff --git a/internal/playback/server.go b/internal/playback/server.go index 3ed4f33a..abea96e7 100644 --- a/internal/playback/server.go +++ b/internal/playback/server.go @@ -43,7 +43,10 @@ type Server struct { func (s *Server) Initialize() error { router := gin.New() router.SetTrustedProxies(s.TrustedProxies.ToTrustedProxies()) //nolint:errcheck + + router.NoRoute(s.middlewareOrigin) group := router.Group("/", s.middlewareOrigin) + group.GET("/list", s.onList) group.GET("/get", s.onGet) From a16ef00e888fc8778fb0145e86aa465990220210 Mon Sep 17 00:00:00 2001 From: aler9 <46489434+aler9@users.noreply.github.com> Date: Thu, 30 May 2024 23:00:12 +0200 Subject: [PATCH 05/52] temp --- .github/workflows/code_test.yml | 11 ++++++++--- internal/core/versiongetter/main.go | 11 +++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/.github/workflows/code_test.yml b/.github/workflows/code_test.yml index bb5a1409..42aebda0 100644 --- a/.github/workflows/code_test.yml +++ b/.github/workflows/code_test.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: make test @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - run: make test32 @@ -31,10 +31,15 @@ jobs: runs-on: ubuntu-22.04 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: actions/setup-go@v2 with: go-version: "1.22" + - name: GitHub Tag Name example + run: | + echo "Tag name from GITHUB_REF_NAME: $GITHUB_REF_NAME" + echo "Tag name from github.ref_name: ${{ github.ref_name }}" + - run: make test-highlevel-nodocker diff --git a/internal/core/versiongetter/main.go b/internal/core/versiongetter/main.go index c406edbc..80965223 100644 --- a/internal/core/versiongetter/main.go +++ b/internal/core/versiongetter/main.go @@ -3,6 +3,7 @@ package main import ( "bytes" + "fmt" "html/template" "log" "os" @@ -19,7 +20,17 @@ const version = "{{ .Version }}" func do() error { log.Println("getting version...") + temp, _ := exec.Command("git", "status").CombinedOutput() + /*if err != nil { + return err + }*/ + + fmt.Println(string(temp)) + stdout, err := exec.Command("git", "describe", "--tags").Output() + + fmt.Println(string(stdout)) + if err != nil { return err } From 6da8aee64f50157d98353a6f56be9692812c36f7 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Sun, 2 Jun 2024 22:14:36 +0200 Subject: [PATCH 06/52] webrtc: show error in case setRemoteDescription fails (#3416) --- internal/servers/webrtc/publish_index.html | 16 ++++++++++------ internal/servers/webrtc/read_index.html | 16 ++++++++++------ 2 files changed, 20 insertions(+), 12 deletions(-) diff --git a/internal/servers/webrtc/publish_index.html b/internal/servers/webrtc/publish_index.html index 82981175..8176a664 100644 --- a/internal/servers/webrtc/publish_index.html +++ b/internal/servers/webrtc/publish_index.html @@ -443,12 +443,16 @@ const onRemoteAnswer = (sdp) => { pc.setRemoteDescription(new RTCSessionDescription({ type: 'answer', sdp, - })); - - if (queuedCandidates.length !== 0) { - sendLocalCandidates(queuedCandidates); - queuedCandidates = []; - } + })) + .then(() => { + if (queuedCandidates.length !== 0) { + sendLocalCandidates(queuedCandidates); + queuedCandidates = []; + } + }) + .catch((err) => { + onError(err.toString()); + }); }; const sendOffer = (offer) => { diff --git a/internal/servers/webrtc/read_index.html b/internal/servers/webrtc/read_index.html index 4839eb87..78a2026c 100644 --- a/internal/servers/webrtc/read_index.html +++ b/internal/servers/webrtc/read_index.html @@ -254,12 +254,16 @@ const onRemoteAnswer = (sdp) => { pc.setRemoteDescription(new RTCSessionDescription({ type: 'answer', sdp, - })); - - if (queuedCandidates.length !== 0) { - sendLocalCandidates(queuedCandidates); - queuedCandidates = []; - } + })) + .then(() => { + if (queuedCandidates.length !== 0) { + sendLocalCandidates(queuedCandidates); + queuedCandidates = []; + } + }) + .catch((err) => { + onError(err.toString()); + }); }; const sendOffer = (offer) => { From ca6e1259fb61d7a116d840617d9f1cf13ce58562 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Sun, 2 Jun 2024 23:08:54 +0200 Subject: [PATCH 07/52] webrtc: support reading and proxying stereo PCMU/PCMA tracks (#3402) --- go.mod | 10 +- go.sum | 19 ++-- internal/protocols/webrtc/incoming_track.go | 86 +++++++++++++--- internal/protocols/webrtc/outgoing_track.go | 38 ++++++- internal/servers/webrtc/publish_index.html | 14 ++- internal/servers/webrtc/read_index.html | 104 ++++++++++++++++++-- internal/servers/webrtc/server_test.go | 2 +- internal/servers/webrtc/session.go | 12 --- 8 files changed, 221 insertions(+), 64 deletions(-) diff --git a/go.mod b/go.mod index 211b046f..abb7e09b 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/gorilla/websocket v1.5.1 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/matthewhartstonge/argon2 v1.0.0 - github.com/pion/ice/v2 v2.3.11 + github.com/pion/ice/v2 v2.3.24 github.com/pion/interceptor v0.1.29 github.com/pion/logging v0.2.2 github.com/pion/rtcp v1.2.14 @@ -58,12 +58,12 @@ require ( github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pion/datachannel v1.5.5 // indirect github.com/pion/dtls/v2 v2.2.7 // indirect - github.com/pion/mdns v0.0.9 // indirect + github.com/pion/mdns v0.0.12 // indirect github.com/pion/randutil v0.1.0 // indirect - github.com/pion/sctp v1.8.8 // indirect + github.com/pion/sctp v1.8.16 // indirect github.com/pion/srtp/v2 v2.0.18 // indirect github.com/pion/stun v0.6.1 // indirect - github.com/pion/transport/v2 v2.2.3 // indirect + github.com/pion/transport/v2 v2.2.4 // indirect github.com/pion/turn/v2 v2.1.3 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect @@ -81,4 +81,4 @@ replace code.cloudfoundry.org/bytefmt => github.com/cloudfoundry/bytefmt v0.0.0- replace github.com/pion/ice/v2 => github.com/aler9/ice/v2 v2.0.0-20231112223552-32d34dfcf3a1 -replace github.com/pion/webrtc/v3 => github.com/aler9/webrtc/v3 v3.0.0-20231112223655-e402ed2689c6 +replace github.com/pion/webrtc/v3 => github.com/aler9/webrtc/v3 v3.0.0-20240527213244-d3ff0bd9230f diff --git a/go.sum b/go.sum index 8ef3b0a0..4f5183ca 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,8 @@ github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/aler9/ice/v2 v2.0.0-20231112223552-32d34dfcf3a1 h1:fD6eZt+3/t8bzFn6ZZA2eP63xBP06v3EPfPJu8DO8ys= github.com/aler9/ice/v2 v2.0.0-20231112223552-32d34dfcf3a1/go.mod h1:lT3kv5uUIlHfXHU/ZRD7uKD/ufM202+eTa3C/umgGf4= -github.com/aler9/webrtc/v3 v3.0.0-20231112223655-e402ed2689c6 h1:wMd3D1mLghoYYh31STig8Kwm2qi8QyQKUy09qUUZrVw= -github.com/aler9/webrtc/v3 v3.0.0-20231112223655-e402ed2689c6/go.mod h1:1CaT2fcZzZ6VZA+O1i9yK2DU4EOcXVvSbWG9pr5jefs= +github.com/aler9/webrtc/v3 v3.0.0-20240527213244-d3ff0bd9230f h1:IcGmhy+mh/cDCHR8BBDI8iUGeV2/O1A/qqoR3yi62Z0= +github.com/aler9/webrtc/v3 v3.0.0-20240527213244-d3ff0bd9230f/go.mod h1:1CaT2fcZzZ6VZA+O1i9yK2DU4EOcXVvSbWG9pr5jefs= github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflxkRsZA= github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0= github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c= @@ -143,8 +143,8 @@ github.com/pion/interceptor v0.1.29/go.mod h1:ri+LGNjRUc5xUNtDEPzfdkmSqISixVTBF/ 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.7/go.mod h1:4iP2UbeFhLI/vWju/bw6ZfwjJzk0z8DNValjGxR/dD8= -github.com/pion/mdns v0.0.9 h1:7Ue5KZsqq8EuqStnpPWV33vYYEH0+skdDN5L7EiEsI4= -github.com/pion/mdns v0.0.9/go.mod h1:2JA5exfxwzXiCihmxpTKgFUpiQws2MnipoPK09vecIc= +github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8= +github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk= 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.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I= @@ -156,8 +156,9 @@ github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU github.com/pion/rtp v1.8.7-0.20240429002300-bc5124c9d0d0 h1:yPAphilskTN7U3URvBVxlVr0PzheMeWqo7PaOqh//Hg= github.com/pion/rtp v1.8.7-0.20240429002300-bc5124c9d0d0/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0= -github.com/pion/sctp v1.8.8 h1:5EdnnKI4gpyR1a1TwbiS/wxEgcUWBHsc7ILAjARJB+U= github.com/pion/sctp v1.8.8/go.mod h1:igF9nZBrjh5AtmKc7U30jXltsFHicFCXSmWA2GWRaWs= +github.com/pion/sctp v1.8.16 h1:PKrMs+o9EMLRvFfXq59WFsC+V8mN1wnKzqrv+3D/gYY= +github.com/pion/sctp v1.8.16/go.mod h1:P6PbDVA++OJMrVNg2AL3XtYHV4uD6dvfyOovCgMs0PE= github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw= github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY= github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M= @@ -169,8 +170,9 @@ github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40 github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI= github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc= github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= -github.com/pion/transport/v2 v2.2.3 h1:XcOE3/x41HOSKbl1BfyY1TF1dERx7lVvlMCbXU7kfvA= github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= +github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo= +github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= github.com/pion/transport/v3 v3.0.2 h1:r+40RJR25S9w3jbA6/5uEPTzcdn7ncyU44RWCbHkLg4= github.com/pion/transport/v3 v3.0.2/go.mod h1:nIToODoOlb5If2jF9y2Igfx3PFYWfuXi37m0IlWa/D0= @@ -215,7 +217,6 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc= golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -236,7 +237,6 @@ golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= -golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -268,7 +268,6 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -280,7 +279,6 @@ golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU= golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -293,7 +291,6 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= diff --git a/internal/protocols/webrtc/incoming_track.go b/internal/protocols/webrtc/incoming_track.go index 26986340..9146e808 100644 --- a/internal/protocols/webrtc/incoming_track.go +++ b/internal/protocols/webrtc/incoming_track.go @@ -22,16 +22,16 @@ const ( var incomingVideoCodecs = []webrtc.RTPCodecParameters{ { RTPCodecCapability: webrtc.RTPCodecCapability{ - MimeType: webrtc.MimeTypeAV1, - ClockRate: 90000, + MimeType: webrtc.MimeTypeAV1, + ClockRate: 90000, + SDPFmtpLine: "profile-id=1", }, PayloadType: 96, }, { RTPCodecCapability: webrtc.RTPCodecCapability{ - MimeType: webrtc.MimeTypeVP9, - ClockRate: 90000, - SDPFmtpLine: "profile-id=0", + MimeType: webrtc.MimeTypeAV1, + ClockRate: 90000, }, PayloadType: 97, }, @@ -39,16 +39,40 @@ var incomingVideoCodecs = []webrtc.RTPCodecParameters{ RTPCodecCapability: webrtc.RTPCodecCapability{ MimeType: webrtc.MimeTypeVP9, ClockRate: 90000, - SDPFmtpLine: "profile-id=1", + SDPFmtpLine: "profile-id=3", }, PayloadType: 98, }, + { + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: webrtc.MimeTypeVP9, + ClockRate: 90000, + SDPFmtpLine: "profile-id=2", + }, + PayloadType: 99, + }, + { + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: webrtc.MimeTypeVP9, + ClockRate: 90000, + SDPFmtpLine: "profile-id=1", + }, + PayloadType: 100, + }, + { + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: webrtc.MimeTypeVP9, + ClockRate: 90000, + SDPFmtpLine: "profile-id=0", + }, + PayloadType: 101, + }, { RTPCodecCapability: webrtc.RTPCodecCapability{ MimeType: webrtc.MimeTypeVP8, ClockRate: 90000, }, - PayloadType: 99, + PayloadType: 102, }, { RTPCodecCapability: webrtc.RTPCodecCapability{ @@ -56,7 +80,7 @@ var incomingVideoCodecs = []webrtc.RTPCodecParameters{ ClockRate: 90000, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f", }, - PayloadType: 100, + PayloadType: 103, }, { RTPCodecCapability: webrtc.RTPCodecCapability{ @@ -64,7 +88,7 @@ var incomingVideoCodecs = []webrtc.RTPCodecParameters{ ClockRate: 90000, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", }, - PayloadType: 101, + PayloadType: 104, }, } @@ -85,6 +109,22 @@ var incomingAudioCodecs = []webrtc.RTPCodecParameters{ }, PayloadType: 9, }, + { + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: webrtc.MimeTypePCMU, + ClockRate: 8000, + Channels: 2, + }, + PayloadType: 118, + }, + { + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: webrtc.MimeTypePCMA, + ClockRate: 8000, + Channels: 2, + }, + PayloadType: 119, + }, { RTPCodecCapability: webrtc.RTPCodecCapability{ MimeType: webrtc.MimeTypePCMU, @@ -166,19 +206,39 @@ func newIncomingTrack( t.format = &format.G722{} case strings.ToLower(webrtc.MimeTypePCMU): + channels := track.Codec().Channels + if channels == 0 { + channels = 1 + } + + payloadType := uint8(0) + if channels > 1 { + payloadType = 118 + } + t.format = &format.G711{ - PayloadTyp: 0, + PayloadTyp: payloadType, MULaw: true, SampleRate: 8000, - ChannelCount: 1, + ChannelCount: int(channels), } case strings.ToLower(webrtc.MimeTypePCMA): + channels := track.Codec().Channels + if channels == 0 { + channels = 1 + } + + payloadType := uint8(8) + if channels > 1 { + payloadType = 119 + } + t.format = &format.G711{ - PayloadTyp: 8, + PayloadTyp: payloadType, MULaw: false, SampleRate: 8000, - ChannelCount: 1, + ChannelCount: int(channels), } default: diff --git a/internal/protocols/webrtc/outgoing_track.go b/internal/protocols/webrtc/outgoing_track.go index f02c66fe..9c67d0d7 100644 --- a/internal/protocols/webrtc/outgoing_track.go +++ b/internal/protocols/webrtc/outgoing_track.go @@ -33,7 +33,7 @@ func (t *OutgoingTrack) codecParameters() (webrtc.RTPCodecParameters, error) { ClockRate: 90000, SDPFmtpLine: "profile-id=1", }, - PayloadType: 98, + PayloadType: 96, }, nil case *format.VP8: @@ -42,7 +42,7 @@ func (t *OutgoingTrack) codecParameters() (webrtc.RTPCodecParameters, error) { MimeType: webrtc.MimeTypeVP8, ClockRate: 90000, }, - PayloadType: 99, + PayloadType: 96, }, nil case *format.H264: @@ -52,17 +52,21 @@ func (t *OutgoingTrack) codecParameters() (webrtc.RTPCodecParameters, error) { ClockRate: 90000, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", }, - PayloadType: 101, + PayloadType: 96, }, nil case *format.Opus: + if forma.ChannelCount > 2 { + return webrtc.RTPCodecParameters{}, fmt.Errorf("unsupported Opus channel count: %d", forma.ChannelCount) + } + return webrtc.RTPCodecParameters{ RTPCodecCapability: webrtc.RTPCodecCapability{ MimeType: webrtc.MimeTypeOpus, ClockRate: 48000, Channels: 2, }, - PayloadType: 111, + PayloadType: 96, }, nil case *format.G722: @@ -75,7 +79,22 @@ func (t *OutgoingTrack) codecParameters() (webrtc.RTPCodecParameters, error) { }, nil case *format.G711: + if forma.SampleRate != 8000 { + return webrtc.RTPCodecParameters{}, fmt.Errorf("unsupported G711 sample rate") + } + if forma.MULaw { + if forma.ChannelCount != 1 { + return webrtc.RTPCodecParameters{ + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: webrtc.MimeTypePCMU, + ClockRate: uint32(forma.SampleRate), + Channels: uint16(forma.ChannelCount), + }, + PayloadType: 96, + }, nil + } + return webrtc.RTPCodecParameters{ RTPCodecCapability: webrtc.RTPCodecCapability{ MimeType: webrtc.MimeTypePCMU, @@ -85,6 +104,17 @@ func (t *OutgoingTrack) codecParameters() (webrtc.RTPCodecParameters, error) { }, nil } + if forma.ChannelCount != 1 { + return webrtc.RTPCodecParameters{ + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: webrtc.MimeTypePCMA, + ClockRate: uint32(forma.SampleRate), + Channels: uint16(forma.ChannelCount), + }, + PayloadType: 96, + }, nil + } + return webrtc.RTPCodecParameters{ RTPCodecCapability: webrtc.RTPCodecCapability{ MimeType: webrtc.MimeTypePCMA, diff --git a/internal/servers/webrtc/publish_index.html b/internal/servers/webrtc/publish_index.html index 8176a664..21e1fbe3 100644 --- a/internal/servers/webrtc/publish_index.html +++ b/internal/servers/webrtc/publish_index.html @@ -376,11 +376,10 @@ const editOffer = (sdp) => { const sections = sdp.split('m='); for (let i = 0; i < sections.length; i++) { - const section = sections[i]; - if (section.startsWith('video')) { - sections[i] = setCodec(section, videoForm.codec.value); - } else if (section.startsWith('audio')) { - sections[i] = setAudioBitrate(setCodec(section, audioForm.codec.value), audioForm.bitrate.value, audioForm.voice.checked); + if (sections[i].startsWith('video')) { + sections[i] = setCodec(sections[i], videoForm.codec.value); + } else if (sections[i].startsWith('audio')) { + sections[i] = setAudioBitrate(setCodec(sections[i], audioForm.codec.value), audioForm.bitrate.value, audioForm.voice.checked); } } @@ -391,9 +390,8 @@ const editAnswer = (sdp) => { const sections = sdp.split('m='); for (let i = 0; i < sections.length; i++) { - const section = sections[i]; - if (section.startsWith('video')) { - sections[i] = setVideoBitrate(section, videoForm.bitrate.value); + if (sections[i].startsWith('video')) { + sections[i] = setVideoBitrate(sections[i], videoForm.bitrate.value); } } diff --git a/internal/servers/webrtc/read_index.html b/internal/servers/webrtc/read_index.html index 78a2026c..c42e8267 100644 --- a/internal/servers/webrtc/read_index.html +++ b/internal/servers/webrtc/read_index.html @@ -50,6 +50,7 @@ const retryPause = 2000; const video = document.getElementById('video'); const message = document.getElementById('message'); +let nonAdvertisedCodecs = []; let pc = null; let restartTimeout = null; let sessionUrl = ''; @@ -87,14 +88,14 @@ const linkToIceServers = (links) => ( }) : [] ); -const parseOffer = (offer) => { +const parseOffer = (sdp) => { const ret = { iceUfrag: '', icePwd: '', medias: [], }; - for (const line of offer.split('\r\n')) { + for (const line of sdp.split('\r\n')) { if (line.startsWith('m=')) { ret.medias.push(line.slice('m='.length)); } else if (ret.iceUfrag === '' && line.startsWith('a=ice-ufrag:')) { @@ -107,6 +108,20 @@ const parseOffer = (offer) => { return ret; }; +const enableStereoPcmau = (section) => { + let lines = section.split('\r\n'); + + lines[0] += ' 118'; + lines.splice(lines.length - 1, 0, 'a=rtpmap:118 PCMU/8000/2'); + lines.splice(lines.length - 1, 0, 'a=rtcp-fb:118 transport-cc'); + + lines[0] += ' 119'; + lines.splice(lines.length - 1, 0, 'a=rtpmap:119 PCMA/8000/2'); + lines.splice(lines.length - 1, 0, 'a=rtcp-fb:119 transport-cc'); + + return lines.join('\r\n'); +}; + const enableStereoOpus = (section) => { let opusPayloadFormat = ''; let lines = section.split('\r\n'); @@ -136,17 +151,22 @@ const enableStereoOpus = (section) => { return lines.join('\r\n'); }; -const editOffer = (offer) => { - const sections = offer.sdp.split('m='); +const editOffer = (sdp) => { + const sections = sdp.split('m='); for (let i = 0; i < sections.length; i++) { - const section = sections[i]; - if (section.startsWith('audio')) { - sections[i] = enableStereoOpus(section); + if (sections[i].startsWith('audio')) { + sections[i] = enableStereoOpus(sections[i]); + + if (nonAdvertisedCodecs.includes('pcma/8000/2')) { + sections[i] = enableStereoPcmau(sections[i]); + } + + break; } } - offer.sdp = sections.join('m='); + return sections.join('m='); }; const generateSdpFragment = (od, candidates) => { @@ -183,6 +203,70 @@ const loadStream = () => { requestICEServers(); }; +const supportsNonAdvertisedCodec = (codec, fmtp) => ( + new Promise((resolve, reject) => { + const pc = new RTCPeerConnection({ iceServers: [] }); + pc.addTransceiver('audio', { direction: 'recvonly' }); + pc.createOffer() + .then((offer) => { + if (offer.sdp.includes(' ' + codec)) { // codec is advertised, there's no need to add it manually + resolve(false); + return; + } + const sections = offer.sdp.split('m=audio'); + const lines = sections[1].split('\r\n'); + lines[0] += ' 118'; + lines.splice(lines.length - 1, 0, 'a=rtpmap:118 ' + codec); + if (fmtp !== undefined) { + lines.splice(lines.length - 1, 0, 'a=fmtp:118 ' + fmtp); + } + sections[1] = lines.join('\r\n'); + offer.sdp = sections.join('m=audio'); + return pc.setLocalDescription(offer); + }) + .then(() => { + return pc.setRemoteDescription(new RTCSessionDescription({ + type: 'answer', + sdp: 'v=0\r\n' + + 'o=- 6539324223450680508 0 IN IP4 0.0.0.0\r\n' + + 's=-\r\n' + + 't=0 0\r\n' + + 'a=fingerprint:sha-256 0D:9F:78:15:42:B5:4B:E6:E2:94:3E:5B:37:78:E1:4B:54:59:A3:36:3A:E5:05:EB:27:EE:8F:D2:2D:41:29:25\r\n' + + 'm=audio 9 UDP/TLS/RTP/SAVPF 118\r\n' + + 'c=IN IP4 0.0.0.0\r\n' + + 'a=ice-pwd:7c3bf4770007e7432ee4ea4d697db675\r\n' + + 'a=ice-ufrag:29e036dc\r\n' + + 'a=sendonly\r\n' + + 'a=rtcp-mux\r\n' + + 'a=rtpmap:118 ' + codec + '\r\n' + + ((fmtp !== undefined) ? 'a=fmtp:118 ' + fmtp + '\r\n' : ''), + })); + }) + .then(() => { + resolve(true); + }) + .catch((err) => { + resolve(false); + }) + .finally(() => { + pc.close(); + }); + }) +); + +const getNonAdvertisedCodecs = () => { + Promise.all([ + ['pcma/8000/2'], + ['multiopus/48000/6', 'channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2'], + ['L16/48000/2'] + ].map((c) => supportsNonAdvertisedCodec(c[0], c[1]).then((r) => (r) ? c[0] : false))) + .then((c) => c.filter((e) => e !== false)) + .then((codecs) => { + nonAdvertisedCodecs = codecs; + loadStream(); + }); +}; + const onError = (err) => { if (restartTimeout === null) { setMessage(err + ', retrying in some seconds'); @@ -295,7 +379,7 @@ const sendOffer = (offer) => { const createOffer = () => { pc.createOffer() .then((offer) => { - editOffer(offer); + offer.sdp = editOffer(offer.sdp); offerData = parseOffer(offer.sdp); pc.setLocalDescription(offer); sendOffer(offer); @@ -366,7 +450,7 @@ const loadAttributesFromQuery = () => { const init = () => { loadAttributesFromQuery(); - loadStream(); + getNonAdvertisedCodecs(); }; window.addEventListener('DOMContentLoaded', init); diff --git a/internal/servers/webrtc/server_test.go b/internal/servers/webrtc/server_test.go index 38727792..b87e63c0 100644 --- a/internal/servers/webrtc/server_test.go +++ b/internal/servers/webrtc/server_test.go @@ -423,7 +423,7 @@ func TestServerRead(t *testing.T) { Header: rtp.Header{ Version: 2, Marker: true, - PayloadType: 101, + PayloadType: 104, SequenceNumber: pkt.SequenceNumber, Timestamp: pkt.Timestamp, SSRC: pkt.SSRC, diff --git a/internal/servers/webrtc/session.go b/internal/servers/webrtc/session.go index e921a4b0..4d582582 100644 --- a/internal/servers/webrtc/session.go +++ b/internal/servers/webrtc/session.go @@ -216,10 +216,6 @@ func findAudioTrack( if opusFormat != nil { return opusFormat, func(track *webrtc.OutgoingTrack) error { - if opusFormat.ChannelCount > 2 { - return fmt.Errorf("unsupported Opus channel count: %d", opusFormat.ChannelCount) - } - stream.AddReader(writer, media, opusFormat, func(u unit.Unit) error { for _, pkt := range u.GetRTPPackets() { track.WriteRTP(pkt) //nolint:errcheck @@ -252,14 +248,6 @@ func findAudioTrack( if g711Format != nil { return g711Format, func(track *webrtc.OutgoingTrack) error { - if g711Format.SampleRate != 8000 { - return fmt.Errorf("unsupported G711 sample rate") - } - - if g711Format.ChannelCount != 1 { - return fmt.Errorf("unsupported G711 channel count") - } - stream.AddReader(writer, media, g711Format, func(u unit.Unit) error { for _, pkt := range u.GetRTPPackets() { track.WriteRTP(pkt) //nolint:errcheck From c37e8953fad0f81815b90b6e889c2860917ba2a4 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Sun, 2 Jun 2024 23:19:58 +0200 Subject: [PATCH 08/52] webrtc: show error when setLocalDescription or createOffer fail (#3417) --- internal/servers/webrtc/publish_index.html | 14 +++++++++++--- internal/servers/webrtc/read_index.html | 14 +++++++++++--- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/internal/servers/webrtc/publish_index.html b/internal/servers/webrtc/publish_index.html index 21e1fbe3..122bba6a 100644 --- a/internal/servers/webrtc/publish_index.html +++ b/internal/servers/webrtc/publish_index.html @@ -480,8 +480,16 @@ const createOffer = () => { pc.createOffer() .then((offer) => { offerData = parseOffer(offer.sdp); - pc.setLocalDescription(offer); - sendOffer(offer.sdp); + pc.setLocalDescription(offer) + .then(() => { + sendOffer(offer.sdp); + }) + .catch((err) => { + onError(err.toString()); + }); + }) + .catch((err) => { + onError(err.toString()); }); }; @@ -491,7 +499,7 @@ const onConnectionState = () => { } if (pc.iceConnectionState === 'disconnected') { - onError('peer connection disconnected', true); + onError('peer connection closed', true); } else if (pc.iceConnectionState === 'connected') { setMessage(''); } diff --git a/internal/servers/webrtc/read_index.html b/internal/servers/webrtc/read_index.html index c42e8267..edaa489a 100644 --- a/internal/servers/webrtc/read_index.html +++ b/internal/servers/webrtc/read_index.html @@ -381,8 +381,16 @@ const createOffer = () => { .then((offer) => { offer.sdp = editOffer(offer.sdp); offerData = parseOffer(offer.sdp); - pc.setLocalDescription(offer); - sendOffer(offer); + pc.setLocalDescription(offer) + .then(() => { + sendOffer(offer); + }) + .catch((err) => { + onError(err.toString()); + }); + }) + .catch((err) => { + onError(err.toString()); }); }; @@ -392,7 +400,7 @@ const onConnectionState = () => { } if (pc.iceConnectionState === 'disconnected') { - onError('peer connection disconnected'); + onError('peer connection closed'); } }; From ca1638976b1135b42a515738c0e759b22826ed27 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Mon, 3 Jun 2024 00:26:32 +0200 Subject: [PATCH 09/52] webrtc: support reading and publishing multichannel Opus (#3371) (#3375) --- internal/protocols/webrtc/incoming_track.go | 62 ++++- internal/protocols/webrtc/outgoing_track.go | 25 +- internal/protocols/webrtc/peer_connection.go | 12 +- .../protocols/webrtc/peer_connection_test.go | 220 +++++++++++++++++- internal/servers/webrtc/read_index.html | 40 ++++ internal/servers/webrtc/session.go | 2 +- 6 files changed, 348 insertions(+), 13 deletions(-) diff --git a/internal/protocols/webrtc/incoming_track.go b/internal/protocols/webrtc/incoming_track.go index 9146e808..785a4327 100644 --- a/internal/protocols/webrtc/incoming_track.go +++ b/internal/protocols/webrtc/incoming_track.go @@ -93,6 +93,60 @@ var incomingVideoCodecs = []webrtc.RTPCodecParameters{ } var incomingAudioCodecs = []webrtc.RTPCodecParameters{ + { + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: mimeMultiopus, + ClockRate: 48000, + Channels: 3, + SDPFmtpLine: "channel_mapping=0,2,1;num_streams=2;coupled_streams=1", + }, + PayloadType: 112, + }, + { + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: mimeMultiopus, + ClockRate: 48000, + Channels: 4, + SDPFmtpLine: "channel_mapping=0,1,2,3;num_streams=2;coupled_streams=2", + }, + PayloadType: 113, + }, + { + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: mimeMultiopus, + ClockRate: 48000, + Channels: 5, + SDPFmtpLine: "channel_mapping=0,4,1,2,3;num_streams=3;coupled_streams=2", + }, + PayloadType: 114, + }, + { + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: mimeMultiopus, + ClockRate: 48000, + Channels: 6, + SDPFmtpLine: "channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2", + }, + PayloadType: 115, + }, + { + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: mimeMultiopus, + ClockRate: 48000, + Channels: 7, + SDPFmtpLine: "channel_mapping=0,4,1,2,3,5,6;num_streams=4;coupled_streams=4", + }, + PayloadType: 116, + }, + { + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: mimeMultiopus, + ClockRate: 48000, + Channels: 8, + SDPFmtpLine: "channel_mapping=0,6,1,4,5,2,3,7;num_streams=5;coupled_streams=4", + }, + PayloadType: 117, + }, { RTPCodecCapability: webrtc.RTPCodecCapability{ MimeType: webrtc.MimeTypeOpus, @@ -191,6 +245,12 @@ func newIncomingTrack( PacketizationMode: 1, } + case strings.ToLower(mimeMultiopus): + t.format = &format.Opus{ + PayloadTyp: uint8(track.PayloadType()), + ChannelCount: int(track.Codec().Channels), + } + case strings.ToLower(webrtc.MimeTypeOpus): t.format = &format.Opus{ PayloadTyp: uint8(track.PayloadType()), @@ -242,7 +302,7 @@ func newIncomingTrack( } default: - return nil, fmt.Errorf("unsupported codec: %v", track.Codec()) + return nil, fmt.Errorf("unsupported codec: %+v", track.Codec()) } // read incoming RTCP packets to make interceptors work diff --git a/internal/protocols/webrtc/outgoing_track.go b/internal/protocols/webrtc/outgoing_track.go index 9c67d0d7..3f790ba1 100644 --- a/internal/protocols/webrtc/outgoing_track.go +++ b/internal/protocols/webrtc/outgoing_track.go @@ -8,6 +8,10 @@ import ( "github.com/pion/webrtc/v3" ) +const ( + mimeMultiopus = "audio/multiopus" +) + // OutgoingTrack is a WebRTC outgoing track type OutgoingTrack struct { Format format.Format @@ -29,9 +33,8 @@ func (t *OutgoingTrack) codecParameters() (webrtc.RTPCodecParameters, error) { case *format.VP9: return webrtc.RTPCodecParameters{ RTPCodecCapability: webrtc.RTPCodecCapability{ - MimeType: webrtc.MimeTypeVP9, - ClockRate: 90000, - SDPFmtpLine: "profile-id=1", + MimeType: webrtc.MimeTypeVP9, + ClockRate: 90000, }, PayloadType: 96, }, nil @@ -57,7 +60,14 @@ func (t *OutgoingTrack) codecParameters() (webrtc.RTPCodecParameters, error) { case *format.Opus: if forma.ChannelCount > 2 { - return webrtc.RTPCodecParameters{}, fmt.Errorf("unsupported Opus channel count: %d", forma.ChannelCount) + return webrtc.RTPCodecParameters{ + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: mimeMultiopus, + ClockRate: 48000, + Channels: uint16(forma.ChannelCount), + }, + PayloadType: 96, + }, nil } return webrtc.RTPCodecParameters{ @@ -65,6 +75,13 @@ func (t *OutgoingTrack) codecParameters() (webrtc.RTPCodecParameters, error) { MimeType: webrtc.MimeTypeOpus, ClockRate: 48000, Channels: 2, + SDPFmtpLine: func() string { + s := "minptime=10;useinbandfec=1" + if forma.ChannelCount == 2 { + s += ";stereo=1;sprop-stereo=1" + } + return s + }(), }, PayloadType: 96, }, nil diff --git a/internal/protocols/webrtc/peer_connection.go b/internal/protocols/webrtc/peer_connection.go index bb984fd7..db1f1cb7 100644 --- a/internal/protocols/webrtc/peer_connection.go +++ b/internal/protocols/webrtc/peer_connection.go @@ -2,6 +2,7 @@ package webrtc import ( "context" + "errors" "fmt" "strconv" "sync" @@ -261,8 +262,8 @@ func (co *PeerConnection) SetAnswer(answer *webrtc.SessionDescription) error { } // AddRemoteCandidate adds a remote candidate. -func (co *PeerConnection) AddRemoteCandidate(candidate webrtc.ICECandidateInit) error { - return co.wr.AddICECandidate(candidate) +func (co *PeerConnection) AddRemoteCandidate(candidate *webrtc.ICECandidateInit) error { + return co.wr.AddICECandidate(*candidate) } // CreateFullAnswer creates a full answer. @@ -277,7 +278,7 @@ func (co *PeerConnection) CreateFullAnswer( answer, err := co.wr.CreateAnswer(nil) if err != nil { - if err.Error() == "unable to populate media section, RTPSender created with no codecs" { + if errors.Is(err, webrtc.ErrSenderWithNoCodecs) { return nil, fmt.Errorf("track codecs are not supported by remote") } return nil, err @@ -288,7 +289,7 @@ func (co *PeerConnection) CreateFullAnswer( return nil, err } - err = co.WaitGatheringDone(ctx) + err = co.waitGatheringDone(ctx) if err != nil { return nil, err } @@ -296,8 +297,7 @@ func (co *PeerConnection) CreateFullAnswer( return co.wr.LocalDescription(), nil } -// WaitGatheringDone waits until candidate gathering is complete. -func (co *PeerConnection) WaitGatheringDone(ctx context.Context) error { +func (co *PeerConnection) waitGatheringDone(ctx context.Context) error { for { select { case <-co.NewLocalCandidate(): diff --git a/internal/protocols/webrtc/peer_connection_test.go b/internal/protocols/webrtc/peer_connection_test.go index b9ed8ba8..3283b0b0 100644 --- a/internal/protocols/webrtc/peer_connection_test.go +++ b/internal/protocols/webrtc/peer_connection_test.go @@ -1,15 +1,18 @@ package webrtc import ( + "context" "testing" "time" + "github.com/bluenviron/gortsplib/v4/pkg/format" "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/test" + "github.com/pion/rtp" "github.com/stretchr/testify/require" ) -func TestPeerConnectionCloseAfterError(t *testing.T) { +func TestPeerConnectionCloseImmediately(t *testing.T) { pc := &PeerConnection{ HandshakeTimeout: conf.StringDuration(10 * time.Second), TrackGatherTimeout: conf.StringDuration(2 * time.Second), @@ -20,6 +23,7 @@ func TestPeerConnectionCloseAfterError(t *testing.T) { } err := pc.Start() require.NoError(t, err) + defer pc.Close() _, err = pc.CreatePartialOffer() require.NoError(t, err) @@ -29,3 +33,217 @@ func TestPeerConnectionCloseAfterError(t *testing.T) { pc.Close() } + +func TestPeerConnectionPublishRead(t *testing.T) { + for _, ca := range []struct { + name string + in format.Format + out format.Format + }{ + { + "av1", + &format.AV1{ + PayloadTyp: 96, + }, + &format.AV1{ + PayloadTyp: 96, + }, + }, + { + "vp9", + &format.VP9{ + PayloadTyp: 96, + }, + &format.VP9{ + PayloadTyp: 96, + }, + }, + { + "vp8", + &format.VP8{ + PayloadTyp: 96, + }, + &format.VP8{ + PayloadTyp: 96, + }, + }, + { + "h264", + test.FormatH264, + &format.H264{ + PayloadTyp: 96, + PacketizationMode: 1, + }, + }, + { + "opus multichannel", + &format.Opus{ + PayloadTyp: 112, + ChannelCount: 6, + }, + &format.Opus{ + PayloadTyp: 96, + ChannelCount: 6, + }, + }, + { + "opus stereo", + &format.Opus{ + PayloadTyp: 111, + ChannelCount: 2, + }, + &format.Opus{ + PayloadTyp: 96, + ChannelCount: 2, + }, + }, + { + "opus mono", + &format.Opus{ + PayloadTyp: 111, + ChannelCount: 1, + }, + &format.Opus{ + PayloadTyp: 96, + ChannelCount: 1, + }, + }, + { + "g722", + &format.G722{}, + &format.G722{}, + }, + { + "g711 pcma stereo", + &format.G711{ + PayloadTyp: 96, + SampleRate: 8000, + ChannelCount: 2, + }, + &format.G711{ + PayloadTyp: 119, + SampleRate: 8000, + ChannelCount: 2, + }, + }, + { + "g711 pcmu stereo", + &format.G711{ + MULaw: true, + PayloadTyp: 96, + SampleRate: 8000, + ChannelCount: 2, + }, + &format.G711{ + MULaw: true, + PayloadTyp: 118, + SampleRate: 8000, + ChannelCount: 2, + }, + }, + { + "g711 pcma mono", + &format.G711{ + PayloadTyp: 8, + SampleRate: 8000, + ChannelCount: 1, + }, + &format.G711{ + PayloadTyp: 8, + SampleRate: 8000, + ChannelCount: 1, + }, + }, + // TODO: check why this fails + /* { + "g711 pcmu mono", + &format.G711{ + MULaw: true, + PayloadTyp: 0, + SampleRate: 8000, + ChannelCount: 1, + }, + &format.G711{ + MULaw: true, + PayloadTyp: 0, + SampleRate: 8000, + ChannelCount: 1, + }, + }, */ + } { + t.Run(ca.name, func(t *testing.T) { + pc1 := &PeerConnection{ + HandshakeTimeout: conf.StringDuration(10 * time.Second), + TrackGatherTimeout: conf.StringDuration(2 * time.Second), + LocalRandomUDP: true, + IPsFromInterfaces: true, + Publish: true, + OutgoingTracks: []*OutgoingTrack{{ + Format: ca.in, + }}, + Log: test.NilLogger, + } + err := pc1.Start() + require.NoError(t, err) + defer pc1.Close() + + pc2 := &PeerConnection{ + HandshakeTimeout: conf.StringDuration(10 * time.Second), + TrackGatherTimeout: conf.StringDuration(2 * time.Second), + LocalRandomUDP: true, + IPsFromInterfaces: true, + Publish: false, + Log: test.NilLogger, + } + err = pc2.Start() + require.NoError(t, err) + defer pc2.Close() + + offer, err := pc1.CreatePartialOffer() + require.NoError(t, err) + + answer, err := pc2.CreateFullAnswer(context.Background(), offer) + require.NoError(t, err) + + err = pc1.SetAnswer(answer) + require.NoError(t, err) + + go func() { + for { + select { + case cnd := <-pc1.NewLocalCandidate(): + err2 := pc2.AddRemoteCandidate(cnd) + require.NoError(t, err2) + + case <-pc1.Connected(): + return + } + } + }() + + err = pc1.WaitUntilConnected(context.Background()) + require.NoError(t, err) + + err = pc2.WaitUntilConnected(context.Background()) + require.NoError(t, err) + + err = pc1.OutgoingTracks[0].WriteRTP(&rtp.Packet{ + Header: rtp.Header{ + Version: 2, + Marker: true, + PayloadType: 111, + SequenceNumber: 1123, + Timestamp: 45343, + SSRC: 563424, + }, + Payload: []byte{5, 2}, + }) + require.NoError(t, err) + + inc, err := pc2.GatherIncomingTracks(context.Background(), 1) + require.NoError(t, err) + + require.Equal(t, ca.out, inc[0].Format()) + }) + } +} diff --git a/internal/servers/webrtc/read_index.html b/internal/servers/webrtc/read_index.html index edaa489a..55a24096 100644 --- a/internal/servers/webrtc/read_index.html +++ b/internal/servers/webrtc/read_index.html @@ -122,6 +122,42 @@ const enableStereoPcmau = (section) => { return lines.join('\r\n'); }; +const enableMultichannelOpus = (section) => { + let lines = section.split('\r\n'); + + lines[0] += " 112"; + lines.splice(lines.length - 1, 0, "a=rtpmap:112 multiopus/48000/3"); + lines.splice(lines.length - 1, 0, "a=fmtp:112 channel_mapping=0,2,1;num_streams=2;coupled_streams=1"); + lines.splice(lines.length - 1, 0, "a=rtcp-fb:112 transport-cc"); + + lines[0] += " 113"; + lines.splice(lines.length - 1, 0, "a=rtpmap:113 multiopus/48000/4"); + lines.splice(lines.length - 1, 0, "a=fmtp:113 channel_mapping=0,1,2,3;num_streams=2;coupled_streams=2"); + lines.splice(lines.length - 1, 0, "a=rtcp-fb:113 transport-cc"); + + lines[0] += " 114"; + lines.splice(lines.length - 1, 0, "a=rtpmap:114 multiopus/48000/5"); + lines.splice(lines.length - 1, 0, "a=fmtp:114 channel_mapping=0,4,1,2,3;num_streams=3;coupled_streams=2"); + lines.splice(lines.length - 1, 0, "a=rtcp-fb:114 transport-cc"); + + lines[0] += " 115"; + lines.splice(lines.length - 1, 0, "a=rtpmap:115 multiopus/48000/6"); + lines.splice(lines.length - 1, 0, "a=fmtp:115 channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2"); + lines.splice(lines.length - 1, 0, "a=rtcp-fb:115 transport-cc"); + + lines[0] += " 116"; + lines.splice(lines.length - 1, 0, "a=rtpmap:116 multiopus/48000/7"); + lines.splice(lines.length - 1, 0, "a=fmtp:116 channel_mapping=0,4,1,2,3,5,6;num_streams=4;coupled_streams=4"); + lines.splice(lines.length - 1, 0, "a=rtcp-fb:116 transport-cc"); + + lines[0] += " 117"; + lines.splice(lines.length - 1, 0, "a=rtpmap:117 multiopus/48000/8"); + lines.splice(lines.length - 1, 0, "a=fmtp:117 channel_mapping=0,6,1,4,5,2,3,7;num_streams=5;coupled_streams=4"); + lines.splice(lines.length - 1, 0, "a=rtcp-fb:117 transport-cc"); + + return lines.join('\r\n'); +}; + const enableStereoOpus = (section) => { let opusPayloadFormat = ''; let lines = section.split('\r\n'); @@ -162,6 +198,10 @@ const editOffer = (sdp) => { sections[i] = enableStereoPcmau(sections[i]); } + if (nonAdvertisedCodecs.includes('multiopus/48000/6')) { + sections[i] = enableMultichannelOpus(sections[i]); + } + break; } } diff --git a/internal/servers/webrtc/session.go b/internal/servers/webrtc/session.go index 4d582582..5a7fc416 100644 --- a/internal/servers/webrtc/session.go +++ b/internal/servers/webrtc/session.go @@ -652,7 +652,7 @@ func (s *session) readRemoteCandidates(pc *webrtc.PeerConnection) { select { case req := <-s.chAddCandidates: for _, candidate := range req.candidates { - err := pc.AddRemoteCandidate(*candidate) + err := pc.AddRemoteCandidate(candidate) if err != nil { req.res <- webRTCAddSessionCandidatesRes{err: err} } From 8bd717cf96768e6b6ade6e6a62bb69814c74edb5 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 4 Jun 2024 10:27:05 +0200 Subject: [PATCH 10/52] bump hls.js to v1.5.10 (#3421) --- internal/servers/hls/hlsjsdownloader/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/servers/hls/hlsjsdownloader/VERSION b/internal/servers/hls/hlsjsdownloader/VERSION index 0fa909eb..341724d6 100644 --- a/internal/servers/hls/hlsjsdownloader/VERSION +++ b/internal/servers/hls/hlsjsdownloader/VERSION @@ -1 +1 @@ -v1.5.9 +v1.5.10 From ca5529e10a12919663d4c9dab0008e522dc34310 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Jun 2024 22:16:05 +0200 Subject: [PATCH 11/52] build(deps): bump golang.org/x/term from 0.20.0 to 0.21.0 (#3424) Bumps [golang.org/x/term](https://github.com/golang/term) from 0.20.0 to 0.21.0. - [Commits](https://github.com/golang/term/compare/v0.20.0...v0.21.0) --- updated-dependencies: - dependency-name: golang.org/x/term dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index abb7e09b..e3afa35e 100644 --- a/go.mod +++ b/go.mod @@ -29,8 +29,8 @@ require ( github.com/pion/webrtc/v3 v3.2.22 github.com/stretchr/testify v1.9.0 golang.org/x/crypto v0.23.0 - golang.org/x/sys v0.20.0 - golang.org/x/term v0.20.0 + golang.org/x/sys v0.21.0 + golang.org/x/term v0.21.0 gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index 4f5183ca..75949bae 100644 --- a/go.sum +++ b/go.sum @@ -268,8 +268,8 @@ golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -279,8 +279,8 @@ golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= -golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= 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= From 011ea471427dc9714dc9a643d7e9218b6ca85dd8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 4 Jun 2024 22:26:37 +0200 Subject: [PATCH 12/52] build(deps): bump golang.org/x/crypto from 0.23.0 to 0.24.0 (#3423) Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.23.0 to 0.24.0. - [Commits](https://github.com/golang/crypto/compare/v0.23.0...v0.24.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index e3afa35e..bbe6caf7 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( github.com/pion/sdp/v3 v3.0.9 github.com/pion/webrtc/v3 v3.2.22 github.com/stretchr/testify v1.9.0 - golang.org/x/crypto v0.23.0 + golang.org/x/crypto v0.24.0 golang.org/x/sys v0.21.0 golang.org/x/term v0.21.0 gopkg.in/yaml.v2 v2.4.0 @@ -71,7 +71,7 @@ require ( github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect golang.org/x/arch v0.8.0 // indirect golang.org/x/net v0.25.0 // indirect - golang.org/x/text v0.15.0 // indirect + golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.5.0 // indirect google.golang.org/protobuf v1.34.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/go.sum b/go.sum index 75949bae..fd9e17a9 100644 --- a/go.sum +++ b/go.sum @@ -217,8 +217,8 @@ golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5y golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= -golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -291,8 +291,8 @@ golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From efb51044db5d3f97e3360ab1aacad0dbb5a35f0f Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Tue, 4 Jun 2024 23:09:50 +0200 Subject: [PATCH 13/52] api: fix crash when itemsPerPage is zero and there are items (#3425) --- internal/api/api.go | 53 --------------- internal/api/api_test.go | 32 --------- internal/api/paginate.go | 63 ++++++++++++++++++ internal/api/paginate_test.go | 65 +++++++++++++++++++ .../fuzz/FuzzPaginate/23731da0f18d31d0 | 3 + .../fuzz/FuzzPaginate/34523a772174e26e | 3 + .../fuzz/FuzzPaginate/85649d45641911d0 | 3 + 7 files changed, 137 insertions(+), 85 deletions(-) create mode 100644 internal/api/paginate.go create mode 100644 internal/api/paginate_test.go create mode 100644 internal/api/testdata/fuzz/FuzzPaginate/23731da0f18d31d0 create mode 100644 internal/api/testdata/fuzz/FuzzPaginate/34523a772174e26e create mode 100644 internal/api/testdata/fuzz/FuzzPaginate/85649d45641911d0 diff --git a/internal/api/api.go b/internal/api/api.go index c86dc778..072ce0a5 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -10,7 +10,6 @@ import ( "os" "reflect" "sort" - "strconv" "strings" "sync" "time" @@ -36,58 +35,6 @@ func interfaceIsEmpty(i interface{}) bool { return reflect.ValueOf(i).Kind() != reflect.Ptr || reflect.ValueOf(i).IsNil() } -func paginate2(itemsPtr interface{}, itemsPerPage int, page int) int { - ritems := reflect.ValueOf(itemsPtr).Elem() - - itemsLen := ritems.Len() - if itemsLen == 0 { - return 0 - } - - pageCount := (itemsLen / itemsPerPage) - if (itemsLen % itemsPerPage) != 0 { - pageCount++ - } - - min := page * itemsPerPage - if min > itemsLen { - min = itemsLen - } - - max := (page + 1) * itemsPerPage - if max > itemsLen { - max = itemsLen - } - - ritems.Set(ritems.Slice(min, max)) - - return pageCount -} - -func paginate(itemsPtr interface{}, itemsPerPageStr string, pageStr string) (int, error) { - itemsPerPage := 100 - - if itemsPerPageStr != "" { - tmp, err := strconv.ParseUint(itemsPerPageStr, 10, 31) - if err != nil { - return 0, err - } - itemsPerPage = int(tmp) - } - - page := 0 - - if pageStr != "" { - tmp, err := strconv.ParseUint(pageStr, 10, 31) - if err != nil { - return 0, err - } - page = int(tmp) - } - - return paginate2(itemsPtr, itemsPerPage, page), nil -} - func sortedKeys(paths map[string]*conf.Path) []string { ret := make([]string, len(paths)) i := 0 diff --git a/internal/api/api_test.go b/internal/api/api_test.go index f34c307a..a88446aa 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -74,38 +74,6 @@ func checkError(t *testing.T, msg string, body io.Reader) { require.Equal(t, map[string]interface{}{"error": msg}, resErr) } -func TestPaginate(t *testing.T) { - items := make([]int, 5) - for i := 0; i < 5; i++ { - items[i] = i - } - - pageCount, err := paginate(&items, "1", "1") - require.NoError(t, err) - require.Equal(t, 5, pageCount) - require.Equal(t, []int{1}, items) - - items = make([]int, 5) - for i := 0; i < 5; i++ { - items[i] = i - } - - pageCount, err = paginate(&items, "3", "2") - require.NoError(t, err) - require.Equal(t, 2, pageCount) - require.Equal(t, []int{}, items) - - items = make([]int, 6) - for i := 0; i < 6; i++ { - items[i] = i - } - - pageCount, err = paginate(&items, "4", "1") - require.NoError(t, err) - require.Equal(t, 2, pageCount) - require.Equal(t, []int{4, 5}, items) -} - func TestConfigAuth(t *testing.T) { cnf := tempConf(t, "api: yes\n") diff --git a/internal/api/paginate.go b/internal/api/paginate.go new file mode 100644 index 00000000..8be72062 --- /dev/null +++ b/internal/api/paginate.go @@ -0,0 +1,63 @@ +package api + +import ( + "fmt" + "reflect" + "strconv" +) + +func paginate2(itemsPtr interface{}, itemsPerPage int, page int) int { + ritems := reflect.ValueOf(itemsPtr).Elem() + + itemsLen := ritems.Len() + if itemsLen == 0 { + return 0 + } + + pageCount := (itemsLen / itemsPerPage) + if (itemsLen % itemsPerPage) != 0 { + pageCount++ + } + + min := page * itemsPerPage + if min > itemsLen { + min = itemsLen + } + + max := (page + 1) * itemsPerPage + if max > itemsLen { + max = itemsLen + } + + ritems.Set(ritems.Slice(min, max)) + + return pageCount +} + +func paginate(itemsPtr interface{}, itemsPerPageStr string, pageStr string) (int, error) { + itemsPerPage := 100 + + if itemsPerPageStr != "" { + tmp, err := strconv.ParseUint(itemsPerPageStr, 10, 31) + if err != nil { + return 0, err + } + itemsPerPage = int(tmp) + + if itemsPerPage == 0 { + return 0, fmt.Errorf("invalid items per page") + } + } + + page := 0 + + if pageStr != "" { + tmp, err := strconv.ParseUint(pageStr, 10, 31) + if err != nil { + return 0, err + } + page = int(tmp) + } + + return paginate2(itemsPtr, itemsPerPage, page), nil +} diff --git a/internal/api/paginate_test.go b/internal/api/paginate_test.go new file mode 100644 index 00000000..cd5daea8 --- /dev/null +++ b/internal/api/paginate_test.go @@ -0,0 +1,65 @@ +package api + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestPaginate(t *testing.T) { + func() { + items := make([]int, 5) + for i := 0; i < 5; i++ { + items[i] = i + } + + pageCount, err := paginate(&items, "1", "1") + require.NoError(t, err) + require.Equal(t, 5, pageCount) + require.Equal(t, []int{1}, items) + }() + + func() { + items := make([]int, 5) + for i := 0; i < 5; i++ { + items[i] = i + } + + pageCount, err := paginate(&items, "3", "2") + require.NoError(t, err) + require.Equal(t, 2, pageCount) + require.Equal(t, []int{}, items) + }() + + func() { + items := make([]int, 6) + for i := 0; i < 6; i++ { + items[i] = i + } + + pageCount, err := paginate(&items, "4", "1") + require.NoError(t, err) + require.Equal(t, 2, pageCount) + require.Equal(t, []int{4, 5}, items) + }() + + func() { + items := make([]int, 0) + + pageCount, err := paginate(&items, "1", "0") + require.NoError(t, err) + require.Equal(t, 0, pageCount) + require.Equal(t, []int{}, items) + }() +} + +func FuzzPaginate(f *testing.F) { + f.Fuzz(func(_ *testing.T, str1 string, str2 string) { + items := make([]int, 6) + for i := 0; i < 6; i++ { + items[i] = i + } + + paginate(&items, str1, str2) //nolint:errcheck + }) +} diff --git a/internal/api/testdata/fuzz/FuzzPaginate/23731da0f18d31d0 b/internal/api/testdata/fuzz/FuzzPaginate/23731da0f18d31d0 new file mode 100644 index 00000000..8e9f4e5e --- /dev/null +++ b/internal/api/testdata/fuzz/FuzzPaginate/23731da0f18d31d0 @@ -0,0 +1,3 @@ +go test fuzz v1 +string("A") +string("0") diff --git a/internal/api/testdata/fuzz/FuzzPaginate/34523a772174e26e b/internal/api/testdata/fuzz/FuzzPaginate/34523a772174e26e new file mode 100644 index 00000000..57b56556 --- /dev/null +++ b/internal/api/testdata/fuzz/FuzzPaginate/34523a772174e26e @@ -0,0 +1,3 @@ +go test fuzz v1 +string("1") +string("A") diff --git a/internal/api/testdata/fuzz/FuzzPaginate/85649d45641911d0 b/internal/api/testdata/fuzz/FuzzPaginate/85649d45641911d0 new file mode 100644 index 00000000..befd7df0 --- /dev/null +++ b/internal/api/testdata/fuzz/FuzzPaginate/85649d45641911d0 @@ -0,0 +1,3 @@ +go test fuzz v1 +string("0") +string("") From b9e9b08759a53521f16f8c2b0162db88f6be3e83 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 5 Jun 2024 09:00:22 +0200 Subject: [PATCH 14/52] bump hls.js to v1.5.11 (#3426) --- internal/servers/hls/hlsjsdownloader/VERSION | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/servers/hls/hlsjsdownloader/VERSION b/internal/servers/hls/hlsjsdownloader/VERSION index 341724d6..bbb1b251 100644 --- a/internal/servers/hls/hlsjsdownloader/VERSION +++ b/internal/servers/hls/hlsjsdownloader/VERSION @@ -1 +1 @@ -v1.5.10 +v1.5.11 From 16d0bb72006c1aa363ce98f71aa4d85bf292dac7 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Wed, 5 Jun 2024 12:46:55 +0200 Subject: [PATCH 15/52] webrtc: fix proxying PCMU tracks (#3427) --- go.mod | 2 +- go.sum | 4 ++-- internal/protocols/webrtc/peer_connection_test.go | 5 ++--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index bbe6caf7..0cc8d05b 100644 --- a/go.mod +++ b/go.mod @@ -81,4 +81,4 @@ replace code.cloudfoundry.org/bytefmt => github.com/cloudfoundry/bytefmt v0.0.0- replace github.com/pion/ice/v2 => github.com/aler9/ice/v2 v2.0.0-20231112223552-32d34dfcf3a1 -replace github.com/pion/webrtc/v3 => github.com/aler9/webrtc/v3 v3.0.0-20240527213244-d3ff0bd9230f +replace github.com/pion/webrtc/v3 => github.com/aler9/webrtc/v3 v3.0.0-20240605103651-01c72f58f2bd diff --git a/go.sum b/go.sum index fd9e17a9..aaa13d92 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,8 @@ github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/aler9/ice/v2 v2.0.0-20231112223552-32d34dfcf3a1 h1:fD6eZt+3/t8bzFn6ZZA2eP63xBP06v3EPfPJu8DO8ys= github.com/aler9/ice/v2 v2.0.0-20231112223552-32d34dfcf3a1/go.mod h1:lT3kv5uUIlHfXHU/ZRD7uKD/ufM202+eTa3C/umgGf4= -github.com/aler9/webrtc/v3 v3.0.0-20240527213244-d3ff0bd9230f h1:IcGmhy+mh/cDCHR8BBDI8iUGeV2/O1A/qqoR3yi62Z0= -github.com/aler9/webrtc/v3 v3.0.0-20240527213244-d3ff0bd9230f/go.mod h1:1CaT2fcZzZ6VZA+O1i9yK2DU4EOcXVvSbWG9pr5jefs= +github.com/aler9/webrtc/v3 v3.0.0-20240605103651-01c72f58f2bd h1:pYkvTEbGnsoN55D55V8okpqZxBwbXp1XpSnZ3elpFs4= +github.com/aler9/webrtc/v3 v3.0.0-20240605103651-01c72f58f2bd/go.mod h1:1CaT2fcZzZ6VZA+O1i9yK2DU4EOcXVvSbWG9pr5jefs= github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflxkRsZA= github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0= github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c= diff --git a/internal/protocols/webrtc/peer_connection_test.go b/internal/protocols/webrtc/peer_connection_test.go index 3283b0b0..adf467f3 100644 --- a/internal/protocols/webrtc/peer_connection_test.go +++ b/internal/protocols/webrtc/peer_connection_test.go @@ -154,8 +154,7 @@ func TestPeerConnectionPublishRead(t *testing.T) { ChannelCount: 1, }, }, - // TODO: check why this fails - /* { + { "g711 pcmu mono", &format.G711{ MULaw: true, @@ -169,7 +168,7 @@ func TestPeerConnectionPublishRead(t *testing.T) { SampleRate: 8000, ChannelCount: 1, }, - }, */ + }, } { t.Run(ca.name, func(t *testing.T) { pc1 := &PeerConnection{ From bf8b68d75737063e06159cd748ae661e4e4fa5ee Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Sat, 8 Jun 2024 23:38:10 +0200 Subject: [PATCH 16/52] bump pion/webrtc and pion/ice (#3436) --- go.mod | 4 ++-- go.sum | 38 ++++++++++++++++---------------------- 2 files changed, 18 insertions(+), 24 deletions(-) diff --git a/go.mod b/go.mod index 0cc8d05b..33a16b65 100644 --- a/go.mod +++ b/go.mod @@ -79,6 +79,6 @@ require ( replace code.cloudfoundry.org/bytefmt => github.com/cloudfoundry/bytefmt v0.0.0-20211005130812-5bb3c17173e5 -replace github.com/pion/ice/v2 => github.com/aler9/ice/v2 v2.0.0-20231112223552-32d34dfcf3a1 +replace github.com/pion/ice/v2 => github.com/aler9/ice/v2 v2.0.0-20240608212222-2eebc68350c9 -replace github.com/pion/webrtc/v3 => github.com/aler9/webrtc/v3 v3.0.0-20240605103651-01c72f58f2bd +replace github.com/pion/webrtc/v3 => github.com/aler9/webrtc/v3 v3.0.0-20240608212857-ef9d26bd8aa6 diff --git a/go.sum b/go.sum index aaa13d92..3036700d 100644 --- a/go.sum +++ b/go.sum @@ -10,10 +10,10 @@ github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= -github.com/aler9/ice/v2 v2.0.0-20231112223552-32d34dfcf3a1 h1:fD6eZt+3/t8bzFn6ZZA2eP63xBP06v3EPfPJu8DO8ys= -github.com/aler9/ice/v2 v2.0.0-20231112223552-32d34dfcf3a1/go.mod h1:lT3kv5uUIlHfXHU/ZRD7uKD/ufM202+eTa3C/umgGf4= -github.com/aler9/webrtc/v3 v3.0.0-20240605103651-01c72f58f2bd h1:pYkvTEbGnsoN55D55V8okpqZxBwbXp1XpSnZ3elpFs4= -github.com/aler9/webrtc/v3 v3.0.0-20240605103651-01c72f58f2bd/go.mod h1:1CaT2fcZzZ6VZA+O1i9yK2DU4EOcXVvSbWG9pr5jefs= +github.com/aler9/ice/v2 v2.0.0-20240608212222-2eebc68350c9 h1:Vax9SzYE68ZYLwFaK7lnCV2ZhX9/YqAJX6xxROPRqEM= +github.com/aler9/ice/v2 v2.0.0-20240608212222-2eebc68350c9/go.mod h1:KXJJcZK7E8WzrBEYnV4UtqEZsGeWfHxsNqhVcVvgjxw= +github.com/aler9/webrtc/v3 v3.0.0-20240608212857-ef9d26bd8aa6 h1:pQLsx1xdKND5mwOO+VTrro1xqZRyRDN+7JAgYv80UYk= +github.com/aler9/webrtc/v3 v3.0.0-20240608212857-ef9d26bd8aa6/go.mod h1:M1RAe3TNTD1tzyvqHrbVODfwdPGSXOUo/OgpoGGJqFY= github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflxkRsZA= github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0= github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c= @@ -81,7 +81,7 @@ github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= @@ -137,29 +137,23 @@ github.com/pion/datachannel v1.5.5 h1:10ef4kwdjije+M9d7Xm9im2Y3O6A6ccQb0zcqZcJew github.com/pion/datachannel v1.5.5/go.mod h1:iMz+lECmfdCMqFRhXhcA/219B0SQlbpoR2V118yimL0= github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= -github.com/pion/interceptor v0.1.25/go.mod h1:wkbPYAak5zKsfpVDYMtEfWEy8D4zL+rpxCxPImLOg3Y= github.com/pion/interceptor v0.1.29 h1:39fsnlP1U8gw2JzOFWdfCU82vHvhW9o0rZnZF56wF+M= github.com/pion/interceptor v0.1.29/go.mod h1:ri+LGNjRUc5xUNtDEPzfdkmSqISixVTBF/z/Zms/6T4= 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.7/go.mod h1:4iP2UbeFhLI/vWju/bw6ZfwjJzk0z8DNValjGxR/dD8= github.com/pion/mdns v0.0.12 h1:CiMYlY+O0azojWDmxdNr7ADGrnZ+V6Ilfner+6mSVK8= github.com/pion/mdns v0.0.12/go.mod h1:VExJjv8to/6Wqm1FXK+Ii/Z9tsVk/F5sD/N70cnYFbk= 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.10/go.mod h1:ztfEwXZNLGyF1oQDttz/ZKIBaeeg/oWbRYqzBM9TL1I= github.com/pion/rtcp v1.2.12/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= github.com/pion/rtcp v1.2.14 h1:KCkGV3vJ+4DAJmvP0vaQShsb0xkRfWkO540Gy102KyE= github.com/pion/rtcp v1.2.14/go.mod h1:sn6qjxvnwyAkkPzPULIbVqSKI5Dv54Rv7VG0kNxh9L4= -github.com/pion/rtp v1.8.2/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= github.com/pion/rtp v1.8.3/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= github.com/pion/rtp v1.8.7-0.20240429002300-bc5124c9d0d0 h1:yPAphilskTN7U3URvBVxlVr0PzheMeWqo7PaOqh//Hg= github.com/pion/rtp v1.8.7-0.20240429002300-bc5124c9d0d0/go.mod h1:pBGHaFt/yW7bf1jjWAoUjpSNoDnw98KTMg+jWWvziqU= github.com/pion/sctp v1.8.5/go.mod h1:SUFFfDpViyKejTAdwD1d/HQsCu+V/40cCs2nZIvC3s0= -github.com/pion/sctp v1.8.8/go.mod h1:igF9nZBrjh5AtmKc7U30jXltsFHicFCXSmWA2GWRaWs= github.com/pion/sctp v1.8.16 h1:PKrMs+o9EMLRvFfXq59WFsC+V8mN1wnKzqrv+3D/gYY= github.com/pion/sctp v1.8.16/go.mod h1:P6PbDVA++OJMrVNg2AL3XtYHV4uD6dvfyOovCgMs0PE= -github.com/pion/sdp/v3 v3.0.6/go.mod h1:iiFWFpQO8Fy3S5ldclBkpXqmWy02ns78NOKoLLL0YQw= github.com/pion/sdp/v3 v3.0.9 h1:pX++dCHoHUwq43kuwf3PyJfHlwIj4hXA7Vrifiq0IJY= github.com/pion/sdp/v3 v3.0.9/go.mod h1:B5xmvENq5IXJimIO4zfp6LAe1fD9N+kFv+V/1lOdz8M= github.com/pion/srtp/v2 v2.0.18 h1:vKpAXfawO9RtTRKZJbG4y0v1b11NZxQnxRl85kGuUlo= @@ -168,21 +162,19 @@ github.com/pion/stun v0.6.1 h1:8lp6YejULeHBF8NmV8e2787BogQhduZugh5PdhDyyN4= github.com/pion/stun v0.6.1/go.mod h1:/hO7APkX4hZKu/D0f2lHzNyvdkTGtIy3NDmLR7kSz/8= github.com/pion/transport v0.14.1 h1:XSM6olwW+o8J4SCmOBb/BpwZypkHeyM0PGFCxNQBr40= github.com/pion/transport v0.14.1/go.mod h1:4tGmbk00NeYA3rUa9+n+dzCCoKkcy3YlYb99Jn2fNnI= -github.com/pion/transport/v2 v2.0.0/go.mod h1:HS2MEBJTwD+1ZI2eSXSvHJx/HnzQqRy2/LXxt6eVMHc= github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= +github.com/pion/transport/v2 v2.2.2/go.mod h1:OJg3ojoBJopjEeECq2yJdXH9YVrUJ1uQ++NjXLOUorc= github.com/pion/transport/v2 v2.2.3/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo= github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= github.com/pion/transport/v3 v3.0.2 h1:r+40RJR25S9w3jbA6/5uEPTzcdn7ncyU44RWCbHkLg4= github.com/pion/transport/v3 v3.0.2/go.mod h1:nIToODoOlb5If2jF9y2Igfx3PFYWfuXi37m0IlWa/D0= -github.com/pion/turn/v2 v2.1.2/go.mod h1:1kjnPkBcex3dhCU2Am+AAmxDcGhLX3WnMfmkNpvSTQU= github.com/pion/turn/v2 v2.1.3 h1:pYxTVWG2gpC97opdRc5IGsQ1lJ9O/IlNhkzj7MMrGAA= github.com/pion/turn/v2 v2.1.3/go.mod h1:huEpByKKHix2/b9kmTAM3YoX6MKP+/D//0ClgUYR2fY= github.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE= 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/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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -215,8 +207,9 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= -golang.org/x/crypto v0.10.0/go.mod h1:o4eNf7Ede1fv+hwOwZsTHl9EsPFO6q6ZvYR8vYfY45I= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -231,12 +224,12 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.11.0/go.mod h1:2L/ixqYpgIVXmeoSA/4Lu7BzTG4KIyPIryS4IsOd1oQ= +golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -261,24 +254,25 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.9.0/go.mod h1:M6DEAAIenWoTxdKrOltXcmDY3rSplQUkrvaDU5FcQyo= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= @@ -286,11 +280,11 @@ 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/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= From eaf47e6598027c1417ca2f1c2f66d7bd7e2ef316 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Sun, 9 Jun 2024 22:51:16 +0200 Subject: [PATCH 17/52] webrtc: support reading, publishing, proxying LPCM tracks (#3437) --- internal/formatprocessor/generic.go | 2 +- internal/protocols/webrtc/incoming_track.go | 51 ++- internal/protocols/webrtc/outgoing_track.go | 28 +- .../protocols/webrtc/peer_connection_test.go | 45 +++ internal/servers/webrtc/read_index.html | 22 ++ internal/servers/webrtc/server_test.go | 301 ++++++++++++------ internal/servers/webrtc/session.go | 40 +++ 7 files changed, 380 insertions(+), 109 deletions(-) diff --git a/internal/formatprocessor/generic.go b/internal/formatprocessor/generic.go index 0140aa80..1faba6a8 100644 --- a/internal/formatprocessor/generic.go +++ b/internal/formatprocessor/generic.go @@ -20,7 +20,7 @@ func newGeneric( generateRTPPackets bool, ) (*formatProcessorGeneric, error) { if generateRTPPackets { - return nil, fmt.Errorf("we don't know how to generate RTP packets of format %+v", forma) + return nil, fmt.Errorf("we don't know how to generate RTP packets of format %T", forma) } return &formatProcessorGeneric{ diff --git a/internal/protocols/webrtc/incoming_track.go b/internal/protocols/webrtc/incoming_track.go index 785a4327..fa5fd878 100644 --- a/internal/protocols/webrtc/incoming_track.go +++ b/internal/protocols/webrtc/incoming_track.go @@ -19,6 +19,11 @@ const ( keyFrameInterval = 2 * time.Second ) +const ( + mimeTypeMultiopus = "audio/multiopus" + mimeTypeL16 = "audio/L16" +) + var incomingVideoCodecs = []webrtc.RTPCodecParameters{ { RTPCodecCapability: webrtc.RTPCodecCapability{ @@ -95,7 +100,7 @@ var incomingVideoCodecs = []webrtc.RTPCodecParameters{ var incomingAudioCodecs = []webrtc.RTPCodecParameters{ { RTPCodecCapability: webrtc.RTPCodecCapability{ - MimeType: mimeMultiopus, + MimeType: mimeTypeMultiopus, ClockRate: 48000, Channels: 3, SDPFmtpLine: "channel_mapping=0,2,1;num_streams=2;coupled_streams=1", @@ -104,7 +109,7 @@ var incomingAudioCodecs = []webrtc.RTPCodecParameters{ }, { RTPCodecCapability: webrtc.RTPCodecCapability{ - MimeType: mimeMultiopus, + MimeType: mimeTypeMultiopus, ClockRate: 48000, Channels: 4, SDPFmtpLine: "channel_mapping=0,1,2,3;num_streams=2;coupled_streams=2", @@ -113,7 +118,7 @@ var incomingAudioCodecs = []webrtc.RTPCodecParameters{ }, { RTPCodecCapability: webrtc.RTPCodecCapability{ - MimeType: mimeMultiopus, + MimeType: mimeTypeMultiopus, ClockRate: 48000, Channels: 5, SDPFmtpLine: "channel_mapping=0,4,1,2,3;num_streams=3;coupled_streams=2", @@ -122,7 +127,7 @@ var incomingAudioCodecs = []webrtc.RTPCodecParameters{ }, { RTPCodecCapability: webrtc.RTPCodecCapability{ - MimeType: mimeMultiopus, + MimeType: mimeTypeMultiopus, ClockRate: 48000, Channels: 6, SDPFmtpLine: "channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2", @@ -131,7 +136,7 @@ var incomingAudioCodecs = []webrtc.RTPCodecParameters{ }, { RTPCodecCapability: webrtc.RTPCodecCapability{ - MimeType: mimeMultiopus, + MimeType: mimeTypeMultiopus, ClockRate: 48000, Channels: 7, SDPFmtpLine: "channel_mapping=0,4,1,2,3,5,6;num_streams=4;coupled_streams=4", @@ -140,7 +145,7 @@ var incomingAudioCodecs = []webrtc.RTPCodecParameters{ }, { RTPCodecCapability: webrtc.RTPCodecCapability{ - MimeType: mimeMultiopus, + MimeType: mimeTypeMultiopus, ClockRate: 48000, Channels: 8, SDPFmtpLine: "channel_mapping=0,6,1,4,5,2,3,7;num_streams=5;coupled_streams=4", @@ -193,6 +198,30 @@ var incomingAudioCodecs = []webrtc.RTPCodecParameters{ }, PayloadType: 8, }, + { + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: mimeTypeL16, + ClockRate: 8000, + Channels: 2, + }, + PayloadType: 120, + }, + { + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: mimeTypeL16, + ClockRate: 16000, + Channels: 2, + }, + PayloadType: 121, + }, + { + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: mimeTypeL16, + ClockRate: 48000, + Channels: 2, + }, + PayloadType: 122, + }, } // IncomingTrack is an incoming track. @@ -245,7 +274,7 @@ func newIncomingTrack( PacketizationMode: 1, } - case strings.ToLower(mimeMultiopus): + case strings.ToLower(mimeTypeMultiopus): t.format = &format.Opus{ PayloadTyp: uint8(track.PayloadType()), ChannelCount: int(track.Codec().Channels), @@ -301,6 +330,14 @@ func newIncomingTrack( ChannelCount: int(channels), } + case strings.ToLower(mimeTypeL16): + t.format = &format.LPCM{ + PayloadTyp: uint8(track.PayloadType()), + BitDepth: 16, + SampleRate: int(track.Codec().ClockRate), + ChannelCount: int(track.Codec().Channels), + } + default: return nil, fmt.Errorf("unsupported codec: %+v", track.Codec()) } diff --git a/internal/protocols/webrtc/outgoing_track.go b/internal/protocols/webrtc/outgoing_track.go index 3f790ba1..efd79cb6 100644 --- a/internal/protocols/webrtc/outgoing_track.go +++ b/internal/protocols/webrtc/outgoing_track.go @@ -8,10 +8,6 @@ import ( "github.com/pion/webrtc/v3" ) -const ( - mimeMultiopus = "audio/multiopus" -) - // OutgoingTrack is a WebRTC outgoing track type OutgoingTrack struct { Format format.Format @@ -62,7 +58,7 @@ func (t *OutgoingTrack) codecParameters() (webrtc.RTPCodecParameters, error) { if forma.ChannelCount > 2 { return webrtc.RTPCodecParameters{ RTPCodecCapability: webrtc.RTPCodecCapability{ - MimeType: mimeMultiopus, + MimeType: mimeTypeMultiopus, ClockRate: 48000, Channels: uint16(forma.ChannelCount), }, @@ -140,6 +136,28 @@ func (t *OutgoingTrack) codecParameters() (webrtc.RTPCodecParameters, error) { PayloadType: 8, }, nil + case *format.LPCM: + if forma.BitDepth != 16 { + return webrtc.RTPCodecParameters{}, fmt.Errorf("unsupported LPCM bit depth: %d", forma.BitDepth) + } + + if forma.ClockRate() != 8000 && forma.ClockRate() != 16000 && forma.ClockRate() != 48000 { + return webrtc.RTPCodecParameters{}, fmt.Errorf("unsupported clock rate: %d", forma.ClockRate()) + } + + if forma.ChannelCount != 1 && forma.ChannelCount != 2 { + return webrtc.RTPCodecParameters{}, fmt.Errorf("unsupported channel count: %d", forma.ChannelCount) + } + + return webrtc.RTPCodecParameters{ + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: mimeTypeL16, + ClockRate: uint32(forma.ClockRate()), + Channels: uint16(forma.ChannelCount), + }, + PayloadType: 96, + }, nil + default: return webrtc.RTPCodecParameters{}, fmt.Errorf("unsupported track type: %T", forma) } diff --git a/internal/protocols/webrtc/peer_connection_test.go b/internal/protocols/webrtc/peer_connection_test.go index adf467f3..8c265dbd 100644 --- a/internal/protocols/webrtc/peer_connection_test.go +++ b/internal/protocols/webrtc/peer_connection_test.go @@ -169,6 +169,51 @@ func TestPeerConnectionPublishRead(t *testing.T) { ChannelCount: 1, }, }, + { + "l16 8000 stereo", + &format.LPCM{ + PayloadTyp: 96, + BitDepth: 16, + SampleRate: 8000, + ChannelCount: 2, + }, + &format.LPCM{ + PayloadTyp: 96, + BitDepth: 16, + SampleRate: 8000, + ChannelCount: 2, + }, + }, + { + "l16 16000 stereo", + &format.LPCM{ + PayloadTyp: 96, + BitDepth: 16, + SampleRate: 16000, + ChannelCount: 2, + }, + &format.LPCM{ + PayloadTyp: 96, + BitDepth: 16, + SampleRate: 16000, + ChannelCount: 2, + }, + }, + { + "l16 48khz stereo", + &format.LPCM{ + PayloadTyp: 96, + BitDepth: 16, + SampleRate: 48000, + ChannelCount: 2, + }, + &format.LPCM{ + PayloadTyp: 96, + BitDepth: 16, + SampleRate: 48000, + ChannelCount: 2, + }, + }, } { t.Run(ca.name, func(t *testing.T) { pc1 := &PeerConnection{ diff --git a/internal/servers/webrtc/read_index.html b/internal/servers/webrtc/read_index.html index 55a24096..f3052d59 100644 --- a/internal/servers/webrtc/read_index.html +++ b/internal/servers/webrtc/read_index.html @@ -158,6 +158,24 @@ const enableMultichannelOpus = (section) => { return lines.join('\r\n'); }; +const enableL16 = (section) => { + let lines = section.split('\r\n'); + + lines[0] += " 120"; + lines.splice(lines.length - 1, 0, "a=rtpmap:120 L16/8000/2"); + lines.splice(lines.length - 1, 0, "a=rtcp-fb:120 transport-cc"); + + lines[0] += " 121"; + lines.splice(lines.length - 1, 0, "a=rtpmap:121 L16/16000/2"); + lines.splice(lines.length - 1, 0, "a=rtcp-fb:121 transport-cc"); + + lines[0] += " 122"; + lines.splice(lines.length - 1, 0, "a=rtpmap:122 L16/48000/2"); + lines.splice(lines.length - 1, 0, "a=rtcp-fb:122 transport-cc"); + + return lines.join('\r\n'); +}; + const enableStereoOpus = (section) => { let opusPayloadFormat = ''; let lines = section.split('\r\n'); @@ -202,6 +220,10 @@ const editOffer = (sdp) => { sections[i] = enableMultichannelOpus(sections[i]); } + if (nonAdvertisedCodecs.includes('L16/48000/2')) { + sections[i] = enableL16(sections[i]); + } + break; } } diff --git a/internal/servers/webrtc/server_test.go b/internal/servers/webrtc/server_test.go index b87e63c0..74e41f00 100644 --- a/internal/servers/webrtc/server_test.go +++ b/internal/servers/webrtc/server_test.go @@ -5,10 +5,12 @@ import ( "context" "net/http" "net/url" + "reflect" "testing" "time" "github.com/bluenviron/gortsplib/v4/pkg/description" + "github.com/bluenviron/gortsplib/v4/pkg/format" "github.com/bluenviron/mediamtx/internal/asyncwriter" "github.com/bluenviron/mediamtx/internal/auth" "github.com/bluenviron/mediamtx/internal/conf" @@ -336,107 +338,214 @@ func TestServerPublish(t *testing.T) { } func TestServerRead(t *testing.T) { - desc := &description.Session{Medias: []*description.Media{test.MediaH264}} - - stream, err := stream.New( - 1460, - desc, - true, - test.NilLogger, - ) - require.NoError(t, err) - - path := &dummyPath{stream: stream} - - pathManager := &dummyPathManager{path: path} - - s := &Server{ - Address: "127.0.0.1:8886", - Encryption: false, - ServerKey: "", - ServerCert: "", - AllowOrigin: "", - TrustedProxies: conf.IPNetworks{}, - ReadTimeout: conf.StringDuration(10 * time.Second), - WriteQueueSize: 512, - LocalUDPAddress: "127.0.0.1:8887", - LocalTCPAddress: "127.0.0.1:8887", - IPsFromInterfaces: true, - IPsFromInterfacesList: []string{}, - AdditionalHosts: []string{}, - ICEServers: []conf.WebRTCICEServer{}, - HandshakeTimeout: conf.StringDuration(10 * time.Second), - TrackGatherTimeout: conf.StringDuration(2 * time.Second), - ExternalCmdPool: nil, - PathManager: pathManager, - Parent: test.NilLogger, - } - err = s.Initialize() - require.NoError(t, err) - defer s.Close() - - u, err := url.Parse("http://myuser:mypass@localhost:8886/teststream/whep?param=value") - require.NoError(t, err) - - tr := &http.Transport{} - defer tr.CloseIdleConnections() - hc := &http.Client{Transport: tr} - - wc := &webrtc.WHIPClient{ - HTTPClient: hc, - URL: u, - Log: test.NilLogger, - } - - writerDone := make(chan struct{}) - defer func() { <-writerDone }() - - writerTerminate := make(chan struct{}) - defer close(writerTerminate) - - go func() { - defer close(writerDone) - for { - select { - case <-time.After(100 * time.Millisecond): - case <-writerTerminate: - return - } - stream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.H264{ - Base: unit.Base{ - NTP: time.Time{}, - }, + for _, ca := range []struct { + name string + medias []*description.Media + unit unit.Unit + outRTPPayload []byte + }{ + { + "av1", + []*description.Media{{ + Type: description.MediaTypeVideo, + Formats: []format.Format{&format.AV1{ + PayloadTyp: 96, + }}, + }}, + &unit.AV1{ + TU: [][]byte{{1, 2}}, + }, + []byte{0, 2, 1, 2}, + }, + // TODO: check why this doesn't work + /*{ + "vp9", + []*description.Media{{ + Type: description.MediaTypeVideo, + Formats: []format.Format{&format.VP9{ + PayloadTyp: 96, + }}, + }}, + &unit.VP9{ + Frame: []byte{1, 2}, + }, + []byte{1, 2}, + },*/ + { + "vp8", + []*description.Media{{ + Type: description.MediaTypeVideo, + Formats: []format.Format{&format.VP8{ + PayloadTyp: 96, + }}, + }}, + &unit.VP8{ + Frame: []byte{1, 2}, + }, + []byte{0x10, 1, 2}, + }, + { + "h264", + []*description.Media{test.MediaH264}, + &unit.H264{ AU: [][]byte{ {5, 1}, }, - }) - } - }() - - tracks, err := wc.Read(context.Background()) - require.NoError(t, err) - defer checkClose(t, wc.Close) - - pkt, err := tracks[0].ReadRTP() - require.NoError(t, err) - require.Equal(t, &rtp.Packet{ - Header: rtp.Header{ - Version: 2, - Marker: true, - PayloadType: 104, - SequenceNumber: pkt.SequenceNumber, - Timestamp: pkt.Timestamp, - SSRC: pkt.SSRC, - CSRC: []uint32{}, + }, + []byte{ + 0x18, 0x00, 0x19, 0x67, 0x42, 0xc0, 0x28, 0xd9, + 0x00, 0x78, 0x02, 0x27, 0xe5, 0x84, 0x00, 0x00, + 0x03, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0xf0, + 0x3c, 0x60, 0xc9, 0x20, 0x00, 0x04, 0x08, 0x06, + 0x07, 0x08, 0x00, 0x02, 0x05, 0x01, + }, }, - Payload: []byte{ - 0x18, 0x00, 0x19, 0x67, 0x42, 0xc0, 0x28, 0xd9, - 0x00, 0x78, 0x02, 0x27, 0xe5, 0x84, 0x00, 0x00, - 0x03, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0xf0, - 0x3c, 0x60, 0xc9, 0x20, 0x00, 0x04, 0x08, 0x06, - 0x07, 0x08, 0x00, 0x02, 0x05, 0x01, + { + "opus", + []*description.Media{{ + Type: description.MediaTypeAudio, + Formats: []format.Format{&format.Opus{ + PayloadTyp: 96, + ChannelCount: 2, + }}, + }}, + &unit.Opus{ + Packets: [][]byte{{1, 2}}, + }, + []byte{1, 2}, }, - }, pkt) + // TODO: check why this doesn't work + /*{ + "g722", + []*description.Media{{ + Type: description.MediaTypeAudio, + Formats: []format.Format{&format.G722{}}, + }}, + &unit.Generic{ + Base: unit.Base{ + RTPPackets: []*rtp.Packet{{ + Header: rtp.Header{}, + Payload: []byte{1, 2}, + }}, + }, + }, + []byte{1, 2}, + },*/ + { + "g711", + []*description.Media{{ + Type: description.MediaTypeAudio, + Formats: []format.Format{&format.G711{ + MULaw: true, + SampleRate: 8000, + ChannelCount: 1, + }}, + }}, + &unit.G711{ + Samples: []byte{1, 2, 3}, + }, + []byte{1, 2, 3}, + }, + { + "lpcm", + []*description.Media{{ + Type: description.MediaTypeAudio, + Formats: []format.Format{&format.LPCM{ + PayloadTyp: 96, + BitDepth: 16, + SampleRate: 48000, + ChannelCount: 2, + }}, + }}, + &unit.LPCM{ + Samples: []byte{1, 2, 3, 4}, + }, + []byte{1, 2, 3, 4}, + }, + } { + t.Run(ca.name, func(t *testing.T) { + desc := &description.Session{Medias: ca.medias} + + stream, err := stream.New( + 1460, + desc, + true, + test.NilLogger, + ) + require.NoError(t, err) + + path := &dummyPath{stream: stream} + + pathManager := &dummyPathManager{path: path} + + s := &Server{ + Address: "127.0.0.1:8886", + Encryption: false, + ServerKey: "", + ServerCert: "", + AllowOrigin: "", + TrustedProxies: conf.IPNetworks{}, + ReadTimeout: conf.StringDuration(10 * time.Second), + WriteQueueSize: 512, + LocalUDPAddress: "127.0.0.1:8887", + LocalTCPAddress: "127.0.0.1:8887", + IPsFromInterfaces: true, + IPsFromInterfacesList: []string{}, + AdditionalHosts: []string{}, + ICEServers: []conf.WebRTCICEServer{}, + HandshakeTimeout: conf.StringDuration(10 * time.Second), + TrackGatherTimeout: conf.StringDuration(2 * time.Second), + ExternalCmdPool: nil, + PathManager: pathManager, + Parent: test.NilLogger, + } + err = s.Initialize() + require.NoError(t, err) + defer s.Close() + + u, err := url.Parse("http://myuser:mypass@localhost:8886/teststream/whep?param=value") + require.NoError(t, err) + + tr := &http.Transport{} + defer tr.CloseIdleConnections() + hc := &http.Client{Transport: tr} + + wc := &webrtc.WHIPClient{ + HTTPClient: hc, + URL: u, + Log: test.NilLogger, + } + + writerDone := make(chan struct{}) + defer func() { <-writerDone }() + + writerTerminate := make(chan struct{}) + defer close(writerTerminate) + + go func() { + defer close(writerDone) + for { + select { + case <-time.After(100 * time.Millisecond): + case <-writerTerminate: + return + } + + r := reflect.New(reflect.TypeOf(ca.unit).Elem()) + r.Elem().Set(reflect.ValueOf(ca.unit).Elem()) + stream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], r.Interface().(unit.Unit)) + } + }() + + tracks, err := wc.Read(context.Background()) + require.NoError(t, err) + defer checkClose(t, wc.Close) + + pkt, err := tracks[0].ReadRTP() + require.NoError(t, err) + require.Equal(t, ca.outRTPPayload, pkt.Payload) + }) + } } func TestServerPostNotFound(t *testing.T) { diff --git a/internal/servers/webrtc/session.go b/internal/servers/webrtc/session.go index 5a7fc416..ed6a2066 100644 --- a/internal/servers/webrtc/session.go +++ b/internal/servers/webrtc/session.go @@ -14,6 +14,7 @@ import ( "github.com/bluenviron/gortsplib/v4/pkg/format" "github.com/bluenviron/gortsplib/v4/pkg/format/rtpav1" "github.com/bluenviron/gortsplib/v4/pkg/format/rtph264" + "github.com/bluenviron/gortsplib/v4/pkg/format/rtplpcm" "github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp8" "github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp9" "github.com/bluenviron/gortsplib/v4/pkg/rtptime" @@ -259,6 +260,45 @@ func findAudioTrack( } } + var lpcmFormat *format.LPCM + media = stream.Desc().FindFormat(&lpcmFormat) + + if lpcmFormat != nil { + return lpcmFormat, func(track *webrtc.OutgoingTrack) error { + encoder := &rtplpcm.Encoder{ + PayloadType: 96, + BitDepth: 16, + ChannelCount: lpcmFormat.ChannelCount, + PayloadMaxSize: webrtcPayloadMaxSize, + } + err := encoder.Init() + if err != nil { + return err + } + + stream.AddReader(writer, media, lpcmFormat, func(u unit.Unit) error { + tunit := u.(*unit.LPCM) + + if tunit.Samples == nil { + return nil + } + + packets, err := encoder.Encode(tunit.Samples) + if err != nil { + return nil //nolint:nilerr + } + + for _, pkt := range packets { + pkt.Timestamp += tunit.RTPPackets[0].Timestamp + track.WriteRTP(pkt) //nolint:errcheck + } + + return nil + }) + return nil + } + } + return nil, nil } From d7bc304e52510bbaa69595fd87a25dea461fccfc Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Sun, 9 Jun 2024 22:58:40 +0200 Subject: [PATCH 18/52] webrtc: speed up gathering of incoming tracks (#3441) --- internal/protocols/webrtc/peer_connection.go | 46 ++++++++++++++++--- .../protocols/webrtc/peer_connection_test.go | 2 +- internal/protocols/webrtc/track_count.go | 37 --------------- internal/protocols/webrtc/whip_client.go | 5 +- internal/servers/webrtc/session.go | 4 +- 5 files changed, 45 insertions(+), 49 deletions(-) delete mode 100644 internal/protocols/webrtc/track_count.go diff --git a/internal/protocols/webrtc/peer_connection.go b/internal/protocols/webrtc/peer_connection.go index db1f1cb7..850ca345 100644 --- a/internal/protocols/webrtc/peer_connection.go +++ b/internal/protocols/webrtc/peer_connection.go @@ -10,6 +10,7 @@ import ( "github.com/pion/ice/v2" "github.com/pion/interceptor" + "github.com/pion/sdp/v3" "github.com/pion/webrtc/v3" "github.com/bluenviron/mediamtx/internal/conf" @@ -29,6 +30,37 @@ func stringInSlice(a string, list []string) bool { return false } +// TracksAreValid checks whether tracks in the SDP are valid +func TracksAreValid(medias []*sdp.MediaDescription) error { + videoTrack := false + audioTrack := false + + for _, media := range medias { + switch media.MediaName.Media { + case "video": + if videoTrack { + return fmt.Errorf("only a single video and a single audio track are supported") + } + videoTrack = true + + case "audio": + if audioTrack { + return fmt.Errorf("only a single video and a single audio track are supported") + } + audioTrack = true + + default: + return fmt.Errorf("unsupported media '%s'", media.MediaName.Media) + } + } + + if !videoTrack && !audioTrack { + return fmt.Errorf("no valid tracks count") + } + + return nil +} + type trackRecvPair struct { track *webrtc.TrackRemote receiver *webrtc.RTPReceiver @@ -334,10 +366,12 @@ outer: } // GatherIncomingTracks gathers incoming tracks. -func (co *PeerConnection) GatherIncomingTracks( - ctx context.Context, - maxCount int, -) ([]*IncomingTrack, error) { +func (co *PeerConnection) GatherIncomingTracks(ctx context.Context) ([]*IncomingTrack, error) { + var sdp sdp.SessionDescription + sdp.Unmarshal([]byte(co.wr.RemoteDescription().SDP)) //nolint:errcheck + + maxTrackCount := len(sdp.MediaDescriptions) + var tracks []*IncomingTrack t := time.NewTimer(time.Duration(co.TrackGatherTimeout)) @@ -346,7 +380,7 @@ func (co *PeerConnection) GatherIncomingTracks( for { select { case <-t.C: - if maxCount == 0 && len(tracks) != 0 { + if len(tracks) != 0 { return tracks, nil } return nil, fmt.Errorf("deadline exceeded while waiting tracks") @@ -358,7 +392,7 @@ func (co *PeerConnection) GatherIncomingTracks( } tracks = append(tracks, track) - if len(tracks) == maxCount || len(tracks) >= 2 { + if len(tracks) >= maxTrackCount { return tracks, nil } diff --git a/internal/protocols/webrtc/peer_connection_test.go b/internal/protocols/webrtc/peer_connection_test.go index 8c265dbd..c85ff31c 100644 --- a/internal/protocols/webrtc/peer_connection_test.go +++ b/internal/protocols/webrtc/peer_connection_test.go @@ -284,7 +284,7 @@ func TestPeerConnectionPublishRead(t *testing.T) { }) require.NoError(t, err) - inc, err := pc2.GatherIncomingTracks(context.Background(), 1) + inc, err := pc2.GatherIncomingTracks(context.Background()) require.NoError(t, err) require.Equal(t, ca.out, inc[0].Format()) diff --git a/internal/protocols/webrtc/track_count.go b/internal/protocols/webrtc/track_count.go deleted file mode 100644 index 99e9abea..00000000 --- a/internal/protocols/webrtc/track_count.go +++ /dev/null @@ -1,37 +0,0 @@ -package webrtc - -import ( - "fmt" - - "github.com/pion/sdp/v3" -) - -// TrackCount returns the track count. -func TrackCount(medias []*sdp.MediaDescription) (int, error) { - videoTrack := false - audioTrack := false - trackCount := 0 - - for _, media := range medias { - switch media.MediaName.Media { - case "video": - if videoTrack { - return 0, fmt.Errorf("only a single video and a single audio track are supported") - } - videoTrack = true - - case "audio": - if audioTrack { - return 0, fmt.Errorf("only a single video and a single audio track are supported") - } - audioTrack = true - - default: - return 0, fmt.Errorf("unsupported media '%s'", media.MediaName.Media) - } - - trackCount++ - } - - return trackCount, nil -} diff --git a/internal/protocols/webrtc/whip_client.go b/internal/protocols/webrtc/whip_client.go index 28c8cd0f..5c1154b8 100644 --- a/internal/protocols/webrtc/whip_client.go +++ b/internal/protocols/webrtc/whip_client.go @@ -169,8 +169,7 @@ func (c *WHIPClient) Read(ctx context.Context) ([]*IncomingTrack, error) { return nil, err } - // check that there are at most two tracks - _, err = TrackCount(sdp.MediaDescriptions) + err = TracksAreValid(sdp.MediaDescriptions) if err != nil { c.deleteSession(context.Background()) //nolint:errcheck c.pc.Close() @@ -210,7 +209,7 @@ outer: } } - tracks, err := c.pc.GatherIncomingTracks(ctx, 0) + tracks, err := c.pc.GatherIncomingTracks(ctx) if err != nil { c.deleteSession(context.Background()) //nolint:errcheck c.pc.Close() diff --git a/internal/servers/webrtc/session.go b/internal/servers/webrtc/session.go index ed6a2066..2a574c21 100644 --- a/internal/servers/webrtc/session.go +++ b/internal/servers/webrtc/session.go @@ -461,7 +461,7 @@ func (s *session) runPublish() (int, error) { return http.StatusBadRequest, err } - trackCount, err := webrtc.TrackCount(sdp.MediaDescriptions) + err = webrtc.TracksAreValid(sdp.MediaDescriptions) if err != nil { // RFC draft-ietf-wish-whip // if the number of audio and or video @@ -489,7 +489,7 @@ func (s *session) runPublish() (int, error) { s.pc = pc s.mutex.Unlock() - tracks, err := pc.GatherIncomingTracks(s.ctx, trackCount) + tracks, err := pc.GatherIncomingTracks(s.ctx) if err != nil { return 0, err } From 44953c8e05b7a1db2acbe5b8a9f30adbc5ff27fd Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Sun, 9 Jun 2024 23:09:55 +0200 Subject: [PATCH 19/52] webrtc: fix supported AV1 profiles (#3442) --- internal/protocols/webrtc/incoming_track.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/protocols/webrtc/incoming_track.go b/internal/protocols/webrtc/incoming_track.go index fa5fd878..96944031 100644 --- a/internal/protocols/webrtc/incoming_track.go +++ b/internal/protocols/webrtc/incoming_track.go @@ -29,7 +29,7 @@ var incomingVideoCodecs = []webrtc.RTPCodecParameters{ RTPCodecCapability: webrtc.RTPCodecCapability{ MimeType: webrtc.MimeTypeAV1, ClockRate: 90000, - SDPFmtpLine: "profile-id=1", + SDPFmtpLine: "profile=1", }, PayloadType: 96, }, From 427fea30ed56b53e18478fd72a2ff4767fe512c4 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Sun, 9 Jun 2024 23:18:47 +0200 Subject: [PATCH 20/52] fix webrtc/VP9 tests (#3443) --- internal/servers/webrtc/server_test.go | 17 ++++++++--------- internal/servers/webrtc/session.go | 11 ++++++++--- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/internal/servers/webrtc/server_test.go b/internal/servers/webrtc/server_test.go index 74e41f00..c5682c34 100644 --- a/internal/servers/webrtc/server_test.go +++ b/internal/servers/webrtc/server_test.go @@ -26,10 +26,6 @@ import ( "github.com/stretchr/testify/require" ) -func uint16Ptr(v uint16) *uint16 { - return &v -} - func checkClose(t *testing.T, closeFunc func() error) { require.NoError(t, closeFunc()) } @@ -357,8 +353,7 @@ func TestServerRead(t *testing.T) { }, []byte{0, 2, 1, 2}, }, - // TODO: check why this doesn't work - /*{ + { "vp9", []*description.Media{{ Type: description.MediaTypeVideo, @@ -367,10 +362,14 @@ func TestServerRead(t *testing.T) { }}, }}, &unit.VP9{ - Frame: []byte{1, 2}, + Frame: []byte{0x82, 0x49, 0x83, 0x42, 0x0, 0x77, 0xf0, 0x32, 0x34}, }, - []byte{1, 2}, - },*/ + []byte{ + 0x8f, 0xa0, 0xfd, 0x18, 0x07, 0x80, 0x03, 0x24, + 0x01, 0x14, 0x01, 0x82, 0x49, 0x83, 0x42, 0x00, + 0x77, 0xf0, 0x32, 0x34, + }, + }, { "vp8", []*description.Media{{ diff --git a/internal/servers/webrtc/session.go b/internal/servers/webrtc/session.go index 2a574c21..ea7f1a49 100644 --- a/internal/servers/webrtc/session.go +++ b/internal/servers/webrtc/session.go @@ -35,10 +35,14 @@ import ( ) var errNoSupportedCodecs = errors.New( - "the stream doesn't contain any supported codec, which are currently AV1, VP9, VP8, H264, Opus, G722, G711") + "the stream doesn't contain any supported codec, which are currently AV1, VP9, VP8, H264, Opus, G722, G711, LPCM") type setupStreamFunc func(*webrtc.OutgoingTrack) error +func uint16Ptr(v uint16) *uint16 { + return &v +} + func findVideoTrack( stream *stream.Stream, writer *asyncwriter.Writer, @@ -87,8 +91,9 @@ func findVideoTrack( if vp9Format != nil { return vp9Format, func(track *webrtc.OutgoingTrack) error { encoder := &rtpvp9.Encoder{ - PayloadType: 96, - PayloadMaxSize: webrtcPayloadMaxSize, + PayloadType: 96, + PayloadMaxSize: webrtcPayloadMaxSize, + InitialPictureID: uint16Ptr(8445), } err := encoder.Init() if err != nil { From 511b276b4d228ffb5efd5b4ad5db780ec5b8146d Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Mon, 10 Jun 2024 00:57:26 +0200 Subject: [PATCH 21/52] webrtc: support reading G711 16khz tracks (#2848) (#3445) --- internal/protocols/webrtc/outgoing_track.go | 65 ++++++++----- .../protocols/webrtc/peer_connection_test.go | 71 ++++++++++----- internal/servers/webrtc/server_test.go | 17 +++- internal/servers/webrtc/session.go | 91 +++++++++++++++++-- 4 files changed, 194 insertions(+), 50 deletions(-) diff --git a/internal/protocols/webrtc/outgoing_track.go b/internal/protocols/webrtc/outgoing_track.go index efd79cb6..ae591832 100644 --- a/internal/protocols/webrtc/outgoing_track.go +++ b/internal/protocols/webrtc/outgoing_track.go @@ -92,15 +92,43 @@ func (t *OutgoingTrack) codecParameters() (webrtc.RTPCodecParameters, error) { }, nil case *format.G711: - if forma.SampleRate != 8000 { - return webrtc.RTPCodecParameters{}, fmt.Errorf("unsupported G711 sample rate") + // These are the sample rates and channels supported by Chrome. + // Different sample rates and channels can be streamed too but we don't want compatibility issues. + // https://webrtc.googlesource.com/src/+/refs/heads/main/modules/audio_coding/codecs/pcm16b/audio_decoder_pcm16b.cc#23 + if forma.ClockRate() != 8000 && forma.ClockRate() != 16000 && + forma.ClockRate() != 32000 && forma.ClockRate() != 48000 { + return webrtc.RTPCodecParameters{}, fmt.Errorf("unsupported clock rate: %d", forma.ClockRate()) + } + if forma.ChannelCount != 1 && forma.ChannelCount != 2 { + return webrtc.RTPCodecParameters{}, fmt.Errorf("unsupported channel count: %d", forma.ChannelCount) } - if forma.MULaw { - if forma.ChannelCount != 1 { + if forma.SampleRate == 8000 { + if forma.MULaw { + if forma.ChannelCount != 1 { + return webrtc.RTPCodecParameters{ + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: webrtc.MimeTypePCMU, + ClockRate: uint32(forma.SampleRate), + Channels: uint16(forma.ChannelCount), + }, + PayloadType: 96, + }, nil + } + return webrtc.RTPCodecParameters{ RTPCodecCapability: webrtc.RTPCodecCapability{ MimeType: webrtc.MimeTypePCMU, + ClockRate: 8000, + }, + PayloadType: 0, + }, nil + } + + if forma.ChannelCount != 1 { + return webrtc.RTPCodecParameters{ + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: webrtc.MimeTypePCMA, ClockRate: uint32(forma.SampleRate), Channels: uint16(forma.ChannelCount), }, @@ -110,30 +138,20 @@ func (t *OutgoingTrack) codecParameters() (webrtc.RTPCodecParameters, error) { return webrtc.RTPCodecParameters{ RTPCodecCapability: webrtc.RTPCodecCapability{ - MimeType: webrtc.MimeTypePCMU, + MimeType: webrtc.MimeTypePCMA, ClockRate: 8000, }, - PayloadType: 0, - }, nil - } - - if forma.ChannelCount != 1 { - return webrtc.RTPCodecParameters{ - RTPCodecCapability: webrtc.RTPCodecCapability{ - MimeType: webrtc.MimeTypePCMA, - ClockRate: uint32(forma.SampleRate), - Channels: uint16(forma.ChannelCount), - }, - PayloadType: 96, + PayloadType: 8, }, nil } return webrtc.RTPCodecParameters{ RTPCodecCapability: webrtc.RTPCodecCapability{ - MimeType: webrtc.MimeTypePCMA, - ClockRate: 8000, + MimeType: mimeTypeL16, + ClockRate: uint32(forma.ClockRate()), + Channels: uint16(forma.ChannelCount), }, - PayloadType: 8, + PayloadType: 96, }, nil case *format.LPCM: @@ -141,10 +159,13 @@ func (t *OutgoingTrack) codecParameters() (webrtc.RTPCodecParameters, error) { return webrtc.RTPCodecParameters{}, fmt.Errorf("unsupported LPCM bit depth: %d", forma.BitDepth) } - if forma.ClockRate() != 8000 && forma.ClockRate() != 16000 && forma.ClockRate() != 48000 { + // These are the sample rates and channels supported by Chrome. + // Different sample rates and channels can be streamed too but we don't want compatibility issues. + // https://webrtc.googlesource.com/src/+/refs/heads/main/modules/audio_coding/codecs/pcm16b/audio_decoder_pcm16b.cc#23 + if forma.ClockRate() != 8000 && forma.ClockRate() != 16000 && + forma.ClockRate() != 32000 && forma.ClockRate() != 48000 { return webrtc.RTPCodecParameters{}, fmt.Errorf("unsupported clock rate: %d", forma.ClockRate()) } - if forma.ChannelCount != 1 && forma.ChannelCount != 2 { return webrtc.RTPCodecParameters{}, fmt.Errorf("unsupported channel count: %d", forma.ChannelCount) } diff --git a/internal/protocols/webrtc/peer_connection_test.go b/internal/protocols/webrtc/peer_connection_test.go index c85ff31c..a737bc45 100644 --- a/internal/protocols/webrtc/peer_connection_test.go +++ b/internal/protocols/webrtc/peer_connection_test.go @@ -114,7 +114,35 @@ func TestPeerConnectionPublishRead(t *testing.T) { &format.G722{}, }, { - "g711 pcma stereo", + "g711 pcma 8khz mono", + &format.G711{ + PayloadTyp: 8, + SampleRate: 8000, + ChannelCount: 1, + }, + &format.G711{ + PayloadTyp: 8, + SampleRate: 8000, + ChannelCount: 1, + }, + }, + { + "g711 pcmu 8khz mono", + &format.G711{ + MULaw: true, + PayloadTyp: 0, + SampleRate: 8000, + ChannelCount: 1, + }, + &format.G711{ + MULaw: true, + PayloadTyp: 0, + SampleRate: 8000, + ChannelCount: 1, + }, + }, + { + "g711 pcma 8khz stereo", &format.G711{ PayloadTyp: 96, SampleRate: 8000, @@ -127,7 +155,7 @@ func TestPeerConnectionPublishRead(t *testing.T) { }, }, { - "g711 pcmu stereo", + "g711 pcmu 8khz stereo", &format.G711{ MULaw: true, PayloadTyp: 96, @@ -142,35 +170,36 @@ func TestPeerConnectionPublishRead(t *testing.T) { }, }, { - "g711 pcma mono", + "g711 pcma 16khz stereo", &format.G711{ - PayloadTyp: 8, - SampleRate: 8000, - ChannelCount: 1, + PayloadTyp: 96, + SampleRate: 16000, + ChannelCount: 2, }, - &format.G711{ - PayloadTyp: 8, - SampleRate: 8000, - ChannelCount: 1, + &format.LPCM{ + PayloadTyp: 96, + BitDepth: 16, + SampleRate: 16000, + ChannelCount: 2, }, }, { - "g711 pcmu mono", + "g711 pcmu 16khz stereo", &format.G711{ MULaw: true, - PayloadTyp: 0, - SampleRate: 8000, - ChannelCount: 1, + PayloadTyp: 96, + SampleRate: 16000, + ChannelCount: 2, }, - &format.G711{ - MULaw: true, - PayloadTyp: 0, - SampleRate: 8000, - ChannelCount: 1, + &format.LPCM{ + PayloadTyp: 96, + BitDepth: 16, + SampleRate: 16000, + ChannelCount: 2, }, }, { - "l16 8000 stereo", + "l16 8khz stereo", &format.LPCM{ PayloadTyp: 96, BitDepth: 16, @@ -185,7 +214,7 @@ func TestPeerConnectionPublishRead(t *testing.T) { }, }, { - "l16 16000 stereo", + "l16 16khz stereo", &format.LPCM{ PayloadTyp: 96, BitDepth: 16, diff --git a/internal/servers/webrtc/server_test.go b/internal/servers/webrtc/server_test.go index c5682c34..0167fe06 100644 --- a/internal/servers/webrtc/server_test.go +++ b/internal/servers/webrtc/server_test.go @@ -431,7 +431,7 @@ func TestServerRead(t *testing.T) { []byte{1, 2}, },*/ { - "g711", + "g711 8khz mono", []*description.Media{{ Type: description.MediaTypeAudio, Formats: []format.Format{&format.G711{ @@ -445,6 +445,21 @@ func TestServerRead(t *testing.T) { }, []byte{1, 2, 3}, }, + { + "g711 16khz stereo", + []*description.Media{{ + Type: description.MediaTypeAudio, + Formats: []format.Format{&format.G711{ + MULaw: true, + SampleRate: 16000, + ChannelCount: 2, + }}, + }}, + &unit.G711{ + Samples: []byte{1, 2, 3, 4}, + }, + []byte{0x86, 0x84, 0x8a, 0x84, 0x8e, 0x84, 0x92, 0x84}, + }, { "lpcm", []*description.Media{{ diff --git a/internal/servers/webrtc/session.go b/internal/servers/webrtc/session.go index ea7f1a49..421b4a18 100644 --- a/internal/servers/webrtc/session.go +++ b/internal/servers/webrtc/session.go @@ -2,6 +2,7 @@ package webrtc import ( "context" + "crypto/rand" "encoding/hex" "errors" "fmt" @@ -18,6 +19,7 @@ import ( "github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp8" "github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp9" "github.com/bluenviron/gortsplib/v4/pkg/rtptime" + "github.com/bluenviron/mediacommon/pkg/codecs/g711" "github.com/google/uuid" "github.com/pion/ice/v2" "github.com/pion/sdp/v3" @@ -43,6 +45,15 @@ func uint16Ptr(v uint16) *uint16 { return &v } +func randUint32() (uint32, error) { + var b [4]byte + _, err := rand.Read(b[:]) + if err != nil { + return 0, err + } + return uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3]), nil +} + func findVideoTrack( stream *stream.Stream, writer *asyncwriter.Writer, @@ -254,13 +265,72 @@ func findAudioTrack( if g711Format != nil { return g711Format, func(track *webrtc.OutgoingTrack) error { - stream.AddReader(writer, media, g711Format, func(u unit.Unit) error { - for _, pkt := range u.GetRTPPackets() { - track.WriteRTP(pkt) //nolint:errcheck + if g711Format.SampleRate == 8000 { + curTimestamp, err := randUint32() + if err != nil { + return err } - return nil - }) + stream.AddReader(writer, media, g711Format, func(u unit.Unit) error { + for _, pkt := range u.GetRTPPackets() { + // recompute timestamp from scratch. + // Chrome requires a precise timestamp that FFmpeg doesn't provide. + pkt.Timestamp = curTimestamp + curTimestamp += uint32(len(pkt.Payload)) / uint32(g711Format.ChannelCount) + + track.WriteRTP(pkt) //nolint:errcheck + } + + return nil + }) + } else { + encoder := &rtplpcm.Encoder{ + PayloadType: 96, + PayloadMaxSize: webrtcPayloadMaxSize, + BitDepth: 16, + ChannelCount: g711Format.ChannelCount, + } + err := encoder.Init() + if err != nil { + return err + } + + curTimestamp, err := randUint32() + if err != nil { + return err + } + + stream.AddReader(writer, media, g711Format, func(u unit.Unit) error { + tunit := u.(*unit.G711) + + if tunit.Samples == nil { + return nil + } + + var lpcmSamples []byte + if g711Format.MULaw { + lpcmSamples = g711.DecodeMulaw(tunit.Samples) + } else { + lpcmSamples = g711.DecodeAlaw(tunit.Samples) + } + + packets, err := encoder.Encode(lpcmSamples) + if err != nil { + return nil //nolint:nilerr + } + + for _, pkt := range packets { + // recompute timestamp from scratch. + // Chrome requires a precise timestamp that FFmpeg doesn't provide. + pkt.Timestamp = curTimestamp + curTimestamp += uint32(len(pkt.Payload)) / 2 / uint32(g711Format.ChannelCount) + + track.WriteRTP(pkt) //nolint:errcheck + } + + return nil + }) + } return nil } } @@ -281,6 +351,11 @@ func findAudioTrack( return err } + curTimestamp, err := randUint32() + if err != nil { + return err + } + stream.AddReader(writer, media, lpcmFormat, func(u unit.Unit) error { tunit := u.(*unit.LPCM) @@ -294,7 +369,11 @@ func findAudioTrack( } for _, pkt := range packets { - pkt.Timestamp += tunit.RTPPackets[0].Timestamp + // recompute timestamp from scratch. + // Chrome requires a precise timestamp that FFmpeg doesn't provide. + pkt.Timestamp = curTimestamp + curTimestamp += uint32(len(pkt.Payload)) / 2 / uint32(lpcmFormat.ChannelCount) + track.WriteRTP(pkt) //nolint:errcheck } From 5fe2819546a09d4c7b08979b07e94f0b3566a404 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Mon, 10 Jun 2024 09:54:08 +0200 Subject: [PATCH 22/52] webrtc: set fmtp of outgoing VP9 and multiopus tracks (#3446) --- internal/protocols/webrtc/outgoing_track.go | 58 +++++++---- .../protocols/webrtc/peer_connection_test.go | 95 ++++++++++++++++++- 2 files changed, 129 insertions(+), 24 deletions(-) diff --git a/internal/protocols/webrtc/outgoing_track.go b/internal/protocols/webrtc/outgoing_track.go index ae591832..3f72c1cc 100644 --- a/internal/protocols/webrtc/outgoing_track.go +++ b/internal/protocols/webrtc/outgoing_track.go @@ -8,6 +8,15 @@ import ( "github.com/pion/webrtc/v3" ) +var multichannelOpusSDP = map[int]string{ + 3: "channel_mapping=0,2,1;num_streams=2;coupled_streams=1", + 4: "channel_mapping=0,1,2,3;num_streams=2;coupled_streams=2", + 5: "channel_mapping=0,4,1,2,3;num_streams=3;coupled_streams=2", + 6: "channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2", + 7: "channel_mapping=0,4,1,2,3,5,6;num_streams=4;coupled_streams=4", + 8: "channel_mapping=0,6,1,4,5,2,3,7;num_streams=5;coupled_streams=4", +} + // OutgoingTrack is a WebRTC outgoing track type OutgoingTrack struct { Format format.Format @@ -29,8 +38,9 @@ func (t *OutgoingTrack) codecParameters() (webrtc.RTPCodecParameters, error) { case *format.VP9: return webrtc.RTPCodecParameters{ RTPCodecCapability: webrtc.RTPCodecCapability{ - MimeType: webrtc.MimeTypeVP9, - ClockRate: 90000, + MimeType: webrtc.MimeTypeVP9, + ClockRate: 90000, + SDPFmtpLine: "profile-id=0", }, PayloadType: 96, }, nil @@ -55,32 +65,38 @@ func (t *OutgoingTrack) codecParameters() (webrtc.RTPCodecParameters, error) { }, nil case *format.Opus: - if forma.ChannelCount > 2 { + switch forma.ChannelCount { + case 1, 2: return webrtc.RTPCodecParameters{ RTPCodecCapability: webrtc.RTPCodecCapability{ - MimeType: mimeTypeMultiopus, + MimeType: webrtc.MimeTypeOpus, ClockRate: 48000, - Channels: uint16(forma.ChannelCount), + Channels: 2, + SDPFmtpLine: func() string { + s := "minptime=10;useinbandfec=1" + if forma.ChannelCount == 2 { + s += ";stereo=1;sprop-stereo=1" + } + return s + }(), }, PayloadType: 96, }, nil - } - return webrtc.RTPCodecParameters{ - RTPCodecCapability: webrtc.RTPCodecCapability{ - MimeType: webrtc.MimeTypeOpus, - ClockRate: 48000, - Channels: 2, - SDPFmtpLine: func() string { - s := "minptime=10;useinbandfec=1" - if forma.ChannelCount == 2 { - s += ";stereo=1;sprop-stereo=1" - } - return s - }(), - }, - PayloadType: 96, - }, nil + case 3, 4, 5, 6, 7, 8: + return webrtc.RTPCodecParameters{ + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: mimeTypeMultiopus, + ClockRate: 48000, + Channels: uint16(forma.ChannelCount), + SDPFmtpLine: multichannelOpusSDP[forma.ChannelCount], + }, + PayloadType: 96, + }, nil + + default: + return webrtc.RTPCodecParameters{}, fmt.Errorf("unsupported channel count: %d", forma.ChannelCount) + } case *format.G722: return webrtc.RTPCodecParameters{ diff --git a/internal/protocols/webrtc/peer_connection_test.go b/internal/protocols/webrtc/peer_connection_test.go index a737bc45..82f1f203 100644 --- a/internal/protocols/webrtc/peer_connection_test.go +++ b/internal/protocols/webrtc/peer_connection_test.go @@ -9,6 +9,7 @@ import ( "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/test" "github.com/pion/rtp" + "github.com/pion/webrtc/v3" "github.com/stretchr/testify/require" ) @@ -36,15 +37,20 @@ func TestPeerConnectionCloseImmediately(t *testing.T) { func TestPeerConnectionPublishRead(t *testing.T) { for _, ca := range []struct { - name string - in format.Format - out format.Format + name string + in format.Format + webrtcOut webrtc.RTPCodecCapability + out format.Format }{ { "av1", &format.AV1{ PayloadTyp: 96, }, + webrtc.RTPCodecCapability{ + MimeType: "video/AV1", + ClockRate: 90000, + }, &format.AV1{ PayloadTyp: 96, }, @@ -54,6 +60,11 @@ func TestPeerConnectionPublishRead(t *testing.T) { &format.VP9{ PayloadTyp: 96, }, + webrtc.RTPCodecCapability{ + MimeType: "video/VP9", + ClockRate: 90000, + SDPFmtpLine: "profile-id=0", + }, &format.VP9{ PayloadTyp: 96, }, @@ -63,6 +74,10 @@ func TestPeerConnectionPublishRead(t *testing.T) { &format.VP8{ PayloadTyp: 96, }, + webrtc.RTPCodecCapability{ + MimeType: "video/VP8", + ClockRate: 90000, + }, &format.VP8{ PayloadTyp: 96, }, @@ -70,6 +85,11 @@ func TestPeerConnectionPublishRead(t *testing.T) { { "h264", test.FormatH264, + webrtc.RTPCodecCapability{ + MimeType: "video/H264", + ClockRate: 90000, + SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", + }, &format.H264{ PayloadTyp: 96, PacketizationMode: 1, @@ -81,6 +101,12 @@ func TestPeerConnectionPublishRead(t *testing.T) { PayloadTyp: 112, ChannelCount: 6, }, + webrtc.RTPCodecCapability{ + MimeType: "audio/multiopus", + ClockRate: 48000, + Channels: 6, + SDPFmtpLine: "channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2", + }, &format.Opus{ PayloadTyp: 96, ChannelCount: 6, @@ -92,6 +118,12 @@ func TestPeerConnectionPublishRead(t *testing.T) { PayloadTyp: 111, ChannelCount: 2, }, + webrtc.RTPCodecCapability{ + MimeType: "audio/opus", + ClockRate: 48000, + Channels: 2, + SDPFmtpLine: "minptime=10;useinbandfec=1;stereo=1;sprop-stereo=1", + }, &format.Opus{ PayloadTyp: 96, ChannelCount: 2, @@ -103,6 +135,12 @@ func TestPeerConnectionPublishRead(t *testing.T) { PayloadTyp: 111, ChannelCount: 1, }, + webrtc.RTPCodecCapability{ + MimeType: "audio/opus", + ClockRate: 48000, + Channels: 2, + SDPFmtpLine: "minptime=10;useinbandfec=1", + }, &format.Opus{ PayloadTyp: 96, ChannelCount: 1, @@ -111,6 +149,10 @@ func TestPeerConnectionPublishRead(t *testing.T) { { "g722", &format.G722{}, + webrtc.RTPCodecCapability{ + MimeType: "audio/G722", + ClockRate: 8000, + }, &format.G722{}, }, { @@ -120,6 +162,10 @@ func TestPeerConnectionPublishRead(t *testing.T) { SampleRate: 8000, ChannelCount: 1, }, + webrtc.RTPCodecCapability{ + MimeType: "audio/PCMA", + ClockRate: 8000, + }, &format.G711{ PayloadTyp: 8, SampleRate: 8000, @@ -134,6 +180,10 @@ func TestPeerConnectionPublishRead(t *testing.T) { SampleRate: 8000, ChannelCount: 1, }, + webrtc.RTPCodecCapability{ + MimeType: "audio/PCMU", + ClockRate: 8000, + }, &format.G711{ MULaw: true, PayloadTyp: 0, @@ -148,6 +198,11 @@ func TestPeerConnectionPublishRead(t *testing.T) { SampleRate: 8000, ChannelCount: 2, }, + webrtc.RTPCodecCapability{ + MimeType: "audio/PCMA", + ClockRate: 8000, + Channels: 2, + }, &format.G711{ PayloadTyp: 119, SampleRate: 8000, @@ -162,6 +217,11 @@ func TestPeerConnectionPublishRead(t *testing.T) { SampleRate: 8000, ChannelCount: 2, }, + webrtc.RTPCodecCapability{ + MimeType: "audio/PCMU", + ClockRate: 8000, + Channels: 2, + }, &format.G711{ MULaw: true, PayloadTyp: 118, @@ -176,6 +236,11 @@ func TestPeerConnectionPublishRead(t *testing.T) { SampleRate: 16000, ChannelCount: 2, }, + webrtc.RTPCodecCapability{ + MimeType: "audio/L16", + ClockRate: 16000, + Channels: 2, + }, &format.LPCM{ PayloadTyp: 96, BitDepth: 16, @@ -191,6 +256,11 @@ func TestPeerConnectionPublishRead(t *testing.T) { SampleRate: 16000, ChannelCount: 2, }, + webrtc.RTPCodecCapability{ + MimeType: "audio/L16", + ClockRate: 16000, + Channels: 2, + }, &format.LPCM{ PayloadTyp: 96, BitDepth: 16, @@ -206,6 +276,11 @@ func TestPeerConnectionPublishRead(t *testing.T) { SampleRate: 8000, ChannelCount: 2, }, + webrtc.RTPCodecCapability{ + MimeType: "audio/L16", + ClockRate: 8000, + Channels: 2, + }, &format.LPCM{ PayloadTyp: 96, BitDepth: 16, @@ -221,6 +296,11 @@ func TestPeerConnectionPublishRead(t *testing.T) { SampleRate: 16000, ChannelCount: 2, }, + webrtc.RTPCodecCapability{ + MimeType: "audio/L16", + ClockRate: 16000, + Channels: 2, + }, &format.LPCM{ PayloadTyp: 96, BitDepth: 16, @@ -236,6 +316,11 @@ func TestPeerConnectionPublishRead(t *testing.T) { SampleRate: 48000, ChannelCount: 2, }, + webrtc.RTPCodecCapability{ + MimeType: "audio/L16", + ClockRate: 48000, + Channels: 2, + }, &format.LPCM{ PayloadTyp: 96, BitDepth: 16, @@ -316,6 +401,10 @@ func TestPeerConnectionPublishRead(t *testing.T) { inc, err := pc2.GatherIncomingTracks(context.Background()) require.NoError(t, err) + exp := ca.webrtcOut + exp.RTCPFeedback = inc[0].track.Codec().RTPCodecCapability.RTCPFeedback + require.Equal(t, exp, inc[0].track.Codec().RTPCodecCapability) + require.Equal(t, ca.out, inc[0].Format()) }) } From d0c8e8922342df304840587585641ea148f044d8 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Mon, 10 Jun 2024 10:15:25 +0200 Subject: [PATCH 23/52] fix webrtc/G722 tests (#3444) --- internal/servers/webrtc/server_test.go | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/internal/servers/webrtc/server_test.go b/internal/servers/webrtc/server_test.go index 0167fe06..621e13ec 100644 --- a/internal/servers/webrtc/server_test.go +++ b/internal/servers/webrtc/server_test.go @@ -413,8 +413,7 @@ func TestServerRead(t *testing.T) { }, []byte{1, 2}, }, - // TODO: check why this doesn't work - /*{ + { "g722", []*description.Media{{ Type: description.MediaTypeAudio, @@ -423,13 +422,20 @@ func TestServerRead(t *testing.T) { &unit.Generic{ Base: unit.Base{ RTPPackets: []*rtp.Packet{{ - Header: rtp.Header{}, + Header: rtp.Header{ + Version: 2, + Marker: true, + PayloadType: 9, + SequenceNumber: 1123, + Timestamp: 45343, + SSRC: 563423, + }, Payload: []byte{1, 2}, }}, }, }, []byte{1, 2}, - },*/ + }, { "g711 8khz mono", []*description.Media{{ @@ -483,7 +489,7 @@ func TestServerRead(t *testing.T) { stream, err := stream.New( 1460, desc, - true, + reflect.TypeOf(ca.unit) != reflect.TypeOf(&unit.Generic{}), test.NilLogger, ) require.NoError(t, err) @@ -547,7 +553,13 @@ func TestServerRead(t *testing.T) { r := reflect.New(reflect.TypeOf(ca.unit).Elem()) r.Elem().Set(reflect.ValueOf(ca.unit).Elem()) - stream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], r.Interface().(unit.Unit)) + + if g, ok := r.Interface().(*unit.Generic); ok { + clone := *g.RTPPackets[0] + stream.WriteRTPPacket(desc.Medias[0], desc.Medias[0].Formats[0], &clone, time.Time{}, 0) + } else { + stream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], r.Interface().(unit.Unit)) + } } }() From 191129453927d1661a63aba303868bdeba312b8b Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Mon, 10 Jun 2024 12:51:24 +0200 Subject: [PATCH 24/52] bump pion/webrtc (#3447) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 33a16b65..a57f472c 100644 --- a/go.mod +++ b/go.mod @@ -81,4 +81,4 @@ replace code.cloudfoundry.org/bytefmt => github.com/cloudfoundry/bytefmt v0.0.0- replace github.com/pion/ice/v2 => github.com/aler9/ice/v2 v2.0.0-20240608212222-2eebc68350c9 -replace github.com/pion/webrtc/v3 => github.com/aler9/webrtc/v3 v3.0.0-20240608212857-ef9d26bd8aa6 +replace github.com/pion/webrtc/v3 => github.com/aler9/webrtc/v3 v3.0.0-20240610104456-eaec24056d06 diff --git a/go.sum b/go.sum index 3036700d..8be60598 100644 --- a/go.sum +++ b/go.sum @@ -12,8 +12,8 @@ github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= github.com/aler9/ice/v2 v2.0.0-20240608212222-2eebc68350c9 h1:Vax9SzYE68ZYLwFaK7lnCV2ZhX9/YqAJX6xxROPRqEM= github.com/aler9/ice/v2 v2.0.0-20240608212222-2eebc68350c9/go.mod h1:KXJJcZK7E8WzrBEYnV4UtqEZsGeWfHxsNqhVcVvgjxw= -github.com/aler9/webrtc/v3 v3.0.0-20240608212857-ef9d26bd8aa6 h1:pQLsx1xdKND5mwOO+VTrro1xqZRyRDN+7JAgYv80UYk= -github.com/aler9/webrtc/v3 v3.0.0-20240608212857-ef9d26bd8aa6/go.mod h1:M1RAe3TNTD1tzyvqHrbVODfwdPGSXOUo/OgpoGGJqFY= +github.com/aler9/webrtc/v3 v3.0.0-20240610104456-eaec24056d06 h1:WtKhXOpd8lgTeXF3RQVOzkNRuy83ygvWEpMYD2aoY3Q= +github.com/aler9/webrtc/v3 v3.0.0-20240610104456-eaec24056d06/go.mod h1:M1RAe3TNTD1tzyvqHrbVODfwdPGSXOUo/OgpoGGJqFY= github.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflxkRsZA= github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0= github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwfKZ1c= From 095921dc26b00889568932756c58982f587a5823 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Mon, 10 Jun 2024 15:41:05 +0200 Subject: [PATCH 25/52] webrtc: on browsers, display error messages from server (#3448) --- internal/protocols/webrtc/peer_connection.go | 2 +- internal/servers/webrtc/publish_index.html | 17 ++++++++++++----- internal/servers/webrtc/read_index.html | 8 ++++++-- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/internal/protocols/webrtc/peer_connection.go b/internal/protocols/webrtc/peer_connection.go index 850ca345..30ea0688 100644 --- a/internal/protocols/webrtc/peer_connection.go +++ b/internal/protocols/webrtc/peer_connection.go @@ -311,7 +311,7 @@ func (co *PeerConnection) CreateFullAnswer( answer, err := co.wr.CreateAnswer(nil) if err != nil { if errors.Is(err, webrtc.ErrSenderWithNoCodecs) { - return nil, fmt.Errorf("track codecs are not supported by remote") + return nil, fmt.Errorf("codecs not supported by client") } return nil, err } diff --git a/internal/servers/webrtc/publish_index.html b/internal/servers/webrtc/publish_index.html index 122bba6a..832006f1 100644 --- a/internal/servers/webrtc/publish_index.html +++ b/internal/servers/webrtc/publish_index.html @@ -409,7 +409,7 @@ const sendLocalCandidates = (candidates) => { }) .then((res) => { if (res.status !== 204) { - throw new Error('bad status code'); + throw new Error(`bad status code ${res.status}`); } }) .catch((err) => { @@ -464,13 +464,20 @@ const sendOffer = (offer) => { body: offer, }) .then((res) => { - if (res.status !== 201) { - throw new Error('bad status code'); + switch (res.status) { + case 201: + break; + case 400: + return res.json().then((e) => { throw new Error(e.error); }); + default: + throw new Error(`bad status code ${res.status}`); } + sessionUrl = new URL(res.headers.get('location'), window.location.href).toString(); - return res.text(); + + return res.text() + .then((answer) => onRemoteAnswer(answer)); }) - .then((answer) => onRemoteAnswer(answer)) .catch((err) => { onError(err.toString(), true); }); diff --git a/internal/servers/webrtc/read_index.html b/internal/servers/webrtc/read_index.html index f3052d59..427073f9 100644 --- a/internal/servers/webrtc/read_index.html +++ b/internal/servers/webrtc/read_index.html @@ -426,13 +426,17 @@ const sendOffer = (offer) => { break; case 404: throw new Error('stream not found'); + case 400: + return res.json().then((e) => { throw new Error(e.error); }); default: throw new Error(`bad status code ${res.status}`); } + sessionUrl = new URL(res.headers.get('location'), window.location.href).toString(); - return res.text(); + + return res.text() + .then((sdp) => onRemoteAnswer(sdp)); }) - .then((sdp) => onRemoteAnswer(sdp)) .catch((err) => { onError(err.toString()); }); From 427249877cbb4c9b9a1b683fab18f38d855b5a3e Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Mon, 10 Jun 2024 15:43:52 +0200 Subject: [PATCH 26/52] webrtc: fix error "Failed to setup RTCP mux" on some readers (#3381) (#3449) --- internal/protocols/webrtc/peer_connection.go | 32 +++++++++ .../protocols/webrtc/peer_connection_test.go | 66 +++++++++++++++++++ 2 files changed, 98 insertions(+) diff --git a/internal/protocols/webrtc/peer_connection.go b/internal/protocols/webrtc/peer_connection.go index 30ea0688..5f1daee6 100644 --- a/internal/protocols/webrtc/peer_connection.go +++ b/internal/protocols/webrtc/peer_connection.go @@ -128,6 +128,9 @@ func (co *PeerConnection) Start() error { mediaEngine := &webrtc.MediaEngine{} if co.Publish { + videoSetupped := false + audioSetupped := false + for _, tr := range co.OutgoingTracks { params, err := tr.codecParameters() if err != nil { @@ -137,8 +140,10 @@ func (co *PeerConnection) Start() error { var codecType webrtc.RTPCodecType if tr.isVideo() { codecType = webrtc.RTPCodecTypeVideo + videoSetupped = true } else { codecType = webrtc.RTPCodecTypeAudio + audioSetupped = true } err = mediaEngine.RegisterCodec(params, codecType) @@ -146,6 +151,33 @@ func (co *PeerConnection) Start() error { return err } } + + // always register at least a video and a audio codec + // otherwise handshake will fail or audio will be muted on some clients (like Firefox) + if !videoSetupped { + err := mediaEngine.RegisterCodec(webrtc.RTPCodecParameters{ + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: webrtc.MimeTypeVP8, + ClockRate: 90000, + }, + PayloadType: 96, + }, webrtc.RTPCodecTypeVideo) + if err != nil { + return err + } + } + if !audioSetupped { + err := mediaEngine.RegisterCodec(webrtc.RTPCodecParameters{ + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: webrtc.MimeTypePCMU, + ClockRate: 8000, + }, + PayloadType: 0, + }, webrtc.RTPCodecTypeAudio) + if err != nil { + return err + } + } } else { for _, codec := range incomingVideoCodecs { err := mediaEngine.RegisterCodec(codec, webrtc.RTPCodecTypeVideo) diff --git a/internal/protocols/webrtc/peer_connection_test.go b/internal/protocols/webrtc/peer_connection_test.go index 82f1f203..1927786f 100644 --- a/internal/protocols/webrtc/peer_connection_test.go +++ b/internal/protocols/webrtc/peer_connection_test.go @@ -9,6 +9,7 @@ import ( "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/test" "github.com/pion/rtp" + "github.com/pion/sdp/v3" "github.com/pion/webrtc/v3" "github.com/stretchr/testify/require" ) @@ -409,3 +410,68 @@ func TestPeerConnectionPublishRead(t *testing.T) { }) } } + +// test that an audio codec is present regardless of the fact that an audio track is not. +func TestPeerConnectionFallbackCodecs(t *testing.T) { + pc1 := &PeerConnection{ + HandshakeTimeout: conf.StringDuration(10 * time.Second), + TrackGatherTimeout: conf.StringDuration(2 * time.Second), + LocalRandomUDP: true, + IPsFromInterfaces: true, + Publish: false, + Log: test.NilLogger, + } + err := pc1.Start() + require.NoError(t, err) + defer pc1.Close() + + pc2 := &PeerConnection{ + HandshakeTimeout: conf.StringDuration(10 * time.Second), + TrackGatherTimeout: conf.StringDuration(2 * time.Second), + LocalRandomUDP: true, + IPsFromInterfaces: true, + Publish: true, + OutgoingTracks: []*OutgoingTrack{{ + Format: &format.AV1{ + PayloadTyp: 96, + }, + }}, + Log: test.NilLogger, + } + err = pc2.Start() + require.NoError(t, err) + defer pc2.Close() + + offer, err := pc1.CreatePartialOffer() + require.NoError(t, err) + + answer, err := pc2.CreateFullAnswer(context.Background(), offer) + require.NoError(t, err) + + var s sdp.SessionDescription + err = s.Unmarshal([]byte(answer.SDP)) + require.NoError(t, err) + + require.Equal(t, []*sdp.MediaDescription{ + { + MediaName: sdp.MediaName{ + Media: "video", + Port: sdp.RangedPort{Value: 9}, + Protos: []string{"UDP", "TLS", "RTP", "SAVPF"}, + Formats: []string{"97"}, + }, + ConnectionInformation: s.MediaDescriptions[0].ConnectionInformation, + Attributes: s.MediaDescriptions[0].Attributes, + }, + { + MediaName: sdp.MediaName{ + Media: "audio", + Port: sdp.RangedPort{Value: 9}, + Protos: []string{"UDP", "TLS", "RTP", "SAVPF"}, + Formats: []string{"0"}, + }, + ConnectionInformation: s.MediaDescriptions[1].ConnectionInformation, + Attributes: s.MediaDescriptions[1].Attributes, + }, + }, s.MediaDescriptions) +} From e996ae5a21371240173068454eed68d9f95a91c4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 10 Jun 2024 20:01:34 +0200 Subject: [PATCH 27/52] build(deps): bump github.com/gorilla/websocket from 1.5.1 to 1.5.2 (#3450) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index a57f472c..f72fe77b 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/uuid v1.6.0 github.com/gookit/color v1.5.4 - github.com/gorilla/websocket v1.5.1 + github.com/gorilla/websocket v1.5.2 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/matthewhartstonge/argon2 v1.0.0 github.com/pion/ice/v2 v2.3.24 diff --git a/go.sum b/go.sum index 8be60598..3eada60c 100644 --- a/go.sum +++ b/go.sum @@ -86,8 +86,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= -github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= -github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= +github.com/gorilla/websocket v1.5.2 h1:qoW6V1GT3aZxybsbC6oLnailWnB+qTMVwMreOso9XUw= +github.com/gorilla/websocket v1.5.2/go.mod h1:0n9H61RBAcf5/38py2MCYbxzPIY9rOkpvvMT24Rqs30= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= From 3a2594d610552120442b251f9befdcbe405e8987 Mon Sep 17 00:00:00 2001 From: Jacob Su Date: Tue, 11 Jun 2024 19:26:45 +0800 Subject: [PATCH 28/52] rtmp: fix error when publishing AAC audio tracks (#3414) * fix single aac file rtmp publish error. * add tests --------- Co-authored-by: aler9 <46489434+aler9@users.noreply.github.com> --- internal/protocols/rtmp/reader.go | 3 ++ internal/protocols/rtmp/reader_test.go | 71 ++++++++++++++++++++++++++ 2 files changed, 74 insertions(+) diff --git a/internal/protocols/rtmp/reader.go b/internal/protocols/rtmp/reader.go index 64f8042f..846a9e01 100644 --- a/internal/protocols/rtmp/reader.go +++ b/internal/protocols/rtmp/reader.go @@ -327,6 +327,9 @@ func tracksFromMetadata(conn *Conn, payload []interface{}) (format.Format, forma } if audioTrack == nil { + if len(msg.Payload) == 0 { + continue + } switch { case msg.Codec == message.CodecMPEG4Audio && msg.AACType == message.AudioAACTypeConfig: diff --git a/internal/protocols/rtmp/reader_test.go b/internal/protocols/rtmp/reader_test.go index 052eeb19..41d1d213 100644 --- a/internal/protocols/rtmp/reader_test.go +++ b/internal/protocols/rtmp/reader_test.go @@ -332,6 +332,77 @@ func TestReadTracks(t *testing.T) { }, }, }, + { + "aac, issue mediamtx/3414 (empty audio payload)", + nil, + &format.MPEG4Audio{ + PayloadTyp: 96, + Config: &mpeg4audio.Config{ + Type: 2, + SampleRate: 44100, + ChannelCount: 2, + }, + SizeLength: 13, + IndexLength: 3, + IndexDeltaLength: 3, + }, + []message.Message{ + &message.DataAMF0{ + ChunkStreamID: 4, + MessageStreamID: 1, + Payload: []interface{}{ + "@setDataFrame", + "onMetaData", + amf0.Object{ + { + Key: "videodatarate", + Value: float64(0), + }, + { + Key: "videocodecid", + Value: float64(0), + }, + { + Key: "audiodatarate", + Value: float64(0), + }, + { + Key: "audiocodecid", + Value: float64(message.CodecMPEG4Audio), + }, + }, + }, + }, + &message.Audio{ + ChunkStreamID: message.AudioChunkStreamID, + MessageStreamID: 0x1000000, + Codec: message.CodecMPEG4Audio, + Rate: message.Rate44100, + Depth: message.Depth16, + IsStereo: true, + AACType: message.AudioAACTypeConfig, + Payload: nil, + }, + &message.Audio{ + ChunkStreamID: message.AudioChunkStreamID, + MessageStreamID: 0x1000000, + Codec: message.CodecMPEG4Audio, + Rate: message.Rate44100, + Depth: message.Depth16, + IsStereo: true, + AACType: message.AudioAACTypeConfig, + Payload: func() []byte { + enc, err2 := mpeg4audio.Config{ + Type: 2, + SampleRate: 44100, + ChannelCount: 2, + }.Marshal() + require.NoError(t, err2) + return enc + }(), + }, + }, + }, { "h265 + aac, obs studio pre 29.1 h265", &format.H265{ From 3eabe6ac547d157fd53fa329fe829066c66c5605 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Tue, 11 Jun 2024 18:30:40 +0200 Subject: [PATCH 29/52] expose MTX_SEGMENT_DURATION to runOnRecordSegmentComplete (#3440) (#2983) (#3456) * improve tests * add duration to OnSegmentComplete * expose MTX_SEGMENT_DURATION to runOnRecordSegmentComplete * add tests --- README.md | 1 + internal/core/path.go | 3 +- internal/core/path_test.go | 120 ++++++++++++---- internal/record/agent.go | 12 +- internal/record/agent_instance.go | 3 - internal/record/agent_test.go | 166 ++++++++++++++--------- internal/record/format_fmp4.go | 32 +++-- internal/record/format_fmp4_part.go | 2 +- internal/record/format_fmp4_segment.go | 11 +- internal/record/format_fmp4_track.go | 5 +- internal/record/format_mpegts.go | 161 +++++++++++++--------- internal/record/format_mpegts_segment.go | 7 +- mediamtx.yml | 1 + 13 files changed, 334 insertions(+), 190 deletions(-) diff --git a/README.md b/README.md index f6fb91f9..72f67964 100644 --- a/README.md +++ b/README.md @@ -1669,6 +1669,7 @@ pathDefaults: # * G1, G2, ...: regular expression groups, if path name is # a regular expression. # * MTX_SEGMENT_PATH: segment file path + # * MTX_SEGMENT_DURATION: segment duration runOnRecordSegmentComplete: curl http://my-custom-server/webhook?path=$MTX_PATH&segment_path=$MTX_SEGMENT_PATH ``` diff --git a/internal/core/path.go b/internal/core/path.go index 226b7c3d..70ab2d54 100644 --- a/internal/core/path.go +++ b/internal/core/path.go @@ -806,10 +806,11 @@ func (pa *path) startRecording() { nil) } }, - OnSegmentComplete: func(segmentPath string) { + OnSegmentComplete: func(segmentPath string, segmentDuration time.Duration) { if pa.conf.RunOnRecordSegmentComplete != "" { env := pa.ExternalCmdEnv() env["MTX_SEGMENT_PATH"] = segmentPath + env["MTX_SEGMENT_DURATION"] = strconv.FormatFloat(segmentDuration.Seconds(), 'f', -1, 64) pa.Log(logger.Info, "runOnRecordSegmentComplete command launched") externalcmd.NewCmd( diff --git a/internal/core/path_test.go b/internal/core/path_test.go index 90e31900..145c00c7 100644 --- a/internal/core/path_test.go +++ b/internal/core/path_test.go @@ -105,12 +105,12 @@ func (sh *testServer) OnPlay(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Respo var _ defs.Path = &path{} func TestPathRunOnDemand(t *testing.T) { - onDemandFile := filepath.Join(os.TempDir(), "ondemand") - onUnDemandFile := filepath.Join(os.TempDir(), "onundemand") + onDemand := filepath.Join(os.TempDir(), "on_demand") + onUnDemand := filepath.Join(os.TempDir(), "on_undemand") srcFile := filepath.Join(os.TempDir(), "ondemand.go") err := os.WriteFile(srcFile, - []byte(strings.ReplaceAll(runOnDemandSampleScript, "ON_DEMAND_FILE", onDemandFile)), 0o644) + []byte(strings.ReplaceAll(runOnDemandSampleScript, "ON_DEMAND_FILE", onDemand)), 0o644) require.NoError(t, err) execFile := filepath.Join(os.TempDir(), "ondemand_cmd") @@ -125,8 +125,8 @@ func TestPathRunOnDemand(t *testing.T) { for _, ca := range []string{"describe", "setup", "describe and setup"} { t.Run(ca, func(t *testing.T) { - defer os.Remove(onDemandFile) - defer os.Remove(onUnDemandFile) + defer os.Remove(onDemand) + defer os.Remove(onUnDemand) p1, ok := newInstance(fmt.Sprintf("rtmp: no\n"+ "hls: no\n"+ @@ -135,7 +135,7 @@ func TestPathRunOnDemand(t *testing.T) { " '~^(on)demand$':\n"+ " runOnDemand: %s\n"+ " runOnDemandCloseAfter: 1s\n"+ - " runOnUnDemand: touch %s\n", execFile, onUnDemandFile)) + " runOnUnDemand: touch %s\n", execFile, onUnDemand)) require.Equal(t, true, ok) defer p1.Close() @@ -204,14 +204,14 @@ func TestPathRunOnDemand(t *testing.T) { }() for { - _, err := os.Stat(onUnDemandFile) + _, err := os.Stat(onUnDemand) if err == nil { break } time.Sleep(100 * time.Millisecond) } - _, err := os.Stat(onDemandFile) + _, err := os.Stat(onDemand) require.NoError(t, err) }) } @@ -220,11 +220,11 @@ func TestPathRunOnDemand(t *testing.T) { func TestPathRunOnConnect(t *testing.T) { for _, ca := range []string{"rtsp", "rtmp", "srt"} { t.Run(ca, func(t *testing.T) { - onConnectFile := filepath.Join(os.TempDir(), "onconnect") - defer os.Remove(onConnectFile) + onConnect := filepath.Join(os.TempDir(), "on_connect") + defer os.Remove(onConnect) - onDisconnectFile := filepath.Join(os.TempDir(), "ondisconnect") - defer os.Remove(onDisconnectFile) + onDisconnect := filepath.Join(os.TempDir(), "on_disconnect") + defer os.Remove(onDisconnect) func() { p, ok := newInstance(fmt.Sprintf( @@ -232,7 +232,7 @@ func TestPathRunOnConnect(t *testing.T) { " test:\n"+ "runOnConnect: touch %s\n"+ "runOnDisconnect: touch %s\n", - onConnectFile, onDisconnectFile)) + onConnect, onDisconnect)) require.Equal(t, true, ok) defer p.Close() @@ -273,21 +273,21 @@ func TestPathRunOnConnect(t *testing.T) { time.Sleep(500 * time.Millisecond) }() - _, err := os.Stat(onConnectFile) + _, err := os.Stat(onConnect) require.NoError(t, err) - _, err = os.Stat(onDisconnectFile) + _, err = os.Stat(onDisconnect) require.NoError(t, err) }) } } func TestPathRunOnReady(t *testing.T) { - onReadyFile := filepath.Join(os.TempDir(), "onready") - defer os.Remove(onReadyFile) + onReady := filepath.Join(os.TempDir(), "on_ready") + defer os.Remove(onReady) - onNotReadyFile := filepath.Join(os.TempDir(), "onunready") - defer os.Remove(onNotReadyFile) + onNotReady := filepath.Join(os.TempDir(), "on_unready") + defer os.Remove(onNotReady) func() { p, ok := newInstance(fmt.Sprintf("rtmp: no\n"+ @@ -297,7 +297,7 @@ func TestPathRunOnReady(t *testing.T) { " test:\n"+ " runOnReady: sh -c 'echo \"$MTX_PATH $MTX_QUERY\" > %s'\n"+ " runOnNotReady: sh -c 'echo \"$MTX_PATH $MTX_QUERY\" > %s'\n", - onReadyFile, onNotReadyFile)) + onReady, onNotReady)) require.Equal(t, true, ok) defer p.Close() @@ -312,11 +312,11 @@ func TestPathRunOnReady(t *testing.T) { time.Sleep(500 * time.Millisecond) }() - byts, err := os.ReadFile(onReadyFile) + byts, err := os.ReadFile(onReady) require.NoError(t, err) require.Equal(t, "test query=value\n", string(byts)) - byts, err = os.ReadFile(onNotReadyFile) + byts, err = os.ReadFile(onNotReady) require.NoError(t, err) require.Equal(t, "test query=value\n", string(byts)) } @@ -324,11 +324,11 @@ func TestPathRunOnReady(t *testing.T) { func TestPathRunOnRead(t *testing.T) { for _, ca := range []string{"rtsp", "rtmp", "srt", "webrtc"} { t.Run(ca, func(t *testing.T) { - onReadFile := filepath.Join(os.TempDir(), "onread") - defer os.Remove(onReadFile) + onRead := filepath.Join(os.TempDir(), "on_read") + defer os.Remove(onRead) - onUnreadFile := filepath.Join(os.TempDir(), "onunread") - defer os.Remove(onUnreadFile) + onUnread := filepath.Join(os.TempDir(), "on_unread") + defer os.Remove(onUnread) func() { p, ok := newInstance(fmt.Sprintf( @@ -336,7 +336,7 @@ func TestPathRunOnRead(t *testing.T) { " test:\n"+ " runOnRead: sh -c 'echo \"$MTX_PATH $MTX_QUERY\" > %s'\n"+ " runOnUnread: sh -c 'echo \"$MTX_PATH $MTX_QUERY\" > %s'\n", - onReadFile, onUnreadFile)) + onRead, onUnread)) require.Equal(t, true, ok) defer p.Close() @@ -449,17 +449,79 @@ func TestPathRunOnRead(t *testing.T) { time.Sleep(500 * time.Millisecond) }() - byts, err := os.ReadFile(onReadFile) + byts, err := os.ReadFile(onRead) require.NoError(t, err) require.Equal(t, "test query=value\n", string(byts)) - byts, err = os.ReadFile(onUnreadFile) + byts, err = os.ReadFile(onUnread) require.NoError(t, err) require.Equal(t, "test query=value\n", string(byts)) }) } } +func TestPathRunOnRecordSegment(t *testing.T) { + onRecordSegmentCreate := filepath.Join(os.TempDir(), "on_record_segment_create") + defer os.Remove(onRecordSegmentCreate) + + onRecordSegmentComplete := filepath.Join(os.TempDir(), "on_record_segment_complete") + defer os.Remove(onRecordSegmentComplete) + + recordDir, err := os.MkdirTemp("", "rtsp-path-record") + require.NoError(t, err) + defer os.RemoveAll(recordDir) + + func() { + p, ok := newInstance("record: yes\n" + + "recordPath: " + filepath.Join(recordDir, "%path/%Y-%m-%d_%H-%M-%S-%f") + "\n" + + "paths:\n" + + " test:\n" + + " runOnRecordSegmentCreate: " + + "sh -c 'echo \"$MTX_SEGMENT_PATH\" > " + onRecordSegmentCreate + "'\n" + + " runOnRecordSegmentComplete: " + + "sh -c 'echo \"$MTX_SEGMENT_PATH $MTX_SEGMENT_DURATION\" > " + onRecordSegmentComplete + "'\n") + require.Equal(t, true, ok) + defer p.Close() + + media0 := test.UniqueMediaH264() + + source := gortsplib.Client{} + + err = source.StartRecording( + "rtsp://localhost:8554/test", + &description.Session{Medias: []*description.Media{media0}}) + require.NoError(t, err) + defer source.Close() + + for i := 0; i < 4; i++ { + err = source.WritePacketRTP(media0, &rtp.Packet{ + Header: rtp.Header{ + Version: 2, + Marker: true, + PayloadType: 96, + SequenceNumber: 1123 + uint16(i), + Timestamp: 45343 + 90000*uint32(i), + SSRC: 563423, + }, + Payload: []byte{5}, + }) + require.NoError(t, err) + } + + time.Sleep(500 * time.Millisecond) + }() + + byts, err := os.ReadFile(onRecordSegmentCreate) + require.NoError(t, err) + require.Equal(t, true, strings.HasPrefix(string(byts), recordDir)) + + byts, err = os.ReadFile(onRecordSegmentComplete) + require.NoError(t, err) + parts := strings.Split(string(byts[:len(byts)-1]), " ") + require.Equal(t, true, strings.HasPrefix(parts[0], recordDir)) + require.Equal(t, "3", parts[1]) +} + func TestPathMaxReaders(t *testing.T) { p, ok := newInstance("paths:\n" + " all_others:\n" + diff --git a/internal/record/agent.go b/internal/record/agent.go index 7ecde7db..f7189f67 100644 --- a/internal/record/agent.go +++ b/internal/record/agent.go @@ -8,6 +8,12 @@ import ( "github.com/bluenviron/mediamtx/internal/stream" ) +// OnSegmentCreateFunc is the prototype of the function passed as OnSegmentCreate +type OnSegmentCreateFunc = func(path string) + +// OnSegmentCompleteFunc is the prototype of the function passed as OnSegmentComplete +type OnSegmentCompleteFunc = func(path string, duration time.Duration) + // Agent writes recordings to disk. type Agent struct { WriteQueueSize int @@ -17,8 +23,8 @@ type Agent struct { SegmentDuration time.Duration PathName string Stream *stream.Stream - OnSegmentCreate OnSegmentFunc - OnSegmentComplete OnSegmentFunc + OnSegmentCreate OnSegmentCreateFunc + OnSegmentComplete OnSegmentCompleteFunc Parent logger.Writer restartPause time.Duration @@ -36,7 +42,7 @@ func (w *Agent) Initialize() { } } if w.OnSegmentComplete == nil { - w.OnSegmentComplete = func(string) { + w.OnSegmentComplete = func(string, time.Duration) { } } if w.restartPause == 0 { diff --git a/internal/record/agent_instance.go b/internal/record/agent_instance.go index 8e722a8f..bce6e3d1 100644 --- a/internal/record/agent_instance.go +++ b/internal/record/agent_instance.go @@ -11,9 +11,6 @@ import ( "github.com/bluenviron/mediamtx/internal/logger" ) -// OnSegmentFunc is the prototype of the function passed as runOnSegmentStart / runOnSegmentComplete -type OnSegmentFunc = func(string) - type sample struct { *fmp4.PartSample dts time.Duration diff --git a/internal/record/agent_test.go b/internal/record/agent_test.go index 0f2a18c6..fb342e9e 100644 --- a/internal/record/agent_test.go +++ b/internal/record/agent_test.go @@ -68,12 +68,15 @@ func TestAgent(t *testing.T) { }, }} - writeToStream := func(stream *stream.Stream, ntp time.Time) { - for i := 0; i < 3; i++ { + writeToStream := func(stream *stream.Stream, startDTS time.Duration, startNTP time.Time) { + for i := 0; i < 2; i++ { + pts := startDTS + time.Duration(i)*100*time.Millisecond + ntp := startNTP.Add(time.Duration(i*60) * time.Second) + stream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.H264{ Base: unit.Base{ - PTS: (50 + time.Duration(i)) * time.Second, - NTP: ntp.Add(time.Duration(i) * 60 * time.Second), + PTS: pts, + NTP: ntp, }, AU: [][]byte{ test.FormatH264.SPS, @@ -84,7 +87,7 @@ func TestAgent(t *testing.T) { stream.WriteUnit(desc.Medias[1], desc.Medias[1].Formats[0], &unit.H265{ Base: unit.Base{ - PTS: (50 + time.Duration(i)) * time.Second, + PTS: pts, }, AU: [][]byte{ test.FormatH265.VPS, @@ -96,21 +99,21 @@ func TestAgent(t *testing.T) { stream.WriteUnit(desc.Medias[2], desc.Medias[2].Formats[0], &unit.MPEG4Audio{ Base: unit.Base{ - PTS: (50 + time.Duration(i)) * time.Second, + PTS: pts, }, AUs: [][]byte{{1, 2, 3, 4}}, }) stream.WriteUnit(desc.Medias[3], desc.Medias[3].Formats[0], &unit.G711{ Base: unit.Base{ - PTS: (50 + time.Duration(i)) * time.Second, + PTS: pts, }, Samples: []byte{1, 2, 3, 4}, }) stream.WriteUnit(desc.Medias[4], desc.Medias[4].Formats[0], &unit.LPCM{ Base: unit.Base{ - PTS: (50 + time.Duration(i)) * time.Second, + PTS: pts, }, Samples: []byte{1, 2, 3, 4}, }) @@ -144,6 +147,15 @@ func TestAgent(t *testing.T) { f = conf.RecordFormatMPEGTS } + var ext string + if ca == "fmp4" { + ext = "mp4" + } else { + ext = "ts" + } + + n := 0 + w := &Agent{ WriteQueueSize: 1024, PathFormat: recordPath, @@ -152,10 +164,30 @@ func TestAgent(t *testing.T) { SegmentDuration: 1 * time.Second, PathName: "mypath", Stream: stream, - OnSegmentCreate: func(_ string) { + OnSegmentCreate: func(segPath string) { + switch n { + case 0: + require.Equal(t, filepath.Join(dir, "mypath", "2008-05-20_22-15-25-000000."+ext), segPath) + case 1: + require.Equal(t, filepath.Join(dir, "mypath", "2008-05-20_22-16-25-000000."+ext), segPath) + default: + require.Equal(t, filepath.Join(dir, "mypath", "2010-05-20_22-15-25-000000."+ext), segPath) + } segCreated <- struct{}{} }, - OnSegmentComplete: func(_ string) { + OnSegmentComplete: func(segPath string, du time.Duration) { + switch n { + case 0: + require.Equal(t, filepath.Join(dir, "mypath", "2008-05-20_22-15-25-000000."+ext), segPath) + require.Equal(t, 2*time.Second, du) + case 1: + require.Equal(t, filepath.Join(dir, "mypath", "2008-05-20_22-16-25-000000."+ext), segPath) + require.Equal(t, 100*time.Millisecond, du) + default: + require.Equal(t, filepath.Join(dir, "mypath", "2010-05-20_22-15-25-000000."+ext), segPath) + require.Equal(t, 100*time.Millisecond, du) + } + n++ segDone <- struct{}{} }, Parent: test.NilLogger, @@ -163,7 +195,13 @@ func TestAgent(t *testing.T) { } w.Initialize() - writeToStream(stream, time.Date(2008, 0o5, 20, 22, 15, 25, 0, time.UTC)) + writeToStream(stream, + 50*time.Second, + time.Date(2008, 0o5, 20, 22, 15, 25, 0, time.UTC)) + + writeToStream(stream, + 52*time.Second, + time.Date(2008, 0o5, 20, 22, 16, 25, 0, time.UTC)) // simulate a write error stream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.H264{ @@ -180,74 +218,68 @@ func TestAgent(t *testing.T) { <-segDone } - var ext string if ca == "fmp4" { - ext = "mp4" - } else { - ext = "ts" - } + var init fmp4.Init - if ca == "fmp4" { func() { f, err2 := os.Open(filepath.Join(dir, "mypath", "2008-05-20_22-15-25-000000."+ext)) require.NoError(t, err2) defer f.Close() - var init fmp4.Init err2 = init.Unmarshal(f) require.NoError(t, err2) + }() - require.Equal(t, fmp4.Init{ - Tracks: []*fmp4.InitTrack{ - { - ID: 1, - TimeScale: 90000, - Codec: &fmp4.CodecH264{ - SPS: test.FormatH264.SPS, - PPS: test.FormatH264.PPS, - }, + require.Equal(t, fmp4.Init{ + Tracks: []*fmp4.InitTrack{ + { + ID: 1, + TimeScale: 90000, + Codec: &fmp4.CodecH264{ + SPS: test.FormatH264.SPS, + PPS: test.FormatH264.PPS, }, - { - ID: 2, - TimeScale: 90000, - Codec: &fmp4.CodecH265{ - VPS: test.FormatH265.VPS, - SPS: test.FormatH265.SPS, - PPS: test.FormatH265.PPS, - }, + }, + { + ID: 2, + TimeScale: 90000, + Codec: &fmp4.CodecH265{ + VPS: test.FormatH265.VPS, + SPS: test.FormatH265.SPS, + PPS: test.FormatH265.PPS, }, - { - ID: 3, - TimeScale: 44100, - Codec: &fmp4.CodecMPEG4Audio{ - Config: mpeg4audio.Config{ - Type: 2, - SampleRate: 44100, - ChannelCount: 2, - }, - }, - }, - { - ID: 4, - TimeScale: 8000, - Codec: &fmp4.CodecLPCM{ - BitDepth: 16, - SampleRate: 8000, - ChannelCount: 1, - }, - }, - { - ID: 5, - TimeScale: 44100, - Codec: &fmp4.CodecLPCM{ - BitDepth: 16, + }, + { + ID: 3, + TimeScale: 44100, + Codec: &fmp4.CodecMPEG4Audio{ + Config: mpeg4audio.Config{ + Type: 2, SampleRate: 44100, ChannelCount: 2, }, }, }, - }, init) - }() + { + ID: 4, + TimeScale: 8000, + Codec: &fmp4.CodecLPCM{ + BitDepth: 16, + SampleRate: 8000, + ChannelCount: 1, + }, + }, + { + ID: 5, + TimeScale: 44100, + Codec: &fmp4.CodecLPCM{ + BitDepth: 16, + SampleRate: 44100, + ChannelCount: 2, + }, + }, + }, + }, init) _, err = os.Stat(filepath.Join(dir, "mypath", "2008-05-20_22-16-25-000000."+ext)) require.NoError(t, err) @@ -261,16 +293,18 @@ func TestAgent(t *testing.T) { time.Sleep(50 * time.Millisecond) - writeToStream(stream, time.Date(2010, 0o5, 20, 22, 15, 25, 0, time.UTC)) + writeToStream(stream, + 300*time.Second, + time.Date(2010, 0o5, 20, 22, 15, 25, 0, time.UTC)) time.Sleep(50 * time.Millisecond) w.Close() - _, err = os.Stat(filepath.Join(dir, "mypath", "2010-05-20_22-15-25-000000."+ext)) - require.NoError(t, err) + <-segCreated + <-segDone - _, err = os.Stat(filepath.Join(dir, "mypath", "2010-05-20_22-16-25-000000."+ext)) + _, err = os.Stat(filepath.Join(dir, "mypath", "2010-05-20_22-15-25-000000."+ext)) require.NoError(t, err) }) } diff --git a/internal/record/format_fmp4.go b/internal/record/format_fmp4.go index a8a0f8cb..5d67b065 100644 --- a/internal/record/format_fmp4.go +++ b/internal/record/format_fmp4.go @@ -191,7 +191,7 @@ func (f *formatFMP4) initialize() { return err } - return track.record(&sample{ + return track.write(&sample{ PartSample: sampl, dts: tunit.PTS, ntp: tunit.NTP, @@ -261,7 +261,7 @@ func (f *formatFMP4) initialize() { firstReceived = true } - return track.record(&sample{ + return track.write(&sample{ PartSample: &fmp4.PartSample{ IsNonSyncSample: !randomAccess, Payload: tunit.Frame, @@ -364,7 +364,7 @@ func (f *formatFMP4) initialize() { return err } - return track.record(&sample{ + return track.write(&sample{ PartSample: sampl, dts: dts, ntp: tunit.NTP, @@ -435,7 +435,7 @@ func (f *formatFMP4) initialize() { return err } - return track.record(&sample{ + return track.write(&sample{ PartSample: sampl, dts: dts, ntp: tunit.NTP, @@ -494,7 +494,7 @@ func (f *formatFMP4) initialize() { } lastPTS = tunit.PTS - return track.record(&sample{ + return track.write(&sample{ PartSample: &fmp4.PartSample{ Payload: tunit.Frame, IsNonSyncSample: !randomAccess, @@ -547,7 +547,7 @@ func (f *formatFMP4) initialize() { } lastPTS = tunit.PTS - return track.record(&sample{ + return track.write(&sample{ PartSample: &fmp4.PartSample{ Payload: tunit.Frame, IsNonSyncSample: !randomAccess, @@ -583,7 +583,7 @@ func (f *formatFMP4) initialize() { updateCodecs() } - return track.record(&sample{ + return track.write(&sample{ PartSample: &fmp4.PartSample{ Payload: tunit.Frame, }, @@ -607,7 +607,7 @@ func (f *formatFMP4) initialize() { var dt time.Duration for _, packet := range tunit.Packets { - err := track.record(&sample{ + err := track.write(&sample{ PartSample: &fmp4.PartSample{ Payload: packet, }, @@ -642,7 +642,7 @@ func (f *formatFMP4) initialize() { dt := time.Duration(i) * mpeg4audio.SamplesPerAccessUnit * time.Second / sampleRate - err := track.record(&sample{ + err := track.write(&sample{ PartSample: &fmp4.PartSample{ Payload: au, }, @@ -688,7 +688,7 @@ func (f *formatFMP4) initialize() { updateCodecs() } - err = track.record(&sample{ + err = track.write(&sample{ PartSample: &fmp4.PartSample{ Payload: frame, }, @@ -756,7 +756,7 @@ func (f *formatFMP4) initialize() { dt := time.Duration(i) * time.Duration(ac3.SamplesPerFrame) * time.Second / time.Duration(codec.SampleRate) - err = track.record(&sample{ + err = track.write(&sample{ PartSample: &fmp4.PartSample{ Payload: frame, }, @@ -796,7 +796,7 @@ func (f *formatFMP4) initialize() { out = g711.DecodeAlaw(tunit.Samples) } - return track.record(&sample{ + return track.write(&sample{ PartSample: &fmp4.PartSample{ Payload: out, }, @@ -820,7 +820,7 @@ func (f *formatFMP4) initialize() { return nil } - return track.record(&sample{ + return track.write(&sample{ PartSample: &fmp4.PartSample{ Payload: tunit.Samples, }, @@ -838,6 +838,12 @@ func (f *formatFMP4) initialize() { func (f *formatFMP4) close() { if f.currentSegment != nil { + for _, track := range f.tracks { + if track.nextSample != nil && track.nextSample.dts > f.currentSegment.lastDTS { + f.currentSegment.lastDTS = track.nextSample.dts + } + } + f.currentSegment.close() //nolint:errcheck } } diff --git a/internal/record/format_fmp4_part.go b/internal/record/format_fmp4_part.go index 1f1968fe..a960c9a7 100644 --- a/internal/record/format_fmp4_part.go +++ b/internal/record/format_fmp4_part.go @@ -81,7 +81,7 @@ func (p *formatFMP4Part) close() error { return writePart(p.s.fi, p.sequenceNumber, p.partTracks) } -func (p *formatFMP4Part) record(track *formatFMP4Track, sample *sample) error { +func (p *formatFMP4Part) write(track *formatFMP4Track, sample *sample) error { partTrack, ok := p.partTracks[track] if !ok { partTrack = &fmp4.PartTrack{ diff --git a/internal/record/format_fmp4_segment.go b/internal/record/format_fmp4_segment.go index f0695c00..7920fcb3 100644 --- a/internal/record/format_fmp4_segment.go +++ b/internal/record/format_fmp4_segment.go @@ -39,9 +39,11 @@ type formatFMP4Segment struct { path string fi *os.File curPart *formatFMP4Part + lastDTS time.Duration } func (s *formatFMP4Segment) initialize() { + s.lastDTS = s.startDTS } func (s *formatFMP4Segment) close() error { @@ -59,14 +61,17 @@ func (s *formatFMP4Segment) close() error { } if err2 == nil { - s.f.a.agent.OnSegmentComplete(s.path) + duration := s.lastDTS - s.startDTS + s.f.a.agent.OnSegmentComplete(s.path, duration) } } return err } -func (s *formatFMP4Segment) record(track *formatFMP4Track, sample *sample) error { +func (s *formatFMP4Segment) write(track *formatFMP4Track, sample *sample) error { + s.lastDTS = sample.dts + if s.curPart == nil { s.curPart = &formatFMP4Part{ s: s, @@ -92,5 +97,5 @@ func (s *formatFMP4Segment) record(track *formatFMP4Track, sample *sample) error s.f.nextSequenceNumber++ } - return s.curPart.record(track, sample) + return s.curPart.write(track, sample) } diff --git a/internal/record/format_fmp4_track.go b/internal/record/format_fmp4_track.go index 1ff214f2..fd7a075e 100644 --- a/internal/record/format_fmp4_track.go +++ b/internal/record/format_fmp4_track.go @@ -11,7 +11,7 @@ type formatFMP4Track struct { nextSample *sample } -func (t *formatFMP4Track) record(sample *sample) error { +func (t *formatFMP4Track) write(sample *sample) error { // wait the first video sample before setting hasVideo if t.initTrack.Codec.IsVideo() { t.f.hasVideo = true @@ -35,7 +35,7 @@ func (t *formatFMP4Track) record(sample *sample) error { return nil } - err := t.f.currentSegment.record(t, sample) + err := t.f.currentSegment.write(t, sample) if err != nil { return err } @@ -43,6 +43,7 @@ func (t *formatFMP4Track) record(sample *sample) error { if (!t.f.hasVideo || t.initTrack.Codec.IsVideo()) && !t.nextSample.IsNonSyncSample && (t.nextSample.dts-t.f.currentSegment.startDTS) >= t.f.a.agent.SegmentDuration { + t.f.currentSegment.lastDTS = t.nextSample.dts err := t.f.currentSegment.close() if err != nil { return err diff --git a/internal/record/format_mpegts.go b/internal/record/format_mpegts.go index 60f778cf..d7d4e652 100644 --- a/internal/record/format_mpegts.go +++ b/internal/record/format_mpegts.go @@ -66,7 +66,7 @@ func (f *formatMPEGTS) initialize() { for _, media := range f.a.agent.Stream.Desc().Medias { for _, forma := range media.Formats { switch forma := forma.(type) { - case *rtspformat.H265: + case *rtspformat.H265: //nolint:dupl track := addTrack(forma, &mpegts.CodecH265{}) var dtsExtractor *h265.DTSExtractor @@ -91,10 +91,18 @@ func (f *formatMPEGTS) initialize() { return err } - return f.recordH26x(track, tunit.PTS, dts, tunit.NTP, randomAccess, tunit.AU) + return f.write( + dts, + tunit.NTP, + true, + randomAccess, + func() error { + return f.mw.WriteH26x(track, durationGoToMPEGTS(tunit.PTS), durationGoToMPEGTS(dts), randomAccess, tunit.AU) + }, + ) }) - case *rtspformat.H264: + case *rtspformat.H264: //nolint:dupl track := addTrack(forma, &mpegts.CodecH264{}) var dtsExtractor *h264.DTSExtractor @@ -105,10 +113,10 @@ func (f *formatMPEGTS) initialize() { return nil } - idrPresent := h264.IDRPresent(tunit.AU) + randomAccess := h264.IDRPresent(tunit.AU) if dtsExtractor == nil { - if !idrPresent { + if !randomAccess { return nil } dtsExtractor = h264.NewDTSExtractor() @@ -119,7 +127,15 @@ func (f *formatMPEGTS) initialize() { return err } - return f.recordH26x(track, tunit.PTS, dts, tunit.NTP, idrPresent, tunit.AU) + return f.write( + dts, + tunit.NTP, + true, + randomAccess, + func() error { + return f.mw.WriteH26x(track, durationGoToMPEGTS(tunit.PTS), durationGoToMPEGTS(dts), randomAccess, tunit.AU) + }, + ) }) case *rtspformat.MPEG4Video: @@ -141,15 +157,17 @@ func (f *formatMPEGTS) initialize() { } lastPTS = tunit.PTS - f.hasVideo = true randomAccess := bytes.Contains(tunit.Frame, []byte{0, 0, 1, byte(mpeg4video.GroupOfVOPStartCode)}) - err := f.setupSegment(tunit.PTS, tunit.NTP, true, randomAccess) - if err != nil { - return err - } - - return f.mw.WriteMPEG4Video(track, durationGoToMPEGTS(tunit.PTS), tunit.Frame) + return f.write( + tunit.PTS, + tunit.NTP, + true, + randomAccess, + func() error { + return f.mw.WriteMPEG4Video(track, durationGoToMPEGTS(tunit.PTS), tunit.Frame) + }, + ) }) case *rtspformat.MPEG1Video: @@ -171,15 +189,17 @@ func (f *formatMPEGTS) initialize() { } lastPTS = tunit.PTS - f.hasVideo = true randomAccess := bytes.Contains(tunit.Frame, []byte{0, 0, 1, 0xB8}) - err := f.setupSegment(tunit.PTS, tunit.NTP, true, randomAccess) - if err != nil { - return err - } - - return f.mw.WriteMPEG1Video(track, durationGoToMPEGTS(tunit.PTS), tunit.Frame) + return f.write( + tunit.PTS, + tunit.NTP, + true, + randomAccess, + func() error { + return f.mw.WriteMPEG1Video(track, durationGoToMPEGTS(tunit.PTS), tunit.Frame) + }, + ) }) case *rtspformat.Opus: @@ -193,12 +213,15 @@ func (f *formatMPEGTS) initialize() { return nil } - err := f.setupSegment(tunit.PTS, tunit.NTP, false, true) - if err != nil { - return err - } - - return f.mw.WriteOpus(track, durationGoToMPEGTS(tunit.PTS), tunit.Packets) + return f.write( + tunit.PTS, + tunit.NTP, + false, + true, + func() error { + return f.mw.WriteOpus(track, durationGoToMPEGTS(tunit.PTS), tunit.Packets) + }, + ) }) case *rtspformat.MPEG4Audio: @@ -212,12 +235,15 @@ func (f *formatMPEGTS) initialize() { return nil } - err := f.setupSegment(tunit.PTS, tunit.NTP, false, true) - if err != nil { - return err - } - - return f.mw.WriteMPEG4Audio(track, durationGoToMPEGTS(tunit.PTS), tunit.AUs) + return f.write( + tunit.PTS, + tunit.NTP, + false, + true, + func() error { + return f.mw.WriteMPEG4Audio(track, durationGoToMPEGTS(tunit.PTS), tunit.AUs) + }, + ) }) case *rtspformat.MPEG1Audio: @@ -229,12 +255,15 @@ func (f *formatMPEGTS) initialize() { return nil } - err := f.setupSegment(tunit.PTS, tunit.NTP, false, true) - if err != nil { - return err - } - - return f.mw.WriteMPEG1Audio(track, durationGoToMPEGTS(tunit.PTS), tunit.Frames) + return f.write( + tunit.PTS, + tunit.NTP, + false, + true, + func() error { + return f.mw.WriteMPEG1Audio(track, durationGoToMPEGTS(tunit.PTS), tunit.Frames) + }, + ) }) case *rtspformat.AC3: @@ -248,17 +277,25 @@ func (f *formatMPEGTS) initialize() { return nil } - for i, frame := range tunit.Frames { - framePTS := tunit.PTS + time.Duration(i)*ac3.SamplesPerFrame* - time.Second/sampleRate + return f.write( + tunit.PTS, + tunit.NTP, + false, + true, + func() error { + for i, frame := range tunit.Frames { + framePTS := tunit.PTS + time.Duration(i)*ac3.SamplesPerFrame* + time.Second/sampleRate - err := f.mw.WriteAC3(track, durationGoToMPEGTS(framePTS), frame) - if err != nil { - return err - } - } + err := f.mw.WriteAC3(track, durationGoToMPEGTS(framePTS), frame) + if err != nil { + return err + } + } - return nil + return nil + }, + ) }) } } @@ -278,12 +315,17 @@ func (f *formatMPEGTS) close() { } } -func (f *formatMPEGTS) setupSegment( +func (f *formatMPEGTS) write( dts time.Duration, ntp time.Time, isVideo bool, randomAccess bool, + writeCB func() error, ) error { + if isVideo { + f.hasVideo = true + } + switch { case f.currentSegment == nil: f.currentSegment = &formatMPEGTSSegment{ @@ -295,6 +337,7 @@ func (f *formatMPEGTS) setupSegment( case (!f.hasVideo || isVideo) && randomAccess && (dts-f.currentSegment.startDTS) >= f.a.agent.SegmentDuration: + f.currentSegment.lastDTS = dts err := f.currentSegment.close() if err != nil { return err @@ -316,23 +359,7 @@ func (f *formatMPEGTS) setupSegment( f.currentSegment.lastFlush = dts } - return nil -} - -func (f *formatMPEGTS) recordH26x( - track *mpegts.Track, - pts time.Duration, - dts time.Duration, - ntp time.Time, - randomAccess bool, - au [][]byte, -) error { - f.hasVideo = true - - err := f.setupSegment(dts, ntp, true, randomAccess) - if err != nil { - return err - } - - return f.mw.WriteH26x(track, durationGoToMPEGTS(pts), durationGoToMPEGTS(dts), randomAccess, au) + f.currentSegment.lastDTS = dts + + return writeCB() } diff --git a/internal/record/format_mpegts_segment.go b/internal/record/format_mpegts_segment.go index 06e16883..754c93aa 100644 --- a/internal/record/format_mpegts_segment.go +++ b/internal/record/format_mpegts_segment.go @@ -13,13 +13,15 @@ type formatMPEGTSSegment struct { startDTS time.Duration startNTP time.Time - lastFlush time.Duration path string fi *os.File + lastFlush time.Duration + lastDTS time.Duration } func (s *formatMPEGTSSegment) initialize() { s.lastFlush = s.startDTS + s.lastDTS = s.startDTS s.f.dw.setTarget(s) } @@ -34,7 +36,8 @@ func (s *formatMPEGTSSegment) close() error { } if err2 == nil { - s.f.a.agent.OnSegmentComplete(s.path) + duration := s.lastDTS - s.startDTS + s.f.a.agent.OnSegmentComplete(s.path, duration) } } diff --git a/mediamtx.yml b/mediamtx.yml index 29468381..e3929d9e 100644 --- a/mediamtx.yml +++ b/mediamtx.yml @@ -675,6 +675,7 @@ pathDefaults: # * G1, G2, ...: regular expression groups, if path name is # a regular expression. # * MTX_SEGMENT_PATH: segment file path + # * MTX_SEGMENT_DURATION: segment duration runOnRecordSegmentComplete: ############################################### From 80a133afc9542d0408d634cfb06fcbf5196ba163 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Tue, 11 Jun 2024 22:43:27 +0200 Subject: [PATCH 30/52] bump dependencies (#3457) --- go.mod | 8 ++++---- go.sum | 16 ++++++++-------- internal/core/api_test.go | 4 ++-- internal/core/metrics_test.go | 2 +- internal/protocols/mpegts/from_stream.go | 4 ++-- internal/protocols/mpegts/to_stream.go | 4 ++-- internal/record/format_mpegts.go | 4 ++-- internal/servers/hls/muxer_instance.go | 4 ++-- internal/servers/hls/server_test.go | 3 --- internal/servers/srt/server_test.go | 6 +++--- internal/staticsources/hls/source_test.go | 2 +- internal/staticsources/srt/source_test.go | 2 +- internal/staticsources/udp/source_test.go | 4 ++-- 13 files changed, 30 insertions(+), 33 deletions(-) diff --git a/go.mod b/go.mod index f72fe77b..12075484 100644 --- a/go.mod +++ b/go.mod @@ -8,9 +8,9 @@ require ( github.com/MicahParks/keyfunc/v3 v3.3.3 github.com/abema/go-mp4 v1.2.0 github.com/alecthomas/kong v0.9.0 - github.com/bluenviron/gohlslib v1.3.3 - github.com/bluenviron/gortsplib/v4 v4.10.0 - github.com/bluenviron/mediacommon v1.11.0 + github.com/bluenviron/gohlslib v1.4.0 + github.com/bluenviron/gortsplib/v4 v4.10.1 + github.com/bluenviron/mediacommon v1.12.0 github.com/datarhei/gosrt v0.6.0 github.com/fsnotify/fsnotify v1.7.0 github.com/gin-gonic/gin v1.10.0 @@ -70,7 +70,7 @@ require ( github.com/ugorji/go/codec v1.2.12 // indirect github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect golang.org/x/arch v0.8.0 // indirect - golang.org/x/net v0.25.0 // indirect + golang.org/x/net v0.26.0 // indirect golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.5.0 // indirect google.golang.org/protobuf v1.34.1 // indirect diff --git a/go.sum b/go.sum index 3eada60c..316ed2d9 100644 --- a/go.sum +++ b/go.sum @@ -20,12 +20,12 @@ github.com/asticode/go-astits v1.13.0 h1:XOgkaadfZODnyZRR5Y0/DWkA9vrkLLPLeeOvDwf github.com/asticode/go-astits v1.13.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI= github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c h1:8XZeJrs4+ZYhJeJ2aZxADI2tGADS15AzIF8MQ8XAhT4= github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c/go.mod h1:x1vxHcL/9AVzuk5HOloOEPrtJY0MaalYr78afXZ+pWI= -github.com/bluenviron/gohlslib v1.3.3 h1:Ji4PW9QHHCbpBteZCKk+rGY6emFNSGVFMsAa/3xFChk= -github.com/bluenviron/gohlslib v1.3.3/go.mod h1:MQcRjI9fYBNb9QhZO3RydgtbfCRhjogj6YMrpCDuTvY= -github.com/bluenviron/gortsplib/v4 v4.10.0 h1:9vJsUDuBgSinm41CR6yWnSMZ7TRWeB/oiAuN4lo30bU= -github.com/bluenviron/gortsplib/v4 v4.10.0/go.mod h1:iLJ1tmwGMbaN04ZYh/KRlAHsCbz9Rycn7cPAvdR+Vkc= -github.com/bluenviron/mediacommon v1.11.0 h1:1xY4QGYz7da9tsV2Xvd+ol+Ul5qq2g7ADJtIlVkQSRI= -github.com/bluenviron/mediacommon v1.11.0/go.mod h1:HDyW2CzjvhYJXtdxstdFPio3G0qSocPhqkhUt/qffec= +github.com/bluenviron/gohlslib v1.4.0 h1:3a9W1x8eqlxJUKt1sJCunPGtti5ALIY2ik4GU0RVe7E= +github.com/bluenviron/gohlslib v1.4.0/go.mod h1:q5ZElzNw5GRbV1VEI45qkcPbKBco6BP58QEY5HyFsmo= +github.com/bluenviron/gortsplib/v4 v4.10.1 h1:v+X5HcNOEiUurK16Y30sl/UjqCDodx4aywvoSsFS49A= +github.com/bluenviron/gortsplib/v4 v4.10.1/go.mod h1:ElIedl4To6FQpxjgGnbf4NK/je57JqZMO2EAndIWX4o= +github.com/bluenviron/mediacommon v1.12.0 h1:j6L3Ikn+dyJvvG3rbm0gjbsUJ11fqh5nIlhNgYAjEx8= +github.com/bluenviron/mediacommon v1.12.0/go.mod h1:HDyW2CzjvhYJXtdxstdFPio3G0qSocPhqkhUt/qffec= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= @@ -230,8 +230,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= -golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= 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= diff --git a/internal/core/api_test.go b/internal/core/api_test.go index 33cf0a3d..3390727c 100644 --- a/internal/core/api_test.go +++ b/internal/core/api_test.go @@ -546,7 +546,7 @@ func TestAPIProtocolListGet(t *testing.T) { w := mpegts.NewWriter(bw, []*mpegts.Track{track}) require.NoError(t, err) - err = w.WriteH26x(track, 0, 0, true, [][]byte{{1}}) + err = w.WriteH264(track, 0, 0, true, [][]byte{{1}}) require.NoError(t, err) err = bw.Flush() @@ -1021,7 +1021,7 @@ func TestAPIProtocolKick(t *testing.T) { w := mpegts.NewWriter(bw, []*mpegts.Track{track}) require.NoError(t, err) - err = w.WriteH26x(track, 0, 0, true, [][]byte{{1}}) + err = w.WriteH264(track, 0, 0, true, [][]byte{{1}}) require.NoError(t, err) err = bw.Flush() diff --git a/internal/core/metrics_test.go b/internal/core/metrics_test.go index 5237e57c..e586c840 100644 --- a/internal/core/metrics_test.go +++ b/internal/core/metrics_test.go @@ -218,7 +218,7 @@ webrtc_sessions_bytes_sent 0 w := mpegts.NewWriter(bw, []*mpegts.Track{track}) require.NoError(t, err) - err = w.WriteH26x(track, 0, 0, true, [][]byte{ + err = w.WriteH264(track, 0, 0, true, [][]byte{ test.FormatH264.SPS, test.FormatH264.PPS, {0x05, 1}, // IDR diff --git a/internal/protocols/mpegts/from_stream.go b/internal/protocols/mpegts/from_stream.go index 48989aff..47abf82a 100644 --- a/internal/protocols/mpegts/from_stream.go +++ b/internal/protocols/mpegts/from_stream.go @@ -69,7 +69,7 @@ func FromStream( } sconn.SetWriteDeadline(time.Now().Add(writeTimeout)) - err = (*w).WriteH26x(track, durationGoToMPEGTS(tunit.PTS), durationGoToMPEGTS(dts), randomAccess, tunit.AU) + err = (*w).WriteH265(track, durationGoToMPEGTS(tunit.PTS), durationGoToMPEGTS(dts), randomAccess, tunit.AU) if err != nil { return err } @@ -102,7 +102,7 @@ func FromStream( } sconn.SetWriteDeadline(time.Now().Add(writeTimeout)) - err = (*w).WriteH26x(track, durationGoToMPEGTS(tunit.PTS), durationGoToMPEGTS(dts), idrPresent, tunit.AU) + err = (*w).WriteH264(track, durationGoToMPEGTS(tunit.PTS), durationGoToMPEGTS(dts), idrPresent, tunit.AU) if err != nil { return err } diff --git a/internal/protocols/mpegts/to_stream.go b/internal/protocols/mpegts/to_stream.go index d3c17228..282a78ae 100644 --- a/internal/protocols/mpegts/to_stream.go +++ b/internal/protocols/mpegts/to_stream.go @@ -41,7 +41,7 @@ func ToStream(r *mpegts.Reader, stream **stream.Stream) ([]*description.Media, e }}, } - r.OnDataH26x(track, func(pts int64, _ int64, au [][]byte) error { + r.OnDataH265(track, func(pts int64, _ int64, au [][]byte) error { (*stream).WriteUnit(medi, medi.Formats[0], &unit.H265{ Base: unit.Base{ NTP: time.Now(), @@ -61,7 +61,7 @@ func ToStream(r *mpegts.Reader, stream **stream.Stream) ([]*description.Media, e }}, } - r.OnDataH26x(track, func(pts int64, _ int64, au [][]byte) error { + r.OnDataH264(track, func(pts int64, _ int64, au [][]byte) error { (*stream).WriteUnit(medi, medi.Formats[0], &unit.H264{ Base: unit.Base{ NTP: time.Now(), diff --git a/internal/record/format_mpegts.go b/internal/record/format_mpegts.go index d7d4e652..381cb839 100644 --- a/internal/record/format_mpegts.go +++ b/internal/record/format_mpegts.go @@ -97,7 +97,7 @@ func (f *formatMPEGTS) initialize() { true, randomAccess, func() error { - return f.mw.WriteH26x(track, durationGoToMPEGTS(tunit.PTS), durationGoToMPEGTS(dts), randomAccess, tunit.AU) + return f.mw.WriteH265(track, durationGoToMPEGTS(tunit.PTS), durationGoToMPEGTS(dts), randomAccess, tunit.AU) }, ) }) @@ -133,7 +133,7 @@ func (f *formatMPEGTS) initialize() { true, randomAccess, func() error { - return f.mw.WriteH26x(track, durationGoToMPEGTS(tunit.PTS), durationGoToMPEGTS(dts), randomAccess, tunit.AU) + return f.mw.WriteH264(track, durationGoToMPEGTS(tunit.PTS), durationGoToMPEGTS(dts), randomAccess, tunit.AU) }, ) }) diff --git a/internal/servers/hls/muxer_instance.go b/internal/servers/hls/muxer_instance.go index a2290760..c1d87c7f 100644 --- a/internal/servers/hls/muxer_instance.go +++ b/internal/servers/hls/muxer_instance.go @@ -155,7 +155,7 @@ func (mi *muxerInstance) createVideoTrack() *gohlslib.Track { return nil } - err := mi.hmuxer.WriteH26x(tunit.NTP, tunit.PTS, tunit.AU) + err := mi.hmuxer.WriteH265(tunit.NTP, tunit.PTS, tunit.AU) if err != nil { return fmt.Errorf("muxer error: %w", err) } @@ -185,7 +185,7 @@ func (mi *muxerInstance) createVideoTrack() *gohlslib.Track { return nil } - err := mi.hmuxer.WriteH26x(tunit.NTP, tunit.PTS, tunit.AU) + err := mi.hmuxer.WriteH264(tunit.NTP, tunit.PTS, tunit.AU) if err != nil { return fmt.Errorf("muxer error: %w", err) } diff --git a/internal/servers/hls/server_test.go b/internal/servers/hls/server_test.go index 0db2bb65..5db01b81 100644 --- a/internal/servers/hls/server_test.go +++ b/internal/servers/hls/server_test.go @@ -11,7 +11,6 @@ import ( "github.com/bluenviron/gohlslib" "github.com/bluenviron/gohlslib/pkg/codecs" "github.com/bluenviron/gortsplib/v4/pkg/description" - "github.com/bluenviron/mediacommon/pkg/codecs/h264" "github.com/bluenviron/mediamtx/internal/auth" "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/defs" @@ -175,7 +174,6 @@ func TestServerRead(t *testing.T) { require.Equal(t, time.Duration(0), pts) require.Equal(t, time.Duration(0), dts) require.Equal(t, [][]byte{ - {byte(h264.NALUTypeAccessUnitDelimiter), 0xf0}, test.FormatH264.SPS, test.FormatH264.PPS, {5, 1}, @@ -276,7 +274,6 @@ func TestServerRead(t *testing.T) { require.Equal(t, time.Duration(0), pts) require.Equal(t, time.Duration(0), dts) require.Equal(t, [][]byte{ - {0x09, 0xf0}, test.FormatH264.SPS, test.FormatH264.PPS, {5, 1}, diff --git a/internal/servers/srt/server_test.go b/internal/servers/srt/server_test.go index 74645d64..e9fc4ce8 100644 --- a/internal/servers/srt/server_test.go +++ b/internal/servers/srt/server_test.go @@ -127,7 +127,7 @@ func TestServerPublish(t *testing.T) { w := mpegts.NewWriter(bw, []*mpegts.Track{track}) require.NoError(t, err) - err = w.WriteH26x(track, 0, 0, true, [][]byte{ + err = w.WriteH264(track, 0, 0, true, [][]byte{ test.FormatH264.SPS, test.FormatH264.PPS, {0x05, 1}, // IDR @@ -156,7 +156,7 @@ func TestServerPublish(t *testing.T) { return nil }) - err = w.WriteH26x(track, 0, 0, true, [][]byte{ + err = w.WriteH264(track, 0, 0, true, [][]byte{ {5, 2}, }) require.NoError(t, err) @@ -237,7 +237,7 @@ func TestServerRead(t *testing.T) { received := false - r.OnDataH26x(r.Tracks()[0], func(pts int64, dts int64, au [][]byte) error { + r.OnDataH264(r.Tracks()[0], func(pts int64, dts int64, au [][]byte) error { require.Equal(t, int64(0), pts) require.Equal(t, int64(0), dts) require.Equal(t, [][]byte{ diff --git a/internal/staticsources/hls/source_test.go b/internal/staticsources/hls/source_test.go index b4d9faef..bd38bede 100644 --- a/internal/staticsources/hls/source_test.go +++ b/internal/staticsources/hls/source_test.go @@ -63,7 +63,7 @@ func TestSource(t *testing.T) { err := w.WriteMPEG4Audio(track2, 1*90000, [][]byte{{1, 2, 3, 4}}) require.NoError(t, err) - err = w.WriteH26x(track1, 2*90000, 2*90000, true, [][]byte{ + err = w.WriteH264(track1, 2*90000, 2*90000, true, [][]byte{ {7, 1, 2, 3}, // SPS {8}, // PPS }) diff --git a/internal/staticsources/srt/source_test.go b/internal/staticsources/srt/source_test.go index 07d179ee..8222956f 100644 --- a/internal/staticsources/srt/source_test.go +++ b/internal/staticsources/srt/source_test.go @@ -40,7 +40,7 @@ func TestSource(t *testing.T) { w := mpegts.NewWriter(bw, []*mpegts.Track{track}) require.NoError(t, err) - err = w.WriteH26x(track, 0, 0, true, [][]byte{{ // IDR + err = w.WriteH264(track, 0, 0, true, [][]byte{{ // IDR 5, 1, }}) require.NoError(t, err) diff --git a/internal/staticsources/udp/source_test.go b/internal/staticsources/udp/source_test.go index f13bd3ad..9b40bad4 100644 --- a/internal/staticsources/udp/source_test.go +++ b/internal/staticsources/udp/source_test.go @@ -41,12 +41,12 @@ func TestSource(t *testing.T) { w := mpegts.NewWriter(bw, []*mpegts.Track{track}) require.NoError(t, err) - err = w.WriteH26x(track, 0, 0, true, [][]byte{{ // IDR + err = w.WriteH264(track, 0, 0, true, [][]byte{{ // IDR 5, 1, }}) require.NoError(t, err) - err = w.WriteH26x(track, 0, 0, true, [][]byte{{ // non-IDR + err = w.WriteH264(track, 0, 0, true, [][]byte{{ // non-IDR 5, 2, }}) require.NoError(t, err) From caa9fa6ca0d3cd09905f8e5ce6525815c450256d Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Tue, 11 Jun 2024 23:33:01 +0200 Subject: [PATCH 31/52] webrtc, hls: support passing JWT through Authorization header (#3248) (#3458) --- README.md | 10 +- internal/servers/hls/http_server.go | 19 ++- internal/servers/hls/server_test.go | 183 +++++++++++++++++++---- internal/servers/webrtc/http_server.go | 26 +++- internal/servers/webrtc/server_test.go | 191 +++++++++++++++++++++---- mediamtx.yml | 2 +- 6 files changed, 373 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 72f67964..7d64fe37 100644 --- a/README.md +++ b/README.md @@ -1180,12 +1180,20 @@ The JWT is expected to contain the `mediamtx_permissions` scope, with a list of } ``` -Clients are expected to pass the JWT in query parameters, for instance: +Clients are expected to pass the JWT in the Authorization header (in case of HLS and WebRTC) or in query parameters (in case of any other protocol), for instance (RTSP): ``` ffmpeg -re -stream_loop -1 -i file.ts -c copy -f rtsp rtsp://localhost:8554/mystream?jwt=MY_JWT ``` +For instance (HLS): + +``` +GET /mypath/index.m3u8 HTTP/1.1 +Host: example.com +Authorization: Bearer MY_JWT +``` + Here's a tutorial on how to setup the [Keycloak identity server](https://www.keycloak.org/) in order to provide such JWTs: 1. Start Keycloak: diff --git a/internal/servers/hls/http_server.go b/internal/servers/hls/http_server.go index 50d019c0..752b5c2a 100644 --- a/internal/servers/hls/http_server.go +++ b/internal/servers/hls/http_server.go @@ -5,6 +5,7 @@ import ( "errors" "net" "net/http" + "net/url" gopath "path" "strings" "time" @@ -36,6 +37,17 @@ func mergePathAndQuery(path string, rawQuery string) string { return res } +func addJWTFromAuthorization(rawQuery string, auth string) string { + jwt := strings.TrimPrefix(auth, "Bearer ") + if rawQuery != "" { + if v, err := url.ParseQuery(rawQuery); err == nil && v.Get("jwt") == "" { + v.Set("jwt", jwt) + return v.Encode() + } + } + return url.Values{"jwt": []string{jwt}}.Encode() +} + type httpServer struct { address string encryption bool @@ -145,10 +157,15 @@ func (s *httpServer) onRequest(ctx *gin.Context) { user, pass, hasCredentials := ctx.Request.BasicAuth() + q := ctx.Request.URL.RawQuery + if h := ctx.Request.Header.Get("Authorization"); strings.HasPrefix(h, "Bearer ") { + q = addJWTFromAuthorization(q, h) + } + pathConf, err := s.pathManager.FindPathConf(defs.PathFindPathConfReq{ AccessRequest: defs.PathAccessRequest{ Name: dir, - Query: ctx.Request.URL.RawQuery, + Query: q, Publish: false, IP: net.ParseIP(ctx.ClientIP()), User: user, diff --git a/internal/servers/hls/server_test.go b/internal/servers/hls/server_test.go index 5db01b81..76ef901d 100644 --- a/internal/servers/hls/server_test.go +++ b/internal/servers/hls/server_test.go @@ -11,7 +11,6 @@ import ( "github.com/bluenviron/gohlslib" "github.com/bluenviron/gohlslib/pkg/codecs" "github.com/bluenviron/gortsplib/v4/pkg/description" - "github.com/bluenviron/mediamtx/internal/auth" "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/externalcmd" @@ -49,21 +48,16 @@ func (pa *dummyPath) RemoveReader(_ defs.PathRemoveReaderReq) { } type dummyPathManager struct { - stream *stream.Stream + findPathConf func(req defs.PathFindPathConfReq) (*conf.Path, error) + addReader func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) } func (pm *dummyPathManager) FindPathConf(req defs.PathFindPathConfReq) (*conf.Path, error) { - if req.AccessRequest.User != "myuser" || req.AccessRequest.Pass != "mypass" { - return nil, auth.Error{} - } - return &conf.Path{}, nil + return pm.findPathConf(req) } func (pm *dummyPathManager) AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) { - if req.AccessRequest.Name == "nonexisting" { - return nil, nil, fmt.Errorf("not found") - } - return &dummyPath{}, pm.stream, nil + return pm.addReader(req) } func TestServerNotFound(t *testing.T) { @@ -72,6 +66,19 @@ func TestServerNotFound(t *testing.T) { "always remux on", } { t.Run(ca, func(t *testing.T) { + pm := &dummyPathManager{ + findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) { + require.Equal(t, "nonexisting", req.AccessRequest.Name) + require.Equal(t, "myuser", req.AccessRequest.User) + require.Equal(t, "mypass", req.AccessRequest.Pass) + return &conf.Path{}, nil + }, + addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) { + require.Equal(t, "nonexisting", req.AccessRequest.Name) + return nil, nil, fmt.Errorf("not found") + }, + } + s := &Server{ Address: "127.0.0.1:8888", Encryption: false, @@ -88,7 +95,7 @@ func TestServerNotFound(t *testing.T) { Directory: "", ReadTimeout: conf.StringDuration(10 * time.Second), WriteQueueSize: 512, - PathManager: &dummyPathManager{}, + PathManager: pm, Parent: test.NilLogger, } err := s.Initialize() @@ -126,7 +133,7 @@ func TestServerRead(t *testing.T) { t.Run("always remux off", func(t *testing.T) { desc := &description.Session{Medias: []*description.Media{test.MediaH264}} - stream, err := stream.New( + str, err := stream.New( 1460, desc, true, @@ -134,7 +141,18 @@ func TestServerRead(t *testing.T) { ) require.NoError(t, err) - pathManager := &dummyPathManager{stream: stream} + pm := &dummyPathManager{ + findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) { + require.Equal(t, "mystream", req.AccessRequest.Name) + require.Equal(t, "myuser", req.AccessRequest.User) + require.Equal(t, "mypass", req.AccessRequest.Pass) + return &conf.Path{}, nil + }, + addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) { + require.Equal(t, "mystream", req.AccessRequest.Name) + return &dummyPath{}, str, nil + }, + } s := &Server{ Address: "127.0.0.1:8888", @@ -152,7 +170,7 @@ func TestServerRead(t *testing.T) { Directory: "", ReadTimeout: conf.StringDuration(10 * time.Second), WriteQueueSize: 512, - PathManager: pathManager, + PathManager: pm, Parent: test.NilLogger, } err = s.Initialize() @@ -192,7 +210,7 @@ func TestServerRead(t *testing.T) { go func() { time.Sleep(100 * time.Millisecond) for i := 0; i < 4; i++ { - stream.WriteUnit(test.MediaH264, test.FormatH264, &unit.H264{ + str.WriteUnit(test.MediaH264, test.FormatH264, &unit.H264{ Base: unit.Base{ NTP: time.Time{}, PTS: time.Duration(i) * time.Second, @@ -210,7 +228,7 @@ func TestServerRead(t *testing.T) { t.Run("always remux on", func(t *testing.T) { desc := &description.Session{Medias: []*description.Media{test.MediaH264}} - stream, err := stream.New( + str, err := stream.New( 1460, desc, true, @@ -218,7 +236,18 @@ func TestServerRead(t *testing.T) { ) require.NoError(t, err) - pathManager := &dummyPathManager{stream: stream} + pm := &dummyPathManager{ + findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) { + require.Equal(t, "mystream", req.AccessRequest.Name) + require.Equal(t, "myuser", req.AccessRequest.User) + require.Equal(t, "mypass", req.AccessRequest.Pass) + return &conf.Path{}, nil + }, + addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) { + require.Equal(t, "mystream", req.AccessRequest.Name) + return &dummyPath{}, str, nil + }, + } s := &Server{ Address: "127.0.0.1:8888", @@ -236,7 +265,7 @@ func TestServerRead(t *testing.T) { Directory: "", ReadTimeout: conf.StringDuration(10 * time.Second), WriteQueueSize: 512, - PathManager: pathManager, + PathManager: pm, Parent: test.NilLogger, } err = s.Initialize() @@ -248,7 +277,7 @@ func TestServerRead(t *testing.T) { time.Sleep(100 * time.Millisecond) for i := 0; i < 4; i++ { - stream.WriteUnit(test.MediaH264, test.FormatH264, &unit.H264{ + str.WriteUnit(test.MediaH264, test.FormatH264, &unit.H264{ Base: unit.Base{ NTP: time.Time{}, PTS: time.Duration(i) * time.Second, @@ -293,14 +322,10 @@ func TestServerRead(t *testing.T) { }) } -func TestDirectory(t *testing.T) { - dir, err := os.MkdirTemp("", "mediamtx-playback") - require.NoError(t, err) - defer os.RemoveAll(dir) - +func TestServerReadAuthorizationHeader(t *testing.T) { desc := &description.Session{Medias: []*description.Media{test.MediaH264}} - stream, err := stream.New( + str, err := stream.New( 1460, desc, true, @@ -308,7 +333,111 @@ func TestDirectory(t *testing.T) { ) require.NoError(t, err) - pathManager := &dummyPathManager{stream: stream} + pm := &dummyPathManager{ + findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) { + require.Equal(t, "jwt=testing", req.AccessRequest.Query) + return &conf.Path{}, nil + }, + addReader: func(_ defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) { + return &dummyPath{}, str, nil + }, + } + + s := &Server{ + Address: "127.0.0.1:8888", + Encryption: false, + ServerKey: "", + ServerCert: "", + AlwaysRemux: true, + Variant: conf.HLSVariant(gohlslib.MuxerVariantMPEGTS), + SegmentCount: 7, + SegmentDuration: conf.StringDuration(1 * time.Second), + PartDuration: conf.StringDuration(200 * time.Millisecond), + SegmentMaxSize: 50 * 1024 * 1024, + AllowOrigin: "", + TrustedProxies: conf.IPNetworks{}, + Directory: "", + ReadTimeout: conf.StringDuration(10 * time.Second), + WriteQueueSize: 512, + PathManager: pm, + Parent: test.NilLogger, + } + err = s.Initialize() + require.NoError(t, err) + defer s.Close() + + s.PathReady(&dummyPath{}) + + time.Sleep(100 * time.Millisecond) + + for i := 0; i < 4; i++ { + str.WriteUnit(test.MediaH264, test.FormatH264, &unit.H264{ + Base: unit.Base{ + NTP: time.Time{}, + PTS: time.Duration(i) * time.Second, + }, + AU: [][]byte{ + {5, 1}, // IDR + }, + }) + } + + c := &gohlslib.Client{ + URI: "http://127.0.0.1:8888/mystream/index.m3u8", + OnRequest: func(r *http.Request) { + r.Header.Set("Authorization", "Bearer testing") + }, + } + + recv := make(chan struct{}) + + c.OnTracks = func(tracks []*gohlslib.Track) error { + require.Equal(t, []*gohlslib.Track{{ + Codec: &codecs.H264{}, + }}, tracks) + + c.OnDataH26x(tracks[0], func(pts, dts time.Duration, au [][]byte) { + require.Equal(t, time.Duration(0), pts) + require.Equal(t, time.Duration(0), dts) + require.Equal(t, [][]byte{ + test.FormatH264.SPS, + test.FormatH264.PPS, + {5, 1}, + }, au) + close(recv) + }) + + return nil + } + + err = c.Start() + require.NoError(t, err) + defer func() { <-c.Wait() }() + defer c.Close() + + <-recv +} + +func TestDirectory(t *testing.T) { + dir, err := os.MkdirTemp("", "mediamtx-playback") + require.NoError(t, err) + defer os.RemoveAll(dir) + + desc := &description.Session{Medias: []*description.Media{test.MediaH264}} + + str, err := stream.New( + 1460, + desc, + true, + test.NilLogger, + ) + require.NoError(t, err) + + pm := &dummyPathManager{ + addReader: func(_ defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) { + return &dummyPath{}, str, nil + }, + } s := &Server{ Address: "127.0.0.1:8888", @@ -326,7 +455,7 @@ func TestDirectory(t *testing.T) { Directory: filepath.Join(dir, "mydir"), ReadTimeout: conf.StringDuration(10 * time.Second), WriteQueueSize: 512, - PathManager: pathManager, + PathManager: pm, Parent: test.NilLogger, } err = s.Initialize() diff --git a/internal/servers/webrtc/http_server.go b/internal/servers/webrtc/http_server.go index fe9b1462..665f6abe 100644 --- a/internal/servers/webrtc/http_server.go +++ b/internal/servers/webrtc/http_server.go @@ -7,6 +7,7 @@ import ( "io" "net" "net/http" + "net/url" "regexp" "strings" "time" @@ -59,6 +60,17 @@ func sessionLocation(publish bool, path string, secret uuid.UUID) string { return ret } +func addJWTFromAuthorization(rawQuery string, auth string) string { + jwt := strings.TrimPrefix(auth, "Bearer ") + if rawQuery != "" { + if v, err := url.ParseQuery(rawQuery); err == nil && v.Get("jwt") == "" { + v.Set("jwt", jwt) + return v.Encode() + } + } + return url.Values{"jwt": []string{jwt}}.Encode() +} + type httpServer struct { address string encryption bool @@ -110,10 +122,15 @@ func (s *httpServer) close() { func (s *httpServer) checkAuthOutsideSession(ctx *gin.Context, pathName string, publish bool) bool { user, pass, hasCredentials := ctx.Request.BasicAuth() + q := ctx.Request.URL.RawQuery + if h := ctx.Request.Header.Get("Authorization"); strings.HasPrefix(h, "Bearer ") { + q = addJWTFromAuthorization(q, h) + } + _, err := s.pathManager.FindPathConf(defs.PathFindPathConfReq{ AccessRequest: defs.PathAccessRequest{ Name: pathName, - Query: ctx.Request.URL.RawQuery, + Query: q, Publish: publish, IP: net.ParseIP(ctx.ClientIP()), User: user, @@ -178,10 +195,15 @@ func (s *httpServer) onWHIPPost(ctx *gin.Context, pathName string, publish bool) user, pass, _ := ctx.Request.BasicAuth() + q := ctx.Request.URL.RawQuery + if h := ctx.Request.Header.Get("Authorization"); strings.HasPrefix(h, "Bearer ") { + q = addJWTFromAuthorization(q, h) + } + res := s.parent.newSession(webRTCNewSessionReq{ pathName: pathName, remoteAddr: httpp.RemoteAddr(ctx), - query: ctx.Request.URL.RawQuery, + query: q, user: user, pass: pass, offer: offer, diff --git a/internal/servers/webrtc/server_test.go b/internal/servers/webrtc/server_test.go index 621e13ec..a8252d67 100644 --- a/internal/servers/webrtc/server_test.go +++ b/internal/servers/webrtc/server_test.go @@ -12,7 +12,6 @@ import ( "github.com/bluenviron/gortsplib/v4/pkg/description" "github.com/bluenviron/gortsplib/v4/pkg/format" "github.com/bluenviron/mediamtx/internal/asyncwriter" - "github.com/bluenviron/mediamtx/internal/auth" "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/externalcmd" @@ -72,34 +71,32 @@ func (p *dummyPath) RemoveReader(_ defs.PathRemoveReaderReq) { } type dummyPathManager struct { - path *dummyPath + findPathConf func(req defs.PathFindPathConfReq) (*conf.Path, error) + addPublisher func(req defs.PathAddPublisherReq) (defs.Path, error) + addReader func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) } func (pm *dummyPathManager) FindPathConf(req defs.PathFindPathConfReq) (*conf.Path, error) { - if req.AccessRequest.User != "myuser" || req.AccessRequest.Pass != "mypass" { - return nil, auth.Error{} - } - return &conf.Path{}, nil + return pm.findPathConf(req) } -func (pm *dummyPathManager) AddPublisher(_ defs.PathAddPublisherReq) (defs.Path, error) { - return pm.path, nil +func (pm *dummyPathManager) AddPublisher(req defs.PathAddPublisherReq) (defs.Path, error) { + return pm.addPublisher(req) } func (pm *dummyPathManager) AddReader(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) { - if req.AccessRequest.Name == "nonexisting" { - return nil, nil, defs.PathNoOnePublishingError{} - } - return pm.path, pm.path.stream, nil + return pm.addReader(req) } func initializeTestServer(t *testing.T) *Server { - path := &dummyPath{ - streamCreated: make(chan struct{}), + pm := &dummyPathManager{ + findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) { + require.Equal(t, "myuser", req.AccessRequest.User) + require.Equal(t, "mypass", req.AccessRequest.Pass) + return &conf.Path{}, nil + }, } - pathManager := &dummyPathManager{path: path} - s := &Server{ Address: "127.0.0.1:8886", Encryption: false, @@ -118,7 +115,7 @@ func initializeTestServer(t *testing.T) *Server { HandshakeTimeout: conf.StringDuration(10 * time.Second), TrackGatherTimeout: conf.StringDuration(2 * time.Second), ExternalCmdPool: nil, - PathManager: pathManager, + PathManager: pm, Parent: test.NilLogger, } err := s.Initialize() @@ -174,7 +171,13 @@ func TestServerOptionsPreflight(t *testing.T) { } func TestServerOptionsICEServer(t *testing.T) { - pathManager := &dummyPathManager{} + pathManager := &dummyPathManager{ + findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) { + require.Equal(t, "myuser", req.AccessRequest.User) + require.Equal(t, "mypass", req.AccessRequest.Pass) + return &conf.Path{}, nil + }, + } s := &Server{ Address: "127.0.0.1:8886", @@ -234,7 +237,20 @@ func TestServerPublish(t *testing.T) { streamCreated: make(chan struct{}), } - pathManager := &dummyPathManager{path: path} + pathManager := &dummyPathManager{ + findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) { + require.Equal(t, "teststream", req.AccessRequest.Name) + require.Equal(t, "myuser", req.AccessRequest.User) + require.Equal(t, "mypass", req.AccessRequest.Pass) + return &conf.Path{}, nil + }, + addPublisher: func(req defs.PathAddPublisherReq) (defs.Path, error) { + require.Equal(t, "teststream", req.AccessRequest.Name) + require.Equal(t, "myuser", req.AccessRequest.User) + require.Equal(t, "mypass", req.AccessRequest.Pass) + return path, nil + }, + } s := &Server{ Address: "127.0.0.1:8886", @@ -486,7 +502,7 @@ func TestServerRead(t *testing.T) { t.Run(ca.name, func(t *testing.T) { desc := &description.Session{Medias: ca.medias} - stream, err := stream.New( + str, err := stream.New( 1460, desc, reflect.TypeOf(ca.unit) != reflect.TypeOf(&unit.Generic{}), @@ -494,9 +510,22 @@ func TestServerRead(t *testing.T) { ) require.NoError(t, err) - path := &dummyPath{stream: stream} + path := &dummyPath{stream: str} - pathManager := &dummyPathManager{path: path} + pathManager := &dummyPathManager{ + findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) { + require.Equal(t, "teststream", req.AccessRequest.Name) + require.Equal(t, "myuser", req.AccessRequest.User) + require.Equal(t, "mypass", req.AccessRequest.Pass) + return &conf.Path{}, nil + }, + addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) { + require.Equal(t, "teststream", req.AccessRequest.Name) + require.Equal(t, "myuser", req.AccessRequest.User) + require.Equal(t, "mypass", req.AccessRequest.Pass) + return path, str, nil + }, + } s := &Server{ Address: "127.0.0.1:8886", @@ -556,9 +585,9 @@ func TestServerRead(t *testing.T) { if g, ok := r.Interface().(*unit.Generic); ok { clone := *g.RTPPackets[0] - stream.WriteRTPPacket(desc.Medias[0], desc.Medias[0].Formats[0], &clone, time.Time{}, 0) + str.WriteRTPPacket(desc.Medias[0], desc.Medias[0].Formats[0], &clone, time.Time{}, 0) } else { - stream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], r.Interface().(unit.Unit)) + str.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], r.Interface().(unit.Unit)) } } }() @@ -574,8 +603,118 @@ func TestServerRead(t *testing.T) { } } -func TestServerPostNotFound(t *testing.T) { - s := initializeTestServer(t) +func TestServerReadAuthorizationHeader(t *testing.T) { + desc := &description.Session{Medias: []*description.Media{test.MediaH264}} + + str, err := stream.New( + 1460, + desc, + true, + test.NilLogger, + ) + require.NoError(t, err) + + path := &dummyPath{stream: str} + + pm := &dummyPathManager{ + findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) { + require.Equal(t, "jwt=testing", req.AccessRequest.Query) + return &conf.Path{}, nil + }, + addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) { + require.Equal(t, "jwt=testing", req.AccessRequest.Query) + return path, str, nil + }, + } + + s := &Server{ + Address: "127.0.0.1:8886", + Encryption: false, + ServerKey: "", + ServerCert: "", + AllowOrigin: "", + TrustedProxies: conf.IPNetworks{}, + ReadTimeout: conf.StringDuration(10 * time.Second), + WriteQueueSize: 512, + LocalUDPAddress: "127.0.0.1:8887", + LocalTCPAddress: "127.0.0.1:8887", + IPsFromInterfaces: true, + IPsFromInterfacesList: []string{}, + AdditionalHosts: []string{}, + ICEServers: []conf.WebRTCICEServer{}, + HandshakeTimeout: conf.StringDuration(10 * time.Second), + TrackGatherTimeout: conf.StringDuration(2 * time.Second), + ExternalCmdPool: nil, + PathManager: pm, + Parent: test.NilLogger, + } + err = s.Initialize() + require.NoError(t, err) + defer s.Close() + + tr := &http.Transport{} + defer tr.CloseIdleConnections() + hc := &http.Client{Transport: tr} + + pc, err := pwebrtc.NewPeerConnection(pwebrtc.Configuration{}) + require.NoError(t, err) + defer pc.Close() //nolint:errcheck + + _, err = pc.AddTransceiverFromKind(pwebrtc.RTPCodecTypeVideo) + require.NoError(t, err) + + offer, err := pc.CreateOffer(nil) + require.NoError(t, err) + + req, err := http.NewRequest(http.MethodPost, + "http://localhost:8886/teststream/whep", bytes.NewReader([]byte(offer.SDP))) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/sdp") + req.Header.Set("Authorization", "Bearer testing") + + res, err := hc.Do(req) + require.NoError(t, err) + defer res.Body.Close() + + require.Equal(t, http.StatusCreated, res.StatusCode) +} + +func TestServerReadNotFound(t *testing.T) { + pm := &dummyPathManager{ + findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) { + require.Equal(t, "myuser", req.AccessRequest.User) + require.Equal(t, "mypass", req.AccessRequest.Pass) + return &conf.Path{}, nil + }, + addReader: func(_ defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) { + return nil, nil, defs.PathNoOnePublishingError{} + }, + } + + s := &Server{ + Address: "127.0.0.1:8886", + Encryption: false, + ServerKey: "", + ServerCert: "", + AllowOrigin: "", + TrustedProxies: conf.IPNetworks{}, + ReadTimeout: conf.StringDuration(10 * time.Second), + WriteQueueSize: 512, + LocalUDPAddress: "127.0.0.1:8887", + LocalTCPAddress: "127.0.0.1:8887", + IPsFromInterfaces: true, + IPsFromInterfacesList: []string{}, + AdditionalHosts: []string{}, + ICEServers: []conf.WebRTCICEServer{}, + HandshakeTimeout: conf.StringDuration(10 * time.Second), + TrackGatherTimeout: conf.StringDuration(2 * time.Second), + ExternalCmdPool: nil, + PathManager: pm, + Parent: test.NilLogger, + } + err := s.Initialize() + require.NoError(t, err) defer s.Close() tr := &http.Transport{} diff --git a/mediamtx.yml b/mediamtx.yml index e3929d9e..dbc451eb 100644 --- a/mediamtx.yml +++ b/mediamtx.yml @@ -117,7 +117,7 @@ authHTTPExclude: # } # ] # } -# Users are then expected to pass the JWT as a query parameter, i.e. ?jwt=... +# Users are expected to pass the JWT in the Authorization header or as a query parameter. # This is the JWKS URL that will be used to pull (once) the public key that allows # to validate JWTs. authJWTJWKS: From 39ae1269ad0ee60c32a171f9e038dd49b56999cc Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Tue, 11 Jun 2024 23:37:59 +0200 Subject: [PATCH 32/52] webrtc: support passing username and password through Bearer Token (#3248) (#3459) --- README.md | 42 +++++++++++-- internal/servers/webrtc/http_server.go | 18 +++++- internal/servers/webrtc/server_test.go | 81 +++++++++++++++++++++++++- 3 files changed, 134 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 7d64fe37..25ec0c46 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,8 @@ _rtsp-simple-server_ has been rebranded as _MediaMTX_. The reason is pretty obvi * [SRT-specific features](#srt-specific-features) * [Standard stream ID syntax](#standard-stream-id-syntax) * [WebRTC-specific features](#webrtc-specific-features) - * [Connectivity issues](#connectivity-issues) + * [Authenticating with WHIP/WHEP](#authenticating-with-whipwhep) + * [Solving WebRTC connectivity issues](#solving-webrtc-connectivity-issues) * [RTSP-specific features](#rtsp-specific-features) * [Transport protocols](#transport-protocols) * [Encryption](#encryption) @@ -338,6 +339,7 @@ Latest versions of OBS Studio can publish to the server with the [WebRTC / WHIP * Service: `WHIP` * Server: `http://localhost:8889/mystream/whip` +* Bearer Token: `myuser:mypass` (if internal authentication is enabled) or JWT (if JWT-based authentication is enabled) Save the configuration and click `Start streaming`. @@ -610,7 +612,9 @@ WHIP is a WebRTC extensions that allows to publish streams by using a URL, witho http://localhost:8889/mystream/whip ``` -Depending on the network it may be difficult to establish a connection between server and clients, see [WebRTC-specific features](#webrtc-specific-features) for remediations. +Regarding authentication, read [Authenticating with WHIP/WHEP](#authenticating-with-whipwhep). + +Depending on the network it may be difficult to establish a connection between server and clients, read [Solving WebRTC connectivity issues](#solving-webrtc-connectivity-issues). Known clients that can publish with WebRTC and WHIP are [FFmpeg](#ffmpeg), [GStreamer](#gstreamer), [OBS Studio](#obs-studio). @@ -876,7 +880,9 @@ WHEP is a WebRTC extensions that allows to read streams by using a URL, without http://localhost:8889/mystream/whep ``` -Depending on the network it may be difficult to establish a connection between server and clients, see [WebRTC-specific features](#webrtc-specific-features) for remediations. +Regarding authentication, read [Authenticating with WHIP/WHEP](#authenticating-with-whipwhep). + +Depending on the network it may be difficult to establish a connection between server and clients, read [Solving WebRTC connectivity issues](#solving-webrtc-connectivity-issues). Known clients that can read with WebRTC and WHEP are [FFmpeg](#ffmpeg-1), [GStreamer](#gstreamer-1) and [web browsers](#web-browsers-1). @@ -1838,7 +1844,35 @@ Where: ### WebRTC-specific features -#### Connectivity issues +#### Authenticating with WHIP/WHEP + +When using WHIP or WHEP to establish a WebRTC connection, there are multiple ways to provide credentials. + +If internal authentication or HTTP-based authentication is enabled, username and password can be passed through the `Authentication: Basic` header: + +``` +Authentication: Basic [base64_encoded_credentials] +``` + +Username and password can be also passed through the `Authentication: Bearer` header (since it's mandated by the specification): + +``` +Authentication: Bearer username:password +``` + +If JWT-based authentication is enabled, JWT can be passed through the `Authentication: Bearer` header: + +``` +Authentication: Bearer [jwt] +``` + +The JWT can also be passed through query parameters: + +``` +http://localhost:8889/mystream/whip?jwt=[jwt] +``` + +#### Solving WebRTC connectivity issues If the server is hosted inside a container or is behind a NAT, additional configuration is required in order to allow the two WebRTC parts (server and client) to establish a connection. diff --git a/internal/servers/webrtc/http_server.go b/internal/servers/webrtc/http_server.go index 665f6abe..4255b907 100644 --- a/internal/servers/webrtc/http_server.go +++ b/internal/servers/webrtc/http_server.go @@ -121,10 +121,17 @@ func (s *httpServer) close() { func (s *httpServer) checkAuthOutsideSession(ctx *gin.Context, pathName string, publish bool) bool { user, pass, hasCredentials := ctx.Request.BasicAuth() - q := ctx.Request.URL.RawQuery + if h := ctx.Request.Header.Get("Authorization"); strings.HasPrefix(h, "Bearer ") { + // JWT in authorization bearer -> JWT in query parameters q = addJWTFromAuthorization(q, h) + + // credentials in authorization bearer -> credentials in authorization basic + if parts := strings.Split(strings.TrimPrefix(h, "Bearer "), ":"); len(parts) == 2 { + user = parts[0] + pass = parts[1] + } } _, err := s.pathManager.FindPathConf(defs.PathFindPathConfReq{ @@ -194,10 +201,17 @@ func (s *httpServer) onWHIPPost(ctx *gin.Context, pathName string, publish bool) } user, pass, _ := ctx.Request.BasicAuth() - q := ctx.Request.URL.RawQuery + if h := ctx.Request.Header.Get("Authorization"); strings.HasPrefix(h, "Bearer ") { + // JWT in authorization bearer -> JWT in query parameters q = addJWTFromAuthorization(q, h) + + // credentials in authorization bearer -> credentials in authorization basic + if parts := strings.Split(strings.TrimPrefix(h, "Bearer "), ":"); len(parts) == 2 { + user = parts[0] + pass = parts[1] + } } res := s.parent.newSession(webRTCNewSessionReq{ diff --git a/internal/servers/webrtc/server_test.go b/internal/servers/webrtc/server_test.go index a8252d67..0fcbf124 100644 --- a/internal/servers/webrtc/server_test.go +++ b/internal/servers/webrtc/server_test.go @@ -603,7 +603,7 @@ func TestServerRead(t *testing.T) { } } -func TestServerReadAuthorizationHeader(t *testing.T) { +func TestServerReadAuthorizationBearerJWT(t *testing.T) { desc := &description.Session{Medias: []*description.Media{test.MediaH264}} str, err := stream.New( @@ -680,6 +680,85 @@ func TestServerReadAuthorizationHeader(t *testing.T) { require.Equal(t, http.StatusCreated, res.StatusCode) } +func TestServerReadAuthorizationUserPass(t *testing.T) { + desc := &description.Session{Medias: []*description.Media{test.MediaH264}} + + str, err := stream.New( + 1460, + desc, + true, + test.NilLogger, + ) + require.NoError(t, err) + + path := &dummyPath{stream: str} + + pm := &dummyPathManager{ + findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) { + require.Equal(t, "myuser", req.AccessRequest.User) + require.Equal(t, "mypass", req.AccessRequest.Pass) + return &conf.Path{}, nil + }, + addReader: func(req defs.PathAddReaderReq) (defs.Path, *stream.Stream, error) { + require.Equal(t, "myuser", req.AccessRequest.User) + require.Equal(t, "mypass", req.AccessRequest.Pass) + return path, str, nil + }, + } + + s := &Server{ + Address: "127.0.0.1:8886", + Encryption: false, + ServerKey: "", + ServerCert: "", + AllowOrigin: "", + TrustedProxies: conf.IPNetworks{}, + ReadTimeout: conf.StringDuration(10 * time.Second), + WriteQueueSize: 512, + LocalUDPAddress: "127.0.0.1:8887", + LocalTCPAddress: "127.0.0.1:8887", + IPsFromInterfaces: true, + IPsFromInterfacesList: []string{}, + AdditionalHosts: []string{}, + ICEServers: []conf.WebRTCICEServer{}, + HandshakeTimeout: conf.StringDuration(10 * time.Second), + TrackGatherTimeout: conf.StringDuration(2 * time.Second), + ExternalCmdPool: nil, + PathManager: pm, + Parent: test.NilLogger, + } + err = s.Initialize() + require.NoError(t, err) + defer s.Close() + + tr := &http.Transport{} + defer tr.CloseIdleConnections() + hc := &http.Client{Transport: tr} + + pc, err := pwebrtc.NewPeerConnection(pwebrtc.Configuration{}) + require.NoError(t, err) + defer pc.Close() //nolint:errcheck + + _, err = pc.AddTransceiverFromKind(pwebrtc.RTPCodecTypeVideo) + require.NoError(t, err) + + offer, err := pc.CreateOffer(nil) + require.NoError(t, err) + + req, err := http.NewRequest(http.MethodPost, + "http://localhost:8886/teststream/whep", bytes.NewReader([]byte(offer.SDP))) + require.NoError(t, err) + + req.Header.Set("Content-Type", "application/sdp") + req.Header.Set("Authorization", "Bearer myuser:mypass") + + res, err := hc.Do(req) + require.NoError(t, err) + defer res.Body.Close() + + require.Equal(t, http.StatusCreated, res.StatusCode) +} + func TestServerReadNotFound(t *testing.T) { pm := &dummyPathManager{ findPathConf: func(req defs.PathFindPathConfReq) (*conf.Path, error) { From 9554fc4ba0402a885f1089984a111ad662d04220 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Wed, 12 Jun 2024 17:38:55 +0200 Subject: [PATCH 33/52] prevent mixing together legacy and current auth mechanism (#3258) (#3460) --- internal/api/api_test.go | 6 +- internal/conf/conf.go | 113 ++++++++++++++++++------------------- internal/conf/conf_test.go | 60 ++++++++++++++++++++ internal/conf/path.go | 12 ++-- 4 files changed, 124 insertions(+), 67 deletions(-) diff --git a/internal/api/api_test.go b/internal/api/api_test.go index a88446aa..9cddc257 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -247,16 +247,14 @@ func TestConfigPathDefaultsPatch(t *testing.T) { httpRequest(t, hc, http.MethodPatch, "http://localhost:9997/v3/config/pathdefaults/patch", map[string]interface{}{ - "readUser": "myuser", - "readPass": "mypass", + "recordFormat": "fmp4", }, nil) time.Sleep(500 * time.Millisecond) var out map[string]interface{} httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/config/pathdefaults/get", nil, &out) - require.Equal(t, "myuser", out["readUser"]) - require.Equal(t, "mypass", out["readPass"]) + require.Equal(t, "fmp4", out["recordFormat"]) } func TestConfigPathsList(t *testing.T) { diff --git a/internal/conf/conf.go b/internal/conf/conf.go index e70da0dc..8516d271 100644 --- a/internal/conf/conf.go +++ b/internal/conf/conf.go @@ -94,24 +94,25 @@ func mustParseCIDR(v string) net.IPNet { return *ne } -func credentialIsNotEmpty(c *Credential) bool { - return c != nil && *c != "" -} +func anyPathHasDeprecatedCredentials(pathDefaults Path, paths map[string]*OptionalPath) bool { + if pathDefaults.PublishUser != nil || + pathDefaults.PublishPass != nil || + pathDefaults.PublishIPs != nil || + pathDefaults.ReadUser != nil || + pathDefaults.ReadPass != nil || + pathDefaults.ReadIPs != nil { + return true + } -func ipNetworkIsNotEmpty(i *IPNetworks) bool { - return i != nil && len(*i) != 0 -} - -func anyPathHasDeprecatedCredentials(paths map[string]*OptionalPath) bool { for _, pa := range paths { if pa != nil { rva := reflect.ValueOf(pa.Values).Elem() - if credentialIsNotEmpty(rva.FieldByName("PublishUser").Interface().(*Credential)) || - credentialIsNotEmpty(rva.FieldByName("PublishPass").Interface().(*Credential)) || - ipNetworkIsNotEmpty(rva.FieldByName("PublishIPs").Interface().(*IPNetworks)) || - credentialIsNotEmpty(rva.FieldByName("ReadUser").Interface().(*Credential)) || - credentialIsNotEmpty(rva.FieldByName("ReadPass").Interface().(*Credential)) || - ipNetworkIsNotEmpty(rva.FieldByName("ReadIPs").Interface().(*IPNetworks)) { + if rva.FieldByName("PublishUser").Interface().(*Credential) != nil || + rva.FieldByName("PublishPass").Interface().(*Credential) != nil || + rva.FieldByName("PublishIPs").Interface().(*IPNetworks) != nil || + rva.FieldByName("ReadUser").Interface().(*Credential) != nil || + rva.FieldByName("ReadPass").Interface().(*Credential) != nil || + rva.FieldByName("ReadIPs").Interface().(*IPNetworks) != nil { return true } } @@ -119,6 +120,40 @@ func anyPathHasDeprecatedCredentials(paths map[string]*OptionalPath) bool { return false } +var defaultAuthInternalUsers = AuthInternalUsers{ + { + User: "any", + Pass: "", + Permissions: []AuthInternalUserPermission{ + { + Action: AuthActionPublish, + }, + { + Action: AuthActionRead, + }, + { + Action: AuthActionPlayback, + }, + }, + }, + { + User: "any", + Pass: "", + IPs: IPNetworks{mustParseCIDR("127.0.0.1/32"), mustParseCIDR("::1/128")}, + Permissions: []AuthInternalUserPermission{ + { + Action: AuthActionAPI, + }, + { + Action: AuthActionMetrics, + }, + { + Action: AuthActionPprof, + }, + }, + }, +} + // Conf is a configuration. // WARNING: Avoid using slices directly due to https://github.com/golang/go/issues/21092 type Conf struct { @@ -276,39 +311,7 @@ func (conf *Conf) setDefaults() { conf.UDPMaxPayloadSize = 1472 // Authentication - conf.AuthInternalUsers = []AuthInternalUser{ - { - User: "any", - Pass: "", - Permissions: []AuthInternalUserPermission{ - { - Action: AuthActionPublish, - }, - { - Action: AuthActionRead, - }, - { - Action: AuthActionPlayback, - }, - }, - }, - { - User: "any", - Pass: "", - IPs: IPNetworks{mustParseCIDR("127.0.0.1/32"), mustParseCIDR("::1/128")}, - Permissions: []AuthInternalUserPermission{ - { - Action: AuthActionAPI, - }, - { - Action: AuthActionMetrics, - }, - { - Action: AuthActionPprof, - }, - }, - }, - } + conf.AuthInternalUsers = defaultAuthInternalUsers conf.AuthHTTPExclude = []AuthInternalUserPermission{ { Action: AuthActionAPI, @@ -501,7 +504,6 @@ func (conf *Conf) Validate() error { } // Authentication - if conf.ExternalAuthenticationURL != nil { conf.AuthMethod = AuthMethodHTTP conf.AuthHTTPAddress = *conf.ExternalAuthenticationURL @@ -517,17 +519,15 @@ func (conf *Conf) Validate() error { return fmt.Errorf("'authJWTJWKS' must be a HTTP URL") } deprecatedCredentialsMode := false - if credentialIsNotEmpty(conf.PathDefaults.PublishUser) || - credentialIsNotEmpty(conf.PathDefaults.PublishPass) || - ipNetworkIsNotEmpty(conf.PathDefaults.PublishIPs) || - credentialIsNotEmpty(conf.PathDefaults.ReadUser) || - credentialIsNotEmpty(conf.PathDefaults.ReadPass) || - ipNetworkIsNotEmpty(conf.PathDefaults.ReadIPs) || - anyPathHasDeprecatedCredentials(conf.OptionalPaths) { + if anyPathHasDeprecatedCredentials(conf.PathDefaults, conf.OptionalPaths) { + if conf.AuthInternalUsers != nil && !reflect.DeepEqual(conf.AuthInternalUsers, defaultAuthInternalUsers) { + return fmt.Errorf("authInternalUsers and legacy credentials " + + "(publishUser, publishPass, publishIPs, readUser, readPass, readIPs) cannot be used together") + } + conf.AuthInternalUsers = []AuthInternalUser{ { User: "any", - Pass: "", Permissions: []AuthInternalUserPermission{ { Action: AuthActionPlayback, @@ -536,7 +536,6 @@ func (conf *Conf) Validate() error { }, { User: "any", - Pass: "", IPs: IPNetworks{mustParseCIDR("127.0.0.1/32"), mustParseCIDR("::1/128")}, Permissions: []AuthInternalUserPermission{ { diff --git a/internal/conf/conf_test.go b/internal/conf/conf_test.go index 540abdaf..0c26086c 100644 --- a/internal/conf/conf_test.go +++ b/internal/conf/conf_test.go @@ -192,6 +192,66 @@ func TestConfEncryption(t *testing.T) { require.Equal(t, true, ok) } +func TestConfDeprecatedAuth(t *testing.T) { + tmpf, err := createTempFile([]byte( + "paths:\n" + + " cam:\n" + + " readUser: myuser\n" + + " readPass: mypass\n")) + require.NoError(t, err) + defer os.Remove(tmpf) + + conf, _, err := Load(tmpf, nil) + require.NoError(t, err) + + require.Equal(t, AuthInternalUsers{ + { + User: "any", + Permissions: []AuthInternalUserPermission{ + { + Action: AuthActionPlayback, + }, + }, + }, + { + User: "any", + IPs: IPNetworks{mustParseCIDR("127.0.0.1/32"), mustParseCIDR("::1/128")}, + Permissions: []AuthInternalUserPermission{ + { + Action: AuthActionAPI, + }, + { + Action: AuthActionMetrics, + }, + { + Action: AuthActionPprof, + }, + }, + }, + { + User: "any", + IPs: IPNetworks{mustParseCIDR("0.0.0.0/0")}, + Permissions: []AuthInternalUserPermission{ + { + Action: AuthActionPublish, + Path: "cam", + }, + }, + }, + { + User: "myuser", + Pass: "mypass", + IPs: IPNetworks{mustParseCIDR("0.0.0.0/0")}, + Permissions: []AuthInternalUserPermission{ + { + Action: AuthActionRead, + Path: "cam", + }, + }, + }, + }, conf.AuthInternalUsers) +} + func TestConfErrors(t *testing.T) { for _, ca := range []struct { name string diff --git a/internal/conf/path.go b/internal/conf/path.go index 042eae22..925f050f 100644 --- a/internal/conf/path.go +++ b/internal/conf/path.go @@ -399,17 +399,17 @@ func (pconf *Path) validate( if deprecatedCredentialsMode { func() { var user Credential = "any" - if credentialIsNotEmpty(pconf.PublishUser) { + if pconf.PublishUser != nil && *pconf.PublishUser != "" { user = *pconf.PublishUser } var pass Credential - if credentialIsNotEmpty(pconf.PublishPass) { + if pconf.PublishPass != nil && *pconf.PublishPass != "" { pass = *pconf.PublishPass } ips := IPNetworks{mustParseCIDR("0.0.0.0/0")} - if ipNetworkIsNotEmpty(pconf.PublishIPs) { + if pconf.PublishIPs != nil && len(*pconf.PublishIPs) != 0 { ips = *pconf.PublishIPs } @@ -431,17 +431,17 @@ func (pconf *Path) validate( func() { var user Credential = "any" - if credentialIsNotEmpty(pconf.ReadUser) { + if pconf.ReadUser != nil && *pconf.ReadUser != "" { user = *pconf.ReadUser } var pass Credential - if credentialIsNotEmpty(pconf.ReadPass) { + if pconf.ReadPass != nil && *pconf.ReadPass != "" { pass = *pconf.ReadPass } ips := IPNetworks{mustParseCIDR("0.0.0.0/0")} - if ipNetworkIsNotEmpty(pconf.ReadIPs) { + if pconf.ReadIPs != nil && len(*pconf.ReadIPs) != 0 { ips = *pconf.ReadIPs } From f227971517c12760ead9504f40cec768091120a6 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Mon, 17 Jun 2024 23:09:25 +0200 Subject: [PATCH 34/52] fix UDP-related tests (#3484) for some reason, using "localhost" with UDP inside GitHub Actions and Docker stopped working. --- internal/servers/srt/server_test.go | 4 ++-- internal/staticsources/srt/source_test.go | 4 ++-- internal/staticsources/udp/source_test.go | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/servers/srt/server_test.go b/internal/servers/srt/server_test.go index e9fc4ce8..8e0f9573 100644 --- a/internal/servers/srt/server_test.go +++ b/internal/servers/srt/server_test.go @@ -106,7 +106,7 @@ func TestServerPublish(t *testing.T) { require.NoError(t, err) defer s.Close() - u := "srt://localhost:8890?streamid=publish:mypath:myuser:mypass" + u := "srt://127.0.0.1:8890?streamid=publish:mypath:myuser:mypass" srtConf := srt.DefaultConfig() address, err := srtConf.UnmarshalURL(u) @@ -205,7 +205,7 @@ func TestServerRead(t *testing.T) { require.NoError(t, err) defer s.Close() - u := "srt://localhost:8890?streamid=read:mypath:myuser:mypass" + u := "srt://127.0.0.1:8890?streamid=read:mypath:myuser:mypass" srtConf := srt.DefaultConfig() address, err := srtConf.UnmarshalURL(u) diff --git a/internal/staticsources/srt/source_test.go b/internal/staticsources/srt/source_test.go index 8222956f..6243a8e1 100644 --- a/internal/staticsources/srt/source_test.go +++ b/internal/staticsources/srt/source_test.go @@ -15,7 +15,7 @@ import ( ) func TestSource(t *testing.T) { - ln, err := srt.Listen("srt", "localhost:9002", srt.DefaultConfig()) + ln, err := srt.Listen("srt", "127.0.0.1:9002", srt.DefaultConfig()) require.NoError(t, err) defer ln.Close() @@ -55,7 +55,7 @@ func TestSource(t *testing.T) { te := test.NewSourceTester( func(p defs.StaticSourceParent) defs.StaticSource { return &Source{ - ResolvedSource: "srt://localhost:9002?streamid=sidname&passphrase=ttest1234567", + ResolvedSource: "srt://127.0.0.1:9002?streamid=sidname&passphrase=ttest1234567", ReadTimeout: conf.StringDuration(10 * time.Second), Parent: p, } diff --git a/internal/staticsources/udp/source_test.go b/internal/staticsources/udp/source_test.go index 9b40bad4..65fb759a 100644 --- a/internal/staticsources/udp/source_test.go +++ b/internal/staticsources/udp/source_test.go @@ -18,7 +18,7 @@ func TestSource(t *testing.T) { te := test.NewSourceTester( func(p defs.StaticSourceParent) defs.StaticSource { return &Source{ - ResolvedSource: "udp://localhost:9001", + ResolvedSource: "udp://127.0.0.1:9001", ReadTimeout: conf.StringDuration(10 * time.Second), Parent: p, } @@ -29,7 +29,7 @@ func TestSource(t *testing.T) { time.Sleep(50 * time.Millisecond) - conn, err := net.Dial("udp", "localhost:9001") + conn, err := net.Dial("udp", "127.0.0.1:9001") require.NoError(t, err) defer conn.Close() From 7dcfd2e46d7aed40e65dff5fa2e7649117e21ecc Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jun 2024 23:12:10 +0200 Subject: [PATCH 35/52] build(deps): bump github.com/gorilla/websocket from 1.5.2 to 1.5.3 (#3470) Bumps [github.com/gorilla/websocket](https://github.com/gorilla/websocket) from 1.5.2 to 1.5.3. - [Release notes](https://github.com/gorilla/websocket/releases) - [Commits](https://github.com/gorilla/websocket/compare/v1.5.2...v1.5.3) --- updated-dependencies: - dependency-name: github.com/gorilla/websocket dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 12075484..3a8f780c 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/uuid v1.6.0 github.com/gookit/color v1.5.4 - github.com/gorilla/websocket v1.5.2 + github.com/gorilla/websocket v1.5.3 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/matthewhartstonge/argon2 v1.0.0 github.com/pion/ice/v2 v2.3.24 diff --git a/go.sum b/go.sum index 316ed2d9..d057a177 100644 --- a/go.sum +++ b/go.sum @@ -86,8 +86,8 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0= github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w= -github.com/gorilla/websocket v1.5.2 h1:qoW6V1GT3aZxybsbC6oLnailWnB+qTMVwMreOso9XUw= -github.com/gorilla/websocket v1.5.2/go.mod h1:0n9H61RBAcf5/38py2MCYbxzPIY9rOkpvvMT24Rqs30= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= From dfa2e81e61319b69db20232d3d3c34d8b2749f1e Mon Sep 17 00:00:00 2001 From: Bouke van der Bijl Date: Mon, 17 Jun 2024 23:15:18 +0200 Subject: [PATCH 36/52] Save a hash of the hls.min.js release (#3464) This ensures the downloaded hls.js matches exactly and removes a dependency on cdn.jsdelivr.net --- .github/workflows/bump_hls_js.yml | 9 +++-- internal/servers/hls/hlsjsdownloader/HASH | 1 + internal/servers/hls/hlsjsdownloader/main.go | 39 +++++++++++++++++--- 3 files changed, 41 insertions(+), 8 deletions(-) create mode 100644 internal/servers/hls/hlsjsdownloader/HASH diff --git a/.github/workflows/bump_hls_js.yml b/.github/workflows/bump_hls_js.yml index dfe767bd..234f3891 100644 --- a/.github/workflows/bump_hls_js.yml +++ b/.github/workflows/bump_hls_js.yml @@ -19,10 +19,13 @@ jobs: && git config user.email bot@mediamtx && ((git checkout deps/hlsjs && git rebase ${GITHUB_REF_NAME}) || git checkout -b deps/hlsjs) - - run: > + - run: | + set -e VERSION=$(curl -s https://api.github.com/repos/video-dev/hls.js/releases?per_page=1 | grep tag_name | sed 's/\s\+"tag_name": "\(.\+\)",/\1/') - && echo $VERSION > internal/servers/hls/hlsjsdownloader/VERSION - && echo VERSION=$VERSION >> $GITHUB_ENV + HASH=$(curl -sL https://github.com/video-dev/hls.js/releases/download/$VERSION/release.zip -o- | sha256sum | cut -f1 -d ' ') + echo $VERSION > internal/servers/hls/hlsjsdownloader/VERSION + echo $HASH > internal/servers/hls/hlsjsdownloader/HASH + echo VERSION=$VERSION >> $GITHUB_ENV - id: check_repo run: > diff --git a/internal/servers/hls/hlsjsdownloader/HASH b/internal/servers/hls/hlsjsdownloader/HASH new file mode 100644 index 00000000..15a7082a --- /dev/null +++ b/internal/servers/hls/hlsjsdownloader/HASH @@ -0,0 +1 @@ +869ea17a6ddb2cf483ca8c692fc6c7ba80de0882105ba300027af2edaed1b902 diff --git a/internal/servers/hls/hlsjsdownloader/main.go b/internal/servers/hls/hlsjsdownloader/main.go index 41008dec..7b9f3357 100644 --- a/internal/servers/hls/hlsjsdownloader/main.go +++ b/internal/servers/hls/hlsjsdownloader/main.go @@ -2,8 +2,13 @@ package main import ( + "archive/zip" + "bytes" + "crypto/sha256" + "encoding/hex" "fmt" "io" + "io/fs" "log" "net/http" "os" @@ -11,15 +16,16 @@ import ( ) func do() error { - log.Println("downloading hls.js...") - buf, err := os.ReadFile("./hlsjsdownloader/VERSION") if err != nil { return err } + version := strings.TrimSpace(string(buf)) - res, err := http.Get("https://cdn.jsdelivr.net/npm/hls.js@" + version + "/dist/hls.min.js") + log.Printf("downloading hls.js version %s...", version) + + res, err := http.Get("https://github.com/video-dev/hls.js/releases/download/" + version + "/release.zip") if err != nil { return err } @@ -29,15 +35,38 @@ func do() error { return fmt.Errorf("bad status code: %v", res.StatusCode) } - buf, err = io.ReadAll(res.Body) + zipBuf, err := io.ReadAll(res.Body) if err != nil { return err } - err = os.WriteFile("hls.min.js", buf, 0o644) + hashBuf, err := os.ReadFile("./hlsjsdownloader/HASH") if err != nil { return err } + hash := make([]byte, hex.DecodedLen(len(hashBuf))) + + if _, err = hex.Decode(hash, bytes.TrimSpace(hashBuf)); err != nil { + return err + } + + if sum := sha256.Sum256(zipBuf); !bytes.Equal(sum[:], hash) { + return fmt.Errorf("hash mismatch") + } + + z, err := zip.NewReader(bytes.NewReader(zipBuf), int64(len(zipBuf))) + if err != nil { + return err + } + + hls, err := fs.ReadFile(z, "dist/hls.min.js") + if err != nil { + return err + } + + if err = os.WriteFile("hls.min.js", hls, 0o644); err != nil { + return err + } log.Println("ok") return nil From 65d90f7cc62f3357c005bf5d67d96cf295b5a5c9 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Tue, 18 Jun 2024 22:10:26 +0200 Subject: [PATCH 37/52] allow using MTX_QUERY inside source (#3486) this allows to pass query parameters to sources, for instance: source: rtsp://my_host/my_path?$MTX_QUERY sourceOnDemand: true --- internal/core/path.go | 19 ++--- internal/core/path_test.go | 7 +- internal/core/static_source_handler.go | 88 ++++++++++++-------- internal/defs/static_source.go | 7 +- internal/staticsources/hls/source.go | 7 +- internal/staticsources/hls/source_test.go | 4 +- internal/staticsources/rtmp/source.go | 9 +- internal/staticsources/rtmp/source_test.go | 16 ++-- internal/staticsources/rtsp/source.go | 3 +- internal/staticsources/rtsp/source_test.go | 8 +- internal/staticsources/srt/source.go | 7 +- internal/staticsources/srt/source_test.go | 6 +- internal/staticsources/udp/source.go | 7 +- internal/staticsources/udp/source_test.go | 6 +- internal/staticsources/webrtc/source.go | 7 +- internal/staticsources/webrtc/source_test.go | 6 +- internal/test/source_tester.go | 11 ++- mediamtx.yml | 6 +- 18 files changed, 118 insertions(+), 106 deletions(-) diff --git a/internal/core/path.go b/internal/core/path.go index 70ab2d54..2119927c 100644 --- a/internal/core/path.go +++ b/internal/core/path.go @@ -169,26 +169,19 @@ func (pa *path) run() { if pa.conf.Source == "redirect" { pa.source = &sourceRedirect{} } else if pa.conf.HasStaticSource() { - resolvedSource := pa.conf.Source - if len(pa.matches) > 1 { - for i, ma := range pa.matches[1:] { - resolvedSource = strings.ReplaceAll(resolvedSource, "$G"+strconv.FormatInt(int64(i+1), 10), ma) - } - } - pa.source = &staticSourceHandler{ conf: pa.conf, logLevel: pa.logLevel, readTimeout: pa.readTimeout, writeTimeout: pa.writeTimeout, writeQueueSize: pa.writeQueueSize, - resolvedSource: resolvedSource, + matches: pa.matches, parent: pa, } pa.source.(*staticSourceHandler).initialize() if !pa.conf.SourceOnDemand { - pa.source.(*staticSourceHandler).start(false) + pa.source.(*staticSourceHandler).start(false, "") } } @@ -431,7 +424,7 @@ func (pa *path) doDescribe(req defs.PathDescribeReq) { if pa.conf.HasOnDemandStaticSource() { if pa.onDemandStaticSourceState == pathOnDemandStateInitial { - pa.onDemandStaticSourceStart() + pa.onDemandStaticSourceStart(req.AccessRequest.Query) } pa.describeRequestsOnHold = append(pa.describeRequestsOnHold, req) return @@ -539,7 +532,7 @@ func (pa *path) doAddReader(req defs.PathAddReaderReq) { if pa.conf.HasOnDemandStaticSource() { if pa.onDemandStaticSourceState == pathOnDemandStateInitial { - pa.onDemandStaticSourceStart() + pa.onDemandStaticSourceStart(req.AccessRequest.Query) } pa.readerAddRequestsOnHold = append(pa.readerAddRequestsOnHold, req) return @@ -655,8 +648,8 @@ func (pa *path) shouldClose() bool { len(pa.readerAddRequestsOnHold) == 0 } -func (pa *path) onDemandStaticSourceStart() { - pa.source.(*staticSourceHandler).start(true) +func (pa *path) onDemandStaticSourceStart(query string) { + pa.source.(*staticSourceHandler).start(true, query) pa.onDemandStaticSourceReadyTimer.Stop() pa.onDemandStaticSourceReadyTimer = time.NewTimer(time.Duration(pa.conf.SourceOnDemandStartTimeout)) diff --git a/internal/core/path_test.go b/internal/core/path_test.go index 145c00c7..5b123061 100644 --- a/internal/core/path_test.go +++ b/internal/core/path_test.go @@ -700,13 +700,14 @@ func TestPathFallback(t *testing.T) { } } -func TestPathSourceRegexp(t *testing.T) { +func TestPathResolveSource(t *testing.T) { var stream *gortsplib.ServerStream s := gortsplib.Server{ Handler: &testServer{ onDescribe: func(ctx *gortsplib.ServerHandlerOnDescribeCtx, ) (*base.Response, *gortsplib.ServerStream, error) { + require.Equal(t, "key=val", ctx.Query) require.Equal(t, "/a", ctx.Path) return &base.Response{ StatusCode: base.StatusOK, @@ -736,7 +737,7 @@ func TestPathSourceRegexp(t *testing.T) { p, ok := newInstance( "paths:\n" + " '~^test_(.+)$':\n" + - " source: rtsp://127.0.0.1:8555/$G1\n" + + " source: rtsp://127.0.0.1:8555/$G1?$MTX_QUERY\n" + " sourceOnDemand: yes\n" + " 'all':\n") require.Equal(t, true, ok) @@ -744,7 +745,7 @@ func TestPathSourceRegexp(t *testing.T) { reader := gortsplib.Client{} - u, err := base.ParseURL("rtsp://127.0.0.1:8554/test_a") + u, err := base.ParseURL("rtsp://127.0.0.1:8554/test_a?key=val") require.NoError(t, err) err = reader.Start(u.Scheme, u.Host) diff --git a/internal/core/static_source_handler.go b/internal/core/static_source_handler.go index 57f4c983..adec4f5b 100644 --- a/internal/core/static_source_handler.go +++ b/internal/core/static_source_handler.go @@ -3,6 +3,7 @@ package core import ( "context" "fmt" + "strconv" "strings" "time" @@ -22,6 +23,18 @@ const ( staticSourceHandlerRetryPause = 5 * time.Second ) +func resolveSource(s string, matches []string, query string) string { + if len(matches) > 1 { + for i, ma := range matches[1:] { + s = strings.ReplaceAll(s, "$G"+strconv.FormatInt(int64(i+1), 10), ma) + } + } + + s = strings.ReplaceAll(s, "$MTX_QUERY", query) + + return s +} + type staticSourceHandlerParent interface { logger.Writer staticSourceHandlerSetReady(context.Context, defs.PathSourceStaticSetReadyReq) @@ -35,13 +48,14 @@ type staticSourceHandler struct { readTimeout conf.StringDuration writeTimeout conf.StringDuration writeQueueSize int - resolvedSource string + matches []string parent staticSourceHandlerParent ctx context.Context ctxCancel func() instance defs.StaticSource running bool + query string // in chReloadConf chan *conf.Path @@ -58,60 +72,57 @@ func (s *staticSourceHandler) initialize() { s.chInstanceSetNotReady = make(chan defs.PathSourceStaticSetNotReadyReq) switch { - case strings.HasPrefix(s.resolvedSource, "rtsp://") || - strings.HasPrefix(s.resolvedSource, "rtsps://"): + case strings.HasPrefix(s.conf.Source, "rtsp://") || + strings.HasPrefix(s.conf.Source, "rtsps://"): s.instance = &rtspsource.Source{ - ResolvedSource: s.resolvedSource, ReadTimeout: s.readTimeout, WriteTimeout: s.writeTimeout, WriteQueueSize: s.writeQueueSize, Parent: s, } - case strings.HasPrefix(s.resolvedSource, "rtmp://") || - strings.HasPrefix(s.resolvedSource, "rtmps://"): + case strings.HasPrefix(s.conf.Source, "rtmp://") || + strings.HasPrefix(s.conf.Source, "rtmps://"): s.instance = &rtmpsource.Source{ - ResolvedSource: s.resolvedSource, - ReadTimeout: s.readTimeout, - WriteTimeout: s.writeTimeout, - Parent: s, + ReadTimeout: s.readTimeout, + WriteTimeout: s.writeTimeout, + Parent: s, } - case strings.HasPrefix(s.resolvedSource, "http://") || - strings.HasPrefix(s.resolvedSource, "https://"): + case strings.HasPrefix(s.conf.Source, "http://") || + strings.HasPrefix(s.conf.Source, "https://"): s.instance = &hlssource.Source{ - ResolvedSource: s.resolvedSource, - ReadTimeout: s.readTimeout, - Parent: s, + ReadTimeout: s.readTimeout, + Parent: s, } - case strings.HasPrefix(s.resolvedSource, "udp://"): + case strings.HasPrefix(s.conf.Source, "udp://"): s.instance = &udpsource.Source{ - ResolvedSource: s.resolvedSource, - ReadTimeout: s.readTimeout, - Parent: s, + ReadTimeout: s.readTimeout, + Parent: s, } - case strings.HasPrefix(s.resolvedSource, "srt://"): + case strings.HasPrefix(s.conf.Source, "srt://"): s.instance = &srtsource.Source{ - ResolvedSource: s.resolvedSource, - ReadTimeout: s.readTimeout, - Parent: s, + ReadTimeout: s.readTimeout, + Parent: s, } - case strings.HasPrefix(s.resolvedSource, "whep://") || - strings.HasPrefix(s.resolvedSource, "wheps://"): + case strings.HasPrefix(s.conf.Source, "whep://") || + strings.HasPrefix(s.conf.Source, "wheps://"): s.instance = &webrtcsource.Source{ - ResolvedSource: s.resolvedSource, - ReadTimeout: s.readTimeout, - Parent: s, + ReadTimeout: s.readTimeout, + Parent: s, } - case s.resolvedSource == "rpiCamera": + case s.conf.Source == "rpiCamera": s.instance = &rpicamerasource.Source{ LogLevel: s.logLevel, Parent: s, } + + default: + panic("should not happen") } } @@ -119,12 +130,16 @@ func (s *staticSourceHandler) close(reason string) { s.stop(reason) } -func (s *staticSourceHandler) start(onDemand bool) { +func (s *staticSourceHandler) start(onDemand bool, query string) { if s.running { panic("should not happen") } s.running = true + s.query = query + s.ctx, s.ctxCancel = context.WithCancel(context.Background()) + s.done = make(chan struct{}) + s.instance.Log(logger.Info, "started%s", func() string { if onDemand { @@ -133,9 +148,6 @@ func (s *staticSourceHandler) start(onDemand bool) { return "" }()) - s.ctx, s.ctxCancel = context.WithCancel(context.Background()) - s.done = make(chan struct{}) - go s.run() } @@ -145,6 +157,7 @@ func (s *staticSourceHandler) stop(reason string) { } s.running = false + s.instance.Log(logger.Info, "stopped: %s", reason) s.ctxCancel() @@ -167,12 +180,15 @@ func (s *staticSourceHandler) run() { runReloadConf := make(chan *conf.Path) recreate := func() { + resolvedSource := resolveSource(s.conf.Source, s.matches, s.query) + runCtx, runCtxCancel = context.WithCancel(context.Background()) go func() { runErr <- s.instance.Run(defs.StaticSourceRunParams{ - Context: runCtx, - Conf: s.conf, - ReloadConf: runReloadConf, + Context: runCtx, + ResolvedSource: resolvedSource, + Conf: s.conf, + ReloadConf: runReloadConf, }) }() } diff --git a/internal/defs/static_source.go b/internal/defs/static_source.go index eaf1cf42..dd5adbac 100644 --- a/internal/defs/static_source.go +++ b/internal/defs/static_source.go @@ -23,7 +23,8 @@ type StaticSourceParent interface { // StaticSourceRunParams is the set of params passed to Run(). type StaticSourceRunParams struct { - Context context.Context - Conf *conf.Path - ReloadConf chan *conf.Path + Context context.Context + ResolvedSource string + Conf *conf.Path + ReloadConf chan *conf.Path } diff --git a/internal/staticsources/hls/source.go b/internal/staticsources/hls/source.go index 4e81d42f..7e333f72 100644 --- a/internal/staticsources/hls/source.go +++ b/internal/staticsources/hls/source.go @@ -20,9 +20,8 @@ import ( // Source is a HLS static source. type Source struct { - ResolvedSource string - ReadTimeout conf.StringDuration - Parent defs.StaticSourceParent + ReadTimeout conf.StringDuration + Parent defs.StaticSourceParent } // Log implements logger.Writer. @@ -49,7 +48,7 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error { var c *gohlslib.Client c = &gohlslib.Client{ - URI: s.ResolvedSource, + URI: params.ResolvedSource, HTTPClient: &http.Client{ Timeout: time.Duration(s.ReadTimeout), Transport: tr, diff --git a/internal/staticsources/hls/source_test.go b/internal/staticsources/hls/source_test.go index bd38bede..6da53776 100644 --- a/internal/staticsources/hls/source_test.go +++ b/internal/staticsources/hls/source_test.go @@ -90,10 +90,10 @@ func TestSource(t *testing.T) { te := test.NewSourceTester( func(p defs.StaticSourceParent) defs.StaticSource { return &Source{ - ResolvedSource: "http://localhost:5780/stream.m3u8", - Parent: p, + Parent: p, } }, + "http://localhost:5780/stream.m3u8", &conf.Path{}, ) defer te.Close() diff --git a/internal/staticsources/rtmp/source.go b/internal/staticsources/rtmp/source.go index 8ad22667..91a62100 100644 --- a/internal/staticsources/rtmp/source.go +++ b/internal/staticsources/rtmp/source.go @@ -23,10 +23,9 @@ import ( // Source is a RTMP static source. type Source struct { - ResolvedSource string - ReadTimeout conf.StringDuration - WriteTimeout conf.StringDuration - Parent defs.StaticSourceParent + ReadTimeout conf.StringDuration + WriteTimeout conf.StringDuration + Parent defs.StaticSourceParent } // Log implements logger.Writer. @@ -38,7 +37,7 @@ func (s *Source) Log(level logger.Level, format string, args ...interface{}) { func (s *Source) Run(params defs.StaticSourceRunParams) error { s.Log(logger.Debug, "connecting") - u, err := url.Parse(s.ResolvedSource) + u, err := url.Parse(params.ResolvedSource) if err != nil { return err } diff --git a/internal/staticsources/rtmp/source_test.go b/internal/staticsources/rtmp/source_test.go index 925a157f..2258ee33 100644 --- a/internal/staticsources/rtmp/source_test.go +++ b/internal/staticsources/rtmp/source_test.go @@ -64,24 +64,24 @@ func TestSource(t *testing.T) { te = test.NewSourceTester( func(p defs.StaticSourceParent) defs.StaticSource { return &Source{ - ResolvedSource: "rtmp://localhost/teststream", - ReadTimeout: conf.StringDuration(10 * time.Second), - WriteTimeout: conf.StringDuration(10 * time.Second), - Parent: p, + ReadTimeout: conf.StringDuration(10 * time.Second), + WriteTimeout: conf.StringDuration(10 * time.Second), + Parent: p, } }, + "rtmp://localhost/teststream", &conf.Path{}, ) } else { te = test.NewSourceTester( func(p defs.StaticSourceParent) defs.StaticSource { return &Source{ - ResolvedSource: "rtmps://localhost/teststream", - ReadTimeout: conf.StringDuration(10 * time.Second), - WriteTimeout: conf.StringDuration(10 * time.Second), - Parent: p, + ReadTimeout: conf.StringDuration(10 * time.Second), + WriteTimeout: conf.StringDuration(10 * time.Second), + Parent: p, } }, + "rtmps://localhost/teststream", &conf.Path{ SourceFingerprint: "33949E05FFFB5FF3E8AA16F8213A6251B4D9363804BA53233C4DA9A46D6F2739", }, diff --git a/internal/staticsources/rtsp/source.go b/internal/staticsources/rtsp/source.go index be603149..e7a2d8ab 100644 --- a/internal/staticsources/rtsp/source.go +++ b/internal/staticsources/rtsp/source.go @@ -62,7 +62,6 @@ func createRangeHeader(cnf *conf.Path) (*headers.Range, error) { // Source is a RTSP static source. type Source struct { - ResolvedSource string ReadTimeout conf.StringDuration WriteTimeout conf.StringDuration WriteQueueSize int @@ -104,7 +103,7 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error { }, } - u, err := base.ParseURL(s.ResolvedSource) + u, err := base.ParseURL(params.ResolvedSource) if err != nil { return err } diff --git a/internal/staticsources/rtsp/source_test.go b/internal/staticsources/rtsp/source_test.go index 91b5ae7e..10494e28 100644 --- a/internal/staticsources/rtsp/source_test.go +++ b/internal/staticsources/rtsp/source_test.go @@ -138,13 +138,13 @@ func TestSource(t *testing.T) { te = test.NewSourceTester( func(p defs.StaticSourceParent) defs.StaticSource { return &Source{ - ResolvedSource: "rtsp://testuser:testpass@localhost:8555/teststream", ReadTimeout: conf.StringDuration(10 * time.Second), WriteTimeout: conf.StringDuration(10 * time.Second), WriteQueueSize: 2048, Parent: p, } }, + "rtsp://testuser:testpass@localhost:8555/teststream", &conf.Path{ RTSPTransport: sp, }, @@ -153,13 +153,13 @@ func TestSource(t *testing.T) { te = test.NewSourceTester( func(p defs.StaticSourceParent) defs.StaticSource { return &Source{ - ResolvedSource: "rtsps://testuser:testpass@localhost:8555/teststream", ReadTimeout: conf.StringDuration(10 * time.Second), WriteTimeout: conf.StringDuration(10 * time.Second), WriteQueueSize: 2048, Parent: p, } }, + "rtsps://testuser:testpass@localhost:8555/teststream", &conf.Path{ SourceFingerprint: "33949E05FFFB5FF3E8AA16F8213A6251B4D9363804BA53233C4DA9A46D6F2739", }, @@ -241,13 +241,13 @@ func TestRTSPSourceNoPassword(t *testing.T) { te := test.NewSourceTester( func(p defs.StaticSourceParent) defs.StaticSource { return &Source{ - ResolvedSource: "rtsp://testuser:@127.0.0.1:8555/teststream", ReadTimeout: conf.StringDuration(10 * time.Second), WriteTimeout: conf.StringDuration(10 * time.Second), WriteQueueSize: 2048, Parent: p, } }, + "rtsp://testuser:@127.0.0.1:8555/teststream", &conf.Path{ RTSPTransport: sp, }, @@ -338,13 +338,13 @@ func TestRTSPSourceRange(t *testing.T) { te := test.NewSourceTester( func(p defs.StaticSourceParent) defs.StaticSource { return &Source{ - ResolvedSource: "rtsp://127.0.0.1:8555/teststream", ReadTimeout: conf.StringDuration(10 * time.Second), WriteTimeout: conf.StringDuration(10 * time.Second), WriteQueueSize: 2048, Parent: p, } }, + "rtsp://127.0.0.1:8555/teststream", cnf, ) defer te.Close() diff --git a/internal/staticsources/srt/source.go b/internal/staticsources/srt/source.go index 8026ed3e..69e65c3f 100644 --- a/internal/staticsources/srt/source.go +++ b/internal/staticsources/srt/source.go @@ -17,9 +17,8 @@ import ( // Source is a SRT static source. type Source struct { - ResolvedSource string - ReadTimeout conf.StringDuration - Parent defs.StaticSourceParent + ReadTimeout conf.StringDuration + Parent defs.StaticSourceParent } // Log implements logger.Writer. @@ -32,7 +31,7 @@ func (s *Source) Run(params defs.StaticSourceRunParams) error { s.Log(logger.Debug, "connecting") conf := srt.DefaultConfig() - address, err := conf.UnmarshalURL(s.ResolvedSource) + address, err := conf.UnmarshalURL(params.ResolvedSource) if err != nil { return err } diff --git a/internal/staticsources/srt/source_test.go b/internal/staticsources/srt/source_test.go index 6243a8e1..f4257ec7 100644 --- a/internal/staticsources/srt/source_test.go +++ b/internal/staticsources/srt/source_test.go @@ -55,11 +55,11 @@ func TestSource(t *testing.T) { te := test.NewSourceTester( func(p defs.StaticSourceParent) defs.StaticSource { return &Source{ - ResolvedSource: "srt://127.0.0.1:9002?streamid=sidname&passphrase=ttest1234567", - ReadTimeout: conf.StringDuration(10 * time.Second), - Parent: p, + ReadTimeout: conf.StringDuration(10 * time.Second), + Parent: p, } }, + "srt://127.0.0.1:9002?streamid=sidname&passphrase=ttest1234567", &conf.Path{}, ) defer te.Close() diff --git a/internal/staticsources/udp/source.go b/internal/staticsources/udp/source.go index 1202b2f1..0d899f32 100644 --- a/internal/staticsources/udp/source.go +++ b/internal/staticsources/udp/source.go @@ -45,9 +45,8 @@ type packetConn interface { // Source is a UDP static source. type Source struct { - ResolvedSource string - ReadTimeout conf.StringDuration - Parent defs.StaticSourceParent + ReadTimeout conf.StringDuration + Parent defs.StaticSourceParent } // Log implements logger.Writer. @@ -59,7 +58,7 @@ func (s *Source) Log(level logger.Level, format string, args ...interface{}) { func (s *Source) Run(params defs.StaticSourceRunParams) error { s.Log(logger.Debug, "connecting") - hostPort := s.ResolvedSource[len("udp://"):] + hostPort := params.ResolvedSource[len("udp://"):] addr, err := net.ResolveUDPAddr("udp", hostPort) if err != nil { diff --git a/internal/staticsources/udp/source_test.go b/internal/staticsources/udp/source_test.go index 65fb759a..fa64dc7e 100644 --- a/internal/staticsources/udp/source_test.go +++ b/internal/staticsources/udp/source_test.go @@ -18,11 +18,11 @@ func TestSource(t *testing.T) { te := test.NewSourceTester( func(p defs.StaticSourceParent) defs.StaticSource { return &Source{ - ResolvedSource: "udp://127.0.0.1:9001", - ReadTimeout: conf.StringDuration(10 * time.Second), - Parent: p, + ReadTimeout: conf.StringDuration(10 * time.Second), + Parent: p, } }, + "udp://127.0.0.1:9001", &conf.Path{}, ) defer te.Close() diff --git a/internal/staticsources/webrtc/source.go b/internal/staticsources/webrtc/source.go index 3fab2a77..f2004a7d 100644 --- a/internal/staticsources/webrtc/source.go +++ b/internal/staticsources/webrtc/source.go @@ -19,9 +19,8 @@ import ( // Source is a WebRTC static source. type Source struct { - ResolvedSource string - ReadTimeout conf.StringDuration - Parent defs.StaticSourceParent + ReadTimeout conf.StringDuration + Parent defs.StaticSourceParent } // Log implements logger.Writer. @@ -33,7 +32,7 @@ func (s *Source) Log(level logger.Level, format string, args ...interface{}) { func (s *Source) Run(params defs.StaticSourceRunParams) error { s.Log(logger.Debug, "connecting") - u, err := url.Parse(s.ResolvedSource) + u, err := url.Parse(params.ResolvedSource) if err != nil { return err } diff --git a/internal/staticsources/webrtc/source_test.go b/internal/staticsources/webrtc/source_test.go index 20edf3e7..c8b2842f 100644 --- a/internal/staticsources/webrtc/source_test.go +++ b/internal/staticsources/webrtc/source_test.go @@ -121,11 +121,11 @@ func TestSource(t *testing.T) { te := test.NewSourceTester( func(p defs.StaticSourceParent) defs.StaticSource { return &Source{ - ResolvedSource: "whep://localhost:9003/my/resource", - ReadTimeout: conf.StringDuration(10 * time.Second), - Parent: p, + ReadTimeout: conf.StringDuration(10 * time.Second), + Parent: p, } }, + "whep://localhost:9003/my/resource", &conf.Path{}, ) defer te.Close() diff --git a/internal/test/source_tester.go b/internal/test/source_tester.go index 7a52501a..92d6e4d3 100644 --- a/internal/test/source_tester.go +++ b/internal/test/source_tester.go @@ -24,7 +24,11 @@ type SourceTester struct { } // NewSourceTester allocates a SourceTester. -func NewSourceTester(createFunc func(defs.StaticSourceParent) defs.StaticSource, conf *conf.Path) *SourceTester { +func NewSourceTester( + createFunc func(defs.StaticSourceParent) defs.StaticSource, + resolvedSource string, + conf *conf.Path, +) *SourceTester { ctx, ctxCancel := context.WithCancel(context.Background()) t := &SourceTester{ @@ -38,8 +42,9 @@ func NewSourceTester(createFunc func(defs.StaticSourceParent) defs.StaticSource, go func() { s.Run(defs.StaticSourceRunParams{ //nolint:errcheck - Context: ctx, - Conf: conf, + Context: ctx, + ResolvedSource: resolvedSource, + Conf: conf, }) close(t.done) }() diff --git a/mediamtx.yml b/mediamtx.yml index dbc451eb..f936a305 100644 --- a/mediamtx.yml +++ b/mediamtx.yml @@ -418,8 +418,10 @@ pathDefaults: # * wheps://existing-url -> the stream is pulled from another WebRTC server / camera with HTTPS # * redirect -> the stream is provided by another path or server # * rpiCamera -> the stream is provided by a Raspberry Pi Camera - # If path name is a regular expression, $G1, G2, etc will be replaced - # with regular expression groups. + # The following variables can be used in the source string: + # * $MTX_QUERY: query parameters (passed by first reader) + # * $G1, $G2, ...: regular expression groups, if path name is + # a regular expression. source: publisher # If the source is a URL, and the source certificate is self-signed # or invalid, you can provide the fingerprint of the certificate in order to From a1dc9f45f5e2a03084274bea139ecaa2e805d659 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Wed, 19 Jun 2024 21:02:08 +0200 Subject: [PATCH 38/52] webrtc: support publishing H265 tracks (#3435) (#3492) IMPORTANT NOTE: this doesn't allow to read H265 tracks with WebRTC, just to publish them. The inability to read H265 tracks with WebRTC is not in any way related to the server but depends on browsers and on the fact that they are not legally entitled to embed a H265 decoder inside them. --- README.md | 4 +- internal/protocols/webrtc/incoming_track.go | 45 ++++++++++++++----- internal/protocols/webrtc/outgoing_track.go | 10 +++++ .../protocols/webrtc/peer_connection_test.go | 11 +++++ internal/protocols/webrtc/tracks_to_medias.go | 16 +------ 5 files changed, 58 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 25ec0c46..3e09e429 100644 --- a/README.md +++ b/README.md @@ -22,8 +22,8 @@ Live streams can be published to the server with: |--------|--------|------------|------------| |[SRT clients](#srt-clients)||H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3| |[SRT cameras and servers](#srt-cameras-and-servers)||H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3| -|[WebRTC clients](#webrtc-clients)|Browser-based, WHIP|AV1, VP9, VP8, H264|Opus, G722, G711 (PCMA, PCMU)| -|[WebRTC servers](#webrtc-servers)|WHEP|AV1, VP9, VP8, H264|Opus, G722, G711 (PCMA, PCMU)| +|[WebRTC clients](#webrtc-clients)|Browser-based, WHIP|AV1, VP9, VP8, H265, H264|Opus, G722, G711 (PCMA, PCMU)| +|[WebRTC servers](#webrtc-servers)|WHEP|AV1, VP9, VP8, H265, H264|Opus, G722, G711 (PCMA, PCMU)| |[RTSP clients](#rtsp-clients)|UDP, TCP, RTSPS|AV1, VP9, VP8, H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video, M-JPEG and any RTP-compatible codec|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, G726, G722, G711 (PCMA, PCMU), LPCM and any RTP-compatible codec| |[RTSP cameras and servers](#rtsp-cameras-and-servers)|UDP, UDP-Multicast, TCP, RTSPS|AV1, VP9, VP8, H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video, M-JPEG and any RTP-compatible codec|Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, G726, G722, G711 (PCMA, PCMU), LPCM and any RTP-compatible codec| |[RTMP clients](#rtmp-clients)|RTMP, RTMPS, Enhanced RTMP|AV1, VP9, H265, H264|MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), G711 (PCMA, PCMU), LPCM| diff --git a/internal/protocols/webrtc/incoming_track.go b/internal/protocols/webrtc/incoming_track.go index 96944031..71016e96 100644 --- a/internal/protocols/webrtc/incoming_track.go +++ b/internal/protocols/webrtc/incoming_track.go @@ -5,6 +5,7 @@ import ( "strings" "time" + "github.com/bluenviron/gortsplib/v4/pkg/description" "github.com/bluenviron/gortsplib/v4/pkg/format" "github.com/bluenviron/gortsplib/v4/pkg/liberrors" "github.com/bluenviron/gortsplib/v4/pkg/rtpreorderer" @@ -81,9 +82,8 @@ var incomingVideoCodecs = []webrtc.RTPCodecParameters{ }, { RTPCodecCapability: webrtc.RTPCodecCapability{ - MimeType: webrtc.MimeTypeH264, - ClockRate: 90000, - SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f", + MimeType: webrtc.MimeTypeH265, + ClockRate: 90000, }, PayloadType: 103, }, @@ -91,10 +91,18 @@ var incomingVideoCodecs = []webrtc.RTPCodecParameters{ RTPCodecCapability: webrtc.RTPCodecCapability{ MimeType: webrtc.MimeTypeH264, ClockRate: 90000, - SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", + SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f", }, PayloadType: 104, }, + { + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: webrtc.MimeTypeH264, + ClockRate: 90000, + SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", + }, + PayloadType: 105, + }, } var incomingAudioCodecs = []webrtc.RTPCodecParameters{ @@ -229,6 +237,7 @@ type IncomingTrack struct { track *webrtc.TrackRemote log logger.Writer + typ description.MediaType format format.Format reorderer *rtpreorderer.Reorderer pkts []*rtp.Packet @@ -246,41 +255,47 @@ func newIncomingTrack( reorderer: rtpreorderer.New(), } - isVideo := false - switch strings.ToLower(track.Codec().MimeType) { case strings.ToLower(webrtc.MimeTypeAV1): - isVideo = true + t.typ = description.MediaTypeVideo t.format = &format.AV1{ PayloadTyp: uint8(track.PayloadType()), } case strings.ToLower(webrtc.MimeTypeVP9): - isVideo = true + t.typ = description.MediaTypeVideo t.format = &format.VP9{ PayloadTyp: uint8(track.PayloadType()), } case strings.ToLower(webrtc.MimeTypeVP8): - isVideo = true + t.typ = description.MediaTypeVideo t.format = &format.VP8{ PayloadTyp: uint8(track.PayloadType()), } + case strings.ToLower(webrtc.MimeTypeH265): + t.typ = description.MediaTypeVideo + t.format = &format.H265{ + PayloadTyp: uint8(track.PayloadType()), + } + case strings.ToLower(webrtc.MimeTypeH264): - isVideo = true + t.typ = description.MediaTypeVideo t.format = &format.H264{ PayloadTyp: uint8(track.PayloadType()), PacketizationMode: 1, } case strings.ToLower(mimeTypeMultiopus): + t.typ = description.MediaTypeAudio t.format = &format.Opus{ PayloadTyp: uint8(track.PayloadType()), ChannelCount: int(track.Codec().Channels), } case strings.ToLower(webrtc.MimeTypeOpus): + t.typ = description.MediaTypeAudio t.format = &format.Opus{ PayloadTyp: uint8(track.PayloadType()), ChannelCount: func() int { @@ -292,9 +307,12 @@ func newIncomingTrack( } case strings.ToLower(webrtc.MimeTypeG722): + t.typ = description.MediaTypeAudio t.format = &format.G722{} case strings.ToLower(webrtc.MimeTypePCMU): + t.typ = description.MediaTypeAudio + channels := track.Codec().Channels if channels == 0 { channels = 1 @@ -313,6 +331,8 @@ func newIncomingTrack( } case strings.ToLower(webrtc.MimeTypePCMA): + t.typ = description.MediaTypeAudio + channels := track.Codec().Channels if channels == 0 { channels = 1 @@ -331,6 +351,7 @@ func newIncomingTrack( } case strings.ToLower(mimeTypeL16): + t.typ = description.MediaTypeAudio t.format = &format.LPCM{ PayloadTyp: uint8(track.PayloadType()), BitDepth: 16, @@ -339,7 +360,7 @@ func newIncomingTrack( } default: - return nil, fmt.Errorf("unsupported codec: %+v", track.Codec()) + return nil, fmt.Errorf("unsupported codec: %+v", track.Codec().RTPCodecCapability) } // read incoming RTCP packets to make interceptors work @@ -354,7 +375,7 @@ func newIncomingTrack( }() // send period key frame requests - if isVideo { + if t.typ == description.MediaTypeVideo { go func() { keyframeTicker := time.NewTicker(keyFrameInterval) defer keyframeTicker.Stop() diff --git a/internal/protocols/webrtc/outgoing_track.go b/internal/protocols/webrtc/outgoing_track.go index 3f72c1cc..adc60e51 100644 --- a/internal/protocols/webrtc/outgoing_track.go +++ b/internal/protocols/webrtc/outgoing_track.go @@ -54,6 +54,15 @@ func (t *OutgoingTrack) codecParameters() (webrtc.RTPCodecParameters, error) { PayloadType: 96, }, nil + case *format.H265: + return webrtc.RTPCodecParameters{ + RTPCodecCapability: webrtc.RTPCodecCapability{ + MimeType: webrtc.MimeTypeH265, + ClockRate: 90000, + }, + PayloadType: 96, + }, nil + case *format.H264: return webrtc.RTPCodecParameters{ RTPCodecCapability: webrtc.RTPCodecCapability{ @@ -205,6 +214,7 @@ func (t *OutgoingTrack) isVideo() bool { case *format.AV1, *format.VP9, *format.VP8, + *format.H265, *format.H264: return true } diff --git a/internal/protocols/webrtc/peer_connection_test.go b/internal/protocols/webrtc/peer_connection_test.go index 1927786f..60454133 100644 --- a/internal/protocols/webrtc/peer_connection_test.go +++ b/internal/protocols/webrtc/peer_connection_test.go @@ -83,6 +83,17 @@ func TestPeerConnectionPublishRead(t *testing.T) { PayloadTyp: 96, }, }, + { + "h265", + test.FormatH265, + webrtc.RTPCodecCapability{ + MimeType: "video/H265", + ClockRate: 90000, + }, + &format.H265{ + PayloadTyp: 96, + }, + }, { "h264", test.FormatH264, diff --git a/internal/protocols/webrtc/tracks_to_medias.go b/internal/protocols/webrtc/tracks_to_medias.go index 809eeec4..81ea0850 100644 --- a/internal/protocols/webrtc/tracks_to_medias.go +++ b/internal/protocols/webrtc/tracks_to_medias.go @@ -10,21 +10,9 @@ func TracksToMedias(tracks []*IncomingTrack) []*description.Media { ret := make([]*description.Media, len(tracks)) for i, track := range tracks { - forma := track.Format() - - var mediaType description.MediaType - - switch forma.(type) { - case *format.AV1, *format.VP9, *format.VP8, *format.H264: - mediaType = description.MediaTypeVideo - - default: - mediaType = description.MediaTypeAudio - } - ret[i] = &description.Media{ - Type: mediaType, - Formats: []format.Format{forma}, + Type: track.typ, + Formats: []format.Format{track.format}, } } From afec0cd86cd07375284cdac9c330743e2d58f667 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Mon, 1 Jul 2024 16:03:12 +0200 Subject: [PATCH 39/52] rtmp: fix publishing from DJI FlightHub Sync (#3301) (#3504) --- internal/protocols/rtmp/reader.go | 6 ++- internal/protocols/rtmp/reader_test.go | 58 ++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/internal/protocols/rtmp/reader.go b/internal/protocols/rtmp/reader.go index 846a9e01..b111780b 100644 --- a/internal/protocols/rtmp/reader.go +++ b/internal/protocols/rtmp/reader.go @@ -182,7 +182,7 @@ func tracksFromMetadata(conn *Conn, payload []interface{}) (format.Format, forma } if !hasVideo && !hasAudio { - return nil, nil, fmt.Errorf("metadata doesn't contain any track") + return nil, nil, nil } firstReceived := false @@ -523,7 +523,9 @@ func (r *Reader) readTracks() (format.Format, format.Format, error) { return nil, nil, err } - return videoTrack, audioTrack, nil + if videoTrack != nil || audioTrack != nil { + return videoTrack, audioTrack, nil + } } } } diff --git a/internal/protocols/rtmp/reader_test.go b/internal/protocols/rtmp/reader_test.go index 41d1d213..f5a2e9a5 100644 --- a/internal/protocols/rtmp/reader_test.go +++ b/internal/protocols/rtmp/reader_test.go @@ -244,6 +244,64 @@ func TestReadTracks(t *testing.T) { return buf }(), }, + &message.Audio{ + ChunkStreamID: message.AudioChunkStreamID, + MessageStreamID: 0x1000000, + Codec: message.CodecMPEG4Audio, + Rate: message.Rate44100, + Depth: message.Depth16, + IsStereo: true, + AACType: message.AudioAACTypeConfig, + Payload: func() []byte { + enc, err2 := mpeg4audio.Config{ + Type: 2, + SampleRate: 44100, + ChannelCount: 2, + }.Marshal() + require.NoError(t, err2) + return enc + }(), + }, + }, + }, + { + "h264 + aac, issue mediamtx/3301 (metadata without tracks)", + &format.H264{ + PayloadTyp: 96, + SPS: test.FormatH264.SPS, + PPS: test.FormatH264.PPS, + PacketizationMode: 1, + }, + &format.MPEG4Audio{ + PayloadTyp: 96, + Config: &mpeg4audio.Config{ + Type: 2, + SampleRate: 44100, + ChannelCount: 2, + }, + SizeLength: 13, + IndexLength: 3, + IndexDeltaLength: 3, + }, + []message.Message{ + &message.DataAMF0{ + ChunkStreamID: 4, + MessageStreamID: 1, + Payload: []interface{}{ + "@setDataFrame", + "onMetaData", + amf0.Object{ + { + Key: "metadatacreator", + Value: "Agora.io SDK", + }, + { + Key: "encoder", + Value: "Agora.io Encoder", + }, + }, + }, + }, &message.Video{ ChunkStreamID: message.VideoChunkStreamID, MessageStreamID: 0x1000000, From c2d60e5a9bdaa8e862f53e3f1bec380b8d4ad16b Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 4 Jul 2024 20:07:59 +0200 Subject: [PATCH 40/52] build(deps): bump golang.org/x/sys from 0.21.0 to 0.22.0 (#3530) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 3a8f780c..9c949e12 100644 --- a/go.mod +++ b/go.mod @@ -29,7 +29,7 @@ require ( github.com/pion/webrtc/v3 v3.2.22 github.com/stretchr/testify v1.9.0 golang.org/x/crypto v0.24.0 - golang.org/x/sys v0.21.0 + golang.org/x/sys v0.22.0 golang.org/x/term v0.21.0 gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index d057a177..8b0b86e8 100644 --- a/go.sum +++ b/go.sum @@ -262,8 +262,8 @@ golang.org/x/sys v0.9.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 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/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= From 3c2d1032342d0328f2cf3444c27eaa4b01a66b11 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Jul 2024 19:28:57 +0200 Subject: [PATCH 41/52] build(deps): bump golang.org/x/crypto from 0.24.0 to 0.25.0 (#3532) Bumps [golang.org/x/crypto](https://github.com/golang/crypto) from 0.24.0 to 0.25.0. - [Commits](https://github.com/golang/crypto/compare/v0.24.0...v0.25.0) --- updated-dependencies: - dependency-name: golang.org/x/crypto dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 9c949e12..964c9309 100644 --- a/go.mod +++ b/go.mod @@ -28,9 +28,9 @@ require ( github.com/pion/sdp/v3 v3.0.9 github.com/pion/webrtc/v3 v3.2.22 github.com/stretchr/testify v1.9.0 - golang.org/x/crypto v0.24.0 + golang.org/x/crypto v0.25.0 golang.org/x/sys v0.22.0 - golang.org/x/term v0.21.0 + golang.org/x/term v0.22.0 gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index 8b0b86e8..94d74fe0 100644 --- a/go.sum +++ b/go.sum @@ -210,8 +210,8 @@ golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.25.0 h1:ypSNr+bnYL2YhwoMt2zPxHFmbAN1KZs/njMG3hxUp30= +golang.org/x/crypto v0.25.0/go.mod h1:T+wALwcMOSE0kXgUAnPAHqTLW+XHgcELELW8VaDgm/M= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -273,8 +273,8 @@ golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= 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= From c4987d020aeb983d5d2c9e36c727394622f8fb2f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 5 Jul 2024 19:29:20 +0200 Subject: [PATCH 42/52] build(deps): bump golang.org/x/term from 0.21.0 to 0.22.0 (#3533) Bumps [golang.org/x/term](https://github.com/golang/term) from 0.21.0 to 0.22.0. - [Commits](https://github.com/golang/term/compare/v0.21.0...v0.22.0) --- updated-dependencies: - dependency-name: golang.org/x/term dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> From 342c257df5189200d7fc789828a1022739c2edef Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Fri, 5 Jul 2024 22:17:40 +0200 Subject: [PATCH 43/52] srt: process connection requests in parallel (#3382) (#3534) --- go.mod | 2 + go.sum | 4 +- internal/servers/srt/conn.go | 106 ++++++---------------- internal/servers/srt/listener.go | 17 +--- internal/servers/srt/server.go | 27 ++---- internal/staticsources/srt/source_test.go | 17 ++-- 6 files changed, 50 insertions(+), 123 deletions(-) diff --git a/go.mod b/go.mod index 964c9309..ebf4fad5 100644 --- a/go.mod +++ b/go.mod @@ -82,3 +82,5 @@ replace code.cloudfoundry.org/bytefmt => github.com/cloudfoundry/bytefmt v0.0.0- replace github.com/pion/ice/v2 => github.com/aler9/ice/v2 v2.0.0-20240608212222-2eebc68350c9 replace github.com/pion/webrtc/v3 => github.com/aler9/webrtc/v3 v3.0.0-20240610104456-eaec24056d06 + +replace github.com/datarhei/gosrt => github.com/aler9/gosrt v0.0.0-20240705192040-d4bc5eaa3ee7 diff --git a/go.sum b/go.sum index 94d74fe0..786bbb70 100644 --- a/go.sum +++ b/go.sum @@ -10,6 +10,8 @@ github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= +github.com/aler9/gosrt v0.0.0-20240705192040-d4bc5eaa3ee7 h1:4WE1Nez3YyD1CgJfWlnyp+uLLPZOKD5ywWPvwbf/Jp4= +github.com/aler9/gosrt v0.0.0-20240705192040-d4bc5eaa3ee7/go.mod h1:fsOWdLSHUHShHjgi/46h6wjtdQrtnSdAQFnlas8ONxs= github.com/aler9/ice/v2 v2.0.0-20240608212222-2eebc68350c9 h1:Vax9SzYE68ZYLwFaK7lnCV2ZhX9/YqAJX6xxROPRqEM= github.com/aler9/ice/v2 v2.0.0-20240608212222-2eebc68350c9/go.mod h1:KXJJcZK7E8WzrBEYnV4UtqEZsGeWfHxsNqhVcVvgjxw= github.com/aler9/webrtc/v3 v3.0.0-20240610104456-eaec24056d06 h1:WtKhXOpd8lgTeXF3RQVOzkNRuy83ygvWEpMYD2aoY3Q= @@ -37,8 +39,6 @@ github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJ github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/datarhei/gosrt v0.6.0 h1:HrrXAw90V78ok4WMIhX6se1aTHPCn82Sg2hj+PhdmGc= -github.com/datarhei/gosrt v0.6.0/go.mod h1:fsOWdLSHUHShHjgi/46h6wjtdQrtnSdAQFnlas8ONxs= 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= diff --git a/internal/servers/srt/conn.go b/internal/servers/srt/conn.go index 971fb5af..01bfeecd 100644 --- a/internal/servers/srt/conn.go +++ b/internal/servers/srt/conn.go @@ -74,9 +74,6 @@ type conn struct { pathName string query string sconn srt.Conn - - chNew chan srtNewConnReq - chSetConn chan srt.Conn } func (c *conn) initialize() { @@ -84,8 +81,6 @@ func (c *conn) initialize() { c.created = time.Now() c.uuid = uuid.New() - c.chNew = make(chan srtNewConnReq) - c.chSetConn = make(chan srt.Conn) c.Log(logger.Info, "opened") @@ -130,36 +125,20 @@ func (c *conn) run() { //nolint:dupl } func (c *conn) runInner() error { - var req srtNewConnReq - select { - case req = <-c.chNew: - case <-c.ctx.Done(): - return errors.New("terminated") - } - - answerSent, err := c.runInner2(req) - - if !answerSent { - req.res <- nil - } - - return err -} - -func (c *conn) runInner2(req srtNewConnReq) (bool, error) { var streamID streamID - err := streamID.unmarshal(req.connReq.StreamId()) + err := streamID.unmarshal(c.connReq.StreamId()) if err != nil { - return false, fmt.Errorf("invalid stream ID '%s': %w", req.connReq.StreamId(), err) + c.connReq.Reject(srt.REJ_PEER) + return fmt.Errorf("invalid stream ID '%s': %w", c.connReq.StreamId(), err) } if streamID.mode == streamIDModePublish { - return c.runPublish(req, &streamID) + return c.runPublish(&streamID) } - return c.runRead(req, &streamID) + return c.runRead(&streamID) } -func (c *conn) runPublish(req srtNewConnReq, streamID *streamID) (bool, error) { +func (c *conn) runPublish(streamID *streamID) error { path, err := c.pathManager.AddPublisher(defs.PathAddPublisherReq{ Author: c, AccessRequest: defs.PathAccessRequest{ @@ -178,21 +157,24 @@ func (c *conn) runPublish(req srtNewConnReq, streamID *streamID) (bool, error) { if errors.As(err, &terr) { // wait some seconds to mitigate brute force attacks <-time.After(auth.PauseAfterError) - return false, terr + c.connReq.Reject(srt.REJ_PEER) + return terr } - return false, err + c.connReq.Reject(srt.REJ_PEER) + return err } defer path.RemovePublisher(defs.PathRemovePublisherReq{Author: c}) - err = srtCheckPassphrase(req.connReq, path.SafeConf().SRTPublishPassphrase) + err = srtCheckPassphrase(c.connReq, path.SafeConf().SRTPublishPassphrase) if err != nil { - return false, err + c.connReq.Reject(srt.REJ_PEER) + return err } - sconn, err := c.exchangeRequestWithConn(req) + sconn, err := c.connReq.Accept() if err != nil { - return true, err + return err } c.mutex.Lock() @@ -210,12 +192,12 @@ func (c *conn) runPublish(req srtNewConnReq, streamID *streamID) (bool, error) { select { case err := <-readerErr: sconn.Close() - return true, err + return err case <-c.ctx.Done(): sconn.Close() <-readerErr - return true, errors.New("terminated") + return errors.New("terminated") } } @@ -256,7 +238,7 @@ func (c *conn) runPublishReader(sconn srt.Conn, path defs.Path) error { } } -func (c *conn) runRead(req srtNewConnReq, streamID *streamID) (bool, error) { +func (c *conn) runRead(streamID *streamID) error { path, stream, err := c.pathManager.AddReader(defs.PathAddReaderReq{ Author: c, AccessRequest: defs.PathAccessRequest{ @@ -274,21 +256,24 @@ func (c *conn) runRead(req srtNewConnReq, streamID *streamID) (bool, error) { if errors.As(err, &terr) { // wait some seconds to mitigate brute force attacks <-time.After(auth.PauseAfterError) - return false, err + c.connReq.Reject(srt.REJ_PEER) + return terr } - return false, err + c.connReq.Reject(srt.REJ_PEER) + return err } defer path.RemoveReader(defs.PathRemoveReaderReq{Author: c}) - err = srtCheckPassphrase(req.connReq, path.SafeConf().SRTReadPassphrase) + err = srtCheckPassphrase(c.connReq, path.SafeConf().SRTReadPassphrase) if err != nil { - return false, err + c.connReq.Reject(srt.REJ_PEER) + return err } - sconn, err := c.exchangeRequestWithConn(req) + sconn, err := c.connReq.Accept() if err != nil { - return true, err + return err } defer sconn.Close() @@ -307,7 +292,7 @@ func (c *conn) runRead(req srtNewConnReq, streamID *streamID) (bool, error) { err = mpegts.FromStream(stream, writer, bw, sconn, time.Duration(c.writeTimeout)) if err != nil { - return true, err + return err } c.Log(logger.Info, "is reading from path '%s', %s", @@ -331,41 +316,10 @@ func (c *conn) runRead(req srtNewConnReq, streamID *streamID) (bool, error) { select { case <-c.ctx.Done(): - return true, fmt.Errorf("terminated") + return fmt.Errorf("terminated") case err := <-writer.Error(): - return true, err - } -} - -func (c *conn) exchangeRequestWithConn(req srtNewConnReq) (srt.Conn, error) { - req.res <- c - - select { - case sconn := <-c.chSetConn: - return sconn, nil - - case <-c.ctx.Done(): - return nil, errors.New("terminated") - } -} - -// new is called by srtListener through srtServer. -func (c *conn) new(req srtNewConnReq) *conn { - select { - case c.chNew <- req: - return <-req.res - - case <-c.ctx.Done(): - return nil - } -} - -// setConn is called by srtListener . -func (c *conn) setConn(sconn srt.Conn) { - select { - case c.chSetConn <- sconn: - case <-c.ctx.Done(): + return err } } diff --git a/internal/servers/srt/listener.go b/internal/servers/srt/listener.go index 38b9e297..000ba895 100644 --- a/internal/servers/srt/listener.go +++ b/internal/servers/srt/listener.go @@ -27,24 +27,11 @@ func (l *listener) run() { func (l *listener) runInner() error { for { - var sconn *conn - conn, _, err := l.ln.Accept(func(req srt.ConnRequest) srt.ConnType { - sconn = l.parent.newConnRequest(req) - if sconn == nil { - return srt.REJECT - } - - // currently it's the same to return SUBSCRIBE or PUBLISH - return srt.SUBSCRIBE - }) + req, err := l.ln.Accept2() if err != nil { return err } - if conn == nil { - continue - } - - sconn.setConn(conn) + l.parent.newConnRequest(req) } } diff --git a/internal/servers/srt/server.go b/internal/servers/srt/server.go index 2c1977b8..33b26f29 100644 --- a/internal/servers/srt/server.go +++ b/internal/servers/srt/server.go @@ -26,11 +26,6 @@ func srtMaxPayloadSize(u int) int { return ((u - 16) / 188) * 188 // 16 = SRT header, 188 = MPEG-TS packet } -type srtNewConnReq struct { - connReq srt.ConnRequest - res chan *conn -} - type serverAPIConnsListRes struct { data *defs.APISRTConnList err error @@ -90,7 +85,7 @@ type Server struct { conns map[*conn]struct{} // in - chNewConnRequest chan srtNewConnReq + chNewConnRequest chan srt.ConnRequest chAcceptErr chan error chCloseConn chan *conn chAPIConnsList chan serverAPIConnsListReq @@ -113,7 +108,7 @@ func (s *Server) Initialize() error { s.ctx, s.ctxCancel = context.WithCancel(context.Background()) s.conns = make(map[*conn]struct{}) - s.chNewConnRequest = make(chan srtNewConnReq) + s.chNewConnRequest = make(chan srt.ConnRequest) s.chAcceptErr = make(chan error) s.chCloseConn = make(chan *conn) s.chAPIConnsList = make(chan serverAPIConnsListReq) @@ -165,7 +160,7 @@ outer: writeTimeout: s.WriteTimeout, writeQueueSize: s.WriteQueueSize, udpMaxPayloadSize: s.UDPMaxPayloadSize, - connReq: req.connReq, + connReq: req, runOnConnect: s.RunOnConnect, runOnConnectRestart: s.RunOnConnectRestart, runOnDisconnect: s.RunOnDisconnect, @@ -176,7 +171,6 @@ outer: } c.initialize() s.conns[c] = struct{}{} - req.res <- c case c := <-s.chCloseConn: delete(s.conns, c) @@ -236,20 +230,11 @@ func (s *Server) findConnByUUID(uuid uuid.UUID) *conn { } // newConnRequest is called by srtListener. -func (s *Server) newConnRequest(connReq srt.ConnRequest) *conn { - req := srtNewConnReq{ - connReq: connReq, - res: make(chan *conn), - } - +func (s *Server) newConnRequest(connReq srt.ConnRequest) { select { - case s.chNewConnRequest <- req: - c := <-req.res - - return c.new(req) - + case s.chNewConnRequest <- connReq: case <-s.ctx.Done(): - return nil + connReq.Reject(srt.REJ_CLOSE) } } diff --git a/internal/staticsources/srt/source_test.go b/internal/staticsources/srt/source_test.go index f4257ec7..230aeeb3 100644 --- a/internal/staticsources/srt/source_test.go +++ b/internal/staticsources/srt/source_test.go @@ -20,16 +20,15 @@ func TestSource(t *testing.T) { defer ln.Close() go func() { - conn, _, err := ln.Accept(func(req srt.ConnRequest) srt.ConnType { - require.Equal(t, "sidname", req.StreamId()) - err := req.SetPassphrase("ttest1234567") - if err != nil { - return srt.REJECT - } - return srt.SUBSCRIBE - }) + req, err := ln.Accept2() + require.NoError(t, err) + + require.Equal(t, "sidname", req.StreamId()) + err = req.SetPassphrase("ttest1234567") + require.NoError(t, err) + + conn, err := req.Accept() require.NoError(t, err) - require.NotNil(t, conn) defer conn.Close() track := &mpegts.Track{ From 3f1d182d2c6ad18ec711940bebb8bf6d60de61cb Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Sat, 6 Jul 2024 21:45:15 +0200 Subject: [PATCH 44/52] fix support for HTTP preflight requests (#1792) (#3535) --- internal/api/api.go | 9 +++++ internal/api/api_test.go | 37 +++++++++++++++++++ internal/metrics/metrics.go | 9 +++++ internal/metrics/metrics_test.go | 49 ++++++++++++++++++++++++++ internal/playback/server.go | 9 +++++ internal/playback/server_test.go | 48 +++++++++++++++++++++++++ internal/pprof/pprof.go | 9 +++++ internal/pprof/pprof_test.go | 48 +++++++++++++++++++++++++ internal/servers/hls/http_server.go | 8 +++-- internal/servers/hls/server_test.go | 37 +++++++++++++++++++ internal/servers/webrtc/server_test.go | 20 +++++++---- 11 files changed, 273 insertions(+), 10 deletions(-) create mode 100644 internal/metrics/metrics_test.go create mode 100644 internal/playback/server_test.go create mode 100644 internal/pprof/pprof_test.go diff --git a/internal/api/api.go b/internal/api/api.go index 072ce0a5..c0324624 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -251,6 +251,15 @@ func (a *API) writeError(ctx *gin.Context, status int, err error) { func (a *API) middlewareOrigin(ctx *gin.Context) { ctx.Writer.Header().Set("Access-Control-Allow-Origin", a.AllowOrigin) ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true") + + // preflight requests + if ctx.Request.Method == http.MethodOptions && + ctx.Request.Header.Get("Access-Control-Request-Method") != "" { + ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PATCH, DELETE") + ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type") + ctx.AbortWithStatus(http.StatusNoContent) + return + } } func (a *API) middlewareAuth(ctx *gin.Context) { diff --git a/internal/api/api_test.go b/internal/api/api_test.go index 9cddc257..a2a6a5ec 100644 --- a/internal/api/api_test.go +++ b/internal/api/api_test.go @@ -74,6 +74,43 @@ func checkError(t *testing.T, msg string, body io.Reader) { require.Equal(t, map[string]interface{}{"error": msg}, resErr) } +func TestPreflightRequest(t *testing.T) { + api := API{ + Address: "localhost:9997", + AllowOrigin: "*", + ReadTimeout: conf.StringDuration(10 * time.Second), + AuthManager: test.NilAuthManager, + Parent: &testParent{}, + } + err := api.Initialize() + require.NoError(t, err) + defer api.Close() + + tr := &http.Transport{} + defer tr.CloseIdleConnections() + hc := &http.Client{Transport: tr} + + req, err := http.NewRequest(http.MethodOptions, "http://localhost:9997", nil) + require.NoError(t, err) + + req.Header.Add("Access-Control-Request-Method", "GET") + + res, err := hc.Do(req) + require.NoError(t, err) + defer res.Body.Close() + + require.Equal(t, http.StatusNoContent, res.StatusCode) + + byts, err := io.ReadAll(res.Body) + require.NoError(t, err) + + require.Equal(t, "*", res.Header.Get("Access-Control-Allow-Origin")) + require.Equal(t, "true", res.Header.Get("Access-Control-Allow-Credentials")) + require.Equal(t, "OPTIONS, GET, POST, PATCH, DELETE", res.Header.Get("Access-Control-Allow-Methods")) + require.Equal(t, "Authorization, Content-Type", res.Header.Get("Access-Control-Allow-Headers")) + require.Equal(t, byts, []byte{}) +} + func TestConfigAuth(t *testing.T) { cnf := tempConf(t, "api: yes\n") diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go index 96c1a645..11a5af72 100644 --- a/internal/metrics/metrics.go +++ b/internal/metrics/metrics.go @@ -107,6 +107,15 @@ func (m *Metrics) onRequest(ctx *gin.Context) { ctx.Writer.Header().Set("Access-Control-Allow-Origin", m.AllowOrigin) ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true") + // preflight requests + if ctx.Request.Method == http.MethodOptions && + ctx.Request.Header.Get("Access-Control-Request-Method") != "" { + ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET") + ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization") + ctx.Writer.WriteHeader(http.StatusNoContent) + return + } + if ctx.Request.URL.Path != "/metrics" || ctx.Request.Method != http.MethodGet { return } diff --git a/internal/metrics/metrics_test.go b/internal/metrics/metrics_test.go new file mode 100644 index 00000000..7bd3099d --- /dev/null +++ b/internal/metrics/metrics_test.go @@ -0,0 +1,49 @@ +package metrics + +import ( + "io" + "net/http" + "testing" + "time" + + "github.com/bluenviron/mediamtx/internal/conf" + "github.com/bluenviron/mediamtx/internal/test" + "github.com/stretchr/testify/require" +) + +func TestPreflightRequest(t *testing.T) { + api := Metrics{ + Address: "localhost:9998", + AllowOrigin: "*", + ReadTimeout: conf.StringDuration(10 * time.Second), + AuthManager: test.NilAuthManager, + Parent: test.NilLogger, + } + err := api.Initialize() + require.NoError(t, err) + defer api.Close() + + tr := &http.Transport{} + defer tr.CloseIdleConnections() + hc := &http.Client{Transport: tr} + + req, err := http.NewRequest(http.MethodOptions, "http://localhost:9998", nil) + require.NoError(t, err) + + req.Header.Add("Access-Control-Request-Method", "GET") + + res, err := hc.Do(req) + require.NoError(t, err) + defer res.Body.Close() + + require.Equal(t, http.StatusNoContent, res.StatusCode) + + byts, err := io.ReadAll(res.Body) + require.NoError(t, err) + + require.Equal(t, "*", res.Header.Get("Access-Control-Allow-Origin")) + require.Equal(t, "true", res.Header.Get("Access-Control-Allow-Credentials")) + require.Equal(t, "OPTIONS, GET", res.Header.Get("Access-Control-Allow-Methods")) + require.Equal(t, "Authorization", res.Header.Get("Access-Control-Allow-Headers")) + require.Equal(t, byts, []byte{}) +} diff --git a/internal/playback/server.go b/internal/playback/server.go index abea96e7..52086169 100644 --- a/internal/playback/server.go +++ b/internal/playback/server.go @@ -109,6 +109,15 @@ func (s *Server) safeFindPathConf(name string) (*conf.Path, error) { func (s *Server) middlewareOrigin(ctx *gin.Context) { ctx.Writer.Header().Set("Access-Control-Allow-Origin", s.AllowOrigin) ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true") + + // preflight requests + if ctx.Request.Method == http.MethodOptions && + ctx.Request.Header.Get("Access-Control-Request-Method") != "" { + ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET") + ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization") + ctx.AbortWithStatus(http.StatusNoContent) + return + } } func (s *Server) doAuth(ctx *gin.Context, pathName string) bool { diff --git a/internal/playback/server_test.go b/internal/playback/server_test.go new file mode 100644 index 00000000..9fb905c6 --- /dev/null +++ b/internal/playback/server_test.go @@ -0,0 +1,48 @@ +package playback + +import ( + "io" + "net/http" + "testing" + "time" + + "github.com/bluenviron/mediamtx/internal/conf" + "github.com/bluenviron/mediamtx/internal/test" + "github.com/stretchr/testify/require" +) + +func TestPreflightRequest(t *testing.T) { + s := &Server{ + Address: "127.0.0.1:9996", + AllowOrigin: "*", + ReadTimeout: conf.StringDuration(10 * time.Second), + Parent: test.NilLogger, + } + err := s.Initialize() + require.NoError(t, err) + defer s.Close() + + tr := &http.Transport{} + defer tr.CloseIdleConnections() + hc := &http.Client{Transport: tr} + + req, err := http.NewRequest(http.MethodOptions, "http://localhost:9996", nil) + require.NoError(t, err) + + req.Header.Add("Access-Control-Request-Method", "GET") + + res, err := hc.Do(req) + require.NoError(t, err) + defer res.Body.Close() + + require.Equal(t, http.StatusNoContent, res.StatusCode) + + byts, err := io.ReadAll(res.Body) + require.NoError(t, err) + + require.Equal(t, "*", res.Header.Get("Access-Control-Allow-Origin")) + require.Equal(t, "true", res.Header.Get("Access-Control-Allow-Credentials")) + require.Equal(t, "OPTIONS, GET", res.Header.Get("Access-Control-Allow-Methods")) + require.Equal(t, "Authorization", res.Header.Get("Access-Control-Allow-Headers")) + require.Equal(t, byts, []byte{}) +} diff --git a/internal/pprof/pprof.go b/internal/pprof/pprof.go index ff6f2dc2..2cb0164f 100644 --- a/internal/pprof/pprof.go +++ b/internal/pprof/pprof.go @@ -83,6 +83,15 @@ func (pp *PPROF) onRequest(ctx *gin.Context) { ctx.Writer.Header().Set("Access-Control-Allow-Origin", pp.AllowOrigin) ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true") + // preflight requests + if ctx.Request.Method == http.MethodOptions && + ctx.Request.Header.Get("Access-Control-Request-Method") != "" { + ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET") + ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization") + ctx.Writer.WriteHeader(http.StatusNoContent) + return + } + user, pass, hasCredentials := ctx.Request.BasicAuth() err := pp.AuthManager.Authenticate(&auth.Request{ diff --git a/internal/pprof/pprof_test.go b/internal/pprof/pprof_test.go new file mode 100644 index 00000000..d26de965 --- /dev/null +++ b/internal/pprof/pprof_test.go @@ -0,0 +1,48 @@ +package pprof + +import ( + "io" + "net/http" + "testing" + "time" + + "github.com/bluenviron/mediamtx/internal/conf" + "github.com/bluenviron/mediamtx/internal/test" + "github.com/stretchr/testify/require" +) + +func TestPreflightRequest(t *testing.T) { + s := &PPROF{ + Address: "127.0.0.1:9999", + AllowOrigin: "*", + ReadTimeout: conf.StringDuration(10 * time.Second), + Parent: test.NilLogger, + } + err := s.Initialize() + require.NoError(t, err) + defer s.Close() + + tr := &http.Transport{} + defer tr.CloseIdleConnections() + hc := &http.Client{Transport: tr} + + req, err := http.NewRequest(http.MethodOptions, "http://localhost:9999", nil) + require.NoError(t, err) + + req.Header.Add("Access-Control-Request-Method", "GET") + + res, err := hc.Do(req) + require.NoError(t, err) + defer res.Body.Close() + + require.Equal(t, http.StatusNoContent, res.StatusCode) + + byts, err := io.ReadAll(res.Body) + require.NoError(t, err) + + require.Equal(t, "*", res.Header.Get("Access-Control-Allow-Origin")) + require.Equal(t, "true", res.Header.Get("Access-Control-Allow-Credentials")) + require.Equal(t, "OPTIONS, GET", res.Header.Get("Access-Control-Allow-Methods")) + require.Equal(t, "Authorization", res.Header.Get("Access-Control-Allow-Headers")) + require.Equal(t, byts, []byte{}) +} diff --git a/internal/servers/hls/http_server.go b/internal/servers/hls/http_server.go index 752b5c2a..882e3443 100644 --- a/internal/servers/hls/http_server.go +++ b/internal/servers/hls/http_server.go @@ -102,9 +102,11 @@ func (s *httpServer) onRequest(ctx *gin.Context) { switch ctx.Request.Method { case http.MethodOptions: - ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET") - ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization, Range") - ctx.Writer.WriteHeader(http.StatusNoContent) + if ctx.Request.Header.Get("Access-Control-Request-Method") != "" { + ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET") + ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization, Range") + ctx.Writer.WriteHeader(http.StatusNoContent) + } return case http.MethodGet: diff --git a/internal/servers/hls/server_test.go b/internal/servers/hls/server_test.go index 76ef901d..b014702b 100644 --- a/internal/servers/hls/server_test.go +++ b/internal/servers/hls/server_test.go @@ -2,6 +2,7 @@ package hls import ( "fmt" + "io" "net/http" "os" "path/filepath" @@ -60,6 +61,42 @@ func (pm *dummyPathManager) AddReader(req defs.PathAddReaderReq) (defs.Path, *st return pm.addReader(req) } +func TestPreflightRequest(t *testing.T) { + s := &Server{ + Address: "127.0.0.1:8888", + AllowOrigin: "*", + ReadTimeout: conf.StringDuration(10 * time.Second), + Parent: test.NilLogger, + } + err := s.Initialize() + require.NoError(t, err) + defer s.Close() + + tr := &http.Transport{} + defer tr.CloseIdleConnections() + hc := &http.Client{Transport: tr} + + req, err := http.NewRequest(http.MethodOptions, "http://localhost:8888", nil) + require.NoError(t, err) + + req.Header.Add("Access-Control-Request-Method", "GET") + + res, err := hc.Do(req) + require.NoError(t, err) + defer res.Body.Close() + + require.Equal(t, http.StatusNoContent, res.StatusCode) + + byts, err := io.ReadAll(res.Body) + require.NoError(t, err) + + require.Equal(t, "*", res.Header.Get("Access-Control-Allow-Origin")) + require.Equal(t, "true", res.Header.Get("Access-Control-Allow-Credentials")) + require.Equal(t, "OPTIONS, GET", res.Header.Get("Access-Control-Allow-Methods")) + require.Equal(t, "Authorization, Range", res.Header.Get("Access-Control-Allow-Headers")) + require.Equal(t, byts, []byte{}) +} + func TestServerNotFound(t *testing.T) { for _, ca := range []string{ "always remux off", diff --git a/internal/servers/webrtc/server_test.go b/internal/servers/webrtc/server_test.go index 0fcbf124..6a05433c 100644 --- a/internal/servers/webrtc/server_test.go +++ b/internal/servers/webrtc/server_test.go @@ -3,6 +3,7 @@ package webrtc import ( "bytes" "context" + "io" "net/http" "net/url" "reflect" @@ -102,7 +103,7 @@ func initializeTestServer(t *testing.T) *Server { Encryption: false, ServerKey: "", ServerCert: "", - AllowOrigin: "", + AllowOrigin: "*", TrustedProxies: conf.IPNetworks{}, ReadTimeout: conf.StringDuration(10 * time.Second), WriteQueueSize: 512, @@ -146,7 +147,7 @@ func TestServerStaticPages(t *testing.T) { } } -func TestServerOptionsPreflight(t *testing.T) { +func TestPreflightRequest(t *testing.T) { s := initializeTestServer(t) defer s.Close() @@ -154,11 +155,10 @@ func TestServerOptionsPreflight(t *testing.T) { defer tr.CloseIdleConnections() hc := &http.Client{Transport: tr} - // preflight requests must always work, without authentication - req, err := http.NewRequest(http.MethodOptions, "http://localhost:8886/teststream/whip", nil) + req, err := http.NewRequest(http.MethodOptions, "http://localhost:8886", nil) require.NoError(t, err) - req.Header.Set("Access-Control-Request-Method", "OPTIONS") + req.Header.Add("Access-Control-Request-Method", "GET") res, err := hc.Do(req) require.NoError(t, err) @@ -166,8 +166,14 @@ func TestServerOptionsPreflight(t *testing.T) { require.Equal(t, http.StatusNoContent, res.StatusCode) - _, ok := res.Header["Link"] - require.Equal(t, false, ok) + byts, err := io.ReadAll(res.Body) + require.NoError(t, err) + + require.Equal(t, "*", res.Header.Get("Access-Control-Allow-Origin")) + require.Equal(t, "true", res.Header.Get("Access-Control-Allow-Credentials")) + require.Equal(t, "OPTIONS, GET, POST, PATCH, DELETE", res.Header.Get("Access-Control-Allow-Methods")) + require.Equal(t, "Authorization, Content-Type, If-Match", res.Header.Get("Access-Control-Allow-Headers")) + require.Equal(t, byts, []byte{}) } func TestServerOptionsICEServer(t *testing.T) { From 1a4fd9cfca27c1feec8a336e8a0a617f0b01c003 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Sun, 7 Jul 2024 19:51:43 +0200 Subject: [PATCH 45/52] bump dependencies (#3537) --- go.mod | 6 +++--- go.sum | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/go.mod b/go.mod index ebf4fad5..c451c8fc 100644 --- a/go.mod +++ b/go.mod @@ -9,8 +9,8 @@ require ( github.com/abema/go-mp4 v1.2.0 github.com/alecthomas/kong v0.9.0 github.com/bluenviron/gohlslib v1.4.0 - github.com/bluenviron/gortsplib/v4 v4.10.1 - github.com/bluenviron/mediacommon v1.12.0 + github.com/bluenviron/gortsplib/v4 v4.10.2 + github.com/bluenviron/mediacommon v1.12.1 github.com/datarhei/gosrt v0.6.0 github.com/fsnotify/fsnotify v1.7.0 github.com/gin-gonic/gin v1.10.0 @@ -70,7 +70,7 @@ require ( github.com/ugorji/go/codec v1.2.12 // indirect github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 // indirect golang.org/x/arch v0.8.0 // indirect - golang.org/x/net v0.26.0 // indirect + golang.org/x/net v0.27.0 // indirect golang.org/x/text v0.16.0 // indirect golang.org/x/time v0.5.0 // indirect google.golang.org/protobuf v1.34.1 // indirect diff --git a/go.sum b/go.sum index 786bbb70..b6e32951 100644 --- a/go.sum +++ b/go.sum @@ -24,10 +24,10 @@ github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c h1:8XZeJrs4+ZYh github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c/go.mod h1:x1vxHcL/9AVzuk5HOloOEPrtJY0MaalYr78afXZ+pWI= github.com/bluenviron/gohlslib v1.4.0 h1:3a9W1x8eqlxJUKt1sJCunPGtti5ALIY2ik4GU0RVe7E= github.com/bluenviron/gohlslib v1.4.0/go.mod h1:q5ZElzNw5GRbV1VEI45qkcPbKBco6BP58QEY5HyFsmo= -github.com/bluenviron/gortsplib/v4 v4.10.1 h1:v+X5HcNOEiUurK16Y30sl/UjqCDodx4aywvoSsFS49A= -github.com/bluenviron/gortsplib/v4 v4.10.1/go.mod h1:ElIedl4To6FQpxjgGnbf4NK/je57JqZMO2EAndIWX4o= -github.com/bluenviron/mediacommon v1.12.0 h1:j6L3Ikn+dyJvvG3rbm0gjbsUJ11fqh5nIlhNgYAjEx8= -github.com/bluenviron/mediacommon v1.12.0/go.mod h1:HDyW2CzjvhYJXtdxstdFPio3G0qSocPhqkhUt/qffec= +github.com/bluenviron/gortsplib/v4 v4.10.2 h1:O7HPRG8Pv4zUbyYD0HYH4Ufu1Hg9FJGTlizx6a09hL0= +github.com/bluenviron/gortsplib/v4 v4.10.2/go.mod h1:re/L/vYh2wLPElQNAYah+bRFHJs0aRkM1MLX3WJ3N6M= +github.com/bluenviron/mediacommon v1.12.1 h1:sgDJaKV6OXrPCSO0KPp9zi/pwNWtKHenn5/dvjtY+Tg= +github.com/bluenviron/mediacommon v1.12.1/go.mod h1:HDyW2CzjvhYJXtdxstdFPio3G0qSocPhqkhUt/qffec= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= @@ -230,8 +230,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.13.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= -golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= -golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.27.0 h1:5K3Njcw06/l2y9vpGCSdcxWOYHOUk3dVNGDXN+FvAys= +golang.org/x/net v0.27.0/go.mod h1:dDi0PyhWNoiUOrAS8uXv/vnScO4wnHQO4mj9fn/RytE= 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= From c3265a554b9fee511abee4b901047b3a4045bea0 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Mon, 8 Jul 2024 18:06:51 +0200 Subject: [PATCH 46/52] fix hls.js update workflow (#3538) --- .github/workflows/bump_hls_js.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/bump_hls_js.yml b/.github/workflows/bump_hls_js.yml index 234f3891..10b14d15 100644 --- a/.github/workflows/bump_hls_js.yml +++ b/.github/workflows/bump_hls_js.yml @@ -29,16 +29,16 @@ jobs: - id: check_repo run: > - echo "clean=$(git status --porcelain)" >> "$GITHUB_OUTPUT" + test -n "$(git status --porcelain)" && echo "update=1" >> "$GITHUB_OUTPUT" || echo "update=0" >> "$GITHUB_OUTPUT" - - if: ${{ steps.check_repo.outputs.clean != '' }} + - if: ${{ steps.check_repo.outputs.update == '1' }} run: > git reset ${GITHUB_REF_NAME} && git add . && git commit -m "bump hls.js to ${VERSION}" && git push --set-upstream origin deps/hlsjs --force - - if: ${{ steps.check_repo.outputs.clean != '' }} + - if: ${{ steps.check_repo.outputs.update == '1' }} uses: actions/github-script@v6 with: github-token: ${{ secrets.GITHUB_TOKEN }} From 976f2252a54f9252ee24d6d0773edbd82d385dc7 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 8 Jul 2024 18:08:49 +0200 Subject: [PATCH 47/52] bump hls.js to v1.5.13 (#3540) Co-authored-by: mediamtx-bot --- internal/servers/hls/hlsjsdownloader/HASH | 2 +- internal/servers/hls/hlsjsdownloader/VERSION | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/servers/hls/hlsjsdownloader/HASH b/internal/servers/hls/hlsjsdownloader/HASH index 15a7082a..894fba52 100644 --- a/internal/servers/hls/hlsjsdownloader/HASH +++ b/internal/servers/hls/hlsjsdownloader/HASH @@ -1 +1 @@ -869ea17a6ddb2cf483ca8c692fc6c7ba80de0882105ba300027af2edaed1b902 +c5ef2cf356b103bf7a19dd4d14257c9e00163551ed03bbf96bf22a12458a1250 diff --git a/internal/servers/hls/hlsjsdownloader/VERSION b/internal/servers/hls/hlsjsdownloader/VERSION index bbb1b251..77fb9c77 100644 --- a/internal/servers/hls/hlsjsdownloader/VERSION +++ b/internal/servers/hls/hlsjsdownloader/VERSION @@ -1 +1 @@ -v1.5.11 +v1.5.13 From ed15f9dde5838e7c57ac2c7286ab0694ef4b8b8b Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Thu, 11 Jul 2024 13:33:03 +0200 Subject: [PATCH 48/52] deps: restore link to datarhei/gosrt (#3546) --- go.mod | 4 +--- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index c451c8fc..6fcf01e0 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/bluenviron/gohlslib v1.4.0 github.com/bluenviron/gortsplib/v4 v4.10.2 github.com/bluenviron/mediacommon v1.12.1 - github.com/datarhei/gosrt v0.6.0 + github.com/datarhei/gosrt v0.0.0-20240708145230-390712a1b3f7 github.com/fsnotify/fsnotify v1.7.0 github.com/gin-gonic/gin v1.10.0 github.com/golang-jwt/jwt/v5 v5.2.1 @@ -82,5 +82,3 @@ replace code.cloudfoundry.org/bytefmt => github.com/cloudfoundry/bytefmt v0.0.0- replace github.com/pion/ice/v2 => github.com/aler9/ice/v2 v2.0.0-20240608212222-2eebc68350c9 replace github.com/pion/webrtc/v3 => github.com/aler9/webrtc/v3 v3.0.0-20240610104456-eaec24056d06 - -replace github.com/datarhei/gosrt => github.com/aler9/gosrt v0.0.0-20240705192040-d4bc5eaa3ee7 diff --git a/go.sum b/go.sum index b6e32951..bd4b15b5 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,6 @@ github.com/alecthomas/kong v0.9.0 h1:G5diXxc85KvoV2f0ZRVuMsi45IrBgx9zDNGNj165aPA github.com/alecthomas/kong v0.9.0/go.mod h1:Y47y5gKfHp1hDc7CH7OeXgLIpp+Q2m1Ni0L5s3bI8Os= github.com/alecthomas/repr v0.4.0 h1:GhI2A8MACjfegCPVq9f1FLvIBS+DrQ2KQBFZP1iFzXc= github.com/alecthomas/repr v0.4.0/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4= -github.com/aler9/gosrt v0.0.0-20240705192040-d4bc5eaa3ee7 h1:4WE1Nez3YyD1CgJfWlnyp+uLLPZOKD5ywWPvwbf/Jp4= -github.com/aler9/gosrt v0.0.0-20240705192040-d4bc5eaa3ee7/go.mod h1:fsOWdLSHUHShHjgi/46h6wjtdQrtnSdAQFnlas8ONxs= github.com/aler9/ice/v2 v2.0.0-20240608212222-2eebc68350c9 h1:Vax9SzYE68ZYLwFaK7lnCV2ZhX9/YqAJX6xxROPRqEM= github.com/aler9/ice/v2 v2.0.0-20240608212222-2eebc68350c9/go.mod h1:KXJJcZK7E8WzrBEYnV4UtqEZsGeWfHxsNqhVcVvgjxw= github.com/aler9/webrtc/v3 v3.0.0-20240610104456-eaec24056d06 h1:WtKhXOpd8lgTeXF3RQVOzkNRuy83ygvWEpMYD2aoY3Q= @@ -39,6 +37,8 @@ github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJ github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/datarhei/gosrt v0.0.0-20240708145230-390712a1b3f7 h1:qtdoyyziqrLTcnkhAat384733VF/ly2/OQA0M9qs9lA= +github.com/datarhei/gosrt v0.0.0-20240708145230-390712a1b3f7/go.mod h1:wTDoyog1z4au8Fd/QJBQAndzvccuxjqUL/qMm0EyJxE= 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= From 3152388c1f5a8edd5b218a1ac5a2a67cecb180d3 Mon Sep 17 00:00:00 2001 From: Alessandro Ros Date: Fri, 12 Jul 2024 13:56:44 +0200 Subject: [PATCH 49/52] fix gosrt import (#3549) --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 6fcf01e0..92d94fe8 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/bluenviron/gohlslib v1.4.0 github.com/bluenviron/gortsplib/v4 v4.10.2 github.com/bluenviron/mediacommon v1.12.1 - github.com/datarhei/gosrt v0.0.0-20240708145230-390712a1b3f7 + github.com/datarhei/gosrt v0.6.1-0.20240708145230-390712a1b3f7 github.com/fsnotify/fsnotify v1.7.0 github.com/gin-gonic/gin v1.10.0 github.com/golang-jwt/jwt/v5 v5.2.1 diff --git a/go.sum b/go.sum index bd4b15b5..6d9a22d0 100644 --- a/go.sum +++ b/go.sum @@ -37,8 +37,8 @@ github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJ github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/datarhei/gosrt v0.0.0-20240708145230-390712a1b3f7 h1:qtdoyyziqrLTcnkhAat384733VF/ly2/OQA0M9qs9lA= -github.com/datarhei/gosrt v0.0.0-20240708145230-390712a1b3f7/go.mod h1:wTDoyog1z4au8Fd/QJBQAndzvccuxjqUL/qMm0EyJxE= +github.com/datarhei/gosrt v0.6.1-0.20240708145230-390712a1b3f7 h1:Tyvgum9NHQi/iDoYUQhuxjUnu/s4tJXNdYCeUZma5Z0= +github.com/datarhei/gosrt v0.6.1-0.20240708145230-390712a1b3f7/go.mod h1:wTDoyog1z4au8Fd/QJBQAndzvccuxjqUL/qMm0EyJxE= 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= From da5420a788d16c40278d36fce46deb6d42fc4db5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 17 Jul 2024 21:29:04 +0200 Subject: [PATCH 50/52] build(deps): bump github.com/datarhei/gosrt (#3559) Bumps [github.com/datarhei/gosrt](https://github.com/datarhei/gosrt) from 0.6.1-0.20240708145230-390712a1b3f7 to 0.7.0. - [Commits](https://github.com/datarhei/gosrt/commits/v0.7.0) --- updated-dependencies: - dependency-name: github.com/datarhei/gosrt dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 92d94fe8..1029528d 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/bluenviron/gohlslib v1.4.0 github.com/bluenviron/gortsplib/v4 v4.10.2 github.com/bluenviron/mediacommon v1.12.1 - github.com/datarhei/gosrt v0.6.1-0.20240708145230-390712a1b3f7 + github.com/datarhei/gosrt v0.7.0 github.com/fsnotify/fsnotify v1.7.0 github.com/gin-gonic/gin v1.10.0 github.com/golang-jwt/jwt/v5 v5.2.1 diff --git a/go.sum b/go.sum index 6d9a22d0..42f27977 100644 --- a/go.sum +++ b/go.sum @@ -37,8 +37,8 @@ github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJ github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= -github.com/datarhei/gosrt v0.6.1-0.20240708145230-390712a1b3f7 h1:Tyvgum9NHQi/iDoYUQhuxjUnu/s4tJXNdYCeUZma5Z0= -github.com/datarhei/gosrt v0.6.1-0.20240708145230-390712a1b3f7/go.mod h1:wTDoyog1z4au8Fd/QJBQAndzvccuxjqUL/qMm0EyJxE= +github.com/datarhei/gosrt v0.7.0 h1:1/IY66HVVgqGA9zkmL5l6jUFuI8t/76WkuamSkJqHqs= +github.com/datarhei/gosrt v0.7.0/go.mod h1:wTDoyog1z4au8Fd/QJBQAndzvccuxjqUL/qMm0EyJxE= 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= From 09ad8e7160a97a37f30f2456de1ec1069efda0af Mon Sep 17 00:00:00 2001 From: Failure Date: Wed, 24 Jul 2024 16:52:58 -0700 Subject: [PATCH 51/52] import changes --- internal/conf/path.go | 4 ++++ internal/hooks/on_ready.go | 23 +++++++++++++++++++++++ internal/servers/webrtc/read_index.html | 1 + 3 files changed, 28 insertions(+) diff --git a/internal/conf/path.go b/internal/conf/path.go index 925f050f..98018dbd 100644 --- a/internal/conf/path.go +++ b/internal/conf/path.go @@ -180,6 +180,10 @@ type Path struct { RunOnUnread string `json:"runOnUnread"` RunOnRecordSegmentCreate string `json:"runOnRecordSegmentCreate"` RunOnRecordSegmentComplete string `json:"runOnRecordSegmentComplete"` + + // Custom hooks + HTTPOnReady string `json:"httpOnReady"` + HTTPOnNotReady string `json:"httpOnNotReady"` } func (pconf *Path) setDefaults() { diff --git a/internal/hooks/on_ready.go b/internal/hooks/on_ready.go index f6e7e9fd..22c0f8a2 100644 --- a/internal/hooks/on_ready.go +++ b/internal/hooks/on_ready.go @@ -1,10 +1,13 @@ package hooks import ( + "bytes" + "encoding/json" "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/defs" "github.com/bluenviron/mediamtx/internal/externalcmd" "github.com/bluenviron/mediamtx/internal/logger" + "net/http" ) // OnReadyParams are the parameters of OnReady. @@ -41,6 +44,16 @@ func OnReady(params OnReadyParams) func() { }) } + if params.Conf.HTTPOnReady != "" { + obj := map[string]any{ + "query": params.Query, + "desc": params.Desc, + "env": params.ExternalCmdEnv, + } + jsonValue, _ := json.Marshal(obj) + http.Post(params.Conf.HTTPOnReady, "application/json", bytes.NewBuffer(jsonValue)) + } + return func() { if onReadyCmd != nil { onReadyCmd.Close() @@ -56,5 +69,15 @@ func OnReady(params OnReadyParams) func() { env, nil) } + if params.Conf.HTTPOnNotReady != "" { + obj := map[string]any{ + "query": params.Query, + "desc": params.Desc, + "env": params.ExternalCmdEnv, + } + jsonValue, _ := json.Marshal(obj) + http.Post(params.Conf.HTTPOnNotReady, "application/json", bytes.NewBuffer(jsonValue)) + } + } } diff --git a/internal/servers/webrtc/read_index.html b/internal/servers/webrtc/read_index.html index 427073f9..718f1fbc 100644 --- a/internal/servers/webrtc/read_index.html +++ b/internal/servers/webrtc/read_index.html @@ -437,6 +437,7 @@ const sendOffer = (offer) => { return res.text() .then((sdp) => onRemoteAnswer(sdp)); }) + .then((sdp) => onRemoteAnswer(sdp)) .catch((err) => { onError(err.toString()); }); From 6934441a5e32b9c01d2cb141b9f7355705ceed2b Mon Sep 17 00:00:00 2001 From: Failure Date: Wed, 24 Jul 2024 18:04:08 -0700 Subject: [PATCH 52/52] add initial dockerfile --- Dockerfile | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..def91df2 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +## build ergo binary +FROM docker.io/golang:1.22-alpine AS build-env + +RUN apk upgrade -U --force-refresh --no-cache +RUN apk add --no-cache --purge --clean-protected -l -u make git + +# copy ergo source +WORKDIR /go/src/cef.icu/mediamtx +COPY . . + +RUN go generate ./... +RUN CGO_ENABLED=0 go build . +WORKDIR /mediamtx +RUN mkdir conf && cp /go/src/cef.icu/mediamtx/mediamtx . + +EXPOSE 8189/udp 8198/tcp 1935/tcp 8889/tcp 9997/tcp +ENTRYPOINT ["/mediamtx/mediamtx"]