diff --git a/.github/workflows/bump_hls_js.yml b/.github/workflows/bump_hls_js.yml index 3d45e961a24..9fde592d95e 100644 --- a/.github/workflows/bump_hls_js.yml +++ b/.github/workflows/bump_hls_js.yml @@ -11,21 +11,48 @@ jobs: steps: - uses: actions/checkout@v4 - with: - fetch-depth: 0 - run: > git config user.name mediamtx-bot && git config user.email bot@mediamtx && ((git checkout deps/hlsjs && git rebase ${GITHUB_REF_NAME}) || git checkout -b deps/hlsjs) - - run: | - set -e - VERSION=$(curl -s https://api.github.com/repos/video-dev/hls.js/releases?per_page=1 | grep tag_name | sed 's/\s\+"tag_name": "\(.\+\)",/\1/') - HASH=$(curl -sL https://github.com/video-dev/hls.js/releases/download/$VERSION/release.zip -o- | sha256sum | cut -f1 -d ' ') - echo $VERSION > internal/servers/hls/hlsjsdownloader/VERSION - echo $HASH > internal/servers/hls/hlsjsdownloader/HASH - echo VERSION=$VERSION >> $GITHUB_ENV + - uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { createHash } = require('crypto'); + const fs = require('fs').promises; + + // get last release + let curRelease = null; + for (let i = 1; i < 20; i++) { + const releases = await github.rest.repos.listReleases({ + owner: 'video-dev', + repo: 'hls.js', + page: i, + }); + for (const release of releases.data) { + if (!release.prerelease) { + curRelease = release; + break; + } + } + if (curRelease !== null) { + break; + } + } + + // compute checksum + const content = await github.request(`https://github.com/video-dev/hls.js/releases/download/${curRelease['tag_name']}/release.zip`); + const hash = createHash('sha256').update(Buffer.from(content.data)).digest('hex'); + + // write version and checksum to disk + await fs.writeFile('internal/servers/hls/hlsjsdownloader/VERSION', curRelease['tag_name'] + '\n', 'utf-8'); + await fs.writeFile('internal/servers/hls/hlsjsdownloader/HASH', hash + '\n', 'utf-8'); + + // make version available to next steps + core.exportVariable('VERSION', curRelease['tag_name']); - id: check_repo run: > diff --git a/.github/workflows/bump_rpicamera.yml b/.github/workflows/bump_rpicamera.yml new file mode 100644 index 00000000000..fa6a8a06730 --- /dev/null +++ b/.github/workflows/bump_rpicamera.yml @@ -0,0 +1,87 @@ +name: bump_rpicamera + +on: + schedule: + - cron: '4 5 * * *' + workflow_dispatch: + +jobs: + bump_rpicamera: + runs-on: ubuntu-22.04 + + steps: + - uses: actions/checkout@v4 + + - run: > + git config user.name mediamtx-bot + && git config user.email bot@mediamtx + && ((git checkout deps/mediamtx-rpicamera && git rebase ${GITHUB_REF_NAME}) || git checkout -b deps/mediamtx-rpicamera) + + - uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs').promises; + + // get last release + let curRelease = null; + for (let i = 1; i < 20; i++) { + const releases = await github.rest.repos.listReleases({ + owner: 'bluenviron', + repo: 'mediamtx-rpicamera', + page: i, + }); + for (const release of releases.data) { + curRelease = release; + break; + } + if (curRelease !== null) { + break; + } + } + + // write version to disk + await fs.writeFile('internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION', curRelease['tag_name'] + '\n', 'utf-8'); + + // make version available to next steps + core.exportVariable('VERSION', curRelease.name); + + - id: check_repo + run: > + test -n "$(git status --porcelain)" && echo "update=1" >> "$GITHUB_OUTPUT" || echo "update=0" >> "$GITHUB_OUTPUT" + + - if: ${{ steps.check_repo.outputs.update == '1' }} + run: > + git reset ${GITHUB_REF_NAME} + && git add . + && git commit -m "bump mediamtx-rpicamera to ${VERSION}" + && git push --set-upstream origin deps/mediamtx-rpicamera --force + + - if: ${{ steps.check_repo.outputs.update == '1' }} + uses: actions/github-script@v6 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const prs = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + head: `${context.repo.owner}:deps/mediamtx-rpicamera`, + state: 'open', + }); + + if (prs.data.length == 0) { + await github.rest.pulls.create({ + owner: context.repo.owner, + repo: context.repo.repo, + head: 'deps/mediamtx-rpicamera', + base: context.ref.slice('refs/heads/'.length), + title: `bump mediamtx-rpicamera to ${process.env.VERSION}`, + }); + } else { + github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prs.data[0].number, + title: `bump mediamtx-rpicamera to ${process.env.VERSION}`, + }); + } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 96c7070b3be..53d748bbab5 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,8 +11,6 @@ jobs: steps: - uses: actions/checkout@v4 - with: - fetch-depth: 0 - run: make binaries diff --git a/go.mod b/go.mod index 6daa5c7237e..4073369418f 100644 --- a/go.mod +++ b/go.mod @@ -10,11 +10,13 @@ require ( github.com/alecthomas/kong v1.2.1 github.com/asticode/go-astits v1.13.0 github.com/bluenviron/gohlslib/v2 v2.0.0 - github.com/bluenviron/gortsplib/v4 v4.11.0 - github.com/bluenviron/mediacommon v1.13.0 + github.com/bluenviron/gortsplib/v4 v4.11.1 + github.com/bluenviron/mediacommon v1.13.1 github.com/datarhei/gosrt v0.7.0 github.com/fsnotify/fsnotify v1.7.0 + github.com/gin-contrib/pprof v1.5.0 github.com/gin-gonic/gin v1.10.0 + github.com/go-git/go-billy/v5 v5.5.0 github.com/go-git/go-git/v5 v5.12.0 github.com/golang-jwt/jwt/v5 v5.2.1 github.com/google/uuid v1.6.0 @@ -53,7 +55,6 @@ require ( github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gin-contrib/sse v0.1.0 // indirect github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.5.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/validator/v10 v10.20.0 // indirect diff --git a/go.sum b/go.sum index 8ba558dc3a5..f285248eaf1 100644 --- a/go.sum +++ b/go.sum @@ -33,10 +33,10 @@ github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c h1:8XZeJrs4+ZYh github.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c/go.mod h1:x1vxHcL/9AVzuk5HOloOEPrtJY0MaalYr78afXZ+pWI= github.com/bluenviron/gohlslib/v2 v2.0.0 h1:qAKT1ksqJT1Cc3xRseYPSiN51vqeSY1HDMnmUgQ6e0g= github.com/bluenviron/gohlslib/v2 v2.0.0/go.mod h1:EoDeIps+MwLfm8o9m5N5YI1XgYXGeFne2Oz1kN9hudk= -github.com/bluenviron/gortsplib/v4 v4.11.0 h1:YPhXtxZS5M6yaMWmPSXt5wmOQr8CA2m2CHcdC+hisu4= -github.com/bluenviron/gortsplib/v4 v4.11.0/go.mod h1:ssggvDYcUCozVQUonQ9NJSjg7rI8zYeRmDt3sL1yqSo= -github.com/bluenviron/mediacommon v1.13.0 h1:axejqONTDkhBxAN1q+L0nEJn/VtARmE97CF3J1Bt414= -github.com/bluenviron/mediacommon v1.13.0/go.mod h1:HDyW2CzjvhYJXtdxstdFPio3G0qSocPhqkhUt/qffec= +github.com/bluenviron/gortsplib/v4 v4.11.1 h1:yq9mCVydwRUjyc4dzGrNs7/R8DvErbZ7fzIzBPlwVl8= +github.com/bluenviron/gortsplib/v4 v4.11.1/go.mod h1:yVEgmJnwHGo3Po7dWW0aVjZGCUmOULEZp9i1IMtWqao= +github.com/bluenviron/mediacommon v1.13.1 h1:agxDtkooknxSxOO/oOpB+tEW48OLMqty1PDMC3x2n4E= +github.com/bluenviron/mediacommon v1.13.1/go.mod h1:HDyW2CzjvhYJXtdxstdFPio3G0qSocPhqkhUt/qffec= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= @@ -69,6 +69,8 @@ github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nos github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/pprof v1.5.0 h1:E/Oy7g+kNw94KfdCy3bZxQFtyDnAX2V7axRS7sNYVrU= +github.com/gin-contrib/pprof v1.5.0/go.mod h1:GqFL6LerKoCQ/RSWnkYczkTJ+tOAUVN/8sbnEtaqOKs= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= diff --git a/internal/api/api.go b/internal/api/api.go index 15045224238..5398642a976 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -149,7 +149,7 @@ type API struct { SRTServer SRTServer Parent apiParent - httpServer *httpp.WrappedServer + httpServer *httpp.Server mutex sync.RWMutex } @@ -158,77 +158,79 @@ func (a *API) Initialize() error { router := gin.New() router.SetTrustedProxies(a.TrustedProxies.ToTrustedProxies()) //nolint:errcheck - router.NoRoute(a.middlewareOrigin, a.middlewareAuth) - group := router.Group("/", a.middlewareOrigin, a.middlewareAuth) + router.Use(a.middlewareOrigin) + router.Use(a.middlewareAuth) - group.GET("/v3/config/global/get", a.onConfigGlobalGet) - group.PATCH("/v3/config/global/patch", a.onConfigGlobalPatch) + group := router.Group("/v3") - group.GET("/v3/config/pathdefaults/get", a.onConfigPathDefaultsGet) - group.PATCH("/v3/config/pathdefaults/patch", a.onConfigPathDefaultsPatch) + group.GET("/config/global/get", a.onConfigGlobalGet) + group.PATCH("/config/global/patch", a.onConfigGlobalPatch) - group.GET("/v3/config/paths/list", a.onConfigPathsList) - group.GET("/v3/config/paths/get/*name", a.onConfigPathsGet) - group.POST("/v3/config/paths/add/*name", a.onConfigPathsAdd) - group.PATCH("/v3/config/paths/patch/*name", a.onConfigPathsPatch) - group.POST("/v3/config/paths/replace/*name", a.onConfigPathsReplace) - group.DELETE("/v3/config/paths/delete/*name", a.onConfigPathsDelete) + group.GET("/config/pathdefaults/get", a.onConfigPathDefaultsGet) + group.PATCH("/config/pathdefaults/patch", a.onConfigPathDefaultsPatch) - group.GET("/v3/paths/list", a.onPathsList) - group.GET("/v3/paths/get/*name", a.onPathsGet) + group.GET("/config/paths/list", a.onConfigPathsList) + group.GET("/config/paths/get/*name", a.onConfigPathsGet) + group.POST("/config/paths/add/*name", a.onConfigPathsAdd) + group.PATCH("/config/paths/patch/*name", a.onConfigPathsPatch) + group.POST("/config/paths/replace/*name", a.onConfigPathsReplace) + group.DELETE("/config/paths/delete/*name", a.onConfigPathsDelete) + + group.GET("/paths/list", a.onPathsList) + group.GET("/paths/get/*name", a.onPathsGet) if !interfaceIsEmpty(a.HLSServer) { - group.GET("/v3/hlsmuxers/list", a.onHLSMuxersList) - group.GET("/v3/hlsmuxers/get/*name", a.onHLSMuxersGet) + group.GET("/hlsmuxers/list", a.onHLSMuxersList) + group.GET("/hlsmuxers/get/*name", a.onHLSMuxersGet) } if !interfaceIsEmpty(a.RTSPServer) { - group.GET("/v3/rtspconns/list", a.onRTSPConnsList) - group.GET("/v3/rtspconns/get/:id", a.onRTSPConnsGet) - group.GET("/v3/rtspsessions/list", a.onRTSPSessionsList) - group.GET("/v3/rtspsessions/get/:id", a.onRTSPSessionsGet) - group.POST("/v3/rtspsessions/kick/:id", a.onRTSPSessionsKick) + group.GET("/rtspconns/list", a.onRTSPConnsList) + group.GET("/rtspconns/get/:id", a.onRTSPConnsGet) + group.GET("/rtspsessions/list", a.onRTSPSessionsList) + group.GET("/rtspsessions/get/:id", a.onRTSPSessionsGet) + group.POST("/rtspsessions/kick/:id", a.onRTSPSessionsKick) } if !interfaceIsEmpty(a.RTSPSServer) { - group.GET("/v3/rtspsconns/list", a.onRTSPSConnsList) - group.GET("/v3/rtspsconns/get/:id", a.onRTSPSConnsGet) - group.GET("/v3/rtspssessions/list", a.onRTSPSSessionsList) - group.GET("/v3/rtspssessions/get/:id", a.onRTSPSSessionsGet) - group.POST("/v3/rtspssessions/kick/:id", a.onRTSPSSessionsKick) + group.GET("/rtspsconns/list", a.onRTSPSConnsList) + group.GET("/rtspsconns/get/:id", a.onRTSPSConnsGet) + group.GET("/rtspssessions/list", a.onRTSPSSessionsList) + group.GET("/rtspssessions/get/:id", a.onRTSPSSessionsGet) + group.POST("/rtspssessions/kick/:id", a.onRTSPSSessionsKick) } if !interfaceIsEmpty(a.RTMPServer) { - group.GET("/v3/rtmpconns/list", a.onRTMPConnsList) - group.GET("/v3/rtmpconns/get/:id", a.onRTMPConnsGet) - group.POST("/v3/rtmpconns/kick/:id", a.onRTMPConnsKick) + group.GET("/rtmpconns/list", a.onRTMPConnsList) + group.GET("/rtmpconns/get/:id", a.onRTMPConnsGet) + group.POST("/rtmpconns/kick/:id", a.onRTMPConnsKick) } if !interfaceIsEmpty(a.RTMPSServer) { - group.GET("/v3/rtmpsconns/list", a.onRTMPSConnsList) - group.GET("/v3/rtmpsconns/get/:id", a.onRTMPSConnsGet) - group.POST("/v3/rtmpsconns/kick/:id", a.onRTMPSConnsKick) + group.GET("/rtmpsconns/list", a.onRTMPSConnsList) + group.GET("/rtmpsconns/get/:id", a.onRTMPSConnsGet) + group.POST("/rtmpsconns/kick/:id", a.onRTMPSConnsKick) } if !interfaceIsEmpty(a.WebRTCServer) { - group.GET("/v3/webrtcsessions/list", a.onWebRTCSessionsList) - group.GET("/v3/webrtcsessions/get/:id", a.onWebRTCSessionsGet) - group.POST("/v3/webrtcsessions/kick/:id", a.onWebRTCSessionsKick) + group.GET("/webrtcsessions/list", a.onWebRTCSessionsList) + group.GET("/webrtcsessions/get/:id", a.onWebRTCSessionsGet) + group.POST("/webrtcsessions/kick/:id", a.onWebRTCSessionsKick) } if !interfaceIsEmpty(a.SRTServer) { - group.GET("/v3/srtconns/list", a.onSRTConnsList) - group.GET("/v3/srtconns/get/:id", a.onSRTConnsGet) - group.POST("/v3/srtconns/kick/:id", a.onSRTConnsKick) + group.GET("/srtconns/list", a.onSRTConnsList) + group.GET("/srtconns/get/:id", a.onSRTConnsGet) + group.POST("/srtconns/kick/:id", a.onSRTConnsKick) } - group.GET("/v3/recordings/list", a.onRecordingsList) - group.GET("/v3/recordings/get/*name", a.onRecordingsGet) - group.DELETE("/v3/recordings/deletesegment", a.onRecordingDeleteSegment) + group.GET("/recordings/list", a.onRecordingsList) + group.GET("/recordings/get/*name", a.onRecordingsGet) + group.DELETE("/recordings/deletesegment", a.onRecordingDeleteSegment) network, address := restrictnetwork.Restrict("tcp", a.Address) - a.httpServer = &httpp.WrappedServer{ + a.httpServer = &httpp.Server{ Network: network, Address: address, ReadTimeout: time.Duration(a.ReadTimeout), @@ -270,14 +272,14 @@ func (a *API) writeError(ctx *gin.Context, status int, err error) { } func (a *API) middlewareOrigin(ctx *gin.Context) { - ctx.Writer.Header().Set("Access-Control-Allow-Origin", a.AllowOrigin) - ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true") + ctx.Header("Access-Control-Allow-Origin", a.AllowOrigin) + ctx.Header("Access-Control-Allow-Credentials", "true") // preflight requests if ctx.Request.Method == http.MethodOptions && ctx.Request.Header.Get("Access-Control-Request-Method") != "" { - ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PATCH, DELETE") - ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type") + ctx.Header("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PATCH, DELETE") + ctx.Header("Access-Control-Allow-Headers", "Authorization, Content-Type") ctx.AbortWithStatus(http.StatusNoContent) return } diff --git a/internal/conf/conf_test.go b/internal/conf/conf_test.go index 5d81674c26d..6c24d8a37e0 100644 --- a/internal/conf/conf_test.go +++ b/internal/conf/conf_test.go @@ -75,7 +75,7 @@ func TestConfFromFile(t *testing.T) { RPICameraTextOverlay: "%Y-%m-%d %H:%M:%S - MediaMTX", RPICameraCodec: "auto", RPICameraIDRPeriod: 60, - RPICameraBitrate: 1000000, + RPICameraBitrate: 5000000, RPICameraProfile: "main", RPICameraLevel: "4.1", RunOnDemandStartTimeout: 5 * StringDuration(time.Second), diff --git a/internal/conf/path.go b/internal/conf/path.go index 0ae1e0bceee..f0f75502a31 100644 --- a/internal/conf/path.go +++ b/internal/conf/path.go @@ -218,7 +218,7 @@ func (pconf *Path) setDefaults() { pconf.RPICameraTextOverlay = "%Y-%m-%d %H:%M:%S - MediaMTX" pconf.RPICameraCodec = "auto" pconf.RPICameraIDRPeriod = 60 - pconf.RPICameraBitrate = 1000000 + pconf.RPICameraBitrate = 5000000 pconf.RPICameraProfile = "main" pconf.RPICameraLevel = "4.1" @@ -578,17 +578,7 @@ func (pconf *Path) Equal(other *Path) bool { // HasStaticSource checks whether the path has a static source. func (pconf Path) HasStaticSource() bool { - return strings.HasPrefix(pconf.Source, "rtsp://") || - strings.HasPrefix(pconf.Source, "rtsps://") || - strings.HasPrefix(pconf.Source, "rtmp://") || - strings.HasPrefix(pconf.Source, "rtmps://") || - strings.HasPrefix(pconf.Source, "http://") || - strings.HasPrefix(pconf.Source, "https://") || - strings.HasPrefix(pconf.Source, "udp://") || - strings.HasPrefix(pconf.Source, "srt://") || - strings.HasPrefix(pconf.Source, "whep://") || - strings.HasPrefix(pconf.Source, "wheps://") || - pconf.Source == "rpiCamera" + return pconf.Source != "publisher" && pconf.Source != "redirect" } // HasOnDemandStaticSource checks whether the path has a on demand static source. diff --git a/internal/core/api_test.go b/internal/core/api_test.go index a54ec12c45f..c868d9d3eb2 100644 --- a/internal/core/api_test.go +++ b/internal/core/api_test.go @@ -1071,7 +1071,7 @@ func TestAPIProtocolKick(t *testing.T) { } `json:"items"` } httpRequest(t, hc, http.MethodGet, "http://localhost:9997/v3/"+pa+"/list", nil, &out2) - require.Equal(t, 0, len(out2.Items)) + require.Empty(t, out2.Items) }) } } diff --git a/internal/core/core.go b/internal/core/core.go index c8b25272fb5..4255d8dd9dc 100644 --- a/internal/core/core.go +++ b/internal/core/core.go @@ -105,7 +105,7 @@ func New(args []string) (*Core, bool) { parser.FatalIfErrorf(err) if cli.Version { - fmt.Println(version) + fmt.Println(string(version)) os.Exit(0) } diff --git a/internal/core/path_manager_test.go b/internal/core/path_manager_test.go index 6b506fced67..cadd5cd58fb 100644 --- a/internal/core/path_manager_test.go +++ b/internal/core/path_manager_test.go @@ -78,7 +78,7 @@ func TestPathAutoDeletion(t *testing.T) { data, err := p.pathManager.APIPathsList() require.NoError(t, err) - require.Equal(t, 0, len(data.Items)) + require.Empty(t, data.Items) }) } } diff --git a/internal/core/versiongetter/main.go b/internal/core/versiongetter/main.go index 91602ef5b51..80f76d37776 100644 --- a/internal/core/versiongetter/main.go +++ b/internal/core/versiongetter/main.go @@ -2,25 +2,29 @@ package main import ( + "fmt" "log" "os" "strconv" "strings" + "github.com/go-git/go-billy/v5/osfs" "github.com/go-git/go-git/v5" "github.com/go-git/go-git/v5/plumbing" + "github.com/go-git/go-git/v5/plumbing/cache" + "github.com/go-git/go-git/v5/storage/filesystem" ) // Golang version of git describe --tags func gitDescribeTags(repo *git.Repository) (string, error) { head, err := repo.Head() if err != nil { - return "", err + return "", fmt.Errorf("failed to get HEAD: %w", err) } tagIterator, err := repo.Tags() if err != nil { - return "", err + return "", fmt.Errorf("failed to get tags: %w", err) } defer tagIterator.Close() @@ -35,12 +39,12 @@ func gitDescribeTags(repo *git.Repository) (string, error) { return nil }) if err != nil { - return "", err + return "", fmt.Errorf("failed to iterate tags: %w", err) } cIter, err := repo.Log(&git.LogOptions{From: head.Hash()}) if err != nil { - return "", err + return "", fmt.Errorf("failed to get log: %w", err) } i := 0 @@ -48,7 +52,7 @@ func gitDescribeTags(repo *git.Repository) (string, error) { for { commit, err := cIter.Next() if err != nil { - return "", err + return "", fmt.Errorf("failed to get next commit: %w", err) } if str, ok := tags[commit.Hash]; ok { @@ -65,27 +69,36 @@ func gitDescribeTags(repo *git.Repository) (string, error) { } } -func do() error { - log.Println("getting mediamtx version...") - - repo, err := git.PlainOpen("../..") +func tagFromGit() error { + // [git.PlainOpen] uses a ChrootOS that limits filesystem access to the .git directory only. + // + // Unfortunately, this can cause issues with package build environments such as Arch Linux's, + // where .git/objects/info/alternates points to a directory outside of the .git directory. + // + // To work around this, specify an AlternatesFS that allows access to the entire filesystem. + storerFs := osfs.New("../../.git", osfs.WithBoundOS()) + storer := filesystem.NewStorageWithOptions(storerFs, cache.NewObjectLRUDefault(), filesystem.Options{ + AlternatesFS: osfs.New("/", osfs.WithBoundOS()), + }) + worktreeFs := osfs.New("../..", osfs.WithBoundOS()) + repo, err := git.Open(storer, worktreeFs) if err != nil { - return err + return fmt.Errorf("failed to open repository: %w", err) } version, err := gitDescribeTags(repo) if err != nil { - return err + return fmt.Errorf("failed to get version: %w", err) } wt, err := repo.Worktree() if err != nil { - return err + return fmt.Errorf("failed to get worktree: %w", err) } status, err := wt.Status() if err != nil { - return err + return fmt.Errorf("failed to get status: %w", err) } if !status.IsClean() { @@ -94,13 +107,28 @@ func do() error { err = os.WriteFile("VERSION", []byte(version), 0o644) if err != nil { - return err + return fmt.Errorf("failed to write version file: %w", err) } log.Printf("ok (%s)", version) return nil } +func do() error { + log.Println("getting mediamtx version...") + + err := tagFromGit() + if err != nil { + log.Println("WARN: cannot get tag from .git folder, using v0.0.0 as version") + err = os.WriteFile("VERSION", []byte("v0.0.0"), 0o644) + if err != nil { + return fmt.Errorf("failed to write version file: %w", err) + } + } + + return nil +} + func main() { err := do() if err != nil { diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go index 9332e956828..0c7ec0078e4 100644 --- a/internal/metrics/metrics.go +++ b/internal/metrics/metrics.go @@ -52,7 +52,7 @@ type Metrics struct { AuthManager metricsAuthManager Parent metricsParent - httpServer *httpp.WrappedServer + httpServer *httpp.Server mutex sync.Mutex pathManager api.PathManager rtspServer api.RTSPServer @@ -68,11 +68,15 @@ type Metrics struct { func (m *Metrics) Initialize() error { router := gin.New() router.SetTrustedProxies(m.TrustedProxies.ToTrustedProxies()) //nolint:errcheck - router.NoRoute(m.onRequest) + + router.Use(m.middlewareOrigin) + router.Use(m.middlewareAuth) + + router.GET("/metrics", m.onMetrics) network, address := restrictnetwork.Restrict("tcp", m.Address) - m.httpServer = &httpp.WrappedServer{ + m.httpServer = &httpp.Server{ Network: network, Address: address, ReadTimeout: time.Duration(m.ReadTimeout), @@ -103,23 +107,21 @@ func (m *Metrics) Log(level logger.Level, format string, args ...interface{}) { m.Parent.Log(level, "[metrics] "+format, args...) } -func (m *Metrics) onRequest(ctx *gin.Context) { - ctx.Writer.Header().Set("Access-Control-Allow-Origin", m.AllowOrigin) - ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true") +func (m *Metrics) middlewareOrigin(ctx *gin.Context) { + ctx.Header("Access-Control-Allow-Origin", m.AllowOrigin) + ctx.Header("Access-Control-Allow-Credentials", "true") // preflight requests if ctx.Request.Method == http.MethodOptions && ctx.Request.Header.Get("Access-Control-Request-Method") != "" { - ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET") - ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization") - ctx.Writer.WriteHeader(http.StatusNoContent) - return - } - - if ctx.Request.URL.Path != "/metrics" || ctx.Request.Method != http.MethodGet { + ctx.Header("Access-Control-Allow-Methods", "OPTIONS, GET") + ctx.Header("Access-Control-Allow-Headers", "Authorization") + ctx.AbortWithStatus(http.StatusNoContent) return } +} +func (m *Metrics) middlewareAuth(ctx *gin.Context) { err := m.AuthManager.Authenticate(&auth.Request{ IP: net.ParseIP(ctx.ClientIP()), Action: conf.AuthActionMetrics, @@ -135,10 +137,12 @@ func (m *Metrics) onRequest(ctx *gin.Context) { // wait some seconds to mitigate brute force attacks <-time.After(auth.PauseAfterError) - ctx.Writer.WriteHeader(http.StatusUnauthorized) + ctx.AbortWithStatus(http.StatusUnauthorized) return } +} +func (m *Metrics) onMetrics(ctx *gin.Context) { out := "" data, err := m.pathManager.APIPathsList() diff --git a/internal/playback/server.go b/internal/playback/server.go index 1550116eb97..a5c7f22615c 100644 --- a/internal/playback/server.go +++ b/internal/playback/server.go @@ -32,7 +32,7 @@ type Server struct { AuthManager serverAuthManager Parent logger.Writer - httpServer *httpp.WrappedServer + httpServer *httpp.Server mutex sync.RWMutex } @@ -41,15 +41,14 @@ func (s *Server) Initialize() error { router := gin.New() router.SetTrustedProxies(s.TrustedProxies.ToTrustedProxies()) //nolint:errcheck - router.NoRoute(s.middlewareOrigin) - group := router.Group("/", s.middlewareOrigin) + router.Use(s.middlewareOrigin) - group.GET("/list", s.onList) - group.GET("/get", s.onGet) + router.GET("/list", s.onList) + router.GET("/get", s.onGet) network, address := restrictnetwork.Restrict("tcp", s.Address) - s.httpServer = &httpp.WrappedServer{ + s.httpServer = &httpp.Server{ Network: network, Address: address, ReadTimeout: time.Duration(s.ReadTimeout), @@ -104,14 +103,14 @@ func (s *Server) safeFindPathConf(name string) (*conf.Path, error) { } func (s *Server) middlewareOrigin(ctx *gin.Context) { - ctx.Writer.Header().Set("Access-Control-Allow-Origin", s.AllowOrigin) - ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true") + ctx.Header("Access-Control-Allow-Origin", s.AllowOrigin) + ctx.Header("Access-Control-Allow-Credentials", "true") // preflight requests if ctx.Request.Method == http.MethodOptions && ctx.Request.Header.Get("Access-Control-Request-Method") != "" { - ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET") - ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization") + ctx.Header("Access-Control-Allow-Methods", "OPTIONS, GET") + ctx.Header("Access-Control-Allow-Headers", "Authorization") ctx.AbortWithStatus(http.StatusNoContent) return } diff --git a/internal/pprof/pprof.go b/internal/pprof/pprof.go index a4ac5c821fc..6575778974f 100644 --- a/internal/pprof/pprof.go +++ b/internal/pprof/pprof.go @@ -6,15 +6,14 @@ import ( "net/http" "time" - // start pprof - _ "net/http/pprof" + "github.com/gin-contrib/pprof" + "github.com/gin-gonic/gin" "github.com/bluenviron/mediamtx/internal/auth" "github.com/bluenviron/mediamtx/internal/conf" "github.com/bluenviron/mediamtx/internal/logger" "github.com/bluenviron/mediamtx/internal/protocols/httpp" "github.com/bluenviron/mediamtx/internal/restrictnetwork" - "github.com/gin-gonic/gin" ) type pprofAuthManager interface { @@ -37,18 +36,22 @@ type PPROF struct { AuthManager pprofAuthManager Parent pprofParent - httpServer *httpp.WrappedServer + httpServer *httpp.Server } // Initialize initializes PPROF. func (pp *PPROF) Initialize() error { router := gin.New() router.SetTrustedProxies(pp.TrustedProxies.ToTrustedProxies()) //nolint:errcheck - router.NoRoute(pp.onRequest) + + router.Use(pp.middlewareOrigin) + router.Use(pp.middlewareAuth) + + pprof.Register(router) network, address := restrictnetwork.Restrict("tcp", pp.Address) - pp.httpServer = &httpp.WrappedServer{ + pp.httpServer = &httpp.Server{ Network: network, Address: address, ReadTimeout: time.Duration(pp.ReadTimeout), @@ -79,37 +82,37 @@ func (pp *PPROF) Log(level logger.Level, format string, args ...interface{}) { pp.Parent.Log(level, "[pprof] "+format, args...) } -func (pp *PPROF) onRequest(ctx *gin.Context) { - ctx.Writer.Header().Set("Access-Control-Allow-Origin", pp.AllowOrigin) - ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true") +func (pp *PPROF) middlewareOrigin(ctx *gin.Context) { + ctx.Header("Access-Control-Allow-Origin", pp.AllowOrigin) + ctx.Header("Access-Control-Allow-Credentials", "true") // preflight requests if ctx.Request.Method == http.MethodOptions && ctx.Request.Header.Get("Access-Control-Request-Method") != "" { - ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET") - ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization") - ctx.Writer.WriteHeader(http.StatusNoContent) + ctx.Header("Access-Control-Allow-Methods", "OPTIONS, GET") + ctx.Header("Access-Control-Allow-Headers", "Authorization") + ctx.AbortWithStatus(http.StatusNoContent) return } +} +func (pp *PPROF) middlewareAuth(ctx *gin.Context) { err := pp.AuthManager.Authenticate(&auth.Request{ IP: net.ParseIP(ctx.ClientIP()), - Action: conf.AuthActionMetrics, + Action: conf.AuthActionPprof, HTTPRequest: ctx.Request, }) if err != nil { if err.(*auth.Error).AskCredentials { //nolint:errorlint - ctx.Writer.Header().Set("WWW-Authenticate", `Basic realm="mediamtx"`) - ctx.Writer.WriteHeader(http.StatusUnauthorized) + ctx.Header("WWW-Authenticate", `Basic realm="mediamtx"`) + ctx.AbortWithStatus(http.StatusUnauthorized) return } // wait some seconds to mitigate brute force attacks <-time.After(auth.PauseAfterError) - ctx.Writer.WriteHeader(http.StatusUnauthorized) + ctx.AbortWithStatus(http.StatusUnauthorized) return } - - http.DefaultServeMux.ServeHTTP(ctx.Writer, ctx.Request) } diff --git a/internal/pprof/pprof_test.go b/internal/pprof/pprof_test.go index d26de96514b..16cb16f43d4 100644 --- a/internal/pprof/pprof_test.go +++ b/internal/pprof/pprof_test.go @@ -46,3 +46,33 @@ func TestPreflightRequest(t *testing.T) { require.Equal(t, "Authorization", res.Header.Get("Access-Control-Allow-Headers")) require.Equal(t, byts, []byte{}) } + +func TestPprof(t *testing.T) { + s := &PPROF{ + Address: "127.0.0.1:9999", + AllowOrigin: "*", + ReadTimeout: conf.StringDuration(10 * time.Second), + AuthManager: test.NilAuthManager, + Parent: test.NilLogger, + } + err := s.Initialize() + require.NoError(t, err) + defer s.Close() + + tr := &http.Transport{} + defer tr.CloseIdleConnections() + hc := &http.Client{Transport: tr} + + req, err := http.NewRequest(http.MethodGet, "http://127.0.0.1:9999/debug/pprof/heap", nil) + require.NoError(t, err) + + res, err := hc.Do(req) + require.NoError(t, err) + defer res.Body.Close() + + require.Equal(t, http.StatusOK, res.StatusCode) + + byts, err := io.ReadAll(res.Body) + require.NoError(t, err) + require.NotEmpty(t, byts) +} diff --git a/internal/protocols/httpp/wrapped_server.go b/internal/protocols/httpp/server.go similarity index 89% rename from internal/protocols/httpp/wrapped_server.go rename to internal/protocols/httpp/server.go index 71a15f004c3..519246e0836 100644 --- a/internal/protocols/httpp/wrapped_server.go +++ b/internal/protocols/httpp/server.go @@ -20,14 +20,14 @@ func (nilWriter) Write(p []byte) (int, error) { return len(p), nil } -// WrappedServer is a wrapper around http.Server that provides: +// Server is a wrapper around http.Server that provides: // - net.Listener allocation and closure // - TLS allocation // - exit on panic // - logging // - server header // - filtering of invalid requests -type WrappedServer struct { +type Server struct { Network string Address string ReadTimeout time.Duration @@ -42,8 +42,8 @@ type WrappedServer struct { loader *certloader.CertLoader } -// Initialize initializes a WrappedServer. -func (s *WrappedServer) Initialize() error { +// Initialize initializes a Server. +func (s *Server) Initialize() error { var tlsConfig *tls.Config if s.Encryption { if s.ServerCert == "" { @@ -91,7 +91,7 @@ func (s *WrappedServer) Initialize() error { } // Close closes all resources and waits for all routines to return. -func (s *WrappedServer) Close() { +func (s *Server) Close() { ctx, ctxCancel := context.WithCancel(context.Background()) ctxCancel() s.inner.Shutdown(ctx) diff --git a/internal/protocols/httpp/wrapped_server_test.go b/internal/protocols/httpp/server_test.go similarity index 97% rename from internal/protocols/httpp/wrapped_server_test.go rename to internal/protocols/httpp/server_test.go index e5deda644ee..a8ace896913 100644 --- a/internal/protocols/httpp/wrapped_server_test.go +++ b/internal/protocols/httpp/server_test.go @@ -12,7 +12,7 @@ import ( ) func TestFilterEmptyPath(t *testing.T) { - s := &WrappedServer{ + s := &Server{ Network: "tcp", Address: "localhost:4555", ReadTimeout: 10 * time.Second, diff --git a/internal/servers/hls/hlsjsdownloader/HASH b/internal/servers/hls/hlsjsdownloader/HASH index a9bd8fcebf2..152f8feec04 100644 --- a/internal/servers/hls/hlsjsdownloader/HASH +++ b/internal/servers/hls/hlsjsdownloader/HASH @@ -1 +1 @@ -7eff587d9b5f6ce78cafd3b2863fc7bfacb6dcfb0696b85714746666b2af2e54 +3c9b6d4c217d8a337dbef838ec7405170f64bdeb3f74dab44507a3328bdfbce8 diff --git a/internal/servers/hls/hlsjsdownloader/VERSION b/internal/servers/hls/hlsjsdownloader/VERSION index f830b2f549f..71e423dfb2a 100644 --- a/internal/servers/hls/hlsjsdownloader/VERSION +++ b/internal/servers/hls/hlsjsdownloader/VERSION @@ -1 +1 @@ -v1.5.15 +v1.5.17 diff --git a/internal/servers/hls/http_server.go b/internal/servers/hls/http_server.go index 47a3cd4de19..a73d79ffbc5 100644 --- a/internal/servers/hls/http_server.go +++ b/internal/servers/hls/http_server.go @@ -46,17 +46,20 @@ type httpServer struct { pathManager serverPathManager parent *Server - inner *httpp.WrappedServer + inner *httpp.Server } func (s *httpServer) initialize() error { router := gin.New() router.SetTrustedProxies(s.trustedProxies.ToTrustedProxies()) //nolint:errcheck - router.NoRoute(s.onRequest) + + router.Use(s.middlewareOrigin) + + router.Use(s.onRequest) network, address := restrictnetwork.Restrict("tcp", s.address) - s.inner = &httpp.WrappedServer{ + s.inner = &httpp.Server{ Network: network, Address: address, ReadTimeout: time.Duration(s.readTimeout), @@ -83,22 +86,22 @@ func (s *httpServer) close() { s.inner.Close() } -func (s *httpServer) onRequest(ctx *gin.Context) { - ctx.Writer.Header().Set("Access-Control-Allow-Origin", s.allowOrigin) - ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true") - - switch ctx.Request.Method { - case http.MethodOptions: - if ctx.Request.Header.Get("Access-Control-Request-Method") != "" { - ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET") - ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization, Range") - ctx.Writer.WriteHeader(http.StatusNoContent) - } - return +func (s *httpServer) middlewareOrigin(ctx *gin.Context) { + ctx.Header("Access-Control-Allow-Origin", s.allowOrigin) + ctx.Header("Access-Control-Allow-Credentials", "true") - case http.MethodGet: + // preflight requests + if ctx.Request.Method == http.MethodOptions && + ctx.Request.Header.Get("Access-Control-Request-Method") != "" { + ctx.Header("Access-Control-Allow-Methods", "OPTIONS, GET") + ctx.Header("Access-Control-Allow-Headers", "Authorization, Range") + ctx.AbortWithStatus(http.StatusNoContent) + return + } +} - default: +func (s *httpServer) onRequest(ctx *gin.Context) { + if ctx.Request.Method != http.MethodGet { return } @@ -110,8 +113,8 @@ func (s *httpServer) onRequest(ctx *gin.Context) { switch { case strings.HasSuffix(pa, "/hls.min.js"): - ctx.Writer.Header().Set("Cache-Control", "max-age=3600") - ctx.Writer.Header().Set("Content-Type", "application/javascript") + ctx.Header("Cache-Control", "max-age=3600") + ctx.Header("Content-Type", "application/javascript") ctx.Writer.WriteHeader(http.StatusOK) ctx.Writer.Write(hlsMinJS) return @@ -133,7 +136,7 @@ func (s *httpServer) onRequest(ctx *gin.Context) { dir, fname = pa, "" if !strings.HasSuffix(dir, "/") { - ctx.Writer.Header().Set("Location", mergePathAndQuery(ctx.Request.URL.Path+"/", ctx.Request.URL.RawQuery)) + ctx.Header("Location", mergePathAndQuery(ctx.Request.URL.Path+"/", ctx.Request.URL.RawQuery)) ctx.Writer.WriteHeader(http.StatusMovedPermanently) return } @@ -177,8 +180,8 @@ func (s *httpServer) onRequest(ctx *gin.Context) { switch fname { case "": - ctx.Writer.Header().Set("Cache-Control", "max-age=3600") - ctx.Writer.Header().Set("Content-Type", "text/html") + ctx.Header("Cache-Control", "max-age=3600") + ctx.Header("Content-Type", "text/html") ctx.Writer.WriteHeader(http.StatusOK) ctx.Writer.Write(hlsIndex) diff --git a/internal/servers/webrtc/http_server.go b/internal/servers/webrtc/http_server.go index 5ba0b698fbc..a193099382f 100644 --- a/internal/servers/webrtc/http_server.go +++ b/internal/servers/webrtc/http_server.go @@ -70,17 +70,20 @@ type httpServer struct { pathManager serverPathManager parent *Server - inner *httpp.WrappedServer + inner *httpp.Server } func (s *httpServer) initialize() error { router := gin.New() router.SetTrustedProxies(s.trustedProxies.ToTrustedProxies()) //nolint:errcheck - router.NoRoute(s.onRequest) + + router.Use(s.middlewareOrigin) + + router.Use(s.onRequest) network, address := restrictnetwork.Restrict("tcp", s.address) - s.inner = &httpp.WrappedServer{ + s.inner = &httpp.Server{ Network: network, Address: address, ReadTimeout: time.Duration(s.readTimeout), @@ -153,9 +156,9 @@ func (s *httpServer) onWHIPOptions(ctx *gin.Context, pathName string, publish bo return } - ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PATCH, DELETE") - ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, If-Match") - ctx.Writer.Header().Set("Access-Control-Expose-Headers", "Link") + ctx.Header("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PATCH, DELETE") + ctx.Header("Access-Control-Allow-Headers", "Authorization, Content-Type, If-Match") + ctx.Header("Access-Control-Expose-Headers", "Link") ctx.Writer.Header()["Link"] = whip.LinkHeaderMarshal(servers) ctx.Writer.WriteHeader(http.StatusNoContent) } @@ -207,13 +210,13 @@ func (s *httpServer) onWHIPPost(ctx *gin.Context, pathName string, publish bool) return } - ctx.Writer.Header().Set("Content-Type", "application/sdp") - ctx.Writer.Header().Set("Access-Control-Expose-Headers", "ETag, ID, Accept-Patch, Link, Location") - ctx.Writer.Header().Set("ETag", "*") - ctx.Writer.Header().Set("ID", res.sx.uuid.String()) - ctx.Writer.Header().Set("Accept-Patch", "application/trickle-ice-sdpfrag") + ctx.Header("Content-Type", "application/sdp") + ctx.Header("Access-Control-Expose-Headers", "ETag, ID, Accept-Patch, Link, Location") + ctx.Header("ETag", "*") + ctx.Header("ID", res.sx.uuid.String()) + ctx.Header("Accept-Patch", "application/trickle-ice-sdpfrag") ctx.Writer.Header()["Link"] = whip.LinkHeaderMarshal(servers) - ctx.Writer.Header().Set("Location", sessionLocation(publish, pathName, res.sx.secret)) + ctx.Header("Location", sessionLocation(publish, pathName, res.sx.secret)) ctx.Writer.WriteHeader(http.StatusCreated) ctx.Writer.Write(res.answer) } @@ -287,8 +290,8 @@ func (s *httpServer) onPage(ctx *gin.Context, pathName string, publish bool) { return } - ctx.Writer.Header().Set("Cache-Control", "max-age=3600") - ctx.Writer.Header().Set("Content-Type", "text/html") + ctx.Header("Cache-Control", "max-age=3600") + ctx.Header("Content-Type", "text/html") ctx.Writer.WriteHeader(http.StatusOK) if publish { @@ -298,19 +301,21 @@ func (s *httpServer) onPage(ctx *gin.Context, pathName string, publish bool) { } } -func (s *httpServer) onRequest(ctx *gin.Context) { - ctx.Writer.Header().Set("Access-Control-Allow-Origin", s.allowOrigin) - ctx.Writer.Header().Set("Access-Control-Allow-Credentials", "true") +func (s *httpServer) middlewareOrigin(ctx *gin.Context) { + ctx.Header("Access-Control-Allow-Origin", s.allowOrigin) + ctx.Header("Access-Control-Allow-Credentials", "true") // preflight requests if ctx.Request.Method == http.MethodOptions && ctx.Request.Header.Get("Access-Control-Request-Method") != "" { - ctx.Writer.Header().Set("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PATCH, DELETE") - ctx.Writer.Header().Set("Access-Control-Allow-Headers", "Authorization, Content-Type, If-Match") - ctx.Writer.WriteHeader(http.StatusNoContent) + ctx.Header("Access-Control-Allow-Methods", "OPTIONS, GET, POST, PATCH, DELETE") + ctx.Header("Access-Control-Allow-Headers", "Authorization, Content-Type, If-Match") + ctx.AbortWithStatus(http.StatusNoContent) return } +} +func (s *httpServer) onRequest(ctx *gin.Context) { // WHIP/WHEP, outside session if m := reWHIPWHEPNoID.FindStringSubmatch(ctx.Request.URL.Path); m != nil { switch ctx.Request.Method { @@ -352,7 +357,7 @@ func (s *httpServer) onRequest(ctx *gin.Context) { s.onPage(ctx, ctx.Request.URL.Path[1:len(ctx.Request.URL.Path)-len("/publish")], true) case ctx.Request.URL.Path[len(ctx.Request.URL.Path)-1] != '/': - ctx.Writer.Header().Set("Location", mergePathAndQuery(ctx.Request.URL.Path+"/", ctx.Request.URL.RawQuery)) + ctx.Header("Location", mergePathAndQuery(ctx.Request.URL.Path+"/", ctx.Request.URL.RawQuery)) ctx.Writer.WriteHeader(http.StatusMovedPermanently) default: diff --git a/internal/servers/webrtc/server_test.go b/internal/servers/webrtc/server_test.go index 80fa556199c..4da99c46104 100644 --- a/internal/servers/webrtc/server_test.go +++ b/internal/servers/webrtc/server_test.go @@ -763,5 +763,5 @@ func TestICEServerClientOnly(t *testing.T) { require.Equal(t, len(s.ICEServers), len(clientICEServers)) serverICEServers, err := s.generateICEServers(false) require.NoError(t, err) - require.Equal(t, 0, len(serverICEServers)) + require.Empty(t, serverICEServers) } diff --git a/internal/staticsources/hls/source_test.go b/internal/staticsources/hls/source_test.go index 6da53776676..44b053e5e22 100644 --- a/internal/staticsources/hls/source_test.go +++ b/internal/staticsources/hls/source_test.go @@ -40,7 +40,7 @@ func TestSource(t *testing.T) { router := gin.New() router.GET("/stream.m3u8", func(ctx *gin.Context) { - ctx.Writer.Header().Set("Content-Type", `application/vnd.apple.mpegurl`) + ctx.Header("Content-Type", `application/vnd.apple.mpegurl`) ctx.Writer.Write([]byte("#EXTM3U\n" + "#EXT-X-VERSION:3\n" + "#EXT-X-ALLOW-CACHE:NO\n" + @@ -56,7 +56,7 @@ func TestSource(t *testing.T) { }) router.GET("/segment1.ts", func(ctx *gin.Context) { - ctx.Writer.Header().Set("Content-Type", `video/MP2T`) + ctx.Header("Content-Type", `video/MP2T`) w := mpegts.NewWriter(ctx.Writer, tracks) @@ -71,7 +71,7 @@ func TestSource(t *testing.T) { }) router.GET("/segment2.ts", func(ctx *gin.Context) { - ctx.Writer.Header().Set("Content-Type", `video/MP2T`) + ctx.Header("Content-Type", `video/MP2T`) w := mpegts.NewWriter(ctx.Writer, tracks) diff --git a/internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION b/internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION index f706a60d854..20dd632231a 100644 --- a/internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION +++ b/internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION @@ -1 +1 @@ -v2.3.2 +v2.3.4 diff --git a/mediamtx.yml b/mediamtx.yml index cf73951ebf9..4203b2ff677 100644 --- a/mediamtx.yml +++ b/mediamtx.yml @@ -589,7 +589,7 @@ pathDefaults: # Period between IDR frames rpiCameraIDRPeriod: 60 # Bitrate - rpiCameraBitrate: 1000000 + rpiCameraBitrate: 5000000 # H264 profile rpiCameraProfile: main # H264 level