diff --git a/trunk/3rdparty/srs-bench/blackbox/rtsp_test.go b/trunk/3rdparty/srs-bench/blackbox/rtsp_test.go new file mode 100644 index 000000000..62dcdbe1e --- /dev/null +++ b/trunk/3rdparty/srs-bench/blackbox/rtsp_test.go @@ -0,0 +1,119 @@ +// The MIT License (MIT) +// +// # Copyright (c) 2025 Winlin +// +// Permission is hereby granted, free of charge, to any person obtaining a copy of +// this software and associated documentation files (the "Software"), to deal in +// the Software without restriction, including without limitation the rights to +// use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software is furnished to do so, +// subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +// FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +// COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +// IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +// CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +package blackbox + +import ( + "context" + "fmt" + "math/rand" + "os" + "path" + "sync" + "testing" + "time" + + "github.com/ossrs/go-oryx-lib/errors" + "github.com/ossrs/go-oryx-lib/logger" +) + +func TestFast_RtmpPublish_RtspPlay_Basic(t *testing.T) { + // This case is run in parallel. + t.Parallel() + + // Setup the max timeout for this case. + ctx, cancel := context.WithTimeout(logger.WithContext(context.Background()), time.Duration(*srsTimeout)*time.Millisecond) + defer cancel() + + // Check a set of errors. + var r0, r1, r2, r3, r4, r5 error + defer func(ctx context.Context) { + if err := filterTestError(ctx.Err(), r0, r1, r2, r3, r4, r5); err != nil { + t.Errorf("Fail for err %+v", err) + } else { + logger.Tf(ctx, "test done with err %+v", err) + } + }(ctx) + + var wg sync.WaitGroup + defer wg.Wait() + + // Start SRS server and wait for it to be ready. + svr := NewSRSServer(func(v *srsServer) { + v.envs = []string{ + "SRS_RTSP_SERVER_ENABLED=on", + "SRS_VHOST_RTSP_ENABLED=on", + "SRS_VHOST_RTSP_RTMP_TO_RTSP=on", + } + }) + wg.Add(1) + go func() { + defer wg.Done() + r0 = svr.Run(ctx, cancel) + }() + + // Start FFmpeg to publish stream. + streamID := fmt.Sprintf("stream-%v-%v", os.Getpid(), rand.Int()) + streamURL := fmt.Sprintf("rtmp://localhost:%v/live/%v", svr.RTMPPort(), streamID) + ffmpeg := NewFFmpeg(func(v *ffmpegClient) { + v.args = []string{ + "-stream_loop", "-1", "-re", "-i", *srsPublishAvatar, "-c", "copy", "-f", "flv", streamURL, + } + }) + wg.Add(1) + go func() { + defer wg.Done() + <-svr.ReadyCtx().Done() + r1 = ffmpeg.Run(ctx, cancel) + }() + + // Start FFprobe to detect and verify stream. + duration := time.Duration(*srsFFprobeDuration) * time.Millisecond + ffprobe := NewFFprobe(func(v *ffprobeClient) { + v.dvrFile = path.Join(svr.WorkDir(), "objs", fmt.Sprintf("srs-ffprobe-%v.mp4", streamID)) + v.streamURL = fmt.Sprintf("rtsp://localhost:%v/live/%v", svr.RTSPPort(), streamID) + v.duration, v.timeout = duration, time.Duration(*srsFFprobeTimeout)*time.Millisecond + }) + wg.Add(1) + go func() { + defer wg.Done() + <-svr.ReadyCtx().Done() + r2 = ffprobe.Run(ctx, cancel) + }() + + // Fast quit for probe done. + select { + case <-ctx.Done(): + case <-ffprobe.ProbeDoneCtx().Done(): + defer cancel() + + str, m := ffprobe.Result() + if len(m.Streams) != 2 { + r3 = errors.Errorf("invalid streams=%v, %v, %v", len(m.Streams), m.String(), str) + } + + if ts := 90; m.Format.ProbeScore < ts { + r4 = errors.Errorf("low score=%v < %v, %v, %v", m.Format.ProbeScore, ts, m.String(), str) + } + if dv := m.Duration(); dv < duration { + r5 = errors.Errorf("short duration=%v < %v, %v, %v", dv, duration, m.String(), str) + } + } +} diff --git a/trunk/3rdparty/srs-bench/blackbox/util.go b/trunk/3rdparty/srs-bench/blackbox/util.go index 3dd76ae7b..ff9d91424 100644 --- a/trunk/3rdparty/srs-bench/blackbox/util.go +++ b/trunk/3rdparty/srs-bench/blackbox/util.go @@ -422,6 +422,8 @@ type SRSServer interface { APIPort() int // SRTPort is the SRT UDP port. SRTPort() int + // RTSPPort is the RTSP port. + RTSPPort() int } // srsServer is a SRS server instance. @@ -450,6 +452,8 @@ type srsServer struct { httpListen int // SRT UDP server listen port. srtListen int + // RTSP server listen port. + rtspListen int // The envs from user. envs []string @@ -476,6 +480,7 @@ func NewSRSServer(opts ...func(v *srsServer)) SRSServer { v.apiListen = allocator.Allocate() v.httpListen = allocator.Allocate() v.srtListen = allocator.Allocate() + v.rtspListen = allocator.Allocate() // Do cleanup. v.process.onDispose = func(ctx context.Context, bs *backendService) error { @@ -483,7 +488,7 @@ func NewSRSServer(opts ...func(v *srsServer)) SRSServer { allocator.Free(v.apiListen) allocator.Free(v.httpListen) allocator.Free(v.srtListen) - + allocator.Free(v.rtspListen) if _, err := os.Stat(v.workDir); err == nil { os.RemoveAll(v.workDir) } @@ -520,6 +525,10 @@ func (v *srsServer) SRTPort() int { return v.srtListen } +func (v *srsServer) RTSPPort() int { + return v.rtspListen +} + func (v *srsServer) WorkDir() string { return v.workDir } @@ -575,6 +584,8 @@ func (v *srsServer) Run(ctx context.Context, cancel context.CancelFunc) error { fmt.Sprintf("SRS_HTTP_SERVER_LISTEN=%v", v.httpListen), // Setup the SRT server listen port. fmt.Sprintf("SRS_SRT_SERVER_LISTEN=%v", v.srtListen), + // Setup the RTSP server listen port. + fmt.Sprintf("SRS_RTSP_SERVER_LISTEN=%v", v.rtspListen), }...) // Rewrite envs by case. if v.envs != nil { diff --git a/trunk/3rdparty/srs-bench/go.mod b/trunk/3rdparty/srs-bench/go.mod index 176db4483..7ce4f16b7 100644 --- a/trunk/3rdparty/srs-bench/go.mod +++ b/trunk/3rdparty/srs-bench/go.mod @@ -3,6 +3,7 @@ module github.com/ossrs/srs-bench go 1.23.0 require ( + github.com/bluenviron/gortsplib/v4 v4.13.1 github.com/ghettovoice/gosip v0.0.0-20220929080231-de8ba881be83 github.com/google/gopacket v1.1.19 github.com/haivision/srtgo v0.0.0-20230627061225-a70d53fcd618 @@ -21,6 +22,7 @@ require ( ) require ( + github.com/bluenviron/mediacommon/v2 v2.1.0 // indirect github.com/gobwas/httphead v0.1.0 // indirect github.com/gobwas/pool v0.2.1 // indirect github.com/gobwas/ws v1.1.0-rc.1 // indirect diff --git a/trunk/3rdparty/srs-bench/go.sum b/trunk/3rdparty/srs-bench/go.sum index 0872ce64e..f680ef9c1 100644 --- a/trunk/3rdparty/srs-bench/go.sum +++ b/trunk/3rdparty/srs-bench/go.sum @@ -1,3 +1,7 @@ +github.com/bluenviron/gortsplib/v4 v4.13.1 h1:FpkfzLTWgeC4C3ytfNZ9ezes+MA4ZSp8rAvxJP5RnLY= +github.com/bluenviron/gortsplib/v4 v4.13.1/go.mod h1:nJVGKKG8KEkt7KKuckZIXQ1FHevSbvdV7y5UcpLmORw= +github.com/bluenviron/mediacommon/v2 v2.1.0 h1:NtlRCaAo7gnCcO+EHHeFJxSIt+v8uNbvJqlH1Gk2PZM= +github.com/bluenviron/mediacommon/v2 v2.1.0/go.mod h1:iHEz1SFIet6zBwAQoh1a92vTQ3dV3LpVFbom6/SLz3k= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/discoviking/fsm v0.0.0-20150126104936-f4a273feecca/go.mod h1:W+3LQaEkN8qAwwcw0KC546sUEnX86GIT8CcMLZC4mG0= diff --git a/trunk/3rdparty/srs-bench/srs/rtmp_test.go b/trunk/3rdparty/srs-bench/srs/rtmp_test.go index 9dd38da0a..cdf3cae43 100644 --- a/trunk/3rdparty/srs-bench/srs/rtmp_test.go +++ b/trunk/3rdparty/srs-bench/srs/rtmp_test.go @@ -30,11 +30,13 @@ import ( "testing" "time" + "github.com/bluenviron/gortsplib/v4/pkg/description" "github.com/ossrs/go-oryx-lib/avc" "github.com/ossrs/go-oryx-lib/flv" "github.com/ossrs/go-oryx-lib/logger" "github.com/ossrs/go-oryx-lib/rtmp" "github.com/pion/interceptor" + "github.com/pion/rtp" "github.com/pkg/errors" ) @@ -190,6 +192,82 @@ func TestRtmpPublish_RtcPlay_AVC(t *testing.T) { } } +func TestRtmpPublish_RtspPlay(t *testing.T) { + ctx := logger.WithContext(context.Background()) + ctx, cancel := context.WithTimeout(ctx, time.Duration(*srsTimeout)*time.Millisecond) + + var r0, r1 error + err := func() (err error) { + streamSuffix := fmt.Sprintf("rtmp-regression-%v-%v", os.Getpid(), rand.Int()) + rtmpUrl := fmt.Sprintf("rtmp://%v/live/%v", *srsServer, streamSuffix) + + // TODO: 8554 + rtspUrl := fmt.Sprintf("rtsp://%v:8554/live/%v", *srsServer, streamSuffix) + + // Publisher connect to a RTMP stream. + publisher := NewRTMPPublisher() + defer publisher.Close() + + if err := publisher.Publish(ctx, rtmpUrl); err != nil { + return err + } + player := NewRTSPPlayer() + defer player.Close() + + // Run publisher and player + var wg sync.WaitGroup + defer wg.Wait() + + publisherReady, publisherReadyCancel := context.WithCancel(context.Background()) + wg.Add(1) + go func() { + defer wg.Done() + time.Sleep(30 * time.Millisecond) + publisherReadyCancel() + }() + + wg.Add(1) + go func() { + defer wg.Done() + <-publisherReady.Done() + + if err := player.Play(ctx, rtspUrl); err != nil { + r1 = err + cancel() + return + } + + var nnPackets int + player.onRTPPacket = func(media *description.Media, format uint8, packet *rtp.Packet) error { + logger.Tf(ctx, "RTSP: recv rtp packet #%v, payload type=%v, size=%vB", + nnPackets, packet.PayloadType, len(packet.Payload)) + if nnPackets += 1; nnPackets > 50 { + cancel() + } + // TODO: Further validate the RTP packets. + return nil + } + }() + + wg.Add(1) + go func() { + defer wg.Done() + publisher.onSendPacket = func(m *rtmp.Message) error { + time.Sleep(1 * time.Millisecond) + return nil + } + if r0 = publisher.Ingest(ctx, *srsPublishAvatar); r0 != nil { + cancel() + } + }() + + return nil + }() + if err := filterTestError(ctx.Err(), err, r0, r1); err != nil { + t.Errorf("err %+v", err) + } +} + func TestRtmpPublish_MultipleSequences(t *testing.T) { ctx := logger.WithContext(context.Background()) ctx, cancel := context.WithTimeout(ctx, time.Duration(*srsTimeout)*time.Millisecond) @@ -235,7 +313,7 @@ func TestRtmpPublish_MultipleSequences(t *testing.T) { } // Ingore the duplicated sps/pps. - if isAvccrEquals(previousAvccr, avccr) { + if IsAvccrEquals(previousAvccr, avccr) { return nil } previousAvccr = avccr diff --git a/trunk/3rdparty/srs-bench/srs/util.go b/trunk/3rdparty/srs-bench/srs/util.go index eb60286a2..9ce080fca 100644 --- a/trunk/3rdparty/srs-bench/srs/util.go +++ b/trunk/3rdparty/srs-bench/srs/util.go @@ -44,6 +44,10 @@ import ( "sync" "time" + "github.com/bluenviron/gortsplib/v4" + "github.com/bluenviron/gortsplib/v4/pkg/base" + "github.com/bluenviron/gortsplib/v4/pkg/description" + "github.com/bluenviron/gortsplib/v4/pkg/format" "github.com/ossrs/go-oryx-lib/amf0" "github.com/ossrs/go-oryx-lib/avc" "github.com/ossrs/go-oryx-lib/flv" @@ -2220,7 +2224,90 @@ func (v *FLVPlayer) consume(ctx context.Context) (err error) { } } -func isAvccrEquals(a, b *avc.AVCDecoderConfigurationRecord) bool { +// RTSPPlayer +type RTSPPlayer struct { + rtspUrl string + + client *gortsplib.Client + + ctx context.Context + cancel context.CancelFunc + + onRTPPacket func(media *description.Media, format uint8, packet *rtp.Packet) error +} + +func NewRTSPPlayer() *RTSPPlayer { + return &RTSPPlayer{} +} + +func (v *RTSPPlayer) Close() error { + if v.cancel != nil { + v.cancel() + } + if v.client != nil { + v.client.Close() + } + return nil +} + +func (v *RTSPPlayer) Play(ctx context.Context, rtspUrl string) error { + v.rtspUrl = rtspUrl + v.ctx, v.cancel = context.WithCancel(ctx) + + transport := gortsplib.Transport(gortsplib.TransportTCP) + v.client = &gortsplib.Client{ + Transport: &transport, + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, + } + + u, err := base.ParseURL(rtspUrl) + if err != nil { + return errors.Wrapf(err, "parse url %v", rtspUrl) + } + + err = v.client.Start(u.Scheme, u.Host) + if err != nil { + return errors.Wrapf(err, "connect rtsp %v", rtspUrl) + } + + desc, _, err := v.client.Describe(u) + if err != nil { + return errors.Wrapf(err, "describe rtsp %v", rtspUrl) + } + + err = v.client.SetupAll(desc.BaseURL, desc.Medias) + if err != nil { + return errors.Wrapf(err, "setup rtsp %v", rtspUrl) + } + + v.client.OnPacketRTPAny(func(media *description.Media, forma format.Format, packet *rtp.Packet) { + if v.onRTPPacket != nil { + if err := v.onRTPPacket(media, uint8(forma.ClockRate()), packet); err != nil { + logger.Wf(ctx, "rtsp: on rtp error %+v", err) + } + } + }) + + _, err = v.client.Play(nil) + if err != nil { + return errors.Wrapf(err, "play rtsp %v", rtspUrl) + } + + go func() { + err := v.client.Wait() + if err != nil { + logger.Wf(ctx, "rtsp: client error %+v", err) + } + if v.cancel != nil { + v.cancel() + } + }() + + return nil +} + +func IsAvccrEquals(a, b *avc.AVCDecoderConfigurationRecord) bool { if a == nil || b == nil { return false } diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/.dockerignore b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/.dockerignore new file mode 100644 index 000000000..6b8710a71 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/.dockerignore @@ -0,0 +1 @@ +.git diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/.gitignore b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/.gitignore new file mode 100644 index 000000000..1273da773 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/.gitignore @@ -0,0 +1 @@ +/coverage*.txt diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/.golangci.yml b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/.golangci.yml new file mode 100644 index 000000000..4bfbdcf65 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/.golangci.yml @@ -0,0 +1,73 @@ +linters: + enable: + - asciicheck + - bidichk + - bodyclose + #- contextcheck + - copyloopvar + - dupl + - errorlint + - gochecknoinits + - gocritic + - gofmt + - gofumpt + - lll + - misspell + - nilerr + - prealloc + - predeclared + - revive + - usestdlibvars + - unconvert + #- usetesting + - tparallel + - wastedassign + - whitespace + +issues: + exclude-use-default: false + +linters-settings: + errcheck: + exclude-functions: + - io.Copy + - (io.Closer).Close + - (io.Writer).Write + - (hash.Hash).Write + - (net.Conn).Close + - (net.Conn).SetReadDeadline + - (net.Conn).SetWriteDeadline + - (*net.TCPConn).SetKeepAlive + - (*net.TCPConn).SetKeepAlivePeriod + - (*net.TCPConn).SetNoDelay + - (net.Listener).Close + - (net.PacketConn).Close + - (net.PacketConn).SetReadDeadline + - (net.PacketConn).SetWriteDeadline + - (net/http.ResponseWriter).Write + - (*net/http.Server).Serve + - (*net/http.Server).ServeTLS + - (*net/http.Server).Shutdown + - os.Chdir + - os.Mkdir + - os.MkdirAll + - os.Remove + - os.RemoveAll + - os.Setenv + - os.Unsetenv + - (*os.File).WriteString + - (*os.File).Close + - (github.com/datarhei/gosrt.Conn).Close + - (github.com/datarhei/gosrt.Conn).SetReadDeadline + - (github.com/datarhei/gosrt.Conn).SetWriteDeadline + - (*github.com/bluenviron/gortsplib/v4.Client).Close + - (*github.com/bluenviron/gortsplib/v4.Server).Close + - (*github.com/bluenviron/gortsplib/v4.ServerSession).Close + - (*github.com/bluenviron/gortsplib/v4.ServerStream).Close + - (*github.com/bluenviron/gortsplib/v4.ServerConn).Close + + govet: + enable-all: true + disable: + - fieldalignment + - reflectvaluecompare diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/LICENSE b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/LICENSE new file mode 100644 index 000000000..023966dad --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 aler9 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/Makefile b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/Makefile new file mode 100644 index 000000000..112928df5 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/Makefile @@ -0,0 +1,26 @@ +BASE_IMAGE = golang:1.24-alpine3.20 +LINT_IMAGE = golangci/golangci-lint:v1.64.5 + +.PHONY: $(shell ls) + +help: + @echo "usage: make [action]" + @echo "" + @echo "available actions:" + @echo "" + @echo " mod-tidy run go mod tidy" + @echo " format format source files" + @echo " test run tests" + @echo " test-32 run tests on a 32-bit system" + @echo " test-e2e run end-to-end tests" + @echo " lint run linter" + @echo " bench run benchmarks" + @echo "" + +blank := +define NL + +$(blank) +endef + +include scripts/*.mk diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/README.md b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/README.md new file mode 100644 index 000000000..70d6730b5 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/README.md @@ -0,0 +1,177 @@ +# gortsplib + +[![Test](https://github.com/bluenviron/gortsplib/actions/workflows/test.yml/badge.svg)](https://github.com/bluenviron/gortsplib/actions/workflows/test.yml) +[![Lint](https://github.com/bluenviron/gortsplib/actions/workflows/lint.yml/badge.svg)](https://github.com/bluenviron/gortsplib/actions/workflows/lint.yml) +[![Go Report Card](https://goreportcard.com/badge/github.com/bluenviron/gortsplib)](https://goreportcard.com/report/github.com/bluenviron/gortsplib) +[![CodeCov](https://codecov.io/gh/bluenviron/gortsplib/branch/main/graph/badge.svg)](https://app.codecov.io/gh/bluenviron/gortsplib/tree/main) +[![PkgGoDev](https://pkg.go.dev/badge/github.com/bluenviron/gortsplib/v4)](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4#pkg-index) + +RTSP 1.0 client and server library for the Go programming language, written for [MediaMTX](https://github.com/bluenviron/mediamtx). + +Go ≥ 1.21 is required. + +Features: + +* Client + * Query servers about available media streams + * Read media streams from a server ("play") + * Read streams with the UDP, UDP-multicast or TCP transport protocol + * Read TLS-encrypted streams (TCP only) + * Switch transport protocol automatically + * Read selected media streams + * Pause or seek without disconnecting from the server + * Write to ONVIF back channels + * Get PTS (relative) timestamp of incoming packets + * Get NTP (absolute) timestamp of incoming packets + * Write media streams to a server ("record") + * Write streams with the UDP or TCP transport protocol + * Write TLS-encrypted streams (TCP only) + * Switch transport protocol automatically + * Pause without disconnecting from the server +* Server + * Handle requests from clients + * Validate client credentials + * Read media streams from clients ("record") + * Read streams with the UDP or TCP transport protocol + * Read TLS-encrypted streams (TCP only) + * Get PTS (relative) timestamp of incoming packets + * Get NTP (absolute) timestamp of incoming packets + * Serve media streams to clients ("play") + * Write streams with the UDP, UDP-multicast or TCP transport protocol + * Write TLS-encrypted streams (TCP only) + * Compute and provide SSRC, RTP-Info to clients +* Utilities + * Parse RTSP elements + * Encode/decode RTP packets into/from codec-specific frames + +## Table of contents + +* [Examples](#examples) +* [API Documentation](#api-documentation) +* [RTP Payload Formats](#rtp-payload-formats) +* [Specifications](#specifications) +* [Related projects](#related-projects) + +## Examples + +* [client-query](examples/client-query/main.go) +* [client-play](examples/client-play/main.go) +* [client-play-timestamp](examples/client-play-timestamp/main.go) +* [client-play-options](examples/client-play-options/main.go) +* [client-play-pause](examples/client-play-pause/main.go) +* [client-play-to-record](examples/client-play-to-record/main.go) +* [client-play-backchannel](examples/client-play-backchannel/main.go) +* [client-play-format-av1](examples/client-play-format-av1/main.go) +* [client-play-format-av1-to-jpeg](examples/client-play-format-av1-to-jpeg/main.go) +* [client-play-format-g711](examples/client-play-format-g711/main.go) +* [client-play-format-h264](examples/client-play-format-h264/main.go) +* [client-play-format-h264-to-jpeg](examples/client-play-format-h264-to-jpeg/main.go) +* [client-play-format-h264-to-disk](examples/client-play-format-h264-to-disk/main.go) +* [client-play-format-h264-mpeg4audio-to-disk](examples/client-play-format-h264-mpeg4audio-to-disk/main.go) +* [client-play-format-h265](examples/client-play-format-h265/main.go) +* [client-play-format-h265-to-jpeg](examples/client-play-format-h265-to-jpeg/main.go) +* [client-play-format-h265-to-disk](examples/client-play-format-h265-to-disk/main.go) +* [client-play-format-lpcm](examples/client-play-format-lpcm/main.go) +* [client-play-format-mjpeg](examples/client-play-format-mjpeg/main.go) +* [client-play-format-mpeg4audio](examples/client-play-format-mpeg4audio/main.go) +* [client-play-format-mpeg4audio-to-disk](examples/client-play-format-mpeg4audio-to-disk/main.go) +* [client-play-format-opus](examples/client-play-format-opus/main.go) +* [client-play-format-opus-to-disk](examples/client-play-format-opus-to-disk/main.go) +* [client-play-format-vp8](examples/client-play-format-vp8/main.go) +* [client-play-format-vp9](examples/client-play-format-vp9/main.go) +* [client-record-options](examples/client-record-options/main.go) +* [client-record-pause](examples/client-record-pause/main.go) +* [client-record-format-av1](examples/client-record-format-av1/main.go) +* [client-record-format-g711](examples/client-record-format-g711/main.go) +* [client-record-format-h264](examples/client-record-format-h264/main.go) +* [client-record-format-h264-from-disk](examples/client-record-format-h264-from-disk/main.go) +* [client-record-format-h265](examples/client-record-format-h265/main.go) +* [client-record-format-lpcm](examples/client-record-format-lpcm/main.go) +* [client-record-format-mjpeg](examples/client-record-format-mjpeg/main.go) +* [client-record-format-mpeg4audio](examples/client-record-format-mpeg4audio/main.go) +* [client-record-format-opus](examples/client-record-format-opus/main.go) +* [client-record-format-vp8](examples/client-record-format-vp8/main.go) +* [client-record-format-vp9](examples/client-record-format-vp9/main.go) +* [server](examples/server/main.go) +* [server-tls](examples/server-tls/main.go) +* [server-auth](examples/server-auth/main.go) +* [server-h264-to-disk](examples/server-h264-to-disk/main.go) +* [server-h264-from-disk](examples/server-h264-from-disk/main.go) +* [proxy](examples/proxy/main.go) + +## API Documentation + +[Click to open the API Documentation](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4#pkg-index) + +## RTP Payload Formats + +In RTSP, media streams are transmitted by using RTP packets, which are encoded in a specific, codec-dependent, format. This library supports formats for the following codecs: + +### Video + +|codec|documentation|encoder and decoder available| +|------|-------------|-----------------------------| +|AV1|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#AV1)|:heavy_check_mark:| +|VP9|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#VP9)|:heavy_check_mark:| +|VP8|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#VP8)|:heavy_check_mark:| +|H265|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#H265)|:heavy_check_mark:| +|H264|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#H264)|:heavy_check_mark:| +|MPEG-4 Video (H263, Xvid)|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#MPEG4Video)|:heavy_check_mark:| +|MPEG-1/2 Video|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#MPEG1Video)|:heavy_check_mark:| +|M-JPEG|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#MJPEG)|:heavy_check_mark:| + +### Audio + +|codec|documentation|encoder and decoder available| +|------|-------------|-----------------------------| +|Opus|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#Opus)|:heavy_check_mark:| +|Vorbis|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#Vorbis)|| +|MPEG-4 Audio (AAC)|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#MPEG4Audio)|:heavy_check_mark:| +|MPEG-1/2 Audio (MP3)|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#MPEG1Audio)|:heavy_check_mark:| +|AC-3|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#AC3)|:heavy_check_mark:| +|Speex|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#Speex)|| +|G726|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#G726)|| +|G722|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#G722)|:heavy_check_mark:| +|G711 (PCMA, PCMU)|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#G711)|:heavy_check_mark:| +|LPCM|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#LPCM)|:heavy_check_mark:| + +### Other + +|codec|documentation|encoder and decoder available| +|------|-------------|-----------------------------| +|MPEG-TS|[link](https://pkg.go.dev/github.com/bluenviron/gortsplib/v4/pkg/format#MPEGTS)|| + +## Specifications + +|name|area| +|----|----| +|[RFC2326, RTSP 1.0](https://datatracker.ietf.org/doc/html/rfc2326)|protocol| +|[RFC7826, RTSP 2.0](https://datatracker.ietf.org/doc/html/rfc7826)|protocol| +|[RFC8866, SDP: Session Description Protocol](https://datatracker.ietf.org/doc/html/rfc8866)|SDP| +|[RTP Payload Format For AV1 (v1.0)](https://aomediacodec.github.io/av1-rtp-spec/)|payload formats / AV1| +|[RTP Payload Format for VP9 Video](https://datatracker.ietf.org/doc/html/draft-ietf-payload-vp9-16)|payload formats / VP9| +|[RFC7741, RTP Payload Format for VP8 Video](https://datatracker.ietf.org/doc/html/rfc7741)|payload formats / VP8| +|[RFC7798, RTP Payload Format for High Efficiency Video Coding (HEVC)](https://datatracker.ietf.org/doc/html/rfc7798)|payload formats / H265| +|[RFC6184, RTP Payload Format for H.264 Video](https://datatracker.ietf.org/doc/html/rfc6184)|payload formats / H264| +|[RFC3640, RTP Payload Format for Transport of MPEG-4 Elementary Streams](https://datatracker.ietf.org/doc/html/rfc3640)|payload formats / MPEG-4 audio, MPEG-4 video| +|[RFC2250, RTP Payload Format for MPEG1/MPEG2 Video](https://datatracker.ietf.org/doc/html/rfc2250)|payload formats / MPEG-1 video, MPEG-2 audio, MPEG-TS| +|[RFC2435, RTP Payload Format for JPEG-compressed Video](https://datatracker.ietf.org/doc/html/rfc2435)|payload formats / M-JPEG| +|[RFC7587, RTP Payload Format for the Opus Speech and Audio Codec](https://datatracker.ietf.org/doc/html/rfc7587)|payload formats / Opus| +|[Multiopus in libwebrtc](https://webrtc-review.googlesource.com/c/src/+/129768)|payload formats / Opus| +|[RFC5215, RTP Payload Format for Vorbis Encoded Audio](https://datatracker.ietf.org/doc/html/rfc5215)|payload formats / Vorbis| +|[RFC4184, RTP Payload Format for AC-3 Audio](https://datatracker.ietf.org/doc/html/rfc4184)|payload formats / AC-3| +|[RFC6416, RTP Payload Format for MPEG-4 Audio/Visual Streams](https://datatracker.ietf.org/doc/html/rfc6416)|payload formats / MPEG-4 audio| +|[RFC5574, RTP Payload Format for the Speex Codec](https://datatracker.ietf.org/doc/html/rfc5574)|payload formats / Speex| +|[RFC3551, RTP Profile for Audio and Video Conferences with Minimal Control](https://datatracker.ietf.org/doc/html/rfc3551)|payload formats / G726, G722, G711, LPCM| +|[RFC3190, RTP Payload Format for 12-bit DAT Audio and 20- and 24-bit Linear Sampled Audio](https://datatracker.ietf.org/doc/html/rfc3190)|payload formats / LPCM| +|[Codec specifications](https://github.com/bluenviron/mediacommon#specifications)|codecs| +|[Golang project layout](https://github.com/golang-standards/project-layout)|project layout| + +## Related projects + +* [MediaMTX](https://github.com/bluenviron/mediamtx) +* [gohlslib](https://github.com/bluenviron/gohlslib) +* [mediacommon](https://github.com/bluenviron/mediacommon) +* [pion/sdp (SDP library used internally)](https://github.com/pion/sdp) +* [pion/rtp (RTP library used internally)](https://github.com/pion/rtp) +* [pion/rtcp (RTCP library used internally)](https://github.com/pion/rtcp) diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/async_processor.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/async_processor.go new file mode 100644 index 000000000..7a5e12d62 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/async_processor.go @@ -0,0 +1,58 @@ +package gortsplib + +import ( + "github.com/bluenviron/gortsplib/v4/pkg/ringbuffer" +) + +// this is an asynchronous queue processor +// that allows to detach the routine that is reading a stream +// from the routine that is writing a stream. +type asyncProcessor struct { + bufferSize int + + running bool + buffer *ringbuffer.RingBuffer + stopError error + + chStopped chan struct{} +} + +func (w *asyncProcessor) initialize() { + w.buffer, _ = ringbuffer.New(uint64(w.bufferSize)) +} + +func (w *asyncProcessor) close() { + if w.running { + w.buffer.Close() + <-w.chStopped + } +} + +func (w *asyncProcessor) start() { + w.running = true + w.chStopped = make(chan struct{}) + go w.run() +} + +func (w *asyncProcessor) run() { + w.stopError = w.runInner() + close(w.chStopped) +} + +func (w *asyncProcessor) runInner() error { + for { + tmp, ok := w.buffer.Pull() + if !ok { + return nil + } + + err := tmp.(func() error)() + if err != nil { + return err + } + } +} + +func (w *asyncProcessor) push(cb func() error) bool { + return w.buffer.Push(cb) +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/client.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/client.go new file mode 100644 index 000000000..7e2b1c4aa --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/client.go @@ -0,0 +1,2256 @@ +/* +Package gortsplib is a RTSP 1.0 library for the Go programming language. + +Examples are available at https://github.com/bluenviron/gortsplib/tree/main/examples +*/ +package gortsplib + +import ( + "context" + "crypto/tls" + "fmt" + "log" + "net" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/pion/rtcp" + "github.com/pion/rtp" + + "github.com/bluenviron/gortsplib/v4/pkg/auth" + "github.com/bluenviron/gortsplib/v4/pkg/base" + "github.com/bluenviron/gortsplib/v4/pkg/bytecounter" + "github.com/bluenviron/gortsplib/v4/pkg/conn" + "github.com/bluenviron/gortsplib/v4/pkg/description" + "github.com/bluenviron/gortsplib/v4/pkg/format" + "github.com/bluenviron/gortsplib/v4/pkg/headers" + "github.com/bluenviron/gortsplib/v4/pkg/liberrors" + "github.com/bluenviron/gortsplib/v4/pkg/rtcpreceiver" + "github.com/bluenviron/gortsplib/v4/pkg/rtcpsender" + "github.com/bluenviron/gortsplib/v4/pkg/rtptime" + "github.com/bluenviron/gortsplib/v4/pkg/sdp" +) + +const ( + clientUserAgent = "gortsplib" +) + +// avoid an int64 overflow and preserve resolution by splitting division into two parts: +// first add the integer part, then the decimal part. +func multiplyAndDivide(v, m, d time.Duration) time.Duration { + secs := v / d + dec := v % d + return (secs*m + dec*m/d) +} + +// convert an URL into an address, in particular: +// * add default port +// * handle IPv6 with or without square brackets. +// Adapted from net/http: +// https://cs.opensource.google/go/go/+/refs/tags/go1.20.5:src/net/http/transport.go;l=2747 +func canonicalAddr(u *base.URL) string { + addr := u.Hostname() + + port := u.Port() + if port == "" { + if u.Scheme == "rtsp" { + port = "554" + } else { // rtsps + port = "322" + } + } + + return net.JoinHostPort(addr, port) +} + +func isAnyPort(p int) bool { + return p == 0 || p == 1 +} + +func findBaseURL(sd *sdp.SessionDescription, res *base.Response, u *base.URL) (*base.URL, error) { + // use global control attribute + if control, ok := sd.Attribute("control"); ok && control != "*" { + ret, err := base.ParseURL(control) + if err != nil { + return nil, fmt.Errorf("invalid control attribute: '%v'", control) + } + + // add credentials + ret.User = u.User + + return ret, nil + } + + // use Content-Base + if cb, ok := res.Header["Content-Base"]; ok { + if len(cb) != 1 { + return nil, fmt.Errorf("invalid Content-Base: '%v'", cb) + } + + if strings.HasPrefix(cb[0], "/") { + // parse as a relative path + ret, err := base.ParseURL(u.Scheme + "://" + u.Host + cb[0]) + if err != nil { + return nil, fmt.Errorf("invalid Content-Base: '%v'", cb) + } + + // add credentials + ret.User = u.User + + return ret, nil + } + + ret, err := base.ParseURL(cb[0]) + if err != nil { + return nil, fmt.Errorf("invalid Content-Base: '%v'", cb) + } + + // add credentials + ret.User = u.User + + return ret, nil + } + + // use URL of request + return u, nil +} + +func prepareForAnnounce(desc *description.Session) { + for i, media := range desc.Medias { + media.Control = "trackID=" + strconv.FormatInt(int64(i), 10) + } +} + +func supportsGetParameter(header base.Header) bool { + pub, ok := header["Public"] + if !ok || len(pub) != 1 { + return false + } + + for _, m := range strings.Split(pub[0], ",") { + if base.Method(strings.Trim(m, " ")) == base.GetParameter { + return true + } + } + return false +} + +type clientState int + +const ( + clientStateInitial clientState = iota + clientStatePrePlay + clientStatePlay + clientStatePreRecord + clientStateRecord +) + +func (s clientState) String() string { + switch s { + case clientStateInitial: + return "initial" + case clientStatePrePlay: + return "prePlay" + case clientStatePlay: + return "play" + case clientStatePreRecord: + return "preRecord" + case clientStateRecord: + return "record" + } + return "unknown" +} + +type optionsReq struct { + url *base.URL + res chan clientRes +} + +type describeReq struct { + url *base.URL + res chan clientRes +} + +type announceReq struct { + url *base.URL + desc *description.Session + res chan clientRes +} + +type setupReq struct { + baseURL *base.URL + media *description.Media + rtpPort int + rtcpPort int + res chan clientRes +} + +type playReq struct { + ra *headers.Range + res chan clientRes +} + +type recordReq struct { + res chan clientRes +} + +type pauseReq struct { + res chan clientRes +} + +type clientRes struct { + sd *description.Session // describe only + res *base.Response + err error +} + +// ClientOnRequestFunc is the prototype of Client.OnRequest. +type ClientOnRequestFunc func(*base.Request) + +// ClientOnResponseFunc is the prototype of Client.OnResponse. +type ClientOnResponseFunc func(*base.Response) + +// ClientOnTransportSwitchFunc is the prototype of Client.OnTransportSwitch. +type ClientOnTransportSwitchFunc func(err error) + +// ClientOnPacketLostFunc is the prototype of Client.OnPacketLost. +// +// Deprecated: replaced by ClientOnPacketsLostFunc +type ClientOnPacketLostFunc func(err error) + +// ClientOnPacketsLostFunc is the prototype of Client.OnPacketsLost. +type ClientOnPacketsLostFunc func(lost uint64) + +// ClientOnDecodeErrorFunc is the prototype of Client.OnDecodeError. +type ClientOnDecodeErrorFunc func(err error) + +// OnPacketRTPFunc is the prototype of the callback passed to OnPacketRTP(). +type OnPacketRTPFunc func(*rtp.Packet) + +// OnPacketRTPAnyFunc is the prototype of the callback passed to OnPacketRTP(Any). +type OnPacketRTPAnyFunc func(*description.Media, format.Format, *rtp.Packet) + +// OnPacketRTCPFunc is the prototype of the callback passed to OnPacketRTCP(). +type OnPacketRTCPFunc func(rtcp.Packet) + +// OnPacketRTCPAnyFunc is the prototype of the callback passed to OnPacketRTCPAny(). +type OnPacketRTCPAnyFunc func(*description.Media, rtcp.Packet) + +// Client is a RTSP client. +type Client struct { + // + // RTSP parameters (all optional) + // + // timeout of read operations. + // It defaults to 10 seconds. + ReadTimeout time.Duration + // timeout of write operations. + // It defaults to 10 seconds. + WriteTimeout time.Duration + // a TLS configuration to connect to TLS (RTSPS) servers. + // It defaults to nil. + TLSConfig *tls.Config + // enable communication with servers which don't provide UDP server ports + // or use different server ports than the announced ones. + // This can be a security issue. + // It defaults to false. + AnyPortEnable bool + // transport protocol (UDP, Multicast or TCP). + // If nil, it is chosen automatically (first UDP, then, if it fails, TCP). + // It defaults to nil. + Transport *Transport + // If the client is reading with UDP, it must receive + // at least a packet within this timeout, otherwise it switches to TCP. + // It defaults to 3 seconds. + InitialUDPReadTimeout time.Duration + // Size of the queue of outgoing packets. + // It defaults to 256. + WriteQueueSize int + // maximum size of outgoing RTP / RTCP packets. + // This must be less than the UDP MTU (1472 bytes). + // It defaults to 1472. + MaxPacketSize int + // user agent header. + // It defaults to "gortsplib" + UserAgent string + // disable automatic RTCP sender reports. + DisableRTCPSenderReports bool + // explicitly request back channels to the server. + RequestBackChannels bool + // pointer to a variable that stores received bytes. + // + // Deprecated: use Client.Stats() + BytesReceived *uint64 + // pointer to a variable that stores sent bytes. + // + // Deprecated: use Client.Stats() + BytesSent *uint64 + + // + // system functions (all optional) + // + // function used to initialize the TCP client. + // It defaults to (&net.Dialer{}).DialContext. + DialContext func(ctx context.Context, network, address string) (net.Conn, error) + // function used to initialize UDP listeners. + // It defaults to net.ListenPacket. + ListenPacket func(network, address string) (net.PacketConn, error) + + // + // callbacks (all optional) + // + // called when sending a request to the server. + OnRequest ClientOnRequestFunc + // called when receiving a response from the server. + OnResponse ClientOnResponseFunc + // called when receiving a request from the server. + OnServerRequest ClientOnRequestFunc + // called when sending a response to the server. + OnServerResponse ClientOnResponseFunc + // called when the transport protocol changes. + OnTransportSwitch ClientOnTransportSwitchFunc + // called when the client detects lost packets. + // + // Deprecated: replaced by OnPacketsLost + OnPacketLost ClientOnPacketLostFunc + // called when the client detects lost packets. + OnPacketsLost ClientOnPacketsLostFunc + // called when a non-fatal decode error occurs. + OnDecodeError ClientOnDecodeErrorFunc + + // + // private + // + + timeNow func() time.Time + senderReportPeriod time.Duration + receiverReportPeriod time.Duration + checkTimeoutPeriod time.Duration + + connURL *base.URL + ctx context.Context + ctxCancel func() + state clientState + nconn net.Conn + conn *conn.Conn + session string + sender *auth.Sender + cseq int + optionsSent bool + useGetParameter bool + lastDescribeURL *base.URL + baseURL *base.URL + effectiveTransport *Transport + backChannelSetupped bool + stdChannelSetupped bool + setuppedMedias map[*description.Media]*clientMedia + tcpCallbackByChannel map[int]readFunc + lastRange *headers.Range + checkTimeoutTimer *time.Timer + checkTimeoutInitial bool + tcpLastFrameTime *int64 + keepalivePeriod time.Duration + keepaliveTimer *time.Timer + closeError error + writer *asyncProcessor + writerMutex sync.RWMutex + reader *clientReader + timeDecoder *rtptime.GlobalDecoder2 + mustClose bool + tcpFrame *base.InterleavedFrame + tcpBuffer []byte + bytesReceived *uint64 + bytesSent *uint64 + + // in + chOptions chan optionsReq + chDescribe chan describeReq + chAnnounce chan announceReq + chSetup chan setupReq + chPlay chan playReq + chRecord chan recordReq + chPause chan pauseReq + + // out + done chan struct{} +} + +// Start initializes the connection to a server. +func (c *Client) Start(scheme string, host string) error { + // RTSP parameters + if c.ReadTimeout == 0 { + c.ReadTimeout = 10 * time.Second + } + if c.WriteTimeout == 0 { + c.WriteTimeout = 10 * time.Second + } + if c.InitialUDPReadTimeout == 0 { + c.InitialUDPReadTimeout = 3 * time.Second + } + if c.WriteQueueSize == 0 { + c.WriteQueueSize = 256 + } else if (c.WriteQueueSize & (c.WriteQueueSize - 1)) != 0 { + return fmt.Errorf("WriteQueueSize must be a power of two") + } + if c.MaxPacketSize == 0 { + c.MaxPacketSize = udpMaxPayloadSize + } else if c.MaxPacketSize > udpMaxPayloadSize { + return fmt.Errorf("MaxPacketSize must be less than %d", udpMaxPayloadSize) + } + if c.UserAgent == "" { + c.UserAgent = clientUserAgent + } + + // system functions + if c.DialContext == nil { + c.DialContext = (&net.Dialer{}).DialContext + } + if c.ListenPacket == nil { + c.ListenPacket = net.ListenPacket + } + + // callbacks + if c.OnRequest == nil { + c.OnRequest = func(*base.Request) { + } + } + if c.OnResponse == nil { + c.OnResponse = func(*base.Response) { + } + } + if c.OnServerRequest == nil { + c.OnServerRequest = func(*base.Request) { + } + } + if c.OnServerResponse == nil { + c.OnServerResponse = func(*base.Response) { + } + } + if c.OnTransportSwitch == nil { + c.OnTransportSwitch = func(err error) { + log.Println(err.Error()) + } + } + if c.OnPacketLost != nil { + c.OnPacketsLost = func(lost uint64) { + c.OnPacketLost(liberrors.ErrClientRTPPacketsLost{Lost: uint(lost)}) //nolint:staticcheck + } + } + if c.OnPacketsLost == nil { + c.OnPacketsLost = func(lost uint64) { + log.Printf("%d RTP %s lost", + lost, + func() string { + if lost == 1 { + return "packet" + } + return "packets" + }()) + } + } + if c.OnDecodeError == nil { + c.OnDecodeError = func(err error) { + log.Println(err.Error()) + } + } + + // private + if c.timeNow == nil { + c.timeNow = time.Now + } + if c.senderReportPeriod == 0 { + c.senderReportPeriod = 10 * time.Second + } + if c.receiverReportPeriod == 0 { + // some cameras require a maximum of 5secs between keepalives + c.receiverReportPeriod = 5 * time.Second + } + if c.checkTimeoutPeriod == 0 { + c.checkTimeoutPeriod = 1 * time.Second + } + + ctx, ctxCancel := context.WithCancel(context.Background()) + + c.connURL = &base.URL{ + Scheme: scheme, + Host: host, + } + c.ctx = ctx + c.ctxCancel = ctxCancel + c.checkTimeoutTimer = emptyTimer() + c.keepalivePeriod = 30 * time.Second + c.keepaliveTimer = emptyTimer() + + if c.BytesReceived != nil { + c.bytesReceived = c.BytesReceived + } else { + c.bytesReceived = new(uint64) + } + if c.BytesSent != nil { + c.bytesSent = c.BytesSent + } else { + c.bytesSent = new(uint64) + } + + c.chOptions = make(chan optionsReq) + c.chDescribe = make(chan describeReq) + c.chAnnounce = make(chan announceReq) + c.chSetup = make(chan setupReq) + c.chPlay = make(chan playReq) + c.chRecord = make(chan recordReq) + c.chPause = make(chan pauseReq) + c.done = make(chan struct{}) + + go c.run() + + return nil +} + +// StartRecording connects to the address and starts publishing given media. +func (c *Client) StartRecording(address string, desc *description.Session) error { + u, err := base.ParseURL(address) + if err != nil { + return err + } + + err = c.Start(u.Scheme, u.Host) + if err != nil { + return err + } + + _, err = c.Announce(u, desc) + if err != nil { + c.Close() + return err + } + + err = c.SetupAll(u, desc.Medias) + if err != nil { + c.Close() + return err + } + + _, err = c.Record() + if err != nil { + c.Close() + return err + } + + return nil +} + +// Close closes all client resources and waits for them to close. +func (c *Client) Close() { + c.ctxCancel() + <-c.done +} + +// Wait waits until all client resources are closed. +// This can happen when a fatal error occurs or when Close() is called. +func (c *Client) Wait() error { + <-c.done + return c.closeError +} + +func (c *Client) run() { + defer close(c.done) + + c.closeError = c.runInner() + + c.ctxCancel() + + c.doClose() +} + +func (c *Client) runInner() error { + for { + chReaderResponse := func() chan *base.Response { + if c.reader != nil { + return c.reader.chResponse + } + return nil + }() + + chReaderRequest := func() chan *base.Request { + if c.reader != nil { + return c.reader.chRequest + } + return nil + }() + + chReaderError := func() chan error { + if c.reader != nil { + return c.reader.chError + } + return nil + }() + + chWriterError := func() chan struct{} { + if c.writer != nil { + return c.writer.chStopped + } + return nil + }() + + select { + case req := <-c.chOptions: + res, err := c.doOptions(req.url) + req.res <- clientRes{res: res, err: err} + + if c.mustClose { + return err + } + + case req := <-c.chDescribe: + sd, res, err := c.doDescribe(req.url) + req.res <- clientRes{sd: sd, res: res, err: err} + + if c.mustClose { + return err + } + + case req := <-c.chAnnounce: + res, err := c.doAnnounce(req.url, req.desc) + req.res <- clientRes{res: res, err: err} + + if c.mustClose { + return err + } + + case req := <-c.chSetup: + res, err := c.doSetup(req.baseURL, req.media, req.rtpPort, req.rtcpPort) + req.res <- clientRes{res: res, err: err} + + if c.mustClose { + return err + } + + case req := <-c.chPlay: + res, err := c.doPlay(req.ra) + req.res <- clientRes{res: res, err: err} + + if c.mustClose { + return err + } + + case req := <-c.chRecord: + res, err := c.doRecord() + req.res <- clientRes{res: res, err: err} + + if c.mustClose { + return err + } + + case req := <-c.chPause: + res, err := c.doPause() + req.res <- clientRes{res: res, err: err} + + if c.mustClose { + return err + } + + case <-c.checkTimeoutTimer.C: + err := c.doCheckTimeout() + if err != nil { + return err + } + c.checkTimeoutTimer = time.NewTimer(c.checkTimeoutPeriod) + + case <-c.keepaliveTimer.C: + err := c.doKeepAlive() + if err != nil { + return err + } + c.keepaliveTimer = time.NewTimer(c.keepalivePeriod) + + case <-chWriterError: + return c.writer.stopError + + case err := <-chReaderError: + c.reader = nil + return err + + case res := <-chReaderResponse: + c.OnResponse(res) + // these are responses to keepalives, ignore them. + + case req := <-chReaderRequest: + err := c.handleServerRequest(req) + if err != nil { + return err + } + + case <-c.ctx.Done(): + return liberrors.ErrClientTerminated{} + } + } +} + +func (c *Client) waitResponse(requestCseqStr string) (*base.Response, error) { + t := time.NewTimer(c.ReadTimeout) + defer t.Stop() + + for { + select { + case <-t.C: + return nil, liberrors.ErrClientRequestTimedOut{} + + case err := <-c.reader.chError: + c.reader = nil + return nil, err + + case res := <-c.reader.chResponse: + c.OnResponse(res) + + // accept response if CSeq equals request CSeq, or if CSeq is not present + if cseq, ok := res.Header["CSeq"]; !ok || len(cseq) != 1 || strings.TrimSpace(cseq[0]) == requestCseqStr { + return res, nil + } + + case req := <-c.reader.chRequest: + err := c.handleServerRequest(req) + if err != nil { + return nil, err + } + + case <-c.ctx.Done(): + return nil, liberrors.ErrClientTerminated{} + } + } +} + +func (c *Client) handleServerRequest(req *base.Request) error { + c.OnServerRequest(req) + + if req.Method != base.Options { + return liberrors.ErrClientUnhandledMethod{Method: req.Method} + } + + h := base.Header{ + "User-Agent": base.HeaderValue{c.UserAgent}, + } + + if cseq, ok := req.Header["CSeq"]; ok { + h["CSeq"] = cseq + } + + res := &base.Response{ + StatusCode: base.StatusOK, + Header: h, + } + + c.OnServerResponse(res) + + c.nconn.SetWriteDeadline(time.Now().Add(c.WriteTimeout)) + return c.conn.WriteResponse(res) +} + +func (c *Client) doClose() { + if c.state == clientStatePlay || c.state == clientStateRecord { + c.destroyWriter() + c.stopTransportRoutines() + } + + if c.nconn != nil && c.baseURL != nil { + header := base.Header{} + + if c.backChannelSetupped { + header["Require"] = base.HeaderValue{"www.onvif.org/ver20/backchannel"} + } + + c.do(&base.Request{ //nolint:errcheck + Method: base.Teardown, + URL: c.baseURL, + Header: header, + }, true) + } + + if c.reader != nil { + c.nconn.Close() + c.reader.wait() + c.reader = nil + c.nconn = nil + c.conn = nil + } else if c.nconn != nil { + c.nconn.Close() + c.nconn = nil + c.conn = nil + } + + for _, cm := range c.setuppedMedias { + cm.close() + } +} + +func (c *Client) reset() { + c.doClose() + + c.state = clientStateInitial + c.session = "" + c.sender = nil + c.cseq = 0 + c.optionsSent = false + c.useGetParameter = false + c.baseURL = nil + c.effectiveTransport = nil + c.backChannelSetupped = false + c.stdChannelSetupped = false + c.setuppedMedias = nil + c.tcpCallbackByChannel = nil +} + +func (c *Client) checkState(allowed map[clientState]struct{}) error { + if _, ok := allowed[c.state]; ok { + return nil + } + + allowedList := make([]fmt.Stringer, len(allowed)) + i := 0 + for a := range allowed { + allowedList[i] = a + i++ + } + + return liberrors.ErrClientInvalidState{AllowedList: allowedList, State: c.state} +} + +func (c *Client) trySwitchingProtocol() error { + c.OnTransportSwitch(liberrors.ErrClientSwitchToTCP{}) + + prevConnURL := c.connURL + prevBaseURL := c.baseURL + prevMedias := c.setuppedMedias + + c.reset() + + v := TransportTCP + c.effectiveTransport = &v + c.connURL = prevConnURL + + // some Hikvision cameras require a describe before a setup + _, _, err := c.doDescribe(c.lastDescribeURL) + if err != nil { + return err + } + + for i, cm := range prevMedias { + _, err = c.doSetup(prevBaseURL, cm.media, 0, 0) + if err != nil { + return err + } + + c.setuppedMedias[i].onPacketRTCP = cm.onPacketRTCP + for j, tr := range cm.formats { + c.setuppedMedias[i].formats[j].onPacketRTP = tr.onPacketRTP + } + } + + _, err = c.doPlay(c.lastRange) + if err != nil { + return err + } + + return nil +} + +func (c *Client) trySwitchingProtocol2(medi *description.Media, baseURL *base.URL) (*base.Response, error) { + c.OnTransportSwitch(liberrors.ErrClientSwitchToTCP2{}) + + prevConnURL := c.connURL + + c.reset() + + v := TransportTCP + c.effectiveTransport = &v + c.connURL = prevConnURL + + // some Hikvision cameras require a describe before a setup + _, _, err := c.doDescribe(c.lastDescribeURL) + if err != nil { + return nil, err + } + + return c.doSetup(baseURL, medi, 0, 0) +} + +func (c *Client) startTransportRoutines() { + c.timeDecoder = &rtptime.GlobalDecoder2{} + c.timeDecoder.Initialize() + + for _, cm := range c.setuppedMedias { + cm.start() + } + + if *c.effectiveTransport == TransportTCP { + c.tcpFrame = &base.InterleavedFrame{} + c.tcpBuffer = make([]byte, c.MaxPacketSize+4) + } + + if c.state == clientStatePlay && c.stdChannelSetupped { + c.keepaliveTimer = time.NewTimer(c.keepalivePeriod) + + switch *c.effectiveTransport { + case TransportUDP: + c.checkTimeoutTimer = time.NewTimer(c.InitialUDPReadTimeout) + c.checkTimeoutInitial = true + + case TransportUDPMulticast: + c.checkTimeoutTimer = time.NewTimer(c.checkTimeoutPeriod) + + default: // TCP + c.checkTimeoutTimer = time.NewTimer(c.checkTimeoutPeriod) + v := c.timeNow().Unix() + c.tcpLastFrameTime = &v + } + } + + if *c.effectiveTransport == TransportTCP { + c.reader.setAllowInterleavedFrames(true) + } +} + +func (c *Client) stopTransportRoutines() { + if c.reader != nil { + c.reader.setAllowInterleavedFrames(false) + } + + c.checkTimeoutTimer = emptyTimer() + c.keepaliveTimer = emptyTimer() + + for _, cm := range c.setuppedMedias { + cm.stop() + } + + c.timeDecoder = nil +} + +func (c *Client) createWriter() { + c.writerMutex.Lock() + + c.writer = &asyncProcessor{ + bufferSize: func() int { + if c.state == clientStateRecord || c.backChannelSetupped { + return c.WriteQueueSize + } + + // when reading, buffer is only used to send RTCP receiver reports, + // that are much smaller than RTP packets and are sent at a fixed interval. + // decrease RAM consumption by allocating less buffers. + return 8 + }(), + } + + c.writer.initialize() + + c.writerMutex.Unlock() +} + +func (c *Client) startWriter() { + c.writer.start() +} + +func (c *Client) destroyWriter() { + c.writer.close() + + c.writerMutex.Lock() + c.writer = nil + c.writerMutex.Unlock() +} + +func (c *Client) connOpen() error { + if c.nconn != nil { + return nil + } + + if c.connURL.Scheme != "rtsp" && c.connURL.Scheme != "rtsps" { + return liberrors.ErrClientUnsupportedScheme{Scheme: c.connURL.Scheme} + } + + if c.connURL.Scheme == "rtsps" && c.Transport != nil && *c.Transport != TransportTCP { + return liberrors.ErrClientRTSPSTCP{} + } + + dialCtx, dialCtxCancel := context.WithTimeout(c.ctx, c.ReadTimeout) + defer dialCtxCancel() + + nconn, err := c.DialContext(dialCtx, "tcp", canonicalAddr(c.connURL)) + if err != nil { + return err + } + + if c.connURL.Scheme == "rtsps" { + tlsConfig := c.TLSConfig + if tlsConfig == nil { + tlsConfig = &tls.Config{} + } + tlsConfig.ServerName = c.connURL.Hostname() + + nconn = tls.Client(nconn, tlsConfig) + } + + c.nconn = nconn + bc := bytecounter.New(c.nconn, c.bytesReceived, c.bytesSent) + c.conn = conn.NewConn(bc) + c.reader = &clientReader{ + c: c, + } + c.reader.start() + + return nil +} + +func (c *Client) do(req *base.Request, skipResponse bool) (*base.Response, error) { + if !c.optionsSent && req.Method != base.Options { + _, err := c.doOptions(req.URL) + if err != nil { + return nil, err + } + } + + if req.Header == nil { + req.Header = make(base.Header) + } + + if c.session != "" { + req.Header["Session"] = base.HeaderValue{c.session} + } + + c.cseq++ + cseqStr := strconv.FormatInt(int64(c.cseq), 10) + req.Header["CSeq"] = base.HeaderValue{cseqStr} + + req.Header["User-Agent"] = base.HeaderValue{c.UserAgent} + + if c.sender != nil { + c.sender.AddAuthorization(req) + } + + c.OnRequest(req) + + c.nconn.SetWriteDeadline(time.Now().Add(c.WriteTimeout)) + err := c.conn.WriteRequest(req) + if err != nil { + return nil, err + } + + if skipResponse { + return nil, nil + } + + res, err := c.waitResponse(cseqStr) + if err != nil { + c.mustClose = true + return nil, err + } + + // get session from response + if v, ok := res.Header["Session"]; ok { + var sx headers.Session + err = sx.Unmarshal(v) + if err != nil { + return nil, liberrors.ErrClientSessionHeaderInvalid{Err: err} + } + c.session = sx.Session + + if sx.Timeout != nil && *sx.Timeout > 0 { + c.keepalivePeriod = time.Duration(*sx.Timeout) * time.Second * 8 / 10 + } + } + + // send request again with authentication + if res.StatusCode == base.StatusUnauthorized && req.URL.User != nil && c.sender == nil { + pass, _ := req.URL.User.Password() + user := req.URL.User.Username() + + sender := &auth.Sender{ + WWWAuth: res.Header["WWW-Authenticate"], + User: user, + Pass: pass, + } + err = sender.Initialize() + if err != nil { + return nil, liberrors.ErrClientAuthSetup{Err: err} + } + c.sender = sender + + return c.do(req, skipResponse) + } + + return res, nil +} + +func (c *Client) atLeastOneUDPPacketHasBeenReceived() bool { + for _, ct := range c.setuppedMedias { + lft := atomic.LoadInt64(ct.udpRTPListener.lastPacketTime) + if lft != 0 { + return true + } + + lft = atomic.LoadInt64(ct.udpRTCPListener.lastPacketTime) + if lft != 0 { + return true + } + } + return false +} + +func (c *Client) isInUDPTimeout() bool { + now := c.timeNow() + for _, ct := range c.setuppedMedias { + lft := time.Unix(atomic.LoadInt64(ct.udpRTPListener.lastPacketTime), 0) + if now.Sub(lft) < c.ReadTimeout { + return false + } + + lft = time.Unix(atomic.LoadInt64(ct.udpRTCPListener.lastPacketTime), 0) + if now.Sub(lft) < c.ReadTimeout { + return false + } + } + return true +} + +func (c *Client) isInTCPTimeout() bool { + now := c.timeNow() + lft := time.Unix(atomic.LoadInt64(c.tcpLastFrameTime), 0) + return now.Sub(lft) >= c.ReadTimeout +} + +func (c *Client) doCheckTimeout() error { + if *c.effectiveTransport == TransportUDP || + *c.effectiveTransport == TransportUDPMulticast { + if c.checkTimeoutInitial && !c.backChannelSetupped && c.Transport == nil { + c.checkTimeoutInitial = false + + if !c.atLeastOneUDPPacketHasBeenReceived() { + err := c.trySwitchingProtocol() + if err != nil { + return err + } + } + } else if c.isInUDPTimeout() { + return liberrors.ErrClientUDPTimeout{} + } + } else if c.isInTCPTimeout() { + return liberrors.ErrClientTCPTimeout{} + } + + return nil +} + +func (c *Client) doKeepAlive() error { + // some cameras do not reply to keepalives, do not wait for responses. + _, err := c.do(&base.Request{ + Method: func() base.Method { + // the VLC integrated rtsp server requires GET_PARAMETER + if c.useGetParameter { + return base.GetParameter + } + return base.Options + }(), + // use the stream base URL, otherwise some cameras do not reply + URL: c.baseURL, + }, true) + return err +} + +func (c *Client) doOptions(u *base.URL) (*base.Response, error) { + err := c.checkState(map[clientState]struct{}{ + clientStateInitial: {}, + clientStatePrePlay: {}, + clientStatePreRecord: {}, + }) + if err != nil { + return nil, err + } + + err = c.connOpen() + if err != nil { + return nil, err + } + + res, err := c.do(&base.Request{ + Method: base.Options, + URL: u, + }, false) + if err != nil { + return nil, err + } + + if res.StatusCode != base.StatusOK { + // since this method is not implemented by every RTSP server, + // return an error only if status code is not 404 + if res.StatusCode == base.StatusNotFound { + return res, nil + } + return nil, liberrors.ErrClientBadStatusCode{Code: res.StatusCode, Message: res.StatusMessage} + } + + c.optionsSent = true + c.useGetParameter = supportsGetParameter(res.Header) + + return res, nil +} + +// Options sends an OPTIONS request. +func (c *Client) Options(u *base.URL) (*base.Response, error) { + cres := make(chan clientRes) + select { + case c.chOptions <- optionsReq{url: u, res: cres}: + res := <-cres + return res.res, res.err + + case <-c.done: + return nil, c.closeError + } +} + +func (c *Client) doDescribe(u *base.URL) (*description.Session, *base.Response, error) { + err := c.checkState(map[clientState]struct{}{ + clientStateInitial: {}, + clientStatePrePlay: {}, + clientStatePreRecord: {}, + }) + if err != nil { + return nil, nil, err + } + + err = c.connOpen() + if err != nil { + return nil, nil, err + } + + header := base.Header{ + "Accept": base.HeaderValue{"application/sdp"}, + } + + if c.RequestBackChannels { + header["Require"] = base.HeaderValue{"www.onvif.org/ver20/backchannel"} + } + + res, err := c.do(&base.Request{ + Method: base.Describe, + URL: u, + Header: header, + }, false) + if err != nil { + return nil, nil, err + } + + if res.StatusCode != base.StatusOK { + // redirect + if res.StatusCode >= base.StatusMovedPermanently && + res.StatusCode <= base.StatusUseProxy && + len(res.Header["Location"]) == 1 { + c.reset() + + var ru *base.URL + ru, err = base.ParseURL(res.Header["Location"][0]) + if err != nil { + return nil, nil, err + } + + if u.User != nil { + ru.User = u.User + } + + c.connURL = &base.URL{ + Scheme: ru.Scheme, + Host: ru.Host, + } + + return c.doDescribe(ru) + } + + return nil, res, liberrors.ErrClientBadStatusCode{Code: res.StatusCode, Message: res.StatusMessage} + } + + ct, ok := res.Header["Content-Type"] + if !ok || len(ct) != 1 { + return nil, nil, liberrors.ErrClientContentTypeMissing{} + } + + // strip encoding information from Content-Type header + ct = base.HeaderValue{strings.Split(ct[0], ";")[0]} + + if ct[0] != "application/sdp" { + return nil, nil, liberrors.ErrClientContentTypeUnsupported{CT: ct} + } + + var ssd sdp.SessionDescription + err = ssd.Unmarshal(res.Body) + if err != nil { + return nil, nil, liberrors.ErrClientSDPInvalid{Err: err} + } + + var desc description.Session + err = desc.Unmarshal(&ssd) + if err != nil { + return nil, nil, liberrors.ErrClientSDPInvalid{Err: err} + } + + baseURL, err := findBaseURL(&ssd, res, u) + if err != nil { + return nil, nil, err + } + desc.BaseURL = baseURL + + c.lastDescribeURL = u + + return &desc, res, nil +} + +// Describe sends a DESCRIBE request. +func (c *Client) Describe(u *base.URL) (*description.Session, *base.Response, error) { + cres := make(chan clientRes) + select { + case c.chDescribe <- describeReq{url: u, res: cres}: + res := <-cres + return res.sd, res.res, res.err + + case <-c.done: + return nil, nil, c.closeError + } +} + +func (c *Client) doAnnounce(u *base.URL, desc *description.Session) (*base.Response, error) { + err := c.checkState(map[clientState]struct{}{ + clientStateInitial: {}, + }) + if err != nil { + return nil, err + } + + err = c.connOpen() + if err != nil { + return nil, err + } + + prepareForAnnounce(desc) + + byts, err := desc.Marshal(false) + if err != nil { + return nil, err + } + + res, err := c.do(&base.Request{ + Method: base.Announce, + URL: u, + Header: base.Header{ + "Content-Type": base.HeaderValue{"application/sdp"}, + }, + Body: byts, + }, false) + if err != nil { + return nil, err + } + + if res.StatusCode != base.StatusOK { + return nil, liberrors.ErrClientBadStatusCode{ + Code: res.StatusCode, Message: res.StatusMessage, + } + } + + c.baseURL = u.Clone() + c.state = clientStatePreRecord + + return res, nil +} + +// Announce sends an ANNOUNCE request. +func (c *Client) Announce(u *base.URL, desc *description.Session) (*base.Response, error) { + cres := make(chan clientRes) + select { + case c.chAnnounce <- announceReq{url: u, desc: desc, res: cres}: + res := <-cres + return res.res, res.err + + case <-c.done: + return nil, c.closeError + } +} + +func (c *Client) doSetup( + baseURL *base.URL, + medi *description.Media, + rtpPort int, + rtcpPort int, +) (*base.Response, error) { + err := c.checkState(map[clientState]struct{}{ + clientStateInitial: {}, + clientStatePrePlay: {}, + clientStatePreRecord: {}, + }) + if err != nil { + return nil, err + } + + err = c.connOpen() + if err != nil { + return nil, err + } + + if c.baseURL != nil && *baseURL != *c.baseURL { + return nil, liberrors.ErrClientCannotSetupMediasDifferentURLs{} + } + + th := headers.Transport{ + Mode: func() *headers.TransportMode { + if c.state == clientStatePreRecord { + v := headers.TransportModeRecord + return &v + } + // when playing, omit mode, since it causes errors with some servers. + return nil + }(), + } + + cm := &clientMedia{ + c: c, + media: medi, + } + cm.initialize() + + if c.effectiveTransport == nil { + if c.connURL.Scheme == "rtsps" { // always use TCP if encrypted + v := TransportTCP + c.effectiveTransport = &v + } else if c.Transport != nil { // take transport from config + c.effectiveTransport = c.Transport + } + } + + var desiredTransport Transport + if c.effectiveTransport != nil { + desiredTransport = *c.effectiveTransport + } else { + desiredTransport = TransportUDP + } + + switch desiredTransport { + case TransportUDP: + if (rtpPort == 0 && rtcpPort != 0) || + (rtpPort != 0 && rtcpPort == 0) { + return nil, liberrors.ErrClientUDPPortsZero{} + } + + if rtpPort != 0 && rtcpPort != (rtpPort+1) { + return nil, liberrors.ErrClientUDPPortsNotConsecutive{} + } + + err = cm.createUDPListeners( + false, + nil, + net.JoinHostPort("", strconv.FormatInt(int64(rtpPort), 10)), + net.JoinHostPort("", strconv.FormatInt(int64(rtcpPort), 10)), + ) + if err != nil { + return nil, err + } + + v1 := headers.TransportDeliveryUnicast + th.Delivery = &v1 + th.Protocol = headers.TransportProtocolUDP + th.ClientPorts = &[2]int{cm.udpRTPListener.port(), cm.udpRTCPListener.port()} + + case TransportUDPMulticast: + v1 := headers.TransportDeliveryMulticast + th.Delivery = &v1 + th.Protocol = headers.TransportProtocolUDP + + case TransportTCP: + v1 := headers.TransportDeliveryUnicast + th.Delivery = &v1 + th.Protocol = headers.TransportProtocolTCP + ch := c.findFreeChannelPair() + th.InterleavedIDs = &[2]int{ch, ch + 1} + } + + mediaURL, err := medi.URL(baseURL) + if err != nil { + cm.close() + return nil, err + } + + header := base.Header{ + "Transport": th.Marshal(), + } + + if medi.IsBackChannel { + header["Require"] = base.HeaderValue{"www.onvif.org/ver20/backchannel"} + } + + res, err := c.do(&base.Request{ + Method: base.Setup, + URL: mediaURL, + Header: header, + }, false) + if err != nil { + cm.close() + return nil, err + } + + if res.StatusCode != base.StatusOK { + cm.close() + + // switch transport automatically + if res.StatusCode == base.StatusUnsupportedTransport && + c.effectiveTransport == nil { + c.OnTransportSwitch(liberrors.ErrClientSwitchToTCP2{}) + v := TransportTCP + c.effectiveTransport = &v + return c.doSetup(baseURL, medi, 0, 0) + } + + return nil, liberrors.ErrClientBadStatusCode{Code: res.StatusCode, Message: res.StatusMessage} + } + + var thRes headers.Transport + err = thRes.Unmarshal(res.Header["Transport"]) + if err != nil { + cm.close() + return nil, liberrors.ErrClientTransportHeaderInvalid{Err: err} + } + + switch desiredTransport { + case TransportUDP, TransportUDPMulticast: + if thRes.Protocol == headers.TransportProtocolTCP { + cm.close() + + // switch transport automatically + if c.effectiveTransport == nil && + c.Transport == nil { + c.baseURL = baseURL + return c.trySwitchingProtocol2(medi, baseURL) + } + + return nil, liberrors.ErrClientServerRequestedTCP{} + } + } + + switch desiredTransport { + case TransportUDP: + if thRes.Delivery != nil && *thRes.Delivery != headers.TransportDeliveryUnicast { + cm.close() + return nil, liberrors.ErrClientTransportHeaderInvalidDelivery{} + } + + serverPortsValid := thRes.ServerPorts != nil && !isAnyPort(thRes.ServerPorts[0]) && !isAnyPort(thRes.ServerPorts[1]) + + if (c.state == clientStatePreRecord || !c.AnyPortEnable) && !serverPortsValid { + cm.close() + return nil, liberrors.ErrClientServerPortsNotProvided{} + } + + var readIP net.IP + if thRes.Source != nil { + readIP = *thRes.Source + } else { + readIP = c.nconn.RemoteAddr().(*net.TCPAddr).IP + } + + if serverPortsValid { + if !c.AnyPortEnable { + cm.udpRTPListener.readPort = thRes.ServerPorts[0] + } + cm.udpRTPListener.writeAddr = &net.UDPAddr{ + IP: c.nconn.RemoteAddr().(*net.TCPAddr).IP, + Zone: c.nconn.RemoteAddr().(*net.TCPAddr).Zone, + Port: thRes.ServerPorts[0], + } + } + cm.udpRTPListener.readIP = readIP + + if serverPortsValid { + if !c.AnyPortEnable { + cm.udpRTCPListener.readPort = thRes.ServerPorts[1] + } + cm.udpRTCPListener.writeAddr = &net.UDPAddr{ + IP: c.nconn.RemoteAddr().(*net.TCPAddr).IP, + Zone: c.nconn.RemoteAddr().(*net.TCPAddr).Zone, + Port: thRes.ServerPorts[1], + } + } + cm.udpRTCPListener.readIP = readIP + + case TransportUDPMulticast: + if thRes.Delivery == nil || *thRes.Delivery != headers.TransportDeliveryMulticast { + return nil, liberrors.ErrClientTransportHeaderInvalidDelivery{} + } + + if thRes.Ports == nil { + return nil, liberrors.ErrClientTransportHeaderNoPorts{} + } + + if thRes.Destination == nil { + return nil, liberrors.ErrClientTransportHeaderNoDestination{} + } + + var readIP net.IP + if thRes.Source != nil { + readIP = *thRes.Source + } else { + readIP = c.nconn.RemoteAddr().(*net.TCPAddr).IP + } + + err = cm.createUDPListeners( + true, + readIP, + net.JoinHostPort(thRes.Destination.String(), strconv.FormatInt(int64(thRes.Ports[0]), 10)), + net.JoinHostPort(thRes.Destination.String(), strconv.FormatInt(int64(thRes.Ports[1]), 10)), + ) + if err != nil { + return nil, err + } + + cm.udpRTPListener.readIP = readIP + cm.udpRTPListener.readPort = thRes.Ports[0] + cm.udpRTPListener.writeAddr = &net.UDPAddr{ + IP: *thRes.Destination, + Port: thRes.Ports[0], + } + + cm.udpRTCPListener.readIP = readIP + cm.udpRTCPListener.readPort = thRes.Ports[1] + cm.udpRTCPListener.writeAddr = &net.UDPAddr{ + IP: *thRes.Destination, + Port: thRes.Ports[1], + } + + case TransportTCP: + if thRes.Protocol != headers.TransportProtocolTCP { + return nil, liberrors.ErrClientServerRequestedUDP{} + } + + if thRes.Delivery != nil && *thRes.Delivery != headers.TransportDeliveryUnicast { + return nil, liberrors.ErrClientTransportHeaderInvalidDelivery{} + } + + if thRes.InterleavedIDs == nil { + return nil, liberrors.ErrClientTransportHeaderNoInterleavedIDs{} + } + + if (thRes.InterleavedIDs[0] + 1) != thRes.InterleavedIDs[1] { + return nil, liberrors.ErrClientTransportHeaderInvalidInterleavedIDs{} + } + + if c.isChannelPairInUse(thRes.InterleavedIDs[0]) { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, liberrors.ErrClientTransportHeaderInterleavedIDsInUse{} + } + + cm.tcpChannel = thRes.InterleavedIDs[0] + } + + if c.setuppedMedias == nil { + c.setuppedMedias = make(map[*description.Media]*clientMedia) + } + + c.setuppedMedias[medi] = cm + + c.baseURL = baseURL + c.effectiveTransport = &desiredTransport + + if medi.IsBackChannel { + c.backChannelSetupped = true + } else { + c.stdChannelSetupped = true + } + + if c.state == clientStateInitial { + c.state = clientStatePrePlay + } + + return res, nil +} + +func (c *Client) isChannelPairInUse(channel int) bool { + for _, cm := range c.setuppedMedias { + if (cm.tcpChannel+1) == channel || cm.tcpChannel == channel || cm.tcpChannel == (channel+1) { + return true + } + } + return false +} + +func (c *Client) findFreeChannelPair() int { + for i := 0; ; i += 2 { // prefer even channels + if !c.isChannelPairInUse(i) { + return i + } + } +} + +// Setup sends a SETUP request. +// rtpPort and rtcpPort are used only if transport is UDP. +// if rtpPort and rtcpPort are zero, they are chosen automatically. +func (c *Client) Setup( + baseURL *base.URL, + media *description.Media, + rtpPort int, + rtcpPort int, +) (*base.Response, error) { + cres := make(chan clientRes) + select { + case c.chSetup <- setupReq{ + baseURL: baseURL, + media: media, + rtpPort: rtpPort, + rtcpPort: rtcpPort, + res: cres, + }: + res := <-cres + return res.res, res.err + + case <-c.done: + return nil, c.closeError + } +} + +// SetupAll setups all the given medias. +func (c *Client) SetupAll(baseURL *base.URL, medias []*description.Media) error { + for _, m := range medias { + _, err := c.Setup(baseURL, m, 0, 0) + if err != nil { + return err + } + } + return nil +} + +func (c *Client) doPlay(ra *headers.Range) (*base.Response, error) { + err := c.checkState(map[clientState]struct{}{ + clientStatePrePlay: {}, + }) + if err != nil { + return nil, err + } + + c.state = clientStatePlay + c.startTransportRoutines() + c.createWriter() + + // Range is mandatory in Parrot Streaming Server + if ra == nil { + ra = &headers.Range{ + Value: &headers.RangeNPT{ + Start: 0, + }, + } + } + + header := base.Header{ + "Range": ra.Marshal(), + } + + if c.backChannelSetupped { + header["Require"] = base.HeaderValue{"www.onvif.org/ver20/backchannel"} + } + + res, err := c.do(&base.Request{ + Method: base.Play, + URL: c.baseURL, + Header: header, + }, false) + if err != nil { + c.destroyWriter() + c.stopTransportRoutines() + c.state = clientStatePrePlay + return nil, err + } + + if res.StatusCode != base.StatusOK { + c.destroyWriter() + c.stopTransportRoutines() + c.state = clientStatePrePlay + return nil, liberrors.ErrClientBadStatusCode{ + Code: res.StatusCode, Message: res.StatusMessage, + } + } + + // open the firewall by sending empty packets to the counterpart. + // do this before sending the request. + // don't do this with multicast, otherwise the RTP packet is going to be broadcasted + // to all listeners, including us, messing up the stream. + if *c.effectiveTransport == TransportUDP { + for _, cm := range c.setuppedMedias { + byts, _ := (&rtp.Packet{Header: rtp.Header{Version: 2}}).Marshal() + cm.udpRTPListener.write(byts) //nolint:errcheck + + byts, _ = (&rtcp.ReceiverReport{}).Marshal() + cm.udpRTCPListener.write(byts) //nolint:errcheck + } + } + + c.startWriter() + + c.lastRange = ra + + return res, nil +} + +// Play sends a PLAY request. +// This can be called only after Setup(). +func (c *Client) Play(ra *headers.Range) (*base.Response, error) { + cres := make(chan clientRes) + select { + case c.chPlay <- playReq{ra: ra, res: cres}: + res := <-cres + return res.res, res.err + + case <-c.done: + return nil, c.closeError + } +} + +func (c *Client) doRecord() (*base.Response, error) { + err := c.checkState(map[clientState]struct{}{ + clientStatePreRecord: {}, + }) + if err != nil { + return nil, err + } + + c.state = clientStateRecord + c.startTransportRoutines() + c.createWriter() + + res, err := c.do(&base.Request{ + Method: base.Record, + URL: c.baseURL, + }, false) + if err != nil { + c.destroyWriter() + c.stopTransportRoutines() + c.state = clientStatePreRecord + return nil, err + } + + if res.StatusCode != base.StatusOK { + c.destroyWriter() + c.stopTransportRoutines() + c.state = clientStatePreRecord + return nil, liberrors.ErrClientBadStatusCode{ + Code: res.StatusCode, Message: res.StatusMessage, + } + } + + c.startWriter() + + return nil, nil +} + +// Record sends a RECORD request. +// This can be called only after Announce() and Setup(). +func (c *Client) Record() (*base.Response, error) { + cres := make(chan clientRes) + select { + case c.chRecord <- recordReq{res: cres}: + res := <-cres + return res.res, res.err + + case <-c.done: + return nil, c.closeError + } +} + +func (c *Client) doPause() (*base.Response, error) { + err := c.checkState(map[clientState]struct{}{ + clientStatePlay: {}, + clientStateRecord: {}, + }) + if err != nil { + return nil, err + } + + c.destroyWriter() + + res, err := c.do(&base.Request{ + Method: base.Pause, + URL: c.baseURL, + }, false) + if err != nil { + c.createWriter() + c.startWriter() + return nil, err + } + + if res.StatusCode != base.StatusOK { + c.createWriter() + c.startWriter() + return nil, liberrors.ErrClientBadStatusCode{ + Code: res.StatusCode, Message: res.StatusMessage, + } + } + + c.stopTransportRoutines() + + switch c.state { + case clientStatePlay: + c.state = clientStatePrePlay + case clientStateRecord: + c.state = clientStatePreRecord + } + + return res, nil +} + +// Pause sends a PAUSE request. +// This can be called only after Play() or Record(). +func (c *Client) Pause() (*base.Response, error) { + cres := make(chan clientRes) + select { + case c.chPause <- pauseReq{res: cres}: + res := <-cres + return res.res, res.err + + case <-c.done: + return nil, c.closeError + } +} + +// Seek asks the server to re-start the stream from a specific timestamp. +func (c *Client) Seek(ra *headers.Range) (*base.Response, error) { + _, err := c.Pause() + if err != nil { + return nil, err + } + + return c.Play(ra) +} + +// OnPacketRTPAny sets a callback that is called when a RTP packet is read from any setupped media. +func (c *Client) OnPacketRTPAny(cb OnPacketRTPAnyFunc) { + for _, cm := range c.setuppedMedias { + cmedia := cm.media + for _, forma := range cm.media.Formats { + c.OnPacketRTP(cm.media, forma, func(pkt *rtp.Packet) { + cb(cmedia, forma, pkt) + }) + } + } +} + +// OnPacketRTCPAny sets a callback that is called when a RTCP packet is read from any setupped media. +func (c *Client) OnPacketRTCPAny(cb OnPacketRTCPAnyFunc) { + for _, cm := range c.setuppedMedias { + cmedia := cm.media + c.OnPacketRTCP(cm.media, func(pkt rtcp.Packet) { + cb(cmedia, pkt) + }) + } +} + +// OnPacketRTP sets a callback that is called when a RTP packet is read. +func (c *Client) OnPacketRTP(medi *description.Media, forma format.Format, cb OnPacketRTPFunc) { + cm := c.setuppedMedias[medi] + ct := cm.formats[forma.PayloadType()] + ct.onPacketRTP = cb +} + +// OnPacketRTCP sets a callback that is called when a RTCP packet is read. +func (c *Client) OnPacketRTCP(medi *description.Media, cb OnPacketRTCPFunc) { + cm := c.setuppedMedias[medi] + cm.onPacketRTCP = cb +} + +// WritePacketRTP writes a RTP packet to the server. +func (c *Client) WritePacketRTP(medi *description.Media, pkt *rtp.Packet) error { + return c.WritePacketRTPWithNTP(medi, pkt, c.timeNow()) +} + +// WritePacketRTPWithNTP writes a RTP packet to the server. +// ntp is the absolute time of the packet, and is sent with periodic RTCP sender reports. +func (c *Client) WritePacketRTPWithNTP(medi *description.Media, pkt *rtp.Packet, ntp time.Time) error { + byts := make([]byte, c.MaxPacketSize) + n, err := pkt.MarshalTo(byts) + if err != nil { + return err + } + byts = byts[:n] + + select { + case <-c.done: + return c.closeError + default: + } + + c.writerMutex.RLock() + defer c.writerMutex.RUnlock() + + if c.writer == nil { + return nil + } + + cm := c.setuppedMedias[medi] + cf := cm.formats[pkt.PayloadType] + + cf.rtcpSender.ProcessPacket(pkt, ntp, cf.format.PTSEqualsDTS(pkt)) + + ok := c.writer.push(func() error { + return cf.writePacketRTPInQueue(byts) + }) + if !ok { + return liberrors.ErrClientWriteQueueFull{} + } + + return nil +} + +// WritePacketRTCP writes a RTCP packet to the server. +func (c *Client) WritePacketRTCP(medi *description.Media, pkt rtcp.Packet) error { + byts, err := pkt.Marshal() + if err != nil { + return err + } + + select { + case <-c.done: + return c.closeError + default: + } + + c.writerMutex.RLock() + defer c.writerMutex.RUnlock() + + if c.writer == nil { + return nil + } + + cm := c.setuppedMedias[medi] + + ok := c.writer.push(func() error { + return cm.writePacketRTCPInQueue(byts) + }) + if !ok { + return liberrors.ErrClientWriteQueueFull{} + } + + return nil +} + +// PacketPTS returns the PTS of an incoming RTP packet. +// It is computed by decoding the packet timestamp and sychronizing it with other tracks. +// +// Deprecated: replaced by PacketPTS2. +func (c *Client) PacketPTS(medi *description.Media, pkt *rtp.Packet) (time.Duration, bool) { + cm := c.setuppedMedias[medi] + ct := cm.formats[pkt.PayloadType] + + v, ok := c.timeDecoder.Decode(ct.format, pkt) + if !ok { + return 0, false + } + + return multiplyAndDivide(time.Duration(v), time.Second, time.Duration(ct.format.ClockRate())), true +} + +// PacketPTS2 returns the PTS of an incoming RTP packet. +// It is computed by decoding the packet timestamp and sychronizing it with other tracks. +func (c *Client) PacketPTS2(medi *description.Media, pkt *rtp.Packet) (int64, bool) { + cm := c.setuppedMedias[medi] + ct := cm.formats[pkt.PayloadType] + return c.timeDecoder.Decode(ct.format, pkt) +} + +// PacketNTP returns the NTP timestamp of an incoming RTP packet. +// The NTP timestamp is computed from RTCP sender reports. +func (c *Client) PacketNTP(medi *description.Media, pkt *rtp.Packet) (time.Time, bool) { + cm := c.setuppedMedias[medi] + ct := cm.formats[pkt.PayloadType] + return ct.rtcpReceiver.PacketNTP(pkt.Timestamp) +} + +// Stats returns client statistics. +func (c *Client) Stats() *ClientStats { + return &ClientStats{ + Conn: StatsConn{ + BytesReceived: atomic.LoadUint64(c.bytesReceived), + BytesSent: atomic.LoadUint64(c.bytesSent), + }, + Session: StatsSession{ + BytesReceived: func() uint64 { + v := uint64(0) + for _, sm := range c.setuppedMedias { + v += atomic.LoadUint64(sm.bytesReceived) + } + return v + }(), + BytesSent: func() uint64 { + v := uint64(0) + for _, sm := range c.setuppedMedias { + v += atomic.LoadUint64(sm.bytesSent) + } + return v + }(), + RTPPacketsReceived: func() uint64 { + v := uint64(0) + for _, sm := range c.setuppedMedias { + for _, f := range sm.formats { + v += atomic.LoadUint64(f.rtpPacketsReceived) + } + } + return v + }(), + RTPPacketsSent: func() uint64 { + v := uint64(0) + for _, sm := range c.setuppedMedias { + for _, f := range sm.formats { + v += atomic.LoadUint64(f.rtpPacketsSent) + } + } + return v + }(), + RTPPacketsLost: func() uint64 { + v := uint64(0) + for _, sm := range c.setuppedMedias { + for _, f := range sm.formats { + v += atomic.LoadUint64(f.rtpPacketsLost) + } + } + return v + }(), + RTPPacketsInError: func() uint64 { + v := uint64(0) + for _, sm := range c.setuppedMedias { + v += atomic.LoadUint64(sm.rtpPacketsInError) + } + return v + }(), + RTPPacketsJitter: func() float64 { + v := float64(0) + n := float64(0) + for _, sm := range c.setuppedMedias { + for _, fo := range sm.formats { + if fo.rtcpReceiver != nil { + stats := fo.rtcpReceiver.Stats() + if stats != nil { + v += stats.Jitter + n++ + } + } + } + } + if n != 0 { + return v / n + } + return 0 + }(), + RTCPPacketsReceived: func() uint64 { + v := uint64(0) + for _, sm := range c.setuppedMedias { + v += atomic.LoadUint64(sm.rtcpPacketsReceived) + } + return v + }(), + RTCPPacketsSent: func() uint64 { + v := uint64(0) + for _, sm := range c.setuppedMedias { + v += atomic.LoadUint64(sm.rtcpPacketsSent) + } + return v + }(), + RTCPPacketsInError: func() uint64 { + v := uint64(0) + for _, sm := range c.setuppedMedias { + v += atomic.LoadUint64(sm.rtcpPacketsInError) + } + return v + }(), + Medias: func() map[*description.Media]StatsSessionMedia { //nolint:dupl + ret := make(map[*description.Media]StatsSessionMedia, len(c.setuppedMedias)) + + for med, sm := range c.setuppedMedias { + ret[med] = StatsSessionMedia{ + BytesReceived: atomic.LoadUint64(sm.bytesReceived), + BytesSent: atomic.LoadUint64(sm.bytesSent), + RTPPacketsInError: atomic.LoadUint64(sm.rtpPacketsInError), + RTCPPacketsReceived: atomic.LoadUint64(sm.rtcpPacketsReceived), + RTCPPacketsSent: atomic.LoadUint64(sm.rtcpPacketsSent), + RTCPPacketsInError: atomic.LoadUint64(sm.rtcpPacketsInError), + Formats: func() map[format.Format]StatsSessionFormat { + ret := make(map[format.Format]StatsSessionFormat, len(sm.formats)) + + for _, fo := range sm.formats { + recvStats := func() *rtcpreceiver.Stats { + if fo.rtcpReceiver != nil { + return fo.rtcpReceiver.Stats() + } + return nil + }() + sentStats := func() *rtcpsender.Stats { + if fo.rtcpSender != nil { + return fo.rtcpSender.Stats() + } + return nil + }() + + ret[fo.format] = StatsSessionFormat{ //nolint:dupl + RTPPacketsReceived: atomic.LoadUint64(fo.rtpPacketsReceived), + RTPPacketsSent: atomic.LoadUint64(fo.rtpPacketsSent), + RTPPacketsLost: atomic.LoadUint64(fo.rtpPacketsLost), + LocalSSRC: func() uint32 { + if fo.rtcpReceiver != nil { + return *fo.rtcpReceiver.LocalSSRC + } + if sentStats != nil { + return sentStats.LocalSSRC + } + return 0 + }(), + RemoteSSRC: func() uint32 { + if recvStats != nil { + return recvStats.RemoteSSRC + } + return 0 + }(), + RTPPacketsLastSequenceNumber: func() uint16 { + if recvStats != nil { + return recvStats.LastSequenceNumber + } + if sentStats != nil { + return sentStats.LastSequenceNumber + } + return 0 + }(), + RTPPacketsLastRTP: func() uint32 { + if recvStats != nil { + return recvStats.LastRTP + } + if sentStats != nil { + return sentStats.LastRTP + } + return 0 + }(), + RTPPacketsLastNTP: func() time.Time { + if recvStats != nil { + return recvStats.LastNTP + } + if sentStats != nil { + return sentStats.LastNTP + } + return time.Time{} + }(), + RTPPacketsJitter: func() float64 { + if recvStats != nil { + return recvStats.Jitter + } + return 0 + }(), + } + } + + return ret + }(), + } + } + + return ret + }(), + }, + } +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/client_format.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/client_format.go new file mode 100644 index 000000000..2f903fd50 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/client_format.go @@ -0,0 +1,159 @@ +package gortsplib + +import ( + "sync/atomic" + "time" + + "github.com/pion/rtcp" + "github.com/pion/rtp" + + "github.com/bluenviron/gortsplib/v4/pkg/format" + "github.com/bluenviron/gortsplib/v4/pkg/rtcpreceiver" + "github.com/bluenviron/gortsplib/v4/pkg/rtcpsender" + "github.com/bluenviron/gortsplib/v4/pkg/rtplossdetector" + "github.com/bluenviron/gortsplib/v4/pkg/rtpreorderer" +) + +type clientFormat struct { + cm *clientMedia + format format.Format + onPacketRTP OnPacketRTPFunc + + udpReorderer *rtpreorderer.Reorderer // play + tcpLossDetector *rtplossdetector.LossDetector // play + rtcpReceiver *rtcpreceiver.RTCPReceiver // play + rtcpSender *rtcpsender.RTCPSender // record or back channel + writePacketRTPInQueue func([]byte) error + rtpPacketsReceived *uint64 + rtpPacketsSent *uint64 + rtpPacketsLost *uint64 +} + +func (cf *clientFormat) initialize() { + cf.rtpPacketsReceived = new(uint64) + cf.rtpPacketsSent = new(uint64) + cf.rtpPacketsLost = new(uint64) +} + +func (cf *clientFormat) start() { + if cf.cm.udpRTPListener != nil { + cf.writePacketRTPInQueue = cf.writePacketRTPInQueueUDP + } else { + cf.writePacketRTPInQueue = cf.writePacketRTPInQueueTCP + } + + if cf.cm.c.state == clientStateRecord || cf.cm.media.IsBackChannel { + cf.rtcpSender = &rtcpsender.RTCPSender{ + ClockRate: cf.format.ClockRate(), + Period: cf.cm.c.senderReportPeriod, + TimeNow: cf.cm.c.timeNow, + WritePacketRTCP: func(pkt rtcp.Packet) { + if !cf.cm.c.DisableRTCPSenderReports { + cf.cm.c.WritePacketRTCP(cf.cm.media, pkt) //nolint:errcheck + } + }, + } + cf.rtcpSender.Initialize() + } else { + if cf.cm.udpRTPListener != nil { + cf.udpReorderer = &rtpreorderer.Reorderer{} + cf.udpReorderer.Initialize() + } else { + cf.tcpLossDetector = &rtplossdetector.LossDetector{} + } + + cf.rtcpReceiver = &rtcpreceiver.RTCPReceiver{ + ClockRate: cf.format.ClockRate(), + Period: cf.cm.c.receiverReportPeriod, + TimeNow: cf.cm.c.timeNow, + WritePacketRTCP: func(pkt rtcp.Packet) { + if cf.cm.udpRTPListener != nil { + cf.cm.c.WritePacketRTCP(cf.cm.media, pkt) //nolint:errcheck + } + }, + } + err := cf.rtcpReceiver.Initialize() + if err != nil { + panic(err) + } + } +} + +func (cf *clientFormat) stop() { + if cf.rtcpReceiver != nil { + cf.rtcpReceiver.Close() + cf.rtcpReceiver = nil + } + + if cf.rtcpSender != nil { + cf.rtcpSender.Close() + } +} + +func (cf *clientFormat) readPacketRTPUDP(pkt *rtp.Packet) { + packets, lost := cf.udpReorderer.Process(pkt) + if lost != 0 { + cf.handlePacketsLost(uint64(lost)) + // do not return + } + + now := cf.cm.c.timeNow() + + for _, pkt := range packets { + cf.handlePacketRTP(pkt, now) + } +} + +func (cf *clientFormat) readPacketRTPTCP(pkt *rtp.Packet) { + lost := cf.tcpLossDetector.Process(pkt) + if lost != 0 { + cf.handlePacketsLost(uint64(lost)) + // do not return + } + + now := cf.cm.c.timeNow() + + cf.handlePacketRTP(pkt, now) +} + +func (cf *clientFormat) handlePacketRTP(pkt *rtp.Packet, now time.Time) { + err := cf.rtcpReceiver.ProcessPacket(pkt, now, cf.format.PTSEqualsDTS(pkt)) + if err != nil { + cf.cm.onPacketRTPDecodeError(err) + return + } + + atomic.AddUint64(cf.rtpPacketsReceived, 1) + + cf.onPacketRTP(pkt) +} + +func (cf *clientFormat) handlePacketsLost(lost uint64) { + atomic.AddUint64(cf.rtpPacketsLost, lost) + cf.cm.c.OnPacketsLost(lost) +} + +func (cf *clientFormat) writePacketRTPInQueueUDP(payload []byte) error { + err := cf.cm.udpRTPListener.write(payload) + if err != nil { + return err + } + + atomic.AddUint64(cf.cm.bytesSent, uint64(len(payload))) + atomic.AddUint64(cf.rtpPacketsSent, 1) + return nil +} + +func (cf *clientFormat) writePacketRTPInQueueTCP(payload []byte) error { + cf.cm.c.tcpFrame.Channel = cf.cm.tcpChannel + cf.cm.c.tcpFrame.Payload = payload + cf.cm.c.nconn.SetWriteDeadline(time.Now().Add(cf.cm.c.WriteTimeout)) + err := cf.cm.c.conn.WriteInterleavedFrame(cf.cm.c.tcpFrame, cf.cm.c.tcpBuffer) + if err != nil { + return err + } + + atomic.AddUint64(cf.cm.bytesSent, uint64(len(payload))) + atomic.AddUint64(cf.rtpPacketsSent, 1) + return nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/client_media.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/client_media.go new file mode 100644 index 000000000..c6c4f3bab --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/client_media.go @@ -0,0 +1,361 @@ +package gortsplib + +import ( + "net" + "sync/atomic" + "time" + + "github.com/pion/rtcp" + "github.com/pion/rtp" + + "github.com/bluenviron/gortsplib/v4/pkg/description" + "github.com/bluenviron/gortsplib/v4/pkg/liberrors" +) + +type clientMedia struct { + c *Client + media *description.Media + + onPacketRTCP OnPacketRTCPFunc + formats map[uint8]*clientFormat + tcpChannel int + udpRTPListener *clientUDPListener + udpRTCPListener *clientUDPListener + writePacketRTCPInQueue func([]byte) error + bytesReceived *uint64 + bytesSent *uint64 + rtpPacketsInError *uint64 + rtcpPacketsReceived *uint64 + rtcpPacketsSent *uint64 + rtcpPacketsInError *uint64 +} + +func (cm *clientMedia) initialize() { + cm.onPacketRTCP = func(rtcp.Packet) {} + cm.bytesReceived = new(uint64) + cm.bytesSent = new(uint64) + cm.rtpPacketsInError = new(uint64) + cm.rtcpPacketsReceived = new(uint64) + cm.rtcpPacketsSent = new(uint64) + cm.rtcpPacketsInError = new(uint64) + + cm.formats = make(map[uint8]*clientFormat) + + for _, forma := range cm.media.Formats { + f := &clientFormat{ + cm: cm, + format: forma, + onPacketRTP: func(*rtp.Packet) {}, + } + f.initialize() + cm.formats[forma.PayloadType()] = f + } +} + +func (cm *clientMedia) close() { + if cm.udpRTPListener != nil { + cm.udpRTPListener.close() + cm.udpRTCPListener.close() + } +} + +func (cm *clientMedia) createUDPListeners( + multicastEnable bool, + multicastSourceIP net.IP, + rtpAddress string, + rtcpAddress string, +) error { + if rtpAddress != ":0" { + l1 := &clientUDPListener{ + c: cm.c, + multicastEnable: multicastEnable, + multicastSourceIP: multicastSourceIP, + address: rtpAddress, + } + err := l1.initialize() + if err != nil { + return err + } + + l2 := &clientUDPListener{ + c: cm.c, + multicastEnable: multicastEnable, + multicastSourceIP: multicastSourceIP, + address: rtcpAddress, + } + err = l2.initialize() + if err != nil { + l1.close() + return err + } + + cm.udpRTPListener, cm.udpRTCPListener = l1, l2 + return nil + } + + var err error + cm.udpRTPListener, cm.udpRTCPListener, err = createUDPListenerPair(cm.c) + return err +} + +func (cm *clientMedia) start() { + if cm.udpRTPListener != nil { + cm.writePacketRTCPInQueue = cm.writePacketRTCPInQueueUDP + + if cm.c.state == clientStateRecord || cm.media.IsBackChannel { + cm.udpRTPListener.readFunc = cm.readPacketRTPUDPRecord + cm.udpRTCPListener.readFunc = cm.readPacketRTCPUDPRecord + } else { + cm.udpRTPListener.readFunc = cm.readPacketRTPUDPPlay + cm.udpRTCPListener.readFunc = cm.readPacketRTCPUDPPlay + } + } else { + cm.writePacketRTCPInQueue = cm.writePacketRTCPInQueueTCP + + if cm.c.tcpCallbackByChannel == nil { + cm.c.tcpCallbackByChannel = make(map[int]readFunc) + } + + if cm.c.state == clientStateRecord || cm.media.IsBackChannel { + cm.c.tcpCallbackByChannel[cm.tcpChannel] = cm.readPacketRTPTCPRecord + cm.c.tcpCallbackByChannel[cm.tcpChannel+1] = cm.readPacketRTCPTCPRecord + } else { + cm.c.tcpCallbackByChannel[cm.tcpChannel] = cm.readPacketRTPTCPPlay + cm.c.tcpCallbackByChannel[cm.tcpChannel+1] = cm.readPacketRTCPTCPPlay + } + } + + for _, ct := range cm.formats { + ct.start() + } + + if cm.udpRTPListener != nil { + cm.udpRTPListener.start() + cm.udpRTCPListener.start() + } +} + +func (cm *clientMedia) stop() { + if cm.udpRTPListener != nil { + cm.udpRTPListener.stop() + cm.udpRTCPListener.stop() + } + + for _, ct := range cm.formats { + ct.stop() + } +} + +func (cm *clientMedia) findFormatWithSSRC(ssrc uint32) *clientFormat { + for _, format := range cm.formats { + stats := format.rtcpReceiver.Stats() + if stats != nil && stats.RemoteSSRC == ssrc { + return format + } + } + return nil +} + +func (cm *clientMedia) writePacketRTCPInQueueUDP(payload []byte) error { + err := cm.udpRTCPListener.write(payload) + if err != nil { + return err + } + + atomic.AddUint64(cm.bytesSent, uint64(len(payload))) + atomic.AddUint64(cm.rtcpPacketsSent, 1) + return nil +} + +func (cm *clientMedia) writePacketRTCPInQueueTCP(payload []byte) error { + cm.c.tcpFrame.Channel = cm.tcpChannel + 1 + cm.c.tcpFrame.Payload = payload + cm.c.nconn.SetWriteDeadline(time.Now().Add(cm.c.WriteTimeout)) + err := cm.c.conn.WriteInterleavedFrame(cm.c.tcpFrame, cm.c.tcpBuffer) + if err != nil { + return err + } + + atomic.AddUint64(cm.bytesSent, uint64(len(payload))) + atomic.AddUint64(cm.rtcpPacketsSent, 1) + return nil +} + +func (cm *clientMedia) readPacketRTPTCPPlay(payload []byte) bool { + atomic.AddUint64(cm.bytesReceived, uint64(len(payload))) + + now := cm.c.timeNow() + atomic.StoreInt64(cm.c.tcpLastFrameTime, now.Unix()) + + pkt := &rtp.Packet{} + err := pkt.Unmarshal(payload) + if err != nil { + cm.onPacketRTPDecodeError(err) + return false + } + + forma, ok := cm.formats[pkt.PayloadType] + if !ok { + cm.onPacketRTPDecodeError(liberrors.ErrClientRTPPacketUnknownPayloadType{PayloadType: pkt.PayloadType}) + return false + } + + forma.readPacketRTPTCP(pkt) + + return true +} + +func (cm *clientMedia) readPacketRTCPTCPPlay(payload []byte) bool { + atomic.AddUint64(cm.bytesReceived, uint64(len(payload))) + + now := cm.c.timeNow() + atomic.StoreInt64(cm.c.tcpLastFrameTime, now.Unix()) + + if len(payload) > udpMaxPayloadSize { + cm.onPacketRTCPDecodeError(liberrors.ErrClientRTCPPacketTooBig{L: len(payload), Max: udpMaxPayloadSize}) + return false + } + + packets, err := rtcp.Unmarshal(payload) + if err != nil { + cm.onPacketRTCPDecodeError(err) + return false + } + + atomic.AddUint64(cm.rtcpPacketsReceived, uint64(len(packets))) + + for _, pkt := range packets { + if sr, ok := pkt.(*rtcp.SenderReport); ok { + format := cm.findFormatWithSSRC(sr.SSRC) + if format != nil { + format.rtcpReceiver.ProcessSenderReport(sr, now) + } + } + + cm.onPacketRTCP(pkt) + } + + return true +} + +func (cm *clientMedia) readPacketRTPTCPRecord(_ []byte) bool { + return false +} + +func (cm *clientMedia) readPacketRTCPTCPRecord(payload []byte) bool { + atomic.AddUint64(cm.bytesReceived, uint64(len(payload))) + + if len(payload) > udpMaxPayloadSize { + cm.onPacketRTCPDecodeError(liberrors.ErrClientRTCPPacketTooBig{L: len(payload), Max: udpMaxPayloadSize}) + return false + } + + packets, err := rtcp.Unmarshal(payload) + if err != nil { + cm.onPacketRTCPDecodeError(err) + return false + } + + atomic.AddUint64(cm.rtcpPacketsReceived, uint64(len(packets))) + + for _, pkt := range packets { + cm.onPacketRTCP(pkt) + } + + return true +} + +func (cm *clientMedia) readPacketRTPUDPPlay(payload []byte) bool { + atomic.AddUint64(cm.bytesReceived, uint64(len(payload))) + + if len(payload) == (udpMaxPayloadSize + 1) { + cm.onPacketRTPDecodeError(liberrors.ErrClientRTPPacketTooBigUDP{}) + return false + } + + pkt := &rtp.Packet{} + err := pkt.Unmarshal(payload) + if err != nil { + cm.onPacketRTPDecodeError(err) + return false + } + + forma, ok := cm.formats[pkt.PayloadType] + if !ok { + cm.onPacketRTPDecodeError(liberrors.ErrClientRTPPacketUnknownPayloadType{PayloadType: pkt.PayloadType}) + return false + } + + forma.readPacketRTPUDP(pkt) + + return true +} + +func (cm *clientMedia) readPacketRTCPUDPPlay(payload []byte) bool { + atomic.AddUint64(cm.bytesReceived, uint64(len(payload))) + + if len(payload) == (udpMaxPayloadSize + 1) { + cm.onPacketRTCPDecodeError(liberrors.ErrClientRTCPPacketTooBigUDP{}) + return false + } + + packets, err := rtcp.Unmarshal(payload) + if err != nil { + cm.onPacketRTCPDecodeError(err) + return false + } + + now := cm.c.timeNow() + + atomic.AddUint64(cm.rtcpPacketsReceived, uint64(len(packets))) + + for _, pkt := range packets { + if sr, ok := pkt.(*rtcp.SenderReport); ok { + format := cm.findFormatWithSSRC(sr.SSRC) + if format != nil { + format.rtcpReceiver.ProcessSenderReport(sr, now) + } + } + + cm.onPacketRTCP(pkt) + } + + return true +} + +func (cm *clientMedia) readPacketRTPUDPRecord(_ []byte) bool { + return false +} + +func (cm *clientMedia) readPacketRTCPUDPRecord(payload []byte) bool { + atomic.AddUint64(cm.bytesReceived, uint64(len(payload))) + + if len(payload) == (udpMaxPayloadSize + 1) { + cm.onPacketRTCPDecodeError(liberrors.ErrClientRTCPPacketTooBigUDP{}) + return false + } + + packets, err := rtcp.Unmarshal(payload) + if err != nil { + cm.onPacketRTCPDecodeError(err) + return false + } + + atomic.AddUint64(cm.rtcpPacketsReceived, uint64(len(packets))) + + for _, pkt := range packets { + cm.onPacketRTCP(pkt) + } + + return true +} + +func (cm *clientMedia) onPacketRTPDecodeError(err error) { + atomic.AddUint64(cm.rtpPacketsInError, 1) + cm.c.OnDecodeError(err) +} + +func (cm *clientMedia) onPacketRTCPDecodeError(err error) { + atomic.AddUint64(cm.rtcpPacketsInError, 1) + cm.c.OnDecodeError(err) +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/client_reader.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/client_reader.go new file mode 100644 index 000000000..f64d115c2 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/client_reader.go @@ -0,0 +1,79 @@ +package gortsplib + +import ( + "sync" + + "github.com/bluenviron/gortsplib/v4/pkg/base" + "github.com/bluenviron/gortsplib/v4/pkg/liberrors" +) + +type clientReader struct { + c *Client + + mutex sync.Mutex + allowInterleavedFrames bool + + chResponse chan *base.Response + chRequest chan *base.Request + chError chan error +} + +func (r *clientReader) start() { + r.chResponse = make(chan *base.Response) + r.chRequest = make(chan *base.Request) + r.chError = make(chan error) + + go r.run() +} + +func (r *clientReader) setAllowInterleavedFrames(v bool) { + r.mutex.Lock() + defer r.mutex.Unlock() + r.allowInterleavedFrames = v +} + +func (r *clientReader) wait() { + for { + select { + case <-r.chError: + return + + case <-r.chResponse: + case <-r.chRequest: + } + } +} + +func (r *clientReader) run() { + r.chError <- r.runInner() +} + +func (r *clientReader) runInner() error { + for { + what, err := r.c.conn.Read() + if err != nil { + return err + } + + switch what := what.(type) { + case *base.Response: + r.chResponse <- what + + case *base.Request: + r.chRequest <- what + + case *base.InterleavedFrame: + r.mutex.Lock() + + if !r.allowInterleavedFrames { + r.mutex.Unlock() + return liberrors.ErrClientUnexpectedFrame{} + } + + if cb, ok := r.c.tcpCallbackByChannel[what.Channel]; ok { + cb(what.Payload) + } + r.mutex.Unlock() + } + } +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/client_stats.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/client_stats.go new file mode 100644 index 000000000..22451ea3f --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/client_stats.go @@ -0,0 +1,7 @@ +package gortsplib + +// ClientStats are client statistics +type ClientStats struct { + Conn StatsConn + Session StatsSession +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/client_udp_listener.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/client_udp_listener.go new file mode 100644 index 000000000..0c7bb2dec --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/client_udp_listener.go @@ -0,0 +1,188 @@ +package gortsplib + +import ( + "crypto/rand" + "math/big" + "net" + "strconv" + "sync/atomic" + "time" + + "github.com/bluenviron/gortsplib/v4/pkg/multicast" +) + +func int64Ptr(v int64) *int64 { + return &v +} + +func randInRange(maxVal int) (int, error) { + b := big.NewInt(int64(maxVal + 1)) + n, err := rand.Int(rand.Reader, b) + if err != nil { + return 0, err + } + return int(n.Int64()), nil +} + +func createUDPListenerPair(c *Client) (*clientUDPListener, *clientUDPListener, error) { + // choose two consecutive ports in range 65535-10000 + // RTP port must be even and RTCP port odd + for { + v, err := randInRange((65535 - 10000) / 2) + if err != nil { + return nil, nil, err + } + + rtpPort := v*2 + 10000 + rtcpPort := rtpPort + 1 + + rtpListener := &clientUDPListener{ + c: c, + multicastEnable: false, + multicastSourceIP: nil, + address: net.JoinHostPort("", strconv.FormatInt(int64(rtpPort), 10)), + } + err = rtpListener.initialize() + if err != nil { + continue + } + + rtcpListener := &clientUDPListener{ + c: c, + multicastEnable: false, + multicastSourceIP: nil, + address: net.JoinHostPort("", strconv.FormatInt(int64(rtcpPort), 10)), + } + err = rtcpListener.initialize() + if err != nil { + rtpListener.close() + continue + } + + return rtpListener, rtcpListener, nil + } +} + +type packetConn interface { + net.PacketConn + SetReadBuffer(int) error +} + +type clientUDPListener struct { + c *Client + multicastEnable bool + multicastSourceIP net.IP + address string + + pc packetConn + readFunc readFunc + readIP net.IP + readPort int + writeAddr *net.UDPAddr + + running bool + lastPacketTime *int64 + + done chan struct{} +} + +func (u *clientUDPListener) initialize() error { + if u.multicastEnable { + intf, err := multicast.InterfaceForSource(u.multicastSourceIP) + if err != nil { + return err + } + + u.pc, err = multicast.NewSingleConn(intf, u.address, u.c.ListenPacket) + if err != nil { + return err + } + } else { + tmp, err := u.c.ListenPacket(restrictNetwork("udp", u.address)) + if err != nil { + return err + } + u.pc = tmp.(*net.UDPConn) + } + + err := u.pc.SetReadBuffer(udpKernelReadBufferSize) + if err != nil { + u.pc.Close() + return err + } + + u.lastPacketTime = int64Ptr(0) + return nil +} + +func (u *clientUDPListener) close() { + if u.running { + u.stop() + } + u.pc.Close() +} + +func (u *clientUDPListener) port() int { + return u.pc.LocalAddr().(*net.UDPAddr).Port +} + +func (u *clientUDPListener) start() { + u.running = true + u.pc.SetReadDeadline(time.Time{}) + u.done = make(chan struct{}) + go u.run() +} + +func (u *clientUDPListener) stop() { + u.pc.SetReadDeadline(time.Now()) + <-u.done + u.running = false +} + +func (u *clientUDPListener) run() { + defer close(u.done) + + var buf []byte + + createNewBuffer := func() { + buf = make([]byte, udpMaxPayloadSize+1) + } + + createNewBuffer() + + for { + n, addr, err := u.pc.ReadFrom(buf) + if err != nil { + return + } + + uaddr := addr.(*net.UDPAddr) + + if !u.readIP.Equal(uaddr.IP) { + continue + } + + // in case of anyPortEnable, store the port of the first packet we receive. + // this reduces security issues + if u.c.AnyPortEnable && u.readPort == 0 { + u.readPort = uaddr.Port + } else if u.readPort != uaddr.Port { + continue + } + + now := u.c.timeNow() + atomic.StoreInt64(u.lastPacketTime, now.Unix()) + + if u.readFunc(buf[:n]) { + createNewBuffer() + } + } +} + +func (u *clientUDPListener) write(payload []byte) error { + // no mutex is needed here since Write() has an internal lock. + // https://github.com/golang/go/issues/27203#issuecomment-534386117 + u.pc.SetWriteDeadline(time.Now().Add(u.c.WriteTimeout)) + _, err := u.pc.WriteTo(payload, u.writeAddr) + return err +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/constants.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/constants.go new file mode 100644 index 000000000..9aa3da013 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/constants.go @@ -0,0 +1,9 @@ +package gortsplib + +const ( + // same size as GStreamer's rtspsrc + udpKernelReadBufferSize = 0x80000 + + // 1500 (UDP MTU) - 20 (IP header) - 8 (UDP header) + udpMaxPayloadSize = 1472 +) diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/empty_timer.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/empty_timer.go new file mode 100644 index 000000000..a1e495aef --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/empty_timer.go @@ -0,0 +1,11 @@ +package gortsplib + +import ( + "time" +) + +func emptyTimer() *time.Timer { + t := time.NewTimer(0) + <-t.C + return t +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/auth/auth.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/auth/auth.go new file mode 100644 index 000000000..7a995fa7f --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/auth/auth.go @@ -0,0 +1,2 @@ +// Package auth contains utilities to perform authentication. +package auth diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/auth/nonce.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/auth/nonce.go new file mode 100644 index 000000000..5d41cb9e9 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/auth/nonce.go @@ -0,0 +1,17 @@ +package auth + +import ( + "crypto/rand" + "encoding/hex" +) + +// GenerateNonce generates a nonce that can be used in Validate(). +func GenerateNonce() (string, error) { + byts := make([]byte, 16) + _, err := rand.Read(byts) + if err != nil { + return "", err + } + + return hex.EncodeToString(byts), nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/auth/sender.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/auth/sender.go new file mode 100644 index 000000000..1efcf38fb --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/auth/sender.go @@ -0,0 +1,89 @@ +package auth + +import ( + "fmt" + + "github.com/bluenviron/gortsplib/v4/pkg/base" + "github.com/bluenviron/gortsplib/v4/pkg/headers" +) + +// NewSender allocates a Sender. +// +// Deprecated: replaced by Sender.Initialize(). +func NewSender(wwwAuth base.HeaderValue, user string, pass string) (*Sender, error) { + s := &Sender{ + WWWAuth: wwwAuth, + User: user, + Pass: pass, + } + err := s.Initialize() + return s, err +} + +// Sender allows to send credentials. +// It requires a WWW-Authenticate header (provided by the server) +// and a set of credentials. +type Sender struct { + WWWAuth base.HeaderValue + User string + Pass string + + authHeader *headers.Authenticate +} + +// Initialize initializes a Sender. +func (se *Sender) Initialize() error { + for _, v := range se.WWWAuth { + var auth headers.Authenticate + err := auth.Unmarshal(base.HeaderValue{v}) + if err != nil { + continue // ignore unrecognized headers + } + + if se.authHeader == nil || + (auth.Algorithm != nil && *auth.Algorithm == headers.AuthAlgorithmSHA256) || + (se.authHeader.Method == headers.AuthMethodBasic) { + se.authHeader = &auth + } + } + + if se.authHeader == nil { + return fmt.Errorf("no authentication methods available") + } + + return nil +} + +// AddAuthorization adds the Authorization header to a Request. +func (se *Sender) AddAuthorization(req *base.Request) { + urStr := req.URL.CloneWithoutCredentials().String() + + h := headers.Authorization{ + Method: se.authHeader.Method, + } + + h.Username = se.User + + if se.authHeader.Method == headers.AuthMethodBasic { + h.BasicPass = se.Pass + } else { // digest + h.Realm = se.authHeader.Realm + h.Nonce = se.authHeader.Nonce + h.URI = urStr + h.Algorithm = se.authHeader.Algorithm + + if se.authHeader.Algorithm == nil || *se.authHeader.Algorithm == headers.AuthAlgorithmMD5 { + h.Response = md5Hex(md5Hex(se.User+":"+se.authHeader.Realm+":"+se.Pass) + ":" + + se.authHeader.Nonce + ":" + md5Hex(string(req.Method)+":"+urStr)) + } else { // sha256 + h.Response = sha256Hex(sha256Hex(se.User+":"+se.authHeader.Realm+":"+se.Pass) + ":" + + se.authHeader.Nonce + ":" + sha256Hex(string(req.Method)+":"+urStr)) + } + } + + if req.Header == nil { + req.Header = make(base.Header) + } + + req.Header["Authorization"] = h.Marshal() +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/auth/validate.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/auth/validate.go new file mode 100644 index 000000000..23acc7013 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/auth/validate.go @@ -0,0 +1,33 @@ +package auth + +import ( + "github.com/bluenviron/gortsplib/v4/pkg/base" +) + +// ValidateMethod is a validation method. +// +// Deprecated: replaced by VerifyMethod +type ValidateMethod = VerifyMethod + +// validation methods. +// +// Deprecated. +const ( + ValidateMethodBasic = VerifyMethodBasic + ValidateMethodDigestMD5 = VerifyMethodDigestMD5 + ValidateMethodSHA256 = VerifyMethodDigestSHA256 +) + +// Validate validates a request sent by a client. +// +// Deprecated: replaced by Verify. +func Validate( + req *base.Request, + user string, + pass string, + methods []ValidateMethod, + realm string, + nonce string, +) error { + return Verify(req, user, pass, methods, realm, nonce) +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/auth/verify.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/auth/verify.go new file mode 100644 index 000000000..af298e6fa --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/auth/verify.go @@ -0,0 +1,135 @@ +package auth + +import ( + "crypto/md5" + "crypto/sha256" + "encoding/hex" + "fmt" + "regexp" + + "github.com/bluenviron/gortsplib/v4/pkg/base" + "github.com/bluenviron/gortsplib/v4/pkg/headers" +) + +var reControlAttribute = regexp.MustCompile("^(.+/)trackID=[0-9]+$") + +func md5Hex(in string) string { + h := md5.New() + h.Write([]byte(in)) + return hex.EncodeToString(h.Sum(nil)) +} + +func sha256Hex(in string) string { + h := sha256.New() + h.Write([]byte(in)) + return hex.EncodeToString(h.Sum(nil)) +} + +func contains(list []VerifyMethod, item VerifyMethod) bool { + for _, i := range list { + if i == item { + return true + } + } + return false +} + +func urlMatches(expected string, received string, isSetup bool) bool { + if received == expected { + return true + } + + // in SETUP requests, VLC uses the base URL of the stream + // instead of the URL of the track. + // Strip the control attribute to obtain the URL of the stream. + if isSetup { + if m := reControlAttribute.FindStringSubmatch(expected); m != nil && received == m[1] { + return true + } + } + + return false +} + +// VerifyMethod is a validation method. +type VerifyMethod int + +// validation methods. +const ( + VerifyMethodBasic VerifyMethod = iota + VerifyMethodDigestMD5 + VerifyMethodDigestSHA256 +) + +// Verify verifies a request sent by a client. +func Verify( + req *base.Request, + user string, + pass string, + methods []VerifyMethod, + realm string, + nonce string, +) error { + if methods == nil { + // disable VerifyMethodDigestSHA256 unless explicitly set + // since it prevents FFmpeg from authenticating + methods = []VerifyMethod{VerifyMethodBasic, VerifyMethodDigestMD5} + } + + var auth headers.Authorization + err := auth.Unmarshal(req.Header["Authorization"]) + if err != nil { + return err + } + + switch { + case auth.Method == headers.AuthMethodDigest && + (contains(methods, VerifyMethodDigestMD5) && + (auth.Algorithm == nil || *auth.Algorithm == headers.AuthAlgorithmMD5) || + contains(methods, VerifyMethodDigestSHA256) && + auth.Algorithm != nil && *auth.Algorithm == headers.AuthAlgorithmSHA256): + if auth.Nonce != nonce { + return fmt.Errorf("wrong nonce") + } + + if auth.Realm != realm { + return fmt.Errorf("wrong realm") + } + + if auth.Username != user { + return fmt.Errorf("authentication failed") + } + + if !urlMatches(req.URL.String(), auth.URI, req.Method == base.Setup) { + return fmt.Errorf("wrong URL") + } + + var response string + + if auth.Algorithm == nil || *auth.Algorithm == headers.AuthAlgorithmMD5 { + response = md5Hex(md5Hex(user+":"+realm+":"+pass) + + ":" + nonce + ":" + md5Hex(string(req.Method)+":"+auth.URI)) + } else { // sha256 + response = sha256Hex(sha256Hex(user+":"+realm+":"+pass) + + ":" + nonce + ":" + sha256Hex(string(req.Method)+":"+auth.URI)) + } + + if auth.Response != response { + return fmt.Errorf("authentication failed") + } + + case auth.Method == headers.AuthMethodBasic && contains(methods, VerifyMethodBasic): + if auth.Username != user { + return fmt.Errorf("authentication failed") + } + + if auth.BasicPass != pass { + return fmt.Errorf("authentication failed") + } + + default: + return fmt.Errorf("no supported authentication methods found") + } + + return nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/auth/www_authenticate.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/auth/www_authenticate.go new file mode 100644 index 000000000..076f3d78a --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/auth/www_authenticate.go @@ -0,0 +1,51 @@ +package auth + +import ( + "github.com/bluenviron/gortsplib/v4/pkg/base" + "github.com/bluenviron/gortsplib/v4/pkg/headers" +) + +// GenerateWWWAuthenticate generates a WWW-Authenticate header. +func GenerateWWWAuthenticate(methods []ValidateMethod, realm string, nonce string) base.HeaderValue { + if methods == nil { + // disable VerifyMethodDigestSHA256 unless explicitly set + // since it prevents FFmpeg from authenticating + methods = []VerifyMethod{VerifyMethodBasic, VerifyMethodDigestMD5} + } + + var ret base.HeaderValue + + for _, m := range methods { + var a base.HeaderValue + + switch m { + case ValidateMethodBasic: + a = headers.Authenticate{ + Method: headers.AuthMethodBasic, + Realm: realm, + }.Marshal() + + case ValidateMethodDigestMD5: + aa := headers.AuthAlgorithmMD5 + a = headers.Authenticate{ + Method: headers.AuthMethodDigest, + Realm: realm, + Nonce: nonce, + Algorithm: &aa, + }.Marshal() + + default: // sha256 + aa := headers.AuthAlgorithmSHA256 + a = headers.Authenticate{ + Method: headers.AuthMethodDigest, + Realm: realm, + Nonce: nonce, + Algorithm: &aa, + }.Marshal() + } + + ret = append(ret, a...) + } + + return ret +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/base/body.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/base/body.go new file mode 100644 index 000000000..cc47f0356 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/base/body.go @@ -0,0 +1,54 @@ +package base + +import ( + "bufio" + "fmt" + "io" + "strconv" +) + +const ( + rtspMaxContentLength = 128 * 1024 +) + +type body []byte + +func (b *body) unmarshal(header Header, rb *bufio.Reader) error { + cls, ok := header["Content-Length"] + if !ok || len(cls) != 1 { + *b = nil + return nil + } + + cl, err := strconv.ParseUint(cls[0], 10, 64) + if err != nil { + return fmt.Errorf("invalid Content-Length") + } + + if cl > rtspMaxContentLength { + return fmt.Errorf("Content-Length exceeds %d (it's %d)", + rtspMaxContentLength, cl) + } + + *b = make([]byte, cl) + n, err := io.ReadFull(rb, *b) + if err != nil && n != len(*b) { + return err + } + + return nil +} + +func (b body) marshalSize() int { + return len(b) +} + +func (b body) marshalTo(buf []byte) int { + return copy(buf, b) +} + +func (b body) marshal() []byte { + buf := make([]byte, b.marshalSize()) + b.marshalTo(buf) + return buf +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/base/header.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/base/header.go new file mode 100644 index 000000000..4e925fad2 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/base/header.go @@ -0,0 +1,148 @@ +package base + +import ( + "bufio" + "fmt" + "net/http" + "sort" + "strings" +) + +const ( + headerMaxEntryCount = 255 + headerMaxKeyLength = 512 + headerMaxValueLength = 2048 +) + +func headerKeyNormalize(in string) string { + switch strings.ToLower(in) { + case "rtp-info": + return "RTP-Info" + + case "www-authenticate": + return "WWW-Authenticate" + + case "cseq": + return "CSeq" + } + return http.CanonicalHeaderKey(in) +} + +// HeaderValue is an header value. +type HeaderValue []string + +// Header is a RTSP reader, present in both Requests and Responses. +type Header map[string]HeaderValue + +func (h *Header) unmarshal(br *bufio.Reader) error { + *h = make(Header) + count := 0 + + for { + byt, err := br.ReadByte() + if err != nil { + return err + } + + if byt == '\r' { + err = readByteEqual(br, '\n') + if err != nil { + return err + } + break + } + + if count >= headerMaxEntryCount { + return fmt.Errorf("headers count exceeds %d", headerMaxEntryCount) + } + + key := string([]byte{byt}) + byts, err := readBytesLimited(br, ':', headerMaxKeyLength-1) + if err != nil { + return fmt.Errorf("value is missing") + } + + key += string(byts[:len(byts)-1]) + key = headerKeyNormalize(key) + + // https://tools.ietf.org/html/rfc2616 + // The field value MAY be preceded by any amount of spaces + for { + byt, err = br.ReadByte() + if err != nil { + return err + } + + if byt != ' ' { + break + } + } + br.UnreadByte() //nolint:errcheck + + byts, err = readBytesLimited(br, '\r', headerMaxValueLength) + if err != nil { + return err + } + val := string(byts[:len(byts)-1]) + + err = readByteEqual(br, '\n') + if err != nil { + return err + } + + (*h)[key] = append((*h)[key], val) + count++ + } + + return nil +} + +func (h Header) marshalSize() int { + // sort headers by key + // in order to obtain deterministic results + keys := make([]string, len(h)) + for key := range h { + keys = append(keys, key) + } + sort.Strings(keys) + + n := 0 + + for _, key := range keys { + for _, val := range h[key] { + n += len([]byte(key + ": " + val + "\r\n")) + } + } + + n += 2 + + return n +} + +func (h Header) marshalTo(buf []byte) int { + // sort headers by key + // in order to obtain deterministic results + keys := make([]string, len(h)) + for key := range h { + keys = append(keys, key) + } + sort.Strings(keys) + + pos := 0 + + for _, key := range keys { + for _, val := range h[key] { + pos += copy(buf[pos:], []byte(key+": "+val+"\r\n")) + } + } + + pos += copy(buf[pos:], []byte("\r\n")) + + return pos +} + +func (h Header) marshal() []byte { + buf := make([]byte, h.marshalSize()) + h.marshalTo(buf) + return buf +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/base/interleaved_frame.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/base/interleaved_frame.go new file mode 100644 index 000000000..88aee8d0a --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/base/interleaved_frame.go @@ -0,0 +1,72 @@ +package base + +import ( + "bufio" + "fmt" + "io" +) + +const ( + // InterleavedFrameMagicByte is the first byte of an interleaved frame. + InterleavedFrameMagicByte = 0x24 +) + +// InterleavedFrame is an interleaved frame, and allows to transfer binary data +// within RTSP/TCP connections. It is used to send and receive RTP and RTCP packets with TCP. +type InterleavedFrame struct { + // channel ID + Channel int + + // payload + Payload []byte +} + +// Unmarshal decodes an interleaved frame. +func (f *InterleavedFrame) Unmarshal(br *bufio.Reader) error { + var header [4]byte + _, err := io.ReadFull(br, header[:]) + if err != nil { + return err + } + + if header[0] != InterleavedFrameMagicByte { + return fmt.Errorf("invalid magic byte (0x%.2x)", header[0]) + } + + // it's useless to check payloadLen since it's limited to 65535 + payloadLen := int(uint16(header[2])<<8 | uint16(header[3])) + + f.Channel = int(header[1]) + f.Payload = make([]byte, payloadLen) + + _, err = io.ReadFull(br, f.Payload) + return err +} + +// MarshalSize returns the size of an InterleavedFrame. +func (f InterleavedFrame) MarshalSize() int { + return 4 + len(f.Payload) +} + +// MarshalTo writes an InterleavedFrame. +func (f InterleavedFrame) MarshalTo(buf []byte) (int, error) { + pos := 0 + + pos += copy(buf[pos:], []byte{0x24, byte(f.Channel)}) + + payloadLen := len(f.Payload) + buf[pos] = byte(payloadLen >> 8) + buf[pos+1] = byte(payloadLen) + pos += 2 + + pos += copy(buf[pos:], f.Payload) + + return pos, nil +} + +// Marshal writes an InterleavedFrame. +func (f InterleavedFrame) Marshal() ([]byte, error) { + buf := make([]byte, f.MarshalSize()) + _, err := f.MarshalTo(buf) + return buf, err +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/base/path.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/base/path.go new file mode 100644 index 000000000..537adc2c1 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/base/path.go @@ -0,0 +1,17 @@ +package base + +import ( + "strings" +) + +// PathSplitQuery splits a path from a query. +// +// Deprecated: not useful anymore. +func PathSplitQuery(pathAndQuery string) (string, string) { + i := strings.Index(pathAndQuery, "?") + if i >= 0 { + return pathAndQuery[:i], pathAndQuery[i+1:] + } + + return pathAndQuery, "" +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/base/request.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/base/request.go new file mode 100644 index 000000000..eb5c5a386 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/base/request.go @@ -0,0 +1,173 @@ +// Package base contains the primitives of the RTSP protocol. +package base + +import ( + "bufio" + "fmt" + "strconv" +) + +const ( + rtspProtocol10 = "RTSP/1.0" + requestMaxMethodLength = 64 + requestMaxURLLength = 2048 + requestMaxProtocolLength = 64 +) + +// Method is the method of a RTSP request. +type Method string + +// methods. +const ( + Announce Method = "ANNOUNCE" + Describe Method = "DESCRIBE" + GetParameter Method = "GET_PARAMETER" + Options Method = "OPTIONS" + Pause Method = "PAUSE" + Play Method = "PLAY" + Record Method = "RECORD" + Setup Method = "SETUP" + SetParameter Method = "SET_PARAMETER" + Teardown Method = "TEARDOWN" +) + +// Request is a RTSP request. +type Request struct { + // request method + Method Method + + // request url + URL *URL + + // map of header values + Header Header + + // optional body + Body []byte +} + +// Unmarshal reads a request. +func (req *Request) Unmarshal(br *bufio.Reader) error { + byts, err := readBytesLimited(br, ' ', requestMaxMethodLength) + if err != nil { + return err + } + req.Method = Method(byts[:len(byts)-1]) + + if req.Method == "" { + return fmt.Errorf("empty method") + } + + byts, err = readBytesLimited(br, ' ', requestMaxURLLength) + if err != nil { + return err + } + rawURL := string(byts[:len(byts)-1]) + + if rawURL != "*" { + var ur *URL + ur, err = ParseURL(rawURL) + if err != nil { + return fmt.Errorf("invalid URL (%v)", rawURL) + } + req.URL = ur + } else { + req.URL = nil + } + + byts, err = readBytesLimited(br, '\r', requestMaxProtocolLength) + if err != nil { + return err + } + proto := byts[:len(byts)-1] + + if string(proto) != rtspProtocol10 { + return fmt.Errorf("expected '%s', got %v", rtspProtocol10, proto) + } + + err = readByteEqual(br, '\n') + if err != nil { + return err + } + + err = req.Header.unmarshal(br) + if err != nil { + return err + } + + err = (*body)(&req.Body).unmarshal(req.Header, br) + if err != nil { + return err + } + + return nil +} + +// MarshalSize returns the size of a Request. +func (req Request) MarshalSize() int { + n := len(req.Method) + 1 + + if req.URL != nil { + n += len(req.URL.CloneWithoutCredentials().String()) + } else { + n++ + } + + n += 1 + len(rtspProtocol10) + 2 + + if len(req.Body) != 0 { + req.Header["Content-Length"] = HeaderValue{strconv.FormatInt(int64(len(req.Body)), 10)} + } + + n += req.Header.marshalSize() + + n += body(req.Body).marshalSize() + + return n +} + +// MarshalTo writes a Request. +func (req Request) MarshalTo(buf []byte) (int, error) { + pos := 0 + + pos += copy(buf[pos:], []byte(req.Method)) + buf[pos] = ' ' + pos++ + + if req.URL != nil { + pos += copy(buf[pos:], []byte(req.URL.CloneWithoutCredentials().String())) + } else { + pos += copy(buf[pos:], []byte("*")) + } + + buf[pos] = ' ' + pos++ + pos += copy(buf[pos:], rtspProtocol10) + buf[pos] = '\r' + pos++ + buf[pos] = '\n' + pos++ + + if len(req.Body) != 0 { + req.Header["Content-Length"] = HeaderValue{strconv.FormatInt(int64(len(req.Body)), 10)} + } + + pos += req.Header.marshalTo(buf[pos:]) + + pos += body(req.Body).marshalTo(buf[pos:]) + + return pos, nil +} + +// Marshal writes a Request. +func (req Request) Marshal() ([]byte, error) { + buf := make([]byte, req.MarshalSize()) + _, err := req.MarshalTo(buf) + return buf, err +} + +// String implements fmt.Stringer. +func (req Request) String() string { + buf, _ := req.Marshal() + return string(buf) +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/base/response.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/base/response.go new file mode 100644 index 000000000..8f6b4ced3 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/base/response.go @@ -0,0 +1,254 @@ +package base + +import ( + "bufio" + "fmt" + "strconv" +) + +// StatusCode is the status code of a RTSP response. +type StatusCode int + +// status codes. +const ( + StatusContinue StatusCode = 100 + StatusOK StatusCode = 200 + StatusMovedPermanently StatusCode = 301 + StatusFound StatusCode = 302 + StatusSeeOther StatusCode = 303 + StatusNotModified StatusCode = 304 + StatusUseProxy StatusCode = 305 + StatusBadRequest StatusCode = 400 + StatusUnauthorized StatusCode = 401 + StatusPaymentRequired StatusCode = 402 + StatusForbidden StatusCode = 403 + StatusNotFound StatusCode = 404 + StatusMethodNotAllowed StatusCode = 405 + StatusNotAcceptable StatusCode = 406 + StatusProxyAuthRequired StatusCode = 407 + StatusRequestTimeout StatusCode = 408 + StatusGone StatusCode = 410 + StatusPreconditionFailed StatusCode = 412 + StatusRequestEntityTooLarge StatusCode = 413 + StatusRequestURITooLong StatusCode = 414 + StatusUnsupportedMediaType StatusCode = 415 + StatusParameterNotUnderstood StatusCode = 451 + StatusNotEnoughBandwidth StatusCode = 453 + StatusSessionNotFound StatusCode = 454 + StatusMethodNotValidInThisState StatusCode = 455 + StatusHeaderFieldNotValidForResource StatusCode = 456 + StatusInvalidRange StatusCode = 457 + StatusParameterIsReadOnly StatusCode = 458 + StatusAggregateOperationNotAllowed StatusCode = 459 + StatusOnlyAggregateOperationAllowed StatusCode = 460 + StatusUnsupportedTransport StatusCode = 461 + StatusDestinationUnreachable StatusCode = 462 + StatusDestinationProhibited StatusCode = 463 + StatusDataTransportNotReadyYet StatusCode = 464 + StatusNotificationReasonUnknown StatusCode = 465 + StatusKeyManagementError StatusCode = 466 + StatusConnectionAuthorizationRequired StatusCode = 470 + StatusConnectionCredentialsNotAccepted StatusCode = 471 + StatusFailureToEstablishSecureConnection StatusCode = 472 + StatusInternalServerError StatusCode = 500 + StatusNotImplemented StatusCode = 501 + StatusBadGateway StatusCode = 502 + StatusServiceUnavailable StatusCode = 503 + StatusGatewayTimeout StatusCode = 504 + StatusRTSPVersionNotSupported StatusCode = 505 + StatusOptionNotSupported StatusCode = 551 + StatusProxyUnavailable StatusCode = 553 +) + +// StatusMessages contains the status messages associated with each status code. +var StatusMessages = statusMessages + +var statusMessages = map[StatusCode]string{ + StatusContinue: "Continue", + + StatusOK: "OK", + + StatusMovedPermanently: "Moved Permanently", + StatusFound: "Found", + StatusSeeOther: "See Other", + StatusNotModified: "Not Modified", + StatusUseProxy: "Use Proxy", + + StatusBadRequest: "Bad Request", + StatusUnauthorized: "Unauthorized", + StatusPaymentRequired: "Payment Required", + StatusForbidden: "Forbidden", + StatusNotFound: "Not Found", + StatusMethodNotAllowed: "Method Not Allowed", + StatusNotAcceptable: "Not Acceptable", + StatusProxyAuthRequired: "Proxy Auth Required", + StatusRequestTimeout: "Request Timeout", + StatusGone: "Gone", + StatusPreconditionFailed: "Precondition Failed", + StatusRequestEntityTooLarge: "Request Entity Too Large", + StatusRequestURITooLong: "Request URI Too Long", + StatusUnsupportedMediaType: "Unsupported Media Type", + StatusParameterNotUnderstood: "Parameter Not Understood", + StatusNotEnoughBandwidth: "Not Enough Bandwidth", + StatusSessionNotFound: "Session Not Found", + StatusMethodNotValidInThisState: "Method Not Valid In This State", + StatusHeaderFieldNotValidForResource: "Header Field Not Valid for Resource", + StatusInvalidRange: "Invalid Range", + StatusParameterIsReadOnly: "Parameter Is Read-Only", + StatusAggregateOperationNotAllowed: "Aggregate Operation Not Allowed", + StatusOnlyAggregateOperationAllowed: "Only Aggregate Operation Allowed", + StatusUnsupportedTransport: "Unsupported Transport", + StatusDestinationUnreachable: "Destination Unreachable", + StatusDestinationProhibited: "Destination Prohibited", + StatusDataTransportNotReadyYet: "Data Transport Not Ready Yet", + StatusNotificationReasonUnknown: "Notification Reason Unknown", + StatusKeyManagementError: "Key Management Error", + StatusConnectionAuthorizationRequired: "Connection Authorization Required", + StatusConnectionCredentialsNotAccepted: "Connection Credentials Not Accepted", + StatusFailureToEstablishSecureConnection: "Failure to Establish Secure Connection", + + StatusInternalServerError: "Internal Server Error", + StatusNotImplemented: "Not Implemented", + StatusBadGateway: "Bad Gateway", + StatusServiceUnavailable: "Service Unavailable", + StatusGatewayTimeout: "Gateway Timeout", + StatusRTSPVersionNotSupported: "RTSP Version Not Supported", + StatusOptionNotSupported: "Option Not Supported", + StatusProxyUnavailable: "Proxy Unavailable", +} + +// Response is a RTSP response. +type Response struct { + // numeric status code + StatusCode StatusCode + + // status message + StatusMessage string + + // map of header values + Header Header + + // optional body + Body []byte +} + +// Unmarshal reads a response. +func (res *Response) Unmarshal(br *bufio.Reader) error { + byts, err := readBytesLimited(br, ' ', 255) + if err != nil { + return err + } + proto := byts[:len(byts)-1] + + if string(proto) != rtspProtocol10 { + return fmt.Errorf("expected '%s', got %v", rtspProtocol10, proto) + } + + byts, err = readBytesLimited(br, ' ', 4) + if err != nil { + return err + } + statusCodeStr := string(byts[:len(byts)-1]) + + tmp, err := strconv.ParseUint(statusCodeStr, 10, 31) + if err != nil { + return fmt.Errorf("unable to parse status code") + } + res.StatusCode = StatusCode(tmp) + + byts, err = readBytesLimited(br, '\r', 255) + if err != nil { + return err + } + res.StatusMessage = string(byts[:len(byts)-1]) + + if len(res.StatusMessage) == 0 { + return fmt.Errorf("empty status message") + } + + err = readByteEqual(br, '\n') + if err != nil { + return err + } + + err = res.Header.unmarshal(br) + if err != nil { + return err + } + + err = (*body)(&res.Body).unmarshal(res.Header, br) + if err != nil { + return err + } + + return nil +} + +// MarshalSize returns the size of a Response. +func (res Response) MarshalSize() int { + n := 0 + + if res.StatusMessage == "" { + if status, ok := statusMessages[res.StatusCode]; ok { + res.StatusMessage = status + } + } + + n += len(rtspProtocol10) + 1 + len(strconv.FormatInt(int64(res.StatusCode), 10)) + 1 + len(res.StatusMessage) + 2 + + if len(res.Body) != 0 { + res.Header["Content-Length"] = HeaderValue{strconv.FormatInt(int64(len(res.Body)), 10)} + } + + n += res.Header.marshalSize() + + n += body(res.Body).marshalSize() + + return n +} + +// MarshalTo writes a Response. +func (res Response) MarshalTo(buf []byte) (int, error) { + if res.StatusMessage == "" { + if status, ok := statusMessages[res.StatusCode]; ok { + res.StatusMessage = status + } + } + + pos := 0 + + pos += copy(buf[pos:], []byte(rtspProtocol10)) + buf[pos] = ' ' + pos++ + pos += copy(buf[pos:], []byte(strconv.FormatInt(int64(res.StatusCode), 10))) + buf[pos] = ' ' + pos++ + pos += copy(buf[pos:], []byte(res.StatusMessage)) + buf[pos] = '\r' + pos++ + buf[pos] = '\n' + pos++ + + if len(res.Body) != 0 { + res.Header["Content-Length"] = HeaderValue{strconv.FormatInt(int64(len(res.Body)), 10)} + } + + pos += res.Header.marshalTo(buf[pos:]) + + pos += body(res.Body).marshalTo(buf[pos:]) + + return pos, nil +} + +// Marshal writes a Response. +func (res Response) Marshal() ([]byte, error) { + buf := make([]byte, res.MarshalSize()) + _, err := res.MarshalTo(buf) + return buf, err +} + +// String implements fmt.Stringer. +func (res Response) String() string { + buf, _ := res.Marshal() + return string(buf) +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/base/url.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/base/url.go new file mode 100644 index 000000000..5f9154532 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/base/url.go @@ -0,0 +1,107 @@ +package base + +import ( + "fmt" + "net/url" + "regexp" + "strings" +) + +// URL is a RTSP URL. +// This is basically an HTTP URL with some additional functions to handle +// control attributes. +type URL url.URL + +var escapeRegexp = regexp.MustCompile(`^(.+?)://(.*?)@(.*?)/(.*?)$`) + +// ParseURL parses a RTSP URL. +func ParseURL(s string) (*URL, error) { + // https://github.com/golang/go/issues/30611 + m := escapeRegexp.FindStringSubmatch(s) + if m != nil { + m[3] = strings.ReplaceAll(m[3], "%25", "%") + m[3] = strings.ReplaceAll(m[3], "%", "%25") + s = m[1] + "://" + m[2] + "@" + m[3] + "/" + m[4] + } + + u, err := url.Parse(s) + if err != nil { + return nil, err + } + + if u.Scheme != "rtsp" && u.Scheme != "rtsps" { + return nil, fmt.Errorf("unsupported scheme '%s'", u.Scheme) + } + + if u.Opaque != "" { + return nil, fmt.Errorf("URLs with opaque data are not supported") + } + + if u.Fragment != "" { + return nil, fmt.Errorf("URLs with fragments are not supported") + } + + return (*URL)(u), nil +} + +// String implements fmt.Stringer. +func (u *URL) String() string { + return (*url.URL)(u).String() +} + +// Clone clones a URL. +func (u *URL) Clone() *URL { + return (*URL)(&url.URL{ + Scheme: u.Scheme, + User: u.User, + Host: u.Host, + Path: u.Path, + RawPath: u.RawPath, + ForceQuery: u.ForceQuery, + RawQuery: u.RawQuery, + }) +} + +// CloneWithoutCredentials clones a URL without its credentials. +func (u *URL) CloneWithoutCredentials() *URL { + return (*URL)(&url.URL{ + Scheme: u.Scheme, + Host: u.Host, + Path: u.Path, + RawPath: u.RawPath, + ForceQuery: u.ForceQuery, + RawQuery: u.RawQuery, + }) +} + +// RTSPPathAndQuery returns the path and query of a RTSP URL. +// +// Deprecated: not useful anymore. +func (u *URL) RTSPPathAndQuery() (string, bool) { + var pathAndQuery string + if u.RawPath != "" { + pathAndQuery = u.RawPath + } else { + pathAndQuery = u.Path + } + if u.RawQuery != "" { + pathAndQuery += "?" + u.RawQuery + } + + return pathAndQuery, true +} + +// Hostname returns u.Host, stripping any valid port number if present. +// +// If the result is enclosed in square brackets, as literal IPv6 addresses are, +// the square brackets are removed from the result. +func (u *URL) Hostname() string { + return (*url.URL)(u).Hostname() +} + +// Port returns the port part of u.Host, without the leading colon. +// +// If u.Host doesn't contain a valid numeric port, Port returns an empty string. +func (u *URL) Port() string { + return (*url.URL)(u).Port() +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/base/utils.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/base/utils.go new file mode 100644 index 000000000..d0c43dc2b --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/base/utils.go @@ -0,0 +1,34 @@ +package base + +import ( + "bufio" + "fmt" +) + +func readByteEqual(rb *bufio.Reader, cmp byte) error { + byt, err := rb.ReadByte() + if err != nil { + return err + } + + if byt != cmp { + return fmt.Errorf("expected '%c', got '%c'", cmp, byt) + } + + return nil +} + +func readBytesLimited(rb *bufio.Reader, delim byte, n int) ([]byte, error) { + for i := 1; i <= n; i++ { + byts, err := rb.Peek(i) + if err != nil { + return nil, err + } + + if byts[len(byts)-1] == delim { + rb.Discard(len(byts)) //nolint:errcheck + return byts, nil + } + } + return nil, fmt.Errorf("buffer length exceeds %d", n) +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/bytecounter/bytecounter.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/bytecounter/bytecounter.go new file mode 100644 index 000000000..abd59d97f --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/bytecounter/bytecounter.go @@ -0,0 +1,54 @@ +// Package bytecounter contains a io.ReadWriter wrapper that allows to count read and written bytes. +package bytecounter + +import ( + "io" + "sync/atomic" +) + +// ByteCounter is a io.ReadWriter wrapper that allows to count read and written bytes. +type ByteCounter struct { + rw io.ReadWriter + received *uint64 + sent *uint64 +} + +// New allocates a ByteCounter. +func New(rw io.ReadWriter, received *uint64, sent *uint64) *ByteCounter { + if received == nil { + received = new(uint64) + } + if sent == nil { + sent = new(uint64) + } + + return &ByteCounter{ + rw: rw, + received: received, + sent: sent, + } +} + +// Read implements io.ReadWriter. +func (bc *ByteCounter) Read(p []byte) (int, error) { + n, err := bc.rw.Read(p) + atomic.AddUint64(bc.received, uint64(n)) + return n, err +} + +// Write implements io.ReadWriter. +func (bc *ByteCounter) Write(p []byte) (int, error) { + n, err := bc.rw.Write(p) + atomic.AddUint64(bc.sent, uint64(n)) + return n, err +} + +// BytesReceived returns the number of bytes received. +func (bc *ByteCounter) BytesReceived() uint64 { + return atomic.LoadUint64(bc.received) +} + +// BytesSent returns the number of bytes sent. +func (bc *ByteCounter) BytesSent() uint64 { + return atomic.LoadUint64(bc.sent) +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/conn/conn.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/conn/conn.go new file mode 100644 index 000000000..2fc08eec9 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/conn/conn.go @@ -0,0 +1,105 @@ +// Package conn contains a RTSP connection implementation. +package conn + +import ( + "bufio" + "io" + + "github.com/bluenviron/gortsplib/v4/pkg/base" +) + +const ( + readBufferSize = 4096 +) + +// Conn is a RTSP connection. +type Conn struct { + w io.Writer + br *bufio.Reader + + // reuse interleaved frames. they should never be passed to secondary routines + fr base.InterleavedFrame +} + +// NewConn allocates a Conn. +func NewConn(rw io.ReadWriter) *Conn { + return &Conn{ + w: rw, + br: bufio.NewReaderSize(rw, readBufferSize), + } +} + +// Read reads a Request, a Response or an Interleaved frame. +func (c *Conn) Read() (interface{}, error) { + for { + byts, err := c.br.Peek(2) + if err != nil { + return nil, err + } + + if byts[0] == base.InterleavedFrameMagicByte { + return c.ReadInterleavedFrame() + } + + if byts[0] == 'R' && byts[1] == 'T' { + return c.ReadResponse() + } + + if (byts[0] == 'A' && byts[1] == 'N') || + (byts[0] == 'D' && byts[1] == 'E') || + (byts[0] == 'G' && byts[1] == 'E') || + (byts[0] == 'O' && byts[1] == 'P') || + (byts[0] == 'P' && byts[1] == 'A') || + (byts[0] == 'P' && byts[1] == 'L') || + (byts[0] == 'R' && byts[1] == 'E') || + (byts[0] == 'S' && byts[1] == 'E') || + (byts[0] == 'T' && byts[1] == 'E') { + return c.ReadRequest() + } + + if _, err := c.br.Discard(1); err != nil { + return nil, err + } + } +} + +// ReadRequest reads a Request. +func (c *Conn) ReadRequest() (*base.Request, error) { + var req base.Request + err := req.Unmarshal(c.br) + return &req, err +} + +// ReadResponse reads a Response. +func (c *Conn) ReadResponse() (*base.Response, error) { + var res base.Response + err := res.Unmarshal(c.br) + return &res, err +} + +// ReadInterleavedFrame reads a InterleavedFrame. +func (c *Conn) ReadInterleavedFrame() (*base.InterleavedFrame, error) { + err := c.fr.Unmarshal(c.br) + return &c.fr, err +} + +// WriteRequest writes a request. +func (c *Conn) WriteRequest(req *base.Request) error { + buf, _ := req.Marshal() + _, err := c.w.Write(buf) + return err +} + +// WriteResponse writes a response. +func (c *Conn) WriteResponse(res *base.Response) error { + buf, _ := res.Marshal() + _, err := c.w.Write(buf) + return err +} + +// WriteInterleavedFrame writes an interleaved frame. +func (c *Conn) WriteInterleavedFrame(fr *base.InterleavedFrame, buf []byte) error { + n, _ := fr.MarshalTo(buf) + _, err := c.w.Write(buf[:n]) + return err +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/description/media.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/description/media.go new file mode 100644 index 000000000..2e2d9540c --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/description/media.go @@ -0,0 +1,218 @@ +// Package description contains objects to describe streams. +package description + +import ( + "fmt" + "reflect" + "sort" + "strconv" + "strings" + "unicode" + + psdp "github.com/pion/sdp/v3" + + "github.com/bluenviron/gortsplib/v4/pkg/base" + "github.com/bluenviron/gortsplib/v4/pkg/format" +) + +func getAttribute(attributes []psdp.Attribute, key string) string { + for _, attr := range attributes { + if attr.Key == key { + return attr.Value + } + } + return "" +} + +func isBackChannel(attributes []psdp.Attribute) bool { + for _, attr := range attributes { + if attr.Key == "sendonly" { + return true + } + } + return false +} + +func sortedKeys(fmtp map[string]string) []string { + keys := make([]string, len(fmtp)) + i := 0 + for key := range fmtp { + keys[i] = key + i++ + } + sort.Strings(keys) + return keys +} + +func isAlphaNumeric(v string) bool { + for _, r := range v { + if !unicode.IsLetter(r) && !unicode.IsNumber(r) { + return false + } + } + return true +} + +// MediaType is the type of a media stream. +type MediaType string + +// media types. +const ( + MediaTypeVideo MediaType = "video" + MediaTypeAudio MediaType = "audio" + MediaTypeApplication MediaType = "application" +) + +// Media is a media stream. +// It contains one or more formats. +type Media struct { + // Media type. + Type MediaType + + // Media ID (optional). + ID string + + // Whether this media is a back channel. + IsBackChannel bool + + // Control attribute. + Control string + + // Formats contained into the media. + Formats []format.Format +} + +// Unmarshal decodes the media from the SDP format. +func (m *Media) Unmarshal(md *psdp.MediaDescription) error { + m.Type = MediaType(md.MediaName.Media) + + m.ID = getAttribute(md.Attributes, "mid") + if m.ID != "" && !isAlphaNumeric(m.ID) { + return fmt.Errorf("invalid mid: %v", m.ID) + } + + m.IsBackChannel = isBackChannel(md.Attributes) + m.Control = getAttribute(md.Attributes, "control") + + m.Formats = nil + + for _, payloadType := range md.MediaName.Formats { + format, err := format.Unmarshal(md, payloadType) + if err != nil { + return err + } + + m.Formats = append(m.Formats, format) + } + + if m.Formats == nil { + return fmt.Errorf("no formats found") + } + + return nil +} + +// Marshal encodes the media in SDP format. +func (m Media) Marshal() *psdp.MediaDescription { + md := &psdp.MediaDescription{ + MediaName: psdp.MediaName{ + Media: string(m.Type), + Protos: []string{"RTP", "AVP"}, + }, + } + + if m.ID != "" { + md.Attributes = append(md.Attributes, psdp.Attribute{ + Key: "mid", + Value: m.ID, + }) + } + + if m.IsBackChannel { + md.Attributes = append(md.Attributes, psdp.Attribute{ + Key: "sendonly", + }) + } + + md.Attributes = append(md.Attributes, psdp.Attribute{ + Key: "control", + Value: m.Control, + }) + + for _, forma := range m.Formats { + typ := strconv.FormatUint(uint64(forma.PayloadType()), 10) + md.MediaName.Formats = append(md.MediaName.Formats, typ) + + rtpmap := forma.RTPMap() + if rtpmap != "" { + md.Attributes = append(md.Attributes, psdp.Attribute{ + Key: "rtpmap", + Value: typ + " " + rtpmap, + }) + } + + fmtp := forma.FMTP() + if len(fmtp) != 0 { + tmp := make([]string, len(fmtp)) + for i, key := range sortedKeys(fmtp) { + tmp[i] = key + "=" + fmtp[key] + } + + md.Attributes = append(md.Attributes, psdp.Attribute{ + Key: "fmtp", + Value: typ + " " + strings.Join(tmp, "; "), + }) + } + } + + return md +} + +// URL returns the absolute URL of the media. +func (m Media) URL(contentBase *base.URL) (*base.URL, error) { + if contentBase == nil { + return nil, fmt.Errorf("Content-Base header not provided") + } + + // no control attribute, use base URL + if m.Control == "" { + return contentBase, nil + } + + // control attribute contains an absolute path + if strings.HasPrefix(m.Control, "rtsp://") || + strings.HasPrefix(m.Control, "rtsps://") { + ur, err := base.ParseURL(m.Control) + if err != nil { + return nil, err + } + + // copy host and credentials + ur.Host = contentBase.Host + ur.User = contentBase.User + return ur, nil + } + + // control attribute contains a relative control attribute + // insert the control attribute at the end of the URL + // if there's a query, insert it after the query + // otherwise insert it after the path + strURL := contentBase.String() + if m.Control[0] != '?' && m.Control[0] != '/' && !strings.HasSuffix(strURL, "/") { + strURL += "/" + } + + ur, _ := base.ParseURL(strURL + m.Control) + return ur, nil +} + +// FindFormat finds a certain format among all the formats in the media. +func (m Media) FindFormat(forma interface{}) bool { + for _, formak := range m.Formats { + if reflect.TypeOf(formak) == reflect.TypeOf(forma).Elem() { + reflect.ValueOf(forma).Elem().Set(reflect.ValueOf(formak)) + return true + } + } + return false +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/description/session.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/description/session.go new file mode 100644 index 000000000..4a77f1cd8 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/description/session.go @@ -0,0 +1,164 @@ +package description + +import ( + "fmt" + "strings" + + psdp "github.com/pion/sdp/v3" + + "github.com/bluenviron/gortsplib/v4/pkg/base" + "github.com/bluenviron/gortsplib/v4/pkg/sdp" +) + +func atLeastOneHasMID(medias []*Media) bool { + for _, media := range medias { + if media.ID != "" { + return true + } + } + return false +} + +func atLeastOneDoesntHaveMID(medias []*Media) bool { + for _, media := range medias { + if media.ID == "" { + return true + } + } + return false +} + +func hasMediaWithID(medias []*Media, id string) bool { + for _, media := range medias { + if media.ID == id { + return true + } + } + return false +} + +// SessionFECGroup is a FEC group. +type SessionFECGroup []string + +// Session is the description of a RTSP stream. +type Session struct { + // Base URL of the stream (read only). + BaseURL *base.URL + + // Title of the stream (optional). + Title string + + // FEC groups (RFC5109). + FECGroups []SessionFECGroup + + // Media streams. + Medias []*Media +} + +// FindFormat finds a certain format among all the formats in all the medias of the stream. +// If the format is found, it is inserted into forma, and its media is returned. +func (d *Session) FindFormat(forma interface{}) *Media { + for _, media := range d.Medias { + ok := media.FindFormat(forma) + if ok { + return media + } + } + return nil +} + +// Unmarshal decodes the description from SDP. +func (d *Session) Unmarshal(ssd *sdp.SessionDescription) error { + d.Title = string(ssd.SessionName) + if d.Title == " " { + d.Title = "" + } + + d.Medias = make([]*Media, len(ssd.MediaDescriptions)) + + for i, md := range ssd.MediaDescriptions { + var m Media + err := m.Unmarshal(md) + if err != nil { + return fmt.Errorf("media %d is invalid: %w", i+1, err) + } + + if m.ID != "" && hasMediaWithID(d.Medias[:i], m.ID) { + return fmt.Errorf("duplicate media IDs") + } + + d.Medias[i] = &m + } + + if atLeastOneHasMID(d.Medias) && atLeastOneDoesntHaveMID(d.Medias) { + return fmt.Errorf("media IDs sent partially") + } + + for _, attr := range ssd.Attributes { + if attr.Key == "group" && strings.HasPrefix(attr.Value, "FEC ") { + group := SessionFECGroup(strings.Split(attr.Value[len("FEC "):], " ")) + + for _, id := range group { + if !hasMediaWithID(d.Medias, id) { + return fmt.Errorf("FEC group points to an invalid media ID: %v", id) + } + } + + d.FECGroups = append(d.FECGroups, group) + } + } + + return nil +} + +// Marshal encodes the description in SDP. +func (d Session) Marshal(multicast bool) ([]byte, error) { + var sessionName psdp.SessionName + if d.Title != "" { + sessionName = psdp.SessionName(d.Title) + } else { + // RFC 4566: If a session has no meaningful name, the + // value "s= " SHOULD be used (i.e., a single space as the session name). + sessionName = psdp.SessionName(" ") + } + + var address string + if multicast { + address = "224.1.0.0" + } else { + address = "0.0.0.0" + } + + sout := &sdp.SessionDescription{ + SessionName: sessionName, + Origin: psdp.Origin{ + Username: "-", + NetworkType: "IN", + AddressType: "IP4", + UnicastAddress: "127.0.0.1", + }, + // required by Darwin Streaming Server + ConnectionInformation: &psdp.ConnectionInformation{ + NetworkType: "IN", + AddressType: "IP4", + Address: &psdp.Address{Address: address}, + }, + TimeDescriptions: []psdp.TimeDescription{ + {Timing: psdp.Timing{StartTime: 0, StopTime: 0}}, + }, + MediaDescriptions: make([]*psdp.MediaDescription, len(d.Medias)), + } + + for i, media := range d.Medias { + sout.MediaDescriptions[i] = media.Marshal() + } + + for _, group := range d.FECGroups { + sout.Attributes = append(sout.Attributes, psdp.Attribute{ + Key: "group", + Value: "FEC " + strings.Join(group, " "), + }) + } + + return sout.Marshal() +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/ac3.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/ac3.go new file mode 100644 index 000000000..3253202a9 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/ac3.go @@ -0,0 +1,101 @@ +package format + +import ( + "strconv" + "strings" + + "github.com/pion/rtp" + + "github.com/bluenviron/gortsplib/v4/pkg/format/rtpac3" +) + +// AC3 is the RTP format for the AC-3 codec. +// Specification: https://datatracker.ietf.org/doc/html/rfc4184 +type AC3 struct { + PayloadTyp uint8 + SampleRate int + ChannelCount int +} + +func (f *AC3) unmarshal(ctx *unmarshalContext) error { + f.PayloadTyp = ctx.payloadType + + tmp := strings.SplitN(ctx.clock, "/", 2) + + tmp1, err := strconv.ParseUint(tmp[0], 10, 31) + if err != nil { + return err + } + f.SampleRate = int(tmp1) + + if len(tmp) >= 2 { + tmp1, err := strconv.ParseUint(tmp[1], 10, 31) + if err != nil { + return err + } + f.ChannelCount = int(tmp1) + } else { + // RFC4184: If the "channels" parameter + // is omitted, a default maximum value of 6 is implied. + f.ChannelCount = 6 + } + + return nil +} + +// Codec implements Format. +func (f *AC3) Codec() string { + return "AC-3" +} + +// ClockRate implements Format. +func (f *AC3) ClockRate() int { + return f.SampleRate +} + +// PayloadType implements Format. +func (f *AC3) PayloadType() uint8 { + return f.PayloadTyp +} + +// RTPMap implements Format. +func (f *AC3) RTPMap() string { + return "AC3/" + strconv.FormatInt(int64(f.SampleRate), 10) + + "/" + strconv.FormatInt(int64(f.ChannelCount), 10) +} + +// FMTP implements Format. +func (f *AC3) FMTP() map[string]string { + return nil +} + +// PTSEqualsDTS implements Format. +func (f *AC3) PTSEqualsDTS(*rtp.Packet) bool { + return true +} + +// CreateDecoder creates a decoder able to decode the content of the format. +func (f *AC3) CreateDecoder() (*rtpac3.Decoder, error) { + d := &rtpac3.Decoder{} + + err := d.Init() + if err != nil { + return nil, err + } + + return d, nil +} + +// CreateEncoder creates an encoder able to encode the content of the format. +func (f *AC3) CreateEncoder() (*rtpac3.Encoder, error) { + e := &rtpac3.Encoder{ + PayloadType: f.PayloadTyp, + } + + err := e.Init() + if err != nil { + return nil, err + } + + return e, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/av1.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/av1.go new file mode 100644 index 000000000..7b311b1ec --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/av1.go @@ -0,0 +1,124 @@ +package format //nolint:dupl + +import ( + "fmt" + "strconv" + + "github.com/pion/rtp" + + "github.com/bluenviron/gortsplib/v4/pkg/format/rtpav1" +) + +// AV1 is the RTP format for the AV1 codec. +// Specification: https://aomediacodec.github.io/av1-rtp-spec/ +type AV1 struct { + PayloadTyp uint8 + LevelIdx *int + Profile *int + Tier *int +} + +func (f *AV1) unmarshal(ctx *unmarshalContext) error { + f.PayloadTyp = ctx.payloadType + + for key, val := range ctx.fmtp { + switch key { + case "level-idx": + n, err := strconv.ParseUint(val, 10, 31) + if err != nil { + return fmt.Errorf("invalid level-idx: %v", val) + } + + v2 := int(n) + f.LevelIdx = &v2 + + case "profile": + n, err := strconv.ParseUint(val, 10, 31) + if err != nil { + return fmt.Errorf("invalid profile: %v", val) + } + + v2 := int(n) + f.Profile = &v2 + + case "tier": + n, err := strconv.ParseUint(val, 10, 31) + if err != nil { + return fmt.Errorf("invalid tier: %v", val) + } + + v2 := int(n) + f.Tier = &v2 + } + } + + return nil +} + +// Codec implements Format. +func (f *AV1) Codec() string { + return "AV1" +} + +// ClockRate implements Format. +func (f *AV1) ClockRate() int { + return 90000 +} + +// PayloadType implements Format. +func (f *AV1) PayloadType() uint8 { + return f.PayloadTyp +} + +// RTPMap implements Format. +func (f *AV1) RTPMap() string { + return "AV1/90000" +} + +// FMTP implements Format. +func (f *AV1) FMTP() map[string]string { + fmtp := make(map[string]string) + + if f.LevelIdx != nil { + fmtp["level-idx"] = strconv.FormatInt(int64(*f.LevelIdx), 10) + } + if f.Profile != nil { + fmtp["profile"] = strconv.FormatInt(int64(*f.Profile), 10) + } + if f.Tier != nil { + fmtp["tier"] = strconv.FormatInt(int64(*f.Tier), 10) + } + + return fmtp +} + +// PTSEqualsDTS implements Format. +func (f *AV1) PTSEqualsDTS(*rtp.Packet) bool { + return true +} + +// CreateDecoder creates a decoder able to decode the content of the format. +func (f *AV1) CreateDecoder() (*rtpav1.Decoder, error) { + d := &rtpav1.Decoder{} + + err := d.Init() + if err != nil { + return nil, err + } + + return d, nil +} + +// CreateEncoder creates an encoder able to encode the content of the format. +func (f *AV1) CreateEncoder() (*rtpav1.Encoder, error) { + e := &rtpav1.Encoder{ + PayloadType: f.PayloadTyp, + } + + err := e.Init() + if err != nil { + return nil, err + } + + return e, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/format.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/format.go new file mode 100644 index 000000000..b63eb78ad --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/format.go @@ -0,0 +1,234 @@ +// Package format contains RTP format definitions, decoders and encoders. +package format + +import ( + "regexp" + "strconv" + "strings" + + "github.com/pion/rtp" + psdp "github.com/pion/sdp/v3" +) + +var ( + smartPayloadTypeRegexp = regexp.MustCompile("^smart/[0-9]/[0-9]+$") + smartRtpmapRegexp = regexp.MustCompile("^([0-9]+) (.+)/[0-9]+$") +) + +func replaceSmartPayloadType(payloadType string, attributes []psdp.Attribute) string { + re1 := smartPayloadTypeRegexp.FindStringSubmatch(payloadType) + if re1 != nil { + for _, attr := range attributes { + if attr.Key == "rtpmap" { + re2 := smartRtpmapRegexp.FindStringSubmatch(attr.Value) + if re2 != nil { + return re2[1] + } + } + } + } + return payloadType +} + +func getFormatAttribute(attributes []psdp.Attribute, payloadType uint8, key string) string { + for _, attr := range attributes { + if attr.Key == key { + v := strings.TrimSpace(attr.Value) + if parts := strings.SplitN(v, " ", 2); len(parts) == 2 { + if tmp, err := strconv.ParseUint(parts[0], 10, 8); err == nil && uint8(tmp) == payloadType { + return parts[1] + } + } + } + } + return "" +} + +func getCodecAndClock(rtpMap string) (string, string) { + parts2 := strings.SplitN(rtpMap, "/", 2) + if len(parts2) != 2 { + return "", "" + } + + return strings.ToLower(parts2[0]), parts2[1] +} + +func decodeFMTP(enc string) map[string]string { + if enc == "" { + return nil + } + + ret := make(map[string]string) + + for _, kv := range strings.Split(enc, ";") { + kv = strings.Trim(kv, " ") + + if len(kv) == 0 { + continue + } + + tmp := strings.SplitN(kv, "=", 2) + if len(tmp) != 2 { + continue + } + + ret[strings.ToLower(tmp[0])] = tmp[1] + } + + return ret +} + +type unmarshalContext struct { + mediaType string + payloadType uint8 + clock string + codec string + rtpMap string + fmtp map[string]string +} + +// Format is a media format. +// It defines the payload type of RTP packets and how to encode/decode them. +type Format interface { + unmarshal(ctx *unmarshalContext) error + + // Codec returns the codec name. + Codec() string + + // ClockRate returns the clock rate. + ClockRate() int + + // PayloadType returns the payload type. + PayloadType() uint8 + + // RTPMap returns the rtpmap attribute. + RTPMap() string + + // FMTP returns the fmtp attribute. + FMTP() map[string]string + + // PTSEqualsDTS checks whether PTS is equal to DTS in RTP packets. + PTSEqualsDTS(*rtp.Packet) bool +} + +// Unmarshal decodes a format from a media description. +func Unmarshal(md *psdp.MediaDescription, payloadTypeStr string) (Format, error) { + mediaType := md.MediaName.Media + payloadTypeStr = replaceSmartPayloadType(payloadTypeStr, md.Attributes) + + tmp, err := strconv.ParseUint(payloadTypeStr, 10, 8) + if err != nil { + return nil, err + } + payloadType := uint8(tmp) + + rtpMap := getFormatAttribute(md.Attributes, payloadType, "rtpmap") + fmtp := decodeFMTP(getFormatAttribute(md.Attributes, payloadType, "fmtp")) + codec, clock := getCodecAndClock(rtpMap) + + format := func() Format { + switch { + /* + * dynamic payload types + **/ + + // video + + case codec == "av1" && clock == "90000" && payloadType >= 96 && payloadType <= 127: + return &AV1{} + + case codec == "vp9" && clock == "90000" && payloadType >= 96 && payloadType <= 127: + return &VP9{} + + case codec == "vp8" && clock == "90000" && payloadType >= 96 && payloadType <= 127: + return &VP8{} + + case codec == "h265" && clock == "90000" && payloadType >= 96 && payloadType <= 127: + return &H265{} + + case codec == "h264" && clock == "90000" && ((payloadType >= 96 && payloadType <= 127) || payloadType == 35): + return &H264{} + + case codec == "mp4v-es" && clock == "90000" && payloadType >= 96 && payloadType <= 127: + return &MPEG4Video{} + + // audio + + case codec == "opus", codec == "multiopus" && payloadType >= 96 && payloadType <= 127: + return &Opus{} + + case codec == "vorbis" && payloadType >= 96 && payloadType <= 127: + return &Vorbis{} + + case (codec == "mpeg4-generic" || codec == "mp4a-latm") && payloadType >= 96 && payloadType <= 127: + return &MPEG4Audio{} + + case codec == "ac3" && payloadType >= 96 && payloadType <= 127: + return &AC3{} + + case codec == "speex" && payloadType >= 96 && payloadType <= 127: + return &Speex{} + + case (codec == "g726-16" || + codec == "g726-24" || + codec == "g726-32" || + codec == "g726-40" || + codec == "aal2-g726-16" || + codec == "aal2-g726-24" || + codec == "aal2-g726-32" || + codec == "aal2-g726-40") && clock == "8000" && payloadType >= 96 && payloadType <= 127: + return &G726{} + + case codec == "pcma", codec == "pcmu" && payloadType >= 96 && payloadType <= 127: + return &G711{} + + case codec == "l8", codec == "l16", codec == "l24" && payloadType >= 96 && payloadType <= 127: + return &LPCM{} + + /* + * static payload types + **/ + + // video + + case payloadType == 32: + return &MPEG1Video{} + + case payloadType == 26: + return &MJPEG{} + + case payloadType == 33: + return &MPEGTS{} + + // audio + + case payloadType == 14: + return &MPEG1Audio{} + + case payloadType == 9: + return &G722{} + + case payloadType == 0, payloadType == 8: + return &G711{} + + case payloadType == 10, payloadType == 11: + return &LPCM{} + } + + return &Generic{} + }() + + err = format.unmarshal(&unmarshalContext{ + mediaType: mediaType, + payloadType: payloadType, + clock: clock, + codec: codec, + rtpMap: rtpMap, + fmtp: fmtp, + }) + if err != nil { + return nil, err + } + + return format, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/g711.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/g711.go new file mode 100644 index 000000000..f2f93883c --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/g711.go @@ -0,0 +1,134 @@ +package format + +import ( + "strconv" + "strings" + + "github.com/pion/rtp" + + "github.com/bluenviron/gortsplib/v4/pkg/format/rtplpcm" +) + +// G711 is the RTP format for the G711 codec, encoded with mu-law or A-law. +// Specification: https://datatracker.ietf.org/doc/html/rfc3551 +type G711 struct { + PayloadTyp uint8 + MULaw bool + SampleRate int + ChannelCount int +} + +func (f *G711) unmarshal(ctx *unmarshalContext) error { + f.PayloadTyp = ctx.payloadType + + if ctx.payloadType == 0 { + f.MULaw = true + f.SampleRate = 8000 + f.ChannelCount = 1 + return nil + } + + if ctx.payloadType == 8 { + f.MULaw = false + f.SampleRate = 8000 + f.ChannelCount = 1 + return nil + } + + f.MULaw = (ctx.codec == "pcmu") + + tmp := strings.SplitN(ctx.clock, "/", 2) + + tmp1, err := strconv.ParseUint(tmp[0], 10, 31) + if err != nil { + return err + } + f.SampleRate = int(tmp1) + + if len(tmp) >= 2 { + tmp1, err := strconv.ParseUint(tmp[1], 10, 31) + if err != nil { + return err + } + f.ChannelCount = int(tmp1) + } else { + f.ChannelCount = 1 + } + + return nil +} + +// Codec implements Format. +func (f *G711) Codec() string { + return "G711" +} + +// ClockRate implements Format. +func (f *G711) ClockRate() int { + return f.SampleRate +} + +// PayloadType implements Format. +func (f *G711) PayloadType() uint8 { + return f.PayloadTyp +} + +// RTPMap implements Format. +func (f *G711) RTPMap() string { + ret := "" + + if f.MULaw { + ret += "PCMU" + } else { + ret += "PCMA" + } + + ret += "/" + strconv.FormatInt(int64(f.SampleRate), 10) + + if f.ChannelCount != 1 { + ret += "/" + strconv.FormatInt(int64(f.ChannelCount), 10) + } + + return ret +} + +// FMTP implements Format. +func (f *G711) FMTP() map[string]string { + return nil +} + +// PTSEqualsDTS implements Format. +func (f *G711) PTSEqualsDTS(*rtp.Packet) bool { + return true +} + +// CreateDecoder creates a decoder able to decode the content of the format. +func (f *G711) CreateDecoder() (*rtplpcm.Decoder, error) { + d := &rtplpcm.Decoder{ + BitDepth: 8, + ChannelCount: f.ChannelCount, + } + + err := d.Init() + if err != nil { + return nil, err + } + + return d, nil +} + +// CreateEncoder creates an encoder able to encode the content of the format. +func (f *G711) CreateEncoder() (*rtplpcm.Encoder, error) { + e := &rtplpcm.Encoder{ + PayloadType: f.PayloadType(), + BitDepth: 8, + ChannelCount: f.ChannelCount, + } + + err := e.Init() + if err != nil { + return nil, err + } + + return e, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/g722.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/g722.go new file mode 100644 index 000000000..86d7d996d --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/g722.go @@ -0,0 +1,76 @@ +package format + +import ( + "github.com/pion/rtp" + + "github.com/bluenviron/gortsplib/v4/pkg/format/rtpsimpleaudio" +) + +// G722 is the RTP format for the G722 codec. +// Specification: https://datatracker.ietf.org/doc/html/rfc3551 +type G722 struct { + // in Go, empty structs share the same pointer, + // therefore they cannot be used as map keys + // or in equality operations. Prevent this. + unused int //nolint:unused +} + +func (f *G722) unmarshal(_ *unmarshalContext) error { + return nil +} + +// Codec implements Format. +func (f *G722) Codec() string { + return "G722" +} + +// ClockRate implements Format. +func (f *G722) ClockRate() int { + return 8000 +} + +// PayloadType implements Format. +func (f *G722) PayloadType() uint8 { + return 9 +} + +// RTPMap implements Format. +func (f *G722) RTPMap() string { + return "G722/8000" +} + +// FMTP implements Format. +func (f *G722) FMTP() map[string]string { + return nil +} + +// PTSEqualsDTS implements Format. +func (f *G722) PTSEqualsDTS(*rtp.Packet) bool { + return true +} + +// CreateDecoder creates a decoder able to decode the content of the format. +func (f *G722) CreateDecoder() (*rtpsimpleaudio.Decoder, error) { + d := &rtpsimpleaudio.Decoder{} + + err := d.Init() + if err != nil { + return nil, err + } + + return d, nil +} + +// CreateEncoder creates an encoder able to encode the content of the format. +func (f *G722) CreateEncoder() (*rtpsimpleaudio.Encoder, error) { + e := &rtpsimpleaudio.Encoder{ + PayloadType: 9, + } + + err := e.Init() + if err != nil { + return nil, err + } + + return e, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/g726.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/g726.go new file mode 100644 index 000000000..1c41725ad --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/g726.go @@ -0,0 +1,73 @@ +package format + +import ( + "strconv" + "strings" + + "github.com/pion/rtp" +) + +// G726 is the RTP format for the G726 codec. +// Specification: https://datatracker.ietf.org/doc/html/rfc3551 +type G726 struct { + PayloadTyp uint8 + BitRate int + BigEndian bool +} + +func (f *G726) unmarshal(ctx *unmarshalContext) error { + f.PayloadTyp = ctx.payloadType + + if strings.HasPrefix(ctx.codec, "aal2-") { + f.BigEndian = true + } + + switch { + case strings.HasSuffix(ctx.codec, "-16"): + f.BitRate = 16 + case strings.HasSuffix(ctx.codec, "-24"): + f.BitRate = 24 + case strings.HasSuffix(ctx.codec, "-32"): + f.BitRate = 32 + default: + f.BitRate = 40 + } + + return nil +} + +// Codec implements Format. +func (f *G726) Codec() string { + return "G726" +} + +// ClockRate implements Format. +func (f *G726) ClockRate() int { + return 8000 +} + +// PayloadType implements Format. +func (f *G726) PayloadType() uint8 { + return f.PayloadTyp +} + +// RTPMap implements Format. +func (f *G726) RTPMap() string { + codec := "" + + if f.BigEndian { + codec += "AAL2-" + } + + return codec + "G726-" + strconv.FormatInt(int64(f.BitRate), 10) + "/8000" +} + +// FMTP implements Format. +func (f *G726) FMTP() map[string]string { + return nil +} + +// PTSEqualsDTS implements Format. +func (f *G726) PTSEqualsDTS(*rtp.Packet) bool { + return true +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/generic.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/generic.go new file mode 100644 index 000000000..270d973a8 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/generic.go @@ -0,0 +1,111 @@ +package format + +import ( + "fmt" + "strconv" + "strings" + + "github.com/pion/rtp" +) + +func findClockRate(payloadType uint8, rtpMap string, isApplication bool) (int, error) { + // get clock rate from payload type + // https://en.wikipedia.org/wiki/RTP_payload_formats + switch payloadType { + case 0, 1, 2, 3, 4, 5, 7, 8, 9, 12, 13, 15, 18: + return 8000, nil + + case 6: + return 16000, nil + + case 10, 11: + return 44100, nil + + case 14, 25, 26, 28, 31, 32, 33, 34: + return 90000, nil + + case 16: + return 11025, nil + + case 17: + return 22050, nil + } + + // get clock rate from rtpmap + // https://tools.ietf.org/html/rfc4566 + // a=rtpmap: / [/] + if rtpMap != "" { + if tmp := strings.Split(rtpMap, "/"); len(tmp) >= 2 { + v, err := strconv.ParseUint(tmp[1], 10, 31) + if err != nil { + return 0, err + } + return int(v), nil + } + } + + // application format without clock rate. + // do not throw an error, but return zero, that disables RTCP sender and receiver reports. + if isApplication || rtpMap != "" { + return 0, nil + } + + return 0, fmt.Errorf("clock rate not found") +} + +// Generic is a generic RTP format. +type Generic struct { + PayloadTyp uint8 + RTPMa string + FMT map[string]string + + // clock rate of the format. Filled when calling Init(). + ClockRat int +} + +// Init computes the clock rate of the format. It is mandatory to call it. +func (f *Generic) Init() error { + var err error + f.ClockRat, err = findClockRate(f.PayloadTyp, f.RTPMa, true) + return err +} + +func (f *Generic) unmarshal(ctx *unmarshalContext) error { + f.PayloadTyp = ctx.payloadType + f.RTPMa = ctx.rtpMap + f.FMT = ctx.fmtp + + var err error + f.ClockRat, err = findClockRate(f.PayloadTyp, f.RTPMa, ctx.mediaType == "application") + return err +} + +// Codec implements Format. +func (f *Generic) Codec() string { + return "Generic" +} + +// ClockRate implements Format. +func (f *Generic) ClockRate() int { + return f.ClockRat +} + +// PayloadType implements Format. +func (f *Generic) PayloadType() uint8 { + return f.PayloadTyp +} + +// RTPMap implements Format. +func (f *Generic) RTPMap() string { + return f.RTPMa +} + +// FMTP implements Format. +func (f *Generic) FMTP() map[string]string { + return f.FMT +} + +// PTSEqualsDTS implements Format. +func (f *Generic) PTSEqualsDTS(*rtp.Packet) bool { + return true +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/h264.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/h264.go new file mode 100644 index 000000000..87115b9cd --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/h264.go @@ -0,0 +1,227 @@ +package format + +import ( + "bytes" + "encoding/base64" + "encoding/hex" + "fmt" + "strconv" + "strings" + "sync" + + "github.com/pion/rtp" + + "github.com/bluenviron/gortsplib/v4/pkg/format/rtph264" + "github.com/bluenviron/mediacommon/v2/pkg/codecs/h264" +) + +// H264 is the RTP format for the H264 codec. +// Specification: https://datatracker.ietf.org/doc/html/rfc6184 +type H264 struct { + PayloadTyp uint8 + SPS []byte + PPS []byte + PacketizationMode int + + mutex sync.RWMutex +} + +func (f *H264) unmarshal(ctx *unmarshalContext) error { + f.PayloadTyp = ctx.payloadType + + for key, val := range ctx.fmtp { + switch key { + case "sprop-parameter-sets": + tmp := strings.Split(val, ",") + if len(tmp) >= 2 { + sps, err := base64.StdEncoding.DecodeString(tmp[0]) + if err != nil { + return fmt.Errorf("invalid sprop-parameter-sets (%v)", val) + } + + // some cameras ship parameters with Annex-B prefix + sps = bytes.TrimPrefix(sps, []byte{0, 0, 0, 1}) + + pps, err := base64.StdEncoding.DecodeString(tmp[1]) + if err != nil { + return fmt.Errorf("invalid sprop-parameter-sets (%v)", val) + } + + // some cameras ship parameters with Annex-B prefix + pps = bytes.TrimPrefix(pps, []byte{0, 0, 0, 1}) + + var spsp h264.SPS + err = spsp.Unmarshal(sps) + if err != nil { + continue + } + + f.SPS = sps + f.PPS = pps + } + + case "packetization-mode": + tmp, err := strconv.ParseUint(val, 10, 31) + if err != nil { + return fmt.Errorf("invalid packetization-mode (%v)", val) + } + + f.PacketizationMode = int(tmp) + } + } + + return nil +} + +// Codec implements Format. +func (f *H264) Codec() string { + return "H264" +} + +// ClockRate implements Format. +func (f *H264) ClockRate() int { + return 90000 +} + +// PayloadType implements Format. +func (f *H264) PayloadType() uint8 { + return f.PayloadTyp +} + +// RTPMap implements Format. +func (f *H264) RTPMap() string { + return "H264/90000" +} + +// FMTP implements Format. +func (f *H264) FMTP() map[string]string { + f.mutex.RLock() + defer f.mutex.RUnlock() + + fmtp := make(map[string]string) + + if f.PacketizationMode != 0 { + fmtp["packetization-mode"] = strconv.FormatInt(int64(f.PacketizationMode), 10) + } + + var tmp []string + if f.SPS != nil { + tmp = append(tmp, base64.StdEncoding.EncodeToString(f.SPS)) + } + if f.PPS != nil { + tmp = append(tmp, base64.StdEncoding.EncodeToString(f.PPS)) + } + if tmp != nil { + fmtp["sprop-parameter-sets"] = strings.Join(tmp, ",") + } + if len(f.SPS) >= 4 { + fmtp["profile-level-id"] = strings.ToUpper(hex.EncodeToString(f.SPS[1:4])) + } + + return fmtp +} + +// PTSEqualsDTS implements Format. +func (f *H264) PTSEqualsDTS(pkt *rtp.Packet) bool { + if len(pkt.Payload) == 0 { + return false + } + + typ := h264.NALUType(pkt.Payload[0] & 0x1F) + + switch typ { + case h264.NALUTypeIDR, h264.NALUTypeSPS, h264.NALUTypePPS: + return true + + case 24: // STAP-A + payload := pkt.Payload[1:] + + for { + if len(payload) < 2 { + return false + } + + size := uint16(payload[0])<<8 | uint16(payload[1]) + payload = payload[2:] + + if size == 0 || int(size) > len(payload) { + return false + } + + var nalu []byte + nalu, payload = payload[:size], payload[size:] + + typ = h264.NALUType(nalu[0] & 0x1F) + switch typ { + case h264.NALUTypeIDR, h264.NALUTypeSPS, h264.NALUTypePPS: + return true + } + + if len(payload) == 0 { + break + } + } + + case 28: // FU-A + if len(pkt.Payload) < 2 { + return false + } + + start := pkt.Payload[1] >> 7 + if start != 1 { + return false + } + + typ := h264.NALUType(pkt.Payload[1] & 0x1F) + switch typ { + case h264.NALUTypeIDR, h264.NALUTypeSPS, h264.NALUTypePPS: + return true + } + } + + return false +} + +// CreateDecoder creates a decoder able to decode the content of the format. +func (f *H264) CreateDecoder() (*rtph264.Decoder, error) { + d := &rtph264.Decoder{ + PacketizationMode: f.PacketizationMode, + } + + err := d.Init() + if err != nil { + return nil, err + } + + return d, nil +} + +// CreateEncoder creates an encoder able to encode the content of the format. +func (f *H264) CreateEncoder() (*rtph264.Encoder, error) { + e := &rtph264.Encoder{ + PayloadType: f.PayloadTyp, + PacketizationMode: f.PacketizationMode, + } + + err := e.Init() + if err != nil { + return nil, err + } + + return e, nil +} + +// SafeSetParams sets the codec parameters. +func (f *H264) SafeSetParams(sps []byte, pps []byte) { + f.mutex.Lock() + defer f.mutex.Unlock() + f.SPS = sps + f.PPS = pps +} + +// SafeParams returns the codec parameters. +func (f *H264) SafeParams() ([]byte, []byte) { + f.mutex.RLock() + defer f.mutex.RUnlock() + return f.SPS, f.PPS +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/h265.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/h265.go new file mode 100644 index 000000000..39931903e --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/h265.go @@ -0,0 +1,240 @@ +package format + +import ( + "bytes" + "encoding/base64" + "fmt" + "strconv" + "sync" + + "github.com/bluenviron/mediacommon/v2/pkg/codecs/h265" + "github.com/pion/rtp" + + "github.com/bluenviron/gortsplib/v4/pkg/format/rtph265" +) + +// H265 is the RTP format for the H265 codec. +// Specification: https://datatracker.ietf.org/doc/html/rfc7798 +type H265 struct { + PayloadTyp uint8 + VPS []byte + SPS []byte + PPS []byte + MaxDONDiff int + + mutex sync.RWMutex +} + +func (f *H265) unmarshal(ctx *unmarshalContext) error { + f.PayloadTyp = ctx.payloadType + + for key, val := range ctx.fmtp { + switch key { + case "sprop-vps": + var err error + f.VPS, err = base64.StdEncoding.DecodeString(val) + if err != nil { + return fmt.Errorf("invalid sprop-vps (%v)", ctx.fmtp) + } + + // some cameras ship parameters with Annex-B prefix + f.VPS = bytes.TrimPrefix(f.VPS, []byte{0, 0, 0, 1}) + + case "sprop-sps": + var err error + f.SPS, err = base64.StdEncoding.DecodeString(val) + if err != nil { + return fmt.Errorf("invalid sprop-sps (%v)", ctx.fmtp) + } + + // some cameras ship parameters with Annex-B prefix + f.SPS = bytes.TrimPrefix(f.SPS, []byte{0, 0, 0, 1}) + + var spsp h265.SPS + err = spsp.Unmarshal(f.SPS) + if err != nil { + return fmt.Errorf("invalid SPS: %w", err) + } + + case "sprop-pps": + var err error + f.PPS, err = base64.StdEncoding.DecodeString(val) + if err != nil { + return fmt.Errorf("invalid sprop-pps (%v)", ctx.fmtp) + } + + // some cameras ship parameters with Annex-B prefix + f.PPS = bytes.TrimPrefix(f.PPS, []byte{0, 0, 0, 1}) + + var ppsp h265.PPS + err = ppsp.Unmarshal(f.PPS) + if err != nil { + return fmt.Errorf("invalid PPS: %w", err) + } + + case "sprop-max-don-diff": + tmp, err := strconv.ParseUint(val, 10, 31) + if err != nil { + return fmt.Errorf("invalid sprop-max-don-diff (%v)", ctx.fmtp) + } + f.MaxDONDiff = int(tmp) + } + } + + return nil +} + +// Codec implements Format. +func (f *H265) Codec() string { + return "H265" +} + +// ClockRate implements Format. +func (f *H265) ClockRate() int { + return 90000 +} + +// PayloadType implements Format. +func (f *H265) PayloadType() uint8 { + return f.PayloadTyp +} + +// RTPMap implements Format. +func (f *H265) RTPMap() string { + return "H265/90000" +} + +// FMTP implements Format. +func (f *H265) FMTP() map[string]string { + f.mutex.RLock() + defer f.mutex.RUnlock() + + fmtp := make(map[string]string) + if f.VPS != nil { + fmtp["sprop-vps"] = base64.StdEncoding.EncodeToString(f.VPS) + } + if f.SPS != nil { + fmtp["sprop-sps"] = base64.StdEncoding.EncodeToString(f.SPS) + } + if f.PPS != nil { + fmtp["sprop-pps"] = base64.StdEncoding.EncodeToString(f.PPS) + } + if f.MaxDONDiff != 0 { + fmtp["sprop-max-don-diff"] = strconv.FormatInt(int64(f.MaxDONDiff), 10) + } + + return fmtp +} + +// PTSEqualsDTS implements Format. +func (f *H265) PTSEqualsDTS(pkt *rtp.Packet) bool { + if len(pkt.Payload) == 0 { + return false + } + + typ := h265.NALUType((pkt.Payload[0] >> 1) & 0b111111) + + switch typ { + case h265.NALUType_IDR_W_RADL, h265.NALUType_IDR_N_LP, h265.NALUType_CRA_NUT, + h265.NALUType_VPS_NUT, h265.NALUType_SPS_NUT, h265.NALUType_PPS_NUT: + return true + + case h265.NALUType_AggregationUnit: + if len(pkt.Payload) < 4 { + return false + } + + payload := pkt.Payload[2:] + + for { + size := uint16(payload[0])<<8 | uint16(payload[1]) + payload = payload[2:] + + if size == 0 || int(size) > len(payload) { + return false + } + + var nalu []byte + nalu, payload = payload[:size], payload[size:] + + typ = h265.NALUType((nalu[0] >> 1) & 0b111111) + switch typ { + case h265.NALUType_IDR_W_RADL, h265.NALUType_IDR_N_LP, h265.NALUType_CRA_NUT, + h265.NALUType_VPS_NUT, h265.NALUType_SPS_NUT, h265.NALUType_PPS_NUT: + return true + } + + if len(payload) == 0 { + break + } + + if len(payload) < 2 { + return false + } + } + + case h265.NALUType_FragmentationUnit: + if len(pkt.Payload) < 3 { + return false + } + + start := pkt.Payload[2] >> 7 + if start != 1 { + return false + } + + typ := h265.NALUType(pkt.Payload[2] & 0b111111) + switch typ { + case h265.NALUType_IDR_W_RADL, h265.NALUType_IDR_N_LP, h265.NALUType_CRA_NUT, + h265.NALUType_VPS_NUT, h265.NALUType_SPS_NUT, h265.NALUType_PPS_NUT: + return true + } + } + + return false +} + +// CreateDecoder creates a decoder able to decode the content of the format. +func (f *H265) CreateDecoder() (*rtph265.Decoder, error) { + d := &rtph265.Decoder{ + MaxDONDiff: f.MaxDONDiff, + } + + err := d.Init() + if err != nil { + return nil, err + } + + return d, nil +} + +// CreateEncoder creates an encoder able to encode the content of the format. +func (f *H265) CreateEncoder() (*rtph265.Encoder, error) { + e := &rtph265.Encoder{ + PayloadType: f.PayloadTyp, + MaxDONDiff: f.MaxDONDiff, + } + + err := e.Init() + if err != nil { + return nil, err + } + + return e, nil +} + +// SafeSetParams sets the codec parameters. +func (f *H265) SafeSetParams(vps []byte, sps []byte, pps []byte) { + f.mutex.Lock() + defer f.mutex.Unlock() + f.VPS = vps + f.SPS = sps + f.PPS = pps +} + +// SafeParams returns the codec parameters. +func (f *H265) SafeParams() ([]byte, []byte, []byte) { + f.mutex.RLock() + defer f.mutex.RUnlock() + return f.VPS, f.SPS, f.PPS +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/lpcm.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/lpcm.go new file mode 100644 index 000000000..a83d96a36 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/lpcm.go @@ -0,0 +1,143 @@ +package format + +import ( + "strconv" + "strings" + + "github.com/pion/rtp" + + "github.com/bluenviron/gortsplib/v4/pkg/format/rtplpcm" +) + +// LPCM is the RTP format for the LPCM codec. +// Specification: https://datatracker.ietf.org/doc/html/rfc3190 +// Specification: https://datatracker.ietf.org/doc/html/rfc3551 +type LPCM struct { + PayloadTyp uint8 + BitDepth int + SampleRate int + ChannelCount int +} + +func (f *LPCM) unmarshal(ctx *unmarshalContext) error { + f.PayloadTyp = ctx.payloadType + + if ctx.payloadType == 10 { + f.BitDepth = 16 + f.SampleRate = 44100 + f.ChannelCount = 2 + return nil + } + + if ctx.payloadType == 11 { + f.BitDepth = 16 + f.SampleRate = 44100 + f.ChannelCount = 1 + return nil + } + + switch ctx.codec { + case "l8": + f.BitDepth = 8 + + case "l16": + f.BitDepth = 16 + + case "l24": + f.BitDepth = 24 + } + + tmp := strings.SplitN(ctx.clock, "/", 2) + + tmp1, err := strconv.ParseUint(tmp[0], 10, 31) + if err != nil { + return err + } + f.SampleRate = int(tmp1) + + if len(tmp) >= 2 { + tmp1, err := strconv.ParseUint(tmp[1], 10, 31) + if err != nil { + return err + } + f.ChannelCount = int(tmp1) + } else { + f.ChannelCount = 1 + } + + return nil +} + +// Codec implements Format. +func (f *LPCM) Codec() string { + return "LPCM" +} + +// ClockRate implements Format. +func (f *LPCM) ClockRate() int { + return f.SampleRate +} + +// PayloadType implements Format. +func (f *LPCM) PayloadType() uint8 { + return f.PayloadTyp +} + +// RTPMap implements Format. +func (f *LPCM) RTPMap() string { + var codec string + switch f.BitDepth { + case 8: + codec = "L8" + + case 16: + codec = "L16" + + case 24: + codec = "L24" + } + + return codec + "/" + strconv.FormatInt(int64(f.SampleRate), 10) + + "/" + strconv.FormatInt(int64(f.ChannelCount), 10) +} + +// FMTP implements Format. +func (f *LPCM) FMTP() map[string]string { + return nil +} + +// PTSEqualsDTS implements Format. +func (f *LPCM) PTSEqualsDTS(*rtp.Packet) bool { + return true +} + +// CreateDecoder creates a decoder able to decode the content of the format. +func (f *LPCM) CreateDecoder() (*rtplpcm.Decoder, error) { + d := &rtplpcm.Decoder{ + BitDepth: f.BitDepth, + ChannelCount: f.ChannelCount, + } + + err := d.Init() + if err != nil { + return nil, err + } + + return d, nil +} + +// CreateEncoder creates an encoder able to encode the content of the format. +func (f *LPCM) CreateEncoder() (*rtplpcm.Encoder, error) { + e := &rtplpcm.Encoder{ + PayloadType: f.PayloadTyp, + BitDepth: f.BitDepth, + ChannelCount: f.ChannelCount, + } + + err := e.Init() + if err != nil { + return nil, err + } + + return e, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/mjpeg.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/mjpeg.go new file mode 100644 index 000000000..21275c69b --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/mjpeg.go @@ -0,0 +1,74 @@ +package format //nolint:dupl + +import ( + "github.com/pion/rtp" + + "github.com/bluenviron/gortsplib/v4/pkg/format/rtpmjpeg" +) + +// MJPEG is the RTP format for the Motion-JPEG codec. +// Specification: https://datatracker.ietf.org/doc/html/rfc2435 +type MJPEG struct { + // in Go, empty structs share the same pointer, + // therefore they cannot be used as map keys + // or in equality operations. Prevent this. + unused int //nolint:unused +} + +func (f *MJPEG) unmarshal(_ *unmarshalContext) error { + return nil +} + +// Codec implements Format. +func (f *MJPEG) Codec() string { + return "M-JPEG" +} + +// ClockRate implements Format. +func (f *MJPEG) ClockRate() int { + return 90000 +} + +// PayloadType implements Format. +func (f *MJPEG) PayloadType() uint8 { + return 26 +} + +// RTPMap implements Format. +func (f *MJPEG) RTPMap() string { + return "JPEG/90000" +} + +// FMTP implements Format. +func (f *MJPEG) FMTP() map[string]string { + return nil +} + +// PTSEqualsDTS implements Format. +func (f *MJPEG) PTSEqualsDTS(*rtp.Packet) bool { + return true +} + +// CreateDecoder creates a decoder able to decode the content of the format. +func (f *MJPEG) CreateDecoder() (*rtpmjpeg.Decoder, error) { + d := &rtpmjpeg.Decoder{} + + err := d.Init() + if err != nil { + return nil, err + } + + return d, nil +} + +// CreateEncoder creates an encoder able to encode the content of the format. +func (f *MJPEG) CreateEncoder() (*rtpmjpeg.Encoder, error) { + e := &rtpmjpeg.Encoder{} + + err := e.Init() + if err != nil { + return nil, err + } + + return e, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/mpeg1_audio.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/mpeg1_audio.go new file mode 100644 index 000000000..603cde9e3 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/mpeg1_audio.go @@ -0,0 +1,74 @@ +package format //nolint:dupl + +import ( + "github.com/pion/rtp" + + "github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg1audio" +) + +// MPEG1Audio is the RTP format for a MPEG-1/2 Audio codec. +// Specification: https://datatracker.ietf.org/doc/html/rfc2250 +type MPEG1Audio struct { + // in Go, empty structs share the same pointer, + // therefore they cannot be used as map keys + // or in equality operations. Prevent this. + unused int //nolint:unused +} + +func (f *MPEG1Audio) unmarshal(_ *unmarshalContext) error { + return nil +} + +// Codec implements Format. +func (f *MPEG1Audio) Codec() string { + return "MPEG-1/2 Audio" +} + +// ClockRate implements Format. +func (f *MPEG1Audio) ClockRate() int { + return 90000 +} + +// PayloadType implements Format. +func (f *MPEG1Audio) PayloadType() uint8 { + return 14 +} + +// RTPMap implements Format. +func (f *MPEG1Audio) RTPMap() string { + return "" +} + +// FMTP implements Format. +func (f *MPEG1Audio) FMTP() map[string]string { + return nil +} + +// PTSEqualsDTS implements Format. +func (f *MPEG1Audio) PTSEqualsDTS(*rtp.Packet) bool { + return true +} + +// CreateDecoder creates a decoder able to decode the content of the format. +func (f *MPEG1Audio) CreateDecoder() (*rtpmpeg1audio.Decoder, error) { + d := &rtpmpeg1audio.Decoder{} + + err := d.Init() + if err != nil { + return nil, err + } + + return d, nil +} + +// CreateEncoder creates an encoder able to encode the content of the format. +func (f *MPEG1Audio) CreateEncoder() (*rtpmpeg1audio.Encoder, error) { + e := &rtpmpeg1audio.Encoder{} + + err := e.Init() + if err != nil { + return nil, err + } + + return e, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/mpeg1_video.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/mpeg1_video.go new file mode 100644 index 000000000..93d4d1584 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/mpeg1_video.go @@ -0,0 +1,74 @@ +package format //nolint:dupl + +import ( + "github.com/pion/rtp" + + "github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg1video" +) + +// MPEG1Video is the RTP format for a MPEG-1/2 Video codec. +// Specification: https://datatracker.ietf.org/doc/html/rfc2250 +type MPEG1Video struct { + // in Go, empty structs share the same pointer, + // therefore they cannot be used as map keys + // or in equality operations. Prevent this. + unused int //nolint:unused +} + +func (f *MPEG1Video) unmarshal(_ *unmarshalContext) error { + return nil +} + +// Codec implements Format. +func (f *MPEG1Video) Codec() string { + return "MPEG-1/2 Video" +} + +// ClockRate implements Format. +func (f *MPEG1Video) ClockRate() int { + return 90000 +} + +// PayloadType implements Format. +func (f *MPEG1Video) PayloadType() uint8 { + return 32 +} + +// RTPMap implements Format. +func (f *MPEG1Video) RTPMap() string { + return "" +} + +// FMTP implements Format. +func (f *MPEG1Video) FMTP() map[string]string { + return nil +} + +// PTSEqualsDTS implements Format. +func (f *MPEG1Video) PTSEqualsDTS(*rtp.Packet) bool { + return true +} + +// CreateDecoder creates a decoder able to decode the content of the format. +func (f *MPEG1Video) CreateDecoder() (*rtpmpeg1video.Decoder, error) { + d := &rtpmpeg1video.Decoder{} + + err := d.Init() + if err != nil { + return nil, err + } + + return d, nil +} + +// CreateEncoder creates an encoder able to encode the content of the format. +func (f *MPEG1Video) CreateEncoder() (*rtpmpeg1video.Encoder, error) { + e := &rtpmpeg1video.Encoder{} + + err := e.Init() + if err != nil { + return nil, err + } + + return e, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/mpeg4_audio.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/mpeg4_audio.go new file mode 100644 index 000000000..91c8b7300 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/mpeg4_audio.go @@ -0,0 +1,343 @@ +package format + +import ( + "encoding/hex" + "fmt" + "strconv" + "strings" + + "github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio" + "github.com/pion/rtp" + + "github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4audio" +) + +// MPEG4Audio is the RTP format for a MPEG-4 Audio codec. +// Specification: https://datatracker.ietf.org/doc/html/rfc3640 +// Specification: https://datatracker.ietf.org/doc/html/rfc6416#section-7.3 +type MPEG4Audio struct { + // payload type of packets. + PayloadTyp uint8 + + // use LATM format (RFC6416) instead of generic format (RFC3640). + LATM bool + + // profile level ID. + ProfileLevelID int + + // generic only + Config *mpeg4audio.Config + SizeLength int + IndexLength int + IndexDeltaLength int + + // LATM only + Bitrate *int + CPresent bool + StreamMuxConfig *mpeg4audio.StreamMuxConfig + SBREnabled *bool +} + +func (f *MPEG4Audio) unmarshal(ctx *unmarshalContext) error { + f.PayloadTyp = ctx.payloadType + f.LATM = (ctx.codec != "mpeg4-generic") + + if !f.LATM { + for key, val := range ctx.fmtp { + switch key { + case "streamtype": + if val != "5" { // AudioStream in ISO 14496-1 + return fmt.Errorf("streamtype of AAC must be 5") + } + + case "mode": + if strings.ToLower(val) != "aac-hbr" && strings.ToLower(val) != "aac_hbr" { + return fmt.Errorf("unsupported AAC mode: %v", val) + } + + case "profile-level-id": + tmp, err := strconv.ParseUint(val, 10, 31) + if err != nil { + return fmt.Errorf("invalid profile-level-id: %v", val) + } + + f.ProfileLevelID = int(tmp) + + case "config": + enc, err := hex.DecodeString(val) + if err != nil { + return fmt.Errorf("invalid AAC config: %v", val) + } + + f.Config = &mpeg4audio.Config{} + err = f.Config.Unmarshal(enc) + if err != nil { + return fmt.Errorf("invalid AAC config: %v", val) + } + + case "sizelength": + n, err := strconv.ParseUint(val, 10, 31) + if err != nil || n > 100 { + return fmt.Errorf("invalid AAC SizeLength: %v", val) + } + f.SizeLength = int(n) + + case "indexlength": + n, err := strconv.ParseUint(val, 10, 31) + if err != nil || n > 100 { + return fmt.Errorf("invalid AAC IndexLength: %v", val) + } + f.IndexLength = int(n) + + case "indexdeltalength": + n, err := strconv.ParseUint(val, 10, 31) + if err != nil || n > 100 { + return fmt.Errorf("invalid AAC IndexDeltaLength: %v", val) + } + f.IndexDeltaLength = int(n) + } + } + + if f.Config == nil { + return fmt.Errorf("config is missing") + } + + if f.SizeLength == 0 { + return fmt.Errorf("sizelength is missing") + } + } else { + // default value set by specification + f.ProfileLevelID = 30 + f.CPresent = true + + for key, val := range ctx.fmtp { + switch key { + case "profile-level-id": + tmp, err := strconv.ParseUint(val, 10, 31) + if err != nil { + return fmt.Errorf("invalid profile-level-id: %v", val) + } + + f.ProfileLevelID = int(tmp) + + case "bitrate": + tmp, err := strconv.ParseUint(val, 10, 31) + if err != nil { + return fmt.Errorf("invalid bitrate: %v", val) + } + + v := int(tmp) + f.Bitrate = &v + + case "cpresent": + f.CPresent = (val == "1") + + case "config": + enc, err := hex.DecodeString(val) + if err != nil { + return fmt.Errorf("invalid AAC config: %v", val) + } + + f.StreamMuxConfig = &mpeg4audio.StreamMuxConfig{} + err = f.StreamMuxConfig.Unmarshal(enc) + if err != nil { + return fmt.Errorf("invalid AAC config: %w", err) + } + + case "sbr-enabled": + v := (val == "1") + f.SBREnabled = &v + } + } + + if f.CPresent { + if f.StreamMuxConfig != nil { + return fmt.Errorf("config and cpresent can't be used at the same time") + } + } else { + if f.StreamMuxConfig == nil { + return fmt.Errorf("config is missing") + } + } + } + + return nil +} + +// Codec implements Format. +func (f *MPEG4Audio) Codec() string { + return "MPEG-4 Audio" +} + +// ClockRate implements Format. +func (f *MPEG4Audio) ClockRate() int { + if !f.LATM { + return f.Config.SampleRate + } + if f.CPresent { + return 16000 + } + return f.StreamMuxConfig.Programs[0].Layers[0].AudioSpecificConfig.SampleRate +} + +// PayloadType implements Format. +func (f *MPEG4Audio) PayloadType() uint8 { + return f.PayloadTyp +} + +// RTPMap implements Format. +func (f *MPEG4Audio) RTPMap() string { + if !f.LATM { + sampleRate := f.Config.SampleRate + if f.Config.ExtensionSampleRate != 0 { + sampleRate = f.Config.ExtensionSampleRate + } + + channelCount := f.Config.ChannelCount + if f.Config.ExtensionType == mpeg4audio.ObjectTypePS { + channelCount = 2 + } + + return "mpeg4-generic/" + strconv.FormatInt(int64(sampleRate), 10) + + "/" + strconv.FormatInt(int64(channelCount), 10) + } + + if f.CPresent { + return "MP4A-LATM/16000/1" + } + + aoc := f.StreamMuxConfig.Programs[0].Layers[0].AudioSpecificConfig + + sampleRate := aoc.SampleRate + if aoc.ExtensionSampleRate != 0 { + sampleRate = aoc.ExtensionSampleRate + } + + channelCount := aoc.ChannelCount + if aoc.ExtensionType == mpeg4audio.ObjectTypePS { + channelCount = 2 + } + + return "MP4A-LATM/" + strconv.FormatInt(int64(sampleRate), 10) + + "/" + strconv.FormatInt(int64(channelCount), 10) +} + +// FMTP implements Format. +func (f *MPEG4Audio) FMTP() map[string]string { + if !f.LATM { + enc, err := f.Config.Marshal() + if err != nil { + return nil + } + + profileLevelID := f.ProfileLevelID + if profileLevelID == 0 { // support legacy definition which didn't include profile-level-id + profileLevelID = 1 + } + + fmtp := map[string]string{ + "streamtype": "5", + "mode": "AAC-hbr", + "profile-level-id": strconv.FormatInt(int64(profileLevelID), 10), + } + + if f.SizeLength > 0 { + fmtp["sizelength"] = strconv.FormatInt(int64(f.SizeLength), 10) + } + + if f.IndexLength > 0 { + fmtp["indexlength"] = strconv.FormatInt(int64(f.IndexLength), 10) + } + + if f.IndexDeltaLength > 0 { + fmtp["indexdeltalength"] = strconv.FormatInt(int64(f.IndexDeltaLength), 10) + } + + fmtp["config"] = hex.EncodeToString(enc) + + return fmtp + } + + fmtp := map[string]string{ + "profile-level-id": strconv.FormatInt(int64(f.ProfileLevelID), 10), + } + + if f.Bitrate != nil { + fmtp["bitrate"] = strconv.FormatInt(int64(*f.Bitrate), 10) + } + + if f.CPresent { + fmtp["cpresent"] = "1" + } else { + fmtp["cpresent"] = "0" + + enc, err := f.StreamMuxConfig.Marshal() + if err != nil { + return nil + } + + fmtp["config"] = hex.EncodeToString(enc) + fmtp["object"] = strconv.FormatInt(int64(f.StreamMuxConfig.Programs[0].Layers[0].AudioSpecificConfig.Type), 10) + } + + if f.SBREnabled != nil { + if *f.SBREnabled { + fmtp["SBR-enabled"] = "1" + } else { + fmtp["SBR-enabled"] = "0" + } + } + + return fmtp +} + +// PTSEqualsDTS implements Format. +func (f *MPEG4Audio) PTSEqualsDTS(*rtp.Packet) bool { + return true +} + +// CreateDecoder creates a decoder able to decode the content of the format. +func (f *MPEG4Audio) CreateDecoder() (*rtpmpeg4audio.Decoder, error) { + d := &rtpmpeg4audio.Decoder{ + LATM: f.LATM, + SizeLength: f.SizeLength, + IndexLength: f.IndexLength, + IndexDeltaLength: f.IndexDeltaLength, + } + + err := d.Init() + if err != nil { + return nil, err + } + + return d, nil +} + +// CreateEncoder creates an encoder able to encode the content of the format. +func (f *MPEG4Audio) CreateEncoder() (*rtpmpeg4audio.Encoder, error) { + e := &rtpmpeg4audio.Encoder{ + LATM: f.LATM, + PayloadType: f.PayloadTyp, + SizeLength: f.SizeLength, + IndexLength: f.IndexLength, + IndexDeltaLength: f.IndexDeltaLength, + } + + err := e.Init() + if err != nil { + return nil, err + } + + return e, nil +} + +// GetConfig returns the MPEG-4 Audio configuration. +func (f *MPEG4Audio) GetConfig() *mpeg4audio.Config { + if !f.LATM { + return f.Config + } + if f.CPresent { + return nil + } + return f.StreamMuxConfig.Programs[0].Layers[0].AudioSpecificConfig +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/mpeg4_video.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/mpeg4_video.go new file mode 100644 index 000000000..8c09bf18c --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/mpeg4_video.go @@ -0,0 +1,133 @@ +package format + +import ( + "encoding/hex" + "fmt" + "strconv" + "strings" + "sync" + + "github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4video" + "github.com/pion/rtp" + + "github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4video" +) + +// MPEG4Video is the RTP format for a MPEG-4 Video codec. +// Specification: https://datatracker.ietf.org/doc/html/rfc6416#section-7.1 +type MPEG4Video struct { + PayloadTyp uint8 + ProfileLevelID int + Config []byte + + mutex sync.RWMutex +} + +func (f *MPEG4Video) unmarshal(ctx *unmarshalContext) error { + f.PayloadTyp = ctx.payloadType + f.ProfileLevelID = 1 // default value imposed by specification + + for key, val := range ctx.fmtp { + switch key { + case "profile-level-id": + tmp, err := strconv.ParseUint(val, 10, 31) + if err != nil { + return fmt.Errorf("invalid profile-level-id: %v", val) + } + + f.ProfileLevelID = int(tmp) + + case "config": + var err error + f.Config, err = hex.DecodeString(val) + if err != nil { + return fmt.Errorf("invalid config: %v", val) + } + + err = mpeg4video.IsValidConfig(f.Config) + if err != nil { + return fmt.Errorf("invalid config: %w", err) + } + } + } + + return nil +} + +// Codec implements Format. +func (f *MPEG4Video) Codec() string { + return "MPEG-4 Video" +} + +// ClockRate implements Format. +func (f *MPEG4Video) ClockRate() int { + return 90000 +} + +// PayloadType implements Format. +func (f *MPEG4Video) PayloadType() uint8 { + return f.PayloadTyp +} + +// RTPMap implements Format. +func (f *MPEG4Video) RTPMap() string { + return "MP4V-ES/90000" +} + +// FMTP implements Format. +func (f *MPEG4Video) FMTP() map[string]string { + fmtp := map[string]string{ + "profile-level-id": strconv.FormatInt(int64(f.ProfileLevelID), 10), + } + + if f.Config != nil { + fmtp["config"] = strings.ToUpper(hex.EncodeToString(f.Config)) + } + + return fmtp +} + +// PTSEqualsDTS implements Format. +func (f *MPEG4Video) PTSEqualsDTS(*rtp.Packet) bool { + return true +} + +// CreateDecoder creates a decoder able to decode the content of the format. +func (f *MPEG4Video) CreateDecoder() (*rtpmpeg4video.Decoder, error) { + d := &rtpmpeg4video.Decoder{} + + err := d.Init() + if err != nil { + return nil, err + } + + return d, nil +} + +// CreateEncoder creates an encoder able to encode the content of the format. +func (f *MPEG4Video) CreateEncoder() (*rtpmpeg4video.Encoder, error) { + e := &rtpmpeg4video.Encoder{ + PayloadType: f.PayloadTyp, + } + + err := e.Init() + if err != nil { + return nil, err + } + + return e, nil +} + +// SafeSetParams sets the codec parameters. +func (f *MPEG4Video) SafeSetParams(config []byte) { + f.mutex.Lock() + defer f.mutex.Unlock() + f.Config = config +} + +// SafeParams returns the codec parameters. +func (f *MPEG4Video) SafeParams() []byte { + f.mutex.RLock() + defer f.mutex.RUnlock() + return f.Config +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/mpegts.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/mpegts.go new file mode 100644 index 000000000..3baf5703f --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/mpegts.go @@ -0,0 +1,48 @@ +package format //nolint:dupl + +import ( + "github.com/pion/rtp" +) + +// MPEGTS is the RTP format for MPEG-TS. +// Specification: https://datatracker.ietf.org/doc/html/rfc2250 +type MPEGTS struct { + // in Go, empty structs share the same pointer, + // therefore they cannot be used as map keys + // or in equality operations. Prevent this. + unused int //nolint:unused +} + +func (f *MPEGTS) unmarshal(_ *unmarshalContext) error { + return nil +} + +// Codec implements Format. +func (f *MPEGTS) Codec() string { + return "MPEG-TS" +} + +// ClockRate implements Format. +func (f *MPEGTS) ClockRate() int { + return 90000 +} + +// PayloadType implements Format. +func (f *MPEGTS) PayloadType() uint8 { + return 33 +} + +// RTPMap implements Format. +func (f *MPEGTS) RTPMap() string { + return "MP2T/90000" +} + +// FMTP implements Format. +func (f *MPEGTS) FMTP() map[string]string { + return nil +} + +// PTSEqualsDTS implements Format. +func (f *MPEGTS) PTSEqualsDTS(*rtp.Packet) bool { + return true +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/opus.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/opus.go new file mode 100644 index 000000000..09902a4a6 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/opus.go @@ -0,0 +1,199 @@ +package format + +import ( + "fmt" + "strconv" + "strings" + + "github.com/pion/rtp" + + "github.com/bluenviron/gortsplib/v4/pkg/format/rtpsimpleaudio" +) + +// Opus is the RTP format for the Opus codec. +// Specification: https://datatracker.ietf.org/doc/html/rfc7587 +// Specification: https://webrtc-review.googlesource.com/c/src/+/129768 +type Opus struct { + PayloadTyp uint8 + ChannelCount int + + // + // Deprecated: replaced by ChannelCount. + IsStereo bool +} + +func (f *Opus) unmarshal(ctx *unmarshalContext) error { + f.PayloadTyp = ctx.payloadType + + if ctx.codec == "opus" { + tmp := strings.SplitN(ctx.clock, "/", 2) + if len(tmp) != 2 { + return fmt.Errorf("invalid clock (%v)", ctx.clock) + } + + sampleRate, err := strconv.ParseUint(tmp[0], 10, 31) + if err != nil || sampleRate != 48000 { + return fmt.Errorf("invalid sample rate: '%s", tmp[0]) + } + + channelCount, err := strconv.ParseUint(tmp[1], 10, 31) + if err != nil || channelCount != 2 { + return fmt.Errorf("invalid channel count: '%s'", tmp[1]) + } + + // assume mono + f.ChannelCount = 1 + f.IsStereo = false + + for key, val := range ctx.fmtp { + if key == "sprop-stereo" { + if val == "1" { + f.ChannelCount = 2 + f.IsStereo = true + } + } + } + } else { + tmp := strings.SplitN(ctx.clock, "/", 2) + if len(tmp) != 2 { + return fmt.Errorf("invalid clock (%v)", ctx.clock) + } + + sampleRate, err := strconv.ParseUint(tmp[0], 10, 31) + if err != nil || sampleRate != 48000 { + return fmt.Errorf("invalid sample rate: '%s'", tmp[0]) + } + + channelCount, err := strconv.ParseUint(tmp[1], 10, 31) + if err != nil { + return fmt.Errorf("invalid channel count: '%s'", tmp[1]) + } + + f.ChannelCount = int(channelCount) + } + + return nil +} + +// Codec implements Format. +func (f *Opus) Codec() string { + return "Opus" +} + +// ClockRate implements Format. +func (f *Opus) ClockRate() int { + // RFC7587: the RTP timestamp is incremented with a 48000 Hz + // clock rate for all modes of Opus and all sampling rates. + return 48000 +} + +// PayloadType implements Format. +func (f *Opus) PayloadType() uint8 { + return f.PayloadTyp +} + +// RTPMap implements Format. +func (f *Opus) RTPMap() string { + if f.ChannelCount <= 2 { + // RFC7587: The RTP clock rate in "a=rtpmap" MUST be 48000, and the + // number of channels MUST be 2. + return "opus/48000/2" + } + + return "multiopus/48000/" + strconv.FormatUint(uint64(f.ChannelCount), 10) +} + +// FMTP implements Format. +func (f *Opus) FMTP() map[string]string { + if f.ChannelCount <= 2 { + return map[string]string{ + "sprop-stereo": func() string { + if f.ChannelCount == 2 || (f.ChannelCount == 0 && f.IsStereo) { + return "1" + } + return "0" + }(), + } + } + + switch f.ChannelCount { + case 3: + return map[string]string{ + "num_streams": "2", + "coupled_streams": "1", + "channel_mapping": "0,2,1", + "sprop-maxcapturerate": "48000", + } + + case 4: + return map[string]string{ + "num_streams": "2", + "coupled_streams": "2", + "channel_mapping": "0,1,2,3", + "sprop-maxcapturerate": "48000", + } + + case 5: + return map[string]string{ + "num_streams": "3", + "coupled_streams": "2", + "channel_mapping": "0,4,1,2,3", + "sprop-maxcapturerate": "48000", + } + + case 6: + return map[string]string{ + "num_streams": "4", + "coupled_streams": "2", + "channel_mapping": "0,4,1,2,3,5", + "sprop-maxcapturerate": "48000", + } + + case 7: + return map[string]string{ + "num_streams": "4", + "coupled_streams": "3", + "channel_mapping": "0,4,1,2,3,5,6", + "sprop-maxcapturerate": "48000", + } + + default: // assume 8 + return map[string]string{ + "num_streams": "5", + "coupled_streams": "3", + "channel_mapping": "0,6,1,4,5,2,3,7", + "sprop-maxcapturerate": "48000", + } + } +} + +// PTSEqualsDTS implements Format. +func (f *Opus) PTSEqualsDTS(*rtp.Packet) bool { + return true +} + +// CreateDecoder creates a decoder able to decode the content of the format. +func (f *Opus) CreateDecoder() (*rtpsimpleaudio.Decoder, error) { + d := &rtpsimpleaudio.Decoder{} + + err := d.Init() + if err != nil { + return nil, err + } + + return d, nil +} + +// CreateEncoder creates an encoder able to encode the content of the format. +func (f *Opus) CreateEncoder() (*rtpsimpleaudio.Encoder, error) { + e := &rtpsimpleaudio.Encoder{ + PayloadType: f.PayloadTyp, + } + + err := e.Init() + if err != nil { + return nil, err + } + + return e, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpac3/decoder.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpac3/decoder.go new file mode 100644 index 000000000..60bce2988 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpac3/decoder.go @@ -0,0 +1,149 @@ +package rtpac3 + +import ( + "errors" + "fmt" + + "github.com/pion/rtp" + + "github.com/bluenviron/mediacommon/v2/pkg/codecs/ac3" +) + +// ErrMorePacketsNeeded is returned when more packets are needed. +var ErrMorePacketsNeeded = errors.New("need more packets") + +// ErrNonStartingPacketAndNoPrevious is returned when we received a non-starting +// packet of a fragmented frame and we didn't received anything before. +// It's normal to receive this when decoding a stream that has been already +// running for some time. +var ErrNonStartingPacketAndNoPrevious = errors.New( + "received a non-starting fragment without any previous starting fragment") + +func joinFragments(fragments [][]byte, size int) []byte { + ret := make([]byte, size) + n := 0 + for _, p := range fragments { + n += copy(ret[n:], p) + } + return ret +} + +// Decoder is a AC-3 decoder. +// Specification: https://datatracker.ietf.org/doc/html/rfc4184 +type Decoder struct { + firstPacketReceived bool + fragments [][]byte + fragmentsSize int + fragmentsExpected int + fragmentNextSeqNum uint16 +} + +// Init initializes the decoder. +func (d *Decoder) Init() error { + return nil +} + +func (d *Decoder) resetFragments() { + d.fragments = d.fragments[:0] + d.fragmentsSize = 0 +} + +// Decode decodes frames from a RTP packet. +// It returns the frames and the PTS of the first frame. +func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, error) { + if len(pkt.Payload) < 2 { + d.resetFragments() + return nil, fmt.Errorf("payload is too short") + } + + mbz := pkt.Payload[0] >> 2 + ft := pkt.Payload[0] & 0b11 + + if mbz != 0 { + d.resetFragments() + return nil, fmt.Errorf("invalid MBZ: %v", mbz) + } + + var frames [][]byte + + switch ft { + case 0: + d.resetFragments() + d.firstPacketReceived = true + + buf := pkt.Payload[2:] + + for { + var syncInfo ac3.SyncInfo + err := syncInfo.Unmarshal(buf) + if err != nil { + return nil, err + } + size := syncInfo.FrameSize() + + if len(buf) < size { + return nil, fmt.Errorf("payload is too short") + } + + frames = append(frames, buf[:size]) + buf = buf[size:] + + if len(buf) == 0 { + break + } + } + + case 1, 2: + d.resetFragments() + + var syncInfo ac3.SyncInfo + err := syncInfo.Unmarshal(pkt.Payload[2:]) + if err != nil { + return nil, err + } + size := syncInfo.FrameSize() + + le := len(pkt.Payload[2:]) + d.fragmentsSize = le + d.fragmentsExpected = size - le + d.fragments = append(d.fragments, pkt.Payload[2:]) + d.fragmentNextSeqNum = pkt.SequenceNumber + 1 + d.firstPacketReceived = true + + return nil, ErrMorePacketsNeeded + + case 3: + if d.fragmentsSize == 0 { + if !d.firstPacketReceived { + return nil, ErrNonStartingPacketAndNoPrevious + } + return nil, fmt.Errorf("received a subsequent fragment without previous fragments") + } + + if pkt.SequenceNumber != d.fragmentNextSeqNum { + d.resetFragments() + return nil, fmt.Errorf("discarding frame since a RTP packet is missing") + } + + le := len(pkt.Payload[2:]) + d.fragmentsSize += le + d.fragmentsExpected -= le + + if d.fragmentsExpected < 0 { + d.resetFragments() + return nil, fmt.Errorf("fragment is too big") + } + + d.fragments = append(d.fragments, pkt.Payload[2:]) + d.fragmentNextSeqNum++ + + if d.fragmentsExpected > 0 { + return nil, ErrMorePacketsNeeded + } + + frames = [][]byte{joinFragments(d.fragments, d.fragmentsSize)} + d.resetFragments() + } + + return frames, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpac3/encoder.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpac3/encoder.go new file mode 100644 index 000000000..6f4e34a7f --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpac3/encoder.go @@ -0,0 +1,201 @@ +package rtpac3 + +import ( + "crypto/rand" + + "github.com/pion/rtp" + + "github.com/bluenviron/mediacommon/v2/pkg/codecs/ac3" +) + +const ( + rtpVersion = 2 + defaultPayloadMaxSize = 1460 // 1500 (UDP MTU) - 20 (IP header) - 8 (UDP header) - 12 (RTP header) +) + +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 packetCount(avail, le int) int { + n := le / avail + if (le % avail) != 0 { + n++ + } + return n +} + +// Encoder is a AC-3 encoder. +// Specification: https://datatracker.ietf.org/doc/html/rfc4184 +type Encoder struct { + // payload type of packets. + PayloadType uint8 + + // SSRC of packets (optional). + // It defaults to a random value. + SSRC *uint32 + + // initial sequence number of packets (optional). + // It defaults to a random value. + InitialSequenceNumber *uint16 + + // maximum size of packet payloads (optional). + // It defaults to 1460. + PayloadMaxSize int + + sequenceNumber uint16 +} + +// Init initializes the encoder. +func (e *Encoder) Init() error { + if e.SSRC == nil { + v, err := randUint32() + if err != nil { + return err + } + e.SSRC = &v + } + if e.InitialSequenceNumber == nil { + v, err := randUint32() + if err != nil { + return err + } + v2 := uint16(v) + e.InitialSequenceNumber = &v2 + } + if e.PayloadMaxSize == 0 { + e.PayloadMaxSize = defaultPayloadMaxSize + } + + e.sequenceNumber = *e.InitialSequenceNumber + return nil +} + +// Encode encodes frames into RTP packets. +func (e *Encoder) Encode(frames [][]byte) ([]*rtp.Packet, error) { + var rets []*rtp.Packet + var batch [][]byte + timestamp := uint32(0) + + // split frames into batches + for _, frame := range frames { + if e.lenAggregated(batch, frame) <= e.PayloadMaxSize { + // add to existing batch + batch = append(batch, frame) + } else { + // write current batch + if batch != nil { + pkts, err := e.writeBatch(batch, timestamp) + if err != nil { + return nil, err + } + rets = append(rets, pkts...) + timestamp += uint32(len(batch)) * ac3.SamplesPerFrame + } + + // initialize new batch + batch = [][]byte{frame} + } + } + + // write last batch + pkts, err := e.writeBatch(batch, timestamp) + if err != nil { + return nil, err + } + rets = append(rets, pkts...) + + return rets, nil +} + +func (e *Encoder) writeBatch(frames [][]byte, timestamp uint32) ([]*rtp.Packet, error) { + if len(frames) != 1 || e.lenAggregated(frames, nil) < e.PayloadMaxSize { + return e.writeAggregated(frames, timestamp) + } + + return e.writeFragmented(frames[0], timestamp) +} + +func (e *Encoder) writeFragmented(frame []byte, timestamp uint32) ([]*rtp.Packet, error) { + avail := e.PayloadMaxSize - 4 + le := len(frame) + packetCount := packetCount(avail, le) + + ret := make([]*rtp.Packet, packetCount) + le = avail + + ft := uint8(2) + if avail >= (len(frame) * 5 / 8) { + ft = 1 + } + + for i := range ret { + if i == (packetCount - 1) { + le = len(frame) + } + + payload := make([]byte, 2+le) + payload[0] = ft + payload[1] = uint8(packetCount) + + n := copy(payload[2:], frame) + frame = frame[n:] + + ret[i] = &rtp.Packet{ + Header: rtp.Header{ + Version: rtpVersion, + PayloadType: e.PayloadType, + SequenceNumber: e.sequenceNumber, + Timestamp: timestamp, + SSRC: *e.SSRC, + Marker: i == (packetCount - 1), + }, + Payload: payload, + } + + e.sequenceNumber++ + ft = 3 + } + + return ret, nil +} + +func (e *Encoder) lenAggregated(frames [][]byte, addFrame []byte) int { + n := 2 + len(addFrame) + for _, frame := range frames { + n += len(frame) + } + return n +} + +func (e *Encoder) writeAggregated(frames [][]byte, timestamp uint32) ([]*rtp.Packet, error) { + payload := make([]byte, e.lenAggregated(frames, nil)) + + payload[1] = uint8(len(frames)) + + n := 2 + for _, frame := range frames { + n += copy(payload[n:], frame) + } + + pkt := &rtp.Packet{ + Header: rtp.Header{ + Version: rtpVersion, + PayloadType: e.PayloadType, + SequenceNumber: e.sequenceNumber, + Timestamp: timestamp, + SSRC: *e.SSRC, + Marker: true, + }, + Payload: payload, + } + + e.sequenceNumber++ + + return []*rtp.Packet{pkt}, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpac3/rtpac3.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpac3/rtpac3.go new file mode 100644 index 000000000..2024084df --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpac3/rtpac3.go @@ -0,0 +1,2 @@ +// Package rtpac3 contains a RTP/AC-3 decoder and encoder. +package rtpac3 diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpav1/decoder.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpav1/decoder.go new file mode 100644 index 000000000..f5dd9abc2 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpav1/decoder.go @@ -0,0 +1,202 @@ +package rtpav1 + +import ( + "errors" + "fmt" + + "github.com/bluenviron/mediacommon/v2/pkg/codecs/av1" + "github.com/pion/rtp" +) + +// ErrMorePacketsNeeded is returned when more packets are needed. +var ErrMorePacketsNeeded = errors.New("need more packets") + +// ErrNonStartingPacketAndNoPrevious is returned when we received a non-starting +// packet of a fragmented NALU and we didn't received anything before. +// It's normal to receive this when decoding a stream that has been already +// running for some time. +var ErrNonStartingPacketAndNoPrevious = errors.New( + "received a non-starting fragment without any previous starting fragment") + +func joinFragments(fragments [][]byte, size int) []byte { + ret := make([]byte, size) + n := 0 + for _, p := range fragments { + n += copy(ret[n:], p) + } + return ret +} + +func tuSize(tu [][]byte) int { + s := 0 + for _, obu := range tu { + s += len(obu) + } + return s +} + +// Decoder is a RTP/AV1 decoder. +// Specification: https://aomediacodec.github.io/av1-rtp-spec/ +type Decoder struct { + firstPacketReceived bool + fragments [][]byte + fragmentsSize int + fragmentNextSeqNum uint16 + + // for Decode() + frameBuffer [][]byte + frameBufferLen int + frameBufferSize int +} + +// Init initializes the decoder. +func (d *Decoder) Init() error { + return nil +} + +func (d *Decoder) resetFragments() { + d.fragments = d.fragments[:0] + d.fragmentsSize = 0 +} + +func (d *Decoder) decodeOBUs(pkt *rtp.Packet) ([][]byte, error) { + if len(pkt.Payload) < 2 { + return nil, fmt.Errorf("invalid payload size") + } + + z := (pkt.Payload[0] & 0b10000000) != 0 + y := (pkt.Payload[0] & 0b01000000) != 0 + w := (pkt.Payload[0] >> 4) & 0b11 + payload := pkt.Payload[1:] + var obus [][]byte + + for len(payload) > 0 { + var obu []byte + + if w == 0 || byte(len(obus)) < (w-1) { + var size av1.LEB128 + n, err := size.Unmarshal(payload) + if err != nil { + d.resetFragments() + return nil, err + } + payload = payload[n:] + + if size == 0 || len(payload) < int(size) { + d.resetFragments() + return nil, fmt.Errorf("invalid OBU size") + } + + obu, payload = payload[:size], payload[size:] + } else { + obu, payload = payload, nil + } + + obus = append(obus, obu) + } + + if w != 0 && len(obus) != int(w) { + return nil, fmt.Errorf("invalid W field") + } + + // first OBU is continuation of previous one + if z { + if d.fragmentsSize == 0 { + if !d.firstPacketReceived { + return nil, ErrNonStartingPacketAndNoPrevious + } + + return nil, fmt.Errorf("received a subsequent fragment without previous fragments") + } + + d.firstPacketReceived = true + + if pkt.SequenceNumber != d.fragmentNextSeqNum { + d.resetFragments() + return nil, fmt.Errorf("discarding frame since a RTP packet is missing") + } + + d.fragmentsSize += len(obus[0]) + + if d.fragmentsSize > av1.MaxTemporalUnitSize { + errSize := d.fragmentsSize + d.resetFragments() + return nil, fmt.Errorf("temporal unit size (%d) is too big, maximum is %d", + errSize, av1.MaxTemporalUnitSize) + } + + d.fragments = append(d.fragments, obus[0]) + d.fragmentNextSeqNum++ + + if len(obus) == 1 && y { + return nil, ErrMorePacketsNeeded + } + + obus[0] = joinFragments(d.fragments, d.fragmentsSize) + d.resetFragments() + } else { + d.firstPacketReceived = true + } + + // last OBU will continue in next packet + if y { + var obu []byte + obu, obus = obus[len(obus)-1], obus[:len(obus)-1] + + d.fragmentsSize = len(obu) + d.fragments = append(d.fragments, obu) + d.fragmentNextSeqNum = pkt.SequenceNumber + 1 + + if len(obus) == 0 { + return nil, ErrMorePacketsNeeded + } + } + + return obus, nil +} + +// Decode decodes a temporal unit from a RTP packet. +func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, error) { + obus, err := d.decodeOBUs(pkt) + if err != nil { + return nil, err + } + l := len(obus) + + if (d.frameBufferLen + l) > av1.MaxOBUsPerTemporalUnit { + errCount := d.frameBufferLen + l + d.frameBuffer = nil + d.frameBufferLen = 0 + d.frameBufferSize = 0 + return nil, fmt.Errorf("OBU count (%d) exceeds maximum allowed (%d)", + errCount, av1.MaxOBUsPerTemporalUnit) + } + + addSize := tuSize(obus) + + if (d.frameBufferSize + addSize) > av1.MaxTemporalUnitSize { + errSize := d.frameBufferSize + addSize + d.frameBuffer = nil + d.frameBufferLen = 0 + d.frameBufferSize = 0 + return nil, fmt.Errorf("temporal unit size (%d) is too big, maximum is %d", + errSize, av1.MaxOBUsPerTemporalUnit) + } + + d.frameBuffer = append(d.frameBuffer, obus...) + d.frameBufferLen += l + d.frameBufferSize += addSize + + if !pkt.Marker { + return nil, ErrMorePacketsNeeded + } + + ret := d.frameBuffer + + // do not reuse frameBuffer to avoid race conditions + d.frameBuffer = nil + d.frameBufferLen = 0 + d.frameBufferSize = 0 + + return ret, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpav1/encoder.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpav1/encoder.go new file mode 100644 index 000000000..161616fea --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpav1/encoder.go @@ -0,0 +1,171 @@ +package rtpav1 + +import ( + "crypto/rand" + + "github.com/bluenviron/mediacommon/v2/pkg/codecs/av1" + "github.com/pion/rtp" +) + +const ( + rtpVersion = 2 + defaultPayloadMaxSize = 1460 // 1500 (UDP MTU) - 20 (IP header) - 8 (UDP header) - 12 (RTP header) +) + +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 +} + +// Encoder is a RTP/AV1 encoder. +// Specification: https://aomediacodec.github.io/av1-rtp-spec/ +type Encoder struct { + // payload type of packets. + PayloadType uint8 + + // SSRC of packets (optional). + // It defaults to a random value. + SSRC *uint32 + + // initial sequence number of packets (optional). + // It defaults to a random value. + InitialSequenceNumber *uint16 + + // maximum size of packet payloads (optional). + // It defaults to 1460. + PayloadMaxSize int + + sequenceNumber uint16 +} + +// Init initializes the encoder. +func (e *Encoder) Init() error { + if e.SSRC == nil { + v, err := randUint32() + if err != nil { + return err + } + e.SSRC = &v + } + if e.InitialSequenceNumber == nil { + v, err := randUint32() + if err != nil { + return err + } + v2 := uint16(v) + e.InitialSequenceNumber = &v2 + } + if e.PayloadMaxSize == 0 { + e.PayloadMaxSize = defaultPayloadMaxSize + } + + e.sequenceNumber = *e.InitialSequenceNumber + return nil +} + +// Encode encodes OBUs into RTP packets. +func (e *Encoder) Encode(obus [][]byte) ([]*rtp.Packet, error) { + var curPacket *rtp.Packet + var packets []*rtp.Packet + obusInPacket := 0 + + createNewPacket := func(z bool) { + curPacket = &rtp.Packet{ + Header: rtp.Header{ + Version: rtpVersion, + PayloadType: e.PayloadType, + SequenceNumber: e.sequenceNumber, + SSRC: *e.SSRC, + }, + Payload: []byte{0}, + } + e.sequenceNumber++ + packets = append(packets, curPacket) + obusInPacket = 0 + + if z { + curPacket.Payload[0] |= 1 << 7 + } + } + + finalizeCurPacket := func(y bool) { + if y { + curPacket.Payload[0] |= 1 << 6 + } + } + + createNewPacket(false) + + maxFragmentedLEBSize := av1.LEB128(e.PayloadMaxSize).MarshalSize() + + for i, obu := range obus { + for { + avail := e.PayloadMaxSize - len(curPacket.Payload) + obuLen := len(obu) + omitSize := (i == (len(obus)-1) && obusInPacket < 3) + + var obuLenLEB av1.LEB128 + var obuLenLEBSize int + var needed int + + if omitSize { + needed = obuLen + } else { + obuLenLEB = av1.LEB128(obuLen) + obuLenLEBSize = obuLenLEB.MarshalSize() + needed = obuLen + obuLenLEBSize + } + + if needed <= avail { + if omitSize { + curPacket.Payload[0] |= byte((obusInPacket + 1) << 4) // W + curPacket.Payload = append(curPacket.Payload, obu...) + } else { + buf := make([]byte, obuLenLEBSize) + obuLenLEB.MarshalTo(buf) + curPacket.Payload = append(curPacket.Payload, buf...) + curPacket.Payload = append(curPacket.Payload, obu...) + obusInPacket++ + } + break + } + + if omitSize { + if avail > 0 { + curPacket.Payload[0] |= byte((obusInPacket + 1) << 4) // W + curPacket.Payload = append(curPacket.Payload, obu[:avail]...) + obu = obu[avail:] + } + } else { + if avail > maxFragmentedLEBSize { + fragmentLen := avail - maxFragmentedLEBSize + fragmentLenLEB := av1.LEB128(fragmentLen) + fragmentLenLEBSize := fragmentLenLEB.MarshalSize() + + buf := make([]byte, fragmentLenLEBSize) + fragmentLenLEB.MarshalTo(buf) + curPacket.Payload = append(curPacket.Payload, buf...) + curPacket.Payload = append(curPacket.Payload, obu[:fragmentLen]...) + obu = obu[fragmentLen:] + } + } + + finalizeCurPacket(true) + createNewPacket(true) + } + } + + finalizeCurPacket(false) + + if av1.IsRandomAccess2(obus) { + packets[0].Payload[0] |= 1 << 3 + } + + packets[len(packets)-1].Marker = true + + return packets, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpav1/rtpav1.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpav1/rtpav1.go new file mode 100644 index 000000000..161b1affc --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpav1/rtpav1.go @@ -0,0 +1,2 @@ +// Package rtpav1 contains a RTP/AV1 decoder and encoder. +package rtpav1 diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtph264/decoder.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtph264/decoder.go new file mode 100644 index 000000000..725ecd5ac --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtph264/decoder.go @@ -0,0 +1,306 @@ +package rtph264 + +import ( + "bytes" + "errors" + "fmt" + + "github.com/pion/rtp" + + "github.com/bluenviron/mediacommon/v2/pkg/codecs/h264" +) + +// ErrMorePacketsNeeded is returned when more packets are needed. +var ErrMorePacketsNeeded = errors.New("need more packets") + +// ErrNonStartingPacketAndNoPrevious is returned when we received a non-starting +// packet of a fragmented NALU and we didn't received anything before. +// It's normal to receive this when decoding a stream that has been already +// running for some time. +var ErrNonStartingPacketAndNoPrevious = errors.New( + "received a non-starting fragment without any previous starting fragment") + +func joinFragments(fragments [][]byte, size int) []byte { + ret := make([]byte, size) + n := 0 + for _, p := range fragments { + n += copy(ret[n:], p) + } + return ret +} + +func isAllZero(buf []byte) bool { + for _, b := range buf { + if b != 0 { + return false + } + } + return true +} + +func auSize(au [][]byte) int { + s := 0 + for _, nalu := range au { + s += len(nalu) + } + return s +} + +// Decoder is a RTP/H264 decoder. +// Specification: https://datatracker.ietf.org/doc/html/rfc6184 +type Decoder struct { + // indicates the packetization mode. + PacketizationMode int + + firstPacketReceived bool + fragments [][]byte + fragmentsSize int + fragmentNextSeqNum uint16 + annexBMode bool + + // for Decode() + frameBuffer [][]byte + frameBufferLen int + frameBufferSize int + frameBufferTimestamp uint32 +} + +// Init initializes the decoder. +func (d *Decoder) Init() error { + if d.PacketizationMode >= 2 { + return fmt.Errorf("PacketizationMode >= 2 is not supported") + } + return nil +} + +func (d *Decoder) resetFragments() { + d.fragments = d.fragments[:0] + d.fragmentsSize = 0 +} + +func (d *Decoder) decodeNALUs(pkt *rtp.Packet) ([][]byte, error) { + if len(pkt.Payload) < 1 { + d.resetFragments() + return nil, fmt.Errorf("payload is too short") + } + + typ := h264.NALUType(pkt.Payload[0] & 0x1F) + var nalus [][]byte + + switch typ { + case h264.NALUTypeFUA: + if len(pkt.Payload) < 2 { + return nil, fmt.Errorf("invalid FU-A packet (invalid size)") + } + + start := pkt.Payload[1] >> 7 + end := (pkt.Payload[1] >> 6) & 0x01 + + if start == 1 { + d.resetFragments() + + nri := (pkt.Payload[0] >> 5) & 0x03 + typ := pkt.Payload[1] & 0x1F + d.fragmentsSize = len(pkt.Payload[1:]) + d.fragments = append(d.fragments, []byte{(nri << 5) | typ}, pkt.Payload[2:]) + d.fragmentNextSeqNum = pkt.SequenceNumber + 1 + d.firstPacketReceived = true + + // RFC 6184 clearly states: + // + // A fragmented NAL unit MUST NOT be transmitted in one FU; that is, the + // Start bit and End bit MUST NOT both be set to one in the same FU + // header. + // + // However, some vendors camera (e.g. CostarHD) have been observed to nevertheless + // emit one fragmented NAL unit for sufficiently small P-frames. + if end != 0 { + nalus = [][]byte{joinFragments(d.fragments, d.fragmentsSize)} + d.resetFragments() + break + } + + return nil, ErrMorePacketsNeeded + } + + if d.fragmentsSize == 0 { + if !d.firstPacketReceived { + return nil, ErrNonStartingPacketAndNoPrevious + } + + return nil, fmt.Errorf("invalid FU-A packet (non-starting)") + } + + if pkt.SequenceNumber != d.fragmentNextSeqNum { + d.resetFragments() + return nil, fmt.Errorf("discarding frame since a RTP packet is missing") + } + + d.fragmentsSize += len(pkt.Payload[2:]) + + if d.fragmentsSize > h264.MaxAccessUnitSize { + errSize := d.fragmentsSize + d.resetFragments() + return nil, fmt.Errorf("NALU size (%d) is too big, maximum is %d", + errSize, h264.MaxAccessUnitSize) + } + + d.fragments = append(d.fragments, pkt.Payload[2:]) + d.fragmentNextSeqNum++ + + if end != 1 { + return nil, ErrMorePacketsNeeded + } + + nalus = [][]byte{joinFragments(d.fragments, d.fragmentsSize)} + d.resetFragments() + + case h264.NALUTypeSTAPA: + d.resetFragments() + + payload := pkt.Payload[1:] + + for { + if len(payload) < 2 { + return nil, fmt.Errorf("invalid STAP-A packet (invalid size)") + } + + size := uint16(payload[0])<<8 | uint16(payload[1]) + payload = payload[2:] + + if size == 0 { + // discard padding + if isAllZero(payload) { + break + } + + return nil, fmt.Errorf("invalid STAP-A packet (invalid size)") + } + + if int(size) > len(payload) { + return nil, fmt.Errorf("invalid STAP-A packet (invalid size)") + } + + nalus = append(nalus, payload[:size]) + payload = payload[size:] + + if len(payload) == 0 { + break + } + } + + if nalus == nil { + return nil, fmt.Errorf("STAP-A packet doesn't contain any NALU") + } + + d.firstPacketReceived = true + + case h264.NALUTypeSTAPB, h264.NALUTypeMTAP16, + h264.NALUTypeMTAP24, h264.NALUTypeFUB: + d.resetFragments() + d.firstPacketReceived = true + return nil, fmt.Errorf("packet type not supported (%v)", typ) + + default: + d.resetFragments() + d.firstPacketReceived = true + nalus = [][]byte{pkt.Payload} + } + + nalus, err := d.removeAnnexB(nalus) + if err != nil { + return nil, err + } + + return nalus, nil +} + +// Decode decodes an access unit from a RTP packet. +func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, error) { + nalus, err := d.decodeNALUs(pkt) + if err != nil { + return nil, err + } + l := len(nalus) + + // support splitting access units by timestamp. + // (some cameras do not use the Marker field, like the FLIR M400) + if d.frameBuffer != nil && pkt.Timestamp != d.frameBufferTimestamp { + ret := d.frameBuffer + d.resetFrameBuffer() + + err = d.addToFrameBuffer(nalus, l, pkt.Timestamp) + if err != nil { + return nil, err + } + + return ret, nil + } + + err = d.addToFrameBuffer(nalus, l, pkt.Timestamp) + if err != nil { + return nil, err + } + + if !pkt.Marker { + return nil, ErrMorePacketsNeeded + } + + ret := d.frameBuffer + d.resetFrameBuffer() + + return ret, nil +} + +func (d *Decoder) resetFrameBuffer() { + d.frameBuffer = nil // do not reuse frameBuffer to avoid race conditions + d.frameBufferLen = 0 + d.frameBufferSize = 0 +} + +func (d *Decoder) addToFrameBuffer(nalus [][]byte, l int, ts uint32) error { + if (d.frameBufferLen + l) > h264.MaxNALUsPerAccessUnit { + errCount := d.frameBufferLen + l + d.resetFrameBuffer() + return fmt.Errorf("NALU count (%d) exceeds maximum allowed (%d)", + errCount, h264.MaxNALUsPerAccessUnit) + } + + addSize := auSize(nalus) + + if (d.frameBufferSize + addSize) > h264.MaxAccessUnitSize { + errSize := d.frameBufferSize + addSize + d.resetFrameBuffer() + return fmt.Errorf("access unit size (%d) is too big, maximum is %d", + errSize, h264.MaxAccessUnitSize) + } + + d.frameBuffer = append(d.frameBuffer, nalus...) + d.frameBufferLen += l + d.frameBufferSize += addSize + d.frameBufferTimestamp = ts + return nil +} + +// some cameras / servers wrap NALUs into Annex-B +func (d *Decoder) removeAnnexB(nalus [][]byte) ([][]byte, error) { + if len(nalus) == 1 { + nalu := nalus[0] + + if !d.annexBMode && bytes.Contains(nalu, []byte{0x00, 0x00, 0x00, 0x01}) { + d.annexBMode = true + } + + if d.annexBMode { + if !bytes.HasPrefix(nalu, []byte{0x00, 0x00, 0x00, 0x01}) { + nalu = append([]byte{0x00, 0x00, 0x00, 0x01}, nalu...) + } + + var annexb h264.AnnexB + err := annexb.Unmarshal(nalu) + return annexb, err + } + } + + return nalus, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtph264/encoder.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtph264/encoder.go new file mode 100644 index 000000000..55938ea25 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtph264/encoder.go @@ -0,0 +1,248 @@ +package rtph264 + +import ( + "crypto/rand" + "fmt" + + "github.com/pion/rtp" + + "github.com/bluenviron/mediacommon/v2/pkg/codecs/h264" +) + +const ( + rtpVersion = 2 + defaultPayloadMaxSize = 1460 // 1500 (UDP MTU) - 20 (IP header) - 8 (UDP header) - 12 (RTP header) +) + +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 lenAggregated(nalus [][]byte, addNALU []byte) int { + n := 1 // header + + for _, nalu := range nalus { + n += 2 // size + n += len(nalu) // nalu + } + + if addNALU != nil { + n += 2 // size + n += len(addNALU) // nalu + } + + return n +} + +func packetCount(avail, le int) int { + n := le / avail + if (le % avail) != 0 { + n++ + } + return n +} + +// Encoder is a RTP/H264 encoder. +// Specification: https://datatracker.ietf.org/doc/html/rfc6184 +type Encoder struct { + // payload type of packets. + PayloadType uint8 + + // SSRC of packets (optional). + // It defaults to a random value. + SSRC *uint32 + + // initial sequence number of packets (optional). + // It defaults to a random value. + InitialSequenceNumber *uint16 + + // maximum size of packet payloads (optional). + // It defaults to 1460. + PayloadMaxSize int + + PacketizationMode int + + sequenceNumber uint16 +} + +// Init initializes the encoder. +func (e *Encoder) Init() error { + if e.PacketizationMode >= 2 { + return fmt.Errorf("PacketizationMode >= 2 is not supported") + } + + if e.SSRC == nil { + v, err := randUint32() + if err != nil { + return err + } + e.SSRC = &v + } + if e.InitialSequenceNumber == nil { + v, err := randUint32() + if err != nil { + return err + } + v2 := uint16(v) + e.InitialSequenceNumber = &v2 + } + if e.PayloadMaxSize == 0 { + e.PayloadMaxSize = defaultPayloadMaxSize + } + + e.sequenceNumber = *e.InitialSequenceNumber + return nil +} + +// Encode encodes an access unit into RTP/H264 packets. +func (e *Encoder) Encode(au [][]byte) ([]*rtp.Packet, error) { + var rets []*rtp.Packet + var batch [][]byte + + // split NALUs into batches + for _, nalu := range au { + if lenAggregated(batch, nalu) <= e.PayloadMaxSize { + // add to existing batch + batch = append(batch, nalu) + } else { + // write current batch + if batch != nil { + pkts, err := e.writeBatch(batch, false) + if err != nil { + return nil, err + } + rets = append(rets, pkts...) + } + + // initialize new batch + batch = [][]byte{nalu} + } + } + + // write final batch + // marker is used to indicate when all NALUs with same PTS have been sent + pkts, err := e.writeBatch(batch, true) + if err != nil { + return nil, err + } + rets = append(rets, pkts...) + + return rets, nil +} + +func (e *Encoder) writeBatch(nalus [][]byte, marker bool) ([]*rtp.Packet, error) { + if len(nalus) == 1 { + // the NALU fits into a single RTP packet + if len(nalus[0]) < e.PayloadMaxSize { + return e.writeSingle(nalus[0], marker) + } + + // split the NALU into multiple fragmentation packet + return e.writeFragmented(nalus[0], marker) + } + + return e.writeAggregated(nalus, marker) +} + +func (e *Encoder) writeSingle(nalu []byte, marker bool) ([]*rtp.Packet, error) { + pkt := &rtp.Packet{ + Header: rtp.Header{ + Version: rtpVersion, + PayloadType: e.PayloadType, + SequenceNumber: e.sequenceNumber, + SSRC: *e.SSRC, + Marker: marker, + }, + Payload: nalu, + } + + e.sequenceNumber++ + + return []*rtp.Packet{pkt}, nil +} + +func (e *Encoder) writeFragmented(nalu []byte, marker bool) ([]*rtp.Packet, error) { + // use only FU-A, not FU-B, since we always use non-interleaved mode + // (packetization-mode=1) + avail := e.PayloadMaxSize - 2 + le := len(nalu) - 1 + packetCount := packetCount(avail, le) + + ret := make([]*rtp.Packet, packetCount) + + nri := (nalu[0] >> 5) & 0x03 + typ := nalu[0] & 0x1F + nalu = nalu[1:] // remove header + le = avail + start := uint8(1) + end := uint8(0) + + for i := range ret { + if i == (packetCount - 1) { + end = 1 + le = len(nalu) + } + + data := make([]byte, 2+le) + data[0] = (nri << 5) | uint8(h264.NALUTypeFUA) + data[1] = (start << 7) | (end << 6) | typ + copy(data[2:], nalu) + nalu = nalu[le:] + + ret[i] = &rtp.Packet{ + Header: rtp.Header{ + Version: rtpVersion, + PayloadType: e.PayloadType, + SequenceNumber: e.sequenceNumber, + SSRC: *e.SSRC, + Marker: (i == (packetCount-1) && marker), + }, + Payload: data, + } + + e.sequenceNumber++ + start = 0 + } + + return ret, nil +} + +func (e *Encoder) writeAggregated(nalus [][]byte, marker bool) ([]*rtp.Packet, error) { + payload := make([]byte, lenAggregated(nalus, nil)) + + // header + payload[0] = uint8(h264.NALUTypeSTAPA) + pos := 1 + + for _, nalu := range nalus { + // size + naluLen := len(nalu) + payload[pos] = uint8(naluLen >> 8) + payload[pos+1] = uint8(naluLen) + pos += 2 + + // nalu + copy(payload[pos:], nalu) + pos += naluLen + } + + pkt := &rtp.Packet{ + Header: rtp.Header{ + Version: rtpVersion, + PayloadType: e.PayloadType, + SequenceNumber: e.sequenceNumber, + SSRC: *e.SSRC, + Marker: marker, + }, + Payload: payload, + } + + e.sequenceNumber++ + + return []*rtp.Packet{pkt}, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtph264/rtph264.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtph264/rtph264.go new file mode 100644 index 000000000..ab0c3f6fd --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtph264/rtph264.go @@ -0,0 +1,2 @@ +// Package rtph264 contains a RTP/H264 decoder and encoder. +package rtph264 diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtph265/decoder.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtph265/decoder.go new file mode 100644 index 000000000..837af18ec --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtph265/decoder.go @@ -0,0 +1,220 @@ +package rtph265 + +import ( + "errors" + "fmt" + + "github.com/pion/rtp" + + "github.com/bluenviron/mediacommon/v2/pkg/codecs/h265" +) + +// ErrMorePacketsNeeded is returned when more packets are needed. +var ErrMorePacketsNeeded = errors.New("need more packets") + +// ErrNonStartingPacketAndNoPrevious is returned when we received a non-starting +// packet of a fragmented NALU and we didn't received anything before. +// It's normal to receive this when decoding a stream that has been already +// running for some time. +var ErrNonStartingPacketAndNoPrevious = errors.New( + "received a non-starting fragment without any previous starting fragment") + +func joinFragments(fragments [][]byte, size int) []byte { + ret := make([]byte, size) + n := 0 + for _, p := range fragments { + n += copy(ret[n:], p) + } + return ret +} + +func auSize(au [][]byte) int { + s := 0 + for _, nalu := range au { + s += len(nalu) + } + return s +} + +// Decoder is a RTP/H265 decoder. +// Specification: https://datatracker.ietf.org/doc/html/rfc7798 +type Decoder struct { + // indicates that NALUs have an additional field that specifies the decoding order. + MaxDONDiff int + + firstPacketReceived bool + fragments [][]byte + fragmentsSize int + fragmentNextSeqNum uint16 + + // for Decode() + frameBuffer [][]byte + frameBufferLen int + frameBufferSize int +} + +// Init initializes the decoder. +func (d *Decoder) Init() error { + if d.MaxDONDiff != 0 { + return fmt.Errorf("MaxDONDiff != 0 is not supported (yet)") + } + return nil +} + +func (d *Decoder) resetFragments() { + d.fragments = d.fragments[:0] + d.fragmentsSize = 0 +} + +func (d *Decoder) decodeNALUs(pkt *rtp.Packet) ([][]byte, error) { + if len(pkt.Payload) < 2 { + d.resetFragments() + return nil, fmt.Errorf("payload is too short") + } + + typ := h265.NALUType((pkt.Payload[0] >> 1) & 0b111111) + var nalus [][]byte + + switch typ { + case h265.NALUType_AggregationUnit: + d.resetFragments() + + payload := pkt.Payload[2:] + + for { + if len(payload) < 2 { + return nil, fmt.Errorf("invalid aggregation unit (invalid size)") + } + + size := uint16(payload[0])<<8 | uint16(payload[1]) + payload = payload[2:] + + if size == 0 || int(size) > len(payload) { + return nil, fmt.Errorf("invalid aggregation unit (invalid size)") + } + + nalus = append(nalus, payload[:size]) + payload = payload[size:] + + if len(payload) == 0 { + break + } + } + + d.firstPacketReceived = true + + case h265.NALUType_FragmentationUnit: + if len(pkt.Payload) < 3 { + d.resetFragments() + return nil, fmt.Errorf("payload is too short") + } + + start := pkt.Payload[2] >> 7 + end := (pkt.Payload[2] >> 6) & 0x01 + + if start == 1 { + d.resetFragments() + + if end != 0 { + return nil, fmt.Errorf("invalid fragmentation unit (can't contain both a start and end bit)") + } + + typ := pkt.Payload[2] & 0b111111 + head := uint16(pkt.Payload[0]&0b10000001)<<8 | uint16(typ)<<9 | uint16(pkt.Payload[1]) + d.fragmentsSize = len(pkt.Payload[1:]) + d.fragments = append(d.fragments, []byte{byte(head >> 8), byte(head)}, pkt.Payload[3:]) + d.fragmentNextSeqNum = pkt.SequenceNumber + 1 + d.firstPacketReceived = true + + return nil, ErrMorePacketsNeeded + } + + if d.fragmentsSize == 0 { + if !d.firstPacketReceived { + return nil, ErrNonStartingPacketAndNoPrevious + } + + return nil, fmt.Errorf("invalid fragmentation unit (non-starting)") + } + + if pkt.SequenceNumber != d.fragmentNextSeqNum { + d.resetFragments() + return nil, fmt.Errorf("discarding frame since a RTP packet is missing") + } + + d.fragmentsSize += len(pkt.Payload[3:]) + + if d.fragmentsSize > h265.MaxAccessUnitSize { + errSize := d.fragmentsSize + d.resetFragments() + return nil, fmt.Errorf("NALU size (%d) is too big, maximum is %d", + errSize, h265.MaxAccessUnitSize) + } + + d.fragments = append(d.fragments, pkt.Payload[3:]) + d.fragmentNextSeqNum++ + + if end != 1 { + return nil, ErrMorePacketsNeeded + } + + nalus = [][]byte{joinFragments(d.fragments, d.fragmentsSize)} + d.resetFragments() + + case h265.NALUType_PACI: + d.resetFragments() + return nil, fmt.Errorf("PACI packets are not supported (yet)") + + default: + d.resetFragments() + nalus = [][]byte{pkt.Payload} + } + + return nalus, nil +} + +// Decode decodes an access unit from a RTP packet. +func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, error) { + nalus, err := d.decodeNALUs(pkt) + if err != nil { + return nil, err + } + l := len(nalus) + + if (d.frameBufferLen + l) > h265.MaxNALUsPerAccessUnit { + errCount := d.frameBufferLen + l + d.frameBuffer = nil + d.frameBufferLen = 0 + d.frameBufferSize = 0 + return nil, fmt.Errorf("NALU count (%d) exceeds maximum allowed (%d)", + errCount, h265.MaxNALUsPerAccessUnit) + } + + addSize := auSize(nalus) + + if (d.frameBufferSize + addSize) > h265.MaxAccessUnitSize { + errSize := d.frameBufferSize + addSize + d.frameBuffer = nil + d.frameBufferLen = 0 + d.frameBufferSize = 0 + return nil, fmt.Errorf("access unit size (%d) is too big, maximum is %d", + errSize, h265.MaxAccessUnitSize) + } + + d.frameBuffer = append(d.frameBuffer, nalus...) + d.frameBufferLen += l + d.frameBufferSize += addSize + + if !pkt.Marker { + return nil, ErrMorePacketsNeeded + } + + ret := d.frameBuffer + + // do not reuse frameBuffer to avoid race conditions + d.frameBuffer = nil + d.frameBufferLen = 0 + d.frameBufferSize = 0 + + return ret, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtph265/encoder.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtph265/encoder.go new file mode 100644 index 000000000..c0d789146 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtph265/encoder.go @@ -0,0 +1,247 @@ +package rtph265 + +import ( + "crypto/rand" + "fmt" + + "github.com/pion/rtp" +) + +const ( + rtpVersion = 2 + defaultPayloadMaxSize = 1460 // 1500 (UDP MTU) - 20 (IP header) - 8 (UDP header) - 12 (RTP header) +) + +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 packetCount(avail, le int) int { + n := le / avail + if (le % avail) != 0 { + n++ + } + return n +} + +// Encoder is a RTP/H265 encoder. +// Specification: https://datatracker.ietf.org/doc/html/rfc7798 +type Encoder struct { + // payload type of packets. + PayloadType uint8 + + // SSRC of packets (optional). + // It defaults to a random value. + SSRC *uint32 + + // initial sequence number of packets (optional). + // It defaults to a random value. + InitialSequenceNumber *uint16 + + // maximum size of packet payloads (optional). + // It defaults to 1460. + PayloadMaxSize int + + // indicates that NALUs have an additional field that specifies the decoding order. + MaxDONDiff int + + sequenceNumber uint16 +} + +// Init initializes the encoder. +func (e *Encoder) Init() error { + if e.MaxDONDiff != 0 { + return fmt.Errorf("MaxDONDiff != 0 is not supported (yet)") + } + + if e.SSRC == nil { + v, err := randUint32() + if err != nil { + return err + } + e.SSRC = &v + } + if e.InitialSequenceNumber == nil { + v, err := randUint32() + if err != nil { + return err + } + v2 := uint16(v) + e.InitialSequenceNumber = &v2 + } + if e.PayloadMaxSize == 0 { + e.PayloadMaxSize = defaultPayloadMaxSize + } + + e.sequenceNumber = *e.InitialSequenceNumber + return nil +} + +// Encode encodes an access unit into RTP/H265 packets. +func (e *Encoder) Encode(au [][]byte) ([]*rtp.Packet, error) { + var rets []*rtp.Packet + var batch [][]byte + + // split NALUs into batches + for _, nalu := range au { + if e.lenAggregationUnit(batch, nalu) <= e.PayloadMaxSize { + // add to existing batch + batch = append(batch, nalu) + } else { + // write batch + if batch != nil { + pkts, err := e.writeBatch(batch, false) + if err != nil { + return nil, err + } + rets = append(rets, pkts...) + } + + // initialize new batch + batch = [][]byte{nalu} + } + } + + // write final batch + // marker is used to indicate that the entire access unit has been sent + pkts, err := e.writeBatch(batch, true) + if err != nil { + return nil, err + } + rets = append(rets, pkts...) + + return rets, nil +} + +func (e *Encoder) writeBatch(nalus [][]byte, marker bool) ([]*rtp.Packet, error) { + if len(nalus) == 1 { + // the NALU fits into a single RTP packet + if len(nalus[0]) < e.PayloadMaxSize { + return e.writeSingle(nalus[0], marker) + } + + // split the NALU into multiple fragmentation packet + return e.writeFragmentationUnits(nalus[0], marker) + } + + return e.writeAggregationUnit(nalus, marker) +} + +func (e *Encoder) writeSingle(nalu []byte, marker bool) ([]*rtp.Packet, error) { + pkt := &rtp.Packet{ + Header: rtp.Header{ + Version: rtpVersion, + PayloadType: e.PayloadType, + SequenceNumber: e.sequenceNumber, + SSRC: *e.SSRC, + Marker: marker, + }, + Payload: nalu, + } + + e.sequenceNumber++ + + return []*rtp.Packet{pkt}, nil +} + +func (e *Encoder) writeFragmentationUnits(nalu []byte, marker bool) ([]*rtp.Packet, error) { + avail := e.PayloadMaxSize - 3 + le := len(nalu) - 2 + packetCount := packetCount(avail, le) + + ret := make([]*rtp.Packet, packetCount) + + head := nalu[:2] + nalu = nalu[2:] + le = avail + start := uint8(1) + end := uint8(0) + + for i := range ret { + if i == (packetCount - 1) { + le = len(nalu) + end = 1 + } + + data := make([]byte, 3+le) + data[0] = head[0]&0b10000001 | 49<<1 + data[1] = head[1] + data[2] = (start << 7) | (end << 6) | (head[0]>>1)&0b111111 + copy(data[3:], nalu) + nalu = nalu[le:] + + ret[i] = &rtp.Packet{ + Header: rtp.Header{ + Version: rtpVersion, + PayloadType: e.PayloadType, + SequenceNumber: e.sequenceNumber, + SSRC: *e.SSRC, + Marker: (i == (packetCount-1) && marker), + }, + Payload: data, + } + + e.sequenceNumber++ + start = 0 + } + + return ret, nil +} + +func (e *Encoder) lenAggregationUnit(nalus [][]byte, addNALU []byte) int { + ret := 2 // header + + for _, nalu := range nalus { + ret += 2 // size + ret += len(nalu) // nalu + } + + if addNALU != nil { + ret += 2 // size + ret += len(addNALU) // nalu + } + + return ret +} + +func (e *Encoder) writeAggregationUnit(nalus [][]byte, marker bool) ([]*rtp.Packet, error) { + payload := make([]byte, e.lenAggregationUnit(nalus, nil)) + + // header + h := uint16(48) << 9 + payload[0] = byte(h >> 8) + payload[1] = byte(h) + pos := 2 + + for _, nalu := range nalus { + // size + naluLen := len(nalu) + payload[pos] = uint8(naluLen >> 8) + payload[pos+1] = uint8(naluLen) + pos += 2 + + // nalu + copy(payload[pos:], nalu) + pos += naluLen + } + + pkt := &rtp.Packet{ + Header: rtp.Header{ + Version: rtpVersion, + PayloadType: e.PayloadType, + SequenceNumber: e.sequenceNumber, + SSRC: *e.SSRC, + Marker: marker, + }, + Payload: payload, + } + + e.sequenceNumber++ + + return []*rtp.Packet{pkt}, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtph265/rtph265.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtph265/rtph265.go new file mode 100644 index 000000000..7e1474cd4 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtph265/rtph265.go @@ -0,0 +1,2 @@ +// Package rtph265 contains a RTP/H265 decoder and encoder. +package rtph265 diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtplpcm/decoder.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtplpcm/decoder.go new file mode 100644 index 000000000..535a472fd --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtplpcm/decoder.go @@ -0,0 +1,33 @@ +package rtplpcm + +import ( + "fmt" + + "github.com/pion/rtp" +) + +// Decoder is a RTP/LPCM decoder. +// Specification: https://datatracker.ietf.org/doc/html/rfc3190 +type Decoder struct { + BitDepth int + ChannelCount int + + sampleSize int +} + +// Init initializes the decoder. +func (d *Decoder) Init() error { + d.sampleSize = d.BitDepth * d.ChannelCount / 8 + return nil +} + +// Decode decodes audio samples from a RTP packet. +// It returns audio samples and PTS of the first sample. +func (d *Decoder) Decode(pkt *rtp.Packet) ([]byte, error) { + plen := len(pkt.Payload) + if (plen % d.sampleSize) != 0 { + return nil, fmt.Errorf("received payload of wrong size") + } + + return pkt.Payload, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtplpcm/encoder.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtplpcm/encoder.go new file mode 100644 index 000000000..099fa57d9 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtplpcm/encoder.go @@ -0,0 +1,124 @@ +package rtplpcm + +import ( + "crypto/rand" + "fmt" + + "github.com/pion/rtp" +) + +const ( + rtpVersion = 2 + defaultPayloadMaxSize = 1460 // 1500 (UDP MTU) - 20 (IP header) - 8 (UDP header) - 12 (RTP header) +) + +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 +} + +// Encoder is a RTP/LPCM encoder. +// Specification: https://datatracker.ietf.org/doc/html/rfc3190 +type Encoder struct { + // payload type of packets. + PayloadType uint8 + + // bit depth. + BitDepth int + + // channel count. + ChannelCount int + + // SSRC of packets (optional). + // It defaults to a random value. + SSRC *uint32 + + // initial sequence number of packets (optional). + // It defaults to a random value. + InitialSequenceNumber *uint16 + + // maximum size of packet payloads (optional). + // It defaults to 1460. + PayloadMaxSize int + + sequenceNumber uint16 + sampleSize int + maxPayloadSize int +} + +// Init initializes the encoder. +func (e *Encoder) Init() error { + if e.SSRC == nil { + v, err := randUint32() + if err != nil { + return err + } + e.SSRC = &v + } + if e.InitialSequenceNumber == nil { + v, err := randUint32() + if err != nil { + return err + } + v2 := uint16(v) + e.InitialSequenceNumber = &v2 + } + if e.PayloadMaxSize == 0 { + e.PayloadMaxSize = defaultPayloadMaxSize + } + + e.sequenceNumber = *e.InitialSequenceNumber + e.sampleSize = e.BitDepth * e.ChannelCount / 8 + e.maxPayloadSize = (e.PayloadMaxSize / e.sampleSize) * e.sampleSize + return nil +} + +func (e *Encoder) packetCount(slen int) int { + n := (slen / e.maxPayloadSize) + if (slen % e.maxPayloadSize) != 0 { + n++ + } + return n +} + +// Encode encodes audio samples into RTP packets. +func (e *Encoder) Encode(samples []byte) ([]*rtp.Packet, error) { + slen := len(samples) + if (slen % e.sampleSize) != 0 { + return nil, fmt.Errorf("invalid samples") + } + + packetCount := e.packetCount(slen) + ret := make([]*rtp.Packet, packetCount) + pos := 0 + payloadSize := e.maxPayloadSize + timestamp := uint32(0) + + for i := range ret { + if payloadSize > len(samples[pos:]) { + payloadSize = len(samples[pos:]) + } + + ret[i] = &rtp.Packet{ + Header: rtp.Header{ + Version: rtpVersion, + PayloadType: e.PayloadType, + SequenceNumber: e.sequenceNumber, + Timestamp: timestamp, + SSRC: *e.SSRC, + Marker: false, + }, + Payload: samples[pos : pos+payloadSize], + } + + e.sequenceNumber++ + pos += payloadSize + timestamp += uint32(payloadSize / e.sampleSize) + } + + return ret, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtplpcm/rtplpcm.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtplpcm/rtplpcm.go new file mode 100644 index 000000000..c600ef3fd --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtplpcm/rtplpcm.go @@ -0,0 +1,2 @@ +// Package rtplpcm contains a RTP/LPCM decoder and encoder. +package rtplpcm diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmjpeg/decoder.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmjpeg/decoder.go new file mode 100644 index 000000000..366c9bece --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmjpeg/decoder.go @@ -0,0 +1,304 @@ +package rtpmjpeg + +import ( + "errors" + "fmt" + + "github.com/pion/rtp" + + "github.com/bluenviron/mediacommon/v2/pkg/codecs/jpeg" +) + +// ErrMorePacketsNeeded is returned when more packets are needed. +var ErrMorePacketsNeeded = errors.New("need more packets") + +// ErrNonStartingPacketAndNoPrevious is returned when we received a non-starting +// fragment of an image and we didn't received anything before. +// It's normal to receive this when decoding a stream that has been already +// running for some time. +var ErrNonStartingPacketAndNoPrevious = errors.New( + "received a non-starting fragment without any previous starting fragment") + +var lumDcCodeLens = []byte{ + 0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, +} + +var lumDcSymbols = []byte{ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, +} + +var lumAcCodelens = []byte{ + 0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 0x7d, +} + +var lumAcSymbols = []byte{ //nolint:dupl + 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12, + 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07, + 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08, + 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0, + 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16, + 0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28, + 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, + 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, + 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59, + 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, + 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, + 0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89, + 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98, + 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7, + 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6, + 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5, + 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4, + 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2, + 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea, + 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, + 0xf9, 0xfa, +} + +var chmDcCodelens = []byte{ + 0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, +} + +var chmDcSymbols = []byte{ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, +} + +var chmAcCodelens = []byte{ + 0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, 1, 2, 0x77, +} + +var chmAcSymbols = []byte{ //nolint:dupl + 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21, + 0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71, + 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91, + 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0, + 0x15, 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34, + 0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26, + 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38, + 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, + 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, + 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, + 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, + 0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, + 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, + 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, + 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, + 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, + 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, + 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, + 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, + 0xea, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8, + 0xf9, 0xfa, +} + +var lumaQuantizers = []int{ + 0x10, 0x0b, 0x0c, 0x0e, 0x0c, 0x0a, 0x10, 0x0e, + 0x0d, 0x0e, 0x12, 0x11, 0x10, 0x13, 0x18, 0x28, + 0x1a, 0x18, 0x16, 0x16, 0x18, 0x31, 0x23, 0x25, + 0x1d, 0x28, 0x3a, 0x33, 0x3d, 0x3c, 0x39, 0x33, + 0x38, 0x37, 0x40, 0x48, 0x5c, 0x4e, 0x40, 0x44, + 0x57, 0x45, 0x37, 0x38, 0x50, 0x6d, 0x51, 0x57, + 0x5f, 0x62, 0x67, 0x68, 0x67, 0x3e, 0x4d, 0x71, + 0x79, 0x70, 0x64, 0x78, 0x5c, 0x65, 0x67, 0x63, +} + +var chromaQuantizers = []int{ + 0x11, 0x12, 0x12, 0x18, 0x15, 0x18, 0x2f, 0x1a, + 0x1a, 0x2f, 0x63, 0x42, 0x38, 0x42, 0x63, 0x63, + 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, + 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, + 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, + 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, + 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, + 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x63, +} + +func makeQuantizationTables(q uint8) [][]byte { + var scale int + if q < 50 { + scale = 5000 / int(q) + } else { + scale = 200 - 2*int(q) + } + + tables := make([][]byte, 2) + + tables[0] = make([]byte, 64) + + for i := 0; i < 64; i++ { + v := (lumaQuantizers[i]*scale + 50) / 100 + if v > 255 { + v = 255 + } else if v == 0 { + v = 1 + } + tables[0][i] = byte(v) + } + + tables[1] = make([]byte, 64) + + for i := 0; i < 64; i++ { + v := (chromaQuantizers[i]*scale + 50) / 100 + if v > 255 { + v = 255 + } else if v == 0 { + v = 1 + } + tables[1][i] = byte(v) + } + + return tables +} + +func joinFragments(fragments [][]byte, size int) []byte { + ret := make([]byte, size) + n := 0 + for _, p := range fragments { + n += copy(ret[n:], p) + } + return ret +} + +// Decoder is a RTP/M-JPEG decoder. +// Specification: https://datatracker.ietf.org/doc/html/rfc2435 +type Decoder struct { + firstPacketReceived bool + fragments [][]byte + fragmentsSize int + firstJpegHeader *headerJPEG + quantizationTables [][]byte +} + +// Init initializes the decoder. +func (d *Decoder) Init() error { + return nil +} + +func (d *Decoder) resetFragments() { + d.fragments = d.fragments[:0] + d.fragmentsSize = 0 +} + +// Decode decodes an image from a RTP packet. +func (d *Decoder) Decode(pkt *rtp.Packet) ([]byte, error) { + byts := pkt.Payload + + var jh headerJPEG + n, err := jh.unmarshal(byts) + if err != nil { + return nil, err + } + byts = byts[n:] + + if jh.Width > maxDimension { + return nil, fmt.Errorf("width of %d is not supported", jh.Width) + } + + if jh.Height > maxDimension { + return nil, fmt.Errorf("height of %d is not supported", jh.Height) + } + + if jh.FragmentOffset == 0 { + d.resetFragments() + d.firstPacketReceived = true + + if jh.Quantization >= 128 { + var hqt headerQuantizationTable + n, err := hqt.unmarshal(byts) + if err != nil { + return nil, err + } + d.quantizationTables = hqt.Tables + byts = byts[n:] + } else { + d.quantizationTables = makeQuantizationTables(jh.Quantization) + } + + d.fragments = append(d.fragments, byts) + d.fragmentsSize = len(byts) + d.firstJpegHeader = &jh + } else { + if int(jh.FragmentOffset) != d.fragmentsSize { + if !d.firstPacketReceived { + return nil, ErrNonStartingPacketAndNoPrevious + } + + d.resetFragments() + return nil, fmt.Errorf("received wrong fragment") + } + + d.fragmentsSize += len(byts) + d.fragments = append(d.fragments, byts) + } + + if !pkt.Marker { + return nil, ErrMorePacketsNeeded + } + + if d.fragmentsSize < 2 { + return nil, fmt.Errorf("invalid data") + } + + data := joinFragments(d.fragments, d.fragmentsSize) + d.resetFragments() + + var buf []byte + + buf = jpeg.StartOfImage{}.Marshal(buf) + + var dqt jpeg.DefineQuantizationTable + id := uint8(0) + for _, table := range d.quantizationTables { + dqt.Tables = append(dqt.Tables, jpeg.QuantizationTable{ + ID: id, + Data: table, + }) + id++ + } + buf = dqt.Marshal(buf) + + buf = jpeg.StartOfFrame1{ + Type: d.firstJpegHeader.Type, + Width: d.firstJpegHeader.Width, + Height: d.firstJpegHeader.Height, + QuantizationTableCount: id, + }.Marshal(buf) + + buf = jpeg.DefineHuffmanTable{ + Codes: lumDcCodeLens, + Symbols: lumDcSymbols, + TableNumber: 0, + TableClass: 0, + }.Marshal(buf) + + buf = jpeg.DefineHuffmanTable{ + Codes: lumAcCodelens, + Symbols: lumAcSymbols, + TableNumber: 0, + TableClass: 1, + }.Marshal(buf) + + buf = jpeg.DefineHuffmanTable{ + Codes: chmDcCodelens, + Symbols: chmDcSymbols, + TableNumber: 1, + TableClass: 0, + }.Marshal(buf) + + buf = jpeg.DefineHuffmanTable{ + Codes: chmAcCodelens, + Symbols: chmAcSymbols, + TableNumber: 1, + TableClass: 1, + }.Marshal(buf) + + buf = jpeg.StartOfScan{}.Marshal(buf) + + buf = append(buf, data...) + + if data[len(data)-2] != 0xFF || data[len(data)-1] != jpeg.MarkerEndOfImage { + buf = append(buf, []byte{0xFF, jpeg.MarkerEndOfImage}...) + } + + return buf, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmjpeg/encoder.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmjpeg/encoder.go new file mode 100644 index 000000000..b3bbdf60f --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmjpeg/encoder.go @@ -0,0 +1,279 @@ +package rtpmjpeg + +import ( + "crypto/rand" + "fmt" + "sort" + + "github.com/pion/rtp" + + "github.com/bluenviron/mediacommon/v2/pkg/codecs/jpeg" +) + +const ( + rtpVersion = 2 + defaultPayloadMaxSize = 1460 // 1500 (UDP MTU) - 20 (IP header) - 8 (UDP header) - 12 (RTP header) +) + +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 +} + +// Encoder is a RTP/M-JPEG encoder. +// Specification: https://datatracker.ietf.org/doc/html/rfc2435 +type Encoder struct { + // SSRC of packets (optional). + // It defaults to a random value. + SSRC *uint32 + + // initial sequence number of packets (optional). + // It defaults to a random value. + InitialSequenceNumber *uint16 + + // maximum size of packet payloads (optional). + // It defaults to 1460. + PayloadMaxSize int + + sequenceNumber uint16 +} + +// Init initializes the encoder. +func (e *Encoder) Init() error { + if e.SSRC == nil { + v, err := randUint32() + if err != nil { + return err + } + e.SSRC = &v + } + if e.InitialSequenceNumber == nil { + v, err := randUint32() + if err != nil { + return err + } + v2 := uint16(v) + e.InitialSequenceNumber = &v2 + } + if e.PayloadMaxSize == 0 { + e.PayloadMaxSize = defaultPayloadMaxSize + } + + e.sequenceNumber = *e.InitialSequenceNumber + return nil +} + +// Encode encodes an image into RTP/M-JPEG packets. +func (e *Encoder) Encode(image []byte) ([]*rtp.Packet, error) { + l := len(image) + if l < 2 || image[0] != 0xFF || image[1] != jpeg.MarkerStartOfImage { + return nil, fmt.Errorf("SOI not found") + } + + image = image[2:] + var sof *jpeg.StartOfFrame1 + var dri *jpeg.DefineRestartInterval + quantizationTables := make(map[uint8][]byte) + var data []byte + +outer: + for { + if len(image) < 2 { + break + } + + h0, h1 := image[0], image[1] + image = image[2:] + + if h0 != 0xFF { + return nil, fmt.Errorf("invalid image") + } + + switch h1 { + case 0xE0, 0xE1, 0xE2, // JFIF + jpeg.MarkerDefineHuffmanTable, + jpeg.MarkerComment: + mlen := int(image[0])<<8 | int(image[1]) + if len(image) < mlen { + return nil, fmt.Errorf("image is too short") + } + image = image[mlen:] + + case jpeg.MarkerDefineQuantizationTable: + mlen := int(image[0])<<8 | int(image[1]) + if len(image) < mlen { + return nil, fmt.Errorf("image is too short") + } + + var dqt jpeg.DefineQuantizationTable + err := dqt.Unmarshal(image[2:mlen]) + if err != nil { + return nil, err + } + image = image[mlen:] + + for _, t := range dqt.Tables { + quantizationTables[t.ID] = t.Data + } + + case jpeg.MarkerDefineRestartInterval: + mlen := int(image[0])<<8 | int(image[1]) + if len(image) < mlen { + return nil, fmt.Errorf("image is too short") + } + + dri = &jpeg.DefineRestartInterval{} + err := dri.Unmarshal(image[2:mlen]) + if err != nil { + return nil, err + } + image = image[mlen:] + + case jpeg.MarkerStartOfFrame1: + mlen := int(image[0])<<8 | int(image[1]) + if len(image) < mlen { + return nil, fmt.Errorf("image is too short") + } + + sof = &jpeg.StartOfFrame1{} + err := sof.Unmarshal(image[2:mlen]) + if err != nil { + return nil, err + } + image = image[mlen:] + + if sof.Width > maxDimension { + return nil, fmt.Errorf("an image with width of %d can't be sent with RTSP", sof.Width) + } + + if sof.Height > maxDimension { + return nil, fmt.Errorf("an image with height of %d can't be sent with RTSP", sof.Height) + } + + if (sof.Width % 8) != 0 { + return nil, fmt.Errorf("width must be multiple of 8") + } + + if (sof.Height % 8) != 0 { + return nil, fmt.Errorf("height must be multiple of 8") + } + + case jpeg.MarkerStartOfScan: + mlen := int(image[0])<<8 | int(image[1]) + if len(image) < mlen { + return nil, fmt.Errorf("image is too short") + } + + var sos jpeg.StartOfScan + err := sos.Unmarshal(image[2:mlen]) + if err != nil { + return nil, err + } + image = image[mlen:] + + data = image + break outer + + default: + return nil, fmt.Errorf("unknown marker: 0x%.2x", h1) + } + } + + if sof == nil { + return nil, fmt.Errorf("SOF not found") + } + + if sof.Type > 63 { + return nil, fmt.Errorf("JPEG type %d is not supported", sof.Type) + } + + if len(data) == 0 { + return nil, fmt.Errorf("image data not found") + } + + jh := headerJPEG{ + TypeSpecific: 0, + Type: sof.Type, + Quantization: 255, + Width: sof.Width, + Height: sof.Height, + } + + if dri != nil { + jh.Type += 64 + } + + first := true + offset := 0 + var ret []*rtp.Packet + + for { + var buf []byte + + jh.FragmentOffset = uint32(offset) + buf = jh.marshal(buf) + + if dri != nil { + buf = headerRestartMarker{ + Interval: dri.Interval, + Count: 0xFFFF, + }.marshal(buf) + } + + if first { + first = false + + qth := headerQuantizationTable{} + + // gather and sort tables IDs + ids := make([]uint8, len(quantizationTables)) + i := 0 + for id := range quantizationTables { + ids[i] = id + i++ + } + sort.Slice(ids, func(i, j int) bool { + return ids[i] < ids[j] + }) + + // add tables sorted by ID + for _, id := range ids { + qth.Tables = append(qth.Tables, quantizationTables[id]) + } + + buf = qth.marshal(buf) + } + + remaining := e.PayloadMaxSize - len(buf) + ldata := len(data) + if remaining > ldata { + remaining = ldata + } + + buf = append(buf, data[:remaining]...) + data = data[remaining:] + offset += remaining + + ret = append(ret, &rtp.Packet{ + Header: rtp.Header{ + Version: rtpVersion, + PayloadType: 26, + SequenceNumber: e.sequenceNumber, + SSRC: *e.SSRC, + Marker: len(data) == 0, + }, + Payload: buf, + }) + e.sequenceNumber++ + + if len(data) == 0 { + break + } + } + + return ret, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmjpeg/header_jpeg.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmjpeg/header_jpeg.go new file mode 100644 index 000000000..377111445 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmjpeg/header_jpeg.go @@ -0,0 +1,49 @@ +package rtpmjpeg + +import ( + "fmt" +) + +type headerJPEG struct { + TypeSpecific uint8 + FragmentOffset uint32 + Type uint8 + Quantization uint8 + Width int + Height int +} + +func (h *headerJPEG) unmarshal(byts []byte) (int, error) { + if len(byts) < 8 { + return 0, fmt.Errorf("buffer is too short") + } + + h.TypeSpecific = byts[0] + h.FragmentOffset = uint32(byts[1])<<16 | uint32(byts[2])<<8 | uint32(byts[3]) + + h.Type = byts[4] + if h.Type > 63 { + return 0, fmt.Errorf("type %d is not supported", h.Type) + } + + h.Quantization = byts[5] + if h.Quantization == 0 || + (h.Quantization > 99 && h.Quantization < 127) { + return 0, fmt.Errorf("quantization %d is invalid", h.Quantization) + } + + h.Width = int(byts[6]) * 8 + h.Height = int(byts[7]) * 8 + + return 8, nil +} + +func (h headerJPEG) marshal(byts []byte) []byte { + byts = append(byts, h.TypeSpecific) + byts = append(byts, []byte{byte(h.FragmentOffset >> 16), byte(h.FragmentOffset >> 8), byte(h.FragmentOffset)}...) + byts = append(byts, h.Type) + byts = append(byts, h.Quantization) + byts = append(byts, byte(h.Width/8)) + byts = append(byts, byte(h.Height/8)) + return byts +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmjpeg/header_quantization_table.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmjpeg/header_quantization_table.go new file mode 100644 index 000000000..851798292 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmjpeg/header_quantization_table.go @@ -0,0 +1,59 @@ +package rtpmjpeg + +import ( + "fmt" +) + +type headerQuantizationTable struct { + MBZ uint8 + Precision uint8 + Tables [][]byte +} + +func (h *headerQuantizationTable) unmarshal(byts []byte) (int, error) { + if len(byts) < 4 { + return 0, fmt.Errorf("buffer is too short") + } + + h.MBZ = byts[0] + h.Precision = byts[1] + if h.Precision != 0 { + return 0, fmt.Errorf("precision %d is not supported", h.Precision) + } + + length := int(byts[2])<<8 | int(byts[3]) + switch length { + case 64, 128: + default: + return 0, fmt.Errorf("table length %d is not supported", length) + } + + if (len(byts) - 4) < length { + return 0, fmt.Errorf("buffer is too short") + } + + tableCount := length / 64 + h.Tables = make([][]byte, tableCount) + n := 0 + + for i := 0; i < tableCount; i++ { + h.Tables[i] = byts[4+n : 4+64+n] + n += 64 + } + + return 4 + length, nil +} + +func (h headerQuantizationTable) marshal(byts []byte) []byte { + byts = append(byts, h.MBZ) + byts = append(byts, h.Precision) + + l := len(h.Tables) * 64 + byts = append(byts, []byte{byte(l >> 8), byte(l)}...) + + for i := 0; i < len(h.Tables); i++ { + byts = append(byts, h.Tables[i]...) + } + + return byts +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmjpeg/header_restart_marker.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmjpeg/header_restart_marker.go new file mode 100644 index 000000000..8dbbdeeb9 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmjpeg/header_restart_marker.go @@ -0,0 +1,26 @@ +package rtpmjpeg + +import ( + "fmt" +) + +type headerRestartMarker struct { + Interval uint16 + Count uint16 +} + +func (h *headerRestartMarker) unmarshal(byts []byte) (int, error) { + if len(byts) < 4 { + return 0, fmt.Errorf("buffer is too short") + } + + h.Interval = uint16(byts[0])<<8 | uint16(byts[1]) + h.Count = uint16(byts[2])<<8 | uint16(byts[3]) + return 4, nil +} + +func (h headerRestartMarker) marshal(byts []byte) []byte { + byts = append(byts, []byte{byte(h.Interval >> 8), byte(h.Interval)}...) + byts = append(byts, []byte{byte(h.Count >> 8), byte(h.Count)}...) + return byts +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmjpeg/rtpmjpeg.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmjpeg/rtpmjpeg.go new file mode 100644 index 000000000..43a6bdf04 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmjpeg/rtpmjpeg.go @@ -0,0 +1,6 @@ +// Package rtpmjpeg contains a RTP/M-JPEG decoder and encoder. +package rtpmjpeg + +const ( + maxDimension = 2040 +) diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg1audio/decoder.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg1audio/decoder.go new file mode 100644 index 000000000..41c9202f0 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg1audio/decoder.go @@ -0,0 +1,127 @@ +package rtpmpeg1audio + +import ( + "errors" + "fmt" + + "github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg1audio" + "github.com/pion/rtp" +) + +// ErrMorePacketsNeeded is returned when more packets are needed. +var ErrMorePacketsNeeded = errors.New("need more packets") + +// ErrNonStartingPacketAndNoPrevious is returned when we received a non-starting +// packet of a fragmented frame and we didn't received anything before. +// It's normal to receive this when decoding a stream that has been already +// running for some time. +var ErrNonStartingPacketAndNoPrevious = errors.New( + "received a non-starting fragment without any previous starting fragment") + +func joinFragments(fragments [][]byte, size int) []byte { + ret := make([]byte, size) + n := 0 + for _, p := range fragments { + n += copy(ret[n:], p) + } + return ret +} + +// Decoder is a RTP/MPEG-1/2 Audio decoder. +// Specification: https://datatracker.ietf.org/doc/html/rfc2250 +type Decoder struct { + firstPacketReceived bool + fragments [][]byte + fragmentsSize int + fragmentsExpected int +} + +// Init initializes the decoder. +func (d *Decoder) Init() error { + return nil +} + +func (d *Decoder) resetFragments() { + d.fragments = d.fragments[:0] + d.fragmentsSize = 0 +} + +// Decode decodes frames from a RTP packet. +func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, error) { + if len(pkt.Payload) < 5 { + d.resetFragments() + return nil, fmt.Errorf("payload is too short") + } + + mbz := uint16(pkt.Payload[0])<<8 | uint16(pkt.Payload[1]) + if mbz != 0 { + d.resetFragments() + return nil, fmt.Errorf("invalid MBZ: %v", mbz) + } + + offset := uint16(pkt.Payload[2])<<8 | uint16(pkt.Payload[3]) + + var frames [][]byte + + if offset == 0 { + d.resetFragments() + d.firstPacketReceived = true + + buf := pkt.Payload[4:] + for { + var h mpeg1audio.FrameHeader + err := h.Unmarshal(buf) + if err != nil { + return nil, err + } + + fl := h.FrameLen() + bl := len(buf) + if bl >= fl { + frames = append(frames, buf[:fl]) + buf = buf[fl:] + if len(buf) == 0 { + break + } + } else { + if len(frames) != 0 { + return nil, fmt.Errorf("invalid packet") + } + + d.fragments = append(d.fragments, buf) + d.fragmentsSize = bl + d.fragmentsExpected = fl - bl + return nil, ErrMorePacketsNeeded + } + } + } else { + if int(offset) != d.fragmentsSize { + if !d.firstPacketReceived { + return nil, ErrNonStartingPacketAndNoPrevious + } + + d.resetFragments() + return nil, fmt.Errorf("unexpected offset %v, expected %v", offset, d.fragmentsSize) + } + + bl := len(pkt.Payload[4:]) + d.fragmentsSize += bl + d.fragmentsExpected -= bl + + if d.fragmentsExpected < 0 { + d.resetFragments() + return nil, fmt.Errorf("fragment is too big") + } + + d.fragments = append(d.fragments, pkt.Payload[4:]) + + if d.fragmentsExpected > 0 { + return nil, ErrMorePacketsNeeded + } + + frames = [][]byte{joinFragments(d.fragments, d.fragmentsSize)} + d.resetFragments() + } + + return frames, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg1audio/encoder.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg1audio/encoder.go new file mode 100644 index 000000000..4aa12172d --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg1audio/encoder.go @@ -0,0 +1,196 @@ +package rtpmpeg1audio + +import ( + "crypto/rand" + + "github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg1audio" + "github.com/pion/rtp" +) + +const ( + rtpVersion = 2 + defaultPayloadMaxSize = 1460 // 1500 (UDP MTU) - 20 (IP header) - 8 (UDP header) - 12 (RTP header) +) + +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 lenAggregated(frames [][]byte, frame []byte) int { + n := 4 + len(frame) + for _, fr := range frames { + n += len(fr) + } + return n +} + +func packetCount(avail, le int) int { + n := le / avail + if (le % avail) != 0 { + n++ + } + return n +} + +// Encoder is a RTP/MPEG-1/2 Audio encoder. +// Specification: https://datatracker.ietf.org/doc/html/rfc2250 +type Encoder struct { + // SSRC of packets (optional). + // It defaults to a random value. + SSRC *uint32 + + // initial sequence number of packets (optional). + // It defaults to a random value. + InitialSequenceNumber *uint16 + + // maximum size of packet payloads (optional). + // It defaults to 1460. + PayloadMaxSize int + + sequenceNumber uint16 +} + +// Init initializes the encoder. +func (e *Encoder) Init() error { + if e.SSRC == nil { + v, err := randUint32() + if err != nil { + return err + } + e.SSRC = &v + } + if e.InitialSequenceNumber == nil { + v, err := randUint32() + if err != nil { + return err + } + v2 := uint16(v) + e.InitialSequenceNumber = &v2 + } + if e.PayloadMaxSize == 0 { + e.PayloadMaxSize = defaultPayloadMaxSize + } + + e.sequenceNumber = *e.InitialSequenceNumber + return nil +} + +// Encode encodes frames into RTP packets. +func (e *Encoder) Encode(frames [][]byte) ([]*rtp.Packet, error) { + var rets []*rtp.Packet + var batch [][]byte + timestamp := uint32(0) + + for _, frame := range frames { + if lenAggregated(batch, frame) <= e.PayloadMaxSize { + batch = append(batch, frame) + } else { + // write current batch + if batch != nil { + pkts, err := e.writeBatch(batch, timestamp) + if err != nil { + return nil, err + } + rets = append(rets, pkts...) + + for _, frame := range batch { + var h mpeg1audio.FrameHeader + err := h.Unmarshal(frame) + if err != nil { + return nil, err + } + + timestamp += uint32(h.SampleCount()) + } + } + + // initialize new batch + batch = [][]byte{frame} + } + } + + // write last batch + pkts, err := e.writeBatch(batch, timestamp) + if err != nil { + return nil, err + } + rets = append(rets, pkts...) + + return rets, nil +} + +func (e *Encoder) writeBatch(frames [][]byte, timestamp uint32) ([]*rtp.Packet, error) { + if len(frames) != 1 || lenAggregated(frames, nil) < e.PayloadMaxSize { + return e.writeAggregated(frames, timestamp) + } + + return e.writeFragmented(frames[0], timestamp) +} + +func (e *Encoder) writeFragmented(frame []byte, timestamp uint32) ([]*rtp.Packet, error) { + avail := e.PayloadMaxSize - 4 + le := len(frame) + packetCount := packetCount(avail, le) + + ret := make([]*rtp.Packet, packetCount) + pos := 0 + le = avail + + for i := range ret { + if i == (packetCount - 1) { + le = len(frame) - pos + } + + payload := make([]byte, 4+le) + payload[2] = byte(pos >> 8) + payload[3] = byte(pos) + + pos += copy(payload[4:], frame[pos:]) + + ret[i] = &rtp.Packet{ + Header: rtp.Header{ + Version: rtpVersion, + PayloadType: 14, + SequenceNumber: e.sequenceNumber, + Timestamp: timestamp, + SSRC: *e.SSRC, + Marker: true, + }, + Payload: payload, + } + + e.sequenceNumber++ + } + + return ret, nil +} + +func (e *Encoder) writeAggregated(frames [][]byte, timestamp uint32) ([]*rtp.Packet, error) { + payload := make([]byte, lenAggregated(frames, nil)) + + n := 4 + for _, frame := range frames { + n += copy(payload[n:], frame) + } + + pkt := &rtp.Packet{ + Header: rtp.Header{ + Version: rtpVersion, + PayloadType: 14, + SequenceNumber: e.sequenceNumber, + Timestamp: timestamp, + SSRC: *e.SSRC, + Marker: true, + }, + Payload: payload, + } + + e.sequenceNumber++ + + return []*rtp.Packet{pkt}, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg1audio/rtpmpeg1audio.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg1audio/rtpmpeg1audio.go new file mode 100644 index 000000000..dbc87fc31 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg1audio/rtpmpeg1audio.go @@ -0,0 +1,2 @@ +// Package rtpmpeg1audio contains a RTP/MPEG-1/2 Audio decoder and encoder. +package rtpmpeg1audio diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg1video/decoder.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg1video/decoder.go new file mode 100644 index 000000000..9309df34f --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg1video/decoder.go @@ -0,0 +1,163 @@ +package rtpmpeg1video + +import ( + "errors" + "fmt" + + "github.com/pion/rtp" +) + +const ( + maxFrameSize = 1 * 1024 * 1024 +) + +// ErrMorePacketsNeeded is returned when more packets are needed. +var ErrMorePacketsNeeded = errors.New("need more packets") + +// ErrNonStartingPacketAndNoPrevious is returned when we received a non-starting +// packet of a fragmented frame and we didn't received anything before. +// It's normal to receive this when decoding a stream that has been already +// running for some time. +var ErrNonStartingPacketAndNoPrevious = errors.New( + "received a non-starting fragment without any previous starting fragment") + +func joinFragments(fragments [][]byte, size int) []byte { + ret := make([]byte, size) + n := 0 + for _, p := range fragments { + n += copy(ret[n:], p) + } + return ret +} + +// Decoder is a RTP/MPEG-1/2 Video decoder. +// Specification: https://datatracker.ietf.org/doc/html/rfc2250 +type Decoder struct { + fragments [][]byte + fragmentsSize int + fragmentNextSeqNum uint16 + + sliceBuffer [][]byte + sliceBufferSize int +} + +// Init initializes the decoder. +func (d *Decoder) Init() error { + return nil +} + +func (d *Decoder) resetFragments() { + d.fragments = d.fragments[:0] + d.fragmentsSize = 0 +} + +func (d *Decoder) decodeSlice(pkt *rtp.Packet) ([]byte, error) { + if len(pkt.Payload) < 4 { + d.resetFragments() + return nil, fmt.Errorf("payload is too short") + } + + mbz := pkt.Payload[0] >> 3 + if mbz != 0 { + d.resetFragments() + return nil, fmt.Errorf("invalid MBZ: %v", mbz) + } + + t := (pkt.Payload[0] >> 2) & 0x01 + if t != 0 { + d.resetFragments() + return nil, fmt.Errorf("MPEG-2 video-specific header extension is not supported yet") + } + + an := pkt.Payload[2] >> 7 + if an != 0 { + d.resetFragments() + return nil, fmt.Errorf("AN not supported yet") + } + + n := (pkt.Payload[2] >> 6) & 0x01 + if n != 0 { + d.resetFragments() + return nil, fmt.Errorf("N not supported yet") + } + + b := (pkt.Payload[2] >> 4) & 0x01 + e := (pkt.Payload[2] >> 3) & 0x01 + + switch { + case b == 1 && e == 1: + return pkt.Payload[4:], nil + + case b == 1: + d.fragments = d.fragments[:0] + d.fragments = append(d.fragments, pkt.Payload[4:]) + d.fragmentsSize = len(pkt.Payload[4:]) + d.fragmentNextSeqNum = pkt.SequenceNumber + 1 + return nil, ErrMorePacketsNeeded + + case e == 1: + if d.fragmentsSize == 0 { + return nil, ErrNonStartingPacketAndNoPrevious + } + + if pkt.SequenceNumber != d.fragmentNextSeqNum { + d.resetFragments() + return nil, fmt.Errorf("discarding frame since a RTP packet is missing") + } + + d.fragments = append(d.fragments, pkt.Payload[4:]) + d.fragmentsSize += len(pkt.Payload[4:]) + + slice := joinFragments(d.fragments, d.fragmentsSize) + d.resetFragments() + return slice, nil + + default: + if d.fragmentsSize == 0 { + return nil, ErrNonStartingPacketAndNoPrevious + } + + if pkt.SequenceNumber != d.fragmentNextSeqNum { + d.resetFragments() + return nil, fmt.Errorf("discarding frame since a RTP packet is missing") + } + + d.fragments = append(d.fragments, pkt.Payload[4:]) + d.fragmentsSize += len(pkt.Payload[4:]) + d.fragmentNextSeqNum++ + return nil, ErrMorePacketsNeeded + } +} + +// Decode decodes frames from a RTP packet. +func (d *Decoder) Decode(pkt *rtp.Packet) ([]byte, error) { + slice, err := d.decodeSlice(pkt) + if err != nil { + return nil, err + } + + addSize := len(slice) + + if (d.sliceBufferSize + addSize) > maxFrameSize { + errSize := d.sliceBufferSize + addSize + d.sliceBuffer = nil + d.sliceBufferSize = 0 + return nil, fmt.Errorf("frame size (%d) is too big, maximum is %d", + errSize, maxFrameSize) + } + + d.sliceBuffer = append(d.sliceBuffer, slice) + d.sliceBufferSize += addSize + + if !pkt.Marker { + return nil, ErrMorePacketsNeeded + } + + ret := joinFragments(d.sliceBuffer, d.sliceBufferSize) + + // do not reuse sliceBuffer to avoid race conditions + d.sliceBuffer = nil + d.sliceBufferSize = 0 + + return ret, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg1video/encoder.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg1video/encoder.go new file mode 100644 index 000000000..d0e838be9 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg1video/encoder.go @@ -0,0 +1,239 @@ +package rtpmpeg1video + +import ( + "bytes" + "crypto/rand" + + "github.com/pion/rtp" +) + +const ( + rtpVersion = 2 + defaultPayloadMaxSize = 1460 // 1500 (UDP MTU) - 20 (IP header) - 8 (UDP header) - 12 (RTP header) +) + +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 lenAggregated(slices [][]byte, slice []byte) int { + n := 4 + len(slice) + for _, fr := range slices { + n += len(fr) + } + return n +} + +func packetCount(avail, le int) int { + n := le / avail + if (le % avail) != 0 { + n++ + } + return n +} + +// Encoder is a RTP/MPEG-1/2 Video encoder. +// Specification: https://datatracker.ietf.org/doc/html/rfc2250 +type Encoder struct { + // SSRC of packets (optional). + // It defaults to a random value. + SSRC *uint32 + + // initial sequence number of packets (optional). + // It defaults to a random value. + InitialSequenceNumber *uint16 + + // maximum size of packet payloads (optional). + // It defaults to 1460. + PayloadMaxSize int + + sequenceNumber uint16 +} + +// Init initializes the encoder. +func (e *Encoder) Init() error { + if e.SSRC == nil { + v, err := randUint32() + if err != nil { + return err + } + e.SSRC = &v + } + if e.InitialSequenceNumber == nil { + v, err := randUint32() + if err != nil { + return err + } + v2 := uint16(v) + e.InitialSequenceNumber = &v2 + } + if e.PayloadMaxSize == 0 { + e.PayloadMaxSize = defaultPayloadMaxSize + } + + e.sequenceNumber = *e.InitialSequenceNumber + return nil +} + +// Encode encodes frames into RTP packets. +func (e *Encoder) Encode(frame []byte) ([]*rtp.Packet, error) { + var rets []*rtp.Packet + var batch [][]byte + + var temporalReference uint16 + beginOfSequence := uint8(0) + var frameType uint8 + + for { + var slice []byte + end := bytes.Index(frame[4:], []byte{0, 0, 1}) + if end >= 0 { + slice, frame = frame[:end+4], frame[end+4:] + } else { + slice, frame = frame, nil + } + + if lenAggregated(batch, slice) <= e.PayloadMaxSize { + batch = append(batch, slice) + } else { + // write current batch + if batch != nil { + pkts, err := e.writeBatch(batch, + temporalReference, + beginOfSequence, + frameType) + if err != nil { + return nil, err + } + rets = append(rets, pkts...) + beginOfSequence = 0 + } + + // initialize new batch + batch = [][]byte{slice} + } + + switch slice[3] { + case 0: + temporalReference = uint16(slice[4])<<2 | uint16(slice[5])>>6 + frameType = (slice[5] >> 3) & 0b111 + + case 0xB8: + beginOfSequence = 1 + } + + if frame == nil { + break + } + } + + // write last batch + pkts, err := e.writeBatch(batch, + temporalReference, + beginOfSequence, + frameType) + if err != nil { + return nil, err + } + rets = append(rets, pkts...) + + rets[len(rets)-1].Marker = true + + return rets, nil +} + +func (e *Encoder) writeBatch( + slices [][]byte, + temporalReference uint16, + beginOfSequence uint8, + frameType uint8, +) ([]*rtp.Packet, error) { + if len(slices) != 1 || lenAggregated(slices, nil) < e.PayloadMaxSize { + return e.writeAggregated(slices, temporalReference, beginOfSequence, frameType) + } + + return e.writeFragmented(slices[0], temporalReference, beginOfSequence, frameType) +} + +func (e *Encoder) writeFragmented( + slice []byte, + temporalReference uint16, + beginOfSequence uint8, + frameType uint8, +) ([]*rtp.Packet, error) { + avail := e.PayloadMaxSize - 4 + le := len(slice) + packetCount := packetCount(avail, le) + + ret := make([]*rtp.Packet, packetCount) + le = avail + start := uint8(1) + end := uint8(0) + + for i := range ret { + if i == (packetCount - 1) { + le = len(slice) + end = 1 + } + + payload := make([]byte, 4+le) + payload[0] = byte(temporalReference >> 8) + payload[1] = byte(temporalReference) + payload[2] = beginOfSequence<<5 | start<<4 | end<<3 | frameType + copy(payload[4:], slice) + slice = slice[le:] + + ret[i] = &rtp.Packet{ + Header: rtp.Header{ + Version: rtpVersion, + PayloadType: 32, + SequenceNumber: e.sequenceNumber, + SSRC: *e.SSRC, + }, + Payload: payload, + } + + e.sequenceNumber++ + start = 0 + beginOfSequence = 0 + } + + return ret, nil +} + +func (e *Encoder) writeAggregated( + slices [][]byte, + temporalReference uint16, + beginOfSequence uint8, + frameType uint8, +) ([]*rtp.Packet, error) { + payload := make([]byte, lenAggregated(slices, nil)) + + payload[0] = byte(temporalReference >> 8) + payload[1] = byte(temporalReference) + payload[2] = beginOfSequence<<5 | 1<<4 | 1<<3 | frameType + + n := 4 + for _, slice := range slices { + n += copy(payload[n:], slice) + } + + pkt := &rtp.Packet{ + Header: rtp.Header{ + Version: rtpVersion, + PayloadType: 32, + SequenceNumber: e.sequenceNumber, + SSRC: *e.SSRC, + }, + Payload: payload, + } + + e.sequenceNumber++ + + return []*rtp.Packet{pkt}, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg1video/rtpmpeg1video.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg1video/rtpmpeg1video.go new file mode 100644 index 000000000..be95588f3 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg1video/rtpmpeg1video.go @@ -0,0 +1,2 @@ +// Package rtpmpeg1video contains a RTP/MPEG-1/2 Video decoder and encoder. +package rtpmpeg1video diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4audio/decoder.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4audio/decoder.go new file mode 100644 index 000000000..4d980e8e9 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4audio/decoder.go @@ -0,0 +1,62 @@ +package rtpmpeg4audio + +import ( + "errors" + + "github.com/pion/rtp" +) + +// ErrMorePacketsNeeded is returned when more packets are needed. +var ErrMorePacketsNeeded = errors.New("need more packets") + +func joinFragments(fragments [][]byte, size int) []byte { + ret := make([]byte, size) + n := 0 + for _, p := range fragments { + n += copy(ret[n:], p) + } + return ret +} + +// Decoder is a RTP/MPEG-4 Audio decoder. +// Specification: https://datatracker.ietf.org/doc/html/rfc3640 +// Specification: https://datatracker.ietf.org/doc/html/rfc6416#section-7.3 +type Decoder struct { + // use RFC6416 (LATM) instead of RFC3640 (generic). + LATM bool + + // Generic-only + // The number of bits in which the AU-size field is encoded in the AU-header. + SizeLength int + // The number of bits in which the AU-Index is encoded in the first AU-header. + IndexLength int + // The number of bits in which the AU-Index-delta field is encoded in any non-first AU-header. + IndexDeltaLength int + + firstAUParsed bool + adtsMode bool + fragments [][]byte + fragmentsSize int + fragmentsExpected int + fragmentNextSeqNum uint16 +} + +// Init initializes the decoder. +func (d *Decoder) Init() error { + return nil +} + +func (d *Decoder) resetFragments() { + d.fragments = d.fragments[:0] + d.fragmentsSize = 0 +} + +// Decode decodes AUs from a RTP packet. +// It returns the AUs and the PTS of the first AU. +// The PTS of subsequent AUs can be calculated by adding time.Second*mpeg4audio.SamplesPerAccessUnit/clockRate. +func (d *Decoder) Decode(pkt *rtp.Packet) ([][]byte, error) { + if !d.LATM { + return d.decodeGeneric(pkt) + } + return d.decodeLATM(pkt) +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4audio/decoder_generic.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4audio/decoder_generic.go new file mode 100644 index 000000000..db69ae7ae --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4audio/decoder_generic.go @@ -0,0 +1,201 @@ +package rtpmpeg4audio + +import ( + "fmt" + + "github.com/bluenviron/mediacommon/v2/pkg/bits" + "github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio" + "github.com/pion/rtp" +) + +func (d *Decoder) decodeGeneric(pkt *rtp.Packet) ([][]byte, error) { + if len(pkt.Payload) < 2 { + d.resetFragments() + return nil, fmt.Errorf("payload is too short") + } + + // AU-headers-length (16 bits) + headersLen := int(uint16(pkt.Payload[0])<<8 | uint16(pkt.Payload[1])) + if headersLen == 0 { + d.resetFragments() + return nil, fmt.Errorf("invalid AU-headers-length") + } + payload := pkt.Payload[2:] + + // AU-headers + dataLens, err := d.readAUHeaders(payload, headersLen) + if err != nil { + d.resetFragments() + return nil, err + } + + pos := (headersLen / 8) + if (headersLen % 8) != 0 { + pos++ + } + payload = payload[pos:] + + var aus [][]byte + + if d.fragmentsSize == 0 { + d.resetFragments() + + if pkt.Marker { + // AUs + aus = make([][]byte, len(dataLens)) + for i, dataLen := range dataLens { + if len(payload) < int(dataLen) { + return nil, fmt.Errorf("payload is too short") + } + + aus[i] = payload[:dataLen] + payload = payload[dataLen:] + } + } else { + if len(dataLens) != 1 { + return nil, fmt.Errorf("a fragmented packet can only contain one AU") + } + + if len(payload) < int(dataLens[0]) { + return nil, fmt.Errorf("payload is too short") + } + + d.fragmentsSize = int(dataLens[0]) + d.fragments = append(d.fragments, payload[:dataLens[0]]) + d.fragmentNextSeqNum = pkt.SequenceNumber + 1 + return nil, ErrMorePacketsNeeded + } + } else { + // we are decoding a fragmented AU + if len(dataLens) != 1 { + d.resetFragments() + return nil, fmt.Errorf("a fragmented packet can only contain one AU") + } + + if len(payload) < int(dataLens[0]) { + d.resetFragments() + return nil, fmt.Errorf("payload is too short") + } + + if pkt.SequenceNumber != d.fragmentNextSeqNum { + d.resetFragments() + return nil, fmt.Errorf("discarding frame since a RTP packet is missing") + } + + d.fragmentsSize += int(dataLens[0]) + + if d.fragmentsSize > mpeg4audio.MaxAccessUnitSize { + errSize := d.fragmentsSize + d.resetFragments() + return nil, fmt.Errorf("access unit size (%d) is too big, maximum is %d", + errSize, mpeg4audio.MaxAccessUnitSize) + } + + d.fragments = append(d.fragments, payload[:dataLens[0]]) + d.fragmentNextSeqNum++ + + if !pkt.Marker { + return nil, ErrMorePacketsNeeded + } + + aus = [][]byte{joinFragments(d.fragments, d.fragmentsSize)} + d.resetFragments() + } + + return d.removeADTS(aus) +} + +func (d *Decoder) readAUHeaders(buf []byte, headersLen int) ([]uint64, error) { + firstRead := false + + count := 0 + for i := 0; i < headersLen; { + if i == 0 { + i += d.SizeLength + i += d.IndexLength + } else { + i += d.SizeLength + i += d.IndexDeltaLength + } + count++ + } + + dataLens := make([]uint64, count) + + pos := 0 + i := 0 + + for headersLen > 0 { + dataLen, err := bits.ReadBits(buf, &pos, d.SizeLength) + if err != nil { + return nil, err + } + headersLen -= d.SizeLength + + if !firstRead { + firstRead = true + if d.IndexLength > 0 { + auIndex, err := bits.ReadBits(buf, &pos, d.IndexLength) + if err != nil { + return nil, err + } + headersLen -= d.IndexLength + + if auIndex != 0 { + return nil, fmt.Errorf("AU-index different than zero is not supported") + } + } + } else if d.IndexDeltaLength > 0 { + auIndexDelta, err := bits.ReadBits(buf, &pos, d.IndexDeltaLength) + if err != nil { + return nil, err + } + headersLen -= d.IndexDeltaLength + + if auIndexDelta != 0 { + return nil, fmt.Errorf("AU-index-delta different than zero is not supported") + } + } + + dataLens[i] = dataLen + i++ + } + + return dataLens, nil +} + +// some cameras wrap AUs into ADTS +func (d *Decoder) removeADTS(aus [][]byte) ([][]byte, error) { + if !d.firstAUParsed { + d.firstAUParsed = true + + if len(aus) == 1 && len(aus[0]) >= 2 { + if aus[0][0] == 0xFF && (aus[0][1]&0xF0) == 0xF0 { + var pkts mpeg4audio.ADTSPackets + err := pkts.Unmarshal(aus[0]) + if err == nil && len(pkts) == 1 { + d.adtsMode = true + aus[0] = pkts[0].AU + } + } + } + } else if d.adtsMode { + if len(aus) != 1 { + return nil, fmt.Errorf("multiple AUs in ADTS mode are not supported") + } + + var pkts mpeg4audio.ADTSPackets + err := pkts.Unmarshal(aus[0]) + if err != nil { + return nil, fmt.Errorf("unable to decode ADTS: %w", err) + } + + if len(pkts) != 1 { + return nil, fmt.Errorf("multiple ADTS packets are not supported") + } + + aus[0] = pkts[0].AU + } + + return aus, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4audio/decoder_latm.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4audio/decoder_latm.go new file mode 100644 index 000000000..cdaec8ecb --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4audio/decoder_latm.go @@ -0,0 +1,63 @@ +package rtpmpeg4audio + +import ( + "fmt" + + "github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio" + "github.com/pion/rtp" +) + +func (d *Decoder) decodeLATM(pkt *rtp.Packet) ([][]byte, error) { + var au []byte + buf := pkt.Payload + + if d.fragmentsSize == 0 { + pl, n, err := payloadLengthInfoDecode(buf) + if err != nil { + return nil, err + } + + buf = buf[n:] + bl := len(buf) + + if pl <= bl { + au = buf[:pl] + // there could be other data, due to otherDataPresent. Ignore it. + } else { + if pl > mpeg4audio.MaxAccessUnitSize { + errSize := pl + d.resetFragments() + return nil, fmt.Errorf("access unit size (%d) is too big, maximum is %d", + errSize, mpeg4audio.MaxAccessUnitSize) + } + + d.fragments = append(d.fragments, buf) + d.fragmentsSize = pl + d.fragmentsExpected = pl - bl + d.fragmentNextSeqNum = pkt.SequenceNumber + 1 + return nil, ErrMorePacketsNeeded + } + } else { + if pkt.SequenceNumber != d.fragmentNextSeqNum { + d.resetFragments() + return nil, fmt.Errorf("discarding frame since a RTP packet is missing") + } + + bl := len(buf) + + if d.fragmentsExpected > bl { + d.fragments = append(d.fragments, buf) + d.fragmentsExpected -= bl + d.fragmentNextSeqNum++ + return nil, ErrMorePacketsNeeded + } + + d.fragments = append(d.fragments, buf[:d.fragmentsExpected]) + // there could be other data, due to otherDataPresent. Ignore it. + + au = joinFragments(d.fragments, d.fragmentsSize) + d.resetFragments() + } + + return [][]byte{au}, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4audio/encoder.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4audio/encoder.go new file mode 100644 index 000000000..9558033a5 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4audio/encoder.go @@ -0,0 +1,88 @@ +package rtpmpeg4audio + +import ( + "crypto/rand" + + "github.com/pion/rtp" +) + +const ( + rtpVersion = 2 + defaultPayloadMaxSize = 1460 // 1500 (UDP MTU) - 20 (IP header) - 8 (UDP header) - 12 (RTP header) +) + +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 +} + +// Encoder is a RTP/MPEG-4 audio encoder. +// Specification: https://datatracker.ietf.org/doc/html/rfc3640 +// Specification: https://datatracker.ietf.org/doc/html/rfc6416#section-7.3 +type Encoder struct { + // payload type of packets. + PayloadType uint8 + + // use RFC6416 (LATM) instead of RFC3640 (generic). + LATM bool + + // The number of bits in which the AU-size field is encoded in the AU-header. + SizeLength int + + // The number of bits in which the AU-Index is encoded in the first AU-header. + IndexLength int + + // The number of bits in which the AU-Index-delta field is encoded in any non-first AU-header. + IndexDeltaLength int + + // SSRC of packets (optional). + // It defaults to a random value. + SSRC *uint32 + + // initial sequence number of packets (optional). + // It defaults to a random value. + InitialSequenceNumber *uint16 + + // maximum size of packet payloads (optional). + // It defaults to 1460. + PayloadMaxSize int + + sequenceNumber uint16 +} + +// Init initializes the encoder. +func (e *Encoder) Init() error { + if e.SSRC == nil { + v, err := randUint32() + if err != nil { + return err + } + e.SSRC = &v + } + if e.InitialSequenceNumber == nil { + v, err := randUint32() + if err != nil { + return err + } + v2 := uint16(v) + e.InitialSequenceNumber = &v2 + } + if e.PayloadMaxSize == 0 { + e.PayloadMaxSize = defaultPayloadMaxSize + } + + e.sequenceNumber = *e.InitialSequenceNumber + return nil +} + +// Encode encodes AUs into RTP packets. +func (e *Encoder) Encode(aus [][]byte) ([]*rtp.Packet, error) { + if !e.LATM { + return e.encodeGeneric(aus) + } + return e.encodeLATM(aus) +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4audio/encoder_generic.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4audio/encoder_generic.go new file mode 100644 index 000000000..5c56bcda3 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4audio/encoder_generic.go @@ -0,0 +1,196 @@ +package rtpmpeg4audio + +import ( + "github.com/pion/rtp" + + "github.com/bluenviron/mediacommon/v2/pkg/bits" + "github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio" +) + +func packetCountGeneric(avail, le int) int { + n := le / avail + if (le % avail) != 0 { + n++ + } + return n +} + +func (e *Encoder) encodeGeneric(aus [][]byte) ([]*rtp.Packet, error) { + var rets []*rtp.Packet + var batch [][]byte + timestamp := uint32(0) + + // split AUs into batches + for _, au := range aus { + if e.lenGenericAggregated(batch, au) <= e.PayloadMaxSize { + // add to existing batch + batch = append(batch, au) + } else { + // write current batch + if batch != nil { + pkts, err := e.writeGenericBatch(batch, timestamp) + if err != nil { + return nil, err + } + rets = append(rets, pkts...) + timestamp += uint32(len(batch)) * mpeg4audio.SamplesPerAccessUnit + } + + // initialize new batch + batch = [][]byte{au} + } + } + + // write last batch + pkts, err := e.writeGenericBatch(batch, timestamp) + if err != nil { + return nil, err + } + rets = append(rets, pkts...) + + return rets, nil +} + +func (e *Encoder) writeGenericBatch(aus [][]byte, timestamp uint32) ([]*rtp.Packet, error) { + if len(aus) != 1 || e.lenGenericAggregated(aus, nil) < e.PayloadMaxSize { + return e.writeGenericAggregated(aus, timestamp) + } + + return e.writeGenericFragmented(aus[0], timestamp) +} + +func (e *Encoder) writeGenericFragmented(au []byte, timestamp uint32) ([]*rtp.Packet, error) { + auHeadersLen := e.SizeLength + e.IndexLength + auHeadersLenBytes := auHeadersLen / 8 + if (auHeadersLen % 8) != 0 { + auHeadersLenBytes++ + } + + avail := e.PayloadMaxSize - 2 - auHeadersLenBytes + le := len(au) + packetCount := packetCountGeneric(avail, le) + + ret := make([]*rtp.Packet, packetCount) + le = avail + + for i := range ret { + if i == (packetCount - 1) { + le = len(au) + } + + payload := make([]byte, 2+auHeadersLenBytes+le) + + // AU-headers-length + payload[0] = byte(auHeadersLen >> 8) + payload[1] = byte(auHeadersLen) + + // AU-headers + pos := 0 + bits.WriteBitsUnsafe(payload[2:], &pos, uint64(le), e.SizeLength) + bits.WriteBitsUnsafe(payload[2:], &pos, 0, e.IndexLength) + + // AU + copy(payload[2+auHeadersLenBytes:], au) + au = au[le:] + + ret[i] = &rtp.Packet{ + Header: rtp.Header{ + Version: rtpVersion, + PayloadType: e.PayloadType, + SequenceNumber: e.sequenceNumber, + Timestamp: timestamp, + SSRC: *e.SSRC, + Marker: (i == packetCount-1), + }, + Payload: payload, + } + + e.sequenceNumber++ + } + + return ret, nil +} + +func (e *Encoder) lenGenericAggregated(aus [][]byte, addAU []byte) int { + n := 2 // AU-headers-length + + // AU-headers + auHeadersLen := 0 + i := 0 + for range aus { + if i == 0 { + auHeadersLen += e.SizeLength + e.IndexLength + } else { + auHeadersLen += e.SizeLength + e.IndexDeltaLength + } + i++ + } + if addAU != nil { + if i == 0 { + auHeadersLen += e.SizeLength + e.IndexLength + } else { + auHeadersLen += e.SizeLength + e.IndexDeltaLength + } + } + n += auHeadersLen / 8 + if (auHeadersLen % 8) != 0 { + n++ + } + + // AU + for _, au := range aus { + n += len(au) + } + n += len(addAU) + + return n +} + +func (e *Encoder) writeGenericAggregated(aus [][]byte, timestamp uint32) ([]*rtp.Packet, error) { + payload := make([]byte, e.lenGenericAggregated(aus, nil)) + + // AU-headers + written := 0 + pos := 0 + for i, au := range aus { + bits.WriteBitsUnsafe(payload[2:], &pos, uint64(len(au)), e.SizeLength) + written += e.SizeLength + if i == 0 { + bits.WriteBitsUnsafe(payload[2:], &pos, 0, e.IndexLength) + written += e.IndexLength + } else { + bits.WriteBitsUnsafe(payload[2:], &pos, 0, e.IndexDeltaLength) + written += e.IndexDeltaLength + } + } + pos = 2 + (written / 8) + if (written % 8) != 0 { + pos++ + } + + // AU-headers-length + payload[0] = byte(written >> 8) + payload[1] = byte(written) + + // AUs + for _, au := range aus { + auLen := copy(payload[pos:], au) + pos += auLen + } + + pkt := &rtp.Packet{ + Header: rtp.Header{ + Version: rtpVersion, + PayloadType: e.PayloadType, + SequenceNumber: e.sequenceNumber, + Timestamp: timestamp, + SSRC: *e.SSRC, + Marker: true, + }, + Payload: payload, + } + + e.sequenceNumber++ + + return []*rtp.Packet{pkt}, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4audio/encoder_latm.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4audio/encoder_latm.go new file mode 100644 index 000000000..16db07e8a --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4audio/encoder_latm.go @@ -0,0 +1,76 @@ +package rtpmpeg4audio + +import ( + "github.com/pion/rtp" + + "github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio" +) + +func (e *Encoder) packetCountLATM(auLen int, plil int) int { + totalLen := plil + auLen + n := totalLen / e.PayloadMaxSize + if (totalLen % e.PayloadMaxSize) != 0 { + n++ + } + return n +} + +func (e *Encoder) encodeLATM(aus [][]byte) ([]*rtp.Packet, error) { + var rets []*rtp.Packet + + for i, au := range aus { + timestamp := uint32(i) * mpeg4audio.SamplesPerAccessUnit + + add, err := e.encodeLATMSingle(au, timestamp) + if err != nil { + return nil, err + } + rets = append(rets, add...) + } + + return rets, nil +} + +func (e *Encoder) encodeLATMSingle(au []byte, timestamp uint32) ([]*rtp.Packet, error) { + auLen := len(au) + plil := payloadLengthInfoEncodeSize(auLen) + packetCount := e.packetCountLATM(auLen, plil) + + ret := make([]*rtp.Packet, packetCount) + le := e.PayloadMaxSize - plil + + for i := range ret { + if i == (packetCount - 1) { + le = len(au) + } + + var payload []byte + + if i == 0 { + payload = make([]byte, plil+le) + payloadLengthInfoEncode(plil, auLen, payload) + copy(payload[plil:], au[:le]) + au = au[le:] + le = e.PayloadMaxSize + } else { + payload = au[:le] + au = au[le:] + } + + ret[i] = &rtp.Packet{ + Header: rtp.Header{ + Version: rtpVersion, + PayloadType: e.PayloadType, + SequenceNumber: e.sequenceNumber, + Timestamp: timestamp, + SSRC: *e.SSRC, + Marker: (i == packetCount-1), + }, + Payload: payload, + } + + e.sequenceNumber++ + } + + return ret, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4audio/payload_length_info.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4audio/payload_length_info.go new file mode 100644 index 000000000..ab248a7aa --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4audio/payload_length_info.go @@ -0,0 +1,38 @@ +package rtpmpeg4audio + +import ( + "fmt" +) + +func payloadLengthInfoDecode(buf []byte) (int, int, error) { + lb := len(buf) + l := 0 + n := 0 + + for { + if (lb - n) == 0 { + return 0, 0, fmt.Errorf("not enough bytes") + } + + b := buf[n] + n++ + l += int(b) + + if b != 255 { + break + } + } + + return l, n, nil +} + +func payloadLengthInfoEncodeSize(auLen int) int { + return auLen/255 + 1 +} + +func payloadLengthInfoEncode(plil int, auLen int, buf []byte) { + for i := 0; i < (plil - 1); i++ { + buf[i] = 255 + } + buf[plil-1] = byte(auLen % 255) +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4audio/rtpmpeg4audio.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4audio/rtpmpeg4audio.go new file mode 100644 index 000000000..6698abbd3 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4audio/rtpmpeg4audio.go @@ -0,0 +1,2 @@ +// Package rtpmpeg4audio contains a RTP/MPEG-4 Audio decoder and encoder. +package rtpmpeg4audio diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4video/decoder.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4video/decoder.go new file mode 100644 index 000000000..b3a569545 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4video/decoder.go @@ -0,0 +1,82 @@ +package rtpmpeg4video + +import ( + "errors" + "fmt" + + "github.com/pion/rtp" + + "github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4video" +) + +// ErrMorePacketsNeeded is returned when more packets are needed. +var ErrMorePacketsNeeded = errors.New("need more packets") + +func joinFragments(fragments [][]byte, size int) []byte { + ret := make([]byte, size) + n := 0 + for _, p := range fragments { + n += copy(ret[n:], p) + } + return ret +} + +// Decoder is a RTP/MPEG-4 Video decoder. +// Specification: https://datatracker.ietf.org/doc/html/rfc6416 +type Decoder struct { + fragments [][]byte + fragmentsSize int + fragmentNextSeqNum uint16 +} + +// Init initializes the decoder. +func (d *Decoder) Init() error { + return nil +} + +func (d *Decoder) resetFragments() { + d.fragments = d.fragments[:0] + d.fragmentsSize = 0 +} + +// Decode decodes a frame from a RTP packet. +func (d *Decoder) Decode(pkt *rtp.Packet) ([]byte, error) { + var frame []byte + + if d.fragmentsSize == 0 { + if pkt.Marker { + frame = pkt.Payload + } else { + d.fragmentsSize = len(pkt.Payload) + d.fragments = append(d.fragments, pkt.Payload) + d.fragmentNextSeqNum = pkt.SequenceNumber + 1 + return nil, ErrMorePacketsNeeded + } + } else { + if pkt.SequenceNumber != d.fragmentNextSeqNum { + d.resetFragments() + return nil, fmt.Errorf("discarding frame since a RTP packet is missing") + } + + d.fragmentsSize += len(pkt.Payload) + + if d.fragmentsSize > mpeg4video.MaxFrameSize { + errSize := d.fragmentsSize + d.resetFragments() + return nil, fmt.Errorf("frame size (%d) is too big, maximum is %d", + errSize, mpeg4video.MaxFrameSize) + } + + d.fragments = append(d.fragments, pkt.Payload) + d.fragmentNextSeqNum++ + + if !pkt.Marker { + return nil, ErrMorePacketsNeeded + } + + frame = joinFragments(d.fragments, d.fragmentsSize) + d.resetFragments() + } + + return frame, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4video/encoder.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4video/encoder.go new file mode 100644 index 000000000..bf3fa0d2e --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4video/encoder.go @@ -0,0 +1,108 @@ +package rtpmpeg4video + +import ( + "crypto/rand" + + "github.com/pion/rtp" +) + +const ( + rtpVersion = 2 + defaultPayloadMaxSize = 1460 // 1500 (UDP MTU) - 20 (IP header) - 8 (UDP header) - 12 (RTP header) +) + +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 packetCount(avail, le int) int { + n := le / avail + if (le % avail) != 0 { + n++ + } + return n +} + +// Encoder is a RTP/MPEG-4 Video encoder. +// Specification: https://datatracker.ietf.org/doc/html/rfc6416 +type Encoder struct { + // payload type of packets. + PayloadType uint8 + + // SSRC of packets (optional). + // It defaults to a random value. + SSRC *uint32 + + // initial sequence number of packets (optional). + // It defaults to a random value. + InitialSequenceNumber *uint16 + + // maximum size of packet payloads (optional). + // It defaults to 1460. + PayloadMaxSize int + + sequenceNumber uint16 +} + +// Init initializes the encoder. +func (e *Encoder) Init() error { + if e.SSRC == nil { + v, err := randUint32() + if err != nil { + return err + } + e.SSRC = &v + } + if e.InitialSequenceNumber == nil { + v, err := randUint32() + if err != nil { + return err + } + v2 := uint16(v) + e.InitialSequenceNumber = &v2 + } + if e.PayloadMaxSize == 0 { + e.PayloadMaxSize = defaultPayloadMaxSize + } + + e.sequenceNumber = *e.InitialSequenceNumber + return nil +} + +// Encode encodes a frame into RTP packets. +func (e *Encoder) Encode(frame []byte) ([]*rtp.Packet, error) { + avail := e.PayloadMaxSize + le := len(frame) + packetCount := packetCount(avail, le) + + ret := make([]*rtp.Packet, packetCount) + pos := 0 + le = avail + + for i := range ret { + if i == (packetCount - 1) { + le = len(frame[pos:]) + } + + ret[i] = &rtp.Packet{ + Header: rtp.Header{ + Version: rtpVersion, + PayloadType: e.PayloadType, + SequenceNumber: e.sequenceNumber, + SSRC: *e.SSRC, + Marker: (i == packetCount-1), + }, + Payload: frame[pos : pos+le], + } + + pos += le + e.sequenceNumber++ + } + + return ret, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4video/rtpmpeg4video.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4video/rtpmpeg4video.go new file mode 100644 index 000000000..1acba5b70 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4video/rtpmpeg4video.go @@ -0,0 +1,2 @@ +// Package rtpmpeg4video contains a RTP/MPEG-4 Video decoder and encoder. +package rtpmpeg4video diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpsimpleaudio/decoder.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpsimpleaudio/decoder.go new file mode 100644 index 000000000..d53dcce59 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpsimpleaudio/decoder.go @@ -0,0 +1,18 @@ +package rtpsimpleaudio + +import ( + "github.com/pion/rtp" +) + +// Decoder is a RTP/simple audio decoder. +type Decoder struct{} + +// Init initializes the decoder. +func (d *Decoder) Init() error { + return nil +} + +// Decode decodes an audio frame from a RTP packet. +func (d *Decoder) Decode(pkt *rtp.Packet) ([]byte, error) { + return pkt.Payload, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpsimpleaudio/encoder.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpsimpleaudio/encoder.go new file mode 100644 index 000000000..0270285b6 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpsimpleaudio/encoder.go @@ -0,0 +1,89 @@ +package rtpsimpleaudio + +import ( + "crypto/rand" + "fmt" + + "github.com/pion/rtp" +) + +const ( + rtpVersion = 2 + defaultPayloadMaxSize = 1460 // 1500 (UDP MTU) - 20 (IP header) - 8 (UDP header) - 12 (RTP header) +) + +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 +} + +// Encoder is a RTP/simple audio encoder. +type Encoder struct { + // payload type of packets. + PayloadType uint8 + + // SSRC of packets (optional). + // It defaults to a random value. + SSRC *uint32 + + // initial sequence number of packets (optional). + // It defaults to a random value. + InitialSequenceNumber *uint16 + + // maximum size of packet payloads (optional). + // It defaults to 1460. + PayloadMaxSize int + + sequenceNumber uint16 +} + +// Init initializes the encoder. +func (e *Encoder) Init() error { + if e.SSRC == nil { + v, err := randUint32() + if err != nil { + return err + } + e.SSRC = &v + } + if e.InitialSequenceNumber == nil { + v, err := randUint32() + if err != nil { + return err + } + v2 := uint16(v) + e.InitialSequenceNumber = &v2 + } + if e.PayloadMaxSize == 0 { + e.PayloadMaxSize = defaultPayloadMaxSize + } + + e.sequenceNumber = *e.InitialSequenceNumber + return nil +} + +// Encode encodes an audio frame into a RTP packet. +func (e *Encoder) Encode(frame []byte) (*rtp.Packet, error) { + if len(frame) > e.PayloadMaxSize { + return nil, fmt.Errorf("frame is too big") + } + + pkt := &rtp.Packet{ + Header: rtp.Header{ + Version: rtpVersion, + PayloadType: e.PayloadType, + SequenceNumber: e.sequenceNumber, + SSRC: *e.SSRC, + Marker: false, + }, + Payload: frame, + } + + e.sequenceNumber++ + + return pkt, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpsimpleaudio/rtpsimpleaudio.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpsimpleaudio/rtpsimpleaudio.go new file mode 100644 index 000000000..047a90010 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpsimpleaudio/rtpsimpleaudio.go @@ -0,0 +1,2 @@ +// Package rtpsimpleaudio contains a RTP decoder and encoder for audio codecs that fit in a single packet. +package rtpsimpleaudio diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp8/decoder.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp8/decoder.go new file mode 100644 index 000000000..df6da538c --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp8/decoder.go @@ -0,0 +1,113 @@ +package rtpvp8 + +import ( + "errors" + "fmt" + + "github.com/bluenviron/mediacommon/v2/pkg/codecs/vp8" + "github.com/pion/rtp" + "github.com/pion/rtp/codecs" +) + +// ErrMorePacketsNeeded is returned when more packets are needed. +var ErrMorePacketsNeeded = errors.New("need more packets") + +// ErrNonStartingPacketAndNoPrevious is returned when we received a non-starting +// packet of a fragmented frame and we didn't received anything before. +// It's normal to receive this when decoding a stream that has been already +// running for some time. +var ErrNonStartingPacketAndNoPrevious = errors.New( + "received a non-starting fragment without any previous starting fragment") + +func joinFragments(fragments [][]byte, size int) []byte { + ret := make([]byte, size) + n := 0 + for _, p := range fragments { + n += copy(ret[n:], p) + } + return ret +} + +// Decoder is a RTP/VP8 decoder. +// Specification: https://datatracker.ietf.org/doc/html/rfc7741 +type Decoder struct { + firstPacketReceived bool + fragmentsSize int + fragments [][]byte + fragmentNextSeqNum uint16 +} + +// Init initializes the decoder. +func (d *Decoder) Init() error { + return nil +} + +func (d *Decoder) resetFragments() { + d.fragments = d.fragments[:0] + d.fragmentsSize = 0 +} + +// Decode decodes a VP8 frame from a RTP packet. +func (d *Decoder) Decode(pkt *rtp.Packet) ([]byte, error) { + var vpkt codecs.VP8Packet + _, err := vpkt.Unmarshal(pkt.Payload) + if err != nil { + d.resetFragments() + return nil, err + } + + if vpkt.PID != 0 { + d.resetFragments() + return nil, fmt.Errorf("packets containing single partitions are not supported") + } + + var frame []byte + + if vpkt.S == 1 { + d.resetFragments() + d.firstPacketReceived = true + + if !pkt.Marker { + d.fragmentsSize = len(vpkt.Payload) + d.fragments = append(d.fragments, vpkt.Payload) + d.fragmentNextSeqNum = pkt.SequenceNumber + 1 + return nil, ErrMorePacketsNeeded + } + + frame = vpkt.Payload + } else { + if d.fragmentsSize == 0 { + if !d.firstPacketReceived { + return nil, ErrNonStartingPacketAndNoPrevious + } + + return nil, fmt.Errorf("received a non-starting fragment") + } + + if pkt.SequenceNumber != d.fragmentNextSeqNum { + d.resetFragments() + return nil, fmt.Errorf("discarding frame since a RTP packet is missing") + } + + d.fragmentsSize += len(vpkt.Payload) + + if d.fragmentsSize > vp8.MaxFrameSize { + errSize := d.fragmentsSize + d.resetFragments() + return nil, fmt.Errorf("frame size (%d) is too big, maximum is %d", + errSize, vp8.MaxFrameSize) + } + + d.fragments = append(d.fragments, vpkt.Payload) + d.fragmentNextSeqNum++ + + if !pkt.Marker { + return nil, ErrMorePacketsNeeded + } + + frame = joinFragments(d.fragments, d.fragmentsSize) + d.resetFragments() + } + + return frame, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp8/encoder.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp8/encoder.go new file mode 100644 index 000000000..eb2c23043 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp8/encoder.go @@ -0,0 +1,98 @@ +package rtpvp8 + +import ( + "crypto/rand" + "fmt" + + "github.com/pion/rtp" + "github.com/pion/rtp/codecs" +) + +const ( + rtpVersion = 2 + defaultPayloadMaxSize = 1460 // 1500 (UDP MTU) - 20 (IP header) - 8 (UDP header) - 12 (RTP header) +) + +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 +} + +// Encoder is a RTP/VP8 encoder. +// Specification: https://datatracker.ietf.org/doc/html/rfc7741 +type Encoder struct { + // payload type of packets. + PayloadType uint8 + + // SSRC of packets (optional). + // It defaults to a random value. + SSRC *uint32 + + // initial sequence number of packets (optional). + // It defaults to a random value. + InitialSequenceNumber *uint16 + + // maximum size of packet payloads (optional). + // It defaults to 1460. + PayloadMaxSize int + + sequenceNumber uint16 + vp codecs.VP8Payloader +} + +// Init initializes the encoder. +func (e *Encoder) Init() error { + if e.SSRC == nil { + v, err := randUint32() + if err != nil { + return err + } + e.SSRC = &v + } + if e.InitialSequenceNumber == nil { + v, err := randUint32() + if err != nil { + return err + } + v2 := uint16(v) + e.InitialSequenceNumber = &v2 + } + if e.PayloadMaxSize == 0 { + e.PayloadMaxSize = defaultPayloadMaxSize + } + + e.sequenceNumber = *e.InitialSequenceNumber + return nil +} + +// Encode encodes a VP8 frame into RTP/VP8 packets. +func (e *Encoder) Encode(frame []byte) ([]*rtp.Packet, error) { + payloads := e.vp.Payload(uint16(e.PayloadMaxSize), frame) + if payloads == nil { + return nil, fmt.Errorf("payloader failed") + } + + plen := len(payloads) + ret := make([]*rtp.Packet, plen) + + for i, payload := range payloads { + ret[i] = &rtp.Packet{ + Header: rtp.Header{ + Version: rtpVersion, + PayloadType: e.PayloadType, + SequenceNumber: e.sequenceNumber, + SSRC: *e.SSRC, + Marker: i == (plen - 1), + }, + Payload: payload, + } + + e.sequenceNumber++ + } + + return ret, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp8/rtpvp8.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp8/rtpvp8.go new file mode 100644 index 000000000..3094c477a --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp8/rtpvp8.go @@ -0,0 +1,2 @@ +// Package rtpvp8 contains a RTP/VP8 decoder and encoder. +package rtpvp8 diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp9/decoder.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp9/decoder.go new file mode 100644 index 000000000..628d17589 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp9/decoder.go @@ -0,0 +1,108 @@ +package rtpvp9 + +import ( + "errors" + "fmt" + + "github.com/bluenviron/mediacommon/v2/pkg/codecs/vp9" + "github.com/pion/rtp" + "github.com/pion/rtp/codecs" +) + +// ErrMorePacketsNeeded is returned when more packets are needed. +var ErrMorePacketsNeeded = errors.New("need more packets") + +// ErrNonStartingPacketAndNoPrevious is returned when we received a non-starting +// packet of a fragmented frame and we didn't received anything before. +// It's normal to receive this when decoding a stream that has been already +// running for some time. +var ErrNonStartingPacketAndNoPrevious = errors.New( + "received a non-starting fragment without any previous starting fragment") + +func joinFragments(fragments [][]byte, size int) []byte { + ret := make([]byte, size) + n := 0 + for _, p := range fragments { + n += copy(ret[n:], p) + } + return ret +} + +// Decoder is a RTP/VP9 decoder. +// Specification: https://datatracker.ietf.org/doc/html/draft-ietf-payload-vp9-16 +type Decoder struct { + firstPacketReceived bool + fragmentsSize int + fragments [][]byte + fragmentNextSeqNum uint16 +} + +// Init initializes the decoder. +func (d *Decoder) Init() error { + return nil +} + +func (d *Decoder) resetFragments() { + d.fragments = d.fragments[:0] + d.fragmentsSize = 0 +} + +// Decode decodes a VP9 frame from a RTP packet. +func (d *Decoder) Decode(pkt *rtp.Packet) ([]byte, error) { + var vpkt codecs.VP9Packet + _, err := vpkt.Unmarshal(pkt.Payload) + if err != nil { + d.resetFragments() + return nil, err + } + + var frame []byte + + if vpkt.B { + d.resetFragments() + d.firstPacketReceived = true + + if !vpkt.E { + d.fragmentsSize = len(vpkt.Payload) + d.fragments = append(d.fragments, vpkt.Payload) + d.fragmentNextSeqNum = pkt.SequenceNumber + 1 + return nil, ErrMorePacketsNeeded + } + + frame = vpkt.Payload + } else { + if d.fragmentsSize == 0 { + if !d.firstPacketReceived { + return nil, ErrNonStartingPacketAndNoPrevious + } + + return nil, fmt.Errorf("received a non-starting fragment") + } + + if pkt.SequenceNumber != d.fragmentNextSeqNum { + d.resetFragments() + return nil, fmt.Errorf("discarding frame since a RTP packet is missing") + } + + d.fragmentsSize += len(vpkt.Payload) + + if d.fragmentsSize > vp9.MaxFrameSize { + errSize := d.fragmentsSize + d.resetFragments() + return nil, fmt.Errorf("frame size (%d) is too big, maximum is %d", + errSize, vp9.MaxFrameSize) + } + + d.fragments = append(d.fragments, vpkt.Payload) + d.fragmentNextSeqNum++ + + if !vpkt.E { + return nil, ErrMorePacketsNeeded + } + + frame = joinFragments(d.fragments, d.fragmentsSize) + d.resetFragments() + } + + return frame, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp9/encoder.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp9/encoder.go new file mode 100644 index 000000000..f222e0c07 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp9/encoder.go @@ -0,0 +1,115 @@ +package rtpvp9 + +import ( + "crypto/rand" + "fmt" + + "github.com/pion/rtp" + "github.com/pion/rtp/codecs" +) + +const ( + rtpVersion = 2 + defaultPayloadMaxSize = 1460 // 1500 (UDP MTU) - 20 (IP header) - 8 (UDP header) - 12 (RTP header) +) + +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 +} + +// Encoder is a RTP/VP9 encoder. +// Specification: https://datatracker.ietf.org/doc/html/draft-ietf-payload-vp9-16 +type Encoder struct { + // payload type of packets. + PayloadType uint8 + + // SSRC of packets (optional). + // It defaults to a random value. + SSRC *uint32 + + // initial sequence number of packets (optional). + // It defaults to a random value. + InitialSequenceNumber *uint16 + + // maximum size of packet payloads (optional). + // It defaults to 1460. + PayloadMaxSize int + + // initial picture ID of frames (optional). + // It defaults to a random value. + InitialPictureID *uint16 + + sequenceNumber uint16 + vp codecs.VP9Payloader +} + +// Init initializes the encoder. +func (e *Encoder) Init() error { + if e.SSRC == nil { + v, err := randUint32() + if err != nil { + return err + } + e.SSRC = &v + } + if e.InitialSequenceNumber == nil { + v, err := randUint32() + if err != nil { + return err + } + v2 := uint16(v) + e.InitialSequenceNumber = &v2 + } + if e.PayloadMaxSize == 0 { + e.PayloadMaxSize = defaultPayloadMaxSize + } + if e.InitialPictureID == nil { + v, err := randUint32() + if err != nil { + return err + } + v2 := uint16(v) + e.InitialPictureID = &v2 + } + + e.sequenceNumber = *e.InitialSequenceNumber + + e.vp.InitialPictureIDFn = func() uint16 { + return *e.InitialPictureID + } + + return nil +} + +// Encode encodes a VP9 frame into RTP/VP9 packets. +func (e *Encoder) Encode(frame []byte) ([]*rtp.Packet, error) { + payloads := e.vp.Payload(uint16(e.PayloadMaxSize), frame) + if payloads == nil { + return nil, fmt.Errorf("payloader failed") + } + + plen := len(payloads) + ret := make([]*rtp.Packet, plen) + + for i, payload := range payloads { + ret[i] = &rtp.Packet{ + Header: rtp.Header{ + Version: rtpVersion, + PayloadType: e.PayloadType, + SequenceNumber: e.sequenceNumber, + SSRC: *e.SSRC, + Marker: i == (plen - 1), + }, + Payload: payload, + } + + e.sequenceNumber++ + } + + return ret, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp9/rtpvp9.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp9/rtpvp9.go new file mode 100644 index 000000000..c89659eef --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp9/rtpvp9.go @@ -0,0 +1,2 @@ +// Package rtpvp9 contains a RTP/VP9 decoder and encoder. +package rtpvp9 diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/speex.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/speex.go new file mode 100644 index 000000000..9dab74b21 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/speex.go @@ -0,0 +1,79 @@ +package format + +import ( + "fmt" + "strconv" + + "github.com/pion/rtp" +) + +// Speex is the RTP format for the Speex codec. +// Specification: https://datatracker.ietf.org/doc/html/rfc5574 +type Speex struct { + PayloadTyp uint8 + SampleRate int + VBR *bool +} + +func (f *Speex) unmarshal(ctx *unmarshalContext) error { + f.PayloadTyp = ctx.payloadType + + sampleRate, err := strconv.ParseUint(ctx.clock, 10, 31) + if err != nil { + return err + } + f.SampleRate = int(sampleRate) + + for key, val := range ctx.fmtp { + if key == "vbr" { + if val != "on" && val != "off" { + return fmt.Errorf("invalid vbr value: %v", val) + } + + v := (val == "on") + f.VBR = &v + } + } + + return nil +} + +// Codec implements Format. +func (f *Speex) Codec() string { + return "Speex" +} + +// ClockRate implements Format. +func (f *Speex) ClockRate() int { + return f.SampleRate +} + +// PayloadType implements Format. +func (f *Speex) PayloadType() uint8 { + return f.PayloadTyp +} + +// RTPMap implements Format. +func (f *Speex) RTPMap() string { + return "speex/" + strconv.FormatInt(int64(f.SampleRate), 10) +} + +// FMTP implements Format. +func (f *Speex) FMTP() map[string]string { + fmtp := make(map[string]string) + + if f.VBR != nil { + if *f.VBR { + fmtp["vbr"] = "on" + } else { + fmtp["vbr"] = "off" + } + } + + return fmtp +} + +// PTSEqualsDTS implements Format. +func (f *Speex) PTSEqualsDTS(*rtp.Packet) bool { + return true +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/vorbis.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/vorbis.go new file mode 100644 index 000000000..9d5d8017b --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/vorbis.go @@ -0,0 +1,92 @@ +package format + +import ( + "encoding/base64" + "fmt" + "strconv" + "strings" + + "github.com/pion/rtp" +) + +// Vorbis is the RTP format for the Vorbis codec. +// Specification: https://datatracker.ietf.org/doc/html/rfc5215 +type Vorbis struct { + PayloadTyp uint8 + SampleRate int + ChannelCount int + Configuration []byte +} + +func (f *Vorbis) unmarshal(ctx *unmarshalContext) error { + f.PayloadTyp = ctx.payloadType + + tmp := strings.SplitN(ctx.clock, "/", 2) + if len(tmp) != 2 { + return fmt.Errorf("invalid clock (%v)", ctx.clock) + } + + sampleRate, err := strconv.ParseUint(tmp[0], 10, 31) + if err != nil { + return err + } + f.SampleRate = int(sampleRate) + + channelCount, err := strconv.ParseUint(tmp[1], 10, 31) + if err != nil { + return err + } + f.ChannelCount = int(channelCount) + + for key, val := range ctx.fmtp { + if key == "configuration" { + conf, err := base64.StdEncoding.DecodeString(val) + if err != nil { + return fmt.Errorf("invalid config: %v", val) + } + + f.Configuration = conf + } + } + + if f.Configuration == nil { + return fmt.Errorf("config is missing") + } + + return nil +} + +// Codec implements Format. +func (f *Vorbis) Codec() string { + return "Vorbis" +} + +// ClockRate implements Format. +func (f *Vorbis) ClockRate() int { + return f.SampleRate +} + +// PayloadType implements Format. +func (f *Vorbis) PayloadType() uint8 { + return f.PayloadTyp +} + +// RTPMap implements Format. +func (f *Vorbis) RTPMap() string { + return "VORBIS/" + strconv.FormatInt(int64(f.SampleRate), 10) + + "/" + strconv.FormatInt(int64(f.ChannelCount), 10) +} + +// FMTP implements Format. +func (f *Vorbis) FMTP() map[string]string { + fmtp := map[string]string{ + "configuration": base64.StdEncoding.EncodeToString(f.Configuration), + } + + return fmtp +} + +// PTSEqualsDTS implements Format. +func (f *Vorbis) PTSEqualsDTS(*rtp.Packet) bool { + return true +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/vp8.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/vp8.go new file mode 100644 index 000000000..cf9337972 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/vp8.go @@ -0,0 +1,112 @@ +package format + +import ( + "fmt" + "strconv" + + "github.com/pion/rtp" + + "github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp8" +) + +// VP8 is the RTP format for the VP8 codec. +// Specification: https://datatracker.ietf.org/doc/html/rfc7741 +type VP8 struct { + PayloadTyp uint8 + MaxFR *int + MaxFS *int +} + +func (f *VP8) unmarshal(ctx *unmarshalContext) error { + f.PayloadTyp = ctx.payloadType + + for key, val := range ctx.fmtp { + switch key { + case "max-fr": + n, err := strconv.ParseUint(val, 10, 31) + if err != nil { + return fmt.Errorf("invalid max-fr: %v", val) + } + + v2 := int(n) + f.MaxFR = &v2 + + case "max-fs": + n, err := strconv.ParseUint(val, 10, 31) + if err != nil { + return fmt.Errorf("invalid max-fs: %v", val) + } + + v2 := int(n) + f.MaxFS = &v2 + } + } + + return nil +} + +// Codec implements Format. +func (f *VP8) Codec() string { + return "VP8" +} + +// ClockRate implements Format. +func (f *VP8) ClockRate() int { + return 90000 +} + +// PayloadType implements Format. +func (f *VP8) PayloadType() uint8 { + return f.PayloadTyp +} + +// RTPMap implements Format. +func (f *VP8) RTPMap() string { + return "VP8/90000" +} + +// FMTP implements Format. +func (f *VP8) FMTP() map[string]string { + fmtp := make(map[string]string) + + if f.MaxFR != nil { + fmtp["max-fr"] = strconv.FormatInt(int64(*f.MaxFR), 10) + } + + if f.MaxFS != nil { + fmtp["max-fs"] = strconv.FormatInt(int64(*f.MaxFS), 10) + } + + return fmtp +} + +// PTSEqualsDTS implements Format. +func (f *VP8) PTSEqualsDTS(*rtp.Packet) bool { + return true +} + +// CreateDecoder creates a decoder able to decode the content of the format. +func (f *VP8) CreateDecoder() (*rtpvp8.Decoder, error) { + d := &rtpvp8.Decoder{} + + err := d.Init() + if err != nil { + return nil, err + } + + return d, nil +} + +// CreateEncoder creates an encoder able to encode the content of the format. +func (f *VP8) CreateEncoder() (*rtpvp8.Encoder, error) { + e := &rtpvp8.Encoder{ + PayloadType: f.PayloadTyp, + } + + err := e.Init() + if err != nil { + return nil, err + } + + return e, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/vp9.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/vp9.go new file mode 100644 index 000000000..bc42443d5 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/format/vp9.go @@ -0,0 +1,124 @@ +package format //nolint:dupl + +import ( + "fmt" + "strconv" + + "github.com/pion/rtp" + + "github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp9" +) + +// VP9 is the RTP format for the VP9 codec. +// Specification: https://datatracker.ietf.org/doc/html/draft-ietf-payload-vp9-16 +type VP9 struct { + PayloadTyp uint8 + MaxFR *int + MaxFS *int + ProfileID *int +} + +func (f *VP9) unmarshal(ctx *unmarshalContext) error { + f.PayloadTyp = ctx.payloadType + + for key, val := range ctx.fmtp { + switch key { + case "max-fr": + n, err := strconv.ParseUint(val, 10, 31) + if err != nil { + return fmt.Errorf("invalid max-fr: %v", val) + } + + v2 := int(n) + f.MaxFR = &v2 + + case "max-fs": + n, err := strconv.ParseUint(val, 10, 31) + if err != nil { + return fmt.Errorf("invalid max-fs: %v", val) + } + + v2 := int(n) + f.MaxFS = &v2 + + case "profile-id": + n, err := strconv.ParseUint(val, 10, 31) + if err != nil { + return fmt.Errorf("invalid profile-id: %v", val) + } + + v2 := int(n) + f.ProfileID = &v2 + } + } + + return nil +} + +// Codec implements Format. +func (f *VP9) Codec() string { + return "VP9" +} + +// ClockRate implements Format. +func (f *VP9) ClockRate() int { + return 90000 +} + +// PayloadType implements Format. +func (f *VP9) PayloadType() uint8 { + return f.PayloadTyp +} + +// RTPMap implements Format. +func (f *VP9) RTPMap() string { + return "VP9/90000" +} + +// FMTP implements Format. +func (f *VP9) FMTP() map[string]string { + fmtp := make(map[string]string) + + if f.MaxFR != nil { + fmtp["max-fr"] = strconv.FormatInt(int64(*f.MaxFR), 10) + } + if f.MaxFS != nil { + fmtp["max-fs"] = strconv.FormatInt(int64(*f.MaxFS), 10) + } + if f.ProfileID != nil { + fmtp["profile-id"] = strconv.FormatInt(int64(*f.ProfileID), 10) + } + + return fmtp +} + +// PTSEqualsDTS implements Format. +func (f *VP9) PTSEqualsDTS(*rtp.Packet) bool { + return true +} + +// CreateDecoder creates a decoder able to decode the content of the format. +func (f *VP9) CreateDecoder() (*rtpvp9.Decoder, error) { + d := &rtpvp9.Decoder{} + + err := d.Init() + if err != nil { + return nil, err + } + + return d, nil +} + +// CreateEncoder creates an encoder able to encode the content of the format. +func (f *VP9) CreateEncoder() (*rtpvp9.Encoder, error) { + e := &rtpvp9.Encoder{ + PayloadType: f.PayloadTyp, + } + + err := e.Init() + if err != nil { + return nil, err + } + + return e, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/headers/authenticate.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/headers/authenticate.go new file mode 100644 index 000000000..3a010a932 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/headers/authenticate.go @@ -0,0 +1,186 @@ +// Package headers contains various RTSP headers. +package headers + +import ( + "fmt" + "strings" + + "github.com/bluenviron/gortsplib/v4/pkg/base" +) + +// AuthMethod is an authentication method. +type AuthMethod int + +// authentication methods. +const ( + AuthMethodBasic AuthMethod = iota + AuthMethodDigest +) + +// AuthAlgorithm is a digest algorithm. +type AuthAlgorithm int + +// digest algorithms. +const ( + AuthAlgorithmMD5 AuthAlgorithm = iota + AuthAlgorithmSHA256 +) + +func parseAuthAlgorithm(v string) (AuthAlgorithm, error) { + switch { + case strings.ToLower(v) == "md5": + return AuthAlgorithmMD5, nil + + case strings.ToLower(v) == "sha-256": + return AuthAlgorithmSHA256, nil + + default: + return 0, fmt.Errorf("unrecognized algorithm: %v", v) + } +} + +// Authenticate is a WWW-Authenticate header. +type Authenticate struct { + // authentication method + Method AuthMethod + + // realm + Realm string + + // + // Digest authentication fields + // + + // nonce + Nonce string + + // opaque + Opaque *string + + // stale + Stale *string + + // algorithm + Algorithm *AuthAlgorithm +} + +// Unmarshal decodes a WWW-Authenticate header. +func (h *Authenticate) Unmarshal(v base.HeaderValue) error { + if len(v) == 0 { + return fmt.Errorf("value not provided") + } + + if len(v) > 1 { + return fmt.Errorf("value provided multiple times (%v)", v) + } + + v0 := v[0] + + i := strings.IndexByte(v0, ' ') + if i < 0 { + return fmt.Errorf("unable to split between method and keys (%v)", v0) + } + method, v0 := v0[:i], v0[i+1:] + + switch method { + case "Basic": + h.Method = AuthMethodBasic + + case "Digest": + h.Method = AuthMethodDigest + + default: + return fmt.Errorf("invalid method (%s)", method) + } + + if h.Method == AuthMethodBasic { + kvs, err := keyValParse(v0, ',') + if err != nil { + return err + } + + realmReceived := false + + for k, rv := range kvs { + v := rv + + if k == "realm" { + h.Realm = v + realmReceived = true + } + } + + if !realmReceived { + return fmt.Errorf("realm is missing") + } + } else { // digest + kvs, err := keyValParse(v0, ',') + if err != nil { + return err + } + + realmReceived := false + nonceReceived := false + + for k, rv := range kvs { + v := rv + + switch k { + case "realm": + h.Realm = v + realmReceived = true + + case "nonce": + h.Nonce = v + nonceReceived = true + + case "opaque": + h.Opaque = &v + + case "stale": + h.Stale = &v + + case "algorithm": + a, err := parseAuthAlgorithm(v) + if err != nil { + return err + } + h.Algorithm = &a + } + } + + if !realmReceived || !nonceReceived { + return fmt.Errorf("one or more digest fields are missing") + } + } + + return nil +} + +// Marshal encodes a WWW-Authenticate header. +func (h Authenticate) Marshal() base.HeaderValue { + if h.Method == AuthMethodBasic { + return base.HeaderValue{"Basic " + + "realm=\"" + h.Realm + "\""} + } + + ret := "Digest realm=\"" + h.Realm + "\", nonce=\"" + h.Nonce + "\"" + + if h.Opaque != nil { + ret += ", opaque=\"" + *h.Opaque + "\"" + } + + if h.Stale != nil { + ret += ", stale=\"" + *h.Stale + "\"" + } + + if h.Algorithm != nil { + if *h.Algorithm == AuthAlgorithmMD5 { + ret += ", algorithm=\"MD5\"" + } else { + ret += ", algorithm=\"SHA-256\"" + } + } + + return base.HeaderValue{ret} +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/headers/authorization.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/headers/authorization.go new file mode 100644 index 000000000..866fdd392 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/headers/authorization.go @@ -0,0 +1,179 @@ +package headers + +import ( + "encoding/base64" + "fmt" + "strings" + + "github.com/bluenviron/gortsplib/v4/pkg/base" +) + +// Authorization is an Authorization header. +type Authorization struct { + // authentication method + Method AuthMethod + + // username + Username string + + // + // Basic authentication fields + // + + // user + // + // Deprecated: replaced by Username. + BasicUser string + + // password + BasicPass string + + // + // Digest authentication fields + // + + // realm + Realm string + + // nonce + Nonce string + + // URI + URI string + + // response + Response string + + // opaque + Opaque *string + + // algorithm + Algorithm *AuthAlgorithm +} + +// Unmarshal decodes an Authorization header. +func (h *Authorization) Unmarshal(v base.HeaderValue) error { + if len(v) == 0 { + return fmt.Errorf("value not provided") + } + + if len(v) > 1 { + return fmt.Errorf("value provided multiple times (%v)", v) + } + + v0 := v[0] + + i := strings.IndexByte(v0, ' ') + if i < 0 { + return fmt.Errorf("unable to split between method and keys (%v)", v0) + } + method, v0 := v0[:i], v0[i+1:] + + switch method { + case "Basic": + h.Method = AuthMethodBasic + + case "Digest": + h.Method = AuthMethodDigest + + default: + return fmt.Errorf("invalid method (%s)", method) + } + + if h.Method == AuthMethodBasic { + tmp, err := base64.StdEncoding.DecodeString(v0) + if err != nil { + return fmt.Errorf("invalid value") + } + + tmp2 := strings.Split(string(tmp), ":") + if len(tmp2) != 2 { + return fmt.Errorf("invalid value") + } + + h.Username, h.BasicPass = tmp2[0], tmp2[1] + h.BasicUser = h.Username + } else { // digest + kvs, err := keyValParse(v0, ',') + if err != nil { + return err + } + + realmReceived := false + usernameReceived := false + nonceReceived := false + uriReceived := false + responseReceived := false + + for k, rv := range kvs { + v := rv + + switch k { + case "realm": + h.Realm = v + realmReceived = true + + case "username": + h.Username = v + usernameReceived = true + + case "nonce": + h.Nonce = v + nonceReceived = true + + case "uri": + h.URI = v + uriReceived = true + + case "response": + h.Response = v + responseReceived = true + + case "opaque": + h.Opaque = &v + + case "algorithm": + a, err := parseAuthAlgorithm(v) + if err != nil { + return err + } + h.Algorithm = &a + } + } + + if !realmReceived || !usernameReceived || !nonceReceived || !uriReceived || !responseReceived { + return fmt.Errorf("one or more digest fields are missing") + } + } + + return nil +} + +// Marshal encodes an Authorization header. +func (h Authorization) Marshal() base.HeaderValue { + if h.Method == AuthMethodBasic { + if h.BasicUser != "" { + h.Username = h.BasicUser + } + return base.HeaderValue{"Basic " + + base64.StdEncoding.EncodeToString([]byte(h.Username+":"+h.BasicPass))} + } + + ret := "Digest " + + "username=\"" + h.Username + "\", realm=\"" + h.Realm + "\", " + + "nonce=\"" + h.Nonce + "\", uri=\"" + h.URI + "\", response=\"" + h.Response + "\"" + + if h.Opaque != nil { + ret += ", opaque=\"" + *h.Opaque + "\"" + } + + if h.Algorithm != nil { + if *h.Algorithm == AuthAlgorithmMD5 { + ret += ", algorithm=\"MD5\"" + } else { + ret += ", algorithm=\"SHA-256\"" + } + } + + return base.HeaderValue{ret} +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/headers/keyval.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/headers/keyval.go new file mode 100644 index 000000000..bd72e27a5 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/headers/keyval.go @@ -0,0 +1,77 @@ +package headers + +import ( + "fmt" +) + +func readKey(str string, separator byte) (string, string) { + i := 0 + for { + if i >= len(str) || str[i] == '=' || str[i] == separator { + break + } + + i++ + } + return str[:i], str[i:] +} + +func readValue(origstr string, str string, separator byte) (string, string, error) { + if len(str) > 0 && str[0] == '"' { + i := 1 + for { + if i >= len(str) { + return "", "", fmt.Errorf("apexes not closed (%v)", origstr) + } + + if str[i] == '"' { + return str[1:i], str[i+1:], nil + } + + i++ + } + } + + i := 0 + for { + if i >= len(str) || str[i] == separator { + return str[:i], str[i:], nil + } + i++ + } +} + +func keyValParse(str string, separator byte) (map[string]string, error) { + ret := make(map[string]string) + origstr := str + + for len(str) > 0 { + var k string + k, str = readKey(str, separator) + + if len(str) > 0 && str[0] == '=' { + var v string + var err error + v, str, err = readValue(origstr, str[1:], separator) + if err != nil { + return nil, err + } + + ret[k] = v + } else { + ret[k] = "" + } + + // skip separator + if len(str) > 0 && str[0] == separator { + str = str[1:] + } + + // skip spaces + for len(str) > 0 && str[0] == ' ' { + str = str[1:] + } + } + + return ret, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/headers/range.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/headers/range.go new file mode 100644 index 000000000..a6665eced --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/headers/range.go @@ -0,0 +1,354 @@ +package headers + +import ( + "fmt" + "strconv" + "strings" + "time" + + "github.com/bluenviron/gortsplib/v4/pkg/base" +) + +func leadingZero(v uint) string { + ret := "" + if v < 10 { + ret += "0" + } + ret += strconv.FormatUint(uint64(v), 10) + return ret +} + +// RangeSMPTETime is a time expressed in SMPTE unit. +type RangeSMPTETime struct { + Time time.Duration + Frame uint + Subframe uint +} + +func (t *RangeSMPTETime) unmarshal(s string) error { + parts := strings.Split(s, ":") + if len(parts) != 3 && len(parts) != 4 { + return fmt.Errorf("invalid SMPTE time (%v)", s) + } + + tmp, err := strconv.ParseUint(parts[0], 10, 64) + if err != nil { + return err + } + hours := tmp + + tmp, err = strconv.ParseUint(parts[1], 10, 64) + if err != nil { + return err + } + mins := tmp + + tmp, err = strconv.ParseUint(parts[2], 10, 64) + if err != nil { + return err + } + seconds := tmp + + t.Time = time.Duration(seconds+mins*60+hours*3600) * time.Second + + if len(parts) == 4 { + parts = strings.Split(parts[3], ".") + if len(parts) == 2 { + tmp, err := strconv.ParseUint(parts[0], 10, 64) + if err != nil { + return err + } + t.Frame = uint(tmp) + + tmp, err = strconv.ParseUint(parts[1], 10, 64) + if err != nil { + return err + } + t.Subframe = uint(tmp) + } else { + tmp, err := strconv.ParseUint(parts[0], 10, 64) + if err != nil { + return err + } + t.Frame = uint(tmp) + } + } + + return nil +} + +func (t RangeSMPTETime) marshal() string { + d := uint64(t.Time.Seconds()) + hours := d / 3600 + d %= 3600 + mins := d / 60 + secs := d % 60 + + ret := strconv.FormatUint(hours, 10) + ":" + leadingZero(uint(mins)) + ":" + leadingZero(uint(secs)) + + if t.Frame > 0 || t.Subframe > 0 { + ret += ":" + leadingZero(t.Frame) + + if t.Subframe > 0 { + ret += "." + leadingZero(t.Subframe) + } + } + + return ret +} + +// RangeSMPTE is a range expressed in SMPTE unit. +type RangeSMPTE struct { + Start RangeSMPTETime + End *RangeSMPTETime +} + +func (r *RangeSMPTE) unmarshal(start string, end string) error { + err := r.Start.unmarshal(start) + if err != nil { + return err + } + + if end != "" { + var v RangeSMPTETime + err := v.unmarshal(end) + if err != nil { + return err + } + r.End = &v + } + + return nil +} + +func (r RangeSMPTE) marshal() string { + ret := "smpte=" + r.Start.marshal() + "-" + if r.End != nil { + ret += r.End.marshal() + } + return ret +} + +func unmarshalRangeNPTTime(d *time.Duration, s string) error { + parts := strings.Split(s, ":") + if len(parts) > 3 { + return fmt.Errorf("invalid NPT time (%v)", s) + } + + var hours uint64 + if len(parts) == 3 { + tmp, err := strconv.ParseUint(parts[0], 10, 64) + if err != nil { + return err + } + hours = tmp + parts = parts[1:] + } + + var mins uint64 + if len(parts) >= 2 { + tmp, err := strconv.ParseUint(parts[0], 10, 64) + if err != nil { + return err + } + mins = tmp + parts = parts[1:] + } + + tmp, err := strconv.ParseFloat(parts[0], 64) + if err != nil { + return err + } + seconds := tmp + + *d = time.Duration(seconds*float64(time.Second)) + + time.Duration(mins*60+hours*3600)*time.Second + + return nil +} + +func marshalRangeNPTTime(d time.Duration) string { + return strconv.FormatFloat(d.Seconds(), 'f', -1, 64) +} + +// RangeNPT is a range expressed in NPT units. +type RangeNPT struct { + Start time.Duration + End *time.Duration +} + +func (r *RangeNPT) unmarshal(start string, end string) error { + err := unmarshalRangeNPTTime(&r.Start, start) + if err != nil { + return err + } + + if end != "" { + var v time.Duration + err := unmarshalRangeNPTTime(&v, end) + if err != nil { + return err + } + r.End = &v + } + + return nil +} + +func (r RangeNPT) marshal() string { + ret := "npt=" + marshalRangeNPTTime(r.Start) + "-" + if r.End != nil { + ret += marshalRangeNPTTime(*r.End) + } + return ret +} + +func unmarshalRangeUTCTime(t *time.Time, s string) error { + tmp, err := time.Parse("20060102T150405Z", s) + if err != nil { + return err + } + *t = tmp + return nil +} + +func marshalRangeUTCTime(t time.Time) string { + return t.Format("20060102T150405Z") +} + +// RangeUTC is a range expressed in UTC units. +type RangeUTC struct { + Start time.Time + End *time.Time +} + +func (r *RangeUTC) unmarshal(start string, end string) error { + err := unmarshalRangeUTCTime(&r.Start, start) + if err != nil { + return err + } + + if end != "" { + var v time.Time + err := unmarshalRangeUTCTime(&v, end) + if err != nil { + return err + } + r.End = &v + } + + return nil +} + +func (r RangeUTC) marshal() string { + ret := "clock=" + marshalRangeUTCTime(r.Start) + "-" + if r.End != nil { + ret += marshalRangeUTCTime(*r.End) + } + return ret +} + +// RangeValue can be +// - RangeSMPTE +// - RangeNPT +// - RangeUTC +type RangeValue interface { + unmarshal(string, string) error + marshal() string +} + +func rangeValueUnmarshal(s RangeValue, v string) error { + parts := strings.Split(v, "-") + if len(parts) != 2 { + return fmt.Errorf("invalid value (%v)", v) + } + + return s.unmarshal(parts[0], parts[1]) +} + +// Range is a Range header. +type Range struct { + // range expressed in a certain unit. + Value RangeValue + + // time at which the operation is to be made effective. + Time *time.Time +} + +// Unmarshal decodes a Range header. +func (h *Range) Unmarshal(v base.HeaderValue) error { + if len(v) == 0 { + return fmt.Errorf("value not provided") + } + + if len(v) > 1 { + return fmt.Errorf("value provided multiple times (%v)", v) + } + + v0 := v[0] + + kvs, err := keyValParse(v0, ';') + if err != nil { + return err + } + + specFound := false + + for k, v := range kvs { + switch k { + case "smpte": + s := &RangeSMPTE{} + err := rangeValueUnmarshal(s, v) + if err != nil { + return err + } + + specFound = true + h.Value = s + + case "npt": + s := &RangeNPT{} + err := rangeValueUnmarshal(s, v) + if err != nil { + return err + } + + specFound = true + h.Value = s + + case "clock": + s := &RangeUTC{} + err := rangeValueUnmarshal(s, v) + if err != nil { + return err + } + + specFound = true + h.Value = s + + case "time": + var t time.Time + err := unmarshalRangeUTCTime(&t, v) + if err != nil { + return err + } + + h.Time = &t + } + } + + if !specFound { + return fmt.Errorf("value not found (%v)", v[0]) + } + + return nil +} + +// Marshal encodes a Range header. +func (h Range) Marshal() base.HeaderValue { + v := h.Value.marshal() + if h.Time != nil { + v += ";time=" + marshalRangeUTCTime(*h.Time) + } + return base.HeaderValue{v} +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/headers/rtpinfo.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/headers/rtpinfo.go new file mode 100644 index 000000000..a87604a77 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/headers/rtpinfo.go @@ -0,0 +1,101 @@ +package headers + +import ( + "fmt" + "strconv" + "strings" + + "github.com/bluenviron/gortsplib/v4/pkg/base" +) + +// RTPInfoEntry is an entry of a RTP-Info header. +type RTPInfoEntry struct { + URL string + SequenceNumber *uint16 + Timestamp *uint32 +} + +// RTPInfo is a RTP-Info header. +type RTPInfo []*RTPInfoEntry + +// Unmarshal decodes a RTP-Info header. +func (h *RTPInfo) Unmarshal(v base.HeaderValue) error { + if len(v) == 0 { + return fmt.Errorf("value not provided") + } + + if len(v) > 1 { + return fmt.Errorf("value provided multiple times (%v)", v) + } + + for _, part := range strings.Split(v[0], ",") { + e := &RTPInfoEntry{} + + // remove leading spaces + part = strings.TrimLeft(part, " ") + + kvs, err := keyValParse(part, ';') + if err != nil { + return err + } + + urlReceived := false + + for k, v := range kvs { + switch k { + case "url": + e.URL = v + urlReceived = true + + case "seq": + vi, err := strconv.ParseUint(v, 10, 16) + if err != nil { + return err + } + vi2 := uint16(vi) + e.SequenceNumber = &vi2 + + case "rtptime": + vi, err := strconv.ParseUint(v, 10, 32) + if err != nil { + return err + } + vi2 := uint32(vi) + e.Timestamp = &vi2 + + default: + // ignore non-standard keys + } + } + + if !urlReceived { + return fmt.Errorf("URL is missing") + } + + *h = append(*h, e) + } + + return nil +} + +// Marshal encodes a RTP-Info header. +func (h RTPInfo) Marshal() base.HeaderValue { + rets := make([]string, len(h)) + + for i, e := range h { + var tmp []string + tmp = append(tmp, "url="+e.URL) + + if e.SequenceNumber != nil { + tmp = append(tmp, "seq="+strconv.FormatUint(uint64(*e.SequenceNumber), 10)) + } + + if e.Timestamp != nil { + tmp = append(tmp, "rtptime="+strconv.FormatUint(uint64(*e.Timestamp), 10)) + } + + rets[i] = strings.Join(tmp, ";") + } + + return base.HeaderValue{strings.Join(rets, ",")} +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/headers/session.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/headers/session.go new file mode 100644 index 000000000..683b3c198 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/headers/session.go @@ -0,0 +1,71 @@ +package headers + +import ( + "fmt" + "strconv" + "strings" + + "github.com/bluenviron/gortsplib/v4/pkg/base" +) + +// Session is a Session header. +type Session struct { + // session id + Session string + + // (optional) a timeout + Timeout *uint +} + +// Unmarshal decodes a Session header. +func (h *Session) Unmarshal(v base.HeaderValue) error { + if len(v) == 0 { + return fmt.Errorf("value not provided") + } + + if len(v) > 1 { + return fmt.Errorf("value provided multiple times (%v)", v) + } + + v0 := v[0] + + i := strings.IndexByte(v0, ';') + if i < 0 { + h.Session = v0 + return nil + } + + h.Session = v0[:i] + v0 = v0[i+1:] + + v0 = strings.TrimLeft(v0, " ") + + kvs, err := keyValParse(v0, ';') + if err != nil { + return err + } + + for k, v := range kvs { + if k == "timeout" { + iv, err := strconv.ParseUint(v, 10, 32) + if err != nil { + return err + } + uiv := uint(iv) + h.Timeout = &uiv + } + } + + return nil +} + +// Marshal encodes a Session header. +func (h Session) Marshal() base.HeaderValue { + ret := h.Session + + if h.Timeout != nil { + ret += ";timeout=" + strconv.FormatUint(uint64(*h.Timeout), 10) + } + + return base.HeaderValue{ret} +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/headers/transport.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/headers/transport.go new file mode 100644 index 000000000..e53d2fe56 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/headers/transport.go @@ -0,0 +1,379 @@ +package headers + +import ( + "encoding/hex" + "fmt" + "net" + "strconv" + "strings" + + "github.com/bluenviron/gortsplib/v4/pkg/base" +) + +func parsePorts(val string) (*[2]int, error) { + ports := strings.Split(val, "-") + if len(ports) == 2 { + port1, err := strconv.ParseUint(ports[0], 10, 31) + if err != nil { + return &[2]int{0, 0}, fmt.Errorf("invalid ports (%v)", val) + } + + port2, err := strconv.ParseUint(ports[1], 10, 31) + if err != nil { + return &[2]int{0, 0}, fmt.Errorf("invalid ports (%v)", val) + } + + return &[2]int{int(port1), int(port2)}, nil + } + + if len(ports) == 1 { + port1, err := strconv.ParseUint(ports[0], 10, 31) + if err != nil { + return &[2]int{0, 0}, fmt.Errorf("invalid ports (%v)", val) + } + + return &[2]int{int(port1), int(port1 + 1)}, nil + } + + return &[2]int{0, 0}, fmt.Errorf("invalid ports (%v)", val) +} + +// TransportProtocol is a transport protocol. +type TransportProtocol int + +// transport protocols. +const ( + TransportProtocolUDP TransportProtocol = iota + TransportProtocolTCP +) + +// String implements fmt.Stringer. +func (p TransportProtocol) String() string { + if p == TransportProtocolUDP { + return "RTP/AVP" + } + return "RTP/AVP/TCP" +} + +// TransportDelivery is a delivery method. +type TransportDelivery int + +// transport delivery methods. +const ( + TransportDeliveryUnicast TransportDelivery = iota + TransportDeliveryMulticast +) + +// String implements fmt.Stringer. +func (d TransportDelivery) String() string { + if d == TransportDeliveryUnicast { + return "unicast" + } + return "multicast" +} + +// TransportMode is a transport mode. +type TransportMode int + +const ( + // TransportModePlay is the "play" transport mode + TransportModePlay TransportMode = iota + + // TransportModeRecord is the "record" transport mode + TransportModeRecord +) + +func (m *TransportMode) unmarshal(v string) error { + str := strings.ToLower(v) + + switch str { + case "play": + *m = TransportModePlay + return nil + + // receive is an old alias for record, used by ffmpeg with the + // -listen flag, and by Darwin Streaming Server + case "record", "receive": + *m = TransportModeRecord + return nil + + default: + return fmt.Errorf("invalid transport mode: '%s'", str) + } +} + +// String implements fmt.Stringer. +func (m TransportMode) String() string { + if m == TransportModePlay { + return "play" + } + return "record" +} + +// Transport is a Transport header. +type Transport struct { + // protocol of the stream + Protocol TransportProtocol + + // (optional) delivery method of the stream + Delivery *TransportDelivery + + // (optional) Source IP + Source *net.IP + + // (optional) destination IP + Destination *net.IP + + // (optional) interleaved frame ids + InterleavedIDs *[2]int + + // (optional) TTL + TTL *uint + + // (optional) ports + Ports *[2]int + + // (optional) client ports + ClientPorts *[2]int + + // (optional) server ports + ServerPorts *[2]int + + // (optional) SSRC of the packets of the stream + SSRC *uint32 + + // (optional) mode + Mode *TransportMode +} + +// Unmarshal decodes a Transport header. +func (h *Transport) Unmarshal(v base.HeaderValue) error { + if len(v) == 0 { + return fmt.Errorf("value not provided") + } + + if len(v) > 1 { + return fmt.Errorf("value provided multiple times (%v)", v) + } + + v0 := v[0] + + kvs, err := keyValParse(v0, ';') + if err != nil { + return err + } + + protocolFound := false + + for k, rv := range kvs { + v := rv + + switch k { + case "RTP/AVP", "RTP/AVP/UDP": + h.Protocol = TransportProtocolUDP + protocolFound = true + + case "RTP/AVP/TCP": + h.Protocol = TransportProtocolTCP + protocolFound = true + + case "unicast": + v := TransportDeliveryUnicast + h.Delivery = &v + + case "multicast": + v := TransportDeliveryMulticast + h.Delivery = &v + + case "source": + if v != "" { + ip := net.ParseIP(v) + if ip == nil { + addrs, err2 := net.LookupHost(v) + if err2 != nil { + return fmt.Errorf("invalid source (%v)", v) + } + ip = net.ParseIP(addrs[0]) + if ip == nil { + return fmt.Errorf("invalid source (%v)", v) + } + } + h.Source = &ip + } + + case "destination": + if v != "" { + ip := net.ParseIP(v) + if ip == nil { + return fmt.Errorf("invalid destination (%v)", v) + } + h.Destination = &ip + } + + case "interleaved": + ports, err2 := parsePorts(v) + if err2 != nil { + return err2 + } + h.InterleavedIDs = ports + + case "ttl": + tmp, err2 := strconv.ParseUint(v, 10, 32) + if err2 != nil { + return err2 + } + vu := uint(tmp) + h.TTL = &vu + + case "port": + ports, err2 := parsePorts(v) + if err2 != nil { + return err2 + } + h.Ports = ports + + case "client_port": + ports, err2 := parsePorts(v) + if err2 != nil { + return err2 + } + h.ClientPorts = ports + + case "server_port": + ports, err2 := parsePorts(v) + if err2 != nil { + return err2 + } + h.ServerPorts = ports + + case "ssrc": + v = strings.TrimLeft(v, " ") + + if (len(v) % 2) != 0 { + v = "0" + v + } + + if tmp, err2 := hex.DecodeString(v); err2 == nil && len(tmp) <= 4 { + var ssrc [4]byte + copy(ssrc[4-len(tmp):], tmp) + v := uint32(ssrc[0])<<24 | uint32(ssrc[1])<<16 | uint32(ssrc[2])<<8 | uint32(ssrc[3]) + h.SSRC = &v + } + + case "mode": + var m TransportMode + err = m.unmarshal(v) + if err != nil { + return err + } + h.Mode = &m + + default: + // ignore non-standard keys + } + } + + if !protocolFound { + return fmt.Errorf("protocol not found (%v)", v[0]) + } + + return nil +} + +// Marshal encodes a Transport header. +func (h Transport) Marshal() base.HeaderValue { + var rets []string + + rets = append(rets, h.Protocol.String()) + + if h.Delivery != nil { + rets = append(rets, h.Delivery.String()) + } + + if h.Source != nil { + rets = append(rets, "source="+h.Source.String()) + } + + if h.Destination != nil { + rets = append(rets, "destination="+h.Destination.String()) + } + + if h.InterleavedIDs != nil { + rets = append(rets, "interleaved="+strconv.FormatInt(int64(h.InterleavedIDs[0]), 10)+ + "-"+strconv.FormatInt(int64(h.InterleavedIDs[1]), 10)) + } + + if h.Ports != nil { + rets = append(rets, "port="+strconv.FormatInt(int64(h.Ports[0]), 10)+ + "-"+strconv.FormatInt(int64(h.Ports[1]), 10)) + } + + if h.TTL != nil { + rets = append(rets, "ttl="+strconv.FormatUint(uint64(*h.TTL), 10)) + } + + if h.ClientPorts != nil { + rets = append(rets, "client_port="+strconv.FormatInt(int64(h.ClientPorts[0]), 10)+ + "-"+strconv.FormatInt(int64(h.ClientPorts[1]), 10)) + } + + if h.ServerPorts != nil { + rets = append(rets, "server_port="+strconv.FormatInt(int64(h.ServerPorts[0]), 10)+ + "-"+strconv.FormatInt(int64(h.ServerPorts[1]), 10)) + } + + if h.SSRC != nil { + tmp := make([]byte, 4) + tmp[0] = byte(*h.SSRC >> 24) + tmp[1] = byte(*h.SSRC >> 16) + tmp[2] = byte(*h.SSRC >> 8) + tmp[3] = byte(*h.SSRC) + rets = append(rets, "ssrc="+strings.ToUpper(hex.EncodeToString(tmp))) + } + + if h.Mode != nil { + rets = append(rets, "mode="+h.Mode.String()) + } + + return base.HeaderValue{strings.Join(rets, ";")} +} + +// Transports is a Transport header with multiple transports. +type Transports []Transport + +// Unmarshal decodes a Transport header. +func (ts *Transports) Unmarshal(v base.HeaderValue) error { + if len(v) == 0 { + return fmt.Errorf("value not provided") + } + + if len(v) > 1 { + return fmt.Errorf("value provided multiple times (%v)", v) + } + + v0 := v[0] + transports := strings.Split(v0, ",") // , separated per RFC2326 section 12.39 + *ts = make([]Transport, len(transports)) + + for i, transport := range transports { + var tr Transport + err := tr.Unmarshal(base.HeaderValue{strings.TrimLeft(transport, " ")}) + if err != nil { + return err + } + (*ts)[i] = tr + } + + return nil +} + +// Marshal encodes a Transport header. +func (ts Transports) Marshal() base.HeaderValue { + vals := make([]string, len(ts)) + + for i, th := range ts { + vals[i] = th.Marshal()[0] + } + + return base.HeaderValue{strings.Join(vals, ",")} +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/liberrors/client.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/liberrors/client.go new file mode 100644 index 000000000..418057222 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/liberrors/client.go @@ -0,0 +1,343 @@ +package liberrors + +import ( + "fmt" + + "github.com/bluenviron/gortsplib/v4/pkg/base" +) + +// ErrClientTerminated is an error that can be returned by a client. +type ErrClientTerminated struct{} + +// Error implements the error interface. +func (e ErrClientTerminated) Error() string { + return "terminated" +} + +// ErrClientInvalidState is an error that can be returned by a client. +type ErrClientInvalidState struct { + AllowedList []fmt.Stringer + State fmt.Stringer +} + +// Error implements the error interface. +func (e ErrClientInvalidState) Error() string { + return fmt.Sprintf("must be in state %v, while is in state %v", + e.AllowedList, e.State) +} + +// ErrClientSessionHeaderInvalid is an error that can be returned by a client. +type ErrClientSessionHeaderInvalid struct { + Err error +} + +// Error implements the error interface. +func (e ErrClientSessionHeaderInvalid) Error() string { + return fmt.Sprintf("invalid session header: %v", e.Err) +} + +// ErrClientBadStatusCode is an error that can be returned by a client. +type ErrClientBadStatusCode struct { + Code base.StatusCode + Message string +} + +// Error implements the error interface. +func (e ErrClientBadStatusCode) Error() string { + return fmt.Sprintf("bad status code: %d (%s)", e.Code, e.Message) +} + +// ErrClientContentTypeMissing is an error that can be returned by a client. +type ErrClientContentTypeMissing struct{} + +// Error implements the error interface. +func (e ErrClientContentTypeMissing) Error() string { + return "Content-Type header is missing" +} + +// ErrClientContentTypeUnsupported is an error that can be returned by a client. +type ErrClientContentTypeUnsupported struct { + CT base.HeaderValue +} + +// Error implements the error interface. +func (e ErrClientContentTypeUnsupported) Error() string { + return fmt.Sprintf("unsupported Content-Type header '%v'", e.CT) +} + +// ErrClientCannotSetupMediasDifferentURLs is an error that can be returned by a client. +type ErrClientCannotSetupMediasDifferentURLs struct{} + +// Error implements the error interface. +func (e ErrClientCannotSetupMediasDifferentURLs) Error() string { + return "cannot setup medias with different base URLs" +} + +// ErrClientUDPPortsZero is an error that can be returned by a client. +type ErrClientUDPPortsZero struct{} + +// Error implements the error interface. +func (e ErrClientUDPPortsZero) Error() string { + return "rtpPort and rtcpPort must be both zero or non-zero" +} + +// ErrClientUDPPortsNotConsecutive is an error that can be returned by a client. +type ErrClientUDPPortsNotConsecutive struct{} + +// Error implements the error interface. +func (e ErrClientUDPPortsNotConsecutive) Error() string { + return "rtcpPort must be rtpPort + 1" +} + +// ErrClientServerPortsNotProvided is an error that can be returned by a client. +type ErrClientServerPortsNotProvided struct{} + +// Error implements the error interface. +func (e ErrClientServerPortsNotProvided) Error() string { + return "server ports have not been provided. Use AnyPortEnable to communicate with this server" +} + +// ErrClientTransportHeaderInvalid is an error that can be returned by a client. +type ErrClientTransportHeaderInvalid struct { + Err error +} + +// Error implements the error interface. +func (e ErrClientTransportHeaderInvalid) Error() string { + return fmt.Sprintf("invalid transport header: %v", e.Err) +} + +// ErrClientServerRequestedTCP is an error that can be returned by a client. +type ErrClientServerRequestedTCP struct{} + +// Error implements the error interface. +func (e ErrClientServerRequestedTCP) Error() string { + return "server wants to use the TCP transport protocol" +} + +// ErrClientServerRequestedUDP is an error that can be returned by a client. +type ErrClientServerRequestedUDP struct{} + +// Error implements the error interface. +func (e ErrClientServerRequestedUDP) Error() string { + return "server wants to use the UDP transport protocol" +} + +// ErrClientTransportHeaderInvalidDelivery is an error that can be returned by a client. +type ErrClientTransportHeaderInvalidDelivery struct{} + +// Error implements the error interface. +func (e ErrClientTransportHeaderInvalidDelivery) Error() string { + return "transport header contains an invalid delivery value" +} + +// ErrClientTransportHeaderNoPorts is an error that can be returned by a client. +type ErrClientTransportHeaderNoPorts struct{} + +// Error implements the error interface. +func (e ErrClientTransportHeaderNoPorts) Error() string { + return "transport header does not contain ports" +} + +// ErrClientTransportHeaderNoDestination is an error that can be returned by a client. +type ErrClientTransportHeaderNoDestination struct{} + +// Error implements the error interface. +func (e ErrClientTransportHeaderNoDestination) Error() string { + return "transport header does not contain a destination" +} + +// ErrClientTransportHeaderNoInterleavedIDs is an error that can be returned by a client. +type ErrClientTransportHeaderNoInterleavedIDs struct{} + +// Error implements the error interface. +func (e ErrClientTransportHeaderNoInterleavedIDs) Error() string { + return "transport header does not contain interleaved IDs" +} + +// ErrClientTransportHeaderInvalidInterleavedIDs is an error that can be returned by a client. +type ErrClientTransportHeaderInvalidInterleavedIDs struct{} + +// Error implements the error interface. +func (e ErrClientTransportHeaderInvalidInterleavedIDs) Error() string { + return "invalid interleaved IDs" +} + +// ErrClientTransportHeaderInterleavedIDsInUse is an error that can be returned by a client. +type ErrClientTransportHeaderInterleavedIDsInUse struct{} + +// Error implements the error interface. +func (e ErrClientTransportHeaderInterleavedIDsInUse) Error() string { + return "interleaved IDs are in use" +} + +// ErrClientUDPTimeout is an error that can be returned by a client. +type ErrClientUDPTimeout struct{} + +// Error implements the error interface. +func (e ErrClientUDPTimeout) Error() string { + return "UDP timeout" +} + +// ErrClientTCPTimeout is an error that can be returned by a client. +type ErrClientTCPTimeout struct{} + +// Error implements the error interface. +func (e ErrClientTCPTimeout) Error() string { + return "TCP timeout" +} + +// ErrClientRTPInfoInvalid is an error that can be returned by a client. +type ErrClientRTPInfoInvalid struct { + Err error +} + +// Error implements the error interface. +func (e ErrClientRTPInfoInvalid) Error() string { + return fmt.Sprintf("invalid RTP-Info: %v", e.Err) +} + +// ErrClientUnexpectedFrame is an error that can be returned by a client. +type ErrClientUnexpectedFrame struct{} + +// Error implements the error interface. +func (e ErrClientUnexpectedFrame) Error() string { + return "received unexpected interleaved frame" +} + +// ErrClientRequestTimedOut is an error that can be returned by a client. +type ErrClientRequestTimedOut struct{} + +// Error implements the error interface. +func (e ErrClientRequestTimedOut) Error() string { + return "request timed out" +} + +// ErrClientUnsupportedScheme is an error that can be returned by a client. +type ErrClientUnsupportedScheme struct { + Scheme string +} + +// Error implements the error interface. +func (e ErrClientUnsupportedScheme) Error() string { + return fmt.Sprintf("unsupported scheme: %v", e.Scheme) +} + +// ErrClientRTSPSTCP is an error that can be returned by a client. +type ErrClientRTSPSTCP struct{} + +// Error implements the error interface. +func (e ErrClientRTSPSTCP) Error() string { + return "RTSPS can be used only with TCP" +} + +// ErrClientUnhandledMethod is an error that can be returned by a client. +type ErrClientUnhandledMethod struct { + Method base.Method +} + +// Error implements the error interface. +func (e ErrClientUnhandledMethod) Error() string { + return fmt.Sprintf("unhandled method: %v", e.Method) +} + +// ErrClientWriteQueueFull is an error that can be returned by a client. +type ErrClientWriteQueueFull struct{} + +// Error implements the error interface. +func (e ErrClientWriteQueueFull) Error() string { + return "write queue is full" +} + +// ErrClientRTPPacketsLost is an error that can be returned by a client. +// +// Deprecated: will be removed in next version. +type ErrClientRTPPacketsLost struct { + Lost uint +} + +// Error implements the error interface. +func (e ErrClientRTPPacketsLost) Error() string { + return fmt.Sprintf("%d RTP %s lost", + e.Lost, + func() string { + if e.Lost == 1 { + return "packet" + } + return "packets" + }()) +} + +// ErrClientRTPPacketUnknownPayloadType is an error that can be returned by a client. +type ErrClientRTPPacketUnknownPayloadType struct { + PayloadType uint8 +} + +// Error implements the error interface. +func (e ErrClientRTPPacketUnknownPayloadType) Error() string { + return fmt.Sprintf("received RTP packet with unknown payload type: %d", e.PayloadType) +} + +// ErrClientRTCPPacketTooBig is an error that can be returned by a client. +type ErrClientRTCPPacketTooBig struct { + L int + Max int +} + +// Error implements the error interface. +func (e ErrClientRTCPPacketTooBig) Error() string { + return fmt.Sprintf("RTCP packet size (%d) is greater than maximum allowed (%d)", + e.L, e.Max) +} + +// ErrClientRTPPacketTooBigUDP is an error that can be returned by a client. +type ErrClientRTPPacketTooBigUDP struct{} + +// Error implements the error interface. +func (e ErrClientRTPPacketTooBigUDP) Error() string { + return "RTP packet is too big to be read with UDP" +} + +// ErrClientRTCPPacketTooBigUDP is an error that can be returned by a client. +type ErrClientRTCPPacketTooBigUDP struct{} + +// Error implements the error interface. +func (e ErrClientRTCPPacketTooBigUDP) Error() string { + return "RTCP packet is too big to be read with UDP" +} + +// ErrClientSwitchToTCP is an error that can be returned by a client. +type ErrClientSwitchToTCP struct{} + +// Error implements the error interface. +func (e ErrClientSwitchToTCP) Error() string { + return "no UDP packets received, switching to TCP" +} + +// ErrClientSwitchToTCP2 is an error that can be returned by a client. +type ErrClientSwitchToTCP2 struct{} + +// Error implements the error interface. +func (e ErrClientSwitchToTCP2) Error() string { + return "switching to TCP because server requested it" +} + +// ErrClientAuthSetup is an error that can be returned by a client. +type ErrClientAuthSetup struct { + Err error +} + +// Error implements the error interface. +func (e ErrClientAuthSetup) Error() string { + return fmt.Sprintf("unable to setup authentication: %s", e.Err) +} + +// ErrClientSDPInvalid is an error that can be returned by a client. +type ErrClientSDPInvalid struct { + Err error +} + +// Error implements the error interface. +func (e ErrClientSDPInvalid) Error() string { + return fmt.Sprintf("invalid SDP: %v", e.Err) +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/liberrors/liberrors.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/liberrors/liberrors.go new file mode 100644 index 000000000..60cc731f2 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/liberrors/liberrors.go @@ -0,0 +1,2 @@ +// Package liberrors contains errors returned by the library. +package liberrors diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/liberrors/server.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/liberrors/server.go new file mode 100644 index 000000000..932f58d35 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/liberrors/server.go @@ -0,0 +1,284 @@ +package liberrors + +import ( + "fmt" + "net" + + "github.com/bluenviron/gortsplib/v4/pkg/headers" +) + +// ErrServerTerminated is an error that can be returned by a server. +type ErrServerTerminated = ErrClientTerminated + +// ErrServerSessionNotFound is an error that can be returned by a server. +type ErrServerSessionNotFound struct{} + +// Error implements the error interface. +func (e ErrServerSessionNotFound) Error() string { + return "session not found" +} + +// ErrServerSessionTimedOut is an error that can be returned by a server. +type ErrServerSessionTimedOut struct{} + +// Error implements the error interface. +func (e ErrServerSessionTimedOut) Error() string { + return "session timed out" +} + +// ErrServerCSeqMissing is an error that can be returned by a server. +type ErrServerCSeqMissing struct{} + +// Error implements the error interface. +func (e ErrServerCSeqMissing) Error() string { + return "CSeq is missing" +} + +// ErrServerInvalidState is an error that can be returned by a server. +type ErrServerInvalidState struct { + AllowedList []fmt.Stringer + State fmt.Stringer +} + +// Error implements the error interface. +func (e ErrServerInvalidState) Error() string { + return fmt.Sprintf("must be in state %v, while is in state %v", + e.AllowedList, e.State) +} + +// ErrServerInvalidPath is an error that can be returned by a server. +type ErrServerInvalidPath struct{} + +// Error implements the error interface. +func (e ErrServerInvalidPath) Error() string { + return "invalid path" +} + +// ErrServerContentTypeMissing is an error that can be returned by a server. +type ErrServerContentTypeMissing = ErrClientContentTypeMissing + +// ErrServerContentTypeUnsupported is an error that can be returned by a server. +type ErrServerContentTypeUnsupported = ErrClientContentTypeUnsupported + +// ErrServerSDPInvalid is an error that can be returned by a server. +type ErrServerSDPInvalid = ErrClientSDPInvalid + +// ErrServerTransportHeaderInvalid is an error that can be returned by a server. +type ErrServerTransportHeaderInvalid = ErrClientTransportHeaderInvalid + +// ErrServerMediaAlreadySetup is an error that can be returned by a server. +type ErrServerMediaAlreadySetup struct{} + +// Error implements the error interface. +func (e ErrServerMediaAlreadySetup) Error() string { + return "media has already been setup" +} + +// ErrServerMediaNotFound is an error that can be returned by a server. +type ErrServerMediaNotFound struct{} + +// Error implements the error interface. +func (e ErrServerMediaNotFound) Error() string { + return "media not found" +} + +// ErrServerTransportHeaderInvalidMode is an error that can be returned by a server. +type ErrServerTransportHeaderInvalidMode struct { + Mode *headers.TransportMode +} + +// Error implements the error interface. +func (e ErrServerTransportHeaderInvalidMode) Error() string { + m := "null" + if e.Mode != nil { + m = e.Mode.String() + } + return fmt.Sprintf("transport header contains a invalid mode (%v)", m) +} + +// ErrServerTransportHeaderNoClientPorts is an error that can be returned by a server. +type ErrServerTransportHeaderNoClientPorts struct{} + +// Error implements the error interface. +func (e ErrServerTransportHeaderNoClientPorts) Error() string { + return "transport header does not contain client ports" +} + +// ErrServerTransportHeaderInvalidInterleavedIDs is an error that can be returned by a server. +type ErrServerTransportHeaderInvalidInterleavedIDs struct{} + +// Error implements the error interface. +func (e ErrServerTransportHeaderInvalidInterleavedIDs) Error() string { + return "invalid interleaved IDs" +} + +// ErrServerTransportHeaderInterleavedIDsInUse is an error that can be returned by a server. +type ErrServerTransportHeaderInterleavedIDsInUse struct{} + +// Error implements the error interface. +func (e ErrServerTransportHeaderInterleavedIDsInUse) Error() string { + return "interleaved IDs are in use" +} + +// ErrServerMediasDifferentPaths is an error that can be returned by a server. +type ErrServerMediasDifferentPaths struct{} + +// Error implements the error interface. +func (e ErrServerMediasDifferentPaths) Error() string { + return "can't setup medias with different paths" +} + +// ErrServerMediasDifferentProtocols is an error that can be returned by a server. +type ErrServerMediasDifferentProtocols struct{} + +// Error implements the error interface. +func (e ErrServerMediasDifferentProtocols) Error() string { + return "can't setup medias with different protocols" +} + +// ErrServerNoMediasSetup is an error that can be returned by a server. +type ErrServerNoMediasSetup struct{} + +// Error implements the error interface. +func (e ErrServerNoMediasSetup) Error() string { + return "no medias have been setup" +} + +// ErrServerNotAllAnnouncedMediasSetup is an error that can be returned by a server. +type ErrServerNotAllAnnouncedMediasSetup struct{} + +// Error implements the error interface. +func (e ErrServerNotAllAnnouncedMediasSetup) Error() string { + return "not all announced medias have been setup" +} + +// ErrServerLinkedToOtherSession is an error that can be returned by a server. +type ErrServerLinkedToOtherSession struct{} + +// Error implements the error interface. +func (e ErrServerLinkedToOtherSession) Error() string { + return "connection is linked to another session" +} + +// ErrServerSessionTornDown is an error that can be returned by a server. +type ErrServerSessionTornDown struct { + Author net.Addr +} + +// Error implements the error interface. +func (e ErrServerSessionTornDown) Error() string { + return fmt.Sprintf("torn down by %v", e.Author) +} + +// ErrServerSessionLinkedToOtherConn is an error that can be returned by a server. +type ErrServerSessionLinkedToOtherConn struct{} + +// Error implements the error interface. +func (e ErrServerSessionLinkedToOtherConn) Error() string { + return "session is linked to another connection" +} + +// ErrServerInvalidSession is an error that can be returned by a server. +type ErrServerInvalidSession struct{} + +// Error implements the error interface. +func (e ErrServerInvalidSession) Error() string { + return "invalid session" +} + +// ErrServerPathHasChanged is an error that can be returned by a server. +type ErrServerPathHasChanged struct { + Prev string + Cur string +} + +// Error implements the error interface. +func (e ErrServerPathHasChanged) Error() string { + return fmt.Sprintf("path has changed, was '%s', now is '%s'", e.Prev, e.Cur) +} + +// ErrServerCannotUseSessionCreatedByOtherIP is an error that can be returned by a server. +type ErrServerCannotUseSessionCreatedByOtherIP struct{} + +// Error implements the error interface. +func (e ErrServerCannotUseSessionCreatedByOtherIP) Error() string { + return "cannot use a session created with a different IP" +} + +// ErrServerUDPPortsAlreadyInUse is an error that can be returned by a server. +type ErrServerUDPPortsAlreadyInUse struct { + Port int +} + +// Error implements the error interface. +func (e ErrServerUDPPortsAlreadyInUse) Error() string { + return fmt.Sprintf("UDP ports %d and %d are already assigned to another reader with the same IP", + e.Port, e.Port+1) +} + +// ErrServerSessionNotInUse is an error that can be returned by a server. +type ErrServerSessionNotInUse struct{} + +// Error implements the error interface. +func (e ErrServerSessionNotInUse) Error() string { + return "not in use" +} + +// ErrServerUnexpectedFrame is an error that can be returned by a server. +type ErrServerUnexpectedFrame = ErrClientUnexpectedFrame + +// ErrServerUnexpectedResponse is an error that can be returned by a server. +type ErrServerUnexpectedResponse struct{} + +// Error implements the error interface. +func (e ErrServerUnexpectedResponse) Error() string { + return "received unexpected response" +} + +// ErrServerWriteQueueFull is an error that can be returned by a server. +type ErrServerWriteQueueFull = ErrClientWriteQueueFull + +// ErrServerRTPPacketsLost is an error that can be returned by a server. +// +// Deprecated: will be removed in next version. +type ErrServerRTPPacketsLost = ErrClientRTPPacketsLost + +// ErrServerRTPPacketUnknownPayloadType is an error that can be returned by a server. +type ErrServerRTPPacketUnknownPayloadType = ErrClientRTPPacketUnknownPayloadType + +// ErrServerRTCPPacketTooBig is an error that can be returned by a server. +type ErrServerRTCPPacketTooBig = ErrClientRTCPPacketTooBig + +// ErrServerRTPPacketTooBigUDP is an error that can be returned by a server. +type ErrServerRTPPacketTooBigUDP = ErrClientRTPPacketTooBigUDP + +// ErrServerRTCPPacketTooBigUDP is an error that can be returned by a server. +type ErrServerRTCPPacketTooBigUDP = ErrClientRTCPPacketTooBigUDP + +// ErrServerStreamClosed is an error that can be returned by a server. +type ErrServerStreamClosed struct{} + +// Error implements the error interface. +func (e ErrServerStreamClosed) Error() string { + return "stream is closed" +} + +// ErrServerInvalidSetupPath is an error that can be returned by a server. +type ErrServerInvalidSetupPath struct{} + +// Error implements the error interface. +func (ErrServerInvalidSetupPath) Error() string { + return "invalid SETUP path. " + + "This typically happens when VLC fails a request, and then switches to an " + + "unsupported RTSP dialect" +} + +// ErrServerAuth is an error that can be returned by a server. +// If a client did not provide credentials, it will be asked for +// credentials instead of being kicked out. +type ErrServerAuth struct{} + +// Error implements the error interface. +func (e ErrServerAuth) Error() string { + return "authentication error" +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/multicast/multi_conn.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/multicast/multi_conn.go new file mode 100644 index 000000000..c7ad89e03 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/multicast/multi_conn.go @@ -0,0 +1,180 @@ +//go:build !linux + +package multicast + +import ( + "fmt" + "net" + "strconv" + "time" + + "golang.org/x/net/ipv4" +) + +// MultiConn is a multicast connection +// that works in parallel on all interfaces. +type MultiConn struct { + addr *net.UDPAddr + readConn *net.UDPConn + readConnIP *ipv4.PacketConn + writeConns []*net.UDPConn + writeConnIPs []*ipv4.PacketConn +} + +// NewMultiConn allocates a MultiConn. +func NewMultiConn( + address string, + readOnly bool, + listenPacket func(network, address string) (net.PacketConn, error), +) (Conn, error) { + addr, err := net.ResolveUDPAddr("udp4", address) + if err != nil { + return nil, err + } + + tmp, err := listenPacket("udp4", "224.0.0.0:"+strconv.FormatInt(int64(addr.Port), 10)) + if err != nil { + return nil, err + } + readConn := tmp.(*net.UDPConn) + + intfs, err := net.Interfaces() + if err != nil { + readConn.Close() //nolint:errcheck + return nil, err + } + + readConnIP := ipv4.NewPacketConn(readConn) + + var enabledInterfaces []*net.Interface //nolint:prealloc + + for _, intf := range intfs { + if (intf.Flags & net.FlagMulticast) == 0 { + continue + } + cintf := intf + + err = readConnIP.JoinGroup(&cintf, &net.UDPAddr{IP: addr.IP}) + if err != nil { + continue + } + + enabledInterfaces = append(enabledInterfaces, &cintf) + } + + if enabledInterfaces == nil { + readConn.Close() //nolint:errcheck + return nil, fmt.Errorf("no multicast-capable interfaces found") + } + + var writeConns []*net.UDPConn + var writeConnIPs []*ipv4.PacketConn + + if !readOnly { + writeConns = make([]*net.UDPConn, len(enabledInterfaces)) + writeConnIPs = make([]*ipv4.PacketConn, len(enabledInterfaces)) + + for i, intf := range enabledInterfaces { + tmp, err := listenPacket("udp4", "224.0.0.0:"+strconv.FormatInt(int64(addr.Port), 10)) + if err != nil { + for j := 0; j < i; j++ { + writeConns[j].Close() //nolint:errcheck + } + readConn.Close() //nolint:errcheck + return nil, err + } + writeConn := tmp.(*net.UDPConn) + + writeConnIP := ipv4.NewPacketConn(writeConn) + + err = writeConnIP.SetMulticastInterface(intf) + if err != nil { + for j := 0; j < i; j++ { + writeConns[j].Close() //nolint:errcheck + } + readConn.Close() //nolint:errcheck + return nil, err + } + + err = writeConnIP.SetMulticastTTL(multicastTTL) + if err != nil { + for j := 0; j < i; j++ { + writeConns[j].Close() //nolint:errcheck + } + readConn.Close() //nolint:errcheck + return nil, err + } + + writeConns[i] = writeConn + writeConnIPs[i] = writeConnIP + } + } + + return &MultiConn{ + addr: addr, + readConn: readConn, + readConnIP: readConnIP, + writeConns: writeConns, + writeConnIPs: writeConnIPs, + }, nil +} + +// Close implements Conn. +func (c *MultiConn) Close() error { + for _, c := range c.writeConns { + c.Close() //nolint:errcheck + } + c.readConn.Close() //nolint:errcheck + return nil +} + +// SetReadBuffer implements Conn. +func (c *MultiConn) SetReadBuffer(bytes int) error { + return c.readConn.SetReadBuffer(bytes) +} + +// LocalAddr implements Conn. +func (c *MultiConn) LocalAddr() net.Addr { + return c.readConn.LocalAddr() +} + +// SetDeadline implements Conn. +func (c *MultiConn) SetDeadline(_ time.Time) error { + panic("unimplemented") +} + +// SetReadDeadline implements Conn. +func (c *MultiConn) SetReadDeadline(t time.Time) error { + return c.readConn.SetReadDeadline(t) +} + +// SetWriteDeadline implements Conn. +func (c *MultiConn) SetWriteDeadline(t time.Time) error { + var err error + for _, c := range c.writeConns { + err2 := c.SetWriteDeadline(t) + if err == nil { + err = err2 + } + } + return err +} + +// WriteTo implements Conn. +func (c *MultiConn) WriteTo(b []byte, addr net.Addr) (int, error) { + var n int + var err error + for _, c := range c.writeConns { + var err2 error + n, err2 = c.WriteTo(b, addr) + if err == nil { + err = err2 + } + } + return n, err +} + +// ReadFrom implements Conn. +func (c *MultiConn) ReadFrom(b []byte) (int, net.Addr, error) { + return c.readConn.ReadFrom(b) +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/multicast/multi_conn_lin.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/multicast/multi_conn_lin.go new file mode 100644 index 000000000..ac638bbcb --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/multicast/multi_conn_lin.go @@ -0,0 +1,233 @@ +//go:build linux + +package multicast + +import ( + "fmt" + "net" + "os" + "syscall" + "time" +) + +// MultiConn is a multicast connection +// that works in parallel on all interfaces. +type MultiConn struct { + addr *net.UDPAddr + readFile *os.File + readConn net.PacketConn + writeFiles []*os.File + writeConns []net.PacketConn +} + +// NewMultiConn allocates a MultiConn. +func NewMultiConn( + address string, + readOnly bool, + _ func(network, address string) (net.PacketConn, error), +) (Conn, error) { + addr, err := net.ResolveUDPAddr("udp4", address) + if err != nil { + return nil, err + } + + readSock, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, syscall.IPPROTO_UDP) + if err != nil { + return nil, err + } + + err = syscall.SetsockoptInt(readSock, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1) + if err != nil { + syscall.Close(readSock) //nolint:errcheck + return nil, err + } + + var lsa syscall.SockaddrInet4 + lsa.Port = addr.Port + copy(lsa.Addr[:], addr.IP.To4()) + err = syscall.Bind(readSock, &lsa) + if err != nil { + syscall.Close(readSock) //nolint:errcheck + return nil, err + } + + intfs, err := net.Interfaces() + if err != nil { + syscall.Close(readSock) //nolint:errcheck + return nil, err + } + + var enabledInterfaces []*net.Interface //nolint:prealloc + for _, intf := range intfs { + if (intf.Flags & net.FlagMulticast) == 0 { + continue + } + cintf := intf + + var mreq syscall.IPMreq + copy(mreq.Multiaddr[:], addr.IP.To4()) + err = setIPMreqInterface(&mreq, &cintf) + if err != nil { + continue + } + + err = syscall.SetsockoptIPMreq(readSock, syscall.IPPROTO_IP, syscall.IP_ADD_MEMBERSHIP, &mreq) + if err != nil { + continue + } + + enabledInterfaces = append(enabledInterfaces, &cintf) + } + + if enabledInterfaces == nil { + syscall.Close(readSock) //nolint:errcheck + return nil, fmt.Errorf("no multicast-capable interfaces found") + } + + var writeFiles []*os.File + var writeConns []net.PacketConn + + if !readOnly { + writeSocks := make([]int, len(enabledInterfaces)) + + for i, intf := range enabledInterfaces { + writeSock, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, syscall.IPPROTO_UDP) + if err != nil { + for j := 0; j < i; j++ { + syscall.Close(writeSocks[j]) //nolint:errcheck + } + syscall.Close(readSock) //nolint:errcheck + return nil, err + } + + err = syscall.SetsockoptInt(writeSock, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1) + if err != nil { + syscall.Close(writeSock) //nolint:errcheck + for j := 0; j < i; j++ { + syscall.Close(writeSocks[j]) //nolint:errcheck + } + syscall.Close(readSock) //nolint:errcheck + return nil, err + } + + var lsa syscall.SockaddrInet4 + lsa.Port = addr.Port + copy(lsa.Addr[:], addr.IP.To4()) + err = syscall.Bind(writeSock, &lsa) + if err != nil { + syscall.Close(writeSock) //nolint:errcheck + for j := 0; j < i; j++ { + syscall.Close(writeSocks[j]) //nolint:errcheck + } + syscall.Close(readSock) //nolint:errcheck + return nil, err + } + + var mreqn syscall.IPMreqn + mreqn.Ifindex = int32(intf.Index) + + err = syscall.SetsockoptIPMreqn(writeSock, syscall.IPPROTO_IP, syscall.IP_MULTICAST_IF, &mreqn) + if err != nil { + syscall.Close(writeSock) //nolint:errcheck + for j := 0; j < i; j++ { + syscall.Close(writeSocks[j]) //nolint:errcheck + } + syscall.Close(readSock) //nolint:errcheck + return nil, err + } + + err = syscall.SetsockoptInt(writeSock, syscall.IPPROTO_IP, syscall.IP_MULTICAST_TTL, multicastTTL) + if err != nil { + syscall.Close(writeSock) //nolint:errcheck + for j := 0; j < i; j++ { + syscall.Close(writeSocks[j]) //nolint:errcheck + } + syscall.Close(readSock) //nolint:errcheck + return nil, err + } + + writeSocks[i] = writeSock + } + + writeFiles = make([]*os.File, len(writeSocks)) + writeConns = make([]net.PacketConn, len(writeSocks)) + + for i, writeSock := range writeSocks { + writeFiles[i] = os.NewFile(uintptr(writeSock), "") + writeConns[i], _ = net.FilePacketConn(writeFiles[i]) + } + } + + readFile := os.NewFile(uintptr(readSock), "") + readConn, _ := net.FilePacketConn(readFile) + + return &MultiConn{ + addr: addr, + readFile: readFile, + readConn: readConn, + writeFiles: writeFiles, + writeConns: writeConns, + }, nil +} + +// Close implements Conn. +func (c *MultiConn) Close() error { + for i, writeConn := range c.writeConns { + writeConn.Close() + c.writeFiles[i].Close() + } + c.readConn.Close() + c.readFile.Close() + return nil +} + +// SetReadBuffer implements Conn. +func (c *MultiConn) SetReadBuffer(bytes int) error { + return syscall.SetsockoptInt(int(c.readFile.Fd()), syscall.SOL_SOCKET, syscall.SO_RCVBUF, bytes) +} + +// LocalAddr implements Conn. +func (c *MultiConn) LocalAddr() net.Addr { + return c.readConn.LocalAddr() +} + +// SetDeadline implements Conn. +func (c *MultiConn) SetDeadline(_ time.Time) error { + panic("unimplemented") +} + +// SetReadDeadline implements Conn. +func (c *MultiConn) SetReadDeadline(t time.Time) error { + return c.readConn.SetReadDeadline(t) +} + +// SetWriteDeadline implements Conn. +func (c *MultiConn) SetWriteDeadline(t time.Time) error { + var err error + for _, c := range c.writeConns { + err2 := c.SetWriteDeadline(t) + if err == nil { + err = err2 + } + } + return err +} + +// WriteTo implements Conn. +func (c *MultiConn) WriteTo(b []byte, addr net.Addr) (int, error) { + var n int + var err error + for _, c := range c.writeConns { + var err2 error + n, err2 = c.WriteTo(b, addr) + if err == nil { + err = err2 + } + } + return n, err +} + +// ReadFrom implements Conn. +func (c *MultiConn) ReadFrom(b []byte) (int, net.Addr, error) { + return c.readConn.ReadFrom(b) +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/multicast/multicast.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/multicast/multicast.go new file mode 100644 index 000000000..ae8221efe --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/multicast/multicast.go @@ -0,0 +1,43 @@ +// Package multicast contains multicast connections. +package multicast + +import ( + "fmt" + "net" +) + +// Conn is a Multicast connection. +type Conn interface { + net.PacketConn + SetReadBuffer(int) error +} + +// InterfaceForSource returns a multicast-capable interface that can communicate with given IP. +func InterfaceForSource(ip net.IP) (*net.Interface, error) { + if ip.Equal(net.ParseIP("127.0.0.1")) { + return nil, fmt.Errorf("IP 127.0.0.1 can't be used as source of a multicast stream. Use the LAN IP of your PC") + } + + intfs, err := net.Interfaces() + if err != nil { + return nil, err + } + + for _, intf := range intfs { + if (intf.Flags & net.FlagMulticast) == 0 { + continue + } + + addrs, err := intf.Addrs() + if err == nil { + for _, addr := range addrs { + _, ipnet, err := net.ParseCIDR(addr.String()) + if err == nil && ipnet.Contains(ip) { + return &intf, nil + } + } + } + } + + return nil, fmt.Errorf("found no interface that is multicast-capable and can communicate with IP %v", ip) +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/multicast/single_conn.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/multicast/single_conn.go new file mode 100644 index 000000000..fd99e8acd --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/multicast/single_conn.go @@ -0,0 +1,108 @@ +//go:build !linux + +package multicast + +import ( + "net" + "strconv" + "time" + + "golang.org/x/net/ipv4" +) + +const ( + // same size as GStreamer's rtspsrc + multicastTTL = 16 +) + +// SingleConn is a multicast connection +// that works on a single interface. +type SingleConn struct { + addr *net.UDPAddr + conn *net.UDPConn + connIP *ipv4.PacketConn +} + +// NewSingleConn allocates a SingleConn. +func NewSingleConn( + intf *net.Interface, + address string, + listenPacket func(network, address string) (net.PacketConn, error), +) (Conn, error) { + addr, err := net.ResolveUDPAddr("udp4", address) + if err != nil { + return nil, err + } + + tmp, err := listenPacket("udp4", "224.0.0.0:"+strconv.FormatInt(int64(addr.Port), 10)) + if err != nil { + return nil, err + } + conn := tmp.(*net.UDPConn) + + connIP := ipv4.NewPacketConn(conn) + + err = connIP.JoinGroup(intf, &net.UDPAddr{IP: addr.IP}) + if err != nil { + conn.Close() //nolint:errcheck + return nil, err + } + + err = connIP.SetMulticastInterface(intf) + if err != nil { + conn.Close() //nolint:errcheck + return nil, err + } + + err = connIP.SetMulticastTTL(multicastTTL) + if err != nil { + conn.Close() //nolint:errcheck + return nil, err + } + + return &SingleConn{ + addr: addr, + conn: conn, + connIP: connIP, + }, nil +} + +// Close implements Conn. +func (c *SingleConn) Close() error { + return c.conn.Close() +} + +// SetReadBuffer implements Conn. +func (c *SingleConn) SetReadBuffer(bytes int) error { + return c.conn.SetReadBuffer(bytes) +} + +// LocalAddr implements Conn. +func (c *SingleConn) LocalAddr() net.Addr { + return c.conn.LocalAddr() +} + +// SetDeadline implements Conn. +func (c *SingleConn) SetDeadline(_ time.Time) error { + panic("unimplemented") +} + +// SetReadDeadline implements Conn. +func (c *SingleConn) SetReadDeadline(t time.Time) error { + return c.conn.SetReadDeadline(t) +} + +// SetWriteDeadline implements Conn. +func (c *SingleConn) SetWriteDeadline(t time.Time) error { + return c.conn.SetWriteDeadline(t) +} + +// WriteTo implements Conn. +func (c *SingleConn) WriteTo(b []byte, addr net.Addr) (int, error) { + return c.conn.WriteTo(b, addr) +} + +// ReadFrom implements Conn. +func (c *SingleConn) ReadFrom(b []byte) (int, net.Addr, error) { + return c.conn.ReadFrom(b) +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/multicast/single_conn_lin.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/multicast/single_conn_lin.go new file mode 100644 index 000000000..ab5cb1640 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/multicast/single_conn_lin.go @@ -0,0 +1,172 @@ +//go:build linux + +package multicast + +import ( + "fmt" + "net" + "os" + "syscall" + "time" +) + +const ( + // same size as GStreamer's rtspsrc + multicastTTL = 16 +) + +// https://cs.opensource.google/go/x/net/+/refs/tags/v0.15.0:ipv4/sys_asmreq.go;l=51 +func setIPMreqInterface(mreq *syscall.IPMreq, ifi *net.Interface) error { + if ifi == nil { + return nil + } + ifat, err := ifi.Addrs() + if err != nil { + return err + } + for _, ifa := range ifat { + switch ifa := ifa.(type) { + case *net.IPAddr: + if ip := ifa.IP.To4(); ip != nil { + copy(mreq.Interface[:], ip) + return nil + } + case *net.IPNet: + if ip := ifa.IP.To4(); ip != nil { + copy(mreq.Interface[:], ip) + return nil + } + } + } + return fmt.Errorf("no such interface") +} + +// SingleConn is a multicast connection +// that works on a single interface. +type SingleConn struct { + addr *net.UDPAddr + file *os.File + conn net.PacketConn +} + +// NewSingleConn allocates a SingleConn. +func NewSingleConn( + intf *net.Interface, + address string, + _ func(network, address string) (net.PacketConn, error), +) (Conn, error) { + addr, err := net.ResolveUDPAddr("udp4", address) + if err != nil { + return nil, err + } + + sock, err := syscall.Socket(syscall.AF_INET, syscall.SOCK_DGRAM, syscall.IPPROTO_UDP) + if err != nil { + return nil, err + } + + err = syscall.SetsockoptInt(sock, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1) + if err != nil { + syscall.Close(sock) //nolint:errcheck + return nil, err + } + + err = syscall.SetsockoptString(sock, syscall.SOL_SOCKET, syscall.SO_BINDTODEVICE, intf.Name) + if err != nil { + syscall.Close(sock) //nolint:errcheck + return nil, err + } + + var lsa syscall.SockaddrInet4 + lsa.Port = addr.Port + copy(lsa.Addr[:], addr.IP.To4()) + err = syscall.Bind(sock, &lsa) + if err != nil { + syscall.Close(sock) //nolint:errcheck + return nil, err + } + + var mreq syscall.IPMreq + copy(mreq.Multiaddr[:], addr.IP.To4()) + err = setIPMreqInterface(&mreq, intf) + if err != nil { + syscall.Close(sock) //nolint:errcheck + return nil, err + } + + err = syscall.SetsockoptIPMreq(sock, syscall.IPPROTO_IP, syscall.IP_ADD_MEMBERSHIP, &mreq) + if err != nil { + syscall.Close(sock) //nolint:errcheck + return nil, err + } + + var mreqn syscall.IPMreqn + mreqn.Ifindex = int32(intf.Index) + + err = syscall.SetsockoptIPMreqn(sock, syscall.IPPROTO_IP, syscall.IP_MULTICAST_IF, &mreqn) + if err != nil { + syscall.Close(sock) //nolint:errcheck + return nil, err + } + + err = syscall.SetsockoptInt(sock, syscall.IPPROTO_IP, syscall.IP_MULTICAST_TTL, multicastTTL) + if err != nil { + syscall.Close(sock) //nolint:errcheck + return nil, err + } + + file := os.NewFile(uintptr(sock), "") + conn, err := net.FilePacketConn(file) + if err != nil { + file.Close() + return nil, err + } + + return &SingleConn{ + addr: addr, + file: file, + conn: conn, + }, nil +} + +// Close implements Conn. +func (c *SingleConn) Close() error { + c.conn.Close() + c.file.Close() + return nil +} + +// SetReadBuffer implements Conn. +func (c *SingleConn) SetReadBuffer(bytes int) error { + return syscall.SetsockoptInt(int(c.file.Fd()), syscall.SOL_SOCKET, syscall.SO_RCVBUF, bytes) +} + +// LocalAddr implements Conn. +func (c *SingleConn) LocalAddr() net.Addr { + return c.conn.LocalAddr() +} + +// SetDeadline implements Conn. +func (c *SingleConn) SetDeadline(_ time.Time) error { + panic("unimplemented") +} + +// SetReadDeadline implements Conn. +func (c *SingleConn) SetReadDeadline(t time.Time) error { + return c.conn.SetReadDeadline(t) +} + +// SetWriteDeadline implements Conn. +func (c *SingleConn) SetWriteDeadline(t time.Time) error { + return c.conn.SetWriteDeadline(t) +} + +// WriteTo implements Conn. +func (c *SingleConn) WriteTo(b []byte, addr net.Addr) (int, error) { + return c.conn.WriteTo(b, addr) +} + +// ReadFrom implements Conn. +func (c *SingleConn) ReadFrom(b []byte) (int, net.Addr, error) { + return c.conn.ReadFrom(b) +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/ringbuffer/ringbuffer.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/ringbuffer/ringbuffer.go new file mode 100644 index 000000000..364aea319 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/ringbuffer/ringbuffer.go @@ -0,0 +1,106 @@ +// Package ringbuffer contains a ring buffer. +package ringbuffer + +import ( + "fmt" + "sync" +) + +// RingBuffer is a ring buffer. +type RingBuffer struct { + size uint64 + mutex sync.Mutex + cond *sync.Cond + buffer []interface{} + readIndex uint64 + writeIndex uint64 + closed bool +} + +// New allocates a RingBuffer. +func New(size uint64) (*RingBuffer, error) { + // when writeIndex overflows, if size is not a power of + // two, only a portion of the buffer is used. + if (size & (size - 1)) != 0 { + return nil, fmt.Errorf("size must be a power of two") + } + + r := &RingBuffer{ + size: size, + buffer: make([]interface{}, size), + } + + r.cond = sync.NewCond(&r.mutex) + + return r, nil +} + +// Close makes Pull() return false. +func (r *RingBuffer) Close() { + r.mutex.Lock() + + r.closed = true + + // discard pending data to make Pull() exit immediately + for i := uint64(0); i < r.size; i++ { + r.buffer[i] = nil + } + + r.mutex.Unlock() + r.cond.Broadcast() +} + +// Reset restores Pull() behavior after a Close(). +func (r *RingBuffer) Reset() { + for i := uint64(0); i < r.size; i++ { + r.buffer[i] = nil + } + + r.writeIndex = 0 + r.readIndex = 0 + r.closed = false +} + +// Push pushes data at the end of the buffer. +func (r *RingBuffer) Push(data interface{}) bool { + r.mutex.Lock() + + if r.buffer[r.writeIndex] != nil { + r.mutex.Unlock() + return false + } + + r.buffer[r.writeIndex] = data + r.writeIndex = (r.writeIndex + 1) % r.size + + r.mutex.Unlock() + + r.cond.Broadcast() + + return true +} + +// Pull pulls data from the beginning of the buffer. +func (r *RingBuffer) Pull() (interface{}, bool) { + for { + r.mutex.Lock() + + data := r.buffer[r.readIndex] + + if data != nil { + r.buffer[r.readIndex] = nil + r.readIndex = (r.readIndex + 1) % r.size + r.mutex.Unlock() + return data, true + } + + if r.closed { + r.mutex.Unlock() + return nil, false + } + + r.cond.Wait() + + r.mutex.Unlock() + } +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/rtcpreceiver/rtcpreceiver.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/rtcpreceiver/rtcpreceiver.go new file mode 100644 index 000000000..931bc671b --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/rtcpreceiver/rtcpreceiver.go @@ -0,0 +1,314 @@ +// Package rtcpreceiver contains a utility to generate RTCP receiver reports. +package rtcpreceiver + +import ( + "crypto/rand" + "fmt" + "sync" + "time" + + "github.com/pion/rtcp" + "github.com/pion/rtp" +) + +// seconds since 1st January 1900 +// higher 32 bits are the integer part, lower 32 bits are the fractional part +func ntpTimeRTCPToGo(v uint64) time.Time { + nano := int64((v>>32)*1000000000+(v&0xFFFFFFFF)) - 2208988800*1000000000 + return time.Unix(0, nano) +} + +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 +} + +// RTCPReceiver is a utility to generate RTCP receiver reports. +type RTCPReceiver struct { + ClockRate int + LocalSSRC *uint32 + Period time.Duration + TimeNow func() time.Time + WritePacketRTCP func(rtcp.Packet) + + mutex sync.RWMutex + + // data from RTP packets + firstRTPPacketReceived bool + timeInitialized bool + sequenceNumberCycles uint16 + lastSequenceNumber uint16 + remoteSSRC uint32 + lastTimeRTP uint32 + lastTimeSystem time.Time + totalLost uint32 + totalLostSinceReport uint32 + totalSinceReport uint32 + jitter float64 + + // data from RTCP packets + firstSenderReportReceived bool + lastSenderReportTimeNTP uint64 + lastSenderReportTimeRTP uint32 + lastSenderReportTimeSystem time.Time + + terminate chan struct{} + done chan struct{} +} + +// New allocates a RTCPReceiver. +// +// Deprecated: replaced by Initialize(). +func New( + clockRate int, + receiverSSRC *uint32, + period time.Duration, + timeNow func() time.Time, + writePacketRTCP func(rtcp.Packet), +) (*RTCPReceiver, error) { + rr := &RTCPReceiver{ + ClockRate: clockRate, + LocalSSRC: receiverSSRC, + Period: period, + TimeNow: timeNow, + WritePacketRTCP: writePacketRTCP, + } + err := rr.Initialize() + if err != nil { + return nil, err + } + + return rr, nil +} + +// Initialize initializes RTCPReceiver. +func (rr *RTCPReceiver) Initialize() error { + if rr.LocalSSRC == nil { + v, err := randUint32() + if err != nil { + return err + } + rr.LocalSSRC = &v + } + + if rr.TimeNow == nil { + rr.TimeNow = time.Now + } + + rr.terminate = make(chan struct{}) + rr.done = make(chan struct{}) + + go rr.run() + + return nil +} + +func (rr *RTCPReceiver) run() { + defer close(rr.done) + + t := time.NewTicker(rr.Period) + defer t.Stop() + + for { + select { + case <-t.C: + report := rr.report() + if report != nil { + rr.WritePacketRTCP(report) + } + + case <-rr.terminate: + return + } + } +} + +func (rr *RTCPReceiver) report() rtcp.Packet { + rr.mutex.Lock() + defer rr.mutex.Unlock() + + if !rr.firstRTPPacketReceived { + return nil + } + + system := rr.TimeNow() + + report := &rtcp.ReceiverReport{ + SSRC: *rr.LocalSSRC, + Reports: []rtcp.ReceptionReport{ + { + SSRC: rr.remoteSSRC, + LastSequenceNumber: uint32(rr.sequenceNumberCycles)<<16 | uint32(rr.lastSequenceNumber), + // equivalent to taking the integer part after multiplying the + // loss fraction by 256 + FractionLost: uint8(float64(rr.totalLostSinceReport*256) / float64(rr.totalSinceReport)), + TotalLost: rr.totalLost, + Jitter: uint32(rr.jitter), + }, + }, + } + + if rr.firstSenderReportReceived { + // middle 32 bits out of 64 in the NTP timestamp of last sender report + report.Reports[0].LastSenderReport = uint32(rr.lastSenderReportTimeNTP >> 16) + + // delay, expressed in units of 1/65536 seconds, between + // receiving the last SR packet from source SSRC_n and sending this + // reception report block + report.Reports[0].Delay = uint32(system.Sub(rr.lastSenderReportTimeSystem).Seconds() * 65536) + } + + rr.totalLostSinceReport = 0 + rr.totalSinceReport = 0 + + return report +} + +// Close closes the RTCPReceiver. +func (rr *RTCPReceiver) Close() { + close(rr.terminate) + <-rr.done +} + +// ProcessPacket extracts the needed data from RTP packets. +func (rr *RTCPReceiver) ProcessPacket(pkt *rtp.Packet, system time.Time, ptsEqualsDTS bool) error { + rr.mutex.Lock() + defer rr.mutex.Unlock() + + // first packet + if !rr.firstRTPPacketReceived { + rr.firstRTPPacketReceived = true + rr.totalSinceReport = 1 + rr.lastSequenceNumber = pkt.SequenceNumber + rr.remoteSSRC = pkt.SSRC + + if ptsEqualsDTS { + rr.timeInitialized = true + rr.lastTimeRTP = pkt.Timestamp + rr.lastTimeSystem = system + } + + // subsequent packets + } else { + if pkt.SSRC != rr.remoteSSRC { + return fmt.Errorf("received packet with wrong SSRC %d, expected %d", pkt.SSRC, rr.remoteSSRC) + } + + diff := int32(pkt.SequenceNumber) - int32(rr.lastSequenceNumber) + + // overflow + if diff < -0x0FFF { + rr.sequenceNumberCycles++ + } + + // detect lost packets + if pkt.SequenceNumber != (rr.lastSequenceNumber + 1) { + rr.totalLost += uint32(uint16(diff) - 1) + rr.totalLostSinceReport += uint32(uint16(diff) - 1) + + // allow up to 24 bits + if rr.totalLost > 0xFFFFFF { + rr.totalLost = 0xFFFFFF + } + if rr.totalLostSinceReport > 0xFFFFFF { + rr.totalLostSinceReport = 0xFFFFFF + } + } + + rr.totalSinceReport += uint32(uint16(diff)) + rr.lastSequenceNumber = pkt.SequenceNumber + + if ptsEqualsDTS { + if rr.timeInitialized { + // update jitter + // https://tools.ietf.org/html/rfc3550#page-39 + D := system.Sub(rr.lastTimeSystem).Seconds()*float64(rr.ClockRate) - + (float64(pkt.Timestamp) - float64(rr.lastTimeRTP)) + if D < 0 { + D = -D + } + rr.jitter += (D - rr.jitter) / 16 + } + + rr.timeInitialized = true + rr.lastTimeRTP = pkt.Timestamp + rr.lastTimeSystem = system + } + } + + return nil +} + +// ProcessSenderReport extracts the needed data from RTCP sender reports. +func (rr *RTCPReceiver) ProcessSenderReport(sr *rtcp.SenderReport, system time.Time) { + rr.mutex.Lock() + defer rr.mutex.Unlock() + + rr.firstSenderReportReceived = true + rr.lastSenderReportTimeNTP = sr.NTPTime + rr.lastSenderReportTimeRTP = sr.RTPTime + rr.lastSenderReportTimeSystem = system +} + +func (rr *RTCPReceiver) packetNTPUnsafe(ts uint32) (time.Time, bool) { + if !rr.firstSenderReportReceived { + return time.Time{}, false + } + + timeDiff := int32(ts - rr.lastSenderReportTimeRTP) + timeDiffGo := (time.Duration(timeDiff) * time.Second) / time.Duration(rr.ClockRate) + + return ntpTimeRTCPToGo(rr.lastSenderReportTimeNTP).Add(timeDiffGo), true +} + +// PacketNTP returns the NTP timestamp of the packet. +func (rr *RTCPReceiver) PacketNTP(ts uint32) (time.Time, bool) { + rr.mutex.Lock() + defer rr.mutex.Unlock() + + return rr.packetNTPUnsafe(ts) +} + +// SenderSSRC returns the SSRC of outgoing RTP packets. +// +// Deprecated: replaced by Stats(). +func (rr *RTCPReceiver) SenderSSRC() (uint32, bool) { + stats := rr.Stats() + if stats == nil { + return 0, false + } + return stats.RemoteSSRC, true +} + +// Stats are statistics. +type Stats struct { + RemoteSSRC uint32 + LastSequenceNumber uint16 + LastRTP uint32 + LastNTP time.Time + Jitter float64 +} + +// Stats returns statistics. +func (rr *RTCPReceiver) Stats() *Stats { + rr.mutex.RLock() + defer rr.mutex.RUnlock() + + if !rr.firstRTPPacketReceived { + return nil + } + + ntp, _ := rr.packetNTPUnsafe(rr.lastTimeRTP) + + return &Stats{ + RemoteSSRC: rr.remoteSSRC, + LastSequenceNumber: rr.lastSequenceNumber, + LastRTP: rr.lastTimeRTP, + LastNTP: ntp, + Jitter: rr.jitter, + } +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/rtcpsender/rtcpsender.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/rtcpsender/rtcpsender.go new file mode 100644 index 000000000..929d7a6ad --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/rtcpsender/rtcpsender.go @@ -0,0 +1,187 @@ +// Package rtcpsender contains a utility to generate RTCP sender reports. +package rtcpsender + +import ( + "sync" + "time" + + "github.com/pion/rtcp" + "github.com/pion/rtp" +) + +// seconds since 1st January 1900 +// higher 32 bits are the integer part, lower 32 bits are the fractional part +func ntpTimeGoToRTCP(v time.Time) uint64 { + s := uint64(v.UnixNano()) + 2208988800*1000000000 + return (s/1000000000)<<32 | (s % 1000000000) +} + +// RTCPSender is a utility to generate RTCP sender reports. +type RTCPSender struct { + ClockRate int + Period time.Duration + TimeNow func() time.Time + WritePacketRTCP func(rtcp.Packet) + + mutex sync.RWMutex + + // data from RTP packets + firstRTPPacketSent bool + lastTimeRTP uint32 + lastTimeNTP time.Time + lastTimeSystem time.Time + localSSRC uint32 + lastSequenceNumber uint16 + packetCount uint32 + octetCount uint32 + + terminate chan struct{} + done chan struct{} +} + +// New allocates a RTCPSender. +// +// Deprecated: replaced by Initialize(). +func New( + clockRate int, + period time.Duration, + timeNow func() time.Time, + writePacketRTCP func(rtcp.Packet), +) *RTCPSender { + rs := &RTCPSender{ + ClockRate: clockRate, + Period: period, + TimeNow: timeNow, + WritePacketRTCP: writePacketRTCP, + } + rs.Initialize() + + return rs +} + +// Initialize initializes a RTCPSender. +func (rs *RTCPSender) Initialize() { + if rs.TimeNow == nil { + rs.TimeNow = time.Now + } + + rs.terminate = make(chan struct{}) + rs.done = make(chan struct{}) + + go rs.run() +} + +func (rs *RTCPSender) run() { + defer close(rs.done) + + t := time.NewTicker(rs.Period) + defer t.Stop() + + for { + select { + case <-t.C: + report := rs.report() + if report != nil { + rs.WritePacketRTCP(report) + } + + case <-rs.terminate: + return + } + } +} + +func (rs *RTCPSender) report() rtcp.Packet { + rs.mutex.Lock() + defer rs.mutex.Unlock() + + if !rs.firstRTPPacketSent { + return nil + } + + systemTimeDiff := rs.TimeNow().Sub(rs.lastTimeSystem) + ntpTime := rs.lastTimeNTP.Add(systemTimeDiff) + rtpTime := rs.lastTimeRTP + uint32(systemTimeDiff.Seconds()*float64(rs.ClockRate)) + + return &rtcp.SenderReport{ + SSRC: rs.localSSRC, + NTPTime: ntpTimeGoToRTCP(ntpTime), + RTPTime: rtpTime, + PacketCount: rs.packetCount, + OctetCount: rs.octetCount, + } +} + +// Close closes the RTCPSender. +func (rs *RTCPSender) Close() { + close(rs.terminate) + <-rs.done +} + +// ProcessPacket extracts data from RTP packets. +func (rs *RTCPSender) ProcessPacket(pkt *rtp.Packet, ntp time.Time, ptsEqualsDTS bool) { + rs.mutex.Lock() + defer rs.mutex.Unlock() + + if ptsEqualsDTS { + rs.firstRTPPacketSent = true + rs.lastTimeRTP = pkt.Timestamp + rs.lastTimeNTP = ntp + rs.lastTimeSystem = rs.TimeNow() + rs.localSSRC = pkt.SSRC + } + + rs.lastSequenceNumber = pkt.SequenceNumber + + rs.packetCount++ + rs.octetCount += uint32(len(pkt.Payload)) +} + +// SenderSSRC returns the SSRC of outgoing RTP packets. +// +// Deprecated: replaced by Stats(). +func (rs *RTCPSender) SenderSSRC() (uint32, bool) { + stats := rs.Stats() + if stats == nil { + return 0, false + } + + return stats.LocalSSRC, true +} + +// LastPacketData returns metadata of the last RTP packet. +// +// Deprecated: replaced by Stats(). +func (rs *RTCPSender) LastPacketData() (uint16, uint32, time.Time, bool) { + stats := rs.Stats() + if stats == nil { + return 0, 0, time.Time{}, false + } + + return stats.LastSequenceNumber, stats.LastRTP, stats.LastNTP, true +} + +// Stats are statistics. +type Stats struct { + LocalSSRC uint32 + LastSequenceNumber uint16 + LastRTP uint32 + LastNTP time.Time +} + +// Stats returns statistics. +func (rs *RTCPSender) Stats() *Stats { + rs.mutex.RLock() + defer rs.mutex.RUnlock() + + if !rs.firstRTPPacketSent { + return nil + } + + return &Stats{ + LocalSSRC: rs.localSSRC, + LastSequenceNumber: rs.lastSequenceNumber, + LastRTP: rs.lastTimeRTP, + LastNTP: rs.lastTimeNTP, + } +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/rtplossdetector/lossdetector.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/rtplossdetector/lossdetector.go new file mode 100644 index 000000000..219af5701 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/rtplossdetector/lossdetector.go @@ -0,0 +1,38 @@ +// Package rtplossdetector implements an algorithm that detects lost packets. +package rtplossdetector + +import ( + "github.com/pion/rtp" +) + +// LossDetector detects lost packets. +type LossDetector struct { + initialized bool + expectedSeqNum uint16 +} + +// New allocates a LossDetector. +// +// Deprecated: Useless. +func New() *LossDetector { + return &LossDetector{} +} + +// Process processes a RTP packet. +// It returns the number of lost packets. +func (r *LossDetector) Process(pkt *rtp.Packet) uint { + if !r.initialized { + r.initialized = true + r.expectedSeqNum = pkt.SequenceNumber + 1 + return 0 + } + + if pkt.SequenceNumber != r.expectedSeqNum { + diff := pkt.SequenceNumber - r.expectedSeqNum + r.expectedSeqNum = pkt.SequenceNumber + 1 + return uint(diff) + } + + r.expectedSeqNum = pkt.SequenceNumber + 1 + return 0 +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/rtpreorderer/reorderer.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/rtpreorderer/reorderer.go new file mode 100644 index 000000000..00043b0fd --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/rtpreorderer/reorderer.go @@ -0,0 +1,142 @@ +// Package rtpreorderer implements a filter to reorder incoming RTP packets. +package rtpreorderer + +import ( + "github.com/pion/rtp" +) + +const ( + bufferSize = 64 +) + +// Reorderer filters incoming RTP packets, in order to +// - order packets +// - remove duplicate packets +type Reorderer struct { + initialized bool + expectedSeqNum uint16 + buffer []*rtp.Packet + absPos uint16 + negativeCount int +} + +// New allocates a Reorderer. +// +// Deprecated: replaced by Initialize(). +func New() *Reorderer { + r := &Reorderer{} + r.Initialize() + return r +} + +// Initialize initializes a Reorderer. +func (r *Reorderer) Initialize() { + r.buffer = make([]*rtp.Packet, bufferSize) +} + +// Process processes a RTP packet. +// It returns a sequence of ordered packets and the number of lost packets. +func (r *Reorderer) Process(pkt *rtp.Packet) ([]*rtp.Packet, uint) { + if !r.initialized { + r.initialized = true + r.expectedSeqNum = pkt.SequenceNumber + 1 + return []*rtp.Packet{pkt}, 0 + } + + relPos := int16(pkt.SequenceNumber - r.expectedSeqNum) + + // packet is a duplicate or has been sent + // before the first packet processed by Reorderer. + // discard. + if relPos < 0 { + r.negativeCount++ + + // stream has been resetted, therefore reset reorderer too + if r.negativeCount > bufferSize { + r.negativeCount = 0 + + // clear buffer + for i := uint16(0); i < bufferSize; i++ { + p := (r.absPos + i) & (bufferSize - 1) + r.buffer[p] = nil + } + + // reset position + r.expectedSeqNum = pkt.SequenceNumber + 1 + return []*rtp.Packet{pkt}, 0 + } + + return nil, 0 + } + r.negativeCount = 0 + + // there's a missing packet and buffer is full. + // return entire buffer and clear it. + if relPos >= bufferSize { + n := 1 + for i := uint16(0); i < bufferSize; i++ { + p := (r.absPos + i) & (bufferSize - 1) + if r.buffer[p] != nil { + n++ + } + } + + ret := make([]*rtp.Packet, n) + pos := 0 + + for i := uint16(0); i < bufferSize; i++ { + p := (r.absPos + i) & (bufferSize - 1) + if r.buffer[p] != nil { + ret[pos], r.buffer[p] = r.buffer[p], nil + pos++ + } + } + + ret[pos] = pkt + + r.expectedSeqNum = pkt.SequenceNumber + 1 + return ret, uint(int(relPos) - n + 1) + } + + // there's a missing packet + if relPos != 0 { + p := (r.absPos + uint16(relPos)) & (bufferSize - 1) + + // current packet is a duplicate. discard + if r.buffer[p] != nil { + return nil, 0 + } + + // put current packet in buffer + r.buffer[p] = pkt + return nil, 0 + } + + // all packets have been received correctly. + // return them + + n := uint16(1) + for { + p := (r.absPos + n) & (bufferSize - 1) + if r.buffer[p] == nil { + break + } + n++ + } + + ret := make([]*rtp.Packet, n) + + ret[0] = pkt + r.absPos++ + r.absPos &= (bufferSize - 1) + + for i := uint16(1); i < n; i++ { + ret[i], r.buffer[r.absPos] = r.buffer[r.absPos], nil + r.absPos++ + r.absPos &= (bufferSize - 1) + } + + r.expectedSeqNum = pkt.SequenceNumber + n + + return ret, 0 +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/rtptime/encoder.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/rtptime/encoder.go new file mode 100644 index 000000000..cf5103f4e --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/rtptime/encoder.go @@ -0,0 +1,61 @@ +package rtptime + +import ( + "crypto/rand" + "time" +) + +func divCeil(n, d uint64) uint64 { + v := n / d + if (n % d) != 0 { + v++ + } + 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 +} + +// Encoder is a RTP timestamp encoder. +// +// Deprecated: not used anymore. +type Encoder struct { + // Clock rate. + ClockRate int + + // (optional) initial timestamp. + // It defaults to a random value. + InitialTimestamp *uint32 + + clockRateTD time.Duration + initialTimestampTD time.Duration +} + +// Initialize initializes an Encoder. +func (e *Encoder) Initialize() error { + e.clockRateTD = time.Duration(e.ClockRate) + + if e.InitialTimestamp == nil { + v, err := randUint32() + if err != nil { + return err + } + e.InitialTimestamp = &v + } + + e.initialTimestampTD = time.Duration(divCeil(uint64(*e.InitialTimestamp)*uint64(time.Second), uint64(e.ClockRate))) + + return nil +} + +// Encode encodes a timestamp. +func (e *Encoder) Encode(ts time.Duration) uint32 { + ts += e.initialTimestampTD + return uint32(multiplyAndDivide(ts, e.clockRateTD, time.Second)) +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/rtptime/global_decoder.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/rtptime/global_decoder.go new file mode 100644 index 000000000..a11cad46a --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/rtptime/global_decoder.go @@ -0,0 +1,81 @@ +package rtptime + +import ( + "time" + + "github.com/pion/rtp" +) + +var timeNow = time.Now + +// avoid an int64 overflow and preserve resolution by splitting division into two parts: +// first add the integer part, then the decimal part. +func multiplyAndDivide(v, m, d time.Duration) time.Duration { + secs := v / d + dec := v % d + return (secs*m + dec*m/d) +} + +type globalDecoderTrackData struct { + startPTS time.Duration + clockRate time.Duration + overall time.Duration + prev uint32 +} + +func newGlobalDecoderTrackData( + startPTS time.Duration, + clockRate int, + startTimestamp uint32, +) *globalDecoderTrackData { + return &globalDecoderTrackData{ + startPTS: startPTS, + clockRate: time.Duration(clockRate), + prev: startTimestamp, + } +} + +func (d *globalDecoderTrackData) decode(ts uint32) time.Duration { + diff := int32(ts - d.prev) + d.prev = ts + d.overall += time.Duration(diff) + + return d.startPTS + multiplyAndDivide(d.overall, time.Second, d.clockRate) +} + +// GlobalDecoderTrack is a track (RTSP format or WebRTC track) of a GlobalDecoder. +// +// Deprecated: replaced by GlobalDecoderTrack2 +type GlobalDecoderTrack interface { + ClockRate() int + PTSEqualsDTS(*rtp.Packet) bool +} + +// GlobalDecoder is a RTP timestamp decoder. +// +// Deprecated: replaced by GlobalDecoder2. +type GlobalDecoder struct { + wrapped *GlobalDecoder2 +} + +// NewGlobalDecoder allocates a GlobalDecoder. +// +// Deprecated: replaced by NewGlobalDecoder2. +func NewGlobalDecoder() *GlobalDecoder { + return &GlobalDecoder{ + wrapped: NewGlobalDecoder2(), + } +} + +// Decode decodes a timestamp. +func (d *GlobalDecoder) Decode( + track GlobalDecoderTrack, + pkt *rtp.Packet, +) (time.Duration, bool) { + v, ok := d.wrapped.Decode(track, pkt) + if !ok { + return 0, false + } + + return multiplyAndDivide(time.Duration(v), time.Second, time.Duration(track.ClockRate())), true +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/rtptime/global_decoder2.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/rtptime/global_decoder2.go new file mode 100644 index 000000000..30f689882 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/rtptime/global_decoder2.go @@ -0,0 +1,110 @@ +package rtptime + +import ( + "sync" + "time" + + "github.com/pion/rtp" +) + +// avoid an int64 overflow and preserve resolution by splitting division into two parts: +// first add the integer part, then the decimal part. +func multiplyAndDivide2(v, m, d int64) int64 { + secs := v / d + dec := v % d + return (secs*m + dec*m/d) +} + +type globalDecoder2TrackData struct { + overall int64 + prev uint32 +} + +func (d *globalDecoder2TrackData) decode(ts uint32) int64 { + d.overall += int64(int32(ts - d.prev)) + d.prev = ts + return d.overall +} + +// GlobalDecoder2Track is a track (RTSP format or WebRTC track) of GlobalDecoder2. +type GlobalDecoder2Track interface { + ClockRate() int + PTSEqualsDTS(*rtp.Packet) bool +} + +// NewGlobalDecoder2 allocates a GlobalDecoder. +// +// Deprecated: replaced by GlobalDecoder2.Initialize(). +func NewGlobalDecoder2() *GlobalDecoder2 { + d := &GlobalDecoder2{} + d.Initialize() + return d +} + +// GlobalDecoder2 is a RTP timestamp decoder. +type GlobalDecoder2 struct { + mutex sync.Mutex + leadingTrack GlobalDecoderTrack + startNTP time.Time + startPTS int64 + startPTSClockRate int64 + tracks map[GlobalDecoder2Track]*globalDecoder2TrackData +} + +// Initialize initializes a GlobalDecoder2. +func (d *GlobalDecoder2) Initialize() { + d.tracks = make(map[GlobalDecoder2Track]*globalDecoder2TrackData) +} + +// Decode decodes a timestamp. +func (d *GlobalDecoder2) Decode( + track GlobalDecoder2Track, + pkt *rtp.Packet, +) (int64, bool) { + if track.ClockRate() == 0 { + return 0, false + } + + d.mutex.Lock() + defer d.mutex.Unlock() + + df, ok := d.tracks[track] + + // never seen before track + if !ok { + if !track.PTSEqualsDTS(pkt) { + return 0, false + } + + now := timeNow() + + if d.leadingTrack == nil { + d.leadingTrack = track + d.startNTP = now + d.startPTS = 0 + d.startPTSClockRate = int64(track.ClockRate()) + } + + // start from the PTS of the leading track + startPTS := multiplyAndDivide2(d.startPTS, int64(track.ClockRate()), d.startPTSClockRate) + startPTS += multiplyAndDivide2(int64(now.Sub(d.startNTP)), int64(track.ClockRate()), int64(time.Second)) + + d.tracks[track] = &globalDecoder2TrackData{ + overall: startPTS, + prev: pkt.Timestamp, + } + + return startPTS, true + } + + pts := df.decode(pkt.Timestamp) + + // update startNTP / startPTS + if d.leadingTrack == track && track.PTSEqualsDTS(pkt) { + now := timeNow() + d.startNTP = now + d.startPTS = pts + } + + return pts, true +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/rtptime/rtptime.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/rtptime/rtptime.go new file mode 100644 index 000000000..1f120317d --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/rtptime/rtptime.go @@ -0,0 +1,2 @@ +// Package rtptime contains a time decoder and encoder. +package rtptime diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/sdp/sdp.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/sdp/sdp.go new file mode 100644 index 000000000..2a645a09a --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/pkg/sdp/sdp.go @@ -0,0 +1,743 @@ +// Package sdp contains a SDP encoder/decoder compatible with most RTSP implementations. +package sdp + +import ( + "errors" + "fmt" + "net/url" + "strconv" + "strings" + + psdp "github.com/pion/sdp/v3" +) + +// SessionDescription is a SDP session description. +type SessionDescription psdp.SessionDescription + +// Attribute returns the value of an attribute and if it exists +func (s *SessionDescription) Attribute(key string) (string, bool) { + return (*psdp.SessionDescription)(s).Attribute(key) +} + +// Marshal encodes a SessionDescription. +func (s *SessionDescription) Marshal() ([]byte, error) { + return (*psdp.SessionDescription)(s).Marshal() +} + +var ( + errSDPInvalidSyntax = errors.New("sdp: invalid syntax") + errSDPInvalidNumericValue = errors.New("sdp: invalid numeric value") + errSDPInvalidValue = errors.New("sdp: invalid value") + errSDPInvalidPortValue = errors.New("sdp: invalid port value") +) + +func indexOf(element string, data []string) int { + for k, v := range data { + if element == v { + return k + } + } + return -1 +} + +func anyOf(element string, data ...string) bool { + for _, v := range data { + if element == v { + return true + } + } + return false +} + +func parsePort(value string) (int, error) { + port, err := strconv.Atoi(value) + if err != nil { + return 0, fmt.Errorf("%w `%v`", errSDPInvalidPortValue, port) + } + + if port < 0 || port > 65536 { + return 0, fmt.Errorf("%w -- out of range `%v`", errSDPInvalidPortValue, port) + } + + return port, nil +} + +func (s *SessionDescription) unmarshalProtocolVersion(value string) error { + if value != "0" { + return fmt.Errorf("invalid version") + } + + return nil +} + +func (s *SessionDescription) unmarshalSessionName(value string) error { + s.SessionName = psdp.SessionName(value) + return nil +} + +func stringsReverseIndexByte(s string, b byte) int { + for i := len(s) - 2; i >= 0; i-- { + if s[i] == b { + return i + } + } + return -1 +} + +// This is rewritten from scratch to make it compatible with most RTSP +// implementations. +func (s *SessionDescription) unmarshalOrigin(value string) error { + value = strings.Replace(value, " IN IPV4 ", " IN IP4 ", 1) + + if strings.HasSuffix(value, "IN IP4") { + value += " " + } + + i := strings.Index(value, " IN IP4 ") + if i < 0 { + i = strings.Index(value, " IN IP6 ") + if i < 0 { + return fmt.Errorf("%w `o=%v`", errSDPInvalidSyntax, value) + } + } + + s.Origin.NetworkType = value[i+1 : i+3] + s.Origin.AddressType = value[i+4 : i+7] + s.Origin.UnicastAddress = strings.TrimSpace(value[i+8:]) + value = value[:i] + + i = stringsReverseIndexByte(value, ' ') + if i < 0 { + return fmt.Errorf("%w `o=%v`", errSDPInvalidSyntax, value) + } + + var tmp string + tmp, value = value[i+1:], value[:i] + + if i = strings.Index(tmp, "."); i >= 0 { + tmp = tmp[:i] + } + tmp = strings.TrimPrefix(tmp, "-") + + var err error + s.Origin.SessionVersion, err = strconv.ParseUint(tmp, 10, 64) + if err != nil { + return fmt.Errorf("%w `%v`", errSDPInvalidNumericValue, tmp) + } + + if value == "-0" { // live reporter app + value = "- 0" + } + + i = stringsReverseIndexByte(value, ' ') + if i < 0 { + return nil + } + + tmp, value = value[i+1:], value[:i] + + switch { + case strings.HasPrefix(tmp, "0x"), strings.HasPrefix(tmp, "0X"): + s.Origin.SessionID, err = strconv.ParseUint(tmp[2:], 16, 64) + case strings.ContainsAny(tmp, "abcdefABCDEF"): + s.Origin.SessionID, err = strconv.ParseUint(tmp, 16, 64) + default: + if i := strings.Index(tmp, "."); i >= 0 { + tmp = tmp[:i] + } + tmp = strings.TrimPrefix(tmp, "-") + + s.Origin.SessionID, err = strconv.ParseUint(tmp, 10, 64) + } + if err != nil { + return fmt.Errorf("%w `%v`", errSDPInvalidNumericValue, tmp) + } + + s.Origin.Username = value + + return nil +} + +func (s *SessionDescription) unmarshalSessionInformation(value string) error { + sessionInformation := psdp.Information(value) + s.SessionInformation = &sessionInformation + return nil +} + +func (s *SessionDescription) unmarshalURI(value string) error { + var err error + s.URI, err = url.Parse(value) + if err != nil { + return err + } + + return nil +} + +func (s *SessionDescription) unmarshalEmail(value string) error { + emailAddress := psdp.EmailAddress(value) + s.EmailAddress = &emailAddress + return nil +} + +func (s *SessionDescription) unmarshalPhone(value string) error { + phoneNumber := psdp.PhoneNumber(value) + s.PhoneNumber = &phoneNumber + return nil +} + +func unmarshalConnectionInformation(value string) (*psdp.ConnectionInformation, error) { + value = strings.Replace(value, "IN IPV4 ", "IN IP4 ", 1) + + if strings.HasPrefix(value, "IN c=IN") { + value = value[len("IN c="):] + } + + fields := strings.Fields(value) + if len(fields) < 2 { + return nil, fmt.Errorf("%w `c=%v`", errSDPInvalidSyntax, fields) + } + + // Set according to currently registered with IANA + // https://tools.ietf.org/html/rfc4566#section-8.2.6 + if i := indexOf(strings.ToUpper(fields[0]), []string{"IN"}); i == -1 { + return nil, fmt.Errorf("%w `%v`", errSDPInvalidValue, fields[0]) + } + + // Set according to currently registered with IANA + // https://tools.ietf.org/html/rfc4566#section-8.2.7 + if i := indexOf(fields[1], []string{"IP4", "IP6"}); i == -1 { + return nil, fmt.Errorf("%w `%v`", errSDPInvalidValue, fields[1]) + } + + connAddr := new(psdp.Address) + if len(fields) > 2 { + connAddr.Address = fields[2] + } + + return &psdp.ConnectionInformation{ + NetworkType: strings.ToUpper(fields[0]), + AddressType: fields[1], + Address: connAddr, + }, nil +} + +func (s *SessionDescription) unmarshalSessionConnectionInformation(value string) error { + var err error + s.ConnectionInformation, err = unmarshalConnectionInformation(value) + if err != nil { + return fmt.Errorf("%w `c=%v`", errSDPInvalidSyntax, value) + } + + return nil +} + +func unmarshalBandwidth(value string) (*psdp.Bandwidth, error) { + parts := strings.Split(value, ":") + if len(parts) != 2 { + return nil, fmt.Errorf("%w `b=%v`", errSDPInvalidValue, parts) + } + + experimental := strings.HasPrefix(parts[0], "X-") + if experimental { + parts[0] = strings.TrimPrefix(parts[0], "X-") + } else if !anyOf(parts[0], "CT", "AS", "TIAS", "RS", "RR") { + // Set according to currently registered with IANA + // https://tools.ietf.org/html/rfc4566#section-5.8 + // https://tools.ietf.org/html/rfc3890#section-6.2 + // https://tools.ietf.org/html/rfc3556#section-2 + return nil, fmt.Errorf("%w `%v`", errSDPInvalidValue, parts[0]) + } + + bandwidth, err := strconv.ParseUint(parts[1], 10, 64) + if err != nil { + return nil, fmt.Errorf("%w `%v`", errSDPInvalidNumericValue, parts[1]) + } + + return &psdp.Bandwidth{ + Experimental: experimental, + Type: parts[0], + Bandwidth: bandwidth, + }, nil +} + +func (s *SessionDescription) unmarshalSessionBandwidth(value string) error { + bandwidth, err := unmarshalBandwidth(value) + if err != nil { + return fmt.Errorf("%w `b=%v`", errSDPInvalidValue, value) + } + s.Bandwidth = append(s.Bandwidth, *bandwidth) + + return nil +} + +func (s *SessionDescription) unmarshalTimeZones(value string) error { + // These fields are transimitted in pairs + // z= .... + // so we are making sure that there are actually multiple of 2 total. + fields := strings.Fields(value) + if len(fields)%2 != 0 { + return fmt.Errorf("%w `t=%v`", errSDPInvalidSyntax, fields) + } + + for i := 0; i < len(fields); i += 2 { + var timeZone psdp.TimeZone + + var err error + timeZone.AdjustmentTime, err = strconv.ParseUint(fields[i], 10, 64) + if err != nil { + return fmt.Errorf("%w `%v`", errSDPInvalidValue, fields) + } + + timeZone.Offset, err = parseTimeUnits(fields[i+1]) + if err != nil { + return err + } + + s.TimeZones = append(s.TimeZones, timeZone) + } + + return nil +} + +func (s *SessionDescription) unmarshalSessionEncryptionKey(value string) error { + encryptionKey := psdp.EncryptionKey(value) + s.EncryptionKey = &encryptionKey + return nil +} + +func (s *SessionDescription) unmarshalSessionAttribute(value string) error { + i := strings.IndexRune(value, ':') + var a psdp.Attribute + if i > 0 { + a = psdp.NewAttribute(value[:i], value[i+1:]) + } else { + a = psdp.NewPropertyAttribute(value) + } + + s.Attributes = append(s.Attributes, a) + return nil +} + +func (s *SessionDescription) unmarshalTiming(value string) error { + if value == "now-" { + // special case for some FLIR cameras with invalid timing element + value = "0 0" + } + fields := strings.Fields(value) + if len(fields) < 2 { + return fmt.Errorf("%w `t=%v`", errSDPInvalidSyntax, fields) + } + + td := psdp.TimeDescription{} + + var err error + td.Timing.StartTime, err = strconv.ParseUint(fields[0], 10, 64) + if err != nil { + return fmt.Errorf("%w `%v`", errSDPInvalidNumericValue, fields[1]) + } + + td.Timing.StopTime, err = strconv.ParseUint(fields[1], 10, 64) + if err != nil { + return fmt.Errorf("%w `%v`", errSDPInvalidNumericValue, fields[1]) + } + + s.TimeDescriptions = append(s.TimeDescriptions, td) + + return nil +} + +func parseTimeUnits(value string) (int64, error) { + // Some time offsets in the protocol can be provided with a shorthand + // notation. This code ensures to convert it to NTP timestamp format. + // d - days (86400 seconds) + // h - hours (3600 seconds) + // m - minutes (60 seconds) + // s - seconds (allowed for completeness) + switch value[len(value)-1:] { + case "d": + num, err := strconv.ParseInt(value[:len(value)-1], 10, 64) + if err != nil { + return 0, fmt.Errorf("%w `%v`", errSDPInvalidValue, value) + } + return num * 86400, nil + case "h": + num, err := strconv.ParseInt(value[:len(value)-1], 10, 64) + if err != nil { + return 0, fmt.Errorf("%w `%v`", errSDPInvalidValue, value) + } + return num * 3600, nil + case "m": + num, err := strconv.ParseInt(value[:len(value)-1], 10, 64) + if err != nil { + return 0, fmt.Errorf("%w `%v`", errSDPInvalidValue, value) + } + return num * 60, nil + } + + num, err := strconv.ParseInt(value, 10, 64) + if err != nil { + return 0, fmt.Errorf("%w `%v`", errSDPInvalidValue, value) + } + + return num, nil +} + +func (s *SessionDescription) unmarshalRepeatTimes(value string) error { + fields := strings.Fields(value) + if len(fields) < 3 { + return fmt.Errorf("%w `r=%v`", errSDPInvalidSyntax, value) + } + + latestTimeDesc := &s.TimeDescriptions[len(s.TimeDescriptions)-1] + + newRepeatTime := psdp.RepeatTime{} + var err error + newRepeatTime.Interval, err = parseTimeUnits(fields[0]) + if err != nil { + return fmt.Errorf("%w `%v`", errSDPInvalidValue, fields) + } + + newRepeatTime.Duration, err = parseTimeUnits(fields[1]) + if err != nil { + return fmt.Errorf("%w `%v`", errSDPInvalidValue, fields) + } + + for i := 2; i < len(fields); i++ { + offset, err := parseTimeUnits(fields[i]) + if err != nil { + return fmt.Errorf("%w `%v`", errSDPInvalidValue, fields) + } + newRepeatTime.Offsets = append(newRepeatTime.Offsets, offset) + } + latestTimeDesc.RepeatTimes = append(latestTimeDesc.RepeatTimes, newRepeatTime) + + return nil +} + +func (s *SessionDescription) unmarshalMediaDescription(value string) error { + fields := strings.Fields(value) + if len(fields) < 4 { + return fmt.Errorf("%w `m=%v`", errSDPInvalidSyntax, fields) + } + + newMediaDesc := &psdp.MediaDescription{} + + // + // Set according to currently registered with IANA + // https://tools.ietf.org/html/rfc4566#section-5.14 + if fields[0] != "video" && + fields[0] != "audio" && + fields[0] != "application" && + !strings.HasPrefix(fields[0], "application/") && + fields[0] != "metadata" && + fields[0] != "text" { + return fmt.Errorf("%w `%v`", errSDPInvalidValue, fields[0]) + } + newMediaDesc.MediaName.Media = fields[0] + + // + parts := strings.Split(fields[1], "/") + var err error + newMediaDesc.MediaName.Port.Value, err = parsePort(parts[0]) + if err != nil { + return fmt.Errorf("%w `%v`", errSDPInvalidPortValue, parts[0]) + } + + if len(parts) > 1 { + portRange, err := strconv.Atoi(parts[1]) + if err != nil { + return fmt.Errorf("%w `%v`", errSDPInvalidValue, parts) + } + newMediaDesc.MediaName.Port.Range = &portRange + } + + // + // Set according to currently registered with IANA + // https://tools.ietf.org/html/rfc4566#section-5.14 + for _, proto := range strings.Split(fields[2], "/") { + if i := indexOf(proto, []string{ + "UDP", "RTP", "AVP", "SAVP", "SAVPF", + "MP2T", "TLS", "DTLS", "SCTP", "AVPF", "TCP", + }); i == -1 { + return fmt.Errorf("%w `%v`", errSDPInvalidNumericValue, fields[2]) + } + newMediaDesc.MediaName.Protos = append(newMediaDesc.MediaName.Protos, proto) + } + + // ... + for i := 3; i < len(fields); i++ { + newMediaDesc.MediaName.Formats = append(newMediaDesc.MediaName.Formats, fields[i]) + } + + s.MediaDescriptions = append(s.MediaDescriptions, newMediaDesc) + + return nil +} + +func (s *SessionDescription) unmarshalMediaTitle(value string) error { + latestMediaDesc := s.MediaDescriptions[len(s.MediaDescriptions)-1] + mediaTitle := psdp.Information(value) + latestMediaDesc.MediaTitle = &mediaTitle + return nil +} + +func (s *SessionDescription) unmarshalMediaConnectionInformation(value string) error { + if strings.HasPrefix(value, "SM ") { + return nil + } + + latestMediaDesc := s.MediaDescriptions[len(s.MediaDescriptions)-1] + var err error + latestMediaDesc.ConnectionInformation, err = unmarshalConnectionInformation(value) + if err != nil { + return fmt.Errorf("%w `c=%v`", errSDPInvalidSyntax, value) + } + + return nil +} + +func (s *SessionDescription) unmarshalMediaBandwidth(value string) error { + latestMediaDesc := s.MediaDescriptions[len(s.MediaDescriptions)-1] + bandwidth, err := unmarshalBandwidth(value) + if err != nil { + return fmt.Errorf("%w `b=%v`", errSDPInvalidSyntax, value) + } + latestMediaDesc.Bandwidth = append(latestMediaDesc.Bandwidth, *bandwidth) + return nil +} + +func (s *SessionDescription) unmarshalMediaEncryptionKey(value string) error { + latestMediaDesc := s.MediaDescriptions[len(s.MediaDescriptions)-1] + encryptionKey := psdp.EncryptionKey(value) + latestMediaDesc.EncryptionKey = &encryptionKey + return nil +} + +func (s *SessionDescription) unmarshalMediaAttribute(value string) error { + i := strings.IndexRune(value, ':') + var a psdp.Attribute + if i > 0 { + a = psdp.NewAttribute(value[:i], value[i+1:]) + } else { + a = psdp.NewPropertyAttribute(value) + } + + latestMediaDesc := s.MediaDescriptions[len(s.MediaDescriptions)-1] + latestMediaDesc.Attributes = append(latestMediaDesc.Attributes, a) + return nil +} + +type unmarshalState int + +const ( + stateInitial unmarshalState = iota + stateSession + stateMedia + stateTimeDescription +) + +func (s *SessionDescription) unmarshalSession(state *unmarshalState, key byte, val string) error { + switch key { + case 'o': + err := s.unmarshalOrigin(val) + if err != nil { + return err + } + + case 's': + err := s.unmarshalSessionName(val) + if err != nil { + return err + } + + case 'i': + err := s.unmarshalSessionInformation(val) + if err != nil { + return err + } + + case 'u': + err := s.unmarshalURI(val) + if err != nil { + return err + } + + case 'e': + err := s.unmarshalEmail(val) + if err != nil { + return err + } + + case 'p': + err := s.unmarshalPhone(val) + if err != nil { + return err + } + + case 'c': + err := s.unmarshalSessionConnectionInformation(val) + if err != nil { + return err + } + + case 'b': + err := s.unmarshalSessionBandwidth(val) + if err != nil { + return err + } + + case 'z': + err := s.unmarshalTimeZones(val) + if err != nil { + return err + } + + case 'k': + err := s.unmarshalSessionEncryptionKey(val) + if err != nil { + return err + } + + case 'a': + err := s.unmarshalSessionAttribute(val) + if err != nil { + return err + } + + case 't': + err := s.unmarshalTiming(val) + if err != nil { + return err + } + *state = stateTimeDescription + + case 'm': + err := s.unmarshalMediaDescription(val) + if err != nil { + return err + } + *state = stateMedia + + default: + return fmt.Errorf("invalid key: %c", key) + } + + return nil +} + +func (s *SessionDescription) unmarshalMedia(key byte, val string) error { + switch key { + case 'm': + err := s.unmarshalMediaDescription(val) + if err != nil { + return err + } + + case 'i': + err := s.unmarshalMediaTitle(val) + if err != nil { + return err + } + + case 'c': + err := s.unmarshalMediaConnectionInformation(val) + if err != nil { + return err + } + + case 'b': + err := s.unmarshalMediaBandwidth(val) + if err != nil { + return err + } + + case 'k': + err := s.unmarshalMediaEncryptionKey(val) + if err != nil { + return err + } + + case 'a': + err := s.unmarshalMediaAttribute(val) + if err != nil { + return err + } + + default: + return fmt.Errorf("invalid key: %c", key) + } + + return nil +} + +// Unmarshal decodes a SessionDescription. +// This is rewritten from scratch to guarantee compatibility with most RTSP +// implementations. +func (s *SessionDescription) Unmarshal(byts []byte) error { + str := string(byts) + + state := stateInitial + + for _, line := range strings.Split(strings.ReplaceAll(str, "\r", ""), "\n") { + if line == "" { + continue + } + + if len(line) < 2 || line[1] != '=' { + return fmt.Errorf("invalid line: (%s)", line) + } + + key := line[0] + val := line[2:] + + switch state { + case stateInitial: + switch key { + case 'v': + err := s.unmarshalProtocolVersion(val) + if err != nil { + return err + } + state = stateSession + + default: + state = stateSession + err := s.unmarshalSession(&state, key, val) + if err != nil { + return err + } + } + + case stateSession: + err := s.unmarshalSession(&state, key, val) + if err != nil { + return err + } + + case stateMedia: + err := s.unmarshalMedia(key, val) + if err != nil { + return err + } + + case stateTimeDescription: + switch key { + case 'r': + err := s.unmarshalRepeatTimes(val) + if err != nil { + return err + } + + default: + state = stateSession + err := s.unmarshalSession(&state, key, val) + if err != nil { + return err + } + } + } + } + + return nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/restrict_network.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/restrict_network.go new file mode 100644 index 000000000..b191d8fad --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/restrict_network.go @@ -0,0 +1,17 @@ +package gortsplib + +import ( + "net" +) + +// do not listen on IPv6 when address is 0.0.0.0. +func restrictNetwork(network string, address string) (string, string) { + host, _, err := net.SplitHostPort(address) + if err == nil { + if host == "0.0.0.0" { + return network + "4", address + } + } + + return network, address +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server.go new file mode 100644 index 000000000..3a21f4d83 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server.go @@ -0,0 +1,533 @@ +package gortsplib + +import ( + "context" + "crypto/tls" + "fmt" + "net" + "strconv" + "sync" + "time" + + "github.com/bluenviron/gortsplib/v4/pkg/auth" + "github.com/bluenviron/gortsplib/v4/pkg/base" + "github.com/bluenviron/gortsplib/v4/pkg/liberrors" +) + +const ( + serverHeader = "gortsplib" + serverAuthRealm = "ipcam" +) + +func extractPort(address string) (int, error) { + _, tmp, err := net.SplitHostPort(address) + if err != nil { + return 0, err + } + + tmp2, err := strconv.ParseUint(tmp, 10, 16) + if err != nil { + return 0, err + } + + return int(tmp2), nil +} + +type sessionRequestRes struct { + ss *ServerSession + res *base.Response + err error +} + +type sessionRequestReq struct { + sc *ServerConn + req *base.Request + id string + create bool + res chan sessionRequestRes +} + +type chGetMulticastIPReq struct { + res chan net.IP +} + +// Server is a RTSP server. +type Server struct { + // + // RTSP parameters (all optional except RTSPAddress) + // + // the RTSP address of the server, to accept connections and send and receive + // packets with the TCP transport. + RTSPAddress string + // a port to send and receive RTP packets with the UDP transport. + // If UDPRTPAddress and UDPRTCPAddress are filled, the server can support the UDP transport. + UDPRTPAddress string + // a port to send and receive RTCP packets with the UDP transport. + // If UDPRTPAddress and UDPRTCPAddress are filled, the server can support the UDP transport. + UDPRTCPAddress string + // a range of multicast IPs to use with the UDP-multicast transport. + // If MulticastIPRange, MulticastRTPPort, MulticastRTCPPort are filled, the server + // can support the UDP-multicast transport. + MulticastIPRange string + // a port to send RTP packets with the UDP-multicast transport. + // If MulticastIPRange, MulticastRTPPort, MulticastRTCPPort are filled, the server + // can support the UDP-multicast transport. + MulticastRTPPort int + // a port to send RTCP packets with the UDP-multicast transport. + // If MulticastIPRange, MulticastRTPPort, MulticastRTCPPort are filled, the server + // can support the UDP-multicast transport. + MulticastRTCPPort int + // timeout of read operations. + // It defaults to 10 seconds + ReadTimeout time.Duration + // timeout of write operations. + // It defaults to 10 seconds + WriteTimeout time.Duration + // a TLS configuration to accept TLS (RTSPS) connections. + TLSConfig *tls.Config + // Size of the queue of outgoing packets. + // It defaults to 256. + WriteQueueSize int + // maximum size of outgoing RTP / RTCP packets. + // This must be less than the UDP MTU (1472 bytes). + // It defaults to 1472. + MaxPacketSize int + // disable automatic RTCP sender reports. + DisableRTCPSenderReports bool + // authentication methods. + // It defaults to plain and digest+MD5. + AuthMethods []auth.VerifyMethod + + // + // handler (optional) + // + // an handler to handle server events. + // It may implement one or more of the ServerHandler* interfaces. + Handler ServerHandler + + // + // system functions (all optional) + // + // function used to initialize the TCP listener. + // It defaults to net.Listen. + Listen func(network string, address string) (net.Listener, error) + // function used to initialize UDP listeners. + // It defaults to net.ListenPacket. + ListenPacket func(network, address string) (net.PacketConn, error) + + // + // private + // + + timeNow func() time.Time + senderReportPeriod time.Duration + receiverReportPeriod time.Duration + sessionTimeout time.Duration + checkStreamPeriod time.Duration + + ctx context.Context + ctxCancel func() + wg sync.WaitGroup + multicastNet *net.IPNet + multicastNextIP net.IP + tcpListener *serverTCPListener + udpRTPListener *serverUDPListener + udpRTCPListener *serverUDPListener + sessions map[string]*ServerSession + conns map[*ServerConn]struct{} + closeError error + + // in + chNewConn chan net.Conn + chAcceptErr chan error + chCloseConn chan *ServerConn + chHandleRequest chan sessionRequestReq + chCloseSession chan *ServerSession + chGetMulticastIP chan chGetMulticastIPReq +} + +// Start starts the server. +func (s *Server) Start() error { + // RTSP parameters + if s.ReadTimeout == 0 { + s.ReadTimeout = 10 * time.Second + } + if s.WriteTimeout == 0 { + s.WriteTimeout = 10 * time.Second + } + if s.WriteQueueSize == 0 { + s.WriteQueueSize = 256 + } else if (s.WriteQueueSize & (s.WriteQueueSize - 1)) != 0 { + return fmt.Errorf("WriteQueueSize must be a power of two") + } + if s.MaxPacketSize == 0 { + s.MaxPacketSize = udpMaxPayloadSize + } else if s.MaxPacketSize > udpMaxPayloadSize { + return fmt.Errorf("MaxPacketSize must be less than %d", udpMaxPayloadSize) + } + if len(s.AuthMethods) == 0 { + // disable VerifyMethodDigestSHA256 unless explicitly set + // since it prevents FFmpeg from authenticating + s.AuthMethods = []auth.VerifyMethod{auth.VerifyMethodBasic, auth.VerifyMethodDigestMD5} + } + + // system functions + if s.Listen == nil { + s.Listen = net.Listen + } + if s.ListenPacket == nil { + s.ListenPacket = net.ListenPacket + } + + // private + if s.timeNow == nil { + s.timeNow = time.Now + } + if s.senderReportPeriod == 0 { + s.senderReportPeriod = 10 * time.Second + } + if s.receiverReportPeriod == 0 { + s.receiverReportPeriod = 10 * time.Second + } + if s.sessionTimeout == 0 { + s.sessionTimeout = 1 * 60 * time.Second + } + if s.checkStreamPeriod == 0 { + s.checkStreamPeriod = 1 * time.Second + } + + if s.TLSConfig != nil && s.UDPRTPAddress != "" { + return fmt.Errorf("TLS can't be used with UDP") + } + + if s.TLSConfig != nil && s.MulticastIPRange != "" { + return fmt.Errorf("TLS can't be used with UDP-multicast") + } + + if s.RTSPAddress == "" { + return fmt.Errorf("RTSPAddress not provided") + } + + if (s.UDPRTPAddress != "" && s.UDPRTCPAddress == "") || + (s.UDPRTPAddress == "" && s.UDPRTCPAddress != "") { + return fmt.Errorf("UDPRTPAddress and UDPRTCPAddress must be used together") + } + + if s.UDPRTPAddress != "" { + rtpPort, err := extractPort(s.UDPRTPAddress) + if err != nil { + return err + } + + rtcpPort, err := extractPort(s.UDPRTCPAddress) + if err != nil { + return err + } + + if (rtpPort % 2) != 0 { + return fmt.Errorf("RTP port must be even") + } + + if rtcpPort != (rtpPort + 1) { + return fmt.Errorf("RTP and RTCP ports must be consecutive") + } + + s.udpRTPListener = &serverUDPListener{ + listenPacket: s.ListenPacket, + writeTimeout: s.WriteTimeout, + multicastEnable: false, + address: s.UDPRTPAddress, + } + err = s.udpRTPListener.initialize() + if err != nil { + return err + } + + s.udpRTCPListener = &serverUDPListener{ + listenPacket: s.ListenPacket, + writeTimeout: s.WriteTimeout, + multicastEnable: false, + address: s.UDPRTCPAddress, + } + err = s.udpRTCPListener.initialize() + if err != nil { + s.udpRTPListener.close() + return err + } + } + + if s.MulticastIPRange != "" && (s.MulticastRTPPort == 0 || s.MulticastRTCPPort == 0) || + (s.MulticastRTPPort != 0 && (s.MulticastRTCPPort == 0 || s.MulticastIPRange == "")) || + s.MulticastRTCPPort != 0 && (s.MulticastRTPPort == 0 || s.MulticastIPRange == "") { + if s.udpRTPListener != nil { + s.udpRTPListener.close() + } + if s.udpRTCPListener != nil { + s.udpRTCPListener.close() + } + return fmt.Errorf("MulticastIPRange, MulticastRTPPort and MulticastRTCPPort must be used together") + } + + if s.MulticastIPRange != "" { + if (s.MulticastRTPPort % 2) != 0 { + if s.udpRTPListener != nil { + s.udpRTPListener.close() + } + if s.udpRTCPListener != nil { + s.udpRTCPListener.close() + } + return fmt.Errorf("RTP port must be even") + } + + if s.MulticastRTCPPort != (s.MulticastRTPPort + 1) { + if s.udpRTPListener != nil { + s.udpRTPListener.close() + } + if s.udpRTCPListener != nil { + s.udpRTCPListener.close() + } + return fmt.Errorf("RTP and RTCP ports must be consecutive") + } + + var err error + _, s.multicastNet, err = net.ParseCIDR(s.MulticastIPRange) + if err != nil { + if s.udpRTPListener != nil { + s.udpRTPListener.close() + } + if s.udpRTCPListener != nil { + s.udpRTCPListener.close() + } + return err + } + + s.multicastNextIP = s.multicastNet.IP + } + + s.ctx, s.ctxCancel = context.WithCancel(context.Background()) + + s.sessions = make(map[string]*ServerSession) + s.conns = make(map[*ServerConn]struct{}) + s.chNewConn = make(chan net.Conn) + s.chAcceptErr = make(chan error) + s.chCloseConn = make(chan *ServerConn) + s.chHandleRequest = make(chan sessionRequestReq) + s.chCloseSession = make(chan *ServerSession) + s.chGetMulticastIP = make(chan chGetMulticastIPReq) + + s.tcpListener = &serverTCPListener{ + s: s, + } + err := s.tcpListener.initialize() + if err != nil { + if s.udpRTPListener != nil { + s.udpRTPListener.close() + } + if s.udpRTCPListener != nil { + s.udpRTCPListener.close() + } + s.ctxCancel() + return err + } + + s.wg.Add(1) + go s.run() + + return nil +} + +// Close closes all the server resources and waits for them to close. +func (s *Server) Close() { + s.ctxCancel() + s.wg.Wait() +} + +// Wait waits until all server resources are closed. +// This can happen when a fatal error occurs or when Close() is called. +func (s *Server) Wait() error { + s.wg.Wait() + return s.closeError +} + +func (s *Server) run() { + defer s.wg.Done() + + s.closeError = s.runInner() + + s.ctxCancel() + + if s.udpRTCPListener != nil { + s.udpRTCPListener.close() + } + + if s.udpRTPListener != nil { + s.udpRTPListener.close() + } + + s.tcpListener.close() +} + +func (s *Server) runInner() error { + for { + select { + case err := <-s.chAcceptErr: + return err + + case nconn := <-s.chNewConn: + sc := &ServerConn{ + s: s, + nconn: nconn, + } + sc.initialize() + s.conns[sc] = struct{}{} + + case sc := <-s.chCloseConn: + if _, ok := s.conns[sc]; !ok { + continue + } + delete(s.conns, sc) + sc.Close() + + case req := <-s.chHandleRequest: + if ss, ok := s.sessions[req.id]; ok { + if !req.sc.ip().Equal(ss.author.ip()) || + req.sc.zone() != ss.author.zone() { + req.res <- sessionRequestRes{ + res: &base.Response{ + StatusCode: base.StatusBadRequest, + }, + err: liberrors.ErrServerCannotUseSessionCreatedByOtherIP{}, + } + continue + } + + select { + case ss.chHandleRequest <- req: + case <-ss.ctx.Done(): + req.res <- sessionRequestRes{ + res: &base.Response{ + StatusCode: base.StatusBadRequest, + }, + err: liberrors.ErrServerTerminated{}, + } + } + } else { + if !req.create { + req.res <- sessionRequestRes{ + res: &base.Response{ + StatusCode: base.StatusSessionNotFound, + }, + err: liberrors.ErrServerSessionNotFound{}, + } + continue + } + + ss := &ServerSession{ + s: s, + author: req.sc, + } + ss.initialize() + s.sessions[ss.secretID] = ss + + select { + case ss.chHandleRequest <- req: + case <-ss.ctx.Done(): + req.res <- sessionRequestRes{ + res: &base.Response{ + StatusCode: base.StatusBadRequest, + }, + err: liberrors.ErrServerTerminated{}, + } + } + } + + case ss := <-s.chCloseSession: + if sss, ok := s.sessions[ss.secretID]; !ok || sss != ss { + continue + } + delete(s.sessions, ss.secretID) + ss.Close() + + case req := <-s.chGetMulticastIP: + ip32 := uint32(s.multicastNextIP[0])<<24 | uint32(s.multicastNextIP[1])<<16 | + uint32(s.multicastNextIP[2])<<8 | uint32(s.multicastNextIP[3]) + mask := uint32(s.multicastNet.Mask[0])<<24 | uint32(s.multicastNet.Mask[1])<<16 | + uint32(s.multicastNet.Mask[2])<<8 | uint32(s.multicastNet.Mask[3]) + ip32 = (ip32 & mask) | ((ip32 + 1) & ^mask) + ip := make(net.IP, 4) + ip[0] = byte(ip32 >> 24) + ip[1] = byte(ip32 >> 16) + ip[2] = byte(ip32 >> 8) + ip[3] = byte(ip32) + s.multicastNextIP = ip + req.res <- ip + + case <-s.ctx.Done(): + return liberrors.ErrServerTerminated{} + } + } +} + +// StartAndWait starts the server and waits until a fatal error. +func (s *Server) StartAndWait() error { + err := s.Start() + if err != nil { + return err + } + defer s.Close() + + return s.Wait() +} + +func (s *Server) getMulticastIP() (net.IP, error) { + res := make(chan net.IP) + select { + case s.chGetMulticastIP <- chGetMulticastIPReq{res: res}: + return <-res, nil + + case <-s.ctx.Done(): + return nil, liberrors.ErrServerTerminated{} + } +} + +func (s *Server) newConn(nconn net.Conn) { + select { + case s.chNewConn <- nconn: + case <-s.ctx.Done(): + nconn.Close() + } +} + +func (s *Server) acceptErr(err error) { + select { + case s.chAcceptErr <- err: + case <-s.ctx.Done(): + } +} + +func (s *Server) closeConn(sc *ServerConn) { + select { + case s.chCloseConn <- sc: + case <-s.ctx.Done(): + } +} + +func (s *Server) closeSession(ss *ServerSession) { + select { + case s.chCloseSession <- ss: + case <-s.ctx.Done(): + } +} + +func (s *Server) handleRequest(req sessionRequestReq) (*base.Response, *ServerSession, error) { + select { + case s.chHandleRequest <- req: + res := <-req.res + return res.res, res.ss, res.err + + case <-s.ctx.Done(): + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, req.sc.session, liberrors.ErrServerTerminated{} + } +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_conn.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_conn.go new file mode 100644 index 000000000..50f24d853 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_conn.go @@ -0,0 +1,528 @@ +package gortsplib + +import ( + "context" + "crypto/tls" + "errors" + "net" + gourl "net/url" + "strconv" + "strings" + "time" + + "github.com/bluenviron/gortsplib/v4/pkg/auth" + "github.com/bluenviron/gortsplib/v4/pkg/base" + "github.com/bluenviron/gortsplib/v4/pkg/bytecounter" + "github.com/bluenviron/gortsplib/v4/pkg/conn" + "github.com/bluenviron/gortsplib/v4/pkg/description" + "github.com/bluenviron/gortsplib/v4/pkg/headers" + "github.com/bluenviron/gortsplib/v4/pkg/liberrors" +) + +func getSessionID(header base.Header) string { + if h, ok := header["Session"]; ok && len(h) == 1 { + return h[0] + } + return "" +} + +func serverSideDescription(d *description.Session) *description.Session { + out := &description.Session{ + Title: d.Title, + FECGroups: d.FECGroups, + Medias: make([]*description.Media, len(d.Medias)), + } + + for i, medi := range d.Medias { + out.Medias[i] = &description.Media{ + Type: medi.Type, + ID: medi.ID, + IsBackChannel: medi.IsBackChannel, + // we have to use trackID=number in order to support clients + // like the Grandstream GXV3500. + Control: "trackID=" + strconv.FormatInt(int64(i), 10), + Formats: medi.Formats, + } + } + + return out +} + +func credentialsProvided(req *base.Request) bool { + var auth headers.Authorization + err := auth.Unmarshal(req.Header["Authorization"]) + return err == nil && auth.Username != "" +} + +type readReq struct { + req *base.Request + res chan error +} + +// ServerConn is a server-side RTSP connection. +type ServerConn struct { + s *Server + nconn net.Conn + + ctx context.Context + ctxCancel func() + userData interface{} + remoteAddr *net.TCPAddr + bc *bytecounter.ByteCounter + conn *conn.Conn + session *ServerSession + reader *serverConnReader + authNonce string + + // in + chRemoveSession chan *ServerSession + + // out + done chan struct{} +} + +func (sc *ServerConn) initialize() { + ctx, ctxCancel := context.WithCancel(sc.s.ctx) + + if sc.s.TLSConfig != nil { + sc.nconn = tls.Server(sc.nconn, sc.s.TLSConfig) + } + + sc.bc = bytecounter.New(sc.nconn, nil, nil) + sc.ctx = ctx + sc.ctxCancel = ctxCancel + sc.remoteAddr = sc.nconn.RemoteAddr().(*net.TCPAddr) + sc.chRemoveSession = make(chan *ServerSession) + sc.done = make(chan struct{}) + + sc.s.wg.Add(1) + go sc.run() +} + +// Close closes the ServerConn. +func (sc *ServerConn) Close() { + sc.ctxCancel() +} + +// NetConn returns the underlying net.Conn. +func (sc *ServerConn) NetConn() net.Conn { + return sc.nconn +} + +// BytesReceived returns the number of read bytes. +// +// Deprecated: replaced by Stats() +func (sc *ServerConn) BytesReceived() uint64 { + return sc.bc.BytesReceived() +} + +// BytesSent returns the number of written bytes. +// +// Deprecated: replaced by Stats() +func (sc *ServerConn) BytesSent() uint64 { + return sc.bc.BytesSent() +} + +// SetUserData sets some user data associated with the connection. +func (sc *ServerConn) SetUserData(v interface{}) { + sc.userData = v +} + +// UserData returns some user data associated with the connection. +func (sc *ServerConn) UserData() interface{} { + return sc.userData +} + +// Session returns associated session. +func (sc *ServerConn) Session() *ServerSession { + return sc.session +} + +// Stats returns connection statistics. +func (sc *ServerConn) Stats() *StatsConn { + return &StatsConn{ + BytesReceived: sc.bc.BytesReceived(), + BytesSent: sc.bc.BytesSent(), + } +} + +// VerifyCredentials verifies credentials provided by the user. +func (sc *ServerConn) VerifyCredentials( + req *base.Request, + expectedUser string, + expectedPass string, +) bool { + // we do not support using an empty string as user + // since it interferes with credentialsProvided() + if expectedUser == "" { + return false + } + + if sc.authNonce == "" { + n, err := auth.GenerateNonce() + if err != nil { + return false + } + sc.authNonce = n + } + + err := auth.Verify( + req, + expectedUser, + expectedPass, + sc.s.AuthMethods, + serverAuthRealm, + sc.authNonce) + + return (err == nil) +} + +func (sc *ServerConn) handleAuthError(req *base.Request, res *base.Response) error { + // if credentials have not been provided, clear error and send the WWW-Authenticate header. + if !credentialsProvided(req) { + res.Header["WWW-Authenticate"] = auth.GenerateWWWAuthenticate(sc.s.AuthMethods, serverAuthRealm, sc.authNonce) + return nil + } + + // if credentials have been provided (and are wrong), close the connection. + return liberrors.ErrServerAuth{} +} + +func (sc *ServerConn) ip() net.IP { + return sc.remoteAddr.IP +} + +func (sc *ServerConn) zone() string { + return sc.remoteAddr.Zone +} + +func (sc *ServerConn) run() { + defer sc.s.wg.Done() + defer close(sc.done) + + if h, ok := sc.s.Handler.(ServerHandlerOnConnOpen); ok { + h.OnConnOpen(&ServerHandlerOnConnOpenCtx{ + Conn: sc, + }) + } + + sc.conn = conn.NewConn(sc.bc) + sc.reader = &serverConnReader{ + sc: sc, + } + sc.reader.initialize() + + err := sc.runInner() + + sc.ctxCancel() + + sc.nconn.Close() + + if sc.reader != nil { + sc.reader.wait() + } + + if sc.session != nil { + sc.session.removeConn(sc) + } + + sc.s.closeConn(sc) + + if h, ok := sc.s.Handler.(ServerHandlerOnConnClose); ok { + h.OnConnClose(&ServerHandlerOnConnCloseCtx{ + Conn: sc, + Error: err, + }) + } +} + +func (sc *ServerConn) runInner() error { + for { + select { + case req := <-sc.reader.chRequest: + req.res <- sc.handleRequestOuter(req.req) + + case err := <-sc.reader.chError: + sc.reader = nil + return err + + case ss := <-sc.chRemoveSession: + if sc.session == ss { + sc.session = nil + } + + case <-sc.ctx.Done(): + return liberrors.ErrServerTerminated{} + } + } +} + +func (sc *ServerConn) handleRequestInner(req *base.Request) (*base.Response, error) { + if cseq, ok := req.Header["CSeq"]; !ok || len(cseq) != 1 { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, liberrors.ErrServerCSeqMissing{} + } + + if req.Method != base.Options && req.URL == nil { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, liberrors.ErrServerInvalidPath{} + } + + sxID := getSessionID(req.Header) + + var path string + var query string + + switch req.Method { + case base.Describe, base.GetParameter, base.SetParameter: + path, query = getPathAndQuery(req.URL, false) + } + + switch req.Method { + case base.Options: + if sxID != "" { + return sc.handleRequestInSession(sxID, req, false) + } + + var methods []string + if _, ok := sc.s.Handler.(ServerHandlerOnDescribe); ok { + methods = append(methods, string(base.Describe)) + } + if _, ok := sc.s.Handler.(ServerHandlerOnAnnounce); ok { + methods = append(methods, string(base.Announce)) + } + if _, ok := sc.s.Handler.(ServerHandlerOnSetup); ok { + methods = append(methods, string(base.Setup)) + } + if _, ok := sc.s.Handler.(ServerHandlerOnPlay); ok { + methods = append(methods, string(base.Play)) + } + if _, ok := sc.s.Handler.(ServerHandlerOnRecord); ok { + methods = append(methods, string(base.Record)) + } + if _, ok := sc.s.Handler.(ServerHandlerOnPause); ok { + methods = append(methods, string(base.Pause)) + } + methods = append(methods, string(base.GetParameter)) + if _, ok := sc.s.Handler.(ServerHandlerOnSetParameter); ok { + methods = append(methods, string(base.SetParameter)) + } + methods = append(methods, string(base.Teardown)) + + return &base.Response{ + StatusCode: base.StatusOK, + Header: base.Header{ + "Public": base.HeaderValue{strings.Join(methods, ", ")}, + }, + }, nil + + case base.Describe: + if h, ok := sc.s.Handler.(ServerHandlerOnDescribe); ok { + res, stream, err := h.OnDescribe(&ServerHandlerOnDescribeCtx{ + Conn: sc, + Request: req, + Path: path, + Query: query, + }) + + if res.StatusCode == base.StatusOK { + if stream == nil && len(res.Body) == 0 { + panic("stream should be not nil or response body should be nonempty when StatusCode is StatusOK") + } + + if res.Header == nil { + res.Header = make(base.Header) + } + + res.Header["Content-Base"] = base.HeaderValue{req.URL.String() + "/"} + res.Header["Content-Type"] = base.HeaderValue{"application/sdp"} + + if stream == nil { + return res, err + } + + // VLC uses multicast if the SDP contains a multicast address. + // therefore, we introduce a special query (vlcmulticast) that allows + // to return a SDP that contains a multicast address. + multicast := false + if sc.s.MulticastIPRange != "" { + if q, err2 := gourl.ParseQuery(query); err2 == nil { + if _, ok := q["vlcmulticast"]; ok { + multicast = true + } + } + } + + byts, _ := serverSideDescription(stream.Desc).Marshal(multicast) + res.Body = byts + } + + return res, err + } + + case base.Announce: + if _, ok := sc.s.Handler.(ServerHandlerOnAnnounce); ok { + return sc.handleRequestInSession(sxID, req, true) + } + + case base.Setup: + if _, ok := sc.s.Handler.(ServerHandlerOnSetup); ok { + return sc.handleRequestInSession(sxID, req, true) + } + + case base.Play: + if sxID != "" { + if _, ok := sc.s.Handler.(ServerHandlerOnPlay); ok { + return sc.handleRequestInSession(sxID, req, false) + } + } + + case base.Record: + if sxID != "" { + if _, ok := sc.s.Handler.(ServerHandlerOnRecord); ok { + return sc.handleRequestInSession(sxID, req, false) + } + } + + case base.Pause: + if sxID != "" { + if _, ok := sc.s.Handler.(ServerHandlerOnPause); ok { + return sc.handleRequestInSession(sxID, req, false) + } + } + + case base.Teardown: + if sxID != "" { + return sc.handleRequestInSession(sxID, req, false) + } + + case base.GetParameter: + if sxID != "" { + return sc.handleRequestInSession(sxID, req, false) + } + + if h, ok := sc.s.Handler.(ServerHandlerOnGetParameter); ok { + return h.OnGetParameter(&ServerHandlerOnGetParameterCtx{ + Conn: sc, + Request: req, + Path: path, + Query: query, + }) + } + + case base.SetParameter: + if sxID != "" { + return sc.handleRequestInSession(sxID, req, false) + } + + if h, ok := sc.s.Handler.(ServerHandlerOnSetParameter); ok { + return h.OnSetParameter(&ServerHandlerOnSetParameterCtx{ + Conn: sc, + Request: req, + Path: path, + Query: query, + }) + } + } + + return &base.Response{ + StatusCode: base.StatusNotImplemented, + }, nil +} + +func (sc *ServerConn) handleRequestOuter(req *base.Request) error { + if h, ok := sc.s.Handler.(ServerHandlerOnRequest); ok { + h.OnRequest(sc, req) + } + + res, err := sc.handleRequestInner(req) + + if res.Header == nil { + res.Header = make(base.Header) + } + + // handle auth errors + var eerr1 liberrors.ErrServerAuth + if errors.As(err, &eerr1) { + err = sc.handleAuthError(req, res) + } + + // add cseq + var eerr2 liberrors.ErrServerCSeqMissing + if !errors.As(err, &eerr2) { + res.Header["CSeq"] = req.Header["CSeq"] + } + + // add server + res.Header["Server"] = base.HeaderValue{serverHeader} + + if h, ok := sc.s.Handler.(ServerHandlerOnResponse); ok { + h.OnResponse(sc, res) + } + + sc.nconn.SetWriteDeadline(time.Now().Add(sc.s.WriteTimeout)) + err2 := sc.conn.WriteResponse(res) + if err == nil && err2 != nil { + err = err2 + } + + return err +} + +func (sc *ServerConn) handleRequestInSession( + sxID string, + req *base.Request, + create bool, +) (*base.Response, error) { + // handle directly in Session + if sc.session != nil { + // session ID is optional in SETUP and ANNOUNCE requests, since + // client may not have received the session ID yet due to multiple reasons: + // * requests can be retries after code 301 + // * SETUP requests comes after ANNOUNCE response, that don't contain the session ID + if sxID != "" { + // the connection can't communicate with two sessions at once. + if sxID != sc.session.secretID { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, liberrors.ErrServerLinkedToOtherSession{} + } + } + + cres := make(chan sessionRequestRes) + sreq := sessionRequestReq{ + sc: sc, + req: req, + id: sxID, + create: create, + res: cres, + } + + res, session, err := sc.session.handleRequest(sreq) + sc.session = session + return res, err + } + + // otherwise, pass through Server + cres := make(chan sessionRequestRes) + sreq := sessionRequestReq{ + sc: sc, + req: req, + id: sxID, + create: create, + res: cres, + } + + res, session, err := sc.s.handleRequest(sreq) + sc.session = session + return res, err +} + +func (sc *ServerConn) removeSession(ss *ServerSession) { + select { + case sc.chRemoveSession <- ss: + case <-sc.ctx.Done(): + } +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_conn_reader.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_conn_reader.go new file mode 100644 index 000000000..4329881a9 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_conn_reader.go @@ -0,0 +1,138 @@ +package gortsplib + +import ( + "errors" + "fmt" + "time" + + "github.com/bluenviron/gortsplib/v4/pkg/base" + "github.com/bluenviron/gortsplib/v4/pkg/liberrors" +) + +type switchReadFuncError struct { + tcp bool +} + +func (switchReadFuncError) Error() string { + return "switching read function" +} + +func isSwitchReadFuncError(err error) bool { + var eerr switchReadFuncError + return errors.As(err, &eerr) +} + +type serverConnReader struct { + sc *ServerConn + + chRequest chan readReq + chError chan error +} + +func (cr *serverConnReader) initialize() { + cr.chRequest = make(chan readReq) + cr.chError = make(chan error) + + go cr.run() +} + +func (cr *serverConnReader) wait() { + for { + select { + case <-cr.chError: + return + + case req := <-cr.chRequest: + req.res <- fmt.Errorf("terminated") + } + } +} + +func (cr *serverConnReader) run() { + readFunc := cr.readFuncStandard + + for { + err := readFunc() + + var eerr switchReadFuncError + if errors.As(err, &eerr) { + if eerr.tcp { + readFunc = cr.readFuncTCP + } else { + readFunc = cr.readFuncStandard + } + continue + } + + cr.chError <- err + break + } +} + +func (cr *serverConnReader) readFuncStandard() error { + // reset deadline + cr.sc.nconn.SetReadDeadline(time.Time{}) + + for { + what, err := cr.sc.conn.Read() + if err != nil { + return err + } + + switch what := what.(type) { + case *base.Request: + cres := make(chan error) + req := readReq{req: what, res: cres} + cr.chRequest <- req + + err := <-cres + if err != nil { + return err + } + + case *base.Response: + return liberrors.ErrServerUnexpectedResponse{} + + case *base.InterleavedFrame: + return liberrors.ErrServerUnexpectedFrame{} + } + } +} + +func (cr *serverConnReader) readFuncTCP() error { + // reset deadline + cr.sc.nconn.SetReadDeadline(time.Time{}) + + cr.sc.session.asyncStartWriter() + + for { + if cr.sc.session.state == ServerSessionStateRecord { + cr.sc.nconn.SetReadDeadline(time.Now().Add(cr.sc.s.ReadTimeout)) + } + + what, err := cr.sc.conn.Read() + if err != nil { + return err + } + + switch what := what.(type) { + case *base.Request: + cres := make(chan error) + req := readReq{req: what, res: cres} + cr.chRequest <- req + + err := <-cres + if err != nil { + return err + } + + case *base.Response: + return liberrors.ErrServerUnexpectedResponse{} + + case *base.InterleavedFrame: + if cb, ok := cr.sc.session.tcpCallbackByChannel[what.Channel]; ok { + cb(what.Payload) + } + } + } +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_handler.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_handler.go new file mode 100644 index 000000000..389dcc166 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_handler.go @@ -0,0 +1,245 @@ +package gortsplib + +import ( + "github.com/bluenviron/gortsplib/v4/pkg/base" + "github.com/bluenviron/gortsplib/v4/pkg/description" +) + +// ServerHandler is the interface implemented by all the server handlers. +type ServerHandler interface{} + +// ServerHandlerOnConnOpenCtx is the context of OnConnOpen. +type ServerHandlerOnConnOpenCtx struct { + Conn *ServerConn +} + +// ServerHandlerOnConnOpen can be implemented by a ServerHandler. +type ServerHandlerOnConnOpen interface { + // called when a connection is opened. + OnConnOpen(*ServerHandlerOnConnOpenCtx) +} + +// ServerHandlerOnConnCloseCtx is the context of OnConnClose. +type ServerHandlerOnConnCloseCtx struct { + Conn *ServerConn + Error error +} + +// ServerHandlerOnConnClose can be implemented by a ServerHandler. +type ServerHandlerOnConnClose interface { + // called when a connection is closed. + OnConnClose(*ServerHandlerOnConnCloseCtx) +} + +// ServerHandlerOnSessionOpenCtx is the context OnSessionOpen. +type ServerHandlerOnSessionOpenCtx struct { + Session *ServerSession + Conn *ServerConn +} + +// ServerHandlerOnSessionOpen can be implemented by a ServerHandler. +type ServerHandlerOnSessionOpen interface { + // called when a session is opened. + OnSessionOpen(*ServerHandlerOnSessionOpenCtx) +} + +// ServerHandlerOnSessionCloseCtx is the context of ServerHandlerOnSessionClose. +type ServerHandlerOnSessionCloseCtx struct { + Session *ServerSession + Error error +} + +// ServerHandlerOnSessionClose can be implemented by a ServerHandler. +type ServerHandlerOnSessionClose interface { + // called when a session is closed. + OnSessionClose(*ServerHandlerOnSessionCloseCtx) +} + +// ServerHandlerOnRequest can be implemented by a ServerHandler. +type ServerHandlerOnRequest interface { + // called when receiving a request from a connection. + OnRequest(*ServerConn, *base.Request) +} + +// ServerHandlerOnResponse can be implemented by a ServerHandler. +type ServerHandlerOnResponse interface { + // called when sending a response to a connection. + OnResponse(*ServerConn, *base.Response) +} + +// ServerHandlerOnDescribeCtx is the context of OnDescribe. +type ServerHandlerOnDescribeCtx struct { + Conn *ServerConn + Request *base.Request + Path string + Query string +} + +// ServerHandlerOnDescribe can be implemented by a ServerHandler. +type ServerHandlerOnDescribe interface { + // called when receiving a DESCRIBE request. + OnDescribe(*ServerHandlerOnDescribeCtx) (*base.Response, *ServerStream, error) +} + +// ServerHandlerOnAnnounceCtx is the context of OnAnnounce. +type ServerHandlerOnAnnounceCtx struct { + Session *ServerSession + Conn *ServerConn + Request *base.Request + Path string + Query string + Description *description.Session +} + +// ServerHandlerOnAnnounce can be implemented by a ServerHandler. +type ServerHandlerOnAnnounce interface { + // called when receiving an ANNOUNCE request. + OnAnnounce(*ServerHandlerOnAnnounceCtx) (*base.Response, error) +} + +// ServerHandlerOnSetupCtx is the context of OnSetup. +type ServerHandlerOnSetupCtx struct { + Session *ServerSession + Conn *ServerConn + Request *base.Request + Path string + Query string + Transport Transport +} + +// ServerHandlerOnSetup can be implemented by a ServerHandler. +type ServerHandlerOnSetup interface { + // called when receiving a SETUP request. + // must return a Response and a stream. + // the stream is needed to + // - add the session the the stream's readers + // - send the stream SSRC to the session + OnSetup(*ServerHandlerOnSetupCtx) (*base.Response, *ServerStream, error) +} + +// ServerHandlerOnPlayCtx is the context of OnPlay. +type ServerHandlerOnPlayCtx struct { + Session *ServerSession + Conn *ServerConn + Request *base.Request + Path string + Query string +} + +// ServerHandlerOnPlay can be implemented by a ServerHandler. +type ServerHandlerOnPlay interface { + // called when receiving a PLAY request. + OnPlay(*ServerHandlerOnPlayCtx) (*base.Response, error) +} + +// ServerHandlerOnRecordCtx is the context of OnRecord. +type ServerHandlerOnRecordCtx struct { + Session *ServerSession + Conn *ServerConn + Request *base.Request + Path string + Query string +} + +// ServerHandlerOnRecord can be implemented by a ServerHandler. +type ServerHandlerOnRecord interface { + // called when receiving a RECORD request. + OnRecord(*ServerHandlerOnRecordCtx) (*base.Response, error) +} + +// ServerHandlerOnPauseCtx is the context of OnPause. +type ServerHandlerOnPauseCtx struct { + Session *ServerSession + Conn *ServerConn + Request *base.Request + Path string + Query string +} + +// ServerHandlerOnPause can be implemented by a ServerHandler. +type ServerHandlerOnPause interface { + // called when receiving a PAUSE request. + OnPause(*ServerHandlerOnPauseCtx) (*base.Response, error) +} + +// ServerHandlerOnGetParameterCtx is the context of OnGetParameter. +type ServerHandlerOnGetParameterCtx struct { + Session *ServerSession + Conn *ServerConn + Request *base.Request + Path string + Query string +} + +// ServerHandlerOnGetParameter can be implemented by a ServerHandler. +type ServerHandlerOnGetParameter interface { + // called when receiving a GET_PARAMETER request. + OnGetParameter(*ServerHandlerOnGetParameterCtx) (*base.Response, error) +} + +// ServerHandlerOnSetParameterCtx is the context of OnSetParameter. +type ServerHandlerOnSetParameterCtx struct { + Session *ServerSession + Conn *ServerConn + Request *base.Request + Path string + Query string +} + +// ServerHandlerOnSetParameter can be implemented by a ServerHandler. +type ServerHandlerOnSetParameter interface { + // called when receiving a SET_PARAMETER request. + OnSetParameter(*ServerHandlerOnSetParameterCtx) (*base.Response, error) +} + +// ServerHandlerOnPacketLostCtx is the context of OnPacketLost. +// +// Deprecated: replaced by ServerHandlerOnPacketsLostCtx +type ServerHandlerOnPacketLostCtx struct { + Session *ServerSession + Error error +} + +// ServerHandlerOnPacketLost can be implemented by a ServerHandler. +// +// Deprecated: replaced by ServerHandlerOnPacketsLost +type ServerHandlerOnPacketLost interface { + // called when the server detects lost packets. + OnPacketLost(*ServerHandlerOnPacketLostCtx) +} + +// ServerHandlerOnPacketsLostCtx is the context of OnPacketsLost. +type ServerHandlerOnPacketsLostCtx struct { + Session *ServerSession + Lost uint64 +} + +// ServerHandlerOnPacketsLost can be implemented by a ServerHandler. +type ServerHandlerOnPacketsLost interface { + // called when the server detects lost packets. + OnPacketsLost(*ServerHandlerOnPacketsLostCtx) +} + +// ServerHandlerOnDecodeErrorCtx is the context of OnDecodeError. +type ServerHandlerOnDecodeErrorCtx struct { + Session *ServerSession + Error error +} + +// ServerHandlerOnDecodeError can be implemented by a ServerHandler. +type ServerHandlerOnDecodeError interface { + // called when a non-fatal decode error occurs. + OnDecodeError(*ServerHandlerOnDecodeErrorCtx) +} + +// ServerHandlerOnStreamWriteErrorCtx is the context of OnStreamWriteError. +type ServerHandlerOnStreamWriteErrorCtx struct { + Session *ServerSession + Error error +} + +// ServerHandlerOnStreamWriteError can be implemented by a ServerHandler. +type ServerHandlerOnStreamWriteError interface { + // called when a ServerStream is unable to write packets to a session. + OnStreamWriteError(*ServerHandlerOnStreamWriteErrorCtx) +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_multicast_writer.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_multicast_writer.go new file mode 100644 index 000000000..e8bb1d8ce --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_multicast_writer.go @@ -0,0 +1,90 @@ +package gortsplib + +import ( + "net" + + "github.com/bluenviron/gortsplib/v4/pkg/liberrors" +) + +type serverMulticastWriter struct { + s *Server + + rtpl *serverUDPListener + rtcpl *serverUDPListener + writer *asyncProcessor + rtpAddr *net.UDPAddr + rtcpAddr *net.UDPAddr +} + +func (h *serverMulticastWriter) initialize() error { + ip, err := h.s.getMulticastIP() + if err != nil { + return err + } + + rtpl, rtcpl, err := createUDPListenerMulticastPair( + h.s.ListenPacket, + h.s.WriteTimeout, + h.s.MulticastRTPPort, + h.s.MulticastRTCPPort, + ip, + ) + if err != nil { + return err + } + + rtpAddr := &net.UDPAddr{ + IP: rtpl.ip(), + Port: rtpl.port(), + } + + rtcpAddr := &net.UDPAddr{ + IP: rtcpl.ip(), + Port: rtcpl.port(), + } + + h.rtpl = rtpl + h.rtcpl = rtcpl + h.rtpAddr = rtpAddr + h.rtcpAddr = rtcpAddr + + h.writer = &asyncProcessor{ + bufferSize: h.s.WriteQueueSize, + } + h.writer.initialize() + h.writer.start() + + return nil +} + +func (h *serverMulticastWriter) close() { + h.rtpl.close() + h.rtcpl.close() + h.writer.close() +} + +func (h *serverMulticastWriter) ip() net.IP { + return h.rtpl.ip() +} + +func (h *serverMulticastWriter) writePacketRTP(byts []byte) error { + ok := h.writer.push(func() error { + return h.rtpl.write(byts, h.rtpAddr) + }) + if !ok { + return liberrors.ErrServerWriteQueueFull{} + } + + return nil +} + +func (h *serverMulticastWriter) writePacketRTCP(byts []byte) error { + ok := h.writer.push(func() error { + return h.rtcpl.write(byts, h.rtcpAddr) + }) + if !ok { + return liberrors.ErrServerWriteQueueFull{} + } + + return nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_session.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_session.go new file mode 100644 index 000000000..beca57a87 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_session.go @@ -0,0 +1,1593 @@ +package gortsplib + +import ( + "context" + "fmt" + "log" + "net" + "strconv" + "strings" + "sync" + "sync/atomic" + "time" + + "github.com/google/uuid" + "github.com/pion/rtcp" + "github.com/pion/rtp" + + "github.com/bluenviron/gortsplib/v4/pkg/base" + "github.com/bluenviron/gortsplib/v4/pkg/description" + "github.com/bluenviron/gortsplib/v4/pkg/format" + "github.com/bluenviron/gortsplib/v4/pkg/headers" + "github.com/bluenviron/gortsplib/v4/pkg/liberrors" + "github.com/bluenviron/gortsplib/v4/pkg/rtcpreceiver" + "github.com/bluenviron/gortsplib/v4/pkg/rtcpsender" + "github.com/bluenviron/gortsplib/v4/pkg/rtptime" + "github.com/bluenviron/gortsplib/v4/pkg/sdp" +) + +type readFunc func([]byte) bool + +func stringsReverseIndex(s, substr string) int { + for i := len(s) - 1 - len(substr); i >= 0; i-- { + if s[i:i+len(substr)] == substr { + return i + } + } + return -1 +} + +// used for all methods except SETUP +func getPathAndQuery(u *base.URL, isAnnounce bool) (string, string) { + if !isAnnounce { + // FFmpeg format + if strings.HasSuffix(u.RawQuery, "/") { + return u.Path, u.RawQuery[:len(u.RawQuery)-1] + } + + // GStreamer format + if len(u.Path) > 1 && strings.HasSuffix(u.Path, "/") { + return u.Path[:len(u.Path)-1], u.RawQuery + } + } + + return u.Path, u.RawQuery +} + +// used for SETUP when playing +func getPathAndQueryAndTrackID(u *base.URL) (string, string, string, error) { + // FFmpeg format + i := stringsReverseIndex(u.RawQuery, "/trackID=") + if i >= 0 { + path := u.Path + query := u.RawQuery[:i] + trackID := u.RawQuery[i+len("/trackID="):] + return path, query, trackID, nil + } + + // GStreamer format + i = stringsReverseIndex(u.Path, "/trackID=") + if i >= 0 { + path := u.Path[:i] + query := u.RawQuery + trackID := u.Path[i+len("/trackID="):] + return path, query, trackID, nil + } + + // no track ID and a trailing slash. + // this happens when trying to read a MPEG-TS stream with FFmpeg. + if strings.HasSuffix(u.RawQuery, "/") { + return u.Path, u.RawQuery[:len(u.RawQuery)-1], "0", nil + } + if len(u.Path) >= 1 && strings.HasSuffix(u.Path[1:], "/") { + return u.Path[:len(u.Path)-1], u.RawQuery, "0", nil + } + + // special case for empty path + if u.Path == "" || u.Path == "/" { + return u.Path, u.RawQuery, "0", nil + } + + // no slash at the end of the path. + return "", "", "", liberrors.ErrServerInvalidSetupPath{} +} + +// used for SETUP when recording +func findMediaByURL( + medias []*description.Media, + path string, + query string, + u *base.URL, +) *description.Media { + for _, media := range medias { + if strings.HasPrefix(media.Control, "rtsp://") || + strings.HasPrefix(media.Control, "rtsps://") { + if media.Control == u.String() { + return media + } + } else { + // FFmpeg format + u1 := &base.URL{ + Scheme: u.Scheme, + Host: u.Host, + Path: path, + RawQuery: query, + } + if query != "" { + u1.RawQuery += "/" + media.Control + } else { + u1.Path += "/" + media.Control + } + if u1.String() == u.String() { + return media + } + + // GStreamer format + u2 := &base.URL{ + Scheme: u.Scheme, + Host: u.Host, + Path: path + "/" + media.Control, + RawQuery: query, + } + if u2.String() == u.String() { + return media + } + } + } + + return nil +} + +func findMediaByTrackID(medias []*description.Media, trackID string) *description.Media { + if trackID == "" { + return medias[0] + } + + tmp, err := strconv.ParseUint(trackID, 10, 31) + if err != nil { + return nil + } + id := int(tmp) + + if len(medias) <= id { + return nil + } + + return medias[id] +} + +func findFirstSupportedTransportHeader(s *Server, tsh headers.Transports) *headers.Transport { + // Per RFC2326 section 12.39, client specifies transports in order of preference. + // Filter out the ones we don't support and then pick first supported transport. + for _, tr := range tsh { + isMulticast := tr.Delivery != nil && *tr.Delivery == headers.TransportDeliveryMulticast + if tr.Protocol == headers.TransportProtocolUDP && + ((!isMulticast && s.udpRTPListener == nil) || + (isMulticast && s.MulticastIPRange == "")) { + continue + } + return &tr + } + return nil +} + +func generateRTPInfo( + now time.Time, + setuppedMediasOrdered []*serverSessionMedia, + setuppedStream *ServerStream, + setuppedPath string, + u *base.URL, +) (headers.RTPInfo, bool) { + var ri headers.RTPInfo + + for _, sm := range setuppedMediasOrdered { + entry := setuppedStream.rtpInfoEntry(sm.media, now) + if entry == nil { + entry = &headers.RTPInfoEntry{} + } + entry.URL = (&base.URL{ + Scheme: u.Scheme, + Host: u.Host, + Path: setuppedPath + "/trackID=" + + strconv.FormatInt(int64(setuppedStream.medias[sm.media].trackID), 10), + }).String() + ri = append(ri, entry) + } + + if len(ri) == 0 { + return nil, false + } + + return ri, true +} + +// ServerSessionState is a state of a ServerSession. +type ServerSessionState int + +// states. +const ( + ServerSessionStateInitial ServerSessionState = iota + ServerSessionStatePrePlay + ServerSessionStatePlay + ServerSessionStatePreRecord + ServerSessionStateRecord +) + +// String implements fmt.Stringer. +func (s ServerSessionState) String() string { + switch s { + case ServerSessionStateInitial: + return "initial" + case ServerSessionStatePrePlay: + return "prePlay" + case ServerSessionStatePlay: + return "play" + case ServerSessionStatePreRecord: + return "preRecord" + case ServerSessionStateRecord: + return "record" + } + return "unknown" +} + +// ServerSession is a server-side RTSP session. +type ServerSession struct { + s *Server + author *ServerConn + + secretID string // must not be shared, allows to take ownership of the session + ctx context.Context + ctxCancel func() + userData interface{} + conns map[*ServerConn]struct{} + state ServerSessionState + setuppedMedias map[*description.Media]*serverSessionMedia + setuppedMediasOrdered []*serverSessionMedia + tcpCallbackByChannel map[int]readFunc + setuppedTransport *Transport + setuppedStream *ServerStream // read + setuppedPath string + setuppedQuery string + lastRequestTime time.Time + tcpConn *ServerConn + announcedDesc *description.Session // publish + udpLastPacketTime *int64 // publish + udpCheckStreamTimer *time.Timer + writer *asyncProcessor + writerMutex sync.RWMutex + timeDecoder *rtptime.GlobalDecoder2 + tcpFrame *base.InterleavedFrame + tcpBuffer []byte + + // in + chHandleRequest chan sessionRequestReq + chRemoveConn chan *ServerConn + chAsyncStartWriter chan struct{} +} + +func (ss *ServerSession) initialize() { + ctx, ctxCancel := context.WithCancel(ss.s.ctx) + + // use an UUID without dashes, since dashes confuse some clients. + secretID := strings.ReplaceAll(uuid.New().String(), "-", "") + + ss.secretID = secretID + ss.ctx = ctx + ss.ctxCancel = ctxCancel + ss.conns = make(map[*ServerConn]struct{}) + ss.lastRequestTime = ss.s.timeNow() + ss.udpCheckStreamTimer = emptyTimer() + + ss.chHandleRequest = make(chan sessionRequestReq) + ss.chRemoveConn = make(chan *ServerConn) + ss.chAsyncStartWriter = make(chan struct{}) + + ss.s.wg.Add(1) + go ss.run() +} + +// Close closes the ServerSession. +func (ss *ServerSession) Close() { + ss.ctxCancel() +} + +// BytesReceived returns the number of read bytes. +// +// Deprecated: replaced by Stats() +func (ss *ServerSession) BytesReceived() uint64 { + v := uint64(0) + for _, sm := range ss.setuppedMedias { + v += atomic.LoadUint64(sm.bytesReceived) + } + return v +} + +// BytesSent returns the number of written bytes. +// +// Deprecated: replaced by Stats() +func (ss *ServerSession) BytesSent() uint64 { + v := uint64(0) + for _, sm := range ss.setuppedMedias { + v += atomic.LoadUint64(sm.bytesSent) + } + return v +} + +// State returns the state of the session. +func (ss *ServerSession) State() ServerSessionState { + return ss.state +} + +// SetuppedTransport returns the transport negotiated during SETUP. +func (ss *ServerSession) SetuppedTransport() *Transport { + return ss.setuppedTransport +} + +// SetuppedStream returns the stream associated with the session. +func (ss *ServerSession) SetuppedStream() *ServerStream { + return ss.setuppedStream +} + +// SetuppedPath returns the path sent during SETUP or ANNOUNCE. +func (ss *ServerSession) SetuppedPath() string { + return ss.setuppedPath +} + +// SetuppedQuery returns the query sent during SETUP or ANNOUNCE. +func (ss *ServerSession) SetuppedQuery() string { + return ss.setuppedQuery +} + +// AnnouncedDescription returns the announced stream description. +func (ss *ServerSession) AnnouncedDescription() *description.Session { + return ss.announcedDesc +} + +// SetuppedMedias returns the setupped medias. +func (ss *ServerSession) SetuppedMedias() []*description.Media { + ret := make([]*description.Media, len(ss.setuppedMedias)) + for i, sm := range ss.setuppedMediasOrdered { + ret[i] = sm.media + } + return ret +} + +// SetUserData sets some user data associated with the session. +func (ss *ServerSession) SetUserData(v interface{}) { + ss.userData = v +} + +// UserData returns some user data associated with the session. +func (ss *ServerSession) UserData() interface{} { + return ss.userData +} + +// Stats returns server session statistics. +func (ss *ServerSession) Stats() *StatsSession { + return &StatsSession{ + BytesReceived: func() uint64 { + v := uint64(0) + for _, sm := range ss.setuppedMedias { + v += atomic.LoadUint64(sm.bytesReceived) + } + return v + }(), + BytesSent: func() uint64 { + v := uint64(0) + for _, sm := range ss.setuppedMedias { + v += atomic.LoadUint64(sm.bytesSent) + } + return v + }(), + RTPPacketsReceived: func() uint64 { + v := uint64(0) + for _, sm := range ss.setuppedMedias { + for _, f := range sm.formats { + v += atomic.LoadUint64(f.rtpPacketsReceived) + } + } + return v + }(), + RTPPacketsSent: func() uint64 { + v := uint64(0) + for _, sm := range ss.setuppedMedias { + for _, f := range sm.formats { + v += atomic.LoadUint64(f.rtpPacketsSent) + } + } + return v + }(), + RTPPacketsLost: func() uint64 { + v := uint64(0) + for _, sm := range ss.setuppedMedias { + for _, f := range sm.formats { + v += atomic.LoadUint64(f.rtpPacketsLost) + } + } + return v + }(), + RTPPacketsInError: func() uint64 { + v := uint64(0) + for _, sm := range ss.setuppedMedias { + v += atomic.LoadUint64(sm.rtpPacketsInError) + } + return v + }(), + RTPPacketsJitter: func() float64 { + v := float64(0) + n := float64(0) + for _, sm := range ss.setuppedMedias { + for _, fo := range sm.formats { + if fo.rtcpReceiver != nil { + stats := fo.rtcpReceiver.Stats() + if stats != nil { + v += stats.Jitter + n++ + } + } + } + } + if n != 0 { + return v / n + } + return 0 + }(), + RTCPPacketsReceived: func() uint64 { + v := uint64(0) + for _, sm := range ss.setuppedMedias { + v += atomic.LoadUint64(sm.rtcpPacketsReceived) + } + return v + }(), + RTCPPacketsSent: func() uint64 { + v := uint64(0) + for _, sm := range ss.setuppedMedias { + v += atomic.LoadUint64(sm.rtcpPacketsSent) + } + return v + }(), + RTCPPacketsInError: func() uint64 { + v := uint64(0) + for _, sm := range ss.setuppedMedias { + v += atomic.LoadUint64(sm.rtcpPacketsInError) + } + return v + }(), + Medias: func() map[*description.Media]StatsSessionMedia { //nolint:dupl + ret := make(map[*description.Media]StatsSessionMedia, len(ss.setuppedMedias)) + + for med, sm := range ss.setuppedMedias { + ret[med] = StatsSessionMedia{ + BytesReceived: atomic.LoadUint64(sm.bytesReceived), + BytesSent: atomic.LoadUint64(sm.bytesSent), + RTPPacketsInError: atomic.LoadUint64(sm.rtpPacketsInError), + RTCPPacketsReceived: atomic.LoadUint64(sm.rtcpPacketsReceived), + RTCPPacketsSent: atomic.LoadUint64(sm.rtcpPacketsSent), + RTCPPacketsInError: atomic.LoadUint64(sm.rtcpPacketsInError), + Formats: func() map[format.Format]StatsSessionFormat { + ret := make(map[format.Format]StatsSessionFormat, len(sm.formats)) + + for _, fo := range sm.formats { + recvStats := func() *rtcpreceiver.Stats { + if fo.rtcpReceiver != nil { + return fo.rtcpReceiver.Stats() + } + return nil + }() + rtcpSender := func() *rtcpsender.RTCPSender { + if ss.setuppedStream != nil { + return ss.setuppedStream.medias[med].formats[fo.format.PayloadType()].rtcpSender + } + return nil + }() + sentStats := func() *rtcpsender.Stats { + if rtcpSender != nil { + return rtcpSender.Stats() + } + return nil + }() + + ret[fo.format] = StatsSessionFormat{ //nolint:dupl + RTPPacketsReceived: atomic.LoadUint64(fo.rtpPacketsReceived), + RTPPacketsSent: atomic.LoadUint64(fo.rtpPacketsSent), + RTPPacketsLost: atomic.LoadUint64(fo.rtpPacketsLost), + LocalSSRC: func() uint32 { + if fo.rtcpReceiver != nil { + return *fo.rtcpReceiver.LocalSSRC + } + if sentStats != nil { + return sentStats.LocalSSRC + } + return 0 + }(), + RemoteSSRC: func() uint32 { + if recvStats != nil { + return recvStats.RemoteSSRC + } + return 0 + }(), + RTPPacketsLastSequenceNumber: func() uint16 { + if recvStats != nil { + return recvStats.LastSequenceNumber + } + if sentStats != nil { + return sentStats.LastSequenceNumber + } + return 0 + }(), + RTPPacketsLastRTP: func() uint32 { + if recvStats != nil { + return recvStats.LastRTP + } + if sentStats != nil { + return sentStats.LastRTP + } + return 0 + }(), + RTPPacketsLastNTP: func() time.Time { + if recvStats != nil { + return recvStats.LastNTP + } + if sentStats != nil { + return sentStats.LastNTP + } + return time.Time{} + }(), + RTPPacketsJitter: func() float64 { + if recvStats != nil { + return recvStats.Jitter + } + return 0 + }(), + } + } + + return ret + }(), + } + } + + return ret + }(), + } +} + +func (ss *ServerSession) onStreamWriteError(err error) { + if h, ok := ss.s.Handler.(ServerHandlerOnStreamWriteError); ok { + h.OnStreamWriteError(&ServerHandlerOnStreamWriteErrorCtx{ + Session: ss, + Error: err, + }) + } else { + log.Println(err.Error()) + } +} + +func (ss *ServerSession) checkState(allowed map[ServerSessionState]struct{}) error { + if _, ok := allowed[ss.state]; ok { + return nil + } + + allowedList := make([]fmt.Stringer, len(allowed)) + i := 0 + for a := range allowed { + allowedList[i] = a + i++ + } + return liberrors.ErrServerInvalidState{AllowedList: allowedList, State: ss.state} +} + +func (ss *ServerSession) createWriter() { + ss.writerMutex.Lock() + + ss.writer = &asyncProcessor{ + bufferSize: func() int { + if ss.state == ServerSessionStatePrePlay { + return ss.s.WriteQueueSize + } + + // when recording, writeBuffer is only used to send RTCP receiver reports, + // that are much smaller than RTP packets and are sent at a fixed interval. + // decrease RAM consumption by allocating less buffers. + return 8 + }(), + } + + ss.writer.initialize() + + ss.writerMutex.Unlock() +} + +func (ss *ServerSession) startWriter() { + ss.writer.start() +} + +func (ss *ServerSession) destroyWriter() { + ss.writer.close() + + ss.writerMutex.Lock() + ss.writer = nil + ss.writerMutex.Unlock() +} + +func (ss *ServerSession) run() { + defer ss.s.wg.Done() + + if h, ok := ss.s.Handler.(ServerHandlerOnSessionOpen); ok { + h.OnSessionOpen(&ServerHandlerOnSessionOpenCtx{ + Session: ss, + Conn: ss.author, + }) + } + + err := ss.runInner() + + ss.ctxCancel() + + // close all associated connections, both UDP and TCP + // except for the ones that called TEARDOWN + // (that are detached from the session just after the request) + for sc := range ss.conns { + sc.Close() + + // make sure that OnFrame() is never called after OnSessionClose() + <-sc.done + + sc.removeSession(ss) + } + + if ss.setuppedStream != nil { + ss.setuppedStream.readerSetInactive(ss) + ss.setuppedStream.readerRemove(ss) + } + + for _, sm := range ss.setuppedMedias { + sm.stop() + } + + if ss.writer != nil { + ss.destroyWriter() + } + + ss.s.closeSession(ss) + + if h, ok := ss.s.Handler.(ServerHandlerOnSessionClose); ok { + h.OnSessionClose(&ServerHandlerOnSessionCloseCtx{ + Session: ss, + Error: err, + }) + } +} + +func (ss *ServerSession) runInner() error { + for { + chWriterError := func() chan struct{} { + if ss.writer != nil { + return ss.writer.chStopped + } + return nil + }() + + select { + case req := <-ss.chHandleRequest: + ss.lastRequestTime = ss.s.timeNow() + + if _, ok := ss.conns[req.sc]; !ok { + ss.conns[req.sc] = struct{}{} + } + + res, err := ss.handleRequestInner(req.sc, req.req) + + returnedSession := ss + + if err == nil || isSwitchReadFuncError(err) { + // ANNOUNCE responses don't contain the session header. + if req.req.Method != base.Announce && + req.req.Method != base.Teardown { + if res.Header == nil { + res.Header = make(base.Header) + } + + res.Header["Session"] = headers.Session{ + Session: ss.secretID, + Timeout: func() *uint { + // timeout controls the sending of RTCP keepalives. + // these are needed only when the client is playing + // and transport is UDP or UDP-multicast. + if (ss.state == ServerSessionStatePrePlay || + ss.state == ServerSessionStatePlay) && + (*ss.setuppedTransport == TransportUDP || + *ss.setuppedTransport == TransportUDPMulticast) { + v := uint(ss.s.sessionTimeout / time.Second) + return &v + } + return nil + }(), + }.Marshal() + } + + // after a TEARDOWN, session must be unpaired with the connection + if req.req.Method == base.Teardown { + delete(ss.conns, req.sc) + returnedSession = nil + } + } + + savedMethod := req.req.Method + + req.res <- sessionRequestRes{ + res: res, + err: err, + ss: returnedSession, + } + + if (err == nil || isSwitchReadFuncError(err)) && savedMethod == base.Teardown { + return liberrors.ErrServerSessionTornDown{Author: req.sc.NetConn().RemoteAddr()} + } + + case sc := <-ss.chRemoveConn: + delete(ss.conns, sc) + + // if session is not in state RECORD or PLAY, or transport is TCP, + // and there are no associated connections, + // close the session. + if ((ss.state != ServerSessionStateRecord && + ss.state != ServerSessionStatePlay) || + *ss.setuppedTransport == TransportTCP) && + len(ss.conns) == 0 { + return liberrors.ErrServerSessionNotInUse{} + } + + case <-ss.chAsyncStartWriter: + if (ss.state == ServerSessionStateRecord || + ss.state == ServerSessionStatePlay) && + *ss.setuppedTransport == TransportTCP { + ss.startWriter() + } + + case <-ss.udpCheckStreamTimer.C: + now := ss.s.timeNow() + + lft := atomic.LoadInt64(ss.udpLastPacketTime) + + // in case of RECORD, timeout happens when no RTP or RTCP packets are being received + if ss.state == ServerSessionStateRecord { + if now.Sub(time.Unix(lft, 0)) >= ss.s.ReadTimeout { + return liberrors.ErrServerSessionTimedOut{} + } + + // in case of PLAY, timeout happens when no RTSP keepalives and no RTCP packets are being received + } else if now.Sub(ss.lastRequestTime) >= ss.s.sessionTimeout && + now.Sub(time.Unix(lft, 0)) >= ss.s.sessionTimeout { + return liberrors.ErrServerSessionTimedOut{} + } + + ss.udpCheckStreamTimer = time.NewTimer(ss.s.checkStreamPeriod) + + case <-chWriterError: + return ss.writer.stopError + + case <-ss.ctx.Done(): + return liberrors.ErrServerTerminated{} + } + } +} + +func (ss *ServerSession) handleRequestInner(sc *ServerConn, req *base.Request) (*base.Response, error) { + if ss.tcpConn != nil && sc != ss.tcpConn { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, liberrors.ErrServerSessionLinkedToOtherConn{} + } + + var path string + var query string + + switch req.Method { + case base.Announce: + path, query = getPathAndQuery(req.URL, true) + case base.Pause, base.GetParameter, base.SetParameter, base.Play, base.Record: + path, query = getPathAndQuery(req.URL, false) + } + + switch req.Method { + case base.Options: + var methods []string + if _, ok := sc.s.Handler.(ServerHandlerOnDescribe); ok { + methods = append(methods, string(base.Describe)) + } + if _, ok := sc.s.Handler.(ServerHandlerOnAnnounce); ok { + methods = append(methods, string(base.Announce)) + } + if _, ok := sc.s.Handler.(ServerHandlerOnSetup); ok { + methods = append(methods, string(base.Setup)) + } + if _, ok := sc.s.Handler.(ServerHandlerOnPlay); ok { + methods = append(methods, string(base.Play)) + } + if _, ok := sc.s.Handler.(ServerHandlerOnRecord); ok { + methods = append(methods, string(base.Record)) + } + if _, ok := sc.s.Handler.(ServerHandlerOnPause); ok { + methods = append(methods, string(base.Pause)) + } + methods = append(methods, string(base.GetParameter)) + if _, ok := sc.s.Handler.(ServerHandlerOnSetParameter); ok { + methods = append(methods, string(base.SetParameter)) + } + methods = append(methods, string(base.Teardown)) + + return &base.Response{ + StatusCode: base.StatusOK, + Header: base.Header{ + "Public": base.HeaderValue{strings.Join(methods, ", ")}, + }, + }, nil + + case base.Announce: + err := ss.checkState(map[ServerSessionState]struct{}{ + ServerSessionStateInitial: {}, + }) + if err != nil { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, err + } + + ct, ok := req.Header["Content-Type"] + if !ok || len(ct) != 1 { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, liberrors.ErrServerContentTypeMissing{} + } + + if ct[0] != "application/sdp" { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, liberrors.ErrServerContentTypeUnsupported{CT: ct} + } + + var ssd sdp.SessionDescription + err = ssd.Unmarshal(req.Body) + if err != nil { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, liberrors.ErrServerSDPInvalid{Err: err} + } + + var desc description.Session + err = desc.Unmarshal(&ssd) + if err != nil { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, liberrors.ErrServerSDPInvalid{Err: err} + } + + res, err := ss.s.Handler.(ServerHandlerOnAnnounce).OnAnnounce(&ServerHandlerOnAnnounceCtx{ + Session: ss, + Conn: sc, + Request: req, + Path: path, + Query: query, + Description: &desc, + }) + + if res.StatusCode == base.StatusOK { + ss.state = ServerSessionStatePreRecord + ss.setuppedPath = path + ss.setuppedQuery = query + ss.announcedDesc = &desc + } + + return res, err + + case base.Setup: + err := ss.checkState(map[ServerSessionState]struct{}{ + ServerSessionStateInitial: {}, + ServerSessionStatePrePlay: {}, + ServerSessionStatePreRecord: {}, + }) + if err != nil { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, err + } + + var transportHeaders headers.Transports + err = transportHeaders.Unmarshal(req.Header["Transport"]) + if err != nil { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, liberrors.ErrServerTransportHeaderInvalid{Err: err} + } + + inTH := findFirstSupportedTransportHeader(ss.s, transportHeaders) + if inTH == nil { + return &base.Response{ + StatusCode: base.StatusUnsupportedTransport, + }, nil + } + + var trackID string + + switch ss.state { + case ServerSessionStateInitial, ServerSessionStatePrePlay: // play + path, query, trackID, err = getPathAndQueryAndTrackID(req.URL) + if err != nil { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, err + } + + if ss.state == ServerSessionStatePrePlay && path != ss.setuppedPath { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, liberrors.ErrServerMediasDifferentPaths{} + } + + default: // record + path = ss.setuppedPath + query = ss.setuppedQuery + } + + var transport Transport + + if inTH.Protocol == headers.TransportProtocolUDP { + if inTH.Delivery != nil && *inTH.Delivery == headers.TransportDeliveryMulticast { + transport = TransportUDPMulticast + } else { + transport = TransportUDP + } + } else { + transport = TransportTCP + } + + if ss.setuppedTransport != nil && *ss.setuppedTransport != transport { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, liberrors.ErrServerMediasDifferentProtocols{} + } + + switch transport { + case TransportUDP: + if inTH.ClientPorts == nil { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, liberrors.ErrServerTransportHeaderNoClientPorts{} + } + + case TransportTCP: + if inTH.InterleavedIDs != nil { + if (inTH.InterleavedIDs[0] + 1) != inTH.InterleavedIDs[1] { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, liberrors.ErrServerTransportHeaderInvalidInterleavedIDs{} + } + + if ss.isChannelPairInUse(inTH.InterleavedIDs[0]) { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, liberrors.ErrServerTransportHeaderInterleavedIDsInUse{} + } + } + } + + switch ss.state { + case ServerSessionStateInitial, ServerSessionStatePrePlay: // play + if inTH.Mode != nil && *inTH.Mode != headers.TransportModePlay { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, liberrors.ErrServerTransportHeaderInvalidMode{Mode: inTH.Mode} + } + + default: // record + if transport == TransportUDPMulticast { + return &base.Response{ + StatusCode: base.StatusUnsupportedTransport, + }, nil + } + + if inTH.Mode == nil || *inTH.Mode != headers.TransportModeRecord { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, liberrors.ErrServerTransportHeaderInvalidMode{Mode: inTH.Mode} + } + } + + res, stream, err := ss.s.Handler.(ServerHandlerOnSetup).OnSetup(&ServerHandlerOnSetupCtx{ + Session: ss, + Conn: sc, + Request: req, + Path: path, + Query: query, + Transport: transport, + }) + + // workaround to prevent a bug in rtspclientsink + // that makes impossible for the client to receive the response + // and send frames. + // this was causing problems during unit tests. + if ua, ok := req.Header["User-Agent"]; ok && len(ua) == 1 && + strings.HasPrefix(ua[0], "GStreamer") { + select { + case <-time.After(1 * time.Second): + case <-ss.ctx.Done(): + } + } + + if ss.state == ServerSessionStatePreRecord && stream != nil { + panic("stream must be nil when handling publishers") + } + + if res.StatusCode == base.StatusOK { + var medi *description.Media + + switch ss.state { + case ServerSessionStateInitial, ServerSessionStatePrePlay: // play + if stream == nil { + panic("stream cannot be nil when StatusCode is StatusOK") + } + + medi = findMediaByTrackID(stream.Desc.Medias, trackID) + default: // record + medi = findMediaByURL(ss.announcedDesc.Medias, path, query, req.URL) + } + + if medi == nil { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, liberrors.ErrServerMediaNotFound{} + } + + if _, ok := ss.setuppedMedias[medi]; ok { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, liberrors.ErrServerMediaAlreadySetup{} + } + + ss.setuppedTransport = &transport + + if ss.state == ServerSessionStateInitial { + err = stream.readerAdd(ss, + inTH.ClientPorts, + ) + if err != nil { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, err + } + + ss.state = ServerSessionStatePrePlay + ss.setuppedPath = path + ss.setuppedQuery = query + ss.setuppedStream = stream + } + + th := headers.Transport{} + + if ss.state == ServerSessionStatePrePlay { + if stream != ss.setuppedStream { + panic("stream cannot be different than the one returned in previous OnSetup call") + } + + ssrc, ok := stream.localSSRC(medi) + if ok { + th.SSRC = &ssrc + } + } + + if res.Header == nil { + res.Header = make(base.Header) + } + + sm := &serverSessionMedia{ + ss: ss, + media: medi, + onPacketRTCP: func(_ rtcp.Packet) {}, + } + sm.initialize() + + switch transport { + case TransportUDP: + sm.udpRTPReadPort = inTH.ClientPorts[0] + sm.udpRTCPReadPort = inTH.ClientPorts[1] + + sm.udpRTPWriteAddr = &net.UDPAddr{ + IP: ss.author.ip(), + Zone: ss.author.zone(), + Port: sm.udpRTPReadPort, + } + + sm.udpRTCPWriteAddr = &net.UDPAddr{ + IP: ss.author.ip(), + Zone: ss.author.zone(), + Port: sm.udpRTCPReadPort, + } + + th.Protocol = headers.TransportProtocolUDP + de := headers.TransportDeliveryUnicast + th.Delivery = &de + th.ClientPorts = inTH.ClientPorts + th.ServerPorts = &[2]int{sc.s.udpRTPListener.port(), sc.s.udpRTCPListener.port()} + + case TransportUDPMulticast: + th.Protocol = headers.TransportProtocolUDP + de := headers.TransportDeliveryMulticast + th.Delivery = &de + v := uint(127) + th.TTL = &v + d := stream.medias[medi].multicastWriter.ip() + th.Destination = &d + th.Ports = &[2]int{ss.s.MulticastRTPPort, ss.s.MulticastRTCPPort} + + default: // TCP + if inTH.InterleavedIDs != nil { + sm.tcpChannel = inTH.InterleavedIDs[0] + } else { + sm.tcpChannel = ss.findFreeChannelPair() + } + + th.Protocol = headers.TransportProtocolTCP + de := headers.TransportDeliveryUnicast + th.Delivery = &de + th.InterleavedIDs = &[2]int{sm.tcpChannel, sm.tcpChannel + 1} + } + + if ss.setuppedMedias == nil { + ss.setuppedMedias = make(map[*description.Media]*serverSessionMedia) + } + ss.setuppedMedias[medi] = sm + ss.setuppedMediasOrdered = append(ss.setuppedMediasOrdered, sm) + + res.Header["Transport"] = th.Marshal() + } + + return res, err + + case base.Play: + // play can be sent twice, allow calling it even if we're already playing + err := ss.checkState(map[ServerSessionState]struct{}{ + ServerSessionStatePrePlay: {}, + ServerSessionStatePlay: {}, + }) + if err != nil { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, err + } + + if ss.State() == ServerSessionStatePrePlay && path != ss.setuppedPath { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, liberrors.ErrServerPathHasChanged{Prev: ss.setuppedPath, Cur: path} + } + + if ss.state != ServerSessionStatePlay && + *ss.setuppedTransport != TransportUDPMulticast { + ss.createWriter() + } + + res, err := sc.s.Handler.(ServerHandlerOnPlay).OnPlay(&ServerHandlerOnPlayCtx{ + Session: ss, + Conn: sc, + Request: req, + Path: path, + Query: query, + }) + + if res.StatusCode == base.StatusOK { + if ss.state != ServerSessionStatePlay { + ss.state = ServerSessionStatePlay + + v := ss.s.timeNow().Unix() + ss.udpLastPacketTime = &v + + ss.timeDecoder = &rtptime.GlobalDecoder2{} + ss.timeDecoder.Initialize() + + for _, sm := range ss.setuppedMedias { + sm.start() + } + + if *ss.setuppedTransport == TransportTCP { + ss.tcpFrame = &base.InterleavedFrame{} + ss.tcpBuffer = make([]byte, ss.s.MaxPacketSize+4) + } + + switch *ss.setuppedTransport { + case TransportUDP: + ss.udpCheckStreamTimer = time.NewTimer(ss.s.checkStreamPeriod) + ss.startWriter() + + case TransportUDPMulticast: + ss.udpCheckStreamTimer = time.NewTimer(ss.s.checkStreamPeriod) + + default: // TCP + ss.tcpConn = sc + err = switchReadFuncError{true} + // startWriter() is called by ServerConn, through chAsyncStartWriter, + // after the response has been sent + } + + ss.setuppedStream.readerSetActive(ss) + + rtpInfo, ok := generateRTPInfo( + ss.s.timeNow(), + ss.setuppedMediasOrdered, + ss.setuppedStream, + ss.setuppedPath, + req.URL) + + if ok { + if res.Header == nil { + res.Header = make(base.Header) + } + res.Header["RTP-Info"] = rtpInfo.Marshal() + } + } + } else { + if ss.state != ServerSessionStatePlay && + *ss.setuppedTransport != TransportUDPMulticast { + ss.destroyWriter() + } + } + + return res, err + + case base.Record: + err := ss.checkState(map[ServerSessionState]struct{}{ + ServerSessionStatePreRecord: {}, + }) + if err != nil { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, err + } + + if len(ss.setuppedMedias) != len(ss.announcedDesc.Medias) { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, liberrors.ErrServerNotAllAnnouncedMediasSetup{} + } + + if path != ss.setuppedPath { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, liberrors.ErrServerPathHasChanged{Prev: ss.setuppedPath, Cur: path} + } + + ss.createWriter() + + res, err := ss.s.Handler.(ServerHandlerOnRecord).OnRecord(&ServerHandlerOnRecordCtx{ + Session: ss, + Conn: sc, + Request: req, + Path: path, + Query: query, + }) + + if res.StatusCode == base.StatusOK { + ss.state = ServerSessionStateRecord + + v := ss.s.timeNow().Unix() + ss.udpLastPacketTime = &v + + ss.timeDecoder = &rtptime.GlobalDecoder2{} + ss.timeDecoder.Initialize() + + for _, sm := range ss.setuppedMedias { + sm.start() + } + + if *ss.setuppedTransport == TransportTCP { + ss.tcpFrame = &base.InterleavedFrame{} + ss.tcpBuffer = make([]byte, ss.s.MaxPacketSize+4) + } + + switch *ss.setuppedTransport { + case TransportUDP: + ss.udpCheckStreamTimer = time.NewTimer(ss.s.checkStreamPeriod) + ss.startWriter() + + default: // TCP + ss.tcpConn = sc + err = switchReadFuncError{true} + // startWriter() is called by ServerConn, through chAsyncStartWriter, + // after the response has been sent + } + } else { + ss.destroyWriter() + } + + return res, err + + case base.Pause: + err := ss.checkState(map[ServerSessionState]struct{}{ + ServerSessionStatePrePlay: {}, + ServerSessionStatePlay: {}, + ServerSessionStatePreRecord: {}, + ServerSessionStateRecord: {}, + }) + if err != nil { + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, err + } + + res, err := ss.s.Handler.(ServerHandlerOnPause).OnPause(&ServerHandlerOnPauseCtx{ + Session: ss, + Conn: sc, + Request: req, + Path: path, + Query: query, + }) + + if res.StatusCode == base.StatusOK { + if ss.state == ServerSessionStatePlay || ss.state == ServerSessionStateRecord { + ss.destroyWriter() + + if ss.setuppedStream != nil { + ss.setuppedStream.readerSetInactive(ss) + } + + for _, sm := range ss.setuppedMedias { + sm.stop() + } + + ss.timeDecoder = nil + + switch ss.state { + case ServerSessionStatePlay: + ss.state = ServerSessionStatePrePlay + + switch *ss.setuppedTransport { + case TransportUDP: + ss.udpCheckStreamTimer = emptyTimer() + + case TransportUDPMulticast: + ss.udpCheckStreamTimer = emptyTimer() + + default: // TCP + err = switchReadFuncError{false} + ss.tcpConn = nil + } + + case ServerSessionStateRecord: + switch *ss.setuppedTransport { + case TransportUDP: + ss.udpCheckStreamTimer = emptyTimer() + + default: // TCP + err = switchReadFuncError{false} + ss.tcpConn = nil + } + + ss.state = ServerSessionStatePreRecord + } + } + } + + return res, err + + case base.Teardown: + var err error + if (ss.state == ServerSessionStatePlay || ss.state == ServerSessionStateRecord) && + *ss.setuppedTransport == TransportTCP { + err = switchReadFuncError{false} + } + + return &base.Response{ + StatusCode: base.StatusOK, + }, err + + case base.GetParameter: + if h, ok := sc.s.Handler.(ServerHandlerOnGetParameter); ok { + return h.OnGetParameter(&ServerHandlerOnGetParameterCtx{ + Session: ss, + Conn: sc, + Request: req, + Path: path, + Query: query, + }) + } + + // GET_PARAMETER is used like a ping when reading, and sometimes + // also when publishing; reply with 200 + return &base.Response{ + StatusCode: base.StatusOK, + Header: base.Header{ + "Content-Type": base.HeaderValue{"text/parameters"}, + }, + Body: []byte{}, + }, nil + + case base.SetParameter: + if h, ok := sc.s.Handler.(ServerHandlerOnSetParameter); ok { + return h.OnSetParameter(&ServerHandlerOnSetParameterCtx{ + Session: ss, + Conn: sc, + Request: req, + Path: path, + Query: query, + }) + } + } + + return &base.Response{ + StatusCode: base.StatusNotImplemented, + }, nil +} + +func (ss *ServerSession) isChannelPairInUse(channel int) bool { + for _, sm := range ss.setuppedMedias { + if (sm.tcpChannel+1) == channel || sm.tcpChannel == channel || sm.tcpChannel == (channel+1) { + return true + } + } + return false +} + +func (ss *ServerSession) findFreeChannelPair() int { + for i := 0; ; i += 2 { // prefer even channels + if !ss.isChannelPairInUse(i) { + return i + } + } +} + +// OnPacketRTPAny sets a callback that is called when a RTP packet is read from any setupped media. +func (ss *ServerSession) OnPacketRTPAny(cb OnPacketRTPAnyFunc) { + for _, sm := range ss.setuppedMedias { + cmedia := sm.media + for _, forma := range sm.media.Formats { + ss.OnPacketRTP(sm.media, forma, func(pkt *rtp.Packet) { + cb(cmedia, forma, pkt) + }) + } + } +} + +// OnPacketRTCPAny sets a callback that is called when a RTCP packet is read from any setupped media. +func (ss *ServerSession) OnPacketRTCPAny(cb OnPacketRTCPAnyFunc) { + for _, sm := range ss.setuppedMedias { + cmedia := sm.media + ss.OnPacketRTCP(sm.media, func(pkt rtcp.Packet) { + cb(cmedia, pkt) + }) + } +} + +// OnPacketRTP sets a callback that is called when a RTP packet is read. +func (ss *ServerSession) OnPacketRTP(medi *description.Media, forma format.Format, cb OnPacketRTPFunc) { + sm := ss.setuppedMedias[medi] + st := sm.formats[forma.PayloadType()] + st.onPacketRTP = cb +} + +// OnPacketRTCP sets a callback that is called when a RTCP packet is read. +func (ss *ServerSession) OnPacketRTCP(medi *description.Media, cb OnPacketRTCPFunc) { + sm := ss.setuppedMedias[medi] + sm.onPacketRTCP = cb +} + +func (ss *ServerSession) writePacketRTP(medi *description.Media, payloadType uint8, byts []byte) error { + sm := ss.setuppedMedias[medi] + sf := sm.formats[payloadType] + + ss.writerMutex.RLock() + defer ss.writerMutex.RUnlock() + + if ss.writer == nil { + return nil + } + + ok := ss.writer.push(func() error { + return sf.writePacketRTPInQueue(byts) + }) + if !ok { + return liberrors.ErrServerWriteQueueFull{} + } + + return nil +} + +// WritePacketRTP writes a RTP packet to the session. +func (ss *ServerSession) WritePacketRTP(medi *description.Media, pkt *rtp.Packet) error { + byts := make([]byte, ss.s.MaxPacketSize) + n, err := pkt.MarshalTo(byts) + if err != nil { + return err + } + byts = byts[:n] + + return ss.writePacketRTP(medi, pkt.PayloadType, byts) +} + +func (ss *ServerSession) writePacketRTCP(medi *description.Media, byts []byte) error { + sm := ss.setuppedMedias[medi] + + ss.writerMutex.RLock() + defer ss.writerMutex.RUnlock() + + if ss.writer == nil { + return nil + } + + ok := ss.writer.push(func() error { + return sm.writePacketRTCPInQueue(byts) + }) + if !ok { + return liberrors.ErrServerWriteQueueFull{} + } + + return nil +} + +// WritePacketRTCP writes a RTCP packet to the session. +func (ss *ServerSession) WritePacketRTCP(medi *description.Media, pkt rtcp.Packet) error { + byts, err := pkt.Marshal() + if err != nil { + return err + } + + return ss.writePacketRTCP(medi, byts) +} + +// PacketPTS returns the PTS of an incoming RTP packet. +// It is computed by decoding the packet timestamp and sychronizing it with other tracks. +// +// Deprecated: replaced by PacketPTS2. +func (ss *ServerSession) PacketPTS(medi *description.Media, pkt *rtp.Packet) (time.Duration, bool) { + sm := ss.setuppedMedias[medi] + sf := sm.formats[pkt.PayloadType] + + v, ok := ss.timeDecoder.Decode(sf.format, pkt) + if !ok { + return 0, false + } + + return multiplyAndDivide(time.Duration(v), time.Second, time.Duration(sf.format.ClockRate())), true +} + +// PacketPTS2 returns the PTS of an incoming RTP packet. +// It is computed by decoding the packet timestamp and sychronizing it with other tracks. +func (ss *ServerSession) PacketPTS2(medi *description.Media, pkt *rtp.Packet) (int64, bool) { + sm := ss.setuppedMedias[medi] + sf := sm.formats[pkt.PayloadType] + return ss.timeDecoder.Decode(sf.format, pkt) +} + +// PacketNTP returns the NTP timestamp of an incoming RTP packet. +// The NTP timestamp is computed from RTCP sender reports. +func (ss *ServerSession) PacketNTP(medi *description.Media, pkt *rtp.Packet) (time.Time, bool) { + sm := ss.setuppedMedias[medi] + sf := sm.formats[pkt.PayloadType] + return sf.rtcpReceiver.PacketNTP(pkt.Timestamp) +} + +func (ss *ServerSession) handleRequest(req sessionRequestReq) (*base.Response, *ServerSession, error) { + select { + case ss.chHandleRequest <- req: + res := <-req.res + return res.res, res.ss, res.err + + case <-ss.ctx.Done(): + return &base.Response{ + StatusCode: base.StatusBadRequest, + }, req.sc.session, liberrors.ErrServerTerminated{} + } +} + +func (ss *ServerSession) removeConn(sc *ServerConn) { + select { + case ss.chRemoveConn <- sc: + case <-ss.ctx.Done(): + } +} + +func (ss *ServerSession) asyncStartWriter() { + select { + case ss.chAsyncStartWriter <- struct{}{}: + case <-ss.ctx.Done(): + } +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_session_format.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_session_format.go new file mode 100644 index 000000000..865a81cd1 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_session_format.go @@ -0,0 +1,163 @@ +package gortsplib + +import ( + "log" + "sync/atomic" + "time" + + "github.com/pion/rtcp" + "github.com/pion/rtp" + + "github.com/bluenviron/gortsplib/v4/pkg/format" + "github.com/bluenviron/gortsplib/v4/pkg/liberrors" + "github.com/bluenviron/gortsplib/v4/pkg/rtcpreceiver" + "github.com/bluenviron/gortsplib/v4/pkg/rtplossdetector" + "github.com/bluenviron/gortsplib/v4/pkg/rtpreorderer" +) + +type serverSessionFormat struct { + sm *serverSessionMedia + format format.Format + onPacketRTP OnPacketRTPFunc + + udpReorderer *rtpreorderer.Reorderer + tcpLossDetector *rtplossdetector.LossDetector + rtcpReceiver *rtcpreceiver.RTCPReceiver + writePacketRTPInQueue func([]byte) error + rtpPacketsReceived *uint64 + rtpPacketsSent *uint64 + rtpPacketsLost *uint64 +} + +func (sf *serverSessionFormat) initialize() { + sf.rtpPacketsReceived = new(uint64) + sf.rtpPacketsSent = new(uint64) + sf.rtpPacketsLost = new(uint64) +} + +func (sf *serverSessionFormat) start() { + switch *sf.sm.ss.setuppedTransport { + case TransportUDP, TransportUDPMulticast: + sf.writePacketRTPInQueue = sf.writePacketRTPInQueueUDP + + default: + sf.writePacketRTPInQueue = sf.writePacketRTPInQueueTCP + } + + if sf.sm.ss.state != ServerSessionStatePlay { + if *sf.sm.ss.setuppedTransport == TransportUDP || *sf.sm.ss.setuppedTransport == TransportUDPMulticast { + sf.udpReorderer = &rtpreorderer.Reorderer{} + sf.udpReorderer.Initialize() + } else { + sf.tcpLossDetector = &rtplossdetector.LossDetector{} + } + + sf.rtcpReceiver = &rtcpreceiver.RTCPReceiver{ + ClockRate: sf.format.ClockRate(), + Period: sf.sm.ss.s.receiverReportPeriod, + TimeNow: sf.sm.ss.s.timeNow, + WritePacketRTCP: func(pkt rtcp.Packet) { + if *sf.sm.ss.setuppedTransport == TransportUDP || *sf.sm.ss.setuppedTransport == TransportUDPMulticast { + sf.sm.ss.WritePacketRTCP(sf.sm.media, pkt) //nolint:errcheck + } + }, + } + err := sf.rtcpReceiver.Initialize() + if err != nil { + panic(err) + } + } +} + +func (sf *serverSessionFormat) stop() { + if sf.rtcpReceiver != nil { + sf.rtcpReceiver.Close() + sf.rtcpReceiver = nil + } +} + +func (sf *serverSessionFormat) readPacketRTPUDP(pkt *rtp.Packet, now time.Time) { + packets, lost := sf.udpReorderer.Process(pkt) + if lost != 0 { + sf.onPacketRTPLost(uint64(lost)) + // do not return + } + + for _, pkt := range packets { + sf.handlePacketRTP(pkt, now) + } +} + +func (sf *serverSessionFormat) readPacketRTPTCP(pkt *rtp.Packet) { + lost := sf.tcpLossDetector.Process(pkt) + if lost != 0 { + sf.onPacketRTPLost(uint64(lost)) + // do not return + } + + now := sf.sm.ss.s.timeNow() + + sf.handlePacketRTP(pkt, now) +} + +func (sf *serverSessionFormat) handlePacketRTP(pkt *rtp.Packet, now time.Time) { + err := sf.rtcpReceiver.ProcessPacket(pkt, now, sf.format.PTSEqualsDTS(pkt)) + if err != nil { + sf.sm.onPacketRTPDecodeError(err) + return + } + + atomic.AddUint64(sf.rtpPacketsReceived, 1) + + sf.onPacketRTP(pkt) +} + +func (sf *serverSessionFormat) onPacketRTPLost(lost uint64) { + atomic.AddUint64(sf.rtpPacketsLost, lost) + + if h, ok := sf.sm.ss.s.Handler.(ServerHandlerOnPacketsLost); ok { + h.OnPacketsLost(&ServerHandlerOnPacketsLostCtx{ + Session: sf.sm.ss, + Lost: lost, + }) + } else if h, ok := sf.sm.ss.s.Handler.(ServerHandlerOnPacketLost); ok { + h.OnPacketLost(&ServerHandlerOnPacketLostCtx{ + Session: sf.sm.ss, + Error: liberrors.ErrServerRTPPacketsLost{Lost: uint(lost)}, //nolint:staticcheck + }) + } else { + log.Printf("%d RTP %s lost", + lost, + func() string { + if lost == 1 { + return "packet" + } + return "packets" + }()) + } +} + +func (sf *serverSessionFormat) writePacketRTPInQueueUDP(payload []byte) error { + err := sf.sm.ss.s.udpRTPListener.write(payload, sf.sm.udpRTPWriteAddr) + if err != nil { + return err + } + + atomic.AddUint64(sf.sm.bytesSent, uint64(len(payload))) + atomic.AddUint64(sf.rtpPacketsSent, 1) + return nil +} + +func (sf *serverSessionFormat) writePacketRTPInQueueTCP(payload []byte) error { + sf.sm.ss.tcpFrame.Channel = sf.sm.tcpChannel + sf.sm.ss.tcpFrame.Payload = payload + sf.sm.ss.tcpConn.nconn.SetWriteDeadline(time.Now().Add(sf.sm.ss.s.WriteTimeout)) + err := sf.sm.ss.tcpConn.conn.WriteInterleavedFrame(sf.sm.ss.tcpFrame, sf.sm.ss.tcpBuffer) + if err != nil { + return err + } + + atomic.AddUint64(sf.sm.bytesSent, uint64(len(payload))) + atomic.AddUint64(sf.rtpPacketsSent, 1) + return nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_session_media.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_session_media.go new file mode 100644 index 000000000..dfe382738 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_session_media.go @@ -0,0 +1,342 @@ +package gortsplib + +import ( + "log" + "net" + "sync/atomic" + "time" + + "github.com/pion/rtcp" + "github.com/pion/rtp" + + "github.com/bluenviron/gortsplib/v4/pkg/description" + "github.com/bluenviron/gortsplib/v4/pkg/liberrors" +) + +type serverSessionMedia struct { + ss *ServerSession + media *description.Media + onPacketRTCP OnPacketRTCPFunc + + tcpChannel int + udpRTPReadPort int + udpRTPWriteAddr *net.UDPAddr + udpRTCPReadPort int + udpRTCPWriteAddr *net.UDPAddr + formats map[uint8]*serverSessionFormat // record only + writePacketRTCPInQueue func([]byte) error + bytesReceived *uint64 + bytesSent *uint64 + rtpPacketsInError *uint64 + rtcpPacketsReceived *uint64 + rtcpPacketsSent *uint64 + rtcpPacketsInError *uint64 +} + +func (sm *serverSessionMedia) initialize() { + sm.bytesReceived = new(uint64) + sm.bytesSent = new(uint64) + sm.rtpPacketsInError = new(uint64) + sm.rtcpPacketsReceived = new(uint64) + sm.rtcpPacketsSent = new(uint64) + sm.rtcpPacketsInError = new(uint64) + + sm.formats = make(map[uint8]*serverSessionFormat) + + for _, forma := range sm.media.Formats { + f := &serverSessionFormat{ + sm: sm, + format: forma, + onPacketRTP: func(*rtp.Packet) {}, + } + f.initialize() + sm.formats[forma.PayloadType()] = f + } +} + +func (sm *serverSessionMedia) start() { + // allocate udpRTCPReceiver before udpRTCPListener + // otherwise udpRTCPReceiver.LastSSRC() can't be called. + for _, sf := range sm.formats { + sf.start() + } + + switch *sm.ss.setuppedTransport { + case TransportUDP, TransportUDPMulticast: + sm.writePacketRTCPInQueue = sm.writePacketRTCPInQueueUDP + + if *sm.ss.setuppedTransport == TransportUDP { + if sm.ss.state == ServerSessionStatePlay { + // firewall opening is performed with RTCP sender reports generated by ServerStream + + // readers can send RTCP packets only + sm.ss.s.udpRTCPListener.addClient(sm.ss.author.ip(), sm.udpRTCPReadPort, sm.readPacketRTCPUDPPlay) + } else { + // open the firewall by sending empty packets to the counterpart. + byts, _ := (&rtp.Packet{Header: rtp.Header{Version: 2}}).Marshal() + sm.ss.s.udpRTPListener.write(byts, sm.udpRTPWriteAddr) //nolint:errcheck + + byts, _ = (&rtcp.ReceiverReport{}).Marshal() + sm.ss.s.udpRTCPListener.write(byts, sm.udpRTCPWriteAddr) //nolint:errcheck + + sm.ss.s.udpRTPListener.addClient(sm.ss.author.ip(), sm.udpRTPReadPort, sm.readPacketRTPUDPRecord) + sm.ss.s.udpRTCPListener.addClient(sm.ss.author.ip(), sm.udpRTCPReadPort, sm.readPacketRTCPUDPRecord) + } + } + + case TransportTCP: + sm.writePacketRTCPInQueue = sm.writePacketRTCPInQueueTCP + + if sm.ss.tcpCallbackByChannel == nil { + sm.ss.tcpCallbackByChannel = make(map[int]readFunc) + } + + if sm.ss.state == ServerSessionStatePlay { + sm.ss.tcpCallbackByChannel[sm.tcpChannel] = sm.readPacketRTPTCPPlay + sm.ss.tcpCallbackByChannel[sm.tcpChannel+1] = sm.readPacketRTCPTCPPlay + } else { + sm.ss.tcpCallbackByChannel[sm.tcpChannel] = sm.readPacketRTPTCPRecord + sm.ss.tcpCallbackByChannel[sm.tcpChannel+1] = sm.readPacketRTCPTCPRecord + } + } +} + +func (sm *serverSessionMedia) stop() { + if *sm.ss.setuppedTransport == TransportUDP { + sm.ss.s.udpRTPListener.removeClient(sm.ss.author.ip(), sm.udpRTPReadPort) + sm.ss.s.udpRTCPListener.removeClient(sm.ss.author.ip(), sm.udpRTCPReadPort) + } + + for _, sf := range sm.formats { + sf.stop() + } +} + +func (sm *serverSessionMedia) findFormatWithSSRC(ssrc uint32) *serverSessionFormat { + for _, format := range sm.formats { + stats := format.rtcpReceiver.Stats() + if stats != nil && stats.RemoteSSRC == ssrc { + return format + } + } + return nil +} + +func (sm *serverSessionMedia) writePacketRTCPInQueueUDP(payload []byte) error { + err := sm.ss.s.udpRTCPListener.write(payload, sm.udpRTCPWriteAddr) + if err != nil { + return err + } + + atomic.AddUint64(sm.bytesSent, uint64(len(payload))) + atomic.AddUint64(sm.rtcpPacketsSent, 1) + return nil +} + +func (sm *serverSessionMedia) writePacketRTCPInQueueTCP(payload []byte) error { + sm.ss.tcpFrame.Channel = sm.tcpChannel + 1 + sm.ss.tcpFrame.Payload = payload + sm.ss.tcpConn.nconn.SetWriteDeadline(time.Now().Add(sm.ss.s.WriteTimeout)) + err := sm.ss.tcpConn.conn.WriteInterleavedFrame(sm.ss.tcpFrame, sm.ss.tcpBuffer) + if err != nil { + return err + } + + atomic.AddUint64(sm.bytesSent, uint64(len(payload))) + atomic.AddUint64(sm.rtcpPacketsSent, 1) + return nil +} + +func (sm *serverSessionMedia) readPacketRTCPUDPPlay(payload []byte) bool { + atomic.AddUint64(sm.bytesReceived, uint64(len(payload))) + + if len(payload) == (udpMaxPayloadSize + 1) { + sm.onPacketRTCPDecodeError(liberrors.ErrServerRTCPPacketTooBigUDP{}) + return false + } + + packets, err := rtcp.Unmarshal(payload) + if err != nil { + sm.onPacketRTCPDecodeError(err) + return false + } + + now := sm.ss.s.timeNow() + atomic.StoreInt64(sm.ss.udpLastPacketTime, now.Unix()) + + atomic.AddUint64(sm.rtcpPacketsReceived, uint64(len(packets))) + + for _, pkt := range packets { + sm.onPacketRTCP(pkt) + } + + return true +} + +func (sm *serverSessionMedia) readPacketRTPUDPRecord(payload []byte) bool { + atomic.AddUint64(sm.bytesReceived, uint64(len(payload))) + + if len(payload) == (udpMaxPayloadSize + 1) { + sm.onPacketRTPDecodeError(liberrors.ErrServerRTPPacketTooBigUDP{}) + return false + } + + pkt := &rtp.Packet{} + err := pkt.Unmarshal(payload) + if err != nil { + sm.onPacketRTPDecodeError(err) + return false + } + + forma, ok := sm.formats[pkt.PayloadType] + if !ok { + sm.onPacketRTPDecodeError(liberrors.ErrServerRTPPacketUnknownPayloadType{PayloadType: pkt.PayloadType}) + return false + } + + now := sm.ss.s.timeNow() + atomic.StoreInt64(sm.ss.udpLastPacketTime, now.Unix()) + + forma.readPacketRTPUDP(pkt, now) + + return true +} + +func (sm *serverSessionMedia) readPacketRTCPUDPRecord(payload []byte) bool { + atomic.AddUint64(sm.bytesReceived, uint64(len(payload))) + + if len(payload) == (udpMaxPayloadSize + 1) { + sm.onPacketRTCPDecodeError(liberrors.ErrServerRTCPPacketTooBigUDP{}) + return false + } + + packets, err := rtcp.Unmarshal(payload) + if err != nil { + sm.onPacketRTCPDecodeError(err) + return false + } + + now := sm.ss.s.timeNow() + atomic.StoreInt64(sm.ss.udpLastPacketTime, now.Unix()) + + atomic.AddUint64(sm.rtcpPacketsReceived, uint64(len(packets))) + + for _, pkt := range packets { + if sr, ok := pkt.(*rtcp.SenderReport); ok { + format := sm.findFormatWithSSRC(sr.SSRC) + if format != nil { + format.rtcpReceiver.ProcessSenderReport(sr, now) + } + } + + sm.onPacketRTCP(pkt) + } + + return true +} + +func (sm *serverSessionMedia) readPacketRTPTCPPlay(_ []byte) bool { + return false +} + +func (sm *serverSessionMedia) readPacketRTCPTCPPlay(payload []byte) bool { + atomic.AddUint64(sm.bytesReceived, uint64(len(payload))) + + if len(payload) > udpMaxPayloadSize { + sm.onPacketRTCPDecodeError(liberrors.ErrServerRTCPPacketTooBig{L: len(payload), Max: udpMaxPayloadSize}) + return false + } + + packets, err := rtcp.Unmarshal(payload) + if err != nil { + sm.onPacketRTCPDecodeError(err) + return false + } + + atomic.AddUint64(sm.rtcpPacketsReceived, uint64(len(packets))) + + for _, pkt := range packets { + sm.onPacketRTCP(pkt) + } + + return true +} + +func (sm *serverSessionMedia) readPacketRTPTCPRecord(payload []byte) bool { + atomic.AddUint64(sm.bytesReceived, uint64(len(payload))) + + pkt := &rtp.Packet{} + err := pkt.Unmarshal(payload) + if err != nil { + sm.onPacketRTPDecodeError(err) + return false + } + + forma, ok := sm.formats[pkt.PayloadType] + if !ok { + sm.onPacketRTPDecodeError(liberrors.ErrServerRTPPacketUnknownPayloadType{PayloadType: pkt.PayloadType}) + return false + } + + forma.readPacketRTPTCP(pkt) + + return true +} + +func (sm *serverSessionMedia) readPacketRTCPTCPRecord(payload []byte) bool { + atomic.AddUint64(sm.bytesReceived, uint64(len(payload))) + + if len(payload) > udpMaxPayloadSize { + sm.onPacketRTCPDecodeError(liberrors.ErrServerRTCPPacketTooBig{L: len(payload), Max: udpMaxPayloadSize}) + return false + } + + packets, err := rtcp.Unmarshal(payload) + if err != nil { + sm.onPacketRTCPDecodeError(err) + return false + } + + now := sm.ss.s.timeNow() + + atomic.AddUint64(sm.rtcpPacketsReceived, uint64(len(packets))) + + for _, pkt := range packets { + if sr, ok := pkt.(*rtcp.SenderReport); ok { + format := sm.findFormatWithSSRC(sr.SSRC) + if format != nil { + format.rtcpReceiver.ProcessSenderReport(sr, now) + } + } + + sm.onPacketRTCP(pkt) + } + + return true +} + +func (sm *serverSessionMedia) onPacketRTPDecodeError(err error) { + atomic.AddUint64(sm.rtpPacketsInError, 1) + + if h, ok := sm.ss.s.Handler.(ServerHandlerOnDecodeError); ok { + h.OnDecodeError(&ServerHandlerOnDecodeErrorCtx{ + Session: sm.ss, + Error: err, + }) + } else { + log.Println(err.Error()) + } +} + +func (sm *serverSessionMedia) onPacketRTCPDecodeError(err error) { + atomic.AddUint64(sm.rtcpPacketsInError, 1) + + if h, ok := sm.ss.s.Handler.(ServerHandlerOnDecodeError); ok { + h.OnDecodeError(&ServerHandlerOnDecodeErrorCtx{ + Session: sm.ss, + Error: err, + }) + } else { + log.Println(err.Error()) + } +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_stream.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_stream.go new file mode 100644 index 000000000..574d112d1 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_stream.go @@ -0,0 +1,378 @@ +package gortsplib + +import ( + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/pion/rtcp" + "github.com/pion/rtp" + + "github.com/bluenviron/gortsplib/v4/pkg/description" + "github.com/bluenviron/gortsplib/v4/pkg/format" + "github.com/bluenviron/gortsplib/v4/pkg/headers" + "github.com/bluenviron/gortsplib/v4/pkg/liberrors" +) + +func firstFormat(formats map[uint8]*serverStreamFormat) *serverStreamFormat { + var firstKey uint8 + for key := range formats { + firstKey = key + break + } + + return formats[firstKey] +} + +// NewServerStream allocates a ServerStream. +// +// Deprecated: replaced by ServerStream.Initialize(). +func NewServerStream(s *Server, desc *description.Session) *ServerStream { + st := &ServerStream{ + Server: s, + Desc: desc, + } + err := st.Initialize() + if err != nil { + panic(err) + } + return st +} + +// ServerStream represents a data stream. +// This is in charge of +// - distributing the stream to each reader +// - allocating multicast listeners +// - gathering infos about the stream in order to generate SSRC and RTP-Info +type ServerStream struct { + Server *Server + Desc *description.Session + + mutex sync.RWMutex + readers map[*ServerSession]struct{} + multicastReaderCount int + activeUnicastReaders map[*ServerSession]struct{} + medias map[*description.Media]*serverStreamMedia + closed bool +} + +// Initialize initializes a ServerStream. +func (st *ServerStream) Initialize() error { + if st.Server == nil || st.Server.sessions == nil { + return fmt.Errorf("server not present or not initialized") + } + + st.readers = make(map[*ServerSession]struct{}) + st.activeUnicastReaders = make(map[*ServerSession]struct{}) + + st.medias = make(map[*description.Media]*serverStreamMedia, len(st.Desc.Medias)) + for i, medi := range st.Desc.Medias { + sm := &serverStreamMedia{ + st: st, + media: medi, + trackID: i, + } + sm.initialize() + st.medias[medi] = sm + } + + return nil +} + +// Close closes a ServerStream. +func (st *ServerStream) Close() { + st.mutex.Lock() + st.closed = true + st.mutex.Unlock() + + for ss := range st.readers { + ss.Close() + } + + for _, sm := range st.medias { + sm.close() + } +} + +// BytesSent returns the number of written bytes. +// +// Deprecated: replaced by Stats() +func (st *ServerStream) BytesSent() uint64 { + v := uint64(0) + for _, me := range st.medias { + v += atomic.LoadUint64(me.bytesSent) + } + return v +} + +// Description returns the description of the stream. +// +// Deprecated: use ServerStream.Desc. +func (st *ServerStream) Description() *description.Session { + return st.Desc +} + +// Stats returns stream statistics. +func (st *ServerStream) Stats() *ServerStreamStats { + return &ServerStreamStats{ + BytesSent: func() uint64 { + v := uint64(0) + for _, me := range st.medias { + v += atomic.LoadUint64(me.bytesSent) + } + return v + }(), + RTPPacketsSent: func() uint64 { + v := uint64(0) + for _, me := range st.medias { + for _, f := range me.formats { + v += atomic.LoadUint64(f.rtpPacketsSent) + } + } + return v + }(), + RTCPPacketsSent: func() uint64 { + v := uint64(0) + for _, me := range st.medias { + v += atomic.LoadUint64(me.rtcpPacketsSent) + } + return v + }(), + Medias: func() map[*description.Media]ServerStreamStatsMedia { + ret := make(map[*description.Media]ServerStreamStatsMedia, len(st.medias)) + + for med, sm := range st.medias { + ret[med] = ServerStreamStatsMedia{ + BytesSent: atomic.LoadUint64(sm.bytesSent), + RTCPPacketsSent: atomic.LoadUint64(sm.rtcpPacketsSent), + Formats: func() map[format.Format]ServerStreamStatsFormat { + ret := make(map[format.Format]ServerStreamStatsFormat) + + for _, fo := range sm.formats { + ret[fo.format] = ServerStreamStatsFormat{ + RTPPacketsSent: atomic.LoadUint64(fo.rtpPacketsSent), + } + } + + return ret + }(), + } + } + + return ret + }(), + } +} + +func (st *ServerStream) localSSRC(medi *description.Media) (uint32, bool) { + st.mutex.Lock() + defer st.mutex.Unlock() + + sm := st.medias[medi] + + // localSSRC() is used to fill SSRC inside the Transport header. + // if there are multiple formats inside a single media stream, + // do not return anything, since Transport headers don't support multiple SSRCs. + if len(sm.formats) > 1 { + return 0, false + } + + stats := firstFormat(sm.formats).rtcpSender.Stats() + if stats == nil { + return 0, false + } + + return stats.LocalSSRC, true +} + +func (st *ServerStream) rtpInfoEntry(medi *description.Media, now time.Time) *headers.RTPInfoEntry { + st.mutex.Lock() + defer st.mutex.Unlock() + + sm := st.medias[medi] + + // if there are multiple formats inside a single media stream, + // do not generate a RTP-Info entry, since RTP-Info doesn't support + // multiple sequence numbers / timestamps. + if len(sm.formats) > 1 { + return nil + } + + format := firstFormat(sm.formats) + + stats := format.rtcpSender.Stats() + if stats == nil { + return nil + } + + clockRate := format.format.ClockRate() + if clockRate == 0 { + return nil + } + + // sequence number of the first packet of the stream + seqNum := stats.LastSequenceNumber + 1 + + // RTP timestamp corresponding to the time value in + // the Range response header. + // remove a small quantity in order to avoid DTS > PTS + ts := uint32(uint64(stats.LastRTP) + + uint64(now.Sub(stats.LastNTP).Seconds()*float64(clockRate)) - + uint64(clockRate)/10) + + return &headers.RTPInfoEntry{ + SequenceNumber: &seqNum, + Timestamp: &ts, + } +} + +func (st *ServerStream) readerAdd( + ss *ServerSession, + clientPorts *[2]int, +) error { + st.mutex.Lock() + defer st.mutex.Unlock() + + if st.closed { + return liberrors.ErrServerStreamClosed{} + } + + switch *ss.setuppedTransport { + case TransportUDP: + // check whether UDP ports and IP are already assigned to another reader + for r := range st.readers { + if *r.setuppedTransport == TransportUDP && + r.author.ip().Equal(ss.author.ip()) && + r.author.zone() == ss.author.zone() { + for _, rt := range r.setuppedMedias { + if rt.udpRTPReadPort == clientPorts[0] { + return liberrors.ErrServerUDPPortsAlreadyInUse{Port: rt.udpRTPReadPort} + } + } + } + } + + case TransportUDPMulticast: + if st.multicastReaderCount == 0 { + for _, media := range st.medias { + mw := &serverMulticastWriter{ + s: st.Server, + } + err := mw.initialize() + if err != nil { + return err + } + media.multicastWriter = mw + } + } + st.multicastReaderCount++ + } + + st.readers[ss] = struct{}{} + + return nil +} + +func (st *ServerStream) readerRemove(ss *ServerSession) { + st.mutex.Lock() + defer st.mutex.Unlock() + + if st.closed { + return + } + + delete(st.readers, ss) + + if *ss.setuppedTransport == TransportUDPMulticast { + st.multicastReaderCount-- + if st.multicastReaderCount == 0 { + for _, media := range st.medias { + media.multicastWriter.close() + media.multicastWriter = nil + } + } + } +} + +func (st *ServerStream) readerSetActive(ss *ServerSession) { + st.mutex.Lock() + defer st.mutex.Unlock() + + if st.closed { + return + } + + if *ss.setuppedTransport == TransportUDPMulticast { + for medi, sm := range ss.setuppedMedias { + streamMedia := st.medias[medi] + streamMedia.multicastWriter.rtcpl.addClient( + ss.author.ip(), streamMedia.multicastWriter.rtcpl.port(), sm.readPacketRTCPUDPPlay) + } + } else { + st.activeUnicastReaders[ss] = struct{}{} + } +} + +func (st *ServerStream) readerSetInactive(ss *ServerSession) { + st.mutex.Lock() + defer st.mutex.Unlock() + + if st.closed { + return + } + + if *ss.setuppedTransport == TransportUDPMulticast { + for medi := range ss.setuppedMedias { + streamMedia := st.medias[medi] + streamMedia.multicastWriter.rtcpl.removeClient(ss.author.ip(), streamMedia.multicastWriter.rtcpl.port()) + } + } else { + delete(st.activeUnicastReaders, ss) + } +} + +// WritePacketRTP writes a RTP packet to all the readers of the stream. +func (st *ServerStream) WritePacketRTP(medi *description.Media, pkt *rtp.Packet) error { + return st.WritePacketRTPWithNTP(medi, pkt, st.Server.timeNow()) +} + +// WritePacketRTPWithNTP writes a RTP packet to all the readers of the stream. +// ntp is the absolute time of the packet, and is sent with periodic RTCP sender reports. +func (st *ServerStream) WritePacketRTPWithNTP(medi *description.Media, pkt *rtp.Packet, ntp time.Time) error { + byts := make([]byte, st.Server.MaxPacketSize) + n, err := pkt.MarshalTo(byts) + if err != nil { + return err + } + byts = byts[:n] + + st.mutex.RLock() + defer st.mutex.RUnlock() + + if st.closed { + return liberrors.ErrServerStreamClosed{} + } + + sm := st.medias[medi] + sf := sm.formats[pkt.PayloadType] + return sf.writePacketRTP(byts, pkt, ntp) +} + +// WritePacketRTCP writes a RTCP packet to all the readers of the stream. +func (st *ServerStream) WritePacketRTCP(medi *description.Media, pkt rtcp.Packet) error { + byts, err := pkt.Marshal() + if err != nil { + return err + } + + st.mutex.RLock() + defer st.mutex.RUnlock() + + if st.closed { + return liberrors.ErrServerStreamClosed{} + } + + sm := st.medias[medi] + return sm.writePacketRTCP(byts) +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_stream_format.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_stream_format.go new file mode 100644 index 000000000..0498ae501 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_stream_format.go @@ -0,0 +1,69 @@ +package gortsplib + +import ( + "sync/atomic" + "time" + + "github.com/pion/rtcp" + "github.com/pion/rtp" + + "github.com/bluenviron/gortsplib/v4/pkg/format" + "github.com/bluenviron/gortsplib/v4/pkg/rtcpsender" +) + +type serverStreamFormat struct { + sm *serverStreamMedia + format format.Format + + rtcpSender *rtcpsender.RTCPSender + rtpPacketsSent *uint64 +} + +func (sf *serverStreamFormat) initialize() { + sf.rtpPacketsSent = new(uint64) + + sf.rtcpSender = &rtcpsender.RTCPSender{ + ClockRate: sf.format.ClockRate(), + Period: sf.sm.st.Server.senderReportPeriod, + TimeNow: sf.sm.st.Server.timeNow, + WritePacketRTCP: func(pkt rtcp.Packet) { + if !sf.sm.st.Server.DisableRTCPSenderReports { + sf.sm.st.WritePacketRTCP(sf.sm.media, pkt) //nolint:errcheck + } + }, + } + sf.rtcpSender.Initialize() +} + +func (sf *serverStreamFormat) writePacketRTP(byts []byte, pkt *rtp.Packet, ntp time.Time) error { + sf.rtcpSender.ProcessPacket(pkt, ntp, sf.format.PTSEqualsDTS(pkt)) + + le := uint64(len(byts)) + + // send unicast + for r := range sf.sm.st.activeUnicastReaders { + if _, ok := r.setuppedMedias[sf.sm.media]; ok { + err := r.writePacketRTP(sf.sm.media, pkt.PayloadType, byts) + if err != nil { + r.onStreamWriteError(err) + continue + } + + atomic.AddUint64(sf.sm.bytesSent, le) + atomic.AddUint64(sf.rtpPacketsSent, 1) + } + } + + // send multicast + if sf.sm.multicastWriter != nil { + err := sf.sm.multicastWriter.writePacketRTP(byts) + if err != nil { + return err + } + + atomic.AddUint64(sf.sm.bytesSent, le) + atomic.AddUint64(sf.rtpPacketsSent, 1) + } + + return nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_stream_media.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_stream_media.go new file mode 100644 index 000000000..26bfc8d79 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_stream_media.go @@ -0,0 +1,76 @@ +package gortsplib + +import ( + "sync/atomic" + + "github.com/bluenviron/gortsplib/v4/pkg/description" +) + +type serverStreamMedia struct { + st *ServerStream + media *description.Media + trackID int + + formats map[uint8]*serverStreamFormat + multicastWriter *serverMulticastWriter + bytesSent *uint64 + rtcpPacketsSent *uint64 +} + +func (sm *serverStreamMedia) initialize() { + sm.bytesSent = new(uint64) + sm.rtcpPacketsSent = new(uint64) + + sm.formats = make(map[uint8]*serverStreamFormat) + for _, forma := range sm.media.Formats { + sf := &serverStreamFormat{ + sm: sm, + format: forma, + } + sf.initialize() + sm.formats[forma.PayloadType()] = sf + } +} + +func (sm *serverStreamMedia) close() { + for _, tr := range sm.formats { + if tr.rtcpSender != nil { + tr.rtcpSender.Close() + } + } + + if sm.multicastWriter != nil { + sm.multicastWriter.close() + } +} + +func (sm *serverStreamMedia) writePacketRTCP(byts []byte) error { + le := len(byts) + + // send unicast + for r := range sm.st.activeUnicastReaders { + if _, ok := r.setuppedMedias[sm.media]; ok { + err := r.writePacketRTCP(sm.media, byts) + if err != nil { + r.onStreamWriteError(err) + continue + } + + atomic.AddUint64(sm.bytesSent, uint64(le)) + atomic.AddUint64(sm.rtcpPacketsSent, 1) + } + } + + // send multicast + if sm.multicastWriter != nil { + err := sm.multicastWriter.writePacketRTCP(byts) + if err != nil { + return err + } + + atomic.AddUint64(sm.bytesSent, uint64(le)) + atomic.AddUint64(sm.rtcpPacketsSent, 1) + } + + return nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_stream_stats.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_stream_stats.go new file mode 100644 index 000000000..6e845a6ef --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_stream_stats.go @@ -0,0 +1,36 @@ +package gortsplib + +import ( + "github.com/bluenviron/gortsplib/v4/pkg/description" + "github.com/bluenviron/gortsplib/v4/pkg/format" +) + +// ServerStreamStatsFormat are stream format statistics. +type ServerStreamStatsFormat struct { + // number of sent RTP packets + RTPPacketsSent uint64 +} + +// ServerStreamStatsMedia are stream media statistics. +type ServerStreamStatsMedia struct { + // sent bytes + BytesSent uint64 + // number of sent RTCP packets + RTCPPacketsSent uint64 + + // format statistics + Formats map[format.Format]ServerStreamStatsFormat +} + +// ServerStreamStats are stream statistics. +type ServerStreamStats struct { + // sent bytes + BytesSent uint64 + // number of sent RTP packets + RTPPacketsSent uint64 + // number of sent RTCP packets + RTCPPacketsSent uint64 + + // media statistics + Medias map[*description.Media]ServerStreamStatsMedia +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_tcp_listener.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_tcp_listener.go new file mode 100644 index 000000000..e90244c70 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_tcp_listener.go @@ -0,0 +1,42 @@ +package gortsplib + +import ( + "net" +) + +type serverTCPListener struct { + s *Server + + ln net.Listener +} + +func (sl *serverTCPListener) initialize() error { + var err error + sl.ln, err = sl.s.Listen(restrictNetwork("tcp", sl.s.RTSPAddress)) + if err != nil { + return err + } + + sl.s.wg.Add(1) + go sl.run() + + return nil +} + +func (sl *serverTCPListener) close() { + sl.ln.Close() +} + +func (sl *serverTCPListener) run() { + defer sl.s.wg.Done() + + for { + nconn, err := sl.ln.Accept() + if err != nil { + sl.s.acceptErr(err) + return + } + + sl.s.newConn(nconn) + } +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_udp_listener.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_udp_listener.go new file mode 100644 index 000000000..96dd756b8 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/server_udp_listener.go @@ -0,0 +1,186 @@ +package gortsplib + +import ( + "net" + "strconv" + "sync" + "time" + + "github.com/bluenviron/gortsplib/v4/pkg/multicast" +) + +type clientAddr struct { + ip [net.IPv6len]byte // use a fixed-size array to enable the equality operator + port int +} + +func (p *clientAddr) fill(ip net.IP, port int) { + p.port = port + + if len(ip) == net.IPv4len { + copy(p.ip[0:], []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xff, 0xff}) // v4InV6Prefix + copy(p.ip[12:], ip) + } else { + copy(p.ip[:], ip) + } +} + +func createUDPListenerMulticastPair( + listenPacket func(network, address string) (net.PacketConn, error), + writeTimeout time.Duration, + multicastRTPPort int, + multicastRTCPPort int, + ip net.IP, +) (*serverUDPListener, *serverUDPListener, error) { + rtpl := &serverUDPListener{ + listenPacket: listenPacket, + writeTimeout: writeTimeout, + multicastEnable: true, + address: net.JoinHostPort(ip.String(), strconv.FormatInt(int64(multicastRTPPort), 10)), + } + err := rtpl.initialize() + if err != nil { + return nil, nil, err + } + + rtcpl := &serverUDPListener{ + listenPacket: listenPacket, + writeTimeout: writeTimeout, + multicastEnable: true, + address: net.JoinHostPort(ip.String(), strconv.FormatInt(int64(multicastRTCPPort), 10)), + } + err = rtcpl.initialize() + if err != nil { + rtpl.close() + return nil, nil, err + } + + return rtpl, rtcpl, nil +} + +type serverUDPListener struct { + listenPacket func(network, address string) (net.PacketConn, error) + writeTimeout time.Duration + multicastEnable bool + address string + + pc packetConn + listenIP net.IP + clientsMutex sync.RWMutex + clients map[clientAddr]readFunc + + done chan struct{} +} + +func (u *serverUDPListener) initialize() error { + if u.multicastEnable { + var err error + u.pc, err = multicast.NewMultiConn(u.address, false, u.listenPacket) + if err != nil { + return err + } + + host, _, err := net.SplitHostPort(u.address) + if err != nil { + return err + } + u.listenIP = net.ParseIP(host) + } else { + tmp, err := u.listenPacket(restrictNetwork("udp", u.address)) + if err != nil { + return err + } + u.pc = tmp.(*net.UDPConn) + u.listenIP = tmp.LocalAddr().(*net.UDPAddr).IP + } + + err := u.pc.SetReadBuffer(udpKernelReadBufferSize) + if err != nil { + u.pc.Close() + return err + } + + u.clients = make(map[clientAddr]readFunc) + u.done = make(chan struct{}) + + go u.run() + + return nil +} + +func (u *serverUDPListener) close() { + u.pc.Close() + <-u.done +} + +func (u *serverUDPListener) ip() net.IP { + return u.listenIP +} + +func (u *serverUDPListener) port() int { + return u.pc.LocalAddr().(*net.UDPAddr).Port +} + +func (u *serverUDPListener) run() { + defer close(u.done) + + var buf []byte + + createNewBuffer := func() { + buf = make([]byte, udpMaxPayloadSize+1) + } + + createNewBuffer() + + for { + n, addr2, err := u.pc.ReadFrom(buf) + if err != nil { + break + } + addr := addr2.(*net.UDPAddr) + + func() { + u.clientsMutex.RLock() + defer u.clientsMutex.RUnlock() + + var ca clientAddr + ca.fill(addr.IP, addr.Port) + cb, ok := u.clients[ca] + if !ok { + return + } + + if cb(buf[:n]) { + createNewBuffer() + } + }() + } +} + +func (u *serverUDPListener) write(buf []byte, addr *net.UDPAddr) error { + // no mutex is needed here since Write() has an internal lock. + // https://github.com/golang/go/issues/27203#issuecomment-534386117 + u.pc.SetWriteDeadline(time.Now().Add(u.writeTimeout)) + _, err := u.pc.WriteTo(buf, addr) + return err +} + +func (u *serverUDPListener) addClient(ip net.IP, port int, cb readFunc) { + var addr clientAddr + addr.fill(ip, port) + + u.clientsMutex.Lock() + defer u.clientsMutex.Unlock() + + u.clients[addr] = cb +} + +func (u *serverUDPListener) removeClient(ip net.IP, port int) { + var addr clientAddr + addr.fill(ip, port) + + u.clientsMutex.Lock() + defer u.clientsMutex.Unlock() + + delete(u.clients, addr) +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/stats_conn.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/stats_conn.go new file mode 100644 index 000000000..635519b93 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/stats_conn.go @@ -0,0 +1,9 @@ +package gortsplib + +// StatsConn are connection statistics. +type StatsConn struct { + // received bytes + BytesReceived uint64 + // sent bytes + BytesSent uint64 +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/stats_session.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/stats_session.go new file mode 100644 index 000000000..5540ef628 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/stats_session.go @@ -0,0 +1,76 @@ +package gortsplib + +import ( + "time" + + "github.com/bluenviron/gortsplib/v4/pkg/description" + "github.com/bluenviron/gortsplib/v4/pkg/format" +) + +// StatsSessionFormat are session format statistics. +type StatsSessionFormat struct { + // number of RTP packets correctly received and processed + RTPPacketsReceived uint64 + // number of sent RTP packets + RTPPacketsSent uint64 + // number of lost RTP packets + RTPPacketsLost uint64 + // mean jitter of received RTP packets + RTPPacketsJitter float64 + // local SSRC + LocalSSRC uint32 + // remote SSRC + RemoteSSRC uint32 + // last sequence number of incoming/outgoing RTP packets + RTPPacketsLastSequenceNumber uint16 + // last RTP time of incoming/outgoing RTP packets + RTPPacketsLastRTP uint32 + // last NTP time of incoming/outgoing NTP packets + RTPPacketsLastNTP time.Time +} + +// StatsSessionMedia are session media statistics. +type StatsSessionMedia struct { + // received bytes + BytesReceived uint64 + // sent bytes + BytesSent uint64 + // number of RTP packets that could not be processed + RTPPacketsInError uint64 + // number of RTCP packets correctly received and processed + RTCPPacketsReceived uint64 + // number of sent RTCP packets + RTCPPacketsSent uint64 + // number of RTCP packets that could not be processed + RTCPPacketsInError uint64 + + // format statistics + Formats map[format.Format]StatsSessionFormat +} + +// StatsSession are session statistics. +type StatsSession struct { + // received bytes + BytesReceived uint64 + // sent bytes + BytesSent uint64 + // number of RTP packets correctly received and processed + RTPPacketsReceived uint64 + // number of sent RTP packets + RTPPacketsSent uint64 + // number of lost RTP packets + RTPPacketsLost uint64 + // number of RTP packets that could not be processed + RTPPacketsInError uint64 + // mean jitter of received RTP packets + RTPPacketsJitter float64 + // number of RTCP packets correctly received and processed + RTCPPacketsReceived uint64 + // number of sent RTCP packets + RTCPPacketsSent uint64 + // number of RTCP packets that could not be processed + RTCPPacketsInError uint64 + + // media statistics + Medias map[*description.Media]StatsSessionMedia +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/transport.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/transport.go new file mode 100644 index 000000000..470f7d7ce --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/gortsplib/v4/transport.go @@ -0,0 +1,25 @@ +package gortsplib + +// Transport is a RTSP transport protocol. +type Transport int + +// transport protocols. +const ( + TransportUDP Transport = iota + TransportUDPMulticast + TransportTCP +) + +var transportLabels = map[Transport]string{ + TransportUDP: "UDP", + TransportUDPMulticast: "UDP-multicast", + TransportTCP: "TCP", +} + +// String implements fmt.Stringer. +func (t Transport) String() string { + if l, ok := transportLabels[t]; ok { + return l + } + return "unknown" +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/LICENSE b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/LICENSE new file mode 100644 index 000000000..e06e71e4c --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2023 aler9 + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/bits/read.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/bits/read.go new file mode 100644 index 000000000..77a694f72 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/bits/read.go @@ -0,0 +1,121 @@ +// Package bits contains functions to read/write bits from/to buffers. +package bits + +import ( + "fmt" +) + +// HasSpace checks whether buffer has space for N bits. +func HasSpace(buf []byte, pos int, n int) error { + if n > ((len(buf) * 8) - pos) { + return fmt.Errorf("not enough bits") + } + return nil +} + +// ReadBits reads N bits. +func ReadBits(buf []byte, pos *int, n int) (uint64, error) { + err := HasSpace(buf, *pos, n) + if err != nil { + return 0, err + } + + return ReadBitsUnsafe(buf, pos, n), nil +} + +// ReadBitsUnsafe reads N bits. +func ReadBitsUnsafe(buf []byte, pos *int, n int) uint64 { + res := 8 - (*pos & 0x07) + if n < res { + v := uint64((buf[*pos>>0x03] >> (res - n)) & (1<>0x03] & (1<= 8 { + v = (v << 8) | uint64(buf[*pos>>0x03]) + *pos += 8 + n -= 8 + } + + if n > 0 { + v = (v << n) | uint64(buf[*pos>>0x03]>>(8-n)) + *pos += n + } + + return v +} + +// ReadGolombUnsigned reads an unsigned golomb-encoded value. +func ReadGolombUnsigned(buf []byte, pos *int) (uint32, error) { + buflen := len(buf) + leadingZeros := uint32(0) + + for { + if (buflen*8 - *pos) == 0 { + return 0, fmt.Errorf("not enough bits") + } + + b := (buf[*pos>>0x03] >> (7 - (*pos & 0x07))) & 0x01 + *pos++ + if b != 0 { + break + } + + leadingZeros++ + if leadingZeros > 32 { + return 0, fmt.Errorf("invalid value") + } + } + + if (buflen*8 - *pos) < int(leadingZeros) { + return 0, fmt.Errorf("not enough bits") + } + + codeNum := uint32(0) + + for n := leadingZeros; n > 0; n-- { + b := (buf[*pos>>0x03] >> (7 - (*pos & 0x07))) & 0x01 + *pos++ + codeNum |= uint32(b) << (n - 1) + } + + codeNum = (1 << leadingZeros) - 1 + codeNum + + return codeNum, nil +} + +// ReadGolombSigned reads a signed golomb-encoded value. +func ReadGolombSigned(buf []byte, pos *int) (int32, error) { + v, err := ReadGolombUnsigned(buf, pos) + if err != nil { + return 0, err + } + + vi := int32(v) + if (vi & 0x01) != 0 { + return (vi + 1) / 2, nil + } + return -vi / 2, nil +} + +// ReadFlag reads a boolean flag. +func ReadFlag(buf []byte, pos *int) (bool, error) { + err := HasSpace(buf, *pos, 1) + if err != nil { + return false, err + } + + return ReadFlagUnsafe(buf, pos), nil +} + +// ReadFlagUnsafe reads a boolean flag. +func ReadFlagUnsafe(buf []byte, pos *int) bool { + b := (buf[*pos>>0x03] >> (7 - (*pos & 0x07))) & 0x01 + *pos++ + return b == 1 +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/bits/write.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/bits/write.go new file mode 100644 index 000000000..ca7823a80 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/bits/write.go @@ -0,0 +1,26 @@ +package bits + +// WriteBitsUnsafe writes N bits. +func WriteBitsUnsafe(buf []byte, pos *int, v uint64, n int) { + res := 8 - (*pos & 0x07) + if n < res { + buf[*pos>>0x03] |= byte(v << (res - n)) + *pos += n + return + } + + buf[*pos>>3] |= byte(v >> (n - res)) + *pos += res + n -= res + + for n >= 8 { + buf[*pos>>3] = byte(v >> (n - 8)) + *pos += 8 + n -= 8 + } + + if n > 0 { + buf[*pos>>3] = byte((v & (1<> 3 + if b.Bsid != 0x08 { + return fmt.Errorf("invalid bsid") + } + + b.Bsmod = buf[0] & 0b111 + + buf = buf[1:] + pos := 0 + + tmp := bits.ReadBitsUnsafe(buf, &pos, 3) + b.Acmod = uint8(tmp) + + if ((b.Acmod & 0x1) != 0) && (b.Acmod != 0x1) { + bits.ReadBitsUnsafe(buf, &pos, 2) // cmixlev + } + + if (b.Acmod & 0x4) != 0 { + bits.ReadBitsUnsafe(buf, &pos, 2) // surmixlev + } + + if b.Acmod == 0x2 { + bits.ReadBitsUnsafe(buf, &pos, 2) // dsurmod + } + + b.LfeOn = bits.ReadFlagUnsafe(buf, &pos) + + return nil +} + +// ChannelCount returns the channel count. +func (b BSI) ChannelCount() int { + var n int + switch b.Acmod { + case 0b001: + n = 1 + case 0b010, 0b000: + n = 2 + case 0b011, 0b100: + n = 3 + case 0b101, 0b110: + n = 4 + default: + n = 5 + } + + if b.LfeOn { + return n + 1 + } + return n +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/ac3/sync_info.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/ac3/sync_info.go new file mode 100644 index 000000000..e6acf586b --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/ac3/sync_info.go @@ -0,0 +1,94 @@ +package ac3 + +import ( + "fmt" +) + +// ATSC, AC-3, Table 5.18 +var frameSizes = [][]int{ + {64, 69, 96}, + {64, 70, 96}, + {80, 87, 120}, + {80, 88, 120}, + {96, 104, 144}, + {96, 105, 144}, + {112, 121, 168}, + {112, 122, 168}, + {128, 139, 192}, + {128, 140, 192}, + {160, 174, 240}, + {160, 175, 240}, + {192, 208, 288}, + {192, 209, 288}, + {224, 243, 336}, + {224, 244, 336}, + {256, 278, 384}, + {256, 279, 384}, + {320, 348, 480}, + {320, 349, 480}, + {384, 417, 576}, + {384, 418, 576}, + {448, 487, 672}, + {448, 488, 672}, + {512, 557, 768}, + {512, 558, 768}, + {640, 696, 960}, + {640, 697, 960}, + {768, 835, 1152}, + {768, 836, 1152}, + {896, 975, 1344}, + {896, 976, 1344}, + {1024, 1114, 1536}, + {1024, 1115, 1536}, + {1152, 1253, 1728}, + {1152, 1254, 1728}, + {1280, 1393, 1920}, + {1280, 1394, 1920}, +} + +// SyncInfo is a synchronization information. +// Specification: ATSC, AC-3, Table 5.1 +type SyncInfo struct { + Fscod uint8 + Frmsizecod uint8 +} + +// Unmarshal decodes a SyncInfo. +func (s *SyncInfo) Unmarshal(frame []byte) error { + if len(frame) < 5 { + return fmt.Errorf("not enough bits") + } + + if frame[0] != 0x0B || frame[1] != 0x77 { + return fmt.Errorf("invalid sync word") + } + + s.Fscod = frame[4] >> 6 + if s.Fscod >= 3 { + return fmt.Errorf("invalid fscod") + } + + s.Frmsizecod = frame[4] & 0x3f + if s.Frmsizecod >= 38 { + return fmt.Errorf("invalid frmsizecod") + } + + return nil +} + +// FrameSize returns the frame size. +func (s SyncInfo) FrameSize() int { + return frameSizes[s.Frmsizecod][s.Fscod] * 2 +} + +// SampleRate returns the frame sample rate. +func (s SyncInfo) SampleRate() int { + switch s.Fscod { + case 0: + return 48000 + case 1: + return 44100 + default: + return 32000 + } +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/av1/av1.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/av1/av1.go new file mode 100644 index 000000000..674ebf312 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/av1/av1.go @@ -0,0 +1,10 @@ +// Package av1 contains utilities to work with the AV1 codec. +package av1 + +const ( + // MaxTemporalUnitSize is the maximum size of a temporal unit. + MaxTemporalUnitSize = 3 * 1024 * 1024 + + // MaxOBUsPerTemporalUnit is the maximum number of OBUs per temporal unit. + MaxOBUsPerTemporalUnit = 10 +) diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/av1/bitstream.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/av1/bitstream.go new file mode 100644 index 000000000..daa4bde93 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/av1/bitstream.go @@ -0,0 +1,86 @@ +package av1 + +import ( + "fmt" +) + +// Bitstream is an AV1 bitstream. +// Specification: https://aomediacodec.github.io/av1-spec/#low-overhead-bitstream-format +type Bitstream [][]byte + +// Unmarshal decodes a Bitstream. +func (bs *Bitstream) Unmarshal(buf []byte) error { + for { + var h OBUHeader + err := h.Unmarshal(buf) + if err != nil { + return err + } + + if !h.HasSize { + return fmt.Errorf("OBU size not present") + } + + var size LEB128 + n, err := size.Unmarshal(buf[1:]) + if err != nil { + return err + } + + obuLen := 1 + n + int(size) + if len(buf) < obuLen { + return fmt.Errorf("not enough bytes") + } + + var obu []byte + obu, buf = buf[:obuLen], buf[obuLen:] + + *bs = append(*bs, obu) + + if len(buf) == 0 { + break + } + } + + return nil +} + +// Marshal encodes a Bitstream. +func (bs Bitstream) Marshal() ([]byte, error) { + n := 0 + + for _, obu := range bs { + n += len(obu) + + var h OBUHeader + err := h.Unmarshal(obu) + if err != nil { + return nil, err + } + + if !h.HasSize { + size := len(obu) - 1 + n += LEB128(uint32(size)).MarshalSize() + } + } + + buf := make([]byte, n) + n = 0 + + for _, obu := range bs { + var h OBUHeader + h.Unmarshal(obu) //nolint:errcheck + + if !h.HasSize { + buf[n] = obu[0] | 0b00000010 + n++ + size := len(obu) - 1 + n += LEB128(uint32(size)).MarshalTo(buf[n:]) + n += copy(buf[n:], obu[1:]) + } else { + n += copy(buf[n:], obu) + } + } + + return buf, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/av1/is_random_access.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/av1/is_random_access.go new file mode 100644 index 000000000..400b5e7d4 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/av1/is_random_access.go @@ -0,0 +1,35 @@ +package av1 + +import ( + "fmt" +) + +// IsRandomAccess2 checks whether a temporal unit can be randomly accessed. +func IsRandomAccess2(tu [][]byte) bool { + for _, obu := range tu { + var h OBUHeader + err := h.Unmarshal(obu) + if err == nil && h.Type == OBUTypeSequenceHeader { + return true + } + } + + return false +} + +// IsRandomAccess checks whether a temporal unit can be randomly accessed. +// +// Deprecated: replaced by IsRandomAccess2. +func IsRandomAccess(tu [][]byte) (bool, error) { + if len(tu) == 0 { + return false, fmt.Errorf("temporal unit is empty") + } + + var h OBUHeader + err := h.Unmarshal(tu[0]) + if err != nil { + return false, err + } + + return (h.Type == OBUTypeSequenceHeader), nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/av1/leb128.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/av1/leb128.go new file mode 100644 index 000000000..39920106e --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/av1/leb128.go @@ -0,0 +1,73 @@ +package av1 + +import ( + "fmt" +) + +// LEB128 is a unsigned integer that can be decoded/encoded from/to the LEB128 format. +// Specification: https://aomediacodec.github.io/av1-spec/#leb128 +type LEB128 uint32 + +// Unmarshal decodes an unsigned integer from the LEB128 format. +// It returns the number of consumed bytes. +func (l *LEB128) Unmarshal(buf []byte) (int, error) { + *l = 0 + n := 0 + + for i := 0; i < 8; i++ { + if len(buf) == 0 { + return 0, fmt.Errorf("not enough bytes") + } + + var b byte + b, buf = buf[0], buf[1:] + + *l |= (LEB128(b&0b01111111) << (i * 7)) + n++ + + if (b & 0b10000000) == 0 { + break + } + } + + return n, nil +} + +// MarshalSize returns the marshal size in bytes of the unsigned integer in LEB128 format. +func (l LEB128) MarshalSize() int { + n := 0 + + for { + l >>= 7 + n++ + + if l <= 0 { + break + } + } + + return n +} + +// MarshalTo encodes the unsigned integer with the LEB128 format. +// It returns the number of consumed bytes. +func (l LEB128) MarshalTo(buf []byte) int { + n := 0 + + for { + curbyte := byte(l) & 0b01111111 + l >>= 7 + + if l <= 0 { + buf[n] = curbyte + n++ + break + } + + curbyte |= 0b10000000 + buf[n] = curbyte + n++ + } + + return n +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/av1/obu_header.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/av1/obu_header.go new file mode 100644 index 000000000..c831a05e2 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/av1/obu_header.go @@ -0,0 +1,35 @@ +package av1 + +import ( + "fmt" +) + +// OBUHeader is a OBU header. +// Specification: https://aomediacodec.github.io/av1-spec/#obu-header-syntax +type OBUHeader struct { + Type OBUType + HasSize bool +} + +// Unmarshal decodes a OBUHeader. +func (h *OBUHeader) Unmarshal(buf []byte) error { + if len(buf) < 1 { + return fmt.Errorf("not enough bytes") + } + + forbidden := (buf[0] >> 7) != 0 + if forbidden { + return fmt.Errorf("forbidden bit is set") + } + + h.Type = OBUType(buf[0] >> 3) + + extensionFlag := ((buf[0] >> 2) & 0b1) != 0 + if extensionFlag { + return fmt.Errorf("extension flag is not supported yet") + } + + h.HasSize = ((buf[0] >> 1) & 0b1) != 0 + + return nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/av1/obu_type.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/av1/obu_type.go new file mode 100644 index 000000000..9eca2b3fa --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/av1/obu_type.go @@ -0,0 +1,9 @@ +package av1 + +// OBUType is an OBU type. +type OBUType uint8 + +// OBU types. +const ( + OBUTypeSequenceHeader OBUType = 1 +) diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/av1/sequence_header.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/av1/sequence_header.go new file mode 100644 index 000000000..ecbdccb1a --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/av1/sequence_header.go @@ -0,0 +1,550 @@ +package av1 + +import ( + "fmt" + + "github.com/bluenviron/mediacommon/v2/pkg/bits" +) + +// SequenceHeader_ColorPrimaries is a ColorPrimaries value. +type SequenceHeader_ColorPrimaries uint8 //nolint:revive + +const ( + SequenceHeader_ColorPrimaries_CP_BT_709 SequenceHeader_ColorPrimaries = 1 //nolint:revive + SequenceHeader_ColorPrimaries_CP_UNSPECIFIED SequenceHeader_ColorPrimaries = 2 //nolint:revive +) + +// SequenceHeader_TransferCharacteristics is a TransferCharacteristics value. +type SequenceHeader_TransferCharacteristics uint8 //nolint:revive + +const ( + SequenceHeader_TransferCharacteristics_TC_UNSPECIFIED SequenceHeader_TransferCharacteristics = 2 //nolint:revive + SequenceHeader_TransferCharacteristics_TC_SRGB SequenceHeader_TransferCharacteristics = 13 //nolint:revive +) + +// SequenceHeader_MatrixCoefficients is a MatrixCoefficients value. +type SequenceHeader_MatrixCoefficients uint8 //nolint:revive + +const ( + SequenceHeader_MatrixCoefficients_MC_IDENTITY SequenceHeader_MatrixCoefficients = 0 //nolint:revive + SequenceHeader_MatrixCoefficients_MC_UNSPECIFIED SequenceHeader_MatrixCoefficients = 2 //nolint:revive +) + +// SequenceHeader_ChromaSamplePosition is a ChromaSamplePosition value. +type SequenceHeader_ChromaSamplePosition uint8 //nolint:revive + +const ( + SequenceHeader_ChromaSamplePosition_CSP_UNKNOWN SequenceHeader_ChromaSamplePosition = 0 //nolint:revive +) + +// SequenceHeader_SeqForceScreenContentTools is a SeqForceScreenContentTools value. +type SequenceHeader_SeqForceScreenContentTools uint8 //nolint:revive + +const ( + SequenceHeader_SeqForceScreenContentTools_SELECT_SCREEN_CONTENT_TOOLS SequenceHeader_SeqForceScreenContentTools = 2 //nolint:revive,lll +) + +// SequenceHeader_SeqForceIntegerMv is a SeqForceIntegerMv value. +type SequenceHeader_SeqForceIntegerMv uint8 //nolint:revive + +const ( + SequenceHeader_SeqForceIntegerMv_SELECT_INTEGER_MV SequenceHeader_SeqForceIntegerMv = 2 //nolint:revive +) + +// SequenceHeader_ColorConfig is a color configuration of a sequence header. +type SequenceHeader_ColorConfig struct { //nolint:revive + HighBitDepth bool + TwelveBit bool + BitDepth int + MonoChrome bool + ColorDescriptionPresentFlag bool + ColorPrimaries SequenceHeader_ColorPrimaries + TransferCharacteristics SequenceHeader_TransferCharacteristics + MatrixCoefficients SequenceHeader_MatrixCoefficients + ColorRange bool + SubsamplingX bool + SubsamplingY bool + ChromaSamplePosition SequenceHeader_ChromaSamplePosition +} + +func (c *SequenceHeader_ColorConfig) unmarshal(seqProfile uint8, buf []byte, pos *int) error { + var err error + c.HighBitDepth, err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + + if seqProfile == 2 && c.HighBitDepth { + c.TwelveBit, err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + + if c.TwelveBit { + c.BitDepth = 12 + } else { + c.BitDepth = 10 + } + } else if seqProfile <= 2 { + if c.HighBitDepth { + c.BitDepth = 10 + } else { + c.BitDepth = 8 + } + } + + if seqProfile == 1 { + c.MonoChrome = false + } else { + c.MonoChrome, err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + } + + c.ColorDescriptionPresentFlag, err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + + if c.ColorDescriptionPresentFlag { + err = bits.HasSpace(buf, *pos, 24) + if err != nil { + return err + } + + c.ColorPrimaries = SequenceHeader_ColorPrimaries(bits.ReadBitsUnsafe(buf, pos, 8)) + c.TransferCharacteristics = SequenceHeader_TransferCharacteristics(bits.ReadBitsUnsafe(buf, pos, 8)) + c.MatrixCoefficients = SequenceHeader_MatrixCoefficients(bits.ReadBitsUnsafe(buf, pos, 8)) + } else { + c.ColorPrimaries = SequenceHeader_ColorPrimaries_CP_UNSPECIFIED + c.TransferCharacteristics = SequenceHeader_TransferCharacteristics_TC_UNSPECIFIED + c.MatrixCoefficients = SequenceHeader_MatrixCoefficients_MC_UNSPECIFIED + } + + switch { + case c.MonoChrome: + c.ColorRange, err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + + c.SubsamplingX = true + c.SubsamplingY = true + c.ChromaSamplePosition = SequenceHeader_ChromaSamplePosition_CSP_UNKNOWN + case c.ColorPrimaries == SequenceHeader_ColorPrimaries_CP_BT_709 && + c.TransferCharacteristics == SequenceHeader_TransferCharacteristics_TC_SRGB && + c.MatrixCoefficients == SequenceHeader_MatrixCoefficients_MC_IDENTITY: + c.ColorRange = true + c.SubsamplingX = false + c.SubsamplingY = false + default: + c.ColorRange, err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + + switch { + case seqProfile == 0: + c.SubsamplingX = true + c.SubsamplingY = true + case seqProfile == 1: + c.SubsamplingX = false + c.SubsamplingY = false + default: + if c.BitDepth == 12 { + c.SubsamplingX, err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + + if c.SubsamplingX { + c.SubsamplingY, err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + } else { + c.SubsamplingY = false + } + } else { + c.SubsamplingX = true + c.SubsamplingY = false + } + } + + if c.SubsamplingX && c.SubsamplingY { + tmp, err := bits.ReadBits(buf, pos, 2) + if err != nil { + return err + } + c.ChromaSamplePosition = SequenceHeader_ChromaSamplePosition(tmp) + } + } + + return nil +} + +// SequenceHeader_TimingInfo is the timing_info() struct in the AV1 specification. +type SequenceHeader_TimingInfo struct { //nolint:revive + NumUnitsInDisplayTick uint32 + TimeScale uint32 + EqualPictureInterval bool + NumTicksPerPictureMinus1 uint32 +} + +func (t *SequenceHeader_TimingInfo) unmarshal(buf []byte, pos *int) error { + err := bits.HasSpace(buf, *pos, 65) + if err != nil { + return err + } + + t.NumUnitsInDisplayTick = uint32(bits.ReadBitsUnsafe(buf, pos, 32)) + t.TimeScale = uint32(bits.ReadBitsUnsafe(buf, pos, 32)) + t.EqualPictureInterval = bits.ReadFlagUnsafe(buf, pos) + + if t.EqualPictureInterval { + t.NumTicksPerPictureMinus1, err = bits.ReadGolombUnsigned(buf, pos) + if err != nil { + return err + } + } else { + t.NumTicksPerPictureMinus1 = 0 + } + + return nil +} + +// SequenceHeader is a AV1 Sequence header OBU. +// Specification: https://aomediacodec.github.io/av1-spec/#sequence-header-obu-syntax +type SequenceHeader struct { + SeqProfile uint8 + StillPicture bool + ReducedStillPictureHeader bool + TimingInfo *SequenceHeader_TimingInfo + DecoderModelInfoPresentFlag bool + InitialDisplayDelayPresentFlag bool + OperatingPointsCntMinus1 uint8 + OperatingPointIdc []uint16 + SeqLevelIdx []uint8 + SeqTier []bool + DecoderModelPresentForThisOp []bool + InitialDisplayPresentForThisOp []bool + InitialDisplayDelayMinus1 []uint8 + MaxFrameWidthMinus1 uint32 + MaxFrameHeightMinus1 uint32 + FrameIDNumbersPresentFlag bool + DeltaFrameIDLengthMinus2 uint8 + AdditionalFrameIDLengthMinus1 uint8 + Use128x128Superblock bool + EnableFilterIntra bool + EnableIntraEdgeFilter bool + EnableInterintraCompound bool + EnableMaskedCompound bool + EnableWarpedMotion bool + EnableDualFilter bool + EnableOrderHint bool + EnableJntComp bool + EnableRefFrameMvs bool + SeqChooseScreenContentTools bool + SeqForceScreenContentTools SequenceHeader_SeqForceScreenContentTools + SeqChooseIntegerMv bool + SeqForceIntegerMv SequenceHeader_SeqForceIntegerMv + OrderHintBitsMinus1 uint8 + EnableSuperRes bool + EnableCdef bool + EnableRestoration bool + ColorConfig SequenceHeader_ColorConfig + FilmGrainParamsPresent bool +} + +// Unmarshal decodes a SequenceHeader. +func (h *SequenceHeader) Unmarshal(buf []byte) error { + var oh OBUHeader + err := oh.Unmarshal(buf) + if err != nil { + return err + } + buf = buf[1:] + + if oh.HasSize { + var size LEB128 + var n int + n, err = size.Unmarshal(buf) + if err != nil { + return err + } + + buf = buf[n:] + if len(buf) != int(size) { + return fmt.Errorf("wrong buffer size: expected %d, got %d", size, len(buf)) + } + } + + pos := 0 + + err = bits.HasSpace(buf, pos, 5) + if err != nil { + return err + } + + h.SeqProfile = uint8(bits.ReadBitsUnsafe(buf, &pos, 3)) + h.StillPicture = bits.ReadFlagUnsafe(buf, &pos) + h.ReducedStillPictureHeader = bits.ReadFlagUnsafe(buf, &pos) + + if h.ReducedStillPictureHeader { + h.TimingInfo = nil + h.DecoderModelInfoPresentFlag = false + h.InitialDisplayDelayPresentFlag = false + h.OperatingPointsCntMinus1 = 0 + h.OperatingPointIdc = []uint16{0} + + err = bits.HasSpace(buf, pos, 5) + if err != nil { + return err + } + + h.SeqLevelIdx = []uint8{uint8(bits.ReadBitsUnsafe(buf, &pos, 5))} + h.SeqTier = []bool{false} + h.DecoderModelPresentForThisOp = []bool{false} + h.InitialDisplayPresentForThisOp = []bool{false} + } else { + var timingInfoPresentFlag bool + timingInfoPresentFlag, err = bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + + if timingInfoPresentFlag { + h.TimingInfo = &SequenceHeader_TimingInfo{} + err = h.TimingInfo.unmarshal(buf, &pos) + if err != nil { + return err + } + + h.DecoderModelInfoPresentFlag, err = bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + if h.DecoderModelInfoPresentFlag { + return fmt.Errorf("decoder_model_info_present_flag is not supported yet") + } + } else { + h.TimingInfo = nil + h.DecoderModelInfoPresentFlag = false + } + + err = bits.HasSpace(buf, pos, 6) + if err != nil { + return err + } + + h.InitialDisplayDelayPresentFlag = bits.ReadFlagUnsafe(buf, &pos) + h.OperatingPointsCntMinus1 = uint8(bits.ReadBitsUnsafe(buf, &pos, 5)) + + h.OperatingPointIdc = make([]uint16, h.OperatingPointsCntMinus1+1) + h.SeqLevelIdx = make([]uint8, h.OperatingPointsCntMinus1+1) + h.SeqTier = make([]bool, h.OperatingPointsCntMinus1+1) + h.DecoderModelPresentForThisOp = make([]bool, h.OperatingPointsCntMinus1+1) + h.InitialDisplayPresentForThisOp = make([]bool, h.OperatingPointsCntMinus1+1) + h.InitialDisplayDelayMinus1 = make([]uint8, h.OperatingPointsCntMinus1+1) + + for i := uint8(0); i <= h.OperatingPointsCntMinus1; i++ { + err = bits.HasSpace(buf, pos, 17) + if err != nil { + return err + } + + h.OperatingPointIdc[i] = uint16(bits.ReadBitsUnsafe(buf, &pos, 12)) + h.SeqLevelIdx[i] = uint8(bits.ReadBitsUnsafe(buf, &pos, 5)) + + if h.SeqLevelIdx[i] > 7 { + h.SeqTier[i], err = bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + } else { + h.SeqTier[i] = false + } + + if h.DecoderModelInfoPresentFlag { + return fmt.Errorf("decoder_model_info_present_flag is not supported yet") + } + h.DecoderModelPresentForThisOp[i] = false + + if h.InitialDisplayDelayPresentFlag { + h.InitialDisplayPresentForThisOp[i], err = bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + + if h.InitialDisplayPresentForThisOp[i] { + var tmp uint64 + tmp, err = bits.ReadBits(buf, &pos, 4) + if err != nil { + return err + } + h.InitialDisplayDelayMinus1[i] = uint8(tmp) + } + return fmt.Errorf("initial_display_delay_present_flag is not supported yet") + } + } + } + + err = bits.HasSpace(buf, pos, 8) + if err != nil { + return err + } + + frameWidthBitsMinus1 := int(bits.ReadBitsUnsafe(buf, &pos, 4)) + frameHeightBitsMinus1 := int(bits.ReadBitsUnsafe(buf, &pos, 4)) + + n1 := (frameWidthBitsMinus1 + 1) + n2 := (frameHeightBitsMinus1 + 1) + + err = bits.HasSpace(buf, pos, n1+n2) + if err != nil { + return err + } + + h.MaxFrameWidthMinus1 = uint32(bits.ReadBitsUnsafe(buf, &pos, n1)) + h.MaxFrameHeightMinus1 = uint32(bits.ReadBitsUnsafe(buf, &pos, n2)) + + if h.ReducedStillPictureHeader { + h.FrameIDNumbersPresentFlag = false + } else { + h.FrameIDNumbersPresentFlag, err = bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + + if h.FrameIDNumbersPresentFlag { + err = bits.HasSpace(buf, pos, 7) + if err != nil { + return err + } + + h.DeltaFrameIDLengthMinus2 = uint8(bits.ReadBitsUnsafe(buf, &pos, 4)) + h.AdditionalFrameIDLengthMinus1 = uint8(bits.ReadBitsUnsafe(buf, &pos, 3)) + } + } + + err = bits.HasSpace(buf, pos, 3) + if err != nil { + return err + } + + h.Use128x128Superblock = bits.ReadFlagUnsafe(buf, &pos) + h.EnableFilterIntra = bits.ReadFlagUnsafe(buf, &pos) + h.EnableIntraEdgeFilter = bits.ReadFlagUnsafe(buf, &pos) + + if h.ReducedStillPictureHeader { + h.EnableInterintraCompound = false + h.EnableMaskedCompound = false + h.EnableWarpedMotion = false + h.EnableDualFilter = false + h.EnableOrderHint = false + h.EnableJntComp = false + h.EnableRefFrameMvs = false + h.SeqForceScreenContentTools = SequenceHeader_SeqForceScreenContentTools_SELECT_SCREEN_CONTENT_TOOLS + h.SeqForceIntegerMv = SequenceHeader_SeqForceIntegerMv_SELECT_INTEGER_MV + } else { + err = bits.HasSpace(buf, pos, 5) + if err != nil { + return err + } + + h.EnableInterintraCompound = bits.ReadFlagUnsafe(buf, &pos) + h.EnableMaskedCompound = bits.ReadFlagUnsafe(buf, &pos) + h.EnableWarpedMotion = bits.ReadFlagUnsafe(buf, &pos) + h.EnableDualFilter = bits.ReadFlagUnsafe(buf, &pos) + h.EnableOrderHint = bits.ReadFlagUnsafe(buf, &pos) + + if h.EnableOrderHint { + err = bits.HasSpace(buf, pos, 2) + if err != nil { + return err + } + + h.EnableJntComp = bits.ReadFlagUnsafe(buf, &pos) + h.EnableRefFrameMvs = bits.ReadFlagUnsafe(buf, &pos) + } + + h.SeqChooseScreenContentTools, err = bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + + if h.SeqChooseScreenContentTools { + h.SeqForceScreenContentTools = SequenceHeader_SeqForceScreenContentTools_SELECT_SCREEN_CONTENT_TOOLS + } else { + var tmp uint64 + tmp, err = bits.ReadBits(buf, &pos, 1) + if err != nil { + return err + } + h.SeqForceScreenContentTools = SequenceHeader_SeqForceScreenContentTools(tmp) + } + + if h.SeqForceScreenContentTools > 0 { + h.SeqChooseIntegerMv, err = bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + + if h.SeqChooseIntegerMv { + h.SeqForceIntegerMv = SequenceHeader_SeqForceIntegerMv_SELECT_INTEGER_MV + } else { + var tmp uint64 + tmp, err = bits.ReadBits(buf, &pos, 1) + if err != nil { + return err + } + h.SeqForceIntegerMv = SequenceHeader_SeqForceIntegerMv(tmp) + } + } else { + h.SeqForceIntegerMv = SequenceHeader_SeqForceIntegerMv_SELECT_INTEGER_MV + } + + if h.EnableOrderHint { + var tmp uint64 + tmp, err = bits.ReadBits(buf, &pos, 3) + if err != nil { + return err + } + h.OrderHintBitsMinus1 = uint8(tmp) + } + } + + err = bits.HasSpace(buf, pos, 3) + if err != nil { + return err + } + + h.EnableSuperRes = bits.ReadFlagUnsafe(buf, &pos) + h.EnableCdef = bits.ReadFlagUnsafe(buf, &pos) + h.EnableRestoration = bits.ReadFlagUnsafe(buf, &pos) + + err = h.ColorConfig.unmarshal(h.SeqProfile, buf, &pos) + if err != nil { + return err + } + + err = bits.HasSpace(buf, pos, 1) + if err != nil { + return err + } + + h.FilmGrainParamsPresent = bits.ReadFlagUnsafe(buf, &pos) + + return nil +} + +// Width returns the video width. +func (h SequenceHeader) Width() int { + return int(h.MaxFrameWidthMinus1 + 1) +} + +// Height returns the video height. +func (h SequenceHeader) Height() int { + return int(h.MaxFrameHeightMinus1 + 1) +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h264/annexb.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h264/annexb.go new file mode 100644 index 000000000..b7eb6d9d1 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h264/annexb.go @@ -0,0 +1,147 @@ +package h264 + +import ( + "errors" + "fmt" +) + +// ErrAnnexBNoNALUs is returned by AnnexBUnmarshal when no NALUs have been decoded. +var ErrAnnexBNoNALUs = errors.New("Annex-B unit doesn't contain any NALU") + +// ErrAnnexBNoInitialDelimiter is returned by AnnexBUnmarshal when the initial delimiter is not found. +var ErrAnnexBNoInitialDelimiter = errors.New("initial delimiter not found") + +// countNalUnits counts the number of NAL units in the Annex-B stream. +func countNalUnits(buf []byte) (int, error) { + n := 0 + i := 0 + start := 0 + auSize := 0 + + for i < len(buf) { + lim := 4 + if lim > len(buf)-i { + lim = len(buf) - i + } + data := buf[i : i+lim] + + switch { + case len(data) >= 3 && data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x01: + if i > start { + auSize += i - start + if auSize > MaxAccessUnitSize { + return 0, fmt.Errorf("access unit size (%d) is too big, maximum is %d", auSize, MaxAccessUnitSize) + } + n++ + } + i += 3 + start = i + case len(data) >= 4 && data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x00 && data[3] == 0x01: + if i > start { + auSize += i - start + if auSize > MaxAccessUnitSize { + return 0, fmt.Errorf("access unit size (%d) is too big, maximum is %d", auSize, MaxAccessUnitSize) + } + n++ + } + i += 4 + start = i + default: + i++ + } + } + + if i > start { + if (auSize + i - start) > MaxAccessUnitSize { + return 0, fmt.Errorf("access unit size (%d) is too big, maximum is %d", auSize+i-start, MaxAccessUnitSize) + } + n++ + } + + return n, nil +} + +func hasInitialDelimiter(buf []byte) bool { + if len(buf) < 4 { + return false + } + return buf[0] == 0x00 && buf[1] == 0x00 && (buf[2] == 0x00 && buf[3] == 0x01) || (buf[2] == 0x01) +} + +// AnnexB is an access unit that can be decoded/encoded from/to the Annex-B stream format. +// Specification: ITU-T Rec. H.264, Annex B +type AnnexB [][]byte + +// Unmarshal decodes an access unit from the Annex-B stream format. +func (a *AnnexB) Unmarshal(buf []byte) error { + count, err := countNalUnits(buf) + if err != nil { + return err + } + + if count == 0 { + return ErrAnnexBNoNALUs + } + + if !hasInitialDelimiter(buf) { + return ErrAnnexBNoInitialDelimiter + } + + *a = make([][]byte, 0, count) + i := 0 + start := 0 + + for i < len(buf) { + lim := 4 + if lim > len(buf)-i { + lim = len(buf) - i + } + data := buf[i : i+lim] + + switch { + case len(data) >= 3 && data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x01: + // Is this a NALU with a 3 byte start code prefix + if i > start { + *a = append(*a, buf[start:i]) + } + i += 3 + start = i + case len(data) >= 4 && data[0] == 0x00 && data[1] == 0x00 && data[2] == 0x00 && data[3] == 0x01: + // OR is this a NALU with a 4 byte start code prefix + if i > start { + *a = append(*a, buf[start:i]) + } + i += 4 + start = i + default: + i++ + } + } + + if i > start { + *a = append(*a, buf[start:i]) + } + + return nil +} + +func (a AnnexB) marshalSize() int { + n := 0 + for _, nalu := range a { + n += 4 + len(nalu) + } + return n +} + +// Marshal encodes an access unit into the Annex-B stream format. +func (a AnnexB) Marshal() ([]byte, error) { + buf := make([]byte, a.marshalSize()) + pos := 0 + + for _, nalu := range a { + pos += copy(buf[pos:], []byte{0x00, 0x00, 0x00, 0x01}) + pos += copy(buf[pos:], nalu) + } + + return buf, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h264/avcc.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h264/avcc.go new file mode 100644 index 000000000..1bf621504 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h264/avcc.go @@ -0,0 +1,100 @@ +package h264 + +import ( + "errors" + "fmt" +) + +// ErrAVCCNoNALUs is returned by AVCCUnmarshal when no NALUs have been decoded. +var ErrAVCCNoNALUs = errors.New("AVCC unit doesn't contain any NALU") + +// AVCC is an access unit that can be decoded/encoded from/to the Annex-B stream format. +// Specification: ISO 14496-15, section 5.3.4.2.1 +type AVCC [][]byte + +// Unmarshal decodes an access unit from the AVCC stream format. +func (a *AVCC) Unmarshal(buf []byte) error { + bl := len(buf) + pos := 0 + n := 0 + auSize := 0 + + for { + if (bl - pos) < 4 { + return fmt.Errorf("invalid length") + } + + l := int(uint32(buf[pos])<<24 | uint32(buf[pos+1])<<16 | uint32(buf[pos+2])<<8 | uint32(buf[pos+3])) + pos += 4 + + if l != 0 { + if (auSize + l) > MaxAccessUnitSize { + return fmt.Errorf("access unit size (%d) is too big, maximum is %d", auSize+l, MaxAccessUnitSize) + } + + if (bl - pos) < l { + return fmt.Errorf("invalid length") + } + + auSize += l + n++ + pos += l + } + + if (bl - pos) == 0 { + break + } + } + + if n == 0 { + return ErrAVCCNoNALUs + } + + if n > MaxNALUsPerAccessUnit { + return fmt.Errorf("NALU count (%d) exceeds maximum allowed (%d)", + n, MaxNALUsPerAccessUnit) + } + + *a = make([][]byte, n) + pos = 0 + + for i := 0; i < n; { + l := int(uint32(buf[pos])<<24 | uint32(buf[pos+1])<<16 | uint32(buf[pos+2])<<8 | uint32(buf[pos+3])) + pos += 4 + + if l != 0 { + (*a)[i] = buf[pos : pos+l] + pos += l + i++ + } + } + + return nil +} + +func (a AVCC) marshalSize() int { + n := 0 + for _, nalu := range a { + n += 4 + len(nalu) + } + return n +} + +// Marshal encodes an access unit into the AVCC stream format. +func (a AVCC) Marshal() ([]byte, error) { + buf := make([]byte, a.marshalSize()) + pos := 0 + + for _, nalu := range a { + naluLen := len(nalu) + buf[pos] = byte(naluLen >> 24) + buf[pos+1] = byte(naluLen >> 16) + buf[pos+2] = byte(naluLen >> 8) + buf[pos+3] = byte(naluLen) + pos += 4 + + pos += copy(buf[pos:], nalu) + } + + return buf, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h264/dts_extractor.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h264/dts_extractor.go new file mode 100644 index 000000000..a069d00c8 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h264/dts_extractor.go @@ -0,0 +1,248 @@ +package h264 + +import ( + "bytes" + "fmt" + + "github.com/bluenviron/mediacommon/v2/pkg/bits" +) + +const ( + maxReorderedFrames = 10 + /* + (max_size(first_mb_in_slice) + max_size(slice_type) + max_size(pic_parameter_set_id) + + max_size(frame_num) + max_size(pic_order_cnt_lsb)) * 4 / 3 = + (3 * max_size(golomb) + (max(Log2MaxFrameNumMinus4) + 4) / 8 + (max(Log2MaxPicOrderCntLsbMinus4) + 4) / 8) * 4 / 3 = + (3 * 4 + 2 + 2) * 4 / 3 = 22 + */ + maxBytesToGetPOC = 22 +) + +func getPictureOrderCount(buf []byte, sps *SPS, idr bool) (uint32, error) { + buf = buf[1:] + lb := len(buf) + + if lb > maxBytesToGetPOC { + lb = maxBytesToGetPOC + } + + buf = EmulationPreventionRemove(buf[:lb]) + pos := 0 + + _, err := bits.ReadGolombUnsigned(buf, &pos) // first_mb_in_slice + if err != nil { + return 0, err + } + + _, err = bits.ReadGolombUnsigned(buf, &pos) // slice_type + if err != nil { + return 0, err + } + + _, err = bits.ReadGolombUnsigned(buf, &pos) // pic_parameter_set_id + if err != nil { + return 0, err + } + + _, err = bits.ReadBits(buf, &pos, int(sps.Log2MaxFrameNumMinus4+4)) // frame_num + if err != nil { + return 0, err + } + + if idr { + _, err = bits.ReadGolombUnsigned(buf, &pos) // idr_pic_id + if err != nil { + return 0, err + } + } + + picOrderCntLsb, err := bits.ReadBits(buf, &pos, int(sps.Log2MaxPicOrderCntLsbMinus4+4)) + if err != nil { + return 0, err + } + + return uint32(picOrderCntLsb), nil +} + +func getPictureOrderCountDiff(a uint32, b uint32, sps *SPS) int32 { + maxVal := uint32(1 << (sps.Log2MaxPicOrderCntLsbMinus4 + 4)) + d := (a - b) & (maxVal - 1) + if d > (maxVal / 2) { + return int32(d) - int32(maxVal) + } + return int32(d) +} + +// DTSExtractor computes DTS from PTS. +type DTSExtractor struct { + sps []byte + spsp *SPS + prevDTSFilled bool + prevDTS int64 + expectedPOC uint32 + reorderedFrames int + pauseDTS int + pocIncrement int +} + +// Initialize initializes a DTSExtractor. +func (d *DTSExtractor) Initialize() { + d.pocIncrement = 2 +} + +// NewDTSExtractor allocates a DTSExtractor. +// +// Deprecated: replaced by DTSExtractor.Initialize. +func NewDTSExtractor() *DTSExtractor { + d := &DTSExtractor{} + d.Initialize() + return d +} + +func (d *DTSExtractor) extractInner(au [][]byte, pts int64) (int64, bool, error) { + var idr []byte + var nonIDR []byte + // a value of 00 indicates that the content of the NAL unit is not + // used to reconstruct reference pictures for inter picture + // prediction. Such NAL units can be discarded without risking + // the integrity of the reference pictures. Values greater than + // 00 indicate that the decoding of the NAL unit is required to + // maintain the integrity of the reference pictures. + nonZeroNalRefIDFound := false + + for _, nalu := range au { + typ := NALUType(nalu[0] & 0x1F) + nonZeroNalRefIDFound = nonZeroNalRefIDFound || ((nalu[0] & 0x60) > 0) + switch typ { + case NALUTypeSPS: + if !bytes.Equal(d.sps, nalu) { + var spsp SPS + err := spsp.Unmarshal(nalu) + if err != nil { + return 0, false, fmt.Errorf("invalid SPS: %w", err) + } + d.sps = nalu + d.spsp = &spsp + + // reset state + d.reorderedFrames = 0 + d.pocIncrement = 2 + } + + case NALUTypeIDR: + idr = nalu + + case NALUTypeNonIDR: + nonIDR = nalu + } + } + + if d.spsp == nil { + return 0, false, fmt.Errorf("SPS not received yet") + } + + if d.spsp.PicOrderCntType == 2 || !d.spsp.FrameMbsOnlyFlag { + return pts, false, nil + } + + if d.spsp.PicOrderCntType == 1 { + return 0, false, fmt.Errorf("pic_order_cnt_type = 1 is not supported yet") + } + + // Implicit processing of PicOrderCountType 0 + switch { + case idr != nil: + d.pauseDTS = 0 + + var err error + d.expectedPOC, err = getPictureOrderCount(idr, d.spsp, true) + if err != nil { + return 0, false, err + } + + if !d.prevDTSFilled || d.reorderedFrames == 0 { + return pts, false, nil + } + + return d.prevDTS + (pts-d.prevDTS)/int64(d.reorderedFrames+1), false, nil + + case nonIDR != nil: + d.expectedPOC += uint32(d.pocIncrement) + d.expectedPOC &= ((1 << (d.spsp.Log2MaxPicOrderCntLsbMinus4 + 4)) - 1) + + if d.pauseDTS > 0 { + d.pauseDTS-- + return d.prevDTS + 90, true, nil + } + + poc, err := getPictureOrderCount(nonIDR, d.spsp, false) + if err != nil { + return 0, false, err + } + + if d.pocIncrement == 2 && (poc%2) != 0 { + d.pocIncrement = 1 + d.expectedPOC /= 2 + } + + pocDiff := int(getPictureOrderCountDiff(poc, d.expectedPOC, d.spsp)) / d.pocIncrement + limit := -(d.reorderedFrames + 1) + + // this happens when there are B-frames immediately following an IDR frame + if pocDiff < limit { + increase := limit - pocDiff + if (d.reorderedFrames + increase) > maxReorderedFrames { + return 0, false, fmt.Errorf("too many reordered frames (%d)", d.reorderedFrames+increase) + } + + d.reorderedFrames += increase + d.pauseDTS = increase + return d.prevDTS + 90, true, nil + } + + if pocDiff == limit { + return pts, false, nil + } + + if pocDiff > d.reorderedFrames { + increase := pocDiff - d.reorderedFrames + if (d.reorderedFrames + increase) > maxReorderedFrames { + return 0, false, fmt.Errorf("too many reordered frames (%d)", d.reorderedFrames+increase) + } + + d.reorderedFrames += increase + d.pauseDTS = increase - 1 + return d.prevDTS + 90, false, nil + } + + return d.prevDTS + (pts-d.prevDTS)/int64(pocDiff+d.reorderedFrames+1), false, nil + + case !nonZeroNalRefIDFound: + return d.prevDTS, false, nil + + default: + return 0, false, fmt.Errorf("access unit doesn't contain an IDR or non-IDR NALU") + } +} + +// Extract extracts the DTS of an access unit. +func (d *DTSExtractor) Extract(au [][]byte, pts int64) (int64, error) { + dts, skipChecks, err := d.extractInner(au, pts) + if err != nil { + return 0, err + } + + if !skipChecks && dts > pts { + return 0, fmt.Errorf("DTS is greater than PTS") + } + + if d.prevDTSFilled && dts < d.prevDTS { + return 0, fmt.Errorf("DTS is not monotonically increasing, was %v, now is %v", + d.prevDTS, dts) + } + + d.prevDTS = dts + d.prevDTSFilled = true + + return dts, err +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h264/emulation_prevention.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h264/emulation_prevention.go new file mode 100644 index 000000000..f06c3904c --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h264/emulation_prevention.go @@ -0,0 +1,36 @@ +package h264 + +// EmulationPreventionRemove removes emulation prevention bytes from a NALU. +// Specification: ITU-T Rec. H.264, 7.4.1 NAL unit semantics +func EmulationPreventionRemove(nalu []byte) []byte { + // 0x00 0x00 0x03 0x00 -> 0x00 0x00 0x00 + // 0x00 0x00 0x03 0x01 -> 0x00 0x00 0x01 + // 0x00 0x00 0x03 0x02 -> 0x00 0x00 0x02 + // 0x00 0x00 0x03 0x03 -> 0x00 0x00 0x03 + + l := len(nalu) + n := l + + for i := 2; i < l; i++ { + if nalu[i-2] == 0 && nalu[i-1] == 0 && nalu[i] == 3 { + n-- + i += 2 + } + } + + ret := make([]byte, n) + pos := 0 + start := 0 + + for i := 2; i < l; i++ { + if nalu[i-2] == 0 && nalu[i-1] == 0 && nalu[i] == 3 { + pos += copy(ret[pos:], nalu[start:i]) + start = i + 1 + i += 2 + } + } + + copy(ret[pos:], nalu[start:]) + + return ret +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h264/h264.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h264/h264.go new file mode 100644 index 000000000..aea95937f --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h264/h264.go @@ -0,0 +1,12 @@ +// Package h264 contains utilities to work with the H264 codec. +package h264 + +const ( + // MaxAccessUnitSize is the maximum size of an access unit. + // With a 50 Mbps 2160p60 H264 video, the maximum size does not seem to exceed 8 MiB. + MaxAccessUnitSize = 8 * 1024 * 1024 + + // MaxNALUsPerAccessUnit is the maximum number of NALUs per access unit. + // with x264, tune=zerolatency and 4K resolution, NALU count is lower than 25. + MaxNALUsPerAccessUnit = 25 +) diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h264/is_random_access.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h264/is_random_access.go new file mode 100644 index 000000000..c1f174462 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h264/is_random_access.go @@ -0,0 +1,12 @@ +package h264 + +// IsRandomAccess checks whether the access unit can be randomly accessed. +func IsRandomAccess(au [][]byte) bool { + for _, nalu := range au { + typ := NALUType(nalu[0] & 0x1F) + if typ == NALUTypeIDR { + return true + } + } + return false +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h264/nalu_type.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h264/nalu_type.go new file mode 100644 index 000000000..30f580a14 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h264/nalu_type.go @@ -0,0 +1,84 @@ +package h264 + +import ( + "fmt" +) + +// NALUType is the type of a NALU. +// Specification: ITU-T Rec. H.264, Table 7-1 +type NALUType uint8 + +// NALU types. +const ( + NALUTypeNonIDR NALUType = 1 + NALUTypeDataPartitionA NALUType = 2 + NALUTypeDataPartitionB NALUType = 3 + NALUTypeDataPartitionC NALUType = 4 + NALUTypeIDR NALUType = 5 + NALUTypeSEI NALUType = 6 + NALUTypeSPS NALUType = 7 + NALUTypePPS NALUType = 8 + NALUTypeAccessUnitDelimiter NALUType = 9 + NALUTypeEndOfSequence NALUType = 10 + NALUTypeEndOfStream NALUType = 11 + NALUTypeFillerData NALUType = 12 + NALUTypeSPSExtension NALUType = 13 + NALUTypePrefix NALUType = 14 + NALUTypeSubsetSPS NALUType = 15 + NALUTypeReserved16 NALUType = 16 + NALUTypeReserved17 NALUType = 17 + NALUTypeReserved18 NALUType = 18 + NALUTypeSliceLayerWithoutPartitioning NALUType = 19 + NALUTypeSliceExtension NALUType = 20 + NALUTypeSliceExtensionDepth NALUType = 21 + NALUTypeReserved22 NALUType = 22 + NALUTypeReserved23 NALUType = 23 + + // additional NALU types for RTP/H264 + NALUTypeSTAPA NALUType = 24 + NALUTypeSTAPB NALUType = 25 + NALUTypeMTAP16 NALUType = 26 + NALUTypeMTAP24 NALUType = 27 + NALUTypeFUA NALUType = 28 + NALUTypeFUB NALUType = 29 +) + +var naluTypeLabels = map[NALUType]string{ + NALUTypeNonIDR: "NonIDR", + NALUTypeDataPartitionA: "DataPartitionA", + NALUTypeDataPartitionB: "DataPartitionB", + NALUTypeDataPartitionC: "DataPartitionC", + NALUTypeIDR: "IDR", + NALUTypeSEI: "SEI", + NALUTypeSPS: "SPS", + NALUTypePPS: "PPS", + NALUTypeAccessUnitDelimiter: "AccessUnitDelimiter", + NALUTypeEndOfSequence: "EndOfSequence", + NALUTypeEndOfStream: "EndOfStream", + NALUTypeFillerData: "FillerData", + NALUTypeSPSExtension: "SPSExtension", + NALUTypePrefix: "Prefix", + NALUTypeSubsetSPS: "SubsetSPS", + NALUTypeReserved16: "Reserved16", + NALUTypeReserved17: "Reserved17", + NALUTypeReserved18: "Reserved18", + NALUTypeSliceLayerWithoutPartitioning: "SliceLayerWithoutPartitioning", + NALUTypeSliceExtension: "SliceExtension", + NALUTypeSliceExtensionDepth: "SliceExtensionDepth", + NALUTypeReserved22: "Reserved22", + NALUTypeReserved23: "Reserved23", + NALUTypeSTAPA: "STAP-A", + NALUTypeSTAPB: "STAP-B", + NALUTypeMTAP16: "MTAP-16", + NALUTypeMTAP24: "MTAP-24", + NALUTypeFUA: "FU-A", + NALUTypeFUB: "FU-B", +} + +// String implements fmt.Stringer. +func (nt NALUType) String() string { + if l, ok := naluTypeLabels[nt]; ok { + return l + } + return fmt.Sprintf("unknown (%d)", nt) +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h264/sps.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h264/sps.go new file mode 100644 index 000000000..e6a998310 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h264/sps.go @@ -0,0 +1,809 @@ +package h264 + +import ( + "fmt" + + "github.com/bluenviron/mediacommon/v2/pkg/bits" +) + +const ( + maxRefFrames = 255 +) + +func readScalingList(buf []byte, pos *int, size int) ([]int32, bool, error) { + lastScale := int32(8) + nextScale := int32(8) + scalingList := make([]int32, size) + var useDefaultScalingMatrixFlag bool + + for j := 0; j < size; j++ { + if nextScale != 0 { + deltaScale, err := bits.ReadGolombSigned(buf, pos) + if err != nil { + return nil, false, err + } + + nextScale = (lastScale + deltaScale + 256) % 256 + useDefaultScalingMatrixFlag = (j == 0 && nextScale == 0) + } + + if nextScale == 0 { + scalingList[j] = lastScale + } else { + scalingList[j] = nextScale + } + + lastScale = scalingList[j] + } + + return scalingList, useDefaultScalingMatrixFlag, nil +} + +// SPS_HRD is a hypotetical reference decoder. +type SPS_HRD struct { //nolint:revive + CpbCntMinus1 uint32 + BitRateScale uint8 + CpbSizeScale uint8 + BitRateValueMinus1 []uint32 + CpbSizeValueMinus1 []uint32 + CbrFlag []bool + InitialCpbRemovalDelayLengthMinus1 uint8 + CpbRemovalDelayLengthMinus1 uint8 + DpbOutputDelayLengthMinus1 uint8 + TimeOffsetLength uint8 +} + +func (h *SPS_HRD) unmarshal(buf []byte, pos *int) error { + var err error + h.CpbCntMinus1, err = bits.ReadGolombUnsigned(buf, pos) + if err != nil { + return err + } + + err = bits.HasSpace(buf, *pos, 8) + if err != nil { + return err + } + + if h.CpbCntMinus1 > 31 { + return fmt.Errorf("invalid cpb_cnt_minus1") + } + + h.BitRateScale = uint8(bits.ReadBitsUnsafe(buf, pos, 4)) + h.CpbSizeScale = uint8(bits.ReadBitsUnsafe(buf, pos, 4)) + + h.BitRateValueMinus1 = make([]uint32, h.CpbCntMinus1+1) + h.CpbSizeValueMinus1 = make([]uint32, h.CpbCntMinus1+1) + h.CbrFlag = make([]bool, h.CpbCntMinus1+1) + + for i := uint32(0); i <= h.CpbCntMinus1; i++ { + h.BitRateValueMinus1[i], err = bits.ReadGolombUnsigned(buf, pos) + if err != nil { + return err + } + + h.CpbSizeValueMinus1[i], err = bits.ReadGolombUnsigned(buf, pos) + if err != nil { + return err + } + + h.CbrFlag[i], err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + } + + err = bits.HasSpace(buf, *pos, 5+5+5+5) + if err != nil { + return err + } + + h.InitialCpbRemovalDelayLengthMinus1 = uint8(bits.ReadBitsUnsafe(buf, pos, 5)) + h.CpbRemovalDelayLengthMinus1 = uint8(bits.ReadBitsUnsafe(buf, pos, 5)) + h.DpbOutputDelayLengthMinus1 = uint8(bits.ReadBitsUnsafe(buf, pos, 5)) + h.TimeOffsetLength = uint8(bits.ReadBitsUnsafe(buf, pos, 5)) + + return nil +} + +// SPS_TimingInfo is a timing info. +type SPS_TimingInfo struct { //nolint:revive + NumUnitsInTick uint32 + TimeScale uint32 + FixedFrameRateFlag bool +} + +func (t *SPS_TimingInfo) unmarshal(buf []byte, pos *int) error { + err := bits.HasSpace(buf, *pos, 32+32+1) + if err != nil { + return err + } + + t.NumUnitsInTick = uint32(bits.ReadBitsUnsafe(buf, pos, 32)) + t.TimeScale = uint32(bits.ReadBitsUnsafe(buf, pos, 32)) + t.FixedFrameRateFlag = bits.ReadFlagUnsafe(buf, pos) + + return nil +} + +// SPS_BitstreamRestriction are bitstream restriction infos. +type SPS_BitstreamRestriction struct { //nolint:revive + MotionVectorsOverPicBoundariesFlag bool + MaxBytesPerPicDenom uint32 + MaxBitsPerMbDenom uint32 + Log2MaxMvLengthHorizontal uint32 + Log2MaxMvLengthVertical uint32 + MaxNumReorderFrames uint32 + MaxDecFrameBuffering uint32 +} + +func (r *SPS_BitstreamRestriction) unmarshal(buf []byte, pos *int) error { + var err error + r.MotionVectorsOverPicBoundariesFlag, err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + + r.MaxBytesPerPicDenom, err = bits.ReadGolombUnsigned(buf, pos) + if err != nil { + return err + } + + r.MaxBitsPerMbDenom, err = bits.ReadGolombUnsigned(buf, pos) + if err != nil { + return err + } + + r.Log2MaxMvLengthHorizontal, err = bits.ReadGolombUnsigned(buf, pos) + if err != nil { + return err + } + + r.Log2MaxMvLengthVertical, err = bits.ReadGolombUnsigned(buf, pos) + if err != nil { + return err + } + + r.MaxNumReorderFrames, err = bits.ReadGolombUnsigned(buf, pos) + if err != nil { + return err + } + + r.MaxDecFrameBuffering, err = bits.ReadGolombUnsigned(buf, pos) + if err != nil { + return err + } + + return nil +} + +// SPS_VUI is a video usability information. +type SPS_VUI struct { //nolint:revive + AspectRatioInfoPresentFlag bool + + // AspectRatioInfoPresentFlag == true + AspectRatioIdc uint8 + SarWidth uint16 + SarHeight uint16 + + OverscanInfoPresentFlag bool + + // OverscanInfoPresentFlag == true + OverscanAppropriateFlag bool + VideoSignalTypePresentFlag bool + + // VideoSignalTypePresentFlag == true + VideoFormat uint8 + VideoFullRangeFlag bool + ColourDescriptionPresentFlag bool + + // ColourDescriptionPresentFlag == true + ColourPrimaries uint8 + TransferCharacteristics uint8 + MatrixCoefficients uint8 + + ChromaLocInfoPresentFlag bool + + // ChromaLocInfoPresentFlag == true + ChromaSampleLocTypeTopField uint32 + ChromaSampleLocTypeBottomField uint32 + + TimingInfo *SPS_TimingInfo + NalHRD *SPS_HRD + VclHRD *SPS_HRD + + LowDelayHrdFlag bool + PicStructPresentFlag bool + BitstreamRestriction *SPS_BitstreamRestriction +} + +func (v *SPS_VUI) unmarshal(buf []byte, pos *int) error { + var err error + v.AspectRatioInfoPresentFlag, err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + + if v.AspectRatioInfoPresentFlag { + var tmp uint64 + tmp, err = bits.ReadBits(buf, pos, 8) + if err != nil { + return err + } + v.AspectRatioIdc = uint8(tmp) + + if v.AspectRatioIdc == 255 { // Extended_SAR + err = bits.HasSpace(buf, *pos, 32) + if err != nil { + return err + } + + v.SarWidth = uint16(bits.ReadBitsUnsafe(buf, pos, 16)) + v.SarHeight = uint16(bits.ReadBitsUnsafe(buf, pos, 16)) + } + } + + v.OverscanInfoPresentFlag, err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + + if v.OverscanInfoPresentFlag { + v.OverscanAppropriateFlag, err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + } + + v.VideoSignalTypePresentFlag, err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + + if v.VideoSignalTypePresentFlag { + err = bits.HasSpace(buf, *pos, 5) + if err != nil { + return err + } + + v.VideoFormat = uint8(bits.ReadBitsUnsafe(buf, pos, 3)) + v.VideoFullRangeFlag = bits.ReadFlagUnsafe(buf, pos) + v.ColourDescriptionPresentFlag = bits.ReadFlagUnsafe(buf, pos) + + if v.ColourDescriptionPresentFlag { + err = bits.HasSpace(buf, *pos, 24) + if err != nil { + return err + } + + v.ColourPrimaries = uint8(bits.ReadBitsUnsafe(buf, pos, 8)) + v.TransferCharacteristics = uint8(bits.ReadBitsUnsafe(buf, pos, 8)) + v.MatrixCoefficients = uint8(bits.ReadBitsUnsafe(buf, pos, 8)) + } + } + + v.ChromaLocInfoPresentFlag, err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + + if v.ChromaLocInfoPresentFlag { + v.ChromaSampleLocTypeTopField, err = bits.ReadGolombUnsigned(buf, pos) + if err != nil { + return err + } + + v.ChromaSampleLocTypeBottomField, err = bits.ReadGolombUnsigned(buf, pos) + if err != nil { + return err + } + } + + timingInfoPresentFlag, err := bits.ReadFlag(buf, pos) + if err != nil { + return err + } + + if timingInfoPresentFlag { + v.TimingInfo = &SPS_TimingInfo{} + err = v.TimingInfo.unmarshal(buf, pos) + if err != nil { + return err + } + } + + nalHrdParametersPresentFlag, err := bits.ReadFlag(buf, pos) + if err != nil { + return err + } + + if nalHrdParametersPresentFlag { + v.NalHRD = &SPS_HRD{} + err = v.NalHRD.unmarshal(buf, pos) + if err != nil { + return err + } + } + + vclHrdParametersPresentFlag, err := bits.ReadFlag(buf, pos) + if err != nil { + return err + } + + if vclHrdParametersPresentFlag { + v.VclHRD = &SPS_HRD{} + err = v.VclHRD.unmarshal(buf, pos) + if err != nil { + return err + } + } + + if nalHrdParametersPresentFlag || vclHrdParametersPresentFlag { + v.LowDelayHrdFlag, err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + } + + v.PicStructPresentFlag, err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + + bitstreamRestrictionFlag, err := bits.ReadFlag(buf, pos) + if err != nil { + return err + } + + if bitstreamRestrictionFlag { + v.BitstreamRestriction = &SPS_BitstreamRestriction{} + err := v.BitstreamRestriction.unmarshal(buf, pos) + if err != nil { + return err + } + } + + return nil +} + +// SPS_FrameCropping is the frame cropping part of a SPS. +type SPS_FrameCropping struct { //nolint:revive + LeftOffset uint32 + RightOffset uint32 + TopOffset uint32 + BottomOffset uint32 +} + +func (c *SPS_FrameCropping) unmarshal(buf []byte, pos *int) error { + var err error + c.LeftOffset, err = bits.ReadGolombUnsigned(buf, pos) + if err != nil { + return err + } + + c.RightOffset, err = bits.ReadGolombUnsigned(buf, pos) + if err != nil { + return err + } + + c.TopOffset, err = bits.ReadGolombUnsigned(buf, pos) + if err != nil { + return err + } + + c.BottomOffset, err = bits.ReadGolombUnsigned(buf, pos) + if err != nil { + return err + } + + return nil +} + +// SPS is a H264 sequence parameter set. +// Specification: ITU-T Rec. H.264, 7.3.2.1.1 +type SPS struct { + ProfileIdc uint8 + ConstraintSet0Flag bool + ConstraintSet1Flag bool + ConstraintSet2Flag bool + ConstraintSet3Flag bool + ConstraintSet4Flag bool + ConstraintSet5Flag bool + LevelIdc uint8 + ID uint32 + + // only for selected ProfileIdcs + ChromaFormatIdc uint32 + SeparateColourPlaneFlag bool + BitDepthLumaMinus8 uint32 + BitDepthChromaMinus8 uint32 + QpprimeYZeroTransformBypassFlag bool + + // seqScalingListPresentFlag == true + ScalingList4x4 [][]int32 + UseDefaultScalingMatrix4x4Flag []bool + ScalingList8x8 [][]int32 + UseDefaultScalingMatrix8x8Flag []bool + + Log2MaxFrameNumMinus4 uint32 + PicOrderCntType uint32 + + // PicOrderCntType == 0 + Log2MaxPicOrderCntLsbMinus4 uint32 + + // PicOrderCntType == 1 + DeltaPicOrderAlwaysZeroFlag bool + OffsetForNonRefPic int32 + OffsetForTopToBottomField int32 + OffsetForRefFrames []int32 + + MaxNumRefFrames uint32 + GapsInFrameNumValueAllowedFlag bool + PicWidthInMbsMinus1 uint32 + PicHeightInMapUnitsMinus1 uint32 + FrameMbsOnlyFlag bool + + // FrameMbsOnlyFlag == false + MbAdaptiveFrameFieldFlag bool + + Direct8x8InferenceFlag bool + FrameCropping *SPS_FrameCropping + VUI *SPS_VUI +} + +// Unmarshal decodes a SPS from bytes. +func (s *SPS) Unmarshal(buf []byte) error { + if len(buf) < 1 { + return fmt.Errorf("not enough bits") + } + + if NALUType(buf[0]&0x1F) != NALUTypeSPS { + return fmt.Errorf("not a SPS") + } + + buf = EmulationPreventionRemove(buf[1:]) + + if len(buf) < 3 { + return fmt.Errorf("not enough bits") + } + + s.ProfileIdc = buf[0] + s.ConstraintSet0Flag = (buf[1] >> 7) == 1 + s.ConstraintSet1Flag = (buf[1] >> 6 & 0x01) == 1 + s.ConstraintSet2Flag = (buf[1] >> 5 & 0x01) == 1 + s.ConstraintSet3Flag = (buf[1] >> 4 & 0x01) == 1 + s.ConstraintSet4Flag = (buf[1] >> 3 & 0x01) == 1 + s.ConstraintSet5Flag = (buf[1] >> 2 & 0x01) == 1 + s.LevelIdc = buf[2] + + buf = buf[3:] + pos := 0 + + var err error + s.ID, err = bits.ReadGolombUnsigned(buf, &pos) + if err != nil { + return err + } + + switch s.ProfileIdc { + case 100, 110, 122, 244, 44, 83, 86, 118, 128, 138, 139, 134, 135: + s.ChromaFormatIdc, err = bits.ReadGolombUnsigned(buf, &pos) + if err != nil { + return err + } + + if s.ChromaFormatIdc == 3 { + s.SeparateColourPlaneFlag, err = bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + } else { + s.SeparateColourPlaneFlag = false + } + + s.BitDepthLumaMinus8, err = bits.ReadGolombUnsigned(buf, &pos) + if err != nil { + return err + } + + s.BitDepthChromaMinus8, err = bits.ReadGolombUnsigned(buf, &pos) + if err != nil { + return err + } + + s.QpprimeYZeroTransformBypassFlag, err = bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + + var seqScalingMatrixPresentFlag bool + seqScalingMatrixPresentFlag, err = bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + + if seqScalingMatrixPresentFlag { + var lim int + if s.ChromaFormatIdc != 3 { + lim = 8 + } else { + lim = 12 + } + + for i := 0; i < lim; i++ { + var seqScalingListPresentFlag bool + seqScalingListPresentFlag, err = bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + + if seqScalingListPresentFlag { + if i < 6 { + var scalingList []int32 + var useDefaultScalingMatrixFlag bool + scalingList, useDefaultScalingMatrixFlag, err = readScalingList(buf, &pos, 16) + if err != nil { + return err + } + + s.ScalingList4x4 = append(s.ScalingList4x4, scalingList) + s.UseDefaultScalingMatrix4x4Flag = append(s.UseDefaultScalingMatrix4x4Flag, + useDefaultScalingMatrixFlag) + } else { + var scalingList []int32 + var useDefaultScalingMatrixFlag bool + scalingList, useDefaultScalingMatrixFlag, err = readScalingList(buf, &pos, 64) + if err != nil { + return err + } + + s.ScalingList8x8 = append(s.ScalingList8x8, scalingList) + s.UseDefaultScalingMatrix8x8Flag = append(s.UseDefaultScalingMatrix8x8Flag, + useDefaultScalingMatrixFlag) + } + } + } + } + + default: + s.ChromaFormatIdc = 1 + s.SeparateColourPlaneFlag = false + s.BitDepthLumaMinus8 = 0 + s.BitDepthChromaMinus8 = 0 + s.QpprimeYZeroTransformBypassFlag = false + } + + s.Log2MaxFrameNumMinus4, err = bits.ReadGolombUnsigned(buf, &pos) + if err != nil { + return err + } + + s.PicOrderCntType, err = bits.ReadGolombUnsigned(buf, &pos) + if err != nil { + return err + } + + switch s.PicOrderCntType { + case 0: + s.Log2MaxPicOrderCntLsbMinus4, err = bits.ReadGolombUnsigned(buf, &pos) + if err != nil { + return err + } + + s.DeltaPicOrderAlwaysZeroFlag = false + s.OffsetForNonRefPic = 0 + s.OffsetForTopToBottomField = 0 + s.OffsetForRefFrames = nil + + case 1: + s.Log2MaxPicOrderCntLsbMinus4 = 0 + + s.DeltaPicOrderAlwaysZeroFlag, err = bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + + s.OffsetForNonRefPic, err = bits.ReadGolombSigned(buf, &pos) + if err != nil { + return err + } + + s.OffsetForTopToBottomField, err = bits.ReadGolombSigned(buf, &pos) + if err != nil { + return err + } + + var numRefFramesInPicOrderCntCycle uint32 + numRefFramesInPicOrderCntCycle, err = bits.ReadGolombUnsigned(buf, &pos) + if err != nil { + return err + } + + if numRefFramesInPicOrderCntCycle > maxRefFrames { + return fmt.Errorf("num_ref_frames_in_pic_order_cnt_cycle exceeds %d", maxRefFrames) + } + + s.OffsetForRefFrames = make([]int32, numRefFramesInPicOrderCntCycle) + for i := uint32(0); i < numRefFramesInPicOrderCntCycle; i++ { + var v int32 + v, err = bits.ReadGolombSigned(buf, &pos) + if err != nil { + return err + } + + s.OffsetForRefFrames[i] = v + } + + case 2: + s.Log2MaxPicOrderCntLsbMinus4 = 0 + s.DeltaPicOrderAlwaysZeroFlag = false + s.OffsetForNonRefPic = 0 + s.OffsetForTopToBottomField = 0 + s.OffsetForRefFrames = nil + + default: + return fmt.Errorf("invalid pic_order_cnt_type: %d", s.PicOrderCntType) + } + + s.MaxNumRefFrames, err = bits.ReadGolombUnsigned(buf, &pos) + if err != nil { + return err + } + + s.GapsInFrameNumValueAllowedFlag, err = bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + + s.PicWidthInMbsMinus1, err = bits.ReadGolombUnsigned(buf, &pos) + if err != nil { + return err + } + + s.PicHeightInMapUnitsMinus1, err = bits.ReadGolombUnsigned(buf, &pos) + if err != nil { + return err + } + + s.FrameMbsOnlyFlag, err = bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + + if !s.FrameMbsOnlyFlag { + s.MbAdaptiveFrameFieldFlag, err = bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + } else { + s.MbAdaptiveFrameFieldFlag = false + } + + s.Direct8x8InferenceFlag, err = bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + + frameCroppingFlag, err := bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + + if frameCroppingFlag { + s.FrameCropping = &SPS_FrameCropping{} + err = s.FrameCropping.unmarshal(buf, &pos) + if err != nil { + return err + } + } else { + s.FrameCropping = nil + } + + vuiParametersPresentFlag, err := bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + + if vuiParametersPresentFlag { + s.VUI = &SPS_VUI{} + err := s.VUI.unmarshal(buf, &pos) + if err != nil { + return err + } + } else { + s.VUI = nil + } + + return nil +} + +// Width returns the video width. +func (s SPS) Width() int { + var subWidthC uint32 + switch { + case s.ChromaFormatIdc == 1 && !s.SeparateColourPlaneFlag: + subWidthC = 2 + + case s.ChromaFormatIdc == 2 && !s.SeparateColourPlaneFlag: + subWidthC = 2 + + case s.ChromaFormatIdc == 3 && !s.SeparateColourPlaneFlag: + subWidthC = 1 + } + + var chromaArrayType uint32 + if !s.SeparateColourPlaneFlag { + chromaArrayType = s.ChromaFormatIdc + } else { + chromaArrayType = 0 + } + + var cropUnitX uint32 + if chromaArrayType == 0 { + cropUnitX = 0 + } else { + cropUnitX = subWidthC + } + + picWidthInSamplesL := ((s.PicWidthInMbsMinus1 + 1) * 16) + + if s.FrameCropping != nil { + return int(picWidthInSamplesL - cropUnitX*(s.FrameCropping.LeftOffset+s.FrameCropping.RightOffset)) + } + + return int(picWidthInSamplesL) +} + +// Height returns the video height. +func (s SPS) Height() int { + var subHeightC uint32 + switch { + case s.ChromaFormatIdc == 1 && !s.SeparateColourPlaneFlag: + subHeightC = 2 + + case s.ChromaFormatIdc == 2 && !s.SeparateColourPlaneFlag: + subHeightC = 1 + + case s.ChromaFormatIdc == 3 && !s.SeparateColourPlaneFlag: + subHeightC = 1 + } + + var frameMbsOnlyFlagUint32 uint32 + if s.FrameMbsOnlyFlag { + frameMbsOnlyFlagUint32 = 1 + } + + var chromaArrayType uint32 + if !s.SeparateColourPlaneFlag { + chromaArrayType = s.ChromaFormatIdc + } else { + chromaArrayType = 0 + } + + var cropUnitY uint32 + if chromaArrayType == 0 { + cropUnitY = 2 - frameMbsOnlyFlagUint32 + } else { + cropUnitY = subHeightC * (2 - frameMbsOnlyFlagUint32) + } + + frameHeightInMbs := (2 - frameMbsOnlyFlagUint32) * (s.PicHeightInMapUnitsMinus1 + 1) + + if s.FrameCropping != nil { + return int(16*frameHeightInMbs - cropUnitY*(s.FrameCropping.TopOffset+s.FrameCropping.BottomOffset)) + } + + picHeightInMbs := frameHeightInMbs // / (1 + s.FieldPicFlag) + picHeightInSamplesL := picHeightInMbs * 16 + + return int(picHeightInSamplesL) +} + +// FPS returns the frames per second of the video. +func (s SPS) FPS() float64 { + if s.VUI == nil || s.VUI.TimingInfo == nil { + return 0 + } + + return float64(s.VUI.TimingInfo.TimeScale) / (2 * float64(s.VUI.TimingInfo.NumUnitsInTick)) +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h265/dts_extractor.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h265/dts_extractor.go new file mode 100644 index 000000000..5865865ae --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h265/dts_extractor.go @@ -0,0 +1,245 @@ +package h265 + +import ( + "fmt" + "math" + + "github.com/bluenviron/mediacommon/v2/pkg/bits" + "github.com/bluenviron/mediacommon/v2/pkg/codecs/h264" +) + +const ( + maxBytesToGetPOC = 12 +) + +func getPTSDTSDiff(buf []byte, sps *SPS, pps *PPS) (uint32, error) { + typ := NALUType((buf[0] >> 1) & 0b111111) + + buf = buf[1:] + lb := len(buf) + + if lb > maxBytesToGetPOC { + lb = maxBytesToGetPOC + } + + buf = h264.EmulationPreventionRemove(buf[:lb]) + pos := 8 + + firstSliceSegmentInPicFlag, err := bits.ReadFlag(buf, &pos) + if err != nil { + return 0, err + } + + if !firstSliceSegmentInPicFlag { + return 0, fmt.Errorf("first_slice_segment_in_pic_flag = 0 is not supported") + } + + if typ >= NALUType_BLA_W_LP && typ <= NALUType_RSV_IRAP_VCL23 { + _, err = bits.ReadFlag(buf, &pos) // no_output_of_prior_pics_flag + if err != nil { + return 0, err + } + } + + _, err = bits.ReadGolombUnsigned(buf, &pos) // slice_pic_parameter_set_id + if err != nil { + return 0, err + } + + if pps.NumExtraSliceHeaderBits > 0 { + err = bits.HasSpace(buf, pos, int(pps.NumExtraSliceHeaderBits)) + if err != nil { + return 0, err + } + pos += int(pps.NumExtraSliceHeaderBits) + } + + sliceType, err := bits.ReadGolombUnsigned(buf, &pos) // slice_type + if err != nil { + return 0, err + } + + if pps.OutputFlagPresentFlag { + _, err = bits.ReadFlag(buf, &pos) // pic_output_flag + if err != nil { + return 0, err + } + } + + if sps.SeparateColourPlaneFlag { + _, err = bits.ReadBits(buf, &pos, 2) // colour_plane_id + if err != nil { + return 0, err + } + } + + _, err = bits.ReadBits(buf, &pos, int(sps.Log2MaxPicOrderCntLsbMinus4+4)) // pic_order_cnt_lsb + if err != nil { + return 0, err + } + + shortTermRefPicSetSpsFlag, err := bits.ReadFlag(buf, &pos) + if err != nil { + return 0, err + } + + var rps *SPS_ShortTermRefPicSet + + if !shortTermRefPicSetSpsFlag { + rps = &SPS_ShortTermRefPicSet{} + err = rps.unmarshal(buf, &pos, uint32(len(sps.ShortTermRefPicSets)), + uint32(len(sps.ShortTermRefPicSets)), sps.ShortTermRefPicSets) + if err != nil { + return 0, err + } + } else { + if len(sps.ShortTermRefPicSets) == 0 { + return 0, fmt.Errorf("invalid short_term_ref_pic_set_idx") + } + + b := int(math.Ceil(math.Log2(float64(len(sps.ShortTermRefPicSets))))) + tmp, err := bits.ReadBits(buf, &pos, b) + if err != nil { + return 0, err + } + shortTermRefPicSetIdx := int(tmp) + + if len(sps.ShortTermRefPicSets) <= shortTermRefPicSetIdx { + return 0, fmt.Errorf("invalid short_term_ref_pic_set_idx") + } + + rps = sps.ShortTermRefPicSets[shortTermRefPicSetIdx] + } + + var v uint32 + + if sliceType == 0 { // B-frame + if typ == NALUType_TRAIL_N || typ == NALUType_RASL_N { + v = sps.MaxNumReorderPics[0] - uint32(len(rps.DeltaPocS1)) + } else if typ == NALUType_TRAIL_R || typ == NALUType_RASL_R { + if len(rps.DeltaPocS0) == 0 { + return 0, fmt.Errorf("invalid DeltaPocS0") + } + v = uint32(-rps.DeltaPocS0[0]-1+int32(sps.MaxNumReorderPics[0])) - uint32(len(rps.DeltaPocS1)) + } + } else { // I or P-frame + if len(rps.DeltaPocS0) == 0 { + return 0, fmt.Errorf("invalid DeltaPocS0") + } + v = uint32(-rps.DeltaPocS0[0] - 1 + int32(sps.MaxNumReorderPics[0])) + } + + return v, nil +} + +// DTSExtractor computes DTS from PTS. +type DTSExtractor struct { + spsp *SPS + ppsp *PPS + prevDTSFilled bool + prevDTS int64 +} + +// Initialize initializes a DTSExtractor. +func (d *DTSExtractor) Initialize() { +} + +// NewDTSExtractor allocates a DTSExtractor. +// +// Deprecated: replaced by DTSExtractor.Initialize. +func NewDTSExtractor() *DTSExtractor { + return &DTSExtractor{} +} + +func (d *DTSExtractor) extractInner(au [][]byte, pts int64) (int64, error) { + var idr []byte + var nonIDR []byte + + for _, nalu := range au { + typ := NALUType((nalu[0] >> 1) & 0b111111) + switch typ { + case NALUType_SPS_NUT: + var spsp SPS + err := spsp.Unmarshal(nalu) + if err != nil { + return 0, fmt.Errorf("invalid SPS: %w", err) + } + d.spsp = &spsp + + case NALUType_PPS_NUT: + var ppsp PPS + err := ppsp.Unmarshal(nalu) + if err != nil { + return 0, fmt.Errorf("invalid PPS: %w", err) + } + d.ppsp = &ppsp + + case NALUType_IDR_W_RADL, NALUType_IDR_N_LP: + idr = nalu + + case NALUType_TRAIL_N, NALUType_TRAIL_R, NALUType_CRA_NUT, NALUType_RASL_N, NALUType_RASL_R: + nonIDR = nalu + } + } + + if d.spsp == nil { + return 0, fmt.Errorf("SPS not received yet") + } + + if d.ppsp == nil { + return 0, fmt.Errorf("PPS not received yet") + } + + if len(d.spsp.MaxNumReorderPics) != 1 || d.spsp.MaxNumReorderPics[0] == 0 { + return pts, nil + } + + if d.spsp.VUI == nil || d.spsp.VUI.TimingInfo == nil { + return pts, nil + } + + var samplesDiff uint32 + + switch { + case idr != nil: + samplesDiff = d.spsp.MaxNumReorderPics[0] + + case nonIDR != nil: + var err error + samplesDiff, err = getPTSDTSDiff(nonIDR, d.spsp, d.ppsp) + if err != nil { + return 0, err + } + + default: + return 0, fmt.Errorf("access unit doesn't contain an IDR or non-IDR NALU") + } + + timeDiff := int64(samplesDiff) * 90000 * + int64(d.spsp.VUI.TimingInfo.NumUnitsInTick) / int64(d.spsp.VUI.TimingInfo.TimeScale) + dts := pts - timeDiff + + return dts, nil +} + +// Extract extracts the DTS of a access unit. +func (d *DTSExtractor) Extract(au [][]byte, pts int64) (int64, error) { + dts, err := d.extractInner(au, pts) + if err != nil { + return 0, err + } + + if dts > pts { + return 0, fmt.Errorf("DTS is greater than PTS") + } + + if d.prevDTSFilled && dts < d.prevDTS { + return 0, fmt.Errorf("DTS is not monotonically increasing, was %v, now is %v", + d.prevDTS, dts) + } + + d.prevDTSFilled = true + d.prevDTS = dts + + return dts, err +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h265/h265.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h265/h265.go new file mode 100644 index 000000000..056ea84f3 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h265/h265.go @@ -0,0 +1,11 @@ +// Package h265 contains utilities to work with the H265 codec. +package h265 + +const ( + // MaxAccessUnitSize is the maximum size of an access unit. + // With a 50 Mbps 2160p60 H265 video, the maximum size does not seem to exceed 8 MiB. + MaxAccessUnitSize = 8 * 1024 * 1024 + + // MaxNALUsPerAccessUnit is the maximum number of NALUs per access unit. + MaxNALUsPerAccessUnit = 21 +) diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h265/is_random_access.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h265/is_random_access.go new file mode 100644 index 000000000..837a0aa36 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h265/is_random_access.go @@ -0,0 +1,13 @@ +package h265 + +// IsRandomAccess checks whether the access unit can be randomly accessed. +func IsRandomAccess(au [][]byte) bool { + for _, nalu := range au { + typ := NALUType((nalu[0] >> 1) & 0b111111) + switch typ { + case NALUType_IDR_W_RADL, NALUType_IDR_N_LP, NALUType_CRA_NUT: + return true + } + } + return false +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h265/nalu_type.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h265/nalu_type.go new file mode 100644 index 000000000..384fba88f --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h265/nalu_type.go @@ -0,0 +1,100 @@ +package h265 + +import ( + "fmt" +) + +// NALUType is the type of a NALU. +// Specification: ITU-T Rec. H.265, Table 7-1 +type NALUType uint8 + +// NALU types. +const ( + NALUType_TRAIL_N NALUType = 0 //nolint:revive + NALUType_TRAIL_R NALUType = 1 //nolint:revive + NALUType_TSA_N NALUType = 2 //nolint:revive + NALUType_TSA_R NALUType = 3 //nolint:revive + NALUType_STSA_N NALUType = 4 //nolint:revive + NALUType_STSA_R NALUType = 5 //nolint:revive + NALUType_RADL_N NALUType = 6 //nolint:revive + NALUType_RADL_R NALUType = 7 //nolint:revive + NALUType_RASL_N NALUType = 8 //nolint:revive + NALUType_RASL_R NALUType = 9 //nolint:revive + NALUType_RSV_VCL_N10 NALUType = 10 //nolint:revive + NALUType_RSV_VCL_N12 NALUType = 12 //nolint:revive + NALUType_RSV_VCL_N14 NALUType = 14 //nolint:revive + NALUType_RSV_VCL_R11 NALUType = 11 //nolint:revive + NALUType_RSV_VCL_R13 NALUType = 13 //nolint:revive + NALUType_RSV_VCL_R15 NALUType = 15 //nolint:revive + NALUType_BLA_W_LP NALUType = 16 //nolint:revive + NALUType_BLA_W_RADL NALUType = 17 //nolint:revive + NALUType_BLA_N_LP NALUType = 18 //nolint:revive + NALUType_IDR_W_RADL NALUType = 19 //nolint:revive + NALUType_IDR_N_LP NALUType = 20 //nolint:revive + NALUType_CRA_NUT NALUType = 21 //nolint:revive + NALUType_RSV_IRAP_VCL22 NALUType = 22 //nolint:revive + NALUType_RSV_IRAP_VCL23 NALUType = 23 //nolint:revive + NALUType_VPS_NUT NALUType = 32 //nolint:revive + NALUType_SPS_NUT NALUType = 33 //nolint:revive + NALUType_PPS_NUT NALUType = 34 //nolint:revive + NALUType_AUD_NUT NALUType = 35 //nolint:revive + NALUType_EOS_NUT NALUType = 36 //nolint:revive + NALUType_EOB_NUT NALUType = 37 //nolint:revive + NALUType_FD_NUT NALUType = 38 //nolint:revive + NALUType_PREFIX_SEI_NUT NALUType = 39 //nolint:revive + NALUType_SUFFIX_SEI_NUT NALUType = 40 //nolint:revive + + // additional NALU types for RTP/H265 + NALUType_AggregationUnit NALUType = 48 //nolint:revive + NALUType_FragmentationUnit NALUType = 49 //nolint:revive + NALUType_PACI NALUType = 50 //nolint:revive +) + +var naluTypeLabels = map[NALUType]string{ + NALUType_TRAIL_N: "TRAIL_N", + NALUType_TRAIL_R: "TRAIL_R", + NALUType_TSA_N: "TSA_N", + NALUType_TSA_R: "TSA_R", + NALUType_STSA_N: "STSA_N", + NALUType_STSA_R: "STSA_R:", + NALUType_RADL_N: "RADL_N", + NALUType_RADL_R: "RADL_R", + NALUType_RASL_N: "RASL_N", + NALUType_RASL_R: "RASL_R", + NALUType_RSV_VCL_N10: "RSV_VCL_N10", + NALUType_RSV_VCL_N12: "RSV_VCL_N12", + NALUType_RSV_VCL_N14: "RSV_VCL_N14", + NALUType_RSV_VCL_R11: "RSV_VCL_R11", + NALUType_RSV_VCL_R13: "RSV_VCL_R13", + NALUType_RSV_VCL_R15: "RSV_VCL_R15", + NALUType_BLA_W_LP: "BLA_W_LP", + NALUType_BLA_W_RADL: "BLA_W_RADL", + NALUType_BLA_N_LP: "BLA_N_LP", + NALUType_IDR_W_RADL: "IDR_W_RADL", + NALUType_IDR_N_LP: "IDR_N_LP", + NALUType_CRA_NUT: "CRA_NUT", + NALUType_RSV_IRAP_VCL22: "RSV_IRAP_VCL22", + NALUType_RSV_IRAP_VCL23: "RSV_IRAP_VCL23", + NALUType_VPS_NUT: "VPS_NUT", + NALUType_SPS_NUT: "SPS_NUT", + NALUType_PPS_NUT: "PPS_NUT", + NALUType_AUD_NUT: "AUD_NUT", + NALUType_EOS_NUT: "EOS_NUT", + NALUType_EOB_NUT: "EOB_NUT", + NALUType_FD_NUT: "FD_NUT", + NALUType_PREFIX_SEI_NUT: "PrefixSEINUT", + NALUType_SUFFIX_SEI_NUT: "SuffixSEINUT", + + // additional NALU types for RTP/H265 + NALUType_AggregationUnit: "AggregationUnit", + NALUType_FragmentationUnit: "FragmentationUnit", + NALUType_PACI: "PACI", +} + +// String implements fmt.Stringer. +func (nt NALUType) String() string { + if l, ok := naluTypeLabels[nt]; ok { + return l + } + return fmt.Sprintf("unknown (%d)", nt) +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h265/pps.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h265/pps.go new file mode 100644 index 000000000..1fe3609b1 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h265/pps.go @@ -0,0 +1,54 @@ +package h265 + +import ( + "fmt" + + "github.com/bluenviron/mediacommon/v2/pkg/bits" + "github.com/bluenviron/mediacommon/v2/pkg/codecs/h264" +) + +// PPS is a H265 picture parameter set. +// Specification: ITU-T Rec. H.265, 7.3.2.3.1 +type PPS struct { + ID uint32 + SPSID uint32 + DependentSliceSegmentsEnabledFlag bool + OutputFlagPresentFlag bool + NumExtraSliceHeaderBits uint8 +} + +// Unmarshal decodes a PPS. +func (p *PPS) Unmarshal(buf []byte) error { + if len(buf) < 2 { + return fmt.Errorf("not enough bits") + } + + if NALUType((buf[0]>>1)&0b111111) != NALUType_PPS_NUT { + return fmt.Errorf("not a PPS") + } + + buf = h264.EmulationPreventionRemove(buf[1:]) + pos := 8 + + var err error + p.ID, err = bits.ReadGolombUnsigned(buf, &pos) + if err != nil { + return err + } + + p.SPSID, err = bits.ReadGolombUnsigned(buf, &pos) + if err != nil { + return err + } + + err = bits.HasSpace(buf, pos, 5) + if err != nil { + return err + } + + p.DependentSliceSegmentsEnabledFlag = bits.ReadFlagUnsafe(buf, &pos) + p.OutputFlagPresentFlag = bits.ReadFlagUnsafe(buf, &pos) + p.NumExtraSliceHeaderBits = uint8(bits.ReadBitsUnsafe(buf, &pos, 3)) + + return nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h265/sps.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h265/sps.go new file mode 100644 index 000000000..82f695b15 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/h265/sps.go @@ -0,0 +1,979 @@ +package h265 + +import ( + "fmt" + + "github.com/bluenviron/mediacommon/v2/pkg/bits" + "github.com/bluenviron/mediacommon/v2/pkg/codecs/h264" +) + +const ( + maxNegativePics = 255 + maxPositivePics = 255 + maxShortTermRefPics = 64 +) + +var subWidthC = []uint32{ + 1, + 2, + 2, + 1, +} + +var subHeightC = []uint32{ + 1, + 2, + 1, + 1, +} + +// SPS_ScalingListData is a scaling list data. +type SPS_ScalingListData struct { //nolint:revive + ScalingListPredModeFlag [4][6]bool + ScalingListPredmatrixIDDelta [4][6]uint32 + ScalingListDcCoefMinus8 [4][6]int32 +} + +func (d *SPS_ScalingListData) unmarshal(buf []byte, pos *int) error { + for sizeID := 0; sizeID < 4; sizeID++ { + var matrixIDIncr int + if sizeID == 3 { + matrixIDIncr = 3 + } else { + matrixIDIncr = 1 + } + + for matrixID := 0; matrixID < 6; matrixID += matrixIDIncr { + var err error + d.ScalingListPredModeFlag[sizeID][matrixID], err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + + if !d.ScalingListPredModeFlag[sizeID][matrixID] { + d.ScalingListPredmatrixIDDelta[sizeID][matrixID], err = bits.ReadGolombUnsigned(buf, pos) + if err != nil { + return err + } + } else { + coefNum := min(64, 1<<(4+(sizeID<<1))) + + if sizeID > 1 { + d.ScalingListDcCoefMinus8[sizeID-2][matrixID], err = bits.ReadGolombSigned(buf, pos) + if err != nil { + return err + } + } + + for i := 0; i < coefNum; i++ { + _, err = bits.ReadGolombSigned(buf, pos) // scalingListDeltaCoef + if err != nil { + return err + } + } + } + } + } + + return nil +} + +// SPS_Window is a window. +type SPS_Window struct { //nolint:revive + LeftOffset uint32 + RightOffset uint32 + TopOffset uint32 + BottomOffset uint32 +} + +func (w *SPS_Window) unmarshal(buf []byte, pos *int) error { + var err error + w.LeftOffset, err = bits.ReadGolombUnsigned(buf, pos) + if err != nil { + return err + } + + w.RightOffset, err = bits.ReadGolombUnsigned(buf, pos) + if err != nil { + return err + } + + w.TopOffset, err = bits.ReadGolombUnsigned(buf, pos) + if err != nil { + return err + } + + w.BottomOffset, err = bits.ReadGolombUnsigned(buf, pos) + if err != nil { + return err + } + + return nil +} + +// SPS_TimingInfo is a timing info. +type SPS_TimingInfo struct { //nolint:revive + NumUnitsInTick uint32 + TimeScale uint32 + POCProportionalToTimingFlag bool + + // POCProportionalToTimingFlag == true + NumTicksPOCDiffOneMinus1 uint32 +} + +func (t *SPS_TimingInfo) unmarshal(buf []byte, pos *int) error { + err := bits.HasSpace(buf, *pos, 32+32+1) + if err != nil { + return err + } + + t.NumUnitsInTick = uint32(bits.ReadBitsUnsafe(buf, pos, 32)) + t.TimeScale = uint32(bits.ReadBitsUnsafe(buf, pos, 32)) + t.POCProportionalToTimingFlag = bits.ReadFlagUnsafe(buf, pos) + + if t.POCProportionalToTimingFlag { + t.NumTicksPOCDiffOneMinus1, err = bits.ReadGolombUnsigned(buf, pos) + if err != nil { + return err + } + } + + return nil +} + +// SPS_VUI is a video usability information. +type SPS_VUI struct { //nolint:revive + AspectRatioInfoPresentFlag bool + + // AspectRatioInfoPresentFlag == true + AspectRatioIdc uint8 + SarWidth uint16 + SarHeight uint16 + + OverscanInfoPresentFlag bool + + // OverscanInfoPresentFlag == true + OverscanAppropriateFlag bool + VideoSignalTypePresentFlag bool + + // VideoSignalTypePresentFlag == true + VideoFormat uint8 + VideoFullRangeFlag bool + ColourDescriptionPresentFlag bool + + // ColourDescriptionPresentFlag == true + ColourPrimaries uint8 + TransferCharacteristics uint8 + MatrixCoefficients uint8 + + ChromaLocInfoPresentFlag bool + + // ChromaLocInfoPresentFlag == true + ChromaSampleLocTypeTopField uint32 + ChromaSampleLocTypeBottomField uint32 + + NeutralChromaIndicationFlag bool + FieldSeqFlag bool + FrameFieldInfoPresentFlag bool + DefaultDisplayWindow *SPS_Window + TimingInfo *SPS_TimingInfo +} + +func (v *SPS_VUI) unmarshal(buf []byte, pos *int) error { + var err error + v.AspectRatioInfoPresentFlag, err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + + if v.AspectRatioInfoPresentFlag { + var tmp uint64 + tmp, err = bits.ReadBits(buf, pos, 8) + if err != nil { + return err + } + v.AspectRatioIdc = uint8(tmp) + + if v.AspectRatioIdc == 255 { // EXTENDED_SAR + err = bits.HasSpace(buf, *pos, 32) + if err != nil { + return err + } + + v.SarWidth = uint16(bits.ReadBitsUnsafe(buf, pos, 16)) + v.SarHeight = uint16(bits.ReadBitsUnsafe(buf, pos, 16)) + } + } + + v.OverscanInfoPresentFlag, err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + + if v.OverscanInfoPresentFlag { + v.OverscanAppropriateFlag, err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + } + + v.VideoSignalTypePresentFlag, err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + + if v.VideoSignalTypePresentFlag { + err = bits.HasSpace(buf, *pos, 5) + if err != nil { + return err + } + + v.VideoFormat = uint8(bits.ReadBitsUnsafe(buf, pos, 3)) + v.VideoFullRangeFlag = bits.ReadFlagUnsafe(buf, pos) + v.ColourDescriptionPresentFlag = bits.ReadFlagUnsafe(buf, pos) + + if v.ColourDescriptionPresentFlag { + err = bits.HasSpace(buf, *pos, 24) + if err != nil { + return err + } + + v.ColourPrimaries = uint8(bits.ReadBitsUnsafe(buf, pos, 8)) + v.TransferCharacteristics = uint8(bits.ReadBitsUnsafe(buf, pos, 8)) + v.MatrixCoefficients = uint8(bits.ReadBitsUnsafe(buf, pos, 8)) + } + } + + v.ChromaLocInfoPresentFlag, err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + + if v.ChromaLocInfoPresentFlag { + v.ChromaSampleLocTypeTopField, err = bits.ReadGolombUnsigned(buf, pos) + if err != nil { + return err + } + + v.ChromaSampleLocTypeBottomField, err = bits.ReadGolombUnsigned(buf, pos) + if err != nil { + return err + } + } + + v.NeutralChromaIndicationFlag, err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + + v.FieldSeqFlag, err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + + v.FrameFieldInfoPresentFlag, err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + + defaultDisplayWindowFlag, err := bits.ReadFlag(buf, pos) + if err != nil { + return err + } + + if defaultDisplayWindowFlag { + v.DefaultDisplayWindow = &SPS_Window{} + err = v.DefaultDisplayWindow.unmarshal(buf, pos) + if err != nil { + return err + } + } else { + v.DefaultDisplayWindow = nil + } + + timingInfoPresentFlag, err := bits.ReadFlag(buf, pos) + if err != nil { + return err + } + + if timingInfoPresentFlag { + v.TimingInfo = &SPS_TimingInfo{} + err := v.TimingInfo.unmarshal(buf, pos) + if err != nil { + return err + } + } else { + v.TimingInfo = nil + } + + return nil +} + +// SPS_ProfileTierLevel is a profile level tier of a SPS. +type SPS_ProfileTierLevel struct { //nolint:revive + GeneralProfileSpace uint8 + GeneralTierFlag uint8 + GeneralProfileIdc uint8 + GeneralProfileCompatibilityFlag [32]bool + GeneralProgressiveSourceFlag bool + GeneralInterlacedSourceFlag bool + GeneralNonPackedConstraintFlag bool + GeneralFrameOnlyConstraintFlag bool + GeneralMax12bitConstraintFlag bool + GeneralMax10bitConstraintFlag bool + GeneralMax8bitConstraintFlag bool + GeneralMax422ChromeConstraintFlag bool + GeneralMax420ChromaConstraintFlag bool + GeneralMaxMonochromeConstraintFlag bool + GeneralIntraConstraintFlag bool + GeneralOnePictureOnlyConstraintFlag bool + GeneralLowerBitRateConstraintFlag bool + GeneralMax14BitConstraintFlag bool + GeneralLevelIdc uint8 + SubLayerProfilePresentFlag []bool + SubLayerLevelPresentFlag []bool +} + +func (p *SPS_ProfileTierLevel) unmarshal(buf []byte, pos *int, maxSubLayersMinus1 uint8) error { + err := bits.HasSpace(buf, *pos, 8+32+12+34+8) + if err != nil { + return err + } + + p.GeneralProfileSpace = uint8(bits.ReadBitsUnsafe(buf, pos, 2)) + p.GeneralTierFlag = uint8(bits.ReadBitsUnsafe(buf, pos, 1)) + p.GeneralProfileIdc = uint8(bits.ReadBitsUnsafe(buf, pos, 5)) + + for j := 0; j < 32; j++ { + p.GeneralProfileCompatibilityFlag[j] = bits.ReadFlagUnsafe(buf, pos) + } + + p.GeneralProgressiveSourceFlag = bits.ReadFlagUnsafe(buf, pos) + p.GeneralInterlacedSourceFlag = bits.ReadFlagUnsafe(buf, pos) + p.GeneralNonPackedConstraintFlag = bits.ReadFlagUnsafe(buf, pos) + p.GeneralFrameOnlyConstraintFlag = bits.ReadFlagUnsafe(buf, pos) + p.GeneralMax12bitConstraintFlag = bits.ReadFlagUnsafe(buf, pos) + p.GeneralMax10bitConstraintFlag = bits.ReadFlagUnsafe(buf, pos) + p.GeneralMax8bitConstraintFlag = bits.ReadFlagUnsafe(buf, pos) + p.GeneralMax422ChromeConstraintFlag = bits.ReadFlagUnsafe(buf, pos) + p.GeneralMax420ChromaConstraintFlag = bits.ReadFlagUnsafe(buf, pos) + p.GeneralMaxMonochromeConstraintFlag = bits.ReadFlagUnsafe(buf, pos) + p.GeneralIntraConstraintFlag = bits.ReadFlagUnsafe(buf, pos) + p.GeneralOnePictureOnlyConstraintFlag = bits.ReadFlagUnsafe(buf, pos) + p.GeneralLowerBitRateConstraintFlag = bits.ReadFlagUnsafe(buf, pos) + + if p.GeneralProfileIdc == 5 || + p.GeneralProfileIdc == 9 || + p.GeneralProfileIdc == 10 || + p.GeneralProfileIdc == 11 || + p.GeneralProfileCompatibilityFlag[5] || + p.GeneralProfileCompatibilityFlag[9] || + p.GeneralProfileCompatibilityFlag[10] || + p.GeneralProfileCompatibilityFlag[11] { + p.GeneralMax14BitConstraintFlag = bits.ReadFlagUnsafe(buf, pos) + *pos += 34 + } else { + *pos += 35 + } + + p.GeneralLevelIdc = uint8(bits.ReadBitsUnsafe(buf, pos, 8)) + + if maxSubLayersMinus1 > 0 { + p.SubLayerProfilePresentFlag = make([]bool, maxSubLayersMinus1) + p.SubLayerLevelPresentFlag = make([]bool, maxSubLayersMinus1) + + err := bits.HasSpace(buf, *pos, int(2*maxSubLayersMinus1)) + if err != nil { + return err + } + + for j := uint8(0); j < maxSubLayersMinus1; j++ { + p.SubLayerProfilePresentFlag[j] = bits.ReadFlagUnsafe(buf, pos) + p.SubLayerLevelPresentFlag[j] = bits.ReadFlagUnsafe(buf, pos) + } + } + + if maxSubLayersMinus1 > 0 { + err := bits.HasSpace(buf, *pos, int(8-maxSubLayersMinus1)*2) + if err != nil { + return err + } + + *pos += int(8-maxSubLayersMinus1) * 2 + } + + for i := uint8(0); i < maxSubLayersMinus1; i++ { + if p.SubLayerProfilePresentFlag[i] { + return fmt.Errorf("SubLayerProfilePresentFlag not supported yet") + } + + if p.SubLayerLevelPresentFlag[i] { + return fmt.Errorf("SubLayerLevelPresentFlag not supported yet") + } + } + + return nil +} + +// SPS_ShortTermRefPicSet is a short-term reference picture set. +type SPS_ShortTermRefPicSet struct { //nolint:revive + InterRefPicSetPredictionFlag bool + DeltaIdxMinus1 uint32 + DeltaRpsSign bool + AbsDeltaRpsMinus1 uint32 + NumNegativePics uint32 + NumPositivePics uint32 + DeltaPocS0 []int32 + UsedByCurrPicS0Flag []bool + DeltaPocS1 []int32 + UsedByCurrPicS1Flag []bool +} + +func (r *SPS_ShortTermRefPicSet) unmarshal(buf []byte, pos *int, stRpsIdx uint32, + numShortTermRefPicSets uint32, shortTermRefPicSets []*SPS_ShortTermRefPicSet, +) error { + var err error + + if stRpsIdx != 0 { + r.InterRefPicSetPredictionFlag, err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + } + + if r.InterRefPicSetPredictionFlag { + if stRpsIdx == numShortTermRefPicSets { + r.DeltaIdxMinus1, err = bits.ReadGolombUnsigned(buf, pos) + if err != nil { + return err + } + } + + r.DeltaRpsSign, err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + + r.AbsDeltaRpsMinus1, err = bits.ReadGolombUnsigned(buf, pos) + if err != nil { + return err + } + + var s int32 + if r.DeltaRpsSign { + s = 1 + } + deltaRps := (1 - 2*s) * (int32(r.AbsDeltaRpsMinus1) + 1) + + refRpsIdx := stRpsIdx - (r.DeltaIdxMinus1 + 1) + if refRpsIdx >= uint32(len(shortTermRefPicSets)) { + return fmt.Errorf("invalid refRpsIdx") + } + + refRPS := shortTermRefPicSets[refRpsIdx] + numDeltaPocs := refRPS.NumNegativePics + refRPS.NumPositivePics + usedByCurrPicFlag := make([]bool, numDeltaPocs+1) + + useDeltaFlag := make([]bool, numDeltaPocs+1) + for i := range useDeltaFlag { + useDeltaFlag[i] = true + } + + for j := uint32(0); j <= numDeltaPocs; j++ { + usedByCurrPicFlag[j], err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + + if !usedByCurrPicFlag[j] { + useDeltaFlag[j], err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + } + } + + i := uint32(0) + + for j := (int32(refRPS.NumPositivePics) - 1); j >= 0; j-- { + dPoc := refRPS.DeltaPocS1[j] + deltaRps + if dPoc < 0 && useDeltaFlag[refRPS.NumNegativePics+uint32(j)] { + r.DeltaPocS0 = append(r.DeltaPocS0, dPoc) + r.UsedByCurrPicS0Flag = append(r.UsedByCurrPicS0Flag, usedByCurrPicFlag[refRPS.NumNegativePics+uint32(j)]) + i++ + } + } + + if deltaRps < 0 && useDeltaFlag[numDeltaPocs] { + r.DeltaPocS0 = append(r.DeltaPocS0, deltaRps) + r.UsedByCurrPicS0Flag = append(r.UsedByCurrPicS0Flag, usedByCurrPicFlag[numDeltaPocs]) + i++ + } + + for j := uint32(0); j < refRPS.NumNegativePics; j++ { + dPoc := refRPS.DeltaPocS0[j] + deltaRps + if dPoc < 0 && useDeltaFlag[j] { + r.DeltaPocS0 = append(r.DeltaPocS0, dPoc) + r.UsedByCurrPicS0Flag = append(r.UsedByCurrPicS0Flag, usedByCurrPicFlag[j]) + i++ + } + } + + r.NumNegativePics = i + + i = uint32(0) + + for j := (int32(refRPS.NumNegativePics) - 1); j >= 0; j-- { + dPoc := refRPS.DeltaPocS0[j] + deltaRps + if dPoc > 0 && useDeltaFlag[j] { + r.DeltaPocS1 = append(r.DeltaPocS1, dPoc) + r.UsedByCurrPicS1Flag = append(r.UsedByCurrPicS1Flag, usedByCurrPicFlag[j]) + i++ + } + } + + if deltaRps > 0 && useDeltaFlag[numDeltaPocs] { + r.DeltaPocS1 = append(r.DeltaPocS1, deltaRps) + r.UsedByCurrPicS1Flag = append(r.UsedByCurrPicS1Flag, usedByCurrPicFlag[numDeltaPocs]) + i++ + } + + for j := uint32(0); j < refRPS.NumPositivePics; j++ { + dPoc := refRPS.DeltaPocS1[j] + deltaRps + if dPoc > 0 && useDeltaFlag[refRPS.NumNegativePics+j] { + r.DeltaPocS1 = append(r.DeltaPocS1, dPoc) + r.UsedByCurrPicS1Flag = append(r.UsedByCurrPicS1Flag, usedByCurrPicFlag[refRPS.NumNegativePics+j]) + i++ + } + } + + r.NumPositivePics = i + } else { + r.NumNegativePics, err = bits.ReadGolombUnsigned(buf, pos) + if err != nil { + return err + } + + r.NumPositivePics, err = bits.ReadGolombUnsigned(buf, pos) + if err != nil { + return err + } + + if r.NumNegativePics > 0 { + if r.NumNegativePics > maxNegativePics { + return fmt.Errorf("num_negative_pics exceeds %d", maxNegativePics) + } + + r.DeltaPocS0 = make([]int32, r.NumNegativePics) + r.UsedByCurrPicS0Flag = make([]bool, r.NumNegativePics) + + for i := uint32(0); i < r.NumNegativePics; i++ { + deltaPocS0Minus1, err := bits.ReadGolombUnsigned(buf, pos) + if err != nil { + return err + } + + if i == 0 { + r.DeltaPocS0[i] = -int32(deltaPocS0Minus1 + 1) + } else { + r.DeltaPocS0[i] = r.DeltaPocS0[i-1] - (int32(deltaPocS0Minus1) + 1) + } + + r.UsedByCurrPicS0Flag[i], err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + } + } + + if r.NumPositivePics > 0 { + if r.NumPositivePics > maxPositivePics { + return fmt.Errorf("num_positive_pics exceeds %d", maxPositivePics) + } + + r.DeltaPocS1 = make([]int32, r.NumPositivePics) + r.UsedByCurrPicS1Flag = make([]bool, r.NumPositivePics) + + for i := uint32(0); i < r.NumPositivePics; i++ { + deltaPocS1Minus1, err := bits.ReadGolombUnsigned(buf, pos) + if err != nil { + return err + } + + if i == 0 { + r.DeltaPocS1[i] = int32(deltaPocS1Minus1) + 1 + } else { + r.DeltaPocS1[i] = r.DeltaPocS1[i-1] + int32(deltaPocS1Minus1) + 1 + } + + r.UsedByCurrPicS1Flag[i], err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + } + } + } + + return nil +} + +// SPS is a H265 sequence parameter set. +// Specification: ITU-T Rec. H.265, 7.3.2.2.1 +type SPS struct { + VPSID uint8 + MaxSubLayersMinus1 uint8 + TemporalIDNestingFlag bool + ProfileTierLevel SPS_ProfileTierLevel + ID uint8 + ChromaFormatIdc uint32 + SeparateColourPlaneFlag bool + PicWidthInLumaSamples uint32 + PicHeightInLumaSamples uint32 + ConformanceWindow *SPS_Window + BitDepthLumaMinus8 uint32 + BitDepthChromaMinus8 uint32 + Log2MaxPicOrderCntLsbMinus4 uint32 + SubLayerOrderingInfoPresentFlag bool + MaxDecPicBufferingMinus1 []uint32 + MaxNumReorderPics []uint32 + MaxLatencyIncreasePlus1 []uint32 + Log2MinLumaCodingBlockSizeMinus3 uint32 + Log2DiffMaxMinLumaCodingBlockSize uint32 + Log2MinLumaTransformBlockSizeMinus2 uint32 + Log2DiffMaxMinLumaTransformBlockSize uint32 + MaxTransformHierarchyDepthInter uint32 + MaxTransformHierarchyDepthIntra uint32 + ScalingListEnabledFlag bool + ScalingListData *SPS_ScalingListData + AmpEnabledFlag bool + SampleAdaptiveOffsetEnabledFlag bool + PcmEnabledFlag bool + + // PcmEnabledFlag == true + PcmSampleBitDepthLumaMinus1 uint8 + PcmSampleBitDepthChromaMinus1 uint8 + Log2MinPcmLumaCodingBlockSizeMinus3 uint32 + Log2DiffMaxMinPcmLumaCodingBlockSize uint32 + PcmLoopFilterDisabledFlag bool + + ShortTermRefPicSets []*SPS_ShortTermRefPicSet + LongTermRefPicsPresentFlag bool + TemporalMvpEnabledFlag bool + StrongIntraSmoothingEnabledFlag bool + VUI *SPS_VUI +} + +// Unmarshal decodes a SPS from bytes. +func (s *SPS) Unmarshal(buf []byte) error { + if len(buf) < 2 { + return fmt.Errorf("not enough bits") + } + + if NALUType((buf[0]>>1)&0b111111) != NALUType_SPS_NUT { + return fmt.Errorf("not a SPS") + } + + buf = h264.EmulationPreventionRemove(buf[1:]) + pos := 8 + + err := bits.HasSpace(buf, pos, 8) + if err != nil { + return err + } + + s.VPSID = uint8(bits.ReadBitsUnsafe(buf, &pos, 4)) + s.MaxSubLayersMinus1 = uint8(bits.ReadBitsUnsafe(buf, &pos, 3)) + s.TemporalIDNestingFlag = bits.ReadFlagUnsafe(buf, &pos) + + err = s.ProfileTierLevel.unmarshal(buf, &pos, s.MaxSubLayersMinus1) + if err != nil { + return err + } + + tmp2, err := bits.ReadGolombUnsigned(buf, &pos) + if err != nil { + return err + } + s.ID = uint8(tmp2) + + s.ChromaFormatIdc, err = bits.ReadGolombUnsigned(buf, &pos) + if err != nil { + return err + } + + // this prevents a panic in Marshal() + if s.ChromaFormatIdc > 3 { + return fmt.Errorf("invalid chroma_format_idc") + } + + if s.ChromaFormatIdc == 3 { + s.SeparateColourPlaneFlag, err = bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + } + + s.PicWidthInLumaSamples, err = bits.ReadGolombUnsigned(buf, &pos) + if err != nil { + return err + } + + s.PicHeightInLumaSamples, err = bits.ReadGolombUnsigned(buf, &pos) + if err != nil { + return err + } + + conformanceWindowFlag, err := bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + + if conformanceWindowFlag { + s.ConformanceWindow = &SPS_Window{} + err = s.ConformanceWindow.unmarshal(buf, &pos) + if err != nil { + return err + } + } else { + s.ConformanceWindow = nil + } + + s.BitDepthLumaMinus8, err = bits.ReadGolombUnsigned(buf, &pos) + if err != nil { + return err + } + + s.BitDepthChromaMinus8, err = bits.ReadGolombUnsigned(buf, &pos) + if err != nil { + return err + } + + s.Log2MaxPicOrderCntLsbMinus4, err = bits.ReadGolombUnsigned(buf, &pos) + if err != nil { + return err + } + + s.SubLayerOrderingInfoPresentFlag, err = bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + + var start uint8 + if s.SubLayerOrderingInfoPresentFlag { + start = 0 + } else { + start = s.MaxSubLayersMinus1 + } + + s.MaxDecPicBufferingMinus1 = make([]uint32, s.MaxSubLayersMinus1+1) + s.MaxNumReorderPics = make([]uint32, s.MaxSubLayersMinus1+1) + s.MaxLatencyIncreasePlus1 = make([]uint32, s.MaxSubLayersMinus1+1) + + for i := start; i <= s.MaxSubLayersMinus1; i++ { + s.MaxDecPicBufferingMinus1[i], err = bits.ReadGolombUnsigned(buf, &pos) + if err != nil { + return err + } + + s.MaxNumReorderPics[i], err = bits.ReadGolombUnsigned(buf, &pos) + if err != nil { + return err + } + + s.MaxLatencyIncreasePlus1[i], err = bits.ReadGolombUnsigned(buf, &pos) + if err != nil { + return err + } + } + + s.Log2MinLumaCodingBlockSizeMinus3, err = bits.ReadGolombUnsigned(buf, &pos) + if err != nil { + return err + } + + s.Log2DiffMaxMinLumaCodingBlockSize, err = bits.ReadGolombUnsigned(buf, &pos) + if err != nil { + return err + } + + s.Log2MinLumaTransformBlockSizeMinus2, err = bits.ReadGolombUnsigned(buf, &pos) + if err != nil { + return err + } + + s.Log2DiffMaxMinLumaTransformBlockSize, err = bits.ReadGolombUnsigned(buf, &pos) + if err != nil { + return err + } + + s.MaxTransformHierarchyDepthInter, err = bits.ReadGolombUnsigned(buf, &pos) + if err != nil { + return err + } + + s.MaxTransformHierarchyDepthIntra, err = bits.ReadGolombUnsigned(buf, &pos) + if err != nil { + return err + } + + s.ScalingListEnabledFlag, err = bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + + if s.ScalingListEnabledFlag { + var scalingListDataPresentFlag bool + scalingListDataPresentFlag, err = bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + + if scalingListDataPresentFlag { + s.ScalingListData = &SPS_ScalingListData{} + err = s.ScalingListData.unmarshal(buf, &pos) + if err != nil { + return err + } + } + } + + s.AmpEnabledFlag, err = bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + + s.SampleAdaptiveOffsetEnabledFlag, err = bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + + s.PcmEnabledFlag, err = bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + + if s.PcmEnabledFlag { + err = bits.HasSpace(buf, pos, 8) + if err != nil { + return err + } + + s.PcmSampleBitDepthLumaMinus1 = uint8(bits.ReadBitsUnsafe(buf, &pos, 4)) + s.PcmSampleBitDepthChromaMinus1 = uint8(bits.ReadBitsUnsafe(buf, &pos, 4)) + + s.Log2MinPcmLumaCodingBlockSizeMinus3, err = bits.ReadGolombUnsigned(buf, &pos) + if err != nil { + return err + } + + s.Log2DiffMaxMinPcmLumaCodingBlockSize, err = bits.ReadGolombUnsigned(buf, &pos) + if err != nil { + return err + } + + s.PcmLoopFilterDisabledFlag, err = bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + } + + numShortTermRefPicSets, err := bits.ReadGolombUnsigned(buf, &pos) + if err != nil { + return err + } + + if numShortTermRefPicSets > 0 { + if numShortTermRefPicSets > maxShortTermRefPics { + return fmt.Errorf("num_short_term_ref_pic_sets exceeds %d", maxShortTermRefPics) + } + + s.ShortTermRefPicSets = make([]*SPS_ShortTermRefPicSet, numShortTermRefPicSets) + + for i := uint32(0); i < numShortTermRefPicSets; i++ { + s.ShortTermRefPicSets[i] = &SPS_ShortTermRefPicSet{} + err = s.ShortTermRefPicSets[i].unmarshal(buf, &pos, i, numShortTermRefPicSets, s.ShortTermRefPicSets) + if err != nil { + return err + } + } + } else { + s.ShortTermRefPicSets = nil + } + + s.LongTermRefPicsPresentFlag, err = bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + + if s.LongTermRefPicsPresentFlag { + var numLongTermRefPicsSPS uint32 + numLongTermRefPicsSPS, err = bits.ReadGolombUnsigned(buf, &pos) + if err != nil { + return err + } + + if numLongTermRefPicsSPS > 0 { + return fmt.Errorf("long term ref pics inside SPS are not supported yet") + } + } + + s.TemporalMvpEnabledFlag, err = bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + + s.StrongIntraSmoothingEnabledFlag, err = bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + + vuiParametersPresentFlag, err := bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + + if vuiParametersPresentFlag { + s.VUI = &SPS_VUI{} + err := s.VUI.unmarshal(buf, &pos) + if err != nil { + return err + } + } else { + s.VUI = nil + } + + return nil +} + +// Width returns the video width. +func (s SPS) Width() int { + width := s.PicWidthInLumaSamples + + if s.ConformanceWindow != nil { + cropUnitX := subWidthC[s.ChromaFormatIdc] + width -= (s.ConformanceWindow.LeftOffset + s.ConformanceWindow.RightOffset) * cropUnitX + } + + return int(width) +} + +// Height returns the video height. +func (s SPS) Height() int { + height := s.PicHeightInLumaSamples + + if s.ConformanceWindow != nil { + cropUnitY := subHeightC[s.ChromaFormatIdc] + height -= (s.ConformanceWindow.TopOffset + s.ConformanceWindow.BottomOffset) * cropUnitY + } + + return int(height) +} + +// FPS returns the frames per second of the video. +func (s SPS) FPS() float64 { + if s.VUI == nil || s.VUI.TimingInfo == nil { + return 0 + } + + return float64(s.VUI.TimingInfo.TimeScale) / float64(s.VUI.TimingInfo.NumUnitsInTick) +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/jpeg/define_huffman_table.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/jpeg/define_huffman_table.go new file mode 100644 index 000000000..bc46dac34 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/jpeg/define_huffman_table.go @@ -0,0 +1,20 @@ +package jpeg + +// DefineHuffmanTable is a DHT marker. +type DefineHuffmanTable struct { + Codes []byte + Symbols []byte + TableNumber int + TableClass int +} + +// Marshal encodes the marker. +func (m DefineHuffmanTable) Marshal(buf []byte) []byte { + buf = append(buf, []byte{0xFF, MarkerDefineHuffmanTable}...) + s := 3 + len(m.Codes) + len(m.Symbols) + buf = append(buf, []byte{byte(s >> 8), byte(s)}...) // length + buf = append(buf, []byte{byte(m.TableClass<<4) | byte(m.TableNumber)}...) + buf = append(buf, m.Codes...) + buf = append(buf, m.Symbols...) + return buf +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/jpeg/define_quantization_table.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/jpeg/define_quantization_table.go new file mode 100644 index 000000000..b141fc695 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/jpeg/define_quantization_table.go @@ -0,0 +1,61 @@ +package jpeg + +import ( + "fmt" +) + +// QuantizationTable is a DQT quantization table. +type QuantizationTable struct { + ID uint8 + Precision uint8 + Data []byte +} + +// DefineQuantizationTable is a DQT marker. +type DefineQuantizationTable struct { + Tables []QuantizationTable +} + +// Unmarshal decodes the marker. +func (m *DefineQuantizationTable) Unmarshal(buf []byte) error { + for len(buf) != 0 { + id := buf[0] & 0x0F + precision := buf[0] >> 4 + buf = buf[1:] + if precision != 0 { + return fmt.Errorf("Precision %d is not supported", precision) + } + + if len(buf) < 64 { + return fmt.Errorf("image is too short") + } + + m.Tables = append(m.Tables, QuantizationTable{ + ID: id, + Precision: precision, + Data: buf[:64], + }) + buf = buf[64:] + } + + return nil +} + +// Marshal encodes the marker. +func (m DefineQuantizationTable) Marshal(buf []byte) []byte { + buf = append(buf, []byte{0xFF, MarkerDefineQuantizationTable}...) + + // length + s := 2 + for _, t := range m.Tables { + s += 1 + len(t.Data) + } + buf = append(buf, []byte{byte(s >> 8), byte(s)}...) + + for _, t := range m.Tables { + buf = append(buf, []byte{(t.ID)}...) + buf = append(buf, t.Data...) + } + + return buf +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/jpeg/define_restart_interval.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/jpeg/define_restart_interval.go new file mode 100644 index 000000000..50a3026ab --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/jpeg/define_restart_interval.go @@ -0,0 +1,20 @@ +package jpeg + +import ( + "fmt" +) + +// DefineRestartInterval is a DRI marker. +type DefineRestartInterval struct { + Interval uint16 +} + +// Unmarshal decodes the marker. +func (m *DefineRestartInterval) Unmarshal(buf []byte) error { + if len(buf) != 2 { + return fmt.Errorf("unsupported DRI size of %d", len(buf)) + } + + m.Interval = uint16(buf[0])<<8 | uint16(buf[1]) + return nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/jpeg/jpeg.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/jpeg/jpeg.go new file mode 100644 index 000000000..ce2408adc --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/jpeg/jpeg.go @@ -0,0 +1,14 @@ +// Package jpeg contains utilities to work with the JPEG codec. +package jpeg + +// standard JPEG markers. +const ( + MarkerStartOfImage = 0xD8 + MarkerDefineQuantizationTable = 0xDB + MarkerDefineHuffmanTable = 0xC4 + MarkerDefineRestartInterval = 0xDD + MarkerStartOfFrame1 = 0xC0 + MarkerStartOfScan = 0xDA + MarkerEndOfImage = 0xD9 + MarkerComment = 0xFE +) diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/jpeg/start_of_frame1.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/jpeg/start_of_frame1.go new file mode 100644 index 000000000..8eb2a25eb --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/jpeg/start_of_frame1.go @@ -0,0 +1,83 @@ +package jpeg + +import ( + "fmt" +) + +// StartOfFrame1 is a SOF1 marker. +type StartOfFrame1 struct { + Type uint8 + Width int + Height int + QuantizationTableCount uint8 // write only +} + +// Unmarshal decodes the marker. +func (m *StartOfFrame1) Unmarshal(buf []byte) error { + if len(buf) != 15 { + return fmt.Errorf("unsupported SOF size of %d", len(buf)) + } + + precision := buf[0] + if precision != 8 { + return fmt.Errorf("precision %d is not supported", precision) + } + + m.Height = int(buf[1])<<8 | int(buf[2]) + m.Width = int(buf[3])<<8 | int(buf[4]) + + components := buf[5] + if components != 3 { + return fmt.Errorf("number of components = %d is not supported", components) + } + + samp0 := buf[7] + switch samp0 { + case 0x21: + m.Type = 0 + + case 0x22: + m.Type = 1 + + default: + return fmt.Errorf("samp0 %x is not supported", samp0) + } + + samp1 := buf[10] + if samp1 != 0x11 { + return fmt.Errorf("samp1 %x is not supported", samp1) + } + + samp2 := buf[13] + if samp2 != 0x11 { + return fmt.Errorf("samp2 %x is not supported", samp2) + } + + return nil +} + +// Marshal encodes the marker. +func (m StartOfFrame1) Marshal(buf []byte) []byte { + buf = append(buf, []byte{0xFF, MarkerStartOfFrame1}...) + buf = append(buf, []byte{0, 17}...) // length + buf = append(buf, []byte{8}...) // precision + buf = append(buf, []byte{byte(m.Height >> 8), byte(m.Height)}...) // height + buf = append(buf, []byte{byte(m.Width >> 8), byte(m.Width)}...) // width + buf = append(buf, []byte{3}...) // components + if (m.Type & 0x3f) == 0 { // component 0 + buf = append(buf, []byte{0x00, 0x21, 0}...) + } else { + buf = append(buf, []byte{0x00, 0x22, 0}...) + } + + var secondQuantizationTable byte + if m.QuantizationTableCount == 2 { + secondQuantizationTable = 1 + } else { + secondQuantizationTable = 0 + } + + buf = append(buf, []byte{1, 0x11, secondQuantizationTable}...) // component 1 + buf = append(buf, []byte{2, 0x11, secondQuantizationTable}...) // component 2 + return buf +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/jpeg/start_of_image.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/jpeg/start_of_image.go new file mode 100644 index 000000000..9a6ea7a5e --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/jpeg/start_of_image.go @@ -0,0 +1,10 @@ +package jpeg + +// StartOfImage is a SOI marker. +type StartOfImage struct{} + +// Marshal encodes the marker. +func (StartOfImage) Marshal(buf []byte) []byte { + buf = append(buf, []byte{0xFF, MarkerStartOfImage}...) + return buf +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/jpeg/start_of_scan.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/jpeg/start_of_scan.go new file mode 100644 index 000000000..da78cb746 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/jpeg/start_of_scan.go @@ -0,0 +1,28 @@ +package jpeg + +import ( + "fmt" +) + +// StartOfScan is a SOS marker. +type StartOfScan struct{} + +// Unmarshal decodes the marker. +func (StartOfScan) Unmarshal(buf []byte) error { + if len(buf) != 10 { + return fmt.Errorf("unsupported SOS size of %d", len(buf)) + } + return nil +} + +// Marshal encodes the marker. +func (StartOfScan) Marshal(buf []byte) []byte { + buf = append(buf, []byte{0xFF, MarkerStartOfScan}...) + buf = append(buf, []byte{0, 12}...) // length + buf = append(buf, []byte{3}...) // components + buf = append(buf, []byte{0, 0}...) // component 0 + buf = append(buf, []byte{1, 0x11}...) // component 1 + buf = append(buf, []byte{2, 0x11}...) // component 2 + buf = append(buf, []byte{0, 63, 0}...) + return buf +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg1audio/frame_header.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg1audio/frame_header.go new file mode 100644 index 000000000..5e4c7a1de --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg1audio/frame_header.go @@ -0,0 +1,202 @@ +package mpeg1audio + +import ( + "fmt" +) + +// http://www.mp3-tech.org/programmer/frame_header.html +var bitrates = [][][]int{ + // MPEG-1 + { + // layer 1 + {}, + // layer 2 + { + 32000, + 48000, + 56000, + 64000, + 80000, + 96000, + 112000, + 128000, + 160000, + 192000, + 224000, + 256000, + 320000, + 384000, + }, + // layer 3 + { + 32000, + 40000, + 48000, + 56000, + 64000, + 80000, + 96000, + 112000, + 128000, + 160000, + 192000, + 224000, + 256000, + 320000, + }, + }, + // MPEG-2 + { + // layer 1 + {}, + // layer 2 + { + 8000, + 16000, + 24000, + 32000, + 40000, + 48000, + 56000, + 64000, + 80000, + 96000, + 112000, + 128000, + 144000, + 160000, + }, + // layer 3 + { + 8000, + 16000, + 24000, + 32000, + 40000, + 48000, + 56000, + 64000, + 80000, + 96000, + 112000, + 128000, + 144000, + 160000, + }, + }, +} + +var sampleRates = [][]int{ + // MPEG-1 + { + 44100, + 48000, + 32000, + }, + // MPEG-2 + { + 22050, + 24000, + 16000, + }, +} + +var samplesPerFrame = [][]int{ + // MPEG-1 + { + 384, + 1152, + 1152, + }, + // MPEG-2 + { + 384, + 1152, + 576, + }, +} + +// ChannelMode is a channel mode of a MPEG-1/2 audio frame. +type ChannelMode int + +// standard channel modes. +const ( + ChannelModeStereo ChannelMode = 0 + ChannelModeJointStereo ChannelMode = 1 + ChannelModeDualChannel ChannelMode = 2 + ChannelModeMono ChannelMode = 3 +) + +// FrameHeader is the header of a MPEG-1/2 audio frame. +// Specification: ISO 11172-3, 2.4.1.3 +type FrameHeader struct { + MPEG2 bool + Layer uint8 + Bitrate int + SampleRate int + Padding bool + ChannelMode ChannelMode +} + +// Unmarshal decodes a FrameHeader. +func (h *FrameHeader) Unmarshal(buf []byte) error { + if len(buf) < 5 { + return fmt.Errorf("not enough bytes") + } + + syncWord := uint16(buf[0])<<4 | uint16(buf[1])>>4 + if syncWord != 0x0FFF { + return fmt.Errorf("sync word not found: %x", syncWord) + } + + h.MPEG2 = ((buf[1] >> 3) & 0x01) == 0 + + var mpegIndex int + if h.MPEG2 { + mpegIndex = 1 + } else { + mpegIndex = 0 + } + + h.Layer = 4 - ((buf[1] >> 1) & 0b11) + if h.Layer <= 1 || h.Layer >= 4 { + return fmt.Errorf("unsupported MPEG layer: %v", h.Layer) + } + + bitrateIndex := (buf[2] >> 4) + if bitrateIndex == 0 || bitrateIndex >= 15 { + return fmt.Errorf("invalid bitrate") + } + h.Bitrate = bitrates[mpegIndex][h.Layer-1][bitrateIndex-1] + + sampleRateIndex := (buf[2] >> 2) & 0b11 + if sampleRateIndex >= 3 { + return fmt.Errorf("invalid sample rate") + } + h.SampleRate = sampleRates[mpegIndex][sampleRateIndex] + + h.Padding = ((buf[2] >> 1) & 0b1) != 0 + h.ChannelMode = ChannelMode(buf[3] >> 6) + + return nil +} + +// FrameLen returns the length of the frame associated with the header. +func (h FrameHeader) FrameLen() int { + if h.Padding { + return (144 * h.Bitrate / h.SampleRate) + 1 + } + return (144 * h.Bitrate / h.SampleRate) +} + +// SampleCount returns the number of samples contained into the frame. +func (h FrameHeader) SampleCount() int { + var mpegIndex int + if h.MPEG2 { + mpegIndex = 1 + } else { + mpegIndex = 0 + } + + return samplesPerFrame[mpegIndex][h.Layer-1] +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg1audio/mpeg1_audio.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg1audio/mpeg1_audio.go new file mode 100644 index 000000000..67b0f4e91 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg1audio/mpeg1_audio.go @@ -0,0 +1,2 @@ +// Package mpeg1audio contains utilities to work with MPEG-1/2 audio codecs. +package mpeg1audio diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio/adts.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio/adts.go new file mode 100644 index 000000000..499bf2c68 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio/adts.go @@ -0,0 +1,153 @@ +package mpeg4audio + +import ( + "fmt" +) + +// ADTSPacket is an ADTS packet. +// Specification: ISO 14496-3, Table 1.A.5 +type ADTSPacket struct { + Type ObjectType + SampleRate int + ChannelCount int + AU []byte +} + +// ADTSPackets is a group of ADTS packets. +type ADTSPackets []*ADTSPacket + +// Unmarshal decodes an ADTS stream into ADTS packets. +func (ps *ADTSPackets) Unmarshal(buf []byte) error { + // refs: https://wiki.multimedia.cx/index.php/ADTS + + bl := len(buf) + pos := 0 + + for { + if (bl - pos) < 8 { + return fmt.Errorf("invalid length") + } + + syncWord := (uint16(buf[pos]) << 4) | (uint16(buf[pos+1]) >> 4) + if syncWord != 0xfff { + return fmt.Errorf("invalid syncword") + } + + protectionAbsent := buf[pos+1] & 0x01 + if protectionAbsent != 1 { + return fmt.Errorf("CRC is not supported") + } + + pkt := &ADTSPacket{} + + pkt.Type = ObjectType((buf[pos+2] >> 6) + 1) + switch pkt.Type { + case ObjectTypeAACLC: + default: + return fmt.Errorf("unsupported audio type: %d", pkt.Type) + } + + sampleRateIndex := (buf[pos+2] >> 2) & 0x0F + switch { + case sampleRateIndex <= 12: + pkt.SampleRate = sampleRates[sampleRateIndex] + + default: + return fmt.Errorf("invalid sample rate index: %d", sampleRateIndex) + } + + channelConfig := ((buf[pos+2] & 0x01) << 2) | ((buf[pos+3] >> 6) & 0x03) + switch { + case channelConfig >= 1 && channelConfig <= 6: + pkt.ChannelCount = int(channelConfig) + + case channelConfig == 7: + pkt.ChannelCount = 8 + + default: + return fmt.Errorf("invalid channel configuration: %d", channelConfig) + } + + frameLen := int(((uint16(buf[pos+3])&0x03)<<11)| + (uint16(buf[pos+4])<<3)| + ((uint16(buf[pos+5])>>5)&0x07)) - 7 + + if frameLen <= 0 { + return fmt.Errorf("invalid FrameLen") + } + + if frameLen > MaxAccessUnitSize { + return fmt.Errorf("access unit size (%d) is too big, maximum is %d", frameLen, MaxAccessUnitSize) + } + + frameCount := buf[pos+6] & 0x03 + if frameCount != 0 { + return fmt.Errorf("frame count greater than 1 is not supported") + } + + if len(buf[pos+7:]) < frameLen { + return fmt.Errorf("invalid frame length") + } + + pkt.AU = buf[pos+7 : pos+7+frameLen] + pos += 7 + frameLen + + *ps = append(*ps, pkt) + + if (bl - pos) == 0 { + break + } + } + + return nil +} + +func (ps ADTSPackets) marshalSize() int { + n := 0 + for _, pkt := range ps { + n += 7 + len(pkt.AU) + } + return n +} + +// Marshal encodes ADTS packets into an ADTS stream. +func (ps ADTSPackets) Marshal() ([]byte, error) { + buf := make([]byte, ps.marshalSize()) + pos := 0 + + for _, pkt := range ps { + sampleRateIndex, ok := reverseSampleRates[pkt.SampleRate] + if !ok { + return nil, fmt.Errorf("invalid sample rate: %d", pkt.SampleRate) + } + + var channelConfig int + switch { + case pkt.ChannelCount >= 1 && pkt.ChannelCount <= 6: + channelConfig = pkt.ChannelCount + + case pkt.ChannelCount == 8: + channelConfig = 7 + + default: + return nil, fmt.Errorf("invalid channel count (%d)", pkt.ChannelCount) + } + + frameLen := len(pkt.AU) + 7 + + fullness := 0x07FF // like ffmpeg does + + buf[pos+0] = 0xFF + buf[pos+1] = 0xF1 + buf[pos+2] = uint8((int(pkt.Type-1) << 6) | (sampleRateIndex << 2) | ((channelConfig >> 2) & 0x01)) + buf[pos+3] = uint8((channelConfig&0x03)<<6 | (frameLen>>11)&0x03) + buf[pos+4] = uint8((frameLen >> 3) & 0xFF) + buf[pos+5] = uint8((frameLen&0x07)<<5 | ((fullness >> 6) & 0x1F)) + buf[pos+6] = uint8((fullness & 0x3F) << 2) + pos += 7 + + pos += copy(buf[pos:], pkt.AU) + } + + return buf, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio/audio_specific_config.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio/audio_specific_config.go new file mode 100644 index 000000000..c9b5181fe --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio/audio_specific_config.go @@ -0,0 +1,261 @@ +package mpeg4audio + +import ( + "fmt" + + "github.com/bluenviron/mediacommon/v2/pkg/bits" +) + +// Config is an alias for AudioSpecificConfig. +type Config = AudioSpecificConfig + +// AudioSpecificConfig is an AudioSpecificConfig. +// Specification: ISO 14496-3, 1.6.2.1 +type AudioSpecificConfig struct { + Type ObjectType + SampleRate int + ChannelCount int + + // SBR / PS specific + ExtensionType ObjectType + ExtensionSampleRate int + + FrameLengthFlag bool + DependsOnCoreCoder bool + CoreCoderDelay uint16 +} + +// Unmarshal decodes a Config. +func (c *AudioSpecificConfig) Unmarshal(buf []byte) error { + pos := 0 + return c.UnmarshalFromPos(buf, &pos) +} + +// UnmarshalFromPos decodes a Config. +func (c *AudioSpecificConfig) UnmarshalFromPos(buf []byte, pos *int) error { + tmp, err := bits.ReadBits(buf, pos, 5) + if err != nil { + return err + } + c.Type = ObjectType(tmp) + + switch c.Type { + case ObjectTypeAACLC, ObjectTypeSBR, ObjectTypePS: + default: + return fmt.Errorf("unsupported object type: %d", c.Type) + } + + sampleRateIndex, err := bits.ReadBits(buf, pos, 4) + if err != nil { + return err + } + + switch { + case sampleRateIndex <= 12: + c.SampleRate = sampleRates[sampleRateIndex] + + case sampleRateIndex == 0x0F: + tmp, err = bits.ReadBits(buf, pos, 24) + if err != nil { + return err + } + c.SampleRate = int(tmp) + + default: + return fmt.Errorf("invalid sample rate index (%d)", sampleRateIndex) + } + + channelConfig, err := bits.ReadBits(buf, pos, 4) + if err != nil { + return err + } + + switch { + case channelConfig == 0: + return fmt.Errorf("not yet supported") + + case channelConfig >= 1 && channelConfig <= 6: + c.ChannelCount = int(channelConfig) + + case channelConfig == 7: + c.ChannelCount = 8 + + default: + return fmt.Errorf("invalid channel configuration (%d)", channelConfig) + } + + if c.Type == ObjectTypeSBR || c.Type == ObjectTypePS { + c.ExtensionType = c.Type + + var extensionSamplingFrequencyIndex uint64 + extensionSamplingFrequencyIndex, err = bits.ReadBits(buf, pos, 4) + if err != nil { + return err + } + + switch { + case extensionSamplingFrequencyIndex <= 12: + c.ExtensionSampleRate = sampleRates[extensionSamplingFrequencyIndex] + + case extensionSamplingFrequencyIndex == 0x0F: + tmp, err = bits.ReadBits(buf, pos, 24) + if err != nil { + return err + } + c.ExtensionSampleRate = int(tmp) + + default: + return fmt.Errorf("invalid extension sample rate index (%d)", extensionSamplingFrequencyIndex) + } + + tmp, err = bits.ReadBits(buf, pos, 5) + if err != nil { + return err + } + c.Type = ObjectType(tmp) + + if c.Type != ObjectTypeAACLC { + return fmt.Errorf("unsupported object type: %d", c.Type) + } + } + + c.FrameLengthFlag, err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + + c.DependsOnCoreCoder, err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + + if c.DependsOnCoreCoder { + tmp, err = bits.ReadBits(buf, pos, 14) + if err != nil { + return err + } + c.CoreCoderDelay = uint16(tmp) + } + + extensionFlag, err := bits.ReadFlag(buf, pos) + if err != nil { + return err + } + + if extensionFlag { + return fmt.Errorf("unsupported") + } + + return nil +} + +func (c AudioSpecificConfig) marshalSizeBits() int { + n := 5 + 4 + 2 + 1 + + _, ok := reverseSampleRates[c.SampleRate] + if !ok { + n += 28 + } else { + n += 4 + } + + if c.ExtensionType == ObjectTypeSBR || c.ExtensionType == ObjectTypePS { + _, ok := reverseSampleRates[c.ExtensionSampleRate] + if !ok { + n += 28 + } else { + n += 4 + } + n += 5 + } + + if c.DependsOnCoreCoder { + n += 14 + } + + return n +} + +func (c AudioSpecificConfig) marshalSize() int { + n := c.marshalSizeBits() + + ret := n / 8 + if (n % 8) != 0 { + ret++ + } + + return ret +} + +// Marshal encodes a Config. +func (c AudioSpecificConfig) Marshal() ([]byte, error) { + buf := make([]byte, c.marshalSize()) + pos := 0 + + err := c.marshalTo(buf, &pos) + if err != nil { + return nil, err + } + + return buf, nil +} + +func (c AudioSpecificConfig) marshalTo(buf []byte, pos *int) error { + if c.ExtensionType == ObjectTypeSBR || c.ExtensionType == ObjectTypePS { + bits.WriteBitsUnsafe(buf, pos, uint64(c.ExtensionType), 5) + } else { + bits.WriteBitsUnsafe(buf, pos, uint64(c.Type), 5) + } + + sampleRateIndex, ok := reverseSampleRates[c.SampleRate] + if !ok { + bits.WriteBitsUnsafe(buf, pos, uint64(15), 4) + bits.WriteBitsUnsafe(buf, pos, uint64(c.SampleRate), 24) + } else { + bits.WriteBitsUnsafe(buf, pos, uint64(sampleRateIndex), 4) + } + + var channelConfig int + switch { + case c.ChannelCount >= 1 && c.ChannelCount <= 6: + channelConfig = c.ChannelCount + + case c.ChannelCount == 8: + channelConfig = 7 + + default: + return fmt.Errorf("invalid channel count (%d)", c.ChannelCount) + } + bits.WriteBitsUnsafe(buf, pos, uint64(channelConfig), 4) + + if c.ExtensionType == ObjectTypeSBR || c.ExtensionType == ObjectTypePS { + sampleRateIndex, ok := reverseSampleRates[c.ExtensionSampleRate] + if !ok { + bits.WriteBitsUnsafe(buf, pos, uint64(0x0F), 4) + bits.WriteBitsUnsafe(buf, pos, uint64(c.ExtensionSampleRate), 24) + } else { + bits.WriteBitsUnsafe(buf, pos, uint64(sampleRateIndex), 4) + } + bits.WriteBitsUnsafe(buf, pos, uint64(c.Type), 5) + } + + if c.FrameLengthFlag { + bits.WriteBitsUnsafe(buf, pos, 1, 1) + } else { + bits.WriteBitsUnsafe(buf, pos, 0, 1) + } + + if c.DependsOnCoreCoder { + bits.WriteBitsUnsafe(buf, pos, 1, 1) + } else { + bits.WriteBitsUnsafe(buf, pos, 0, 1) + } + + if c.DependsOnCoreCoder { + bits.WriteBitsUnsafe(buf, pos, uint64(c.CoreCoderDelay), 14) + } + + *pos++ // extensionFlag + + return nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio/mpeg4_audio.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio/mpeg4_audio.go new file mode 100644 index 000000000..9d7ddcbe1 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio/mpeg4_audio.go @@ -0,0 +1,10 @@ +// Package mpeg4audio contains utilities to work with MPEG-4 audio codecs. +package mpeg4audio + +const ( + // MaxAccessUnitSize is the maximum size of an access unit. + MaxAccessUnitSize = 5 * 1024 + + // SamplesPerAccessUnit is the number of samples contained inside an access unit. + SamplesPerAccessUnit = 1024 +) diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio/object_type.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio/object_type.go new file mode 100644 index 000000000..e84603992 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio/object_type.go @@ -0,0 +1,12 @@ +package mpeg4audio + +// ObjectType is a MPEG-4 Audio object type. +// Specification: ISO 14496-3, Table 1.17 +type ObjectType int + +// supported types. +const ( + ObjectTypeAACLC ObjectType = 2 + ObjectTypeSBR ObjectType = 5 + ObjectTypePS ObjectType = 29 +) diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio/sample_rates.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio/sample_rates.go new file mode 100644 index 000000000..660b86e27 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio/sample_rates.go @@ -0,0 +1,33 @@ +package mpeg4audio + +var sampleRates = []int{ + 96000, + 88200, + 64000, + 48000, + 44100, + 32000, + 24000, + 22050, + 16000, + 12000, + 11025, + 8000, + 7350, +} + +var reverseSampleRates = map[int]int{ + 96000: 0, + 88200: 1, + 64000: 2, + 48000: 3, + 44100: 4, + 32000: 5, + 24000: 6, + 22050: 7, + 16000: 8, + 12000: 9, + 11025: 10, + 8000: 11, + 7350: 12, +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio/stream_mux_config.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio/stream_mux_config.go new file mode 100644 index 000000000..348b9c641 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio/stream_mux_config.go @@ -0,0 +1,324 @@ +package mpeg4audio + +import ( + "fmt" + + "github.com/bluenviron/mediacommon/v2/pkg/bits" +) + +// StreamMuxConfigLayer is a layer of a StreamMuxConfig. +type StreamMuxConfigLayer struct { + AudioSpecificConfig *AudioSpecificConfig + FrameLengthType uint + LatmBufferFullness uint + FrameLength uint + CELPframeLengthTableIndex uint + HVXCframeLengthTableIndex bool +} + +// StreamMuxConfigProgram is a program of a StreamMuxConfig. +type StreamMuxConfigProgram struct { + Layers []*StreamMuxConfigLayer +} + +// StreamMuxConfig is a StreamMuxConfig. +// Specification: ISO 14496-3, Table 1.42 +type StreamMuxConfig struct { + NumSubFrames uint + Programs []*StreamMuxConfigProgram + OtherDataPresent bool + OtherDataLenBits uint32 + CRCCheckPresent bool + CRCCheckSum uint8 +} + +// Unmarshal decodes a StreamMuxConfig. +func (c *StreamMuxConfig) Unmarshal(buf []byte) error { + pos := 0 + + err := bits.HasSpace(buf, pos, 12) + if err != nil { + return err + } + + audioMuxVersion := bits.ReadFlagUnsafe(buf, &pos) + if audioMuxVersion { + return fmt.Errorf("audioMuxVersion = 1 is not supported") + } + + allStreamsSameTimeFraming := bits.ReadFlagUnsafe(buf, &pos) + if !allStreamsSameTimeFraming { + return fmt.Errorf("allStreamsSameTimeFraming = 0 is not supported") + } + + c.NumSubFrames = uint(bits.ReadBitsUnsafe(buf, &pos, 6)) + numProgram := uint(bits.ReadBitsUnsafe(buf, &pos, 4)) + + c.Programs = make([]*StreamMuxConfigProgram, numProgram+1) + + for prog := uint(0); prog <= numProgram; prog++ { + p := &StreamMuxConfigProgram{} + c.Programs[prog] = p + + var numLayer uint64 + numLayer, err = bits.ReadBits(buf, &pos, 3) + if err != nil { + return err + } + + p.Layers = make([]*StreamMuxConfigLayer, numLayer+1) + + for lay := uint(0); lay <= uint(numLayer); lay++ { + l := &StreamMuxConfigLayer{} + p.Layers[lay] = l + + var useSameConfig bool + + if prog == 0 && lay == 0 { + useSameConfig = false + } else { + useSameConfig, err = bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + } + + if !useSameConfig { + l.AudioSpecificConfig = &AudioSpecificConfig{} + err = l.AudioSpecificConfig.UnmarshalFromPos(buf, &pos) + if err != nil { + return err + } + } + + var tmp uint64 + tmp, err = bits.ReadBits(buf, &pos, 3) + if err != nil { + // support truncated configs + l.LatmBufferFullness = 255 + c.Programs = c.Programs[:prog+1] + p.Layers = p.Layers[:lay+1] + return nil //nolint:nilerr + } + l.FrameLengthType = uint(tmp) + + switch l.FrameLengthType { + case 0: + tmp, err = bits.ReadBits(buf, &pos, 8) + if err != nil { + return err + } + l.LatmBufferFullness = uint(tmp) + + case 1: + tmp, err = bits.ReadBits(buf, &pos, 9) + if err != nil { + return err + } + l.FrameLength = uint(tmp) + + case 4, 5, 3: + tmp, err = bits.ReadBits(buf, &pos, 6) + if err != nil { + return err + } + l.CELPframeLengthTableIndex = uint(tmp) + + case 6, 7: + l.HVXCframeLengthTableIndex, err = bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + } + } + } + + c.OtherDataPresent, err = bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + + if c.OtherDataPresent { + for { + c.OtherDataLenBits *= 256 + + err = bits.HasSpace(buf, pos, 9) + if err != nil { + return err + } + + otherDataLenEsc := bits.ReadFlagUnsafe(buf, &pos) + otherDataLenTmp := uint32(bits.ReadBitsUnsafe(buf, &pos, 8)) + c.OtherDataLenBits += otherDataLenTmp + + if !otherDataLenEsc { + break + } + } + } + + c.CRCCheckPresent, err = bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + + if c.CRCCheckPresent { + tmp, err := bits.ReadBits(buf, &pos, 8) + if err != nil { + return err + } + c.CRCCheckSum = uint8(tmp) + } + + return nil +} + +func (c StreamMuxConfig) marshalSize() int { + n := 12 + + for prog, p := range c.Programs { + n += 3 + + for lay, l := range p.Layers { + if prog != 0 || lay != 0 { + n++ + } + + if l.AudioSpecificConfig != nil { + n += l.AudioSpecificConfig.marshalSizeBits() + } + + n += 3 + + switch l.FrameLengthType { + case 0: + n += 8 + + case 1: + n += 9 + + case 4, 5, 3: + n += 6 + + case 6, 7: + n++ + } + } + } + + n++ // otherDataPresent + + if c.OtherDataPresent { + tmp := c.OtherDataLenBits + for { + tmp /= 256 + n += 9 + + if tmp == 0 { + break + } + } + } + + n++ // crcCheckPresent + + if c.CRCCheckPresent { + n += 8 + } + + ret := n / 8 + if (n % 8) != 0 { + ret++ + } + + return ret +} + +// Marshal encodes a StreamMuxConfig. +func (c StreamMuxConfig) Marshal() ([]byte, error) { + buf := make([]byte, c.marshalSize()) + pos := 0 + + bits.WriteBitsUnsafe(buf, &pos, 0, 1) // audioMuxVersion + bits.WriteBitsUnsafe(buf, &pos, 1, 1) // allStreamsSameTimeFraming + bits.WriteBitsUnsafe(buf, &pos, uint64(c.NumSubFrames), 6) + bits.WriteBitsUnsafe(buf, &pos, uint64(len(c.Programs)-1), 4) + + for prog, p := range c.Programs { + bits.WriteBitsUnsafe(buf, &pos, uint64(len(p.Layers)-1), 3) + + for lay, l := range p.Layers { + if prog != 0 || lay != 0 { + if l.AudioSpecificConfig != nil { + bits.WriteBitsUnsafe(buf, &pos, 0, 1) + } else { + bits.WriteBitsUnsafe(buf, &pos, 1, 1) + } + } + + if l.AudioSpecificConfig != nil { + err := l.AudioSpecificConfig.marshalTo(buf, &pos) + if err != nil { + return nil, err + } + } + + bits.WriteBitsUnsafe(buf, &pos, uint64(l.FrameLengthType), 3) + + switch l.FrameLengthType { + case 0: + bits.WriteBitsUnsafe(buf, &pos, uint64(l.LatmBufferFullness), 8) + + case 1: + bits.WriteBitsUnsafe(buf, &pos, uint64(l.FrameLength), 9) + + case 4, 5, 3: + bits.WriteBitsUnsafe(buf, &pos, uint64(l.CELPframeLengthTableIndex), 6) + + case 6, 7: + if l.HVXCframeLengthTableIndex { + bits.WriteBitsUnsafe(buf, &pos, 1, 1) + } else { + bits.WriteBitsUnsafe(buf, &pos, 0, 1) + } + } + } + } + + if c.OtherDataPresent { + bits.WriteBitsUnsafe(buf, &pos, 1, 1) + + var lenBytes []byte + tmp := c.OtherDataLenBits + + for { + mod := tmp % 256 + tmp -= mod + tmp /= 256 + lenBytes = append(lenBytes, uint8(mod)) + + if tmp == 0 { + break + } + } + + for i := len(lenBytes) - 1; i > 0; i-- { + bits.WriteBitsUnsafe(buf, &pos, 1, 1) + bits.WriteBitsUnsafe(buf, &pos, uint64(lenBytes[i]), 8) + } + + bits.WriteBitsUnsafe(buf, &pos, 0, 1) + bits.WriteBitsUnsafe(buf, &pos, uint64(lenBytes[0]), 8) + } else { + bits.WriteBitsUnsafe(buf, &pos, 0, 1) + } + + if c.CRCCheckPresent { + bits.WriteBitsUnsafe(buf, &pos, 1, 1) + bits.WriteBitsUnsafe(buf, &pos, uint64(c.CRCCheckSum), 8) + } else { + bits.WriteBitsUnsafe(buf, &pos, 0, 1) + } + + return buf, nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4video/is_valid_config.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4video/is_valid_config.go new file mode 100644 index 000000000..41e5c8c0f --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4video/is_valid_config.go @@ -0,0 +1,48 @@ +package mpeg4video + +import ( + "bytes" + "fmt" +) + +// IsValidConfig checks whether a MPEG-4 Video configuration is valid. +func IsValidConfig(config []byte) error { + if !bytes.HasPrefix(config, []byte{0, 0, 1, byte(VisualObjectSequenceStartCode)}) { + return fmt.Errorf("doesn't start with visual_object_sequence_start_code") + } + + videoObjectFound := false + videoObjectLayerFound := false + + for i := 4; i < (len(config) - 4); i++ { + if bytes.Equal(config[i:i+3], []byte{0, 0, 1}) { + startCode := StartCode(config[i+3]) + + switch { + case startCode >= VideoObjectStartCodeFirst && startCode <= VideoObjectStartCodeLast: + videoObjectFound = true + + case startCode >= VideoObjectLayerStartCodeFirst && startCode <= VideoObjectLayerStartCodeLast: + videoObjectLayerFound = true + + case startCode == VisualObjectStartCode, + startCode == UserDataStartCode: + + default: + return fmt.Errorf("unexpected start code: %x", startCode) + } + + i += 3 + } + } + + if !videoObjectFound { + return fmt.Errorf("video object not found") + } + + if !videoObjectLayerFound { + return fmt.Errorf("video object layer not found") + } + + return nil +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4video/mpeg4_video.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4video/mpeg4_video.go new file mode 100644 index 000000000..49f3c7045 --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4video/mpeg4_video.go @@ -0,0 +1,24 @@ +// Package mpeg4video contains utilities to work with MPEG-4 part 2 video codecs. +package mpeg4video + +const ( + // MaxFrameSize is the maximum size of a frame. + MaxFrameSize = 1 * 1024 * 1024 +) + +// StartCode is a MPEG-4 Video start code. +// Specification: ISO 14496-2, Table 6-3 +type StartCode uint8 + +// start codes. +const ( + VideoObjectStartCodeFirst StartCode = 0x00 + VideoObjectStartCodeLast StartCode = 0x1F + VisualObjectSequenceStartCode StartCode = 0xB0 + VideoObjectLayerStartCodeFirst StartCode = 0x20 + VideoObjectLayerStartCodeLast StartCode = 0x2F + UserDataStartCode StartCode = 0xB2 + GroupOfVOPStartCode StartCode = 0xB3 + VisualObjectStartCode StartCode = 0xB5 + VOPStartCode StartCode = 0xB6 +) diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/vp8/vp8.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/vp8/vp8.go new file mode 100644 index 000000000..3cee8f1ee --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/vp8/vp8.go @@ -0,0 +1,7 @@ +// Package vp8 contains utilities to work with the VP8 codec. +package vp8 + +const ( + // MaxFrameSize is the maximum size of a frame. + MaxFrameSize = 2 * 1024 * 1024 +) diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/vp9/header.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/vp9/header.go new file mode 100644 index 000000000..ad52d5a9a --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/vp9/header.go @@ -0,0 +1,227 @@ +package vp9 + +import ( + "fmt" + + "github.com/bluenviron/mediacommon/v2/pkg/bits" +) + +// Header_ColorConfig is the color_config member of an header. +type Header_ColorConfig struct { //nolint:revive + TenOrTwelveBit bool + BitDepth uint8 + ColorSpace uint8 + ColorRange bool + SubsamplingX bool + SubsamplingY bool +} + +func (c *Header_ColorConfig) unmarshal(profile uint8, buf []byte, pos *int) error { + if profile >= 2 { + var err error + c.TenOrTwelveBit, err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + + if c.TenOrTwelveBit { + c.BitDepth = 12 + } else { + c.BitDepth = 10 + } + } else { + c.BitDepth = 8 + } + + tmp, err := bits.ReadBits(buf, pos, 3) + if err != nil { + return err + } + c.ColorSpace = uint8(tmp) + + if c.ColorSpace != 7 { + var err error + c.ColorRange, err = bits.ReadFlag(buf, pos) + if err != nil { + return err + } + + if profile == 1 || profile == 3 { + err := bits.HasSpace(buf, *pos, 3) + if err != nil { + return err + } + + c.SubsamplingX = bits.ReadFlagUnsafe(buf, pos) + c.SubsamplingY = bits.ReadFlagUnsafe(buf, pos) + *pos++ + } else { + c.SubsamplingX = true + c.SubsamplingY = true + } + } else { + c.ColorRange = true + + if profile == 1 || profile == 3 { + c.SubsamplingX = false + c.SubsamplingY = false + + err := bits.HasSpace(buf, *pos, 1) + if err != nil { + return err + } + *pos++ + } + } + + return nil +} + +// Header_FrameSize is the frame_size member of an header. +type Header_FrameSize struct { //nolint:revive + FrameWidthMinus1 uint16 + FrameHeightMinus1 uint16 +} + +func (s *Header_FrameSize) unmarshal(buf []byte, pos *int) error { + err := bits.HasSpace(buf, *pos, 32) + if err != nil { + return err + } + + s.FrameWidthMinus1 = uint16(bits.ReadBitsUnsafe(buf, pos, 16)) + s.FrameHeightMinus1 = uint16(bits.ReadBitsUnsafe(buf, pos, 16)) + return nil +} + +// Header is a VP9 Frame header. +// Specification: +// https://storage.googleapis.com/downloads.webmproject.org/docs/vp9/vp9-bitstream-specification-v0.6-20160331-draft.pdf +type Header struct { + Profile uint8 + ShowExistingFrame bool + FrameToShowMapIdx uint8 + NonKeyFrame bool + ShowFrame bool + ErrorResilientMode bool + ColorConfig *Header_ColorConfig + FrameSize *Header_FrameSize +} + +// Unmarshal decodes a Header. +func (h *Header) Unmarshal(buf []byte) error { + pos := 0 + + err := bits.HasSpace(buf, pos, 4) + if err != nil { + return err + } + + frameMarker := bits.ReadBitsUnsafe(buf, &pos, 2) + if frameMarker != 2 { + return fmt.Errorf("invalid frame marker") + } + + profileLowBit := uint8(bits.ReadBitsUnsafe(buf, &pos, 1)) + profileHighBit := uint8(bits.ReadBitsUnsafe(buf, &pos, 1)) + h.Profile = profileHighBit<<1 + profileLowBit + + if h.Profile == 3 { + err = bits.HasSpace(buf, pos, 1) + if err != nil { + return err + } + pos++ + } + + h.ShowExistingFrame, err = bits.ReadFlag(buf, &pos) + if err != nil { + return err + } + + if h.ShowExistingFrame { + var tmp uint64 + tmp, err = bits.ReadBits(buf, &pos, 3) + if err != nil { + return err + } + h.FrameToShowMapIdx = uint8(tmp) + return nil + } + + err = bits.HasSpace(buf, pos, 3) + if err != nil { + return err + } + + h.NonKeyFrame = bits.ReadFlagUnsafe(buf, &pos) + h.ShowFrame = bits.ReadFlagUnsafe(buf, &pos) + h.ErrorResilientMode = bits.ReadFlagUnsafe(buf, &pos) + + if !h.NonKeyFrame { + err := bits.HasSpace(buf, pos, 24) + if err != nil { + return err + } + + frameSyncByte0 := uint8(bits.ReadBitsUnsafe(buf, &pos, 8)) + if frameSyncByte0 != 0x49 { + return fmt.Errorf("wrong frame_sync_byte_0") + } + + frameSyncByte1 := uint8(bits.ReadBitsUnsafe(buf, &pos, 8)) + if frameSyncByte1 != 0x83 { + return fmt.Errorf("wrong frame_sync_byte_1") + } + + frameSyncByte2 := uint8(bits.ReadBitsUnsafe(buf, &pos, 8)) + if frameSyncByte2 != 0x42 { + return fmt.Errorf("wrong frame_sync_byte_2") + } + + h.ColorConfig = &Header_ColorConfig{} + err = h.ColorConfig.unmarshal(h.Profile, buf, &pos) + if err != nil { + return err + } + + h.FrameSize = &Header_FrameSize{} + err = h.FrameSize.unmarshal(buf, &pos) + if err != nil { + return err + } + } + + return nil +} + +// Width returns the video width. +func (h Header) Width() int { + if h.FrameSize == nil { + return 0 + } + return int(h.FrameSize.FrameWidthMinus1) + 1 +} + +// Height returns the video height. +func (h Header) Height() int { + if h.FrameSize == nil { + return 0 + } + return int(h.FrameSize.FrameHeightMinus1) + 1 +} + +// ChromaSubsampling returns the chroma subsampling format, in ISO-BMFF/vpcC format. +func (h Header) ChromaSubsampling() uint8 { + if h.ColorConfig == nil { + return 1 + } + switch { + case !h.ColorConfig.SubsamplingX && !h.ColorConfig.SubsamplingY: + return 3 // 4:4:4 + case h.ColorConfig.SubsamplingX && !h.ColorConfig.SubsamplingY: + return 2 // 4:2:2 + default: + return 1 // 4:2:0 colocated with luma + } +} diff --git a/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/vp9/vp9.go b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/vp9/vp9.go new file mode 100644 index 000000000..1fed4f73c --- /dev/null +++ b/trunk/3rdparty/srs-bench/vendor/github.com/bluenviron/mediacommon/v2/pkg/codecs/vp9/vp9.go @@ -0,0 +1,7 @@ +// Package vp9 contains utilities to work with the VP9 codec. +package vp9 + +const ( + // MaxFrameSize is the maximum size of a frame. + MaxFrameSize = 2 * 1024 * 1024 +) diff --git a/trunk/3rdparty/srs-bench/vendor/modules.txt b/trunk/3rdparty/srs-bench/vendor/modules.txt index 2c6673217..3b629f45c 100644 --- a/trunk/3rdparty/srs-bench/vendor/modules.txt +++ b/trunk/3rdparty/srs-bench/vendor/modules.txt @@ -1,3 +1,48 @@ +# github.com/bluenviron/gortsplib/v4 v4.13.1 +## explicit; go 1.21.0 +github.com/bluenviron/gortsplib/v4 +github.com/bluenviron/gortsplib/v4/pkg/auth +github.com/bluenviron/gortsplib/v4/pkg/base +github.com/bluenviron/gortsplib/v4/pkg/bytecounter +github.com/bluenviron/gortsplib/v4/pkg/conn +github.com/bluenviron/gortsplib/v4/pkg/description +github.com/bluenviron/gortsplib/v4/pkg/format +github.com/bluenviron/gortsplib/v4/pkg/format/rtpac3 +github.com/bluenviron/gortsplib/v4/pkg/format/rtpav1 +github.com/bluenviron/gortsplib/v4/pkg/format/rtph264 +github.com/bluenviron/gortsplib/v4/pkg/format/rtph265 +github.com/bluenviron/gortsplib/v4/pkg/format/rtplpcm +github.com/bluenviron/gortsplib/v4/pkg/format/rtpmjpeg +github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg1audio +github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg1video +github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4audio +github.com/bluenviron/gortsplib/v4/pkg/format/rtpmpeg4video +github.com/bluenviron/gortsplib/v4/pkg/format/rtpsimpleaudio +github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp8 +github.com/bluenviron/gortsplib/v4/pkg/format/rtpvp9 +github.com/bluenviron/gortsplib/v4/pkg/headers +github.com/bluenviron/gortsplib/v4/pkg/liberrors +github.com/bluenviron/gortsplib/v4/pkg/multicast +github.com/bluenviron/gortsplib/v4/pkg/ringbuffer +github.com/bluenviron/gortsplib/v4/pkg/rtcpreceiver +github.com/bluenviron/gortsplib/v4/pkg/rtcpsender +github.com/bluenviron/gortsplib/v4/pkg/rtplossdetector +github.com/bluenviron/gortsplib/v4/pkg/rtpreorderer +github.com/bluenviron/gortsplib/v4/pkg/rtptime +github.com/bluenviron/gortsplib/v4/pkg/sdp +# github.com/bluenviron/mediacommon/v2 v2.1.0 +## explicit; go 1.21.0 +github.com/bluenviron/mediacommon/v2/pkg/bits +github.com/bluenviron/mediacommon/v2/pkg/codecs/ac3 +github.com/bluenviron/mediacommon/v2/pkg/codecs/av1 +github.com/bluenviron/mediacommon/v2/pkg/codecs/h264 +github.com/bluenviron/mediacommon/v2/pkg/codecs/h265 +github.com/bluenviron/mediacommon/v2/pkg/codecs/jpeg +github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg1audio +github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio +github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4video +github.com/bluenviron/mediacommon/v2/pkg/codecs/vp8 +github.com/bluenviron/mediacommon/v2/pkg/codecs/vp9 # github.com/ghettovoice/gosip v0.0.0-20220929080231-de8ba881be83 ## explicit; go 1.13 github.com/ghettovoice/gosip/log diff --git a/trunk/Dockerfile.test b/trunk/Dockerfile.test index cd7bde7c5..4b10ffad1 100644 --- a/trunk/Dockerfile.test +++ b/trunk/Dockerfile.test @@ -18,7 +18,7 @@ WORKDIR /srs/trunk # Note that we must enable the gcc7 or link failed. # Please note that we must disable the ffmpeg-opus, as it negatively impacts performance. We may consider # enabling it in the future when support for multi-threading transcoding is available. -RUN ./configure --srt=on --gb28181=on --srt=on --apm=on --utest=on --ffmpeg-opus=off --build-cache=on +RUN ./configure --srt=on --gb28181=on --srt=on --rtsp=on --apm=on --utest=on --ffmpeg-opus=off --build-cache=on RUN make utest ${MAKEARGS} # Build benchmark tool. diff --git a/trunk/auto/auto_headers.sh b/trunk/auto/auto_headers.sh index f15bbb8b2..6fac1b2c3 100755 --- a/trunk/auto/auto_headers.sh +++ b/trunk/auto/auto_headers.sh @@ -80,6 +80,12 @@ else srs_undefine_macro "SRS_RTC" $SRS_AUTO_HEADERS_H fi +if [[ $SRS_RTSP == YES ]]; then + srs_define_macro "SRS_RTSP" $SRS_AUTO_HEADERS_H +else + srs_undefine_macro "SRS_RTSP" $SRS_AUTO_HEADERS_H +fi + if [[ $SRS_FFMPEG_FIT == YES ]]; then srs_define_macro "SRS_FFMPEG_FIT" $SRS_AUTO_HEADERS_H else diff --git a/trunk/auto/options.sh b/trunk/auto/options.sh index 1b36046cd..72459cc42 100755 --- a/trunk/auto/options.sh +++ b/trunk/auto/options.sh @@ -6,6 +6,7 @@ help=no SRS_HDS=NO SRS_SRT=YES SRS_RTC=YES +SRS_RTSP=NO # SRS_H265 is always enabled, no longer configurable SRS_H265=RESERVED SRS_GB28181=NO @@ -184,6 +185,7 @@ Features: --utest=on|off Whether build the utest. Default: $(value2switch $SRS_UTEST) --srt=on|off Whether build the SRT. Default: $(value2switch $SRS_SRT) --rtc=on|off Whether build the WebRTC. Default: $(value2switch $SRS_RTC) + --rtsp=on|off Whether build the RTSP (requires RTC). Default: $(value2switch $SRS_RTSP) --gb28181=on|off Whether build the GB28181. Default: $(value2switch $SRS_GB28181) --cxx11=on|off Whether enable the C++11. Default: $(value2switch $SRS_CXX11) --cxx14=on|off Whether enable the C++14. Default: $(value2switch $SRS_CXX14) @@ -345,6 +347,7 @@ function parse_user_option() { --apm) SRS_APM=$(switch2value $value) ;; --srt) SRS_SRT=$(switch2value $value) ;; --rtc) SRS_RTC=$(switch2value $value) ;; + --rtsp) SRS_RTSP=$(switch2value $value) ;; --simulator) SRS_SIMULATOR=$(switch2value $value) ;; --generate-objs) SRS_GENERATE_OBJS=$(switch2value $value) ;; --single-thread) SRS_SINGLE_THREAD=$(switch2value $value) ;; @@ -537,6 +540,7 @@ function apply_auto_options() { if [[ $SRS_RTC == YES && $SRS_FFMPEG_FIT == RESERVED ]]; then SRS_FFMPEG_FIT=YES fi + if [[ $SRS_USE_SYS_FFMPEG == YES && $SRS_SHARED_FFMPEG == RESERVED ]]; then SRS_SHARED_FFMPEG=YES fi @@ -661,6 +665,7 @@ function regenerate_options() { SRS_AUTO_CONFIGURE="${SRS_AUTO_CONFIGURE} --srt=$(value2switch $SRS_SRT)" SRS_AUTO_CONFIGURE="${SRS_AUTO_CONFIGURE} --sys-srt=$(value2switch $SRS_USE_SYS_SRT)" SRS_AUTO_CONFIGURE="${SRS_AUTO_CONFIGURE} --rtc=$(value2switch $SRS_RTC)" + SRS_AUTO_CONFIGURE="${SRS_AUTO_CONFIGURE} --rtsp=$(value2switch $SRS_RTSP)" SRS_AUTO_CONFIGURE="${SRS_AUTO_CONFIGURE} --gb28181=$(value2switch $SRS_GB28181)" SRS_AUTO_CONFIGURE="${SRS_AUTO_CONFIGURE} --simulator=$(value2switch $SRS_SIMULATOR)" diff --git a/trunk/conf/console.conf b/trunk/conf/console.conf index 0c3c11bdb..064e71f17 100644 --- a/trunk/conf/console.conf +++ b/trunk/conf/console.conf @@ -19,6 +19,10 @@ rtc_server { # @see https://ossrs.io/lts/en-us/docs/v7/doc/webrtc#config-candidate candidate $CANDIDATE; } +rtsp_server { + enabled on; + listen 8554; +} vhost __defaultVhost__ { hls { enabled on; @@ -34,4 +38,8 @@ vhost __defaultVhost__ { # @see https://ossrs.io/lts/en-us/docs/v7/doc/webrtc#rtc-to-rtmp rtc_to_rtmp on; } + rtsp { + enabled on; + rtmp_to_rtsp on; + } } diff --git a/trunk/conf/full.conf b/trunk/conf/full.conf index 78ec9e261..cd7df4e7f 100644 --- a/trunk/conf/full.conf +++ b/trunk/conf/full.conf @@ -609,6 +609,33 @@ vhost rtc.vhost.srs.com { } } +############################################################################################# +# RTSP server sections +############################################################################################# +rtsp_server { + # Whether enable RTSP server. + # Overwrite by env SRS_RTSP_SERVER_ENABLED + # default: off + enabled on; + # The listen port for RTSP server. + # Overwrite by env SRS_RTSP_SERVER_LISTEN + # default: 554 + listen 8554; +} + +vhost rtsp.vhost.srs.com { + rtsp { + # Whether enable RTSP server. + # Overwrite by env SRS_VHOST_RTSP_ENABLED for all vhosts. + # default: off + enabled on; + # Whether transmux RTMP to RTSP. + # Overwrite by env SRS_VHOST_RTSP_RTMP_TO_RTSP for all vhosts. + # default: on + rtmp_to_rtsp on; + } +} + ############################################################################################# # Stream converter sections ############################################################################################# diff --git a/trunk/conf/regression-test-for-clion.conf b/trunk/conf/regression-test-for-clion.conf index 953fe5f83..dbd870f9c 100644 --- a/trunk/conf/regression-test-for-clion.conf +++ b/trunk/conf/regression-test-for-clion.conf @@ -37,6 +37,10 @@ rtc_server { listen 8000; candidate $CANDIDATE; } +rtsp_server { + enabled on; + listen 8554; +} vhost __defaultVhost__ { rtc { @@ -45,6 +49,10 @@ vhost __defaultVhost__ { keep_bframe off; rtc_to_rtmp on; } + rtsp { + enabled on; + rtmp_to_rtsp on; + } play { atc on; } diff --git a/trunk/conf/regression-test.conf b/trunk/conf/regression-test.conf index 87dac9255..61434b4ec 100644 --- a/trunk/conf/regression-test.conf +++ b/trunk/conf/regression-test.conf @@ -33,6 +33,10 @@ http_api { stats { network 0; } +rtsp_server { + enabled on; + listen 8554; +} rtc_server { enabled on; listen 8000; @@ -46,6 +50,10 @@ vhost __defaultVhost__ { keep_bframe off; rtc_to_rtmp on; } + rtsp { + enabled on; + rtmp_to_rtsp on; + } play { atc on; } diff --git a/trunk/conf/rtsp.conf b/trunk/conf/rtsp.conf new file mode 100644 index 000000000..d73fe0351 --- /dev/null +++ b/trunk/conf/rtsp.conf @@ -0,0 +1,52 @@ + +listen 1935; +max_connections 1000; +daemon off; +srs_log_tank console; + +http_server { + enabled on; + listen 8080; + dir ./objs/nginx/html; +} + +http_api { + enabled on; + listen 1985; +} +stats { + network 0; +} + +rtsp_server { + enabled on; + listen 8554; +} + +rtc_server { + enabled on; + listen 8000; # UDP port + # @see https://ossrs.io/lts/en-us/docs/v7/doc/webrtc#config-candidate + candidate $CANDIDATE; +} + +vhost __defaultVhost__ { + rtsp { + enabled on; + rtmp_to_rtsp on; + } + rtc { + enabled on; + # @see https://ossrs.io/lts/en-us/docs/v7/doc/webrtc#rtmp-to-rtc + rtmp_to_rtc on; + # @see https://ossrs.io/lts/en-us/docs/v7/doc/webrtc#rtc-to-rtmp + rtc_to_rtmp on; + # for rtsp + keep_bframe on; + } + http_remux { + enabled on; + mount [vhost]/[app]/[stream].flv; + } +} + diff --git a/trunk/configure b/trunk/configure index 7e59afbcd..cbe49c8f3 100755 --- a/trunk/configure +++ b/trunk/configure @@ -302,7 +302,10 @@ if [[ $SRS_SRT == YES ]]; then ModuleLibIncs+=(${LibSRTRoot}) fi if [[ $SRS_RTC == YES ]]; then - MODULE_FILES+=("srs_protocol_rtc_stun") + MODULE_FILES+=("srs_protocol_rtc_stun" "srs_protocol_rtp") +fi +if [[ $SRS_RTSP == YES ]]; then + MODULE_FILES+=("srs_protocol_rtsp_stack") fi PROTOCOL_INCS="src/protocol"; MODULE_DIR=${PROTOCOL_INCS} . $SRS_WORKDIR/auto/modules.sh PROTOCOL_OBJS="${MODULE_OBJS[@]}" @@ -339,6 +342,9 @@ if [[ $SRS_RTC == YES ]]; then MODULE_FILES+=("srs_app_rtc_conn" "srs_app_rtc_dtls" "srs_app_rtc_sdp" "srs_app_rtc_network" "srs_app_rtc_queue" "srs_app_rtc_server" "srs_app_rtc_source" "srs_app_rtc_api") fi +if [[ $SRS_RTSP == YES ]]; then + MODULE_FILES+=("srs_app_rtsp_source" "srs_app_rtsp_conn") +fi if [[ $SRS_APM == YES ]]; then MODULE_FILES+=("srs_app_tencentcloud") fi @@ -747,6 +753,11 @@ if [[ $SRS_RTC == YES ]]; then else echo -e "${GREEN}Warning: RTC is disabled.${BLACK}" fi +if [[ $SRS_RTSP == YES ]]; then + echo -e "${YELLOW}Experiment: RTSP is enabled (requires RTC).${BLACK}" +else + echo -e "${GREEN}Warning: RTSP is disabled.${BLACK}" +fi if [[ $SRS_HTTPS == YES ]]; then echo -e "${YELLOW}Experiment: HTTPS is enabled. https://github.com/ossrs/srs/issues/1657${BLACK}" else diff --git a/trunk/doc/CHANGELOG.md b/trunk/doc/CHANGELOG.md index 58c11e4f4..2a2ba9626 100644 --- a/trunk/doc/CHANGELOG.md +++ b/trunk/doc/CHANGELOG.md @@ -7,6 +7,7 @@ The changelog for SRS. ## SRS 7.0 Changelog +* v7.0, 2025-07-11, Merge [#4333](https://github.com/ossrs/srs/pull/4333): NEW PROTOCOL: Support viewing stream over RTSP. v7.0.47 (#4333) * v7.0, 2025-07-10, Merge [#4414](https://github.com/ossrs/srs/pull/4414): Fix H.264 B-frame detection logic to comply with specification. v7.0.46 (#4414) * v7.0, 2025-07-04, Merge [#4412](https://github.com/ossrs/srs/pull/4412): Refine code and add tests for #4289. v7.0.45 (#4412) * v7.0, 2025-07-04, Merge [#4413](https://github.com/ossrs/srs/pull/4413): RTMP2RTC: Support dual video track for bridge. v7.0.44 (#4413) diff --git a/trunk/ide/srs_clion/CMakeLists.txt b/trunk/ide/srs_clion/CMakeLists.txt index 6b55c6652..f5bf2ba70 100755 --- a/trunk/ide/srs_clion/CMakeLists.txt +++ b/trunk/ide/srs_clion/CMakeLists.txt @@ -27,11 +27,11 @@ ProcessorCount(JOBS) # We should always configure SRS for switching between branches. IF (${CMAKE_SYSTEM_NAME} MATCHES "Darwin") EXECUTE_PROCESS( - COMMAND ./configure --osx --srt=on --gb28181=on --apm=on --h265=on --hds=on --utest=on --ffmpeg-opus=off --jobs=${JOBS} + COMMAND ./configure --osx --srt=on --gb28181=on --rtsp=on --apm=on --h265=on --hds=on --utest=on --ffmpeg-opus=off --jobs=${JOBS} WORKING_DIRECTORY ${SRS_DIR} RESULT_VARIABLE ret) ELSE () EXECUTE_PROCESS( - COMMAND ./configure --srt=on --gb28181=on --apm=on --h265=on --hds=on --utest=on --ffmpeg-opus=off --jobs=${JOBS} + COMMAND ./configure --srt=on --gb28181=on --rtsp=on --apm=on --h265=on --hds=on --utest=on --ffmpeg-opus=off --jobs=${JOBS} WORKING_DIRECTORY ${SRS_DIR} RESULT_VARIABLE ret) ENDIF () if(NOT ret EQUAL 0) diff --git a/trunk/src/app/srs_app_config.cpp b/trunk/src/app/srs_app_config.cpp index d74f4b410..3f469876d 100644 --- a/trunk/src/app/srs_app_config.cpp +++ b/trunk/src/app/srs_app_config.cpp @@ -2348,7 +2348,7 @@ srs_error_t SrsConfig::check_normal_config() && n != "inotify_auto_reload" && n != "auto_reload_for_docker" && n != "tcmalloc_release_rate" && n != "query_latest_version" && n != "first_wait_for_qlv" && n != "threads" && n != "circuit_breaker" && n != "is_full" && n != "in_docker" && n != "tencentcloud_cls" - && n != "exporter" + && n != "exporter" && n != "rtsp_server" ) { return srs_error_new(ERROR_SYSTEM_CONFIG_INVALID, "illegal directive %s", n.c_str()); } @@ -2435,6 +2435,15 @@ srs_error_t SrsConfig::check_normal_config() } } } + if (true) { + SrsConfDirective* conf = root->get("rtsp_server"); + for (int i = 0; conf && i < (int)conf->directives.size(); i++) { + string n = conf->at(i)->name; + if (n != "enabled" && n != "listen") { + return srs_error_new(ERROR_SYSTEM_CONFIG_INVALID, "illegal rtsp_server.%s", n.c_str()); + } + } + } if (true) { SrsConfDirective* conf = root->get("exporter"); for (int i = 0; conf && i < (int)conf->directives.size(); i++) { @@ -2595,7 +2604,8 @@ srs_error_t SrsConfig::check_normal_config() && n != "play" && n != "publish" && n != "cluster" && n != "security" && n != "http_remux" && n != "dash" && n != "http_static" && n != "hds" && n != "exec" - && n != "in_ack_size" && n != "out_ack_size" && n != "rtc" && n != "srt") { + && n != "in_ack_size" && n != "out_ack_size" && n != "rtc" && n != "srt" + && n != "rtsp") { return srs_error_new(ERROR_SYSTEM_CONFIG_INVALID, "illegal vhost.%s", n.c_str()); } // for each sub directives of vhost. @@ -2755,6 +2765,13 @@ srs_error_t SrsConfig::check_normal_config() return srs_error_new(ERROR_SYSTEM_CONFIG_INVALID, "illegal vhost.srt.%s of %s", m.c_str(), vhost->arg0().c_str()); } } + } else if (n == "rtsp") { + for (int j = 0; j < (int)conf->directives.size(); j++) { + string m = conf->at(j)->name; + if (m != "enabled" && m != "rtmp_to_rtsp") { + return srs_error_new(ERROR_SYSTEM_CONFIG_INVALID, "illegal vhost.srt.%s of %s", m.c_str(), vhost->arg0().c_str()); + } + } } } } @@ -4069,6 +4086,96 @@ std::string SrsConfig::get_stream_caster_sip_candidate(SrsConfDirective* conf) return conf->arg0(); } +bool SrsConfig::get_rtsp_server_enabled() +{ + SrsConfDirective* conf = root->get("rtsp_server"); + return get_rtsp_server_enabled(conf); +} + +bool SrsConfig::get_rtsp_server_enabled(SrsConfDirective* conf) +{ + SRS_OVERWRITE_BY_ENV_BOOL("srs.rtsp_server.enabled"); // SRS_RTSP_SERVER_ENABLED + + static bool DEFAULT = false; + + if (!conf) { + return DEFAULT; + } + + conf = conf->get("enabled"); + if (!conf || conf->arg0().empty()) { + return DEFAULT; + } + + return SRS_CONF_PREFER_FALSE(conf->arg0()); +} + +int SrsConfig::get_rtsp_server_listen() +{ + SRS_OVERWRITE_BY_ENV_INT("srs.rtsp_server.listen"); // SRS_RTSP_SERVER_LISTEN + + SrsConfDirective* conf = root->get("rtsp_server"); + + static int DEFAULT = 554; + + if (!conf) { + return DEFAULT; + } + + conf = conf->get("listen"); + if (!conf || conf->arg0().empty()) { + return DEFAULT; + } + + return ::atoi(conf->arg0().c_str()); +} + +SrsConfDirective* SrsConfig::get_rtsp(string vhost) +{ + SrsConfDirective* conf = get_vhost(vhost); + return conf? conf->get("rtsp") : NULL; +} + +bool SrsConfig::get_rtsp_enabled(string vhost) +{ + SRS_OVERWRITE_BY_ENV_BOOL("srs.vhost.rtsp.enabled"); // SRS_VHOST_RTSP_ENABLED + + static bool DEFAULT = false; + + SrsConfDirective* conf = get_rtsp(vhost); + + if (!conf) { + return DEFAULT; + } + + conf = conf->get("enabled"); + if (!conf || conf->arg0().empty()) { + return DEFAULT; + } + + return SRS_CONF_PREFER_FALSE(conf->arg0()); +} + +bool SrsConfig::get_rtsp_from_rtmp(string vhost) +{ + SRS_OVERWRITE_BY_ENV_BOOL("srs.vhost.rtsp.rtmp_to_rtsp"); // SRS_VHOST_RTSP_RTMP_TO_RTSP + + static bool DEFAULT = true; + + SrsConfDirective* conf = get_rtsp(vhost); + + if (!conf) { + return DEFAULT; + } + + conf = conf->get("rtmp_to_rtsp"); + if (!conf || conf->arg0().empty()) { + return DEFAULT; + } + + return SRS_CONF_PREFER_TRUE(conf->arg0()); +} + bool SrsConfig::get_rtc_server_enabled() { SrsConfDirective* conf = root->get("rtc_server"); diff --git a/trunk/src/app/srs_app_config.hpp b/trunk/src/app/srs_app_config.hpp index 9bbe52a1e..74e7617a0 100644 --- a/trunk/src/app/srs_app_config.hpp +++ b/trunk/src/app/srs_app_config.hpp @@ -508,6 +508,16 @@ public: virtual srs_utime_t get_stream_caster_sip_reinvite(SrsConfDirective* conf); // Get the candidate for SDP. virtual std::string get_stream_caster_sip_candidate(SrsConfDirective* conf); +// rtsp section +public: + virtual bool get_rtsp_server_enabled(); + virtual bool get_rtsp_server_enabled(SrsConfDirective* conf); + virtual int get_rtsp_server_listen(); +public: + SrsConfDirective* get_rtsp(std::string vhost); + bool get_rtsp_enabled(std::string vhost); + bool get_rtsp_from_rtmp(std::string vhost); + // rtc section public: virtual bool get_rtc_server_enabled(); diff --git a/trunk/src/app/srs_app_heartbeat.cpp b/trunk/src/app/srs_app_heartbeat.cpp index f7548573d..73428244d 100644 --- a/trunk/src/app/srs_app_heartbeat.cpp +++ b/trunk/src/app/srs_app_heartbeat.cpp @@ -119,6 +119,15 @@ srs_error_t SrsHttpHeartbeat::do_heartbeat() o->append(SrsJsonAny::str(srs_fmt("udp://0.0.0.0:%d", endpoint).c_str())); } + // For RTSP listen endpoints. + if (_srs_config->get_rtsp_server_enabled()) { + SrsJsonArray* o = SrsJsonAny::array(); + obj->set("rtsp", o); + + int endpoint = _srs_config->get_rtsp_server_listen(); + o->append(SrsJsonAny::str(srs_fmt("rtsp://0.0.0.0:%d", endpoint).c_str())); + } + // For WebRTC listen endpoints. if (_srs_config->get_rtc_server_enabled()) { SrsJsonArray* o = SrsJsonAny::array(); diff --git a/trunk/src/app/srs_app_latest_version.cpp b/trunk/src/app/srs_app_latest_version.cpp index 20dc4adec..f06534df6 100644 --- a/trunk/src/app/srs_app_latest_version.cpp +++ b/trunk/src/app/srs_app_latest_version.cpp @@ -63,6 +63,7 @@ void srs_build_features(stringstream& ss) SRS_CHECK_FEATURE3(!string(SRS_PACKAGER).empty(), "packager", SRS_PACKAGER, ss); SRS_CHECK_FEATURE2(SRS_CROSSBUILD_BOOL, "cross", ss); + SRS_CHECK_FEATURE2(SRS_RTC_BOOL && _srs_config->get_rtsp_server_enabled(), "rtsp", ss); SRS_CHECK_FEATURE2(SRS_RTC_BOOL && _srs_config->get_rtc_server_enabled(), "rtc", ss); SRS_CHECK_FEATURE2(SRS_SRT_BOOL && _srs_config->get_srt_enabled(), "srt", ss); SRS_CHECK_FEATURE2(_srs_config->get_http_api_enabled(), "api", ss); diff --git a/trunk/src/app/srs_app_rtc_sdp.cpp b/trunk/src/app/srs_app_rtc_sdp.cpp index 4d8028a81..256deef76 100644 --- a/trunk/src/app/srs_app_rtc_sdp.cpp +++ b/trunk/src/app/srs_app_rtc_sdp.cpp @@ -470,6 +470,10 @@ srs_error_t SrsMediaDesc::encode(std::ostringstream& os) os << "a=rtcp-rsize" << kCRLF; } + if (!control_.empty()) { + os << "a=control:" << control_ << kCRLF; + } + for (std::vector::iterator iter = payload_types_.begin(); iter != payload_types_.end(); ++iter) { if ((err = iter->encode(os)) != srs_success) { return srs_error_wrap(err, "encode media payload failed"); @@ -896,6 +900,10 @@ srs_error_t SrsSdp::encode(std::ostringstream& os) return srs_error_wrap(err, "encode session info failed"); } + if (!control_.empty()) { + os << "a=control:" << control_ << kCRLF; + } + for (std::vector::iterator iter = media_descs_.begin(); iter != media_descs_.end(); ++iter) { if ((err = (*iter).encode(os)) != srs_success) { return srs_error_wrap(err, "encode media description failed"); diff --git a/trunk/src/app/srs_app_rtc_sdp.hpp b/trunk/src/app/srs_app_rtc_sdp.hpp index 261555124..994a4237a 100644 --- a/trunk/src/app/srs_app_rtc_sdp.hpp +++ b/trunk/src/app/srs_app_rtc_sdp.hpp @@ -175,6 +175,10 @@ public: bool sendrecv_; bool inactive_; + // Control URL, ONLY for RTSP, media control. + // @see rfc2326-1998-rtsp.pdf, page 159 + std::string control_; + std::string mid_; std::string msid_; std::string msid_tracker_; @@ -255,6 +259,10 @@ public: std::string msid_semantic_; std::vector msids_; + // Control URL, ONLY for RTSP. + // @see rfc2326-1998-rtsp.pdf, page 159 + std::string control_; + // m-line, media sessions std::vector media_descs_; diff --git a/trunk/src/app/srs_app_rtc_source.cpp b/trunk/src/app/srs_app_rtc_source.cpp index 82cc40fa8..99ba4c512 100644 --- a/trunk/src/app/srs_app_rtc_source.cpp +++ b/trunk/src/app/srs_app_rtc_source.cpp @@ -37,6 +37,7 @@ #include #include +#include // The NACK sent by us(SFU). SrsPps* _srs_pps_snack = NULL; @@ -53,25 +54,13 @@ SrsPps* _srs_pps_rmnack = NULL; extern SrsPps* _srs_pps_aloss2; -const int kAudioChannel = 2; -const int kAudioSamplerate = 48000; +static const int kAudioChannel = 2; +static const int kAudioSamplerate = 48000; -const int kVideoSamplerate = 90000; +static const int kVideoSamplerate = 90000; using namespace std; -#ifdef SRS_FFMPEG_FIT -// The RTP payload max size, reserved some paddings for SRTP as such: -// kRtpPacketSize = kRtpMaxPayloadSize + paddings -// For example, if kRtpPacketSize is 1500, recommend to set kRtpMaxPayloadSize to 1400, -// which reserves 100 bytes for SRTP or paddings. -// otherwise, the kRtpPacketSize must less than MTU, in webrtc source code, -// the rtp max size is assigned by kVideoMtu = 1200. -// so we set kRtpMaxPayloadSize = 1200. -// see @doc https://groups.google.com/g/discuss-webrtc/c/gH5ysR3SoZI -const int kRtpMaxPayloadSize = kRtpPacketSize - 300; -#endif - // the time to cleanup source. #define SRS_RTC_SOURCE_CLEANUP (3 * SRS_UTIME_SECONDS) @@ -873,13 +862,11 @@ SrsRtcRtpBuilder::SrsRtcRtpBuilder(SrsFrameToRtcBridge* bridge, SrsSharedPtr descs = source_->get_track_desc("video", codec_name); + uint32_t video_ssrc = 0; + uint8_t video_payload_type = 0; if (!descs.empty()) { // Note we must use the PT of source, see https://github.com/ossrs/srs/pull/3079 SrsRtcTrackDescription* track = descs.at(0); - video_ssrc_ = track->ssrc_; - video_payload_type_ = track->media_->pt_; + video_ssrc = track->ssrc_; + video_payload_type = track->media_->pt_; } else { - video_payload_type_ = kVideoPayloadType; + video_payload_type = kVideoPayloadType; + } + + SrsFormat* format = meta->vsh_format(); + if ((err = video_builder_->initialize(format, video_ssrc, video_payload_type)) != srs_success) { + return srs_error_wrap(err, "initialize video builder"); } srs_trace("RTMP2RTC: Initialize video track with codec=%s, ssrc=%u, pt=%d", - codec_name.c_str(), video_ssrc_, video_payload_type_); + codec_name.c_str(), video_ssrc, video_payload_type); return err; } @@ -1299,63 +1294,7 @@ srs_error_t SrsRtcRtpBuilder::package_stap_a(SrsSharedPtrMessage* msg, SrsRtpPac return err; } - pkt->header.set_payload_type(video_payload_type_); - pkt->header.set_ssrc(video_ssrc_); - pkt->frame_type = SrsFrameTypeVideo; - pkt->header.set_marker(false); - pkt->header.set_sequence(video_sequence++); - pkt->header.set_timestamp(msg->timestamp * 90); - - ISrsRtpPayloader* stap = NULL; - vector*> params; - int size = 0; - - if (format->vcodec->id == SrsVideoCodecIdHEVC) { - for (size_t i = 0; i < format->vcodec->hevc_dec_conf_record_.nalu_vec.size(); i++) { - const SrsHevcHvccNalu& nalu = format->vcodec->hevc_dec_conf_record_.nalu_vec[i]; - if (nalu.nal_unit_type == SrsHevcNaluType_VPS - || nalu.nal_unit_type == SrsHevcNaluType_SPS - || nalu.nal_unit_type == SrsHevcNaluType_PPS) { - const SrsHevcNalData& nal_data = nalu.nal_data_vec[0]; - params.push_back(&(vector&)nal_data.nal_unit_data); - size += nal_data.nal_unit_length; - } - } - - stap = new SrsRtpSTAPPayloadHevc(); - pkt->set_payload(stap, SrsRtpPacketPayloadTypeSTAPHevc); - pkt->nalu_type = kStapHevc; - } else if (format->vcodec->id == SrsVideoCodecIdAVC) { - params.push_back(&format->vcodec->sequenceParameterSetNALUnit); - params.push_back(&format->vcodec->pictureParameterSetNALUnit); - size = format->vcodec->sequenceParameterSetNALUnit.size() + format->vcodec->pictureParameterSetNALUnit.size(); - - stap = new SrsRtpSTAPPayload(); - pkt->set_payload(stap, SrsRtpPacketPayloadTypeSTAP); - pkt->nalu_type = kStapA; - } - - if (size == 0) { - return srs_error_new(ERROR_RTC_RTP_MUXER, "vps/sps/pps empty"); - } - char* payload = pkt->wrap(size); - - for (vector*>::iterator it = params.begin(); it != params.end(); ++it) { - vector* param = *it; - SrsSample* sample = new SrsSample(); - sample->bytes = payload; - sample->size = param->size(); - if (format->vcodec->id == SrsVideoCodecIdHEVC) { - static_cast(stap)->nalus.push_back(sample); - } else { - static_cast(stap)->nalus.push_back(sample); - } - - memcpy(payload, (char*)param->data(), param->size()); - payload += (int)param->size(); - } - - return err; + return video_builder_->package_stap_a(msg, pkt); } srs_error_t SrsRtcRtpBuilder::package_nalus(SrsSharedPtrMessage* msg, const vector& samples, vector& pkts) @@ -1366,130 +1305,14 @@ srs_error_t SrsRtcRtpBuilder::package_nalus(SrsSharedPtrMessage* msg, const vect if (!format || !format->vcodec) { return err; } - bool is_hevc = format->vcodec->id == SrsVideoCodecIdHEVC; - - SrsRtpRawNALUs* raw_raw = new SrsRtpRawNALUs(); - uint8_t first_nalu_type = 0; - - for (int i = 0; i < (int)samples.size(); i++) { - SrsSample* sample = samples[i]; - - if (!sample->size) { - continue; - } - - if (first_nalu_type == 0) { - first_nalu_type = is_hevc ? uint8_t(SrsHevcNaluTypeParse(sample->bytes[0])) : uint8_t(SrsAvcNaluTypeParse(sample->bytes[0])); - } - - raw_raw->push_back(sample->copy()); - } - - // Ignore empty. - int nn_bytes = raw_raw->nb_bytes(); - if (nn_bytes <= 0) { - srs_freep(raw_raw); - return err; - } - - if (nn_bytes < kRtpMaxPayloadSize) { - // Package NALUs in a single RTP packet. - SrsRtpPacket* pkt = new SrsRtpPacket(); - pkts.push_back(pkt); - - pkt->header.set_payload_type(video_payload_type_); - pkt->header.set_ssrc(video_ssrc_); - pkt->frame_type = SrsFrameTypeVideo; - pkt->nalu_type = first_nalu_type; - pkt->header.set_sequence(video_sequence++); - pkt->header.set_timestamp(msg->timestamp * 90); - pkt->set_payload(raw_raw, SrsRtpPacketPayloadTypeNALU); - pkt->wrap(msg); - } else { - // We must free it, should never use RTP packets to free it, - // because more than one RTP packet will refer to it. - SrsUniquePtr raw(raw_raw); - - int header_size = is_hevc ? SrsHevcNaluHeaderSize : SrsAvcNaluHeaderSize; - - // Package NALUs in FU-A RTP packets. - int fu_payload_size = kRtpMaxPayloadSize; - - // The first byte is store in FU-A header. - uint8_t header = raw->skip_bytes(header_size); - - int nb_left = nn_bytes - header_size; - - int num_of_packet = 1 + (nn_bytes - 1) / fu_payload_size; - for (int i = 0; i < num_of_packet; ++i) { - int packet_size = srs_min(nb_left, fu_payload_size); - - SrsRtpPacket* pkt = new SrsRtpPacket(); - pkts.push_back(pkt); - - pkt->header.set_payload_type(video_payload_type_); - pkt->header.set_ssrc(video_ssrc_); - pkt->frame_type = SrsFrameTypeVideo; - pkt->nalu_type = kFuA; - pkt->header.set_sequence(video_sequence++); - pkt->header.set_timestamp(msg->timestamp * 90); - - if (is_hevc) { - SrsRtpFUAPayloadHevc* fua = new SrsRtpFUAPayloadHevc(); - if ((err = raw->read_samples(fua->nalus, packet_size)) != srs_success) { - srs_freep(fua); - return srs_error_wrap(err, "read hevc samples %d bytes, left %d, total %d", packet_size, nb_left, nn_bytes); - } - fua->nalu_type = SrsHevcNaluTypeParse(header); - fua->start = bool(i == 0); - fua->end = bool(i == num_of_packet - 1); - - pkt->set_payload(fua, SrsRtpPacketPayloadTypeFUAHevc); - } else { - SrsRtpFUAPayload* fua = new SrsRtpFUAPayload(); - if ((err = raw->read_samples(fua->nalus, packet_size)) != srs_success) { - srs_freep(fua); - return srs_error_wrap(err, "read samples %d bytes, left %d, total %d", packet_size, nb_left, nn_bytes); - } - fua->nalu_type = SrsAvcNaluTypeParse(header); - fua->start = bool(i == 0); - fua->end = bool(i == num_of_packet - 1); - - pkt->set_payload(fua, SrsRtpPacketPayloadTypeFUA); - } - - pkt->wrap(msg); - - nb_left -= packet_size; - } - } - - return err; + + return video_builder_->package_nalus(msg, samples, pkts); } // Single NAL Unit Packet @see https://tools.ietf.org/html/rfc6184#section-5.6 srs_error_t SrsRtcRtpBuilder::package_single_nalu(SrsSharedPtrMessage* msg, SrsSample* sample, vector& pkts) { - srs_error_t err = srs_success; - - SrsRtpPacket* pkt = new SrsRtpPacket(); - pkts.push_back(pkt); - - pkt->header.set_payload_type(video_payload_type_); - pkt->header.set_ssrc(video_ssrc_); - pkt->frame_type = SrsFrameTypeVideo; - pkt->header.set_sequence(video_sequence++); - pkt->header.set_timestamp(msg->timestamp * 90); - - SrsRtpRawPayload* raw = new SrsRtpRawPayload(); - pkt->set_payload(raw, SrsRtpPacketPayloadTypeRaw); - - raw->payload = sample->bytes; - raw->nn_payload = sample->size; - - pkt->wrap(msg); - - return err; + return video_builder_->package_single_nalu(msg, sample, pkts); } srs_error_t SrsRtcRtpBuilder::package_fu_a(SrsSharedPtrMessage* msg, SrsSample* sample, int fu_payload_size, vector& pkts) @@ -1501,60 +1324,7 @@ srs_error_t SrsRtcRtpBuilder::package_fu_a(SrsSharedPtrMessage* msg, SrsSample* return err; } - bool is_hevc = format->vcodec->id == SrsVideoCodecIdHEVC; - int header_size = is_hevc ? SrsHevcNaluHeaderSize : SrsAvcNaluHeaderSize; - srs_assert(sample->size >= header_size); - - char* p = sample->bytes + header_size; - int nb_left = sample->size - header_size; - uint8_t header = sample->bytes[0]; - - int num_of_packet = 1 + (nb_left - 1) / fu_payload_size; - for (int i = 0; i < num_of_packet; ++i) { - int packet_size = srs_min(nb_left, fu_payload_size); - - SrsRtpPacket* pkt = new SrsRtpPacket(); - pkts.push_back(pkt); - - pkt->header.set_payload_type(video_payload_type_); - pkt->header.set_ssrc(video_ssrc_); - pkt->frame_type = SrsFrameTypeVideo; - pkt->header.set_sequence(video_sequence++); - pkt->header.set_timestamp(msg->timestamp * 90); - pkt->nalu_type = is_hevc ? kFuHevc : kFuA; - - if (is_hevc) { - // H265 FU-A header - SrsRtpFUAPayloadHevc2* fua = new SrsRtpFUAPayloadHevc2(); - pkt->set_payload(fua, SrsRtpPacketPayloadTypeFUAHevc2); - - fua->nalu_type = SrsHevcNaluTypeParse(header); - fua->start = bool(i == 0); - fua->end = bool(i == num_of_packet - 1); - - fua->payload = p; - fua->size = packet_size; - } else { - // H264 FU-A header - SrsRtpFUAPayload2* fua = new SrsRtpFUAPayload2(); - pkt->set_payload(fua, SrsRtpPacketPayloadTypeFUA2); - - fua->nri = (SrsAvcNaluType)header; - fua->nalu_type = SrsAvcNaluTypeParse(header); - fua->start = bool(i == 0); - fua->end = bool(i == num_of_packet - 1); - - fua->payload = p; - fua->size = packet_size; - } - - pkt->wrap(msg); - - p += packet_size; - nb_left -= packet_size; - } - - return err; + return video_builder_->package_fu_a(msg, sample, fu_payload_size, pkts); } srs_error_t SrsRtcRtpBuilder::consume_packets(vector& pkts) @@ -2668,11 +2438,11 @@ SrsMediaPayloadType SrsVideoPayload::generate_media_payload_type_h265() return media_payload_type; } +// level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f srs_error_t SrsVideoPayload::set_h264_param_desc(std::string fmtp) { srs_error_t err = srs_success; - // For example: level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f std::vector attributes = split_str(fmtp, ";"); for (size_t i = 0; i < attributes.size(); ++i) { @@ -2762,6 +2532,7 @@ SrsAudioPayload* SrsAudioPayload::copy() cp->rtcp_fbs_ = rtcp_fbs_; cp->channel_ = channel_; cp->opus_param_ = opus_param_; + cp->aac_config_hex_ = aac_config_hex_; return cp; } diff --git a/trunk/src/app/srs_app_rtc_source.hpp b/trunk/src/app/srs_app_rtc_source.hpp index 535d59ef9..32056c452 100644 --- a/trunk/src/app/srs_app_rtc_source.hpp +++ b/trunk/src/app/srs_app_rtc_source.hpp @@ -43,6 +43,7 @@ class SrsJsonObject; class SrsErrorPithyPrint; class SrsRtcFrameBuilder; class SrsLiveSource; +class SrsRtpVideoBuilder; // Firefox defaults as 109, Chrome is 111. const int kAudioPayloadType = 111; @@ -276,6 +277,8 @@ private: SrsRtmpFormat* format; // The metadata cache. SrsMetaCache* meta; + // The video builder, convert frame to RTP packets. + SrsRtpVideoBuilder* video_builder_; private: SrsAudioCodecId latest_codec_; SrsAudioTranscoder* codec_; @@ -283,12 +286,9 @@ private: bool keep_avc_nalu_sei; bool merge_nalus; uint16_t audio_sequence; - uint16_t video_sequence; private: uint32_t audio_ssrc_; - uint32_t video_ssrc_; uint8_t audio_payload_type_; - uint8_t video_payload_type_; private: SrsSharedPtr source_; // Lazy initialization flags @@ -498,6 +498,8 @@ class SrsAudioPayload : public SrsCodecPayload public: int channel_; SrsOpusParameter opus_param_; + // AAC configuration hex string for SDP fmtp line + std::string aac_config_hex_; public: SrsAudioPayload(); SrsAudioPayload(uint8_t pt, std::string encode_name, int sample, int channel); diff --git a/trunk/src/app/srs_app_rtmp_conn.cpp b/trunk/src/app/srs_app_rtmp_conn.cpp index b49b3fc6a..434427c81 100644 --- a/trunk/src/app/srs_app_rtmp_conn.cpp +++ b/trunk/src/app/srs_app_rtmp_conn.cpp @@ -40,6 +40,9 @@ using namespace std; #include #include #include +#ifdef SRS_RTSP +#include +#endif // the timeout in srs_utime_t to wait encoder to republish // if timeout, close the connection. @@ -1110,21 +1113,43 @@ srs_error_t SrsRtmpConn::acquire_publish(SrsSharedPtr source) } #endif - // Bridge to RTC streaming. -#if defined(SRS_RTC) && defined(SRS_FFMPEG_FIT) - if (rtc.get() && _srs_config->get_rtc_from_rtmp(req->vhost)) { - SrsCompositeBridge* bridge = new SrsCompositeBridge(); - bridge->append(new SrsFrameToRtcBridge(rtc)); - - if ((err = bridge->initialize(req)) != srs_success) { - srs_freep(bridge); - return srs_error_wrap(err, "bridge init"); +#ifdef SRS_RTSP + // RTSP only support viewer, so we don't need to check it. + SrsSharedPtr rtsp; + bool rtsp_server_enabled = _srs_config->get_rtsp_server_enabled(); + bool rtsp_enabled = _srs_config->get_rtsp_enabled(req->vhost); + if (rtsp_server_enabled && rtsp_enabled && !info->edge) { + if ((err = _srs_rtsp_sources->fetch_or_create(req, rtsp)) != srs_success) { + return srs_error_wrap(err, "create source"); } - - source->set_bridge(bridge); } #endif + // Bridge to RTC streaming. + // TODO: FIXME: Need to convert RTMP to SRT. + SrsCompositeBridge* bridge = new SrsCompositeBridge(); + +#if defined(SRS_RTC) && defined(SRS_FFMPEG_FIT) + if (rtc.get() && _srs_config->get_rtc_from_rtmp(req->vhost)) { + bridge->append(new SrsFrameToRtcBridge(rtc)); + } +#endif + +#ifdef SRS_RTSP + if (rtsp.get() && _srs_config->get_rtsp_from_rtmp(req->vhost)) { + bridge->append(new SrsFrameToRtspBridge(rtsp)); + } +#endif + + if (bridge->empty()) { + srs_freep(bridge); + } else if ((err = bridge->initialize(req)) != srs_success) { + srs_freep(bridge); + return srs_error_wrap(err, "bridge init"); + } + + source->set_bridge(bridge); + // Start publisher now. if (info->edge) { err = source->on_edge_start_publish(); diff --git a/trunk/src/app/srs_app_rtsp_conn.cpp b/trunk/src/app/srs_app_rtsp_conn.cpp new file mode 100644 index 000000000..393e95976 --- /dev/null +++ b/trunk/src/app/srs_app_rtsp_conn.cpp @@ -0,0 +1,934 @@ +// +// Copyright (c) 2013-2025 The SRS Authors +// +// SPDX-License-Identifier: MIT +// + +#include + +using namespace std; + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +extern SrsPps* _srs_pps_snack; +extern SrsPps* _srs_pps_snack2; +extern SrsPps* _srs_pps_snack3; +extern SrsPps* _srs_pps_snack4; + +extern SrsPps* _srs_pps_rnack; +extern SrsPps* _srs_pps_rnack2; + +extern SrsPps* _srs_pps_pub; +extern SrsPps* _srs_pps_conn; + +SrsRtspPlayStream::SrsRtspPlayStream(SrsRtspConnection* s, const SrsContextId& cid) : source_(new SrsRtspSource()) +{ + cid_ = cid; + trd_ = NULL; + + req_ = NULL; + + is_started = false; + session_ = s; + + cache_ssrc0_ = cache_ssrc1_ = cache_ssrc2_ = 0; + cache_track0_ = cache_track1_ = cache_track2_ = NULL; +} + +SrsRtspPlayStream::~SrsRtspPlayStream() +{ + srs_freep(trd_); + srs_freep(req_); + + if (true) { + std::map::iterator it; + for (it = audio_tracks_.begin(); it != audio_tracks_.end(); ++it) { + srs_freep(it->second); + } + } + + if (true) { + std::map::iterator it; + for (it = video_tracks_.begin(); it != video_tracks_.end(); ++it) { + srs_freep(it->second); + } + } + + // update the statistic when client coveried. + SrsStatistic* stat = SrsStatistic::instance(); + // TODO: FIXME: Should finger out the err. + stat->on_disconnect(cid_.c_str(), srs_success); +} + +srs_error_t SrsRtspPlayStream::initialize(SrsRequest* req, std::map sub_relations) +{ + srs_error_t err = srs_success; + + req_ = req->copy(); + + // We must do stat the client before hooks, because hooks depends on it. + SrsStatistic* stat = SrsStatistic::instance(); + if ((err = stat->on_client(cid_.c_str(), req_, session_, SrsRtcConnPlay)) != srs_success) { + return srs_error_wrap(err, "RTSP: stat client"); + } + + if ((err = _srs_rtsp_sources->fetch_or_create(req_, source_)) != srs_success) { + return srs_error_wrap(err, "RTSP: fetch source failed"); + } + + for (map::iterator it = sub_relations.begin(); it != sub_relations.end(); ++it) { + uint32_t ssrc = it->first; + SrsRtcTrackDescription* desc = it->second; + + if (desc->type_ == "audio") { + SrsRtspAudioSendTrack* track = new SrsRtspAudioSendTrack(session_, desc); + audio_tracks_.insert(make_pair(ssrc, track)); + } + + if (desc->type_ == "video") { + SrsRtspVideoSendTrack* track = new SrsRtspVideoSendTrack(session_, desc); + video_tracks_.insert(make_pair(ssrc, track)); + } + } + + return err; +} + +// TODO: Remove it for RTSP? +void SrsRtspPlayStream::on_stream_change(SrsRtcSourceDescription* desc) +{ + if (!desc) return; + + // Refresh the relation for audio. + // TODO: FIXME: Match by label? + if (desc && desc->audio_track_desc_ && audio_tracks_.size() == 1) { + if (!audio_tracks_.empty()) { + uint32_t ssrc = desc->audio_track_desc_->ssrc_; + SrsRtspAudioSendTrack* track = audio_tracks_.begin()->second; + + if (track->track_desc_->media_->pt_of_publisher_ != desc->audio_track_desc_->media_->pt_) { + track->track_desc_->media_->pt_of_publisher_ = desc->audio_track_desc_->media_->pt_; + } + + if (desc->audio_track_desc_->red_ && track->track_desc_->red_ && + track->track_desc_->red_->pt_of_publisher_ != desc->audio_track_desc_->red_->pt_) { + track->track_desc_->red_->pt_of_publisher_ = desc->audio_track_desc_->red_->pt_; + } + + audio_tracks_.clear(); + audio_tracks_.insert(make_pair(ssrc, track)); + } + } + + // Refresh the relation for video. + // TODO: FIMXE: Match by label? + if (desc && desc->video_track_descs_.size() == 1) { + if (!video_tracks_.empty()) { + SrsRtcTrackDescription* vdesc = desc->video_track_descs_.at(0); + uint32_t ssrc = vdesc->ssrc_; + SrsRtspVideoSendTrack* track = video_tracks_.begin()->second; + + if (track->track_desc_->media_->pt_of_publisher_ != vdesc->media_->pt_) { + track->track_desc_->media_->pt_of_publisher_ = vdesc->media_->pt_; + } + + if (vdesc->red_ && track->track_desc_->red_ && + track->track_desc_->red_->pt_of_publisher_ != vdesc->red_->pt_) { + track->track_desc_->red_->pt_of_publisher_ = vdesc->red_->pt_; + } + + video_tracks_.clear(); + video_tracks_.insert(make_pair(ssrc, track)); + } + } +} + +const SrsContextId& SrsRtspPlayStream::context_id() +{ + return cid_; +} + +srs_error_t SrsRtspPlayStream::start() +{ + srs_error_t err = srs_success; + + // If player coroutine allocated, we think the player is started. + // To prevent play multiple times for this play stream. + // @remark Allow start multiple times, for DTLS may retransmit the final packet. + if (is_started) { + return err; + } + + srs_freep(trd_); + trd_ = new SrsFastCoroutine("rtsp_sender", this, cid_); + + if ((err = trd_->start()) != srs_success) { + return srs_error_wrap(err, "rtsp_sender"); + } + + is_started = true; + + return err; +} + +void SrsRtspPlayStream::stop() +{ + if (trd_) { + trd_->stop(); + } +} + +srs_error_t SrsRtspPlayStream::cycle() +{ + srs_error_t err = srs_success; + + SrsSharedPtr& source = source_; + srs_assert(source.get()); + + SrsRtspConsumer* consumer_raw = NULL; + if ((err = source->create_consumer(consumer_raw)) != srs_success) { + return srs_error_wrap(err, "create consumer, source=%s", req_->get_stream_url().c_str()); + } + + srs_assert(consumer_raw); + SrsUniquePtr consumer(consumer_raw); + + consumer->set_handler(this); + + // TODO: FIXME: Dumps the SPS/PPS from gop cache, without other frames. + if ((err = source->consumer_dumps(consumer.get())) != srs_success) { + return srs_error_wrap(err, "dumps consumer, url=%s", req_->get_stream_url().c_str()); + } + + // TODO: FIXME: Add cost in ms. + SrsContextId cid = source->source_id(); + srs_trace("RTSP: start play url=%s, source_id=%s/%s", req_->get_stream_url().c_str(), + cid.c_str(), source->pre_source_id().c_str()); + + SrsUniquePtr epp(new SrsErrorPithyPrint()); + + // For RTSP, donot use merged write. + int mw_msgs = 1; + while (true) { + if ((err = trd_->pull()) != srs_success) { + return srs_error_wrap(err, "RTSP sender thread"); + } + + // Wait for amount of packets. + SrsRtpPacket* pkt = NULL; + consumer->dump_packet(&pkt); + if (!pkt) { + // TODO: FIXME: We should check the quit event. + consumer->wait(mw_msgs); + continue; + } + + // Send-out the RTP packet and do cleanup + // @remark Note that the pkt might be set to NULL. + if ((err = send_packet(pkt)) != srs_success) { + uint32_t nn = 0; + if (epp->can_print(err, &nn)) { + srs_warn("play send packets=%u, nn=%u/%u, err: %s", 1, epp->nn_count, nn, srs_error_desc(err).c_str()); + } + srs_freep(err); + } + + // Free the packet. + // @remark Note that the pkt might be set to NULL. + srs_freep(pkt); + } +} + +srs_error_t SrsRtspPlayStream::send_packet(SrsRtpPacket*& pkt) +{ + srs_error_t err = srs_success; + + uint32_t ssrc = pkt->header.get_ssrc(); + + // Try to find track from cache. + SrsRtspSendTrack* track = NULL; + if (cache_ssrc0_ == ssrc) { + track = cache_track0_; + } else if (cache_ssrc1_ == ssrc) { + track = cache_track1_; + } else if (cache_ssrc2_ == ssrc) { + track = cache_track2_; + } + + // Find by original tracks and build fast cache. + if (!track) { + if (pkt->is_audio()) { + map::iterator it = audio_tracks_.find(ssrc); + if (it != audio_tracks_.end()) { + track = it->second; + } + } else { + map::iterator it = video_tracks_.find(ssrc); + if (it != video_tracks_.end()) { + track = it->second; + } + } + + if (track && !cache_ssrc2_) { + if (!cache_ssrc0_) { + cache_ssrc0_ = ssrc; + cache_track0_ = track; + } else if (!cache_ssrc1_) { + cache_ssrc1_ = ssrc; + cache_track1_ = track; + } else if (!cache_ssrc2_) { + cache_ssrc2_ = ssrc; + cache_track2_ = track; + } + } + } + + // Ignore if no track found. + if (!track) { + srs_warn("RTSP: Drop for ssrc %u not found", ssrc); + return err; + } + + // Consume packet by track. + if ((err = track->on_rtp(pkt)) != srs_success) { + return srs_error_wrap(err, "audio track, SSRC=%u, SEQ=%u", ssrc, pkt->header.get_sequence()); + } + + return err; +} + +void SrsRtspPlayStream::set_all_tracks_status(bool status) +{ + std::ostringstream merged_log; + + // set video track status + if (true) { + std::map::iterator it; + for (it = video_tracks_.begin(); it != video_tracks_.end(); ++it) { + SrsRtspVideoSendTrack* track = it->second; + + bool previous = track->set_track_status(status); + merged_log << "{track: " << track->get_track_id() << ", is_active: " << previous << "=>" << status << "},"; + } + } + + // set audio track status + if (true) { + std::map::iterator it; + for (it = audio_tracks_.begin(); it != audio_tracks_.end(); ++it) { + SrsRtspAudioSendTrack* track = it->second; + + bool previous = track->set_track_status(status); + merged_log << "{track: " << track->get_track_id() << ", is_active: " << previous << "=>" << status << "},"; + } + } + + srs_trace("RTSP: Init tracks %s ok", merged_log.str().c_str()); +} + +SrsRtspConnection::SrsRtspConnection(ISrsResourceManager* cm, ISrsProtocolReadWriter* skt, std::string cip, int port) +{ + manager_ = cm; + cid_ = _srs_context->generate_id(); + _srs_context->set_id(cid_); + + // Initialize timeout management fields from SrsRtspConnection2 + last_stun_time = 0; + session_timeout = 0; + disposing_ = false; + + request_ = new SrsRequest(); + request_->ip = cip; + ip_ = cip; + port_ = port; + rtsp_ = new SrsRtspStack(skt); + trd_ = new SrsSTCoroutine("rtsp", this, _srs_context->get_id()); + + // Initialize merged SrsRtspSession members + skt_ = skt; + source_ = NULL; + player_ = NULL; + + cache_iov_ = new iovec(); + cache_iov_->iov_base = new char[kRtpPacketSize]; + cache_iov_->iov_len = kRtpPacketSize; + cache_buffer_ = new SrsBuffer((char*)cache_iov_->iov_base, kRtpPacketSize); + + delta_ = new SrsEphemeralDelta(); + security_ = new SrsSecurity(); + + _srs_rtsp_manager->subscribe(this); +} + +SrsRtspConnection::~SrsRtspConnection() +{ + _srs_rtsp_manager->unsubscribe(this); + + srs_freep(request_); + srs_freep(rtsp_); + srs_freep(trd_); + + // Cleanup merged SrsRtspSession members + for (std::map::iterator it = tracks_.begin(); it != tracks_.end(); ++it) { + srs_freep(it->second); + } + tracks_.clear(); + + for (std::map::iterator it = networks_.begin(); it != networks_.end(); ++it) { + srs_freep(it->second); + } + networks_.clear(); + + srs_freep(delta_); + srs_freep(security_); + srs_freep(player_); + + if (true) { + char* iov_base = (char*)cache_iov_->iov_base; + srs_freepa(iov_base); + srs_freep(cache_iov_); + } + srs_freep(cache_buffer_); +} + +srs_error_t SrsRtspConnection::do_send_packet(SrsRtpPacket* pkt) +{ + srs_error_t err = srs_success; + + uint32_t ssrc = pkt->header.get_ssrc(); + ISrsStreamWriter* network = networks_[ssrc]; + if (!network) { + return srs_error_new(ERROR_RTSP_NO_TRACK, "network not found for ssrc: %u", ssrc); + } + + iovec* iov = cache_iov_; + cache_buffer_->skip(-1 * cache_buffer_->pos()); + + // Marshal packet to bytes in iovec. + if (true) { + if ((err = pkt->encode(cache_buffer_)) != srs_success) { + return srs_error_wrap(err, "encode packet"); + } + iov->iov_len = cache_buffer_->pos(); + } + + ssize_t write = 0; + if ((err = network->write(iov->iov_base, iov->iov_len, &write)) != srs_success) { + return srs_error_wrap(err, "send rtp packet"); + } + + delta_->add_delta(0, write); + + return err; +} + +ISrsKbpsDelta* SrsRtspConnection::delta() +{ + return delta_; +} + +std::string SrsRtspConnection::desc() +{ + return "Rtsp"; +} + +const SrsContextId& SrsRtspConnection::get_id() +{ + return cid_; +} + +std::string SrsRtspConnection::remote_ip() +{ + return ip_; +} + +void SrsRtspConnection::expire() +{ + trd_->interrupt(); +} + +srs_error_t SrsRtspConnection::start() +{ + srs_error_t err = srs_success; + + if ((err = trd_->start()) != srs_success) { + return srs_error_wrap(err, "coroutine"); + } + + return err; +} + +srs_error_t SrsRtspConnection::cycle() +{ + srs_error_t err = srs_success; + + // Serve the client. + err = do_cycle(); + + // Update statistic when done. + SrsStatistic* stat = SrsStatistic::instance(); + stat->kbps_add_delta(get_id().c_str(), delta()); + + do_teardown(); + + // Notify manager to remove it. + // Note that we create this object, so we use manager to remove it. + manager_->remove(this); + + // success. + if (err == srs_success) { + srs_trace("RTSP: client finished."); + return err; + } + + // It maybe success with message. + if (srs_error_code(err) == ERROR_SUCCESS) { + srs_trace("RTSP: client finished%s.", srs_error_summary(err).c_str()); + srs_freep(err); + return err; + } + + // client close peer. + // TODO: FIXME: Only reset the error when client closed it. + if (srs_is_client_gracefully_close(err)) { + srs_warn("RTSP: client disconnect peer. ret=%d", srs_error_code(err)); + } else if (srs_is_server_gracefully_close(err)) { + srs_warn("RTSP: server disconnect. ret=%d", srs_error_code(err)); + } else { + srs_error("RTSP: serve error %s", srs_error_desc(err).c_str()); + } + + srs_freep(err); + return srs_success; +} + +srs_error_t SrsRtspConnection::do_cycle() +{ + srs_error_t err = srs_success; + srs_trace("RTSP: client ip=%s, port=%d", ip_.c_str(), port_); + + // consume all rtsp messages. + while (true) { + if ((err = trd_->pull()) != srs_success) { + return srs_error_wrap(err, "rtsp cycle"); + } + + SrsRtspRequest* req_raw = NULL; + if ((err = rtsp_->recv_message(&req_raw)) != srs_success) { + return srs_error_wrap(err, "recv message"); + } + SrsUniquePtr req(req_raw); + + if (req->is_options()) { + srs_trace("RTSP: OPTIONS cseq=%ld, url=%s, client=%s:%d", req->seq, req->uri.c_str(), ip_.c_str(), port_); + SrsUniquePtr res(new SrsRtspOptionsResponse((int)req->seq)); + if ((err = rtsp_->send_message(res.get())) != srs_success) { + return srs_error_wrap(err, "response option"); + } + } else if (req->is_describe()) { + // create session. + if (session_id_.empty()) { + session_id_ = srs_random_str(8); + } + + SrsUniquePtr res(new SrsRtspDescribeResponse((int)req->seq)); + res->session = session_id_; + + std::string sdp; + if ((err = do_describe(req.get(), sdp)) != srs_success) { + res->status = SRS_CONSTS_RTSP_InternalServerError; + if (srs_error_code(err) == ERROR_RTSP_NO_TRACK) { + res->status = SRS_CONSTS_RTSP_NotFound; + } else if (srs_error_code(err) == ERROR_SYSTEM_SECURITY_DENY) { + res->status = SRS_CONSTS_RTSP_Forbidden; + } + srs_warn("RTSP: DESCRIBE failed: %s", srs_error_desc(err).c_str()); + srs_error_reset(err); + } + + res->sdp = sdp; + if ((err = rtsp_->send_message(res.get())) != srs_success) { + return srs_error_wrap(err, "response describe"); + } + + // Filter the \r\n to \\r\\n for JSON. + std::string local_sdp_escaped = srs_string_replace(sdp.c_str(), "\r\n", "\\r\\n"); + srs_trace("RTSP: DESCRIBE cseq=%ld, session=%s, sdp: %s", req->seq, session_id_.c_str(), local_sdp_escaped.c_str()); + } else if (req->is_setup()) { + srs_assert(req->transport); + + SrsUniquePtr res(new SrsRtspSetupResponse((int)req->seq)); + res->session = session_id_; + + uint32_t ssrc = 0; + if ((err = do_setup(req.get(), &ssrc)) != srs_success) { + if (srs_error_code(err) == ERROR_RTSP_TRANSPORT_NOT_SUPPORTED) { + res->status = SRS_CONSTS_RTSP_UnsupportedTransport; + srs_warn("RTSP: SETUP failed: %s", srs_error_summary(err).c_str()); + } else { + res->status = SRS_CONSTS_RTSP_InternalServerError; + srs_warn("RTSP: SETUP failed: %s", srs_error_desc(err).c_str()); + } + srs_error_reset(err); + } + + res->transport->copy(req->transport); + res->session = session_id_; + res->ssrc = srs_int2str(ssrc); + res->client_port_min = req->transport->client_port_min; + res->client_port_max = req->transport->client_port_max; + // TODO: FIXME: listen local port + res->local_port_min = 0; + res->local_port_max = 0; + if ((err = rtsp_->send_message(res.get())) != srs_success) { + return srs_error_wrap(err, "response setup"); + } + srs_trace("RTSP: SETUP cseq=%ld, session=%s, transport=%s/%s/%s, ssrc=%u, client_port=%d-%d", + req->seq, session_id_.c_str(), req->transport->transport.c_str(), req->transport->profile.c_str(), + req->transport->lower_transport.c_str(), ssrc, req->transport->client_port_min, req->transport->client_port_max); + } else if (req->is_play()) { + SrsUniquePtr res(new SrsRtspResponse((int)req->seq)); + res->session = session_id_; + if ((err = rtsp_->send_message(res.get())) != srs_success) { + return srs_error_wrap(err, "response record"); + } + + if ((err = do_play(req.get(), this)) != srs_success) { + return srs_error_wrap(err, "prepare play"); + } + srs_trace("RTSP: PLAY cseq=%ld, session=%s, streaming started", req->seq, session_id_.c_str()); + } else if (req->is_teardown()) { + SrsUniquePtr res(new SrsRtspResponse((int)req->seq)); + res->session = session_id_; + if ((err = rtsp_->send_message(res.get())) != srs_success) { + return srs_error_wrap(err, "response teardown"); + } + + if ((err = do_teardown()) != srs_success) { + return srs_error_wrap(err, "teardown"); + } + srs_trace("RTSP: TEARDOWN cseq=%ld, session=%s, streaming stopped", req->seq, session_id_.c_str()); + } + } + + return err; +} + +void SrsRtspConnection::on_before_dispose(ISrsResource* c) +{ + if (disposing_) { + return; + } + + SrsRtspConnection* session = dynamic_cast(c); + if (session == this) { + disposing_ = true; + } + + if (session && session == this) { + _srs_context->set_id(cid_); + srs_trace("RTSP: session detach from [%s](%s), disposing=%d", c->get_id().c_str(), + c->desc().c_str(), disposing_); + } +} + +void SrsRtspConnection::on_disposing(ISrsResource* c) +{ + if (disposing_) { + return; + } +} + +void SrsRtspConnection::switch_to_context() +{ + _srs_context->set_id(cid_); +} + +const SrsContextId& SrsRtspConnection::context_id() +{ + return cid_; +} + +bool SrsRtspConnection::is_alive() +{ + return last_stun_time + session_timeout > srs_get_system_time(); +} + +void SrsRtspConnection::alive() +{ + last_stun_time = srs_get_system_time(); +} + +srs_error_t SrsRtspConnection::do_describe(SrsRtspRequest* req, std::string& sdp) +{ + srs_error_t err = srs_success; + srs_parse_rtmp_url(req->uri, request_->tcUrl, request_->stream); + + srs_discovery_tc_url(request_->tcUrl, request_->schema, request_->host, request_->vhost, + request_->app, request_->stream, request_->port, request_->param); + + // discovery vhost, resolve the vhost from config + SrsConfDirective* parsed_vhost = _srs_config->get_vhost(request_->vhost); + if (parsed_vhost) { + request_->vhost = parsed_vhost->arg0(); + } + + if ((err = security_->check(SrsRtcConnPlay, ip_, request_)) != srs_success) { + return srs_error_wrap(err, "RTSP: security check"); + } + + if ((err = http_hooks_on_play(request_)) != srs_success) { + return srs_error_wrap(err, "RTSP: http_hooks_on_play"); + } + + if ((err = _srs_rtsp_sources->fetch_or_create(request_, source_)) != srs_success) { + return srs_error_wrap(err, "create source"); + } + + SrsSdp local_sdp; + local_sdp.version_ = "0"; + local_sdp.username_ = "SRS RTSP Server"; + local_sdp.session_id_ = "0"; + local_sdp.session_version_ = "0"; + local_sdp.nettype_ = "IN"; + local_sdp.addrtype_ = "IP4"; + local_sdp.unicast_address_ = "0.0.0.0"; + local_sdp.session_name_ = "Play"; + local_sdp.control_ = req->uri; + local_sdp.ice_lite_ = ""; // Disable this line. + + uint32_t track_id = 0; + SrsRtcTrackDescription* audio_desc = source_->audio_desc(); + if (audio_desc) { + SrsRtcTrackDescription* audio_track_desc = audio_desc->copy(); + audio_track_desc->id_ = srs_int2str(track_id); + tracks_.insert(std::make_pair(audio_track_desc->ssrc_, audio_track_desc)); + + SrsMediaDesc media_audio("audio"); + media_audio.port_ = 0; // Port 0 indicates no UDP transport available + media_audio.protos_ = "RTP/AVP"; // MUST be RTP/AVP + media_audio.control_ = req->uri + "/trackID=" + srs_int2str(track_id); + media_audio.recvonly_ = true; + media_audio.rtcp_mux_ = true; + + media_audio.payload_types_.push_back(SrsMediaPayloadType(audio_track_desc->media_->pt_)); + SrsMediaPayloadType& ps_audio = media_audio.payload_types_.at(0); + ps_audio.encoding_name_ = audio_track_desc->media_->name_; + ps_audio.clock_rate_ = audio_track_desc->media_->sample_; + + // if the payload is opus, and the encoding_param_ is channel + SrsAudioPayload* ap = dynamic_cast(audio_track_desc->media_); + if (ap) { + ps_audio.encoding_param_ = srs_int2str(ap->channel_); + + // Append the AAC config hex to the fmtp line. + if (ap->name_ == "MPEG4-GENERIC" && !ap->aac_config_hex_.empty()) { + // streamtype=5 - Mandatory (indicates audio stream) + // mode=AAC-hbr - Mandatory (AAC High Bit Rate mode) + // sizelength=13 - Mandatory, defaults to 13 bits for AAC + // indexlength=3 - Mandatory, defaults to 3 bits + // profile-level-id=1 - Optional, defaults to 1 (AAC Main Profile) + // indexdeltalength=3 - Optional, defaults to 3 bits + ps_audio.format_specific_param_ = "streamtype=5;mode=AAC-hbr;sizelength=13;indexlength=3"; + ps_audio.format_specific_param_ += ";config=" + ap->aac_config_hex_; + srs_trace("RTSP: Added AAC fmtp: %s", ps_audio.format_specific_param_.c_str()); + } + } + + local_sdp.media_descs_.push_back(media_audio); + track_id++; + } + + SrsRtcTrackDescription* video_desc = source_->video_desc(); + if (video_desc) { + SrsRtcTrackDescription* video_track_desc = video_desc->copy(); + video_track_desc->id_ = srs_int2str(track_id); + tracks_.insert(std::make_pair(video_track_desc->ssrc_, video_track_desc)); + + SrsMediaDesc media_video("video"); + media_video.port_ = 0; // Port 0 indicates no UDP transport available + media_video.protos_ = "RTP/AVP"; // MUST be RTP/AVP + media_video.control_ = req->uri + "/trackID=" + srs_int2str(track_id); + media_video.recvonly_ = true; + media_video.rtcp_mux_ = true; + + media_video.payload_types_.push_back(SrsMediaPayloadType(video_track_desc->media_->pt_)); + SrsMediaPayloadType& ps_video = media_video.payload_types_.at(0); + ps_video.encoding_name_ = video_track_desc->media_->name_; + ps_video.clock_rate_ = video_track_desc->media_->sample_; + + local_sdp.media_descs_.push_back(media_video); + track_id++; + } + + if (track_id == 0) { + return srs_error_new(ERROR_RTSP_NO_TRACK, "no track found"); + } + + std::ostringstream ss; + if ((err = local_sdp.encode(ss)) != srs_success) { + return srs_error_wrap(err, "encode sdp"); + } + + sdp = ss.str(); + return srs_success; +} + +srs_error_t SrsRtspConnection::do_setup(SrsRtspRequest* req, uint32_t* pssrc) +{ + srs_error_t err = srs_success; + + uint32_t ssrc = 0; + if ((err = get_ssrc_by_stream_id(req->stream_id, &ssrc)) != srs_success) { + return srs_error_wrap(err, "get ssrc by stream_id"); + } + + // Only support TCP transport, reject UDP + // This ensures better firewall/NAT compatibility and eliminates port allocation complexity + if (req->transport->lower_transport != "TCP") { + return srs_error_new(ERROR_RTSP_TRANSPORT_NOT_SUPPORTED, + "UDP transport not supported, only TCP/interleaved mode is supported"); + } + + SrsRtspTcpNetwork* network = new SrsRtspTcpNetwork(skt_, req->transport->interleaved_min); + networks_[ssrc] = network; + + *pssrc = ssrc; + + return srs_success; +} + +srs_error_t SrsRtspConnection::do_play(SrsRtspRequest* req, SrsRtspConnection* conn) +{ + srs_error_t err = srs_success; + + srs_freep(player_); + player_ = new SrsRtspPlayStream(conn, cid_); + + if ((err = player_->initialize(request_, tracks_)) != srs_success) { + srs_freep(player_); + return srs_error_wrap(err, "SrsRtspPlayStream init"); + } + player_->set_all_tracks_status(true); + if ((err = player_->start()) != srs_success) { + return srs_error_wrap(err, "start play"); + } + + srs_trace("RTSP: Subscriber url=%s established", req->uri.c_str()); + + return err; +} + +srs_error_t SrsRtspConnection::do_teardown() +{ + if (player_) { + player_->stop(); + srs_freep(player_); + } + + return srs_success; +} + +srs_error_t SrsRtspConnection::http_hooks_on_play(SrsRequest* req) +{ + srs_error_t err = srs_success; + + if (!_srs_config->get_vhost_http_hooks_enabled(req->vhost)) { + return err; + } + + // the http hooks will cause context switch, + // so we must copy all hooks for the on_connect may freed. + // @see https://github.com/ossrs/srs/issues/475 + std::vector hooks; + + if (true) { + SrsConfDirective* conf = _srs_config->get_vhost_on_play(req->vhost); + + if (!conf) { + return err; + } + + hooks = conf->args; + } + + for (int i = 0; i < (int)hooks.size(); i++) { + std::string url = hooks.at(i); + if ((err = SrsHttpHooks::on_play(url, req)) != srs_success) { + return srs_error_wrap(err, "on_play %s", url.c_str()); + } + } + + return err; +} + +srs_error_t SrsRtspConnection::get_ssrc_by_stream_id(uint32_t stream_id, uint32_t* ssrc) +{ + for (std::map::iterator it = tracks_.begin(); it != tracks_.end(); ++it) { + if (it->second->id_ == srs_int2str(stream_id)) { + *ssrc = it->second->ssrc_; + return srs_success; + } + } + return srs_error_new(ERROR_RTSP_NO_TRACK, "track not found for stream_id: %u", stream_id); +} + +SrsRtspTcpNetwork::SrsRtspTcpNetwork(ISrsProtocolReadWriter* skt, int ch) : skt_(skt), channel_(ch) +{ +} + +SrsRtspTcpNetwork::~SrsRtspTcpNetwork() +{ +} + +srs_error_t SrsRtspTcpNetwork::write(void* buf, size_t size, ssize_t* nwrite) +{ + srs_error_t err = srs_success; + + srs_assert(size <= 65535); + + // Encode and send 4 bytes size, in network order. + const int kRtpTcpPacketHeaderSize = 4; + char header[kRtpTcpPacketHeaderSize]; + + // Use SrsBuffer to handle endianness properly + SrsBuffer hb(header, kRtpTcpPacketHeaderSize); + hb.write_1bytes(0x24); // Magic byte '$' + hb.write_1bytes(uint8_t(channel_)); // Channel number + hb.write_2bytes(uint16_t(size)); // Packet size in network order + + if((err = skt_->write(header, kRtpTcpPacketHeaderSize, NULL)) != srs_success) { + return srs_error_wrap(err, "RTSP tcp write len(%d)", size); + } + + if ((err = skt_->write(buf, size, nwrite)) != srs_success) { + return srs_error_wrap(err, "RTSP send rtp packet"); + } + + // Add the size of the header to the write count. + *nwrite += kRtpTcpPacketHeaderSize; + + return err; +} + diff --git a/trunk/src/app/srs_app_rtsp_conn.hpp b/trunk/src/app/srs_app_rtsp_conn.hpp new file mode 100644 index 000000000..102f382f7 --- /dev/null +++ b/trunk/src/app/srs_app_rtsp_conn.hpp @@ -0,0 +1,181 @@ +// +// Copyright (c) 2013-2025 The SRS Authors +// +// SPDX-License-Identifier: MIT +// + +#ifndef SRS_APP_RTSP_CONN_HPP +#define SRS_APP_RTSP_CONN_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +class SrsRequest; +class SrsRtpPacket; +class SrsRtspSource; +class SrsRtspAudioSendTrack; +class SrsRtspVideoSendTrack; +class SrsRtspSendTrack; +class SrsEphemeralDelta; +class SrsRtspConnection; +class SrsSecurity; +class SrsRtspRequest; +class SrsRtspStack; + +// A RTSP play stream, client pull and play stream from SRS. +class SrsRtspPlayStream : public ISrsCoroutineHandler, public ISrsRtcSourceChangeCallback +{ +private: + SrsContextId cid_; + SrsFastCoroutine* trd_; + SrsRtspConnection* session_; +private: + SrsRequest* req_; + SrsSharedPtr source_; + // key: publish_ssrc, value: send track to process rtp/rtcp + std::map audio_tracks_; + std::map video_tracks_; +private: + // Fast cache for tracks. + uint32_t cache_ssrc0_; + uint32_t cache_ssrc1_; + uint32_t cache_ssrc2_; + SrsRtspSendTrack* cache_track0_; + SrsRtspSendTrack* cache_track1_; + SrsRtspSendTrack* cache_track2_; +private: + // Whether player started. + bool is_started; +public: + SrsRtspPlayStream(SrsRtspConnection* s, const SrsContextId& cid); + virtual ~SrsRtspPlayStream(); +public: + srs_error_t initialize(SrsRequest* request, std::map sub_relations); +// Interface ISrsRtcSourceChangeCallback +public: + void on_stream_change(SrsRtcSourceDescription* desc); +public: + virtual const SrsContextId& context_id(); +public: + virtual srs_error_t start(); + virtual void stop(); +public: + virtual srs_error_t cycle(); +private: + srs_error_t send_packet(SrsRtpPacket*& pkt); +public: + // Directly set the status of track, generally for init to set the default value. + void set_all_tracks_status(bool status); +}; + +class SrsRtspConnection : public ISrsResource, public ISrsDisposingHandler, public ISrsExpire, public ISrsCoroutineHandler, public ISrsStartable +{ +private: + bool disposing_; +private: + // TODO: FIXME: Rename it. + // The timeout of session, keep alive by STUN ping pong. + srs_utime_t session_timeout; + // TODO: FIXME: Rename it. + srs_utime_t last_stun_time; + SrsContextId cid_; + SrsRequest* request_; + // The manager object to manage the connection. + ISrsResourceManager* manager_; + // Each connection start a green thread, + // when thread stop, the connection will be delete by server. + SrsCoroutine* trd_; + // The ip and port of client. + std::string ip_; + int port_; + SrsRtspStack* rtsp_; + std::string session_id_; + SrsSharedPtr source_; + SrsEphemeralDelta* delta_; + ISrsProtocolReadWriter* skt_; + SrsSecurity* security_; + iovec* cache_iov_; + SrsBuffer* cache_buffer_; + // key: ssrc + std::map tracks_; + // key: ssrc + std::map networks_; + SrsRtspPlayStream* player_; +public: + SrsRtspConnection(ISrsResourceManager* cm, ISrsProtocolReadWriter* skt, std::string cip, int port); + virtual ~SrsRtspConnection(); +// interface ISrsDisposingHandler +public: + virtual void on_before_dispose(ISrsResource* c); + virtual void on_disposing(ISrsResource* c); +public: + virtual srs_error_t do_send_packet(SrsRtpPacket* pkt); +public: + ISrsKbpsDelta* delta(); +private: + virtual srs_error_t do_describe(SrsRtspRequest* req, std::string& sdp); + virtual srs_error_t do_setup(SrsRtspRequest* req, uint32_t* ssrc); + virtual srs_error_t do_play(SrsRtspRequest* req, SrsRtspConnection* conn); + virtual srs_error_t do_teardown(); +// Interface ISrsResource. +public: + virtual std::string desc(); + virtual const SrsContextId& get_id(); +// Interface ISrsConnection. +public: + virtual std::string remote_ip(); +// Interface ISrsStartable +public: + // Start the client green thread. + // when server get a client from listener, + // 1. server will create an concrete connection(for instance, RTMP connection), + // 2. then add connection to its connection manager, + // 3. start the client thread by invoke this start() + // when client cycle thread stop, invoke the on_thread_stop(), which will use server + // To remove the client by server->remove(this). + virtual srs_error_t start(); +// Interface ISrsCoroutineHandler +public: + virtual srs_error_t cycle(); +// Interface ISrsExpire. +public: + virtual void expire(); +public: + void switch_to_context(); + const SrsContextId& context_id(); +public: + bool is_alive(); + void alive(); +private: + srs_error_t do_cycle(); +private: + srs_error_t http_hooks_on_play(SrsRequest* req); + srs_error_t get_ssrc_by_stream_id(uint32_t stream_id, uint32_t* ssrc); +}; + +class SrsRtspTcpNetwork : public ISrsStreamWriter +{ +private: + ISrsProtocolReadWriter* skt_; + int channel_; +public: + SrsRtspTcpNetwork(ISrsProtocolReadWriter* skt, int ch); + virtual ~SrsRtspTcpNetwork(); +// Interface ISrsStreamWriter. +public: + virtual srs_error_t write(void* buf, size_t size, ssize_t* nwrite); +}; + +#endif + diff --git a/trunk/src/app/srs_app_rtsp_source.cpp b/trunk/src/app/srs_app_rtsp_source.cpp new file mode 100644 index 000000000..c6bbd25e4 --- /dev/null +++ b/trunk/src/app/srs_app_rtsp_source.cpp @@ -0,0 +1,1079 @@ +// +// Copyright (c) 2013-2025 The SRS Authors +// +// SPDX-License-Identifier: MIT +// + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +extern SrsPps* _srs_pps_aloss2; + +static const int kVideoSamplerate = 90000; + +// the time to cleanup source. +#define SRS_RTSP_SOURCE_CLEANUP (3 * SRS_UTIME_SECONDS) + +SrsRtspConsumer::SrsRtspConsumer(SrsRtspSource* s) +{ + source_ = s; + should_update_source_id = false; + handler_ = NULL; + + mw_wait = srs_cond_new(); + mw_min_msgs = 0; + mw_waiting = false; +} + +SrsRtspConsumer::~SrsRtspConsumer() +{ + source_->on_consumer_destroy(this); + + vector::iterator it; + for (it = queue.begin(); it != queue.end(); ++it) { + SrsRtpPacket* pkt = *it; + srs_freep(pkt); + } + + srs_cond_destroy(mw_wait); +} + +void SrsRtspConsumer::update_source_id() +{ + should_update_source_id = true; +} + +srs_error_t SrsRtspConsumer::enqueue(SrsRtpPacket* pkt) +{ + srs_error_t err = srs_success; + + queue.push_back(pkt); + + if (mw_waiting) { + if ((int)queue.size() > mw_min_msgs) { + srs_cond_signal(mw_wait); + mw_waiting = false; + return err; + } + } + + return err; +} + +srs_error_t SrsRtspConsumer::dump_packet(SrsRtpPacket** ppkt) +{ + srs_error_t err = srs_success; + + // TODO: FIXME: Refine performance by ring buffer. + if (!queue.empty()) { + *ppkt = queue.front(); + queue.erase(queue.begin()); + } + + return err; +} + +void SrsRtspConsumer::wait(int nb_msgs) +{ + mw_min_msgs = nb_msgs; + + // when duration ok, signal to flush. + if ((int)queue.size() > mw_min_msgs) { + return; + } + + // the enqueue will notify this cond. + mw_waiting = true; + + // use cond block wait for high performance mode. + srs_cond_wait(mw_wait); +} + +void SrsRtspConsumer::on_stream_change(SrsRtcSourceDescription* desc) +{ + if (handler_) { + handler_->on_stream_change(desc); + } +} + +SrsRtspSourceManager::SrsRtspSourceManager() +{ + lock = srs_mutex_new(); + timer_ = new SrsHourGlass("sources", this, 1 * SRS_UTIME_SECONDS); +} + +SrsRtspSourceManager::~SrsRtspSourceManager() +{ + srs_mutex_destroy(lock); + srs_freep(timer_); +} + +srs_error_t SrsRtspSourceManager::initialize() +{ + return setup_ticks(); +} + +srs_error_t SrsRtspSourceManager::setup_ticks() +{ + srs_error_t err = srs_success; + + if ((err = timer_->tick(1, 3 * SRS_UTIME_SECONDS)) != srs_success) { + return srs_error_wrap(err, "tick"); + } + + if ((err = timer_->start()) != srs_success) { + return srs_error_wrap(err, "timer"); + } + + return err; +} + +srs_error_t SrsRtspSourceManager::notify(int event, srs_utime_t interval, srs_utime_t tick) +{ + srs_error_t err = srs_success; + + std::map< std::string, SrsSharedPtr >::iterator it; + for (it = pool.begin(); it != pool.end();) { + SrsSharedPtr& source = it->second; + + // When source expired, remove it. + // @see https://github.com/ossrs/srs/issues/713 + if (source->stream_is_dead()) { + SrsContextId cid = source->source_id(); + if (cid.empty()) cid = source->pre_source_id(); + srs_trace("RTSP: cleanup die source, id=[%s], total=%d", cid.c_str(), (int)pool.size()); + pool.erase(it++); + } else { + ++it; + } + } + + return err; +} + +srs_error_t SrsRtspSourceManager::fetch_or_create(SrsRequest* r, SrsSharedPtr& pps) +{ + srs_error_t err = srs_success; + + // Use lock to protect coroutine switch. + // @bug https://github.com/ossrs/srs/issues/1230 + SrsLocker(lock); + + string stream_url = r->get_stream_url(); + std::map< std::string, SrsSharedPtr >::iterator it = pool.find(stream_url); + + if (it != pool.end()) { + SrsSharedPtr source = it->second; + + // we always update the request of resource, + // for origin auth is on, the token in request maybe invalid, + // and we only need to update the token of request, it's simple. + source->update_auth(r); + pps = source; + + return err; + } + + SrsSharedPtr source = SrsSharedPtr(new SrsRtspSource()); + srs_trace("new rtsp source, stream_url=%s", stream_url.c_str()); + + if ((err = source->initialize(r)) != srs_success) { + return srs_error_wrap(err, "init source %s", r->get_stream_url().c_str()); + } + + pool[stream_url] = source; + pps = source; + + return err; +} + +SrsSharedPtr SrsRtspSourceManager::fetch(SrsRequest* r) +{ + // Use lock to protect coroutine switch. + // @bug https://github.com/ossrs/srs/issues/1230 + SrsLocker(lock); + + string stream_url = r->get_stream_url(); + std::map< std::string, SrsSharedPtr >::iterator it = pool.find(stream_url); + + SrsSharedPtr source; + if (it == pool.end()) { + return source; + } + + source = it->second; + return source; +} + +SrsRtspSourceManager* _srs_rtsp_sources = NULL; + +SrsResourceManager* _srs_rtsp_manager = NULL; + +SrsRtspSource::SrsRtspSource() +{ + is_created_ = false; + is_delivering_packets_ = false; + + audio_desc_ = NULL; + video_desc_ = NULL; + + req = NULL; + + stream_die_at_ = 0; +} + +SrsRtspSource::~SrsRtspSource() +{ + // never free the consumers, + // for all consumers are auto free. + consumers.clear(); + + srs_freep(req); + srs_freep(audio_desc_); + srs_freep(video_desc_); + + SrsContextId cid = _source_id; + if (cid.empty()) cid = _pre_source_id; + srs_trace("free rtc source id=[%s]", cid.c_str()); +} + +srs_error_t SrsRtspSource::initialize(SrsRequest* r) +{ + srs_error_t err = srs_success; + + req = r->copy(); + + return err; +} + +bool SrsRtspSource::stream_is_dead() +{ + // still publishing? + if (is_created_) { + return false; + } + + // has any consumers? + if (!consumers.empty()) { + return false; + } + + // Delay cleanup source. + srs_utime_t now = srs_get_system_time(); + if (now < stream_die_at_ + SRS_RTSP_SOURCE_CLEANUP) { + return false; + } + + return true; +} + +void SrsRtspSource::update_auth(SrsRequest* r) +{ + req->update_auth(r); +} + +srs_error_t SrsRtspSource::on_source_changed() +{ + srs_error_t err = srs_success; + + // Update context id if changed. + bool id_changed = false; + const SrsContextId& id = _srs_context->get_id(); + if (_source_id.compare(id)) { + id_changed = true; + + if (_pre_source_id.empty()) { + _pre_source_id = id; + } + _source_id = id; + } + + // Build stream description. + SrsUniquePtr stream_desc(new SrsRtcSourceDescription()); + if (audio_desc_) { + stream_desc->audio_track_desc_ = audio_desc_->copy(); + } + if (video_desc_) { + stream_desc->video_track_descs_.push_back(video_desc_->copy()); + } + + // Notify all consumers. + std::vector::iterator it; + for (it = consumers.begin(); it != consumers.end(); ++it) { + SrsRtspConsumer* consumer = *it; + + // Notify if context id changed. + if (id_changed) { + consumer->update_source_id(); + } + + // Notify about stream description. + consumer->on_stream_change(stream_desc.get()); + } + + return err; +} + +SrsContextId SrsRtspSource::source_id() +{ + return _source_id; +} + +SrsContextId SrsRtspSource::pre_source_id() +{ + return _pre_source_id; +} + +srs_error_t SrsRtspSource::create_consumer(SrsRtspConsumer*& consumer) +{ + srs_error_t err = srs_success; + + consumer = new SrsRtspConsumer(this); + consumers.push_back(consumer); + + stream_die_at_ = 0; + + // TODO: FIXME: Implements edge cluster. + + return err; +} + +srs_error_t SrsRtspSource::consumer_dumps(SrsRtspConsumer* consumer, bool ds, bool dm, bool dg) +{ + srs_error_t err = srs_success; + + // print status. + srs_trace("create rtsp consumer, no gop cache"); + + return err; +} + +void SrsRtspSource::on_consumer_destroy(SrsRtspConsumer* consumer) +{ + std::vector::iterator it; + it = std::find(consumers.begin(), consumers.end(), consumer); + if (it != consumers.end()) { + it = consumers.erase(it); + } + + // TODO: When all consumers finished, notify publisher to handle it. + + // Destroy and cleanup source when no publishers and consumers. + if (!is_created_ && consumers.empty()) { + stream_die_at_ = srs_get_system_time(); + } +} + +bool SrsRtspSource::can_publish() +{ + // TODO: FIXME: Should check the status of bridge. + + return !is_created_; +} + +void SrsRtspSource::set_stream_created() +{ + srs_assert(!is_created_ && !is_delivering_packets_); + is_created_ = true; +} + +srs_error_t SrsRtspSource::on_publish() +{ + srs_error_t err = srs_success; + + // update the request object. + srs_assert(req); + + // For RTC, DTLS is done, and we are ready to deliver packets. + // @note For compatible with RTMP, we also set the is_created_, it MUST be created here. + is_created_ = true; + is_delivering_packets_ = true; + + // Notify the consumers about stream change event. + if ((err = on_source_changed()) != srs_success) { + return srs_error_wrap(err, "source id change"); + } + + SrsStatistic* stat = SrsStatistic::instance(); + stat->on_stream_publish(req, _source_id.c_str()); + + return err; +} + +void SrsRtspSource::on_unpublish() +{ + // ignore when already unpublished. + if (!is_created_) { + return; + } + + srs_trace("cleanup when unpublish, created=%u, deliver=%u", is_created_, is_delivering_packets_); + + is_created_ = false; + is_delivering_packets_ = false; + + if (!_source_id.empty()) { + _pre_source_id = _source_id; + } + _source_id = SrsContextId(); + + SrsStatistic* stat = SrsStatistic::instance(); + stat->on_stream_close(req); + + // Destroy and cleanup source when no publishers and consumers. + if (consumers.empty()) { + stream_die_at_ = srs_get_system_time(); + } +} + +srs_error_t SrsRtspSource::on_rtp(SrsRtpPacket* pkt) +{ + srs_error_t err = srs_success; + + // If circuit-breaker is dying, drop packet. + if (_srs_circuit_breaker->hybrid_dying_water_level()) { + _srs_pps_aloss2->sugar += (int64_t)consumers.size(); + return err; + } + + for (int i = 0; i < (int)consumers.size(); i++) { + SrsRtspConsumer* consumer = consumers.at(i); + if ((err = consumer->enqueue(pkt->copy())) != srs_success) { + return srs_error_wrap(err, "consume message"); + } + } + + return err; +} + +SrsRtcTrackDescription* SrsRtspSource::audio_desc() +{ + return audio_desc_; +} + +void SrsRtspSource::set_audio_desc(SrsRtcTrackDescription* audio_desc) +{ + srs_freep(audio_desc_); + audio_desc_ = audio_desc->copy(); +} + +SrsRtcTrackDescription* SrsRtspSource::video_desc() +{ + return video_desc_; +} + +void SrsRtspSource::set_video_desc(SrsRtcTrackDescription* video_desc) +{ + srs_freep(video_desc_); + video_desc_ = video_desc->copy(); +} + +SrsRtspRtpBuilder::SrsRtspRtpBuilder(SrsFrameToRtspBridge* bridge, SrsSharedPtr source) +{ + bridge_ = bridge; + source_ = source; + + req = NULL; + format = new SrsRtmpFormat(); + meta = new SrsMetaCache(); + video_builder_ = new SrsRtpVideoBuilder(); + audio_sequence = 0; + + // Initialize with default values - will be set during lazy initialization + audio_ssrc_ = 0; + audio_payload_type_ = 0; + audio_sample_rate_ = 0; + + // Lazy initialization flags + audio_initialized_ = false; + video_initialized_ = false; +} + +SrsRtspRtpBuilder::~SrsRtspRtpBuilder() +{ + srs_freep(format); + srs_freep(meta); + srs_freep(video_builder_); +} + +srs_error_t SrsRtspRtpBuilder::initialize_audio_track(SrsAudioCodecId codec) +{ + srs_error_t err = srs_success; + + // RTSP behavior: Build track description from real audio format, not default values + // This is different from RTC which uses default track descriptions + + // Create audio track description from actual format data + SrsUniquePtr audio_desc(new SrsRtcTrackDescription()); + audio_desc->type_ = "audio"; + audio_desc->id_ = "audio-" + srs_random_str(8); + audio_desc->direction_ = "recvonly"; + + // Generate SSRC for this track + audio_ssrc_ = SrsRtcSSRCGenerator::instance()->generate_ssrc(); + audio_desc->ssrc_ = audio_ssrc_; + + int sample_rate = srs_flv_srates[format->acodec->sound_rate]; + audio_sample_rate_ = sample_rate; + + // Build payload from actual audio format + if (codec == SrsAudioCodecIdOpus) { + // For Opus, use actual format parameters if available + int channels = (format->acodec->sound_type == SrsAudioChannelsStereo) ? 2 : 1; + audio_payload_type_ = kAudioPayloadType; + audio_desc->media_ = new SrsAudioPayload(audio_payload_type_, "opus", sample_rate, channels); + } else if (codec == SrsAudioCodecIdAAC) { + // For AAC, extract parameters from format + int channels = format->acodec->aac_channels; + audio_payload_type_ = kAudioPayloadType; + + // Note: Use "MPEG4-GENERIC" instead of "AAC" for RTSP/SDP compliance + // RFC 3640 specifies that AAC should be advertised as "MPEG4-GENERIC" in SDP rtpmap + // "AAC" is non-standard and not widely supported by RTSP clients + SrsAudioPayload* aac_payload = new SrsAudioPayload(audio_payload_type_, "MPEG4-GENERIC", sample_rate, channels); + + // AAC requires AudioSpecificConfig in SDP fmtp line + // Build the config string from AAC sequence header + const std::vector& asc = format->acodec->aac_extra_data; + if (!asc.empty()) { + int hex_len = asc.size() * 2; + SrsUniquePtr hex_buf(new char[hex_len + 1]); + srs_data_to_hex(hex_buf.get(), (const uint8_t*)asc.data(), asc.size()); + + hex_buf.get()[hex_len] = '\0'; // Null terminate + std::string config_hex = std::string(hex_buf.get()); + + // Set the AAC configuration directly in the audio payload + aac_payload->aac_config_hex_ = config_hex; + srs_trace("RTSP: AAC config hex: %s", config_hex.c_str()); + } + + audio_desc->media_ = aac_payload; + } else { + return srs_error_new(ERROR_RTC_RTP_MUXER, "unsupported audio codec %d", codec); + } + + // Extract info for logging before setting to source + int sample_rate_for_log = audio_desc->media_->sample_; + int channels_for_log = (audio_desc->media_->type_ == "audio") ? ((SrsAudioPayload*)audio_desc->media_)->channel_ : 0; + + // Set the audio description to source + source_->set_audio_desc(audio_desc.get()); + + srs_trace("RTSP: Initialize audio track from format - codec=%s, ssrc=%u, pt=%d, sample_rate=%d, channels=%d", + srs_audio_codec_id2str(codec).c_str(), audio_ssrc_, audio_payload_type_, + sample_rate_for_log, channels_for_log); + + return err; +} + +srs_error_t SrsRtspRtpBuilder::initialize_video_track(SrsVideoCodecId codec) +{ + srs_error_t err = srs_success; + + // RTSP behavior: Build track description from real video format, not default values + // This is different from RTC which uses default track descriptions + + std::string codec_name = srs_video_codec_id2str(codec); + + // Create video track description from actual format data + SrsUniquePtr video_desc(new SrsRtcTrackDescription()); + video_desc->type_ = "video"; + video_desc->id_ = "video-" + codec_name + "-" + srs_random_str(8); + video_desc->direction_ = "recvonly"; + + // Generate SSRC for this track + uint32_t video_ssrc = SrsRtcSSRCGenerator::instance()->generate_ssrc(); + video_desc->ssrc_ = video_ssrc; + + // Build payload from actual video format + uint8_t video_payload_type = 0; + if (codec == SrsVideoCodecIdAVC) { + // H.264 track with actual format parameters + video_payload_type = kVideoPayloadType; + SrsVideoPayload* h264_payload = new SrsVideoPayload(video_payload_type, "H264", kVideoSamplerate); + h264_payload->set_h264_param_desc("level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f"); + video_desc->media_ = h264_payload; + + } else if (codec == SrsVideoCodecIdHEVC) { + // H.265 track with actual format parameters + video_payload_type = KVideoPayloadTypeHevc; + SrsVideoPayload* h265_payload = new SrsVideoPayload(video_payload_type, "H265", kVideoSamplerate); + h265_payload->set_h265_param_desc("level-id=156;profile-id=1;tier-flag=0;tx-mode=SRST"); + video_desc->media_ = h265_payload; + + } else { + return srs_error_new(ERROR_RTC_RTP_MUXER, "unsupported video codec %d", codec); + } + + SrsFormat* format = meta->vsh_format(); + if ((err = video_builder_->initialize(format, video_ssrc, video_payload_type)) != srs_success) { + return srs_error_wrap(err, "initialize video builder"); + } + + // Set the video description to source + source_->set_video_desc(video_desc.get()); + + srs_trace("RTSP: Initialize video track from format - codec=%s, ssrc=%u, pt=%d, sample_rate=%d", + codec_name.c_str(), video_ssrc, video_payload_type, kVideoSamplerate); + + return err; +} + +srs_error_t SrsRtspRtpBuilder::initialize(SrsRequest* r) +{ + srs_error_t err = srs_success; + + req = r; + + if ((err = format->initialize()) != srs_success) { + return srs_error_wrap(err, "format initialize"); + } + + // Setup the SPS/PPS parsing strategy. + format->try_annexb_first = _srs_config->try_annexb_first(r->vhost); + + srs_trace("RTSP bridge from RTMP, try_annexb_first=%d", format->try_annexb_first); + + return err; +} + +srs_error_t SrsRtspRtpBuilder::on_publish() +{ + srs_error_t err = srs_success; + + // Reset the metadata cache, to make VLC happy when disable/enable stream. + // @see https://github.com/ossrs/srs/issues/1630#issuecomment-597979448 + meta->clear(); + + return err; +} + +void SrsRtspRtpBuilder::on_unpublish() +{ + // Reset the metadata cache, to make VLC happy when disable/enable stream. + // @see https://github.com/ossrs/srs/issues/1630#issuecomment-597979448 + meta->update_previous_vsh(); + meta->update_previous_ash(); +} + +srs_error_t SrsRtspRtpBuilder::on_frame(SrsSharedPtrMessage* frame) +{ + if (frame->is_audio()) { + return on_audio(frame); + } else if (frame->is_video()) { + return on_video(frame); + } + return srs_success; +} + +srs_error_t SrsRtspRtpBuilder::on_audio(SrsSharedPtrMessage* msg) +{ + srs_error_t err = srs_success; + + if ((err = format->on_audio(msg)) != srs_success) { + return srs_error_wrap(err, "format consume audio"); + } + + // Ignore if no format->acodec, it means the codec is not parsed, or unknown codec. + // @issue https://github.com/ossrs/srs/issues/1506#issuecomment-562079474 + if (!format->acodec) { + return err; + } + + // support audio codec: aac/opus + SrsAudioCodecId acodec = format->acodec->id; + if (acodec != SrsAudioCodecIdAAC && acodec != SrsAudioCodecIdOpus) { + return err; + } + + // Initialize audio track on first packet with actual codec + if (!audio_initialized_) { + if ((err = initialize_audio_track(acodec)) != srs_success) { + return srs_error_wrap(err, "init audio track"); + } + audio_initialized_ = true; + } + + // Skip empty audio frames + if (format->audio->nb_samples == 0) { + return err; + } + + // Convert to RTP packet. + SrsUniquePtr pkt(new SrsRtpPacket()); + + if (acodec == SrsAudioCodecIdAAC) { + if ((err = package_aac(format->audio, pkt.get())) != srs_success) { + return srs_error_wrap(err, "package aac"); + } + } else { + return srs_error_new(ERROR_NOT_IMPLEMENTED, "codec %d not implemented", acodec); + } + + if ((err = bridge_->on_rtp(pkt.get())) != srs_success) { + return srs_error_wrap(err, "consume audio packet"); + } + + return err; +} + +srs_error_t SrsRtspRtpBuilder::package_aac(SrsAudioFrame* audio, SrsRtpPacket* pkt) +{ + srs_error_t err = srs_success; + + srs_assert(audio->nb_samples); + + // For RTSP, audio TBN is not fixed, but use the sample rate, so we + // need to convert FLV TBN(1000) to the sample rate TBN. + int64_t dts = (int64_t)audio->dts; + dts *= (int64_t)audio_sample_rate_; + dts /= 1000; + + pkt->header.set_payload_type(audio_payload_type_); + pkt->header.set_ssrc(audio_ssrc_); + pkt->frame_type = SrsFrameTypeAudio; + pkt->header.set_marker(true); + pkt->header.set_sequence(audio_sequence++); + pkt->header.set_timestamp(dts); + + SrsRtpRawPayload* raw = new SrsRtpRawPayload(); + pkt->set_payload(raw, SrsRtpPacketPayloadTypeRaw); + + // For AAC, we need to package according to RFC 3640 (MPEG-4 Audio) + // Use AAC-hbr mode with AU-headers + // Calculate total size for all AU samples + int total_au_size = 0; + for (int i = 0; i < audio->nb_samples; i++) { + total_au_size += audio->samples[i].size; + } + + // AU-headers: 16 bits per AU (13 bits for size + 3 bits for index) + int au_headers_length = audio->nb_samples * 16; // bits + int au_headers_bytes = (au_headers_length + 7) / 8; // convert to bytes + int payload_size = 2 + au_headers_bytes + total_au_size; // AU-headers-length(2) + AU-headers + AU data + + // Use SrsBuffer for proper byte marshaling + SrsUniquePtr payload(new char[payload_size]); + SrsBuffer buffer(payload.get(), payload_size); + + // AU-headers-length (16 bits) - this is the length in BITS, not bytes + buffer.write_2bytes(au_headers_length); + + // Write AU-headers for each sample + for (int i = 0; i < audio->nb_samples; i++) { + // AU-header: AU-size(13 bits) + AU-index(3 bits) = 16 bits + // According to RFC 3640, AU-size comes first (MSB), then AU-index (LSB) + uint16_t au_size = audio->samples[i].size & 0x1FFF; // 13 bits mask + uint16_t au_index = i & 0x07; // 3 bits mask + buffer.write_2bytes((au_size << 3) | au_index); + } + + // Copy all AAC AU data + for (int i = 0; i < audio->nb_samples; i++) { + buffer.write_bytes(audio->samples[i].bytes, audio->samples[i].size); + } + + // Wrap the payload in the RTP packet + raw->payload = pkt->wrap(payload.get(), payload_size); + raw->nn_payload = payload_size; + + return err; +} + +static void free_packets(vector* pkts) { + if (!pkts) return; + + for (size_t i = 0; i < pkts->size(); i++) { + srs_freep((*pkts)[i]); + } + pkts->clear(); +} + +srs_error_t SrsRtspRtpBuilder::on_video(SrsSharedPtrMessage* msg) +{ + srs_error_t err = srs_success; + + // cache the sequence header if h264 + bool is_sequence_header = SrsFlvVideo::sh(msg->payload, msg->size); + if (is_sequence_header && (err = meta->update_vsh(msg)) != srs_success) { + return srs_error_wrap(err, "meta update video"); + } + + if ((err = format->on_video(msg)) != srs_success) { + return srs_error_wrap(err, "format consume video"); + } + + // Ignore if no format->vcodec, it means the codec is not parsed, or unsupport/unknown codec + // such as H.263 codec + if (!format->vcodec) { + return err; + } + + // support video codec: h264/h265 + SrsVideoCodecId vcodec = format->vcodec->id; + if (vcodec != SrsVideoCodecIdAVC && vcodec != SrsVideoCodecIdHEVC) { + return err; + } + + // Initialize video track on first packet with actual codec + if (!video_initialized_) { + if ((err = initialize_video_track(vcodec)) != srs_success) { + return srs_error_wrap(err, "init video track"); + } + video_initialized_ = true; + } + + bool has_idr = false; + vector samples; + if ((err = filter(msg, format, has_idr, samples)) != srs_success) { + return srs_error_wrap(err, "filter video"); + } + int nn_samples = (int)samples.size(); + + // Well, for each IDR, we append a SPS/PPS before it, which is packaged in STAP-A. + if (has_idr) { + SrsUniquePtr pkt(new SrsRtpPacket()); + + if ((err = package_stap_a(msg, pkt.get())) != srs_success) { + return srs_error_wrap(err, "package stap-a"); + } + + if ((err = bridge_->on_rtp(pkt.get())) != srs_success) { + return srs_error_wrap(err, "consume sps/pps"); + } + } + + // If merge Nalus, we pcakges all NALUs(samples) as one NALU, in a RTP or FUA packet. + vector pkts; + // auto free when exit + SrsUniquePtr> pkts_ptr(&pkts, free_packets); + + // By default, we package each NALU(sample) to a RTP or FUA packet. + for (int i = 0; i < nn_samples; i++) { + SrsSample* sample = samples[i]; + + if (sample->size <= kRtpMaxPayloadSize) { + if ((err = package_single_nalu(msg, sample, pkts)) != srs_success) { + return srs_error_wrap(err, "package single nalu"); + } + } else { + if ((err = package_fu_a(msg, sample, kRtpMaxPayloadSize, pkts)) != srs_success) { + return srs_error_wrap(err, "package fu-a"); + } + } + } + + if (!pkts.empty()) { + pkts.back()->header.set_marker(true); + } + + return consume_packets(pkts); +} + +srs_error_t SrsRtspRtpBuilder::filter(SrsSharedPtrMessage* msg, SrsFormat* format, bool& has_idr, vector& samples) +{ + srs_error_t err = srs_success; + + // If IDR, we will insert SPS/PPS before IDR frame. + if (format->video && format->video->has_idr) { + has_idr = true; + } + + // Update samples to shared frame. + for (int i = 0; i < format->video->nb_samples; ++i) { + SrsSample* sample = &format->video->samples[i]; + samples.push_back(sample); + } + + return err; +} + +srs_error_t SrsRtspRtpBuilder::package_stap_a(SrsSharedPtrMessage* msg, SrsRtpPacket* pkt) +{ + srs_error_t err = srs_success; + + SrsFormat* format = meta->vsh_format(); + if (!format || !format->vcodec) { + return err; + } + + return video_builder_->package_stap_a(msg, pkt); +} + +srs_error_t SrsRtspRtpBuilder::package_nalus(SrsSharedPtrMessage* msg, const vector& samples, vector& pkts) +{ + srs_error_t err = srs_success; + + SrsFormat* format = meta->vsh_format(); + if (!format || !format->vcodec) { + return err; + } + + return video_builder_->package_nalus(msg, samples, pkts); +} + +// Single NAL Unit Packet @see https://tools.ietf.org/html/rfc6184#section-5.6 +srs_error_t SrsRtspRtpBuilder::package_single_nalu(SrsSharedPtrMessage* msg, SrsSample* sample, vector& pkts) +{ + return video_builder_->package_single_nalu(msg, sample, pkts); +} + +srs_error_t SrsRtspRtpBuilder::package_fu_a(SrsSharedPtrMessage* msg, SrsSample* sample, int fu_payload_size, vector& pkts) +{ + srs_error_t err = srs_success; + + SrsFormat* format = meta->vsh_format(); + if (!format || !format->vcodec) { + return err; + } + + return video_builder_->package_fu_a(msg, sample, fu_payload_size, pkts); +} + +srs_error_t SrsRtspRtpBuilder::consume_packets(vector& pkts) +{ + srs_error_t err = srs_success; + + // TODO: FIXME: Consume a range of packets. + for (int i = 0; i < (int)pkts.size(); i++) { + SrsRtpPacket* pkt = pkts[i]; + if ((err = bridge_->on_rtp(pkt)) != srs_success) { + err = srs_error_wrap(err, "consume sps/pps"); + break; + } + } + + return err; +} + +SrsRtspSendTrack::SrsRtspSendTrack(SrsRtspConnection* session, SrsRtcTrackDescription* track_desc, bool is_audio) +{ + session_ = session; + track_desc_ = track_desc->copy(); +} + +SrsRtspSendTrack::~SrsRtspSendTrack() +{ + srs_freep(track_desc_); +} + +bool SrsRtspSendTrack::has_ssrc(uint32_t ssrc) +{ + return track_desc_->has_ssrc(ssrc); +} + +// TODO: FIXME: Should refine logs, set tracks in a time. +bool SrsRtspSendTrack::set_track_status(bool active) +{ + bool previous_status = track_desc_->is_active_; + track_desc_->is_active_ = active; + return previous_status; +} + +bool SrsRtspSendTrack::get_track_status() +{ + return track_desc_->is_active_; +} + +std::string SrsRtspSendTrack::get_track_id() +{ + return track_desc_->id_; +} + +SrsRtspAudioSendTrack::SrsRtspAudioSendTrack(SrsRtspConnection* session, SrsRtcTrackDescription* track_desc) + : SrsRtspSendTrack(session, track_desc, true) +{ +} + +SrsRtspAudioSendTrack::~SrsRtspAudioSendTrack() +{ +} + +srs_error_t SrsRtspAudioSendTrack::on_rtp(SrsRtpPacket* pkt) +{ + srs_error_t err = srs_success; + + if (!track_desc_->is_active_) { + return err; + } + + pkt->header.set_ssrc(track_desc_->ssrc_); + + // Should update PT, because subscriber may use different PT to publisher. + if (track_desc_->media_ && pkt->header.get_payload_type() == track_desc_->media_->pt_of_publisher_) { + // If PT is media from publisher, change to PT of media for subscriber. + pkt->header.set_payload_type(track_desc_->media_->pt_); + } else if (track_desc_->red_ && pkt->header.get_payload_type() == track_desc_->red_->pt_of_publisher_) { + // If PT is RED from publisher, change to PT of RED for subscriber. + pkt->header.set_payload_type(track_desc_->red_->pt_); + } else { + // TODO: FIXME: Should update PT for RTX. + } + + if ((err = session_->do_send_packet(pkt)) != srs_success) { + return srs_error_wrap(err, "raw send"); + } + + srs_info("RTSP: Send audio ssrc=%d, seqno=%d, keyframe=%d, ts=%u", pkt->header.get_ssrc(), + pkt->header.get_sequence(), pkt->is_keyframe(), pkt->header.get_timestamp()); + + return err; +} + +SrsRtspVideoSendTrack::SrsRtspVideoSendTrack(SrsRtspConnection* session, SrsRtcTrackDescription* track_desc) + : SrsRtspSendTrack(session, track_desc, false) +{ +} + +SrsRtspVideoSendTrack::~SrsRtspVideoSendTrack() +{ +} + +srs_error_t SrsRtspVideoSendTrack::on_rtp(SrsRtpPacket* pkt) +{ + srs_error_t err = srs_success; + + if (!track_desc_->is_active_) { + return err; + } + + pkt->header.set_ssrc(track_desc_->ssrc_); + + // Should update PT, because subscriber may use different PT to publisher. + if (track_desc_->media_ && pkt->header.get_payload_type() == track_desc_->media_->pt_of_publisher_) { + // If PT is media from publisher, change to PT of media for subscriber. + pkt->header.set_payload_type(track_desc_->media_->pt_); + } else if (track_desc_->red_ && pkt->header.get_payload_type() == track_desc_->red_->pt_of_publisher_) { + // If PT is RED from publisher, change to PT of RED for subscriber. + pkt->header.set_payload_type(track_desc_->red_->pt_); + } else { + // TODO: FIXME: Should update PT for RTX. + } + + if ((err = session_->do_send_packet(pkt)) != srs_success) { + return srs_error_wrap(err, "raw send"); + } + + srs_info("RTSP: Send video ssrc=%d, seqno=%d, keyframe=%d, ts=%u", pkt->header.get_ssrc(), + pkt->header.get_sequence(), pkt->is_keyframe(), pkt->header.get_timestamp()); + + return err; +} + diff --git a/trunk/src/app/srs_app_rtsp_source.hpp b/trunk/src/app/srs_app_rtsp_source.hpp new file mode 100644 index 000000000..11b6d0abe --- /dev/null +++ b/trunk/src/app/srs_app_rtsp_source.hpp @@ -0,0 +1,256 @@ +// +// Copyright (c) 2013-2025 The SRS Authors +// +// SPDX-License-Identifier: MIT +// + +#ifndef SRS_APP_RTSP_SOURCE_HPP +#define SRS_APP_RTSP_SOURCE_HPP + +#include + +#include +#include +#include + +#include +#include +#include + +class SrsRequest; +class SrsRtpPacket; +class SrsRtspSource; +class SrsRtspConsumer; +class SrsRtcTrackDescription; +class SrsRtcSourceDescription; +class SrsResourceManager; +class SrsRtspConnection; +class SrsRtpVideoBuilder; + +// The RTSP stream consumer, consume packets from RTSP stream source. +class SrsRtspConsumer +{ +private: + // Because source references to this object, so we should directly use the source ptr. + SrsRtspSource* source_; +private: + std::vector queue; + // when source id changed, notice all consumers + bool should_update_source_id; + // The cond wait for mw. + srs_cond_t mw_wait; + bool mw_waiting; + int mw_min_msgs; +private: + // The callback for stream change event. + ISrsRtcSourceChangeCallback* handler_; +public: + SrsRtspConsumer(SrsRtspSource* s); + virtual ~SrsRtspConsumer(); +public: + // When source id changed, notice client to print. + virtual void update_source_id(); + // Put RTP packet into queue. + // @note We do not drop packet here, but drop it in sender. + srs_error_t enqueue(SrsRtpPacket* pkt); + // For RTSP, we only got one packet, because there is not many packets in queue. + virtual srs_error_t dump_packet(SrsRtpPacket** ppkt); + // Wait for at-least some messages incoming in queue. + virtual void wait(int nb_msgs); +public: + void set_handler(ISrsRtcSourceChangeCallback* h) { handler_ = h; } // SrsRtspConsumer::set_handler() + void on_stream_change(SrsRtcSourceDescription* desc); +}; + +class SrsRtspSourceManager : public ISrsHourGlass +{ +private: + srs_mutex_t lock; + std::map< std::string, SrsSharedPtr > pool; + SrsHourGlass* timer_; +public: + SrsRtspSourceManager(); + virtual ~SrsRtspSourceManager(); +public: + virtual srs_error_t initialize(); +// interface ISrsHourGlass +private: + virtual srs_error_t setup_ticks(); + virtual srs_error_t notify(int event, srs_utime_t interval, srs_utime_t tick); +public: + // create source when fetch from cache failed. + // @param r the client request. + // @param pps the matched source, if success never be NULL. + virtual srs_error_t fetch_or_create(SrsRequest* r, SrsSharedPtr& pps); +public: + // Get the exists source, NULL when not exists. + virtual SrsSharedPtr fetch(SrsRequest* r); +}; + +// The global RTSP source manager. +extern SrsRtspSourceManager* _srs_rtsp_sources; + +extern SrsResourceManager* _srs_rtsp_manager; + +// A Source is a stream, to publish and to play with, binding to SrsRtspPlayStream. +class SrsRtspSource +{ +private: + // For publish, it's the publish client id. + // For edge, it's the edge ingest id. + // when source id changed, for example, the edge reconnect, + // invoke the on_source_changed() to let all clients know. + SrsContextId _source_id; + // previous source id. + SrsContextId _pre_source_id; + SrsRequest* req; + // Steam description for this steam. + SrsRtcTrackDescription* audio_desc_; + SrsRtcTrackDescription* video_desc_; +private: + // To delivery stream to clients. + std::vector consumers; + // Whether stream is created, that is, SDP is done. + bool is_created_; + // Whether stream is delivering data, that is, DTLS is done. + bool is_delivering_packets_; +private: + // The last die time, while die means neither publishers nor players. + srs_utime_t stream_die_at_; +public: + SrsRtspSource(); + virtual ~SrsRtspSource(); +public: + virtual srs_error_t initialize(SrsRequest* r); +public: + // Whether stream is dead, which is no publisher or player. + virtual bool stream_is_dead(); +public: + // Update the authentication information in request. + virtual void update_auth(SrsRequest* r); +private: + // The stream source changed. + virtual srs_error_t on_source_changed(); +public: + // Get current source id. + virtual SrsContextId source_id(); + virtual SrsContextId pre_source_id(); +public: + // Create consumer + // @param consumer, output the create consumer. + virtual srs_error_t create_consumer(SrsRtspConsumer*& consumer); + // Dumps packets in cache to consumer. + // @param ds, whether dumps the sequence header. + // @param dm, whether dumps the metadata. + // @param dg, whether dumps the gop cache. + virtual srs_error_t consumer_dumps(SrsRtspConsumer* consumer, bool ds = true, bool dm = true, bool dg = true); + virtual void on_consumer_destroy(SrsRtspConsumer* consumer); + // Whether we can publish stream to the source, return false if it exists. + // @remark Note that when SDP is done, we set the stream is not able to publish. + virtual bool can_publish(); + // For RTSP, the stream is created when SDP is done, and then do DTLS + virtual void set_stream_created(); + // When start publish stream. + virtual srs_error_t on_publish(); + // When stop publish stream. + virtual void on_unpublish(); +public: + // Consume the shared RTP packet, user must free it. + srs_error_t on_rtp(SrsRtpPacket* pkt); +public: + SrsRtcTrackDescription* audio_desc(); + void set_audio_desc(SrsRtcTrackDescription* audio_desc); + SrsRtcTrackDescription* video_desc(); + void set_video_desc(SrsRtcTrackDescription* video_desc); +}; + +// Convert AV frame to RTSP RTP packets. +class SrsRtspRtpBuilder +{ +private: + SrsRequest* req; + SrsFrameToRtspBridge* bridge_; + // The format, codec information. + SrsRtmpFormat* format; + // The metadata cache. + SrsMetaCache* meta; + // The video builder, convert frame to RTP packets. + SrsRtpVideoBuilder* video_builder_; +private: + uint16_t audio_sequence; + uint32_t audio_ssrc_; + uint8_t audio_payload_type_; + int audio_sample_rate_; +private: + SrsSharedPtr source_; + // Lazy initialization flags + bool audio_initialized_; + bool video_initialized_; +public: + SrsRtspRtpBuilder(SrsFrameToRtspBridge* bridge, SrsSharedPtr source); + virtual ~SrsRtspRtpBuilder(); +private: + // Lazy initialization methods + srs_error_t initialize_audio_track(SrsAudioCodecId codec); + srs_error_t initialize_video_track(SrsVideoCodecId codec); +public: + virtual srs_error_t initialize(SrsRequest* r); + virtual srs_error_t on_publish(); + virtual void on_unpublish(); + virtual srs_error_t on_frame(SrsSharedPtrMessage* frame); +private: + virtual srs_error_t on_audio(SrsSharedPtrMessage* msg); +private: + srs_error_t package_aac(SrsAudioFrame* audio, SrsRtpPacket* pkt); +private: + virtual srs_error_t on_video(SrsSharedPtrMessage* msg); +private: + srs_error_t filter(SrsSharedPtrMessage* msg, SrsFormat* format, bool& has_idr, std::vector& samples); + srs_error_t package_stap_a(SrsSharedPtrMessage* msg, SrsRtpPacket* pkt); + srs_error_t package_nalus(SrsSharedPtrMessage* msg, const std::vector& samples, std::vector& pkts); + srs_error_t package_single_nalu(SrsSharedPtrMessage* msg, SrsSample* sample, std::vector& pkts); + srs_error_t package_fu_a(SrsSharedPtrMessage* msg, SrsSample* sample, int fu_payload_size, std::vector& pkts); + srs_error_t consume_packets(std::vector& pkts); +}; + +class SrsRtspSendTrack +{ +public: + // send track description + SrsRtcTrackDescription* track_desc_; +protected: + // The owner connection for this track. + SrsRtspConnection* session_; +public: + SrsRtspSendTrack(SrsRtspConnection* session, SrsRtcTrackDescription* track_desc, bool is_audio); + virtual ~SrsRtspSendTrack(); +public: + // SrsRtspSendTrack::set_nack_no_copy + bool has_ssrc(uint32_t ssrc); + bool set_track_status(bool active); + bool get_track_status(); + std::string get_track_id(); +public: + virtual srs_error_t on_rtp(SrsRtpPacket* pkt) = 0; +}; + +class SrsRtspAudioSendTrack : public SrsRtspSendTrack +{ +public: + SrsRtspAudioSendTrack(SrsRtspConnection* session, SrsRtcTrackDescription* track_desc); + virtual ~SrsRtspAudioSendTrack(); +public: + virtual srs_error_t on_rtp(SrsRtpPacket* pkt); +}; + +class SrsRtspVideoSendTrack : public SrsRtspSendTrack +{ +public: + SrsRtspVideoSendTrack(SrsRtspConnection* session, SrsRtcTrackDescription* track_desc); + virtual ~SrsRtspVideoSendTrack(); +public: + virtual srs_error_t on_rtp(SrsRtpPacket* pkt); +}; + +#endif + diff --git a/trunk/src/app/srs_app_server.cpp b/trunk/src/app/srs_app_server.cpp index d1d5c7fbe..78e505f8d 100644 --- a/trunk/src/app/srs_app_server.cpp +++ b/trunk/src/app/srs_app_server.cpp @@ -48,6 +48,10 @@ using namespace std; #ifdef SRS_SRT #include #endif +#ifdef SRS_RTSP +#include +#include +#endif SrsSignalManager* SrsSignalManager::instance = NULL; @@ -341,6 +345,9 @@ SrsServer::SrsServer() http_listener_ = new SrsTcpListener(this); https_listener_ = new SrsTcpListener(this); webrtc_listener_ = new SrsTcpListener(this); +#ifdef SRS_RTSP + rtsp_listener_ = new SrsTcpListener(this); +#endif stream_caster_flv_listener_ = new SrsHttpFlvListener(); stream_caster_mpegts_ = new SrsUdpCasterListener(); exporter_listener_ = new SrsTcpListener(this); @@ -398,6 +405,9 @@ void SrsServer::destroy() srs_freep(http_listener_); srs_freep(https_listener_); srs_freep(webrtc_listener_); +#ifdef SRS_RTSP + srs_freep(rtsp_listener_); +#endif srs_freep(stream_caster_flv_listener_); srs_freep(stream_caster_mpegts_); srs_freep(exporter_listener_); @@ -417,6 +427,9 @@ void SrsServer::dispose() http_listener_->close(); https_listener_->close(); webrtc_listener_->close(); +#ifdef SRS_RTSP + rtsp_listener_->close(); +#endif stream_caster_flv_listener_->close(); stream_caster_mpegts_->close(); exporter_listener_->close(); @@ -448,6 +461,9 @@ void SrsServer::gracefully_dispose() http_listener_->close(); https_listener_->close(); webrtc_listener_->close(); +#ifdef SRS_RTSP + rtsp_listener_->close(); +#endif stream_caster_flv_listener_->close(); stream_caster_mpegts_->close(); exporter_listener_->close(); @@ -630,6 +646,16 @@ srs_error_t SrsServer::listen() } #endif +#ifdef SRS_RTSP + // Start RTSP listener. RTC is a critical dependency. + if (_srs_config->get_rtsp_server_enabled()) { + rtsp_listener_->set_endpoint(srs_int2str(_srs_config->get_rtsp_server_listen()))->set_label("RTSP"); + if ((err = rtsp_listener_->listen()) != srs_success) { + return srs_error_wrap(err, "rtsp listen"); + } + } +#endif + // Start all listeners for stream caster. std::vector confs = _srs_config->get_stream_casters(); for (vector::iterator it = confs.begin(); it != confs.end(); ++it) { @@ -838,6 +864,12 @@ srs_error_t SrsServer::start(SrsWaitGroup* wg) } #endif +#ifdef SRS_RTSP + if ((err = _srs_rtsp_sources->initialize()) != srs_success) { + return srs_error_wrap(err, "rtsp sources"); + } +#endif + if ((err = trd_->start()) != srs_success) { return srs_error_wrap(err, "start"); } @@ -1137,6 +1169,14 @@ void SrsServer::resample_kbps() continue; } +#ifdef SRS_RTSP + SrsRtspConnection* rtsp = dynamic_cast(c); + if (rtsp) { + stat->kbps_add_delta(c->get_id().c_str(), rtsp->delta()); + continue; + } +#endif + #ifdef SRS_RTC SrsRtcTcpConn* tcp = dynamic_cast(c); if (tcp) { @@ -1244,6 +1284,10 @@ srs_error_t SrsServer::do_on_tcp_client(ISrsListener* listener, srs_netfd_t& stf #ifdef SRS_RTC } else if (listener == webrtc_listener_) { resource = new SrsRtcTcpConn(new SrsTcpConnection(stfd2), ip, port); +#endif +#ifdef SRS_RTSP + } else if (listener == rtsp_listener_) { + resource = new SrsRtspConnection(this, new SrsTcpConnection(stfd2), ip, port); #endif } else if (listener == exporter_listener_) { // TODO: FIXME: Maybe should support https metrics. diff --git a/trunk/src/app/srs_app_server.hpp b/trunk/src/app/srs_app_server.hpp index 78e9b8b6c..801990458 100644 --- a/trunk/src/app/srs_app_server.hpp +++ b/trunk/src/app/srs_app_server.hpp @@ -130,6 +130,8 @@ private: SrsTcpListener* https_listener_; // WebRTC over TCP listener. Please note that there is always a UDP listener by RTC server. SrsTcpListener* webrtc_listener_; + // RTSP listener, over TCP. + SrsTcpListener* rtsp_listener_; // Stream Caster for push over HTTP-FLV. SrsHttpFlvListener* stream_caster_flv_listener_; // Stream Caster for push over MPEGTS-UDP diff --git a/trunk/src/app/srs_app_stream_bridge.cpp b/trunk/src/app/srs_app_stream_bridge.cpp index cc9bf0e89..41aba6e96 100644 --- a/trunk/src/app/srs_app_stream_bridge.cpp +++ b/trunk/src/app/srs_app_stream_bridge.cpp @@ -13,6 +13,9 @@ #include #include #include +#ifdef SRS_RTSP +#include +#endif #include using namespace std; @@ -134,6 +137,61 @@ srs_error_t SrsFrameToRtcBridge::on_rtp(SrsRtpPacket* pkt) #endif +#ifdef SRS_RTSP +SrsFrameToRtspBridge::SrsFrameToRtspBridge(SrsSharedPtr source) +{ + source_ = source; + + // Use lazy initialization - no need to determine codec/track parameters here + rtp_builder_ = new SrsRtspRtpBuilder(this, source); +} + +SrsFrameToRtspBridge::~SrsFrameToRtspBridge() +{ + srs_freep(rtp_builder_); +} + +srs_error_t SrsFrameToRtspBridge::initialize(SrsRequest* r) +{ + return rtp_builder_->initialize(r); +} + +srs_error_t SrsFrameToRtspBridge::on_publish() +{ + srs_error_t err = srs_success; + + // TODO: FIXME: Should sync with bridge? + if ((err = source_->on_publish()) != srs_success) { + return srs_error_wrap(err, "source publish"); + } + + if ((err = rtp_builder_->on_publish()) != srs_success) { + return srs_error_wrap(err, "rtp builder publish"); + } + + return err; +} + +void SrsFrameToRtspBridge::on_unpublish() +{ + rtp_builder_->on_unpublish(); + + // @remark This bridge might be disposed here, so never use it. + // TODO: FIXME: Should sync with bridge? + source_->on_unpublish(); +} + +srs_error_t SrsFrameToRtspBridge::on_frame(SrsSharedPtrMessage* frame) +{ + return rtp_builder_->on_frame(frame); +} + +srs_error_t SrsFrameToRtspBridge::on_rtp(SrsRtpPacket* pkt) +{ + return source_->on_rtp(pkt); +} +#endif + SrsCompositeBridge::SrsCompositeBridge() { } diff --git a/trunk/src/app/srs_app_stream_bridge.hpp b/trunk/src/app/srs_app_stream_bridge.hpp index dfa4857c8..5daf8981a 100644 --- a/trunk/src/app/srs_app_stream_bridge.hpp +++ b/trunk/src/app/srs_app_stream_bridge.hpp @@ -22,6 +22,8 @@ class SrsRtmpFormat; class SrsMetaCache; class SrsRtpPacket; class SrsRtcRtpBuilder; +class SrsRtspSource; +class SrsRtspRtpBuilder; // A stream bridge is used to convert stream via different protocols, such as bridge for RTMP and RTC. Generally, we use // frame as message for bridge. A frame is a audio or video frame, such as an I/B/P frame, a general frame for decoder. @@ -77,12 +79,34 @@ public: }; #endif +#ifdef SRS_RTSP +// A bridge to covert AV frame to RTSP stream. +class SrsFrameToRtspBridge : public ISrsStreamBridge +{ +private: + SrsSharedPtr source_; +private: + SrsRtspRtpBuilder* rtp_builder_; +public: + SrsFrameToRtspBridge(SrsSharedPtr source); + virtual ~SrsFrameToRtspBridge(); +public: + virtual srs_error_t initialize(SrsRequest* r); + virtual srs_error_t on_publish(); + virtual void on_unpublish(); + virtual srs_error_t on_frame(SrsSharedPtrMessage* frame); + srs_error_t on_rtp(SrsRtpPacket* pkt); +}; +#endif + // A bridge chain, a set of bridges. class SrsCompositeBridge : public ISrsStreamBridge { public: SrsCompositeBridge(); virtual ~SrsCompositeBridge(); +public: + bool empty() { return bridges_.empty(); } // SrsCompositeBridge::empty() public: srs_error_t initialize(SrsRequest* r); public: diff --git a/trunk/src/app/srs_app_threads.cpp b/trunk/src/app/srs_app_threads.cpp index 12e4587a6..aee7a39e8 100644 --- a/trunk/src/app/srs_app_threads.cpp +++ b/trunk/src/app/srs_app_threads.cpp @@ -28,6 +28,9 @@ #ifdef SRS_GB28181 #include #endif +#ifdef SRS_RTSP +#include +#endif #include #include @@ -331,6 +334,10 @@ srs_error_t srs_global_initialize() _srs_rtc_manager = new SrsResourceManager("RTC", true); _srs_rtc_dtls_certificate = new SrsDtlsCertificate(); #endif +#ifdef SRS_RTSP + _srs_rtsp_sources = new SrsRtspSourceManager(); + _srs_rtsp_manager = new SrsResourceManager("RTSP", true); +#endif #ifdef SRS_GB28181 _srs_gb_manager = new SrsResourceManager("GB", true); #endif diff --git a/trunk/src/core/srs_core_version7.hpp b/trunk/src/core/srs_core_version7.hpp index 74c774f4f..deb6c501d 100644 --- a/trunk/src/core/srs_core_version7.hpp +++ b/trunk/src/core/srs_core_version7.hpp @@ -9,6 +9,6 @@ #define VERSION_MAJOR 7 #define VERSION_MINOR 0 -#define VERSION_REVISION 46 +#define VERSION_REVISION 47 #endif \ No newline at end of file diff --git a/trunk/src/kernel/srs_kernel_codec.cpp b/trunk/src/kernel/srs_kernel_codec.cpp index 5ac35c6a9..9971a7dc3 100644 --- a/trunk/src/kernel/srs_kernel_codec.cpp +++ b/trunk/src/kernel/srs_kernel_codec.cpp @@ -908,7 +908,7 @@ srs_error_t SrsFormat::on_audio(int64_t timestamp, char* data, int size) uint8_t v = buffer->read_1bytes(); SrsAudioCodecId codec = (SrsAudioCodecId)((v >> 4) & 0x0f); - if (codec != SrsAudioCodecIdMP3 && codec != SrsAudioCodecIdAAC) { + if (codec != SrsAudioCodecIdMP3 && codec != SrsAudioCodecIdAAC && codec != SrsAudioCodecIdOpus) { return err; } @@ -929,9 +929,11 @@ srs_error_t SrsFormat::on_audio(int64_t timestamp, char* data, int size) if (codec == SrsAudioCodecIdMP3) { return audio_mp3_demux(buffer.get(), timestamp, fresh); + } else if (codec == SrsAudioCodecIdAAC) { + return audio_aac_demux(buffer.get(), timestamp); + } else { + return srs_error_new(ERROR_NOT_IMPLEMENTED, "opus demuxer not implemented"); } - - return audio_aac_demux(buffer.get(), timestamp); } srs_error_t SrsFormat::on_video(int64_t timestamp, char* data, int size) diff --git a/trunk/src/kernel/srs_kernel_error.hpp b/trunk/src/kernel/srs_kernel_error.hpp index afc7f904f..e19ac622f 100644 --- a/trunk/src/kernel/srs_kernel_error.hpp +++ b/trunk/src/kernel/srs_kernel_error.hpp @@ -108,7 +108,8 @@ XX(ERROR_SYSTEM_FILE_NOT_OPEN , 1095, "FileNotOpen", "File is not opened") \ XX(ERROR_SYSTEM_FILE_SETVBUF , 1096, "FileSetVBuf", "Failed to set file vbuf") \ XX(ERROR_NO_SOURCE , 1097, "NoSource", "No source found") \ - XX(ERROR_STREAM_DISPOSING , 1098, "StreamDisposing", "Stream is disposing") + XX(ERROR_STREAM_DISPOSING , 1098, "StreamDisposing", "Stream is disposing") \ + XX(ERROR_NOT_IMPLEMENTED , 1099, "NotImplemented", "Feature is not implemented") /**************************************************/ /* RTMP protocol error. */ @@ -155,12 +156,6 @@ XX(ERROR_OpenSslComputeSharedKey , 2039, "SslShareKey", "Failed to get shared key of SSL") \ XX(ERROR_RTMP_MIC_CHUNKSIZE_CHANGED , 2040, "RtmpMicChunk", "Invalid RTMP mic for chunk size changed") \ XX(ERROR_RTMP_MIC_CACHE_OVERFLOW , 2041, "RtmpMicCache", "Invalid RTMP mic for cache overflow") \ - XX(ERROR_RTSP_TOKEN_NOT_NORMAL , 2042, "RtspToken", "Invalid RTSP token state not normal") \ - XX(ERROR_RTSP_REQUEST_HEADER_EOF , 2043, "RtspHeaderEof", "Invalid RTSP request for header EOF") \ - XX(ERROR_RTP_HEADER_CORRUPT , 2044, "RtspHeaderCorrupt", "Invalid RTSP RTP packet for header corrupt") \ - XX(ERROR_RTP_TYPE96_CORRUPT , 2045, "RtspP96Corrupt", "Invalid RTSP RTP packet for P96 corrupt") \ - XX(ERROR_RTP_TYPE97_CORRUPT , 2046, "RtspP97Corrupt", "Invalid RTSP RTP packet for P97 corrupt") \ - XX(ERROR_RTSP_AUDIO_CONFIG , 2047, "RtspAudioConfig", "RTSP no audio sequence header config") \ XX(ERROR_RTMP_STREAM_NOT_FOUND , 2048, "StreamNotFound", "Request stream is not found") \ XX(ERROR_RTMP_CLIENT_NOT_FOUND , 2049, "ClientNotFound", "Request client is not found") \ XX(ERROR_OpenSslCreateHMAC , 2050, "SslCreateHmac", "Failed to create HMAC for SSL") \ @@ -340,7 +335,7 @@ /**************************************************/ -/* RTC protocol error. */ +/* RTC/RTSP protocol error. */ #define SRS_ERRNO_MAP_RTC(XX) \ XX(ERROR_RTC_PORT , 5000, "RtcPort", "Invalid RTC config for listen port") \ XX(ERROR_RTP_PACKET_CREATE , 5001, "RtcPacketCreate", "Failed to create RTP packet for RTC") \ @@ -379,7 +374,12 @@ XX(ERROR_RTC_TCP_STUN , 5034, "RtcTcpSession", "RTC TCP packet is invalid for session not found") \ XX(ERROR_RTC_TCP_UNIQUE , 5035, "RtcUnique", "RTC only support one UDP or TCP network") \ XX(ERROR_RTC_INVALID_SESSION , 5036, "RtcInvalidSession", "Invalid request for no RTC session matched") \ - XX(ERROR_RTC_INVALID_ICE , 5037, "RtcInvalidIce", "Invalid ICE ufrag or pwd") + XX(ERROR_RTC_INVALID_ICE , 5037, "RtcInvalidIce", "Invalid ICE ufrag or pwd") \ + XX(ERROR_RTSP_TRANSPORT_NOT_SUPPORTED , 5038, "RtspTransportNotSupported", "RTSP transport not supported, only TCP/interleaved mode is supported") \ + XX(ERROR_RTSP_NO_TRACK , 5039, "RtspNoTrack", "Drop RTSP packet for track not found") \ + XX(ERROR_RTSP_TOKEN_NOT_NORMAL , 5040, "RtspToken", "Invalid RTSP token state not normal") \ + XX(ERROR_RTSP_REQUEST_HEADER_EOF , 5041, "RtspHeaderEof", "Invalid RTSP request for header EOF") \ + XX(ERROR_RTSP_NEED_MORE_DATA , 5042, "RtspNeedMoreData", "Need more data to complete RTCP frame parsing") /**************************************************/ /* SRT protocol error. */ diff --git a/trunk/src/kernel/srs_kernel_rtc_rtp.hpp b/trunk/src/kernel/srs_kernel_rtc_rtp.hpp index 09d48008b..509886007 100644 --- a/trunk/src/kernel/srs_kernel_rtc_rtp.hpp +++ b/trunk/src/kernel/srs_kernel_rtc_rtp.hpp @@ -31,6 +31,16 @@ class SrsRtpPacket; // The RTP packet max size, should never exceed this size. const int kRtpPacketSize = 1500; +// The RTP payload max size, reserved some paddings for SRTP as such: +// kRtpPacketSize = kRtpMaxPayloadSize + paddings +// For example, if kRtpPacketSize is 1500, recommend to set kRtpMaxPayloadSize to 1400, +// which reserves 100 bytes for SRTP or paddings. +// otherwise, the kRtpPacketSize must less than MTU, in webrtc source code, +// the rtp max size is assigned by kVideoMtu = 1200. +// so we set kRtpMaxPayloadSize = 1200. +// see @doc https://groups.google.com/g/discuss-webrtc/c/gH5ysR3SoZI +const int kRtpMaxPayloadSize = kRtpPacketSize - 300; + const int kRtpHeaderFixedSize = 12; const uint8_t kRtpMarker = 0x80; diff --git a/trunk/src/protocol/srs_protocol_rtp.cpp b/trunk/src/protocol/srs_protocol_rtp.cpp new file mode 100644 index 000000000..b56e8adb2 --- /dev/null +++ b/trunk/src/protocol/srs_protocol_rtp.cpp @@ -0,0 +1,305 @@ +// +// Copyright (c) 2013-2025 The SRS Authors +// +// SPDX-License-Identifier: MIT +// + +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace std; + +SrsRtpVideoBuilder::SrsRtpVideoBuilder() +{ + video_sequence_ = 0; + video_ssrc_ = 0; + video_payload_type_ = 0; + format_ = NULL; +} + +SrsRtpVideoBuilder::~SrsRtpVideoBuilder() +{ +} + +srs_error_t SrsRtpVideoBuilder::initialize(SrsFormat* format, uint32_t ssrc, uint8_t payload_type) +{ + format_ = format; + video_ssrc_ = ssrc; + video_payload_type_ = payload_type; + return srs_success; +} + +srs_error_t SrsRtpVideoBuilder::package_stap_a(SrsSharedPtrMessage* msg, SrsRtpPacket* pkt) +{ + srs_error_t err = srs_success; + + SrsFormat* format = format_; + if (!format || !format->vcodec) { + return err; + } + + pkt->header.set_payload_type(video_payload_type_); + pkt->header.set_ssrc(video_ssrc_); + pkt->frame_type = SrsFrameTypeVideo; + pkt->header.set_marker(false); + pkt->header.set_sequence(video_sequence_++); + pkt->header.set_timestamp(msg->timestamp * 90); + + ISrsRtpPayloader* stap = NULL; + vector*> params; + int size = 0; + + if (format->vcodec->id == SrsVideoCodecIdHEVC) { + for (size_t i = 0; i < format->vcodec->hevc_dec_conf_record_.nalu_vec.size(); i++) { + const SrsHevcHvccNalu& nalu = format->vcodec->hevc_dec_conf_record_.nalu_vec[i]; + if (nalu.nal_unit_type == SrsHevcNaluType_VPS + || nalu.nal_unit_type == SrsHevcNaluType_SPS + || nalu.nal_unit_type == SrsHevcNaluType_PPS) { + const SrsHevcNalData& nal_data = nalu.nal_data_vec[0]; + params.push_back(&(vector&)nal_data.nal_unit_data); + size += nal_data.nal_unit_length; + } + } + + stap = new SrsRtpSTAPPayloadHevc(); + pkt->set_payload(stap, SrsRtpPacketPayloadTypeSTAPHevc); + pkt->nalu_type = kStapHevc; + } else if (format->vcodec->id == SrsVideoCodecIdAVC) { + params.push_back(&format->vcodec->sequenceParameterSetNALUnit); + params.push_back(&format->vcodec->pictureParameterSetNALUnit); + size = format->vcodec->sequenceParameterSetNALUnit.size() + format->vcodec->pictureParameterSetNALUnit.size(); + + stap = new SrsRtpSTAPPayload(); + pkt->set_payload(stap, SrsRtpPacketPayloadTypeSTAP); + pkt->nalu_type = kStapA; + } + + if (size == 0) { + return srs_error_new(ERROR_RTC_RTP_MUXER, "vps/sps/pps empty"); + } + char* payload = pkt->wrap(size); + + for (vector*>::iterator it = params.begin(); it != params.end(); ++it) { + vector* param = *it; + SrsSample* sample = new SrsSample(); + sample->bytes = payload; + sample->size = param->size(); + if (format->vcodec->id == SrsVideoCodecIdHEVC) { + static_cast(stap)->nalus.push_back(sample); + } else { + static_cast(stap)->nalus.push_back(sample); + } + + memcpy(payload, (char*)param->data(), param->size()); + payload += (int)param->size(); + } + + return err; +} + +srs_error_t SrsRtpVideoBuilder::package_nalus(SrsSharedPtrMessage* msg, const vector& samples, vector& pkts) +{ + srs_error_t err = srs_success; + + SrsFormat* format = format_; + if (!format || !format->vcodec) { + return err; + } + bool is_hevc = format->vcodec->id == SrsVideoCodecIdHEVC; + + SrsRtpRawNALUs* raw_raw = new SrsRtpRawNALUs(); + uint8_t first_nalu_type = 0; + + for (int i = 0; i < (int)samples.size(); i++) { + SrsSample* sample = samples[i]; + + if (!sample->size) { + continue; + } + + if (first_nalu_type == 0) { + first_nalu_type = is_hevc ? uint8_t(SrsHevcNaluTypeParse(sample->bytes[0])) : uint8_t(SrsAvcNaluTypeParse(sample->bytes[0])); + } + + raw_raw->push_back(sample->copy()); + } + + // Ignore empty. + int nn_bytes = raw_raw->nb_bytes(); + if (nn_bytes <= 0) { + srs_freep(raw_raw); + return err; + } + + if (nn_bytes < kRtpMaxPayloadSize) { + // Package NALUs in a single RTP packet. + SrsRtpPacket* pkt = new SrsRtpPacket(); + pkts.push_back(pkt); + + pkt->header.set_payload_type(video_payload_type_); + pkt->header.set_ssrc(video_ssrc_); + pkt->frame_type = SrsFrameTypeVideo; + pkt->nalu_type = first_nalu_type; + pkt->header.set_sequence(video_sequence_++); + pkt->header.set_timestamp(msg->timestamp * 90); + pkt->set_payload(raw_raw, SrsRtpPacketPayloadTypeNALU); + pkt->wrap(msg); + } else { + // We must free it, should never use RTP packets to free it, + // because more than one RTP packet will refer to it. + SrsUniquePtr raw(raw_raw); + + int header_size = is_hevc ? SrsHevcNaluHeaderSize : SrsAvcNaluHeaderSize; + + // Package NALUs in FU-A RTP packets. + int fu_payload_size = kRtpMaxPayloadSize; + + // The first byte is store in FU-A header. + uint8_t header = raw->skip_bytes(header_size); + + int nb_left = nn_bytes - header_size; + + int num_of_packet = 1 + (nn_bytes - 1) / fu_payload_size; + for (int i = 0; i < num_of_packet; ++i) { + int packet_size = srs_min(nb_left, fu_payload_size); + + SrsRtpPacket* pkt = new SrsRtpPacket(); + pkts.push_back(pkt); + + pkt->header.set_payload_type(video_payload_type_); + pkt->header.set_ssrc(video_ssrc_); + pkt->frame_type = SrsFrameTypeVideo; + pkt->nalu_type = kFuA; + pkt->header.set_sequence(video_sequence_++); + pkt->header.set_timestamp(msg->timestamp * 90); + + if (is_hevc) { + SrsRtpFUAPayloadHevc* fua = new SrsRtpFUAPayloadHevc(); + if ((err = raw->read_samples(fua->nalus, packet_size)) != srs_success) { + srs_freep(fua); + return srs_error_wrap(err, "read hevc samples %d bytes, left %d, total %d", packet_size, nb_left, nn_bytes); + } + fua->nalu_type = SrsHevcNaluTypeParse(header); + fua->start = bool(i == 0); + fua->end = bool(i == num_of_packet - 1); + + pkt->set_payload(fua, SrsRtpPacketPayloadTypeFUAHevc); + } else { + SrsRtpFUAPayload* fua = new SrsRtpFUAPayload(); + if ((err = raw->read_samples(fua->nalus, packet_size)) != srs_success) { + srs_freep(fua); + return srs_error_wrap(err, "read samples %d bytes, left %d, total %d", packet_size, nb_left, nn_bytes); + } + fua->nalu_type = SrsAvcNaluTypeParse(header); + fua->start = bool(i == 0); + fua->end = bool(i == num_of_packet - 1); + + pkt->set_payload(fua, SrsRtpPacketPayloadTypeFUA); + } + + pkt->wrap(msg); + + nb_left -= packet_size; + } + } + + return err; +} + +// Single NAL Unit Packet @see https://tools.ietf.org/html/rfc6184#section-5.6 +srs_error_t SrsRtpVideoBuilder::package_single_nalu(SrsSharedPtrMessage* msg, SrsSample* sample, vector& pkts) +{ + srs_error_t err = srs_success; + + SrsRtpPacket* pkt = new SrsRtpPacket(); + pkts.push_back(pkt); + + pkt->header.set_payload_type(video_payload_type_); + pkt->header.set_ssrc(video_ssrc_); + pkt->frame_type = SrsFrameTypeVideo; + pkt->header.set_sequence(video_sequence_++); + pkt->header.set_timestamp(msg->timestamp * 90); + + SrsRtpRawPayload* raw = new SrsRtpRawPayload(); + pkt->set_payload(raw, SrsRtpPacketPayloadTypeRaw); + + raw->payload = sample->bytes; + raw->nn_payload = sample->size; + + pkt->wrap(msg); + + return err; +} + +srs_error_t SrsRtpVideoBuilder::package_fu_a(SrsSharedPtrMessage* msg, SrsSample* sample, int fu_payload_size, vector& pkts) +{ + srs_error_t err = srs_success; + + SrsFormat* format = format_; + if (!format || !format->vcodec) { + return err; + } + + bool is_hevc = format->vcodec->id == SrsVideoCodecIdHEVC; + int header_size = is_hevc ? SrsHevcNaluHeaderSize : SrsAvcNaluHeaderSize; + srs_assert(sample->size >= header_size); + + char* p = sample->bytes + header_size; + int nb_left = sample->size - header_size; + uint8_t header = sample->bytes[0]; + + int num_of_packet = 1 + (nb_left - 1) / fu_payload_size; + for (int i = 0; i < num_of_packet; ++i) { + int packet_size = srs_min(nb_left, fu_payload_size); + + SrsRtpPacket* pkt = new SrsRtpPacket(); + pkts.push_back(pkt); + + pkt->header.set_payload_type(video_payload_type_); + pkt->header.set_ssrc(video_ssrc_); + pkt->frame_type = SrsFrameTypeVideo; + pkt->header.set_sequence(video_sequence_++); + pkt->header.set_timestamp(msg->timestamp * 90); + pkt->nalu_type = is_hevc ? kFuHevc : kFuA; + + if (is_hevc) { + // H265 FU-A header + SrsRtpFUAPayloadHevc2* fua = new SrsRtpFUAPayloadHevc2(); + pkt->set_payload(fua, SrsRtpPacketPayloadTypeFUAHevc2); + + fua->nalu_type = SrsHevcNaluTypeParse(header); + fua->start = bool(i == 0); + fua->end = bool(i == num_of_packet - 1); + + fua->payload = p; + fua->size = packet_size; + } else { + // H264 FU-A header + SrsRtpFUAPayload2* fua = new SrsRtpFUAPayload2(); + pkt->set_payload(fua, SrsRtpPacketPayloadTypeFUA2); + + fua->nri = (SrsAvcNaluType)header; + fua->nalu_type = SrsAvcNaluTypeParse(header); + fua->start = bool(i == 0); + fua->end = bool(i == num_of_packet - 1); + + fua->payload = p; + fua->size = packet_size; + } + + pkt->wrap(msg); + + p += packet_size; + nb_left -= packet_size; + } + + return err; +} + diff --git a/trunk/src/protocol/srs_protocol_rtp.hpp b/trunk/src/protocol/srs_protocol_rtp.hpp new file mode 100644 index 000000000..c6e4211d8 --- /dev/null +++ b/trunk/src/protocol/srs_protocol_rtp.hpp @@ -0,0 +1,43 @@ +// +// Copyright (c) 2013-2025 The SRS Authors +// +// SPDX-License-Identifier: MIT +// + +#ifndef SRS_PROTOCOL_RTP_HPP +#define SRS_PROTOCOL_RTP_HPP + +#include + +#include + +#include +#include +#include +#include + +class SrsSharedPtrMessage; +class SrsSample; +class SrsRtpPacket; +class SrsFormat; + +// RTP video builder for packaging video NALUs into RTP packets +class SrsRtpVideoBuilder +{ +private: + uint16_t video_sequence_; + uint32_t video_ssrc_; + uint8_t video_payload_type_; + SrsFormat* format_; +public: + SrsRtpVideoBuilder(); + virtual ~SrsRtpVideoBuilder(); +public: + srs_error_t initialize(SrsFormat* format, uint32_t ssrc, uint8_t payload_type); + srs_error_t package_stap_a(SrsSharedPtrMessage* msg, SrsRtpPacket* pkt); + srs_error_t package_nalus(SrsSharedPtrMessage* msg, const std::vector& samples, std::vector& pkts); + srs_error_t package_single_nalu(SrsSharedPtrMessage* msg, SrsSample* sample, std::vector& pkts); + srs_error_t package_fu_a(SrsSharedPtrMessage* msg, SrsSample* sample, int fu_payload_size, std::vector& pkts); +}; + +#endif diff --git a/trunk/src/protocol/srs_protocol_rtsp_stack.cpp b/trunk/src/protocol/srs_protocol_rtsp_stack.cpp new file mode 100644 index 000000000..78994d805 --- /dev/null +++ b/trunk/src/protocol/srs_protocol_rtsp_stack.cpp @@ -0,0 +1,750 @@ +// +// Copyright (c) 2013-2025 The SRS Authors +// +// SPDX-License-Identifier: MIT +// + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +using namespace std; + +#define SRS_RTSP_BUFFER 4096 + +// Forward declaration of RTCP detection function +extern bool srs_is_rtcp(const uint8_t* data, size_t len); + +// get the status text of code. +string srs_generate_rtsp_status_text(int status) +{ + static std::map _status_map; + if (_status_map.empty()) { + _status_map[SRS_CONSTS_RTSP_Continue] = SRS_CONSTS_RTSP_Continue_str; + _status_map[SRS_CONSTS_RTSP_OK] = SRS_CONSTS_RTSP_OK_str; + _status_map[SRS_CONSTS_RTSP_Created] = SRS_CONSTS_RTSP_Created_str; + _status_map[SRS_CONSTS_RTSP_LowOnStorageSpace] = SRS_CONSTS_RTSP_LowOnStorageSpace_str; + _status_map[SRS_CONSTS_RTSP_MultipleChoices] = SRS_CONSTS_RTSP_MultipleChoices_str; + _status_map[SRS_CONSTS_RTSP_MovedPermanently] = SRS_CONSTS_RTSP_MovedPermanently_str; + _status_map[SRS_CONSTS_RTSP_MovedTemporarily] = SRS_CONSTS_RTSP_MovedTemporarily_str; + _status_map[SRS_CONSTS_RTSP_SeeOther] = SRS_CONSTS_RTSP_SeeOther_str; + _status_map[SRS_CONSTS_RTSP_NotModified] = SRS_CONSTS_RTSP_NotModified_str; + _status_map[SRS_CONSTS_RTSP_UseProxy] = SRS_CONSTS_RTSP_UseProxy_str; + _status_map[SRS_CONSTS_RTSP_BadRequest] = SRS_CONSTS_RTSP_BadRequest_str; + _status_map[SRS_CONSTS_RTSP_Unauthorized] = SRS_CONSTS_RTSP_Unauthorized_str; + _status_map[SRS_CONSTS_RTSP_PaymentRequired] = SRS_CONSTS_RTSP_PaymentRequired_str; + _status_map[SRS_CONSTS_RTSP_Forbidden] = SRS_CONSTS_RTSP_Forbidden_str; + _status_map[SRS_CONSTS_RTSP_NotFound] = SRS_CONSTS_RTSP_NotFound_str; + _status_map[SRS_CONSTS_RTSP_MethodNotAllowed] = SRS_CONSTS_RTSP_MethodNotAllowed_str; + _status_map[SRS_CONSTS_RTSP_NotAcceptable] = SRS_CONSTS_RTSP_NotAcceptable_str; + _status_map[SRS_CONSTS_RTSP_ProxyAuthenticationRequired] = SRS_CONSTS_RTSP_ProxyAuthenticationRequired_str; + _status_map[SRS_CONSTS_RTSP_RequestTimeout] = SRS_CONSTS_RTSP_RequestTimeout_str; + _status_map[SRS_CONSTS_RTSP_Gone] = SRS_CONSTS_RTSP_Gone_str; + _status_map[SRS_CONSTS_RTSP_LengthRequired] = SRS_CONSTS_RTSP_LengthRequired_str; + _status_map[SRS_CONSTS_RTSP_PreconditionFailed] = SRS_CONSTS_RTSP_PreconditionFailed_str; + _status_map[SRS_CONSTS_RTSP_RequestEntityTooLarge] = SRS_CONSTS_RTSP_RequestEntityTooLarge_str; + _status_map[SRS_CONSTS_RTSP_RequestURITooLarge] = SRS_CONSTS_RTSP_RequestURITooLarge_str; + _status_map[SRS_CONSTS_RTSP_UnsupportedMediaType] = SRS_CONSTS_RTSP_UnsupportedMediaType_str; + _status_map[SRS_CONSTS_RTSP_ParameterNotUnderstood] = SRS_CONSTS_RTSP_ParameterNotUnderstood_str; + _status_map[SRS_CONSTS_RTSP_ConferenceNotFound] = SRS_CONSTS_RTSP_ConferenceNotFound_str; + _status_map[SRS_CONSTS_RTSP_NotEnoughBandwidth] = SRS_CONSTS_RTSP_NotEnoughBandwidth_str; + _status_map[SRS_CONSTS_RTSP_SessionNotFound] = SRS_CONSTS_RTSP_SessionNotFound_str; + _status_map[SRS_CONSTS_RTSP_MethodNotValidInThisState] = SRS_CONSTS_RTSP_MethodNotValidInThisState_str; + _status_map[SRS_CONSTS_RTSP_HeaderFieldNotValidForResource] = SRS_CONSTS_RTSP_HeaderFieldNotValidForResource_str; + _status_map[SRS_CONSTS_RTSP_InvalidRange] = SRS_CONSTS_RTSP_InvalidRange_str; + _status_map[SRS_CONSTS_RTSP_ParameterIsReadOnly] = SRS_CONSTS_RTSP_ParameterIsReadOnly_str; + _status_map[SRS_CONSTS_RTSP_AggregateOperationNotAllowed] = SRS_CONSTS_RTSP_AggregateOperationNotAllowed_str; + _status_map[SRS_CONSTS_RTSP_OnlyAggregateOperationAllowed] = SRS_CONSTS_RTSP_OnlyAggregateOperationAllowed_str; + _status_map[SRS_CONSTS_RTSP_UnsupportedTransport] = SRS_CONSTS_RTSP_UnsupportedTransport_str; + _status_map[SRS_CONSTS_RTSP_DestinationUnreachable] = SRS_CONSTS_RTSP_DestinationUnreachable_str; + _status_map[SRS_CONSTS_RTSP_InternalServerError] = SRS_CONSTS_RTSP_InternalServerError_str; + _status_map[SRS_CONSTS_RTSP_NotImplemented] = SRS_CONSTS_RTSP_NotImplemented_str; + _status_map[SRS_CONSTS_RTSP_BadGateway] = SRS_CONSTS_RTSP_BadGateway_str; + _status_map[SRS_CONSTS_RTSP_ServiceUnavailable] = SRS_CONSTS_RTSP_ServiceUnavailable_str; + _status_map[SRS_CONSTS_RTSP_GatewayTimeout] = SRS_CONSTS_RTSP_GatewayTimeout_str; + _status_map[SRS_CONSTS_RTSP_RTSPVersionNotSupported] = SRS_CONSTS_RTSP_RTSPVersionNotSupported_str; + _status_map[SRS_CONSTS_RTSP_OptionNotSupported] = SRS_CONSTS_RTSP_OptionNotSupported_str; + } + + std::string status_text; + if (_status_map.find(status) == _status_map.end()) { + status_text = "Status Unknown"; + } else { + status_text = _status_map[status]; + } + + return status_text; +} + +std::string srs_generate_rtsp_method_str(SrsRtspMethod method) +{ + switch (method) { + case SrsRtspMethodDescribe: return SRS_RTSP_METHOD_DESCRIBE; + case SrsRtspMethodAnnounce: return SRS_RTSP_METHOD_ANNOUNCE; + case SrsRtspMethodGetParameter: return SRS_RTSP_METHOD_GET_PARAMETER; + case SrsRtspMethodOptions: return SRS_RTSP_METHOD_OPTIONS; + case SrsRtspMethodPause: return SRS_RTSP_METHOD_PAUSE; + case SrsRtspMethodPlay: return SRS_RTSP_METHOD_PLAY; + case SrsRtspMethodRecord: return SRS_RTSP_METHOD_RECORD; + case SrsRtspMethodRedirect: return SRS_RTSP_METHOD_REDIRECT; + case SrsRtspMethodSetup: return SRS_RTSP_METHOD_SETUP; + case SrsRtspMethodSetParameter: return SRS_RTSP_METHOD_SET_PARAMETER; + case SrsRtspMethodTeardown: return SRS_RTSP_METHOD_TEARDOWN; + default: return "Unknown"; + } +} + +SrsRtspTransport::SrsRtspTransport() +{ + client_port_min = 0; + client_port_max = 0; + interleaved_min = 0; + interleaved_max = 0; +} + +SrsRtspTransport::~SrsRtspTransport() +{ +} + +srs_error_t SrsRtspTransport::parse(string attr) +{ + srs_error_t err = srs_success; + + size_t pos = string::npos; + std::string token = attr; + + while (!token.empty()) { + std::string item = token; + if ((pos = item.find(";")) != string::npos) { + item = token.substr(0, pos); + token = token.substr(pos + 1); + } else { + token = ""; + } + + std::string item_key = item, item_value; + if ((pos = item.find("=")) != string::npos) { + item_key = item.substr(0, pos); + item_value = item.substr(pos + 1); + } + + if (transport.empty() && item.find("=") == string::npos && item_key != "unicast" && item_key != "multicast") { + transport = item_key; + if ((pos = transport.find("/")) != string::npos) { + profile = transport.substr(pos + 1); + transport = transport.substr(0, pos); + } + if ((pos = profile.find("/")) != string::npos) { + lower_transport = profile.substr(pos + 1); + profile = profile.substr(0, pos); + } + } + + if (item_key == "unicast" || item_key == "multicast") { + cast_type = item_key; + } else if (item_key == "interleaved") { + interleaved = item_value; + if ((pos = interleaved.find("-")) != string::npos) { + interleaved_min = ::atoi(interleaved.substr(0, pos).c_str()); + interleaved_max = ::atoi(interleaved.substr(pos + 1).c_str()); + } + } else if (item_key == "mode") { + mode = item_value; + } else if (item_key == "client_port") { + std::string sport = item_value; + std::string eport = item_value; + if ((pos = eport.find("-")) != string::npos) { + sport = eport.substr(0, pos); + eport = eport.substr(pos + 1); + } + client_port_min = ::atoi(sport.c_str()); + client_port_max = ::atoi(eport.c_str()); + } + } + + return err; +} + +void SrsRtspTransport::copy(SrsRtspTransport *src) +{ + transport = src->transport; + profile = src->profile; + lower_transport = src->lower_transport; + cast_type = src->cast_type; + interleaved = src->interleaved; + mode = src->mode; +} + +SrsRtspRequest::SrsRtspRequest() +{ + seq = 0; + content_length = 0; + stream_id = 0; + transport = NULL; +} + +SrsRtspRequest::~SrsRtspRequest() +{ + srs_freep(transport); +} + +bool SrsRtspRequest::is_options() +{ + return method == SRS_RTSP_METHOD_OPTIONS; +} + +bool SrsRtspRequest::is_describe() +{ + return method == SRS_RTSP_METHOD_DESCRIBE; +} + +bool SrsRtspRequest::is_setup() +{ + return method == SRS_RTSP_METHOD_SETUP; +} + +bool SrsRtspRequest::is_play() +{ + return method == SRS_RTSP_METHOD_PLAY; +} + +bool SrsRtspRequest::is_teardown() +{ + return method == SRS_RTSP_METHOD_TEARDOWN; +} + +SrsRtspResponse::SrsRtspResponse(int cseq) +{ + seq = cseq; + status = SRS_CONSTS_RTSP_OK; +} + +SrsRtspResponse::~SrsRtspResponse() +{ +} + +srs_error_t SrsRtspResponse::encode(stringstream& ss) +{ + srs_error_t err = srs_success; + + // status line + ss << SRS_RTSP_VERSION << SRS_RTSP_SP + << status << SRS_RTSP_SP + << srs_generate_rtsp_status_text(status) << SRS_RTSP_CRLF; + + // cseq + ss << SRS_RTSP_TOKEN_CSEQ << ":" << SRS_RTSP_SP << seq << SRS_RTSP_CRLF; + + // others. + ss << "Cache-Control: no-store" << SRS_RTSP_CRLF + << "Pragma: no-cache" << SRS_RTSP_CRLF + << "Server: " << RTMP_SIG_SRS_SERVER << SRS_RTSP_CRLF; + + // session if specified. + if (!session.empty()) { + ss << SRS_RTSP_TOKEN_SESSION << ":" << SRS_RTSP_SP << session << SRS_RTSP_CRLF; + } + + if ((err = encode_header(ss)) != srs_success) { + return srs_error_wrap(err, "encode header"); + }; + + // header EOF. + ss << SRS_RTSP_CRLF; + + return err; +} + +srs_error_t SrsRtspResponse::encode_header(std::stringstream& ss) +{ + return srs_success; +} + +SrsRtspOptionsResponse::SrsRtspOptionsResponse(int cseq) : SrsRtspResponse(cseq) +{ + methods = (SrsRtspMethod)(SrsRtspMethodDescribe | SrsRtspMethodOptions + | SrsRtspMethodPlay | SrsRtspMethodSetup | SrsRtspMethodTeardown); +} + +SrsRtspOptionsResponse::~SrsRtspOptionsResponse() +{ +} + +srs_error_t SrsRtspOptionsResponse::encode_header(stringstream& ss) +{ + static const SrsRtspMethod rtsp_methods[] = { + SrsRtspMethodDescribe, + SrsRtspMethodGetParameter, + SrsRtspMethodOptions, + SrsRtspMethodPause, + SrsRtspMethodPlay, + SrsRtspMethodRedirect, + SrsRtspMethodSetup, + SrsRtspMethodSetParameter, + SrsRtspMethodTeardown, + }; + + ss << SRS_RTSP_TOKEN_PUBLIC << ":" << SRS_RTSP_SP; + + bool appended = false; + int nb_methods = (int)(sizeof(rtsp_methods) / sizeof(SrsRtspMethod)); + for (int i = 0; i < nb_methods; i++) { + SrsRtspMethod method = rtsp_methods[i]; + if (((int)methods & (int)method) == 0) { + continue; + } + + if (appended) { + ss << ", "; + } + ss << srs_generate_rtsp_method_str(method); + appended = true; + } + ss << SRS_RTSP_CRLF; + + return srs_success; +} + +SrsRtspDescribeResponse::SrsRtspDescribeResponse(int cseq) : SrsRtspResponse(cseq) +{ +} + +SrsRtspDescribeResponse::~SrsRtspDescribeResponse() +{ +} + +srs_error_t SrsRtspDescribeResponse::encode_header(stringstream& ss) +{ + ss << SRS_RTSP_TOKEN_CONTENT_TYPE << ":" << SRS_RTSP_SP << "application/sdp" << SRS_RTSP_CRLF; + // WILL add CRLF to the end of sdp in SrsRtspResponse::encode, so add 2. + ss << SRS_RTSP_TOKEN_CONTENT_LENGTH << ":" << SRS_RTSP_SP << sdp.length() + 2 << SRS_RTSP_CRLF; + ss << SRS_RTSP_CRLF; + ss << sdp; + return srs_success; +} + +SrsRtspSetupResponse::SrsRtspSetupResponse(int seq) : SrsRtspResponse(seq) +{ + transport = new SrsRtspTransport(); + local_port_min = 0; + local_port_max = 0; + + client_port_min = 0; + client_port_max = 0; +} + +SrsRtspSetupResponse::~SrsRtspSetupResponse() +{ + srs_freep(transport); +} + +srs_error_t SrsRtspSetupResponse::encode_header(stringstream& ss) +{ + ss << SRS_RTSP_TOKEN_TRANSPORT << ":" << SRS_RTSP_SP; + ss << transport->transport << "/" << transport->profile; + if (!transport->lower_transport.empty()) { + ss << "/" << transport->lower_transport; + } + + if (!transport->cast_type.empty()) { + ss << ";" << transport->cast_type; + } + + if (!transport->interleaved.empty()) { + ss << ";interleaved=" << transport->interleaved; + } + + if (transport->lower_transport != "TCP") { + ss << ";client_port=" << client_port_min << "-" << client_port_max; + ss << ";server_port=" << local_port_min << "-" << local_port_max; + } + + ss << ";ssrc=" << ssrc << ";mode=\"play\""; + + ss << SRS_RTSP_CRLF; + + return srs_success; +} + +SrsRtspPlayResponse::SrsRtspPlayResponse(int cseq) : SrsRtspResponse(cseq) +{ +} + +SrsRtspPlayResponse::~SrsRtspPlayResponse() +{ +} + +srs_error_t SrsRtspPlayResponse::encode_header(stringstream& ss) +{ + return srs_success; +} + +SrsRtspStack::SrsRtspStack(ISrsProtocolReadWriter* s) +{ + buf = new SrsSimpleStream(); + skt = s; +} + +SrsRtspStack::~SrsRtspStack() +{ + srs_freep(buf); +} + +srs_error_t SrsRtspStack::recv_message(SrsRtspRequest** preq) +{ + srs_error_t err = srs_success; + + SrsRtspRequest* req = new SrsRtspRequest(); + if ((err = do_recv_message(req)) != srs_success) { + srs_freep(req); + return srs_error_wrap(err, "recv message"); + } + + *preq = req; + + return err; +} + +srs_error_t SrsRtspStack::send_message(SrsRtspResponse* res) +{ + srs_error_t err = srs_success; + + std::stringstream ss; + // encode the message to string. + if ((err = res->encode(ss)) != srs_success) { + return srs_error_wrap(err, "encode message"); + } + + std::string str = ss.str(); + srs_assert(!str.empty()); + + if ((err = skt->write((char*)str.c_str(), (int)str.length(), NULL)) != srs_success) { + return srs_error_wrap(err, "write message"); + } + + return err; +} + +srs_error_t SrsRtspStack::do_recv_message(SrsRtspRequest* req) +{ + srs_error_t err = srs_success; + + // Parse RTSP request line: "METHOD URI VERSION" + // Example: "PLAY rtsp://example.com/stream RTSP/1.0" + + // Parse the RTSP method (PLAY, SETUP, DESCRIBE, etc.) + if ((err = recv_token_normal(req->method)) != srs_success) { + return srs_error_wrap(err, "method"); + } + + // Parse the request URI (resource path or full URL) + if ((err = recv_token_normal(req->uri)) != srs_success) { + return srs_error_wrap(err, "uri"); + } + + // Parse the RTSP version (typically "RTSP/1.0") + if ((err = recv_token_eof(req->version)) != srs_success) { + return srs_error_wrap(err, "version"); + } + + // Parse RTSP headers in "Name: Value" format + // Example headers: + // CSeq: 1 + // Content-Type: application/sdp + // Content-Length: 460 + // Transport: RTP/AVP;unicast;client_port=8000-8001 + // Session: 12345678 + for (;;) { + // Parse the header name (before the colon) + std::string token; + if ((err = recv_token_normal(token)) != srs_success) { + if (srs_error_code(err) == ERROR_RTSP_REQUEST_HEADER_EOF) { + srs_error_reset(err); + break; // End of headers reached (empty line) + } + return srs_error_wrap(err, "recv token"); + } + + // Parse the header value (after the colon) based on header name + if (token == SRS_RTSP_TOKEN_CSEQ) { + // CSeq: sequence number for request/response matching + std::string seq; + if ((err = recv_token_eof(seq)) != srs_success) { + return srs_error_wrap(err, "seq"); + } + req->seq = ::atoll(seq.c_str()); + } else if (token == SRS_RTSP_TOKEN_CONTENT_TYPE) { + // Content-Type: MIME type of the message body (e.g., application/sdp) + std::string ct; + if ((err = recv_token_eof(ct)) != srs_success) { + return srs_error_wrap(err, "ct"); + } + req->content_type = ct; + } else if (token == SRS_RTSP_TOKEN_CONTENT_LENGTH) { + // Content-Length: size of the message body in bytes + std::string cl; + if ((err = recv_token_eof(cl)) != srs_success) { + return srs_error_wrap(err, "cl"); + } + req->content_length = ::atoll(cl.c_str()); + } else if (token == SRS_RTSP_TOKEN_TRANSPORT) { + // Transport: RTP transport parameters (protocol, ports, etc.) + std::string transport; + if ((err = recv_token_eof(transport)) != srs_success) { + return srs_error_wrap(err, "transport"); + } + if (!req->transport) { + req->transport = new SrsRtspTransport(); + } + if ((err = req->transport->parse(transport)) != srs_success) { + return srs_error_wrap(err, "parse transport=%s", transport.c_str()); + } + } else if (token == SRS_RTSP_TOKEN_SESSION) { + // Session: session identifier for maintaining state + if ((err = recv_token_eof(req->session)) != srs_success) { + return srs_error_wrap(err, "session"); + } + } else if (token == SRS_RTSP_TOKEN_ACCEPT) { + // Accept: acceptable media types for the response + if ((err = recv_token_eof(req->accept)) != srs_success) { + return srs_error_wrap(err, "accept"); + } + } else if (token == SRS_RTSP_TOKEN_USER_AGENT) { + // User-Agent: client software identification + if ((err = recv_token_util_eof(req->user_agent)) != srs_success) { + return srs_error_wrap(err, "user_agent"); + } + } else if (token == SRS_RTSP_TOKEN_RANGE) { + // Range: time range for playback (e.g., npt=0-30) + if ((err = recv_token_eof(req->range)) != srs_success) { + return srs_error_wrap(err, "range"); + } + } else { + // unknown header name, parse util EOF. + std::string value; + if ((err = recv_token_util_eof(value)) != srs_success) { + return srs_error_wrap(err, "state"); + } + srs_trace("rtsp: ignore header %s=%s", token.c_str(), value.c_str()); + } + } + + // for setup, parse the stream id from uri. + if (req->is_setup()) { + size_t pos = string::npos; + std::string stream_id = srs_path_basename(req->uri); + if ((pos = stream_id.find("=")) != string::npos) { + stream_id = stream_id.substr(pos + 1); + } + req->stream_id = ::atoi(stream_id.c_str()); + srs_info("rtsp: setup stream id=%d", req->stream_id); + } + + return err; +} + +srs_error_t SrsRtspStack::recv_token_normal(std::string& token) +{ + srs_error_t err = srs_success; + + SrsRtspTokenState state; + + if ((err = recv_token(token, state)) != srs_success) { + if (srs_error_code(err) == ERROR_RTSP_REQUEST_HEADER_EOF) { + return srs_error_wrap(err, "EOF"); + } + return srs_error_wrap(err, "recv token"); + } + + if (state != SrsRtspTokenStateNormal) { + return srs_error_new(ERROR_RTSP_TOKEN_NOT_NORMAL, "invalid state=%d", state); + } + + return err; +} + +srs_error_t SrsRtspStack::recv_token_eof(std::string& token) +{ + srs_error_t err = srs_success; + + SrsRtspTokenState state; + + if ((err = recv_token(token, state)) != srs_success) { + if (srs_error_code(err) == ERROR_RTSP_REQUEST_HEADER_EOF) { + return srs_error_wrap(err, "EOF"); + } + return srs_error_wrap(err, "recv token"); + } + + if (state != SrsRtspTokenStateEOF) { + return srs_error_new(ERROR_RTSP_TOKEN_NOT_NORMAL, "invalid state=%d", state); + } + + return err; +} + +srs_error_t SrsRtspStack::recv_token_util_eof(std::string& token, int* pconsumed) +{ + srs_error_t err = srs_success; + + SrsRtspTokenState state; + + // use 0x00 as ignore the normal token flag. + if ((err = recv_token(token, state, 0x00, pconsumed)) != srs_success) { + if (srs_error_code(err) == ERROR_RTSP_REQUEST_HEADER_EOF) { + return srs_error_wrap(err, "EOF"); + } + return srs_error_wrap(err, "recv token"); + } + + if (state != SrsRtspTokenStateEOF) { + return srs_error_new(ERROR_RTSP_TOKEN_NOT_NORMAL, "invalid state=%d", state); + } + + return err; +} + +srs_error_t SrsRtspStack::recv_token(std::string& token, SrsRtspTokenState& state, char normal_ch, int* pconsumed) +{ + srs_error_t err = srs_success; + + // whatever, default to error state. + state = SrsRtspTokenStateError; + + // when buffer is empty, append bytes first. + bool append_bytes = buf->length() == 0; + + // parse util token. + for (;;) { + // append bytes if required. + if (append_bytes) { + append_bytes = false; + + char buffer[SRS_RTSP_BUFFER]; + ssize_t nb_read = 0; + if ((err = skt->read(buffer, SRS_RTSP_BUFFER, &nb_read)) != srs_success) { + return srs_error_wrap(err, "recv data"); + } + + buf->append(buffer, (int)nb_read); + } + + // Try to detect and consume any RTCP frames from the buffer + while (buf->length() > 0) { + srs_error_t rtcp_err = try_consume_rtcp_frame(); + + if (rtcp_err == srs_success) { + // Successfully consumed an RTCP frame, continue to check for more + continue; + } else if (srs_error_code(rtcp_err) == ERROR_RTSP_NEED_MORE_DATA) { + // Need more data to complete RTCP frame, let the outer loop read more + srs_freep(rtcp_err); + append_bytes = true; + break; + } else { + // Not an RTCP frame or other error, break and try RTSP parsing + srs_freep(rtcp_err); + break; + } + } + + // parse one by one. + char* start = buf->bytes(); + char* end = start + buf->length(); + char* p = start; + + // find util SP/CR/LF, max 2 EOF, to finger out the EOF of message. + for (; p < end && p[0] != normal_ch && p[0] != SRS_RTSP_CR && p[0] != SRS_RTSP_LF; p++) { + } + + // matched. + if (p < end) { + // finger out the state. + if (p[0] == normal_ch) { + state = SrsRtspTokenStateNormal; + } else { + state = SrsRtspTokenStateEOF; + } + + // got the token. + int nb_token = (int)(p - start); + // trim last ':' character. + if (nb_token && p[-1] == ':') { + nb_token--; + } + if (nb_token) { + token.append(start, nb_token); + } else { + err = srs_error_new(ERROR_RTSP_REQUEST_HEADER_EOF, "EOF"); + } + + // ignore SP/CR/LF + for (int i = 0; i < 2 && p < end && (p[0] == normal_ch || p[0] == SRS_RTSP_CR || p[0] == SRS_RTSP_LF); p++, i++) { + } + + // consume the token bytes. + srs_assert(p - start); + buf->erase((int)(p - start)); + if (pconsumed) { + *pconsumed = (int)(p - start); + } + break; + } + + // append more and parse again. + append_bytes = true; + } + + return err; +} + +srs_error_t SrsRtspStack::try_consume_rtcp_frame() +{ + // Need at least 4 bytes for RTCP over TCP header: $ + channel + length + if (buf->length() < 4) { + // Not enough data, let caller read more + return srs_error_new(ERROR_RTSP_NEED_MORE_DATA, "need more data for rtcp header"); + } + + char* data = buf->bytes(); + + // Check for RTCP over TCP format: $ + channel + length(2 bytes) + if (data[0] == '$') { + uint8_t channel = (uint8_t)data[1]; + uint16_t payload_length = (uint16_t(data[2]) << 8) | uint16_t(data[3]); + int total_frame_size = 4 + payload_length; // 4-byte header + payload + + // Check if we have the complete frame + if (buf->length() < total_frame_size) { + // Not enough data for complete frame, let caller read more + return srs_error_new(ERROR_RTSP_NEED_MORE_DATA, "need more data for complete rtcp frame"); + } + + // Check if the payload is RTCP (starts at offset 4) + if (payload_length >= 8 && srs_is_rtcp((const uint8_t*)(data + 4), payload_length)) { + // This is an RTCP packet in RTSP over TCP format + srs_trace("RTSP: Consuming RTCP packet(%d), channel=%d, size=%d bytes", + (uint8_t)data[5], channel, payload_length); + buf->erase(total_frame_size); + return srs_success; + } else { + // Unknown interleaved frame, consume it anyway to avoid blocking RTSP parsing + srs_trace("RTSP: Consuming unknown interleaved frame, channel=%d, size=%d bytes", + channel, payload_length); + buf->erase(total_frame_size); + return srs_success; + } + } + + // Not an interleaved frame (RTP/RTCP over TCP) + return srs_error_new(ERROR_RTSP_TOKEN_NOT_NORMAL, "not interleaved frame"); +} diff --git a/trunk/src/protocol/srs_protocol_rtsp_stack.hpp b/trunk/src/protocol/srs_protocol_rtsp_stack.hpp new file mode 100644 index 000000000..79de30a13 --- /dev/null +++ b/trunk/src/protocol/srs_protocol_rtsp_stack.hpp @@ -0,0 +1,384 @@ +// +// Copyright (c) 2013-2025 The SRS Authors +// +// SPDX-License-Identifier: MIT +// + +#ifndef SRS_PROTOCOL_RTSP_HPP +#define SRS_PROTOCOL_RTSP_HPP + +#include +#include + +#include +#include + +class SrsBuffer; +class SrsSimpleStream; +class SrsAudioFrame; +class ISrsProtocolReadWriter; + +// From rtsp specification +// CR = +#define SRS_RTSP_CR SRS_CONSTS_CR // 0x0D +// LF = +#define SRS_RTSP_LF SRS_CONSTS_LF // 0x0A +// SP = +#define SRS_RTSP_SP ' ' // 0x20 + +// 4 RTSP Message, @see rfc2326-1998-rtsp.pdf, page 37 +// Lines are terminated by CRLF, but +// receivers should be prepared to also interpret CR and LF by +// themselves as line terminators. +#define SRS_RTSP_CRLF "\r\n" // 0x0D0A +#define SRS_RTSP_CRLFCRLF "\r\n\r\n" // 0x0D0A0D0A + +// RTSP token +#define SRS_RTSP_TOKEN_CSEQ "CSeq" +#define SRS_RTSP_TOKEN_PUBLIC "Public" +#define SRS_RTSP_TOKEN_CONTENT_TYPE "Content-Type" +#define SRS_RTSP_TOKEN_CONTENT_LENGTH "Content-Length" +#define SRS_RTSP_TOKEN_TRANSPORT "Transport" +#define SRS_RTSP_TOKEN_SESSION "Session" +#define SRS_RTSP_TOKEN_ACCEPT "Accept" +#define SRS_RTSP_TOKEN_USER_AGENT "User-Agent" +#define SRS_RTSP_TOKEN_RANGE "Range" + +// RTSP methods +#define SRS_RTSP_METHOD_OPTIONS "OPTIONS" +#define SRS_RTSP_METHOD_DESCRIBE "DESCRIBE" +#define SRS_RTSP_METHOD_ANNOUNCE "ANNOUNCE" +#define SRS_RTSP_METHOD_SETUP "SETUP" +#define SRS_RTSP_METHOD_PLAY "PLAY" +#define SRS_RTSP_METHOD_PAUSE "PAUSE" +#define SRS_RTSP_METHOD_TEARDOWN "TEARDOWN" +#define SRS_RTSP_METHOD_GET_PARAMETER "GET_PARAMETER" +#define SRS_RTSP_METHOD_SET_PARAMETER "SET_PARAMETER" +#define SRS_RTSP_METHOD_REDIRECT "REDIRECT" +#define SRS_RTSP_METHOD_RECORD "RECORD" + +// RTSP-Version +#define SRS_RTSP_VERSION "RTSP/1.0" + +// 10 Method Definitions, @see rfc2326-1998-rtsp.pdf, page 57 +// The method token indicates the method to be performed on the resource +// identified by the Request-URI. The method is case-sensitive. New +// methods may be defined in the future. Method names may not start with +// a $ character (decimal 24) and must be a token. Methods are +// summarized in Table 2. +// Notes on Table 2: PAUSE is recommended, but not required in that a +// fully functional server can be built that does not support this +// method, for example, for live feeds. If a server does not support a +// particular method, it MUST return "501 Not Implemented" and a client +// SHOULD not try this method again for this server. +enum SrsRtspMethod +{ + SrsRtspMethodDescribe = 0x0001, + SrsRtspMethodAnnounce = 0x0002, + SrsRtspMethodGetParameter = 0x0004, + SrsRtspMethodOptions = 0x0008, + SrsRtspMethodPause = 0x0010, + SrsRtspMethodPlay = 0x0020, + SrsRtspMethodRecord = 0x0040, + SrsRtspMethodRedirect = 0x0080, + SrsRtspMethodSetup = 0x0100, + SrsRtspMethodSetParameter = 0x0200, + SrsRtspMethodTeardown = 0x0400, +}; + +// The state of rtsp token. +enum SrsRtspTokenState +{ + // Parse token failed, default state. + SrsRtspTokenStateError = 100, + // When SP follow the token. + SrsRtspTokenStateNormal = 101, + // When CRLF follow the token. + SrsRtspTokenStateEOF = 102, +}; + +// The rtsp transport. +// 12.39 Transport, @see rfc2326-1998-rtsp.pdf, page 115 +// This request header indicates which transport protocol is to be used +// and configures its parameters such as destination address, +// compression, multicast time-to-live and destination port for a single +// stream. It sets those values not already determined by a presentation +// description. +class SrsRtspTransport +{ +public: + // The syntax for the transport specifier is + // transport/profile/lower-transport + std::string transport; + std::string profile; + std::string lower_transport; + // unicast | multicast + // mutually exclusive indication of whether unicast or multicast + // delivery will be attempted. Default value is multicast. + // Clients that are capable of handling both unicast and + // multicast transmission MUST indicate such capability by + // including two full transport-specs with separate parameters + // For each. + std::string cast_type; + // The interleaved parameter implies mixing the media stream with + // the control stream in whatever protocol is being used by the + // control stream, using the mechanism defined in Section 10.12. + // The argument provides the channel number to be used in the $ + // statement. This parameter may be specified as a range, e.g., + // interleaved=4-5 in cases where the transport choice for the + // media stream requires it. + std::string interleaved; + int interleaved_min; + int interleaved_max; + // The mode parameter indicates the methods to be supported for + // this session. Valid values are PLAY and RECORD. If not + // provided, the default is PLAY. + std::string mode; + // This parameter provides the unicast RTP/RTCP port pair on + // which the client has chosen to receive media data and control + // information. It is specified as a range, e.g., + // client_port=3456-3457. + // where client will use port in: + // [client_port_min, client_port_max) + int client_port_min; + int client_port_max; +public: + SrsRtspTransport(); + virtual ~SrsRtspTransport(); +public: + // Parse a line of token for transport. + virtual srs_error_t parse(std::string attr); + // Copy the transport from src. + virtual void copy(SrsRtspTransport* src); +}; + +// The rtsp request message. +// 6 Request, @see rfc2326-1998-rtsp.pdf, page 39 +// A request message from a client to a server or vice versa includes, +// within the first line of that message, the method to be applied to +// The resource, the identifier of the resource, and the protocol +// version in use. +// Request = Request-Line ; Section 6.1 +// // ( general-header ; Section 5 +// | request-header ; Section 6.2 +// | entity-header ) ; Section 8.1 +// CRLF +// [ message-body ] ; Section 4.3 +class SrsRtspRequest +{ +public: + // 6.1 Request Line + // Request-Line = Method SP Request-URI SP RTSP-Version CRLF + std::string method; + std::string uri; + std::string version; + // 12.17 CSeq + // The CSeq field specifies the sequence number for an RTSP requestresponse + // pair. This field MUST be present in all requests and + // responses. For every RTSP request containing the given sequence + // number, there will be a corresponding response having the same + // number. Any retransmitted request must contain the same sequence + // number as the original (i.e. the sequence number is not incremented + // For retransmissions of the same request). + long seq; + // 12.16 Content-Type, @see rfc2326-1998-rtsp.pdf, page 99 + // See [H14.18]. Note that the content types suitable for RTSP are + // likely to be restricted in practice to presentation descriptions and + // parameter-value types. + std::string content_type; + // 12.14 Content-Length, @see rfc2326-1998-rtsp.pdf, page 99 + // This field contains the length of the content of the method (i.e. + // after the double CRLF following the last header). Unlike HTTP, it + // MUST be included in all messages that carry content beyond the header + // portion of the message. If it is missing, a default value of zero is + // assumed. It is interpreted according to [H14.14]. + long content_length; + // The session id. + std::string session; + + // The transport in setup, NULL for no transport. + SrsRtspTransport* transport; + // For setup message, parse the stream id from uri. + int stream_id; + + std::string accept; + std::string user_agent; + std::string range; +public: + SrsRtspRequest(); + virtual ~SrsRtspRequest(); +public: + virtual bool is_options(); + virtual bool is_describe(); + virtual bool is_setup(); + virtual bool is_play(); + virtual bool is_teardown(); +}; + +// The rtsp response message. +// 7 Response, @see rfc2326-1998-rtsp.pdf, page 43 +// [H6] applies except that HTTP-Version is replaced by RTSP-Version. +// Also, RTSP defines additional status codes and does not define some +// HTTP codes. The valid response codes and the methods they can be used +// with are defined in Table 1. +// After receiving and interpreting a request message, the recipient +// responds with an RTSP response message. +// Response = Status-Line ; Section 7.1 +// // ( general-header ; Section 5 +// | response-header ; Section 7.1.2 +// | entity-header ) ; Section 8.1 +// CRLF +// [ message-body ] ; Section 4.3 +class SrsRtspResponse +{ +public: + // 7.1 Status-Line + // The first line of a Response message is the Status-Line, consisting + // of the protocol version followed by a numeric status code, and the + // textual phrase associated with the status code, with each element + // separated by SP characters. No CR or LF is allowed except in the + // final CRLF sequence. + // Status-Line = RTSP-Version SP Status-Code SP Reason-Phrase CRLF + // @see about the version of rtsp, see SRS_RTSP_VERSION + // @see about the status of rtsp, see SRS_CONSTS_RTSP_OK + int status; + // 12.17 CSeq, @see rfc2326-1998-rtsp.pdf, page 99 + // The CSeq field specifies the sequence number for an RTSP requestresponse + // pair. This field MUST be present in all requests and + // responses. For every RTSP request containing the given sequence + // number, there will be a corresponding response having the same + // number. Any retransmitted request must contain the same sequence + // number as the original (i.e. the sequence number is not incremented + // For retransmissions of the same request). + long seq; + // The session id. + std::string session; +public: + SrsRtspResponse(int cseq); + virtual ~SrsRtspResponse(); +public: + // Encode message to string. + virtual srs_error_t encode(std::stringstream& ss); +protected: + // Sub classes override this to encode the headers. + virtual srs_error_t encode_header(std::stringstream& ss); +}; + +// 10.1 OPTIONS, @see rfc2326-1998-rtsp.pdf, page 59 +// The behavior is equivalent to that described in [H9.2]. An OPTIONS +// request may be issued at any time, e.g., if the client is about to +// try a nonstandard request. It does not influence server state. +class SrsRtspOptionsResponse : public SrsRtspResponse +{ +public: + // Join of SrsRtspMethod + SrsRtspMethod methods; +public: + SrsRtspOptionsResponse(int cseq); + virtual ~SrsRtspOptionsResponse(); +protected: + virtual srs_error_t encode_header(std::stringstream& ss); +}; + +// 10.2 DESCRIBE, @see rfc2326-1998-rtsp.pdf, page 61 +class SrsRtspDescribeResponse : public SrsRtspResponse +{ +public: + // The sdp in describe. + std::string sdp; +public: + SrsRtspDescribeResponse(int cseq); + virtual ~SrsRtspDescribeResponse(); +protected: + virtual srs_error_t encode_header(std::stringstream& ss); +}; + +// 10.4 SETUP, @see rfc2326-1998-rtsp.pdf, page 65 +// The SETUP request for a URI specifies the transport mechanism to be +// used for the streamed media. A client can issue a SETUP request for a +// stream that is already playing to change transport parameters, which +// a server MAY allow. If it does not allow this, it MUST respond with +// error "455 Method Not Valid In This State". For the benefit of any +// intervening firewalls, a client must indicate the transport +// parameters even if it has no influence over these parameters, for +// example, where the server advertises a fixed multicast address. +class SrsRtspSetupResponse : public SrsRtspResponse +{ +public: + // The client specified port. + int client_port_min; + int client_port_max; + // The client will use the port in: + // [local_port_min, local_port_max) + int local_port_min; + int local_port_max; + + SrsRtspTransport* transport; + // The ssrc of the stream. + std::string ssrc; +public: + SrsRtspSetupResponse(int cseq); + virtual ~SrsRtspSetupResponse(); +protected: + virtual srs_error_t encode_header(std::stringstream& ss); +}; + +// 10.5 PLAY, @see rfc2326-1998-rtsp.pdf, page 67 +class SrsRtspPlayResponse : public SrsRtspResponse +{ +public: + SrsRtspPlayResponse(int cseq); + virtual ~SrsRtspPlayResponse(); +protected: + virtual srs_error_t encode_header(std::stringstream& ss); +}; + +// The rtsp protocol stack to parse the rtsp packets. +class SrsRtspStack +{ +private: + // The cached bytes buffer. + SrsSimpleStream* buf; + // The underlayer socket object, send/recv bytes. + ISrsProtocolReadWriter* skt; +public: + SrsRtspStack(ISrsProtocolReadWriter* s); + virtual ~SrsRtspStack(); +public: + // Recv rtsp message from underlayer io. + // @param preq the output rtsp request message, which user must free it. + // @return an int error code. + // ERROR_RTSP_REQUEST_HEADER_EOF indicates request header EOF. + virtual srs_error_t recv_message(SrsRtspRequest** preq); + // Try to detect and consume RTCP frame from buffered data. + // @return srs_success if RTCP frame is consumed successfully. + // ERROR_RTSP_NEED_MORE_DATA if more data is needed to complete the frame. + // ERROR_RTSP_TOKEN_NOT_NORMAL if the data is not an RTCP interleaved frame. + virtual srs_error_t try_consume_rtcp_frame(); + // Send rtsp message over underlayer io. + // @param res the rtsp response message, which user should never free it. + // @return an int error code. + virtual srs_error_t send_message(SrsRtspResponse* res); +private: + // Recv the rtsp message. + virtual srs_error_t do_recv_message(SrsRtspRequest* req); + // Read a normal token from io, error when token state is not normal. + virtual srs_error_t recv_token_normal(std::string& token); + // Read a normal token from io, error when token state is not eof. + virtual srs_error_t recv_token_eof(std::string& token); + // Read the token util got eof, for example, to read the response status Reason-Phrase + // @param pconsumed, output the token parsed length. NULL to ignore. + virtual srs_error_t recv_token_util_eof(std::string& token, int* pconsumed = NULL); + // Read a token from io, split by SP, endswith CRLF: + // token1 SP token2 SP ... tokenN CRLF + // @param token, output the read token. + // @param state, output the token parse state. + // @param normal_ch, the char to indicates the normal token. + // the SP use to indicates the normal token, @see SRS_RTSP_SP + // the 0x00 use to ignore normal token flag. @see recv_token_util_eof + // @param pconsumed, output the token parsed length. NULL to ignore. + virtual srs_error_t recv_token(std::string& token, SrsRtspTokenState& state, char normal_ch = SRS_RTSP_SP, int* pconsumed = NULL); +}; + +#endif + diff --git a/trunk/src/utest/srs_utest_config.cpp b/trunk/src/utest/srs_utest_config.cpp index 4a907dd56..3c6561567 100644 --- a/trunk/src/utest/srs_utest_config.cpp +++ b/trunk/src/utest/srs_utest_config.cpp @@ -4383,6 +4383,19 @@ VOID TEST(ConfigEnvTest, CheckEnvValuesVhostSrt) } } +VOID TEST(ConfigEnvTest, CheckEnvValuesRtspServer) +{ + if (true) { + MockSrsConfig conf; + + SrsSetEnvConfig(rtsp_server_enabled, "SRS_RTSP_SERVER_ENABLED", "on"); + EXPECT_TRUE(conf.get_rtsp_server_enabled()); + + SrsSetEnvConfig(rtsp_server_listen, "SRS_RTSP_SERVER_LISTEN", "554"); + EXPECT_EQ(554, conf.get_rtsp_server_listen()); + } +} + VOID TEST(ConfigEnvTest, CheckEnvValuesRtcServer) { if (true) { diff --git a/trunk/src/utest/srs_utest_protocol.cpp b/trunk/src/utest/srs_utest_protocol.cpp index b03a0b867..951c97e6c 100644 --- a/trunk/src/utest/srs_utest_protocol.cpp +++ b/trunk/src/utest/srs_utest_protocol.cpp @@ -12,6 +12,7 @@ using namespace std; #include #include #include +#include #include #include #include @@ -21,6 +22,10 @@ using namespace std; #include #include +#ifdef SRS_RTC +#include +#endif + MockEmptyIO::MockEmptyIO() { } @@ -3616,3 +3621,633 @@ VOID TEST(ProtocolRTMPTest, RTMPHandshakeBytes) EXPECT_TRUE(bytes.s0s1s2 != NULL); } +#ifdef SRS_RTSP +VOID TEST(ProtocolRTSPTest, RTSPRequest) +{ + srs_error_t err = srs_success; + + MockBufferIO bio; + SrsRtspStack stack(&bio); + + // OPTIONS + if (true) { + const char* options_req = + "OPTIONS rtsp://server.example.com/stream RTSP/1.0\r\n" + "CSeq: 1\r\n" + "User-Agent: SRS RTSP Client\r\n\r\n"; + bio.in_buffer.append(options_req, strlen(options_req)); + + SrsRtspRequest* req = NULL; + HELPER_ASSERT_SUCCESS(stack.recv_message(&req)); + SrsUniquePtr req_uptr(req); + + EXPECT_TRUE(req->is_options()); + EXPECT_STREQ("OPTIONS", req->method.c_str()); + EXPECT_STREQ("rtsp://server.example.com/stream", req->uri.c_str()); + EXPECT_STREQ("RTSP/1.0", req->version.c_str()); + EXPECT_EQ(1, req->seq); + EXPECT_STREQ("SRS RTSP Client", req->user_agent.c_str()); + + SrsRtspOptionsResponse* res = new SrsRtspOptionsResponse(req->seq); + HELPER_ASSERT_SUCCESS(stack.send_message(res)); + + string response = std::string(bio.out_buffer.bytes(), bio.out_buffer.length()); + EXPECT_TRUE(response.find("RTSP/1.0 200 OK") != string::npos); + EXPECT_TRUE(response.find("CSeq: 1") != string::npos); + EXPECT_TRUE(response.find("Public:") != string::npos); + bio.out_buffer.erase(bio.out_buffer.length()); + } + + // DESCRIBE + if (true) { + const char* describe_req = + "DESCRIBE rtsp://server.example.com/stream RTSP/1.0\r\n" + "CSeq: 2\r\n" + "Accept: application/sdp\r\n\r\n"; + bio.in_buffer.append(describe_req, strlen(describe_req)); + + SrsRtspRequest* req = NULL; + HELPER_ASSERT_SUCCESS(stack.recv_message(&req)); + SrsUniquePtr req_uptr(req); + + EXPECT_TRUE(req->is_describe()); + EXPECT_STREQ("DESCRIBE", req->method.c_str()); + EXPECT_EQ(2, req->seq); + EXPECT_STREQ("application/sdp", req->accept.c_str()); + + SrsRtspDescribeResponse* res = new SrsRtspDescribeResponse(req->seq); + res->sdp = "v=0\r\n" + "o=- 123456 0 IN IP4 127.0.0.1\r\n" + "s=SRS RTSP Server\r\n" + "c=IN IP4 127.0.0.1\r\n" + "t=0 0\r\n" + "m=video 0 RTP/AVP 96\r\n" + "a=rtpmap:96 H264/90000\r\n"; + HELPER_ASSERT_SUCCESS(stack.send_message(res)); + + string response = std::string(bio.out_buffer.bytes(), bio.out_buffer.length()); + EXPECT_TRUE(response.find("RTSP/1.0 200 OK") != string::npos); + EXPECT_TRUE(response.find("CSeq: 2") != string::npos); + EXPECT_TRUE(response.find("Content-Type: application/sdp") != string::npos); + EXPECT_TRUE(response.find("m=video 0 RTP/AVP 96") != string::npos); + bio.out_buffer.erase(bio.out_buffer.length()); + } + + // SETUP + if (true) { + const char* setup_req = + "SETUP rtsp://server.example.com/stream/trackID=0 RTSP/1.0\r\n" + "CSeq: 3\r\n" + "Transport: RTP/AVP;unicast;client_port=9000-9001\r\n\r\n"; + bio.in_buffer.append(setup_req, strlen(setup_req)); + + SrsRtspRequest* req = NULL; + HELPER_ASSERT_SUCCESS(stack.recv_message(&req)); + SrsUniquePtr req_uptr(req); + + EXPECT_TRUE(req->is_setup()); + EXPECT_STREQ("SETUP", req->method.c_str()); + EXPECT_EQ(3, req->seq); + EXPECT_TRUE(req->transport != NULL); + EXPECT_STREQ("RTP", req->transport->transport.c_str()); + EXPECT_STREQ("AVP", req->transport->profile.c_str()); + EXPECT_STREQ("unicast", req->transport->cast_type.c_str()); + EXPECT_EQ(9000, req->transport->client_port_min); + EXPECT_EQ(9001, req->transport->client_port_max); + EXPECT_EQ(0, req->stream_id); + + SrsRtspSetupResponse* res = new SrsRtspSetupResponse(req->seq); + res->session = "12345678"; + res->client_port_min = 9000; + res->client_port_max = 9001; + res->local_port_min = 5000; + res->local_port_max = 5001; + res->transport->transport = "RTP"; + res->transport->profile = "AVP"; + res->transport->cast_type = "unicast"; + res->ssrc = "1234ABCD"; + HELPER_ASSERT_SUCCESS(stack.send_message(res)); + + string response = std::string(bio.out_buffer.bytes(), bio.out_buffer.length()); + EXPECT_TRUE(response.find("RTSP/1.0 200 OK") != string::npos); + EXPECT_TRUE(response.find("CSeq: 3") != string::npos); + EXPECT_TRUE(response.find("Transport: RTP/AVP;unicast;client_port=9000-9001;server_port=5000-5001;ssrc=1234ABCD;mode=\"play\"") != string::npos); + bio.out_buffer.erase(bio.out_buffer.length()); + } + + // PLAY + if (true) { + const char* play_req = + "PLAY rtsp://server.example.com/stream RTSP/1.0\r\n" + "CSeq: 4\r\n" + "Session: 12345678\r\n" + "Range: npt=0.000-\r\n\r\n"; + bio.in_buffer.append(play_req, strlen(play_req)); + + SrsRtspRequest* req = NULL; + HELPER_ASSERT_SUCCESS(stack.recv_message(&req)); + SrsUniquePtr req_uptr(req); + + EXPECT_TRUE(req->is_play()); + EXPECT_STREQ("PLAY", req->method.c_str()); + EXPECT_EQ(4, req->seq); + EXPECT_STREQ("12345678", req->session.c_str()); + EXPECT_STREQ("npt=0.000-", req->range.c_str()); + + SrsRtspPlayResponse* res = new SrsRtspPlayResponse(req->seq); + res->session = req->session; + HELPER_ASSERT_SUCCESS(stack.send_message(res)); + + string response = std::string(bio.out_buffer.bytes(), bio.out_buffer.length()); + EXPECT_TRUE(response.find("RTSP/1.0 200 OK") != string::npos); + EXPECT_TRUE(response.find("CSeq: 4") != string::npos); + EXPECT_TRUE(response.find("Session: 12345678") != string::npos); + bio.out_buffer.erase(bio.out_buffer.length()); + } + + // TEARDOWN + if (true) { + const char* teardown_req = + "TEARDOWN rtsp://server.example.com/stream RTSP/1.0\r\n" + "CSeq: 5\r\n" + "Session: 12345678\r\n\r\n"; + bio.in_buffer.append(teardown_req, strlen(teardown_req)); + + SrsRtspRequest* req = NULL; + HELPER_ASSERT_SUCCESS(stack.recv_message(&req)); + SrsUniquePtr req_uptr(req); + + EXPECT_TRUE(req->is_teardown()); + EXPECT_STREQ("TEARDOWN", req->method.c_str()); + EXPECT_EQ(5, req->seq); + EXPECT_STREQ("12345678", req->session.c_str()); + + SrsRtspResponse* res = new SrsRtspResponse(req->seq); + res->session = req->session; + HELPER_ASSERT_SUCCESS(stack.send_message(res)); + + string response = std::string(bio.out_buffer.bytes(), bio.out_buffer.length()); + EXPECT_TRUE(response.find("RTSP/1.0 200 OK") != string::npos); + EXPECT_TRUE(response.find("CSeq: 5") != string::npos); + EXPECT_TRUE(response.find("Session: 12345678") != string::npos); + bio.out_buffer.erase(bio.out_buffer.length()); + } +} + +// Test TCP-only transport support +VOID TEST(ProtocolRTSPTest, RTSPTcpOnlyTransport) +{ + srs_error_t err = srs_success; + + MockBufferIO bio; + SrsRtspStack stack(&bio); + + // Test TCP transport (should succeed) + if (true) { + const char* tcp_setup_req = + "SETUP rtsp://server.example.com/stream/trackID=0 RTSP/1.0\r\n" + "CSeq: 3\r\n" + "Transport: RTP/AVP/TCP;unicast;interleaved=0-1\r\n\r\n"; + bio.in_buffer.append(tcp_setup_req, strlen(tcp_setup_req)); + + SrsRtspRequest* req = NULL; + HELPER_ASSERT_SUCCESS(stack.recv_message(&req)); + SrsUniquePtr req_uptr(req); + + EXPECT_TRUE(req->is_setup()); + EXPECT_STREQ("SETUP", req->method.c_str()); + EXPECT_EQ(3, req->seq); + EXPECT_TRUE(req->transport != NULL); + EXPECT_STREQ("RTP", req->transport->transport.c_str()); + EXPECT_STREQ("AVP", req->transport->profile.c_str()); + EXPECT_STREQ("TCP", req->transport->lower_transport.c_str()); + EXPECT_STREQ("unicast", req->transport->cast_type.c_str()); + EXPECT_EQ(0, req->transport->interleaved_min); + EXPECT_EQ(1, req->transport->interleaved_max); + + SrsRtspSetupResponse* res = new SrsRtspSetupResponse(req->seq); + res->session = "12345678"; + res->transport->copy(req->transport); + res->ssrc = "1234ABCD"; + HELPER_ASSERT_SUCCESS(stack.send_message(res)); + + string response = std::string(bio.out_buffer.bytes(), bio.out_buffer.length()); + EXPECT_TRUE(response.find("RTSP/1.0 200 OK") != string::npos); + EXPECT_TRUE(response.find("CSeq: 3") != string::npos); + EXPECT_TRUE(response.find("Transport: RTP/AVP/TCP;unicast;interleaved=0-1;ssrc=1234ABCD;mode=\"play\"") != string::npos); + bio.out_buffer.erase(bio.out_buffer.length()); + } + + // Test UDP transport (should be rejected with 461 Unsupported Transport) + if (true) { + const char* udp_setup_req = + "SETUP rtsp://server.example.com/stream/trackID=0 RTSP/1.0\r\n" + "CSeq: 4\r\n" + "Transport: RTP/AVP;unicast;client_port=9000-9001\r\n\r\n"; + bio.in_buffer.append(udp_setup_req, strlen(udp_setup_req)); + + SrsRtspRequest* req = NULL; + HELPER_ASSERT_SUCCESS(stack.recv_message(&req)); + SrsUniquePtr req_uptr(req); + + EXPECT_TRUE(req->is_setup()); + EXPECT_STREQ("SETUP", req->method.c_str()); + EXPECT_EQ(4, req->seq); + EXPECT_TRUE(req->transport != NULL); + EXPECT_STREQ("RTP", req->transport->transport.c_str()); + EXPECT_STREQ("AVP", req->transport->profile.c_str()); + EXPECT_STREQ("", req->transport->lower_transport.c_str()); // UDP has empty lower_transport + EXPECT_STREQ("unicast", req->transport->cast_type.c_str()); + EXPECT_EQ(9000, req->transport->client_port_min); + EXPECT_EQ(9001, req->transport->client_port_max); + + // Simulate server rejecting UDP transport + SrsRtspSetupResponse* res = new SrsRtspSetupResponse(req->seq); + res->status = SRS_CONSTS_RTSP_UnsupportedTransport; + HELPER_ASSERT_SUCCESS(stack.send_message(res)); + + string response = std::string(bio.out_buffer.bytes(), bio.out_buffer.length()); + EXPECT_TRUE(response.find("RTSP/1.0 461 Unsupported Transport") != string::npos); + EXPECT_TRUE(response.find("CSeq: 4") != string::npos); + bio.out_buffer.erase(bio.out_buffer.length()); + } +} + +#ifdef SRS_RTC +// Test SDP advertisement of TCP-only transport +VOID TEST(ProtocolRTSPTest, RTSPSdpTcpOnlyAdvertisement) +{ + srs_error_t err; + + // Test that SDP properly advertises TCP-only transport + if (true) { + // Simulate SDP generation for TCP-only RTSP + SrsSdp sdp; + sdp.version_ = "0"; + sdp.username_ = "SRS RTSP Server"; + sdp.session_name_ = "Play"; + sdp.session_info_.setup_ = "passive"; // TCP-only indication + + // Add audio media with TCP transport + SrsMediaDesc media_audio("audio"); + media_audio.port_ = 0; // Port 0 = no UDP + media_audio.protos_ = "RTP/AVP/TCP"; // TCP transport + media_audio.session_info_.setup_ = "passive"; + media_audio.payload_types_.push_back(SrsMediaPayloadType(111)); + sdp.media_descs_.push_back(media_audio); + + // Add video media with TCP transport + SrsMediaDesc media_video("video"); + media_video.port_ = 0; // Port 0 = no UDP + media_video.protos_ = "RTP/AVP/TCP"; // TCP transport + media_video.session_info_.setup_ = "passive"; + media_video.payload_types_.push_back(SrsMediaPayloadType(96)); + sdp.media_descs_.push_back(media_video); + + // Encode SDP + std::ostringstream ss; + err = sdp.encode(ss); + HELPER_EXPECT_SUCCESS(err); + + string sdp_content = ss.str(); + + // Verify TCP-only indicators in SDP + EXPECT_TRUE(sdp_content.find("a=setup:passive") != string::npos); // Session-level TCP setup + EXPECT_TRUE(sdp_content.find("m=audio 0 RTP/AVP/TCP") != string::npos); // Audio TCP transport + EXPECT_TRUE(sdp_content.find("m=video 0 RTP/AVP/TCP") != string::npos); // Video TCP transport + + // Verify no UDP port allocation + EXPECT_FALSE(sdp_content.find("m=audio 9") != string::npos); // No UDP audio ports + EXPECT_FALSE(sdp_content.find("m=video 9") != string::npos); // No UDP video ports + + srs_trace("Generated TCP-only SDP:\n%s", sdp_content.c_str()); + } +} +#endif + +// Invalid RTSP Request +VOID TEST(ProtocolRTSPTest, RTSPInvalidRequest) +{ + srs_error_t err = srs_success; + + MockBufferIO bio; + SrsRtspStack stack(&bio); + + if (true) { + // Invalid request missing RTSP version + const char* invalid_req = + "OPTIONS rtsp://server.example.com/stream\r\n" + "CSeq: 1\r\n\r\n"; + bio.in_buffer.append(invalid_req, strlen(invalid_req)); + + SrsRtspRequest* req = NULL; + err = stack.recv_message(&req); + EXPECT_TRUE(err != srs_success); + srs_freep(req); + srs_error_reset(err); + + bio.in_buffer.erase(bio.in_buffer.length()); + } + + if (true) { + // Invalid method name + const char* invalid_method_req = + "INVALID_METHOD rtsp://server.example.com/stream RTSP/1.0\r\n" + "CSeq: 1\r\n\r\n"; + bio.in_buffer.append(invalid_method_req, strlen(invalid_method_req)); + + SrsRtspRequest* req = NULL; + err = stack.recv_message(&req); + EXPECT_TRUE(err != srs_success); + srs_freep(req); + srs_error_reset(err); + + bio.in_buffer.erase(bio.in_buffer.length()); + } + + if (true) { + // Missing CSeq header + const char* missing_cseq_req = + "DESCRIBE rtsp://server.example.com/stream RTSP/1.0\r\n" + "Accept: application/sdp\r\n\r\n"; + bio.in_buffer.append(missing_cseq_req, strlen(missing_cseq_req)); + + SrsRtspRequest* req = NULL; + err = stack.recv_message(&req); + EXPECT_TRUE(err != srs_success); + srs_freep(req); + srs_error_reset(err); + + bio.in_buffer.erase(bio.in_buffer.length()); + } + + if (true) { + // SETUP request missing Transport header + const char* missing_transport_req = + "SETUP rtsp://server.example.com/stream/trackID=0 RTSP/1.0\r\n" + "CSeq: 3\r\n\r\n"; + bio.in_buffer.append(missing_transport_req, strlen(missing_transport_req)); + + SrsRtspRequest* req = NULL; + HELPER_ASSERT_SUCCESS(stack.recv_message(&req)); + SrsUniquePtr req_uptr(req); + + EXPECT_TRUE(req->is_setup()); + EXPECT_TRUE(req->transport == NULL); + } + + if (true) { + // Invalid client port format + const char* invalid_port_req = + "SETUP rtsp://server.example.com/stream/trackID=0 RTSP/1.0\r\n" + "CSeq: 3\r\n" + "Transport: RTP/AVP;unicast;client_port=invalid\r\n\r\n"; + bio.in_buffer.append(invalid_port_req, strlen(invalid_port_req)); + + SrsRtspRequest* req = NULL; + HELPER_ASSERT_SUCCESS(stack.recv_message(&req)); + SrsUniquePtr req_uptr(req); + + EXPECT_TRUE(req->is_setup()); + EXPECT_TRUE(req->transport != NULL); + // Invalid port will be parsed as 0 + EXPECT_EQ(0, req->transport->client_port_min); + EXPECT_EQ(0, req->transport->client_port_max); + } + + if (true) { + // Missing transport protocol + const char* missing_proto_req = + "SETUP rtsp://server.example.com/stream/trackID=0 RTSP/1.0\r\n" + "CSeq: 4\r\n" + "Transport: ;unicast;client_port=9000-9001\r\n\r\n"; + bio.in_buffer.append(missing_proto_req, strlen(missing_proto_req)); + + SrsRtspRequest* req = NULL; + HELPER_ASSERT_SUCCESS(stack.recv_message(&req)); + SrsUniquePtr req_uptr(req); + + EXPECT_TRUE(req->is_setup()); + EXPECT_TRUE(req->transport != NULL); + // Transport field is empty + EXPECT_STREQ("", req->transport->transport.c_str()); + EXPECT_STREQ("", req->transport->profile.c_str()); + EXPECT_STREQ("", req->transport->lower_transport.c_str()); + EXPECT_STREQ("unicast", req->transport->cast_type.c_str()); + EXPECT_EQ(9000, req->transport->client_port_min); + EXPECT_EQ(9001, req->transport->client_port_max); + } + + if (true) { + // Try PLAY without session ID + const char* no_session_play_req = + "PLAY rtsp://server.example.com/stream RTSP/1.0\r\n" + "CSeq: 4\r\n" + "Range: npt=0.000-\r\n\r\n"; + bio.in_buffer.append(no_session_play_req, strlen(no_session_play_req)); + + SrsRtspRequest* req = NULL; + HELPER_ASSERT_SUCCESS(stack.recv_message(&req)); + SrsUniquePtr req_uptr(req); + + EXPECT_TRUE(req->is_play()); + EXPECT_STREQ("", req->session.c_str()); // 会话ID为空 + + // Should return 454 Session Not Found error + SrsRtspResponse* res = new SrsRtspResponse(req->seq); + res->status = SRS_CONSTS_RTSP_SessionNotFound; + HELPER_ASSERT_SUCCESS(stack.send_message(res)); + + string response = std::string(bio.out_buffer.bytes(), bio.out_buffer.length()); + EXPECT_TRUE(response.find("RTSP/1.0 454 Session Not Found") != string::npos); + EXPECT_TRUE(response.find("CSeq: 4") != string::npos); + bio.out_buffer.erase(bio.out_buffer.length()); + } + + if (true) { + // Use invalid session ID + const char* invalid_session_req = + "PLAY rtsp://server.example.com/stream RTSP/1.0\r\n" + "CSeq: 5\r\n" + "Session: invalid_session_id\r\n\r\n"; + bio.in_buffer.append(invalid_session_req, strlen(invalid_session_req)); + + SrsRtspRequest* req = NULL; + HELPER_ASSERT_SUCCESS(stack.recv_message(&req)); + SrsUniquePtr req_uptr(req); + + EXPECT_TRUE(req->is_play()); + EXPECT_STREQ("invalid_session_id", req->session.c_str()); + + // Should return 454 Session Not Found error + SrsRtspResponse* res = new SrsRtspResponse(req->seq); + res->status = SRS_CONSTS_RTSP_SessionNotFound; + HELPER_ASSERT_SUCCESS(stack.send_message(res)); + + string response = std::string(bio.out_buffer.bytes(), bio.out_buffer.length()); + EXPECT_TRUE(response.find("RTSP/1.0 454 Session Not Found") != string::npos); + EXPECT_TRUE(response.find("CSeq: 5") != string::npos); + bio.out_buffer.erase(bio.out_buffer.length()); + } + + if (true) { + // Request with custom headers + const char* custom_headers_req = + "OPTIONS rtsp://server.example.com/stream RTSP/1.0\r\n" + "CSeq: 7\r\n" + "X-Custom-Header1: CustomValue1\r\n" + "X-Custom-Header2: CustomValue2\r\n" + "X-Custom-Header3: CustomValue3\r\n" + "User-Agent: CustomUserAgent\r\n\r\n"; + bio.in_buffer.append(custom_headers_req, strlen(custom_headers_req)); + + SrsRtspRequest* req = NULL; + HELPER_ASSERT_SUCCESS(stack.recv_message(&req)); + SrsUniquePtr req_uptr(req); + + EXPECT_TRUE(req->is_options()); + EXPECT_EQ(7, req->seq); + // Custom headers will be ignored + EXPECT_STREQ("CustomUserAgent", req->user_agent.c_str()); + + SrsRtspOptionsResponse* res = new SrsRtspOptionsResponse(req->seq); + HELPER_ASSERT_SUCCESS(stack.send_message(res)); + + string response = std::string(bio.out_buffer.bytes(), bio.out_buffer.length()); + EXPECT_TRUE(response.find("RTSP/1.0 200 OK") != string::npos); + EXPECT_TRUE(response.find("CSeq: 7") != string::npos); + bio.out_buffer.erase(bio.out_buffer.length()); + } + + if (true) { + // Use invalid RTSP version + const char* invalid_version_req = + "OPTIONS rtsp://server.example.com/stream RTSP/2.0\r\n" + "CSeq: 1\r\n\r\n"; + bio.in_buffer.append(invalid_version_req, strlen(invalid_version_req)); + + SrsRtspRequest* req = NULL; + HELPER_ASSERT_SUCCESS(stack.recv_message(&req)); + SrsUniquePtr req_uptr(req); + + EXPECT_TRUE(req->is_options()); + EXPECT_STREQ("RTSP/2.0", req->version.c_str()); // Different version but still parsed + + // Should return RTSP version not supported error + SrsRtspResponse* res = new SrsRtspResponse(req->seq); + res->status = SRS_CONSTS_RTSP_RTSPVersionNotSupported; + HELPER_ASSERT_SUCCESS(stack.send_message(res)); + + string response = std::string(bio.out_buffer.bytes(), bio.out_buffer.length()); + EXPECT_TRUE(response.find("RTSP/1.0 505 RTSP Version Not Supported") != string::npos); + EXPECT_TRUE(response.find("CSeq: 1") != string::npos); + bio.out_buffer.erase(bio.out_buffer.length()); + } +} + +VOID TEST(ProtocolRTSPTest, RTSPConsumeRTCPThenRTSP) +{ + srs_error_t err = srs_success; + + MockBufferIO bio; + SrsRtspStack stack(&bio); + + if (true) { + // Create data with RTCP packet followed by RTSP message + string combined_data; + + // RTCP RR payload + unsigned char rtcp_payload[] = { + 0x81, // V=2, P=0, RC=1 + 0xC9, // PT=201 (RR) + 0x00, 0x07, // Length=7 (32-bit words) + 0x12, 0x34, 0x56, 0x78, // SSRC of packet sender + 0x87, 0x65, 0x43, 0x21, // SSRC_1 (source being reported on) + 0x00, // fraction lost + 0x00, 0x00, 0x00, // cumulative number of packets lost + 0x00, 0x00, 0x12, 0x34, // extended highest sequence number + 0x00, 0x00, 0x00, 0x10, // interarrival jitter + 0x00, 0x00, 0x00, 0x20, // last SR timestamp (LSR) + 0x00, 0x00, 0x00, 0x30 // delay since last SR (DLSR) + }; + + // Create RTSP over TCP frame: $ + channel + length + payload + unsigned char tcp_frame[4 + sizeof(rtcp_payload)]; + tcp_frame[0] = '$'; // Magic byte + tcp_frame[1] = 1; // Channel 1 (RTCP) + tcp_frame[2] = (sizeof(rtcp_payload) >> 8) & 0xFF; // Length high byte + tcp_frame[3] = sizeof(rtcp_payload) & 0xFF; // Length low byte + memcpy(tcp_frame + 4, rtcp_payload, sizeof(rtcp_payload)); + + // RTSP OPTIONS message + string rtsp_msg = "OPTIONS rtsp://example.com/stream RTSP/1.0\r\n" + "CSeq: 1\r\n" + "\r\n"; + + // Combine RTCP frame and RTSP data + combined_data.append((char*)tcp_frame, sizeof(tcp_frame)); + combined_data.append(rtsp_msg); + + bio.in_buffer.append(combined_data.c_str(), combined_data.length()); + + // Should successfully receive RTSP message after consuming RTCP + SrsRtspRequest* req = NULL; + HELPER_EXPECT_SUCCESS(stack.recv_message(&req)); + EXPECT_TRUE(req != NULL); + if (req != NULL) { + EXPECT_TRUE(req->is_options()); + EXPECT_EQ(1, req->seq); + } + + srs_freep(req); + bio.in_buffer.erase(bio.in_buffer.length()); + } +} + +VOID TEST(ProtocolRTSPTest, RTSPNotRTCPPacket) +{ + srs_error_t err = srs_success; + + MockBufferIO bio; + SrsRtspStack stack(&bio); + + if (true) { + // Regular RTSP message (not RTCP) + const char* rtsp_msg = "OPTIONS rtsp://example.com/stream RTSP/1.0\r\n" + "CSeq: 1\r\n" + "\r\n"; + bio.in_buffer.append(rtsp_msg, strlen(rtsp_msg)); + + // Should fail to consume as RTCP + HELPER_EXPECT_FAILED(stack.try_consume_rtcp_frame()); + + bio.in_buffer.erase(bio.in_buffer.length()); + } +} + +VOID TEST(ProtocolRTSPTest, RTSPIncompleteRTCPPacket) +{ + srs_error_t err = srs_success; + + MockBufferIO bio; + SrsRtspStack stack(&bio); + + if (true) { + // Create incomplete RTSP over TCP frame (only partial header) + unsigned char incomplete_frame[] = { + '$', // Magic byte + 1, // Channel + 0x00, // Length high byte + // Missing length low byte and payload + }; + + bio.in_buffer.append((char*)incomplete_frame, sizeof(incomplete_frame)); + + // Should fail due to incomplete frame + HELPER_EXPECT_FAILED(stack.try_consume_rtcp_frame()); + + bio.in_buffer.erase(bio.in_buffer.length()); + } +} +#endif + diff --git a/trunk/src/utest/srs_utest_rtc2.cpp b/trunk/src/utest/srs_utest_rtc2.cpp index cb70fc5f5..db1cfec78 100644 --- a/trunk/src/utest/srs_utest_rtc2.cpp +++ b/trunk/src/utest/srs_utest_rtc2.cpp @@ -342,14 +342,14 @@ VOID TEST(KernelRTC2Test, SrsCodecPayloadPerformanceCache) payload.name_ = "H264"; // First call does parsing and caching - auto start = std::chrono::high_resolution_clock::now(); + std::chrono::high_resolution_clock::now(); int8_t codec1 = payload.codec(true); - auto end1 = std::chrono::high_resolution_clock::now(); + std::chrono::high_resolution_clock::now(); // Subsequent calls should be faster (cached) - auto start2 = std::chrono::high_resolution_clock::now(); + std::chrono::high_resolution_clock::now(); int8_t codec2 = payload.codec(true); - auto end2 = std::chrono::high_resolution_clock::now(); + std::chrono::high_resolution_clock::now(); // Both should return the same result EXPECT_EQ(codec1, codec2);