diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 386dd8d6..aad738c7 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,6 +1,5 @@ version: 2 updates: - - package-ecosystem: "gomod" directory: "/" schedule: diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index fb7ccbc9..cc4c6ea7 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -2,101 +2,101 @@ name: lint on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] jobs: go: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 + - uses: actions/checkout@v6 + with: + fetch-depth: 0 - - uses: actions/setup-go@v6 - with: - go-version: "1.26" + - uses: actions/setup-go@v6 + with: + go-version: "1.26" - - run: go generate ./... + - run: go generate ./... - - uses: golangci/golangci-lint-action@v9 - with: - version: v2.12.2 + - uses: golangci/golangci-lint-action@v9 + with: + version: v2.12.2 go_mod: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 - with: - go-version: "1.26" + - uses: actions/setup-go@v6 + with: + go-version: "1.26" - - run: make lint-go-mod + - run: make lint-go-mod conf: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 - with: - go-version: "1.26" + - uses: actions/setup-go@v6 + with: + go-version: "1.26" - - run: make lint-conf + - run: make lint-conf go2api: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 - with: - go-version: "1.26" + - uses: actions/setup-go@v6 + with: + go-version: "1.26" - - run: make lint-go2api + - run: make lint-go2api docslinks: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 - with: - go-version: "1.26" + - uses: actions/setup-go@v6 + with: + go-version: "1.26" - - run: make lint-docslinks + - run: make lint-docslinks docsorder: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v6 - - uses: actions/setup-go@v6 - with: - go-version: "1.26" + - uses: actions/setup-go@v6 + with: + go-version: "1.26" - - run: make lint-docsorder - - docs: - runs-on: ubuntu-24.04 - - steps: - - uses: actions/checkout@v6 - - - run: make lint-docs + - run: make lint-docsorder api_docs: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v6 - - run: make lint-api-docs + - run: make lint-api-docs + + other: + runs-on: ubuntu-24.04 + + steps: + - uses: actions/checkout@v6 + + - run: make lint-other diff --git a/.github/workflows/nightly_binaries.yml b/.github/workflows/nightly_binaries.yml index 525b42a0..59e20a9b 100644 --- a/.github/workflows/nightly_binaries.yml +++ b/.github/workflows/nightly_binaries.yml @@ -8,13 +8,13 @@ jobs: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 + - uses: actions/checkout@v6 + with: + fetch-depth: 0 - - run: make binaries + - run: make binaries - - uses: actions/upload-artifact@v7 - with: - name: binaries - path: binaries + - uses: actions/upload-artifact@v7 + with: + name: binaries + path: binaries diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7c1ed9c5..291f426a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,7 @@ name: release on: push: tags: - - 'v*' + - "v*" permissions: id-token: write @@ -18,144 +18,144 @@ jobs: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v6 - - run: make binaries + - run: make binaries - - run: cd binaries && sha256sum -b * > checksums.sha256 + - run: cd binaries && sha256sum -b * > checksums.sha256 - - uses: actions/attest@v4 - with: - subject-path: '${{ github.workspace }}/binaries/*' + - uses: actions/attest@v4 + with: + subject-path: "${{ github.workspace }}/binaries/*" - - uses: actions/upload-artifact@v7 - with: - name: binaries - path: binaries + - uses: actions/upload-artifact@v7 + with: + name: binaries + path: binaries github_release: needs: binaries runs-on: ubuntu-24.04 steps: - - uses: actions/download-artifact@v8 - with: - name: binaries - path: binaries + - uses: actions/download-artifact@v8 + with: + name: binaries + path: binaries - - uses: actions/github-script@v9 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const fs = require('fs').promises; - const { repo: { owner, repo } } = context; + - uses: actions/github-script@v9 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const fs = require('fs').promises; + const { repo: { owner, repo } } = context; - const currentRelease = context.ref.split('/')[2]; + const currentRelease = context.ref.split('/')[2]; - let body = `## New major features\n` - + `\n` - + `TODO\n` - + `\n` - + `## Fixes and improvements\n` - + `\n` - + `TODO\n` - + `\n` - + `## Security\n` - + `\n` - + `Binaries are compiled from source code by the [Release workflow](https://github.com/${owner}/${repo}/actions/workflows/release.yml), which is a fully-visible process that prevents any change or external interference in produced artifacts.\n` - + `\n` - + 'Checksums of binaries are also published in a public blockchain by using [GitHub Attestations](https://docs.github.com/en/actions/concepts/security/artifact-attestations), and they can be verified by running:\n' - + `\n` - + '```\n' - + `ls mediamtx_* | xargs -L1 gh attestation verify --repo bluenviron/mediamtx\n` - + '```\n' - + `\n` - + 'You can verify checksums of binaries by downloading `checksums.sha256` and running:\n' - + `\n` - + '```\n' - + `cat checksums.sha256 | grep "$(ls mediamtx_*)" | sha256sum --check\n` - + '```\n' - + `\n`; + let body = `## New major features\n` + + `\n` + + `TODO\n` + + `\n` + + `## Fixes and improvements\n` + + `\n` + + `TODO\n` + + `\n` + + `## Security\n` + + `\n` + + `Binaries are compiled from source code by the [Release workflow](https://github.com/${owner}/${repo}/actions/workflows/release.yml), which is a fully-visible process that prevents any change or external interference in produced artifacts.\n` + + `\n` + + 'Checksums of binaries are also published in a public blockchain by using [GitHub Attestations](https://docs.github.com/en/actions/concepts/security/artifact-attestations), and they can be verified by running:\n' + + `\n` + + '```\n' + + `ls mediamtx_* | xargs -L1 gh attestation verify --repo bluenviron/mediamtx\n` + + '```\n' + + `\n` + + 'You can verify checksums of binaries by downloading `checksums.sha256` and running:\n' + + `\n` + + '```\n' + + `cat checksums.sha256 | grep "$(ls mediamtx_*)" | sha256sum --check\n` + + '```\n' + + `\n`; - const res = await github.rest.repos.createRelease({ - owner, - repo, - tag_name: currentRelease, - name: currentRelease, - body, - }); - const release_id = res.data.id; - - for (const name of await fs.readdir('./binaries/')) { - await github.rest.repos.uploadReleaseAsset({ + const res = await github.rest.repos.createRelease({ owner, repo, - release_id, - name, - data: await fs.readFile(`./binaries/${name}`), + tag_name: currentRelease, + name: currentRelease, + body, }); - } + const release_id = res.data.id; + + for (const name of await fs.readdir('./binaries/')) { + await github.rest.repos.uploadReleaseAsset({ + owner, + repo, + release_id, + name, + data: await fs.readFile(`./binaries/${name}`), + }); + } github_notify_issues: needs: github_release runs-on: ubuntu-24.04 steps: - - uses: actions/github-script@v9 - with: - github-token: ${{ secrets.GITHUB_TOKEN }} - script: | - const { repo: { owner, repo } } = context; + - uses: actions/github-script@v9 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const { repo: { owner, repo } } = context; - const tags = await github.rest.repos.listTags({ - owner, - repo, - }); + const tags = await github.rest.repos.listTags({ + owner, + repo, + }); - const curTag = tags.data[0]; - const prevTag = tags.data[1]; + const curTag = tags.data[0]; + const prevTag = tags.data[1]; - const diff = await github.rest.repos.compareCommitsWithBasehead({ - owner, - repo, - basehead: `${prevTag.commit.sha}...${curTag.commit.sha}`, - }); + const diff = await github.rest.repos.compareCommitsWithBasehead({ + owner, + repo, + basehead: `${prevTag.commit.sha}...${curTag.commit.sha}`, + }); - const issues = {}; + const issues = {}; - for (const commit of diff.data.commits) { - for (const match of commit.commit.message.matchAll(/(^| |\()#([0-9]+)( |\)|$)/g)) { - issues[match[2]] = 1; + for (const commit of diff.data.commits) { + for (const match of commit.commit.message.matchAll(/(^| |\()#([0-9]+)( |\)|$)/g)) { + issues[match[2]] = 1; + } } - } - for (const issue in issues) { - try { - await github.rest.issues.createComment({ - owner, - repo, - issue_number: parseInt(issue), - body: `This issue is mentioned in release ${curTag.name} 🚀\n` - + `Check out the entire changelog by [clicking here](https://github.com/${owner}/${repo}/releases/tag/${curTag.name})`, - }); - } catch (exc) { - console.error(exc.toString()); + for (const issue in issues) { + try { + await github.rest.issues.createComment({ + owner, + repo, + issue_number: parseInt(issue), + body: `This issue is mentioned in release ${curTag.name} 🚀\n` + + `Check out the entire changelog by [clicking here](https://github.com/${owner}/${repo}/releases/tag/${curTag.name})`, + }); + } catch (exc) { + console.error(exc.toString()); + } } - } dockerhub: needs: binaries runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v6 - - uses: actions/download-artifact@v8 - with: - name: binaries - path: binaries + - uses: actions/download-artifact@v8 + with: + name: binaries + path: binaries - - run: make dockerhub - env: - DOCKER_USER: ${{ secrets.DOCKER_USER }} - DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} + - run: make dockerhub + env: + DOCKER_USER: ${{ secrets.DOCKER_USER }} + DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b7272e11..58cbe9dd 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,45 +2,45 @@ name: test on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] jobs: test_64: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 + - uses: actions/checkout@v6 + with: + fetch-depth: 0 - - run: make test + - run: make test - - uses: codecov/codecov-action@v6 - with: - token: ${{ secrets.CODECOV_TOKEN }} + - uses: codecov/codecov-action@v6 + with: + token: ${{ secrets.CODECOV_TOKEN }} test_32: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 + - uses: actions/checkout@v6 + with: + fetch-depth: 0 - - run: make test-32 + - run: make test-32 test_e2e: runs-on: ubuntu-24.04 steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 + - uses: actions/checkout@v6 + with: + fetch-depth: 0 - - uses: actions/setup-go@v6 - with: - go-version: "1.26" + - uses: actions/setup-go@v6 + with: + go-version: "1.26" - - run: make test-e2e-nodocker + - run: make test-e2e-nodocker diff --git a/.golangci.yml b/.golangci.yml index feb3d164..0e2a514f 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -2,90 +2,90 @@ version: "2" linters: enable: - - asciicheck - - bidichk - - bodyclose - - copyloopvar - - dupl - - errorlint - - gochecknoinits - - gocritic - - lll - - misspell - - modernize - - nilerr - - prealloc - - predeclared - - reassign - - revive - - usestdlibvars - - unconvert - - tparallel - - wastedassign - - whitespace + - asciicheck + - bidichk + - bodyclose + - copyloopvar + - dupl + - errorlint + - gochecknoinits + - gocritic + - lll + - misspell + - modernize + - nilerr + - prealloc + - predeclared + - reassign + - revive + - usestdlibvars + - unconvert + - tparallel + - wastedassign + - whitespace 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/v5.Client).Close - - (*github.com/bluenviron/gortsplib/v5.Server).Close - - (*github.com/bluenviron/gortsplib/v5.ServerSession).Close - - (*github.com/bluenviron/gortsplib/v5.ServerStream).Close - - (*github.com/bluenviron/gortsplib/v5.ServerConn).Close + - 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/v5.Client).Close + - (*github.com/bluenviron/gortsplib/v5.Server).Close + - (*github.com/bluenviron/gortsplib/v5.ServerSession).Close + - (*github.com/bluenviron/gortsplib/v5.ServerStream).Close + - (*github.com/bluenviron/gortsplib/v5.ServerConn).Close govet: enable-all: true disable: - - fieldalignment - - reflectvaluecompare + - fieldalignment + - reflectvaluecompare settings: shadow: strict: true modernize: disable: - - reflecttypefor - - stringsbuilder - - testingcontext + - reflecttypefor + - stringsbuilder + - testingcontext exclusions: rules: - - linters: - - lll - source: "^\\s*// https?://" + - linters: + - lll + source: "^\\s*// https?://" formatters: enable: - - gofmt - - gofumpt - - goimports + - gofmt + - gofumpt + - goimports diff --git a/README.md b/README.md index d5338411..6f3f79aa 100644 --- a/README.md +++ b/README.md @@ -6,12 +6,13 @@

- [![Website](https://img.shields.io/badge/website-mediamtx.org-1c94b5)](https://mediamtx.org) - [![Test](https://github.com/bluenviron/mediamtx/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/bluenviron/mediamtx/actions/workflows/test.yml?query=branch%3Amain) - [![Lint](https://github.com/bluenviron/mediamtx/actions/workflows/lint.yml/badge.svg?branch=main)](https://github.com/bluenviron/mediamtx/actions/workflows/lint.yml?query=branch%3Amain) - [![CodeCov](https://codecov.io/gh/bluenviron/mediamtx/branch/main/graph/badge.svg)](https://app.codecov.io/gh/bluenviron/mediamtx/tree/main) - [![Release](https://img.shields.io/github/v/release/bluenviron/mediamtx)](https://github.com/bluenviron/mediamtx/releases) - [![Docker Hub](https://img.shields.io/badge/docker-bluenviron/mediamtx-blue)](https://hub.docker.com/r/bluenviron/mediamtx) +[![Website](https://img.shields.io/badge/website-mediamtx.org-1c94b5)](https://mediamtx.org) +[![Test](https://github.com/bluenviron/mediamtx/actions/workflows/test.yml/badge.svg?branch=main)](https://github.com/bluenviron/mediamtx/actions/workflows/test.yml?query=branch%3Amain) +[![Lint](https://github.com/bluenviron/mediamtx/actions/workflows/lint.yml/badge.svg?branch=main)](https://github.com/bluenviron/mediamtx/actions/workflows/lint.yml?query=branch%3Amain) +[![CodeCov](https://codecov.io/gh/bluenviron/mediamtx/branch/main/graph/badge.svg)](https://app.codecov.io/gh/bluenviron/mediamtx/tree/main) +[![Release](https://img.shields.io/github/v/release/bluenviron/mediamtx)](https://github.com/bluenviron/mediamtx/releases) +[![Docker Hub](https://img.shields.io/badge/docker-bluenviron/mediamtx-blue)](https://hub.docker.com/r/bluenviron/mediamtx) +
@@ -20,8 +21,8 @@ _MediaMTX_ is a ready-to-use and zero-dependency real-time media server and medi
- |[Install](https://mediamtx.org/docs/kickoff/install)|[Documentation](https://mediamtx.org/docs/kickoff/introduction)| - |-|-| +| [Install](https://mediamtx.org/docs/kickoff/install) | [Documentation](https://mediamtx.org/docs/kickoff/introduction) | +| ---------------------------------------------------- | --------------------------------------------------------------- |
diff --git a/api/openapi.yaml b/api/openapi.yaml index d3238300..87942c45 100644 --- a/api/openapi.yaml +++ b/api/openapi.yaml @@ -26,82 +26,82 @@ components: PathSourceType: type: string enum: - - hlsSource - - redirect - - rpiCameraSource - - rtmpConn - - rtmpsConn - - rtmpSource - - rtspSession - - rtspSource - - rtspsSession - - srtConn - - srtSource - - mpegtsSource - - rtpSource - - webRTCSession - - webRTCSource + - hlsSource + - redirect + - rpiCameraSource + - rtmpConn + - rtmpsConn + - rtmpSource + - rtspSession + - rtspSource + - rtspsSession + - srtConn + - srtSource + - mpegtsSource + - rtpSource + - webRTCSession + - webRTCSource PathReaderType: type: string enum: - - hlsSession - - rtmpConn - - rtmpsConn - - rtspConn - - rtspSession - - rtspsConn - - rtspsSession - - srtConn - - webRTCSession + - hlsSession + - rtmpConn + - rtmpsConn + - rtspConn + - rtspSession + - rtspsConn + - rtspsSession + - srtConn + - webRTCSession PathTrackCodec: type: string enum: - - AV1 - - VP9 - - VP8 - - H265 - - H264 - - MPEG-4 Video - - MPEG-1/2 Video - - M-JPEG - - Opus - - Vorbis - - MPEG-4 Audio - - MPEG-4 Audio LATM - - MPEG-1/2 Audio - - AC3 - - Speex - - G726 - - G722 - - G711 - - LPCM - - MPEG-TS - - KLV - - Generic + - AV1 + - VP9 + - VP8 + - H265 + - H264 + - MPEG-4 Video + - MPEG-1/2 Video + - M-JPEG + - Opus + - Vorbis + - MPEG-4 Audio + - MPEG-4 Audio LATM + - MPEG-1/2 Audio + - AC3 + - Speex + - G726 + - G722 + - G711 + - LPCM + - MPEG-TS + - KLV + - Generic AlwaysAvailableTrackCodec: type: string enum: - - AV1 - - VP9 - - H265 - - H264 - - MPEG4Audio - - Opus - - G711 - - LPCM + - AV1 + - VP9 + - H265 + - H264 + - MPEG4Audio + - Opus + - G711 + - LPCM AuthAction: type: string enum: - - publish - - read - - playback - - api - - metrics - - pprof + - publish + - read + - playback + - api + - metrics + - pprof AuthMethod: type: string @@ -133,7 +133,7 @@ components: RTSPRangeType: type: string - enum: ['', clock, npt, smpte] + enum: ["", clock, npt, smpte] RTSPTransport: type: string @@ -159,13 +159,13 @@ components: type: object properties: status: - $ref: '#/components/schemas/OKStatus' + $ref: "#/components/schemas/OKStatus" Error: type: object properties: status: - $ref: '#/components/schemas/ErrorStatus' + $ref: "#/components/schemas/ErrorStatus" error: type: string @@ -191,13 +191,13 @@ components: permissions: type: array items: - $ref: '#/components/schemas/AuthInternalUserPermission' + $ref: "#/components/schemas/AuthInternalUserPermission" AuthInternalUserPermission: type: object properties: action: - $ref: '#/components/schemas/AuthAction' + $ref: "#/components/schemas/AuthAction" path: type: string @@ -206,11 +206,11 @@ components: properties: # General logLevel: - $ref: '#/components/schemas/LogLevel' + $ref: "#/components/schemas/LogLevel" logDestinations: type: array items: - $ref: '#/components/schemas/LogDestination' + $ref: "#/components/schemas/LogDestination" logStructured: type: boolean logFile: @@ -246,11 +246,11 @@ components: # Authentication authMethod: - $ref: '#/components/schemas/AuthMethod' + $ref: "#/components/schemas/AuthMethod" authInternalUsers: type: array items: - $ref: '#/components/schemas/AuthInternalUser' + $ref: "#/components/schemas/AuthInternalUser" authHTTPAddress: type: string externalAuthenticationURL: @@ -262,7 +262,7 @@ components: authHTTPExclude: type: array items: - $ref: '#/components/schemas/AuthInternalUserPermission' + $ref: "#/components/schemas/AuthInternalUserPermission" authJWTJWKS: type: string authJWTJWKSFingerprint: @@ -280,7 +280,7 @@ components: authJWTExclude: type: array items: - $ref: '#/components/schemas/AuthInternalUserPermission' + $ref: "#/components/schemas/AuthInternalUserPermission" # Control API api: @@ -402,9 +402,9 @@ components: nullable: true deprecated: true allOf: - - $ref: '#/components/schemas/Encryption' + - $ref: "#/components/schemas/Encryption" rtspEncryption: - $ref: '#/components/schemas/Encryption' + $ref: "#/components/schemas/Encryption" rtspAddress: type: string rtspsAddress: @@ -439,12 +439,12 @@ components: type: array nullable: true items: - $ref: '#/components/schemas/RTSPAuthMethod' + $ref: "#/components/schemas/RTSPAuthMethod" deprecated: true rtspAuthMethods: type: array items: - $ref: '#/components/schemas/RTSPAuthMethod' + $ref: "#/components/schemas/RTSPAuthMethod" rtspUDPReadBufferSize: type: integer format: uint64 @@ -459,7 +459,7 @@ components: nullable: true deprecated: true rtmpEncryption: - $ref: '#/components/schemas/Encryption' + $ref: "#/components/schemas/Encryption" rtmpAddress: type: string rtmpsAddress: @@ -499,7 +499,7 @@ components: hlsAlwaysRemux: type: boolean hlsVariant: - $ref: '#/components/schemas/HLSVariant' + $ref: "#/components/schemas/HLSVariant" hlsSegmentCount: type: integer format: int64 @@ -560,7 +560,7 @@ components: webrtcICEServers2: type: array items: - $ref: '#/components/schemas/WebRTCICEServer' + $ref: "#/components/schemas/WebRTCICEServer" webrtcSTUNGatherTimeout: type: string webrtcHandshakeTimeout: @@ -608,7 +608,7 @@ components: nullable: true deprecated: true allOf: - - $ref: '#/components/schemas/RecordFormat' + - $ref: "#/components/schemas/RecordFormat" recordPartDuration: type: string nullable: true @@ -657,7 +657,7 @@ components: alwaysAvailableTracks: type: array items: - $ref: '#/components/schemas/AlwaysAvailableTrack' + $ref: "#/components/schemas/AlwaysAvailableTrack" alwaysAvailableFile: type: string @@ -671,7 +671,7 @@ components: recordPath: type: string recordFormat: - $ref: '#/components/schemas/RecordFormat' + $ref: "#/components/schemas/RecordFormat" recordPartDuration: type: string recordMaxPartSize: @@ -725,7 +725,7 @@ components: # RTSP source rtspTransport: - $ref: '#/components/schemas/RTSPTransport' + $ref: "#/components/schemas/RTSPTransport" rtspAnyPort: type: boolean sourceProtocol: @@ -733,13 +733,13 @@ components: nullable: true deprecated: true allOf: - - $ref: '#/components/schemas/RTSPTransport' + - $ref: "#/components/schemas/RTSPTransport" sourceAnyPortEnable: type: boolean nullable: true deprecated: true rtspRangeType: - $ref: '#/components/schemas/RTSPRangeType' + $ref: "#/components/schemas/RTSPRangeType" rtspRangeStart: type: string rtspUDPReadBufferSize: @@ -943,7 +943,7 @@ components: items: type: array items: - $ref: '#/components/schemas/PathConf' + $ref: "#/components/schemas/PathConf" Path: type: object @@ -956,7 +956,7 @@ components: type: object nullable: true allOf: - - $ref: '#/components/schemas/PathSource' + - $ref: "#/components/schemas/PathSource" ready: type: boolean deprecated: true @@ -978,11 +978,11 @@ components: type: array deprecated: true items: - $ref: '#/components/schemas/PathTrackCodec' + $ref: "#/components/schemas/PathTrackCodec" tracks2: type: array items: - $ref: '#/components/schemas/PathTrack' + $ref: "#/components/schemas/PathTrack" inboundBytes: type: integer format: uint64 @@ -1003,7 +1003,7 @@ components: readers: type: array items: - $ref: '#/components/schemas/PathReader' + $ref: "#/components/schemas/PathReader" PathList: type: object @@ -1017,13 +1017,13 @@ components: items: type: array items: - $ref: '#/components/schemas/Path' + $ref: "#/components/schemas/Path" PathSource: type: object properties: type: - $ref: '#/components/schemas/PathSourceType' + $ref: "#/components/schemas/PathSourceType" id: type: string @@ -1031,7 +1031,7 @@ components: type: object properties: type: - $ref: '#/components/schemas/PathReaderType' + $ref: "#/components/schemas/PathReaderType" id: type: string @@ -1039,24 +1039,24 @@ components: type: object properties: codec: - $ref: '#/components/schemas/PathTrackCodec' + $ref: "#/components/schemas/PathTrackCodec" codecProps: type: object nullable: true allOf: - - $ref: '#/components/schemas/PathTrackCodecProps' + - $ref: "#/components/schemas/PathTrackCodecProps" PathTrackCodecProps: oneOf: - - $ref: '#/components/schemas/PathTrackCodecPropsAV1' - - $ref: '#/components/schemas/PathTrackCodecPropsVP9' - - $ref: '#/components/schemas/PathTrackCodecPropsH265' - - $ref: '#/components/schemas/PathTrackCodecPropsH264' - - $ref: '#/components/schemas/PathTrackCodecPropsOpus' - - $ref: '#/components/schemas/PathTrackCodecPropsMPEG4Audio' - - $ref: '#/components/schemas/PathTrackCodecPropsAC3' - - $ref: '#/components/schemas/PathTrackCodecPropsG711' - - $ref: '#/components/schemas/PathTrackCodecPropsLPCM' + - $ref: "#/components/schemas/PathTrackCodecPropsAV1" + - $ref: "#/components/schemas/PathTrackCodecPropsVP9" + - $ref: "#/components/schemas/PathTrackCodecPropsH265" + - $ref: "#/components/schemas/PathTrackCodecPropsH264" + - $ref: "#/components/schemas/PathTrackCodecPropsOpus" + - $ref: "#/components/schemas/PathTrackCodecPropsMPEG4Audio" + - $ref: "#/components/schemas/PathTrackCodecPropsAC3" + - $ref: "#/components/schemas/PathTrackCodecPropsG711" + - $ref: "#/components/schemas/PathTrackCodecPropsLPCM" PathTrackCodecPropsAV1: type: object @@ -1195,7 +1195,7 @@ components: items: type: array items: - $ref: '#/components/schemas/HLSMuxer' + $ref: "#/components/schemas/HLSMuxer" HLSSession: type: object @@ -1231,7 +1231,7 @@ components: items: type: array items: - $ref: '#/components/schemas/HLSSession' + $ref: "#/components/schemas/HLSSession" Recording: type: object @@ -1241,7 +1241,7 @@ components: segments: type: array items: - $ref: '#/components/schemas/RecordingSegment' + $ref: "#/components/schemas/RecordingSegment" RecordingList: type: object @@ -1255,7 +1255,7 @@ components: items: type: array items: - $ref: '#/components/schemas/Recording' + $ref: "#/components/schemas/Recording" RecordingSegment: type: object @@ -1274,7 +1274,7 @@ components: remoteAddr: type: string state: - $ref: '#/components/schemas/RTMPConnState' + $ref: "#/components/schemas/RTMPConnState" path: type: string query: @@ -1311,7 +1311,7 @@ components: items: type: array items: - $ref: '#/components/schemas/RTMPConn' + $ref: "#/components/schemas/RTMPConn" RTSPConn: type: object @@ -1356,7 +1356,7 @@ components: items: type: array items: - $ref: '#/components/schemas/RTSPConn' + $ref: "#/components/schemas/RTSPConn" RTSPSession: type: object @@ -1369,7 +1369,7 @@ components: remoteAddr: type: string state: - $ref: '#/components/schemas/RTSPSessionState' + $ref: "#/components/schemas/RTSPSessionState" path: type: string query: @@ -1476,7 +1476,7 @@ components: items: type: array items: - $ref: '#/components/schemas/RTSPSession' + $ref: "#/components/schemas/RTSPSession" SRTConn: type: object @@ -1489,7 +1489,7 @@ components: remoteAddr: type: string state: - $ref: '#/components/schemas/SRTConnState' + $ref: "#/components/schemas/SRTConnState" path: type: string query: @@ -1722,13 +1722,13 @@ components: items: type: array items: - $ref: '#/components/schemas/SRTConn' + $ref: "#/components/schemas/SRTConn" AlwaysAvailableTrack: type: object properties: codec: - $ref: '#/components/schemas/AlwaysAvailableTrackCodec' + $ref: "#/components/schemas/AlwaysAvailableTrackCodec" sampleRate: type: integer format: int64 @@ -1767,7 +1767,7 @@ components: remoteCandidate: type: string state: - $ref: '#/components/schemas/WebRTCSessionState' + $ref: "#/components/schemas/WebRTCSessionState" path: type: string query: @@ -1846,28 +1846,27 @@ components: items: type: array items: - $ref: '#/components/schemas/WebRTCSession' + $ref: "#/components/schemas/WebRTCSession" paths: - /v3/info: get: operationId: info tags: [General] summary: returns informations about the instance. responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/Info' - '500': + $ref: "#/components/schemas/Info" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/auth/jwks/refresh: post: @@ -1875,44 +1874,44 @@ paths: tags: [Authentication] summary: Manually refreshes the JWT JWKS. responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/OK' - '500': + $ref: "#/components/schemas/OK" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/config/global/get: get: operationId: configGlobalGet tags: [Configuration] summary: returns the global configuration. - description: '' + description: "" responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/GlobalConf' - '400': + $ref: "#/components/schemas/GlobalConf" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/config/global/patch: patch: @@ -1925,52 +1924,52 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/GlobalConf' + $ref: "#/components/schemas/GlobalConf" responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/OK' - '400': + $ref: "#/components/schemas/OK" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/config/pathdefaults/get: get: operationId: configPathDefaultsGet tags: [Configuration] summary: returns the default path configuration. - description: '' + description: "" responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/PathConf' - '400': + $ref: "#/components/schemas/PathConf" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/config/pathdefaults/patch: patch: @@ -1983,104 +1982,104 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/PathConf' + $ref: "#/components/schemas/PathConf" responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/OK' - '400': + $ref: "#/components/schemas/OK" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/config/paths/list: get: operationId: configPathsList tags: [Configuration] summary: returns all path configurations. - description: '' + description: "" parameters: - - name: page - in: query - description: page number. - schema: - type: integer - default: 0 - - name: itemsPerPage - in: query - description: items per page. - schema: - type: integer - default: 100 + - name: page + in: query + description: page number. + schema: + type: integer + default: 0 + - name: itemsPerPage + in: query + description: items per page. + schema: + type: integer + default: 100 responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/PathConfList' - '400': + $ref: "#/components/schemas/PathConfList" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/config/paths/get/{name}: get: operationId: configPathsGet tags: [Configuration] summary: returns a path configuration. - description: '' + description: "" parameters: - - name: name - in: path - required: true - description: the name of the path. - schema: - type: string + - name: name + in: path + required: true + description: the name of the path. + schema: + type: string responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/PathConf' - '400': + $ref: "#/components/schemas/PathConf" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '404': + $ref: "#/components/schemas/Error" + "404": description: path not found. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/config/paths/add/{name}: post: @@ -2089,37 +2088,37 @@ paths: summary: adds a path configuration. description: all fields are optional. parameters: - - name: name - in: path - required: true - description: the name of the path. - schema: - type: string + - name: name + in: path + required: true + description: the name of the path. + schema: + type: string requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/PathConf' + $ref: "#/components/schemas/PathConf" responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/OK' - '400': + $ref: "#/components/schemas/OK" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/config/paths/patch/{name}: patch: @@ -2128,43 +2127,43 @@ paths: summary: patches a path configuration. description: all fields are optional. parameters: - - name: name - in: path - required: true - description: the name of the path. - schema: - type: string + - name: name + in: path + required: true + description: the name of the path. + schema: + type: string requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/PathConf' + $ref: "#/components/schemas/PathConf" responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/OK' - '400': + $ref: "#/components/schemas/OK" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '404': + $ref: "#/components/schemas/Error" + "404": description: path not found. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/config/paths/replace/{name}: post: @@ -2173,1333 +2172,1333 @@ paths: summary: replaces all values of a path configuration. description: all fields are optional. parameters: - - name: name - in: path - required: true - description: the name of the path. - schema: - type: string + - name: name + in: path + required: true + description: the name of the path. + schema: + type: string requestBody: required: true content: application/json: schema: - $ref: '#/components/schemas/PathConf' + $ref: "#/components/schemas/PathConf" responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/OK' - '400': + $ref: "#/components/schemas/OK" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '404': + $ref: "#/components/schemas/Error" + "404": description: path not found. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/config/paths/delete/{name}: delete: operationId: configPathsDelete tags: [Configuration] summary: removes a path configuration. - description: '' + description: "" parameters: - - name: name - in: path - required: true - description: the name of the path. - schema: - type: string + - name: name + in: path + required: true + description: the name of the path. + schema: + type: string responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/OK' - '400': + $ref: "#/components/schemas/OK" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '404': + $ref: "#/components/schemas/Error" + "404": description: path not found. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/hlsmuxers/list: get: operationId: hlsMuxersList tags: [HLS] summary: returns all HLS muxers. - description: '' + description: "" parameters: - - name: page - in: query - description: page number. - schema: - type: integer - default: 0 - - name: itemsPerPage - in: query - description: items per page. - schema: - type: integer - default: 100 + - name: page + in: query + description: page number. + schema: + type: integer + default: 0 + - name: itemsPerPage + in: query + description: items per page. + schema: + type: integer + default: 100 responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/HLSMuxerList' - '400': + $ref: "#/components/schemas/HLSMuxerList" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/hlsmuxers/get/{name}: get: operationId: hlsMuxersGet tags: [HLS] summary: returns a HLS muxer. - description: '' + description: "" parameters: - - name: name - in: path - required: true - description: name of the muxer. - schema: - type: string + - name: name + in: path + required: true + description: name of the muxer. + schema: + type: string responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/HLSMuxer' - '400': + $ref: "#/components/schemas/HLSMuxer" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '404': + $ref: "#/components/schemas/Error" + "404": description: muxer not found. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/hlssessions/list: get: operationId: hlssessionsList tags: [HLS] summary: returns all HLS sessions. - description: '' + description: "" parameters: - - name: page - in: query - description: page number. - schema: - type: integer - default: 0 - - name: itemsPerPage - in: query - description: items per page. - schema: - type: integer - default: 100 + - name: page + in: query + description: page number. + schema: + type: integer + default: 0 + - name: itemsPerPage + in: query + description: items per page. + schema: + type: integer + default: 100 responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/HLSSessionList' - '400': + $ref: "#/components/schemas/HLSSessionList" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/hlssessions/get/{id}: get: operationId: hlssessionsGet tags: [HLS] summary: returns a HLS session. - description: '' + description: "" parameters: - - name: id - in: path - required: true - description: ID of the session. - schema: - type: string + - name: id + in: path + required: true + description: ID of the session. + schema: + type: string responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/HLSSession' - '400': + $ref: "#/components/schemas/HLSSession" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '404': + $ref: "#/components/schemas/Error" + "404": description: session not found. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/hlssessions/kick/{id}: post: operationId: hlssessionsKick tags: [HLS] summary: kicks out a HLS session from the server. - description: '' + description: "" parameters: - - name: id - in: path - required: true - description: ID of the session. - schema: - type: string + - name: id + in: path + required: true + description: ID of the session. + schema: + type: string responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/OK' - '400': + $ref: "#/components/schemas/OK" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '404': + $ref: "#/components/schemas/Error" + "404": description: session not found. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/paths/list: get: operationId: pathsList tags: [Paths] summary: returns all paths. - description: '' + description: "" parameters: - - name: page - in: query - description: page number. - schema: - type: integer - default: 0 - - name: itemsPerPage - in: query - description: items per page. - schema: - type: integer - default: 100 + - name: page + in: query + description: page number. + schema: + type: integer + default: 0 + - name: itemsPerPage + in: query + description: items per page. + schema: + type: integer + default: 100 responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/PathList' - '400': + $ref: "#/components/schemas/PathList" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/paths/get/{name}: get: operationId: pathsGet tags: [Paths] summary: returns a path. - description: '' + description: "" parameters: - - name: name - in: path - required: true - description: name of the path. - schema: - type: string + - name: name + in: path + required: true + description: name of the path. + schema: + type: string responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/Path' - '400': + $ref: "#/components/schemas/Path" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '404': + $ref: "#/components/schemas/Error" + "404": description: path not found. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/rtspconns/list: get: operationId: rtspConnsList tags: [RTSP] summary: returns all RTSP connections. - description: '' + description: "" parameters: - - name: page - in: query - description: page number. - schema: - type: integer - default: 0 - - name: itemsPerPage - in: query - description: items per page. - schema: - type: integer - default: 100 + - name: page + in: query + description: page number. + schema: + type: integer + default: 0 + - name: itemsPerPage + in: query + description: items per page. + schema: + type: integer + default: 100 responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/RTSPConnList' - '400': + $ref: "#/components/schemas/RTSPConnList" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/rtspconns/get/{id}: get: operationId: rtspConnsGet tags: [RTSP] summary: returns a RTSP connection. - description: '' + description: "" parameters: - - name: id - in: path - required: true - description: ID of the connection. - schema: - type: string + - name: id + in: path + required: true + description: ID of the connection. + schema: + type: string responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/RTSPConn' - '400': + $ref: "#/components/schemas/RTSPConn" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '404': + $ref: "#/components/schemas/Error" + "404": description: connection not found. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/rtspsessions/list: get: operationId: rtspSessionsList tags: [RTSP] summary: returns all RTSP sessions. - description: '' + description: "" parameters: - - name: page - in: query - description: page number. - schema: - type: integer - default: 0 - - name: itemsPerPage - in: query - description: items per page. - schema: - type: integer - default: 100 + - name: page + in: query + description: page number. + schema: + type: integer + default: 0 + - name: itemsPerPage + in: query + description: items per page. + schema: + type: integer + default: 100 responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/RTSPSessionList' - '400': + $ref: "#/components/schemas/RTSPSessionList" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/rtspsessions/get/{id}: get: operationId: rtspSessionsGet tags: [RTSP] summary: returns a RTSP session. - description: '' + description: "" parameters: - - name: id - in: path - required: true - description: ID of the connection. - schema: - type: string + - name: id + in: path + required: true + description: ID of the connection. + schema: + type: string responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/RTSPSession' - '400': + $ref: "#/components/schemas/RTSPSession" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '404': + $ref: "#/components/schemas/Error" + "404": description: session not found. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/rtspsessions/kick/{id}: post: operationId: rtspSessionsKick tags: [RTSP] summary: kicks out a RTSP session from the server. - description: '' + description: "" parameters: - - name: id - in: path - required: true - description: ID of the session. - schema: - type: string + - name: id + in: path + required: true + description: ID of the session. + schema: + type: string responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/OK' - '400': + $ref: "#/components/schemas/OK" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '404': + $ref: "#/components/schemas/Error" + "404": description: session not found. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/rtspsconns/list: get: operationId: rtspsConnsList tags: [RTSP] summary: returns all RTSPS connections. - description: '' + description: "" parameters: - - name: page - in: query - description: page number. - schema: - type: integer - default: 0 - - name: itemsPerPage - in: query - description: items per page. - schema: - type: integer - default: 100 + - name: page + in: query + description: page number. + schema: + type: integer + default: 0 + - name: itemsPerPage + in: query + description: items per page. + schema: + type: integer + default: 100 responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/RTSPConnList' - '400': + $ref: "#/components/schemas/RTSPConnList" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/rtspsconns/get/{id}: get: operationId: rtspsConnsGet tags: [RTSP] summary: returns a RTSPS connection. - description: '' + description: "" parameters: - - name: id - in: path - required: true - description: ID of the connection. - schema: - type: string + - name: id + in: path + required: true + description: ID of the connection. + schema: + type: string responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/RTSPConn' - '400': + $ref: "#/components/schemas/RTSPConn" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '404': + $ref: "#/components/schemas/Error" + "404": description: connection not found. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/rtspssessions/list: get: operationId: rtspsSessionsList tags: [RTSP] summary: returns all RTSPS sessions. - description: '' + description: "" parameters: - - name: page - in: query - description: page number. - schema: - type: integer - default: 0 - - name: itemsPerPage - in: query - description: items per page. - schema: - type: integer - default: 100 + - name: page + in: query + description: page number. + schema: + type: integer + default: 0 + - name: itemsPerPage + in: query + description: items per page. + schema: + type: integer + default: 100 responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/RTSPSessionList' - '400': + $ref: "#/components/schemas/RTSPSessionList" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/rtspssessions/get/{id}: get: operationId: rtspsSessionsGet tags: [RTSP] summary: returns a RTSPS session. - description: '' + description: "" parameters: - - name: id - in: path - required: true - description: ID of the connection. - schema: - type: string + - name: id + in: path + required: true + description: ID of the connection. + schema: + type: string responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/RTSPSession' - '400': + $ref: "#/components/schemas/RTSPSession" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '404': + $ref: "#/components/schemas/Error" + "404": description: session not found. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/rtspssessions/kick/{id}: post: operationId: rtspsSessionsKick tags: [RTSP] summary: kicks out a RTSPS session from the server. - description: '' + description: "" parameters: - - name: id - in: path - required: true - description: ID of the session. - schema: - type: string + - name: id + in: path + required: true + description: ID of the session. + schema: + type: string responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/OK' - '400': + $ref: "#/components/schemas/OK" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '404': + $ref: "#/components/schemas/Error" + "404": description: session not found. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/rtmpconns/list: get: operationId: rtmpConnsList tags: [RTMP] summary: returns all RTMP connections. - description: '' + description: "" parameters: - - name: page - in: query - description: page number. - schema: - type: integer - default: 0 - - name: itemsPerPage - in: query - description: items per page. - schema: - type: integer - default: 100 + - name: page + in: query + description: page number. + schema: + type: integer + default: 0 + - name: itemsPerPage + in: query + description: items per page. + schema: + type: integer + default: 100 responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/RTMPConnList' - '400': + $ref: "#/components/schemas/RTMPConnList" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/rtmpconns/get/{id}: get: operationId: rtmpConnectionsGet tags: [RTMP] summary: returns a RTMP connection. - description: '' + description: "" parameters: - - name: id - in: path - required: true - description: ID of the connection. - schema: - type: string + - name: id + in: path + required: true + description: ID of the connection. + schema: + type: string responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/RTMPConn' - '400': + $ref: "#/components/schemas/RTMPConn" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '404': + $ref: "#/components/schemas/Error" + "404": description: connection not found. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/rtmpconns/kick/{id}: post: operationId: rtmpConnsKick tags: [RTMP] summary: kicks out a RTMP connection from the server. - description: '' + description: "" parameters: - - name: id - in: path - required: true - description: ID of the connection. - schema: - type: string + - name: id + in: path + required: true + description: ID of the connection. + schema: + type: string responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/OK' - '400': + $ref: "#/components/schemas/OK" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '404': + $ref: "#/components/schemas/Error" + "404": description: connection not found. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/rtmpsconns/list: get: operationId: rtmpsConnsList tags: [RTMP] summary: returns all RTMPS connections. - description: '' + description: "" parameters: - - name: page - in: query - description: page number. - schema: - type: integer - default: 0 - - name: itemsPerPage - in: query - description: items per page. - schema: - type: integer - default: 100 + - name: page + in: query + description: page number. + schema: + type: integer + default: 0 + - name: itemsPerPage + in: query + description: items per page. + schema: + type: integer + default: 100 responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/RTMPConnList' - '400': + $ref: "#/components/schemas/RTMPConnList" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/rtmpsconns/get/{id}: get: operationId: rtmpsConnectionsGet tags: [RTMP] summary: returns a RTMPS connection. - description: '' + description: "" parameters: - - name: id - in: path - required: true - description: ID of the connection. - schema: - type: string + - name: id + in: path + required: true + description: ID of the connection. + schema: + type: string responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/RTMPConn' - '400': + $ref: "#/components/schemas/RTMPConn" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '404': + $ref: "#/components/schemas/Error" + "404": description: connection not found. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/rtmpsconns/kick/{id}: post: operationId: rtmpsConnsKick tags: [RTMP] summary: kicks out a RTMPS connection from the server. - description: '' + description: "" parameters: - - name: id - in: path - required: true - description: ID of the connection. - schema: - type: string + - name: id + in: path + required: true + description: ID of the connection. + schema: + type: string responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/OK' - '400': + $ref: "#/components/schemas/OK" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '404': + $ref: "#/components/schemas/Error" + "404": description: connection not found. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/srtconns/list: get: operationId: srtConnsList tags: [SRT] summary: returns all SRT connections. - description: '' + description: "" parameters: - - name: page - in: query - description: page number. - schema: - type: integer - default: 0 - - name: itemsPerPage - in: query - description: items per page. - schema: - type: integer - default: 100 + - name: page + in: query + description: page number. + schema: + type: integer + default: 0 + - name: itemsPerPage + in: query + description: items per page. + schema: + type: integer + default: 100 responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/SRTConnList' - '400': + $ref: "#/components/schemas/SRTConnList" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/srtconns/get/{id}: get: operationId: srtConnsGet tags: [SRT] summary: returns a SRT connection. - description: '' + description: "" parameters: - - name: id - in: path - required: true - description: ID of the connection. - schema: - type: string + - name: id + in: path + required: true + description: ID of the connection. + schema: + type: string responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/SRTConn' - '400': + $ref: "#/components/schemas/SRTConn" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '404': + $ref: "#/components/schemas/Error" + "404": description: connection not found. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/srtconns/kick/{id}: post: operationId: srtConnsKick tags: [SRT] summary: kicks out a SRT connection from the server. - description: '' + description: "" parameters: - - name: id - in: path - required: true - description: ID of the connection. - schema: - type: string + - name: id + in: path + required: true + description: ID of the connection. + schema: + type: string responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/OK' - '400': + $ref: "#/components/schemas/OK" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '404': + $ref: "#/components/schemas/Error" + "404": description: connection not found. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/webrtcsessions/list: get: operationId: webrtcSessionsList tags: [WebRTC] summary: returns all WebRTC sessions. - description: '' + description: "" parameters: - - name: page - in: query - description: page number. - schema: - type: integer - default: 0 - - name: itemsPerPage - in: query - description: items per page. - schema: - type: integer - default: 100 + - name: page + in: query + description: page number. + schema: + type: integer + default: 0 + - name: itemsPerPage + in: query + description: items per page. + schema: + type: integer + default: 100 responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/WebRTCSessionList' - '400': + $ref: "#/components/schemas/WebRTCSessionList" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/webrtcsessions/get/{id}: get: operationId: webrtcSessionsGet tags: [WebRTC] summary: returns a WebRTC session. - description: '' + description: "" parameters: - - name: id - in: path - required: true - description: ID of the session. - schema: - type: string + - name: id + in: path + required: true + description: ID of the session. + schema: + type: string responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/WebRTCSession' - '400': + $ref: "#/components/schemas/WebRTCSession" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '404': + $ref: "#/components/schemas/Error" + "404": description: session not found. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/webrtcsessions/kick/{id}: post: operationId: webrtcSessionsKick tags: [WebRTC] summary: kicks out a WebRTC session from the server. - description: '' + description: "" parameters: - - name: id - in: path - required: true - description: ID of the session. - schema: - type: string + - name: id + in: path + required: true + description: ID of the session. + schema: + type: string responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/OK' - '400': + $ref: "#/components/schemas/OK" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '404': + $ref: "#/components/schemas/Error" + "404": description: session not found. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/recordings/list: get: operationId: recordingsList tags: [Recordings] summary: returns all recordings, splitted by path. - description: '' + description: "" parameters: - - name: page - in: query - description: page number. - schema: - type: integer - default: 0 - - name: itemsPerPage - in: query - description: items per page. - schema: - type: integer - default: 100 + - name: page + in: query + description: page number. + schema: + type: integer + default: 0 + - name: itemsPerPage + in: query + description: items per page. + schema: + type: integer + default: 100 responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/RecordingList' - '400': + $ref: "#/components/schemas/RecordingList" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/recordings/get/{name}: get: operationId: recordingsGet tags: [Recordings] summary: returns recordings of a path. - description: '' + description: "" parameters: - - name: name - in: path - required: true - description: name of the path. - schema: - type: string + - name: name + in: path + required: true + description: name of the path. + schema: + type: string responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/Recording' - '400': + $ref: "#/components/schemas/Recording" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '404': + $ref: "#/components/schemas/Error" + "404": description: path not found. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" /v3/recordings/deletesegment: delete: operationId: recordingsDeleteSegment tags: [Recordings] summary: deletes a recording segment. - description: '' + description: "" parameters: - - name: path - in: query - required: true - description: path. - schema: - type: string - - name: start - in: query - required: true - description: starting date of the segment. - schema: - type: string + - name: path + in: query + required: true + description: path. + schema: + type: string + - name: start + in: query + required: true + description: starting date of the segment. + schema: + type: string responses: - '200': + "200": description: the request was successful. content: application/json: schema: - $ref: '#/components/schemas/OK' - '400': + $ref: "#/components/schemas/OK" + "400": description: invalid request. content: application/json: schema: - $ref: '#/components/schemas/Error' - '404': + $ref: "#/components/schemas/Error" + "404": description: connection not found. content: application/json: schema: - $ref: '#/components/schemas/Error' - '500': + $ref: "#/components/schemas/Error" + "500": description: server error. content: application/json: schema: - $ref: '#/components/schemas/Error' + $ref: "#/components/schemas/Error" diff --git a/internal/servers/hls/index.html b/internal/servers/hls/index.html index c10291f9..c07446d2 100644 --- a/internal/servers/hls/index.html +++ b/internal/servers/hls/index.html @@ -1,204 +1,203 @@ - + - - - - - - + + + + + + + +
+
- -
-
+ - + - - + // use load instead of DOMContentLoaded, otherwise, in Firefox, + // the page gets stuck in the "loading" state. + window.addEventListener("load", () => { + loadAttributesFromQuery(); + loadStream(); + }); + + diff --git a/internal/servers/webrtc/publish_index.html b/internal/servers/webrtc/publish_index.html index 0f4897ab..a6df7e47 100644 --- a/internal/servers/webrtc/publish_index.html +++ b/internal/servers/webrtc/publish_index.html @@ -1,434 +1,453 @@ - + - - - - - - - - - - -
-
-
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- -
- -
-
-
- -
- -
-
- -
- - + + + - navigator.mediaDevices.getUserMedia({ - video: videoOpts, - audio: audioOpts, - }) - .then((stream) => onStream(stream)) - .catch((err) => { - setMessage(err.toString()); - }); - } else { - navigator.mediaDevices.getDisplayMedia({ - video: { - width: { ideal: videoForm.width.value }, - height: { ideal: videoForm.height.value }, - frameRate: { ideal: videoForm.framerate.value }, - cursor: 'always', - }, - audio: true, - }) - .then((stream) => onStream(stream)) - .catch((err) => { - setMessage(err.toString()); - }); - } -}; +
+
+
+ + +
-const selectHasOption = (select, option) => { - for (const opt of select.querySelectorAll('option')) { - if (opt.value === option) { - return true; - } - } - return false; -}; +
+ + +
-const populateDevices = () => { - return navigator.mediaDevices.enumerateDevices() - .then((devices) => { - for (const device of devices) { - if (device.kind === 'videoinput' || device.kind === 'audioinput') { - const select = (device.kind === 'videoinput') ? videoForm.device : audioForm.device; +
+ + +
- if (!selectHasOption(select, device.deviceId)) { - const opt = document.createElement('option'); - opt.value = device.deviceId; - opt.text = device.label; - select.appendChild(opt); +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ +
+
+
+ +
+ +
+
+ +
+ + + video.style.display = "none"; + controls.style.display = "flex"; + }, + ); + }) + .catch((err) => { + setMessage(err.toString()); + }); + }; - + const setupEventListeners = () => { + const url = new URL(window.location.href); + const inputs = [ + ...Object.values(videoForm), + ...Object.values(audioForm), + ]; + + for (const input of inputs) { + if (input instanceof HTMLInputElement && input.type === "text") { + input.addEventListener("input", () => { + url.searchParams.set(input.id, input.value); + window.history.replaceState(null, null, url); + }); + } + + if (input instanceof HTMLInputElement && input.type === "checkbox") { + input.addEventListener("input", () => { + url.searchParams.set(input.id, input.checked); + window.history.replaceState(null, null, url); + }); + } + + if (input instanceof HTMLSelectElement) { + input.addEventListener("input", () => { + url.searchParams.set(input.id, input.value); + window.history.replaceState(null, null, url); + }); + } + } + }; + + const loadValuesFromQuery = () => { + const params = new URLSearchParams(window.location.search); + const inputs = [ + ...Object.values(videoForm), + ...Object.values(audioForm), + ]; + + for (const input of inputs) { + const value = params.get(input.id); + if (value) { + if (input instanceof HTMLInputElement && input.type === "text") { + input.value = value; + } else if ( + input instanceof HTMLInputElement && + input.type === "checkbox" + ) { + input.checked = value === "true"; + } else if (input instanceof HTMLSelectElement) { + input.value = value; + } + } + } + }; + + window.addEventListener("load", () => { + if (navigator.mediaDevices === undefined) { + setMessage( + `can't access webcams or microphones. Make sure that WebRTC encryption is enabled.`, + ); + return; + } + + publishButton.addEventListener("click", onPublish); + populateOptions(); + }); + + window.addEventListener("beforeunload", () => { + if (publisher !== null) { + publisher.close(); + } + }); + + diff --git a/internal/servers/webrtc/publisher.js b/internal/servers/webrtc/publisher.js index 6d3336a4..627864c4 100644 --- a/internal/servers/webrtc/publisher.js +++ b/internal/servers/webrtc/publisher.js @@ -1,4 +1,4 @@ -'use strict'; +"use strict"; /** * @callback OnError @@ -44,7 +44,7 @@ class MediaMTXWebRTCPublisher { constructor(conf) { this.#retryPause = 2000; this.#conf = conf; - this.#state = 'running'; + this.#state = "running"; this.#restartTimeout = null; this.#pc = null; this.#offerData = null; @@ -57,7 +57,7 @@ class MediaMTXWebRTCPublisher { * Close the publisher and all its resources. */ close = () => { - this.#state = 'closed'; + this.#state = "closed"; if (this.#pc !== null) { this.#pc.close(); @@ -73,36 +73,40 @@ class MediaMTXWebRTCPublisher { } static #linkToIceServers(links) { - return (links !== null) ? links.split(', ').map((link) => { - const m = link.match(/^<(.+?)>; rel="ice-server"(; username="(.*?)"; credential="(.*?)"; credential-type="password")?/i); - const ret = { - urls: [m[1]], - }; + return links !== null + ? links.split(", ").map((link) => { + const m = link.match( + /^<(.+?)>; rel="ice-server"(; username="(.*?)"; credential="(.*?)"; credential-type="password")?/i, + ); + const ret = { + urls: [m[1]], + }; - if (m[3] !== undefined) { - ret.username = this.#unquoteCredential(m[3]); - ret.credential = this.#unquoteCredential(m[4]); - ret.credentialType = 'password'; - } + if (m[3] !== undefined) { + ret.username = this.#unquoteCredential(m[3]); + ret.credential = this.#unquoteCredential(m[4]); + ret.credentialType = "password"; + } - return ret; - }) : []; + return ret; + }) + : []; } static #parseOffer(offer) { const ret = { - iceUfrag: '', - icePwd: '', + iceUfrag: "", + icePwd: "", medias: [], }; - for (const line of offer.split('\r\n')) { - if (line.startsWith('m=')) { - ret.medias.push(line.slice('m='.length)); - } else if (ret.iceUfrag === '' && line.startsWith('a=ice-ufrag:')) { - ret.iceUfrag = line.slice('a=ice-ufrag:'.length); - } else if (ret.icePwd === '' && line.startsWith('a=ice-pwd:')) { - ret.icePwd = line.slice('a=ice-pwd:'.length); + for (const line of offer.split("\r\n")) { + if (line.startsWith("m=")) { + ret.medias.push(line.slice("m=".length)); + } else if (ret.iceUfrag === "" && line.startsWith("a=ice-ufrag:")) { + ret.iceUfrag = line.slice("a=ice-ufrag:".length); + } else if (ret.icePwd === "" && line.startsWith("a=ice-pwd:")) { + ret.icePwd = line.slice("a=ice-pwd:".length); } } @@ -119,18 +123,17 @@ class MediaMTXWebRTCPublisher { candidatesByMedia[mid].push(candidate); } - let frag = 'a=ice-ufrag:' + od.iceUfrag + '\r\n' - + 'a=ice-pwd:' + od.icePwd + '\r\n'; + let frag = + "a=ice-ufrag:" + od.iceUfrag + "\r\n" + "a=ice-pwd:" + od.icePwd + "\r\n"; let mid = 0; for (const media of od.medias) { if (candidatesByMedia[mid] !== undefined) { - frag += 'm=' + media + '\r\n' - + 'a=mid:' + mid + '\r\n'; + frag += "m=" + media + "\r\n" + "a=mid:" + mid + "\r\n"; for (const candidate of candidatesByMedia[mid]) { - frag += 'a=' + candidate.candidate + '\r\n'; + frag += "a=" + candidate.candidate + "\r\n"; } } mid++; @@ -140,16 +143,16 @@ class MediaMTXWebRTCPublisher { } static #setCodec(section, codec) { - const lines = section.split('\r\n'); + const lines = section.split("\r\n"); const lines2 = []; const payloadFormats = []; for (const line of lines) { - if (!line.startsWith('a=rtpmap:')) { + if (!line.startsWith("a=rtpmap:")) { lines2.push(line); } else { if (line.toLowerCase().includes(codec)) { - payloadFormats.push(line.slice('a=rtpmap:'.length).split(' ')[0]); + payloadFormats.push(line.slice("a=rtpmap:".length).split(" ")[0]); lines2.push(line); } } @@ -161,13 +164,19 @@ class MediaMTXWebRTCPublisher { for (const line of lines2) { if (firstLine) { firstLine = false; - lines3.push(line.split(' ').slice(0, 3).concat(payloadFormats).join(' ')); - } else if (line.startsWith('a=fmtp:')) { - if (payloadFormats.includes(line.slice('a=fmtp:'.length).split(' ')[0])) { + lines3.push( + line.split(" ").slice(0, 3).concat(payloadFormats).join(" "), + ); + } else if (line.startsWith("a=fmtp:")) { + if ( + payloadFormats.includes(line.slice("a=fmtp:".length).split(" ")[0]) + ) { lines3.push(line); } - } else if (line.startsWith('a=rtcp-fb:')) { - if (payloadFormats.includes(line.slice('a=rtcp-fb:'.length).split(' ')[0])) { + } else if (line.startsWith("a=rtcp-fb:")) { + if ( + payloadFormats.includes(line.slice("a=rtcp-fb:".length).split(" ")[0]) + ) { lines3.push(line); } } else { @@ -175,76 +184,93 @@ class MediaMTXWebRTCPublisher { } } - return lines3.join('\r\n'); + return lines3.join("\r\n"); } static #setVideoBitrate(section, bitrate) { - let lines = section.split('\r\n'); + let lines = section.split("\r\n"); for (let i = 0; i < lines.length; i++) { - if (lines[i].startsWith('c=')) { - lines = [...lines.slice(0, i+1), 'b=TIAS:' + (parseInt(bitrate) * 1024).toString(), ...lines.slice(i+1)]; - break - } - } - - return lines.join('\r\n'); - } - - static #setAudioBitrate(section, bitrate, voice) { - let opusPayloadFormat = ''; - let lines = section.split('\r\n'); - - for (let i = 0; i < lines.length; i++) { - if (lines[i].startsWith('a=rtpmap:') && lines[i].toLowerCase().includes('opus/')) { - opusPayloadFormat = lines[i].slice('a=rtpmap:'.length).split(' ')[0]; + if (lines[i].startsWith("c=")) { + lines = [ + ...lines.slice(0, i + 1), + "b=TIAS:" + (parseInt(bitrate) * 1024).toString(), + ...lines.slice(i + 1), + ]; break; } } - if (opusPayloadFormat === '') { + return lines.join("\r\n"); + } + + static #setAudioBitrate(section, bitrate, voice) { + let opusPayloadFormat = ""; + let lines = section.split("\r\n"); + + for (let i = 0; i < lines.length; i++) { + if ( + lines[i].startsWith("a=rtpmap:") && + lines[i].toLowerCase().includes("opus/") + ) { + opusPayloadFormat = lines[i].slice("a=rtpmap:".length).split(" ")[0]; + break; + } + } + + if (opusPayloadFormat === "") { return section; } for (let i = 0; i < lines.length; i++) { - if (lines[i].startsWith('a=fmtp:' + opusPayloadFormat + ' ')) { + if (lines[i].startsWith("a=fmtp:" + opusPayloadFormat + " ")) { if (voice) { - lines[i] = 'a=fmtp:' + opusPayloadFormat + ' minptime=10;useinbandfec=1;maxaveragebitrate=' - + (parseInt(bitrate) * 1024).toString(); + lines[i] = + "a=fmtp:" + + opusPayloadFormat + + " minptime=10;useinbandfec=1;maxaveragebitrate=" + + (parseInt(bitrate) * 1024).toString(); } else { - lines[i] = 'a=fmtp:' + opusPayloadFormat + ' maxplaybackrate=48000;stereo=1;sprop-stereo=1;maxaveragebitrate=' - + (parseInt(bitrate) * 1024).toString(); + lines[i] = + "a=fmtp:" + + opusPayloadFormat + + " maxplaybackrate=48000;stereo=1;sprop-stereo=1;maxaveragebitrate=" + + (parseInt(bitrate) * 1024).toString(); } } } - return lines.join('\r\n'); + return lines.join("\r\n"); } static #editOffer(sdp, videoCodec, audioCodec, audioBitrate, audioVoice) { - const sections = sdp.split('m='); + const sections = sdp.split("m="); for (let i = 0; i < sections.length; i++) { - if (sections[i].startsWith('video')) { + if (sections[i].startsWith("video")) { sections[i] = this.#setCodec(sections[i], videoCodec); - } else if (sections[i].startsWith('audio')) { - sections[i] = this.#setAudioBitrate(this.#setCodec(sections[i], audioCodec), audioBitrate, audioVoice); + } else if (sections[i].startsWith("audio")) { + sections[i] = this.#setAudioBitrate( + this.#setCodec(sections[i], audioCodec), + audioBitrate, + audioVoice, + ); } } - return sections.join('m='); + return sections.join("m="); } static #editAnswer(sdp, videoBitrate) { - const sections = sdp.split('m='); + const sections = sdp.split("m="); for (let i = 0; i < sections.length; i++) { - if (sections[i].startsWith('video')) { + if (sections[i].startsWith("video")) { sections[i] = this.#setVideoBitrate(sections[i], videoBitrate); } } - return sections.join('m='); + return sections.join("m="); } #start() { @@ -259,7 +285,7 @@ class MediaMTXWebRTCPublisher { /** @param {string} err */ #handleError(err) { - if (this.#state === 'running') { + if (this.#state === "running") { if (this.#pc !== null) { this.#pc.close(); this.#pc = null; @@ -269,17 +295,17 @@ class MediaMTXWebRTCPublisher { if (this.#sessionUrl !== null) { fetch(this.#sessionUrl, { - method: 'DELETE', + method: "DELETE", }); this.#sessionUrl = null; } this.#queuedCandidates = []; - this.#state = 'restarting'; + this.#state = "restarting"; this.#restartTimeout = window.setTimeout(() => { this.#restartTimeout = null; - this.#state = 'running'; + this.#state = "running"; this.#start(); }, this.#retryPause); @@ -290,35 +316,36 @@ class MediaMTXWebRTCPublisher { } #authHeader() { - if (this.#conf.user !== undefined && this.#conf.user !== '') { + if (this.#conf.user !== undefined && this.#conf.user !== "") { const credentials = btoa(`${this.#conf.user}:${this.#conf.pass}`); - return {'Authorization': `Basic ${credentials}`}; + return { Authorization: `Basic ${credentials}` }; } - if (this.#conf.token !== undefined && this.#conf.token !== '') { - return {'Authorization': `Bearer ${this.#conf.token}`}; + if (this.#conf.token !== undefined && this.#conf.token !== "") { + return { Authorization: `Bearer ${this.#conf.token}` }; } return {}; } #requestICEServers() { return fetch(this.#conf.url, { - method: 'OPTIONS', + method: "OPTIONS", headers: { ...this.#authHeader(), }, - }) - .then((res) => MediaMTXWebRTCPublisher.#linkToIceServers(res.headers.get('Link'))); + }).then((res) => + MediaMTXWebRTCPublisher.#linkToIceServers(res.headers.get("Link")), + ); } #setupPeerConnection(iceServers) { - if (this.#state !== 'running') { - throw new Error('closed'); + if (this.#state !== "running") { + throw new Error("closed"); } this.#pc = new RTCPeerConnection({ iceServers, // https://webrtc.org/getting-started/unified-plan-transition-guide - sdpSemantics: 'unified-plan', + sdpSemantics: "unified-plan", }); this.#pc.onicecandidate = (evt) => this.#onLocalCandidate(evt); @@ -328,18 +355,16 @@ class MediaMTXWebRTCPublisher { this.#pc.addTrack(track, this.#conf.stream); }); - return this.#pc.createOffer() - .then((offer) => { - this.#offerData = MediaMTXWebRTCPublisher.#parseOffer(offer.sdp); + return this.#pc.createOffer().then((offer) => { + this.#offerData = MediaMTXWebRTCPublisher.#parseOffer(offer.sdp); - return this.#pc.setLocalDescription(offer) - .then(() => offer.sdp); - }); + return this.#pc.setLocalDescription(offer).then(() => offer.sdp); + }); } #sendOffer(offer) { - if (this.#state !== 'running') { - throw new Error('closed'); + if (this.#state !== "running") { + throw new Error("closed"); } offer = MediaMTXWebRTCPublisher.#editOffer( @@ -347,45 +372,56 @@ class MediaMTXWebRTCPublisher { this.#conf.videoCodec, this.#conf.audioCodec, this.#conf.audioBitrate, - this.#conf.audioVoice); + this.#conf.audioVoice, + ); return fetch(this.#conf.url, { - method: 'POST', + method: "POST", headers: { ...this.#authHeader(), - 'Content-Type': 'application/sdp', + "Content-Type": "application/sdp", }, body: offer, - }) - .then((res) => { - switch (res.status) { - case 201: - break; - case 400: - return res.json().then((e) => { throw new Error(e.error); }); - default: - throw new Error(`bad status code ${res.status}`); - } + }).then((res) => { + switch (res.status) { + case 201: + break; + case 400: + return res.json().then((e) => { + throw new Error(e.error); + }); + default: + throw new Error(`bad status code ${res.status}`); + } - this.#sessionUrl = new URL(res.headers.get('location'), this.#conf.url).toString(); + this.#sessionUrl = new URL( + res.headers.get("location"), + this.#conf.url, + ).toString(); - return res.text(); - }); + return res.text(); + }); } #setAnswer(answer) { - if (this.#state !== 'running') { - throw new Error('closed'); + if (this.#state !== "running") { + throw new Error("closed"); } - answer = MediaMTXWebRTCPublisher.#editAnswer(answer, this.#conf.videoBitrate); + answer = MediaMTXWebRTCPublisher.#editAnswer( + answer, + this.#conf.videoBitrate, + ); - return this.#pc.setRemoteDescription(new RTCSessionDescription({ - type: 'answer', - sdp: answer, - })) + return this.#pc + .setRemoteDescription( + new RTCSessionDescription({ + type: "answer", + sdp: answer, + }), + ) .then(() => { - if (this.#state !== 'running') { + if (this.#state !== "running") { return; } @@ -397,7 +433,7 @@ class MediaMTXWebRTCPublisher { } #onLocalCandidate(evt) { - if (this.#state !== 'running') { + if (this.#state !== "running") { return; } @@ -412,19 +448,22 @@ class MediaMTXWebRTCPublisher { #sendLocalCandidates(candidates) { fetch(this.#sessionUrl, { - method: 'PATCH', + method: "PATCH", headers: { - 'Content-Type': 'application/trickle-ice-sdpfrag', - 'If-Match': '*', + "Content-Type": "application/trickle-ice-sdpfrag", + "If-Match": "*", }, - body: MediaMTXWebRTCPublisher.#generateSdpFragment(this.#offerData, candidates), + body: MediaMTXWebRTCPublisher.#generateSdpFragment( + this.#offerData, + candidates, + ), }) .then((res) => { switch (res.status) { case 204: break; case 404: - throw new Error('stream not found'); + throw new Error("stream not found"); default: throw new Error(`bad status code ${res.status}`); } @@ -435,7 +474,7 @@ class MediaMTXWebRTCPublisher { } #onConnectionState() { - if (this.#state !== 'running') { + if (this.#state !== "running") { return; } @@ -443,17 +482,17 @@ class MediaMTXWebRTCPublisher { // the close() method being called at all. // It happens when the other peer sends a termination // message like a DTLS CloseNotify. - if (this.#pc.connectionState === 'failed' - || this.#pc.connectionState === 'closed' + if ( + this.#pc.connectionState === "failed" || + this.#pc.connectionState === "closed" ) { - this.#handleError('peer connection closed'); - } else if (this.#pc.connectionState === 'connected') { + this.#handleError("peer connection closed"); + } else if (this.#pc.connectionState === "connected") { if (this.#conf.onConnected !== undefined) { this.#conf.onConnected(); } } } - } window.MediaMTXWebRTCPublisher = MediaMTXWebRTCPublisher; diff --git a/internal/servers/webrtc/read_index.html b/internal/servers/webrtc/read_index.html index d2924eb9..75e1dd42 100644 --- a/internal/servers/webrtc/read_index.html +++ b/internal/servers/webrtc/read_index.html @@ -1,115 +1,115 @@ - + - - - - - - - + + + + + + + + +
- -
+ + if (["1", "yes", "true"].includes(str.toLowerCase())) { + return true; + } + if (["0", "no", "false"].includes(str.toLowerCase())) { + return false; + } + return defaultVal; + }; - + const loadAttributesFromQuery = () => { + const params = new URLSearchParams(window.location.search); + video.controls = parseBoolString(params.get("controls"), true); + video.muted = parseBoolString(params.get("muted"), true); + video.autoplay = parseBoolString(params.get("autoplay"), true); + video.playsInline = parseBoolString(params.get("playsinline"), true); + video.disablepictureinpicture = parseBoolString( + params.get("disablepictureinpicture"), + false, + ); + defaultControls = video.controls; + }; + + window.addEventListener("load", () => { + loadAttributesFromQuery(); + + reader = new MediaMTXWebRTCReader({ + url: new URL("whep", window.location.href) + window.location.search, + onError: (err) => { + setMessage(err); + }, + onTrack: (evt) => { + setMessage(""); + video.srcObject = evt.streams[0]; + }, + onDataChannel: (evt) => { + evt.channel.binaryType = "arraybuffer"; + evt.channel.onmessage = (evt) => { + console.log("data channel message", evt.data); + }; + }, + }); + }); + + window.addEventListener("beforeunload", () => { + if (reader !== null) { + reader.close(); + } + }); + + diff --git a/internal/servers/webrtc/reader.js b/internal/servers/webrtc/reader.js index 2851c77f..e5494ee9 100644 --- a/internal/servers/webrtc/reader.js +++ b/internal/servers/webrtc/reader.js @@ -1,4 +1,4 @@ -'use strict'; +"use strict"; /** * @callback OnError @@ -46,7 +46,7 @@ class MediaMTXWebRTCReader { constructor(conf) { this.#retryPause = 2000; this.#conf = conf; - this.#state = 'getting_codecs'; + this.#state = "getting_codecs"; this.#restartTimeout = null; this.#pc = null; this.#offerData = null; @@ -59,7 +59,7 @@ class MediaMTXWebRTCReader { * Close the reader and all its resources. */ close() { - this.#state = 'closed'; + this.#state = "closed"; if (this.#pc !== null) { this.#pc.close(); @@ -73,54 +73,59 @@ class MediaMTXWebRTCReader { static #supportsNonAdvertisedCodec(codec, fmtp) { return new Promise((resolve) => { const pc = new RTCPeerConnection({ iceServers: [] }); - const mediaType = 'audio'; - let payloadType = ''; + const mediaType = "audio"; + let payloadType = ""; - pc.addTransceiver(mediaType, { direction: 'recvonly' }); + pc.addTransceiver(mediaType, { direction: "recvonly" }); pc.createOffer() .then((offer) => { if (offer.sdp === undefined) { - throw new Error('SDP not present'); + throw new Error("SDP not present"); } - if (offer.sdp.includes(` ${codec}`)) { // codec is advertised, there's no need to add it manually - throw new Error('already present'); + if (offer.sdp.includes(` ${codec}`)) { + // codec is advertised, there's no need to add it manually + throw new Error("already present"); } const sections = offer.sdp.split(`m=${mediaType}`); - const payloadTypes = sections.slice(1) - .map((s) => s.split('\r\n')[0].split(' ').slice(3)) + const payloadTypes = sections + .slice(1) + .map((s) => s.split("\r\n")[0].split(" ").slice(3)) .reduce((prev, cur) => [...prev, ...cur], []); payloadType = this.#reservePayloadType(payloadTypes); - const lines = sections[1].split('\r\n'); + const lines = sections[1].split("\r\n"); lines[0] += ` ${payloadType}`; lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} ${codec}`); if (fmtp !== undefined) { lines.splice(lines.length - 1, 0, `a=fmtp:${payloadType} ${fmtp}`); } - sections[1] = lines.join('\r\n'); + sections[1] = lines.join("\r\n"); offer.sdp = sections.join(`m=${mediaType}`); return pc.setLocalDescription(offer); }) - .then(() => ( - pc.setRemoteDescription(new RTCSessionDescription({ - type: 'answer', - sdp: 'v=0\r\n' - + 'o=- 6539324223450680508 0 IN IP4 0.0.0.0\r\n' - + 's=-\r\n' - + 't=0 0\r\n' - + 'a=fingerprint:sha-256 0D:9F:78:15:42:B5:4B:E6:E2:94:3E:5B:37:78:E1:4B:54:59:A3:36:3A:E5:05:EB:27:EE:8F:D2:2D:41:29:25\r\n' - + `m=${mediaType} 9 UDP/TLS/RTP/SAVPF ${payloadType}\r\n` - + 'c=IN IP4 0.0.0.0\r\n' - + 'a=ice-pwd:7c3bf4770007e7432ee4ea4d697db675\r\n' - + 'a=ice-ufrag:29e036dc\r\n' - + 'a=sendonly\r\n' - + 'a=rtcp-mux\r\n' - + `a=rtpmap:${payloadType} ${codec}\r\n` - + ((fmtp !== undefined) ? `a=fmtp:${payloadType} ${fmtp}\r\n` : ''), - })) - )) + .then(() => + pc.setRemoteDescription( + new RTCSessionDescription({ + type: "answer", + sdp: + "v=0\r\n" + + "o=- 6539324223450680508 0 IN IP4 0.0.0.0\r\n" + + "s=-\r\n" + + "t=0 0\r\n" + + "a=fingerprint:sha-256 0D:9F:78:15:42:B5:4B:E6:E2:94:3E:5B:37:78:E1:4B:54:59:A3:36:3A:E5:05:EB:27:EE:8F:D2:2D:41:29:25\r\n" + + `m=${mediaType} 9 UDP/TLS/RTP/SAVPF ${payloadType}\r\n` + + "c=IN IP4 0.0.0.0\r\n" + + "a=ice-pwd:7c3bf4770007e7432ee4ea4d697db675\r\n" + + "a=ice-ufrag:29e036dc\r\n" + + "a=sendonly\r\n" + + "a=rtcp-mux\r\n" + + `a=rtpmap:${payloadType} ${codec}\r\n` + + (fmtp !== undefined ? `a=fmtp:${payloadType} ${fmtp}\r\n` : ""), + }), + ), + ) .then(() => { resolve(true); }) @@ -138,36 +143,40 @@ class MediaMTXWebRTCReader { } static #linkToIceServers(links) { - return (links !== null) ? links.split(', ').map((link) => { - const m = link.match(/^<(.+?)>; rel="ice-server"(; username="(.*?)"; credential="(.*?)"; credential-type="password")?/i); - const ret = { - urls: [m[1]], - }; + return links !== null + ? links.split(", ").map((link) => { + const m = link.match( + /^<(.+?)>; rel="ice-server"(; username="(.*?)"; credential="(.*?)"; credential-type="password")?/i, + ); + const ret = { + urls: [m[1]], + }; - if (m[3] !== undefined) { - ret.username = this.#unquoteCredential(m[3]); - ret.credential = this.#unquoteCredential(m[4]); - ret.credentialType = 'password'; - } + if (m[3] !== undefined) { + ret.username = this.#unquoteCredential(m[3]); + ret.credential = this.#unquoteCredential(m[4]); + ret.credentialType = "password"; + } - return ret; - }) : []; + return ret; + }) + : []; } static #parseOffer(sdp) { const ret = { - iceUfrag: '', - icePwd: '', + iceUfrag: "", + icePwd: "", medias: [], }; - for (const line of sdp.split('\r\n')) { - if (line.startsWith('m=')) { - ret.medias.push(line.slice('m='.length)); - } else if (ret.iceUfrag === '' && line.startsWith('a=ice-ufrag:')) { - ret.iceUfrag = line.slice('a=ice-ufrag:'.length); - } else if (ret.icePwd === '' && line.startsWith('a=ice-pwd:')) { - ret.icePwd = line.slice('a=ice-pwd:'.length); + for (const line of sdp.split("\r\n")) { + if (line.startsWith("m=")) { + ret.medias.push(line.slice("m=".length)); + } else if (ret.iceUfrag === "" && line.startsWith("a=ice-ufrag:")) { + ret.iceUfrag = line.slice("a=ice-ufrag:".length); + } else if (ret.icePwd === "" && line.startsWith("a=ice-pwd:")) { + ret.icePwd = line.slice("a=ice-pwd:".length); } } @@ -184,11 +193,11 @@ class MediaMTXWebRTCReader { return pl; } } - throw Error('unable to find a free payload type'); + throw Error("unable to find a free payload type"); } static #enableStereoPcmau(payloadTypes, section) { - const lines = section.split('\r\n'); + const lines = section.split("\r\n"); let payloadType = this.#reservePayloadType(payloadTypes); lines[0] += ` ${payloadType}`; @@ -200,53 +209,101 @@ class MediaMTXWebRTCReader { lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} PCMA/8000/2`); lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`); - return lines.join('\r\n'); + return lines.join("\r\n"); } static #enableMultichannelOpus(payloadTypes, section) { - const lines = section.split('\r\n'); + const lines = section.split("\r\n"); let payloadType = this.#reservePayloadType(payloadTypes); lines[0] += ` ${payloadType}`; - lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} multiopus/48000/3`); - lines.splice(lines.length - 1, 0, `a=fmtp:${payloadType} channel_mapping=0,2,1;num_streams=2;coupled_streams=1`); + lines.splice( + lines.length - 1, + 0, + `a=rtpmap:${payloadType} multiopus/48000/3`, + ); + lines.splice( + lines.length - 1, + 0, + `a=fmtp:${payloadType} channel_mapping=0,2,1;num_streams=2;coupled_streams=1`, + ); lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`); payloadType = this.#reservePayloadType(payloadTypes); lines[0] += ` ${payloadType}`; - lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} multiopus/48000/4`); - lines.splice(lines.length - 1, 0, `a=fmtp:${payloadType} channel_mapping=0,1,2,3;num_streams=2;coupled_streams=2`); + lines.splice( + lines.length - 1, + 0, + `a=rtpmap:${payloadType} multiopus/48000/4`, + ); + lines.splice( + lines.length - 1, + 0, + `a=fmtp:${payloadType} channel_mapping=0,1,2,3;num_streams=2;coupled_streams=2`, + ); lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`); payloadType = this.#reservePayloadType(payloadTypes); lines[0] += ` ${payloadType}`; - lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} multiopus/48000/5`); - lines.splice(lines.length - 1, 0, `a=fmtp:${payloadType} channel_mapping=0,4,1,2,3;num_streams=3;coupled_streams=2`); + lines.splice( + lines.length - 1, + 0, + `a=rtpmap:${payloadType} multiopus/48000/5`, + ); + lines.splice( + lines.length - 1, + 0, + `a=fmtp:${payloadType} channel_mapping=0,4,1,2,3;num_streams=3;coupled_streams=2`, + ); lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`); payloadType = this.#reservePayloadType(payloadTypes); lines[0] += ` ${payloadType}`; - lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} multiopus/48000/6`); - lines.splice(lines.length - 1, 0, `a=fmtp:${payloadType} channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2`); + lines.splice( + lines.length - 1, + 0, + `a=rtpmap:${payloadType} multiopus/48000/6`, + ); + lines.splice( + lines.length - 1, + 0, + `a=fmtp:${payloadType} channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2`, + ); lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`); payloadType = this.#reservePayloadType(payloadTypes); lines[0] += ` ${payloadType}`; - lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} multiopus/48000/7`); - lines.splice(lines.length - 1, 0, `a=fmtp:${payloadType} channel_mapping=0,4,1,2,3,5,6;num_streams=4;coupled_streams=4`); + lines.splice( + lines.length - 1, + 0, + `a=rtpmap:${payloadType} multiopus/48000/7`, + ); + lines.splice( + lines.length - 1, + 0, + `a=fmtp:${payloadType} channel_mapping=0,4,1,2,3,5,6;num_streams=4;coupled_streams=4`, + ); lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`); payloadType = this.#reservePayloadType(payloadTypes); lines[0] += ` ${payloadType}`; - lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} multiopus/48000/8`); - lines.splice(lines.length - 1, 0, `a=fmtp:${payloadType} channel_mapping=0,6,1,4,5,2,3,7;num_streams=5;coupled_streams=4`); + lines.splice( + lines.length - 1, + 0, + `a=rtpmap:${payloadType} multiopus/48000/8`, + ); + lines.splice( + lines.length - 1, + 0, + `a=fmtp:${payloadType} channel_mapping=0,6,1,4,5,2,3,7;num_streams=5;coupled_streams=4`, + ); lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`); - return lines.join('\r\n'); + return lines.join("\r\n"); } static #enableL16(payloadTypes, section) { - const lines = section.split('\r\n'); + const lines = section.split("\r\n"); let payloadType = this.#reservePayloadType(payloadTypes); lines[0] += ` ${payloadType}`; @@ -263,56 +320,60 @@ class MediaMTXWebRTCReader { lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} L16/48000/2`); lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`); - return lines.join('\r\n'); + return lines.join("\r\n"); } static #enableStereoOpus(section) { - let opusPayloadFormat = ''; - const lines = section.split('\r\n'); + let opusPayloadFormat = ""; + const lines = section.split("\r\n"); for (let i = 0; i < lines.length; i++) { - if (lines[i].startsWith('a=rtpmap:') && lines[i].toLowerCase().includes('opus/')) { - opusPayloadFormat = lines[i].slice('a=rtpmap:'.length).split(' ')[0]; + if ( + lines[i].startsWith("a=rtpmap:") && + lines[i].toLowerCase().includes("opus/") + ) { + opusPayloadFormat = lines[i].slice("a=rtpmap:".length).split(" ")[0]; break; } } - if (opusPayloadFormat === '') { + if (opusPayloadFormat === "") { return section; } for (let i = 0; i < lines.length; i++) { if (lines[i].startsWith(`a=fmtp:${opusPayloadFormat} `)) { - if (!lines[i].includes('stereo')) { - lines[i] += ';stereo=1'; + if (!lines[i].includes("stereo")) { + lines[i] += ";stereo=1"; } - if (!lines[i].includes('sprop-stereo')) { - lines[i] += ';sprop-stereo=1'; + if (!lines[i].includes("sprop-stereo")) { + lines[i] += ";sprop-stereo=1"; } } } - return lines.join('\r\n'); + return lines.join("\r\n"); } static #editOffer(sdp, nonAdvertisedCodecs) { - const sections = sdp.split('m='); + const sections = sdp.split("m="); - const payloadTypes = sections.slice(1) - .map((s) => s.split('\r\n')[0].split(' ').slice(3)) + const payloadTypes = sections + .slice(1) + .map((s) => s.split("\r\n")[0].split(" ").slice(3)) .reduce((prev, cur) => [...prev, ...cur], []); for (let i = 1; i < sections.length; i++) { - if (sections[i].startsWith('audio')) { + if (sections[i].startsWith("audio")) { sections[i] = this.#enableStereoOpus(sections[i]); - if (nonAdvertisedCodecs.includes('pcma/8000/2')) { + if (nonAdvertisedCodecs.includes("pcma/8000/2")) { sections[i] = this.#enableStereoPcmau(payloadTypes, sections[i]); } - if (nonAdvertisedCodecs.includes('multiopus/48000/6')) { + if (nonAdvertisedCodecs.includes("multiopus/48000/6")) { sections[i] = this.#enableMultichannelOpus(payloadTypes, sections[i]); } - if (nonAdvertisedCodecs.includes('L16/48000/2')) { + if (nonAdvertisedCodecs.includes("L16/48000/2")) { sections[i] = this.#enableL16(payloadTypes, sections[i]); } @@ -320,7 +381,7 @@ class MediaMTXWebRTCReader { } } - return sections.join('m='); + return sections.join("m="); } static #generateSdpFragment(od, candidates) { @@ -333,15 +394,13 @@ class MediaMTXWebRTCReader { candidatesByMedia[mid].push(candidate); } - let frag = `a=ice-ufrag:${od.iceUfrag}\r\n` - + `a=ice-pwd:${od.icePwd}\r\n`; + let frag = `a=ice-ufrag:${od.iceUfrag}\r\n` + `a=ice-pwd:${od.icePwd}\r\n`; let mid = 0; for (const media of od.medias) { if (candidatesByMedia[mid] !== undefined) { - frag += `m=${media}\r\n` - + `a=mid:${mid}\r\n`; + frag += `m=${media}\r\n` + `a=mid:${mid}\r\n`; for (const candidate of candidatesByMedia[mid]) { frag += `a=${candidate.candidate}\r\n`; @@ -355,7 +414,7 @@ class MediaMTXWebRTCReader { /** @param {string} err */ #handleError(err) { - if (this.#state === 'running') { + if (this.#state === "running") { if (this.#pc !== null) { this.#pc.close(); this.#pc = null; @@ -365,25 +424,25 @@ class MediaMTXWebRTCReader { if (this.#sessionUrl !== null) { fetch(this.#sessionUrl, { - method: 'DELETE', + method: "DELETE", }); this.#sessionUrl = null; } this.#queuedCandidates = []; - this.#state = 'restarting'; + this.#state = "restarting"; this.#restartTimeout = window.setTimeout(() => { this.#restartTimeout = null; - this.#state = 'running'; + this.#state = "running"; this.#start(); }, this.#retryPause); if (this.#conf.onError !== undefined) { this.#conf.onError(`${err}, retrying in some seconds`); } - } else if (this.#state === 'getting_codecs') { - this.#state = 'failed'; + } else if (this.#state === "getting_codecs") { + this.#state = "failed"; if (this.#conf.onError !== undefined) { this.#conf.onError(err); @@ -392,20 +451,28 @@ class MediaMTXWebRTCReader { } #getNonAdvertisedCodecs() { - Promise.all([ - ['pcma/8000/2'], - ['multiopus/48000/6', 'channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2'], - ['L16/48000/2'], - ] - .map((c) => MediaMTXWebRTCReader.#supportsNonAdvertisedCodec(c[0], c[1]).then((r) => ((r) ? c[0] : false)))) + Promise.all( + [ + ["pcma/8000/2"], + [ + "multiopus/48000/6", + "channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2", + ], + ["L16/48000/2"], + ].map((c) => + MediaMTXWebRTCReader.#supportsNonAdvertisedCodec(c[0], c[1]).then( + (r) => (r ? c[0] : false), + ), + ), + ) .then((c) => c.filter((e) => e !== false)) .then((codecs) => { - if (this.#state !== 'getting_codecs') { - throw new Error('closed'); + if (this.#state !== "getting_codecs") { + throw new Error("closed"); } this.#nonAdvertisedCodecs = codecs; - this.#state = 'running'; + this.#state = "running"; this.#start(); }) .catch((err) => { @@ -424,101 +491,110 @@ class MediaMTXWebRTCReader { } #authHeader() { - if (this.#conf.user !== undefined && this.#conf.user !== '') { + if (this.#conf.user !== undefined && this.#conf.user !== "") { const credentials = btoa(`${this.#conf.user}:${this.#conf.pass}`); - return {'Authorization': `Basic ${credentials}`}; + return { Authorization: `Basic ${credentials}` }; } - if (this.#conf.token !== undefined && this.#conf.token !== '') { - return {'Authorization': `Bearer ${this.#conf.token}`}; + if (this.#conf.token !== undefined && this.#conf.token !== "") { + return { Authorization: `Bearer ${this.#conf.token}` }; } return {}; } #requestICEServers() { return fetch(this.#conf.url, { - method: 'OPTIONS', + method: "OPTIONS", headers: { ...this.#authHeader(), }, - }) - .then((res) => MediaMTXWebRTCReader.#linkToIceServers(res.headers.get('Link'))); + }).then((res) => + MediaMTXWebRTCReader.#linkToIceServers(res.headers.get("Link")), + ); } #setupPeerConnection(iceServers) { - if (this.#state !== 'running') { - throw new Error('closed'); + if (this.#state !== "running") { + throw new Error("closed"); } this.#pc = new RTCPeerConnection({ iceServers, // https://webrtc.org/getting-started/unified-plan-transition-guide - sdpSemantics: 'unified-plan', + sdpSemantics: "unified-plan", }); - const direction = 'recvonly'; - this.#pc.addTransceiver('video', { direction }); - this.#pc.addTransceiver('audio', { direction }); + const direction = "recvonly"; + this.#pc.addTransceiver("video", { direction }); + this.#pc.addTransceiver("audio", { direction }); // using data channels requires creating a data channel locally - this.#pc.createDataChannel(''); + this.#pc.createDataChannel(""); this.#pc.onicecandidate = (evt) => this.#onLocalCandidate(evt); this.#pc.onconnectionstatechange = () => this.#onConnectionState(); this.#pc.ontrack = (evt) => this.#onTrack(evt); this.#pc.ondatachannel = (evt) => this.#onDataChannel(evt); - return this.#pc.createOffer() - .then((offer) => { - offer.sdp = MediaMTXWebRTCReader.#editOffer(offer.sdp, this.#nonAdvertisedCodecs); - this.#offerData = MediaMTXWebRTCReader.#parseOffer(offer.sdp); + return this.#pc.createOffer().then((offer) => { + offer.sdp = MediaMTXWebRTCReader.#editOffer( + offer.sdp, + this.#nonAdvertisedCodecs, + ); + this.#offerData = MediaMTXWebRTCReader.#parseOffer(offer.sdp); - return this.#pc.setLocalDescription(offer) - .then(() => offer.sdp); - }); + return this.#pc.setLocalDescription(offer).then(() => offer.sdp); + }); } #sendOffer(offer) { - if (this.#state !== 'running') { - throw new Error('closed'); + if (this.#state !== "running") { + throw new Error("closed"); } return fetch(this.#conf.url, { - method: 'POST', + method: "POST", headers: { ...this.#authHeader(), - 'Content-Type': 'application/sdp', + "Content-Type": "application/sdp", }, body: offer, - }) - .then((res) => { - switch (res.status) { - case 201: - break; - case 404: - throw new Error('stream not found'); - case 400: - return res.json().then((e) => { throw new Error(e.error); }); - default: - throw new Error(`bad status code ${res.status}`); - } + }).then((res) => { + switch (res.status) { + case 201: + break; + case 404: + throw new Error("stream not found"); + case 400: + return res.json().then((e) => { + throw new Error(e.error); + }); + default: + throw new Error(`bad status code ${res.status}`); + } - this.#sessionUrl = new URL(res.headers.get('location'), this.#conf.url).toString(); + this.#sessionUrl = new URL( + res.headers.get("location"), + this.#conf.url, + ).toString(); - return res.text(); - }); + return res.text(); + }); } #setAnswer(answer) { - if (this.#state !== 'running') { - throw new Error('closed'); + if (this.#state !== "running") { + throw new Error("closed"); } - return this.#pc.setRemoteDescription(new RTCSessionDescription({ - type: 'answer', - sdp: answer, - })) + return this.#pc + .setRemoteDescription( + new RTCSessionDescription({ + type: "answer", + sdp: answer, + }), + ) .then(() => { - if (this.#state !== 'running') { + if (this.#state !== "running") { return; } @@ -530,7 +606,7 @@ class MediaMTXWebRTCReader { } #onLocalCandidate(evt) { - if (this.#state !== 'running') { + if (this.#state !== "running") { return; } @@ -545,19 +621,22 @@ class MediaMTXWebRTCReader { #sendLocalCandidates(candidates) { fetch(this.#sessionUrl, { - method: 'PATCH', + method: "PATCH", headers: { - 'Content-Type': 'application/trickle-ice-sdpfrag', - 'If-Match': '*', + "Content-Type": "application/trickle-ice-sdpfrag", + "If-Match": "*", }, - body: MediaMTXWebRTCReader.#generateSdpFragment(this.#offerData, candidates), + body: MediaMTXWebRTCReader.#generateSdpFragment( + this.#offerData, + candidates, + ), }) .then((res) => { switch (res.status) { case 204: break; case 404: - throw new Error('stream not found'); + throw new Error("stream not found"); default: throw new Error(`bad status code ${res.status}`); } @@ -568,7 +647,7 @@ class MediaMTXWebRTCReader { } #onConnectionState() { - if (this.#state !== 'running') { + if (this.#state !== "running") { return; } @@ -576,10 +655,11 @@ class MediaMTXWebRTCReader { // the close() method being called at all. // It happens when the other peer sends a termination // message like a DTLS CloseNotify. - if (this.#pc.connectionState === 'failed' - || this.#pc.connectionState === 'closed' + if ( + this.#pc.connectionState === "failed" || + this.#pc.connectionState === "closed" ) { - this.#handleError('peer connection closed'); + this.#handleError("peer connection closed"); } } diff --git a/mediamtx.yml b/mediamtx.yml index 973cb99d..156988a7 100644 --- a/mediamtx.yml +++ b/mediamtx.yml @@ -62,34 +62,34 @@ authMethod: internal authInternalUsers: # Default unprivileged user. # Username. 'any' means any user, including anonymous ones. -- user: any - # Password. Not used in case of 'any' user. - pass: - # IPs or networks allowed to use this user. An empty list means any IP. - ips: [] - # Permissions. - permissions: - # Available actions are: publish, read, playback, api, metrics, pprof. - - action: publish - # Paths can be set to further restrict access to a specific path. - # An empty path means any path. - # Regular expressions can be used by using a tilde as prefix. - path: - - action: read - path: - - action: playback - path: + - user: any + # Password. Not used in case of 'any' user. + pass: + # IPs or networks allowed to use this user. An empty list means any IP. + ips: [] + # Permissions. + permissions: + # Available actions are: publish, read, playback, api, metrics, pprof. + - action: publish + # Paths can be set to further restrict access to a specific path. + # An empty path means any path. + # Regular expressions can be used by using a tilde as prefix. + path: + - action: read + path: + - action: playback + path: - # Default administrator. - # This allows to use API, metrics and PPROF without authentication, - # if the IP is localhost. -- user: any - pass: - ips: ['127.0.0.1', '::1'] - permissions: - - action: api - - action: metrics - - action: pprof + # Default administrator. + # This allows to use API, metrics and PPROF without authentication, + # if the IP is localhost. + - user: any + pass: + ips: ["127.0.0.1", "::1"] + permissions: + - action: api + - action: metrics + - action: pprof # HTTP-based authentication. # URL called to perform authentication. Every time a user wants @@ -118,9 +118,9 @@ authHTTPFingerprint: # Actions to exclude from HTTP-based authentication. # Format is the same as the one of user permissions. authHTTPExclude: -- action: api -- action: metrics -- action: pprof + - action: api + - action: metrics + - action: pprof # JWT-based authentication. # Users have to log in through an external identity server and obtain a JWT. @@ -172,7 +172,7 @@ apiServerKey: server.key apiServerCert: server.crt # Allowed CORS origins. # Supports wildcards: ['http://*.example.com'] -apiAllowOrigins: ['*'] +apiAllowOrigins: ["*"] # IPs or CIDRs of proxies placed before the HTTP server. # These proxies can use the X-Forwarded-For header to set the real IP of clients, # and the X-Forwarded-Proto header to set the original protocol. @@ -196,7 +196,7 @@ metricsServerKey: server.key metricsServerCert: server.crt # Allowed CORS origins. # Supports wildcards: ['http://*.example.com'] -metricsAllowOrigins: ['*'] +metricsAllowOrigins: ["*"] # IPs or CIDRs of proxies placed before the HTTP server. # These proxies can use the X-Forwarded-For header to set the real IP of clients, # and the X-Forwarded-Proto header to set the original protocol. @@ -220,7 +220,7 @@ pprofServerKey: server.key pprofServerCert: server.crt # Allowed CORS origins. # Supports wildcards: ['http://*.example.com'] -pprofAllowOrigins: ['*'] +pprofAllowOrigins: ["*"] # IPs or CIDRs of proxies placed before the HTTP server. # These proxies can use the X-Forwarded-For header to set the real IP of clients, # and the X-Forwarded-Proto header to set the original protocol. @@ -244,7 +244,7 @@ playbackServerKey: server.key playbackServerCert: server.crt # Allowed CORS origins. # Supports wildcards: ['http://*.example.com'] -playbackAllowOrigins: ['*'] +playbackAllowOrigins: ["*"] # IPs or CIDRs of proxies placed before the HTTP server. # These proxies can use the X-Forwarded-For header to set the real IP of clients, # and the X-Forwarded-Proto header to set the original protocol. @@ -332,7 +332,7 @@ hlsServerKey: server.key hlsServerCert: server.crt # Allowed CORS origins. # Supports wildcards: ['http://*.example.com'] -hlsAllowOrigins: ['*'] +hlsAllowOrigins: ["*"] # IPs or CIDRs of proxies placed before the HLS server. # If the server receives a request from one of these entries, IP in logs # will be taken from the X-Forwarded-For header. @@ -367,14 +367,14 @@ hlsSegmentMaxSize: 50M # Directory in which to save segments and non-low-latency playlists. # This has two purposes: offloading RAM and creating a self-consistent directory # that can be served by a CDN. -hlsDirectory: '' +hlsDirectory: "" # The muxer will be closed when there are no # reader requests and this amount of time has passed. hlsMuxerCloseAfter: 60s # Secret to identify requests coming from a CDN. # The CDN must insert this secret in every request in the # 'Authorization: Bearer' header. -hlsCDNSecret: '' +hlsCDNSecret: "" ############################################### # Global settings -> WebRTC server @@ -396,7 +396,7 @@ webrtcServerKey: server.key webrtcServerCert: server.crt # Allowed CORS origins. # Supports wildcards: ['http://*.example.com'] -webrtcAllowOrigins: ['*'] +webrtcAllowOrigins: ["*"] # IPs or CIDRs of proxies placed before the WebRTC server. # If the server receives a request from one of these entries, IP in logs # will be taken from the X-Forwarded-For header. @@ -407,7 +407,7 @@ webrtcLocalUDPAddress: :8189 # Address of a local TCP listener that will receive connections. # This is disabled by default since TCP is less efficient than UDP and # introduces a progressive delay when network is congested. -webrtcLocalTCPAddress: '' +webrtcLocalTCPAddress: "" # WebRTC clients need to know the IP of the server. # Gather IPs from interfaces and send them to clients. webrtcIPsFromInterfaces: true @@ -447,7 +447,6 @@ srtAddress: :8890 # Settings in "pathDefaults" are applied anywhere, # unless they are overridden in "paths". pathDefaults: - ############################################### # Default path settings -> General @@ -513,7 +512,7 @@ pathDefaults: # # in case of G711, muLaw must be provided too. # muLaw: false # An MP4 file can be used instead of the default offline segment. - alwaysAvailableFile: '' + alwaysAvailableFile: "" ############################################### # Default path settings -> Record @@ -586,7 +585,7 @@ pathDefaults: # Default path settings -> WebRTC / WHEP source (when source is WHEP) # Token to insert in the Authorization: Bearer header. - whepBearerToken: '' + whepBearerToken: "" # Maximum time to gather STUN candidates. whepSTUNGatherTimeout: 5s # Time to wait for the WebRTC handshake to complete. @@ -675,7 +674,7 @@ pathDefaults: rpiCameraTextOverlayEnable: false # Text that is printed on each frame. # format is the one of the strftime() function. - rpiCameraTextOverlay: '%Y-%m-%d %H:%M:%S - MediaMTX' + rpiCameraTextOverlay: "%Y-%m-%d %H:%M:%S - MediaMTX" # Codec (auto, hardwareH264, softwareH264 or mjpeg). # When is "auto" and stream is primary, it defaults to hardwareH264 (if available) or softwareH264. # When is "auto" and stream is secondary, it defaults to mjpeg. @@ -687,11 +686,11 @@ pathDefaults: # Hardware H264 profile (baseline, main or high) (when codec is hardwareH264). rpiCameraHardwareH264Profile: main # Hardware H264 level (4.0, 4.1 or 4.2) (when codec is hardwareH264). - rpiCameraHardwareH264Level: '4.1' + rpiCameraHardwareH264Level: "4.1" # Software H264 profile (baseline, main or high) (when codec is softwareH264). rpiCameraSoftwareH264Profile: baseline # Software H264 level (4.0, 4.1 or 4.2) (when codec is softwareH264). - rpiCameraSoftwareH264Level: '4.1' + rpiCameraSoftwareH264Level: "4.1" # M-JPEG JPEG quality (when codec is mjpeg). rpiCameraMJPEGQuality: 60 diff --git a/scripts/format.mk b/scripts/format.mk index e27f77a0..b947b8b7 100644 --- a/scripts/format.mk +++ b/scripts/format.mk @@ -15,9 +15,9 @@ format-go: docker run --rm -it -v "$(shell pwd):/s" -w /s temp \ sh -c "gofumpt -l -w ." -format-docs: +format-other: echo "$$DOCKERFILE_PRETTIER" | docker build . -f - -t temp - docker run --rm -v "$(shell pwd)/docs:/s" -w /s temp \ + docker run --rm -v "$(shell pwd)/:/s" -w /s temp \ sh -c "prettier --write ." -format: format-go format-docs +format: format-go format-other diff --git a/scripts/lint.mk b/scripts/lint.mk index 0dad5bcf..0212469b 100644 --- a/scripts/lint.mk +++ b/scripts/lint.mk @@ -24,14 +24,14 @@ lint-docslinks: lint-docsorder: go test -v -tags enable_linters ./internal/linters/docsorder -lint-docs: - echo "$$DOCKERFILE_PRETTIER" | docker build . -f - -t temp - docker run --rm -v "$(shell pwd)/docs:/s" -w /s temp \ - sh -c "prettier --check ." - lint-api-docs: echo "$$DOCKERFILE_API_DOCS_LINT" | docker build . -f - -t temp docker run --rm -v "$(shell pwd)/api:/s" -w /s temp \ sh -c "openapi lint openapi.yaml" -lint: lint-go lint-go-mod lint-conf lint-go2api lint-docslinks lint-docsorder lint-docs lint-api-docs +lint-other: + echo "$$DOCKERFILE_PRETTIER" | docker build . -f - -t temp + docker run --rm -v "$(shell pwd)/:/s" -w /s temp \ + sh -c "prettier --check ." + +lint: lint-go lint-go-mod lint-conf lint-go2api lint-docslinks lint-docsorder lint-api-docs lint-other