[
  {
    "path": ".dockerignore",
    "content": "# do not add .git, since it is needed to extract the tag\n# do not add /binaries, since it is needed by Docker images\n/tmp\n/coverage*.txt\n/api/*.html\n/internal/core/VERSION\n/internal/servers/hls/hls.min.js\n/internal/staticsources/rpicamera/mtxrpicam_*/\n"
  },
  {
    "path": ".github/DISCUSSION_TEMPLATE/questions.yml",
    "content": "body:\n  - type: markdown\n    attributes:\n      value: |\n        * Please create a discussion FOR EACH question. Do not ask for multiple questions all at once, otherwise they'll probably never get all answered.\n        * If you are asking for help because you're having trouble doing something, provide enough informations to replicate the problem. In particular, include in the question:\n\n          * MediaMTX version\n          * precise instructions on how to replicate the problem\n          * MediaMTX configuration\n          * MediaMTX logs with setting `logLevel` set to `debug`\n\n        * If you are asking for help and you think MediaMTX is misbehaving, open a bug report, NOT a discussion.\n\n  - type: textarea\n    attributes:\n      label: Question\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug.yml",
    "content": "name: Bug report\ndescription: Report a bug\n\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        To increase the chance of your bug getting fixed, open an issue FOR EACH bug. Do not report multiple problems in a single issue, otherwise they'll probably never get all fixed.\n\n  - type: input\n    id: version\n    attributes:\n      label: Which version are you using?\n      description: MediaMTX version or commit\n    validations:\n      required: true\n\n  - type: dropdown\n    id: os\n    attributes:\n      label: Which operating system are you using?\n      multiple: true\n      options:\n        - Linux amd64 standard\n        - Linux amd64 Docker\n        - Linux arm64 standard\n        - Linux arm64 Docker\n        - Linux arm7 standard\n        - Linux arm7 Docker\n        - Linux arm6 standard\n        - Linux arm6 Docker\n        - Windows amd64 standard\n        - Windows amd64 Docker (WSL backend)\n        - macOS amd64 standard\n        - macOS amd64 Docker\n        - Other (please describe)\n    validations:\n      required: true\n\n  - type: textarea\n    id: description\n    attributes:\n      label: Describe the issue\n    validations:\n      required: true\n\n  - type: textarea\n    id: replica\n    attributes:\n      label: Describe how to replicate the issue\n      description: |\n        The maintainers must be able to REPLICATE your issue to solve it - therefore, describe in a very detailed way how to replicate it.\n      value: |\n        1. start the MediaMTX\n        2. publish with ...\n        3. read with ...\n    validations:\n      required: true\n\n  - type: textarea\n    id: conf\n    attributes:\n      label: MediaMTX configuration\n      description: |\n        MediaMTX configuration is often required to replicate the issue.\n      placeholder: Paste the configuration file here\n      render: yaml\n\n  - type: textarea\n    id: logs\n    attributes:\n      label: MediaMTX logs\n      description: |\n        MediaMTX logs are often useful to identify the issue. If you think this is the case, set 'logLevel' to 'debug' and attach logs.\n      placeholder: Paste or drag the log file here\n\n  - type: textarea\n    id: network\n    attributes:\n      label: Packet dump\n      description: |\n        If the bug arises when using MediaMTX with external hardware or software, the most helpful information you can provide is a packet dump, that can be generated in this way:\n\n        1. In mediamtx.yml, set 'dumpPackets' to 'true'\n        2. Start the server and replicate the issue\n        3. Stop the server, find the generated .pcapng files in the current directory\n        4. Attach the pcapng files by dragging them here\n\n      placeholder: Attach the pcapng files by dragging them here\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\n\ncontact_links:\n  - name: Question\n    url: https://github.com/bluenviron/mediamtx/discussions/new?category=questions\n    about: Ask the community for help\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature.yml",
    "content": "name: Feature request\ndescription: Share ideas for new features\n\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        Please create a request FOR EACH feature. Do not report multiple features in a single request, otherwise they'll probably never get all implemented.\n\n  - type: textarea\n    id: description\n    attributes:\n      label: Describe the feature\n    validations:\n      required: true\n"
  },
  {
    "path": ".github/dependabot.yml",
    "content": "version: 2\nupdates:\n\n  - package-ecosystem: \"gomod\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n\n  - package-ecosystem: \"github-actions\"\n    directory: \"/\"\n    schedule:\n      interval: \"daily\"\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "content": "name: lint\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main ]\n\njobs:\n  go:\n    runs-on: ubuntu-22.04\n\n    steps:\n    - uses: actions/checkout@v6\n      with:\n        fetch-depth: 0\n\n    - uses: actions/setup-go@v6\n      with:\n        go-version: \"1.25\"\n\n    - run: go generate ./...\n\n    - uses: golangci/golangci-lint-action@v9\n      with:\n        version: v2.11.3\n\n  go_mod:\n    runs-on: ubuntu-22.04\n\n    steps:\n    - uses: actions/checkout@v6\n\n    - uses: actions/setup-go@v6\n      with:\n        go-version: \"1.25\"\n\n    - run: make lint-go-mod\n\n  conf:\n    runs-on: ubuntu-22.04\n\n    steps:\n    - uses: actions/checkout@v6\n\n    - uses: actions/setup-go@v6\n      with:\n        go-version: \"1.25\"\n\n    - run: make lint-conf\n\n  go2api:\n    runs-on: ubuntu-22.04\n\n    steps:\n    - uses: actions/checkout@v6\n\n    - uses: actions/setup-go@v6\n      with:\n        go-version: \"1.25\"\n\n    - run: make lint-go2api\n\n  docs:\n    runs-on: ubuntu-22.04\n\n    steps:\n    - uses: actions/checkout@v6\n\n    - run: make lint-docs\n\n  api_docs:\n    runs-on: ubuntu-22.04\n\n    steps:\n    - uses: actions/checkout@v6\n\n    - run: make lint-api-docs\n"
  },
  {
    "path": ".github/workflows/nightly_binaries.yml",
    "content": "name: nightly_binaries\n\non:\n  workflow_dispatch:\n\njobs:\n  nightly_binaries:\n    runs-on: ubuntu-22.04\n\n    steps:\n    - uses: actions/checkout@v6\n      with:\n        fetch-depth: 0\n\n    - run: make binaries\n\n    - uses: actions/upload-artifact@v7\n      with:\n        name: binaries\n        path: binaries\n"
  },
  {
    "path": ".github/workflows/release.yml",
    "content": "name: release\n\non:\n  push:\n    tags:\n    - 'v*'\n\npermissions:\n  id-token: write\n  attestations: write\n  artifact-metadata: write\n  contents: write\n  issues: write\n\njobs:\n  binaries:\n    runs-on: ubuntu-22.04\n\n    steps:\n    - uses: actions/checkout@v6\n\n    - run: make binaries\n\n    - run: cd binaries && sha256sum -b * > checksums.sha256\n\n    - uses: actions/attest@v4\n      with:\n        subject-path: '${{ github.workspace }}/binaries/*'\n\n    - uses: actions/upload-artifact@v7\n      with:\n        name: binaries\n        path: binaries\n\n  github_release:\n    needs: binaries\n    runs-on: ubuntu-22.04\n\n    steps:\n    - uses: actions/download-artifact@v8\n      with:\n        name: binaries\n        path: binaries\n\n    - uses: actions/github-script@v8\n      with:\n        github-token: ${{ secrets.GITHUB_TOKEN }}\n        script: |\n          const fs = require('fs').promises;\n          const { repo: { owner, repo } } = context;\n\n          const currentRelease = context.ref.split('/')[2];\n\n          let body = `## New major features\\n`\n            + `\\n`\n            + `TODO\\n`\n            + `\\n`\n            + `## Fixes and improvements\\n`\n            + `\\n`\n            + `TODO\\n`\n            + `\\n`\n            + `## Security\\n`\n            + `\\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            + `\\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`\n            + '```\\n'\n            + `ls mediamtx_* | xargs -L1 gh attestation verify --repo bluenviron/mediamtx\\n`\n            + '```\\n'\n            + `\\n`\n            + 'You can verify checksums of binaries by downloading `checksums.sha256` and running:\\n'\n            + `\\n`\n            + '```\\n'\n            + `cat checksums.sha256 | grep \"$(ls mediamtx_*)\" | sha256sum --check\\n`\n            + '```\\n'\n            + `\\n`;\n\n          const res = await github.rest.repos.createRelease({\n            owner,\n            repo,\n            tag_name: currentRelease,\n            name: currentRelease,\n            body,\n          });\n          const release_id = res.data.id;\n\n          for (const name of await fs.readdir('./binaries/')) {\n            await github.rest.repos.uploadReleaseAsset({\n              owner,\n              repo,\n              release_id,\n              name,\n              data: await fs.readFile(`./binaries/${name}`),\n            });\n          }\n\n  github_notify_issues:\n    needs: github_release\n    runs-on: ubuntu-22.04\n\n    steps:\n    - uses: actions/github-script@v8\n      with:\n        github-token: ${{ secrets.GITHUB_TOKEN }}\n        script: |\n          const { repo: { owner, repo } } = context;\n\n          const tags = await github.rest.repos.listTags({\n            owner,\n            repo,\n          });\n\n          const curTag = tags.data[0];\n          const prevTag = tags.data[1];\n\n          const diff = await github.rest.repos.compareCommitsWithBasehead({\n            owner,\n            repo,\n            basehead: `${prevTag.commit.sha}...${curTag.commit.sha}`,\n          });\n\n          const issues = {};\n\n          for (const commit of diff.data.commits) {\n            for (const match of commit.commit.message.matchAll(/(^| |\\()#([0-9]+)( |\\)|$)/g)) {\n              issues[match[2]] = 1;\n            }\n          }\n\n          for (const issue in issues) {\n            try {\n              await github.rest.issues.createComment({\n                owner,\n                repo,\n                issue_number: parseInt(issue),\n                body: `This issue is mentioned in release ${curTag.name} 🚀\\n`\n                  + `Check out the entire changelog by [clicking here](https://github.com/${owner}/${repo}/releases/tag/${curTag.name})`,\n              });\n            } catch (exc) {\n              console.error(exc.toString());\n            }\n          }\n\n  dockerhub:\n    needs: binaries\n    runs-on: ubuntu-22.04\n\n    steps:\n    - uses: actions/checkout@v6\n\n    - uses: actions/download-artifact@v8\n      with:\n        name: binaries\n        path: binaries\n\n    - run: make dockerhub\n      env:\n        DOCKER_USER: ${{ secrets.DOCKER_USER }}\n        DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: test\n\non:\n  push:\n    branches: [ main ]\n  pull_request:\n    branches: [ main ]\n\njobs:\n  test_64:\n    runs-on: ubuntu-22.04\n\n    steps:\n    - uses: actions/checkout@v6\n      with:\n        fetch-depth: 0\n\n    - run: make test\n\n    - uses: codecov/codecov-action@v5\n      with:\n        token: ${{ secrets.CODECOV_TOKEN }}\n\n  test_32:\n    runs-on: ubuntu-22.04\n\n    steps:\n    - uses: actions/checkout@v6\n      with:\n        fetch-depth: 0\n\n    - run: make test-32\n\n  test_e2e:\n    runs-on: ubuntu-22.04\n\n    steps:\n    - uses: actions/checkout@v6\n      with:\n        fetch-depth: 0\n\n    - uses: actions/setup-go@v6\n      with:\n        go-version: \"1.25\"\n\n    - run: make test-e2e-nodocker\n"
  },
  {
    "path": ".gitignore",
    "content": "/tmp\n/binaries\n/coverage*.txt\n/api/*.html\n/internal/core/VERSION\n/internal/servers/hls/hls.min.js\n/internal/staticsources/rpicamera/mtxrpicam_*/\n"
  },
  {
    "path": ".golangci.yml",
    "content": "version: \"2\"\n\nlinters:\n  enable:\n  - asciicheck\n  - bidichk\n  - bodyclose\n  - copyloopvar\n  - dupl\n  - errorlint\n  - gochecknoinits\n  - gocritic\n  - lll\n  - misspell\n  - modernize\n  - nilerr\n  - prealloc\n  - predeclared\n  - reassign\n  - revive\n  - usestdlibvars\n  - unconvert\n  - tparallel\n  - wastedassign\n  - whitespace\n\n  settings:\n    errcheck:\n      exclude-functions:\n      - io.Copy\n      - (io.Closer).Close\n      - (io.Writer).Write\n      - (hash.Hash).Write\n      - (net.Conn).Close\n      - (net.Conn).SetReadDeadline\n      - (net.Conn).SetWriteDeadline\n      - (*net.TCPConn).SetKeepAlive\n      - (*net.TCPConn).SetKeepAlivePeriod\n      - (*net.TCPConn).SetNoDelay\n      - (net.Listener).Close\n      - (net.PacketConn).Close\n      - (net.PacketConn).SetReadDeadline\n      - (net.PacketConn).SetWriteDeadline\n      - (net/http.ResponseWriter).Write\n      - (*net/http.Server).Serve\n      - (*net/http.Server).ServeTLS\n      - (*net/http.Server).Shutdown\n      - os.Chdir\n      - os.Mkdir\n      - os.MkdirAll\n      - os.Remove\n      - os.RemoveAll\n      - os.Setenv\n      - os.Unsetenv\n      - (*os.File).WriteString\n      - (*os.File).Close\n      - (github.com/datarhei/gosrt.Conn).Close\n      - (github.com/datarhei/gosrt.Conn).SetReadDeadline\n      - (github.com/datarhei/gosrt.Conn).SetWriteDeadline\n      - (*github.com/bluenviron/gortsplib/v5.Client).Close\n      - (*github.com/bluenviron/gortsplib/v5.Server).Close\n      - (*github.com/bluenviron/gortsplib/v5.ServerSession).Close\n      - (*github.com/bluenviron/gortsplib/v5.ServerStream).Close\n      - (*github.com/bluenviron/gortsplib/v5.ServerConn).Close\n\n    govet:\n      enable-all: true\n      disable:\n      - fieldalignment\n      - reflectvaluecompare\n      settings:\n        shadow:\n          strict: true\n\n    modernize:\n      disable:\n      - reflecttypefor\n      - stringsbuilder\n      - testingcontext\n\nformatters:\n  enable:\n  - gofmt\n  - gofumpt\n  - goimports\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2019 aler9\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "BASE_IMAGE = golang:1.25-alpine3.22\nGOLANGCI_LINT_IMAGE = golangci/golangci-lint:v2.11.3\nNODE_IMAGE = node:20-alpine3.22\n\n.PHONY: $(shell ls)\n\nhelp:\n\t@echo \"usage: make [action]\"\n\t@echo \"\"\n\t@echo \"available actions:\"\n\t@echo \"\"\n\t@echo \"  format           format code\"\n\t@echo \"  test             run tests\"\n\t@echo \"  test-32          run tests on a 32-bit system\"\n\t@echo \"  test-e2e         run end-to-end tests\"\n\t@echo \"  lint             run linters\"\n\t@echo \"  binaries         build binaries for all supported platforms\"\n\t@echo \"  dockerhub        build and push images to Docker Hub\"\n\t@echo \"\"\n\nblank :=\ndefine NL\n\n$(blank)\nendef\n\ninclude scripts/*.mk\n"
  },
  {
    "path": "README.md",
    "content": "<h1 align=\"center\">\n  <a href=\"https://mediamtx.org\">\n    <img src=\"logo.png\" alt=\"MediaMTX\">\n  </a>\n\n  <br>\n  <br>\n\n  [![Website](https://img.shields.io/badge/website-mediamtx.org-1c94b5)](https://mediamtx.org)\n  [![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)\n  [![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)\n  [![CodeCov](https://codecov.io/gh/bluenviron/mediamtx/branch/main/graph/badge.svg)](https://app.codecov.io/gh/bluenviron/mediamtx/tree/main)\n  [![Release](https://img.shields.io/github/v/release/bluenviron/mediamtx)](https://github.com/bluenviron/mediamtx/releases)\n  [![Docker Hub](https://img.shields.io/badge/docker-bluenviron/mediamtx-blue)](https://hub.docker.com/r/bluenviron/mediamtx)\n</h1>\n\n<br>\n\n_MediaMTX_ is a ready-to-use and zero-dependency real-time media server and media proxy that allows to publish, read, proxy, record and playback video and audio streams. It has been conceived as a \"media router\" that routes media streams from one end to the other, with a focus on efficiency and portability.\n\n<div align=\"center\">\n\n  |[Install](https://mediamtx.org/docs/kickoff/install)|[Documentation](https://mediamtx.org/docs/kickoff/introduction)|\n  |-|-|\n\n</div>\n\n<h3>Features</h3>\n\n- [Publish](https://mediamtx.org/docs/usage/publish) live streams to the server with SRT, WebRTC, RTSP, RTMP, HLS, MPEG-TS, RTP, using FFmpeg, GStreamer, OBS Studio, Python , Golang, Unity, web browsers, Raspberry Pi Cameras and more.\n- [Read](https://mediamtx.org/docs/usage/read) live streams from the server with SRT, WebRTC, RTSP, RTMP, HLS, using FFmpeg, GStreamer, VLC, OBS Studio, Python , Golang, Unity, web browsers and more.\n- Streams are automatically converted from a protocol to another\n- Serve several streams at once in separate paths\n- Reload the configuration without disconnecting existing clients (hot reloading)\n- [Serve always-available streams](https://mediamtx.org/docs/usage/always-available) even when the publisher is offline\n- [Record](https://mediamtx.org/docs/usage/record) streams to disk in fMP4 or MPEG-TS format\n- [Playback](https://mediamtx.org/docs/usage/playback) recorded streams\n- [Authenticate](https://mediamtx.org/docs/usage/authentication) users with internal, HTTP or JWT authentication\n- [Forward](https://mediamtx.org/docs/usage/forward) streams to other servers\n- [Proxy](https://mediamtx.org/docs/usage/proxy) requests to other servers\n- [Control](https://mediamtx.org/docs/usage/control-api) the server through the Control API\n- [Extract metrics](https://mediamtx.org/docs/usage/metrics) from the server in a Prometheus-compatible format\n- [Monitor performance](https://mediamtx.org/docs/usage/performance) to investigate CPU and RAM consumption\n- [Run hooks](https://mediamtx.org/docs/usage/hooks) (external commands) when clients connect, disconnect, read or publish streams\n- Compatible with Linux, Windows and macOS, does not require any dependency or interpreter, it's a single executable\n- ...and many [others](https://mediamtx.org/docs/kickoff/introduction).\n"
  },
  {
    "path": "SECURITY.md",
    "content": "# Security\n\nCheck the [Security page](https://mediamtx.org/docs/other/security) on the website.\n"
  },
  {
    "path": "api/.redocly.yaml",
    "content": "extends:\n  - recommended\n\nrules:\n  operation-4xx-response: off\n"
  },
  {
    "path": "api/openapi.yaml",
    "content": "openapi: 3.0.0\n\ninfo:\n  version: 1.0.0\n  title: MediaMTX API\n  description: API of MediaMTX, a server and proxy that supports various protocols.\n  license:\n    name: MIT\n    url: https://opensource.org/licenses/MIT\n\nservers:\n  - url: http://localhost:9997\n\nsecurity: []\n\ncomponents:\n  schemas:\n    OKStatus:\n      type: string\n      enum: [ok]\n\n    ErrorStatus:\n      type: string\n      enum: [error]\n\n    PathSourceType:\n      type: string\n      enum:\n      - hlsSource\n      - redirect\n      - rpiCameraSource\n      - rtmpConn\n      - rtmpsConn\n      - rtmpSource\n      - rtspSession\n      - rtspSource\n      - rtspsSession\n      - srtConn\n      - srtSource\n      - mpegtsSource\n      - rtpSource\n      - webRTCSession\n      - webRTCSource\n\n    PathReaderType:\n      type: string\n      enum:\n      - hlsMuxer\n      - rpiCameraSecondary\n      - rtmpConn\n      - rtmpsConn\n      - rtspConn\n      - rtspSession\n      - rtspsConn\n      - rtspsSession\n      - srtConn\n      - webRTCSession\n\n    PathTrackCodec:\n      type: string\n      enum:\n      - AV1\n      - VP9\n      - VP8\n      - H265\n      - H264\n      - MPEG-4 Video\n      - MPEG-1 Video\n      - MJPEG\n      - Opus\n      - Vorbis\n      - MPEG-4 Audio\n      - MPEG-4 Audio LATM\n      - MPEG-1 Audio\n      - AC3\n      - Speex\n      - G726\n      - G722\n      - G711\n      - LPCM\n      - MPEG-TS\n      - KLV\n      - Generic\n\n    AlwaysAvailableTrackCodec:\n      type: string\n      enum:\n      - AV1\n      - VP9\n      - H265\n      - H264\n      - MPEG4Audio\n      - Opus\n      - G711\n      - LPCM\n\n    AuthAction:\n      type: string\n      enum:\n      - publish\n      - read\n      - playback\n      - api\n      - metrics\n      - pprof\n\n    AuthMethod:\n      type: string\n      enum: [internal, http, jwt]\n\n    Encryption:\n      type: string\n      enum: [no, optional, strict]\n\n    HLSVariant:\n      type: string\n      enum: [mpegts, fmp4, lowLatency]\n\n    LogDestination:\n      type: string\n      enum: [stdout, file, syslog]\n\n    LogLevel:\n      type: string\n      enum: [error, warn, info, debug]\n\n    RecordFormat:\n      type: string\n      enum: [fmp4, mpegts]\n\n    RTSPAuthMethod:\n      type: string\n      enum: [basic, digest]\n\n    RTSPRangeType:\n      type: string\n      enum: ['', clock, npt, smpte]\n\n    RTSPTransport:\n      type: string\n      enum: [udp, multicast, tcp, automatic]\n\n    RTMPConnState:\n      type: string\n      enum: [idle, read, publish]\n\n    RTSPSessionState:\n      type: string\n      enum: [idle, read, publish]\n\n    SRTConnState:\n      type: string\n      enum: [idle, read, publish]\n\n    WebRTCSessionState:\n      type: string\n      enum: [read, publish]\n\n    OK:\n      type: object\n      properties:\n        status:\n          $ref: '#/components/schemas/OKStatus'\n\n    Error:\n      type: object\n      properties:\n        status:\n          $ref: '#/components/schemas/ErrorStatus'\n        error:\n          type: string\n\n    Info:\n      type: object\n      properties:\n        version:\n          type: string\n        started:\n          type: string\n\n    AuthInternalUser:\n      type: object\n      properties:\n        user:\n          type: string\n        pass:\n          type: string\n        ips:\n          type: array\n          items:\n            type: string\n        permissions:\n          type: array\n          items:\n            $ref: '#/components/schemas/AuthInternalUserPermission'\n\n    AuthInternalUserPermission:\n      type: object\n      properties:\n        action:\n          $ref: '#/components/schemas/AuthAction'\n        path:\n          type: string\n\n    GlobalConf:\n      type: object\n      properties:\n        # General\n        logLevel:\n          $ref: '#/components/schemas/LogLevel'\n        logDestinations:\n          type: array\n          items:\n            $ref: '#/components/schemas/LogDestination'\n        logStructured:\n          type: boolean\n        logFile:\n          type: string\n        sysLogPrefix:\n          type: string\n        dumpPackets:\n          type: boolean\n        readTimeout:\n          type: string\n        writeTimeout:\n          type: string\n        readBufferCount:\n          type: integer\n          format: int64\n          nullable: true\n          deprecated: true\n        writeQueueSize:\n          type: integer\n          format: int64\n        udpMaxPayloadSize:\n          type: integer\n          format: int64\n        udpReadBufferSize:\n          type: integer\n          format: uint64\n        runOnConnect:\n          type: string\n        runOnConnectRestart:\n          type: boolean\n        runOnDisconnect:\n          type: string\n\n        # Authentication\n        authMethod:\n          $ref: '#/components/schemas/AuthMethod'\n        authInternalUsers:\n          type: array\n          items:\n            $ref: '#/components/schemas/AuthInternalUser'\n        authHTTPAddress:\n          type: string\n        externalAuthenticationURL:\n          type: string\n          nullable: true\n          deprecated: true\n        authHTTPFingerprint:\n          type: string\n        authHTTPExclude:\n          type: array\n          items:\n            $ref: '#/components/schemas/AuthInternalUserPermission'\n        authJWTJWKS:\n          type: string\n        authJWTJWKSFingerprint:\n          type: string\n        authJWTClaimKey:\n          type: string\n        authJWTIssuer:\n          type: string\n        authJWTAudience:\n          type: string\n        authJWTExclude:\n          type: array\n          items:\n            $ref: '#/components/schemas/AuthInternalUserPermission'\n        authJWTInHTTPQuery:\n          type: boolean\n\n        # Control API\n        api:\n          type: boolean\n        apiAddress:\n          type: string\n        apiEncryption:\n          type: boolean\n        apiServerKey:\n          type: string\n        apiServerCert:\n          type: string\n        apiAllowOrigin:\n          type: string\n          nullable: true\n          deprecated: true\n        apiAllowOrigins:\n          type: array\n          items:\n            type: string\n        apiTrustedProxies:\n          type: array\n          items:\n            type: string\n\n        # Metrics\n        metrics:\n          type: boolean\n        metricsAddress:\n          type: string\n        metricsEncryption:\n          type: boolean\n        metricsServerKey:\n          type: string\n        metricsServerCert:\n          type: string\n        metricsAllowOrigin:\n          type: string\n          nullable: true\n          deprecated: true\n        metricsAllowOrigins:\n          type: array\n          items:\n            type: string\n        metricsTrustedProxies:\n          type: array\n          items:\n            type: string\n\n        # PPROF\n        pprof:\n          type: boolean\n        pprofAddress:\n          type: string\n        pprofEncryption:\n          type: boolean\n        pprofServerKey:\n          type: string\n        pprofServerCert:\n          type: string\n        pprofAllowOrigin:\n          type: string\n          nullable: true\n          deprecated: true\n        pprofAllowOrigins:\n          type: array\n          items:\n            type: string\n        pprofTrustedProxies:\n          type: array\n          items:\n            type: string\n\n        # Playback server\n        playback:\n          type: boolean\n        playbackAddress:\n          type: string\n        playbackEncryption:\n          type: boolean\n        playbackServerKey:\n          type: string\n        playbackServerCert:\n          type: string\n        playbackAllowOrigin:\n          type: string\n          nullable: true\n          deprecated: true\n        playbackAllowOrigins:\n          type: array\n          items:\n            type: string\n        playbackTrustedProxies:\n          type: array\n          items:\n            type: string\n\n        # RTSP server\n        rtsp:\n          type: boolean\n        rtspDisable:\n          type: boolean\n          nullable: true\n          deprecated: true\n        protocols:\n          type: array\n          nullable: true\n          items:\n            type: string\n            enum: [udp, multicast, tcp]\n          deprecated: true\n        rtspTransports:\n          type: array\n          items:\n            type: string\n            enum: [udp, multicast, tcp]\n        encryption:\n          type: string\n          nullable: true\n          deprecated: true\n          allOf:\n            - $ref: '#/components/schemas/Encryption'\n        rtspEncryption:\n          $ref: '#/components/schemas/Encryption'\n        rtspAddress:\n          type: string\n        rtspsAddress:\n          type: string\n        rtpAddress:\n          type: string\n        rtcpAddress:\n          type: string\n        multicastIPRange:\n          type: string\n        multicastRTPPort:\n          type: integer\n          format: int64\n        multicastRTCPPort:\n          type: integer\n          format: int64\n        srtpAddress:\n          type: string\n        srtcpAddress:\n          type: string\n        multicastSRTPPort:\n          type: integer\n          format: int64\n        multicastSRTCPPort:\n          type: integer\n          format: int64\n        rtspServerKey:\n          type: string\n        rtspServerCert:\n          type: string\n        authMethods:\n          type: array\n          nullable: true\n          items:\n            $ref: '#/components/schemas/RTSPAuthMethod'\n          deprecated: true\n        rtspAuthMethods:\n          type: array\n          items:\n            $ref: '#/components/schemas/RTSPAuthMethod'\n        rtspUDPReadBufferSize:\n          type: integer\n          format: uint64\n          nullable: true\n          deprecated: true\n\n        # RTMP server\n        rtmp:\n          type: boolean\n        rtmpDisable:\n          type: boolean\n          nullable: true\n          deprecated: true\n        rtmpEncryption:\n          $ref: '#/components/schemas/Encryption'\n        rtmpAddress:\n          type: string\n        rtmpsAddress:\n          type: string\n        rtmpServerKey:\n          type: string\n        rtmpServerCert:\n          type: string\n\n        # HLS server\n        hls:\n          type: boolean\n        hlsDisable:\n          type: boolean\n          nullable: true\n          deprecated: true\n        hlsAddress:\n          type: string\n        hlsEncryption:\n          type: boolean\n        hlsServerKey:\n          type: string\n        hlsServerCert:\n          type: string\n        hlsAllowOrigin:\n          type: string\n          nullable: true\n          deprecated: true\n        hlsAllowOrigins:\n          type: array\n          items:\n            type: string\n        hlsTrustedProxies:\n          type: array\n          items:\n            type: string\n        hlsAlwaysRemux:\n          type: boolean\n        hlsVariant:\n          $ref: '#/components/schemas/HLSVariant'\n        hlsSegmentCount:\n          type: integer\n          format: int64\n        hlsSegmentDuration:\n          type: string\n        hlsPartDuration:\n          type: string\n        hlsSegmentMaxSize:\n          type: string\n        hlsDirectory:\n          type: string\n        hlsMuxerCloseAfter:\n          type: string\n\n        # WebRTC server\n        webrtc:\n          type: boolean\n        webrtcDisable:\n          type: boolean\n          nullable: true\n          deprecated: true\n        webrtcAddress:\n          type: string\n        webrtcEncryption:\n          type: boolean\n        webrtcServerKey:\n          type: string\n        webrtcServerCert:\n          type: string\n        webrtcAllowOrigin:\n          type: string\n          nullable: true\n          deprecated: true\n        webrtcAllowOrigins:\n          type: array\n          items:\n            type: string\n        webrtcTrustedProxies:\n          type: array\n          items:\n            type: string\n        webrtcLocalUDPAddress:\n          type: string\n        webrtcLocalTCPAddress:\n          type: string\n        webrtcIPsFromInterfaces:\n          type: boolean\n        webrtcIPsFromInterfacesList:\n          type: array\n          items:\n            type: string\n        webrtcAdditionalHosts:\n          type: array\n          items:\n            type: string\n        webrtcICEServers2:\n          type: array\n          items:\n            $ref: '#/components/schemas/WebRTCICEServer'\n        webrtcSTUNGatherTimeout:\n          type: string\n        webrtcHandshakeTimeout:\n          type: string\n        webrtcTrackGatherTimeout:\n          type: string\n        webrtcICEUDPMuxAddress:\n          type: string\n          nullable: true\n          deprecated: true\n        webrtcICETCPMuxAddress:\n          type: string\n          nullable: true\n          deprecated: true\n        webrtcICEHostNAT1To1IPs:\n          type: array\n          nullable: true\n          items:\n            type: string\n          deprecated: true\n        webrtcICEServers:\n          type: array\n          nullable: true\n          items:\n            type: string\n          deprecated: true\n\n        # SRT server\n        srt:\n          type: boolean\n        srtAddress:\n          type: string\n\n        # Record (deprecated)\n        record:\n          type: boolean\n          nullable: true\n          deprecated: true\n        recordPath:\n          type: string\n          nullable: true\n          deprecated: true\n        recordFormat:\n          type: string\n          nullable: true\n          deprecated: true\n          allOf:\n            - $ref: '#/components/schemas/RecordFormat'\n        recordPartDuration:\n          type: string\n          nullable: true\n          deprecated: true\n        recordSegmentDuration:\n          type: string\n          nullable: true\n          deprecated: true\n        recordDeleteAfter:\n          type: string\n          nullable: true\n          deprecated: true\n\n    PathConf:\n      type: object\n      properties:\n        name:\n          type: string\n\n        # General\n        source:\n          type: string\n        sourceFingerprint:\n          type: string\n        sourceOnDemand:\n          type: boolean\n        sourceOnDemandStartTimeout:\n          type: string\n        sourceOnDemandCloseAfter:\n          type: string\n        maxReaders:\n          type: integer\n          format: int64\n        srtReadPassphrase:\n          type: string\n        fallback:\n          type: string\n          nullable: true\n          deprecated: true\n        useAbsoluteTimestamp:\n          type: boolean\n\n        # Always available\n        alwaysAvailable:\n          type: boolean\n        alwaysAvailableTracks:\n          type: array\n          items:\n            $ref: '#/components/schemas/AlwaysAvailableTrack'\n        alwaysAvailableFile:\n          type: string\n\n        # Record\n        record:\n          type: boolean\n        playback:\n          type: boolean\n          nullable: true\n          deprecated: true\n        recordPath:\n          type: string\n        recordFormat:\n          $ref: '#/components/schemas/RecordFormat'\n        recordPartDuration:\n          type: string\n        recordMaxPartSize:\n          type: string\n        recordSegmentDuration:\n          type: string\n        recordDeleteAfter:\n          type: string\n\n        # Authentication (deprecated)\n        publishUser:\n          type: string\n          nullable: true\n          deprecated: true\n        publishPass:\n          type: string\n          nullable: true\n          deprecated: true\n        publishIPs:\n          type: array\n          nullable: true\n          items:\n            type: string\n          deprecated: true\n        readUser:\n          type: string\n          nullable: true\n          deprecated: true\n        readPass:\n          type: string\n          nullable: true\n          deprecated: true\n        readIPs:\n          type: array\n          nullable: true\n          items:\n            type: string\n          deprecated: true\n\n        # Publisher source\n        overridePublisher:\n          type: boolean\n        disablePublisherOverride:\n          type: boolean\n          nullable: true\n          deprecated: true\n        srtPublishPassphrase:\n          type: string\n        rtspDemuxMpegts:\n          type: boolean\n\n        # RTSP source\n        rtspTransport:\n          $ref: '#/components/schemas/RTSPTransport'\n        rtspAnyPort:\n          type: boolean\n        sourceProtocol:\n          type: string\n          nullable: true\n          deprecated: true\n          allOf:\n            - $ref: '#/components/schemas/RTSPTransport'\n        sourceAnyPortEnable:\n          type: boolean\n          nullable: true\n          deprecated: true\n        rtspRangeType:\n          $ref: '#/components/schemas/RTSPRangeType'\n        rtspRangeStart:\n          type: string\n        rtspUDPReadBufferSize:\n          type: integer\n          format: uint64\n          nullable: true\n          deprecated: true\n        rtspUDPSourcePortRange:\n          type: array\n          minItems: 2\n          maxItems: 2\n          items:\n            type: integer\n            format: uint64\n\n        # MPEG-TS source\n        mpegtsUDPReadBufferSize:\n          type: integer\n          format: uint64\n          nullable: true\n          deprecated: true\n\n        # RTP source\n        rtpSDP:\n          type: string\n        rtpUDPReadBufferSize:\n          type: integer\n          format: uint64\n          nullable: true\n          deprecated: true\n\n        # WHEP source\n        whepBearerToken:\n          type: string\n        whepSTUNGatherTimeout:\n          type: string\n        whepHandshakeTimeout:\n          type: string\n        whepTrackGatherTimeout:\n          type: string\n\n        # Redirect source\n        sourceRedirect:\n          type: string\n\n        # Raspberry Pi Camera source\n        rpiCameraCamID:\n          type: integer\n          format: uint64\n        rpiCameraSecondary:\n          type: boolean\n        rpiCameraWidth:\n          type: integer\n          format: uint64\n        rpiCameraHeight:\n          type: integer\n          format: uint64\n        rpiCameraHFlip:\n          type: boolean\n        rpiCameraVFlip:\n          type: boolean\n        rpiCameraBrightness:\n          type: number\n          format: double\n        rpiCameraContrast:\n          type: number\n          format: double\n        rpiCameraSaturation:\n          type: number\n          format: double\n        rpiCameraSharpness:\n          type: number\n          format: double\n        rpiCameraExposure:\n          type: string\n        rpiCameraAWB:\n          type: string\n        rpiCameraAWBGains:\n          type: array\n          minItems: 2\n          maxItems: 2\n          items:\n            type: number\n            format: double\n        rpiCameraDenoise:\n          type: string\n        rpiCameraShutter:\n          type: integer\n          format: uint64\n        rpiCameraMetering:\n          type: string\n        rpiCameraGain:\n          type: number\n          format: double\n        rpiCameraEV:\n          type: number\n          format: double\n        rpiCameraROI:\n          type: string\n        rpiCameraHDR:\n          type: boolean\n        rpiCameraTuningFile:\n          type: string\n        rpiCameraMode:\n          type: string\n        rpiCameraFPS:\n          type: number\n          format: double\n        rpiCameraAfMode:\n          type: string\n        rpiCameraAfRange:\n          type: string\n        rpiCameraAfSpeed:\n          type: string\n        rpiCameraLensPosition:\n          type: number\n          format: double\n        rpiCameraAfWindow:\n          type: string\n        rpiCameraFlickerPeriod:\n          type: integer\n          format: uint64\n        rpiCameraTextOverlayEnable:\n          type: boolean\n        rpiCameraTextOverlay:\n          type: string\n        rpiCameraCodec:\n          type: string\n        rpiCameraIDRPeriod:\n          type: integer\n          format: uint64\n        rpiCameraBitrate:\n          type: integer\n          format: uint64\n        rpiCameraProfile:\n          type: string\n          nullable: true\n          deprecated: true\n        rpiCameraLevel:\n          type: string\n          nullable: true\n          deprecated: true\n        rpiCameraHardwareH264Profile:\n          type: string\n        rpiCameraHardwareH264Level:\n          type: string\n        rpiCameraSoftwareH264Profile:\n          type: string\n        rpiCameraSoftwareH264Level:\n          type: string\n        rpiCameraJPEGQuality:\n          type: integer\n          format: uint64\n          nullable: true\n          deprecated: true\n        rpiCameraMJPEGQuality:\n          type: integer\n          format: uint64\n\n        # Hooks\n        runOnInit:\n          type: string\n        runOnInitRestart:\n          type: boolean\n        runOnDemand:\n          type: string\n        runOnDemandRestart:\n          type: boolean\n        runOnDemandStartTimeout:\n          type: string\n        runOnDemandCloseAfter:\n          type: string\n        runOnUnDemand:\n          type: string\n        runOnReady:\n          type: string\n        runOnReadyRestart:\n          type: boolean\n        runOnNotReady:\n          type: string\n        runOnRead:\n          type: string\n        runOnReadRestart:\n          type: boolean\n        runOnUnread:\n          type: string\n        runOnRecordSegmentCreate:\n          type: string\n        runOnRecordSegmentComplete:\n          type: string\n\n    PathConfList:\n      type: object\n      properties:\n        pageCount:\n          type: integer\n          format: int64\n        itemCount:\n          type: integer\n          format: int64\n        items:\n          type: array\n          items:\n            $ref: '#/components/schemas/PathConf'\n\n    Path:\n      type: object\n      properties:\n        name:\n          type: string\n        confName:\n          type: string\n        source:\n          type: object\n          nullable: true\n          allOf:\n            - $ref: '#/components/schemas/PathSource'\n        ready:\n          type: boolean\n          deprecated: true\n        readyTime:\n          type: string\n          nullable: true\n          deprecated: true\n        available:\n          type: boolean\n        availableTime:\n          type: string\n          nullable: true\n        online:\n          type: boolean\n        onlineTime:\n          type: string\n          nullable: true\n        tracks:\n          type: array\n          deprecated: true\n          items:\n            $ref: '#/components/schemas/PathTrackCodec'\n        tracks2:\n          type: array\n          items:\n            $ref: '#/components/schemas/PathTrack'\n        inboundBytes:\n          type: integer\n          format: uint64\n        outboundBytes:\n          type: integer\n          format: uint64\n        inboundFramesInError:\n          type: integer\n          format: uint64\n        bytesReceived:\n          type: integer\n          format: uint64\n          deprecated: true\n        bytesSent:\n          type: integer\n          format: uint64\n          deprecated: true\n        readers:\n          type: array\n          items:\n            $ref: '#/components/schemas/PathReader'\n\n    PathList:\n      type: object\n      properties:\n        pageCount:\n          type: integer\n          format: int64\n        itemCount:\n          type: integer\n          format: int64\n        items:\n          type: array\n          items:\n            $ref: '#/components/schemas/Path'\n\n    PathSource:\n      type: object\n      properties:\n        type:\n          $ref: '#/components/schemas/PathSourceType'\n        id:\n          type: string\n\n    PathReader:\n      type: object\n      properties:\n        type:\n          $ref: '#/components/schemas/PathReaderType'\n        id:\n          type: string\n\n    PathTrack:\n      type: object\n      properties:\n        codec:\n          $ref: '#/components/schemas/PathTrackCodec'\n        codecProps:\n          type: object\n          nullable: true\n          allOf:\n          - $ref: '#/components/schemas/PathTrackCodecProps'\n\n    PathTrackCodecProps:\n      oneOf:\n      - $ref: '#/components/schemas/PathTrackCodecPropsAV1'\n      - $ref: '#/components/schemas/PathTrackCodecPropsVP9'\n      - $ref: '#/components/schemas/PathTrackCodecPropsH265'\n      - $ref: '#/components/schemas/PathTrackCodecPropsH264'\n      - $ref: '#/components/schemas/PathTrackCodecPropsOpus'\n      - $ref: '#/components/schemas/PathTrackCodecPropsMPEG4Audio'\n      - $ref: '#/components/schemas/PathTrackCodecPropsAC3'\n      - $ref: '#/components/schemas/PathTrackCodecPropsG711'\n      - $ref: '#/components/schemas/PathTrackCodecPropsLPCM'\n\n    PathTrackCodecPropsAV1:\n      type: object\n      properties:\n        width:\n          type: integer\n          format: int64\n        height:\n          type: integer\n          format: int64\n        profile:\n          type: integer\n          format: int64\n        level:\n          type: integer\n          format: int64\n        tier:\n          type: integer\n          format: int64\n    PathTrackCodecPropsVP9:\n      type: object\n      properties:\n        profile:\n          type: integer\n          format: int64\n\n    PathTrackCodecPropsH265:\n      type: object\n      properties:\n        width:\n          type: integer\n          format: int64\n        height:\n          type: integer\n          format: int64\n        profile:\n          type: string\n        level:\n          type: string\n\n    PathTrackCodecPropsH264:\n      type: object\n      properties:\n        width:\n          type: integer\n          format: int64\n        height:\n          type: integer\n          format: int64\n        profile:\n          type: string\n        level:\n          type: string\n\n    PathTrackCodecPropsOpus:\n      type: object\n      properties:\n        channelCount:\n          type: integer\n          format: int64\n\n    PathTrackCodecPropsMPEG4Audio:\n      type: object\n      properties:\n        sampleRate:\n          type: integer\n          format: int64\n        channelCount:\n          type: integer\n          format: int64\n\n    PathTrackCodecPropsAC3:\n      type: object\n      properties:\n        sampleRate:\n          type: integer\n          format: int64\n        channelCount:\n          type: integer\n          format: int64\n\n    PathTrackCodecPropsG711:\n      type: object\n      properties:\n        muLaw:\n          type: boolean\n        sampleRate:\n          type: integer\n          format: int64\n        channelCount:\n          type: integer\n          format: int64\n\n    PathTrackCodecPropsLPCM:\n      type: object\n      properties:\n        bitDepth:\n          type: integer\n          format: int64\n        sampleRate:\n          type: integer\n          format: int64\n        channelCount:\n          type: integer\n          format: int64\n\n    HLSMuxer:\n      type: object\n      properties:\n        path:\n          type: string\n        created:\n          type: string\n        lastRequest:\n          type: string\n        outboundBytes:\n          type: integer\n          format: uint64\n        outboundFramesDiscarded:\n          type: integer\n          format: uint64\n        bytesSent:\n          type: integer\n          format: uint64\n          deprecated: true\n\n    HLSMuxerList:\n      type: object\n      properties:\n        pageCount:\n          type: integer\n          format: int64\n        itemCount:\n          type: integer\n          format: int64\n        items:\n          type: array\n          items:\n            $ref: '#/components/schemas/HLSMuxer'\n\n    Recording:\n      type: object\n      properties:\n        name:\n          type: string\n        segments:\n          type: array\n          items:\n            $ref: '#/components/schemas/RecordingSegment'\n\n    RecordingList:\n      type: object\n      properties:\n        pageCount:\n          type: integer\n          format: int64\n        itemCount:\n          type: integer\n          format: int64\n        items:\n          type: array\n          items:\n            $ref: '#/components/schemas/Recording'\n\n    RecordingSegment:\n      type: object\n      properties:\n        start:\n          type: string\n\n    RTMPConn:\n      type: object\n      properties:\n        id:\n          type: string\n          format: uuid\n        created:\n          type: string\n        remoteAddr:\n          type: string\n        state:\n          $ref: '#/components/schemas/RTMPConnState'\n        path:\n          type: string\n        query:\n          type: string\n        user:\n          type: string\n        inboundBytes:\n          type: integer\n          format: uint64\n        outboundBytes:\n          type: integer\n          format: uint64\n        outboundFramesDiscarded:\n          type: integer\n          format: uint64\n        bytesReceived:\n          type: integer\n          format: uint64\n          deprecated: true\n        bytesSent:\n          type: integer\n          format: uint64\n          deprecated: true\n\n    RTMPConnList:\n      type: object\n      properties:\n        pageCount:\n          type: integer\n          format: int64\n        itemCount:\n          type: integer\n          format: int64\n        items:\n          type: array\n          items:\n            $ref: '#/components/schemas/RTMPConn'\n\n    RTSPConn:\n      type: object\n      properties:\n        id:\n          type: string\n          format: uuid\n        created:\n          type: string\n        remoteAddr:\n          type: string\n        session:\n          type: string\n          format: uuid\n          nullable: true\n        tunnel:\n          type: string\n        inboundBytes:\n          type: integer\n          format: uint64\n        outboundBytes:\n          type: integer\n          format: uint64\n        bytesReceived:\n          type: integer\n          format: uint64\n          deprecated: true\n        bytesSent:\n          type: integer\n          format: uint64\n          deprecated: true\n\n    RTSPConnList:\n      type: object\n      properties:\n        pageCount:\n          type: integer\n          format: int64\n        itemCount:\n          type: integer\n          format: int64\n        items:\n          type: array\n          items:\n            $ref: '#/components/schemas/RTSPConn'\n\n    RTSPSession:\n      type: object\n      properties:\n        id:\n          type: string\n          format: uuid\n        created:\n          type: string\n        remoteAddr:\n          type: string\n        state:\n          $ref: '#/components/schemas/RTSPSessionState'\n        path:\n          type: string\n        query:\n          type: string\n        user:\n          type: string\n        transport:\n          type: string\n          nullable: true\n        profile:\n          type: string\n          nullable: true\n        conns:\n          type: array\n          items:\n            type: string\n            format: uuid\n        inboundBytes:\n          type: integer\n          format: uint64\n        inboundRTPPackets:\n          type: integer\n          format: uint64\n        inboundRTPPacketsLost:\n          type: integer\n          format: uint64\n        inboundRTPPacketsInError:\n          type: integer\n          format: uint64\n        inboundRTPPacketsJitter:\n          type: number\n          format: double\n        inboundRTCPPackets:\n          type: integer\n          format: uint64\n        inboundRTCPPacketsInError:\n          type: integer\n          format: uint64\n        outboundBytes:\n          type: integer\n          format: uint64\n        outboundRTPPackets:\n          type: integer\n          format: uint64\n        outboundRTPPacketsReportedLost:\n          type: integer\n          format: uint64\n        outboundRTPPacketsDiscarded:\n          type: integer\n          format: uint64\n        outboundRTCPPackets:\n          type: integer\n          format: uint64\n        bytesReceived:\n          type: integer\n          format: uint64\n          deprecated: true\n        bytesSent:\n          type: integer\n          format: uint64\n          deprecated: true\n        rtpPacketsReceived:\n          type: integer\n          format: uint64\n          deprecated: true\n        rtpPacketsSent:\n          type: integer\n          format: uint64\n          deprecated: true\n        rtpPacketsLost:\n          type: integer\n          format: uint64\n          deprecated: true\n        rtpPacketsInError:\n          type: integer\n          format: uint64\n          deprecated: true\n        rtpPacketsJitter:\n          type: number\n          format: double\n          deprecated: true\n        rtcpPacketsReceived:\n          type: integer\n          format: uint64\n          deprecated: true\n        rtcpPacketsSent:\n          type: integer\n          format: uint64\n          deprecated: true\n        rtcpPacketsInError:\n          type: integer\n          format: uint64\n          deprecated: true\n\n    RTSPSessionList:\n      type: object\n      properties:\n        pageCount:\n          type: integer\n          format: int64\n        itemCount:\n          type: integer\n          format: int64\n        items:\n          type: array\n          items:\n            $ref: '#/components/schemas/RTSPSession'\n\n    SRTConn:\n      type: object\n      properties:\n        id:\n          type: string\n          format: uuid\n        created:\n          type: string\n        remoteAddr:\n          type: string\n        state:\n          $ref: '#/components/schemas/SRTConnState'\n        path:\n          type: string\n        query:\n          type: string\n        user:\n          type: string\n        packetsSent:\n          type: integer\n          format: uint64\n          description: The total number of sent DATA packets, including retransmitted packets\n        packetsReceived:\n          type: integer\n          format: uint64\n          description: The total number of received DATA packets, including retransmitted packets\n        packetsReceivedBelated:\n          type: integer\n          format: uint64\n        packetsSentUnique:\n          type: integer\n          format: uint64\n          description: The total number of unique DATA packets sent by the SRT sender\n        packetsReceivedUnique:\n          type: integer\n          format: uint64\n          description: The total number of unique original, retransmitted or recovered by the packet filter DATA packets received in time, decrypted without errors and, as a result, scheduled for delivery to the upstream application by the SRT receiver.\n        packetsSendLoss:\n          type: integer\n          format: uint64\n          description: The total number of data packets considered or reported as lost at the sender side. Does not correspond to the packets detected as lost at the receiver side.\n        packetsReceivedLoss:\n          type: integer\n          format: uint64\n          description: The total number of SRT DATA packets detected as presently missing (either reordered or lost) at the receiver side\n        packetsRetrans:\n          type: integer\n          format: uint64\n          description: The total number of retransmitted packets sent by the SRT sender\n        packetsReceivedRetrans:\n          type: integer\n          format: uint64\n          description: The total number of retransmitted packets registered at the receiver side\n        packetsSentACK:\n          type: integer\n          format: uint64\n          description: The total number of sent ACK (Acknowledgement) control packets\n        packetsReceivedACK:\n          type: integer\n          format: uint64\n          description: The total number of received ACK (Acknowledgement) control packets\n        packetsSentNAK:\n          type: integer\n          format: uint64\n          description: The total number of sent NAK (Negative Acknowledgement) control packets\n        packetsReceivedNAK:\n          type: integer\n          format: uint64\n          description: The total number of received NAK (Negative Acknowledgement) control packets\n        packetsSentKM:\n          type: integer\n          format: uint64\n          description: The total number of sent KM (Key Material) control packets\n        packetsReceivedKM:\n          type: integer\n          format: uint64\n          description: The total number of received KM (Key Material) control packets\n        usSndDuration:\n          type: integer\n          format: uint64\n          description: The total accumulated time in microseconds, during which the SRT sender has some data to transmit, including packets that have been sent, but not yet acknowledged\n        packetsSendDrop:\n          type: integer\n          format: uint64\n          description: The total number of dropped by the SRT sender DATA packets that have no chance to be delivered in time\n        packetsReceivedDrop:\n          type: integer\n          format: uint64\n          description: The total number of dropped by the SRT receiver and, as a result, not delivered to the upstream application DATA packets\n        packetsReceivedUndecrypt:\n          type: integer\n          format: uint64\n          description: The total number of packets that failed to be decrypted at the receiver side\n        bytesSent:\n          type: integer\n          format: uint64\n          description: Same as packetsSent, but expressed in bytes, including payload and all the headers (IP, TCP, SRT)\n        bytesReceived:\n          type: integer\n          format: uint64\n          description: Same as packetsReceived, but expressed in bytes, including payload and all the headers (IP, TCP, SRT)\n        bytesReceivedBelated:\n          type: integer\n          format: uint64\n        bytesSentUnique:\n          type: integer\n          format: uint64\n          description: Same as packetsSentUnique, but expressed in bytes, including payload and all the headers (IP, TCP, SRT)\n        bytesReceivedUnique:\n          type: integer\n          format: uint64\n          description: Same as packetsReceivedUnique, but expressed in bytes, including payload and all the headers (IP, TCP, SRT)\n        bytesReceivedLoss:\n          type: integer\n          format: uint64\n          description: Same as packetsReceivedLoss, but expressed in bytes, including payload and all the headers (IP, TCP, SRT), bytes for the presently missing (either reordered or lost) packets' payloads are estimated based on the average packet size\n        bytesRetrans:\n          type: integer\n          format: uint64\n          description: Same as packetsRetrans, but expressed in bytes, including payload and all the headers (IP, TCP, SRT)\n        bytesReceivedRetrans:\n          type: integer\n          format: uint64\n          description: Same as packetsReceivedRetrans, but expressed in bytes, including payload and all the headers (IP, TCP, SRT)\n        bytesSendDrop:\n          type: integer\n          format: uint64\n          description: Same as packetsSendDrop, but expressed in bytes, including payload and all the headers (IP, TCP, SRT)\n        bytesReceivedDrop:\n          type: integer\n          format: uint64\n          description: Same as packetsReceivedDrop, but expressed in bytes, including payload and all the headers (IP, TCP, SRT)\n        bytesReceivedUndecrypt:\n          type: integer\n          format: uint64\n          description: Same as packetsReceivedUndecrypt, but expressed in bytes, including payload and all the headers (IP, TCP, SRT)\n        usPacketsSendPeriod:\n          type: number\n          format: double\n          description: Current minimum time interval between which consecutive packets are sent, in microseconds\n        packetsFlowWindow:\n          type: integer\n          format: uint64\n          description: The maximum number of packets that can be \"in flight\"\n        packetsFlightSize:\n          type: integer\n          format: uint64\n          description: The number of packets in flight\n        msRTT:\n          type: number\n          format: double\n          description: Smoothed round-trip time (SRTT), an exponentially-weighted moving average (EWMA) of an endpoint's RTT samples, in milliseconds\n        mbpsSendRate:\n          type: number\n          format: double\n          description: Current transmission bandwidth, in Mbps\n        mbpsReceiveRate:\n          type: number\n          format: double\n          description: Current receiving bandwidth, in Mbps\n        mbpsLinkCapacity:\n          type: number\n          format: double\n          description: Estimated capacity of the network link, in Mbps\n        bytesAvailSendBuf:\n          type: integer\n          format: uint64\n          description: The available space in the sender's buffer, in bytes\n        bytesAvailReceiveBuf:\n          type: integer\n          format: uint64\n          description: The available space in the receiver's buffer, in bytes\n        mbpsMaxBW:\n          type: number\n          format: double\n          description: Transmission bandwidth limit, in Mbps\n        byteMSS:\n          type: integer\n          format: uint64\n          description: Maximum Segment Size (MSS), in bytes\n        packetsSendBuf:\n          type: integer\n          format: uint64\n          description: The number of packets in the sender's buffer that are already scheduled for sending or even possibly sent, but not yet acknowledged\n        bytesSendBuf:\n          type: integer\n          format: uint64\n          description: Instantaneous (current) value of packetsSndBuf, but expressed in bytes, including payload and all headers (IP, TCP, SRT)\n        msSendBuf:\n          type: integer\n          format: uint64\n          description: The timespan (msec) of packets in the sender's buffer (unacknowledged packets)\n        msSendTsbPdDelay:\n          type: integer\n          format: uint64\n          description: Timestamp-based Packet Delivery Delay value of the peer\n        packetsReceiveBuf:\n          type: integer\n          format: uint64\n          description: The number of acknowledged packets in receiver's buffer\n        bytesReceiveBuf:\n          type: integer\n          format: uint64\n          description: Instantaneous (current) value of packetsRcvBuf, expressed in bytes, including payload and all headers (IP, TCP, SRT)\n        msReceiveBuf:\n          type: integer\n          format: uint64\n          description: The timespan (msec) of acknowledged packets in the receiver's buffer\n        msReceiveTsbPdDelay:\n          type: integer\n          format: uint64\n          description: Timestamp-based Packet Delivery Delay value set on the socket via SRTO_RCVLATENCY or SRTO_LATENCY\n        packetsReorderTolerance:\n          type: integer\n          format: uint64\n          description: Instant value of the packet reorder tolerance\n        packetsReceivedAvgBelatedTime:\n          type: integer\n          format: uint64\n          description: Accumulated difference between the current time and the time-to-play of a packet that is received late\n        packetsSendLossRate:\n          type: number\n          format: double\n          description: Percentage of resent data vs. sent data\n        packetsReceivedLossRate:\n          type: number\n          format: double\n          description: Percentage of retransmitted data vs. received data\n        outboundFramesDiscarded:\n          type: integer\n          format: uint64\n\n    SRTConnList:\n      type: object\n      properties:\n        pageCount:\n          type: integer\n          format: int64\n        itemCount:\n          type: integer\n          format: int64\n        items:\n          type: array\n          items:\n            $ref: '#/components/schemas/SRTConn'\n\n    AlwaysAvailableTrack:\n      type: object\n      properties:\n        codec:\n          $ref: '#/components/schemas/AlwaysAvailableTrackCodec'\n        sampleRate:\n          type: integer\n          format: int64\n        channelCount:\n          type: integer\n          format: int64\n        muLaw:\n          type: boolean\n\n    WebRTCICEServer:\n      type: object\n      properties:\n        url:\n          type: string\n        username:\n          type: string\n        password:\n          type: string\n        clientOnly:\n          type: boolean\n\n    WebRTCSession:\n      type: object\n      properties:\n        id:\n          type: string\n          format: uuid\n        created:\n          type: string\n        remoteAddr:\n          type: string\n        peerConnectionEstablished:\n          type: boolean\n        localCandidate:\n          type: string\n        remoteCandidate:\n          type: string\n        state:\n          $ref: '#/components/schemas/WebRTCSessionState'\n        path:\n          type: string\n        query:\n          type: string\n        user:\n          type: string\n        inboundBytes:\n          type: integer\n          format: uint64\n        inboundRTPPackets:\n          type: integer\n          format: uint64\n        inboundRTPPacketsLost:\n          type: integer\n          format: uint64\n        inboundRTPPacketsJitter:\n          type: number\n          format: double\n        inboundRTCPPackets:\n          type: integer\n          format: uint64\n        outboundBytes:\n          type: integer\n          format: uint64\n        outboundRTPPackets:\n          type: integer\n          format: uint64\n        outboundRTCPPackets:\n          type: integer\n          format: uint64\n        outboundFramesDiscarded:\n          type: integer\n          format: uint64\n        bytesReceived:\n          type: integer\n          format: uint64\n          deprecated: true\n        bytesSent:\n          type: integer\n          format: uint64\n          deprecated: true\n        rtpPacketsReceived:\n          type: integer\n          format: uint64\n          deprecated: true\n        rtpPacketsSent:\n          type: integer\n          format: uint64\n          deprecated: true\n        rtpPacketsLost:\n          type: integer\n          format: uint64\n          deprecated: true\n        rtpPacketsJitter:\n          type: number\n          format: double\n          deprecated: true\n        rtcpPacketsReceived:\n          type: integer\n          format: uint64\n          deprecated: true\n        rtcpPacketsSent:\n          type: integer\n          format: uint64\n          deprecated: true\n\n    WebRTCSessionList:\n      type: object\n      properties:\n        pageCount:\n          type: integer\n          format: int64\n        itemCount:\n          type: integer\n          format: int64\n        items:\n          type: array\n          items:\n            $ref: '#/components/schemas/WebRTCSession'\n\npaths:\n\n  /v3/info:\n    get:\n      operationId: info\n      tags: [General]\n      summary: returns informations about the instance.\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Info'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/auth/jwks/refresh:\n    post:\n      operationId: authJwksRefresh\n      tags: [Authentication]\n      summary: Manually refreshes the JWT JWKS.\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/OK'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/config/global/get:\n    get:\n      operationId: configGlobalGet\n      tags: [Configuration]\n      summary: returns the global configuration.\n      description: ''\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/GlobalConf'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/config/global/patch:\n    patch:\n      operationId: configGlobalSet\n      tags: [Configuration]\n      summary: patches the global configuration.\n      description: all fields are optional.\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/GlobalConf'\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/OK'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/config/pathdefaults/get:\n    get:\n      operationId: configPathDefaultsGet\n      tags: [Configuration]\n      summary: returns the default path configuration.\n      description: ''\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/PathConf'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/config/pathdefaults/patch:\n    patch:\n      operationId: configPathDefaultsPatch\n      tags: [Configuration]\n      summary: patches the default path configuration.\n      description: all fields are optional.\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/PathConf'\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/OK'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/config/paths/list:\n    get:\n      operationId: configPathsList\n      tags: [Configuration]\n      summary: returns all path configurations.\n      description: ''\n      parameters:\n      - name: page\n        in: query\n        description: page number.\n        schema:\n          type: integer\n          default: 0\n      - name: itemsPerPage\n        in: query\n        description: items per page.\n        schema:\n          type: integer\n          default: 100\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/PathConfList'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/config/paths/get/{name}:\n    get:\n      operationId: configPathsGet\n      tags: [Configuration]\n      summary: returns a path configuration.\n      description: ''\n      parameters:\n      - name: name\n        in: path\n        required: true\n        description: the name of the path.\n        schema:\n          type: string\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/PathConf'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '404':\n          description: path not found.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/config/paths/add/{name}:\n    post:\n      operationId: configPathsAdd\n      tags: [Configuration]\n      summary: adds a path configuration.\n      description: all fields are optional.\n      parameters:\n      - name: name\n        in: path\n        required: true\n        description: the name of the path.\n        schema:\n          type: string\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/PathConf'\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/OK'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/config/paths/patch/{name}:\n    patch:\n      operationId: configPathsPatch\n      tags: [Configuration]\n      summary: patches a path configuration.\n      description: all fields are optional.\n      parameters:\n      - name: name\n        in: path\n        required: true\n        description: the name of the path.\n        schema:\n          type: string\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/PathConf'\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/OK'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '404':\n          description: path not found.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/config/paths/replace/{name}:\n    post:\n      operationId: configPathsReplace\n      tags: [Configuration]\n      summary: replaces all values of a path configuration.\n      description: all fields are optional.\n      parameters:\n      - name: name\n        in: path\n        required: true\n        description: the name of the path.\n        schema:\n          type: string\n      requestBody:\n        required: true\n        content:\n          application/json:\n            schema:\n              $ref: '#/components/schemas/PathConf'\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/OK'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '404':\n          description: path not found.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/config/paths/delete/{name}:\n    delete:\n      operationId: configPathsDelete\n      tags: [Configuration]\n      summary: removes a path configuration.\n      description: ''\n      parameters:\n      - name: name\n        in: path\n        required: true\n        description: the name of the path.\n        schema:\n          type: string\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/OK'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '404':\n          description: path not found.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/hlsmuxers/list:\n    get:\n      operationId: hlsMuxersList\n      tags: [HLS]\n      summary: returns all HLS muxers.\n      description: ''\n      parameters:\n      - name: page\n        in: query\n        description: page number.\n        schema:\n          type: integer\n          default: 0\n      - name: itemsPerPage\n        in: query\n        description: items per page.\n        schema:\n          type: integer\n          default: 100\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/HLSMuxerList'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/hlsmuxers/get/{name}:\n    get:\n      operationId: hlsMuxersGet\n      tags: [HLS]\n      summary: returns a HLS muxer.\n      description: ''\n      parameters:\n      - name: name\n        in: path\n        required: true\n        description: name of the muxer.\n        schema:\n          type: string\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/HLSMuxer'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '404':\n          description: muxer not found.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/paths/list:\n    get:\n      operationId: pathsList\n      tags: [Paths]\n      summary: returns all paths.\n      description: ''\n      parameters:\n      - name: page\n        in: query\n        description: page number.\n        schema:\n          type: integer\n          default: 0\n      - name: itemsPerPage\n        in: query\n        description: items per page.\n        schema:\n          type: integer\n          default: 100\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/PathList'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/paths/get/{name}:\n    get:\n      operationId: pathsGet\n      tags: [Paths]\n      summary: returns a path.\n      description: ''\n      parameters:\n      - name: name\n        in: path\n        required: true\n        description: name of the path.\n        schema:\n          type: string\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Path'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '404':\n          description: path not found.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/rtspconns/list:\n    get:\n      operationId: rtspConnsList\n      tags: [RTSP]\n      summary: returns all RTSP connections.\n      description: ''\n      parameters:\n      - name: page\n        in: query\n        description: page number.\n        schema:\n          type: integer\n          default: 0\n      - name: itemsPerPage\n        in: query\n        description: items per page.\n        schema:\n          type: integer\n          default: 100\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/RTSPConnList'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/rtspconns/get/{id}:\n    get:\n      operationId: rtspConnsGet\n      tags: [RTSP]\n      summary: returns a RTSP connection.\n      description: ''\n      parameters:\n      - name: id\n        in: path\n        required: true\n        description: ID of the connection.\n        schema:\n          type: string\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/RTSPConn'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '404':\n          description: connection not found.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/rtspsessions/list:\n    get:\n      operationId: rtspSessionsList\n      tags: [RTSP]\n      summary: returns all RTSP sessions.\n      description: ''\n      parameters:\n      - name: page\n        in: query\n        description: page number.\n        schema:\n          type: integer\n          default: 0\n      - name: itemsPerPage\n        in: query\n        description: items per page.\n        schema:\n          type: integer\n          default: 100\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/RTSPSessionList'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/rtspsessions/get/{id}:\n    get:\n      operationId: rtspSessionsGet\n      tags: [RTSP]\n      summary: returns a RTSP session.\n      description: ''\n      parameters:\n      - name: id\n        in: path\n        required: true\n        description: ID of the connection.\n        schema:\n          type: string\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/RTSPSession'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '404':\n          description: session not found.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/rtspsessions/kick/{id}:\n    post:\n      operationId: rtspSessionsKick\n      tags: [RTSP]\n      summary: kicks out a RTSP session from the server.\n      description: ''\n      parameters:\n      - name: id\n        in: path\n        required: true\n        description: ID of the session.\n        schema:\n          type: string\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/OK'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '404':\n          description: session not found.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/rtspsconns/list:\n    get:\n      operationId: rtspsConnsList\n      tags: [RTSP]\n      summary: returns all RTSPS connections.\n      description: ''\n      parameters:\n      - name: page\n        in: query\n        description: page number.\n        schema:\n          type: integer\n          default: 0\n      - name: itemsPerPage\n        in: query\n        description: items per page.\n        schema:\n          type: integer\n          default: 100\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/RTSPConnList'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/rtspsconns/get/{id}:\n    get:\n      operationId: rtspsConnsGet\n      tags: [RTSP]\n      summary: returns a RTSPS connection.\n      description: ''\n      parameters:\n      - name: id\n        in: path\n        required: true\n        description: ID of the connection.\n        schema:\n          type: string\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/RTSPConn'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '404':\n          description: connection not found.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/rtspssessions/list:\n    get:\n      operationId: rtspsSessionsList\n      tags: [RTSP]\n      summary: returns all RTSPS sessions.\n      description: ''\n      parameters:\n      - name: page\n        in: query\n        description: page number.\n        schema:\n          type: integer\n          default: 0\n      - name: itemsPerPage\n        in: query\n        description: items per page.\n        schema:\n          type: integer\n          default: 100\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/RTSPSessionList'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/rtspssessions/get/{id}:\n    get:\n      operationId: rtspsSessionsGet\n      tags: [RTSP]\n      summary: returns a RTSPS session.\n      description: ''\n      parameters:\n      - name: id\n        in: path\n        required: true\n        description: ID of the connection.\n        schema:\n          type: string\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/RTSPSession'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '404':\n          description: session not found.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/rtspssessions/kick/{id}:\n    post:\n      operationId: rtspsSessionsKick\n      tags: [RTSP]\n      summary: kicks out a RTSPS session from the server.\n      description: ''\n      parameters:\n      - name: id\n        in: path\n        required: true\n        description: ID of the session.\n        schema:\n          type: string\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/OK'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '404':\n          description: session not found.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/rtmpconns/list:\n    get:\n      operationId: rtmpConnsList\n      tags: [RTMP]\n      summary: returns all RTMP connections.\n      description: ''\n      parameters:\n      - name: page\n        in: query\n        description: page number.\n        schema:\n          type: integer\n          default: 0\n      - name: itemsPerPage\n        in: query\n        description: items per page.\n        schema:\n          type: integer\n          default: 100\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/RTMPConnList'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/rtmpconns/get/{id}:\n    get:\n      operationId: rtmpConnectionsGet\n      tags: [RTMP]\n      summary: returns a RTMP connection.\n      description: ''\n      parameters:\n      - name: id\n        in: path\n        required: true\n        description: ID of the connection.\n        schema:\n          type: string\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/RTMPConn'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '404':\n          description: connection not found.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/rtmpconns/kick/{id}:\n    post:\n      operationId: rtmpConnsKick\n      tags: [RTMP]\n      summary: kicks out a RTMP connection from the server.\n      description: ''\n      parameters:\n      - name: id\n        in: path\n        required: true\n        description: ID of the connection.\n        schema:\n          type: string\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/OK'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '404':\n          description: connection not found.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/rtmpsconns/list:\n    get:\n      operationId: rtmpsConnsList\n      tags: [RTMP]\n      summary: returns all RTMPS connections.\n      description: ''\n      parameters:\n      - name: page\n        in: query\n        description: page number.\n        schema:\n          type: integer\n          default: 0\n      - name: itemsPerPage\n        in: query\n        description: items per page.\n        schema:\n          type: integer\n          default: 100\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/RTMPConnList'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/rtmpsconns/get/{id}:\n    get:\n      operationId: rtmpsConnectionsGet\n      tags: [RTMP]\n      summary: returns a RTMPS connection.\n      description: ''\n      parameters:\n      - name: id\n        in: path\n        required: true\n        description: ID of the connection.\n        schema:\n          type: string\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/RTMPConn'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '404':\n          description: connection not found.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/rtmpsconns/kick/{id}:\n    post:\n      operationId: rtmpsConnsKick\n      tags: [RTMP]\n      summary: kicks out a RTMPS connection from the server.\n      description: ''\n      parameters:\n      - name: id\n        in: path\n        required: true\n        description: ID of the connection.\n        schema:\n          type: string\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/OK'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '404':\n          description: connection not found.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/srtconns/list:\n    get:\n      operationId: srtConnsList\n      tags: [SRT]\n      summary: returns all SRT connections.\n      description: ''\n      parameters:\n      - name: page\n        in: query\n        description: page number.\n        schema:\n          type: integer\n          default: 0\n      - name: itemsPerPage\n        in: query\n        description: items per page.\n        schema:\n          type: integer\n          default: 100\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/SRTConnList'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/srtconns/get/{id}:\n    get:\n      operationId: srtConnsGet\n      tags: [SRT]\n      summary: returns a SRT connection.\n      description: ''\n      parameters:\n      - name: id\n        in: path\n        required: true\n        description: ID of the connection.\n        schema:\n          type: string\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/SRTConn'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '404':\n          description: connection not found.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/srtconns/kick/{id}:\n    post:\n      operationId: srtConnsKick\n      tags: [SRT]\n      summary: kicks out a SRT connection from the server.\n      description: ''\n      parameters:\n      - name: id\n        in: path\n        required: true\n        description: ID of the connection.\n        schema:\n          type: string\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/OK'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '404':\n          description: connection not found.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/webrtcsessions/list:\n    get:\n      operationId: webrtcSessionsList\n      tags: [WebRTC]\n      summary: returns all WebRTC sessions.\n      description: ''\n      parameters:\n      - name: page\n        in: query\n        description: page number.\n        schema:\n          type: integer\n          default: 0\n      - name: itemsPerPage\n        in: query\n        description: items per page.\n        schema:\n          type: integer\n          default: 100\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/WebRTCSessionList'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/webrtcsessions/get/{id}:\n    get:\n      operationId: webrtcSessionsGet\n      tags: [WebRTC]\n      summary: returns a WebRTC session.\n      description: ''\n      parameters:\n      - name: id\n        in: path\n        required: true\n        description: ID of the session.\n        schema:\n          type: string\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/WebRTCSession'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '404':\n          description: session not found.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/webrtcsessions/kick/{id}:\n    post:\n      operationId: webrtcSessionsKick\n      tags: [WebRTC]\n      summary: kicks out a WebRTC session from the server.\n      description: ''\n      parameters:\n      - name: id\n        in: path\n        required: true\n        description: ID of the session.\n        schema:\n          type: string\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/OK'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '404':\n          description: session not found.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/recordings/list:\n    get:\n      operationId: recordingsList\n      tags: [Recordings]\n      summary: returns all recordings, splitted by path.\n      description: ''\n      parameters:\n      - name: page\n        in: query\n        description: page number.\n        schema:\n          type: integer\n          default: 0\n      - name: itemsPerPage\n        in: query\n        description: items per page.\n        schema:\n          type: integer\n          default: 100\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/RecordingList'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/recordings/get/{name}:\n    get:\n      operationId: recordingsGet\n      tags: [Recordings]\n      summary: returns recordings of a path.\n      description: ''\n      parameters:\n      - name: name\n        in: path\n        required: true\n        description: name of the path.\n        schema:\n          type: string\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Recording'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '404':\n          description: path not found.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n\n  /v3/recordings/deletesegment:\n    delete:\n      operationId: recordingsDeleteSegment\n      tags: [Recordings]\n      summary: deletes a recording segment.\n      description: ''\n      parameters:\n      - name: path\n        in: query\n        required: true\n        description: path.\n        schema:\n          type: string\n      - name: start\n        in: query\n        required: true\n        description: starting date of the segment.\n        schema:\n          type: string\n      responses:\n        '200':\n          description: the request was successful.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/OK'\n        '400':\n          description: invalid request.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '404':\n          description: connection not found.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n        '500':\n          description: server error.\n          content:\n            application/json:\n              schema:\n                $ref: '#/components/schemas/Error'\n"
  },
  {
    "path": "docker/ffmpeg-rpi.Dockerfile",
    "content": "#################################################################\nFROM --platform=linux/amd64 scratch AS binaries\n\nADD binaries/mediamtx_*_linux_armv6.tar.gz /linux/arm/v6\nADD binaries/mediamtx_*_linux_armv7.tar.gz /linux/arm/v7\nADD binaries/mediamtx_*_linux_arm64.tar.gz /linux/arm64\n\n#################################################################\nFROM --platform=linux/arm/v7 debian:bullseye-slim AS base-arm-v7\n\n# even though the base image is arm v7,\n# Raspbian libraries and compilers provide arm v6 compatibility.\n\nRUN apt update \\\n\t&& apt install -y wget gpg \\\n\t&& echo \"deb http://archive.raspbian.org/raspbian bullseye main rpi firmware\" > /etc/apt/sources.list \\\n\t&& echo \"deb http://archive.raspberrypi.org/debian bullseye main\" > /etc/apt/sources.list.d/raspi.list \\\n\t&& wget -O- https://archive.raspbian.org/raspbian.public.key | gpg --dearmor -o /etc/apt/trusted.gpg.d/raspbian.gpg \\\n\t&& wget -O- https://archive.raspberrypi.org/debian/raspberrypi.gpg.key | gpg --dearmor -o /etc/apt/trusted.gpg.d/raspberrypi.gpg \\\n\t&& rm -rf /var/lib/apt/lists/*\n\nRUN apt update && apt install --reinstall -y \\\n    libc6 \\\n    libc-bin \\\n    libstdc++6 \\\n    && rm -rf /var/lib/apt/lists/*\n\n#################################################################\nFROM --platform=linux/arm64 debian:bullseye-slim AS base-arm64\n\nRUN apt update \\\n\t&& apt install -y wget gpg \\\n\t&& echo \"deb http://archive.raspberrypi.org/debian bullseye main\" > /etc/apt/sources.list.d/raspi.list \\\n\t&& wget -O- https://archive.raspberrypi.org/debian/raspberrypi.gpg.key | gpg --dearmor -o /etc/apt/trusted.gpg.d/raspberrypi.gpg \\\n\t&& rm -rf /var/lib/apt/lists/*\n\n#################################################################\nFROM --platform=linux/amd64 scratch AS base\n\nCOPY --from=base-arm-v7 / /linux/arm/v6\nCOPY --from=base-arm-v7 / /linux/arm/v7\nCOPY --from=base-arm64 / /linux/arm64\n\n#################################################################\nFROM scratch\n\nARG TARGETPLATFORM\nCOPY --from=base /$TARGETPLATFORM /\n\nRUN apt update \\\n    && apt install -y --no-install-recommends ffmpeg \\\n    && rm -rf /var/lib/apt/lists/*\n\nCOPY --from=binaries /$TARGETPLATFORM /\n\nENTRYPOINT [ \"/mediamtx\" ]\n"
  },
  {
    "path": "docker/ffmpeg.Dockerfile",
    "content": "#################################################################\nFROM --platform=linux/amd64 scratch AS binaries\n\nADD binaries/mediamtx_*_linux_amd64.tar.gz /linux/amd64\nADD binaries/mediamtx_*_linux_armv6.tar.gz /linux/arm/v6\nADD binaries/mediamtx_*_linux_armv7.tar.gz /linux/arm/v7\nADD binaries/mediamtx_*_linux_arm64.tar.gz /linux/arm64\n\n#################################################################\nFROM alpine:3.22\n\nRUN apk add --no-cache ffmpeg\n\nARG TARGETPLATFORM\nCOPY --from=binaries /$TARGETPLATFORM /\n\nENTRYPOINT [ \"/mediamtx\" ]\n"
  },
  {
    "path": "docker/rpi.Dockerfile",
    "content": "#################################################################\nFROM --platform=linux/amd64 scratch AS binaries\n\nADD binaries/mediamtx_*_linux_armv6.tar.gz /linux/arm/v6\nADD binaries/mediamtx_*_linux_armv7.tar.gz /linux/arm/v7\nADD binaries/mediamtx_*_linux_arm64.tar.gz /linux/arm64\n\n#################################################################\nFROM --platform=linux/arm/v7 debian:bullseye-slim AS base-arm-v7\n\n# even though the base image is arm v7,\n# Raspbian libraries and compilers provide arm v6 compatibility.\n\nRUN apt update \\\n\t&& apt install -y wget gpg \\\n\t&& echo \"deb http://archive.raspbian.org/raspbian bullseye main rpi firmware\" > /etc/apt/sources.list \\\n\t&& echo \"deb http://archive.raspberrypi.org/debian bullseye main\" > /etc/apt/sources.list.d/raspi.list \\\n\t&& wget -O- https://archive.raspbian.org/raspbian.public.key | gpg --dearmor -o /etc/apt/trusted.gpg.d/raspbian.gpg \\\n\t&& wget -O- https://archive.raspberrypi.org/debian/raspberrypi.gpg.key | gpg --dearmor -o /etc/apt/trusted.gpg.d/raspberrypi.gpg \\\n\t&& rm -rf /var/lib/apt/lists/*\n\nRUN apt update && apt install --reinstall -y \\\n    libc6 \\\n    libc-bin \\\n    libstdc++6 \\\n    && rm -rf /var/lib/apt/lists/*\n\n#################################################################\nFROM --platform=linux/arm64 debian:bullseye-slim AS base-arm64\n\nRUN apt update \\\n\t&& apt install -y wget gpg \\\n\t&& echo \"deb http://archive.raspberrypi.org/debian bullseye main\" > /etc/apt/sources.list.d/raspi.list \\\n\t&& wget -O- https://archive.raspberrypi.org/debian/raspberrypi.gpg.key | gpg --dearmor -o /etc/apt/trusted.gpg.d/raspberrypi.gpg \\\n\t&& rm -rf /var/lib/apt/lists/*\n\n#################################################################\nFROM --platform=linux/amd64 scratch AS base\n\nCOPY --from=base-arm-v7 / /linux/arm/v6\nCOPY --from=base-arm-v7 / /linux/arm/v7\nCOPY --from=base-arm64 / /linux/arm64\n\n#################################################################\nFROM scratch\n\nARG TARGETPLATFORM\nCOPY --from=base /$TARGETPLATFORM /\n\nCOPY --from=binaries /$TARGETPLATFORM /\n\nENTRYPOINT [ \"/mediamtx\" ]\n"
  },
  {
    "path": "docker/standard.Dockerfile",
    "content": "#################################################################\nFROM --platform=linux/amd64 scratch AS binaries\n\nADD binaries/mediamtx_*_linux_amd64.tar.gz /linux/amd64\nADD binaries/mediamtx_*_linux_armv6.tar.gz /linux/arm/v6\nADD binaries/mediamtx_*_linux_armv7.tar.gz /linux/arm/v7\nADD binaries/mediamtx_*_linux_arm64.tar.gz /linux/arm64\n\n#################################################################\nFROM scratch\n\nARG TARGETPLATFORM\nCOPY --from=binaries /$TARGETPLATFORM /\n\nENTRYPOINT [ \"/mediamtx\" ]\n"
  },
  {
    "path": "docs/1-kickoff/1-introduction.md",
    "content": "# Introduction\n\nWelcome to the MediaMTX documentation!\n\n_MediaMTX_ is a ready-to-use and zero-dependency live media server and media proxy. It has been conceived as a \"media router\" that routes media streams from one end to the other, with a focus on efficiency and portability.\n\nMain features:\n\n- [Publish](../2-publish/01-overview.md) live streams to the server with SRT, WebRTC, RTSP, RTMP, HLS, MPEG-TS, RTP, using FFmpeg, GStreamer, OBS Studio, Python , Golang, Unity, Web browsers, Raspberry Pi Cameras and more.\n- [Read](../3-read/01-overview.md) live streams from the server with SRT, WebRTC, RTSP, RTMP, HLS, using FFmpeg, GStreamer, VLC, OBS Studio, Python , Golang, Unity, Web browsers and more.\n- Streams are automatically converted from a protocol to another\n- Serve several streams at once in separate paths\n- Reload the configuration without disconnecting existing clients (hot reloading)\n- [Serve always-available streams](../4-other/05-always-available.md) even when the publisher is offline\n- [Record](../4-other/06-record.md) streams to disk in fMP4 or MPEG-TS format\n- [Playback](../4-other/07-playback.md) recorded streams\n- [Authenticate](../4-other/03-authentication.md) users with internal, HTTP or JWT authentication\n- [Forward](../4-other/08-forward.md) streams to other servers\n- [Proxy](../4-other/09-proxy.md) requests to other servers\n- [Control](../4-other/18-control-api.md) the server through the Control API\n- [Extract metrics](../4-other/19-metrics.md) from the server in a Prometheus-compatible format\n- [Monitor performance](../4-other/20-performance.md) to investigate CPU and RAM consumption\n- [Run hooks](../4-other/17-hooks.md) (external commands) when clients connect, disconnect, read or publish streams\n- Compatible with Linux, Windows and macOS, does not require any dependency or interpreter, it's a single executable\n\nUse the menu to navigate through the documentation.\n"
  },
  {
    "path": "docs/1-kickoff/2-install.md",
    "content": "# Install\n\nThere are several installation methods available:\n\n- [Standalone binary](#standalone-binary): use this if you are running Windows, macOS or you just want to try out _MediaMTX_.\n- [Docker image](#docker-image): use this if you want to run _MediaMTX_ in an isolated and deterministic way. This is recommended for production environments.\n- [Arch Linux package](#arch-linux-package): use this if you are running Arch Linux.\n- [FreeBSD package](#freebsd-package): use this if you are running FreeBSD.\n- [OpenWrt binary](#openwrt-binary): use this if you are running OpenWrt.\n\n## Standalone binary\n\n1. Visit the [Releases page](https://github.com/bluenviron/mediamtx/releases) on GitHub, download and extract a standalone binary that corresponds to your operating system and architecture (example: `mediamtx_{version_tag}_linux_amd64.tar.gz`).\n\n2. Start the server by double clicking on `mediamtx` (`mediamtx.exe` on Windows) or writing in the terminal:\n\n   ```sh\n   ./mediamtx\n   ```\n\n## Docker image\n\nDownload and launch the `bluenviron/mediamtx:1` image with the following environment variables and ports:\n\n```sh\ndocker run --rm -it \\\n-e MTX_RTSPTRANSPORTS=tcp \\\n-e MTX_WEBRTCADDITIONALHOSTS=192.168.x.x \\\n-p 8554:8554 \\\n-p 1935:1935 \\\n-p 8888:8888 \\\n-p 8889:8889 \\\n-p 8890:8890/udp \\\n-p 8189:8189/udp \\\nbluenviron/mediamtx:1\n```\n\nFill the `MTX_WEBRTCADDITIONALHOSTS` environment variable with the IP that will be used to connect to the server.\n\nThe `MTX_RTSPTRANSPORTS=tcp` environment variable is meant to disable the UDP transport protocol of the RTSP server (which requires the real IP address and port of incoming UDP packets, that are sometimes replaced by the Docker network stack). If you want to use it, you need to bypass the Docker network stack through the `--network=host` flag (which is not compatible with Windows, macOS and Kubernetes):\n\n```sh\ndocker run --rm -it --network=host bluenviron/mediamtx:1\n```\n\nThere are four image variants:\n\n| name                             | FFmpeg included    | RPI Camera support |\n| -------------------------------- | ------------------ | ------------------ |\n| bluenviron/mediamtx:1            | :x:                | :x:                |\n| bluenviron/mediamtx:1-ffmpeg     | :heavy_check_mark: | :x:                |\n| bluenviron/mediamtx:1-rpi        | :x:                | :heavy_check_mark: |\n| bluenviron/mediamtx:1-ffmpeg-rpi | :heavy_check_mark: | :heavy_check_mark: |\n\nThe `1` tag corresponds to the latest `1.x.x` release, that should guarantee backward compatibility when upgrading. It is also possible to bind the image to a specific release, by using the release name as tag (`bluenviron/mediamtx:{docker_version_tag}`).\n\nThe base image does not contain any utility, in order to minimize size and frequency of updates. If you need additional software (like curl, wget, GStreamer), you can build a custom image by creating a file named `Dockerfile` with this content:\n\n```Dockerfile\nFROM bluenviron/mediamtx:1 AS mediamtx\nFROM ubuntu:24.04\n\nCOPY --from=mediamtx /mediamtx /\nCOPY --from=mediamtx /mediamtx.yml /\n\n# add anything you need.\nRUN apt update && apt install -y \\\n   gstreamer1.0-tools\n\nENTRYPOINT [ \"/mediamtx\" ]\n```\n\nAnd then build it:\n\n```sh\ndocker build . -t my-mediamtx\n```\n\nIn particular, the custom image is using the official _MediaMTX_ image as a base stage, and then adds a Linux-based operating system on top of it. Since _MediaMTX_ binaries are not tied to a specific Linux distribution or version, you can use anything you like.\n\n## Arch Linux package\n\nIf you are running the Arch Linux distribution, launch:\n\n```sh\ngit clone https://aur.archlinux.org/mediamtx.git\ncd mediamtx\nmakepkg -si\n```\n\n## FreeBSD package\n\nAvailable via ports tree or using packages (2025Q2 and later) as listed below:\n\n```sh\ncd /usr/ports/multimedia/mediamtx && make install clean\npkg install mediamtx\n```\n\n## OpenWrt binary\n\nIf the architecture of the OpenWrt device is amd64, armv6, armv7 or arm64, use the [standalone binary method](#standalone-binary) and download a Linux binary that corresponds to your architecture.\n\nOtherwise, [compile the server from source](../6-misc/1-compile.md).\n"
  },
  {
    "path": "docs/1-kickoff/3-upgrade.md",
    "content": "# Upgrade\n\nIf you have an existing _MediaMTX_ installation, you can upgrade it to the latest version. The procedure depends on how _MediaMTX_ was installed.\n\n## Standalone binary\n\nThe standalone binary comes with an upgrade utility that can be launched with:\n\n```sh\n./mediamtx --upgrade\n```\n\nThis will replace the _MediaMTX_ executable with its latest version. Privileges to write to the executable location are required.\n\n## Docker image\n\nStop and remove the container:\n\n```sh\ndocker stop id-of-mediamtx-container\ndocker rm id-of-mediamtx-container\n```\n\nRemove the _MediaMTX_ image from cache:\n\n```sh\ndocker rm bluenviron/mediamtx:1\n```\n\nThen recreate the container as described in [Install](2-install.md#docker-image).\n\n## Arch Linux package\n\nRepeat the installation procedure.\n\n## FreeBSD package\n\nRepeat the installation procedure.\n\n## OpenWrt binary\n\nIf the architecture of the OpenWrt device is amd64, armv6, armv7 or arm64, you can use the standalone binary method.\n\nOtherwise, recompile the server from source.\n"
  },
  {
    "path": "docs/1-kickoff/4-basic-usage.md",
    "content": "# Basic usage\n\n1. [Publish](../2-publish/01-overview.md) a stream. For instance, you can publish a stream from a MP4 file with _FFmpeg_:\n\n   ```sh\n   ffmpeg -re -stream_loop -1 -i file.mp4 -c copy \\\n   -f rtsp rtsp://localhost:8554/mystream\n   ```\n\n   or _GStreamer_:\n\n   ```sh\n   gst-launch-1.0 rtspclientsink name=s location=rtsp://localhost:8554/mystream filesrc location=file.mp4 \\\n   ! qtdemux name=d d.video_0 ! queue ! s.sink_0 d.audio_0 ! queue ! s.sink_1\n   ```\n\n2. [Read](../3-read/01-overview.md) the stream. For instance, you can read the stream with _VLC_:\n\n   ```sh\n   vlc --network-caching=50 rtsp://localhost:8554/mystream\n   ```\n\n   or _GStreamer_:\n\n   ```sh\n   gst-play-1.0 rtsp://localhost:8554/mystream\n   ```\n\n   or _FFmpeg_:\n\n   ```sh\n   ffmpeg -i rtsp://localhost:8554/mystream -c copy output.mp4\n   ```\n"
  },
  {
    "path": "docs/1-kickoff/index.md",
    "content": "# Kickoff\n"
  },
  {
    "path": "docs/2-publish/01-overview.md",
    "content": "# Publish a stream\n\nLive streams can be published to the server with the following protocols and codecs:\n\n| protocol                                                   | variants                                   | codecs                                                                                                                                                                                                                                                 |\n| ---------------------------------------------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [SRT clients](02-srt-clients.md)                           |                                            | **Video**: H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video<br/>**Audio**: Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3<br/>**Other**: KLV                                                                                                |\n| [SRT cameras and servers](03-srt-cameras-and-servers.md)   |                                            | **Video**: H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video<br/>**Audio**: Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3<br/>**Other**: KLV                                                                                                |\n| [WebRTC clients](04-webrtc-clients.md)                     | WHIP                                       | **Video**: AV1, VP9, VP8, H265, H264<br/>**Audio**: Opus, G722, G711 (PCMA, PCMU)                                                                                                                                                                      |\n| [WebRTC servers](05-webrtc-servers.md)                     | WHEP                                       | **Video**: AV1, VP9, VP8, H265, H264<br/>**Audio**: Opus, G722, G711 (PCMA, PCMU)                                                                                                                                                                      |\n| [RTSP clients](06-rtsp-clients.md)                         | UDP, TCP, RTSPS                            | **Video**: AV1, VP9, VP8, H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video, MJPEG<br/>**Audio**: Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, G726, G722, G711 (PCMA, PCMU), LPCM<br/>**Other**: KLV, MPEG-TS, any RTP-compatible codec  |\n| [RTSP cameras and servers](07-rtsp-cameras-and-servers.md) | UDP, UDP-Multicast, TCP, RTSPS             | **Video**: AV1, VP9, VP8, H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video, MJPEG<br/>**Audio**: Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, G726, G722, G711 (PCMA, PCMU), LPCM<br/>**Other**: KLV, MPEG-TS, any RTP-compatible codec  |\n| [RTMP clients](08-rtmp-clients.md)                         | RTMP, RTMPS, Enhanced RTMP                 | **Video**: AV1, VP9, H265, H264<br/>**Audio**: Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, G711 (PCMA, PCMU), LPCM                                                                                                                           |\n| [RTMP cameras and servers](09-rtmp-cameras-and-servers.md) | RTMP, RTMPS, Enhanced RTMP                 | **Video**: AV1, VP9, H265, H264<br/>**Audio**: Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, G711 (PCMA, PCMU), LPCM                                                                                                                           |\n| [HLS cameras and servers](10-hls-cameras-and-servers.md)   | Low-Latency HLS, MP4-based HLS, legacy HLS | **Video**: AV1, VP9, H265, H264<br/>**Audio**: Opus, MPEG-4 Audio (AAC)                                                                                                                                                                                |\n| [MPEG-TS](11-mpeg-ts.md)                                   | MPEG-TS over UDP, MPEG-TS over Unix socket | **Video**: H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video<br/>**Audio**: Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3<br/>**Other**: KLV                                                                                                |\n| [RTP](12-rtp.md)                                           | RTP over UDP                               | **Video**: AV1, VP9, VP8, H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video, M-JPEG<br/>**Audio**: Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, G726, G722, G711 (PCMA, PCMU), LPCM<br/>**Other**: KLV, MPEG-TS, any RTP-compatible codec |\n\nWe provide instructions for publishing with the following devices:\n\n- [Raspberry Pi Cameras](13-raspberry-pi-cameras.md)\n- [Generic webcams](14-generic-webcams.md)\n\nWe provide instructions for publishing with the following software:\n\n- [FFmpeg](15-ffmpeg.md)\n- [GStreamer](16-gstreamer.md)\n- [OBS Studio](17-obs-studio.md)\n- [Python and OpenCV](18-python-opencv.md)\n- [Golang](19-golang.md)\n- [Unity](20-unity.md)\n- [Web browsers](21-web-browsers.md)\n"
  },
  {
    "path": "docs/2-publish/02-srt-clients.md",
    "content": "# SRT clients\n\nSRT is a protocol that allows to publish and read live data stream, providing encryption, integrity and a retransmission mechanism. It is usually used to transfer media streams encoded with MPEG-TS. In order to publish a stream to the server with the SRT protocol, use this URL:\n\n```\nsrt://localhost:8890?streamid=publish:mystream&pkt_size=1316\n```\n\nReplace `mystream` with any name you want. The resulting stream will be available on path `/mystream`.\n\nIf you need to use the standard stream ID syntax instead of the custom one in use by this server, read [Standard stream ID syntax](../4-other/21-srt-specific-features.md#standard-stream-id-syntax).\n\nIf you want to publish a stream by using a client in listening mode (i.e. with `mode=listener` appended to the URL), read the next section.\n\nSome clients that can publish with SRT are [FFmpeg](15-ffmpeg.md), [GStreamer](16-gstreamer.md), [OBS Studio](17-obs-studio.md).\n"
  },
  {
    "path": "docs/2-publish/03-srt-cameras-and-servers.md",
    "content": "# SRT cameras and servers\n\nIn order to ingest a SRT stream from a remote server, camera or client in listening mode (i.e. with `mode=listener` appended to the URL), add the corresponding URL into the `source` parameter of a path:\n\n```yml\npaths:\n  proxied:\n    # url of the source stream, in the format srt://host:port?streamid=streamid&other_parameters\n    source: srt://original-url\n```\n"
  },
  {
    "path": "docs/2-publish/04-webrtc-clients.md",
    "content": "# WebRTC clients\n\nWebRTC is an API that makes use of a set of protocols and methods to connect two clients together and allow them to exchange live media or data streams. You can publish a stream with WebRTC and a web browser by visiting:\n\n```\nhttp://localhost:8889/mystream/publish\n```\n\nThe resulting stream will be available on path `/mystream`.\n\nWHIP is a WebRTC extension that allows to publish streams by using a URL, without passing through a web page. This allows to use WebRTC as a general purpose streaming protocol. If you are using a software that supports WHIP (for instance, latest versions of OBS Studio), you can publish a stream to the server by using this URL:\n\n```\nhttp://localhost:8889/mystream/whip\n```\n\nBe aware that not all browsers can read any codec, check [Supported browsers](../4-other/22-webrtc-specific-features.md#supported-browsers).\n\nDepending on the network it might be difficult to establish a connection between server and clients, read [Solving WebRTC connectivity issues](../4-other/22-webrtc-specific-features.md#solving-webrtc-connectivity-issues).\n\nSome clients that can publish with WebRTC and WHIP are [FFmpeg](15-ffmpeg.md), [GStreamer](16-gstreamer.md), [OBS Studio](17-obs-studio.md), [Unity](20-unity.md) and [Web browsers](21-web-browsers.md).\n"
  },
  {
    "path": "docs/2-publish/05-webrtc-servers.md",
    "content": "# WebRTC servers\n\nIn order to ingest a WebRTC stream from a remote server, add the corresponding WHEP URL into the `source` parameter of a path:\n\n```yml\npaths:\n  proxied:\n    # url of the source stream, in the format whep://host:port/path (HTTP) or wheps:// (HTTPS)\n    source: wheps://host:port/path\n```\n\nIf the remote server is a _MediaMTX_ instance, remember to add a `/whep` suffix after the stream name, since in _MediaMTX_ [it's part of the WHEP URL](../3-read/03-webrtc.md):\n\n```yml\npaths:\n  proxied:\n    source: whep://host:port/mystream/whep\n```\n"
  },
  {
    "path": "docs/2-publish/06-rtsp-clients.md",
    "content": "# RTSP clients\n\nRTSP is a protocol that allows to publish and read streams. It supports several underlying transport protocols and encryption. In order to publish a stream to the server with the RTSP protocol, use this URL:\n\n```\nrtsp://localhost:8554/mystream\n```\n\nThe resulting stream will be available on path `/mystream`.\n\nSome clients that can publish with RTSP are [FFmpeg](15-ffmpeg.md), [GStreamer](16-gstreamer.md), [OBS Studio](17-obs-studio.md), [Python and OpenCV](18-python-opencv.md).\n\nAdvanced RTSP features and settings are described in [RTSP-specific features](../4-other/23-rtsp-specific-features.md).\n\n## MPEG-TS inside RTSP\n\nSome RTSP clients encode tracks with MPEG-TS before sending them to the server, causing the server to see a single \"MPEG-TS\" track, and preventing track conversion from a protocol to another.\n\nIt's possible to automatically demux these MPEG-TS-encoded streams, by toggling `rtspDemuxMpegts`:\n\n```yml\npathDefaults:\n  # Demux MPEG-TS over RTSP into elementary streams.\n  # When enabled, RTSP publishers sending MP2T/90000 will be demultiplexed\n  # and their elementary streams (H.264, H.265, AAC, etc.) exposed as native tracks.\n  # This allows HLS, WebRTC, and other outputs to work transparently with MPEG-TS sources.\n  rtspDemuxMpegts: true\n```\n"
  },
  {
    "path": "docs/2-publish/07-rtsp-cameras-and-servers.md",
    "content": "# RTSP cameras and servers\n\nMost IP cameras expose their video stream by using a RTSP server that is embedded into the camera itself. In particular, cameras that are compliant with ONVIF profile S or T meet this requirement. You can use _MediaMTX_ to connect to one or several existing RTSP servers and read their media streams:\n\n```yml\npaths:\n  proxied:\n    # url of the source stream, in the format rtsp://user:pass@host:port/path\n    source: rtsp://original-url\n```\n\nThe resulting stream will be available on path `/proxied`.\n\nIt is possible to tune the connection by using some additional parameters:\n\n```yml\npaths:\n  proxied:\n    # url of the source stream, in the format rtsp://user:pass@host:port/path\n    source: rtsp://original-url\n    # Transport protocol used to pull the stream. available values are \"automatic\", \"udp\", \"multicast\", \"tcp\".\n    rtspTransport: automatic\n    # Support sources that don't provide server ports or use random server ports. This is a security issue\n    # and must be used only when interacting with sources that require it.\n    rtspAnyPort: no\n    # Range header to send to the source, in order to start streaming from the specified offset.\n    # available values:\n    # * clock: Absolute time\n    # * npt: Normal Play Time\n    # * smpte: SMPTE timestamps relative to the start of the recording\n    rtspRangeType:\n    # Available values:\n    # * clock: UTC ISO 8601 combined date and time string, e.g. 20230812T120000Z\n    # * npt: duration such as \"300ms\", \"1.5m\" or \"2h45m\", valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\"\n    # * smpte: duration such as \"300ms\", \"1.5m\" or \"2h45m\", valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\"\n    rtspRangeStart:\n    # Size of the UDP buffer of the RTSP client.\n    # This can be increased to mitigate packet losses.\n    # It defaults to the default value of the operating system.\n    rtspUDPReadBufferSize: 0\n    # Range of ports used as source port in outgoing UDP packets.\n    rtspUDPSourcePortRange: [10000, 65535]\n```\n\nAll available parameters are listed in the [configuration file](../5-references/1-configuration-file.md).\n\nAdvanced RTSP features and settings are described in [RTSP-specific features](../4-other/23-rtsp-specific-features.md).\n"
  },
  {
    "path": "docs/2-publish/08-rtmp-clients.md",
    "content": "# RTMP clients\n\nRTMP is a protocol that allows to read and publish streams. It supports encryption, read [RTMP-specific features](../4-other/24-rtmp-specific-features.md). Streams can be published to the server by using the URL:\n\n```\nrtmp://localhost/mystream\n```\n\nThe resulting stream will be available on path `/mystream`.\n\nSome clients that can publish with RTMP are [FFmpeg](15-ffmpeg.md), [GStreamer](16-gstreamer.md), [OBS Studio](17-obs-studio.md).\n"
  },
  {
    "path": "docs/2-publish/09-rtmp-cameras-and-servers.md",
    "content": "# RTMP cameras and servers\n\nYou can use _MediaMTX_ to connect to one or several existing RTMP servers and read their media streams:\n\n```yml\npaths:\n  proxied:\n    # url of the source stream, in the format rtmp://user:pass@host:port/path\n    source: rtmp://original-url\n```\n\nThe resulting stream will be available on path `/proxied`.\n"
  },
  {
    "path": "docs/2-publish/10-hls-cameras-and-servers.md",
    "content": "# HLS cameras and servers\n\nHLS is a streaming protocol that works by splitting streams into segments, and by serving these segments and a playlist with the HTTP protocol. You can use _MediaMTX_ to connect to one or several existing HLS servers and read their media streams:\n\n```yml\npaths:\n  proxied:\n    # url of the playlist of the stream, in the format http://user:pass@host:port/path\n    source: http://original-url/stream/index.m3u8\n```\n\nThe resulting stream will be available on path `/proxied`.\n"
  },
  {
    "path": "docs/2-publish/11-mpeg-ts.md",
    "content": "# MPEG-TS\n\nThe server supports ingesting MPEG-TS streams, shipped in two different ways (UDP packets or Unix sockets).\n\nIn order to read a UDP MPEG-TS stream, edit `mediamtx.yml` and replace everything inside section `paths` with the following content:\n\n```yml\npaths:\n  mypath:\n    source: udp+mpegts://238.0.0.1:1234\n```\n\nWhere `238.0.0.1` is the IP for listening packets, in this case a multicast IP.\n\nIf the listening IP is a multicast IP, _MediaMTX_ will listen for incoming packets on the default multicast interface, picked by the operating system. It is possible to specify the interface manually by using the `interface` parameter:\n\n```yml\npaths:\n  mypath:\n    source: udp+mpegts://238.0.0.1:1234?interface=eth0\n```\n\nIt is possible to restrict who can send packets by using the `source` parameter:\n\n```yml\npaths:\n  mypath:\n    source: udp+mpegts://0.0.0.0:1234?source=192.168.3.5\n```\n\nSome clients that can publish with UDP and MPEG-TS are [FFmpeg](15-ffmpeg.md) and [GStreamer](16-gstreamer.md).\n\nUnix sockets are more efficient than UDP packets and can be used as transport by specifying the `unix+mpegts` scheme:\n\n```yml\npaths:\n  mypath:\n    source: unix+mpegts:///tmp/socket.sock\n```\n"
  },
  {
    "path": "docs/2-publish/12-rtp.md",
    "content": "# RTP\n\nThe server supports ingesting RTP streams, transmitted with UDP packets.\n\nIn order to read a UDP RTP stream, edit `mediamtx.yml` and replace everything inside section `paths` with the following content:\n\n```yml\npaths:\n  mypath:\n    source: udp+rtp://238.0.0.1:1234\n    rtpSDP: |\n      v=0\n      o=- 123456789 123456789 IN IP4 192.168.1.100\n      s=H264 Video Stream\n      c=IN IP4 192.168.1.100\n      t=0 0\n      m=video 5004 RTP/AVP 96\n      a=rtpmap:96 H264/90000\n      a=fmtp:96 profile-level-id=42e01e;packetization-mode=1;sprop-parameter-sets=Z0LAHtkDxWhAAAADAEAAAAwDxYuS,aMuMsg==\n```\n\n`rtpSDP` must contain a valid SDP, that is a description of the RTP session.\n\nSome clients that can publish with UDP and MPEG-TS are [FFmpeg](15-ffmpeg.md) and [GStreamer](16-gstreamer.md).\n"
  },
  {
    "path": "docs/2-publish/13-raspberry-pi-cameras.md",
    "content": "# Raspberry Pi Cameras\n\n_MediaMTX_ natively supports most Raspberry Pi Camera models, enabling high-quality and low-latency video streaming from the camera to any user, for any purpose. There are some additional requirements:\n\n1. The server must run on a Raspberry Pi, with one of the following operating systems:\n   - Raspberry Pi OS Trixie\n   - Raspberry Pi OS Bookworm\n   - Raspberry Pi OS Bullseye\n\n   Both 32-bit and 64-bit architectures are supported.\n\n2. If you are using Raspberry Pi OS Bullseye, make sure that the legacy camera stack is disabled. Type `sudo raspi-config`, then go to `Interfacing options`, `enable/disable legacy camera support`, choose `no`. Reboot the system.\n\nThe setup procedure depends on whether you want to run the server outside or inside Docker:\n\n- If you want to run the standard (non-Dockerized) version of the server:\n  1. Download the server executable. If you're using 64-bit version of the operative system, make sure to pick the `arm64` variant.\n\n  2. Edit `mediamtx.yml` and replace everything inside section `paths` with the following content:\n\n  ```yml\n  paths:\n    cam:\n      source: rpiCamera\n  ```\n\n  The resulting stream will be available on path `/cam`.\n\n- If you want to run the server inside Docker, you need to use the `1-rpi` image and launch the container with some additional flags:\n\n  ```sh\n  docker run --rm -it \\\n  --network=host \\\n  --privileged \\\n  --tmpfs /dev/shm:exec \\\n  -v /run/udev:/run/udev:ro \\\n  -e MTX_PATHS_CAM_SOURCE=rpiCamera \\\n  bluenviron/mediamtx:1-rpi\n  ```\n\nThe Raspberry Pi Camera can be controlled through a wide range of parameters, that are listed in the [configuration file](../5-references/1-configuration-file.md).\n\nBe aware that cameras that require a custom `libcamera` (like some ArduCam products) are not compatible with precompiled binaries and Docker images of _MediaMTX_, since these come with a bundled `libcamera`. If you want to use a custom one, you need to [compile from source](../6-misc/1-compile.md#custom-libcamera).\n\n## Adding audio\n\nIn order to add audio from a USB microphone, install GStreamer and alsa-utils:\n\n```sh\nsudo apt install -y gstreamer1.0-tools gstreamer1.0-rtsp gstreamer1.0-alsa alsa-utils\n```\n\nlist available audio cards with:\n\n```sh\narecord -L\n```\n\nSample output:\n\n```\nsurround51:CARD=ICH5,DEV=0\n    Intel ICH5, Intel ICH5\n    5.1 Surround output to Front, Center, Rear and Subwoofer speakers\ndefault:CARD=U0x46d0x809\n    USB Device 0x46d:0x809, USB Audio\n    Default Audio Device\n```\n\nFind the audio card of the microphone and take note of its name, for instance `default:CARD=U0x46d0x809`. Then create a new path that takes the video stream from the camera and audio from the microphone:\n\n```yml\npaths:\n  cam:\n    source: rpiCamera\n\n  cam_with_audio:\n    runOnInit: >\n      gst-launch-1.0\n      rtspclientsink name=s location=rtsp://localhost:$RTSP_PORT/cam_with_audio\n      rtspsrc location=rtsp://127.0.0.1:$RTSP_PORT/cam latency=0 ! rtph264depay ! s.\n      alsasrc device=default:CARD=U0x46d0x809 ! opusenc bitrate=16000 ! s.\n    runOnInitRestart: yes\n```\n\nThe resulting stream will be available on path `/cam_with_audio`.\n\n## Secondary stream\n\nIt is possible to enable a secondary stream from the same camera, with a different resolution, FPS and codec. Configuration is the same of a primary stream, with `rpiCameraSecondary` set to `true` and parameters adjusted accordingly:\n\n```yml\npaths:\n  # primary stream\n  rpi:\n    source: rpiCamera\n    # Width of frames.\n    rpiCameraWidth: 1920\n    # Height of frames.\n    rpiCameraHeight: 1080\n    # FPS.\n    rpiCameraFPS: 30\n\n  # secondary stream\n  secondary:\n    source: rpiCamera\n    # This is a secondary stream.\n    rpiCameraSecondary: true\n    # Width of frames.\n    rpiCameraWidth: 640\n    # Height of frames.\n    rpiCameraHeight: 480\n    # FPS.\n    rpiCameraFPS: 10\n    # Codec. in case of secondary streams, it defaults to M-JPEG.\n    rpiCameraCodec: auto\n    # JPEG quality.\n    rpiCameraMJPEGQuality: 60\n```\n\nThe secondary stream will be available on path `/secondary`.\n"
  },
  {
    "path": "docs/2-publish/14-generic-webcams.md",
    "content": "# Generic webcams\n\nIf the operating system is Linux, edit `mediamtx.yml` and replace everything inside section `paths` with the following content:\n\n```yml\npaths:\n  cam:\n    runOnInit: ffmpeg -f v4l2 -i /dev/video0 -c:v libx264 -pix_fmt yuv420p -preset ultrafast -b:v 600k -f rtsp rtsp://localhost:$RTSP_PORT/$MTX_PATH\n    runOnInitRestart: yes\n```\n\nIf the operating system is Windows:\n\n```yml\npaths:\n  cam:\n    runOnInit: ffmpeg -f dshow -i video=\"USB2.0 HD UVC WebCam\" -c:v libx264 -pix_fmt yuv420p -preset ultrafast -b:v 600k -f rtsp rtsp://localhost:$RTSP_PORT/$MTX_PATH\n    runOnInitRestart: yes\n```\n\nWhere `USB2.0 HD UVC WebCam` is the name of a webcam, that can be obtained with:\n\n```sh\nffmpeg -list_devices true -f dshow -i dummy\n```\n\nThe resulting stream will be available on path `/cam`.\n"
  },
  {
    "path": "docs/2-publish/15-ffmpeg.md",
    "content": "# FFmpeg\n\nFFmpeg can publish a stream to the server in several ways. The recommended one consists in publishing with RTSP.\n\n## FFmpeg and RTSP\n\n```sh\nffmpeg -re -stream_loop -1 -i file.mp4 -c copy -f rtsp rtsp://localhost:8554/mystream\n```\n\nThe resulting stream will be available on path `/mystream`.\n\n## FFmpeg and RTMP\n\n```sh\nffmpeg -re -stream_loop -1 -i file.mp4 -c copy -f flv rtmp://localhost:1935/mystream\n```\n\n## FFmpeg and MPEG-TS over UDP\n\nIn _MediaMTX_ configuration, add a path with `source: udp+mpegts://238.0.0.1:1234`. Then:\n\n```sh\nffmpeg -re -stream_loop -1 -i file.mp4 -c copy -f mpegts 'udp://238.0.0.1:1234?pkt_size=1316'\n```\n\n## FFmpeg and MPEG-TS over Unix socket\n\nIn _MediaMTX_ configuration, add a path with `source: unix+mpegts:///tmp/socket.sock`. Then:\n\n```sh\nffmpeg -re -f lavfi -i testsrc=size=1280x720:rate=30 \\\n-c:v libx264 -pix_fmt yuv420p -preset ultrafast -b:v 600k \\\n-f mpegts unix:/tmp/socket.sock\n```\n\n## FFmpeg and RTP over UDP\n\nIn _MediaMTX_ configuration, add a path with `source: udp+rtp://238.0.0.1:1234` and a valid `rtpSDP` (read [RTP](12-rtp.md)). Then:\n\n```sh\nffmpeg -re -f lavfi -i testsrc=size=1280x720:rate=30 \\\n-c:v libx264 -pix_fmt yuv420p -preset ultrafast -b:v 600k \\\n-f rtp udp://238.0.0.1:1234?pkt_size=1316\n```\n\n## FFmpeg and SRT\n\n```sh\nffmpeg -re -stream_loop -1 -i file.mp4 -c copy -f mpegts 'srt://localhost:8890?streamid=publish:stream&pkt_size=1316'\n```\n\n## FFmpeg and WebRTC\n\n```sh\nffmpeg -re -f lavfi -i testsrc=size=1280x720:rate=30 \\\n-f lavfi -i \"sine=frequency=1000:sample_rate=48000\" \\\n-c:v libx264 -pix_fmt yuv420p -preset ultrafast -b:v 600k \\\n-c:a libopus -ar 48000 -ac 2 -b:a 128k \\\n-f whip http://localhost:8889/stream/whip\n```\n\nWARNING: in case of FFmpeg 8.0, a video track and an audio track must both be present.\n"
  },
  {
    "path": "docs/2-publish/16-gstreamer.md",
    "content": "# GStreamer\n\nGStreamer can publish a stream to the server in several ways. The recommended one consists in publishing with RTSP.\n\n## GStreamer and RTSP\n\n```sh\ngst-launch-1.0 rtspclientsink name=s location=rtsp://localhost:8554/mystream \\\nfilesrc location=file.mp4 ! qtdemux name=d \\\nd.video_0 ! queue ! s.sink_0 \\\nd.audio_0 ! queue ! s.sink_1\n```\n\nIf the stream is video only:\n\n```sh\ngst-launch-1.0 filesrc location=file.mp4 ! qtdemux name=d \\\nd.video_0 ! rtspclientsink location=rtsp://localhost:8554/mystream\n```\n\nThe resulting stream will be available on path `/mystream`.\n\nFor advanced options, read [RTSP-specific features](../4-other/23-rtsp-specific-features.md).\n\n## GStreamer and RTMP\n\n```sh\ngst-launch-1.0 -v flvmux name=mux ! rtmpsink location=rtmp://localhost/stream \\\nvideotestsrc ! video/x-raw,width=1280,height=720,format=I420 ! x264enc speed-preset=ultrafast bitrate=3000 key-int-max=60 ! video/x-h264,profile=high ! mux. \\\naudiotestsrc ! audioconvert ! avenc_aac ! mux.\n```\n\n## GStreamer and MPEG-TS over UDP\n\n```sh\ngst-launch-1.0 -v mpegtsmux name=mux alignment=1 ! udpsink host=238.0.0.1 port=1234 \\\nvideotestsrc ! video/x-raw,width=1280,height=720,format=I420 ! x264enc speed-preset=ultrafast bitrate=3000 key-int-max=60 ! video/x-h264,profile=high ! mux. \\\naudiotestsrc ! audioconvert ! avenc_aac ! mux.\n```\n\nFor advanced options, read [RTSP-specific features](../4-other/23-rtsp-specific-features.md).\n\n## GStreamer and WebRTC\n\nMake sure that GStreamer version is at least 1.22, and that if the codec is H264, the profile is baseline. Use the `whipclientsink` element:\n\n```sh\ngst-launch-1.0 videotestsrc \\\n! video/x-raw,width=1920,height=1080,format=I420 \\\n! x264enc speed-preset=ultrafast bitrate=2000 \\\n! video/x-h264,profile=baseline \\\n! whipclientsink signaller::whip-endpoint=http://localhost:8889/mystream/whip\n```\n"
  },
  {
    "path": "docs/2-publish/17-obs-studio.md",
    "content": "# OBS Studio\n\nOBS Studio can publish streams to the server in several ways. The recommended one consists in publishing with RTMP.\n\n## OBS Studio and RTMP\n\nIn `Settings -> Stream` (or in the Auto-configuration Wizard), use the following parameters:\n\n- Service: `Custom...`\n- Server: `rtmp://localhost/mystream`\n- Stream key: (empty)\n\nSave the configuration and click `Start streaming`.\n\nThe resulting stream will be available on path `/mystream`.\n\nIf you want to generate a stream that can be read with WebRTC, open `Settings -> Output -> Recording` and use the following parameters:\n\n- FFmpeg output type: `Output to URL`\n- File path or URL: `rtsp://localhost:8554/mystream`\n- Container format: `rtsp`\n- Check `show all codecs (even if potentially incompatible)`\n- Video encoder: `h264_nvenc (libx264)`\n- Video encoder settings (if any): `bf=0`\n- Audio track: `1`\n- Audio encoder: `libopus`\n\nThen use the button `Start Recording` (instead of `Start Streaming`) to start streaming.\n\n## OBS Studio and RTMP, multitrack video\n\nOBS Studio can publish multiple video tracks or renditions at once (simulcast). Make sure that the OBS Studio version is &ge; 31.0.0. Open `Settings -> Stream` and use the following parameters:\n\n- Service: `Custom...`\n- Server: `rtmp://localhost/mystream`\n- Stream key: (empty)\n- Turn on `Enable Multitrack Video`\n- Leave `Maximum Streaming Bandwidth` and `Maximum Video Tracks` to `Auto`\n- Turn on `Enable Config Override`\n- Fill `Config Override (JSON)` with the following text:\n\n  ```json\n  {\n    \"encoder_configurations\": [\n      {\n        \"type\": \"obs_x264\",\n        \"width\": 1920,\n        \"height\": 1080,\n        \"framerate\": {\n          \"numerator\": 30,\n          \"denominator\": 1\n        },\n        \"settings\": {\n          \"rate_control\": \"CBR\",\n          \"bitrate\": 6000,\n          \"keyint_sec\": 2,\n          \"preset\": \"veryfast\",\n          \"profile\": \"high\",\n          \"tune\": \"zerolatency\"\n        },\n        \"canvas_index\": 0\n      },\n      {\n        \"type\": \"obs_x264\",\n        \"width\": 640,\n        \"height\": 480,\n        \"framerate\": {\n          \"numerator\": 30,\n          \"denominator\": 1\n        },\n        \"settings\": {\n          \"rate_control\": \"CBR\",\n          \"bitrate\": 3000,\n          \"keyint_sec\": 2,\n          \"preset\": \"veryfast\",\n          \"profile\": \"main\",\n          \"tune\": \"zerolatency\"\n        },\n        \"canvas_index\": 0\n      }\n    ],\n    \"audio_configurations\": {\n      \"live\": [\n        {\n          \"codec\": \"ffmpeg_aac\",\n          \"track_id\": 1,\n          \"channels\": 2,\n          \"settings\": {\n            \"bitrate\": 160\n          }\n        }\n      ]\n    }\n  }\n  ```\n\n  This can be adjusted according to specific needs. In particular, the `type` field is used to set the video encoder, and these are the available parameters:\n  - `obs_nvenc_av1_tex`: NVIDIA NVENC AV1\n  - `obs_nvenc_hevc_tex`: NVIDIA NVENC H265\n  - `obs_nvenc_h264_tex`: NVIDIA NVENC H264\n  - `av1_texture_amf`: AMD AV1\n  - `h265_texture_amf`: AMD H265\n  - `h264_texture_amf`: AMD H264\n  - `obs_qsv11_av1`: QuickSync AV1\n  - `obs_qsv11_v2`: QuickSync H264\n  - `obs_x264`: software H264\n\nSave the configuration and click `Start streaming`.\n\nThe resulting stream will be available on path `/mystream`.\n\n## OBS Studio and WebRTC\n\nRecent versions of OBS Studio can also publish streams to the server with the [WebRTC / WHIP protocol](04-webrtc-clients.md) Use the following parameters:\n\n- Service: `WHIP`\n- Server: `http://localhost:8889/mystream/whip`\n\nSave the configuration and click `Start streaming`.\n\nThe resulting stream will be available on path `/mystream`.\n\n## OBS Studio and WebRTC, multitrack video\n\nOBS Studio can publish multiple video tracks or renditions at once (simulcast) with WebRTC / WHIP too. Make sure that the OBS Studio version is &ge; 32.1.0. Open `Settings -> Stream` and use the following parameters:\n\n- Service: `WHIP`\n- Server: `http://localhost:8889/mystream/whip`\n- Simulcast, Total Layers: `2` (or greater)\n\nCurrently it's not possible to change resolution or bitrate (or canvas) of renditions, since quality of secondary renditions is hardcoded as a percentage of the main one. You can find details on the [OBS documentation](https://obsproject.com/kb/whip-streaming-guide).\n\nSave the configuration and click `Start streaming`.\n\nThe resulting stream will be available on path `/mystream`.\n"
  },
  {
    "path": "docs/2-publish/18-python-opencv.md",
    "content": "# Python and OpenCV\n\nPython-based software can publish streams to the server with the OpenCV library and its GStreamer plugin, acting as a [RTSP client](06-rtsp-clients.md). OpenCV must be compiled with support for GStreamer, by following this procedure:\n\n```sh\nsudo apt install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev gstreamer1.0-plugins-ugly gstreamer1.0-rtsp python3-dev python3-numpy\ngit clone --depth=1 -b 4.5.4 https://github.com/opencv/opencv\ncd opencv\nmkdir build && cd build\ncmake -D CMAKE_INSTALL_PREFIX=/usr -D WITH_GSTREAMER=ON ..\nmake -j$(nproc)\nsudo make install\n```\n\nYou can check that OpenCV has been installed correctly by running:\n\n```sh\npython3 -c 'import cv2; print(cv2.getBuildInformation())'\n```\n\nCheck that the output contains `GStreamer: YES`.\n\nVideos can then be published with `cv2.VideoWriter`:\n\n```python\nfrom datetime import datetime\nfrom time import sleep, time\n\nimport cv2\nimport numpy as np\n\nfps = 15\nwidth = 800\nheight = 600\ncolors = [\n    (0, 0, 255),\n    (255, 0, 0),\n    (0, 255, 0),\n]\n\nout = cv2.VideoWriter('appsrc ! videoconvert' + \\\n    ' ! video/x-raw,format=I420' + \\\n    ' ! x264enc speed-preset=ultrafast bitrate=600 key-int-max=' + str(fps * 2) + \\\n    ' ! video/x-h264,profile=baseline' + \\\n    ' ! rtspclientsink location=rtsp://localhost:8554/mystream',\n    cv2.CAP_GSTREAMER, 0, fps, (width, height), True)\nif not out.isOpened():\n    raise Exception(\"can't open video writer\")\n\ncurcolor = 0\nstart = time()\n\nwhile True:\n    frame = np.zeros((height, width, 3), np.uint8)\n\n    # create a rectangle\n    color = colors[curcolor]\n    curcolor += 1\n    curcolor %= len(colors)\n    for y in range(0, int(frame.shape[0] / 2)):\n        for x in range(0, int(frame.shape[1] / 2)):\n            frame[y][x] = color\n\n    out.write(frame)\n    print(\"%s frame written to the server\" % datetime.now())\n\n    now = time()\n    diff = (1 / fps) - now - start\n    if diff > 0:\n        sleep(diff)\n    start = now\n```\n\nThe resulting stream will be available on path `/mystream`.\n"
  },
  {
    "path": "docs/2-publish/19-golang.md",
    "content": "# Golang\n\nYou can publish a stream to the server by using the Go programming language and the following libraries:\n\n- [gortsplib](https://github.com/bluenviron/gortsplib) to publish with RTSP.\n- [gortmplib](https://github.com/bluenviron/gortmplib) to publish with RTMP.\n\nBoth powers _MediaMTX_ itself. In the repositories of these projects there are several examples on how to connect to a server and push data.\n"
  },
  {
    "path": "docs/2-publish/20-unity.md",
    "content": "# Unity\n\nSoftware written with the Unity Engine can publish a stream to the server by using the [WebRTC protocol](04-webrtc-clients.md).\n\nCreate a new Unity project or open an existing one.\n\nOpen _Window -> Package Manager_, click on the plus sign, _Add Package by name..._ and insert `com.unity.webrtc`. Wait for the package to be installed.\n\nIn the _Project_ window, under `Assets`, create a new C# Script called `WebRTCPublisher.cs` with this content:\n\n```cs\nusing System.Collections;\nusing UnityEngine;\nusing Unity.WebRTC;\nusing UnityEngine.Networking;\n\npublic class WebRTCPublisher : MonoBehaviour\n{\n    public string url = \"http://localhost:8889/unity/whip\";\n    public int videoWidth = 1280;\n    public int videoHeight = 720;\n\n    private RTCPeerConnection pc;\n    private MediaStream videoStream;\n\n    void Start()\n    {\n        pc = new RTCPeerConnection();\n        Camera sourceCamera = gameObject.GetComponent<Camera>();\n        videoStream = sourceCamera.CaptureStream(videoWidth, videoHeight);\n        foreach (var track in videoStream.GetTracks())\n        {\n            pc.AddTrack(track);\n        }\n\n        StartCoroutine(WebRTC.Update());\n        StartCoroutine(createOffer());\n    }\n\n    private IEnumerator createOffer()\n    {\n        var op = pc.CreateOffer();\n        yield return op;\n        if (op.IsError) {\n            Debug.LogError(\"CreateOffer() failed\");\n            yield break;\n        }\n\n        yield return setLocalDescription(op.Desc);\n    }\n\n    private IEnumerator setLocalDescription(RTCSessionDescription offer)\n    {\n        var op = pc.SetLocalDescription(ref offer);\n        yield return op;\n        if (op.IsError) {\n            Debug.LogError(\"SetLocalDescription() failed\");\n            yield break;\n        }\n\n        yield return postOffer(offer);\n    }\n\n    private IEnumerator postOffer(RTCSessionDescription offer)\n    {\n        var content = new System.Net.Http.StringContent(offer.sdp);\n        content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(\"application/sdp\");\n        var client = new System.Net.Http.HttpClient();\n\n        var task = System.Threading.Tasks.Task.Run(async () => {\n            var res = await client.PostAsync(new System.UriBuilder(url).Uri, content);\n            res.EnsureSuccessStatusCode();\n            return await res.Content.ReadAsStringAsync();\n        });\n        yield return new WaitUntil(() => task.IsCompleted);\n        if (task.Exception != null) {\n            Debug.LogError(task.Exception);\n            yield break;\n        }\n\n        yield return setRemoteDescription(task.Result);\n    }\n\n    private IEnumerator setRemoteDescription(string answer)\n    {\n        RTCSessionDescription desc = new RTCSessionDescription();\n        desc.type = RTCSdpType.Answer;\n        desc.sdp = answer;\n        var op = pc.SetRemoteDescription(ref desc);\n        yield return op;\n        if (op.IsError) {\n            Debug.LogError(\"SetRemoteDescription() failed\");\n            yield break;\n        }\n\n        yield break;\n    }\n\n    void OnDestroy()\n    {\n        pc?.Close();\n        pc?.Dispose();\n        videoStream?.Dispose();\n    }\n}\n```\n\nIn the _Hierarchy_ window, find or create a scene and a camera, then add the `WebRTCPublisher.cs` script as component of the camera, by dragging it inside the _Inspector_ window. then Press the _Play_ button at the top of the page.\n\nThe resulting stream will be available on path `/unity`.\n"
  },
  {
    "path": "docs/2-publish/21-web-browsers.md",
    "content": "# Web browsers\n\nWeb browsers can publish a stream to the server by using the [WebRTC protocol](04-webrtc-clients.md). Start the server and open the web page:\n\n```\nhttp://localhost:8889/mystream/publish\n```\n\nThe resulting stream will be available on path `/mystream`.\n\nThis web page can be embedded into another web page by using an iframe:\n\n```html\n<iframe src=\"http://mediamtx-ip:8889/mystream/publish\" scrolling=\"no\"></iframe>\n```\n\nFor more advanced setups, you can create and serve a custom web page by starting from the [source code of the WebRTC publish page](https://github.com/bluenviron/mediamtx/blob/{version_tag}/internal/servers/webrtc/publish_index.html). In particular, there's a ready-to-use, standalone JavaScript class for publishing streams with WebRTC, available in [publisher.js](https://github.com/bluenviron/mediamtx/blob/{version_tag}/internal/servers/webrtc/publisher.js).\n"
  },
  {
    "path": "docs/2-publish/index.md",
    "content": "# Publish\n"
  },
  {
    "path": "docs/3-read/01-overview.md",
    "content": "# Read a stream\n\nLive streams can be read from the server with the following protocols and codecs:\n\n| protocol                       | variants                                   | codecs                                                                                                                                                                                                                                                 |\n| ------------------------------ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |\n| [SRT clients](02-srt.md)       |                                            | **Video**: H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video<br/>**Audio**: Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3<br/>**Other**: KLV                                                                                                |\n| [WebRTC clients](03-webrtc.md) | WHEP                                       | **Video**: AV1, VP9, VP8, H265, H264<br/>**Audio**: Opus, G722, G711 (PCMA, PCMU)<br/>**Other**: KLV                                                                                                                                                   |\n| [RTSP clients](04-rtsp.md)     | UDP, UDP-Multicast, TCP, RTSPS             | **Video**: AV1, VP9, VP8, H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video, M-JPEG<br/>**Audio**: Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, G726, G722, G711 (PCMA, PCMU), LPCM<br/>**Other**: KLV, MPEG-TS, any RTP-compatible codec |\n| [RTMP clients](05-rtmp.md)     | RTMP, RTMPS, Enhanced RTMP                 | **Video**: AV1, VP9, H265, H264<br/>**Audio**: Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, G711 (PCMA, PCMU), LPCM                                                                                                                           |\n| [HLS](06-hls.md)               | Low-Latency HLS, MP4-based HLS, legacy HLS | **Video**: AV1, VP9, H265, H264<br/>**Audio**: Opus, MPEG-4 Audio (AAC)                                                                                                                                                                                |\n\nWe provide instructions for reading with the following software:\n\n- [FFmpeg](07-ffmpeg.md)\n- [GStreamer](08-gstreamer.md)\n- [VLC](09-vlc.md)\n- [OBS Studio](10-obs-studio.md)\n- [Python and OpenCV](11-python-opencv.md)\n- [Golang](12-golang.md)\n- [Unity](13-unity.md)\n- [Web browsers](14-web-browsers.md)\n"
  },
  {
    "path": "docs/3-read/02-srt.md",
    "content": "# SRT clients\n\nSRT is a protocol that allows to publish and read live data stream, providing encryption, integrity and a retransmission mechanism. It is usually used to transfer media streams encoded with MPEG-TS. In order to read a stream from the server with the SRT protocol, use this URL:\n\n```\nsrt://localhost:8890?streamid=read:mystream\n```\n\nReplace `mystream` with the path name.\n\nIf you need to use the standard stream ID syntax instead of the custom one in use by this server, read [Standard stream ID syntax](../4-other/21-srt-specific-features.md#standard-stream-id-syntax).\n\nSome clients that can read with SRT are [FFmpeg](07-ffmpeg.md), [GStreamer](08-gstreamer.md) and [VLC](09-vlc.md).\n"
  },
  {
    "path": "docs/3-read/03-webrtc.md",
    "content": "# WebRTC clients\n\nWebRTC is an API that makes use of a set of protocols and methods to connect two clients together and allow them to exchange live media or data streams. You can read a stream with WebRTC and a web browser by visiting:\n\n```\nhttp://localhost:8889/mystream\n```\n\nWHEP is a WebRTC extension that allows to read streams by using a URL, without passing through a web page. This allows to use WebRTC as a general purpose streaming protocol. If you are using a software that supports WHEP, you can read a stream from the server by using this URL:\n\n```\nhttp://localhost:8889/mystream/whep\n```\n\nBe aware that not all browsers can read any codec, check [Supported browsers](../4-other/22-webrtc-specific-features.md#supported-browsers).\n\nDepending on the network it may be difficult to establish a connection between server and clients, read [Solving WebRTC connectivity issues](../4-other/22-webrtc-specific-features.md#solving-webrtc-connectivity-issues).\n\nSome clients that can read with WebRTC and WHEP are [FFmpeg](07-ffmpeg.md), [GStreamer](08-gstreamer.md), [Unity](13-unity.md) and [web browsers](14-web-browsers.md).\n"
  },
  {
    "path": "docs/3-read/04-rtsp.md",
    "content": "# RTSP clients\n\nRTSP is a protocol that allows to publish and read streams. It supports several underlying transport protocols and encryption (read [RTSP-specific features](../4-other/23-rtsp-specific-features.md)). In order to read a stream with the RTSP protocol, use this URL:\n\n```\nrtsp://localhost:8554/mystream\n```\n\nSome clients that can read with RTSP are [FFmpeg](07-ffmpeg.md), [GStreamer](08-gstreamer.md) and [VLC](09-vlc.md).\n"
  },
  {
    "path": "docs/3-read/05-rtmp.md",
    "content": "# RTMP clients\n\nRTMP is a protocol that allows to read and publish streams. It supports encryption, read [RTMP-specific features](../4-other/24-rtmp-specific-features.md). Streams can be read from the server by using the URL:\n\n```\nrtmp://localhost/mystream\n```\n\nSome clients that can read with RTMP are [FFmpeg](07-ffmpeg.md), [GStreamer](08-gstreamer.md) and [VLC](09-vlc.md).\n"
  },
  {
    "path": "docs/3-read/06-hls.md",
    "content": "# HLS\n\nHLS is a protocol that works by splitting streams into segments, and by serving these segments and a playlist with the HTTP protocol. You can use _MediaMTX_ to generate an HLS stream, that is accessible through a web page:\n\n```\nhttp://localhost:8888/mystream\n```\n\nand can also be accessed without using the browsers, by software that supports the HLS protocol (for instance VLC or _MediaMTX_ itself) by using this URL:\n\n```\nhttp://localhost:8888/mystream/index.m3u8\n```\n\nSome clients that can read with HLS are [FFmpeg](07-ffmpeg.md), [GStreamer](08-gstreamer.md), [VLC](09-vlc.md) and [web browsers](14-web-browsers.md).\n"
  },
  {
    "path": "docs/3-read/07-ffmpeg.md",
    "content": "# FFmpeg\n\nFFmpeg can read a stream from the server in several ways. The recommended one consists in reading with RTSP.\n\n## FFmpeg and RTSP\n\n```sh\nffmpeg -i rtsp://localhost:8554/mystream -c copy output.mp4\n```\n\n## FFmpeg and RTMP\n\n```sh\nffmpeg -i rtmp://localhost/mystream -c copy output.mp4\n```\n\nIn order to read AV1, VP9, H265, Opus, AC3 tracks and in order to read multiple video or audio tracks, the `-rtmp_enhanced_codecs` flag must be present:\n\n```sh\nffmpeg -rtmp_enhanced_codecs ac-3,av01,avc1,ec-3,fLaC,hvc1,.mp3,mp4a,Opus,vp09 \\\n-i rtmp://localhost/mystream -c copy output.mp4\n```\n\n## FFmpeg and SRT\n\n```sh\nffmpeg -i 'srt://localhost:8890?streamid=read:test' -c copy output.mp4\n```\n"
  },
  {
    "path": "docs/3-read/08-gstreamer.md",
    "content": "# GStreamer\n\nGStreamer can read a stream from the server in several ways. The recommended one consists in reading with RTSP.\n\n## GStreamer and RTSP\n\n```sh\ngst-launch-1.0 rtspsrc location=rtsp://127.0.0.1:8554/mystream latency=0 ! decodebin ! autovideosink\n```\n\nFor advanced options, read [RTSP-specific features](../4-other/23-rtsp-specific-features.md).\n\n## GStreamer and WebRTC\n\nGStreamer also supports reading streams with WebRTC/WHEP, although track codecs must be specified in advance through the `video-caps` and `audio-caps` parameters. Furthermore, if audio is not present, `audio-caps` must be set anyway and must point to a PCMU codec. For instance, the command for reading a video-only H264 stream is:\n\n```sh\ngst-launch-1.0 whepsrc whep-endpoint=http://127.0.0.1:8889/stream/whep use-link-headers=true \\\nvideo-caps=\"application/x-rtp,media=video,encoding-name=H264,payload=127,clock-rate=90000\" \\\naudio-caps=\"application/x-rtp,media=audio,encoding-name=PCMU,payload=0,clock-rate=8000\" \\\n! rtph264depay ! decodebin ! autovideosink\n```\n\nWhile the command for reading an audio-only Opus stream is:\n\n```sh\ngst-launch-1.0 whepsrc whep-endpoint=\"http://127.0.0.1:8889/stream/whep\" use-link-headers=true \\\naudio-caps=\"application/x-rtp,media=audio,encoding-name=OPUS,payload=111,clock-rate=48000,encoding-params=(string)2\" \\\n! rtpopusdepay ! decodebin ! autoaudiosink\n```\n\nWhile the command for reading a H264 and Opus stream is:\n\n```sh\ngst-launch-1.0 whepsrc whep-endpoint=http://127.0.0.1:8889/stream/whep use-link-headers=true \\\nvideo-caps=\"application/x-rtp,media=video,encoding-name=H264,payload=127,clock-rate=90000\" \\\naudio-caps=\"application/x-rtp,media=audio,encoding-name=OPUS,payload=111,clock-rate=48000,encoding-params=(string)2\" \\\n! decodebin ! autovideosink\n```\n"
  },
  {
    "path": "docs/3-read/09-vlc.md",
    "content": "# VLC\n\nVLC can read a stream from the server in several ways. The recommended one consists in reading with RTSP:\n\n```sh\nvlc --network-caching=50 rtsp://localhost:8554/mystream\n```\n\n## RTSP and Ubuntu compatibility\n\nThe VLC shipped with Ubuntu 21.10 doesn't support playing RTSP due to a license issue (read [here](https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=982299) and [here](https://stackoverflow.com/questions/69766748/cvlc-cannot-play-rtsp-omxplayer-instead-can)). To fix the issue, remove the default VLC instance and install the snap version:\n\n```sh\nsudo apt purge -y vlc\nsnap install vlc\n```\n\n## Encrypted RTSP compatibility\n\nAt the moment VLC doesn't support reading encrypted RTSP streams. However, you can use a proxy like [stunnel](https://www.stunnel.org) or [nginx](https://nginx.org/) or a local _MediaMTX_ instance to decrypt streams before reading them.\n"
  },
  {
    "path": "docs/3-read/10-obs-studio.md",
    "content": "# OBS Studio\n\nOBS Studio can read streams from the server by using the [RTSP protocol](04-rtsp.md).\n\nOpen OBS, click on _Add Source_, _Media source_, _OK_, uncheck _Local file_, insert in _Input_:\n\n```\nrtsp://localhost:8554/stream\n```\n\nThen _Ok_.\n"
  },
  {
    "path": "docs/3-read/11-python-opencv.md",
    "content": "# Python and OpenCV\n\nPython-based software can read streams from the server with the OpenCV library, acting as a [RTSP client](04-rtsp.md).\n\n```python\nimport cv2\n\ncap = cv2.VideoCapture('rtsp://localhost:8554/mystream')\nif not cap.isOpened():\n    raise Exception(\"can't open video capture\")\n\nwhile True:\n    ret, frame = cap.read()\n    if not ret:\n        raise Exception(\"can't receive frame\")\n\n    cv2.imshow('frame', frame)\n\n    if cv2.waitKey(1) == ord('q'):\n        break\n\ncap.release()\ncv2.destroyAllWindows()\n```\n"
  },
  {
    "path": "docs/3-read/12-golang.md",
    "content": "# Golang\n\nYou can read a stream from the server by using the Go programming language and the following libraries:\n\n- [gortsplib](https://github.com/bluenviron/gortsplib) to read with RTSP.\n- [gortmplib](https://github.com/bluenviron/gortmplib) to read with RTMP.\n- [gohlslib](https://github.com/bluenviron/gohlslib) to read with HLS.\n\nAll these power _MediaMTX_ itself. In the repositories of these projects there are several examples on how to connect to a server and read data.\n"
  },
  {
    "path": "docs/3-read/13-unity.md",
    "content": "# Unity\n\nSoftware written with the Unity Engine can read a stream from the server by using the [WebRTC protocol](03-webrtc.md).\n\nCreate a new Unity project or open an existing one.\n\nOpen _Window -> Package Manager_, click on the plus sign, _Add Package by name..._ and insert `com.unity.webrtc`. Wait for the package to be installed.\n\nIn the _Project_ window, under `Assets`, create a new C# Script called `WebRTCReader.cs` with this content:\n\n```cs\nusing System.Collections;\nusing UnityEngine;\nusing Unity.WebRTC;\n\npublic class WebRTCReader : MonoBehaviour\n{\n    public string url = \"http://localhost:8889/stream/whep\";\n\n    private RTCPeerConnection pc;\n    private MediaStream receiveStream;\n\n    void Start()\n    {\n        UnityEngine.UI.RawImage rawImage = gameObject.GetComponentInChildren<UnityEngine.UI.RawImage>();\n        AudioSource audioSource = gameObject.GetComponentInChildren<AudioSource>();\n        pc = new RTCPeerConnection();\n        receiveStream = new MediaStream();\n\n        pc.OnTrack = e =>\n        {\n            receiveStream.AddTrack(e.Track);\n        };\n\n        receiveStream.OnAddTrack = e =>\n        {\n            if (e.Track is VideoStreamTrack videoTrack)\n            {\n                videoTrack.OnVideoReceived += (tex) =>\n                {\n                    rawImage.texture = tex;\n                };\n            }\n            else if (e.Track is AudioStreamTrack audioTrack)\n            {\n                audioSource.SetTrack(audioTrack);\n                audioSource.loop = true;\n                audioSource.Play();\n            }\n        };\n\n        RTCRtpTransceiverInit init = new RTCRtpTransceiverInit();\n        init.direction = RTCRtpTransceiverDirection.RecvOnly;\n        pc.AddTransceiver(TrackKind.Audio, init);\n        pc.AddTransceiver(TrackKind.Video, init);\n\n        StartCoroutine(WebRTC.Update());\n        StartCoroutine(createOffer());\n    }\n\n    private IEnumerator createOffer()\n    {\n        var op = pc.CreateOffer();\n        yield return op;\n        if (op.IsError) {\n            Debug.LogError(\"CreateOffer() failed\");\n            yield break;\n        }\n\n        yield return setLocalDescription(op.Desc);\n    }\n\n    private IEnumerator setLocalDescription(RTCSessionDescription offer)\n    {\n        var op = pc.SetLocalDescription(ref offer);\n        yield return op;\n        if (op.IsError) {\n            Debug.LogError(\"SetLocalDescription() failed\");\n            yield break;\n        }\n\n        yield return postOffer(offer);\n    }\n\n    private IEnumerator postOffer(RTCSessionDescription offer)\n    {\n        var content = new System.Net.Http.StringContent(offer.sdp);\n        content.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue(\"application/sdp\");\n        var client = new System.Net.Http.HttpClient();\n\n        var task = System.Threading.Tasks.Task.Run(async () => {\n            var res = await client.PostAsync(new System.UriBuilder(url).Uri, content);\n            res.EnsureSuccessStatusCode();\n            return await res.Content.ReadAsStringAsync();\n        });\n        yield return new WaitUntil(() => task.IsCompleted);\n        if (task.Exception != null) {\n            Debug.LogError(task.Exception);\n            yield break;\n        }\n\n        yield return setRemoteDescription(task.Result);\n    }\n\n    private IEnumerator setRemoteDescription(string answer)\n    {\n        RTCSessionDescription desc = new RTCSessionDescription();\n        desc.type = RTCSdpType.Answer;\n        desc.sdp = answer;\n        var op = pc.SetRemoteDescription(ref desc);\n        yield return op;\n        if (op.IsError) {\n            Debug.LogError(\"SetRemoteDescription() failed\");\n            yield break;\n        }\n\n        yield break;\n    }\n\n    void OnDestroy()\n    {\n        pc?.Close();\n        pc?.Dispose();\n        receiveStream?.Dispose();\n    }\n}\n```\n\nEdit the `url` variable according to your needs.\n\nIn the _Hierarchy_ window, find or create a scene. Inside the scene, add a _Canvas_. Inside the Canvas, add a _Raw Image_ and an _Audio Source_. Then add the `WebRTCReader.cs` script as component of the canvas, by dragging it inside the _Inspector_ window. then Press the _Play_ button at the top of the page.\n"
  },
  {
    "path": "docs/3-read/14-web-browsers.md",
    "content": "# Web browsers\n\nWeb browsers can read a stream from the server in several ways.\n\n## Web browsers and WebRTC\n\nYou can read a stream by using the [WebRTC protocol](03-webrtc.md) by visiting the web page:\n\n```\nhttp://localhost:8889/mystream\n```\n\nSee [Embed streams in a website](../4-other/14-embed-streams-in-a-website.md) for instructions on how to embed the stream into an external website.\n\n## Web browsers and HLS\n\nWeb browsers can also read a stream with the [HLS protocol](06-hls.md). Latency is higher but there are fewer problems related to connectivity between server and clients, furthermore the server load can be balanced by using a common HTTP CDN (like Cloudflare or CloudFront), and this allows to handle an unlimited amount of readers. Visit the web page:\n\n```\nhttp://localhost:8888/mystream\n```\n\nSee [Embed streams in a website](../4-other/14-embed-streams-in-a-website.md) for instructions on how to embed the stream into an external website.\n"
  },
  {
    "path": "docs/3-read/index.md",
    "content": "# Read\n"
  },
  {
    "path": "docs/4-other/02-configuration.md",
    "content": "# Configuration\n\nAll the configuration parameters are listed and commented in the [configuration file](../5-references/1-configuration-file.md) (`mediamtx.yml`).\n\n## Change the configuration\n\nThere are several ways to change the configuration:\n\n1. By editing the configuration file, that is\n   - included into the release bundle\n   - available in the root folder of the Docker image (`/mediamtx.yml`); it can be overridden in this way:\n\n     ```sh\n     docker run --rm -it --network=host -v \"$PWD/mediamtx.yml:/mediamtx.yml:ro\" bluenviron/mediamtx:1\n     ```\n\n   The configuration can be changed dynamically when the server is running (hot reloading) by writing to the configuration file. Changes are detected and applied without disconnecting existing clients, whenever it's possible.\n\n2. By overriding configuration parameters with environment variables, in the format `MTX_PARAMNAME`, where `PARAMNAME` is the uppercase name of a parameter. For instance, the `rtspAddress` parameter can be overridden in the following way:\n\n   ```\n   MTX_RTSPADDRESS=\"127.0.0.1:8554\" ./mediamtx\n   ```\n\n   Parameters that have array as value can be overridden by setting a comma-separated list. For example:\n\n   ```\n   MTX_RTSPTRANSPORTS=\"tcp,udp\"\n   ```\n\n   Parameters in maps can be overridden by using underscores, in the following way:\n\n   ```\n   MTX_PATHS_TEST_SOURCE=rtsp://myurl ./mediamtx\n   ```\n\n   Parameters in lists can be overridden in the same way as parameters in maps, using their position like an additional key. This is particularly useful if you want to use internal users but define credentials through environment variables:\n\n   ```\n   MTX_AUTHINTERNALUSERS_0_USER=username\n   MTX_AUTHINTERNALUSERS_0_PASS=password\n   ```\n\n   This method is particularly useful when using Docker; any configuration parameter can be changed by passing environment variables with the `-e` flag:\n\n   ```\n   docker run --rm -it --network=host -e MTX_PATHS_TEST_SOURCE=rtsp://myurl bluenviron/mediamtx:1\n   ```\n\n3. By using the [Control API](18-control-api.md).\n\n## Encrypt the configuration\n\nThe configuration file can be entirely encrypted for security purposes by using the `crypto_secretbox` function of the NaCL function. An online tool for performing this operation is [available here](https://play.golang.org/p/rX29jwObNe4).\n\nAfter performing the encryption, put the base64-encoded result into the configuration file, and launch the server with the `MTX_CONFKEY` variable:\n\n```\nMTX_CONFKEY=mykey ./mediamtx\n```\n"
  },
  {
    "path": "docs/4-other/03-authentication.md",
    "content": "# Authentication\n\n_MediaMTX_ can be configured to ask clients for credentials, either in the form of username/password or a string-based token. These credentials are then validated through a chosen method.\n\n## Credential validation\n\nCredentials can be validated through one of these methods:\n\n- Internal database: credentials are stored in the configuration file\n- External HTTP server: an external HTTP URL is contacted to perform authentication\n- External JWT provider: an external identity server provides signed tokens that are then verified by the server\n\n### Internal database\n\nThe internal authentication method is the default one. Users are stored inside the configuration file, in this format:\n\n```yml\nauthInternalUsers:\n  # Username. 'any' means any user, including anonymous ones.\n  - user: any\n    # Password. Not used in case of 'any' user.\n    pass:\n    # IPs or networks allowed to use this user. An empty list means any IP.\n    ips: []\n    # Permissions.\n    permissions:\n      # Available actions are: publish, read, playback, api, metrics, pprof.\n      - action: publish\n        # Paths can be set to further restrict access to a specific path.\n        # An empty path means any path.\n        # Regular expressions can be used by using a tilde as prefix.\n        path:\n      - action: read\n        path:\n      - action: playback\n        path:\n```\n\nOnly clients that provide a valid username and password will be able to perform a certain action.\n\nIf storing plain credentials in the configuration file is a security problem, username and passwords can be stored as hashed strings. The Argon2 and SHA256 hashing algorithms are supported. To use Argon2, the string must be hashed using Argon2id (recommended) or Argon2i:\n\n```\necho -n \"mypass\" | argon2 saltItWithSalt -id -l 32 -e\n```\n\nThen stored with the `argon2:` prefix:\n\n```yml\nauthInternalUsers:\n  - user: argon2:$argon2id$v=19$m=4096,t=3,p=1$MTIzNDU2Nzg$OGGO0eCMN0ievb4YGSzvS/H+Vajx1pcbUmtLp2tRqRU\n    pass: argon2:$argon2i$v=19$m=4096,t=3,p=1$MTIzNDU2Nzg$oct3kOiFywTdDdt19kT07hdvmsPTvt9zxAUho2DLqZw\n    permissions:\n      - action: publish\n```\n\nTo use SHA256, the string must be hashed with SHA256 and encoded with base64:\n\n```\necho -n \"mypass\" | openssl dgst -binary -sha256 | openssl base64\n```\n\nThen stored with the `sha256:` prefix:\n\n```yml\nauthInternalUsers:\n  - user: sha256:j1tsRqDEw9xvq/D7/9tMx6Jh/jMhk3UfjwIB2f1zgMo=\n    pass: sha256:BdSWkrdV+ZxFBLUQQY7+7uv9RmiSVA8nrPmjGjJtZQQ=\n    permissions:\n      - action: publish\n```\n\n**WARNING**: enable encryption or use a VPN to ensure that no one is intercepting the credentials in transit.\n\n### External HTTP server\n\nAuthentication can be delegated to an external HTTP server:\n\n```yml\nauthMethod: http\nauthHTTPAddress: http://myauthserver/auth\n```\n\nEach time a user needs to be authenticated, the specified URL will be requested with the POST method and this payload:\n\n```json\n{\n  \"user\": \"user\",\n  \"password\": \"password\",\n  \"token\": \"token\",\n  \"ip\": \"ip\",\n  \"action\": \"publish|read|playback|api|metrics|pprof\",\n  \"path\": \"path\",\n  \"protocol\": \"rtsp|rtmp|hls|webrtc|srt\",\n  \"id\": \"id\",\n  \"query\": \"query\"\n}\n```\n\nIf the URL returns a status code that begins with `20` (i.e. `200`), authentication is successful, otherwise it fails. Be aware that it's perfectly normal for the authentication server to receive requests with empty users and passwords, i.e.:\n\n```json\n{\n  \"user\": \"\",\n  \"password\": \"\"\n}\n```\n\nThis happens because RTSP clients don't provide credentials until they are asked to. In order to receive the credentials, the authentication server must reply with status code `401`, then the client will send credentials.\n\nSome actions can be excluded from the process:\n\n```yml\n# Actions to exclude from HTTP-based authentication.\n# Format is the same as the one of user permissions.\nauthHTTPExclude:\n  - action: api\n  - action: metrics\n  - action: pprof\n```\n\nIf the authentication server uses HTTPS and has a self-signed or invalid TLS certificate, you can provide the fingerprint of the certificate to validate it anyway:\n\n```yml\nauthMethod: http\nauthHTTPAddress: https://myauthserver/auth\nauthHTTPFingerprint: 33949e05fffb5ff3e8aa16f8213a6251b4d9363804ba53233c4da9a46d6f2739\n```\n\nThe fingerprint can be obtained with:\n\n```sh\nopenssl s_client -connect myauthserver:443 </dev/null 2>/dev/null | sed -n '/BEGIN/,/END/p' > server.crt\nopenssl x509 -in server.crt -noout -fingerprint -sha256 | cut -d \"=\" -f2 | tr -d ':'\n```\n\n### External JWT provider\n\nAuthentication can be delegated to an external identity server, that is capable of generating JWTs and provides a JWKS endpoint. With respect to the HTTP-based method, this has the advantage that the external server is contacted once, and not for every request, greatly improving performance. In order to use the JWT-based authentication method, set `authMethod` and `authJWTJWKS`:\n\n```yml\nauthMethod: jwt\nauthJWTJWKS: http://my_identity_server/jwks_endpoint\nauthJWTClaimKey: mediamtx_permissions\n```\n\nUsers are expected to pass the encoded JWT as token.\n\nThe JWT is expected to contain a claim, with a list of permissions in the same format as the one of user permissions:\n\n```json\n{\n  \"mediamtx_permissions\": [\n    {\n      \"action\": \"publish\",\n      \"path\": \"\"\n    }\n  ]\n}\n```\n\nIf the JWKS server uses TLS and has a self-signed or invalid TLS certificate, you can provide the fingerprint of the certificate to validate it anyway:\n\n```yml\nauthMethod: jwt\nauthJWTJWKS: https://my_identity_server/jwks_endpoint\nauthJWTJWKSFingerprint: 33949e05fffb5ff3e8aa16f8213a6251b4d9363804ba53233c4da9a46d6f2739\nauthJWTClaimKey: mediamtx_permissions\n```\n\nThe fingerprint can be obtained with:\n\n```sh\nopenssl s_client -connect my_identity_server:443 </dev/null 2>/dev/null | sed -n '/BEGIN/,/END/p' > server.crt\nopenssl x509 -in server.crt -noout -fingerprint -sha256 | cut -d \"=\" -f2 | tr -d ':'\n```\n\nOptionally, the JWT `iss` (issuer) and `aud` (audience) claims can be validated by setting `authJWTIssuer` and `authJWTAudience`. When set, tokens that don't contain the expected values will be rejected:\n\n```yml\nauthMethod: jwt\nauthJWTJWKS: http://my_identity_server/jwks_endpoint\nauthJWTClaimKey: mediamtx_permissions\nauthJWTIssuer: http://my_identity_server\nauthJWTAudience: mediamtx\n```\n\nLeave these fields empty to skip validation of the respective claims.\n\n#### Keycloak setup\n\nHere's a tutorial on how to setup the [Keycloak identity server](https://www.keycloak.org/) in order to provide JWTs.\n\n1. Start Keycloak:\n\n   ```sh\n   docker run --name=keycloak -p 8080:8080 \\\n   -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin \\\n   quay.io/keycloak/keycloak:23.0.7 start-dev\n   ```\n\n2. Open the Keycloak web UI on http://localhost:8080, click on _Administration Console_ and log in.\n\n3. Click on _master_ in the top left corner, _Create realm_, set realm name to `mediamtx`, _Create_.\n\n4. Open page _Client scopes_, _Create client scope_, set name to `mediamtx`, _Save_.\n\n5. Open tab _Mappers_, _Configure a new Mapper_, _User Attribute_:\n   - Name: `mediamtx_permissions`\n   - User Attribute: `mediamtx_permissions`\n   - Token Claim Name: `mediamtx_permissions`\n   - Claim JSON Type: `JSON`\n   - Multivalued: `On`\n\n   Save.\n\n6. Open page _Clients_, _Create client_, set Client ID to `mediamtx`, _Next_, _Client authentication_ `On`, _Next_, _Save_.\n\n7. Open tab _Credentials_, copy client secret somewhere.\n\n8. Open tab _Client scopes_, set _Assigned type_ of all existing client scopes to _Optional_. This decreases the length of the JWT, since many clients impose limits on it.\n\n9. In tab _Client scopes_, _Add client scope_, Select `mediamtx`, _Add_, _Default_.\n\n10. Open page _Users_, _Add user_, Username `testuser`, _Create_, Tab _Credentials_, _Set password_, pick a password, _Save_.\n\n11. Open tab _Attributes_, _Add an attribute_:\n    - Key: `mediamtx_permissions`\n    - Value: `{\"action\":\"publish\", \"path\": \"\"}`\n\n    You can add as many attributes with key `mediamtx_permissions` as you want, each with a single permission in it.\n\n12. In MediaMTX, use the following JWKS URL:\n\n    ```yml\n    authJWTJWKS: http://localhost:8080/realms/mediamtx/protocol/openid-connect/certs\n    ```\n\n13. Perform authentication on Keycloak:\n\n    ```\n    curl \\\n    -d \"client_id=mediamtx\" \\\n    -d \"client_secret=$CLIENT_SECRET\" \\\n    -d \"username=$USER\" \\\n    -d \"password=$PASS\" \\\n    -d \"grant_type=password\" \\\n    http://localhost:8080/realms/mediamtx/protocol/openid-connect/token\n    ```\n\n    The JWT is inside the `access_token` key of the response:\n\n    ```json\n    {\n      \"access_token\": \"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIyNzVjX3ptOVlOdHQ0TkhwWVk4Und6ZndUclVGSzRBRmQwY3lsM2wtY3pzIn0.eyJleHAiOjE3MDk1NTUwOTIsImlhdCI6MTcwOTU1NDc5MiwianRpIjoiMzE3ZTQ1NGUtNzczMi00OTM1LWExNzAtOTNhYzQ2ODhhYWIxIiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9tZWRpYW10eCIsImF1ZCI6ImFjY291bnQiLCJzdWIiOiI2NTBhZDA5Zi03MDgxLTQyNGItODI4Ni0xM2I3YTA3ZDI0MWEiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJtZWRpYW10eCIsInNlc3Npb25fc3RhdGUiOiJjYzJkNDhjYy1kMmU5LTQ0YjAtODkzZS0wYTdhNjJiZDI1YmQiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbIi8qIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsInVtYV9hdXRob3JpemF0aW9uIiwiZGVmYXVsdC1yb2xlcy1tZWRpYW10eCJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoibWVkaWFtdHggcHJvZmlsZSBlbWFpbCIsInNpZCI6ImNjMmQ0OGNjLWQyZTktNDRiMC04OTNlLTBhN2E2MmJkMjViZCIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibWVkaWFtdHhfcGVybWlzc2lvbnMiOlt7ImFjdGlvbiI6InB1Ymxpc2giLCJwYXRocyI6ImFsbCJ9XSwicHJlZmVycmVkX3VzZXJuYW1lIjoidGVzdHVzZXIifQ.Gevz7rf1qHqFg7cqtSfSP31v_NS0VH7MYfwAdra1t6Yt5rTr9vJzqUeGfjYLQWR3fr4XC58DrPOhNnILCpo7jWRdimCnbPmuuCJ0AYM-Aoi3PAsWZNxgmtopq24_JokbFArY9Y1wSGFvF8puU64lt1jyOOyxf2M4cBHCs_EarCKOwuQmEZxSf8Z-QV9nlfkoTUszDCQTiKyeIkLRHL2Iy7Fw7_T3UI7sxJjVIt0c6HCNJhBBazGsYzmcSQ_GrmhbUteMTg00o6FicqkMBe99uZFnx9wIBm_QbO9hbAkkzF923I-DTAQrFLxT08ESMepDwmzFrmnwWYBLE3u8zuUlCA\",\n      \"expires_in\": 300,\n      \"refresh_expires_in\": 1800,\n      \"refresh_token\": \"eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICI3OTI3Zjg4Zi05YWM4LTRlNmEtYWE1OC1kZmY0MDQzZDRhNGUifQ.eyJleHAiOjE3MDk1NTY1OTIsImlhdCI6MTcwOTU1NDc5MiwianRpIjoiMGVhZWFhMWItYzNhMC00M2YxLWJkZjAtZjI2NTRiODlkOTE3IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo4MDgwL3JlYWxtcy9tZWRpYW10eCIsImF1ZCI6Imh0dHA6Ly9sb2NhbGhvc3Q6ODA4MC9yZWFsbXMvbWVkaWFtdHgiLCJzdWIiOiI2NTBhZDA5Zi03MDgxLTQyNGItODI4Ni0xM2I3YTA3ZDI0MWEiLCJ0eXAiOiJSZWZyZXNoIiwiYXpwIjoibWVkaWFtdHgiLCJzZXNzaW9uX3N0YXRlIjoiY2MyZDQ4Y2MtZDJlOS00NGIwLTg5M2UtMGE3YTYyYmQyNWJkIiwic2NvcGUiOiJtZWRpYW10eCBwcm9maWxlIGVtYWlsIiwic2lkIjoiY2MyZDQ4Y2MtZDJlOS00NGIwLTg5M2UtMGE3YTYyYmQyNWJkIn0.yuXV8_JU0TQLuosNdp5xlYMjn7eO5Xq-PusdHzE7bsQ\",\n      \"token_type\": \"Bearer\",\n      \"not-before-policy\": 0,\n      \"session_state\": \"cc2d48cc-d2e9-44b0-893e-0a7a62bd25bd\",\n      \"scope\": \"mediamtx profile email\"\n    }\n    ```\n\n## Providing username and password\n\n### RTSP\n\nPrepend username and password and a `@` to the host:\n\n```\nrtsp rtsp://myuser:mypass@localhost:8554/mystream\n```\n\n### RTMP\n\nUse the `user` and `pass` query parameters:\n\n```\nrtmp://localhost/mystream?user=myuser&pass=mypass\n```\n\n### SRT\n\nAppend username and password to `streamid`:\n\n```\nsrt://localhost:8890?streamid=publish:mystream:user:pass&pkt_size=1316\n```\n\n### HLS and WebRTC\n\nUsername and password can be passed through the `Authorization: Basic` HTTP header:\n\n```\nAuthorization: Basic base64(user:pass)\n```\n\nWhen using a web browser, a dialog is first shown to users, asking for credentials, and then the header is automatically inserted into every request. If you need to automatically fill credentials from a parent web page, read [Embed streams in a website](14-embed-streams-in-a-website.md).\n\nIf the `Authorization: Basic` header cannot be used (for instance, in software like OBS Studio, which only allows to provide a \"Bearer Token\"), credentials can be passed through the `Authorization: Bearer` header (i.e. the \"Bearer Token\" in OBS), where the value is the concatenation of username and password, separated by a colon:\n\n```\nAuthorization: Bearer username:password\n```\n\n## Providing tokens / JWTs\n\n### RTSP\n\nPass the token as a query parameter:\n\n```\nrtsp://localhost:8554/mystream?jwt=jwt\n```\n\nWARNING: FFmpeg implementation of RTSP does not support URLs that are longer than 4096 characters (this is the [MAX_URL_SIZE constant](https://github.com/FFmpeg/FFmpeg/blob/f951aa9ef382d6bb517e05d04d52710f751de427/libavformat/internal.h#L30)), therefore you have to configure your identity server in order to produce JWTs that are shorter than this threshold.\n\n### RTMP\n\nPass the token as a query parameter:\n\n```\nrtmp://localhost/mystream?jwt=jwt\n```\n\nWARNING: FFmpeg implementation of RTMP does not support URLs that are longer than 1024 characters (this is the [TCURL_MAX_LENGTH constant](https://github.com/FFmpeg/FFmpeg/blob/f951aa9ef382d6bb517e05d04d52710f751de427/libavformat/rtmpproto.c#L55)), therefore you have to configure your identity server in order to produce JWTs that are shorter than this threshold.\n\n### SRT\n\nPass the token as password, with an arbitrary user:\n\n```\nsrt://localhost:8890?streamid=publish:mystream:user:jwt&pkt_size=1316\n```\n\nWARNING: SRT does not support Stream IDs that are longer than 512 characters, therefore you have to configure your identity server in order to produce JWTs that are shorter than this threshold.\n\n### HLS and WebRTC\n\nThe token can be passed through the `Authorization: Bearer` header:\n\n```\nAuthorization: Bearer MY_JWT\n```\n\nIn OBS Studio, this is the \"Bearer Token\" field.\n\nIf the `Authorization: Bearer` token cannot be directly provided (for instance, with web browsers that directly access _MediaMTX_ and show a credential dialog), you can pass the token as password, using an arbitrary user.\n\nIn web browsers, if you need to automatically fill credentials from a parent web page, read [Embed streams in a website](14-embed-streams-in-a-website.md).\n"
  },
  {
    "path": "docs/4-other/04-remuxing-reencoding-compression.md",
    "content": "# Re-encoding\n\nTo change the format, codec or compression of a stream, use _FFmpeg_ or _GStreamer_ together with _MediaMTX_. For instance, to re-encode an existing stream, that is available in the `/original` path, and publish the resulting stream in the `/compressed` path, edit `mediamtx.yml` and replace everything inside section `paths` with the following content:\n\n```yml\npaths:\n  compressed:\n  original:\n    runOnReady: >\n      ffmpeg -i rtsp://localhost:$RTSP_PORT/$MTX_PATH\n        -c:v libx264 -pix_fmt yuv420p -preset ultrafast -b:v 600k\n        -max_muxing_queue_size 1024 -f rtsp rtsp://localhost:$RTSP_PORT/compressed\n    runOnReadyRestart: yes\n```\n"
  },
  {
    "path": "docs/4-other/05-always-available.md",
    "content": "# Always-available\n\nWhen the publisher or source of a stream is offline, the server can be configured to fill gaps in the stream with an offline segment that is played on repeat until a publisher comes back online. This allows readers to stay connected regardless of the state of the stream. The offline segment and online stream are concatenated without re-encoding any frame, using the original codec.\n\nThis feature can be enabled by toggling the `alwaysAvailable` flag and filling `alwaysAvailableTracks`:\n\n```yml\npaths:\n  mypath:\n    alwaysAvailable: true\n    alwaysAvailableTracks:\n      # Available values are: AV1, VP9, H265, H264, Opus, MPEG4Audio, G711, LPCM\n      - codec: H264\n        # in case of MPEG4Audio, G711, LPCM, sampleRate and ChannelCount must be provided too.\n        #  sampleRate: 48000\n        #  channelCount: 2\n        #  in case of G711, muLaw must be provided too.\n        #  muLaw: false\n```\n\nBy default, the server uses a default offline segment with the text \"STREAM IS OFFLINE\". The segment can be replaced with an external MP4 file:\n\n```yml\npaths:\n  mypath:\n    alwaysAvailable: true\n    alwaysAvailableFile: \"./h264.mp4\"\n```\n"
  },
  {
    "path": "docs/4-other/06-record.md",
    "content": "# Record\n\nLive streams be recorded to disk and played back with the following file containers and codecs:\n\n| container | codecs                                                                                                                                                                          |\n| --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| fMP4      | **Video**: AV1, VP9, H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video, M-JPEG<br/>**Audio**: Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3, G711 (PCMA, PCMU), LPCM |\n| MPEG-TS   | **Video**: H265, H264, MPEG-4 Video (H263, Xvid), MPEG-1/2 Video<br/>**Audio**: Opus, MPEG-4 Audio (AAC), MPEG-1/2 Audio (MP3), AC-3                                            |\n\n## Usage\n\nTo record available streams to disk, set the `record` parameter in the configuration file:\n\n```yml\npathDefaults:\n  # Record streams to disk.\n  record: yes\n```\n\nIt's also possible to specify additional parameters:\n\n```yml\npathDefaults:\n  # Record streams to disk.\n  record: yes\n  # Path of recording segments.\n  # Extension is added automatically.\n  # Available variables are %path (path name), %Y %m %d (year, month, day),\n  # %H %M %S (hours, minutes, seconds), %f (microseconds), %z (time zone), %s (unix epoch).\n  recordPath: ./recordings/%path/%Y-%m-%d_%H-%M-%S-%f\n  # Format of recorded segments.\n  # Available formats are \"fmp4\" (fragmented MP4) and \"mpegts\" (MPEG-TS).\n  recordFormat: fmp4\n  # fMP4 segments are concatenation of small MP4 files (parts), each with this duration.\n  # MPEG-TS segments are concatenation of 188-bytes packets, flushed to disk with this period.\n  # When a system failure occurs, the last part gets lost.\n  # Therefore, the part duration is equal to the RPO (recovery point objective).\n  recordPartDuration: 1s\n  # This prevents RAM exhaustion.\n  recordMaxPartSize: 50M\n  # Minimum duration of each segment.\n  recordSegmentDuration: 1h\n  # Delete segments after this timespan.\n  # Set to 0s to disable automatic deletion.\n  recordDeleteAfter: 1d\n```\n\nAll available recording parameters are listed in the [configuration file](../5-references/1-configuration-file.md).\n\n## Remote upload\n\nTo upload recordings to a remote location, you can use _MediaMTX_ together with [rclone](https://github.com/rclone/rclone), a command line tool that provides file synchronization capabilities with a huge variety of services (including S3, FTP, SMB, Google Drive):\n\n1. Download and install [rclone](https://github.com/rclone/rclone).\n\n2. Configure _rclone_:\n\n   ```sh\n   rclone config\n   ```\n\n3. Place `rclone` into the `runOnInit` and `runOnRecordSegmentComplete` hooks:\n\n   ```yml\n   pathDefaults:\n     # this is needed to sync segments after a crash.\n     # replace myconfig with the name of the rclone config.\n     runOnInit: rclone sync -v ./recordings myconfig:/my-path/recordings\n\n     # this is called when a segment has been finalized.\n     # replace myconfig with the name of the rclone config.\n     runOnRecordSegmentComplete: rclone sync -v --min-age=1ms ./recordings myconfig:/my-path/recordings\n   ```\n\n   If you want to delete local segments after they are uploaded, replace `rclone sync` with `rclone move`.\n"
  },
  {
    "path": "docs/4-other/07-playback.md",
    "content": "# Playback\n\nExisting recordings can be played back to users through a dedicated HTTP server, that can be enabled inside the configuration:\n\n```yml\nplayback: yes\nplaybackAddress: :9996\n```\n\nThe server provides an endpoint to list recorded timespans:\n\n```\nhttp://localhost:9996/list?path=[mypath]&start=[start]&end=[end]\n```\n\nWhere:\n\n- [mypath] is the name of a path\n- [start] (optional) is the start date in [RFC3339 format](https://www.utctime.net/)\n- [end] (optional) is the end date in [RFC3339 format](https://www.utctime.net/)\n\nThe server will return a list of timespans in JSON format:\n\n```json\n[\n  {\n    \"start\": \"2006-01-02T15:04:05Z07:00\",\n    \"duration\": 60.0,\n    \"url\": \"http://localhost:9996/get?path=[mypath]&start=2006-01-02T15%3A04%3A05Z07%3A00&duration=60.0\"\n  },\n  {\n    \"start\": \"2006-01-02T15:07:05Z07:00\",\n    \"duration\": 32.33,\n    \"url\": \"http://localhost:9996/get?path=[mypath]&start=2006-01-02T15%3A07%3A05Z07%3A00&duration=32.33\"\n  }\n]\n```\n\nThe server provides an endpoint to download recordings:\n\n```\nhttp://localhost:9996/get?path=[mypath]&start=[start]&duration=[duration]&format=[format]\n```\n\nWhere:\n\n- [mypath] is the path name\n- [start] is the start date in [RFC3339 format](https://www.utctime.net/)\n- [duration] is the maximum duration of the recording in seconds\n- [format] (optional) is the output format of the stream. Available values are \"fmp4\" (default) and \"mp4\"\n\nAll parameters must be [url-encoded](https://www.urlencoder.org/). For instance:\n\n```\nhttp://localhost:9996/get?path=mypath&start=2024-01-14T16%3A33%3A17%2B00%3A00&duration=200.5\n```\n\nThe resulting stream uses the fMP4 format, that is natively compatible with any browser, therefore its URL can be directly inserted into a \\<video> tag:\n\n```html\n<video controls>\n  <source\n    src=\"http://localhost:9996/get?path=[mypath]&start=[start_date]&duration=[duration]\"\n    type=\"video/mp4\"\n  />\n</video>\n```\n\nThe fMP4 format may offer limited compatibility with some players. To fix the issue, it's possible to use the standard MP4 format, by adding `format=mp4` to a `/get` request:\n\n```\nhttp://localhost:9996/get?path=[mypath]&start=[start_date]&duration=[duration]&format=mp4\n```\n"
  },
  {
    "path": "docs/4-other/08-forward.md",
    "content": "# Forward\n\nTo forward incoming streams to another server, use _FFmpeg_ inside the `runOnReady` parameter:\n\n```yml\npathDefaults:\n  runOnReady: >\n    ffmpeg -i rtsp://localhost:$RTSP_PORT/$MTX_PATH\n    -c copy\n    -f rtsp rtsp://other-server:8554/another-path\n  runOnReadyRestart: yes\n```\n"
  },
  {
    "path": "docs/4-other/09-proxy.md",
    "content": "# Proxy\n\nThe server allows to proxy incoming requests to other servers or cameras. This is useful to expose servers or cameras behind a NAT. Edit `mediamtx.yml` and replace everything inside section `paths` with the following content:\n\n```yml\npaths:\n  \"~^proxy_(.+)$\":\n    # If path name is a regular expression, $G1, $G2, etc will be replaced\n    # with regular expression groups.\n    source: rtsp://other-server:8554/$G1\n    sourceOnDemand: yes\n```\n\nAll requests addressed to `rtsp://server:8854/proxy_a` will be forwarded to `rtsp://other-server:8854/a` and so on.\n"
  },
  {
    "path": "docs/4-other/10-extract-snapshots.md",
    "content": "# Extract snapshots\n\nYou can periodically extract snapshots from available streams by using FFmpeg inside the `runOnReady` hook:\n\n```yml\npathDefaults:\n  runOnReady: |\n    bash -c \"\n    while true; do\n      mkdir -p $(dirname snapshots/$MTX_PATH)\n      ffmpeg -i rtsp://localhost:8554/$MTX_PATH -frames:v 1 -update true -y snapshots/$MTX_PATH.jpg\n      sleep 10\n    done\"\n```\n\nWhere `10` is the interval between snapshots.\n"
  },
  {
    "path": "docs/4-other/11-on-demand-publishing.md",
    "content": "# On-demand publishing\n\nEdit `mediamtx.yml` and replace everything inside section `paths` with the following content:\n\n```yml\npaths:\n  ondemand:\n    runOnDemand: ffmpeg -re -stream_loop -1 -i file.mp4 -c copy -f rtsp rtsp://localhost:$RTSP_PORT/$MTX_PATH\n    runOnDemandRestart: yes\n```\n\nThe command inserted into `runOnDemand` will start only when a client requests the path `ondemand`, therefore the file will start streaming only when requested.\n"
  },
  {
    "path": "docs/4-other/12-absolute-timestamps.md",
    "content": "# Route absolute timestamps\n\nSome streaming protocols allow to route absolute timestamps, associated with each frame, that are useful for synchronizing several video or data streams together. In particular, _MediaMTX_ supports receiving absolute timestamps with the following protocols and devices:\n\n- HLS\n- RTSP\n- WebRTC\n- Raspberry Pi Camera\n\nand supports sending absolute timestamps with the following protocols:\n\n- HLS\n- RTSP\n- WebRTC\n\nBy default, absolute timestamps of incoming frames are not used, instead they are replaced with the system timestamp. This prevents users from arbitrarily changing recording dates, and also allows to support sources that do not send absolute timestamps. It is possible to preserve original absolute timestamps by toggling the `useAbsoluteTimestamp` parameter:\n\n```yml\npathDefaults:\n  # Use absolute timestamp of frames, instead of replacing them with the current time.\n  useAbsoluteTimestamp: false\n```\n\n## Absolute timestamp in HLS\n\nIn the HLS protocol, absolute timestamps are routed by adding a `EXT-X-PROGRAM-DATE-TIME` tag before each segment:\n\n```\n#EXTM3U\n#EXT-X-VERSION:9\n#EXT-X-MEDIA-SEQUENCE:20\n#EXT-X-TARGETDURATION:2\n#EXT-X-PROGRAM-DATE-TIME:2015-02-05T01:02:00Z\n#EXTINF:2,\nsegment1.mp4\n#EXT-X-PROGRAM-DATE-TIME:2015-02-05T01:04:00Z\n#EXTINF:2,\nsegment2.mp4\n```\n\nThe `EXT-X-PROGRAM-DATE-TIME` value is the absolute timestamp that corresponds to the first frame of the segment. The absolute timestamp of following frames can be obtained by summing `EXT-X-PROGRAM-DATE-TIME` with the relative frame timestamp.\n\nA library that can read absolute timestamps with HLS is [gohlslib](https://github.com/bluenviron/gohlslib).\n\n## Absolute timestamp in RTSP and WebRTC\n\nIn RTSP and WebRTC, absolute timestamps are routed through periodic RTCP sender reports:\n\n```\n        0                   1                   2                   3\n        0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1\n       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\nheader |V=2|P|    RC   |   PT=SR=200   |             length            |\n       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n       |                         SSRC of sender                        |\n       +=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+=+\nsender |              NTP timestamp, most significant word             |\ninfo   +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n       |             NTP timestamp, least significant word             |\n       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n       |                         RTP timestamp                         |\n...\n```\n\nThe sender report contains a reference absolute timestamp (NTP timestamp) and a reference relative timestamp (RTP timestamp). The absolute timestamp of each frame can be computed by using these values together with the RTP timestamp of the frame (shipped with each frame), through the formula:\n\n```\nframe_abs_timestamp = ref_ntp_timestamp + (frame_rtp_timestamp - ref_rtp_timestamp) / clock_rate\n```\n\nA library that can read absolute timestamps with RTSP is [gortsplib](https://github.com/bluenviron/gortsplib).\n\nA browser can read absolute timestamps with WebRTC if it exposes the [estimatedPlayoutTimestamp](https://www.w3.org/TR/webrtc-stats/#dom-rtcinboundrtpstreamstats-estimatedplayouttimestamp) statistic.\n"
  },
  {
    "path": "docs/4-other/13-expose-the-server-in-a-subfolder.md",
    "content": "# Expose the server in a subfolder\n\nHTTP-based services (WebRTC, HLS, Control API, Playback Server, Metrics, pprof) can be exposed in a subfolder of an existing HTTP server or reverse proxy. The reverse proxy must be able to intercept HTTP requests addressed to _MediaMTX_ and corresponding responses, and perform the following changes:\n\n- The subfolder path must be stripped from request paths. For instance, if the server is exposed behind `/subpath` and the reverse proxy receives a request with path `/subpath/mystream/index.m3u8`, this has to be changed into `/mystream/index.m3u8`.\n\n- Any `Location` header in responses must be prefixed with the subfolder path. For instance, if the server is exposed behind `/subpath` and the server sends a response with `Location: /mystream/index.m3u8`, this has to be changed into `Location: /subfolder/mystream/index.m3u8`.\n\nIf _nginx_ is the reverse proxy, this can be achieved with the following configuration:\n\n```\nlocation /subpath/ {\n    proxy_pass http://mediamtx-ip:8889/;\n    proxy_redirect / /subpath/;\n}\n```\n\nIf _Apache HTTP Server_ is the reverse proxy, this can be achieved with the following configuration:\n\n```\n<Location /subpath>\n    ProxyPass http://mediamtx-ip:8889\n    ProxyPassReverse http://mediamtx-ip:8889\n    Header edit Location ^(.*)$ \"/subpath$1\"\n</Location>\n```\n\nIf _Caddy_ is the reverse proxy, this can be achieved with the following configuration:\n\n```\n:80 {\n    handle_path /subpath/* {\n        reverse_proxy {\n            to mediamtx-ip:8889\n            header_down Location ^/ /subpath/\n        }\n    }\n}\n```\n"
  },
  {
    "path": "docs/4-other/14-embed-streams-in-a-website.md",
    "content": "# Embed streams in a website\n\nLive streams can be embedded into an external website by using the WebRTC or HLS protocol. Before embedding, check that the stream is ready and can be accessed with intended protocol by using URLs mentioned in [Read a stream](../3-read/01-overview.md).\n\n## WebRTC in iframe\n\nThe simplest way to embed a live stream in a web page, using the WebRTC protocol, consists in adding an `<iframe>` tag to the body section of the HTML:\n\n```html\n<iframe src=\"http://mediamtx-ip:8889/mystream\" scrolling=\"no\"></iframe>\n```\n\nThe iframe can be controlled by adding query parameters to the URL (example: `http://mediamtx-ip:8889/mystream?muted=false`). The following parameters are available:\n\n- `controls` (boolean): whether to show controls. Default is true.\n- `muted` (boolean): whether to start the stream muted. Default is true.\n- `autoplay` (boolean): whether to autoplay the stream. Default is true.\n- `playsInline` (boolean): whether to play the stream without using the entire window of mobile devices. Default is true.\n- `disablepictureinpicture` (boolean): whether to disable the ability to open the stream in a dedicated window. Default is false.\n\nThe iframe method is fit for most use cases, but it has some limitations:\n\n- it doesn't allow to pass credentials (username, password or token) from the website to _MediaMTX_; credentials are asked directly to users.\n- it doesn't allow to directly access the video tag, to extract data from it, or to perform dynamic actions.\n\n## WebRTC with JavaScript\n\nIn order to overcome the limitations of the iframe-based method, it is possible to load the stream directly inside a `<video>` tag in the web page, through a JavaScript library.\n\nDownload [reader.js](https://github.com/bluenviron/mediamtx/blob/{version_tag}/internal/servers/webrtc/reader.js) from the repository and serve it together with the other assets of the website.\n\nIf you are using a JavaScript bundler, you can import it by using:\n\n```js\nimport \"./reader.js\";\n```\n\nOtherwise, you can add a `<script>` tag to the `<head>` section of the page:\n\n```html\n<script defer src=\"./reader.js\"></script>\n```\n\nAdd a `<video>` tag:\n\n```html\n<video id=\"myvideo\" controls muted autoplay width=\"640\" height=\"480\"></video>\n```\n\nAfter the video tag, add a script that initializes the stream when the page is fully loaded:\n\n```html\n<script>\n  let reader = null;\n\n  window.addEventListener(\"load\", () => {\n    reader = new MediaMTXWebRTCReader({\n      url: \"http://mediamtx-ip:8889/mystream/whep\",\n      user: \"\", // fill if needed\n      pass: \"\", // fill if needed\n      token: \"\", // fill if needed\n      onError: (err) => {\n        console.error(err);\n      },\n      onTrack: (evt) => {\n        document.getElementById(\"myvideo\").srcObject = evt.streams[0];\n      },\n      onDataChannel: (evt) => {\n        evt.channel.binaryType = \"arraybuffer\";\n        evt.channel.onmessage = (evt) => {\n          console.log(\"data channel message\", evt.data);\n        };\n      },\n    });\n  });\n\n  window.addEventListener(\"beforeunload\", () => {\n    if (reader !== null) {\n      reader.close();\n    }\n  });\n</script>\n```\n\nIf _MediaMTX_ is hosted on a different domain with respect to the website (in the sample code this is implied), you need to set the `webrtcAllowOrigins` parameter in the configuration file. For example, to allow requests from `https://example.com`:\n\n```yaml\nwebrtcAllowOrigins: [\"https://example.com\"]\n```\n\nThe parameter also supports wildcards, for instance `['http://*.example.com']`.\n\n## HLS in iframe\n\nReading a stream with the HLS protocol introduces some latency, but is usually easier to setup since it doesn't involve managing additional ports that in WebRTC are used to transmit the stream.\n\nThe simplest way to embed a live stream in a web page, using the HLS protocol, consists in adding an `<iframe>` tag to the body section of the HTML:\n\n```html\n<iframe src=\"http://mediamtx-ip:8888/mystream\" scrolling=\"no\"></iframe>\n```\n\nThe iframe can be controlled by adding query parameters to the URL (example: `http://mediamtx-ip:8888/mystream?muted=false`). The following parameters are available:\n\n- `controls` (boolean): whether to show controls. Default is true.\n- `muted` (boolean): whether to start the stream muted. Default is true.\n- `autoplay` (boolean): whether to autoplay the stream. Default is true.\n- `playsInline` (boolean): whether to play the stream without using the entire window of mobile devices. Default is true.\n- `disablepictureinpicture` (boolean): whether to disable the ability to open the stream in a dedicated window. Default is false.\n\nThe iframe method is fit for most use cases, but it has some limitations:\n\n- it doesn't allow to pass credentials (username, password or token) from the website to _MediaMTX_; credentials are asked directly to users.\n- it doesn't allow to directly access the video tag, to extract data from it, or to perform dynamic actions.\n\n## HLS with JavaScript\n\nIn order to overcome the limitations of the iframe-based method, it is possible to load the stream directly inside a `<video>` tag in the web page, through the _hls.js_ library.\n\nIf you are using a JavaScript bundler, you can import _hls.js_ by adding [its npm package](https://www.npmjs.com/package/hls.js) as dependency and then importing it:\n\n```js\nimport Hls from \"hls.js\";\n```\n\nOtherwise, you can use a `<script>` tag inside the `<head>` section that points to a CDN:\n\n```html\n<script\n  defer\n  src=\"https://cdnjs.cloudflare.com/ajax/libs/hls.js/1.6.13/hls.min.js\"\n></script>\n```\n\nAdd a `<video>` tag:\n\n```html\n<video id=\"myvideo\" controls muted autoplay width=\"640\" height=\"480\"></video>\n```\n\nAfter the video tag, add a script that initializes the stream when the page is fully loaded:\n\n```html\n<script>\n  window.addEventListener(\"load\", () => {\n    if (Hls.isSupported()) {\n      const hls = new Hls({\n        xhrSetup: function (xhr, url) {\n          let user = \"\"; // fill if needed\n          let pass = \"\"; // fill if needed\n          let token = \"\"; // fill if needed\n\n          if (user !== \"\") {\n            const credentials = btoa(`${user}:${pass}`);\n            xhr.setRequestHeader(\"Authorization\", `Basic ${credentials}`);\n          } else if (token !== \"\") {\n            xhr.setRequestHeader(\"Authorization\", `Bearer ${token}`);\n          }\n        },\n      });\n\n      hls.on(Hls.Events.MEDIA_ATTACHED, () => {\n        hls.loadSource(\"http://mediamtx-ip:8888/mystream/index.m3u8\");\n      });\n\n      hls.attachMedia(document.getElementById(\"myvideo\"));\n    }\n  });\n</script>\n```\n\nIf _MediaMTX_ is hosted on a different domain with respect to the website (in the sample code this is implied), you need to set the `hlsAllowOrigins` parameter in the configuration file. For example:\n\n```yaml\nhlsAllowOrigins: [\"https://example.com\"]\n```\n\nThe parameter also supports wildcards, for instance `['http://*.example.com']`.\n"
  },
  {
    "path": "docs/4-other/15-start-on-boot.md",
    "content": "# Start on boot\n\n## Linux\n\nOn most Linux distributions (including Ubuntu and Debian, but not OpenWrt), _systemd_ is in charge of managing services and starting them on boot.\n\nMove the server executable and configuration in global folders:\n\n```sh\nsudo mv mediamtx /usr/local/bin/\nsudo mv mediamtx.yml /usr/local/etc/\n```\n\nCreate a _systemd_ service:\n\n```sh\nsudo tee /etc/systemd/system/mediamtx.service >/dev/null << EOF\n[Unit]\nAfter=network-online.target\nWants=network-online.target\n[Service]\nExecStart=/usr/local/bin/mediamtx /usr/local/etc/mediamtx.yml\n[Install]\nWantedBy=multi-user.target\nEOF\n```\n\nEnable a _wait-online_ service to make sure that _MediaMTX_ is started after network has been properly initialized:\n\n```sh\nsudo systemctl enable systemd-networkd-wait-online.service\n```\n\nIf SELinux is enabled (for instance in case of RedHat, Rocky, CentOS++), add correct security context:\n\n```sh\nsemanage fcontext -a -t bin_t /usr/local/bin/mediamtx\nrestorecon -Fv /usr/local/bin/mediamtx\n```\n\nEnable and start the service:\n\n```sh\nsudo systemctl daemon-reload\nsudo systemctl enable mediamtx\nsudo systemctl start mediamtx\n```\n\n## OpenWrt\n\nMove the server executable and configuration in global folders:\n\n```sh\nmv mediamtx /usr/bin/\nmkdir -p /usr/etc && mv mediamtx.yml /usr/etc/\n```\n\nCreate a procd service:\n\n```sh\ntee /etc/init.d/mediamtx >/dev/null << EOF\n#!/bin/sh /etc/rc.common\nUSE_PROCD=1\nSTART=95\nSTOP=01\nstart_service() {\n    procd_open_instance\n    procd_set_param command /usr/bin/mediamtx\n    procd_set_param stdout 1\n    procd_set_param stderr 1\n    procd_close_instance\n}\nEOF\n```\n\nEnable and start the service:\n\n```sh\nchmod +x /etc/init.d/mediamtx\n/etc/init.d/mediamtx enable\n/etc/init.d/mediamtx start\n```\n\nRead the server logs:\n\n```sh\nlogread\n```\n\n## Windows\n\nDownload the [WinSW v2 executable](https://github.com/winsw/winsw/releases) and place it into the same folder of `mediamtx.exe`.\n\nIn the same folder, create a file named `WinSW-x64.xml` with this content:\n\n```xml\n<service>\n  <id>mediamtx</id>\n  <name>mediamtx</name>\n  <description></description>\n  <executable>%BASE%/mediamtx.exe</executable>\n</service>\n```\n\nOpen a terminal, navigate to the folder and run:\n\n```\nWinSW-x64 install\n```\n\nThe server is now installed as a system service and will start at boot time.\n"
  },
  {
    "path": "docs/4-other/16-logging.md",
    "content": "# Logging\n\n## Log verbosity\n\nLog verbosity can be set with the `logLevel` parameter:\n\n```yml\n# Verbosity of the program; available values are \"error\", \"warn\", \"info\", \"debug\".\nlogLevel: info\n```\n\n## Log destinations\n\nLog entries can be sent to multiple destinations. By default, they are printed on the console (stdout).\n\nIt is possible to write logs to a file by using these parameters:\n\n```yml\n# Destinations of log messages; available values are \"stdout\", \"file\" and \"syslog\".\nlogDestinations: [file]\n# If \"file\" is in logDestinations, this is the file which will receive the logs.\nlogFile: mediamtx.log\n```\n\nIt is possible to write logs to the system logging server (syslog) by using these parameters:\n\n```yml\n# Destinations of log messages; available values are \"stdout\", \"file\" and \"syslog\".\nlogDestinations: [syslog]\n# If \"syslog\" is in logDestinations, use prefix for logs.\nsysLogPrefix: mediamtx\n```\n\nLog entries can be queried by using:\n\n```sh\njournalctl SYSLOG_IDENTIFIER=mediamtx\n```\n\nIf _MediaMTX_ is also running as a [system service](15-start-on-boot.md), log entries can be queried by using:\n\n```sh\njournalctl -u mediamtx\n```\n\n## Structured logging\n\nLog collectors (like Loki, Logstash, CloudWatch and fluentd) parse logs in a more reliable way if they are fed with entries in structured format (JSONL). This can be enabled with the `logStructured` parameter:\n\n```yml\n# When destination is \"stdout\" or \"file\", emit logs in structured format (JSONL).\nlogStructured: true\n```\n\nObtaining:\n\n```\n{\"timestamp\":\"20XX-YY-ZZT10:45:05.999999999+01:00\",\"level\":\"INF\",\"message\":\"[RTSP] listener opened on :8554 (TCP), :8000 (UDP/RTP), :8001 (UDP/RTCP)\"}\n{\"timestamp\":\"20XX-YY-ZZT10:45:05.999999999+01:00\",\"level\":\"INF\",\"message\":\"[RTMP] listener opened on :1935\"}\n{\"timestamp\":\"20XX-YY-ZZT10:45:05.999999999+01:00\",\"level\":\"INF\",\"message\":\"[HLS] listener opened on :8888\"}\n{\"timestamp\":\"20XX-YY-ZZT10:45:05.999999999+01:00\",\"level\":\"INF\",\"message\":\"[WebRTC] listener opened on :8889 (HTTP), :8189 (ICE/UDP)\"}\n{\"timestamp\":\"20XX-YY-ZZT10:45:05.999999999+01:00\",\"level\":\"INF\",\"message\":\"[SRT] listener opened on :8890 (UDP)\"}\n```\n\n## Log file rotation\n\nThe log file can be periodically rotated or truncated by using an external utility.\n\nOn most Linux distributions, the `logrotate` utility is in charge of managing log files. It can be configured to handle the _MediaMTX_ log file too by creating a configuration file, placed in `/etc/logrotate.d/mediamtx`, with this content:\n\n```\n/my/mediamtx/path/mediamtx.log {\n    daily\n    copytruncate\n    rotate 7\n    compress\n    delaycompress\n    missingok\n    notifempty\n}\n```\n\nThis file will rotate the log file every day, adding a `.NUMBER` suffix to older copies:\n\n```\nmediamtx.log.1\nmediamtx.log.2\nmediamtx.log.3\n...\n```\n"
  },
  {
    "path": "docs/4-other/17-hooks.md",
    "content": "# Hooks\n\nThe server allows to specify commands that are executed when a certain event happens, allowing the propagation of events to external software.\n\n## runOnConnect\n\n`runOnConnect` allows to run a command when a client connects to the server:\n\n```yml\n# Command to run when a client connects to the server.\n# This is terminated with SIGINT when a client disconnects from the server.\n# The following environment variables are available:\n# * MTX_CONN_TYPE: connection type\n# * MTX_CONN_ID: connection ID\n# * RTSP_PORT: RTSP server port\nrunOnConnect: curl http://my-custom-server/webhook?conn_type=$MTX_CONN_TYPE&conn_id=$MTX_CONN_ID\n# Restart the command if it exits.\nrunOnConnectRestart: no\n```\n\n## runOnDisconnect\n\n`runOnDisconnect` allows to run a command when a client disconnects from the server:\n\n```yml\n# Command to run when a client disconnects from the server.\n# Environment variables are the same as runOnConnect.\nrunOnDisconnect: curl http://my-custom-server/webhook?conn_type=$MTX_CONN_TYPE&conn_id=$MTX_CONN_ID\n```\n\n## runOnInit\n\n`runOnInit` allows to run a command when a path is initialized. This can be used to publish a stream when the server is launched:\n\n```yml\npaths:\n  mypath:\n    # Command to run when this path is initialized.\n    # This can be used to publish a stream when the server is launched.\n    # The following environment variables are available:\n    # * MTX_PATH: path name\n    # * RTSP_PORT: RTSP server port\n    # * G1, G2, ...: regular expression groups, if path name is\n    #   a regular expression.\n    runOnInit: ffmpeg -i my_file.mp4 -c copy -f rtsp rtsp://localhost:8554/mypath\n    # Restart the command if it exits.\n    runOnInitRestart: no\n```\n\n## runOnDemand\n\n`runOnDemand` allows to run a command when a path is requested by a reader. This can be used to publish a stream on demand:\n\n```yml\npathDefaults:\n  # Command to run when this path is requested by a reader\n  # and no one is publishing to this path yet.\n  # This is terminated with SIGINT when there are no readers anymore.\n  # The following environment variables are available:\n  # * MTX_PATH: path name\n  # * MTX_QUERY: query parameters (passed by first reader)\n  # * RTSP_PORT: RTSP server port\n  # * G1, G2, ...: regular expression groups, if path name is\n  #   a regular expression.\n  runOnDemand: ffmpeg -i my_file.mp4 -c copy -f rtsp rtsp://localhost:8554/mypath\n  # Restart the command if it exits.\n  runOnDemandRestart: no\n```\n\n## runOnUnDemand\n\n`runOnUnDemand` allows to run a command when there are no readers anymore:\n\n```yml\npathDefaults:\n  # Command to run when there are no readers anymore.\n  # Environment variables are the same as runOnDemand.\n  runOnUnDemand:\n```\n\n## runOnReady\n\n`runOnReady` allows to run a command when a stream is ready to be read:\n\n```yml\npathDefaults:\n  # Command to run when the stream is ready to be read, whenever it is\n  # published by a client or pulled from a server / camera.\n  # This is terminated with SIGINT when the stream is not ready anymore.\n  # The following environment variables are available:\n  # * MTX_PATH: path name\n  # * MTX_QUERY: query parameters (passed by publisher)\n  # * MTX_SOURCE_TYPE: source type\n  # * MTX_SOURCE_ID: source ID\n  # * RTSP_PORT: RTSP server port\n  # * G1, G2, ...: regular expression groups, if path name is\n  #   a regular expression.\n  runOnReady: curl http://my-custom-server/webhook?path=$MTX_PATH&source_type=$MTX_SOURCE_TYPE&source_id=$MTX_SOURCE_ID\n  # Restart the command if it exits.\n  runOnReadyRestart: no\n```\n\n## runOnNotReady\n\n`runOnNotReady` allows to run a command when a stream is not available anymore:\n\n```yml\npathDefaults:\n  # Command to run when the stream is not available anymore.\n  # Environment variables are the same as runOnReady.\n  runOnNotReady: curl http://my-custom-server/webhook?path=$MTX_PATH&source_type=$MTX_SOURCE_TYPE&source_id=$MTX_SOURCE_ID\n```\n\n## runOnRead\n\n`runOnRead` allows to run a command when a client starts reading:\n\n```yml\npathDefaults:\n  # Command to run when a client starts reading.\n  # This is terminated with SIGINT when a client stops reading.\n  # The following environment variables are available:\n  # * MTX_PATH: path name\n  # * MTX_QUERY: query parameters (passed by reader)\n  # * MTX_READER_TYPE: reader type\n  # * MTX_READER_ID: reader ID\n  # * RTSP_PORT: RTSP server port\n  # * G1, G2, ...: regular expression groups, if path name is\n  #   a regular expression.\n  runOnRead: curl http://my-custom-server/webhook?path=$MTX_PATH&reader_type=$MTX_READER_TYPE&reader_id=$MTX_READER_ID\n  # Restart the command if it exits.\n  runOnReadRestart: no\n```\n\n## runOnUnread\n\n`runOnUnread` allows to run a command when a client stops reading:\n\n```yml\npathDefaults:\n  # Command to run when a client stops reading.\n  # Environment variables are the same as runOnRead.\n  runOnUnread: curl http://my-custom-server/webhook?path=$MTX_PATH&reader_type=$MTX_READER_TYPE&reader_id=$MTX_READER_ID\n```\n\n## runOnRecordSegmentCreate\n\n`runOnRecordSegmentCreate` allows to run a command when a recording segment is created:\n\n```yml\npathDefaults:\n  # Command to run when a recording segment is created.\n  # The following environment variables are available:\n  # * MTX_PATH: path name\n  # * MTX_SEGMENT_PATH: segment file path\n  # * RTSP_PORT: RTSP server port\n  # * G1, G2, ...: regular expression groups, if path name is\n  #   a regular expression.\n  runOnRecordSegmentCreate: curl http://my-custom-server/webhook?path=$MTX_PATH&segment_path=$MTX_SEGMENT_PATH\n```\n\n## runOnRecordSegmentComplete\n\n`runOnRecordSegmentComplete` allows to run a command when a recording segment is complete:\n\n```yml\npathDefaults:\n  # Command to run when a recording segment is complete.\n  # The following environment variables are available:\n  # * MTX_PATH: path name\n  # * MTX_SEGMENT_PATH: segment file path\n  # * MTX_SEGMENT_DURATION: segment duration\n  # * RTSP_PORT: RTSP server port\n  # * G1, G2, ...: regular expression groups, if path name is\n  #   a regular expression.\n  runOnRecordSegmentComplete: curl http://my-custom-server/webhook?path=$MTX_PATH&segment_path=$MTX_SEGMENT_PATH\n```\n"
  },
  {
    "path": "docs/4-other/18-control-api.md",
    "content": "# Control API\n\nThe server can be queried and controlled with an API, that can be enabled by toggling the `api` parameter in the configuration:\n\n```yml\napi: yes\n```\n\nTo obtain a list of active paths, run:\n\n```\ncurl http://127.0.0.1:9997/v3/paths/list\n```\n\nThe control API is documented in the [Control API Reference page](../5-references/2-control-api.md) and in the [OpenAPI / Swagger file](https://github.com/bluenviron/mediamtx/blob/{version_tag}/api/openapi.yaml).\n\nBe aware that by default the Control API is accessible by localhost only; to increase visibility or add authentication, check [Authentication](03-authentication.md).\n"
  },
  {
    "path": "docs/4-other/19-metrics.md",
    "content": "# Extract metrics\n\n_MediaMTX_ provides several metrics through a dedicated HTTP server, in a format compatible with [Prometheus](https://prometheus.io/).\n\nThis server can be enabled by setting `metrics: yes` in the configuration.\n\nMetrics can be extracted with Prometheus or with a simple HTTP request:\n\n```\ncurl localhost:9998/metrics\n```\n\nObtaining:\n\n```ini\n# metrics of every path\npaths{name=\"[path_name]\",state=\"[state]\"} 1\npaths_bytes_received{name=\"[path_name]\",state=\"[state]\"} 1234\npaths_bytes_sent{name=\"[path_name]\",state=\"[state]\"} 1234\npaths_readers{name=\"[path_name]\",state=\"[state]\"} 1234\n\n# metrics of every HLS muxer\nhls_muxers{name=\"[name]\"} 1\nhls_muxers_bytes_sent{name=\"[name]\"} 187\n\n# metrics of every RTSP connection\nrtsp_conns{id=\"[id]\"} 1\nrtsp_conns_bytes_received{id=\"[id]\"} 1234\nrtsp_conns_bytes_sent{id=\"[id]\"} 187\n\n# metrics of every RTSP session\nrtsp_sessions{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 1\nrtsp_sessions_bytes_received{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 1234\nrtsp_sessions_bytes_sent{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 187\nrtsp_sessions_rtp_packets_received{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nrtsp_sessions_rtp_packets_sent{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nrtsp_sessions_rtp_packets_lost{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nrtsp_sessions_rtp_packets_in_error{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nrtsp_sessions_rtp_packets_jitter{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nrtsp_sessions_rtcp_packets_received{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nrtsp_sessions_rtcp_packets_sent{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nrtsp_sessions_rtcp_packets_in_error{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\n\n# metrics of every RTSPS connection\nrtsps_conns{id=\"[id]\"} 1\nrtsps_conns_bytes_received{id=\"[id]\"} 1234\nrtsps_conns_bytes_sent{id=\"[id]\"} 187\n\n# metrics of every RTSPS session\nrtsps_sessions{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 1\nrtsps_sessions_bytes_received{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 1234\nrtsps_sessions_bytes_sent{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 187\nrtsps_sessions_rtp_packets_received{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nrtsps_sessions_rtp_packets_sent{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nrtsps_sessions_rtp_packets_lost{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nrtsps_sessions_rtp_packets_in_error{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nrtsps_sessions_rtp_packets_jitter{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nrtsps_sessions_rtcp_packets_received{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nrtsps_sessions_rtcp_packets_sent{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nrtsps_sessions_rtcp_packets_in_error{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\n\n# metrics of every RTMP connection\nrtmp_conns{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 1\nrtmp_conns_bytes_received{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 1234\nrtmp_conns_bytes_sent{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 187\n\n# metrics of every RTMPS connection\nrtmps_conns{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 1\nrtmps_conns_bytes_received{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 1234\nrtmps_conns_bytes_sent{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 187\n\n# metrics of every SRT connection\nsrt_conns{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 1\nsrt_conns_packets_sent{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_packets_received{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_packets_sent_unique{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_packets_received_unique{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_packets_send_loss{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_packets_received_loss{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_packets_retrans{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_packets_received_retrans{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_packets_sent_ack{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_packets_received_ack{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_packets_sent_nak{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_packets_received_nak{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_packets_sent_km{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_packets_received_km{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_us_snd_duration{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_packets_send_drop{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_packets_received_drop{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_packets_received_undecrypt{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_bytes_sent{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 187\nsrt_conns_bytes_received{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 1234\nsrt_conns_bytes_sent_unique{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_bytes_received_unique{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_bytes_received_loss{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_bytes_retrans{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_bytes_received_retrans{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_bytes_send_drop{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_bytes_received_drop{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_bytes_received_undecrypt{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_us_packets_send_period{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123.123\nsrt_conns_packets_flow_window{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_packets_flight_size{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_ms_rtt{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123.123\nsrt_conns_mbps_send_rate{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_mbps_receive_rate{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123.123\nsrt_conns_mbps_link_capacity{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123.123\nsrt_conns_bytes_avail_send_buf{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_bytes_avail_receive_buf{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_mbps_max_bw{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} -123\nsrt_conns_bytes_mss{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_packets_send_buf{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_bytes_send_buf{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_ms_send_buf{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_ms_send_tsb_pd_delay{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_packets_receive_buf{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_bytes_receive_buf{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_ms_receive_buf{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_ms_receive_tsb_pd_delay{iid=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_packets_reorder_tolerance{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_packets_received_avg_belated_time{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_packets_send_loss_rate{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nsrt_conns_packets_received_loss_rate{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\n\n# metrics of every WebRTC session\nwebrtc_sessions{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 1\nwebrtc_sessions_bytes_received{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 1234\nwebrtc_sessions_bytes_sent{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 187\nwebrtc_sessions_rtp_packets_received{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nwebrtc_sessions_rtp_packets_sent{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nwebrtc_sessions_rtp_packets_lost{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nwebrtc_sessions_rtp_packets_jitter{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nwebrtc_sessions_rtcp_packets_received{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\nwebrtc_sessions_rtcp_packets_sent{id=\"[id]\",path=\"[path]\",remoteAddr=\"[remoteAddr]\",state=\"[state]\"} 123\n```\n\nBitrates are not provided directly as metrics because they can be computed from received and sent bytes by any metrics analyzer (i.e. Grafana).\n\nMetrics can be filtered by using HTTP query parameters:\n\n- `type=[TYPE]`: show metrics of a certain type only (where TYPE can be `paths`, `hls_muxers`, `rtsp_conns`, `rtsp_sessions`, `rtsps_conns`, `rtsps_sessions`, `rtmp_conns`, `rtmps_conns`, `srt_conns`, `webrtc_sessions`)\n- `path=[PATH]`: show metrics belonging to a specific path only\n- `hls_muxer=[PATH]`: show metrics belonging to a specific HLS muxer only\n- `rtsp_conn=[ID]` show metrics belonging to a specific RTSP connection only\n- `rtsp_session=[SESSION]`: show metrics belonging to a specific RTSP session only\n- `rtsps_conn=[ID]` show metrics belonging to a specific RTSPS connection only\n- `rtsps_session=[SESSION]`: show metrics belonging to a specific RTSPS session only\n- `rtmp_conn=[ID]` show metrics belonging to a specific RTMP connection only\n- `rtmps_conn=[ID]` show metrics belonging to a specific RTMPS connection only\n- `srt_conn=[ID]` show metrics belonging to a specific SRT connection only\n- `webrtc_session=[ID]` show metrics belonging to a specific WebRTC session only\n"
  },
  {
    "path": "docs/4-other/20-performance.md",
    "content": "# Monitor performance\n\nCPU and memory consumption can be monitored over time through an integrated performance monitor, that produces reports in a format compatible with [pprof](https://github.com/google/pprof), which is a tool included in any Go installation.\n\nThe performance monitor can be enabled by setting `pprof: yes` in the configuration.\n\nReports can be extracted and displayed by using the `go tool pprof` command.\n\nOccupied memory can be analyzed by running:\n\n```sh\ngo tool pprof -text http://localhost:9999/debug/pprof/heap\n```\n\nObtaining:\n\n```\nFetching profile over HTTP from http://localhost:9999/debug/pprof/heap\nSaved profile in /home/xxx/pprof/pprof.mediamtx.alloc_objects.alloc_space.inuse_objects.inuse_space.007.pb.gz\nFile: mediamtx\nBuild ID: dfb7c97dbb5e9ce59438172269231b35a873e3e9\nType: inuse_space\nTime: Sep 99, 9999 at 12:00pm (CEST)\nShowing nodes accounting for 5145.10kB, 100% of 5145.10kB total\n      flat  flat%   sum%        cum   cum%\n    2052kB 39.88% 39.88%     2052kB 39.88%  runtime.allocm\n  525.43kB 10.21% 50.09%   525.43kB 10.21%  github.com/go-playground/validator/v10.map.init.7\n  518.65kB 10.08% 60.18%   518.65kB 10.08%  github.com/go-playground/validator/v10.map.init.3\n  512.75kB  9.97% 70.14%   512.75kB  9.97%  github.com/bluenviron/gortsplib/v4.(*serverUDPListener).run.func1 (inline)\n  512.22kB  9.96% 80.10%   512.22kB  9.96%  runtime.malg\n  512.02kB  9.95% 90.05%   512.02kB  9.95%  crypto/tls.init\n  512.02kB  9.95%   100%   512.02kB  9.95%  internal/abi.NewName\n         0     0%   100%   512.75kB  9.97%  github.com/bluenviron/gortsplib/v4.(*serverUDPListener).run\n         0     0%   100%   512.02kB  9.95%  github.com/bluenviron/mediamtx/internal/conf.init\n         0     0%   100%   512.02kB  9.95%  github.com/bluenviron/mediamtx/internal/conf.init.func2 (inline)\n         0     0%   100%  1044.08kB 20.29%  github.com/go-playground/validator/v10.init\n         0     0%   100%   512.02kB  9.95%  reflect.StructOf\n         0     0%   100%   512.02kB  9.95%  reflect.newName (inline)\n         0     0%   100%   512.02kB  9.95%  reflect.runtimeStructField\n         0     0%   100%  2068.13kB 40.20%  runtime.doInit (inline)\n         0     0%   100%  2068.13kB 40.20%  runtime.doInit1\n         0     0%   100%  2068.13kB 40.20%  runtime.main\n         0     0%   100%      513kB  9.97%  runtime.mcall\n         0     0%   100%     1539kB 29.91%  runtime.mstart\n         0     0%   100%     1539kB 29.91%  runtime.mstart0\n         0     0%   100%     1539kB 29.91%  runtime.mstart1\n         0     0%   100%     2052kB 39.88%  runtime.newm\n         0     0%   100%   512.22kB  9.96%  runtime.newproc.func1\n         0     0%   100%   512.22kB  9.96%  runtime.newproc1\n         0     0%   100%      513kB  9.97%  runtime.park_m\n         0     0%   100%     2052kB 39.88%  runtime.resetspinning\n         0     0%   100%     2052kB 39.88%  runtime.schedule\n         0     0%   100%     2052kB 39.88%  runtime.startm\n         0     0%   100%   512.22kB  9.96%  runtime.systemstack\n         0     0%   100%     2052kB 39.88%  runtime.wakep\n```\n\nConsumed CPU can be analyzed by running:\n\n```sh\ngo tool pprof -text http://localhost:9999/debug/pprof/profile?seconds=15\n```\n\nObtaining:\n\n```\nFetching profile over HTTP from http://localhost:9999/debug/pprof/profile?seconds=15\nSaved profile in /home/xxx/pprof/pprof.mediamtx.samples.cpu.003.pb.gz\nFile: mediamtx\nBuild ID: dfb7c97dbb5e9ce59438172269231b35a873e3e9\nType: cpu\nTime: Sep 99, 9999 at 12:00pm (CEST)\nDuration: 15s, Total samples = 0\nShowing nodes accounting for 70ms, 100% of 70ms total\n      flat  flat%   sum%        cum   cum%\n      30ms 42.86% 42.86%       30ms 42.86%  internal/runtime/syscall.Syscall6\n      10ms 14.29% 57.14%       10ms 14.29%  runtime.(*consistentHeapStats).acquire\n      10ms 14.29% 71.43%       10ms 14.29%  runtime.futex\n      10ms 14.29% 85.71%       10ms 14.29%  runtime.mapIterStart\n      10ms 14.29%   100%       10ms 14.29%  runtime.netpollblockcommit\n         0     0%   100%       10ms 14.29%  github.com/bluenviron/gortsplib/v4.(*serverSessionFormat).readPacketRTP\n         0     0%   100%       10ms 14.29%  github.com/bluenviron/gortsplib/v4.(*serverSessionMedia).readPacketRTPUDPRecord\n         0     0%   100%       50ms 71.43%  github.com/bluenviron/gortsplib/v4.(*serverUDPListener).run\n         0     0%   100%       10ms 14.29%  github.com/bluenviron/gortsplib/v4.(*serverUDPListener).run.func2\n         0     0%   100%       10ms 14.29%  github.com/bluenviron/mediamtx/internal/protocols/rtsp.ToStream.func2\n         0     0%   100%       10ms 14.29%  github.com/bluenviron/mediamtx/internal/stream.(*Stream).WriteRTPPacket\n         0     0%   100%       10ms 14.29%  github.com/bluenviron/mediamtx/internal/stream.(*streamFormat).writeRTPPacket\n         0     0%   100%       10ms 14.29%  github.com/bluenviron/mediamtx/internal/stream.(*streamFormat).writeUnitInner\n         0     0%   100%       30ms 42.86%  internal/poll.(*FD).ReadFromInet6\n         0     0%   100%       10ms 14.29%  internal/runtime/syscall.EpollWait\n         0     0%   100%       40ms 57.14%  net.(*UDPConn).ReadFrom\n         0     0%   100%       30ms 42.86%  net.(*UDPConn).readFrom\n         0     0%   100%       30ms 42.86%  net.(*UDPConn).readFromUDP\n         0     0%   100%       30ms 42.86%  net.(*netFD).readFromInet6\n         0     0%   100%       10ms 14.29%  runtime.(*mcache).nextFree\n         0     0%   100%       10ms 14.29%  runtime.(*mcache).refill\n         0     0%   100%       10ms 14.29%  runtime.entersyscall\n         0     0%   100%       10ms 14.29%  runtime.entersyscall_sysmon\n         0     0%   100%       10ms 14.29%  runtime.findRunnable\n         0     0%   100%       10ms 14.29%  runtime.futexwakeup\n         0     0%   100%       10ms 14.29%  runtime.mallocgc\n         0     0%   100%       10ms 14.29%  runtime.mallocgcSmallScanNoHeader\n         0     0%   100%       20ms 28.57%  runtime.mcall\n         0     0%   100%       10ms 14.29%  runtime.netpoll\n         0     0%   100%       10ms 14.29%  runtime.newobject\n         0     0%   100%       10ms 14.29%  runtime.notewakeup\n         0     0%   100%       20ms 28.57%  runtime.park_m\n         0     0%   100%       10ms 14.29%  runtime.reentersyscall\n         0     0%   100%       10ms 14.29%  runtime.schedule\n         0     0%   100%       10ms 14.29%  runtime.systemstack\n         0     0%   100%       20ms 28.57%  syscall.RawSyscall6\n         0     0%   100%       30ms 42.86%  syscall.Syscall6\n         0     0%   100%       30ms 42.86%  syscall.recvfrom\n         0     0%   100%       30ms 42.86%  syscall.recvfromInet6\n```\n\nActive routines can be listed by running:\n\n```sh\ngo tool pprof -text http://localhost:9999/debug/pprof/goroutine\n```\n\nObtaining:\n\n```\nFetching profile over HTTP from http://localhost:9999/debug/pprof/goroutine\nSaved profile in /home/xxx/pprof/pprof.mediamtx.goroutine.044.pb.gz\nFile: mediamtx\nBuild ID: dfb7c97dbb5e9ce59438172269231b35a873e3e9\nType: goroutine\nTime: Sep 99, 9999 at 12:00pm (CEST)\nShowing nodes accounting for 27, 100% of 27 total\n      flat  flat%   sum%        cum   cum%\n        25 92.59% 92.59%         25 92.59%  runtime.gopark\n         1  3.70% 96.30%          1  3.70%  runtime.goroutineProfileWithLabels\n         1  3.70%   100%          1  3.70%  runtime.notetsleepg\n         0     0%   100%          1  3.70%  github.com/bluenviron/gortsplib/v4.(*Server).Wait (inline)\n         0     0%   100%          1  3.70%  github.com/bluenviron/gortsplib/v4.(*Server).run\n         0     0%   100%          1  3.70%  github.com/bluenviron/gortsplib/v4.(*Server).runInner\n         0     0%   100%          1  3.70%  github.com/bluenviron/gortsplib/v4.(*serverTCPListener).run\n         0     0%   100%          2  7.41%  github.com/bluenviron/gortsplib/v4.(*serverUDPListener).run\n         0     0%   100%          1  3.70%  github.com/bluenviron/mediamtx/internal/confwatcher.(*ConfWatcher).run\n         0     0%   100%          1  3.70%  github.com/bluenviron/mediamtx/internal/core.(*Core).Wait (inline)\n         0     0%   100%          1  3.70%  github.com/bluenviron/mediamtx/internal/core.(*Core).run\n         0     0%   100%          1  3.70%  github.com/bluenviron/mediamtx/internal/core.(*pathManager).run\n         0     0%   100%          1  3.70%  github.com/bluenviron/mediamtx/internal/protocols/httpp.(*handlerExitOnPanic).ServeHTTP\n         0     0%   100%          1  3.70%  github.com/bluenviron/mediamtx/internal/protocols/httpp.(*handlerFilterRequests).ServeHTTP\n         0     0%   100%          1  3.70%  github.com/bluenviron/mediamtx/internal/protocols/httpp.(*handlerLogger).ServeHTTP\n         0     0%   100%          1  3.70%  github.com/bluenviron/mediamtx/internal/protocols/httpp.(*handlerServerHeader).ServeHTTP\n         0     0%   100%          1  3.70%  github.com/bluenviron/mediamtx/internal/recordcleaner.(*Cleaner).run\n         0     0%   100%          1  3.70%  github.com/bluenviron/mediamtx/internal/servers/hls.(*Server).run\n         0     0%   100%          1  3.70%  github.com/bluenviron/mediamtx/internal/servers/rtmp.(*Server).run\n         0     0%   100%          1  3.70%  github.com/bluenviron/mediamtx/internal/servers/rtmp.(*listener).run\n         0     0%   100%          1  3.70%  github.com/bluenviron/mediamtx/internal/servers/rtmp.(*listener).runInner\n         0     0%   100%          1  3.70%  github.com/bluenviron/mediamtx/internal/servers/rtsp.(*Server).run\n         0     0%   100%          1  3.70%  github.com/bluenviron/mediamtx/internal/servers/rtsp.(*Server).run.func1\n         0     0%   100%          1  3.70%  github.com/bluenviron/mediamtx/internal/servers/srt.(*Server).run\n         0     0%   100%          1  3.70%  github.com/bluenviron/mediamtx/internal/servers/srt.(*listener).run\n         0     0%   100%          1  3.70%  github.com/bluenviron/mediamtx/internal/servers/srt.(*listener).runInner\n         0     0%   100%          1  3.70%  github.com/bluenviron/mediamtx/internal/servers/webrtc.(*Server).run\n         0     0%   100%          1  3.70%  github.com/datarhei/gosrt.(*listener).Accept2\n         0     0%   100%          1  3.70%  github.com/datarhei/gosrt.(*listener).reader\n         0     0%   100%          1  3.70%  github.com/datarhei/gosrt.Listen.func1\n         0     0%   100%          1  3.70%  github.com/fsnotify/fsnotify.(*inotify).readEvents\n         0     0%   100%          1  3.70%  github.com/gin-contrib/pprof.RouteRegister.WrapH.func9\n         0     0%   100%          1  3.70%  github.com/gin-gonic/gin.(*Context).Next (inline)\n         0     0%   100%          1  3.70%  github.com/gin-gonic/gin.(*Engine).ServeHTTP\n         0     0%   100%          1  3.70%  github.com/gin-gonic/gin.(*Engine).handleHTTPRequest\n         0     0%   100%          1  3.70%  github.com/pion/ice/v4.(*UDPMuxDefault).connWorker\n         0     0%   100%          5 18.52%  internal/poll.(*FD).Accept\n         0     0%   100%          2  7.41%  internal/poll.(*FD).Read\n         0     0%   100%          4 14.81%  internal/poll.(*FD).ReadFromInet6\n         0     0%   100%         11 40.74%  internal/poll.(*pollDesc).wait\n         0     0%   100%         11 40.74%  internal/poll.(*pollDesc).waitRead (inline)\n         0     0%   100%         11 40.74%  internal/poll.runtime_pollWait\n         0     0%   100%          1  3.70%  main.main\n         0     0%   100%          5 18.52%  net.(*TCPListener).Accept\n         0     0%   100%          5 18.52%  net.(*TCPListener).accept\n         0     0%   100%          4 14.81%  net.(*UDPConn).ReadFrom\n         0     0%   100%          4 14.81%  net.(*UDPConn).readFrom\n         0     0%   100%          4 14.81%  net.(*UDPConn).readFromUDP\n         0     0%   100%          1  3.70%  net.(*conn).Read\n         0     0%   100%          1  3.70%  net.(*netFD).Read\n         0     0%   100%          5 18.52%  net.(*netFD).accept\n         0     0%   100%          4 14.81%  net.(*netFD).readFromInet6\n         0     0%   100%          3 11.11%  net/http.(*Server).Serve\n         0     0%   100%          1  3.70%  net/http.(*conn).serve\n         0     0%   100%          1  3.70%  net/http.(*connReader).backgroundRead\n         0     0%   100%          1  3.70%  net/http.serverHandler.ServeHTTP\n         0     0%   100%          1  3.70%  net/http/pprof.handler.ServeHTTP\n         0     0%   100%          1  3.70%  os.(*File).Read\n         0     0%   100%          1  3.70%  os.(*File).read (inline)\n         0     0%   100%          1  3.70%  os/signal.loop\n         0     0%   100%          1  3.70%  os/signal.signal_recv\n         0     0%   100%          1  3.70%  runtime.chanrecv\n         0     0%   100%          1  3.70%  runtime.chanrecv1\n         0     0%   100%          1  3.70%  runtime.goparkunlock (inline)\n         0     0%   100%          1  3.70%  runtime.main\n         0     0%   100%         11 40.74%  runtime.netpollblock\n         0     0%   100%          1  3.70%  runtime.pprof_goroutineProfileWithLabels\n         0     0%   100%         12 44.44%  runtime.selectgo\n         0     0%   100%          1  3.70%  runtime.semacquire1\n         0     0%   100%          1  3.70%  runtime/pprof.(*Profile).WriteTo\n         0     0%   100%          1  3.70%  runtime/pprof.writeGoroutine\n         0     0%   100%          1  3.70%  runtime/pprof.writeRuntimeProfile\n         0     0%   100%          1  3.70%  sync.(*WaitGroup).Wait\n         0     0%   100%          1  3.70%  sync.runtime_SemacquireWaitGroup\n```\n"
  },
  {
    "path": "docs/4-other/21-srt-specific-features.md",
    "content": "# SRT-specific features\n\nSRT is a protocol that can be used for publishing and reading streams. Regarding specific tasks, read [Publish](../2-publish/02-srt-clients.md) and [Read](../3-read/02-srt.md). Features in this page are shared among both tasks.\n\n## Standard stream ID syntax\n\nIn SRT, the stream ID is a string that is sent to the remote part in order to advertise what action the caller is going to do (publish or read), the path and the credentials. All this information has to be encoded into a single string. This server supports two stream ID syntaxes, a custom one (that is the one reported in the rest of the README) and also a [standard one](https://github.com/Haivision/srt/blob/master/docs/features/access-control.md) proposed by the authors of the protocol and enforced by some hardware. The standard syntax can be used in this way:\n\n```\nsrt://localhost:8890?streamid=#!::m=publish,r=mypath,u=myuser,s=mypass&pkt_size=1316\n```\n\nWhere:\n\n- key `m` contains the action (`publish` or `request`)\n- key `r` contains the path\n- key `u` contains the username\n- key `s` contains the password\n"
  },
  {
    "path": "docs/4-other/22-webrtc-specific-features.md",
    "content": "# WebRTC-specific features\n\nWebRTC is a protocol that can be used for publishing and reading streams. Regarding specific tasks, read [Publish](../2-publish/04-webrtc-clients.md) and [Read](../3-read/03-webrtc.md). Features in this page are shared among both tasks.\n\n## Codec support in browsers\n\nThe server can ingest and broadcast with WebRTC a wide variety of video and audio codecs (that are listed at the beginning of the README), but not all browsers can publish and read all codecs due to internal limitations that cannot be overcome by this or any other server.\n\nIn particular, reading and publishing H265 tracks with WebRTC was not possible until some time ago due to lack of browser support. The situation improved recently and can be described as following:\n\n- Safari on iOS and macOS fully support publishing and reading H265 tracks.\n- Chrome on Windows supports publishing and reading H265 tracks when a capable GPU is present.\n\nYou can check what codecs your browser can publish or read with WebRTC by [using this tool](https://jsfiddle.net/v24s8q1f/).\n\nIf you want to support most browsers, you can re-encode the stream by using H264 and Opus codecs, for instance by using FFmpeg:\n\n```sh\nffmpeg -i rtsp://original-source \\\n-c:v libx264 -pix_fmt yuv420p -preset ultrafast -b:v 600k \\\n-c:a libopus -b:a 64K -async 50 \\\n-f rtsp rtsp://localhost:8554/mystream\n```\n\n## Solving WebRTC connectivity issues\n\nIn WebRTC, the handshake between server and clients happens through standard HTTP requests and responses, while media streaming takes place inside a dedicated communication channel (peer connection) that is set up shortly after the handshake. The server supports establishing peer connections through the following methods (ordered by efficiency and simplicity):\n\n1. using a static UDP server port (`webrtcLocalUDPAddress` must be filled, it is by default)\n2. using a static TCP server port (`webrtcLocalTCPAddress` must be filled, it is not by default)\n3. using a random UDP server port and UDP client port with the hole-punching technique (`webrtcICEServers2` must contain a STUN server, not present by default)\n4. using a relay (TURN server) that exposes a TCP port that is accessed by both server and clients (`webrtcICEServers2` must contain a TURN server, not present by default)\n\nEstablishing the peer connection might get difficult when the server is hosted inside a container or there is a NAT / firewall between server and clients.\n\nThe first thing to do is making sure that `webrtcAdditionalHosts` includes your public IPs, that are IPs that can be used by clients to reach the server. If clients are on the same LAN as the server, add the LAN address of the server. If clients are coming from the internet, add the public IP address of the server, or alternatively a DNS name, if you have one. You can add several values to support all scenarios:\n\n```yml\nwebrtcAdditionalHosts: [192.168.x.x, 1.2.3.4, my-dns.example.org, ...]\n```\n\nIf there's a NAT / container between server and clients, it must be configured to route all incoming UDP packets on port 8189 to the server. If you're using Docker, this can be achieved with the flag:\n\n```sh\ndocker run --rm -it \\\n-p 8189:8189/udp\n....\nbluenviron/mediamtx:1\n```\n\nIf you still have problems, the UDP protocol might be blocked by a firewall. Switch to the TCP protocol by enabling the TCP server port:\n\n```yml\nwebrtcLocalTCPAddress: :8189\n```\n\nIf there's a NAT / container between server and clients, it must be configured to route all incoming TCP packets on port 8189 to the server.\n\nIf you still have problems, add a STUN server, that is used by both server and clients to find out their public IP. Connections are then established with the \"UDP hole punching\" technique, that uses a random UDP port that does not need to be explicitly opened. For instance:\n\n```yml\nwebrtcICEServers2:\n  - url: stun:stun.l.google.com:19302\n```\n\nIf you really still have problems, you can force all WebRTC/ICE connections to pass through a TURN server. The server address and credentials must be set in the configuration file:\n\n```yml\nwebrtcICEServers2:\n  - url: turn:host:port\n    username: user\n    password: password\n```\n\nWhere user and pass are the username and password of the server. Note that port is not optional.\n\nIf the server uses a secret-based authentication (for instance, Coturn with the use-auth-secret option), it must be configured by using AUTH_SECRET as username, and the secret as password:\n\n```yml\nwebrtcICEServers2:\n  - url: turn:host:port\n    username: AUTH_SECRET\n    password: secret\n```\n\nWhere secret is the secret of the TURN server. _MediaMTX_ will generate a set of credentials by using the secret, and credentials will be sent to clients before the WebRTC/ICE connection is established.\n\nIn some cases you may want the browser to connect using TURN servers but have _MediaMTX_ not using TURN (for example if the TURN server is on the same network as mediamtx). To allow this you can configure the TURN server to be client only:\n\n```yml\nwebrtcICEServers2:\n  - url: turn:host:port\n    username: user\n    password: password\n    clientOnly: true\n```\n\n## Coturn setup\n\nHere's how to setup the [Coturn](https://github.com/coturn/coturn) TURN server and use it with _MediaMTX_. This is needed only if all other WebRTC connectivity methods have failed. Start Coturn with Docker:\n\n```sh\ndocker run --rm -it \\\n--network=host \\\ncoturn/coturn \\\n--log-file=stdout -v \\\n--no-udp --no-dtls --no-tls \\\n--min-port=49152 --max-port=65535 \\\n--use-auth-secret --static-auth-secret=mysecret -r myrealm\n```\n\nWe are suggesting and using the following settings:\n\n- enable the TCP transport only. We are assuming you are setting up Coturn because other connectivity methods have failed, thus TCP is more reliable.\n- toggle `--network=host` since Coturn allocates a TCP port for each peer connection.\n- set `min-port` and `max-port` to specify the range of TCP ports.\n- enable secret-based authentication, that prevents clients from storing permanently valid credentials.\n\nConfigure MediaMTX to use the TURN server:\n\n```yml\nwebrtcICEServers2:\n  - url: turn:REPLACE_WITH_COTURN_IP:3478?transport=tcp\n    username: AUTH_SECRET\n    password: mysecret\n```\n\nThe `?transport=tcp` suffix is needed to force TCP usage. Use `AUTH_SECRET` as username and the shared secret as the password.\n"
  },
  {
    "path": "docs/4-other/23-rtsp-specific-features.md",
    "content": "# RTSP-specific features\n\nRTSP is a protocol that can be used for publishing and reading streams. Regarding specific tasks, read [Publish](../2-publish/06-rtsp-clients.md) and [Read](../3-read/04-rtsp.md). Features in this page are shared among both tasks.\n\n## Transport protocols\n\nA RTSP session is split in two parts: the handshake, which is always performed with the TCP protocol, and data streaming, which can be performed with an arbitrary underlying transport protocol, which is chosen by the client during the handshake:\n\n- UDP: the most performant, but requires clients to access two additional UDP ports on the server, which is often impossible due to blocking or remapping by NATs/firewalls in between.\n- UDP-multicast: allows to save bandwidth when clients are all in the same LAN, by sending packets once to a fixed multicast IP.\n- TCP: the most versatile.\n\nTo change the transport protocol, you have to tune the configuration of the client you are using to publish or read streams. In most clients, the default transport protocol is UDP.\n\nFFmpeg allows to change the transport protocol with the `-rtsp_transport` flag:\n\n```sh\nffmpeg -rtsp_transport tcp -i rtsp://localhost:8554/mystream -c copy output.mp4\n```\n\nAvailable options are:\n\n- `-rtsp_transport tcp` to pick the TCP transport protocol\n- `-rtsp_transport udp` to pick the UDP transport protocol\n- `-rtsp_transport udp_multicast` to pick the UDP-multicast transport protocol\n\nGStreamer allows to change the transport protocol with the `protocols` property of `rtspsrc` and `rtspclientsink`:\n\n```sh\ngst-launch-1.0 filesrc location=file.mp4 ! qtdemux name=d \\\nd.video_0 ! rtspclientsink location=rtsp://localhost:8554/mystream protocols=tcp\n```\n\nAvailable options are:\n\n- `protocols=tcp` to pick the TCP transport protocol\n- `protocols=udp` to pick the UDP transport protocol\n- `protocols=udp-mcast` to pick the UDP-multicast transport protocol\n\nVLC allows to use the TCP transport protocol through the `--rtsp_tcp` flag:\n\n```sh\nvlc --network-caching=50 --rtsp-tcp rtsp://localhost:8554/mystream\n```\n\nVLC allows to use the UDP-multicast transport protocol by appending `?vlcmulticast` to the URL:\n\n```sh\nvlc --network-caching=50 rtsp://localhost:8554/mystream?vlcmulticast\n```\n\n## Encryption\n\nIncoming and outgoing RTSP streams can be encrypted by replacing all the subprotocols that are normally used in RTSP with their secure variants (RTSPS, SRTP, SRTCP). A TLS certificate is needed and can be generated with OpenSSL:\n\n```sh\nopenssl genrsa -out server.key 2048\nopenssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650\n```\n\nEdit `mediamtx.yml` and set the `encryption`, `serverKey` and serverCert parameters:\n\n```yml\nrtspEncryption: optional\nrtspServerKey: server.key\nrtspServerCert: server.crt\n```\n\nStreams can be published and read with the `rtsps` scheme and the `8322` port:\n\n```\nrtsps://localhost:8322/mystream\n```\n\nSome clients require additional flags for encryption to work properly.\n\nWhen reading with GStreamer, set `tls-validation-flags` to `0`:\n\n```sh\ngst-launch-1.0 rtspsrc tls-validation-flags=0 location=rtsps://ip:8322/...\n```\n\nWhen publishing with GStreamer, set `tls-validation-flags` to `0` and `profiles` to `GST_RTSP_PROFILE_SAVP`:\n\n```sh\ngst-launch-1.0 filesrc location=file.mp4 ! qtdemux name=d \\\nd.video_0 ! rtspclientsink location=rtsp://localhost:8554/mystream tls-validation-flags=0 profiles=GST_RTSP_PROFILE_SAVP\n```\n\n## Tunneling\n\nIn environments where HTTP is the only protocol available for exposing services (for instance, when there are mandatory API gateways or strict firewalls), the RTSP protocol can be tunneled inside HTTP. There are two standardized HTTP tunneling variants:\n\n- RTSP over WebSocket: more efficient, requires WebSocket support from the gateway / firewall\n- RTSP over HTTP: older variant, should work even in extreme cases\n\n_MediaMTX_ is automatically able to handle incoming HTTP-tunneled RTSP connections without any configuration required.\n\nIn order to read a stream from an external RTSP server using HTTP tunneling, you can use the `rtsp+http` scheme:\n\n```yml\npaths:\n  source: rtsp+http://standard-rtsp-url\n```\n\nThere are also the `rtsp+https`, `rtsp+ws`, `rtsp+wss` schemes to handle any combination.\n\n## MPEG-TS inside RTSP\n\nread [MPEG-TS inside RTSP](../2-publish/06-rtsp-clients.md#mpeg-ts-inside-rtsp) in the \"Publish with RTSP clients\" page.\n"
  },
  {
    "path": "docs/4-other/24-rtmp-specific-features.md",
    "content": "# RTMP-specific features\n\nRTMP is a protocol that can be used for publishing and reading streams. Regarding specific tasks, read [Publish](../2-publish/08-rtmp-clients.md) and [Read](../3-read/05-rtmp.md). Features in this page are shared among both tasks.\n\n## Encryption\n\nRTMP connections can be encrypted by using the secure protocol variant (RTMPS). A TLS certificate is needed and can be generated with OpenSSL:\n\n```yml\nopenssl genrsa -out server.key 2048\nopenssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650\n```\n\nEdit mediamtx.yml and set the `rtmpEncryption`, `rtmpServerKey` and `rtmpServerCert` parameters:\n\n```yml\nrtmpEncryption: optional\nrtmpServerKey: server.key\nrtmpServerCert: server.crt\n```\n\nStreams can be published and read with the rtmps scheme and the 1937 port:\n\n```\nrtmps://localhost:1937/...\n```\n\nBe aware that RTMPS is currently unsupported by all major players. However, you can use a local _MediaMTX_ instance to decrypt streams before reading them, or alternatively a proxy like [stunnel](https://www.stunnel.org) or [nginx](https://nginx.org/). For instance, you can launch a local _MediaMTX_ instance with this configuration:\n\n```yml\npaths:\n  decrypted:\n    source: rtmps://original-stream\n```\n\nAnd then read `rtmp://localhost/decrypted` instead of `rtmps://original-stream`.\n"
  },
  {
    "path": "docs/4-other/25-decrease-packet-loss.md",
    "content": "# Decrease packet loss\n\nMediaMTX is meant for routing live streams, and makes use of a series of protocols and techniques which try to preserve the real-time aspect of streams and minimize latency at cost of losing packets in transmit, in particular:\n\n- most protocols are built on UDP, which is an \"unreliable transport\", specifically picked because it allows dropping late packets in case of network congestion.\n- there's a circular buffer that stores outgoing packets and drops packets if full.\n\nPacket losses are usually detected and printed in _MediaMTX_ logs.\n\nIf you need to improve the stream reliability and decrease packet losses, the first thing to do is to check whether the physical network between the _MediaMTX_ instance and the intended publishers and readers has sufficient bandwidth for transmitting the media stream. Most of the time, packet losses are caused by a network which is not fit for this scope. This limitation can be overcome by either recompressing the stream with a lower bitrate, or by upgrading the network infrastructure (routers, cables, Wi-Fi, firewalls, topology, etc).\n\nNonetheless there are some parameters that can be tuned to improve the situation, at cost of increasing RAM consumption:\n\n- When publishing a stream with a UDP-based protocol (currently RTSP, MPEG-TS, RTP, SRT, WebRTC), packets might get discarded by the server because the read buffer size of UDP sockets is too small. It can be increased with this parameter:\n\n  ```yml\n  udpReadBufferSize: 1000000\n  ```\n\n  The `udpReadBufferSize` parameter requires the `net.core.rmem_max` system parameter to be equal or greater than it. It can be set with this command:\n\n  ```sh\n  sudo sysctl net.core.rmem_max=100000000\n  ```\n\n- When reading a stream, packets might get discarded because the write queue is too small. This can be noticed in logs through the \"reader is too slow\" message. Try increasing the write queue:\n\n  ```yml\n  writeQueueSize: 1024\n  ```\n\n- When publishing or reading a stream with RTSP, it's possible to switch from the UDP transport protocol to the TCP transport protocol, which is less performant but has a packet retransmission mechanism:\n\n  ```yml\n  rtspTransports: [tcp]\n  ```\n\n  In case the source is a camera:\n\n  ```yml\n  paths:\n    test:\n      source: rtsp://..\n      rtspTransport: tcp\n  ```\n"
  },
  {
    "path": "docs/4-other/index.md",
    "content": "# Other features\n"
  },
  {
    "path": "docs/5-references/1-configuration-file.md",
    "content": "# Configuration file reference\n\nThis is a copy of the configuration file (`mediamtx.yml`) of the latest _MediaMTX_ release ({version_tag}), that contains all available parameters. Check the [Configuration usage page](../4-other/02-configuration.md) for instructions on how to change it.\n"
  },
  {
    "path": "docs/5-references/2-control-api.md",
    "content": "# Control API reference\n\nThis is the reference of the Control API of the latest _MediaMTX_ release ({version_tag}), generated automatically from the [OpenAPI / Swagger file](https://github.com/bluenviron/mediamtx/blob/{version_tag}/api/openapi.yaml) available in the repository. Check the [Control API usage page](../4-other/18-control-api.md) for instructions on how to use the API.\n"
  },
  {
    "path": "docs/5-references/index.md",
    "content": "# References\n"
  },
  {
    "path": "docs/6-misc/1-compile.md",
    "content": "# Compile from source\n\n## Standard procedure\n\n1. Install git and Go &ge; 1.25.\n\n2. Clone the repository, enter into the folder and start the building process:\n\n   ```sh\n   git clone https://github.com/bluenviron/mediamtx\n   cd mediamtx\n   go generate ./...\n   CGO_ENABLED=0 go build .\n   ```\n\n   This will produce the `mediamtx` binary.\n\n## Custom libcamera\n\nIf you need to use a custom or external libcamera to interact with some Raspberry Pi Camera model that requires it, additional steps are required:\n\n1. Download [mediamtx-rpicamera source code](https://github.com/bluenviron/mediamtx-rpicamera) and compile it against the external libcamera. Instructions are in the repository.\n\n2. Install git and Go &ge; 1.25.\n\n3. Clone the _MediaMTX_ repository:\n\n   ```sh\n   git clone https://github.com/bluenviron/mediamtx\n   ```\n\n4. Inside the _MediaMTX_ folder, run:\n\n   ```sh\n   go generate ./...\n   ```\n\n5. Copy `build/mtxrpicam_32` and/or `build/mtxrpicam_64` (depending on the architecture) from `mediamtx-rpicamera` to `mediamtx`, inside folder `internal/staticsources/rpicamera/`, overriding existing folders.\n\n6. Compile:\n\n   ```sh\n   go run .\n   ```\n\n   This will produce the `mediamtx` binary.\n\n## Cross compile\n\nCross compilation allows to build an executable for a target machine from another machine with a different operating system or architecture. This is useful in case the target machine doesn't have enough resources for compilation or if you don't want to install the compilation dependencies on it.\n\n1. On the machine you want to use to compile, install git and Go &ge; 1.25.\n\n2. Clone the repository, enter into the folder and start the building process:\n\n   ```sh\n   git clone https://github.com/bluenviron/mediamtx\n   cd mediamtx\n   go generate ./...\n   CGO_ENABLED=0 GOOS=my_os GOARCH=my_arch go build .\n   ```\n\n   Replace `my_os` and `my_arch` with the operating system and architecture of your target machine. A list of all supported combinations can be obtained with:\n\n   ```sh\n   go tool dist list\n   ```\n\n   For instance:\n\n   ```sh\n   CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build .\n   ```\n\n   In case of the `arm` architecture, there's an additional flag available, `GOARM`, that allows to set the ARM version:\n\n   ```sh\n   CGO_ENABLED=0 GOOS=linux GOARCH=arm64 GOARM=7 go build .\n   ```\n\n   In case of the `mips` architecture, there's an additional flag available, `GOMIPS`, that allows to set additional parameters:\n\n   ```sh\n   CGO_ENABLED=0 GOOS=linux GOARCH=mips GOMIPS=softfloat go build .\n   ```\n\n   The command will produce the `mediamtx` binary.\n\n## Compile for all supported platforms\n\nInstall Docker and launch:\n\n```sh\nmake binaries\n```\n\nThe command will produce tarballs in folder `binaries/`.\n\n## Docker image\n\nThe official Docker image can be recompiled by following these steps:\n\n1. Build binaries for all supported platforms:\n\n   ```sh\n   make binaries\n   ```\n\n2. Build the image by using one of the Dockerfiles inside the `docker/` folder:\n\n   ```\n   docker build . -f docker/standard.Dockerfile -t my-mediamtx\n   ```\n\n   This will produce the `my-mediamtx` image.\n\n   A Dockerfile is available for each image variant (`standard.Dockerfile`, `ffmpeg.Dockerfile`, `rpi.Dockerfile`, `ffmpeg-rpi.Dockerfile`).\n"
  },
  {
    "path": "docs/6-misc/2-license.md",
    "content": "# License\n\nAll the code in the main _MediaMTX_ repository is released under the [MIT License](https://github.com/bluenviron/mediamtx/blob/{version_tag}/LICENSE). Compiled binaries include some third-party dependencies:\n\n- all the Golang dependencies listed into the [go.mod file](https://github.com/bluenviron/mediamtx/blob/{version_tag}/go.mod), which are all released under either the MIT license, BSD 3-Clause license or Apache License 2.0.\n- hls.js, released under the [Apache License 2.0](https://github.com/video-dev/hls.js/blob/master/LICENSE).\n- mediamtx-rpicamera, which is released under the same license of _MediaMTX_ but includes some [third-party dependencies](https://github.com/bluenviron/mediamtx-rpicamera?tab=readme-ov-file#license).\n"
  },
  {
    "path": "docs/6-misc/3-security.md",
    "content": "# Security\n\n## Security of released binaries\n\nBinaries published in the [Releases](https://github.com/bluenviron/mediamtx/releases) section of GitHub are the output of a building process that is fully visible, prevents hidden changes or external interferences in published artifacts, and allows validation by third parties:\n\n1. During every release, the [Release workflow](https://github.com/bluenviron/mediamtx/actions/workflows/release.yml) is triggered on GitHub.\n\n2. The release workflow pulls the source code and builds binaries.\n\n3. The release workflow computes SHA256 checksums of binaries and publishes them to a public blockchain (Sigstore Public Good Instance) through [GitHub Attestations](https://docs.github.com/en/actions/concepts/security/artifact-attestations).\n\n4. Checksums and binaries are published on the Release page.\n\n5. Binaries can be downloaded by users.\n\nIt is possible to verify that SHA256 checksums of binaries correspond to the one published on Sigstore by running:\n\n```sh\nls mediamtx_* | xargs -L1 gh attestation verify --repo bluenviron/mediamtx\n```\n\nIt is possible to verify that binaries have not been altered during transfer from GitHub to the final destination by downloading `checksums.sha256` and running:\n\n```sh\ncat checksums.sha256 | grep \"$(ls mediamtx_*)\" | sha256sum --check\n```\n\n## Reporting vulnerabilities\n\nVulnerabilities can be reported privately by using the [Security Advisory](https://github.com/bluenviron/mediamtx/security/advisories/new) feature of GitHub.\n"
  },
  {
    "path": "docs/6-misc/4-specifications.md",
    "content": "# Specifications\n\n| name                                                                                                          | area           |\n| ------------------------------------------------------------------------------------------------------------- | -------------- |\n| [RTSP / RTP / RTCP specifications](https://github.com/bluenviron/gortsplib#specifications)                    | RTSP           |\n| [HLS specifications](https://github.com/bluenviron/gohlslib#specifications)                                   | HLS            |\n| [RTMP specifications](https://github.com/bluenviron/gortmplib#specifications)                                 | RTMP           |\n| [WebRTC: Real-Time Communication in Browsers](https://www.w3.org/TR/webrtc/)                                  | WebRTC         |\n| [RFC8835, Transports for WebRTC](https://datatracker.ietf.org/doc/html/rfc8835)                               | WebRTC         |\n| [RFC7742, WebRTC Video Processing and Codec Requirements](https://datatracker.ietf.org/doc/html/rfc7742)      | WebRTC         |\n| [RFC7874, WebRTC Audio Codec and Processing Requirements](https://datatracker.ietf.org/doc/html/rfc7874)      | WebRTC         |\n| [RFC7875, Additional WebRTC Audio Codecs for Interoperability](https://datatracker.ietf.org/doc/html/rfc7875) | WebRTC         |\n| [H.265 Profile for WebRTC](https://datatracker.ietf.org/doc/draft-ietf-avtcore-hevc-webrtc/)                  | WebRTC         |\n| [WebRTC HTTP Ingestion Protocol (WHIP)](https://datatracker.ietf.org/doc/draft-ietf-wish-whip/)               | WebRTC         |\n| [WebRTC HTTP Egress Protocol (WHEP)](https://datatracker.ietf.org/doc/draft-murillo-whep/)                    | WebRTC         |\n| [The SRT Protocol](https://haivision.github.io/srt-rfc/draft-sharabayko-srt.html)                             | SRT            |\n| [Codec specifications](https://github.com/bluenviron/mediacommon#specifications)                              | codecs         |\n| [Golang project layout](https://github.com/golang-standards/project-layout)                                   | project layout |\n"
  },
  {
    "path": "docs/6-misc/5-related-projects.md",
    "content": "# Related projects\n\n- [gortsplib (RTSP library used internally)](https://github.com/bluenviron/gortsplib)\n- [gohlslib (HLS library used internally)](https://github.com/bluenviron/gohlslib)\n- [gortmplib (RTMP library used internally)](https://github.com/bluenviron/gortmplib)\n- [mediacommon (codecs and formats library used internally)](https://github.com/bluenviron/mediacommon)\n- [mediamtx-rpicamera (Raspberry Pi Camera component)](https://github.com/bluenviron/mediamtx-rpicamera)\n- [datarhei/gosrt (SRT library used internally)](https://github.com/datarhei/gosrt)\n- [pion/webrtc (WebRTC library used internally)](https://github.com/pion/webrtc)\n- [pion/sdp (SDP library used internally)](https://github.com/pion/sdp)\n- [pion/rtp (RTP library used internally)](https://github.com/pion/rtp)\n- [pion/rtcp (RTCP library used internally)](https://github.com/pion/rtcp)\n- [go-astits (MPEG-TS library used internally)](https://github.com/asticode/go-astits)\n- [go-mp4 (MP4 library used internally)](https://github.com/abema/go-mp4)\n- [hls.js (browser-side HLS library used internally)](https://github.com/video-dev/hls.js)\n"
  },
  {
    "path": "docs/6-misc/index.md",
    "content": "# Misc\n"
  },
  {
    "path": "docs/redirects.yaml",
    "content": "kickoff/installation: kickoff/install\n\nusage/publish: publish/overview\nusage/read: read/overview\nusage/log-management: other/logging\nusage/basic-usage: other/basic-usage\nusage/configuration: other/configuration\nusage/authentication: other/authentication\nusage/remuxing-reencoding-compression: other/remuxing-reencoding-compression\nusage/always-available: other/always-available\nusage/record: other/record\nusage/playback: other/playback\nusage/forward: other/forward\nusage/proxy: other/proxy\nusage/extract-snapshots: other/extract-snapshots\nusage/on-demand-publishing: other/on-demand-publishing\nusage/route-absolute-timestamps: other/route-absolute-timestamps\nusage/expose-the-server-in-a-subfolder: other/expose-the-server-in-a-subfolder\nusage/embed-streams-in-a-website: other/embed-streams-in-a-website\nusage/start-on-boot: other/start-on-boot\nusage/logging: other/logging\nusage/hooks: other/hooks\nusage/control-api: other/control-api\nusage/metrics: other/metrics\nusage/performance: other/performance\nusage/srt-specific-features: other/srt-specific-features\nusage/webrtc-specific-features: other/webrtc-specific-features\nusage/rtsp-specific-features: other/rtsp-specific-features\nusage/rtmp-specific-features: other/rtmp-specific-features\nusage/decrease-packet-loss: other/decrease-packet-loss\n\nother/compile: misc/compile\nother/license: misc/license\nother/security: misc/security\nother/specifications: misc/specifications\nother/related-projects: misc/related-projects\nother/route-absolute-timestamps: other/absolute-timestamps\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/bluenviron/mediamtx\n\ngo 1.25.0\n\nrequire (\n\tcode.cloudfoundry.org/bytefmt v0.67.0\n\tgithub.com/Masterminds/semver/v3 v3.4.0\n\tgithub.com/MicahParks/jwkset v0.11.0\n\tgithub.com/MicahParks/keyfunc/v3 v3.8.0\n\tgithub.com/abema/go-mp4 v1.5.0\n\tgithub.com/alecthomas/kong v1.14.0\n\tgithub.com/asticode/go-astits v1.15.0\n\tgithub.com/bluenviron/gohlslib/v2 v2.2.9\n\tgithub.com/bluenviron/gortmplib v0.3.0\n\tgithub.com/bluenviron/gortsplib/v5 v5.5.0\n\tgithub.com/bluenviron/mediacommon/v2 v2.8.3\n\tgithub.com/datarhei/gosrt v0.10.0\n\tgithub.com/fsnotify/fsnotify v1.9.0\n\tgithub.com/gin-contrib/pprof v1.5.3\n\tgithub.com/gin-gonic/gin v1.12.0\n\tgithub.com/go-git/go-billy/v5 v5.8.0\n\tgithub.com/go-git/go-git/v5 v5.17.0\n\tgithub.com/goccy/go-yaml v1.19.2\n\tgithub.com/golang-jwt/jwt/v5 v5.3.1\n\tgithub.com/google/gopacket v1.1.19\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/gookit/color v1.6.0\n\tgithub.com/gorilla/websocket v1.5.3\n\tgithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51\n\tgithub.com/matthewhartstonge/argon2 v1.4.6\n\tgithub.com/minio/selfupdate v0.6.0\n\tgithub.com/pion/ice/v4 v4.2.1\n\tgithub.com/pion/interceptor v0.1.44\n\tgithub.com/pion/logging v0.2.4\n\tgithub.com/pion/rtcp v1.2.16\n\tgithub.com/pion/rtp v1.10.1\n\tgithub.com/pion/sdp/v3 v3.0.18\n\tgithub.com/pion/transport/v4 v4.0.1\n\tgithub.com/pion/webrtc/v4 v4.2.9\n\tgithub.com/stretchr/testify v1.11.1\n\tgolang.org/x/crypto v0.49.0\n\tgolang.org/x/sys v0.42.0\n\tgolang.org/x/term v0.41.0\n)\n\nrequire (\n\taead.dev/minisign v0.2.0 // indirect\n\tdario.cat/mergo v1.0.0 // indirect\n\tgithub.com/Microsoft/go-winio v0.6.2 // indirect\n\tgithub.com/ProtonMail/go-crypto v1.1.6 // indirect\n\tgithub.com/asticode/go-astikit v0.30.0 // indirect\n\tgithub.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c // indirect\n\tgithub.com/bytedance/gopkg v0.1.3 // indirect\n\tgithub.com/bytedance/sonic v1.15.0 // indirect\n\tgithub.com/bytedance/sonic/loader v0.5.0 // indirect\n\tgithub.com/cloudflare/circl v1.6.3 // indirect\n\tgithub.com/cloudwego/base64x v0.1.6 // indirect\n\tgithub.com/cyphar/filepath-securejoin v0.4.1 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/emirpasic/gods v1.18.1 // indirect\n\tgithub.com/gabriel-vasile/mimetype v1.4.12 // indirect\n\tgithub.com/gin-contrib/sse v1.1.0 // indirect\n\tgithub.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect\n\tgithub.com/go-playground/locales v0.14.1 // indirect\n\tgithub.com/go-playground/universal-translator v0.18.1 // indirect\n\tgithub.com/go-playground/validator/v10 v10.30.1 // indirect\n\tgithub.com/goccy/go-json v0.10.5 // indirect\n\tgithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect\n\tgithub.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect\n\tgithub.com/json-iterator/go v1.1.12 // indirect\n\tgithub.com/kevinburke/ssh_config v1.2.0 // indirect\n\tgithub.com/klauspost/cpuid/v2 v2.3.0 // indirect\n\tgithub.com/leodido/go-urn v1.4.0 // indirect\n\tgithub.com/mattn/go-isatty v0.0.20 // indirect\n\tgithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect\n\tgithub.com/modern-go/reflect2 v1.0.2 // indirect\n\tgithub.com/pelletier/go-toml/v2 v2.2.4 // indirect\n\tgithub.com/pion/datachannel v1.6.0 // indirect\n\tgithub.com/pion/dtls/v3 v3.1.2 // indirect\n\tgithub.com/pion/mdns/v2 v2.1.0 // indirect\n\tgithub.com/pion/randutil v0.1.0 // indirect\n\tgithub.com/pion/sctp v1.9.2 // indirect\n\tgithub.com/pion/srtp/v3 v3.0.10 // indirect\n\tgithub.com/pion/stun/v3 v3.1.1 // indirect\n\tgithub.com/pion/turn/v4 v4.1.4 // indirect\n\tgithub.com/pjbgf/sha1cd v0.3.2 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/quic-go/qpack v0.6.0 // indirect\n\tgithub.com/quic-go/quic-go v0.59.0 // indirect\n\tgithub.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect\n\tgithub.com/skeema/knownhosts v1.3.1 // indirect\n\tgithub.com/twitchyliquid64/golang-asm v0.15.1 // indirect\n\tgithub.com/ugorji/go/codec v1.3.1 // indirect\n\tgithub.com/wlynxg/anet v0.0.5 // indirect\n\tgithub.com/xanzy/ssh-agent v0.3.3 // indirect\n\tgithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect\n\tgo.mongodb.org/mongo-driver/v2 v2.5.0 // indirect\n\tgolang.org/x/arch v0.22.0 // indirect\n\tgolang.org/x/net v0.52.0 // indirect\n\tgolang.org/x/text v0.35.0 // indirect\n\tgolang.org/x/time v0.12.0 // indirect\n\tgoogle.golang.org/protobuf v1.36.10 // indirect\n\tgopkg.in/warnings.v0 v0.1.2 // indirect\n\tgopkg.in/yaml.v3 v3.0.1 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "aead.dev/minisign v0.2.0 h1:kAWrq/hBRu4AARY6AlciO83xhNnW9UaC8YipS2uhLPk=\naead.dev/minisign v0.2.0/go.mod h1:zdq6LdSd9TbuSxchxwhpA9zEb9YXcVGoE8JakuiGaIQ=\ncode.cloudfoundry.org/bytefmt v0.67.0 h1:5zOnQBHYlHQMXXs42nzUJHUVTlhni0Kdlaxzwguxudg=\ncode.cloudfoundry.org/bytefmt v0.67.0/go.mod h1:JT2/SZbghzGk207djL8l4a1IMsxG/48vf+RbqTa/SBA=\ndario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=\ndario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=\ngithub.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0=\ngithub.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=\ngithub.com/MicahParks/jwkset v0.11.0 h1:yc0zG+jCvZpWgFDFmvs8/8jqqVBG9oyIbmBtmjOhoyQ=\ngithub.com/MicahParks/jwkset v0.11.0/go.mod h1:U2oRhRaLgDCLjtpGL2GseNKGmZtLs/3O7p+OZaL5vo0=\ngithub.com/MicahParks/keyfunc/v3 v3.8.0 h1:Hx2dgIjAXGk9slakM6rV9BOeaWDPEXXZ4Us8guNBfds=\ngithub.com/MicahParks/keyfunc/v3 v3.8.0/go.mod h1:z66bkCviwqfg2YUp+Jcc/xRE9IXLcMq6DrgV/+Htru0=\ngithub.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY=\ngithub.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=\ngithub.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=\ngithub.com/ProtonMail/go-crypto v1.1.6 h1:ZcV+Ropw6Qn0AX9brlQLAUXfqLBc7Bl+f/DmNxpLfdw=\ngithub.com/ProtonMail/go-crypto v1.1.6/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE=\ngithub.com/abema/go-mp4 v1.5.0 h1:aJnu723gFuNswIiM08h4kO28pUZr0QXNAJGoZWT96AU=\ngithub.com/abema/go-mp4 v1.5.0/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws=\ngithub.com/alecthomas/assert/v2 v2.11.0 h1:2Q9r3ki8+JYXvGsDyBXwH3LcJ+WK5D0gc5E8vS6K3D0=\ngithub.com/alecthomas/assert/v2 v2.11.0/go.mod h1:Bze95FyfUr7x34QZrjL+XP+0qgp/zg8yS+TtBj1WA3k=\ngithub.com/alecthomas/kong v1.14.0 h1:gFgEUZWu2ZmZ+UhyZ1bDhuutbKN1nTtJTwh19Wsn21s=\ngithub.com/alecthomas/kong v1.14.0/go.mod h1:wrlbXem1CWqUV5Vbmss5ISYhsVPkBb1Yo7YKJghju2I=\ngithub.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=\ngithub.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=\ngithub.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8=\ngithub.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4=\ngithub.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=\ngithub.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=\ngithub.com/asticode/go-astikit v0.30.0 h1:DkBkRQRIxYcknlaU7W7ksNfn4gMFsB0tqMJflxkRsZA=\ngithub.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=\ngithub.com/asticode/go-astits v1.15.0 h1:yRyCiUc8Jj4F7clt2GDxHghMpWuFL5rkaLuGUd2/0J4=\ngithub.com/asticode/go-astits v1.15.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=\ngithub.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c h1:8XZeJrs4+ZYhJeJ2aZxADI2tGADS15AzIF8MQ8XAhT4=\ngithub.com/benburkert/openpgp v0.0.0-20160410205803-c2471f86866c/go.mod h1:x1vxHcL/9AVzuk5HOloOEPrtJY0MaalYr78afXZ+pWI=\ngithub.com/bluenviron/gohlslib/v2 v2.2.9 h1:ahRbuBzw+FvdVDwli731RgJsErunyBDWhghpS5fT2qQ=\ngithub.com/bluenviron/gohlslib/v2 v2.2.9/go.mod h1:kA0hTg96hmTZjeZ/Vxwpu/Njy0emAoidEoDGQ/KlMH0=\ngithub.com/bluenviron/gortmplib v0.3.0 h1:jBNl7bYtXSq+USx70gQ4kzRsffM1h0XRjGwlHQHZVq8=\ngithub.com/bluenviron/gortmplib v0.3.0/go.mod h1:3tUsWceMOrs8Ylt5UyQEHNHCVl52V1gi9tz+IaLTafI=\ngithub.com/bluenviron/gortsplib/v5 v5.5.0 h1:2wlUKhlSw1ptKEVnkEuZMpQuN8Xt311Fdd/SB124JPA=\ngithub.com/bluenviron/gortsplib/v5 v5.5.0/go.mod h1:otcPqR836QZej/EYx7njn8vl4TLj8Ya3QAf+GBwh2cQ=\ngithub.com/bluenviron/mediacommon/v2 v2.8.3 h1:T6xb7ZK3eBixi/HynzhtGRCEIrazwcmGIeu0WDTVISY=\ngithub.com/bluenviron/mediacommon/v2 v2.8.3/go.mod h1:CsYjGgzIz8RbloQf4BHR4uReogZsB4PEKWfePVIzJv8=\ngithub.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=\ngithub.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=\ngithub.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=\ngithub.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=\ngithub.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=\ngithub.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=\ngithub.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=\ngithub.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=\ngithub.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=\ngithub.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=\ngithub.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY=\ngithub.com/cyphar/filepath-securejoin v0.4.1 h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=\ngithub.com/cyphar/filepath-securejoin v0.4.1/go.mod h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=\ngithub.com/datarhei/gosrt v0.10.0 h1:dPn+gOo93JTvW2Rfd8Jfz+J/sUbdr+miE9eHUNMoPZY=\ngithub.com/datarhei/gosrt v0.10.0/go.mod h1:M+KtvdduBtW64WqCByr6Mb27eNYBMNs0/RtTgY7d6TQ=\ngithub.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=\ngithub.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=\ngithub.com/elazarl/goproxy v1.7.2 h1:Y2o6urb7Eule09PjlhQRGNsqRfPmYI3KKQLFpCAV3+o=\ngithub.com/elazarl/goproxy v1.7.2/go.mod h1:82vkLNir0ALaW14Rc399OTTjyNREgmdL2cVoIbS6XaE=\ngithub.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc=\ngithub.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ=\ngithub.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=\ngithub.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=\ngithub.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=\ngithub.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=\ngithub.com/gin-contrib/pprof v1.5.3 h1:Bj5SxJ3kQDVez/s/+f9+meedJIqLS+xlkIVDe/lcvgM=\ngithub.com/gin-contrib/pprof v1.5.3/go.mod h1:0+LQSZ4SLO0B6+2n6JBzaEygpTBxe/nI+YEYpfQQ6xY=\ngithub.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=\ngithub.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=\ngithub.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=\ngithub.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=\ngithub.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c=\ngithub.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU=\ngithub.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI=\ngithub.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic=\ngithub.com/go-git/go-billy/v5 v5.8.0 h1:I8hjc3LbBlXTtVuFNJuwYuMiHvQJDq1AT6u4DwDzZG0=\ngithub.com/go-git/go-billy/v5 v5.8.0/go.mod h1:RpvI/rw4Vr5QA+Z60c6d6LXH0rYJo0uD5SqfmrrheCY=\ngithub.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=\ngithub.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399/go.mod h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=\ngithub.com/go-git/go-git/v5 v5.17.0 h1:AbyI4xf+7DsjINHMu35quAh4wJygKBKBuXVjV/pxesM=\ngithub.com/go-git/go-git/v5 v5.17.0/go.mod h1:f82C4YiLx+Lhi8eHxltLeGC5uBTXSFa6PC5WW9o4SjI=\ngithub.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=\ngithub.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=\ngithub.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=\ngithub.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=\ngithub.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=\ngithub.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=\ngithub.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=\ngithub.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=\ngithub.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=\ngithub.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=\ngithub.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=\ngithub.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=\ngithub.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=\ngithub.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=\ngithub.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=\ngithub.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=\ngithub.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY=\ngithub.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=\ngithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 h1:f+oWsMOmNPc8JmEHVZIycC7hBoQxHH9pNKQORJNozsQ=\ngithub.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8/go.mod h1:wcDNUvekVysuuOpQKo3191zZyTpiI6se1N1ULghS0sw=\ngithub.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=\ngithub.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=\ngithub.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=\ngithub.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=\ngithub.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=\ngithub.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc h1:VBbFa1lDYWEeV5FZKUiYKYT0VxCp9twUmmaq9eb8sXw=\ngithub.com/google/pprof v0.0.0-20260302011040-a15ffb7f9dcc/go.mod h1:MxpfABSjhmINe3F1It9d+8exIHFvUqtLIRCdOGNXqiI=\ngithub.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=\ngithub.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=\ngithub.com/gookit/assert v0.1.1 h1:lh3GcawXe/p+cU7ESTZ5Ui3Sm/x8JWpIis4/1aF0mY0=\ngithub.com/gookit/assert v0.1.1/go.mod h1:jS5bmIVQZTIwk42uXl4lyj4iaaxx32tqH16CFj0VX2E=\ngithub.com/gookit/color v1.6.0 h1:JjJXBTk1ETNyqyilJhkTXJYYigHG24TM9Xa2M1xAhRA=\ngithub.com/gookit/color v1.6.0/go.mod h1:9ACFc7/1IpHGBW8RwuDm/0YEnhg3dwwXpoMsmtyHfjs=\ngithub.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=\ngithub.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM=\ngithub.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg=\ngithub.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=\ngithub.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=\ngithub.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=\ngithub.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=\ngithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=\ngithub.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=\ngithub.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=\ngithub.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=\ngithub.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=\ngithub.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=\ngithub.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=\ngithub.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw=\ngithub.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=\ngithub.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=\ngithub.com/matthewhartstonge/argon2 v1.4.6 h1:CI9OKgahL9wxUQbbONgh8s03snO0b4uvaSXhVcjpRXI=\ngithub.com/matthewhartstonge/argon2 v1.4.6/go.mod h1:mskW9VTvhcsq1shvh9IfHw0v+tdDRd+lFITnW9IKnMk=\ngithub.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=\ngithub.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\ngithub.com/minio/selfupdate v0.6.0 h1:i76PgT0K5xO9+hjzKcacQtO7+MjJ4JKA8Ak8XQ9DDwU=\ngithub.com/minio/selfupdate v0.6.0/go.mod h1:bO02GTIPCMQFTEvE5h4DjYB58bCoZ35XLeBf0buTDdM=\ngithub.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=\ngithub.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=\ngithub.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=\ngithub.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=\ngithub.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=\ngithub.com/onsi/ginkgo/v2 v2.28.1 h1:S4hj+HbZp40fNKuLUQOYLDgZLwNUVn19N3Atb98NCyI=\ngithub.com/onsi/ginkgo/v2 v2.28.1/go.mod h1:CLtbVInNckU3/+gC8LzkGUb9oF+e8W8TdUsxPwvdOgE=\ngithub.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=\ngithub.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=\ngithub.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e h1:s2RNOM/IGdY0Y6qfTeUKhDawdHDpK9RGBdx80qN4Ttw=\ngithub.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e/go.mod h1:nBdnFKj15wFbf94Rwfq4m30eAcyY9V/IyKAGQFtqkW0=\ngithub.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=\ngithub.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=\ngithub.com/pion/datachannel v1.6.0 h1:XecBlj+cvsxhAMZWFfFcPyUaDZtd7IJvrXqlXD/53i0=\ngithub.com/pion/datachannel v1.6.0/go.mod h1:ur+wzYF8mWdC+Mkis5Thosk+u/VOL287apDNEbFpsIk=\ngithub.com/pion/dtls/v3 v3.1.2 h1:gqEdOUXLtCGW+afsBLO0LtDD8GnuBBjEy6HRtyofZTc=\ngithub.com/pion/dtls/v3 v3.1.2/go.mod h1:Hw/igcX4pdY69z1Hgv5x7wJFrUkdgHwAn/Q/uo7YHRo=\ngithub.com/pion/ice/v4 v4.2.1 h1:XPRYXaLiFq3LFDG7a7bMrmr3mFr27G/gtXN3v/TVfxY=\ngithub.com/pion/ice/v4 v4.2.1/go.mod h1:2quLV1S5v1tAx3VvAJaH//KGitRXvo4RKlX6D3tnN+c=\ngithub.com/pion/interceptor v0.1.44 h1:sNlZwM8dWXU9JQAkJh8xrarC0Etn8Oolcniukmuy0/I=\ngithub.com/pion/interceptor v0.1.44/go.mod h1:4atVlBkcgXuUP+ykQF0qOCGU2j7pQzX2ofvPRFsY5RY=\ngithub.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=\ngithub.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=\ngithub.com/pion/mdns/v2 v2.1.0 h1:3IJ9+Xio6tWYjhN6WwuY142P/1jA0D5ERaIqawg/fOY=\ngithub.com/pion/mdns/v2 v2.1.0/go.mod h1:pcez23GdynwcfRU1977qKU0mDxSeucttSHbCSfFOd9A=\ngithub.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=\ngithub.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=\ngithub.com/pion/rtcp v1.2.16 h1:fk1B1dNW4hsI78XUCljZJlC4kZOPk67mNRuQ0fcEkSo=\ngithub.com/pion/rtcp v1.2.16/go.mod h1:/as7VKfYbs5NIb4h6muQ35kQF/J0ZVNz2Z3xKoCBYOo=\ngithub.com/pion/rtp v1.10.1 h1:xP1prZcCTUuhO2c83XtxyOHJteISg6o8iPsE2acaMtA=\ngithub.com/pion/rtp v1.10.1/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=\ngithub.com/pion/sctp v1.9.2 h1:HxsOzEV9pWoeggv7T5kewVkstFNcGvhMPx0GvUOUQXo=\ngithub.com/pion/sctp v1.9.2/go.mod h1:OTOlsQ5EDQ6mQ0z4MUGXt2CgQmKyafBEXhUVqLRB6G8=\ngithub.com/pion/sdp/v3 v3.0.18 h1:l0bAXazKHpepazVdp+tPYnrsy9dfh7ZbT8DxesH5ZnI=\ngithub.com/pion/sdp/v3 v3.0.18/go.mod h1:ZREGo6A9ZygQ9XkqAj5xYCQtQpif0i6Pa81HOiAdqQ8=\ngithub.com/pion/srtp/v3 v3.0.10 h1:tFirkpBb3XccP5VEXLi50GqXhv5SKPxqrdlhDCJlZrQ=\ngithub.com/pion/srtp/v3 v3.0.10/go.mod h1:3mOTIB0cq9qlbn59V4ozvv9ClW/BSEbRp4cY0VtaR7M=\ngithub.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw=\ngithub.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM=\ngithub.com/pion/transport/v3 v3.1.1 h1:Tr684+fnnKlhPceU+ICdrw6KKkTms+5qHMgw6bIkYOM=\ngithub.com/pion/transport/v3 v3.1.1/go.mod h1:+c2eewC5WJQHiAA46fkMMzoYZSuGzA/7E2FPrOYHctQ=\ngithub.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=\ngithub.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=\ngithub.com/pion/turn/v4 v4.1.4 h1:EU11yMXKIsK43FhcUnjLlrhE4nboHZq+TXBIi3QpcxQ=\ngithub.com/pion/turn/v4 v4.1.4/go.mod h1:ES1DXVFKnOhuDkqn9hn5VJlSWmZPaRJLyBXoOeO/BmQ=\ngithub.com/pion/webrtc/v4 v4.2.9 h1:DZIh1HAhPIL3RvwEDFsmL5hfPSLEpxsQk9/Jir2vkJE=\ngithub.com/pion/webrtc/v4 v4.2.9/go.mod h1:9EmLZve0H76eTzf8v2FmchZ6tcBXtDgpfTEu+drW6SY=\ngithub.com/pjbgf/sha1cd v0.3.2 h1:a9wb0bp1oC2TGwStyn0Umc/IGKQnEgF0vVaZ8QF8eo4=\ngithub.com/pjbgf/sha1cd v0.3.2/go.mod h1:zQWigSxVmsHEZow5qaLtPYxpcKMMQpa09ixqBxuCS6A=\ngithub.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=\ngithub.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=\ngithub.com/pkg/profile v1.4.0/go.mod h1:NWz/XGvpEW1FyYQ7fCx4dqYBLlfTcE+A9FLAkNKqjFE=\ngithub.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=\ngithub.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=\ngithub.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=\ngithub.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=\ngithub.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw=\ngithub.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU=\ngithub.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=\ngithub.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=\ngithub.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8=\ngithub.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4=\ngithub.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=\ngithub.com/skeema/knownhosts v1.3.1 h1:X2osQ+RAjK76shCbvhHHHVl3ZlgDm8apHEHFqRjnBY8=\ngithub.com/skeema/knownhosts v1.3.1/go.mod h1:r7KTdC8l4uxWRyK2TpQZ/1o5HaSzh06ePQNxPwTcfiY=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=\ngithub.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=\ngithub.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=\ngithub.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=\ngithub.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\ngithub.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=\ngithub.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=\ngithub.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=\ngithub.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=\ngithub.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=\ngithub.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=\ngithub.com/sunfish-shogi/bufseekio v0.0.0-20210207115823-a4185644b365/go.mod h1:dEzdXgvImkQ3WLI+0KQpmEx8T/C/ma9KeS3AfmU899I=\ngithub.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=\ngithub.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=\ngithub.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=\ngithub.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=\ngithub.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=\ngithub.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=\ngithub.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM=\ngithub.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=\ngithub.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=\ngo.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=\ngo.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=\ngo.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=\ngo.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=\ngo.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=\ngo.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=\ngolang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=\ngolang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=\ngolang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=\ngolang.org/x/crypto v0.0.0-20211209193657-4570a0811e8b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=\ngolang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=\ngolang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=\ngolang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=\ngolang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=\ngolang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=\ngolang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=\ngolang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=\ngolang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=\ngolang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=\ngolang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=\ngolang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=\ngolang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210228012217-479acdf4ea46/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=\ngolang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=\ngolang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=\ngolang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=\ngolang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=\ngolang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=\ngolang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=\ngolang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=\ngolang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=\ngolang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=\ngolang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=\ngolang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s=\ngolang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0=\ngolang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\ngoogle.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=\ngoogle.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=\ngopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=\ngopkg.in/src-d/go-billy.v4 v4.3.2 h1:0SQA1pRztfTFx2miS8sA97XvooFeNOmvUenF4o0EcVg=\ngopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98=\ngopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=\ngopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\ngopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=\ngopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\ngopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=\ngopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=\n"
  },
  {
    "path": "internal/api/api.go",
    "content": "// Package api contains the API server.\npackage api //nolint:revive\n\nimport (\n\t\"net\"\n\t\"net/http\"\n\t\"reflect\"\n\t\"sort\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gin-gonic/gin\"\n\n\t\"github.com/bluenviron/mediamtx/internal/auth\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/httpp\"\n\t\"github.com/bluenviron/mediamtx/internal/recordstore\"\n)\n\nfunc interfaceIsEmpty(i any) bool {\n\treturn reflect.ValueOf(i).Kind() != reflect.Pointer || reflect.ValueOf(i).IsNil()\n}\n\nfunc sortedKeys(paths map[string]*conf.Path) []string {\n\tret := make([]string, len(paths))\n\ti := 0\n\tfor name := range paths {\n\t\tret[i] = name\n\t\ti++\n\t}\n\tsort.Strings(ret)\n\treturn ret\n}\n\nfunc paramName(ctx *gin.Context) (string, bool) {\n\tname := ctx.Param(\"name\")\n\n\tif len(name) < 2 || name[0] != '/' {\n\t\treturn \"\", false\n\t}\n\n\treturn name[1:], true\n}\n\nfunc recordingsOfPath(\n\tpathConf *conf.Path,\n\tpathName string,\n) *defs.APIRecording {\n\tret := &defs.APIRecording{\n\t\tName: pathName,\n\t}\n\n\tsegments, _ := recordstore.FindSegments(pathConf, pathName, nil, nil)\n\n\tret.Segments = make([]defs.APIRecordingSegment, len(segments))\n\n\tfor i, seg := range segments {\n\t\tret.Segments[i] = defs.APIRecordingSegment{\n\t\t\tStart: seg.Start,\n\t\t}\n\t}\n\n\treturn ret\n}\n\ntype apiAuthManager interface {\n\tAuthenticate(req *auth.Request) (string, *auth.Error)\n\tRefreshJWTJWKS()\n}\n\ntype apiParent interface {\n\tlogger.Writer\n\tAPIConfigSet(conf *conf.Conf)\n}\n\n// API is an API server.\ntype API struct {\n\tVersion        string\n\tStarted        time.Time\n\tAddress        string\n\tDumpPackets    bool\n\tEncryption     bool\n\tServerKey      string\n\tServerCert     string\n\tAllowOrigins   []string\n\tTrustedProxies conf.IPNetworks\n\tReadTimeout    conf.Duration\n\tWriteTimeout   conf.Duration\n\tConf           *conf.Conf\n\tAuthManager    apiAuthManager\n\tPathManager    defs.APIPathManager\n\tRTSPServer     defs.APIRTSPServer\n\tRTSPSServer    defs.APIRTSPServer\n\tRTMPServer     defs.APIRTMPServer\n\tRTMPSServer    defs.APIRTMPServer\n\tHLSServer      defs.APIHLSServer\n\tWebRTCServer   defs.APIWebRTCServer\n\tSRTServer      defs.APISRTServer\n\tParent         apiParent\n\n\thttpServer *httpp.Server\n\tmutex      sync.RWMutex\n}\n\n// Initialize initializes API.\nfunc (a *API) Initialize() error {\n\trouter := gin.New()\n\trouter.SetTrustedProxies(a.TrustedProxies.ToTrustedProxies()) //nolint:errcheck\n\n\trouter.Use(a.middlewarePreflightRequests)\n\trouter.Use(a.middlewareAuth)\n\n\tgroup := router.Group(\"/v3\")\n\n\tgroup.GET(\"/info\", a.onInfo)\n\n\tgroup.POST(\"/auth/jwks/refresh\", a.onAuthJwksRefresh)\n\n\tgroup.GET(\"/config/global/get\", a.onConfigGlobalGet)\n\tgroup.PATCH(\"/config/global/patch\", a.onConfigGlobalPatch)\n\n\tgroup.GET(\"/config/pathdefaults/get\", a.onConfigPathDefaultsGet)\n\tgroup.PATCH(\"/config/pathdefaults/patch\", a.onConfigPathDefaultsPatch)\n\n\tgroup.GET(\"/config/paths/list\", a.onConfigPathsList)\n\tgroup.GET(\"/config/paths/get/*name\", a.onConfigPathsGet)\n\tgroup.POST(\"/config/paths/add/*name\", a.onConfigPathsAdd)\n\tgroup.PATCH(\"/config/paths/patch/*name\", a.onConfigPathsPatch)\n\tgroup.POST(\"/config/paths/replace/*name\", a.onConfigPathsReplace)\n\tgroup.DELETE(\"/config/paths/delete/*name\", a.onConfigPathsDelete)\n\n\tgroup.GET(\"/paths/list\", a.onPathsList)\n\tgroup.GET(\"/paths/get/*name\", a.onPathsGet)\n\n\tif !interfaceIsEmpty(a.HLSServer) {\n\t\tgroup.GET(\"/hlsmuxers/list\", a.onHLSMuxersList)\n\t\tgroup.GET(\"/hlsmuxers/get/*name\", a.onHLSMuxersGet)\n\t}\n\n\tif !interfaceIsEmpty(a.RTSPServer) {\n\t\tgroup.GET(\"/rtspconns/list\", a.onRTSPConnsList)\n\t\tgroup.GET(\"/rtspconns/get/:id\", a.onRTSPConnsGet)\n\t\tgroup.GET(\"/rtspsessions/list\", a.onRTSPSessionsList)\n\t\tgroup.GET(\"/rtspsessions/get/:id\", a.onRTSPSessionsGet)\n\t\tgroup.POST(\"/rtspsessions/kick/:id\", a.onRTSPSessionsKick)\n\t}\n\n\tif !interfaceIsEmpty(a.RTSPSServer) {\n\t\tgroup.GET(\"/rtspsconns/list\", a.onRTSPSConnsList)\n\t\tgroup.GET(\"/rtspsconns/get/:id\", a.onRTSPSConnsGet)\n\t\tgroup.GET(\"/rtspssessions/list\", a.onRTSPSSessionsList)\n\t\tgroup.GET(\"/rtspssessions/get/:id\", a.onRTSPSSessionsGet)\n\t\tgroup.POST(\"/rtspssessions/kick/:id\", a.onRTSPSSessionsKick)\n\t}\n\n\tif !interfaceIsEmpty(a.RTMPServer) {\n\t\tgroup.GET(\"/rtmpconns/list\", a.onRTMPConnsList)\n\t\tgroup.GET(\"/rtmpconns/get/:id\", a.onRTMPConnsGet)\n\t\tgroup.POST(\"/rtmpconns/kick/:id\", a.onRTMPConnsKick)\n\t}\n\n\tif !interfaceIsEmpty(a.RTMPSServer) {\n\t\tgroup.GET(\"/rtmpsconns/list\", a.onRTMPSConnsList)\n\t\tgroup.GET(\"/rtmpsconns/get/:id\", a.onRTMPSConnsGet)\n\t\tgroup.POST(\"/rtmpsconns/kick/:id\", a.onRTMPSConnsKick)\n\t}\n\n\tif !interfaceIsEmpty(a.WebRTCServer) {\n\t\tgroup.GET(\"/webrtcsessions/list\", a.onWebRTCSessionsList)\n\t\tgroup.GET(\"/webrtcsessions/get/:id\", a.onWebRTCSessionsGet)\n\t\tgroup.POST(\"/webrtcsessions/kick/:id\", a.onWebRTCSessionsKick)\n\t}\n\n\tif !interfaceIsEmpty(a.SRTServer) {\n\t\tgroup.GET(\"/srtconns/list\", a.onSRTConnsList)\n\t\tgroup.GET(\"/srtconns/get/:id\", a.onSRTConnsGet)\n\t\tgroup.POST(\"/srtconns/kick/:id\", a.onSRTConnsKick)\n\t}\n\n\tgroup.GET(\"/recordings/list\", a.onRecordingsList)\n\tgroup.GET(\"/recordings/get/*name\", a.onRecordingsGet)\n\tgroup.DELETE(\"/recordings/deletesegment\", a.onRecordingDeleteSegment)\n\n\ta.httpServer = &httpp.Server{\n\t\tAddress:           a.Address,\n\t\tAllowOrigins:      a.AllowOrigins,\n\t\tDumpPackets:       a.DumpPackets,\n\t\tDumpPacketsPrefix: \"api_server_conn\",\n\t\tReadTimeout:       time.Duration(a.ReadTimeout),\n\t\tWriteTimeout:      time.Duration(a.WriteTimeout),\n\t\tEncryption:        a.Encryption,\n\t\tServerCert:        a.ServerCert,\n\t\tServerKey:         a.ServerKey,\n\t\tHandler:           router,\n\t\tParent:            a,\n\t}\n\terr := a.httpServer.Initialize()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ta.Log(logger.Info, \"listener opened on \"+a.Address)\n\n\treturn nil\n}\n\n// Close closes the API.\nfunc (a *API) Close() {\n\ta.Log(logger.Info, \"listener is closing\")\n\ta.httpServer.Close()\n}\n\n// Log implements logger.Writer.\nfunc (a *API) Log(level logger.Level, format string, args ...any) {\n\ta.Parent.Log(level, \"[API] \"+format, args...)\n}\n\nfunc (a *API) writeError(ctx *gin.Context, status int, err error) {\n\t// show error in logs\n\ta.Log(logger.Error, err.Error())\n\n\t// add error to response\n\tctx.JSON(status, &defs.APIError{\n\t\tStatus: defs.APIErrorStatusError,\n\t\tError:  err.Error(),\n\t})\n}\n\nfunc (a *API) writeOK(ctx *gin.Context) {\n\tctx.JSON(http.StatusOK, &defs.APIOK{Status: defs.APIOKStatusOK})\n}\n\nfunc (a *API) middlewarePreflightRequests(ctx *gin.Context) {\n\tif ctx.Request.Method == http.MethodOptions &&\n\t\tctx.Request.Header.Get(\"Access-Control-Request-Method\") != \"\" {\n\t\tctx.Header(\"Access-Control-Allow-Methods\", \"OPTIONS, GET, POST, PATCH, DELETE\")\n\t\tctx.Header(\"Access-Control-Allow-Headers\", \"Authorization, Content-Type\")\n\t\tctx.AbortWithStatus(http.StatusNoContent)\n\t\treturn\n\t}\n}\n\nfunc (a *API) middlewareAuth(ctx *gin.Context) {\n\treq := &auth.Request{\n\t\tAction:      conf.AuthActionAPI,\n\t\tQuery:       ctx.Request.URL.RawQuery,\n\t\tCredentials: httpp.Credentials(ctx.Request),\n\t\tIP:          net.ParseIP(ctx.ClientIP()),\n\t}\n\n\t_, err := a.AuthManager.Authenticate(req)\n\tif err != nil {\n\t\tif err.AskCredentials {\n\t\t\tctx.Header(\"WWW-Authenticate\", `Basic realm=\"mediamtx\"`)\n\t\t\tctx.AbortWithStatusJSON(http.StatusUnauthorized, &defs.APIError{\n\t\t\t\tStatus: defs.APIErrorStatusError,\n\t\t\t\tError:  \"authentication error\",\n\t\t\t})\n\t\t\treturn\n\t\t}\n\n\t\ta.Log(logger.Info, \"connection %v failed to authenticate: %v\", httpp.RemoteAddr(ctx), err.Wrapped)\n\n\t\t// wait some seconds to delay brute force attacks\n\t\t<-time.After(auth.PauseAfterError)\n\n\t\tctx.AbortWithStatusJSON(http.StatusUnauthorized, &defs.APIError{\n\t\t\tStatus: defs.APIErrorStatusError,\n\t\t\tError:  \"authentication error\",\n\t\t})\n\t\treturn\n\t}\n}\n\nfunc (a *API) onInfo(ctx *gin.Context) {\n\tctx.JSON(http.StatusOK, &defs.APIInfo{\n\t\tVersion: a.Version,\n\t\tStarted: a.Started,\n\t})\n}\n\nfunc (a *API) onAuthJwksRefresh(ctx *gin.Context) {\n\ta.AuthManager.RefreshJWTJWKS()\n\ta.writeOK(ctx)\n}\n\n// ReloadConf is called by core.\nfunc (a *API) ReloadConf(conf *conf.Conf) {\n\ta.mutex.Lock()\n\tdefer a.mutex.Unlock()\n\ta.Conf = conf\n}\n"
  },
  {
    "path": "internal/api/api_config_global.go",
    "content": "package api //nolint:revive\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc (a *API) onConfigGlobalGet(ctx *gin.Context) {\n\ta.mutex.RLock()\n\tc := a.Conf\n\ta.mutex.RUnlock()\n\n\tctx.JSON(http.StatusOK, c.Global())\n}\n\nfunc (a *API) onConfigGlobalPatch(ctx *gin.Context) {\n\tvar c conf.OptionalGlobal\n\terr := jsonwrapper.Decode(ctx.Request.Body, &c)\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\ta.mutex.Lock()\n\tdefer a.mutex.Unlock()\n\n\tnewConf := a.Conf.Clone()\n\n\tnewConf.PatchGlobal(&c)\n\n\terr = newConf.Validate(nil)\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\ta.Conf = newConf\n\n\t// since reloading the configuration can cause the shutdown of the API,\n\t// call it in a goroutine\n\tgo a.Parent.APIConfigSet(newConf)\n\n\ta.writeOK(ctx)\n}\n"
  },
  {
    "path": "internal/api/api_config_global_test.go",
    "content": "package api //nolint:revive\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/auth\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestConfigGlobalGet(t *testing.T) {\n\tcnf := tempConf(t, \"api: yes\\n\")\n\tchecked := false\n\n\tapi := API{\n\t\tAddress:      \"localhost:9997\",\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tConf:         cnf,\n\t\tAuthManager: &test.AuthManager{\n\t\t\tAuthenticateImpl: func(req *auth.Request) (string, *auth.Error) {\n\t\t\t\trequire.Equal(t, conf.AuthActionAPI, req.Action)\n\t\t\t\trequire.Equal(t, \"myuser\", req.Credentials.User)\n\t\t\t\trequire.Equal(t, \"mypass\", req.Credentials.Pass)\n\t\t\t\tchecked = true\n\t\t\t\treturn req.Credentials.User, nil\n\t\t\t},\n\t\t},\n\t\tParent: &testParent{},\n\t}\n\terr := api.Initialize()\n\trequire.NoError(t, err)\n\tdefer api.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\tvar out map[string]any\n\thttpRequest(t, hc, http.MethodGet, \"http://myuser:mypass@localhost:9997/v3/config/global/get\", nil, &out)\n\trequire.Equal(t, true, out[\"api\"])\n\n\trequire.True(t, checked)\n}\n\nfunc TestConfigGlobalPatch(t *testing.T) {\n\tcnf := tempConf(t, \"api: yes\\n\")\n\n\tapi := API{\n\t\tAddress:      \"localhost:9997\",\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tConf:         cnf,\n\t\tAuthManager:  test.NilAuthManager,\n\t\tParent:       &testParent{},\n\t}\n\terr := api.Initialize()\n\trequire.NoError(t, err)\n\tdefer api.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\thttpRequest(t, hc, http.MethodPatch, \"http://localhost:9997/v3/config/global/patch\",\n\t\tmap[string]any{\n\t\t\t\"rtmp\":            false,\n\t\t\t\"readTimeout\":     \"7s\",\n\t\t\t\"protocols\":       []string{\"tcp\"},\n\t\t\t\"readBufferCount\": 4096, // test setting a deprecated parameter\n\t\t}, nil)\n\n\ttime.Sleep(500 * time.Millisecond)\n\n\tvar out map[string]any\n\thttpRequest(t, hc, http.MethodGet, \"http://localhost:9997/v3/config/global/get\", nil, &out)\n\trequire.Equal(t, false, out[\"rtmp\"])\n\trequire.Equal(t, \"7s\", out[\"readTimeout\"])\n\trequire.Equal(t, []any{\"tcp\"}, out[\"protocols\"])\n\trequire.Equal(t, float64(4096), out[\"readBufferCount\"])\n}\n\nfunc TestConfigGlobalPatchUnknownField(t *testing.T) { //nolint:dupl\n\tcnf := tempConf(t, \"api: yes\\n\")\n\n\tapi := API{\n\t\tAddress:      \"localhost:9997\",\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tConf:         cnf,\n\t\tAuthManager:  test.NilAuthManager,\n\t\tParent:       &testParent{},\n\t}\n\terr := api.Initialize()\n\trequire.NoError(t, err)\n\tdefer api.Close()\n\n\tb := map[string]any{\n\t\t\"test\": \"asd\",\n\t}\n\n\tbyts, err := json.Marshal(b)\n\trequire.NoError(t, err)\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\treq, err := http.NewRequest(http.MethodPatch, \"http://localhost:9997/v3/config/global/patch\",\n\t\tbytes.NewReader(byts))\n\trequire.NoError(t, err)\n\n\tres, err := hc.Do(req)\n\trequire.NoError(t, err)\n\tdefer res.Body.Close()\n\n\trequire.Equal(t, http.StatusBadRequest, res.StatusCode)\n\tcheckError(t, res.Body, \"json: unknown field \\\"test\\\"\")\n}\n"
  },
  {
    "path": "internal/api/api_config_pathdefaults.go",
    "content": "package api //nolint:revive\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc (a *API) onConfigPathDefaultsGet(ctx *gin.Context) {\n\ta.mutex.RLock()\n\tc := a.Conf\n\ta.mutex.RUnlock()\n\n\tctx.JSON(http.StatusOK, c.PathDefaults)\n}\n\nfunc (a *API) onConfigPathDefaultsPatch(ctx *gin.Context) {\n\tvar p conf.OptionalPath\n\terr := jsonwrapper.Decode(ctx.Request.Body, &p)\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\ta.mutex.Lock()\n\tdefer a.mutex.Unlock()\n\n\tnewConf := a.Conf.Clone()\n\n\tnewConf.PatchPathDefaults(&p)\n\n\terr = newConf.Validate(nil)\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\ta.Conf = newConf\n\ta.Parent.APIConfigSet(newConf)\n\n\ta.writeOK(ctx)\n}\n"
  },
  {
    "path": "internal/api/api_config_pathdefaults_test.go",
    "content": "package api //nolint:revive\n\nimport (\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestConfigPathDefaultsGet(t *testing.T) {\n\tcnf := tempConf(t, \"api: yes\\n\")\n\n\tapi := API{\n\t\tAddress:      \"localhost:9997\",\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tConf:         cnf,\n\t\tAuthManager:  test.NilAuthManager,\n\t\tParent:       &testParent{},\n\t}\n\terr := api.Initialize()\n\trequire.NoError(t, err)\n\tdefer api.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\tvar out map[string]any\n\thttpRequest(t, hc, http.MethodGet, \"http://localhost:9997/v3/config/pathdefaults/get\", nil, &out)\n\trequire.Equal(t, \"publisher\", out[\"source\"])\n}\n\nfunc TestConfigPathDefaultsPatch(t *testing.T) {\n\tcnf := tempConf(t, \"api: yes\\n\")\n\n\tapi := API{\n\t\tAddress:      \"localhost:9997\",\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tConf:         cnf,\n\t\tAuthManager:  test.NilAuthManager,\n\t\tParent:       &testParent{},\n\t}\n\terr := api.Initialize()\n\trequire.NoError(t, err)\n\tdefer api.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\thttpRequest(t, hc, http.MethodPatch, \"http://localhost:9997/v3/config/pathdefaults/patch\",\n\t\tmap[string]any{\n\t\t\t\"recordFormat\": \"fmp4\",\n\t\t}, nil)\n\n\ttime.Sleep(500 * time.Millisecond)\n\n\tvar out map[string]any\n\thttpRequest(t, hc, http.MethodGet, \"http://localhost:9997/v3/config/pathdefaults/get\", nil, &out)\n\trequire.Equal(t, \"fmp4\", out[\"recordFormat\"])\n}\n"
  },
  {
    "path": "internal/api/api_config_paths.go",
    "content": "package api //nolint:revive\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc (a *API) onConfigPathsList(ctx *gin.Context) {\n\ta.mutex.RLock()\n\tc := a.Conf\n\ta.mutex.RUnlock()\n\n\tdata := &defs.APIPathConfList{\n\t\tItems: make([]conf.Path, len(c.Paths)),\n\t}\n\n\tfor i, key := range sortedKeys(c.Paths) {\n\t\tdata.Items[i] = *c.Paths[key]\n\t}\n\n\tdata.ItemCount = len(data.Items)\n\tpageCount, err := paginate(&data.Items, ctx.Query(\"itemsPerPage\"), ctx.Query(\"page\"))\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\tdata.PageCount = pageCount\n\n\tctx.JSON(http.StatusOK, data)\n}\n\nfunc (a *API) onConfigPathsGet(ctx *gin.Context) {\n\tconfName, ok := paramName(ctx)\n\tif !ok {\n\t\ta.writeError(ctx, http.StatusBadRequest, fmt.Errorf(\"invalid name\"))\n\t\treturn\n\t}\n\n\ta.mutex.RLock()\n\tc := a.Conf\n\ta.mutex.RUnlock()\n\n\tp, ok := c.Paths[confName]\n\tif !ok {\n\t\ta.writeError(ctx, http.StatusNotFound, fmt.Errorf(\"path configuration not found\"))\n\t\treturn\n\t}\n\n\tctx.JSON(http.StatusOK, p)\n}\n\nfunc (a *API) onConfigPathsAdd(ctx *gin.Context) { //nolint:dupl\n\tconfName, ok := paramName(ctx)\n\tif !ok {\n\t\ta.writeError(ctx, http.StatusBadRequest, fmt.Errorf(\"invalid name\"))\n\t\treturn\n\t}\n\n\tvar p conf.OptionalPath\n\terr := jsonwrapper.Decode(ctx.Request.Body, &p)\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\ta.mutex.Lock()\n\tdefer a.mutex.Unlock()\n\n\tnewConf := a.Conf.Clone()\n\n\terr = newConf.AddPath(confName, &p)\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\terr = newConf.Validate(nil)\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\ta.Conf = newConf\n\ta.Parent.APIConfigSet(newConf)\n\n\ta.writeOK(ctx)\n}\n\nfunc (a *API) onConfigPathsPatch(ctx *gin.Context) { //nolint:dupl\n\tconfName, ok := paramName(ctx)\n\tif !ok {\n\t\ta.writeError(ctx, http.StatusBadRequest, fmt.Errorf(\"invalid name\"))\n\t\treturn\n\t}\n\n\tvar p conf.OptionalPath\n\terr := jsonwrapper.Decode(ctx.Request.Body, &p)\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\ta.mutex.Lock()\n\tdefer a.mutex.Unlock()\n\n\tnewConf := a.Conf.Clone()\n\n\terr = newConf.PatchPath(confName, &p)\n\tif err != nil {\n\t\tif errors.Is(err, conf.ErrPathNotFound) {\n\t\t\ta.writeError(ctx, http.StatusNotFound, err)\n\t\t} else {\n\t\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\t}\n\t\treturn\n\t}\n\n\terr = newConf.Validate(nil)\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\ta.Conf = newConf\n\ta.Parent.APIConfigSet(newConf)\n\n\ta.writeOK(ctx)\n}\n\nfunc (a *API) onConfigPathsReplace(ctx *gin.Context) { //nolint:dupl\n\tconfName, ok := paramName(ctx)\n\tif !ok {\n\t\ta.writeError(ctx, http.StatusBadRequest, fmt.Errorf(\"invalid name\"))\n\t\treturn\n\t}\n\n\tvar p conf.OptionalPath\n\terr := jsonwrapper.Decode(ctx.Request.Body, &p)\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\ta.mutex.Lock()\n\tdefer a.mutex.Unlock()\n\n\tnewConf := a.Conf.Clone()\n\n\terr = newConf.ReplacePath(confName, &p)\n\tif err != nil {\n\t\tif errors.Is(err, conf.ErrPathNotFound) {\n\t\t\ta.writeError(ctx, http.StatusNotFound, err)\n\t\t} else {\n\t\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\t}\n\t\treturn\n\t}\n\n\terr = newConf.Validate(nil)\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\ta.Conf = newConf\n\ta.Parent.APIConfigSet(newConf)\n\n\ta.writeOK(ctx)\n}\n\nfunc (a *API) onConfigPathsDelete(ctx *gin.Context) {\n\tconfName, ok := paramName(ctx)\n\tif !ok {\n\t\ta.writeError(ctx, http.StatusBadRequest, fmt.Errorf(\"invalid name\"))\n\t\treturn\n\t}\n\n\ta.mutex.Lock()\n\tdefer a.mutex.Unlock()\n\n\tnewConf := a.Conf.Clone()\n\n\terr := newConf.RemovePath(confName)\n\tif err != nil {\n\t\tif errors.Is(err, conf.ErrPathNotFound) {\n\t\t\ta.writeError(ctx, http.StatusNotFound, err)\n\t\t} else {\n\t\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\t}\n\t\treturn\n\t}\n\n\terr = newConf.Validate(nil)\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\ta.Conf = newConf\n\ta.Parent.APIConfigSet(newConf)\n\n\ta.writeOK(ctx)\n}\n"
  },
  {
    "path": "internal/api/api_config_paths_test.go",
    "content": "package api //nolint:revive\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestConfigPathsList(t *testing.T) {\n\tcnf := tempConf(t, \"api: yes\\n\"+\n\t\t\"paths:\\n\"+\n\t\t\"  path1:\\n\"+\n\t\t\"    readUser: myuser1\\n\"+\n\t\t\"    readPass: mypass1\\n\"+\n\t\t\"  path2:\\n\"+\n\t\t\"    readUser: myuser2\\n\"+\n\t\t\"    readPass: mypass2\\n\")\n\n\tapi := API{\n\t\tAddress:      \"localhost:9997\",\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tConf:         cnf,\n\t\tAuthManager:  test.NilAuthManager,\n\t\tParent:       &testParent{},\n\t}\n\terr := api.Initialize()\n\trequire.NoError(t, err)\n\tdefer api.Close()\n\n\ttype pathConfig map[string]any\n\n\ttype listRes struct {\n\t\tItemCount int          `json:\"itemCount\"`\n\t\tPageCount int          `json:\"pageCount\"`\n\t\tItems     []pathConfig `json:\"items\"`\n\t}\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\tvar out listRes\n\thttpRequest(t, hc, http.MethodGet, \"http://localhost:9997/v3/config/paths/list\", nil, &out)\n\trequire.Equal(t, 2, out.ItemCount)\n\trequire.Equal(t, 1, out.PageCount)\n\trequire.Equal(t, \"path1\", out.Items[0][\"name\"])\n\trequire.Equal(t, \"myuser1\", out.Items[0][\"readUser\"])\n\trequire.Equal(t, \"mypass1\", out.Items[0][\"readPass\"])\n\trequire.Equal(t, \"path2\", out.Items[1][\"name\"])\n\trequire.Equal(t, \"myuser2\", out.Items[1][\"readUser\"])\n\trequire.Equal(t, \"mypass2\", out.Items[1][\"readPass\"])\n}\n\nfunc TestConfigPathsGet(t *testing.T) {\n\tcnf := tempConf(t, \"api: yes\\n\"+\n\t\t\"paths:\\n\"+\n\t\t\"  my/path:\\n\"+\n\t\t\"    readUser: myuser\\n\"+\n\t\t\"    readPass: mypass\\n\")\n\n\tapi := API{\n\t\tAddress:      \"localhost:9997\",\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tConf:         cnf,\n\t\tAuthManager:  test.NilAuthManager,\n\t\tParent:       &testParent{},\n\t}\n\terr := api.Initialize()\n\trequire.NoError(t, err)\n\tdefer api.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\tvar out map[string]any\n\thttpRequest(t, hc, http.MethodGet, \"http://localhost:9997/v3/config/paths/get/my/path\", nil, &out)\n\trequire.Equal(t, \"my/path\", out[\"name\"])\n\trequire.Equal(t, \"myuser\", out[\"readUser\"])\n}\n\nfunc TestConfigPathsAdd(t *testing.T) {\n\tcnf := tempConf(t, \"api: yes\\n\")\n\n\tapi := API{\n\t\tAddress:      \"localhost:9997\",\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tConf:         cnf,\n\t\tAuthManager:  test.NilAuthManager,\n\t\tParent:       &testParent{},\n\t}\n\terr := api.Initialize()\n\trequire.NoError(t, err)\n\tdefer api.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\thttpRequest(t, hc, http.MethodPost, \"http://localhost:9997/v3/config/paths/add/my/path\",\n\t\tmap[string]any{\n\t\t\t\"source\":                   \"rtsp://127.0.0.1:9999/mypath\",\n\t\t\t\"sourceOnDemand\":           true,\n\t\t\t\"disablePublisherOverride\": true, // test setting a deprecated parameter\n\t\t\t\"rpiCameraVFlip\":           true,\n\t\t}, nil)\n\n\tvar out map[string]any\n\thttpRequest(t, hc, http.MethodGet, \"http://localhost:9997/v3/config/paths/get/my/path\", nil, &out)\n\trequire.Equal(t, \"rtsp://127.0.0.1:9999/mypath\", out[\"source\"])\n\trequire.Equal(t, true, out[\"sourceOnDemand\"])\n\trequire.Equal(t, true, out[\"disablePublisherOverride\"])\n\trequire.Equal(t, true, out[\"rpiCameraVFlip\"])\n}\n\nfunc TestConfigPathsAddUnknownField(t *testing.T) { //nolint:dupl\n\tcnf := tempConf(t, \"api: yes\\n\")\n\n\tapi := API{\n\t\tAddress:      \"localhost:9997\",\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tConf:         cnf,\n\t\tAuthManager:  test.NilAuthManager,\n\t\tParent:       &testParent{},\n\t}\n\terr := api.Initialize()\n\trequire.NoError(t, err)\n\tdefer api.Close()\n\n\tb := map[string]any{\n\t\t\"test\": \"asd\",\n\t}\n\n\tbyts, err := json.Marshal(b)\n\trequire.NoError(t, err)\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\treq, err := http.NewRequest(http.MethodPost,\n\t\t\"http://localhost:9997/v3/config/paths/add/my/path\", bytes.NewReader(byts))\n\trequire.NoError(t, err)\n\n\tres, err := hc.Do(req)\n\trequire.NoError(t, err)\n\tdefer res.Body.Close()\n\n\trequire.Equal(t, http.StatusBadRequest, res.StatusCode)\n\tcheckError(t, res.Body, \"json: unknown field \\\"test\\\"\")\n}\n\nfunc TestConfigPathsPatch(t *testing.T) { //nolint:dupl\n\tcnf := tempConf(t, \"api: yes\\n\")\n\n\tapi := API{\n\t\tAddress:      \"localhost:9997\",\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tConf:         cnf,\n\t\tAuthManager:  test.NilAuthManager,\n\t\tParent:       &testParent{},\n\t}\n\terr := api.Initialize()\n\trequire.NoError(t, err)\n\tdefer api.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\thttpRequest(t, hc, http.MethodPost, \"http://localhost:9997/v3/config/paths/add/my/path\",\n\t\tmap[string]any{\n\t\t\t\"source\":                   \"rtsp://127.0.0.1:9999/mypath\",\n\t\t\t\"sourceOnDemand\":           true,\n\t\t\t\"disablePublisherOverride\": true, // test setting a deprecated parameter\n\t\t\t\"rpiCameraVFlip\":           true,\n\t\t}, nil)\n\n\thttpRequest(t, hc, http.MethodPatch, \"http://localhost:9997/v3/config/paths/patch/my/path\",\n\t\tmap[string]any{\n\t\t\t\"source\":         \"rtsp://127.0.0.1:9998/mypath\",\n\t\t\t\"sourceOnDemand\": true,\n\t\t}, nil)\n\n\tvar out map[string]any\n\thttpRequest(t, hc, http.MethodGet, \"http://localhost:9997/v3/config/paths/get/my/path\", nil, &out)\n\trequire.Equal(t, \"rtsp://127.0.0.1:9998/mypath\", out[\"source\"])\n\trequire.Equal(t, true, out[\"sourceOnDemand\"])\n\trequire.Equal(t, true, out[\"disablePublisherOverride\"])\n\trequire.Equal(t, true, out[\"rpiCameraVFlip\"])\n}\n\nfunc TestConfigPathsReplace(t *testing.T) { //nolint:dupl\n\tcnf := tempConf(t, \"api: yes\\n\")\n\n\tapi := API{\n\t\tAddress:      \"localhost:9997\",\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tConf:         cnf,\n\t\tAuthManager:  test.NilAuthManager,\n\t\tParent:       &testParent{},\n\t}\n\terr := api.Initialize()\n\trequire.NoError(t, err)\n\tdefer api.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\thttpRequest(t, hc, http.MethodPost, \"http://localhost:9997/v3/config/paths/add/my/path\",\n\t\tmap[string]any{\n\t\t\t\"source\":                   \"rtsp://127.0.0.1:9999/mypath\",\n\t\t\t\"sourceOnDemand\":           true,\n\t\t\t\"disablePublisherOverride\": true, // test setting a deprecated parameter\n\t\t\t\"rpiCameraVFlip\":           true,\n\t\t}, nil)\n\n\thttpRequest(t, hc, http.MethodPost, \"http://localhost:9997/v3/config/paths/replace/my/path\",\n\t\tmap[string]any{\n\t\t\t\"source\":         \"rtsp://127.0.0.1:9998/mypath\",\n\t\t\t\"sourceOnDemand\": true,\n\t\t}, nil)\n\n\tvar out map[string]any\n\thttpRequest(t, hc, http.MethodGet, \"http://localhost:9997/v3/config/paths/get/my/path\", nil, &out)\n\trequire.Equal(t, \"rtsp://127.0.0.1:9998/mypath\", out[\"source\"])\n\trequire.Equal(t, true, out[\"sourceOnDemand\"])\n\trequire.Equal(t, nil, out[\"disablePublisherOverride\"])\n\trequire.Equal(t, false, out[\"rpiCameraVFlip\"])\n}\n\nfunc TestConfigPathsReplaceNonExisting(t *testing.T) { //nolint:dupl\n\tcnf := tempConf(t, \"api: yes\\n\")\n\n\tapi := API{\n\t\tAddress:      \"localhost:9997\",\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tConf:         cnf,\n\t\tAuthManager:  test.NilAuthManager,\n\t\tParent:       &testParent{},\n\t}\n\terr := api.Initialize()\n\trequire.NoError(t, err)\n\tdefer api.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\thttpRequest(t, hc, http.MethodPost, \"http://localhost:9997/v3/config/paths/replace/my/path\",\n\t\tmap[string]any{\n\t\t\t\"source\":         \"rtsp://127.0.0.1:9998/mypath\",\n\t\t\t\"sourceOnDemand\": true,\n\t\t}, nil)\n\n\tvar out map[string]any\n\thttpRequest(t, hc, http.MethodGet, \"http://localhost:9997/v3/config/paths/get/my/path\", nil, &out)\n\trequire.Equal(t, \"rtsp://127.0.0.1:9998/mypath\", out[\"source\"])\n\trequire.Equal(t, true, out[\"sourceOnDemand\"])\n\trequire.Equal(t, nil, out[\"disablePublisherOverride\"])\n\trequire.Equal(t, false, out[\"rpiCameraVFlip\"])\n}\n\nfunc TestConfigPathsDelete(t *testing.T) {\n\tcnf := tempConf(t, \"api: yes\\n\")\n\n\tapi := API{\n\t\tAddress:      \"localhost:9997\",\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tConf:         cnf,\n\t\tAuthManager:  test.NilAuthManager,\n\t\tParent:       &testParent{},\n\t}\n\terr := api.Initialize()\n\trequire.NoError(t, err)\n\tdefer api.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\thttpRequest(t, hc, http.MethodPost, \"http://localhost:9997/v3/config/paths/add/my/path\",\n\t\tmap[string]any{\n\t\t\t\"source\":         \"rtsp://127.0.0.1:9999/mypath\",\n\t\t\t\"sourceOnDemand\": true,\n\t\t}, nil)\n\n\thttpRequest(t, hc, http.MethodDelete, \"http://localhost:9997/v3/config/paths/delete/my/path\", nil, nil)\n\n\treq, err := http.NewRequest(http.MethodGet, \"http://localhost:9997/v3/config/paths/get/my/path\", nil)\n\trequire.NoError(t, err)\n\n\tres, err := hc.Do(req)\n\trequire.NoError(t, err)\n\tdefer res.Body.Close()\n\n\trequire.Equal(t, http.StatusNotFound, res.StatusCode)\n\tcheckError(t, res.Body, \"path configuration not found\")\n}\n"
  },
  {
    "path": "internal/api/api_hls.go",
    "content": "//nolint:dupl\npackage api //nolint:revive\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/bluenviron/mediamtx/internal/servers/hls\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc (a *API) onHLSMuxersList(ctx *gin.Context) {\n\tdata, err := a.HLSServer.APIMuxersList()\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusInternalServerError, err)\n\t\treturn\n\t}\n\n\tdata.ItemCount = len(data.Items)\n\tpageCount, err := paginate(&data.Items, ctx.Query(\"itemsPerPage\"), ctx.Query(\"page\"))\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\tdata.PageCount = pageCount\n\n\tctx.JSON(http.StatusOK, data)\n}\n\nfunc (a *API) onHLSMuxersGet(ctx *gin.Context) {\n\tpathName, ok := paramName(ctx)\n\tif !ok {\n\t\ta.writeError(ctx, http.StatusBadRequest, fmt.Errorf(\"invalid name\"))\n\t\treturn\n\t}\n\n\tdata, err := a.HLSServer.APIMuxersGet(pathName)\n\tif err != nil {\n\t\tif errors.Is(err, hls.ErrMuxerNotFound) {\n\t\t\ta.writeError(ctx, http.StatusNotFound, err)\n\t\t} else {\n\t\t\ta.writeError(ctx, http.StatusInternalServerError, err)\n\t\t}\n\t\treturn\n\t}\n\n\tctx.JSON(http.StatusOK, data)\n}\n"
  },
  {
    "path": "internal/api/api_hls_test.go",
    "content": "package api //nolint:revive\n\nimport (\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/servers/hls\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype testHLSServer struct {\n\tmuxers map[string]*defs.APIHLSMuxer\n}\n\nfunc (s *testHLSServer) APIMuxersList() (*defs.APIHLSMuxerList, error) {\n\titems := make([]defs.APIHLSMuxer, 0, len(s.muxers))\n\tfor _, muxer := range s.muxers {\n\t\titems = append(items, *muxer)\n\t}\n\treturn &defs.APIHLSMuxerList{Items: items}, nil\n}\n\nfunc (s *testHLSServer) APIMuxersGet(name string) (*defs.APIHLSMuxer, error) {\n\tmuxer, ok := s.muxers[name]\n\tif !ok {\n\t\treturn nil, hls.ErrMuxerNotFound\n\t}\n\treturn muxer, nil\n}\n\nfunc TestHLSMuxersList(t *testing.T) {\n\tnow := time.Now()\n\thlsServer := &testHLSServer{\n\t\tmuxers: map[string]*defs.APIHLSMuxer{\n\t\t\t\"test1\": {\n\t\t\t\tPath:                    \"test1\",\n\t\t\t\tCreated:                 now,\n\t\t\t\tLastRequest:             now.Add(5 * time.Second),\n\t\t\t\tOutboundBytes:           1234,\n\t\t\t\tOutboundFramesDiscarded: 10,\n\t\t\t\tBytesSent:               1234,\n\t\t\t},\n\t\t\t\"test2\": {\n\t\t\t\tPath:                    \"test2\",\n\t\t\t\tCreated:                 now.Add(time.Minute),\n\t\t\t\tLastRequest:             now.Add(time.Minute + 10*time.Second),\n\t\t\t\tOutboundBytes:           5678,\n\t\t\t\tOutboundFramesDiscarded: 20,\n\t\t\t\tBytesSent:               5678,\n\t\t\t},\n\t\t},\n\t}\n\n\tapi := API{\n\t\tAddress:      \"localhost:9997\",\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tAuthManager:  test.NilAuthManager,\n\t\tHLSServer:    hlsServer,\n\t\tParent:       &testParent{},\n\t}\n\terr := api.Initialize()\n\trequire.NoError(t, err)\n\tdefer api.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\tvar out defs.APIHLSMuxerList\n\thttpRequest(t, hc, http.MethodGet, \"http://localhost:9997/v3/hlsmuxers/list\", nil, &out)\n\n\trequire.Equal(t, 2, out.ItemCount)\n\trequire.Equal(t, 1, out.PageCount)\n\trequire.Len(t, out.Items, 2)\n}\n\nfunc TestHLSMuxersGet(t *testing.T) {\n\tnow := time.Now()\n\thlsServer := &testHLSServer{\n\t\tmuxers: map[string]*defs.APIHLSMuxer{\n\t\t\t\"mypath\": {\n\t\t\t\tPath:                    \"mypath\",\n\t\t\t\tCreated:                 now,\n\t\t\t\tLastRequest:             now.Add(5 * time.Second),\n\t\t\t\tOutboundBytes:           9999,\n\t\t\t\tOutboundFramesDiscarded: 12,\n\t\t\t\tBytesSent:               9999,\n\t\t\t},\n\t\t},\n\t}\n\n\tapi := API{\n\t\tAddress:      \"localhost:9997\",\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tAuthManager:  test.NilAuthManager,\n\t\tHLSServer:    hlsServer,\n\t\tParent:       &testParent{},\n\t}\n\terr := api.Initialize()\n\trequire.NoError(t, err)\n\tdefer api.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\tvar out defs.APIHLSMuxer\n\thttpRequest(t, hc, http.MethodGet, \"http://localhost:9997/v3/hlsmuxers/get/mypath\", nil, &out)\n\n\trequire.Equal(t, \"mypath\", out.Path)\n\trequire.Equal(t, uint64(9999), out.OutboundBytes)\n\trequire.Equal(t, uint64(12), out.OutboundFramesDiscarded)\n\trequire.Equal(t, uint64(9999), out.BytesSent)\n}\n"
  },
  {
    "path": "internal/api/api_paths.go",
    "content": "//nolint:dupl\npackage api //nolint:revive\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc (a *API) onPathsList(ctx *gin.Context) {\n\tdata, err := a.PathManager.APIPathsList()\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusInternalServerError, err)\n\t\treturn\n\t}\n\n\tdata.ItemCount = len(data.Items)\n\tpageCount, err := paginate(&data.Items, ctx.Query(\"itemsPerPage\"), ctx.Query(\"page\"))\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\tdata.PageCount = pageCount\n\n\tctx.JSON(http.StatusOK, data)\n}\n\nfunc (a *API) onPathsGet(ctx *gin.Context) {\n\tpathName, ok := paramName(ctx)\n\tif !ok {\n\t\ta.writeError(ctx, http.StatusBadRequest, fmt.Errorf(\"invalid name\"))\n\t\treturn\n\t}\n\n\tdata, err := a.PathManager.APIPathsGet(pathName)\n\tif err != nil {\n\t\tif errors.Is(err, conf.ErrPathNotFound) {\n\t\t\ta.writeError(ctx, http.StatusNotFound, err)\n\t\t} else {\n\t\t\ta.writeError(ctx, http.StatusInternalServerError, err)\n\t\t}\n\t\treturn\n\t}\n\n\tctx.JSON(http.StatusOK, data)\n}\n"
  },
  {
    "path": "internal/api/api_paths_test.go",
    "content": "package api //nolint:revive\n\nimport (\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype testPathManager struct {\n\tpaths map[string]*defs.APIPath\n}\n\nfunc (m *testPathManager) APIPathsList() (*defs.APIPathList, error) {\n\titems := make([]defs.APIPath, 0, len(m.paths))\n\tfor _, path := range m.paths {\n\t\titems = append(items, *path)\n\t}\n\treturn &defs.APIPathList{Items: items}, nil\n}\n\nfunc (m *testPathManager) APIPathsGet(name string) (*defs.APIPath, error) {\n\tpath, ok := m.paths[name]\n\tif !ok {\n\t\treturn nil, conf.ErrPathNotFound\n\t}\n\treturn path, nil\n}\n\nfunc TestPathsList(t *testing.T) {\n\tnow := time.Now()\n\tpathManager := &testPathManager{\n\t\tpaths: map[string]*defs.APIPath{\n\t\t\t\"test1\": {\n\t\t\t\tName:                 \"test1\",\n\t\t\t\tConfName:             \"test1\",\n\t\t\t\tSource:               &defs.APIPathSource{Type: defs.APIPathSourceTypeRTSPSession, ID: \"pub1\"},\n\t\t\t\tReady:                true,\n\t\t\t\tReadyTime:            &now,\n\t\t\t\tTracks:               []defs.APIPathTrackCodec{defs.APIPathTrackCodecH264, defs.APIPathTrackCodecOpus},\n\t\t\t\tInboundBytes:         1000,\n\t\t\t\tOutboundBytes:        2000,\n\t\t\t\tInboundFramesInError: 3,\n\t\t\t\tBytesReceived:        1000,\n\t\t\t\tBytesSent:            2000,\n\t\t\t\tReaders: []defs.APIPathReader{\n\t\t\t\t\t{Type: defs.APIPathReaderTypeRTSPSession, ID: \"reader1\"},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"test2\": {\n\t\t\t\tName:                 \"test2\",\n\t\t\t\tConfName:             \"test2\",\n\t\t\t\tReady:                false,\n\t\t\t\tTracks:               []defs.APIPathTrackCodec{},\n\t\t\t\tInboundBytes:         500,\n\t\t\t\tOutboundBytes:        100,\n\t\t\t\tInboundFramesInError: 1,\n\t\t\t\tBytesReceived:        500,\n\t\t\t\tBytesSent:            100,\n\t\t\t\tReaders:              []defs.APIPathReader{},\n\t\t\t},\n\t\t},\n\t}\n\n\tapi := API{\n\t\tAddress:      \"localhost:9997\",\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tAuthManager:  test.NilAuthManager,\n\t\tPathManager:  pathManager,\n\t\tParent:       &testParent{},\n\t}\n\terr := api.Initialize()\n\trequire.NoError(t, err)\n\tdefer api.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\tvar out defs.APIPathList\n\thttpRequest(t, hc, http.MethodGet, \"http://localhost:9997/v3/paths/list\", nil, &out)\n\n\trequire.Equal(t, 2, out.ItemCount)\n\trequire.Equal(t, 1, out.PageCount)\n\trequire.Len(t, out.Items, 2)\n}\n\nfunc TestPathsGet(t *testing.T) {\n\tnow := time.Now()\n\tpathManager := &testPathManager{\n\t\tpaths: map[string]*defs.APIPath{\n\t\t\t\"mystream\": {\n\t\t\t\tName:                 \"mystream\",\n\t\t\t\tConfName:             \"mystream\",\n\t\t\t\tSource:               &defs.APIPathSource{Type: defs.APIPathSourceTypeRTSPSession, ID: \"session123\"},\n\t\t\t\tReady:                true,\n\t\t\t\tReadyTime:            &now,\n\t\t\t\tTracks:               []defs.APIPathTrackCodec{defs.APIPathTrackCodecH264, defs.APIPathTrackCodecOpus},\n\t\t\t\tInboundBytes:         123456,\n\t\t\t\tOutboundBytes:        789012,\n\t\t\t\tInboundFramesInError: 12,\n\t\t\t\tBytesReceived:        123456,\n\t\t\t\tBytesSent:            789012,\n\t\t\t\tReaders: []defs.APIPathReader{\n\t\t\t\t\t{Type: defs.APIPathReaderTypeHLSMuxer, ID: \"muxer1\"},\n\t\t\t\t\t{Type: defs.APIPathReaderTypeWebRTCSession, ID: \"session456\"},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tapi := API{\n\t\tAddress:      \"localhost:9997\",\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tAuthManager:  test.NilAuthManager,\n\t\tPathManager:  pathManager,\n\t\tParent:       &testParent{},\n\t}\n\terr := api.Initialize()\n\trequire.NoError(t, err)\n\tdefer api.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\tvar out defs.APIPath\n\thttpRequest(t, hc, http.MethodGet, \"http://localhost:9997/v3/paths/get/mystream\", nil, &out)\n\n\trequire.Equal(t, \"mystream\", out.Name)\n\trequire.Equal(t, \"mystream\", out.ConfName)\n\trequire.True(t, out.Ready)\n\trequire.NotNil(t, out.Source)\n\trequire.Equal(t, defs.APIPathSourceTypeRTSPSession, out.Source.Type)\n\trequire.Len(t, out.Tracks, 2)\n\trequire.Len(t, out.Readers, 2)\n\trequire.Equal(t, uint64(123456), out.InboundBytes)\n\trequire.Equal(t, uint64(789012), out.OutboundBytes)\n\trequire.Equal(t, uint64(12), out.InboundFramesInError)\n\trequire.Equal(t, uint64(123456), out.BytesReceived)\n\trequire.Equal(t, uint64(789012), out.BytesSent)\n}\n"
  },
  {
    "path": "internal/api/api_recordings.go",
    "content": "package api //nolint:revive\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/recordstore\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nfunc (a *API) onRecordingsList(ctx *gin.Context) {\n\ta.mutex.RLock()\n\tc := a.Conf\n\ta.mutex.RUnlock()\n\n\tpathNames := recordstore.FindAllPathsWithSegments(c.Paths)\n\n\tdata := defs.APIRecordingList{}\n\n\tdata.ItemCount = len(pathNames)\n\tpageCount, err := paginate(&pathNames, ctx.Query(\"itemsPerPage\"), ctx.Query(\"page\"))\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\tdata.PageCount = pageCount\n\n\tdata.Items = make([]defs.APIRecording, len(pathNames))\n\n\tfor i, pathName := range pathNames {\n\t\tpathConf, _, _ := conf.FindPathConf(c.Paths, pathName)\n\t\tdata.Items[i] = *recordingsOfPath(pathConf, pathName)\n\t}\n\n\tctx.JSON(http.StatusOK, data)\n}\n\nfunc (a *API) onRecordingsGet(ctx *gin.Context) {\n\tpathName, ok := paramName(ctx)\n\tif !ok {\n\t\ta.writeError(ctx, http.StatusBadRequest, fmt.Errorf(\"invalid name\"))\n\t\treturn\n\t}\n\n\ta.mutex.RLock()\n\tc := a.Conf\n\ta.mutex.RUnlock()\n\n\tpathConf, _, err := conf.FindPathConf(c.Paths, pathName)\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\tctx.JSON(http.StatusOK, recordingsOfPath(pathConf, pathName))\n}\n\nfunc (a *API) onRecordingDeleteSegment(ctx *gin.Context) {\n\tpathName := ctx.Query(\"path\")\n\n\tstart, err := time.Parse(time.RFC3339, ctx.Query(\"start\"))\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, fmt.Errorf(\"invalid 'start' parameter: %w\", err))\n\t\treturn\n\t}\n\n\ta.mutex.RLock()\n\tc := a.Conf\n\ta.mutex.RUnlock()\n\n\tpathConf, _, err := conf.FindPathConf(c.Paths, pathName)\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\tpathFormat := recordstore.PathAddExtension(\n\t\tstrings.ReplaceAll(pathConf.RecordPath, \"%path\", pathName),\n\t\tpathConf.RecordFormat,\n\t)\n\n\tsegmentPath := recordstore.Path{\n\t\tStart: start,\n\t}.Encode(pathFormat)\n\n\terr = os.Remove(segmentPath)\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\ta.writeOK(ctx)\n}\n"
  },
  {
    "path": "internal/api/api_recordings_test.go",
    "content": "package api //nolint:revive\n\nimport (\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRecordingsList(t *testing.T) {\n\tdir, err := os.MkdirTemp(\"\", \"mediamtx-playback\")\n\trequire.NoError(t, err)\n\tdefer os.RemoveAll(dir)\n\n\tcnf := tempConf(t, \"pathDefaults:\\n\"+\n\t\t\"  recordPath: \"+filepath.Join(dir, \"%path/%Y-%m-%d_%H-%M-%S-%f\")+\"\\n\"+\n\t\t\"paths:\\n\"+\n\t\t\"  mypath1:\\n\"+\n\t\t\"  all_others:\\n\")\n\n\tapi := API{\n\t\tAddress:      \"localhost:9997\",\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tConf:         cnf,\n\t\tAuthManager:  test.NilAuthManager,\n\t\tParent:       &testParent{},\n\t}\n\terr = api.Initialize()\n\trequire.NoError(t, err)\n\tdefer api.Close()\n\n\terr = os.Mkdir(filepath.Join(dir, \"mypath1\"), 0o755)\n\trequire.NoError(t, err)\n\n\terr = os.Mkdir(filepath.Join(dir, \"mypath2\"), 0o755)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(dir, \"mypath1\", \"2008-11-07_11-22-00-500000.mp4\"), []byte(\"\"), 0o644)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(dir, \"mypath1\", \"2009-11-07_11-22-00-900000.mp4\"), []byte(\"\"), 0o644)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(dir, \"mypath2\", \"2009-11-07_11-22-00-900000.mp4\"), []byte(\"\"), 0o644)\n\trequire.NoError(t, err)\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\tvar out any\n\thttpRequest(t, hc, http.MethodGet, \"http://localhost:9997/v3/recordings/list\", nil, &out)\n\trequire.Equal(t, map[string]any{\n\t\t\"itemCount\": float64(2),\n\t\t\"pageCount\": float64(1),\n\t\t\"items\": []any{\n\t\t\tmap[string]any{\n\t\t\t\t\"name\": \"mypath1\",\n\t\t\t\t\"segments\": []any{\n\t\t\t\t\tmap[string]any{\n\t\t\t\t\t\t\"start\": time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano),\n\t\t\t\t\t},\n\t\t\t\t\tmap[string]any{\n\t\t\t\t\t\t\"start\": time.Date(2009, 11, 7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tmap[string]any{\n\t\t\t\t\"name\": \"mypath2\",\n\t\t\t\t\"segments\": []any{\n\t\t\t\t\tmap[string]any{\n\t\t\t\t\t\t\"start\": time.Date(2009, 11, 7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}, out)\n}\n\nfunc TestRecordingsGet(t *testing.T) {\n\tdir, err := os.MkdirTemp(\"\", \"mediamtx-playback\")\n\trequire.NoError(t, err)\n\tdefer os.RemoveAll(dir)\n\n\tcnf := tempConf(t, \"pathDefaults:\\n\"+\n\t\t\"  recordPath: \"+filepath.Join(dir, \"%path/%Y-%m-%d_%H-%M-%S-%f\")+\"\\n\"+\n\t\t\"paths:\\n\"+\n\t\t\"  all_others:\\n\")\n\n\tapi := API{\n\t\tAddress:      \"localhost:9997\",\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tConf:         cnf,\n\t\tAuthManager:  test.NilAuthManager,\n\t\tParent:       &testParent{},\n\t}\n\terr = api.Initialize()\n\trequire.NoError(t, err)\n\tdefer api.Close()\n\n\terr = os.Mkdir(filepath.Join(dir, \"mypath1\"), 0o755)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(dir, \"mypath1\", \"2008-11-07_11-22-00-000000.mp4\"), []byte(\"\"), 0o644)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(dir, \"mypath1\", \"2009-11-07_11-22-00-900000.mp4\"), []byte(\"\"), 0o644)\n\trequire.NoError(t, err)\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\tvar out any\n\thttpRequest(t, hc, http.MethodGet, \"http://localhost:9997/v3/recordings/get/mypath1\", nil, &out)\n\trequire.Equal(t, map[string]any{\n\t\t\"name\": \"mypath1\",\n\t\t\"segments\": []any{\n\t\t\tmap[string]any{\n\t\t\t\t\"start\": time.Date(2008, 11, 7, 11, 22, 0, 0, time.Local).Format(time.RFC3339Nano),\n\t\t\t},\n\t\t\tmap[string]any{\n\t\t\t\t\"start\": time.Date(2009, 11, 7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano),\n\t\t\t},\n\t\t},\n\t}, out)\n}\n\nfunc TestRecordingsDeleteSegment(t *testing.T) {\n\tdir, err := os.MkdirTemp(\"\", \"mediamtx-playback\")\n\trequire.NoError(t, err)\n\tdefer os.RemoveAll(dir)\n\n\tcnf := tempConf(t, \"pathDefaults:\\n\"+\n\t\t\"  recordPath: \"+filepath.Join(dir, \"%path/%Y-%m-%d_%H-%M-%S-%f\")+\"\\n\"+\n\t\t\"paths:\\n\"+\n\t\t\"  all_others:\\n\")\n\n\tapi := API{\n\t\tAddress:      \"localhost:9997\",\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tConf:         cnf,\n\t\tAuthManager:  test.NilAuthManager,\n\t\tParent:       &testParent{},\n\t}\n\terr = api.Initialize()\n\trequire.NoError(t, err)\n\tdefer api.Close()\n\n\terr = os.Mkdir(filepath.Join(dir, \"mypath1\"), 0o755)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(dir, \"mypath1\", \"2008-11-07_11-22-00-900000.mp4\"), []byte(\"\"), 0o644)\n\trequire.NoError(t, err)\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\tu, err := url.Parse(\"http://localhost:9997/v3/recordings/deletesegment\")\n\trequire.NoError(t, err)\n\n\tv := url.Values{}\n\tv.Set(\"path\", \"mypath1\")\n\tv.Set(\"start\", time.Date(2008, 11, 7, 11, 22, 0, 900000000, time.Local).Format(time.RFC3339Nano))\n\tu.RawQuery = v.Encode()\n\n\thttpRequest(t, hc, http.MethodDelete, u.String(), nil, nil)\n}\n"
  },
  {
    "path": "internal/api/api_rtmp.go",
    "content": "package api //nolint:revive\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\n\t\"github.com/bluenviron/mediamtx/internal/servers/rtmp\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/google/uuid\"\n)\n\nfunc (a *API) onRTMPConnsList(ctx *gin.Context) {\n\tdata, err := a.RTMPServer.APIConnsList()\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusInternalServerError, err)\n\t\treturn\n\t}\n\n\tdata.ItemCount = len(data.Items)\n\tpageCount, err := paginate(&data.Items, ctx.Query(\"itemsPerPage\"), ctx.Query(\"page\"))\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\tdata.PageCount = pageCount\n\n\tctx.JSON(http.StatusOK, data)\n}\n\nfunc (a *API) onRTMPConnsGet(ctx *gin.Context) {\n\tuuid, err := uuid.Parse(ctx.Param(\"id\"))\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\tdata, err := a.RTMPServer.APIConnsGet(uuid)\n\tif err != nil {\n\t\tif errors.Is(err, rtmp.ErrConnNotFound) {\n\t\t\ta.writeError(ctx, http.StatusNotFound, err)\n\t\t} else {\n\t\t\ta.writeError(ctx, http.StatusInternalServerError, err)\n\t\t}\n\t\treturn\n\t}\n\n\tctx.JSON(http.StatusOK, data)\n}\n\nfunc (a *API) onRTMPConnsKick(ctx *gin.Context) {\n\tuuid, err := uuid.Parse(ctx.Param(\"id\"))\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\terr = a.RTMPServer.APIConnsKick(uuid)\n\tif err != nil {\n\t\tif errors.Is(err, rtmp.ErrConnNotFound) {\n\t\t\ta.writeError(ctx, http.StatusNotFound, err)\n\t\t} else {\n\t\t\ta.writeError(ctx, http.StatusInternalServerError, err)\n\t\t}\n\t\treturn\n\t}\n\n\ta.writeOK(ctx)\n}\n\nfunc (a *API) onRTMPSConnsList(ctx *gin.Context) {\n\tdata, err := a.RTMPSServer.APIConnsList()\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusInternalServerError, err)\n\t\treturn\n\t}\n\n\tdata.ItemCount = len(data.Items)\n\tpageCount, err := paginate(&data.Items, ctx.Query(\"itemsPerPage\"), ctx.Query(\"page\"))\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\tdata.PageCount = pageCount\n\n\tctx.JSON(http.StatusOK, data)\n}\n\nfunc (a *API) onRTMPSConnsGet(ctx *gin.Context) {\n\tuuid, err := uuid.Parse(ctx.Param(\"id\"))\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\tdata, err := a.RTMPSServer.APIConnsGet(uuid)\n\tif err != nil {\n\t\tif errors.Is(err, rtmp.ErrConnNotFound) {\n\t\t\ta.writeError(ctx, http.StatusNotFound, err)\n\t\t} else {\n\t\t\ta.writeError(ctx, http.StatusInternalServerError, err)\n\t\t}\n\t\treturn\n\t}\n\n\tctx.JSON(http.StatusOK, data)\n}\n\nfunc (a *API) onRTMPSConnsKick(ctx *gin.Context) {\n\tuuid, err := uuid.Parse(ctx.Param(\"id\"))\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\terr = a.RTMPSServer.APIConnsKick(uuid)\n\tif err != nil {\n\t\tif errors.Is(err, rtmp.ErrConnNotFound) {\n\t\t\ta.writeError(ctx, http.StatusNotFound, err)\n\t\t} else {\n\t\t\ta.writeError(ctx, http.StatusInternalServerError, err)\n\t\t}\n\t\treturn\n\t}\n\n\ta.writeOK(ctx)\n}\n"
  },
  {
    "path": "internal/api/api_rtmp_test.go",
    "content": "package api //nolint:revive\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/servers/rtmp\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/google/uuid\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype testRTMPServer struct {\n\tconns map[uuid.UUID]*defs.APIRTMPConn\n}\n\nfunc (s *testRTMPServer) APIConnsList() (*defs.APIRTMPConnList, error) {\n\titems := make([]defs.APIRTMPConn, 0, len(s.conns))\n\tfor _, conn := range s.conns {\n\t\titems = append(items, *conn)\n\t}\n\treturn &defs.APIRTMPConnList{Items: items}, nil\n}\n\nfunc (s *testRTMPServer) APIConnsGet(id uuid.UUID) (*defs.APIRTMPConn, error) {\n\tconn, ok := s.conns[id]\n\tif !ok {\n\t\treturn nil, rtmp.ErrConnNotFound\n\t}\n\treturn conn, nil\n}\n\nfunc (s *testRTMPServer) APIConnsKick(id uuid.UUID) error {\n\t_, ok := s.conns[id]\n\tif !ok {\n\t\treturn rtmp.ErrConnNotFound\n\t}\n\treturn nil\n}\n\nfunc TestRTMPConnsList(t *testing.T) {\n\tfor _, ca := range []struct {\n\t\tname     string\n\t\tendpoint string\n\t\tisSecure bool\n\t}{\n\t\t{\n\t\t\tname:     \"rtmp\",\n\t\t\tendpoint: \"rtmpconns\",\n\t\t\tisSecure: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"rtmps\",\n\t\t\tendpoint: \"rtmpsconns\",\n\t\t\tisSecure: true,\n\t\t},\n\t} {\n\t\tt.Run(ca.name, func(t *testing.T) {\n\t\t\tid1 := uuid.New()\n\t\t\tid2 := uuid.New()\n\t\t\tnow := time.Now()\n\n\t\t\trtmpServer := &testRTMPServer{\n\t\t\t\tconns: map[uuid.UUID]*defs.APIRTMPConn{\n\t\t\t\t\tid1: {\n\t\t\t\t\t\tID:                      id1,\n\t\t\t\t\t\tCreated:                 now,\n\t\t\t\t\t\tRemoteAddr:              \"192.168.1.1:5000\",\n\t\t\t\t\t\tState:                   defs.APIRTMPConnStatePublish,\n\t\t\t\t\t\tPath:                    \"stream1\",\n\t\t\t\t\t\tQuery:                   \"token=abc\",\n\t\t\t\t\t\tInboundBytes:            1000,\n\t\t\t\t\t\tOutboundBytes:           2000,\n\t\t\t\t\t\tOutboundFramesDiscarded: 11,\n\t\t\t\t\t\tBytesReceived:           1000,\n\t\t\t\t\t\tBytesSent:               2000,\n\t\t\t\t\t},\n\t\t\t\t\tid2: {\n\t\t\t\t\t\tID:                      id2,\n\t\t\t\t\t\tCreated:                 now.Add(time.Minute),\n\t\t\t\t\t\tRemoteAddr:              \"192.168.1.2:5001\",\n\t\t\t\t\t\tState:                   defs.APIRTMPConnStateRead,\n\t\t\t\t\t\tPath:                    \"stream2\",\n\t\t\t\t\t\tQuery:                   \"\",\n\t\t\t\t\t\tInboundBytes:            500,\n\t\t\t\t\t\tOutboundBytes:           1500,\n\t\t\t\t\t\tOutboundFramesDiscarded: 22,\n\t\t\t\t\t\tBytesReceived:           500,\n\t\t\t\t\t\tBytesSent:               1500,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tapi := API{\n\t\t\t\tAddress:      \"localhost:9997\",\n\t\t\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\t\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\t\t\tAuthManager:  test.NilAuthManager,\n\t\t\t\tParent:       &testParent{},\n\t\t\t}\n\n\t\t\tif ca.isSecure {\n\t\t\t\tapi.RTMPSServer = rtmpServer\n\t\t\t} else {\n\t\t\t\tapi.RTMPServer = rtmpServer\n\t\t\t}\n\n\t\t\terr := api.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer api.Close()\n\n\t\t\ttr := &http.Transport{}\n\t\t\tdefer tr.CloseIdleConnections()\n\t\t\thc := &http.Client{Transport: tr}\n\n\t\t\tvar out defs.APIRTMPConnList\n\t\t\thttpRequest(t, hc, http.MethodGet, fmt.Sprintf(\"http://localhost:9997/v3/%s/list\", ca.endpoint), nil, &out)\n\n\t\t\trequire.Equal(t, 2, out.ItemCount)\n\t\t\trequire.Equal(t, 1, out.PageCount)\n\t\t\trequire.Len(t, out.Items, 2)\n\t\t})\n\t}\n}\n\nfunc TestRTMPConnsGet(t *testing.T) {\n\tfor _, ca := range []struct {\n\t\tname     string\n\t\tendpoint string\n\t\tpath     string\n\t\tisSecure bool\n\t}{\n\t\t{\n\t\t\tname:     \"rtmp\",\n\t\t\tendpoint: \"rtmpconns\",\n\t\t\tpath:     \"mystream\",\n\t\t\tisSecure: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"rtmps\",\n\t\t\tendpoint: \"rtmpsconns\",\n\t\t\tpath:     \"secure-stream\",\n\t\t\tisSecure: true,\n\t\t},\n\t} {\n\t\tt.Run(ca.name, func(t *testing.T) {\n\t\t\tid := uuid.New()\n\t\t\tnow := time.Now()\n\n\t\t\trtmpServer := &testRTMPServer{\n\t\t\t\tconns: map[uuid.UUID]*defs.APIRTMPConn{\n\t\t\t\t\tid: {\n\t\t\t\t\t\tID:                      id,\n\t\t\t\t\t\tCreated:                 now,\n\t\t\t\t\t\tRemoteAddr:              \"192.168.1.100:5000\",\n\t\t\t\t\t\tState:                   defs.APIRTMPConnStatePublish,\n\t\t\t\t\t\tPath:                    ca.path,\n\t\t\t\t\t\tQuery:                   \"key=value\",\n\t\t\t\t\t\tInboundBytes:            999999,\n\t\t\t\t\t\tOutboundBytes:           888888,\n\t\t\t\t\t\tOutboundFramesDiscarded: 33,\n\t\t\t\t\t\tBytesReceived:           999999,\n\t\t\t\t\t\tBytesSent:               888888,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tapi := API{\n\t\t\t\tAddress:      \"localhost:9997\",\n\t\t\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\t\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\t\t\tAuthManager:  test.NilAuthManager,\n\t\t\t\tParent:       &testParent{},\n\t\t\t}\n\n\t\t\tif ca.isSecure {\n\t\t\t\tapi.RTMPSServer = rtmpServer\n\t\t\t} else {\n\t\t\t\tapi.RTMPServer = rtmpServer\n\t\t\t}\n\n\t\t\terr := api.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer api.Close()\n\n\t\t\ttr := &http.Transport{}\n\t\t\tdefer tr.CloseIdleConnections()\n\t\t\thc := &http.Client{Transport: tr}\n\n\t\t\tvar out defs.APIRTMPConn\n\t\t\thttpRequest(t, hc, http.MethodGet, fmt.Sprintf(\"http://localhost:9997/v3/%s/get/%s\", ca.endpoint, id), nil, &out)\n\n\t\t\trequire.Equal(t, id, out.ID)\n\t\t\trequire.Equal(t, \"192.168.1.100:5000\", out.RemoteAddr)\n\t\t\trequire.Equal(t, defs.APIRTMPConnStatePublish, out.State)\n\t\t\trequire.Equal(t, ca.path, out.Path)\n\t\t\trequire.Equal(t, uint64(999999), out.InboundBytes)\n\t\t\trequire.Equal(t, uint64(888888), out.OutboundBytes)\n\t\t\trequire.Equal(t, uint64(33), out.OutboundFramesDiscarded)\n\t\t\trequire.Equal(t, uint64(999999), out.BytesReceived)\n\t\t})\n\t}\n}\n\nfunc TestRTMPConnsKick(t *testing.T) {\n\tfor _, ca := range []struct {\n\t\tname     string\n\t\tendpoint string\n\t\tpath     string\n\t\tisSecure bool\n\t}{\n\t\t{\n\t\t\tname:     \"rtmp\",\n\t\t\tendpoint: \"rtmpconns\",\n\t\t\tpath:     \"mystream\",\n\t\t\tisSecure: false,\n\t\t},\n\t\t{\n\t\t\tname:     \"rtmps\",\n\t\t\tendpoint: \"rtmpsconns\",\n\t\t\tpath:     \"secure-stream\",\n\t\t\tisSecure: true,\n\t\t},\n\t} {\n\t\tt.Run(ca.name, func(t *testing.T) {\n\t\t\tid := uuid.New()\n\t\t\tnow := time.Now()\n\n\t\t\trtmpServer := &testRTMPServer{\n\t\t\t\tconns: map[uuid.UUID]*defs.APIRTMPConn{\n\t\t\t\t\tid: {\n\t\t\t\t\t\tID:            id,\n\t\t\t\t\t\tCreated:       now,\n\t\t\t\t\t\tRemoteAddr:    \"192.168.1.100:5000\",\n\t\t\t\t\t\tState:         defs.APIRTMPConnStatePublish,\n\t\t\t\t\t\tPath:          ca.path,\n\t\t\t\t\t\tQuery:         \"\",\n\t\t\t\t\t\tBytesReceived: 1000,\n\t\t\t\t\t\tBytesSent:     2000,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tapi := API{\n\t\t\t\tAddress:      \"localhost:9997\",\n\t\t\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\t\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\t\t\tAuthManager:  test.NilAuthManager,\n\t\t\t\tParent:       &testParent{},\n\t\t\t}\n\n\t\t\tif ca.isSecure {\n\t\t\t\tapi.RTMPSServer = rtmpServer\n\t\t\t} else {\n\t\t\t\tapi.RTMPServer = rtmpServer\n\t\t\t}\n\n\t\t\terr := api.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer api.Close()\n\n\t\t\ttr := &http.Transport{}\n\t\t\tdefer tr.CloseIdleConnections()\n\t\t\thc := &http.Client{Transport: tr}\n\n\t\t\thttpRequest(t, hc, http.MethodPost, fmt.Sprintf(\"http://localhost:9997/v3/%s/kick/%s\", ca.endpoint, id), nil, nil)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/api/api_rtsp.go",
    "content": "package api //nolint:revive\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\n\t\"github.com/bluenviron/mediamtx/internal/servers/rtsp\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/google/uuid\"\n)\n\nfunc (a *API) onRTSPConnsList(ctx *gin.Context) {\n\tdata, err := a.RTSPServer.APIConnsList()\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusInternalServerError, err)\n\t\treturn\n\t}\n\n\tdata.ItemCount = len(data.Items)\n\tpageCount, err := paginate(&data.Items, ctx.Query(\"itemsPerPage\"), ctx.Query(\"page\"))\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\tdata.PageCount = pageCount\n\n\tctx.JSON(http.StatusOK, data)\n}\n\nfunc (a *API) onRTSPConnsGet(ctx *gin.Context) {\n\tuuid, err := uuid.Parse(ctx.Param(\"id\"))\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\tdata, err := a.RTSPServer.APIConnsGet(uuid)\n\tif err != nil {\n\t\tif errors.Is(err, rtsp.ErrConnNotFound) {\n\t\t\ta.writeError(ctx, http.StatusNotFound, err)\n\t\t} else {\n\t\t\ta.writeError(ctx, http.StatusInternalServerError, err)\n\t\t}\n\t\treturn\n\t}\n\n\tctx.JSON(http.StatusOK, data)\n}\n\nfunc (a *API) onRTSPSessionsList(ctx *gin.Context) {\n\tdata, err := a.RTSPServer.APISessionsList()\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusInternalServerError, err)\n\t\treturn\n\t}\n\n\tdata.ItemCount = len(data.Items)\n\tpageCount, err := paginate(&data.Items, ctx.Query(\"itemsPerPage\"), ctx.Query(\"page\"))\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\tdata.PageCount = pageCount\n\n\tctx.JSON(http.StatusOK, data)\n}\n\nfunc (a *API) onRTSPSessionsGet(ctx *gin.Context) {\n\tuuid, err := uuid.Parse(ctx.Param(\"id\"))\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\tdata, err := a.RTSPServer.APISessionsGet(uuid)\n\tif err != nil {\n\t\tif errors.Is(err, rtsp.ErrSessionNotFound) {\n\t\t\ta.writeError(ctx, http.StatusNotFound, err)\n\t\t} else {\n\t\t\ta.writeError(ctx, http.StatusInternalServerError, err)\n\t\t}\n\t\treturn\n\t}\n\n\tctx.JSON(http.StatusOK, data)\n}\n\nfunc (a *API) onRTSPSessionsKick(ctx *gin.Context) {\n\tuuid, err := uuid.Parse(ctx.Param(\"id\"))\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\terr = a.RTSPServer.APISessionsKick(uuid)\n\tif err != nil {\n\t\tif errors.Is(err, rtsp.ErrSessionNotFound) {\n\t\t\ta.writeError(ctx, http.StatusNotFound, err)\n\t\t} else {\n\t\t\ta.writeError(ctx, http.StatusInternalServerError, err)\n\t\t}\n\t\treturn\n\t}\n\n\ta.writeOK(ctx)\n}\n\nfunc (a *API) onRTSPSConnsList(ctx *gin.Context) {\n\tdata, err := a.RTSPSServer.APIConnsList()\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusInternalServerError, err)\n\t\treturn\n\t}\n\n\tdata.ItemCount = len(data.Items)\n\tpageCount, err := paginate(&data.Items, ctx.Query(\"itemsPerPage\"), ctx.Query(\"page\"))\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\tdata.PageCount = pageCount\n\n\tctx.JSON(http.StatusOK, data)\n}\n\nfunc (a *API) onRTSPSConnsGet(ctx *gin.Context) {\n\tuuid, err := uuid.Parse(ctx.Param(\"id\"))\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\tdata, err := a.RTSPSServer.APIConnsGet(uuid)\n\tif err != nil {\n\t\tif errors.Is(err, rtsp.ErrConnNotFound) {\n\t\t\ta.writeError(ctx, http.StatusNotFound, err)\n\t\t} else {\n\t\t\ta.writeError(ctx, http.StatusInternalServerError, err)\n\t\t}\n\t\treturn\n\t}\n\n\tctx.JSON(http.StatusOK, data)\n}\n\nfunc (a *API) onRTSPSSessionsList(ctx *gin.Context) {\n\tdata, err := a.RTSPSServer.APISessionsList()\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusInternalServerError, err)\n\t\treturn\n\t}\n\n\tdata.ItemCount = len(data.Items)\n\tpageCount, err := paginate(&data.Items, ctx.Query(\"itemsPerPage\"), ctx.Query(\"page\"))\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\tdata.PageCount = pageCount\n\n\tctx.JSON(http.StatusOK, data)\n}\n\nfunc (a *API) onRTSPSSessionsGet(ctx *gin.Context) {\n\tuuid, err := uuid.Parse(ctx.Param(\"id\"))\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\tdata, err := a.RTSPSServer.APISessionsGet(uuid)\n\tif err != nil {\n\t\tif errors.Is(err, rtsp.ErrSessionNotFound) {\n\t\t\ta.writeError(ctx, http.StatusNotFound, err)\n\t\t} else {\n\t\t\ta.writeError(ctx, http.StatusInternalServerError, err)\n\t\t}\n\t\treturn\n\t}\n\n\tctx.JSON(http.StatusOK, data)\n}\n\nfunc (a *API) onRTSPSSessionsKick(ctx *gin.Context) {\n\tuuid, err := uuid.Parse(ctx.Param(\"id\"))\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\terr = a.RTSPSServer.APISessionsKick(uuid)\n\tif err != nil {\n\t\tif errors.Is(err, rtsp.ErrSessionNotFound) {\n\t\t\ta.writeError(ctx, http.StatusNotFound, err)\n\t\t} else {\n\t\t\ta.writeError(ctx, http.StatusInternalServerError, err)\n\t\t}\n\t\treturn\n\t}\n\n\ta.writeOK(ctx)\n}\n"
  },
  {
    "path": "internal/api/api_rtsp_test.go",
    "content": "package api //nolint:revive\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/servers/rtsp\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/google/uuid\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype testRTSPServer struct {\n\tconns    map[uuid.UUID]*defs.APIRTSPConn\n\tsessions map[uuid.UUID]*defs.APIRTSPSession\n}\n\nfunc (s *testRTSPServer) APIConnsList() (*defs.APIRTSPConnsList, error) {\n\titems := make([]defs.APIRTSPConn, 0, len(s.conns))\n\tfor _, conn := range s.conns {\n\t\titems = append(items, *conn)\n\t}\n\treturn &defs.APIRTSPConnsList{Items: items}, nil\n}\n\nfunc (s *testRTSPServer) APIConnsGet(id uuid.UUID) (*defs.APIRTSPConn, error) {\n\tconn, ok := s.conns[id]\n\tif !ok {\n\t\treturn nil, rtsp.ErrConnNotFound\n\t}\n\treturn conn, nil\n}\n\nfunc (s *testRTSPServer) APISessionsList() (*defs.APIRTSPSessionList, error) {\n\titems := make([]defs.APIRTSPSession, 0, len(s.sessions))\n\tfor _, session := range s.sessions {\n\t\titems = append(items, *session)\n\t}\n\treturn &defs.APIRTSPSessionList{Items: items}, nil\n}\n\nfunc (s *testRTSPServer) APISessionsGet(id uuid.UUID) (*defs.APIRTSPSession, error) {\n\tsession, ok := s.sessions[id]\n\tif !ok {\n\t\treturn nil, rtsp.ErrSessionNotFound\n\t}\n\treturn session, nil\n}\n\nfunc (s *testRTSPServer) APISessionsKick(id uuid.UUID) error {\n\t_, ok := s.sessions[id]\n\tif !ok {\n\t\treturn rtsp.ErrSessionNotFound\n\t}\n\treturn nil\n}\n\nfunc TestRTSPConnsList(t *testing.T) {\n\tfor _, ca := range []struct {\n\t\tname     string\n\t\tendpoint string\n\t\tsecure   bool\n\t}{\n\t\t{\n\t\t\tname:     \"rtsp\",\n\t\t\tendpoint: \"rtspconns\",\n\t\t\tsecure:   false,\n\t\t},\n\t\t{\n\t\t\tname:     \"rtsps\",\n\t\t\tendpoint: \"rtspsconns\",\n\t\t\tsecure:   true,\n\t\t},\n\t} {\n\t\tt.Run(ca.name, func(t *testing.T) {\n\t\t\tid1 := uuid.New()\n\t\t\tid2 := uuid.New()\n\t\t\tsessionID := uuid.New()\n\t\t\tnow := time.Now()\n\n\t\t\trtspServer := &testRTSPServer{\n\t\t\t\tconns: map[uuid.UUID]*defs.APIRTSPConn{\n\t\t\t\t\tid1: {\n\t\t\t\t\t\tID:            id1,\n\t\t\t\t\t\tCreated:       now,\n\t\t\t\t\t\tRemoteAddr:    \"192.168.1.1:5000\",\n\t\t\t\t\t\tInboundBytes:  1000,\n\t\t\t\t\t\tOutboundBytes: 2000,\n\t\t\t\t\t\tBytesReceived: 1000,\n\t\t\t\t\t\tBytesSent:     2000,\n\t\t\t\t\t\tSession:       &sessionID,\n\t\t\t\t\t\tTunnel:        \"\",\n\t\t\t\t\t},\n\t\t\t\t\tid2: {\n\t\t\t\t\t\tID:            id2,\n\t\t\t\t\t\tCreated:       now.Add(time.Minute),\n\t\t\t\t\t\tRemoteAddr:    \"192.168.1.2:5001\",\n\t\t\t\t\t\tInboundBytes:  500,\n\t\t\t\t\t\tOutboundBytes: 1500,\n\t\t\t\t\t\tBytesReceived: 500,\n\t\t\t\t\t\tBytesSent:     1500,\n\t\t\t\t\t\tSession:       nil,\n\t\t\t\t\t\tTunnel:        \"http\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tapi := API{\n\t\t\t\tAddress:      \"localhost:9997\",\n\t\t\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\t\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\t\t\tAuthManager:  test.NilAuthManager,\n\t\t\t\tParent:       &testParent{},\n\t\t\t}\n\t\t\tif ca.secure {\n\t\t\t\tapi.RTSPSServer = rtspServer\n\t\t\t} else {\n\t\t\t\tapi.RTSPServer = rtspServer\n\t\t\t}\n\t\t\terr := api.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer api.Close()\n\n\t\t\ttr := &http.Transport{}\n\t\t\tdefer tr.CloseIdleConnections()\n\t\t\thc := &http.Client{Transport: tr}\n\n\t\t\treq, err := http.NewRequest(http.MethodGet,\n\t\t\t\tfmt.Sprintf(\"http://localhost:9997/v3/%s/list\", ca.endpoint), nil)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tres, err := hc.Do(req)\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer res.Body.Close()\n\n\t\t\trequire.Equal(t, http.StatusOK, res.StatusCode)\n\n\t\t\tvar out defs.APIRTSPConnsList\n\t\t\terr = json.NewDecoder(res.Body).Decode(&out)\n\t\t\trequire.NoError(t, err)\n\n\t\t\trequire.Equal(t, 2, out.ItemCount)\n\t\t\trequire.Equal(t, 1, out.PageCount)\n\t\t\trequire.Len(t, out.Items, 2)\n\t\t})\n\t}\n}\n\nfunc TestRTSPConnsGet(t *testing.T) {\n\tfor _, ca := range []struct {\n\t\tname     string\n\t\tendpoint string\n\t\tsecure   bool\n\t}{\n\t\t{\n\t\t\tname:     \"rtsp\",\n\t\t\tendpoint: \"rtspconns\",\n\t\t\tsecure:   false,\n\t\t},\n\t\t{\n\t\t\tname:     \"rtsps\",\n\t\t\tendpoint: \"rtspsconns\",\n\t\t\tsecure:   true,\n\t\t},\n\t} {\n\t\tt.Run(ca.name, func(t *testing.T) {\n\t\t\tid := uuid.New()\n\t\t\tsessionID := uuid.New()\n\t\t\tnow := time.Now()\n\n\t\t\trtspServer := &testRTSPServer{\n\t\t\t\tconns: map[uuid.UUID]*defs.APIRTSPConn{\n\t\t\t\t\tid: {\n\t\t\t\t\t\tID:            id,\n\t\t\t\t\t\tCreated:       now,\n\t\t\t\t\t\tRemoteAddr:    \"192.168.1.100:5000\",\n\t\t\t\t\t\tInboundBytes:  999999,\n\t\t\t\t\t\tOutboundBytes: 888888,\n\t\t\t\t\t\tBytesReceived: 999999,\n\t\t\t\t\t\tBytesSent:     888888,\n\t\t\t\t\t\tSession:       &sessionID,\n\t\t\t\t\t\tTunnel:        \"\",\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tapi := API{\n\t\t\t\tAddress:      \"localhost:9997\",\n\t\t\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\t\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\t\t\tAuthManager:  test.NilAuthManager,\n\t\t\t\tParent:       &testParent{},\n\t\t\t}\n\t\t\tif ca.secure {\n\t\t\t\tapi.RTSPSServer = rtspServer\n\t\t\t} else {\n\t\t\t\tapi.RTSPServer = rtspServer\n\t\t\t}\n\t\t\terr := api.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer api.Close()\n\n\t\t\ttr := &http.Transport{}\n\t\t\tdefer tr.CloseIdleConnections()\n\t\t\thc := &http.Client{Transport: tr}\n\n\t\t\treq, err := http.NewRequest(http.MethodGet,\n\t\t\t\tfmt.Sprintf(\"http://localhost:9997/v3/%s/get/%s\", ca.endpoint, id), nil)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tres, err := hc.Do(req)\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer res.Body.Close()\n\n\t\t\trequire.Equal(t, http.StatusOK, res.StatusCode)\n\n\t\t\tvar out defs.APIRTSPConn\n\t\t\terr = json.NewDecoder(res.Body).Decode(&out)\n\t\t\trequire.NoError(t, err)\n\n\t\t\trequire.Equal(t, id, out.ID)\n\t\t\trequire.Equal(t, \"192.168.1.100:5000\", out.RemoteAddr)\n\t\t\trequire.Equal(t, uint64(999999), out.InboundBytes)\n\t\t\trequire.Equal(t, uint64(888888), out.OutboundBytes)\n\t\t\trequire.Equal(t, uint64(999999), out.BytesReceived)\n\t\t\trequire.NotNil(t, out.Session)\n\t\t\trequire.Equal(t, sessionID, *out.Session)\n\t\t})\n\t}\n}\n\nfunc TestRTSPSessionsList(t *testing.T) {\n\tfor _, ca := range []struct {\n\t\tname     string\n\t\tendpoint string\n\t\tsecure   bool\n\t}{\n\t\t{\n\t\t\tname:     \"rtsp\",\n\t\t\tendpoint: \"rtspsessions\",\n\t\t\tsecure:   false,\n\t\t},\n\t\t{\n\t\t\tname:     \"rtsps\",\n\t\t\tendpoint: \"rtspssessions\",\n\t\t\tsecure:   true,\n\t\t},\n\t} {\n\t\tt.Run(ca.name, func(t *testing.T) {\n\t\t\tid1 := uuid.New()\n\t\t\tid2 := uuid.New()\n\t\t\tnow := time.Now()\n\t\t\ttransport := \"UDP\"\n\t\t\tprofile := \"AVP\"\n\n\t\t\trtspServer := &testRTSPServer{\n\t\t\t\tsessions: map[uuid.UUID]*defs.APIRTSPSession{\n\t\t\t\t\tid1: {\n\t\t\t\t\t\tID:                             id1,\n\t\t\t\t\t\tCreated:                        now,\n\t\t\t\t\t\tRemoteAddr:                     \"192.168.1.1:5000\",\n\t\t\t\t\t\tState:                          defs.APIRTSPSessionStatePublish,\n\t\t\t\t\t\tPath:                           \"stream1\",\n\t\t\t\t\t\tQuery:                          \"token=abc\",\n\t\t\t\t\t\tTransport:                      &transport,\n\t\t\t\t\t\tProfile:                        &profile,\n\t\t\t\t\t\tInboundBytes:                   1000,\n\t\t\t\t\t\tInboundRTPPackets:              100,\n\t\t\t\t\t\tInboundRTPPacketsLost:          5,\n\t\t\t\t\t\tInboundRTPPacketsInError:       2,\n\t\t\t\t\t\tInboundRTPPacketsJitter:        0.5,\n\t\t\t\t\t\tInboundRTCPPackets:             10,\n\t\t\t\t\t\tInboundRTCPPacketsInError:      1,\n\t\t\t\t\t\tOutboundBytes:                  2000,\n\t\t\t\t\t\tOutboundRTPPackets:             200,\n\t\t\t\t\t\tOutboundRTPPacketsReportedLost: 7,\n\t\t\t\t\t\tOutboundRTCPPackets:            15,\n\t\t\t\t\t\tBytesReceived:                  1000,\n\t\t\t\t\t\tBytesSent:                      2000,\n\t\t\t\t\t\tRTPPacketsReceived:             100,\n\t\t\t\t\t\tRTPPacketsSent:                 200,\n\t\t\t\t\t\tRTPPacketsLost:                 5,\n\t\t\t\t\t\tRTPPacketsInError:              2,\n\t\t\t\t\t\tRTPPacketsJitter:               0.5,\n\t\t\t\t\t\tRTCPPacketsReceived:            10,\n\t\t\t\t\t\tRTCPPacketsSent:                15,\n\t\t\t\t\t\tRTCPPacketsInError:             1,\n\t\t\t\t\t},\n\t\t\t\t\tid2: {\n\t\t\t\t\t\tID:                             id2,\n\t\t\t\t\t\tCreated:                        now.Add(time.Minute),\n\t\t\t\t\t\tRemoteAddr:                     \"192.168.1.2:5001\",\n\t\t\t\t\t\tState:                          defs.APIRTSPSessionStateRead,\n\t\t\t\t\t\tPath:                           \"stream2\",\n\t\t\t\t\t\tQuery:                          \"\",\n\t\t\t\t\t\tTransport:                      nil,\n\t\t\t\t\t\tProfile:                        nil,\n\t\t\t\t\t\tInboundBytes:                   500,\n\t\t\t\t\t\tInboundRTPPackets:              50,\n\t\t\t\t\t\tInboundRTPPacketsLost:          0,\n\t\t\t\t\t\tInboundRTPPacketsInError:       0,\n\t\t\t\t\t\tInboundRTPPacketsJitter:        0.1,\n\t\t\t\t\t\tInboundRTCPPackets:             5,\n\t\t\t\t\t\tInboundRTCPPacketsInError:      0,\n\t\t\t\t\t\tOutboundBytes:                  1500,\n\t\t\t\t\t\tOutboundRTPPackets:             150,\n\t\t\t\t\t\tOutboundRTPPacketsReportedLost: 0,\n\t\t\t\t\t\tOutboundRTCPPackets:            10,\n\t\t\t\t\t\tBytesReceived:                  500,\n\t\t\t\t\t\tBytesSent:                      1500,\n\t\t\t\t\t\tRTPPacketsReceived:             50,\n\t\t\t\t\t\tRTPPacketsSent:                 150,\n\t\t\t\t\t\tRTPPacketsLost:                 0,\n\t\t\t\t\t\tRTPPacketsInError:              0,\n\t\t\t\t\t\tRTPPacketsJitter:               0.1,\n\t\t\t\t\t\tRTCPPacketsReceived:            5,\n\t\t\t\t\t\tRTCPPacketsSent:                10,\n\t\t\t\t\t\tRTCPPacketsInError:             0,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tapi := API{\n\t\t\t\tAddress:      \"localhost:9997\",\n\t\t\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\t\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\t\t\tAuthManager:  test.NilAuthManager,\n\t\t\t\tParent:       &testParent{},\n\t\t\t}\n\t\t\tif ca.secure {\n\t\t\t\tapi.RTSPSServer = rtspServer\n\t\t\t} else {\n\t\t\t\tapi.RTSPServer = rtspServer\n\t\t\t}\n\t\t\terr := api.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer api.Close()\n\n\t\t\ttr := &http.Transport{}\n\t\t\tdefer tr.CloseIdleConnections()\n\t\t\thc := &http.Client{Transport: tr}\n\n\t\t\treq, err := http.NewRequest(http.MethodGet,\n\t\t\t\tfmt.Sprintf(\"http://localhost:9997/v3/%s/list\", ca.endpoint), nil)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tres, err := hc.Do(req)\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer res.Body.Close()\n\n\t\t\trequire.Equal(t, http.StatusOK, res.StatusCode)\n\n\t\t\tvar out defs.APIRTSPSessionList\n\t\t\terr = json.NewDecoder(res.Body).Decode(&out)\n\t\t\trequire.NoError(t, err)\n\n\t\t\trequire.Equal(t, 2, out.ItemCount)\n\t\t\trequire.Equal(t, 1, out.PageCount)\n\t\t\trequire.Len(t, out.Items, 2)\n\t\t})\n\t}\n}\n\nfunc TestRTSPSessionsGet(t *testing.T) {\n\tfor _, ca := range []struct {\n\t\tname     string\n\t\tendpoint string\n\t\tsecure   bool\n\t}{\n\t\t{\n\t\t\tname:     \"rtsp\",\n\t\t\tendpoint: \"rtspsessions\",\n\t\t\tsecure:   false,\n\t\t},\n\t\t{\n\t\t\tname:     \"rtsps\",\n\t\t\tendpoint: \"rtspssessions\",\n\t\t\tsecure:   true,\n\t\t},\n\t} {\n\t\tt.Run(ca.name, func(t *testing.T) {\n\t\t\tid := uuid.New()\n\t\t\tnow := time.Now()\n\t\t\ttransport := \"UDP\"\n\t\t\tprofile := \"AVP\"\n\n\t\t\trtspServer := &testRTSPServer{\n\t\t\t\tsessions: map[uuid.UUID]*defs.APIRTSPSession{\n\t\t\t\t\tid: {\n\t\t\t\t\t\tID:                             id,\n\t\t\t\t\t\tCreated:                        now,\n\t\t\t\t\t\tRemoteAddr:                     \"192.168.1.100:5000\",\n\t\t\t\t\t\tState:                          defs.APIRTSPSessionStatePublish,\n\t\t\t\t\t\tPath:                           \"mystream\",\n\t\t\t\t\t\tQuery:                          \"key=value\",\n\t\t\t\t\t\tTransport:                      &transport,\n\t\t\t\t\t\tProfile:                        &profile,\n\t\t\t\t\t\tInboundBytes:                   999999,\n\t\t\t\t\t\tInboundRTPPackets:              10000,\n\t\t\t\t\t\tInboundRTPPacketsLost:          50,\n\t\t\t\t\t\tInboundRTPPacketsInError:       10,\n\t\t\t\t\t\tInboundRTPPacketsJitter:        1.5,\n\t\t\t\t\t\tInboundRTCPPackets:             100,\n\t\t\t\t\t\tInboundRTCPPacketsInError:      5,\n\t\t\t\t\t\tOutboundBytes:                  888888,\n\t\t\t\t\t\tOutboundRTPPackets:             20000,\n\t\t\t\t\t\tOutboundRTPPacketsReportedLost: 25,\n\t\t\t\t\t\tOutboundRTCPPackets:            200,\n\t\t\t\t\t\tBytesReceived:                  999999,\n\t\t\t\t\t\tBytesSent:                      888888,\n\t\t\t\t\t\tRTPPacketsReceived:             10000,\n\t\t\t\t\t\tRTPPacketsSent:                 20000,\n\t\t\t\t\t\tRTPPacketsLost:                 50,\n\t\t\t\t\t\tRTPPacketsInError:              10,\n\t\t\t\t\t\tRTPPacketsJitter:               1.5,\n\t\t\t\t\t\tRTCPPacketsReceived:            100,\n\t\t\t\t\t\tRTCPPacketsSent:                200,\n\t\t\t\t\t\tRTCPPacketsInError:             5,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tapi := API{\n\t\t\t\tAddress:      \"localhost:9997\",\n\t\t\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\t\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\t\t\tAuthManager:  test.NilAuthManager,\n\t\t\t\tParent:       &testParent{},\n\t\t\t}\n\t\t\tif ca.secure {\n\t\t\t\tapi.RTSPSServer = rtspServer\n\t\t\t} else {\n\t\t\t\tapi.RTSPServer = rtspServer\n\t\t\t}\n\t\t\terr := api.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer api.Close()\n\n\t\t\ttr := &http.Transport{}\n\t\t\tdefer tr.CloseIdleConnections()\n\t\t\thc := &http.Client{Transport: tr}\n\n\t\t\treq, err := http.NewRequest(http.MethodGet,\n\t\t\t\tfmt.Sprintf(\"http://localhost:9997/v3/%s/get/%s\", ca.endpoint, id), nil)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tres, err := hc.Do(req)\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer res.Body.Close()\n\n\t\t\trequire.Equal(t, http.StatusOK, res.StatusCode)\n\n\t\t\tvar out defs.APIRTSPSession\n\t\t\terr = json.NewDecoder(res.Body).Decode(&out)\n\t\t\trequire.NoError(t, err)\n\n\t\t\trequire.Equal(t, id, out.ID)\n\t\t\trequire.Equal(t, \"192.168.1.100:5000\", out.RemoteAddr)\n\t\t\trequire.Equal(t, defs.APIRTSPSessionStatePublish, out.State)\n\t\t\trequire.Equal(t, \"mystream\", out.Path)\n\t\t\trequire.Equal(t, uint64(999999), out.InboundBytes)\n\t\t\trequire.Equal(t, uint64(888888), out.OutboundBytes)\n\t\t\trequire.Equal(t, uint64(10000), out.InboundRTPPackets)\n\t\t\trequire.Equal(t, uint64(20000), out.OutboundRTPPackets)\n\t\t\trequire.Equal(t, uint64(25), out.OutboundRTPPacketsReportedLost)\n\t\t\trequire.Equal(t, uint64(999999), out.BytesReceived)\n\t\t\trequire.NotNil(t, out.Transport)\n\t\t\trequire.Equal(t, \"UDP\", *out.Transport)\n\t\t})\n\t}\n}\n\nfunc TestRTSPSessionsKick(t *testing.T) {\n\tfor _, ca := range []struct {\n\t\tname     string\n\t\tendpoint string\n\t\tsecure   bool\n\t}{\n\t\t{\n\t\t\tname:     \"rtsp\",\n\t\t\tendpoint: \"rtspsessions\",\n\t\t\tsecure:   false,\n\t\t},\n\t\t{\n\t\t\tname:     \"rtsps\",\n\t\t\tendpoint: \"rtspssessions\",\n\t\t\tsecure:   true,\n\t\t},\n\t} {\n\t\tt.Run(ca.name, func(t *testing.T) {\n\t\t\tid := uuid.New()\n\t\t\tnow := time.Now()\n\t\t\ttransport := \"UDP\"\n\t\t\tprofile := \"AVP\"\n\n\t\t\trtspServer := &testRTSPServer{\n\t\t\t\tsessions: map[uuid.UUID]*defs.APIRTSPSession{\n\t\t\t\t\tid: {\n\t\t\t\t\t\tID:                  id,\n\t\t\t\t\t\tCreated:             now,\n\t\t\t\t\t\tRemoteAddr:          \"192.168.1.100:5000\",\n\t\t\t\t\t\tState:               defs.APIRTSPSessionStatePublish,\n\t\t\t\t\t\tPath:                \"mystream\",\n\t\t\t\t\t\tQuery:               \"\",\n\t\t\t\t\t\tTransport:           &transport,\n\t\t\t\t\t\tProfile:             &profile,\n\t\t\t\t\t\tBytesReceived:       1000,\n\t\t\t\t\t\tBytesSent:           2000,\n\t\t\t\t\t\tRTPPacketsReceived:  100,\n\t\t\t\t\t\tRTPPacketsSent:      200,\n\t\t\t\t\t\tRTPPacketsLost:      0,\n\t\t\t\t\t\tRTPPacketsInError:   0,\n\t\t\t\t\t\tRTPPacketsJitter:    0.5,\n\t\t\t\t\t\tRTCPPacketsReceived: 10,\n\t\t\t\t\t\tRTCPPacketsSent:     15,\n\t\t\t\t\t\tRTCPPacketsInError:  0,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tapi := API{\n\t\t\t\tAddress:      \"localhost:9997\",\n\t\t\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\t\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\t\t\tAuthManager:  test.NilAuthManager,\n\t\t\t\tParent:       &testParent{},\n\t\t\t}\n\t\t\tif ca.secure {\n\t\t\t\tapi.RTSPSServer = rtspServer\n\t\t\t} else {\n\t\t\t\tapi.RTSPServer = rtspServer\n\t\t\t}\n\t\t\terr := api.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer api.Close()\n\n\t\t\ttr := &http.Transport{}\n\t\t\tdefer tr.CloseIdleConnections()\n\t\t\thc := &http.Client{Transport: tr}\n\n\t\t\treq, err := http.NewRequest(http.MethodPost,\n\t\t\t\tfmt.Sprintf(\"http://localhost:9997/v3/%s/kick/%s\", ca.endpoint, id), nil)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tres, err := hc.Do(req)\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer res.Body.Close()\n\n\t\t\trequire.Equal(t, http.StatusOK, res.StatusCode)\n\t\t\tcheckOK(t, res.Body)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/api/api_srt.go",
    "content": "//nolint:dupl\npackage api //nolint:revive\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\n\t\"github.com/bluenviron/mediamtx/internal/servers/srt\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/google/uuid\"\n)\n\nfunc (a *API) onSRTConnsList(ctx *gin.Context) {\n\tdata, err := a.SRTServer.APIConnsList()\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusInternalServerError, err)\n\t\treturn\n\t}\n\n\tdata.ItemCount = len(data.Items)\n\tpageCount, err := paginate(&data.Items, ctx.Query(\"itemsPerPage\"), ctx.Query(\"page\"))\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\tdata.PageCount = pageCount\n\n\tctx.JSON(http.StatusOK, data)\n}\n\nfunc (a *API) onSRTConnsGet(ctx *gin.Context) {\n\tuuid, err := uuid.Parse(ctx.Param(\"id\"))\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\tdata, err := a.SRTServer.APIConnsGet(uuid)\n\tif err != nil {\n\t\tif errors.Is(err, srt.ErrConnNotFound) {\n\t\t\ta.writeError(ctx, http.StatusNotFound, err)\n\t\t} else {\n\t\t\ta.writeError(ctx, http.StatusInternalServerError, err)\n\t\t}\n\t\treturn\n\t}\n\n\tctx.JSON(http.StatusOK, data)\n}\n\nfunc (a *API) onSRTConnsKick(ctx *gin.Context) {\n\tuuid, err := uuid.Parse(ctx.Param(\"id\"))\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\terr = a.SRTServer.APIConnsKick(uuid)\n\tif err != nil {\n\t\tif errors.Is(err, srt.ErrConnNotFound) {\n\t\t\ta.writeError(ctx, http.StatusNotFound, err)\n\t\t} else {\n\t\t\ta.writeError(ctx, http.StatusInternalServerError, err)\n\t\t}\n\t\treturn\n\t}\n\n\ta.writeOK(ctx)\n}\n"
  },
  {
    "path": "internal/api/api_srt_test.go",
    "content": "package api //nolint:revive\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/servers/srt\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/google/uuid\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype testSRTServer struct {\n\tconns map[uuid.UUID]*defs.APISRTConn\n}\n\nfunc (s *testSRTServer) APIConnsList() (*defs.APISRTConnList, error) {\n\titems := make([]defs.APISRTConn, 0, len(s.conns))\n\tfor _, conn := range s.conns {\n\t\titems = append(items, *conn)\n\t}\n\treturn &defs.APISRTConnList{Items: items}, nil\n}\n\nfunc (s *testSRTServer) APIConnsGet(id uuid.UUID) (*defs.APISRTConn, error) {\n\tconn, ok := s.conns[id]\n\tif !ok {\n\t\treturn nil, srt.ErrConnNotFound\n\t}\n\treturn conn, nil\n}\n\nfunc (s *testSRTServer) APIConnsKick(id uuid.UUID) error {\n\t_, ok := s.conns[id]\n\tif !ok {\n\t\treturn srt.ErrConnNotFound\n\t}\n\treturn nil\n}\n\nfunc TestSRTConnsList(t *testing.T) {\n\tid1 := uuid.New()\n\tid2 := uuid.New()\n\tnow := time.Now()\n\n\tsrtServer := &testSRTServer{\n\t\tconns: map[uuid.UUID]*defs.APISRTConn{\n\t\t\tid1: {\n\t\t\t\tID:                      id1,\n\t\t\t\tCreated:                 now,\n\t\t\t\tRemoteAddr:              \"192.168.1.1:5000\",\n\t\t\t\tState:                   defs.APISRTConnStatePublish,\n\t\t\t\tPath:                    \"stream1\",\n\t\t\t\tQuery:                   \"token=abc\",\n\t\t\t\tPacketsSent:             1000,\n\t\t\t\tPacketsReceived:         2000,\n\t\t\t\tPacketsSentUnique:       950,\n\t\t\t\tPacketsReceivedUnique:   1950,\n\t\t\t\tBytesReceived:           100000,\n\t\t\t\tBytesSent:               200000,\n\t\t\t\tOutboundFramesDiscarded: 5,\n\t\t\t\tMsRTT:                   10.5,\n\t\t\t\tMbpsSendRate:            5.2,\n\t\t\t\tMbpsReceiveRate:         4.8,\n\t\t\t},\n\t\t\tid2: {\n\t\t\t\tID:                      id2,\n\t\t\t\tCreated:                 now.Add(time.Minute),\n\t\t\t\tRemoteAddr:              \"192.168.1.2:5001\",\n\t\t\t\tState:                   defs.APISRTConnStateRead,\n\t\t\t\tPath:                    \"stream2\",\n\t\t\t\tQuery:                   \"\",\n\t\t\t\tPacketsSent:             500,\n\t\t\t\tPacketsReceived:         1500,\n\t\t\t\tPacketsSentUnique:       480,\n\t\t\t\tPacketsReceivedUnique:   1470,\n\t\t\t\tBytesReceived:           50000,\n\t\t\t\tBytesSent:               150000,\n\t\t\t\tOutboundFramesDiscarded: 6,\n\t\t\t\tMsRTT:                   15.2,\n\t\t\t\tMbpsSendRate:            3.5,\n\t\t\t\tMbpsReceiveRate:         3.2,\n\t\t\t},\n\t\t},\n\t}\n\n\tapi := API{\n\t\tAddress:      \"localhost:9997\",\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tAuthManager:  test.NilAuthManager,\n\t\tSRTServer:    srtServer,\n\t\tParent:       &testParent{},\n\t}\n\terr := api.Initialize()\n\trequire.NoError(t, err)\n\tdefer api.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\tvar out defs.APISRTConnList\n\thttpRequest(t, hc, http.MethodGet, \"http://localhost:9997/v3/srtconns/list\", nil, &out)\n\n\trequire.Equal(t, 2, out.ItemCount)\n\trequire.Equal(t, 1, out.PageCount)\n\trequire.Len(t, out.Items, 2)\n}\n\nfunc TestSRTConnsGet(t *testing.T) {\n\tid := uuid.New()\n\tnow := time.Now()\n\n\tsrtServer := &testSRTServer{\n\t\tconns: map[uuid.UUID]*defs.APISRTConn{\n\t\t\tid: {\n\t\t\t\tID:                            id,\n\t\t\t\tCreated:                       now,\n\t\t\t\tRemoteAddr:                    \"192.168.1.100:5000\",\n\t\t\t\tState:                         defs.APISRTConnStatePublish,\n\t\t\t\tPath:                          \"mystream\",\n\t\t\t\tQuery:                         \"key=value\",\n\t\t\t\tPacketsSent:                   10000,\n\t\t\t\tPacketsReceived:               20000,\n\t\t\t\tPacketsSentUnique:             9900,\n\t\t\t\tPacketsReceivedUnique:         19800,\n\t\t\t\tPacketsSendLoss:               50,\n\t\t\t\tPacketsReceivedLoss:           100,\n\t\t\t\tPacketsRetrans:                60,\n\t\t\t\tPacketsReceivedRetrans:        80,\n\t\t\t\tPacketsSentACK:                500,\n\t\t\t\tPacketsReceivedACK:            600,\n\t\t\t\tPacketsSentNAK:                10,\n\t\t\t\tPacketsReceivedNAK:            15,\n\t\t\t\tPacketsSentKM:                 2,\n\t\t\t\tPacketsReceivedKM:             2,\n\t\t\t\tUsSndDuration:                 1000000,\n\t\t\t\tPacketsReceivedBelated:        5,\n\t\t\t\tPacketsSendDrop:               3,\n\t\t\t\tPacketsReceivedDrop:           4,\n\t\t\t\tPacketsReceivedUndecrypt:      0,\n\t\t\t\tBytesReceived:                 999999,\n\t\t\t\tBytesSent:                     888888,\n\t\t\t\tBytesSentUnique:               880000,\n\t\t\t\tBytesReceivedUnique:           990000,\n\t\t\t\tBytesReceivedLoss:             5000,\n\t\t\t\tBytesRetrans:                  3000,\n\t\t\t\tBytesReceivedRetrans:          4000,\n\t\t\t\tBytesReceivedBelated:          200,\n\t\t\t\tBytesSendDrop:                 150,\n\t\t\t\tBytesReceivedDrop:             180,\n\t\t\t\tBytesReceivedUndecrypt:        0,\n\t\t\t\tUsPacketsSendPeriod:           1000.5,\n\t\t\t\tPacketsFlowWindow:             8192,\n\t\t\t\tPacketsFlightSize:             256,\n\t\t\t\tMsRTT:                         25.5,\n\t\t\t\tMbpsSendRate:                  10.5,\n\t\t\t\tMbpsReceiveRate:               9.8,\n\t\t\t\tMbpsLinkCapacity:              100.0,\n\t\t\t\tBytesAvailSendBuf:             65536,\n\t\t\t\tBytesAvailReceiveBuf:          131072,\n\t\t\t\tMbpsMaxBW:                     50.0,\n\t\t\t\tByteMSS:                       1500,\n\t\t\t\tPacketsSendBuf:                128,\n\t\t\t\tBytesSendBuf:                  192000,\n\t\t\t\tMsSendBuf:                     1000,\n\t\t\t\tMsSendTsbPdDelay:              120,\n\t\t\t\tPacketsReceiveBuf:             256,\n\t\t\t\tBytesReceiveBuf:               384000,\n\t\t\t\tMsReceiveBuf:                  2000,\n\t\t\t\tMsReceiveTsbPdDelay:           120,\n\t\t\t\tPacketsReorderTolerance:       10,\n\t\t\t\tPacketsReceivedAvgBelatedTime: 50,\n\t\t\t\tPacketsSendLossRate:           0.5,\n\t\t\t\tPacketsReceivedLossRate:       0.6,\n\t\t\t\tOutboundFramesDiscarded:       7,\n\t\t\t},\n\t\t},\n\t}\n\n\tapi := API{\n\t\tAddress:      \"localhost:9997\",\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tAuthManager:  test.NilAuthManager,\n\t\tSRTServer:    srtServer,\n\t\tParent:       &testParent{},\n\t}\n\terr := api.Initialize()\n\trequire.NoError(t, err)\n\tdefer api.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\tvar out defs.APISRTConn\n\thttpRequest(t, hc, http.MethodGet, fmt.Sprintf(\"http://localhost:9997/v3/srtconns/get/%s\", id), nil, &out)\n\n\trequire.Equal(t, id, out.ID)\n\trequire.Equal(t, \"192.168.1.100:5000\", out.RemoteAddr)\n\trequire.Equal(t, defs.APISRTConnStatePublish, out.State)\n\trequire.Equal(t, \"mystream\", out.Path)\n\trequire.Equal(t, uint64(999999), out.BytesReceived)\n\trequire.Equal(t, uint64(888888), out.BytesSent)\n\trequire.Equal(t, uint64(7), out.OutboundFramesDiscarded)\n\trequire.Equal(t, 25.5, out.MsRTT)\n\trequire.Equal(t, 10.5, out.MbpsSendRate)\n\trequire.Equal(t, 9.8, out.MbpsReceiveRate)\n}\n\nfunc TestSRTConnsKick(t *testing.T) {\n\tid := uuid.New()\n\tnow := time.Now()\n\n\tsrtServer := &testSRTServer{\n\t\tconns: map[uuid.UUID]*defs.APISRTConn{\n\t\t\tid: {\n\t\t\t\tID:                    id,\n\t\t\t\tCreated:               now,\n\t\t\t\tRemoteAddr:            \"192.168.1.100:5000\",\n\t\t\t\tState:                 defs.APISRTConnStatePublish,\n\t\t\t\tPath:                  \"mystream\",\n\t\t\t\tQuery:                 \"\",\n\t\t\t\tPacketsSent:           1000,\n\t\t\t\tPacketsReceived:       2000,\n\t\t\t\tPacketsSentUnique:     950,\n\t\t\t\tPacketsReceivedUnique: 1950,\n\t\t\t\tBytesReceived:         100000,\n\t\t\t\tBytesSent:             200000,\n\t\t\t\tMsRTT:                 10.5,\n\t\t\t\tMbpsSendRate:          5.2,\n\t\t\t\tMbpsReceiveRate:       4.8,\n\t\t\t},\n\t\t},\n\t}\n\n\tapi := API{\n\t\tAddress:      \"localhost:9997\",\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tAuthManager:  test.NilAuthManager,\n\t\tSRTServer:    srtServer,\n\t\tParent:       &testParent{},\n\t}\n\terr := api.Initialize()\n\trequire.NoError(t, err)\n\tdefer api.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\thttpRequest(t, hc, http.MethodPost, fmt.Sprintf(\"http://localhost:9997/v3/srtconns/kick/%s\", id), nil, nil)\n}\n"
  },
  {
    "path": "internal/api/api_test.go",
    "content": "package api //nolint:revive\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/auth\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype testParent struct {\n\tlog func(_ logger.Level, _ string, _ ...any)\n}\n\nfunc (p testParent) Log(l logger.Level, s string, a ...any) {\n\tif p.log != nil {\n\t\tp.log(l, s, a...)\n\t}\n}\n\nfunc (testParent) APIConfigSet(_ *conf.Conf) {}\n\nfunc tempConf(t *testing.T, cnt string) *conf.Conf {\n\tfi, err := test.CreateTempFile([]byte(cnt))\n\trequire.NoError(t, err)\n\tdefer os.Remove(fi)\n\n\tcnf, _, err := conf.Load(fi, nil, nil)\n\trequire.NoError(t, err)\n\n\treturn cnf\n}\n\nfunc httpRequest(t *testing.T, hc *http.Client, method string, ur string, in any, out any) {\n\tbuf := func() io.Reader {\n\t\tif in == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tbyts, err := json.Marshal(in)\n\t\trequire.NoError(t, err)\n\n\t\treturn bytes.NewBuffer(byts)\n\t}()\n\n\treq, err := http.NewRequest(method, ur, buf)\n\trequire.NoError(t, err)\n\n\tres, err := hc.Do(req)\n\trequire.NoError(t, err)\n\tdefer res.Body.Close()\n\n\tif res.StatusCode != http.StatusOK {\n\t\tt.Errorf(\"bad status code: %d\", res.StatusCode)\n\t}\n\n\tif out == nil {\n\t\tcheckOK(t, res.Body)\n\t\treturn\n\t}\n\n\terr = json.NewDecoder(res.Body).Decode(out)\n\trequire.NoError(t, err)\n}\n\nfunc checkError(t *testing.T, body io.Reader, msg string) {\n\tvar raw map[string]any\n\terr := json.NewDecoder(body).Decode(&raw)\n\trequire.NoError(t, err)\n\trequire.Equal(t, map[string]any{\"status\": \"error\", \"error\": msg}, raw)\n}\n\nfunc checkOK(t *testing.T, body io.Reader) {\n\tvar raw map[string]any\n\terr := json.NewDecoder(body).Decode(&raw)\n\trequire.NoError(t, err)\n\trequire.Equal(t, map[string]any{\"status\": \"ok\"}, raw)\n}\n\nfunc TestPreflightRequest(t *testing.T) {\n\tapi := API{\n\t\tAddress:      \"localhost:9997\",\n\t\tAllowOrigins: []string{\"*\"},\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tAuthManager:  test.NilAuthManager,\n\t\tParent:       &testParent{},\n\t}\n\terr := api.Initialize()\n\trequire.NoError(t, err)\n\tdefer api.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\treq, err := http.NewRequest(http.MethodOptions, \"http://localhost:9997\", nil)\n\trequire.NoError(t, err)\n\n\treq.Header.Add(\"Access-Control-Request-Method\", \"GET\")\n\n\tres, err := hc.Do(req)\n\trequire.NoError(t, err)\n\tdefer res.Body.Close()\n\n\trequire.Equal(t, http.StatusNoContent, res.StatusCode)\n\n\tbyts, err := io.ReadAll(res.Body)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, \"*\", res.Header.Get(\"Access-Control-Allow-Origin\"))\n\trequire.Equal(t, \"true\", res.Header.Get(\"Access-Control-Allow-Credentials\"))\n\trequire.Equal(t, \"OPTIONS, GET, POST, PATCH, DELETE\", res.Header.Get(\"Access-Control-Allow-Methods\"))\n\trequire.Equal(t, \"Authorization, Content-Type\", res.Header.Get(\"Access-Control-Allow-Headers\"))\n\trequire.Equal(t, byts, []byte{})\n}\n\nfunc TestInfo(t *testing.T) {\n\tcnf := tempConf(t, \"api: yes\\n\")\n\n\tapi := API{\n\t\tVersion:      \"v1.2.3\",\n\t\tStarted:      time.Date(2008, 11, 7, 11, 22, 0, 0, time.Local),\n\t\tAddress:      \"localhost:9997\",\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tConf:         cnf,\n\t\tAuthManager:  test.NilAuthManager,\n\t\tParent:       &testParent{},\n\t}\n\terr := api.Initialize()\n\trequire.NoError(t, err)\n\tdefer api.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\tvar out map[string]any\n\thttpRequest(t, hc, http.MethodGet, \"http://localhost:9997/v3/info\", nil, &out)\n\trequire.Equal(t, map[string]any{\n\t\t\"started\": time.Date(2008, 11, 7, 11, 22, 0, 0, time.Local).Format(time.RFC3339),\n\t\t\"version\": \"v1.2.3\",\n\t}, out)\n}\n\nfunc TestAuthJWKSRefresh(t *testing.T) {\n\tok := false\n\n\tapi := API{\n\t\tAddress:      \"localhost:9997\",\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tAuthManager: &test.AuthManager{\n\t\t\tAuthenticateImpl: func(_ *auth.Request) (string, *auth.Error) {\n\t\t\t\treturn \"\", nil\n\t\t\t},\n\t\t\tRefreshJWTJWKSImpl: func() {\n\t\t\t\tok = true\n\t\t\t},\n\t\t},\n\t\tParent: &testParent{},\n\t}\n\terr := api.Initialize()\n\trequire.NoError(t, err)\n\tdefer api.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\tu, err := url.Parse(\"http://localhost:9997/v3/auth/jwks/refresh\")\n\trequire.NoError(t, err)\n\n\thttpRequest(t, hc, http.MethodPost, u.String(), nil, nil)\n\n\trequire.True(t, ok)\n}\n\nfunc TestAuthError(t *testing.T) {\n\tcnf := tempConf(t, \"api: yes\\n\")\n\tn := 0\n\n\tapi := API{\n\t\tAddress:      \"localhost:9997\",\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tConf:         cnf,\n\t\tAuthManager: &test.AuthManager{\n\t\t\tAuthenticateImpl: func(req *auth.Request) (string, *auth.Error) {\n\t\t\t\tif req.Credentials.User == \"\" {\n\t\t\t\t\treturn \"\", &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf(\"auth error\")}\n\t\t\t\t}\n\t\t\t\treturn \"\", &auth.Error{Wrapped: fmt.Errorf(\"auth error\")}\n\t\t\t},\n\t\t},\n\t\tParent: &testParent{\n\t\t\tlog: func(l logger.Level, s string, i ...any) {\n\t\t\t\tif l == logger.Info {\n\t\t\t\t\tif n == 1 {\n\t\t\t\t\t\trequire.Regexp(t, \"failed to authenticate: auth error$\", fmt.Sprintf(s, i...))\n\t\t\t\t\t}\n\t\t\t\t\tn++\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t}\n\terr := api.Initialize()\n\trequire.NoError(t, err)\n\tdefer api.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\tres, err := hc.Get(\"http://localhost:9997/v3/config/global/get\")\n\trequire.NoError(t, err)\n\tdefer res.Body.Close()\n\n\trequire.Equal(t, http.StatusUnauthorized, res.StatusCode)\n\trequire.Equal(t, `Basic realm=\"mediamtx\"`, res.Header.Get(\"WWW-Authenticate\"))\n\tcheckError(t, res.Body, \"authentication error\")\n\n\tres, err = hc.Get(\"http://myuser:mypass@localhost:9997/v3/config/global/get\")\n\trequire.NoError(t, err)\n\tdefer res.Body.Close()\n\n\trequire.Equal(t, http.StatusUnauthorized, res.StatusCode)\n\trequire.Equal(t, ``, res.Header.Get(\"WWW-Authenticate\"))\n\tcheckError(t, res.Body, \"authentication error\")\n\n\trequire.Equal(t, 2, n)\n}\n"
  },
  {
    "path": "internal/api/api_webrtc.go",
    "content": "//nolint:dupl\npackage api //nolint:revive\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\n\t\"github.com/bluenviron/mediamtx/internal/servers/webrtc\"\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/google/uuid\"\n)\n\nfunc (a *API) onWebRTCSessionsList(ctx *gin.Context) {\n\tdata, err := a.WebRTCServer.APISessionsList()\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusInternalServerError, err)\n\t\treturn\n\t}\n\n\tdata.ItemCount = len(data.Items)\n\tpageCount, err := paginate(&data.Items, ctx.Query(\"itemsPerPage\"), ctx.Query(\"page\"))\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\tdata.PageCount = pageCount\n\n\tctx.JSON(http.StatusOK, data)\n}\n\nfunc (a *API) onWebRTCSessionsGet(ctx *gin.Context) {\n\tuuid, err := uuid.Parse(ctx.Param(\"id\"))\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\tdata, err := a.WebRTCServer.APISessionsGet(uuid)\n\tif err != nil {\n\t\tif errors.Is(err, webrtc.ErrSessionNotFound) {\n\t\t\ta.writeError(ctx, http.StatusNotFound, err)\n\t\t} else {\n\t\t\ta.writeError(ctx, http.StatusInternalServerError, err)\n\t\t}\n\t\treturn\n\t}\n\n\tctx.JSON(http.StatusOK, data)\n}\n\nfunc (a *API) onWebRTCSessionsKick(ctx *gin.Context) {\n\tuuid, err := uuid.Parse(ctx.Param(\"id\"))\n\tif err != nil {\n\t\ta.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\terr = a.WebRTCServer.APISessionsKick(uuid)\n\tif err != nil {\n\t\tif errors.Is(err, webrtc.ErrSessionNotFound) {\n\t\t\ta.writeError(ctx, http.StatusNotFound, err)\n\t\t} else {\n\t\t\ta.writeError(ctx, http.StatusInternalServerError, err)\n\t\t}\n\t\treturn\n\t}\n\n\ta.writeOK(ctx)\n}\n"
  },
  {
    "path": "internal/api/api_webrtc_test.go",
    "content": "package api //nolint:revive\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/servers/webrtc\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/google/uuid\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype testWebRTCServer struct {\n\tsessions map[uuid.UUID]*defs.APIWebRTCSession\n}\n\nfunc (s *testWebRTCServer) APISessionsList() (*defs.APIWebRTCSessionList, error) {\n\titems := make([]defs.APIWebRTCSession, 0, len(s.sessions))\n\tfor _, session := range s.sessions {\n\t\titems = append(items, *session)\n\t}\n\treturn &defs.APIWebRTCSessionList{Items: items}, nil\n}\n\nfunc (s *testWebRTCServer) APISessionsGet(id uuid.UUID) (*defs.APIWebRTCSession, error) {\n\tsession, ok := s.sessions[id]\n\tif !ok {\n\t\treturn nil, webrtc.ErrSessionNotFound\n\t}\n\treturn session, nil\n}\n\nfunc (s *testWebRTCServer) APISessionsKick(id uuid.UUID) error {\n\t_, ok := s.sessions[id]\n\tif !ok {\n\t\treturn webrtc.ErrSessionNotFound\n\t}\n\treturn nil\n}\n\nfunc TestWebRTCSessionsList(t *testing.T) {\n\tid1 := uuid.New()\n\tid2 := uuid.New()\n\tnow := time.Now()\n\n\twebrtcServer := &testWebRTCServer{\n\t\tsessions: map[uuid.UUID]*defs.APIWebRTCSession{\n\t\t\tid1: {\n\t\t\t\tID:                        id1,\n\t\t\t\tCreated:                   now,\n\t\t\t\tRemoteAddr:                \"192.168.1.1:5000\",\n\t\t\t\tPeerConnectionEstablished: true,\n\t\t\t\tLocalCandidate:            \"192.168.1.100:8000\",\n\t\t\t\tRemoteCandidate:           \"192.168.1.1:5000\",\n\t\t\t\tState:                     defs.APIWebRTCSessionStatePublish,\n\t\t\t\tPath:                      \"stream1\",\n\t\t\t\tQuery:                     \"token=abc\",\n\t\t\t\tInboundBytes:              1000,\n\t\t\t\tInboundRTPPackets:         100,\n\t\t\t\tInboundRTPPacketsLost:     5,\n\t\t\t\tInboundRTPPacketsJitter:   0.5,\n\t\t\t\tInboundRTCPPackets:        10,\n\t\t\t\tOutboundBytes:             2000,\n\t\t\t\tOutboundRTPPackets:        200,\n\t\t\t\tOutboundRTCPPackets:       15,\n\t\t\t\tOutboundFramesDiscarded:   11,\n\t\t\t\tBytesReceived:             1000,\n\t\t\t\tBytesSent:                 2000,\n\t\t\t\tRTPPacketsReceived:        100,\n\t\t\t\tRTPPacketsSent:            200,\n\t\t\t\tRTPPacketsLost:            5,\n\t\t\t\tRTPPacketsJitter:          0.5,\n\t\t\t\tRTCPPacketsReceived:       10,\n\t\t\t\tRTCPPacketsSent:           15,\n\t\t\t},\n\t\t\tid2: {\n\t\t\t\tID:                        id2,\n\t\t\t\tCreated:                   now.Add(time.Minute),\n\t\t\t\tRemoteAddr:                \"192.168.1.2:5001\",\n\t\t\t\tPeerConnectionEstablished: true,\n\t\t\t\tLocalCandidate:            \"192.168.1.100:8001\",\n\t\t\t\tRemoteCandidate:           \"192.168.1.2:5001\",\n\t\t\t\tState:                     defs.APIWebRTCSessionStateRead,\n\t\t\t\tPath:                      \"stream2\",\n\t\t\t\tQuery:                     \"\",\n\t\t\t\tInboundBytes:              500,\n\t\t\t\tInboundRTPPackets:         50,\n\t\t\t\tInboundRTPPacketsLost:     0,\n\t\t\t\tInboundRTPPacketsJitter:   0.1,\n\t\t\t\tInboundRTCPPackets:        5,\n\t\t\t\tOutboundBytes:             1500,\n\t\t\t\tOutboundRTPPackets:        150,\n\t\t\t\tOutboundRTCPPackets:       10,\n\t\t\t\tOutboundFramesDiscarded:   22,\n\t\t\t\tBytesReceived:             500,\n\t\t\t\tBytesSent:                 1500,\n\t\t\t\tRTPPacketsReceived:        50,\n\t\t\t\tRTPPacketsSent:            150,\n\t\t\t\tRTPPacketsLost:            0,\n\t\t\t\tRTPPacketsJitter:          0.1,\n\t\t\t\tRTCPPacketsReceived:       5,\n\t\t\t\tRTCPPacketsSent:           10,\n\t\t\t},\n\t\t},\n\t}\n\n\tapi := API{\n\t\tAddress:      \"localhost:9997\",\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tAuthManager:  test.NilAuthManager,\n\t\tWebRTCServer: webrtcServer,\n\t\tParent:       &testParent{},\n\t}\n\terr := api.Initialize()\n\trequire.NoError(t, err)\n\tdefer api.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\tvar out defs.APIWebRTCSessionList\n\thttpRequest(t, hc, http.MethodGet, \"http://localhost:9997/v3/webrtcsessions/list\", nil, &out)\n\n\trequire.Equal(t, 2, out.ItemCount)\n\trequire.Equal(t, 1, out.PageCount)\n\trequire.Len(t, out.Items, 2)\n}\n\nfunc TestWebRTCSessionsGet(t *testing.T) {\n\tid := uuid.New()\n\tnow := time.Now()\n\n\twebrtcServer := &testWebRTCServer{\n\t\tsessions: map[uuid.UUID]*defs.APIWebRTCSession{\n\t\t\tid: {\n\t\t\t\tID:                        id,\n\t\t\t\tCreated:                   now,\n\t\t\t\tRemoteAddr:                \"192.168.1.100:5000\",\n\t\t\t\tPeerConnectionEstablished: true,\n\t\t\t\tLocalCandidate:            \"192.168.1.200:8000\",\n\t\t\t\tRemoteCandidate:           \"192.168.1.100:5000\",\n\t\t\t\tState:                     defs.APIWebRTCSessionStatePublish,\n\t\t\t\tPath:                      \"mystream\",\n\t\t\t\tQuery:                     \"key=value\",\n\t\t\t\tInboundBytes:              999999,\n\t\t\t\tInboundRTPPackets:         10000,\n\t\t\t\tInboundRTPPacketsLost:     50,\n\t\t\t\tInboundRTPPacketsJitter:   1.5,\n\t\t\t\tInboundRTCPPackets:        100,\n\t\t\t\tOutboundBytes:             888888,\n\t\t\t\tOutboundRTPPackets:        20000,\n\t\t\t\tOutboundRTCPPackets:       200,\n\t\t\t\tOutboundFramesDiscarded:   33,\n\t\t\t\tBytesReceived:             999999,\n\t\t\t\tBytesSent:                 888888,\n\t\t\t\tRTPPacketsReceived:        10000,\n\t\t\t\tRTPPacketsSent:            20000,\n\t\t\t\tRTPPacketsLost:            50,\n\t\t\t\tRTPPacketsJitter:          1.5,\n\t\t\t\tRTCPPacketsReceived:       100,\n\t\t\t\tRTCPPacketsSent:           200,\n\t\t\t},\n\t\t},\n\t}\n\n\tapi := API{\n\t\tAddress:      \"localhost:9997\",\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tAuthManager:  test.NilAuthManager,\n\t\tWebRTCServer: webrtcServer,\n\t\tParent:       &testParent{},\n\t}\n\terr := api.Initialize()\n\trequire.NoError(t, err)\n\tdefer api.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\tvar out defs.APIWebRTCSession\n\thttpRequest(t, hc, http.MethodGet, fmt.Sprintf(\"http://localhost:9997/v3/webrtcsessions/get/%s\", id), nil, &out)\n\n\trequire.Equal(t, id, out.ID)\n\trequire.Equal(t, \"192.168.1.100:5000\", out.RemoteAddr)\n\trequire.Equal(t, defs.APIWebRTCSessionStatePublish, out.State)\n\trequire.Equal(t, \"mystream\", out.Path)\n\trequire.True(t, out.PeerConnectionEstablished)\n\trequire.Equal(t, \"192.168.1.200:8000\", out.LocalCandidate)\n\trequire.Equal(t, \"192.168.1.100:5000\", out.RemoteCandidate)\n\trequire.Equal(t, uint64(999999), out.InboundBytes)\n\trequire.Equal(t, uint64(888888), out.OutboundBytes)\n\trequire.Equal(t, uint64(10000), out.InboundRTPPackets)\n\trequire.Equal(t, uint64(20000), out.OutboundRTPPackets)\n\trequire.Equal(t, uint64(33), out.OutboundFramesDiscarded)\n\trequire.Equal(t, uint64(999999), out.BytesReceived)\n\trequire.Equal(t, uint64(888888), out.BytesSent)\n\trequire.Equal(t, uint64(10000), out.RTPPacketsReceived)\n\trequire.Equal(t, uint64(20000), out.RTPPacketsSent)\n\trequire.Equal(t, uint64(50), out.RTPPacketsLost)\n\trequire.Equal(t, 1.5, out.RTPPacketsJitter)\n}\n\nfunc TestWebRTCSessionsKick(t *testing.T) {\n\tid := uuid.New()\n\tnow := time.Now()\n\n\twebrtcServer := &testWebRTCServer{\n\t\tsessions: map[uuid.UUID]*defs.APIWebRTCSession{\n\t\t\tid: {\n\t\t\t\tID:                        id,\n\t\t\t\tCreated:                   now,\n\t\t\t\tRemoteAddr:                \"192.168.1.100:5000\",\n\t\t\t\tPeerConnectionEstablished: true,\n\t\t\t\tLocalCandidate:            \"192.168.1.200:8000\",\n\t\t\t\tRemoteCandidate:           \"192.168.1.100:5000\",\n\t\t\t\tState:                     defs.APIWebRTCSessionStatePublish,\n\t\t\t\tPath:                      \"mystream\",\n\t\t\t\tQuery:                     \"\",\n\t\t\t\tInboundBytes:              1000,\n\t\t\t\tInboundRTPPackets:         100,\n\t\t\t\tInboundRTPPacketsLost:     0,\n\t\t\t\tInboundRTPPacketsJitter:   0.5,\n\t\t\t\tInboundRTCPPackets:        10,\n\t\t\t\tOutboundBytes:             2000,\n\t\t\t\tOutboundRTPPackets:        200,\n\t\t\t\tOutboundRTCPPackets:       15,\n\t\t\t\tBytesReceived:             1000,\n\t\t\t\tBytesSent:                 2000,\n\t\t\t\tRTPPacketsReceived:        100,\n\t\t\t\tRTPPacketsSent:            200,\n\t\t\t\tRTPPacketsLost:            0,\n\t\t\t\tRTPPacketsJitter:          0.5,\n\t\t\t\tRTCPPacketsReceived:       10,\n\t\t\t\tRTCPPacketsSent:           15,\n\t\t\t},\n\t\t},\n\t}\n\n\tapi := API{\n\t\tAddress:      \"localhost:9997\",\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tAuthManager:  test.NilAuthManager,\n\t\tWebRTCServer: webrtcServer,\n\t\tParent:       &testParent{},\n\t}\n\terr := api.Initialize()\n\trequire.NoError(t, err)\n\tdefer api.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\thttpRequest(t, hc, http.MethodPost, fmt.Sprintf(\"http://localhost:9997/v3/webrtcsessions/kick/%s\", id), nil, nil)\n}\n"
  },
  {
    "path": "internal/api/paginate.go",
    "content": "package api //nolint:revive\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n\t\"strconv\"\n)\n\nfunc paginate2(itemsPtr any, itemsPerPage int, page int) int {\n\tritems := reflect.ValueOf(itemsPtr).Elem()\n\n\titemsLen := ritems.Len()\n\tif itemsLen == 0 {\n\t\treturn 0\n\t}\n\n\tpageCount := (itemsLen / itemsPerPage)\n\tif (itemsLen % itemsPerPage) != 0 {\n\t\tpageCount++\n\t}\n\n\tminVal := min(page*itemsPerPage, itemsLen)\n\n\tmaxVal := min((page+1)*itemsPerPage, itemsLen)\n\n\tritems.Set(ritems.Slice(minVal, maxVal))\n\n\treturn pageCount\n}\n\nfunc paginate(itemsPtr any, itemsPerPageStr string, pageStr string) (int, error) {\n\titemsPerPage := 100\n\n\tif itemsPerPageStr != \"\" {\n\t\ttmp, err := strconv.ParseUint(itemsPerPageStr, 10, 31)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\titemsPerPage = int(tmp)\n\n\t\tif itemsPerPage == 0 {\n\t\t\treturn 0, fmt.Errorf(\"invalid items per page\")\n\t\t}\n\t}\n\n\tpage := 0\n\n\tif pageStr != \"\" {\n\t\ttmp, err := strconv.ParseUint(pageStr, 10, 31)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\tpage = int(tmp)\n\t}\n\n\treturn paginate2(itemsPtr, itemsPerPage, page), nil\n}\n"
  },
  {
    "path": "internal/api/paginate_test.go",
    "content": "package api //nolint:revive\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPaginate(t *testing.T) {\n\tfunc() {\n\t\titems := make([]int, 5)\n\t\tfor i := range 5 {\n\t\t\titems[i] = i\n\t\t}\n\n\t\tpageCount, err := paginate(&items, \"1\", \"1\")\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, 5, pageCount)\n\t\trequire.Equal(t, []int{1}, items)\n\t}()\n\n\tfunc() {\n\t\titems := make([]int, 5)\n\t\tfor i := range 5 {\n\t\t\titems[i] = i\n\t\t}\n\n\t\tpageCount, err := paginate(&items, \"3\", \"2\")\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, 2, pageCount)\n\t\trequire.Equal(t, []int{}, items)\n\t}()\n\n\tfunc() {\n\t\titems := make([]int, 6)\n\t\tfor i := range 6 {\n\t\t\titems[i] = i\n\t\t}\n\n\t\tpageCount, err := paginate(&items, \"4\", \"1\")\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, 2, pageCount)\n\t\trequire.Equal(t, []int{4, 5}, items)\n\t}()\n\n\tfunc() {\n\t\titems := make([]int, 0)\n\n\t\tpageCount, err := paginate(&items, \"1\", \"0\")\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, 0, pageCount)\n\t\trequire.Equal(t, []int{}, items)\n\t}()\n}\n\nfunc FuzzPaginate(f *testing.F) {\n\tf.Fuzz(func(_ *testing.T, str1 string, str2 string) {\n\t\titems := make([]int, 6)\n\t\tfor i := range 6 {\n\t\t\titems[i] = i\n\t\t}\n\n\t\tpaginate(&items, str1, str2) //nolint:errcheck\n\t})\n}\n"
  },
  {
    "path": "internal/api/testdata/fuzz/FuzzPaginate/23731da0f18d31d0",
    "content": "go test fuzz v1\nstring(\"A\")\nstring(\"0\")\n"
  },
  {
    "path": "internal/api/testdata/fuzz/FuzzPaginate/34523a772174e26e",
    "content": "go test fuzz v1\nstring(\"1\")\nstring(\"A\")\n"
  },
  {
    "path": "internal/api/testdata/fuzz/FuzzPaginate/85649d45641911d0",
    "content": "go test fuzz v1\nstring(\"0\")\nstring(\"\")\n"
  },
  {
    "path": "internal/auth/credentials.go",
    "content": "package auth\n\n// Credentials is a set of credentials (either user+pass or a token).\ntype Credentials struct {\n\tUser  string\n\tPass  string\n\tToken string\n}\n"
  },
  {
    "path": "internal/auth/error.go",
    "content": "package auth\n\n// Error is an authentication error.\ntype Error struct {\n\tWrapped        error\n\tAskCredentials bool\n}\n\n// Error implements the error interface.\nfunc (e Error) Error() string {\n\treturn \"authentication failed: \" + e.Wrapped.Error()\n}\n"
  },
  {
    "path": "internal/auth/jwt_claims.go",
    "content": "package auth\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper\"\n\t\"github.com/golang-jwt/jwt/v5\"\n)\n\ntype jwtClaims struct {\n\tjwt.RegisteredClaims\n\tpermissionsKey string\n\tpermissions    []conf.AuthInternalUserPermission\n}\n\nfunc (c *jwtClaims) UnmarshalJSON(b []byte) error {\n\terr := json.Unmarshal(b, &c.RegisteredClaims)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar claimMap map[string]json.RawMessage\n\terr = json.Unmarshal(b, &claimMap)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\trawPermissions, ok := claimMap[c.permissionsKey]\n\tif !ok {\n\t\treturn fmt.Errorf(\"claim '%s' not found inside JWT\", c.permissionsKey)\n\t}\n\n\terr = jsonwrapper.Unmarshal(rawPermissions, &c.permissions)\n\tif err != nil {\n\t\tvar str string\n\t\terr = json.Unmarshal(rawPermissions, &str)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\terr = jsonwrapper.Unmarshal([]byte(str), &c.permissions)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/auth/manager.go",
    "content": "// Package auth contains the authentication system.\npackage auth\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/MicahParks/keyfunc/v3\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/tls\"\n\t\"github.com/golang-jwt/jwt/v5\"\n\t\"github.com/google/uuid\"\n)\n\nconst (\n\t// PauseAfterError is the pause to apply after an authentication failure.\n\tPauseAfterError = 2 * time.Second\n\n\tjwksRefreshPeriod = 60 * 60 * time.Second\n)\n\nfunc isHTTP(req *Request) bool {\n\treturn req.Protocol == ProtocolHLS || req.Protocol == ProtocolWebRTC ||\n\t\treq.Action == conf.AuthActionPlayback ||\n\t\treq.Action == conf.AuthActionAPI ||\n\t\treq.Action == conf.AuthActionMetrics ||\n\t\treq.Action == conf.AuthActionPprof\n}\n\nfunc matchesPermission(perms []conf.AuthInternalUserPermission, req *Request) bool {\n\tfor _, perm := range perms {\n\t\tif perm.Action == req.Action {\n\t\t\tif perm.Action == conf.AuthActionPublish ||\n\t\t\t\tperm.Action == conf.AuthActionRead ||\n\t\t\t\tperm.Action == conf.AuthActionPlayback {\n\t\t\t\tswitch {\n\t\t\t\tcase perm.Path == \"\":\n\t\t\t\t\treturn true\n\n\t\t\t\tcase strings.HasPrefix(perm.Path, \"~\"):\n\t\t\t\t\tregexp, err := regexp.Compile(perm.Path[1:])\n\t\t\t\t\tif err == nil && regexp.MatchString(req.Path) {\n\t\t\t\t\t\treturn true\n\t\t\t\t\t}\n\n\t\t\t\tcase perm.Path == req.Path:\n\t\t\t\t\treturn true\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\n\treturn false\n}\n\n// Manager is the authentication manager.\ntype Manager struct {\n\tMethod             conf.AuthMethod\n\tInternalUsers      []conf.AuthInternalUser\n\tHTTPAddress        string\n\tHTTPFingerprint    string\n\tHTTPExclude        []conf.AuthInternalUserPermission\n\tJWTJWKS            string\n\tJWTJWKSFingerprint string\n\tJWTClaimKey        string\n\tJWTExclude         []conf.AuthInternalUserPermission\n\tJWTInHTTPQuery     bool\n\tJWTIssuer          string\n\tJWTAudience        string\n\tReadTimeout        time.Duration\n\n\tmutex           sync.RWMutex\n\tjwksLastRefresh time.Time\n\tjwtKeyFunc      keyfunc.Keyfunc\n}\n\n// ReloadInternalUsers reloads InternalUsers.\nfunc (m *Manager) ReloadInternalUsers(u []conf.AuthInternalUser) {\n\tm.mutex.Lock()\n\tdefer m.mutex.Unlock()\n\tm.InternalUsers = u\n}\n\n// Authenticate authenticates a request.\n// It returns the user name.\nfunc (m *Manager) Authenticate(req *Request) (string, *Error) {\n\tvar user string\n\tvar err error\n\n\tswitch m.Method {\n\tcase conf.AuthMethodInternal:\n\t\tuser, err = m.authenticateInternal(req)\n\n\tcase conf.AuthMethodHTTP:\n\t\tuser, err = m.authenticateHTTP(req)\n\n\tdefault:\n\t\tuser, err = m.authenticateJWT(req)\n\t}\n\n\tif err != nil {\n\t\treturn \"\", &Error{\n\t\t\tWrapped:        err,\n\t\t\tAskCredentials: (req.Credentials.User == \"\" && req.Credentials.Pass == \"\" && req.Credentials.Token == \"\"),\n\t\t}\n\t}\n\n\treturn user, nil\n}\n\nfunc (m *Manager) authenticateInternal(req *Request) (string, error) {\n\tm.mutex.RLock()\n\tdefer m.mutex.RUnlock()\n\n\tfor _, u := range m.InternalUsers {\n\t\tif ok := m.authenticateWithUser(req, &u); ok {\n\t\t\treturn req.Credentials.User, nil\n\t\t}\n\t}\n\n\treturn \"\", fmt.Errorf(\"authentication failed\")\n}\n\nfunc (m *Manager) authenticateWithUser(\n\treq *Request,\n\tu *conf.AuthInternalUser,\n) bool {\n\tif len(u.IPs) != 0 && !u.IPs.Contains(req.IP) {\n\t\treturn false\n\t}\n\n\tif !matchesPermission(u.Permissions, req) {\n\t\treturn false\n\t}\n\n\tif u.User != \"any\" {\n\t\tif req.CustomVerifyFunc != nil {\n\t\t\tif ok := req.CustomVerifyFunc(string(u.User), string(u.Pass)); !ok {\n\t\t\t\treturn false\n\t\t\t}\n\t\t} else {\n\t\t\tif !u.User.Check(req.Credentials.User) || !u.Pass.Check(req.Credentials.Pass) {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\t}\n\n\treturn true\n}\n\nfunc (m *Manager) authenticateHTTP(req *Request) (string, error) {\n\tif matchesPermission(m.HTTPExclude, req) {\n\t\treturn \"\", nil\n\t}\n\n\tenc, _ := json.Marshal(struct {\n\t\tIP       string     `json:\"ip\"`\n\t\tUser     string     `json:\"user\"`\n\t\tPassword string     `json:\"password\"`\n\t\tToken    string     `json:\"token\"`\n\t\tAction   string     `json:\"action\"`\n\t\tPath     string     `json:\"path\"`\n\t\tProtocol string     `json:\"protocol\"`\n\t\tID       *uuid.UUID `json:\"id\"`\n\t\tQuery    string     `json:\"query\"`\n\t}{\n\t\tIP:       req.IP.String(),\n\t\tUser:     req.Credentials.User,\n\t\tPassword: req.Credentials.Pass,\n\t\tToken:    req.Credentials.Token,\n\t\tAction:   string(req.Action),\n\t\tPath:     req.Path,\n\t\tProtocol: string(req.Protocol),\n\t\tID:       req.ID,\n\t\tQuery:    req.Query,\n\t})\n\n\tu, err := url.Parse(m.HTTPAddress)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\ttr := &http.Transport{\n\t\tTLSClientConfig: tls.MakeConfig(u.Hostname(), m.HTTPFingerprint),\n\t}\n\tdefer tr.CloseIdleConnections()\n\n\thttpClient := &http.Client{\n\t\tTimeout:   m.ReadTimeout,\n\t\tTransport: tr,\n\t}\n\n\tres, err := httpClient.Post(m.HTTPAddress, \"application/json\", bytes.NewReader(enc))\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"HTTP request failed: %w\", err)\n\t}\n\tdefer res.Body.Close()\n\n\tif res.StatusCode < 200 || res.StatusCode > 299 {\n\t\tif resBody, err2 := io.ReadAll(res.Body); err2 == nil && len(resBody) != 0 {\n\t\t\treturn \"\", fmt.Errorf(\"server replied with code %d: %s\", res.StatusCode, string(resBody))\n\t\t}\n\n\t\treturn \"\", fmt.Errorf(\"server replied with code %d\", res.StatusCode)\n\t}\n\n\treturn req.Credentials.User, nil\n}\n\nfunc (m *Manager) authenticateJWT(req *Request) (string, error) {\n\tif matchesPermission(m.JWTExclude, req) {\n\t\treturn \"\", nil\n\t}\n\n\tkeyfunc, err := m.pullJWTJWKS()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar encodedJWT string\n\n\tswitch {\n\tcase req.Credentials.Token != \"\":\n\t\tencodedJWT = req.Credentials.Token\n\n\tcase req.Credentials.Pass != \"\":\n\t\tencodedJWT = req.Credentials.Pass\n\n\t\t// always allow passing JWT through query parameters with RTSP and RTMP since there's no alternative.\n\tcase req.Protocol == ProtocolRTSP || req.Protocol == ProtocolRTMP || (isHTTP(req) && m.JWTInHTTPQuery):\n\t\tvar v url.Values\n\t\tv, err = url.ParseQuery(req.Query)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tif len(v[\"jwt\"]) != 1 || len(v[\"jwt\"][0]) == 0 {\n\t\t\treturn \"\", fmt.Errorf(\"JWT not provided\")\n\t\t}\n\n\t\tencodedJWT = v[\"jwt\"][0]\n\n\tdefault:\n\t\treturn \"\", fmt.Errorf(\"JWT not provided\")\n\t}\n\n\tvar opts []jwt.ParserOption\n\tif m.JWTIssuer != \"\" {\n\t\topts = append(opts, jwt.WithIssuer(m.JWTIssuer))\n\t}\n\tif m.JWTAudience != \"\" {\n\t\topts = append(opts, jwt.WithAudience(m.JWTAudience))\n\t}\n\n\tvar cc jwtClaims\n\tcc.permissionsKey = m.JWTClaimKey\n\t_, err = jwt.ParseWithClaims(encodedJWT, &cc, keyfunc, opts...)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif !matchesPermission(cc.permissions, req) {\n\t\treturn \"\", fmt.Errorf(\"user doesn't have permission to perform action\")\n\t}\n\n\treturn cc.Subject, nil\n}\n\nfunc (m *Manager) pullJWTJWKS() (jwt.Keyfunc, error) {\n\tnow := time.Now()\n\n\tm.mutex.Lock()\n\tdefer m.mutex.Unlock()\n\n\tif now.Sub(m.jwksLastRefresh) >= jwksRefreshPeriod {\n\t\tu, err := url.Parse(m.JWTJWKS)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\ttr := &http.Transport{\n\t\t\tTLSClientConfig: tls.MakeConfig(u.Hostname(), m.JWTJWKSFingerprint),\n\t\t}\n\t\tdefer tr.CloseIdleConnections()\n\n\t\thttpClient := &http.Client{\n\t\t\tTimeout:   (m.ReadTimeout),\n\t\t\tTransport: tr,\n\t\t}\n\n\t\tres, err := httpClient.Get(m.JWTJWKS)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tdefer res.Body.Close()\n\n\t\tvar raw json.RawMessage\n\t\terr = json.NewDecoder(res.Body).Decode(&raw)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\ttmp, err := keyfunc.NewJWKSetJSON(raw)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tm.jwtKeyFunc = tmp\n\t\tm.jwksLastRefresh = now\n\t}\n\n\treturn m.jwtKeyFunc.Keyfunc, nil\n}\n\n// RefreshJWTJWKS refreshes the JWT JWKS.\nfunc (m *Manager) RefreshJWTJWKS() {\n\tm.mutex.Lock()\n\tdefer m.mutex.Unlock()\n\n\tm.jwksLastRefresh = time.Time{}\n}\n"
  },
  {
    "path": "internal/auth/manager_test.go",
    "content": "package auth\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"net\"\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/MicahParks/jwkset\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/golang-jwt/jwt/v5\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar testTLSCertPub = []byte(`-----BEGIN CERTIFICATE-----\nMIIDazCCAlOgAwIBAgIUXw1hEC3LFpTsllv7D3ARJyEq7sIwDQYJKoZIhvcNAQEL\nBQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM\nGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDEyMTMxNzQ0NThaFw0zMDEy\nMTExNzQ0NThaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw\nHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB\nAQUAA4IBDwAwggEKAoIBAQDG8DyyS51810GsGwgWr5rjJK7OE1kTTLSNEEKax8Bj\nzOyiaz8rA2JGl2VUEpi2UjDr9Cm7nd+YIEVs91IIBOb7LGqObBh1kGF3u5aZxLkv\nNJE+HrLVvUhaDobK2NU+Wibqc/EI3DfUkt1rSINvv9flwTFu1qHeuLWhoySzDKEp\nOzYxpFhwjVSokZIjT4Red3OtFz7gl2E6OAWe2qoh5CwLYVdMWtKR0Xuw3BkDPk9I\nqkQKx3fqv97LPEzhyZYjDT5WvGrgZ1WDAN3booxXF3oA1H3GHQc4m/vcLatOtb8e\nnI59gMQLEbnp08cl873bAuNuM95EZieXTHNbwUnq5iybAgMBAAGjUzBRMB0GA1Ud\nDgQWBBQBKhJh8eWu0a4au9X/2fKhkFX2vjAfBgNVHSMEGDAWgBQBKhJh8eWu0a4a\nu9X/2fKhkFX2vjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBj\n3aCW0YPKukYgVK9cwN0IbVy/D0C1UPT4nupJcy/E0iC7MXPZ9D/SZxYQoAkdptdO\nxfI+RXkpQZLdODNx9uvV+cHyZHZyjtE5ENu/i5Rer2cWI/mSLZm5lUQyx+0KZ2Yu\ntEI1bsebDK30msa8QSTn0WidW9XhFnl3gRi4wRdimcQapOWYVs7ih+nAlSvng7NI\nXpAyRs8PIEbpDDBMWnldrX4TP6EWYUi49gCp8OUDRREKX3l6Ls1vZ02F34yHIt/7\n7IV/XSKG096bhW+icKBWV0IpcEsgTzPK1J1hMxgjhzIMxGboAeUU+kidthOob6Sd\nXQxaORfgM//NzX9LhUPk\n-----END CERTIFICATE-----\n`)\n\nvar testTLSCertKey = []byte(`-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAxvA8skudfNdBrBsIFq+a4ySuzhNZE0y0jRBCmsfAY8zsoms/\nKwNiRpdlVBKYtlIw6/Qpu53fmCBFbPdSCATm+yxqjmwYdZBhd7uWmcS5LzSRPh6y\n1b1IWg6GytjVPlom6nPxCNw31JLda0iDb7/X5cExbtah3ri1oaMkswyhKTs2MaRY\ncI1UqJGSI0+EXndzrRc+4JdhOjgFntqqIeQsC2FXTFrSkdF7sNwZAz5PSKpECsd3\n6r/eyzxM4cmWIw0+Vrxq4GdVgwDd26KMVxd6ANR9xh0HOJv73C2rTrW/HpyOfYDE\nCxG56dPHJfO92wLjbjPeRGYnl0xzW8FJ6uYsmwIDAQABAoIBACi0BKcyQ3HElSJC\nkaAao+Uvnzh4yvPg8Nwf5JDIp/uDdTMyIEWLtrLczRWrjGVZYbsVROinP5VfnPTT\nkYwkfKINj2u+gC6lsNuPnRuvHXikF8eO/mYvCTur1zZvsQnF5kp4GGwIqr+qoPUP\nbB0UMndG1PdpoMryHe+JcrvTrLHDmCeH10TqOwMsQMLHYLkowvxwJWsmTY7/Qr5S\nWm3PPpOcW2i0uyPVuyuv4yD1368fqnqJ8QFsQp1K6QtYsNnJ71Hut1/IoxK/e6hj\n5Z+byKtHVtmcLnABuoOT7BhleJNFBksX9sh83jid4tMBgci+zXNeGmgqo2EmaWAb\nagQslkECgYEA8B1rzjOHVQx/vwSzDa4XOrpoHQRfyElrGNz9JVBvnoC7AorezBXQ\nM9WTHQIFTGMjzD8pb+YJGi3gj93VN51r0SmJRxBaBRh1ZZI9kFiFzngYev8POgD3\nygmlS3kTHCNxCK/CJkB+/jMBgtPj5ygDpCWVcTSuWlQFphePkW7jaaECgYEA1Blz\nulqgAyJHZaqgcbcCsI2q6m527hVr9pjzNjIVmkwu38yS9RTCgdlbEVVDnS0hoifl\n+jVMEGXjF3xjyMvL50BKbQUH+KAa+V4n1WGlnZOxX9TMny8MBjEuSX2+362vQ3BX\n4vOlX00gvoc+sY+lrzvfx/OdPCHQGVYzoKCxhLsCgYA07HcviuIAV/HsO2/vyvhp\nxF5gTu+BqNUHNOZDDDid+ge+Jre2yfQLCL8VPLXIQW3Jff53IH/PGl+NtjphuLvj\n7UDJvgvpZZuymIojP6+2c3gJ3CASC9aR3JBnUzdoE1O9s2eaoMqc4scpe+SWtZYf\n3vzSZ+cqF6zrD/Rf/M35IQKBgHTU4E6ShPm09CcoaeC5sp2WK8OevZw/6IyZi78a\nr5Oiy18zzO97U/k6xVMy6F+38ILl/2Rn31JZDVJujniY6eSkIVsUHmPxrWoXV1HO\ny++U32uuSFiXDcSLarfIsE992MEJLSAynbF1Rsgsr3gXbGiuToJRyxbIeVy7gwzD\n94TpAoGAY4/PejWQj9psZfAhyk5dRGra++gYRQ/gK1IIc1g+Dd2/BxbT/RHr05GK\n6vwrfjsoRyMWteC1SsNs/CurjfQ/jqCfHNP5XPvxgd5Ec8sRJIiV7V5RTuWJsPu1\n+3K6cnKEyg+0ekYmLertRFIY6SwWmY1fyKgTvxudMcsBY7dC4xs=\n-----END RSA PRIVATE KEY-----\n`)\n\nfunc mustParseCIDR(v string) conf.IPNetwork {\n\t_, ne, err := net.ParseCIDR(v)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif ipv4 := ne.IP.To4(); ipv4 != nil {\n\t\treturn conf.IPNetwork{IP: ipv4, Mask: ne.Mask[len(ne.Mask)-4 : len(ne.Mask)]}\n\t}\n\treturn conf.IPNetwork(*ne)\n}\n\nfunc TestAuthInternal(t *testing.T) {\n\tfor _, outcome := range []string{\n\t\t\"ok\",\n\t\t\"wrong user\",\n\t\t\"wrong pass\",\n\t\t\"wrong ip\",\n\t\t\"wrong action\",\n\t\t\"wrong path\",\n\t} {\n\t\tfor _, encryption := range []string{\n\t\t\t\"plain\",\n\t\t\t\"sha256\",\n\t\t\t\"argon2\",\n\t\t} {\n\t\t\tt.Run(outcome+\" \"+encryption, func(t *testing.T) {\n\t\t\t\tm := Manager{\n\t\t\t\t\tMethod: conf.AuthMethodInternal,\n\t\t\t\t\tInternalUsers: []conf.AuthInternalUser{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tIPs: conf.IPNetworks{mustParseCIDR(\"127.1.1.1/32\")},\n\t\t\t\t\t\t\tPermissions: []conf.AuthInternalUserPermission{{\n\t\t\t\t\t\t\t\tAction: conf.AuthActionPublish,\n\t\t\t\t\t\t\t\tPath:   \"mypath\",\n\t\t\t\t\t\t\t}},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}\n\n\t\t\t\tswitch encryption {\n\t\t\t\tcase \"plain\":\n\t\t\t\t\tm.InternalUsers[0].User = conf.Credential(\"testuser\")\n\t\t\t\t\tm.InternalUsers[0].Pass = conf.Credential(\"testpass\")\n\n\t\t\t\tcase \"sha256\":\n\t\t\t\t\tm.InternalUsers[0].User = conf.Credential(\"sha256:rl3rgi4NcZkpAEcacZnQ2VuOfJ0FxAqCRaKB/SwdZoQ=\")\n\t\t\t\t\tm.InternalUsers[0].Pass = conf.Credential(\"sha256:E9JJ8stBJ7QM+nV4ZoUCeHk/gU3tPFh/5YieiJp6n2w=\")\n\n\t\t\t\tcase \"argon2\":\n\t\t\t\t\tm.InternalUsers[0].User = conf.Credential(\n\t\t\t\t\t\t\"argon2:$argon2id$v=19$m=4096,t=3,p=1$MTIzNDU2Nzg$Ux/LWeTgJQPyfMMJo1myR64+o8rALHoPmlE1i/TR+58\")\n\t\t\t\t\tm.InternalUsers[0].Pass = conf.Credential(\n\t\t\t\t\t\t\"argon2:$argon2i$v=19$m=4096,t=3,p=1$MTIzNDU2Nzg$/mrZ42TiTv1mcPnpMUera5oi0SFYbbyueAbdx5sUvWo\")\n\t\t\t\t}\n\n\t\t\t\tvar req *Request\n\n\t\t\t\tswitch outcome {\n\t\t\t\tcase \"ok\":\n\t\t\t\t\treq = &Request{\n\t\t\t\t\t\tAction: conf.AuthActionPublish,\n\t\t\t\t\t\tPath:   \"mypath\",\n\t\t\t\t\t\tCredentials: &Credentials{\n\t\t\t\t\t\t\tUser: \"testuser\",\n\t\t\t\t\t\t\tPass: \"testpass\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tIP: net.ParseIP(\"127.1.1.1\"),\n\t\t\t\t\t}\n\n\t\t\t\tcase \"wrong user\":\n\t\t\t\t\treq = &Request{\n\t\t\t\t\t\tAction: conf.AuthActionPublish,\n\t\t\t\t\t\tPath:   \"mypath\",\n\t\t\t\t\t\tCredentials: &Credentials{\n\t\t\t\t\t\t\tUser: \"wrong\",\n\t\t\t\t\t\t\tPass: \"testpass\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tIP: net.ParseIP(\"127.1.1.1\"),\n\t\t\t\t\t}\n\n\t\t\t\tcase \"wrong pass\":\n\t\t\t\t\treq = &Request{\n\t\t\t\t\t\tAction: conf.AuthActionPublish,\n\t\t\t\t\t\tPath:   \"mypath\",\n\t\t\t\t\t\tCredentials: &Credentials{\n\t\t\t\t\t\t\tUser: \"testuser\",\n\t\t\t\t\t\t\tPass: \"wrong\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tIP: net.ParseIP(\"127.1.1.1\"),\n\t\t\t\t\t}\n\n\t\t\t\tcase \"wrong ip\":\n\t\t\t\t\treq = &Request{\n\t\t\t\t\t\tAction: conf.AuthActionPublish,\n\t\t\t\t\t\tPath:   \"mypath\",\n\t\t\t\t\t\tCredentials: &Credentials{\n\t\t\t\t\t\t\tUser: \"testuser\",\n\t\t\t\t\t\t\tPass: \"testpass\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tIP: net.ParseIP(\"127.1.1.2\"),\n\t\t\t\t\t}\n\n\t\t\t\tcase \"wrong action\":\n\t\t\t\t\treq = &Request{\n\t\t\t\t\t\tAction: conf.AuthActionRead,\n\t\t\t\t\t\tPath:   \"mypath\",\n\t\t\t\t\t\tCredentials: &Credentials{\n\t\t\t\t\t\t\tUser: \"testuser\",\n\t\t\t\t\t\t\tPass: \"testpass\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tIP: net.ParseIP(\"127.1.1.1\"),\n\t\t\t\t\t}\n\n\t\t\t\tcase \"wrong path\":\n\t\t\t\t\treq = &Request{\n\t\t\t\t\t\tAction: conf.AuthActionPublish,\n\t\t\t\t\t\tPath:   \"wrong\",\n\t\t\t\t\t\tCredentials: &Credentials{\n\t\t\t\t\t\t\tUser: \"testuser\",\n\t\t\t\t\t\t\tPass: \"testpass\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tIP: net.ParseIP(\"127.1.1.1\"),\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// first request with empty credentials\n\t\t\t\t_, err := m.Authenticate(&Request{\n\t\t\t\t\tAction:      req.Action,\n\t\t\t\t\tPath:        req.Path,\n\t\t\t\t\tCredentials: &Credentials{},\n\t\t\t\t\tIP:          req.IP,\n\t\t\t\t})\n\t\t\t\trequire.Equal(t, &Error{\n\t\t\t\t\tWrapped:        err.Wrapped,\n\t\t\t\t\tAskCredentials: true,\n\t\t\t\t}, err)\n\n\t\t\t\t// second request\n\t\t\t\tuser, err := m.Authenticate(req)\n\t\t\t\tif outcome == \"ok\" {\n\t\t\t\t\trequire.Nil(t, err)\n\t\t\t\t\trequire.Equal(t, \"testuser\", user)\n\t\t\t\t} else {\n\t\t\t\t\trequire.EqualError(t, err.Wrapped, \"authentication failed\")\n\t\t\t\t\trequire.False(t, err.AskCredentials)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc TestAuthInternalCustomVerifyFunc(t *testing.T) {\n\tfor _, ca := range []string{\"ok\", \"invalid\"} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tm := Manager{\n\t\t\t\tMethod: conf.AuthMethodInternal,\n\t\t\t\tInternalUsers: []conf.AuthInternalUser{\n\t\t\t\t\t{\n\t\t\t\t\t\tUser: \"myuser\",\n\t\t\t\t\t\tPass: \"mypass\",\n\t\t\t\t\t\tIPs:  conf.IPNetworks{mustParseCIDR(\"127.1.1.1/32\")},\n\t\t\t\t\t\tPermissions: []conf.AuthInternalUserPermission{{\n\t\t\t\t\t\t\tAction: conf.AuthActionPublish,\n\t\t\t\t\t\t\tPath:   \"mypath\",\n\t\t\t\t\t\t}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\treq1 := &Request{\n\t\t\t\tAction: conf.AuthActionPublish,\n\t\t\t\tPath:   \"mypath\",\n\t\t\t\tCredentials: &Credentials{\n\t\t\t\t\tUser: \"myuser\",\n\t\t\t\t},\n\t\t\t\tIP: net.ParseIP(\"127.1.1.1\"),\n\t\t\t\tCustomVerifyFunc: func(expectedUser, expectedPass string) bool {\n\t\t\t\t\trequire.Equal(t, \"myuser\", expectedUser)\n\t\t\t\t\trequire.Equal(t, \"mypass\", expectedPass)\n\t\t\t\t\treturn (ca == \"ok\")\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tuser, err := m.Authenticate(req1)\n\t\t\tif ca == \"ok\" {\n\t\t\t\trequire.Nil(t, err)\n\t\t\t\trequire.Equal(t, \"myuser\", user)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err.Wrapped, \"authentication failed\")\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthHTTP(t *testing.T) {\n\tfor _, outcome := range []string{\"ok\", \"fail\"} {\n\t\tt.Run(outcome, func(t *testing.T) {\n\t\t\tfirstReceived := false\n\n\t\t\thttpServ := &http.Server{\n\t\t\t\tHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\trequire.Equal(t, http.MethodPost, r.Method)\n\t\t\t\t\trequire.Equal(t, \"/auth\", r.URL.Path)\n\n\t\t\t\t\tvar in struct {\n\t\t\t\t\t\tIP       string `json:\"ip\"`\n\t\t\t\t\t\tUser     string `json:\"user\"`\n\t\t\t\t\t\tPassword string `json:\"password\"`\n\t\t\t\t\t\tPath     string `json:\"path\"`\n\t\t\t\t\t\tProtocol string `json:\"protocol\"`\n\t\t\t\t\t\tID       string `json:\"id\"`\n\t\t\t\t\t\tAction   string `json:\"action\"`\n\t\t\t\t\t\tQuery    string `json:\"query\"`\n\t\t\t\t\t}\n\t\t\t\t\terr := json.NewDecoder(r.Body).Decode(&in)\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\tif in.IP != \"127.0.0.1\" ||\n\t\t\t\t\t\tin.User != \"testpublisher\" ||\n\t\t\t\t\t\tin.Password != \"testpass\" ||\n\t\t\t\t\t\tin.Path != \"teststream\" ||\n\t\t\t\t\t\tin.Protocol != \"rtsp\" ||\n\t\t\t\t\t\t(firstReceived && in.ID == \"\") ||\n\t\t\t\t\t\tin.Action != \"publish\" ||\n\t\t\t\t\t\t(in.Query != \"user=testreader&pass=testpass&param=value\" &&\n\t\t\t\t\t\t\tin.Query != \"user=testpublisher&pass=testpass&param=value\" &&\n\t\t\t\t\t\t\tin.Query != \"param=value\") {\n\t\t\t\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\tfirstReceived = true\n\t\t\t\t}),\n\t\t\t}\n\n\t\t\tln, err := net.Listen(\"tcp\", \"127.0.0.1:9120\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\tgo httpServ.Serve(ln)\n\t\t\tdefer httpServ.Shutdown(context.Background())\n\n\t\t\tm := Manager{\n\t\t\t\tMethod:      conf.AuthMethodHTTP,\n\t\t\t\tHTTPAddress: \"http://127.0.0.1:9120/auth\",\n\t\t\t}\n\n\t\t\tvar req *Request\n\n\t\t\tif outcome == \"ok\" {\n\t\t\t\treq = &Request{\n\t\t\t\t\tAction:   conf.AuthActionPublish,\n\t\t\t\t\tPath:     \"teststream\",\n\t\t\t\t\tQuery:    \"param=value\",\n\t\t\t\t\tProtocol: ProtocolRTSP,\n\t\t\t\t\tCredentials: &Credentials{\n\t\t\t\t\t\tUser: \"testpublisher\",\n\t\t\t\t\t\tPass: \"testpass\",\n\t\t\t\t\t},\n\t\t\t\t\tIP: net.ParseIP(\"127.0.0.1\"),\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\treq = &Request{\n\t\t\t\t\tAction:   conf.AuthActionPublish,\n\t\t\t\t\tPath:     \"teststream\",\n\t\t\t\t\tQuery:    \"param=value\",\n\t\t\t\t\tProtocol: ProtocolRTSP,\n\t\t\t\t\tCredentials: &Credentials{\n\t\t\t\t\t\tUser: \"invalid\",\n\t\t\t\t\t\tPass: \"testpass\",\n\t\t\t\t\t},\n\t\t\t\t\tIP: net.ParseIP(\"127.0.0.1\"),\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// first request with empty credentials\n\t\t\t_, err2 := m.Authenticate(&Request{\n\t\t\t\tAction:      req.Action,\n\t\t\t\tPath:        req.Path,\n\t\t\t\tCredentials: &Credentials{},\n\t\t\t\tIP:          req.IP,\n\t\t\t})\n\t\t\trequire.Equal(t, &Error{\n\t\t\t\tWrapped:        err2.Wrapped,\n\t\t\t\tAskCredentials: true,\n\t\t\t}, err2)\n\n\t\t\t// second request\n\t\t\tuser, err2 := m.Authenticate(req)\n\t\t\tif outcome == \"ok\" {\n\t\t\t\trequire.Nil(t, err2)\n\t\t\t\trequire.Equal(t, \"testpublisher\", user)\n\t\t\t} else {\n\t\t\t\trequire.EqualError(t, err2.Wrapped, \"server replied with code 400\")\n\t\t\t\trequire.False(t, err2.AskCredentials)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthHTTPFingerprint(t *testing.T) {\n\thttpServ := &http.Server{\n\t\tHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\trequire.Equal(t, http.MethodPost, r.Method)\n\t\t\trequire.Equal(t, \"/auth\", r.URL.Path)\n\n\t\t\tvar in struct {\n\t\t\t\tIP       string `json:\"ip\"`\n\t\t\t\tUser     string `json:\"user\"`\n\t\t\t\tPassword string `json:\"password\"`\n\t\t\t\tPath     string `json:\"path\"`\n\t\t\t\tProtocol string `json:\"protocol\"`\n\t\t\t\tID       string `json:\"id\"`\n\t\t\t\tAction   string `json:\"action\"`\n\t\t\t\tQuery    string `json:\"query\"`\n\t\t\t}\n\t\t\terr := json.NewDecoder(r.Body).Decode(&in)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tif in.User != \"testuser\" || in.Password != \"testpass\" {\n\t\t\t\tw.WriteHeader(http.StatusBadRequest)\n\t\t\t\treturn\n\t\t\t}\n\t\t}),\n\t}\n\n\tln, err := net.Listen(\"tcp\", \"localhost:9121\")\n\trequire.NoError(t, err)\n\n\tcert, err := tls.X509KeyPair(testTLSCertPub, testTLSCertKey)\n\trequire.NoError(t, err)\n\n\thttpServ.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cert}}\n\n\tgo httpServ.ServeTLS(ln, \"\", \"\")\n\tdefer httpServ.Shutdown(context.Background())\n\n\tm := Manager{\n\t\tMethod:          conf.AuthMethodHTTP,\n\t\tHTTPAddress:     \"https://localhost:9121/auth\",\n\t\tHTTPFingerprint: \"33949e05fffb5ff3e8aa16f8213a6251b4d9363804ba53233c4da9a46d6f2739\",\n\t}\n\n\tuser, err2 := m.Authenticate(&Request{\n\t\tAction:   conf.AuthActionPublish,\n\t\tPath:     \"teststream\",\n\t\tProtocol: ProtocolRTSP,\n\t\tCredentials: &Credentials{\n\t\t\tUser: \"testuser\",\n\t\t\tPass: \"testpass\",\n\t\t},\n\t\tIP: net.ParseIP(\"127.0.0.1\"),\n\t})\n\trequire.Nil(t, err2)\n\trequire.Equal(t, \"testuser\", user)\n}\n\nfunc TestAuthHTTPExclude(t *testing.T) {\n\tm := Manager{\n\t\tMethod:      conf.AuthMethodHTTP,\n\t\tHTTPAddress: \"http://not-to-be-used:9120/auth\",\n\t\tHTTPExclude: []conf.AuthInternalUserPermission{{\n\t\t\tAction: conf.AuthActionPublish,\n\t\t}},\n\t}\n\n\tuser, err := m.Authenticate(&Request{\n\t\tAction:   conf.AuthActionPublish,\n\t\tPath:     \"teststream\",\n\t\tQuery:    \"param=value\",\n\t\tProtocol: ProtocolRTSP,\n\t\tCredentials: &Credentials{\n\t\t\tUser: \"\",\n\t\t\tPass: \"\",\n\t\t},\n\t\tIP: net.ParseIP(\"127.0.0.1\"),\n\t})\n\trequire.Nil(t, err)\n\trequire.Equal(t, \"\", user)\n}\n\nfunc TestAuthJWT(t *testing.T) {\n\tfor _, ca := range []string{\"object\", \"string\"} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\t// reference:\n\t\t\t// https://github.com/MicahParks/jwkset/blob/master/examples/http_server/main.go\n\n\t\t\tkey, err := rsa.GenerateKey(rand.Reader, 1024)\n\t\t\trequire.NoError(t, err)\n\n\t\t\thttpServ := &http.Server{\n\t\t\t\tHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tjwk, err2 := jwkset.NewJWKFromKey(key, jwkset.JWKOptions{\n\t\t\t\t\t\tMetadata: jwkset.JWKMetadataOptions{\n\t\t\t\t\t\t\tKID: \"test-key-id\",\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t\trequire.NoError(t, err2)\n\n\t\t\t\t\tjwkSet := jwkset.NewMemoryStorage()\n\t\t\t\t\terr2 = jwkSet.KeyWrite(context.Background(), jwk)\n\t\t\t\t\trequire.NoError(t, err2)\n\n\t\t\t\t\tresponse, err2 := jwkSet.JSONPublic(r.Context())\n\t\t\t\t\tif err2 != nil {\n\t\t\t\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\t\t\t_, _ = w.Write(response)\n\t\t\t\t}),\n\t\t\t}\n\n\t\t\tln, err := net.Listen(\"tcp\", \"localhost:4567\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\tgo httpServ.Serve(ln)\n\t\t\tdefer httpServ.Shutdown(context.Background())\n\n\t\t\tvar req *Request\n\n\t\t\tif ca == \"object\" {\n\t\t\t\ttype customClaims struct {\n\t\t\t\t\tjwt.RegisteredClaims\n\t\t\t\t\tMediaMTXPermissions string `json:\"my_permission_key\"`\n\t\t\t\t}\n\n\t\t\t\tvar enc []byte\n\t\t\t\tenc, err = json.Marshal([]conf.AuthInternalUserPermission{{\n\t\t\t\t\tAction: conf.AuthActionPublish,\n\t\t\t\t\tPath:   \"mypath\",\n\t\t\t\t}})\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tclaims := customClaims{\n\t\t\t\t\tRegisteredClaims: jwt.RegisteredClaims{\n\t\t\t\t\t\tExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),\n\t\t\t\t\t\tIssuedAt:  jwt.NewNumericDate(time.Now()),\n\t\t\t\t\t\tNotBefore: jwt.NewNumericDate(time.Now()),\n\t\t\t\t\t\tIssuer:    \"test\",\n\t\t\t\t\t\tSubject:   \"somebody\",\n\t\t\t\t\t\tID:        \"1\",\n\t\t\t\t\t},\n\t\t\t\t\tMediaMTXPermissions: string(enc),\n\t\t\t\t}\n\n\t\t\t\ttoken := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)\n\t\t\t\ttoken.Header[jwkset.HeaderKID] = \"test-key-id\"\n\t\t\t\tvar ss string\n\t\t\t\tss, err = token.SignedString(key)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\treq = &Request{\n\t\t\t\t\tAction:   conf.AuthActionPublish,\n\t\t\t\t\tPath:     \"mypath\",\n\t\t\t\t\tQuery:    \"param=value\",\n\t\t\t\t\tProtocol: ProtocolRTSP,\n\t\t\t\t\tCredentials: &Credentials{\n\t\t\t\t\t\tToken: ss,\n\t\t\t\t\t},\n\t\t\t\t\tIP: net.ParseIP(\"127.0.0.1\"),\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttype customClaims struct {\n\t\t\t\t\tjwt.RegisteredClaims\n\t\t\t\t\tMediaMTXPermissions []conf.AuthInternalUserPermission `json:\"my_permission_key\"`\n\t\t\t\t}\n\n\t\t\t\tclaims := customClaims{\n\t\t\t\t\tRegisteredClaims: jwt.RegisteredClaims{\n\t\t\t\t\t\tExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),\n\t\t\t\t\t\tIssuedAt:  jwt.NewNumericDate(time.Now()),\n\t\t\t\t\t\tNotBefore: jwt.NewNumericDate(time.Now()),\n\t\t\t\t\t\tIssuer:    \"test\",\n\t\t\t\t\t\tSubject:   \"somebody\",\n\t\t\t\t\t\tID:        \"1\",\n\t\t\t\t\t},\n\t\t\t\t\tMediaMTXPermissions: []conf.AuthInternalUserPermission{{\n\t\t\t\t\t\tAction: conf.AuthActionPublish,\n\t\t\t\t\t\tPath:   \"mypath\",\n\t\t\t\t\t}},\n\t\t\t\t}\n\n\t\t\t\ttoken := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)\n\t\t\t\ttoken.Header[jwkset.HeaderKID] = \"test-key-id\"\n\t\t\t\tvar ss string\n\t\t\t\tss, err = token.SignedString(key)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\treq = &Request{\n\t\t\t\t\tAction:   conf.AuthActionPublish,\n\t\t\t\t\tPath:     \"mypath\",\n\t\t\t\t\tProtocol: ProtocolWebRTC,\n\t\t\t\t\tCredentials: &Credentials{\n\t\t\t\t\t\tToken: ss,\n\t\t\t\t\t},\n\t\t\t\t\tIP: net.ParseIP(\"127.0.0.1\"),\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tm := Manager{\n\t\t\t\tMethod:      conf.AuthMethodJWT,\n\t\t\t\tJWTJWKS:     \"http://localhost:4567/jwks\",\n\t\t\t\tJWTClaimKey: \"my_permission_key\",\n\t\t\t}\n\n\t\t\t// first request with empty credentials\n\t\t\t_, err2 := m.Authenticate(&Request{\n\t\t\t\tAction:      req.Action,\n\t\t\t\tPath:        req.Path,\n\t\t\t\tCredentials: &Credentials{},\n\t\t\t\tIP:          req.IP,\n\t\t\t})\n\t\t\trequire.Equal(t, &Error{\n\t\t\t\tWrapped:        err2.Wrapped,\n\t\t\t\tAskCredentials: true,\n\t\t\t}, err2)\n\n\t\t\t// second request\n\t\t\tuser, err2 := m.Authenticate(req)\n\t\t\trequire.Nil(t, err2)\n\t\t\trequire.Equal(t, \"somebody\", user)\n\t\t})\n\t}\n}\n\nfunc TestAuthJWTExclude(t *testing.T) {\n\tm := Manager{\n\t\tMethod:      conf.AuthMethodJWT,\n\t\tJWTJWKS:     \"http://localhost:4567/jwks\",\n\t\tJWTClaimKey: \"my_permission_key\",\n\t\tJWTExclude: []conf.AuthInternalUserPermission{{\n\t\t\tAction: conf.AuthActionPublish,\n\t\t}},\n\t}\n\n\tuser, err := m.Authenticate(&Request{\n\t\tAction:   conf.AuthActionPublish,\n\t\tPath:     \"teststream\",\n\t\tQuery:    \"param=value\",\n\t\tProtocol: ProtocolRTSP,\n\t\tIP:       net.ParseIP(\"127.0.0.1\"),\n\t})\n\trequire.Nil(t, err)\n\trequire.Equal(t, \"\", user)\n}\n\nfunc TestAuthJWTIssuer(t *testing.T) {\n\tkey, err := rsa.GenerateKey(rand.Reader, 1024)\n\trequire.NoError(t, err)\n\n\thttpServ := &http.Server{\n\t\tHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tjwk, err2 := jwkset.NewJWKFromKey(key, jwkset.JWKOptions{\n\t\t\t\tMetadata: jwkset.JWKMetadataOptions{\n\t\t\t\t\tKID: \"test-key-id\",\n\t\t\t\t},\n\t\t\t})\n\t\t\trequire.NoError(t, err2)\n\n\t\t\tjwkSet := jwkset.NewMemoryStorage()\n\t\t\terr2 = jwkSet.KeyWrite(context.Background(), jwk)\n\t\t\trequire.NoError(t, err2)\n\n\t\t\tresponse, err2 := jwkSet.JSONPublic(r.Context())\n\t\t\tif err2 != nil {\n\t\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\t_, _ = w.Write(response)\n\t\t}),\n\t}\n\n\tln, err := net.Listen(\"tcp\", \"localhost:4568\")\n\trequire.NoError(t, err)\n\n\tgo httpServ.Serve(ln)\n\tdefer httpServ.Shutdown(context.Background())\n\n\tfor _, ca := range []struct {\n\t\tname      string\n\t\tjwtIssuer string\n\t\ttokenIss  string\n\t\texpectErr bool\n\t}{\n\t\t{\n\t\t\tname:      \"matching\",\n\t\t\tjwtIssuer: \"my-issuer\",\n\t\t\ttokenIss:  \"my-issuer\",\n\t\t\texpectErr: false,\n\t\t},\n\t\t{\n\t\t\tname:      \"mismatched\",\n\t\t\tjwtIssuer: \"my-issuer\",\n\t\t\ttokenIss:  \"wrong-issuer\",\n\t\t\texpectErr: true,\n\t\t},\n\t} {\n\t\tt.Run(ca.name, func(t *testing.T) {\n\t\t\tsignToken := func(issuer string) string {\n\t\t\t\ttype customClaims struct {\n\t\t\t\t\tjwt.RegisteredClaims\n\t\t\t\t\tMediaMTXPermissions []conf.AuthInternalUserPermission `json:\"my_permission_key\"`\n\t\t\t\t}\n\n\t\t\t\tclaims := customClaims{\n\t\t\t\t\tRegisteredClaims: jwt.RegisteredClaims{\n\t\t\t\t\t\tExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),\n\t\t\t\t\t\tIssuedAt:  jwt.NewNumericDate(time.Now()),\n\t\t\t\t\t\tNotBefore: jwt.NewNumericDate(time.Now()),\n\t\t\t\t\t\tIssuer:    issuer,\n\t\t\t\t\t},\n\t\t\t\t\tMediaMTXPermissions: []conf.AuthInternalUserPermission{{\n\t\t\t\t\t\tAction: conf.AuthActionPublish,\n\t\t\t\t\t\tPath:   \"mypath\",\n\t\t\t\t\t}},\n\t\t\t\t}\n\n\t\t\t\ttoken := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)\n\t\t\t\ttoken.Header[jwkset.HeaderKID] = \"test-key-id\"\n\t\t\t\tvar ss string\n\t\t\t\tss, err = token.SignedString(key)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\treturn ss\n\t\t\t}\n\t\t\tss := signToken(ca.tokenIss)\n\n\t\t\tm := Manager{\n\t\t\t\tMethod:      conf.AuthMethodJWT,\n\t\t\t\tJWTJWKS:     \"http://localhost:4568/jwks\",\n\t\t\t\tJWTClaimKey: \"my_permission_key\",\n\t\t\t\tJWTIssuer:   ca.jwtIssuer,\n\t\t\t}\n\n\t\t\t_, err := m.Authenticate(&Request{\n\t\t\t\tAction:   conf.AuthActionPublish,\n\t\t\t\tPath:     \"mypath\",\n\t\t\t\tProtocol: ProtocolRTSP,\n\t\t\t\tCredentials: &Credentials{\n\t\t\t\t\tToken: ss,\n\t\t\t\t},\n\t\t\t\tIP: net.ParseIP(\"127.0.0.1\"),\n\t\t\t})\n\n\t\t\tif ca.expectErr {\n\t\t\t\trequire.NotNil(t, err)\n\t\t\t} else {\n\t\t\t\trequire.Nil(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthJWTAudience(t *testing.T) {\n\tkey, err := rsa.GenerateKey(rand.Reader, 1024)\n\trequire.NoError(t, err)\n\n\thttpServ := &http.Server{\n\t\tHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tjwk, err2 := jwkset.NewJWKFromKey(key, jwkset.JWKOptions{\n\t\t\t\tMetadata: jwkset.JWKMetadataOptions{\n\t\t\t\t\tKID: \"test-key-id\",\n\t\t\t\t},\n\t\t\t})\n\t\t\trequire.NoError(t, err2)\n\n\t\t\tjwkSet := jwkset.NewMemoryStorage()\n\t\t\terr2 = jwkSet.KeyWrite(context.Background(), jwk)\n\t\t\trequire.NoError(t, err2)\n\n\t\t\tresponse, err2 := jwkSet.JSONPublic(r.Context())\n\t\t\tif err2 != nil {\n\t\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\t_, _ = w.Write(response)\n\t\t}),\n\t}\n\n\tln, err := net.Listen(\"tcp\", \"localhost:4569\")\n\trequire.NoError(t, err)\n\n\tgo httpServ.Serve(ln)\n\tdefer httpServ.Shutdown(context.Background())\n\n\tfor _, ca := range []struct {\n\t\tname        string\n\t\tjwtAudience string\n\t\ttokenAud    jwt.ClaimStrings\n\t\texpectErr   bool\n\t}{\n\t\t{\n\t\t\tname:        \"matching\",\n\t\t\tjwtAudience: \"my-audience\",\n\t\t\ttokenAud:    jwt.ClaimStrings{\"my-audience\"},\n\t\t\texpectErr:   false,\n\t\t},\n\t\t{\n\t\t\tname:        \"mismatched\",\n\t\t\tjwtAudience: \"my-audience\",\n\t\t\ttokenAud:    jwt.ClaimStrings{\"wrong-audience\"},\n\t\t\texpectErr:   true,\n\t\t},\n\t\t{\n\t\t\tname:        \"present in list\",\n\t\t\tjwtAudience: \"my-audience\",\n\t\t\ttokenAud:    jwt.ClaimStrings{\"other\", \"my-audience\"},\n\t\t\texpectErr:   false,\n\t\t},\n\t} {\n\t\tt.Run(ca.name, func(t *testing.T) {\n\t\t\tsignToken := func(audience jwt.ClaimStrings) string {\n\t\t\t\ttype customClaims struct {\n\t\t\t\t\tjwt.RegisteredClaims\n\t\t\t\t\tMediaMTXPermissions []conf.AuthInternalUserPermission `json:\"my_permission_key\"`\n\t\t\t\t}\n\n\t\t\t\tclaims := customClaims{\n\t\t\t\t\tRegisteredClaims: jwt.RegisteredClaims{\n\t\t\t\t\t\tExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),\n\t\t\t\t\t\tIssuedAt:  jwt.NewNumericDate(time.Now()),\n\t\t\t\t\t\tNotBefore: jwt.NewNumericDate(time.Now()),\n\t\t\t\t\t\tAudience:  audience,\n\t\t\t\t\t},\n\t\t\t\t\tMediaMTXPermissions: []conf.AuthInternalUserPermission{{\n\t\t\t\t\t\tAction: conf.AuthActionPublish,\n\t\t\t\t\t\tPath:   \"mypath\",\n\t\t\t\t\t}},\n\t\t\t\t}\n\n\t\t\t\ttoken := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)\n\t\t\t\ttoken.Header[jwkset.HeaderKID] = \"test-key-id\"\n\t\t\t\tvar ss string\n\t\t\t\tss, err = token.SignedString(key)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\treturn ss\n\t\t\t}\n\t\t\tss := signToken(ca.tokenAud)\n\n\t\t\tm := Manager{\n\t\t\t\tMethod:      conf.AuthMethodJWT,\n\t\t\t\tJWTJWKS:     \"http://localhost:4569/jwks\",\n\t\t\t\tJWTClaimKey: \"my_permission_key\",\n\t\t\t\tJWTAudience: ca.jwtAudience,\n\t\t\t}\n\n\t\t\t_, err := m.Authenticate(&Request{\n\t\t\t\tAction:   conf.AuthActionPublish,\n\t\t\t\tPath:     \"mypath\",\n\t\t\t\tProtocol: ProtocolRTSP,\n\t\t\t\tCredentials: &Credentials{\n\t\t\t\t\tToken: ss,\n\t\t\t\t},\n\t\t\t\tIP: net.ParseIP(\"127.0.0.1\"),\n\t\t\t})\n\n\t\t\tif ca.expectErr {\n\t\t\t\trequire.NotNil(t, err)\n\t\t\t} else {\n\t\t\t\trequire.Nil(t, err)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAuthJWTRefresh(t *testing.T) {\n\t// reference:\n\t// https://github.com/MicahParks/jwkset/blob/master/examples/http_server/main.go\n\n\tvar key *rsa.PrivateKey\n\n\thttpServ := &http.Server{\n\t\tHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tjwk, err := jwkset.NewJWKFromKey(key, jwkset.JWKOptions{\n\t\t\t\tMetadata: jwkset.JWKMetadataOptions{\n\t\t\t\t\tKID: \"test-key-id\",\n\t\t\t\t},\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\n\t\t\tjwkSet := jwkset.NewMemoryStorage()\n\t\t\terr = jwkSet.KeyWrite(context.Background(), jwk)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tresponse, err2 := jwkSet.JSONPublic(r.Context())\n\t\t\tif err2 != nil {\n\t\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\t_, _ = w.Write(response)\n\t\t}),\n\t}\n\n\tln, err := net.Listen(\"tcp\", \"localhost:4567\")\n\trequire.NoError(t, err)\n\n\tgo httpServ.Serve(ln)\n\tdefer httpServ.Shutdown(context.Background())\n\n\tm := Manager{\n\t\tMethod:      conf.AuthMethodJWT,\n\t\tJWTJWKS:     \"http://localhost:4567/jwks\",\n\t\tJWTClaimKey: \"my_permission_key\",\n\t}\n\n\tfor range 2 {\n\t\tkey, err = rsa.GenerateKey(rand.Reader, 1024)\n\t\trequire.NoError(t, err)\n\n\t\ttype customClaims struct {\n\t\t\tjwt.RegisteredClaims\n\t\t\tMediaMTXPermissions []conf.AuthInternalUserPermission `json:\"my_permission_key\"`\n\t\t}\n\n\t\tclaims := customClaims{\n\t\t\tRegisteredClaims: jwt.RegisteredClaims{\n\t\t\t\tExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),\n\t\t\t\tIssuedAt:  jwt.NewNumericDate(time.Now()),\n\t\t\t\tNotBefore: jwt.NewNumericDate(time.Now()),\n\t\t\t\tIssuer:    \"test\",\n\t\t\t\tSubject:   \"somebody\",\n\t\t\t\tID:        \"1\",\n\t\t\t},\n\t\t\tMediaMTXPermissions: []conf.AuthInternalUserPermission{{\n\t\t\t\tAction: conf.AuthActionPublish,\n\t\t\t\tPath:   \"mypath\",\n\t\t\t}},\n\t\t}\n\n\t\ttoken := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)\n\t\ttoken.Header[jwkset.HeaderKID] = \"test-key-id\"\n\t\tvar ss string\n\t\tss, err = token.SignedString(key)\n\t\trequire.NoError(t, err)\n\n\t\tuser, err2 := m.Authenticate(&Request{\n\t\t\tAction:   conf.AuthActionPublish,\n\t\t\tPath:     \"mypath\",\n\t\t\tQuery:    \"param=value\",\n\t\t\tProtocol: ProtocolRTSP,\n\t\t\tCredentials: &Credentials{\n\t\t\t\tToken: ss,\n\t\t\t},\n\t\t\tIP: net.ParseIP(\"127.0.0.1\"),\n\t\t})\n\t\trequire.Nil(t, err2)\n\t\trequire.Equal(t, \"somebody\", user)\n\n\t\tm.RefreshJWTJWKS()\n\t}\n}\n\nfunc TestAuthJWTFingerprint(t *testing.T) {\n\tkey, err := rsa.GenerateKey(rand.Reader, 1024)\n\trequire.NoError(t, err)\n\n\thttpServ := &http.Server{\n\t\tHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tjwk, err2 := jwkset.NewJWKFromKey(key, jwkset.JWKOptions{\n\t\t\t\tMetadata: jwkset.JWKMetadataOptions{\n\t\t\t\t\tKID: \"test-key-id\",\n\t\t\t\t},\n\t\t\t})\n\t\t\trequire.NoError(t, err2)\n\n\t\t\tjwkSet := jwkset.NewMemoryStorage()\n\t\t\terr2 = jwkSet.KeyWrite(context.Background(), jwk)\n\t\t\trequire.NoError(t, err2)\n\n\t\t\tresponse, err2 := jwkSet.JSONPublic(r.Context())\n\t\t\tif err2 != nil {\n\t\t\t\tw.WriteHeader(http.StatusInternalServerError)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tw.Header().Set(\"Content-Type\", \"application/json\")\n\t\t\t_, _ = w.Write(response)\n\t\t}),\n\t}\n\n\tln, err := net.Listen(\"tcp\", \"localhost:4568\")\n\trequire.NoError(t, err)\n\n\tcert, err := tls.X509KeyPair(testTLSCertPub, testTLSCertKey)\n\trequire.NoError(t, err)\n\n\thttpServ.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cert}}\n\n\tgo httpServ.ServeTLS(ln, \"\", \"\")\n\tdefer httpServ.Shutdown(context.Background())\n\n\ttype customClaims struct {\n\t\tjwt.RegisteredClaims\n\t\tMediaMTXPermissions []conf.AuthInternalUserPermission `json:\"my_permission_key\"`\n\t}\n\n\tclaims := customClaims{\n\t\tRegisteredClaims: jwt.RegisteredClaims{\n\t\t\tExpiresAt: jwt.NewNumericDate(time.Now().Add(24 * time.Hour)),\n\t\t\tIssuedAt:  jwt.NewNumericDate(time.Now()),\n\t\t\tNotBefore: jwt.NewNumericDate(time.Now()),\n\t\t\tIssuer:    \"test\",\n\t\t\tSubject:   \"somebody\",\n\t\t\tID:        \"1\",\n\t\t},\n\t\tMediaMTXPermissions: []conf.AuthInternalUserPermission{{\n\t\t\tAction: conf.AuthActionPublish,\n\t\t\tPath:   \"mypath\",\n\t\t}},\n\t}\n\n\ttoken := jwt.NewWithClaims(jwt.SigningMethodRS256, claims)\n\ttoken.Header[jwkset.HeaderKID] = \"test-key-id\"\n\tss, err := token.SignedString(key)\n\trequire.NoError(t, err)\n\n\tm := Manager{\n\t\tMethod:             conf.AuthMethodJWT,\n\t\tJWTJWKS:            \"https://localhost:4568/jwks\",\n\t\tJWTJWKSFingerprint: \"33949e05fffb5ff3e8aa16f8213a6251b4d9363804ba53233c4da9a46d6f2739\",\n\t\tJWTClaimKey:        \"my_permission_key\",\n\t}\n\n\tuser, err2 := m.Authenticate(&Request{\n\t\tAction:   conf.AuthActionPublish,\n\t\tPath:     \"mypath\",\n\t\tProtocol: ProtocolRTSP,\n\t\tCredentials: &Credentials{\n\t\t\tToken: ss,\n\t\t},\n\t\tIP: net.ParseIP(\"127.0.0.1\"),\n\t})\n\trequire.Nil(t, err2)\n\trequire.Equal(t, \"somebody\", user)\n}\n"
  },
  {
    "path": "internal/auth/request.go",
    "content": "package auth\n\nimport (\n\t\"net\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/google/uuid\"\n)\n\n// Protocol is a protocol.\ntype Protocol string\n\n// protocols.\nconst (\n\tProtocolRTSP   Protocol = \"rtsp\"\n\tProtocolRTMP   Protocol = \"rtmp\"\n\tProtocolHLS    Protocol = \"hls\"\n\tProtocolWebRTC Protocol = \"webrtc\"\n\tProtocolSRT    Protocol = \"srt\"\n)\n\n// Request is an authentication request.\ntype Request struct {\n\tAction           conf.AuthAction\n\tPath             string // only for ActionPublish, ActionRead, ActionPlayback\n\tQuery            string\n\tProtocol         Protocol   // only for ActionPublish, ActionRead\n\tID               *uuid.UUID // only for ActionPublish, ActionRead\n\tCredentials      *Credentials\n\tIP               net.IP\n\tCustomVerifyFunc func(expectedUser string, expectedPass string) bool\n}\n"
  },
  {
    "path": "internal/certloader/certloader.go",
    "content": "// Package certloader contains a certicate loader.\npackage certloader\n\nimport (\n\t\"crypto/tls\"\n\t\"sync\"\n\n\t\"github.com/bluenviron/mediamtx/internal/confwatcher\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n)\n\n// CertLoader is a certificate loader. It watches for changes to the certificate and key files.\ntype CertLoader struct {\n\tCertPath string\n\tKeyPath  string\n\tParent   logger.Writer\n\n\tcertWatcher, keyWatcher *confwatcher.ConfWatcher\n\tcert                    *tls.Certificate\n\tcertMu                  sync.RWMutex\n\n\tdone chan struct{}\n}\n\n// Initialize initializes a CertLoader.\nfunc (cl *CertLoader) Initialize() error {\n\tcl.done = make(chan struct{})\n\n\tcl.certWatcher = &confwatcher.ConfWatcher{FilePath: cl.CertPath}\n\terr := cl.certWatcher.Initialize()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcl.keyWatcher = &confwatcher.ConfWatcher{FilePath: cl.KeyPath}\n\terr = cl.keyWatcher.Initialize()\n\tif err != nil {\n\t\tcl.certWatcher.Close() //nolint:errcheck\n\t\treturn err\n\t}\n\n\tcert, err := tls.LoadX509KeyPair(cl.CertPath, cl.KeyPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcl.certMu.Lock()\n\tcl.cert = &cert\n\tcl.certMu.Unlock()\n\n\tgo cl.watch()\n\n\treturn nil\n}\n\n// Close closes a CertLoader and releases any underlying resources.\nfunc (cl *CertLoader) Close() {\n\tclose(cl.done)\n\tcl.certWatcher.Close() //nolint:errcheck\n\tcl.keyWatcher.Close()  //nolint:errcheck\n\tcl.certMu.Lock()\n\tdefer cl.certMu.Unlock()\n\tcl.cert = nil\n}\n\n// GetCertificate returns a function that returns the certificate for use in a tls.Config.\nfunc (cl *CertLoader) GetCertificate() func(*tls.ClientHelloInfo) (*tls.Certificate, error) {\n\treturn func(_ *tls.ClientHelloInfo) (*tls.Certificate, error) {\n\t\tcl.certMu.RLock()\n\t\tdefer cl.certMu.RUnlock()\n\t\treturn cl.cert, nil\n\t}\n}\n\nfunc (cl *CertLoader) watch() {\n\tfor {\n\t\tselect {\n\t\tcase <-cl.certWatcher.Watch():\n\t\t\tcert, err := tls.LoadX509KeyPair(cl.CertPath, cl.KeyPath)\n\t\t\tif err != nil {\n\t\t\t\tcl.Parent.Log(logger.Error, \"certloader failed to load after change to %s: %s\", cl.CertPath, err.Error())\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcl.certMu.Lock()\n\t\t\tcl.cert = &cert\n\t\t\tcl.certMu.Unlock()\n\n\t\t\tcl.Parent.Log(logger.Info, \"certificate reloaded after change to %s\", cl.CertPath)\n\t\tcase <-cl.keyWatcher.Watch():\n\t\t\tcert, err := tls.LoadX509KeyPair(cl.CertPath, cl.KeyPath)\n\t\t\tif err != nil {\n\t\t\t\tcl.Parent.Log(logger.Error, \"certloader failed to load after change to %s: %s\", cl.KeyPath, err.Error())\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcl.certMu.Lock()\n\t\t\tcl.cert = &cert\n\t\t\tcl.certMu.Unlock()\n\n\t\t\tcl.Parent.Log(logger.Info, \"certificate reloaded after change to %s\", cl.KeyPath)\n\t\tcase <-cl.done:\n\t\t\treturn\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/certloader/certloader_test.go",
    "content": "package certloader\n\nimport (\n\t\"crypto/tls\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCertReload(t *testing.T) {\n\ttestData, err := tls.X509KeyPair(test.TLSCertPub, test.TLSCertKey)\n\trequire.NoError(t, err)\n\n\tserverCertPath, err := test.CreateTempFile(test.TLSCertPub)\n\trequire.NoError(t, err)\n\tdefer os.Remove(serverCertPath)\n\n\tserverKeyPath, err := test.CreateTempFile(test.TLSCertKey)\n\trequire.NoError(t, err)\n\tdefer os.Remove(serverKeyPath)\n\n\tloader := &CertLoader{\n\t\tCertPath: serverCertPath,\n\t\tKeyPath:  serverKeyPath,\n\t\tParent:   test.NilLogger,\n\t}\n\terr = loader.Initialize()\n\trequire.NoError(t, err)\n\tdefer loader.Close()\n\n\tgetCert := loader.GetCertificate()\n\trequire.NotNil(t, getCert)\n\n\tcert, err := getCert(nil)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, cert)\n\trequire.Equal(t, &testData, cert)\n\n\ttestData, err = tls.X509KeyPair(test.TLSCertPubAlt, test.TLSCertKeyAlt)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(serverCertPath, test.TLSCertPubAlt, 0o644)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(serverKeyPath, test.TLSCertKeyAlt, 0o644)\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\tcert, err = getCert(nil)\n\trequire.NoError(t, err)\n\trequire.NotNil(t, cert)\n\trequire.Equal(t, &testData, cert)\n}\n"
  },
  {
    "path": "internal/conf/always_available_track.go",
    "content": "package conf\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper\"\n)\n\n// AlwaysAvailableTrack is an item of alwaysAvailableTracks.\ntype AlwaysAvailableTrack struct {\n\tCodec        AlwaysAvailableTrackCodec `json:\"codec\"`\n\tSampleRate   int                       `json:\"sampleRate\"`\n\tChannelCount int                       `json:\"channelCount\"`\n\tMULaw        bool                      `json:\"muLaw\"`\n}\n\n// UnmarshalJSON implements json.Unmarshaler.\nfunc (t *AlwaysAvailableTrack) UnmarshalJSON(b []byte) error {\n\ttype alias AlwaysAvailableTrack\n\terr := jsonwrapper.Unmarshal(b, (*alias)(t))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tswitch t.Codec {\n\tcase CodecAV1, CodecVP9, CodecH265, CodecH264, CodecOpus:\n\t\tif t.SampleRate != 0 {\n\t\t\treturn fmt.Errorf(\"sampleRate must not be specified for codec '%s'\", t.Codec)\n\t\t}\n\t\tif t.ChannelCount != 0 {\n\t\t\treturn fmt.Errorf(\"channelCount must not be specified for codec '%s'\", t.Codec)\n\t\t}\n\n\tcase CodecMPEG4Audio, CodecG711, CodecLPCM:\n\t\tif t.SampleRate == 0 {\n\t\t\treturn fmt.Errorf(\"sampleRate is mandatory for codec '%s'\", t.Codec)\n\t\t}\n\t\tif t.ChannelCount == 0 {\n\t\t\treturn fmt.Errorf(\"channelCount is mandatory for codec '%s'\", t.Codec)\n\t\t}\n\n\tdefault:\n\t\treturn fmt.Errorf(\"unsupported codec '%s'\", t.Codec)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/conf/always_available_track_codec.go",
    "content": "package conf\n\nimport \"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper\"\n\n// AlwaysAvailableTrackCodec is a codec of AlwaysAvailableTrack.\ntype AlwaysAvailableTrackCodec string\n\n// available codecs.\nconst (\n\tCodecAV1        AlwaysAvailableTrackCodec = \"AV1\"\n\tCodecVP9        AlwaysAvailableTrackCodec = \"VP9\"\n\tCodecH265       AlwaysAvailableTrackCodec = \"H265\"\n\tCodecH264       AlwaysAvailableTrackCodec = \"H264\"\n\tCodecMPEG4Audio AlwaysAvailableTrackCodec = \"MPEG4Audio\"\n\tCodecOpus       AlwaysAvailableTrackCodec = \"Opus\"\n\tCodecG711       AlwaysAvailableTrackCodec = \"G711\"\n\tCodecLPCM       AlwaysAvailableTrackCodec = \"LPCM\"\n)\n\n// UnmarshalEnv implements env.Unmarshaler.\nfunc (d *AlwaysAvailableTrackCodec) UnmarshalEnv(_ string, v string) error {\n\treturn jsonwrapper.Unmarshal([]byte(`\"`+v+`\"`), d)\n}\n"
  },
  {
    "path": "internal/conf/auth_action.go",
    "content": "package conf\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper\"\n)\n\n// AuthAction is an authentication action.\ntype AuthAction string\n\n// auth actions\nconst (\n\tAuthActionPublish  AuthAction = \"publish\"\n\tAuthActionRead     AuthAction = \"read\"\n\tAuthActionPlayback AuthAction = \"playback\"\n\tAuthActionAPI      AuthAction = \"api\"\n\tAuthActionMetrics  AuthAction = \"metrics\"\n\tAuthActionPprof    AuthAction = \"pprof\"\n)\n\n// UnmarshalJSON implements json.Unmarshaler.\nfunc (d *AuthAction) UnmarshalJSON(b []byte) error {\n\tvar in string\n\tif err := jsonwrapper.Unmarshal(b, &in); err != nil {\n\t\treturn err\n\t}\n\n\tswitch in {\n\tcase string(AuthActionPublish),\n\t\tstring(AuthActionRead),\n\t\tstring(AuthActionPlayback),\n\t\tstring(AuthActionAPI),\n\t\tstring(AuthActionMetrics),\n\t\tstring(AuthActionPprof):\n\t\t*d = AuthAction(in)\n\n\tdefault:\n\t\treturn fmt.Errorf(\"invalid auth action: '%s'\", in)\n\t}\n\n\treturn nil\n}\n\n// UnmarshalEnv implements env.Unmarshaler.\nfunc (d *AuthAction) UnmarshalEnv(_ string, v string) error {\n\treturn d.UnmarshalJSON([]byte(`\"` + v + `\"`))\n}\n"
  },
  {
    "path": "internal/conf/auth_internal_user.go",
    "content": "package conf\n\n// AuthInternalUser is an user.\ntype AuthInternalUser struct {\n\tUser        Credential                   `json:\"user\"`\n\tPass        Credential                   `json:\"pass\"`\n\tIPs         IPNetworks                   `json:\"ips\"`\n\tPermissions []AuthInternalUserPermission `json:\"permissions\"`\n}\n"
  },
  {
    "path": "internal/conf/auth_internal_user_permission.go",
    "content": "package conf\n\n// AuthInternalUserPermission is a permission of a user.\ntype AuthInternalUserPermission struct {\n\tAction AuthAction `json:\"action\"`\n\tPath   string     `json:\"path\"`\n}\n"
  },
  {
    "path": "internal/conf/auth_method.go",
    "content": "package conf\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper\"\n)\n\n// AuthMethod is an authentication method.\ntype AuthMethod string\n\n// authentication methods.\nconst (\n\tAuthMethodInternal AuthMethod = \"internal\"\n\tAuthMethodHTTP     AuthMethod = \"http\"\n\tAuthMethodJWT      AuthMethod = \"jwt\"\n)\n\n// UnmarshalJSON implements json.Unmarshaler.\nfunc (d *AuthMethod) UnmarshalJSON(b []byte) error {\n\ttype alias AuthMethod\n\tif err := jsonwrapper.Unmarshal(b, (*alias)(d)); err != nil {\n\t\treturn err\n\t}\n\n\tswitch *d {\n\tcase AuthMethodInternal, AuthMethodHTTP, AuthMethodJWT:\n\n\tdefault:\n\t\treturn fmt.Errorf(\"invalid authMethod: '%s'\", *d)\n\t}\n\n\treturn nil\n}\n\n// UnmarshalEnv implements env.Unmarshaler.\nfunc (d *AuthMethod) UnmarshalEnv(_ string, v string) error {\n\treturn d.UnmarshalJSON([]byte(`\"` + v + `\"`))\n}\n"
  },
  {
    "path": "internal/conf/conf.go",
    "content": "// Package conf contains the struct that holds the configuration of the software.\npackage conf\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"reflect\"\n\t\"slices\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gohlslib/v2\"\n\t\"github.com/bluenviron/gortsplib/v5\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/auth\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf/decrypt\"\n\t\"github.com/bluenviron/mediamtx/internal/conf/env\"\n\t\"github.com/bluenviron/mediamtx/internal/conf/yamlwrapper\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n)\n\n// ErrPathNotFound is returned when a path is not found.\nvar ErrPathNotFound = errors.New(\"path not found\")\n\nfunc sortedKeys(paths map[string]*OptionalPath) []string {\n\tret := make([]string, len(paths))\n\ti := 0\n\tfor name := range paths {\n\t\tret[i] = name\n\t\ti++\n\t}\n\tsort.Strings(ret)\n\treturn ret\n}\n\nfunc firstThatExists(paths []string) string {\n\tfor _, pa := range paths {\n\t\t_, err := os.Stat(pa)\n\t\tif err == nil {\n\t\t\treturn pa\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc setAllNilSlicesToEmptyRecursive(rv reflect.Value) {\n\tif rv.Kind() == reflect.Pointer {\n\t\trv = rv.Elem()\n\t}\n\n\tif rv.Kind() == reflect.Struct {\n\t\tfor i := range rv.NumField() {\n\t\t\tfield := rv.Field(i)\n\t\t\tswitch field.Kind() {\n\t\t\tcase reflect.Slice:\n\t\t\t\tif field.IsNil() {\n\t\t\t\t\tfield.Set(reflect.MakeSlice(field.Type(), 0, 0))\n\t\t\t\t} else {\n\t\t\t\t\tfor j := range field.Len() {\n\t\t\t\t\t\telem := field.Index(j)\n\t\t\t\t\t\tif elem.Kind() == reflect.Pointer || elem.Kind() == reflect.Struct {\n\t\t\t\t\t\t\tsetAllNilSlicesToEmptyRecursive(elem)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\tcase reflect.Pointer:\n\t\t\t\tif !field.IsNil() {\n\t\t\t\t\tsetAllNilSlicesToEmptyRecursive(field)\n\t\t\t\t}\n\n\t\t\tcase reflect.Struct:\n\t\t\t\tsetAllNilSlicesToEmptyRecursive(field.Addr())\n\n\t\t\tcase reflect.Map:\n\t\t\t\tif !field.IsNil() {\n\t\t\t\t\tfor _, key := range field.MapKeys() {\n\t\t\t\t\t\tmapValue := field.MapIndex(key)\n\t\t\t\t\t\tif mapValue.Kind() == reflect.Pointer {\n\t\t\t\t\t\t\tsetAllNilSlicesToEmptyRecursive(mapValue)\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc copyStructFields(dest any, source any) {\n\trvsource := reflect.ValueOf(source).Elem()\n\trvdest := reflect.ValueOf(dest)\n\tnf := rvsource.NumField()\n\tvar zero reflect.Value\n\n\tfor i := range nf {\n\t\tfnew := rvsource.Field(i)\n\t\tf := rvdest.Elem().FieldByName(rvsource.Type().Field(i).Name)\n\t\tif f == zero {\n\t\t\tcontinue\n\t\t}\n\n\t\tif fnew.Kind() == reflect.Pointer {\n\t\t\tif !fnew.IsNil() {\n\t\t\t\tif f.Kind() == reflect.Pointer {\n\t\t\t\t\tf.Set(fnew)\n\t\t\t\t} else {\n\t\t\t\t\tf.Set(fnew.Elem())\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tf.Set(fnew)\n\t\t}\n\t}\n}\n\nfunc mustParseCIDR(v string) IPNetwork {\n\t_, ne, err := net.ParseCIDR(v)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tif ipv4 := ne.IP.To4(); ipv4 != nil {\n\t\treturn IPNetwork{IP: ipv4, Mask: ne.Mask[len(ne.Mask)-4 : len(ne.Mask)]}\n\t}\n\treturn IPNetwork(*ne)\n}\n\nfunc anyPathHasDeprecatedCredentials(pathDefaults Path, paths map[string]*OptionalPath) bool {\n\tif pathDefaults.PublishUser != nil ||\n\t\tpathDefaults.PublishPass != nil ||\n\t\tpathDefaults.PublishIPs != nil ||\n\t\tpathDefaults.ReadUser != nil ||\n\t\tpathDefaults.ReadPass != nil ||\n\t\tpathDefaults.ReadIPs != nil {\n\t\treturn true\n\t}\n\n\tfor _, pa := range paths {\n\t\tif pa != nil {\n\t\t\trva := reflect.ValueOf(pa.Values).Elem()\n\t\t\tif rva.FieldByName(\"PublishUser\").Interface().(*Credential) != nil ||\n\t\t\t\trva.FieldByName(\"PublishPass\").Interface().(*Credential) != nil ||\n\t\t\t\trva.FieldByName(\"PublishIPs\").Interface().(*IPNetworks) != nil ||\n\t\t\t\trva.FieldByName(\"ReadUser\").Interface().(*Credential) != nil ||\n\t\t\t\trva.FieldByName(\"ReadPass\").Interface().(*Credential) != nil ||\n\t\t\t\trva.FieldByName(\"ReadIPs\").Interface().(*IPNetworks) != nil {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\treturn false\n}\n\nfunc deepClone(rv reflect.Value) reflect.Value {\n\tswitch rv.Kind() {\n\tcase reflect.Pointer:\n\t\tif rv.IsNil() {\n\t\t\treturn rv\n\t\t}\n\t\tnewPtr := reflect.New(rv.Elem().Type())\n\t\tnewPtr.Elem().Set(deepClone(rv.Elem()))\n\t\treturn newPtr\n\n\tcase reflect.Struct:\n\t\tnewStruct := reflect.New(rv.Type()).Elem()\n\t\tfor i := range rv.NumField() {\n\t\t\tfield := rv.Field(i)\n\t\t\tnewField := newStruct.Field(i)\n\t\t\tif newField.CanSet() {\n\t\t\t\tnewField.Set(deepClone(field))\n\t\t\t}\n\t\t}\n\t\treturn newStruct\n\n\tcase reflect.Slice:\n\t\tif rv.IsNil() {\n\t\t\treturn reflect.Zero(rv.Type())\n\t\t}\n\t\tnewSlice := reflect.MakeSlice(rv.Type(), rv.Len(), rv.Cap())\n\t\tfor i := range rv.Len() {\n\t\t\tnewSlice.Index(i).Set(deepClone(rv.Index(i)))\n\t\t}\n\t\treturn newSlice\n\n\tcase reflect.Map:\n\t\tif rv.IsNil() {\n\t\t\treturn reflect.Zero(rv.Type())\n\t\t}\n\t\tnewMap := reflect.MakeMap(rv.Type())\n\t\tfor _, key := range rv.MapKeys() {\n\t\t\tnewMap.SetMapIndex(key, deepClone(rv.MapIndex(key)))\n\t\t}\n\t\treturn newMap\n\n\tdefault:\n\t\treturn rv\n\t}\n}\n\ntype nilLogger struct{}\n\nfunc (nilLogger) Log(_ logger.Level, _ string, _ ...any) {\n}\n\nvar defaultAuthInternalUsers = []AuthInternalUser{\n\t{\n\t\tUser: \"any\",\n\t\tPass: \"\",\n\t\tPermissions: []AuthInternalUserPermission{\n\t\t\t{\n\t\t\t\tAction: AuthActionPublish,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAction: AuthActionRead,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAction: AuthActionPlayback,\n\t\t\t},\n\t\t},\n\t},\n\t{\n\t\tUser: \"any\",\n\t\tPass: \"\",\n\t\tIPs:  IPNetworks{mustParseCIDR(\"127.0.0.1/32\"), mustParseCIDR(\"::1/128\")},\n\t\tPermissions: []AuthInternalUserPermission{\n\t\t\t{\n\t\t\t\tAction: AuthActionAPI,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAction: AuthActionMetrics,\n\t\t\t},\n\t\t\t{\n\t\t\t\tAction: AuthActionPprof,\n\t\t\t},\n\t\t},\n\t},\n}\n\n// Conf is a configuration.\ntype Conf struct {\n\t// General\n\tLogLevel            LogLevel        `json:\"logLevel\"`\n\tLogDestinations     LogDestinations `json:\"logDestinations\"`\n\tLogStructured       bool            `json:\"logStructured\"`\n\tLogFile             string          `json:\"logFile\"`\n\tSysLogPrefix        string          `json:\"sysLogPrefix\"`\n\tDumpPackets         bool            `json:\"dumpPackets\"`\n\tReadTimeout         Duration        `json:\"readTimeout\"`\n\tWriteTimeout        Duration        `json:\"writeTimeout\"`\n\tReadBufferCount     *int            `json:\"readBufferCount,omitempty\" deprecated:\"true\"`\n\tWriteQueueSize      int             `json:\"writeQueueSize\"`\n\tUDPMaxPayloadSize   int             `json:\"udpMaxPayloadSize\"`\n\tUDPReadBufferSize   uint            `json:\"udpReadBufferSize\"`\n\tRunOnConnect        string          `json:\"runOnConnect\"`\n\tRunOnConnectRestart bool            `json:\"runOnConnectRestart\"`\n\tRunOnDisconnect     string          `json:\"runOnDisconnect\"`\n\n\t// Authentication\n\tAuthMethod                AuthMethod                   `json:\"authMethod\"`\n\tAuthInternalUsers         []AuthInternalUser           `json:\"authInternalUsers\"`\n\tAuthHTTPAddress           string                       `json:\"authHTTPAddress\"`\n\tExternalAuthenticationURL *string                      `json:\"externalAuthenticationURL,omitempty\" deprecated:\"true\"`\n\tAuthHTTPFingerprint       string                       `json:\"authHTTPFingerprint\"`\n\tAuthHTTPExclude           []AuthInternalUserPermission `json:\"authHTTPExclude\"`\n\tAuthJWTJWKS               string                       `json:\"authJWTJWKS\"`\n\tAuthJWTJWKSFingerprint    string                       `json:\"authJWTJWKSFingerprint\"`\n\tAuthJWTClaimKey           string                       `json:\"authJWTClaimKey\"`\n\tAuthJWTExclude            []AuthInternalUserPermission `json:\"authJWTExclude\"`\n\tAuthJWTInHTTPQuery        bool                         `json:\"authJWTInHTTPQuery\"`\n\tAuthJWTIssuer             string                       `json:\"authJWTIssuer\"`\n\tAuthJWTAudience           string                       `json:\"authJWTAudience\"`\n\n\t// Control API\n\tAPI               bool       `json:\"api\"`\n\tAPIAddress        string     `json:\"apiAddress\"`\n\tAPIEncryption     bool       `json:\"apiEncryption\"`\n\tAPIServerKey      string     `json:\"apiServerKey\"`\n\tAPIServerCert     string     `json:\"apiServerCert\"`\n\tAPIAllowOrigin    *string    `json:\"apiAllowOrigin,omitempty\" deprecated:\"true\"`\n\tAPIAllowOrigins   []string   `json:\"apiAllowOrigins\"`\n\tAPITrustedProxies IPNetworks `json:\"apiTrustedProxies\"`\n\n\t// Metrics\n\tMetrics               bool       `json:\"metrics\"`\n\tMetricsAddress        string     `json:\"metricsAddress\"`\n\tMetricsEncryption     bool       `json:\"metricsEncryption\"`\n\tMetricsServerKey      string     `json:\"metricsServerKey\"`\n\tMetricsServerCert     string     `json:\"metricsServerCert\"`\n\tMetricsAllowOrigin    *string    `json:\"metricsAllowOrigin,omitempty\" deprecated:\"true\"`\n\tMetricsAllowOrigins   []string   `json:\"metricsAllowOrigins\"`\n\tMetricsTrustedProxies IPNetworks `json:\"metricsTrustedProxies\"`\n\n\t// PPROF\n\tPPROF               bool       `json:\"pprof\"`\n\tPPROFAddress        string     `json:\"pprofAddress\"`\n\tPPROFEncryption     bool       `json:\"pprofEncryption\"`\n\tPPROFServerKey      string     `json:\"pprofServerKey\"`\n\tPPROFServerCert     string     `json:\"pprofServerCert\"`\n\tPPROFAllowOrigin    *string    `json:\"pprofAllowOrigin,omitempty\" deprecated:\"true\"`\n\tPPROFAllowOrigins   []string   `json:\"pprofAllowOrigins\"`\n\tPPROFTrustedProxies IPNetworks `json:\"pprofTrustedProxies\"`\n\n\t// Playback\n\tPlayback               bool       `json:\"playback\"`\n\tPlaybackAddress        string     `json:\"playbackAddress\"`\n\tPlaybackEncryption     bool       `json:\"playbackEncryption\"`\n\tPlaybackServerKey      string     `json:\"playbackServerKey\"`\n\tPlaybackServerCert     string     `json:\"playbackServerCert\"`\n\tPlaybackAllowOrigin    *string    `json:\"playbackAllowOrigin,omitempty\" deprecated:\"true\"`\n\tPlaybackAllowOrigins   []string   `json:\"playbackAllowOrigins\"`\n\tPlaybackTrustedProxies IPNetworks `json:\"playbackTrustedProxies\"`\n\n\t// RTSP server\n\tRTSP                  bool             `json:\"rtsp\"`\n\tRTSPDisable           *bool            `json:\"rtspDisable,omitempty\" deprecated:\"true\"`\n\tProtocols             *RTSPTransports  `json:\"protocols,omitempty\" deprecated:\"true\"`\n\tRTSPTransports        RTSPTransports   `json:\"rtspTransports\"`\n\tEncryption            *Encryption      `json:\"encryption,omitempty\" deprecated:\"true\"`\n\tRTSPEncryption        Encryption       `json:\"rtspEncryption\"`\n\tRTSPAddress           string           `json:\"rtspAddress\"`\n\tRTSPSAddress          string           `json:\"rtspsAddress\"`\n\tRTPAddress            string           `json:\"rtpAddress\"`\n\tRTCPAddress           string           `json:\"rtcpAddress\"`\n\tMulticastIPRange      string           `json:\"multicastIPRange\"`\n\tMulticastRTPPort      int              `json:\"multicastRTPPort\"`\n\tMulticastRTCPPort     int              `json:\"multicastRTCPPort\"`\n\tSRTPAddress           string           `json:\"srtpAddress\"`\n\tSRTCPAddress          string           `json:\"srtcpAddress\"`\n\tMulticastSRTPPort     int              `json:\"multicastSRTPPort\"`\n\tMulticastSRTCPPort    int              `json:\"multicastSRTCPPort\"`\n\tServerKey             *string          `json:\"serverKey,omitempty\"`\n\tServerCert            *string          `json:\"serverCert,omitempty\"`\n\tRTSPServerKey         string           `json:\"rtspServerKey\"`\n\tRTSPServerCert        string           `json:\"rtspServerCert\"`\n\tAuthMethods           *RTSPAuthMethods `json:\"authMethods,omitempty\" deprecated:\"true\"`\n\tRTSPAuthMethods       RTSPAuthMethods  `json:\"rtspAuthMethods\"`\n\tRTSPUDPReadBufferSize *uint            `json:\"rtspUDPReadBufferSize,omitempty\" deprecated:\"true\"`\n\n\t// RTMP server\n\tRTMP           bool       `json:\"rtmp\"`\n\tRTMPDisable    *bool      `json:\"rtmpDisable,omitempty\" deprecated:\"true\"`\n\tRTMPEncryption Encryption `json:\"rtmpEncryption\"`\n\tRTMPAddress    string     `json:\"rtmpAddress\"`\n\tRTMPSAddress   string     `json:\"rtmpsAddress\"`\n\tRTMPServerKey  string     `json:\"rtmpServerKey\"`\n\tRTMPServerCert string     `json:\"rtmpServerCert\"`\n\n\t// HLS server\n\tHLS                bool       `json:\"hls\"`\n\tHLSDisable         *bool      `json:\"hlsDisable,omitempty\" deprecated:\"true\"`\n\tHLSAddress         string     `json:\"hlsAddress\"`\n\tHLSEncryption      bool       `json:\"hlsEncryption\"`\n\tHLSServerKey       string     `json:\"hlsServerKey\"`\n\tHLSServerCert      string     `json:\"hlsServerCert\"`\n\tHLSAllowOrigin     *string    `json:\"hlsAllowOrigin,omitempty\" deprecated:\"true\"`\n\tHLSAllowOrigins    []string   `json:\"hlsAllowOrigins\"`\n\tHLSTrustedProxies  IPNetworks `json:\"hlsTrustedProxies\"`\n\tHLSAlwaysRemux     bool       `json:\"hlsAlwaysRemux\"`\n\tHLSVariant         HLSVariant `json:\"hlsVariant\"`\n\tHLSSegmentCount    int        `json:\"hlsSegmentCount\"`\n\tHLSSegmentDuration Duration   `json:\"hlsSegmentDuration\"`\n\tHLSPartDuration    Duration   `json:\"hlsPartDuration\"`\n\tHLSSegmentMaxSize  StringSize `json:\"hlsSegmentMaxSize\"`\n\tHLSDirectory       string     `json:\"hlsDirectory\"`\n\tHLSMuxerCloseAfter Duration   `json:\"hlsMuxerCloseAfter\"`\n\n\t// WebRTC server\n\tWebRTC                      bool              `json:\"webrtc\"`\n\tWebRTCDisable               *bool             `json:\"webrtcDisable,omitempty\" deprecated:\"true\"`\n\tWebRTCAddress               string            `json:\"webrtcAddress\"`\n\tWebRTCEncryption            bool              `json:\"webrtcEncryption\"`\n\tWebRTCServerKey             string            `json:\"webrtcServerKey\"`\n\tWebRTCServerCert            string            `json:\"webrtcServerCert\"`\n\tWebRTCAllowOrigin           *string           `json:\"webrtcAllowOrigin,omitempty\" deprecated:\"true\"`\n\tWebRTCAllowOrigins          []string          `json:\"webrtcAllowOrigins\"`\n\tWebRTCTrustedProxies        IPNetworks        `json:\"webrtcTrustedProxies\"`\n\tWebRTCLocalUDPAddress       string            `json:\"webrtcLocalUDPAddress\"`\n\tWebRTCLocalTCPAddress       string            `json:\"webrtcLocalTCPAddress\"`\n\tWebRTCIPsFromInterfaces     bool              `json:\"webrtcIPsFromInterfaces\"`\n\tWebRTCIPsFromInterfacesList []string          `json:\"webrtcIPsFromInterfacesList\"`\n\tWebRTCAdditionalHosts       []string          `json:\"webrtcAdditionalHosts\"`\n\tWebRTCICEServers2           []WebRTCICEServer `json:\"webrtcICEServers2\"`\n\tWebRTCSTUNGatherTimeout     Duration          `json:\"webrtcSTUNGatherTimeout\"`\n\tWebRTCHandshakeTimeout      Duration          `json:\"webrtcHandshakeTimeout\"`\n\tWebRTCTrackGatherTimeout    Duration          `json:\"webrtcTrackGatherTimeout\"`\n\tWebRTCICEUDPMuxAddress      *string           `json:\"webrtcICEUDPMuxAddress,omitempty\" deprecated:\"true\"`\n\tWebRTCICETCPMuxAddress      *string           `json:\"webrtcICETCPMuxAddress,omitempty\" deprecated:\"true\"`\n\tWebRTCICEHostNAT1To1IPs     *[]string         `json:\"webrtcICEHostNAT1To1IPs,omitempty\" deprecated:\"true\"`\n\tWebRTCICEServers            *[]string         `json:\"webrtcICEServers,omitempty\" deprecated:\"true\"`\n\n\t// SRT server\n\tSRT        bool   `json:\"srt\"`\n\tSRTAddress string `json:\"srtAddress\"`\n\n\t// Record (deprecated)\n\tRecord                *bool         `json:\"record,omitempty\" deprecated:\"true\"`\n\tRecordPath            *string       `json:\"recordPath,omitempty\" deprecated:\"true\"`\n\tRecordFormat          *RecordFormat `json:\"recordFormat,omitempty\" deprecated:\"true\"`\n\tRecordPartDuration    *Duration     `json:\"recordPartDuration,omitempty\" deprecated:\"true\"`\n\tRecordSegmentDuration *Duration     `json:\"recordSegmentDuration,omitempty\" deprecated:\"true\"`\n\tRecordDeleteAfter     *Duration     `json:\"recordDeleteAfter,omitempty\" deprecated:\"true\"`\n\n\t// Path defaults\n\tPathDefaults Path `json:\"pathDefaults\"`\n\n\t// Paths\n\tOptionalPaths map[string]*OptionalPath `json:\"paths\"`\n\tPaths         map[string]*Path         `json:\"-\"` // filled by Validate()\n}\n\nfunc (conf *Conf) setDefaults() {\n\t// General\n\tconf.LogLevel = LogLevel(logger.Info)\n\tconf.LogDestinations = LogDestinations{LogDestination(logger.DestinationStdout)}\n\tconf.LogStructured = false\n\tconf.LogFile = \"mediamtx.log\"\n\tconf.SysLogPrefix = \"mediamtx\"\n\tconf.ReadTimeout = 10 * Duration(time.Second)\n\tconf.WriteTimeout = 10 * Duration(time.Second)\n\tconf.WriteQueueSize = 512\n\tconf.UDPMaxPayloadSize = 1452\n\n\t// Authentication\n\tconf.AuthMethod = AuthMethodInternal\n\tconf.AuthInternalUsers = defaultAuthInternalUsers\n\tconf.AuthHTTPExclude = []AuthInternalUserPermission{\n\t\t{\n\t\t\tAction: AuthActionAPI,\n\t\t},\n\t\t{\n\t\t\tAction: AuthActionMetrics,\n\t\t},\n\t\t{\n\t\t\tAction: AuthActionPprof,\n\t\t},\n\t}\n\tconf.AuthJWTClaimKey = \"mediamtx_permissions\"\n\tconf.AuthJWTInHTTPQuery = true\n\n\t// Control API\n\tconf.APIAddress = \":9997\"\n\tconf.APIServerKey = \"server.key\"\n\tconf.APIServerCert = \"server.crt\"\n\tconf.APIAllowOrigins = []string{\"*\"}\n\n\t// Metrics\n\tconf.MetricsAddress = \":9998\"\n\tconf.MetricsServerKey = \"server.key\"\n\tconf.MetricsServerCert = \"server.crt\"\n\tconf.MetricsAllowOrigins = []string{\"*\"}\n\n\t// PPROF\n\tconf.PPROFAddress = \":9999\"\n\tconf.PPROFServerKey = \"server.key\"\n\tconf.PPROFServerCert = \"server.crt\"\n\tconf.PPROFAllowOrigins = []string{\"*\"}\n\n\t// Playback server\n\tconf.PlaybackAddress = \":9996\"\n\tconf.PlaybackServerKey = \"server.key\"\n\tconf.PlaybackServerCert = \"server.crt\"\n\tconf.PlaybackAllowOrigins = []string{\"*\"}\n\n\t// RTSP server\n\tconf.RTSP = true\n\tconf.RTSPEncryption = EncryptionNo\n\tconf.RTSPTransports = RTSPTransports{\n\t\tgortsplib.ProtocolUDP:          {},\n\t\tgortsplib.ProtocolUDPMulticast: {},\n\t\tgortsplib.ProtocolTCP:          {},\n\t}\n\tconf.RTSPAddress = \":8554\"\n\tconf.RTSPSAddress = \":8322\"\n\tconf.RTPAddress = \":8000\"\n\tconf.RTCPAddress = \":8001\"\n\tconf.MulticastIPRange = \"224.1.0.0/16\"\n\tconf.MulticastRTPPort = 8002\n\tconf.MulticastRTCPPort = 8003\n\tconf.SRTPAddress = \":8004\"\n\tconf.SRTCPAddress = \":8005\"\n\tconf.MulticastSRTPPort = 8006\n\tconf.MulticastSRTCPPort = 8007\n\tconf.RTSPServerKey = \"server.key\"\n\tconf.RTSPServerCert = \"server.crt\"\n\tconf.RTSPAuthMethods = RTSPAuthMethods{RTSPAuthMethod(auth.VerifyMethodBasic)}\n\n\t// RTMP server\n\tconf.RTMP = true\n\tconf.RTMPEncryption = EncryptionNo\n\tconf.RTMPAddress = \":1935\"\n\tconf.RTMPSAddress = \":1936\"\n\tconf.RTMPServerKey = \"server.key\"\n\tconf.RTMPServerCert = \"server.crt\"\n\n\t// HLS\n\tconf.HLS = true\n\tconf.HLSAddress = \":8888\"\n\tconf.HLSServerKey = \"server.key\"\n\tconf.HLSServerCert = \"server.crt\"\n\tconf.HLSAllowOrigins = []string{\"*\"}\n\tconf.HLSVariant = HLSVariant(gohlslib.MuxerVariantLowLatency)\n\tconf.HLSSegmentCount = 7\n\tconf.HLSSegmentDuration = 1 * Duration(time.Second)\n\tconf.HLSPartDuration = 200 * Duration(time.Millisecond)\n\tconf.HLSSegmentMaxSize = 50 * 1024 * 1024\n\tconf.HLSMuxerCloseAfter = 60 * Duration(time.Second)\n\n\t// WebRTC server\n\tconf.WebRTC = true\n\tconf.WebRTCAddress = \":8889\"\n\tconf.WebRTCServerKey = \"server.key\"\n\tconf.WebRTCServerCert = \"server.crt\"\n\tconf.WebRTCAllowOrigins = []string{\"*\"}\n\tconf.WebRTCLocalUDPAddress = \":8189\"\n\tconf.WebRTCIPsFromInterfaces = true\n\tconf.WebRTCSTUNGatherTimeout = 5 * Duration(time.Second)\n\tconf.WebRTCHandshakeTimeout = 10 * Duration(time.Second)\n\tconf.WebRTCTrackGatherTimeout = 2 * Duration(time.Second)\n\n\t// SRT server\n\tconf.SRT = true\n\tconf.SRTAddress = \":8890\"\n\n\tconf.PathDefaults.setDefaults()\n}\n\n// Load loads a Conf.\nfunc Load(fpath string, defaultConfPaths []string, l logger.Writer) (*Conf, string, error) {\n\tconf := &Conf{}\n\n\tconf.setDefaults()\n\n\tfpath, err := conf.loadFromFile(fpath, defaultConfPaths)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\n\terr = env.Load(\"RTSP\", conf) // legacy prefix\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\n\terr = env.Load(\"MTX\", conf)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\n\t// disallow nil slices for ease of use and compatibility\n\tsetAllNilSlicesToEmptyRecursive(reflect.ValueOf(conf))\n\n\terr = conf.Validate(l)\n\tif err != nil {\n\t\treturn nil, \"\", err\n\t}\n\n\treturn conf, fpath, nil\n}\n\nfunc (conf *Conf) loadFromFile(fpath string, defaultConfPaths []string) (string, error) {\n\tif fpath == \"\" {\n\t\tfpath = firstThatExists(defaultConfPaths)\n\n\t\t// when the configuration file is not explicitly set,\n\t\t// it is optional.\n\t\tif fpath == \"\" {\n\t\t\treturn \"\", nil\n\t\t}\n\t}\n\n\tbyts, err := os.ReadFile(fpath)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif key, ok := os.LookupEnv(\"RTSP_CONFKEY\"); ok { // legacy format\n\t\tbyts, err = decrypt.Decrypt(key, byts)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\tif key, ok := os.LookupEnv(\"MTX_CONFKEY\"); ok {\n\t\tbyts, err = decrypt.Decrypt(key, byts)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\terr = yamlwrapper.Unmarshal(byts, conf)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn fpath, nil\n}\n\n// Clone clones the configuration.\nfunc (conf Conf) Clone() *Conf {\n\tcloned := deepClone(reflect.ValueOf(conf)).Interface().(Conf)\n\treturn &cloned\n}\n\n// Validate checks the configuration for errors, converts deprecated fields into new ones, fills dependent fields.\nfunc (conf *Conf) Validate(l logger.Writer) error {\n\tif l == nil {\n\t\tl = &nilLogger{}\n\t}\n\n\t// General (deprecated params)\n\n\tif conf.ReadBufferCount != nil {\n\t\tl.Log(logger.Warn, \"parameter 'readBufferCount' is deprecated and has been replaced with 'writeQueueSize'\")\n\t\tconf.WriteQueueSize = *conf.ReadBufferCount\n\t}\n\n\t// General\n\n\tif conf.ReadTimeout <= 0 {\n\t\treturn fmt.Errorf(\"'readTimeout' must be greater than zero\")\n\t}\n\n\tif conf.WriteTimeout <= 0 {\n\t\treturn fmt.Errorf(\"'writeTimeout' must be greater than zero\")\n\t}\n\n\tif conf.WriteQueueSize <= 0 {\n\t\treturn fmt.Errorf(\"'writeQueueSize' must be greater than zero\")\n\t}\n\n\tif (conf.WriteQueueSize & (conf.WriteQueueSize - 1)) != 0 {\n\t\treturn fmt.Errorf(\"'writeQueueSize' must be a power of two\")\n\t}\n\n\tif conf.UDPMaxPayloadSize > 1472 {\n\t\treturn fmt.Errorf(\"'udpMaxPayloadSize' must be less than 1472\")\n\t}\n\n\t// Authentication (deprecated params)\n\n\tif conf.ExternalAuthenticationURL != nil {\n\t\tl.Log(logger.Warn, \"parameter 'externalAuthenticationURL' is deprecated \"+\n\t\t\t\"and has been replaced with 'authMethod' and 'authHTTPAddress'\")\n\t\tconf.AuthMethod = AuthMethodHTTP\n\t\tconf.AuthHTTPAddress = *conf.ExternalAuthenticationURL\n\t}\n\n\tdeprecatedCredentialsMode := false\n\tif anyPathHasDeprecatedCredentials(conf.PathDefaults, conf.OptionalPaths) {\n\t\tl.Log(logger.Warn, \"you are using one or more authentication-related deprecated parameters \"+\n\t\t\t\"(publishUser, publishPass, publishIPs, readUser, readPass, readIPs). \"+\n\t\t\t\"These have been replaced by 'authInternalUsers'\")\n\n\t\tif conf.AuthInternalUsers != nil && !reflect.DeepEqual(conf.AuthInternalUsers, defaultAuthInternalUsers) {\n\t\t\treturn fmt.Errorf(\"authInternalUsers and legacy credentials \" +\n\t\t\t\t\"(publishUser, publishPass, publishIPs, readUser, readPass, readIPs) cannot be used together\")\n\t\t}\n\n\t\tconf.AuthInternalUsers = []AuthInternalUser{\n\t\t\t{\n\t\t\t\tUser: \"any\",\n\t\t\t\tPermissions: []AuthInternalUserPermission{\n\t\t\t\t\t{\n\t\t\t\t\t\tAction: AuthActionPlayback,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tUser: \"any\",\n\t\t\t\tIPs:  IPNetworks{mustParseCIDR(\"127.0.0.1/32\"), mustParseCIDR(\"::1/128\")},\n\t\t\t\tPermissions: []AuthInternalUserPermission{\n\t\t\t\t\t{\n\t\t\t\t\t\tAction: AuthActionAPI,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tAction: AuthActionMetrics,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tAction: AuthActionPprof,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t\tdeprecatedCredentialsMode = true\n\t}\n\n\t// Authentication\n\n\tswitch conf.AuthMethod {\n\tcase AuthMethodInternal:\n\t\tfor _, u := range conf.AuthInternalUsers {\n\t\t\t// https://github.com/bluenviron/gortsplib/blob/55556f1ecfa2bd51b29fe14eddd70512a0361cbd/server_conn.go#L155-L156\n\t\t\tif u.User == \"\" {\n\t\t\t\treturn fmt.Errorf(\"empty usernames are not supported\")\n\t\t\t}\n\n\t\t\tif u.User == \"any\" && u.Pass != \"\" {\n\t\t\t\treturn fmt.Errorf(\"using a password with 'any' user is not supported\")\n\t\t\t}\n\t\t}\n\n\tcase AuthMethodHTTP:\n\t\tif conf.AuthHTTPAddress == \"\" {\n\t\t\treturn fmt.Errorf(\"'authHTTPAddress' is empty\")\n\t\t}\n\n\t\tif conf.AuthHTTPAddress != \"\" &&\n\t\t\t!strings.HasPrefix(conf.AuthHTTPAddress, \"http://\") &&\n\t\t\t!strings.HasPrefix(conf.AuthHTTPAddress, \"https://\") {\n\t\t\treturn fmt.Errorf(\"'externalAuthenticationURL' must be a HTTP URL\")\n\t\t}\n\n\tcase AuthMethodJWT:\n\t\tif conf.AuthJWTJWKS == \"\" {\n\t\t\treturn fmt.Errorf(\"'authJWTJWKS' is empty\")\n\t\t}\n\n\t\tif conf.AuthJWTJWKS != \"\" &&\n\t\t\t!strings.HasPrefix(conf.AuthJWTJWKS, \"http://\") &&\n\t\t\t!strings.HasPrefix(conf.AuthJWTJWKS, \"https://\") {\n\t\t\treturn fmt.Errorf(\"'authJWTJWKS' must be a HTTP URL\")\n\t\t}\n\n\t\tif conf.AuthJWTClaimKey == \"\" {\n\t\t\treturn fmt.Errorf(\"'authJWTClaimKey' is empty\")\n\t\t}\n\t}\n\n\t// Control API (deprecated params)\n\n\tif conf.APIAllowOrigin != nil {\n\t\tl.Log(logger.Warn, \"parameter 'apiAllowOrigin' is deprecated and has been replaced with 'apiAllowOrigins'\")\n\t\tconf.APIAllowOrigins = []string{*conf.APIAllowOrigin}\n\t}\n\n\t// Control API\n\n\tif conf.API {\n\t\tif conf.APIAddress == \"\" {\n\t\t\treturn fmt.Errorf(\"'apiAddress' must be set when API is enabled\")\n\t\t}\n\t}\n\n\t// Metrics (deprecated params)\n\n\tif conf.MetricsAllowOrigin != nil {\n\t\tl.Log(logger.Warn, \"parameter 'metricsAllowOrigin' is deprecated and has been replaced with 'metricsAllowOrigins'\")\n\t\tconf.MetricsAllowOrigins = []string{*conf.MetricsAllowOrigin}\n\t}\n\n\t// Metrics\n\n\tif conf.Metrics {\n\t\tif conf.MetricsAddress == \"\" {\n\t\t\treturn fmt.Errorf(\"'metricsAddress' must be set when metrics are enabled\")\n\t\t}\n\t}\n\n\t// PPROF (deprecated params)\n\n\tif conf.PPROFAllowOrigin != nil {\n\t\tl.Log(logger.Warn, \"parameter 'pprofAllowOrigin' is deprecated and has been replaced with 'pprofAllowOrigins'\")\n\t\tconf.PPROFAllowOrigins = []string{*conf.PPROFAllowOrigin}\n\t}\n\n\t// PPROF\n\n\tif conf.PPROF {\n\t\tif conf.PPROFAddress == \"\" {\n\t\t\treturn fmt.Errorf(\"'pprofAddress' must be set when pprof is enabled\")\n\t\t}\n\t}\n\n\t// Playback (deprecated params)\n\n\tif conf.PlaybackAllowOrigin != nil {\n\t\tl.Log(logger.Warn, \"parameter 'playbackAllowOrigin' is deprecated and has been replaced with 'playbackAllowOrigins'\")\n\t\tconf.PlaybackAllowOrigins = []string{*conf.PlaybackAllowOrigin}\n\t}\n\n\t// Playback\n\n\tif conf.Playback {\n\t\tif conf.PlaybackAddress == \"\" {\n\t\t\treturn fmt.Errorf(\"'playbackAddress' must be set when playback is enabled\")\n\t\t}\n\t}\n\n\t// RTSP server (deprecated params)\n\n\tif conf.RTSPDisable != nil {\n\t\tl.Log(logger.Warn, \"parameter 'rtspDisabled' is deprecated and has been replaced with 'rtsp'\")\n\t\tconf.RTSP = !*conf.RTSPDisable\n\t}\n\n\tif conf.Protocols != nil {\n\t\tl.Log(logger.Warn, \"parameter 'protocols' is deprecated and has been replaced with 'rtspTransports'\")\n\t\tconf.RTSPTransports = *conf.Protocols\n\t}\n\n\tif conf.Encryption != nil {\n\t\tl.Log(logger.Warn, \"parameter 'encryption' is deprecated and has been replaced with 'rtspEncryption'\")\n\t\tconf.RTSPEncryption = *conf.Encryption\n\t}\n\n\tif conf.AuthMethods != nil {\n\t\tl.Log(logger.Warn, \"parameter 'authMethods' is deprecated and has been replaced with 'rtspAuthMethods'\")\n\t\tconf.RTSPAuthMethods = *conf.AuthMethods\n\t}\n\n\tif conf.ServerCert != nil {\n\t\tl.Log(logger.Warn, \"parameter 'serverCert' is deprecated and has been replaced with 'rtspServerCert'\")\n\t\tconf.RTSPServerCert = *conf.ServerCert\n\t}\n\n\tif conf.ServerKey != nil {\n\t\tl.Log(logger.Warn, \"parameter 'serverKey' is deprecated and has been replaced with 'rtspServerKey'\")\n\t\tconf.RTSPServerKey = *conf.ServerKey\n\t}\n\n\t// RTSP server\n\n\tif conf.RTSP {\n\t\tif conf.RTSPEncryption == EncryptionNo || conf.RTSPEncryption == EncryptionOptional {\n\t\t\tif conf.RTSPAddress == \"\" {\n\t\t\t\treturn fmt.Errorf(\"'rtspAddress' must be set when RTSP is enabled and RTSP encryption is 'no' or 'optional'\")\n\t\t\t}\n\n\t\t\tif _, ok := conf.RTSPTransports[gortsplib.ProtocolUDP]; ok {\n\t\t\t\tif conf.RTPAddress == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"'rtpAddress' must be set when UDP is enabled and RTSP encryption is 'no' or 'optional'\")\n\t\t\t\t}\n\t\t\t\tif conf.RTCPAddress == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"'rtcpAddress' must be set when UDP is enabled and RTSP encryption is 'no' or 'optional'\")\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif _, ok := conf.RTSPTransports[gortsplib.ProtocolUDPMulticast]; ok {\n\t\t\t\tif conf.MulticastIPRange == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"'multicastIPRange' must be set when UDP multicast is enabled\" +\n\t\t\t\t\t\t\" and RTSP encryption is 'no' or 'optional'\")\n\t\t\t\t}\n\t\t\t\tif conf.MulticastRTPPort == 0 {\n\t\t\t\t\treturn fmt.Errorf(\"'multicastRTPPort' must be set when UDP multicast is enabled\" +\n\t\t\t\t\t\t\" and RTSP encryption is 'no' or 'optional'\")\n\t\t\t\t}\n\t\t\t\tif conf.MulticastRTCPPort == 0 {\n\t\t\t\t\treturn fmt.Errorf(\"'multicastRTCPPort' must be set when UDP multicast is enabled\" +\n\t\t\t\t\t\t\" and RTSP encryption is 'no' or 'optional'\")\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif conf.RTSPEncryption == EncryptionOptional || conf.RTSPEncryption == EncryptionStrict {\n\t\t\tif conf.RTSPSAddress == \"\" {\n\t\t\t\treturn fmt.Errorf(\"'rtspsAddress' must be set when RTSP is enabled and RTSP encryption is 'optional' or 'strict'\")\n\t\t\t}\n\n\t\t\tif _, ok := conf.RTSPTransports[gortsplib.ProtocolUDP]; ok {\n\t\t\t\tif conf.SRTPAddress == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"'srtpAddress' must be set when UDP is enabled\" +\n\t\t\t\t\t\t\" and RTSP encryption is 'optional' or 'strict'\")\n\t\t\t\t}\n\t\t\t\tif conf.SRTCPAddress == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"'srtcpAddress' must be set when UDP is enabled\" +\n\t\t\t\t\t\t\" and RTSP encryption is 'optional' or 'strict'\")\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif _, ok := conf.RTSPTransports[gortsplib.ProtocolUDPMulticast]; ok {\n\t\t\t\tif conf.MulticastIPRange == \"\" {\n\t\t\t\t\treturn fmt.Errorf(\"'multicastIPRange' must be set when UDP multicast is enabled\" +\n\t\t\t\t\t\t\" and RTSP encryption is 'optional' or 'strict'\")\n\t\t\t\t}\n\t\t\t\tif conf.MulticastSRTPPort == 0 {\n\t\t\t\t\treturn fmt.Errorf(\"'multicastSRTPPort' must be set when UDP multicast is enabled\" +\n\t\t\t\t\t\t\" and RTSP encryption is 'optional' or 'strict'\")\n\t\t\t\t}\n\t\t\t\tif conf.MulticastSRTCPPort == 0 {\n\t\t\t\t\treturn fmt.Errorf(\"'multicastSRTCPPort' must be set when UDP multicast is enabled\" +\n\t\t\t\t\t\t\" and RTSP encryption is 'optional' or 'strict'\")\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif len(conf.RTSPAuthMethods) == 0 {\n\t\t\treturn fmt.Errorf(\"at least one 'rtspAuthMethods' must be provided\")\n\t\t}\n\n\t\tif slices.Contains(conf.RTSPAuthMethods, RTSPAuthMethod(auth.VerifyMethodDigestMD5)) {\n\t\t\tif conf.AuthMethod != AuthMethodInternal {\n\t\t\t\treturn fmt.Errorf(\"when RTSP digest is enabled, the only supported auth method is 'internal'\")\n\t\t\t}\n\t\t\tfor _, user := range conf.AuthInternalUsers {\n\t\t\t\tif user.User.IsHashed() || user.Pass.IsHashed() {\n\t\t\t\t\treturn fmt.Errorf(\"when RTSP digest is enabled, hashed credentials cannot be used\")\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// RTMP (deprecated params)\n\n\tif conf.RTMPDisable != nil {\n\t\tl.Log(logger.Warn, \"parameter 'rtmpDisabled' is deprecated and has been replaced with 'rtmp'\")\n\t\tconf.RTMP = !*conf.RTMPDisable\n\t}\n\n\t// RTMP\n\n\tif conf.RTMP {\n\t\tif conf.RTMPAddress == \"\" {\n\t\t\treturn fmt.Errorf(\"'rtmpAddress' must be set when RTMP is enabled\")\n\t\t}\n\t}\n\n\t// HLS (deprecated params)\n\n\tif conf.HLSDisable != nil {\n\t\tl.Log(logger.Warn, \"parameter 'hlsDisable' is deprecated and has been replaced with 'hls'\")\n\t\tconf.HLS = !*conf.HLSDisable\n\t}\n\n\tif conf.HLSAllowOrigin != nil {\n\t\tl.Log(logger.Warn, \"parameter 'hlsAllowOrigin' is deprecated and has been replaced with 'hlsAllowOrigins'\")\n\t\tconf.HLSAllowOrigins = []string{*conf.HLSAllowOrigin}\n\t}\n\n\t// HLS\n\n\tif conf.HLS {\n\t\tif conf.HLSAddress == \"\" {\n\t\t\treturn fmt.Errorf(\"'hlsAddress' must be set when HLS is enabled\")\n\t\t}\n\t}\n\n\t// WebRTC (deprecated params)\n\n\tif conf.WebRTCDisable != nil {\n\t\tl.Log(logger.Warn, \"parameter 'webrtcDisable' is deprecated and has been replaced with 'webrtc'\")\n\t\tconf.WebRTC = !*conf.WebRTCDisable\n\t}\n\n\tif conf.WebRTCICEUDPMuxAddress != nil {\n\t\tl.Log(logger.Warn, \"parameter 'webrtcICEUDPMuxAdderss' is deprecated \"+\n\t\t\t\"and has been replaced with 'webrtcLocalUDPAddress'\")\n\t\tconf.WebRTCLocalUDPAddress = *conf.WebRTCICEUDPMuxAddress\n\t}\n\n\tif conf.WebRTCICETCPMuxAddress != nil {\n\t\tl.Log(logger.Warn, \"parameter 'webrtcICETCPMuxAddress' is deprecated \"+\n\t\t\t\"and has been replaced with 'webrtcLocalTCPAddress'\")\n\t\tconf.WebRTCLocalTCPAddress = *conf.WebRTCICETCPMuxAddress\n\t}\n\n\tif conf.WebRTCICEHostNAT1To1IPs != nil {\n\t\tl.Log(logger.Warn, \"parameter 'webrtcICEHostNAT1To1IPs' is deprecated \"+\n\t\t\t\"and has been replaced with 'webrtcAdditionalHosts'\")\n\t\tconf.WebRTCAdditionalHosts = *conf.WebRTCICEHostNAT1To1IPs\n\t}\n\n\tif conf.WebRTCICEServers != nil {\n\t\tl.Log(logger.Warn, \"parameter 'webrtcICEServers' is deprecated \"+\n\t\t\t\"and has been replaced with 'webrtcICEServers2'\")\n\n\t\tfor _, server := range *conf.WebRTCICEServers {\n\t\t\tparts := strings.Split(server, \":\")\n\t\t\tif len(parts) == 5 {\n\t\t\t\tconf.WebRTCICEServers2 = append(conf.WebRTCICEServers2, WebRTCICEServer{\n\t\t\t\t\tURL:      parts[0] + \":\" + parts[3] + \":\" + parts[4],\n\t\t\t\t\tUsername: parts[1],\n\t\t\t\t\tPassword: parts[2],\n\t\t\t\t})\n\t\t\t} else {\n\t\t\t\tconf.WebRTCICEServers2 = append(conf.WebRTCICEServers2, WebRTCICEServer{\n\t\t\t\t\tURL: server,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\tif conf.WebRTCAllowOrigin != nil {\n\t\tl.Log(logger.Warn, \"parameter 'webrtcAllowOrigin' is deprecated and has been replaced with 'webrtcAllowOrigins'\")\n\t\tconf.WebRTCAllowOrigins = []string{*conf.WebRTCAllowOrigin}\n\t}\n\n\t// WebRTC\n\n\tif conf.WebRTC {\n\t\tif conf.WebRTCAddress == \"\" {\n\t\t\treturn fmt.Errorf(\"'webrtcAddress' must be set when WebRTC is enabled\")\n\t\t}\n\n\t\tfor _, server := range conf.WebRTCICEServers2 {\n\t\t\tif !strings.HasPrefix(server.URL, \"stun:\") &&\n\t\t\t\t!strings.HasPrefix(server.URL, \"turn:\") &&\n\t\t\t\t!strings.HasPrefix(server.URL, \"turns:\") {\n\t\t\t\treturn fmt.Errorf(\"invalid ICE server: '%s'\", server.URL)\n\t\t\t}\n\t\t}\n\n\t\tif conf.WebRTCLocalUDPAddress == \"\" &&\n\t\t\tconf.WebRTCLocalTCPAddress == \"\" &&\n\t\t\tlen(conf.WebRTCICEServers2) == 0 {\n\t\t\treturn fmt.Errorf(\"at least one between 'webrtcLocalUDPAddress',\" +\n\t\t\t\t\" 'webrtcLocalTCPAddress' or 'webrtcICEServers2' must be filled\")\n\t\t}\n\n\t\tif conf.WebRTCLocalUDPAddress != \"\" || conf.WebRTCLocalTCPAddress != \"\" {\n\t\t\tif !conf.WebRTCIPsFromInterfaces && len(conf.WebRTCAdditionalHosts) == 0 {\n\t\t\t\treturn fmt.Errorf(\"at least one between 'webrtcIPsFromInterfaces' or 'webrtcAdditionalHosts' must be filled\")\n\t\t\t}\n\t\t}\n\t}\n\n\t// Record (deprecated)\n\n\tif conf.Record != nil {\n\t\tl.Log(logger.Warn, \"parameter 'record' is deprecated \"+\n\t\t\t\"and has been replaced with 'pathDefaults.record'\")\n\t\tconf.PathDefaults.Record = *conf.Record\n\t}\n\n\tif conf.RecordPath != nil {\n\t\tl.Log(logger.Warn, \"parameter 'recordPath' is deprecated \"+\n\t\t\t\"and has been replaced with 'pathDefaults.recordPath'\")\n\t\tconf.PathDefaults.RecordPath = *conf.RecordPath\n\t}\n\n\tif conf.RecordFormat != nil {\n\t\tl.Log(logger.Warn, \"parameter 'recordFormat' is deprecated \"+\n\t\t\t\"and has been replaced with 'pathDefaults.recordFormat'\")\n\t\tconf.PathDefaults.RecordFormat = *conf.RecordFormat\n\t}\n\n\tif conf.RecordPartDuration != nil {\n\t\tl.Log(logger.Warn, \"parameter 'recordPartDuration' is deprecated \"+\n\t\t\t\"and has been replaced with 'pathDefaults.recordPartDuration'\")\n\t\tconf.PathDefaults.RecordPartDuration = *conf.RecordPartDuration\n\t}\n\n\tif conf.RecordSegmentDuration != nil {\n\t\tl.Log(logger.Warn, \"parameter 'recordSegmentDuration' is deprecated \"+\n\t\t\t\"and has been replaced with 'pathDefaults.recordSegmentDuration'\")\n\t\tconf.PathDefaults.RecordSegmentDuration = *conf.RecordSegmentDuration\n\t}\n\n\tif conf.RecordDeleteAfter != nil {\n\t\tl.Log(logger.Warn, \"parameter 'recordDeleteAfter' is deprecated \"+\n\t\t\t\"and has been replaced with 'pathDefaults.recordDeleteAfter'\")\n\t\tconf.PathDefaults.RecordDeleteAfter = *conf.RecordDeleteAfter\n\t}\n\n\t// paths\n\n\thasAllOthers := false\n\tfor name := range conf.OptionalPaths {\n\t\tif name == \"all\" || name == \"all_others\" || name == \"~^.*$\" {\n\t\t\tif hasAllOthers {\n\t\t\t\treturn fmt.Errorf(\"all_others, all and '~^.*$' are aliases\")\n\t\t\t}\n\t\t\thasAllOthers = true\n\t\t}\n\t}\n\n\tconf.Paths = make(map[string]*Path)\n\n\tfor _, name := range sortedKeys(conf.OptionalPaths) {\n\t\toptional := conf.OptionalPaths[name]\n\t\tif optional == nil {\n\t\t\toptional = &OptionalPath{\n\t\t\t\tValues: newOptionalPathValues(),\n\t\t\t}\n\t\t\tconf.OptionalPaths[name] = optional\n\t\t}\n\n\t\tpconf := newPath(&conf.PathDefaults, optional)\n\t\tconf.Paths[name] = pconf\n\t}\n\n\tfor _, name := range sortedKeys(conf.OptionalPaths) {\n\t\terr := conf.Paths[name].validate(conf, name, deprecatedCredentialsMode, l)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Global returns the global part of Conf.\nfunc (conf *Conf) Global() *Global {\n\tg := &Global{\n\t\tValues: newGlobalValues(),\n\t}\n\tcopyStructFields(g.Values, conf)\n\treturn g\n}\n\n// PatchGlobal patches the global configuration.\nfunc (conf *Conf) PatchGlobal(optional *OptionalGlobal) {\n\tcopyStructFields(conf, optional.Values)\n}\n\n// PatchPathDefaults patches path default settings.\nfunc (conf *Conf) PatchPathDefaults(optional *OptionalPath) {\n\tcopyStructFields(&conf.PathDefaults, optional.Values)\n}\n\n// AddPath adds a path.\nfunc (conf *Conf) AddPath(name string, p *OptionalPath) error {\n\tif _, ok := conf.OptionalPaths[name]; ok {\n\t\treturn fmt.Errorf(\"path already exists\")\n\t}\n\n\tif conf.OptionalPaths == nil {\n\t\tconf.OptionalPaths = make(map[string]*OptionalPath)\n\t}\n\n\tconf.OptionalPaths[name] = p\n\treturn nil\n}\n\n// PatchPath patches a path.\nfunc (conf *Conf) PatchPath(name string, optional2 *OptionalPath) error {\n\toptional, ok := conf.OptionalPaths[name]\n\tif !ok {\n\t\treturn ErrPathNotFound\n\t}\n\n\tcopyStructFields(optional.Values, optional2.Values)\n\treturn nil\n}\n\n// ReplacePath replaces a path.\nfunc (conf *Conf) ReplacePath(name string, optional2 *OptionalPath) error {\n\tif conf.OptionalPaths == nil {\n\t\tconf.OptionalPaths = make(map[string]*OptionalPath)\n\t}\n\n\tconf.OptionalPaths[name] = optional2\n\treturn nil\n}\n\n// RemovePath removes a path.\nfunc (conf *Conf) RemovePath(name string) error {\n\tif _, ok := conf.OptionalPaths[name]; !ok {\n\t\treturn ErrPathNotFound\n\t}\n\n\tdelete(conf.OptionalPaths, name)\n\treturn nil\n}\n"
  },
  {
    "path": "internal/conf/conf_test.go",
    "content": "package conf\n\nimport (\n\t\"crypto/rand\"\n\t\"encoding/base64\"\n\t\"io\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5\"\n\t\"github.com/stretchr/testify/require\"\n\t\"golang.org/x/crypto/nacl/secretbox\"\n\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n)\n\nfunc createTempFile(byts []byte) (string, error) {\n\ttmpf, err := os.CreateTemp(os.TempDir(), \"rtsp-\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer tmpf.Close()\n\n\t_, err = tmpf.Write(byts)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn tmpf.Name(), nil\n}\n\nfunc TestConfFromFile(t *testing.T) {\n\tfunc() {\n\t\ttmpf, err := createTempFile([]byte(\"logLevel: debug\\n\" +\n\t\t\t\"paths:\\n\" +\n\t\t\t\"  cam1:\\n\" +\n\t\t\t\"    runOnDemandStartTimeout: 5s\\n\"))\n\t\trequire.NoError(t, err)\n\t\tdefer os.Remove(tmpf)\n\n\t\tconf, confPath, err := Load(tmpf, nil, nil)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, tmpf, confPath)\n\n\t\trequire.Equal(t, LogLevel(logger.Debug), conf.LogLevel)\n\n\t\tpa, ok := conf.Paths[\"cam1\"]\n\t\trequire.Equal(t, true, ok)\n\t\trequire.Equal(t, &Path{\n\t\t\tName:                         \"cam1\",\n\t\t\tSource:                       \"publisher\",\n\t\t\tSourceOnDemandStartTimeout:   10 * Duration(time.Second),\n\t\t\tSourceOnDemandCloseAfter:     10 * Duration(time.Second),\n\t\t\tOverridePublisher:            true,\n\t\t\tAlwaysAvailableTracks:        []AlwaysAvailableTrack{},\n\t\t\tRecordPath:                   \"./recordings/%path/%Y-%m-%d_%H-%M-%S-%f\",\n\t\t\tRecordFormat:                 RecordFormatFMP4,\n\t\t\tRecordPartDuration:           Duration(1 * time.Second),\n\t\t\tRecordMaxPartSize:            50 * 1024 * 1024,\n\t\t\tRecordSegmentDuration:        3600000000000,\n\t\t\tRecordDeleteAfter:            86400000000000,\n\t\t\tRTSPUDPSourcePortRange:       []uint{10000, 65535},\n\t\t\tWHEPSTUNGatherTimeout:        5 * Duration(time.Second),\n\t\t\tWHEPHandshakeTimeout:         10 * Duration(time.Second),\n\t\t\tWHEPTrackGatherTimeout:       2 * Duration(time.Second),\n\t\t\tRPICameraWidth:               1920,\n\t\t\tRPICameraHeight:              1080,\n\t\t\tRPICameraContrast:            1,\n\t\t\tRPICameraSaturation:          1,\n\t\t\tRPICameraSharpness:           1,\n\t\t\tRPICameraExposure:            \"normal\",\n\t\t\tRPICameraAWB:                 \"auto\",\n\t\t\tRPICameraAWBGains:            []float64{0, 0},\n\t\t\tRPICameraDenoise:             \"off\",\n\t\t\tRPICameraMetering:            \"centre\",\n\t\t\tRPICameraFPS:                 30,\n\t\t\tRPICameraAfMode:              \"continuous\",\n\t\t\tRPICameraAfRange:             \"normal\",\n\t\t\tRPICameraAfSpeed:             \"normal\",\n\t\t\tRPICameraTextOverlay:         \"%Y-%m-%d %H:%M:%S - MediaMTX\",\n\t\t\tRPICameraCodec:               \"auto\",\n\t\t\tRPICameraIDRPeriod:           60,\n\t\t\tRPICameraBitrate:             5000000,\n\t\t\tRPICameraHardwareH264Profile: \"main\",\n\t\t\tRPICameraHardwareH264Level:   \"4.1\",\n\t\t\tRPICameraSoftwareH264Profile: \"baseline\",\n\t\t\tRPICameraSoftwareH264Level:   \"4.1\",\n\t\t\tRPICameraMJPEGQuality:        60,\n\t\t\tRunOnDemandStartTimeout:      5 * Duration(time.Second),\n\t\t\tRunOnDemandCloseAfter:        10 * Duration(time.Second),\n\t\t}, pa)\n\t}()\n\n\tfunc() {\n\t\ttmpf, err := createTempFile([]byte(``))\n\t\trequire.NoError(t, err)\n\t\tdefer os.Remove(tmpf)\n\n\t\t_, _, err = Load(tmpf, nil, nil)\n\t\trequire.NoError(t, err)\n\t}()\n\n\tfunc() {\n\t\ttmpf, err := createTempFile([]byte(`paths:`))\n\t\trequire.NoError(t, err)\n\t\tdefer os.Remove(tmpf)\n\n\t\t_, _, err = Load(tmpf, nil, nil)\n\t\trequire.NoError(t, err)\n\t}()\n\n\tfunc() {\n\t\ttmpf, err := createTempFile([]byte(\n\t\t\t\"paths:\\n\" +\n\t\t\t\t\"  mypath:\\n\"))\n\t\trequire.NoError(t, err)\n\t\tdefer os.Remove(tmpf)\n\n\t\t_, _, err = Load(tmpf, nil, nil)\n\t\trequire.NoError(t, err)\n\t}()\n}\n\nfunc TestConfFromFileAndEnv(t *testing.T) {\n\t// global parameter\n\tt.Setenv(\"RTSP_PROTOCOLS\", \"tcp\")\n\n\t// path parameter\n\tt.Setenv(\"MTX_PATHS_CAM1_SOURCE\", \"rtsp://testing\")\n\n\t// deprecated global parameter\n\tt.Setenv(\"MTX_RTMPDISABLE\", \"yes\")\n\n\t// deprecated path parameter\n\tt.Setenv(\"MTX_PATHS_CAM2_DISABLEPUBLISHEROVERRIDE\", \"yes\")\n\n\ttmpf, err := createTempFile([]byte(\"{}\"))\n\trequire.NoError(t, err)\n\tdefer os.Remove(tmpf)\n\n\tconf, confPath, err := Load(tmpf, nil, nil)\n\trequire.NoError(t, err)\n\trequire.Equal(t, tmpf, confPath)\n\n\trequire.Equal(t, RTSPTransports{gortsplib.ProtocolTCP: {}}, conf.RTSPTransports)\n\trequire.Equal(t, false, conf.RTMP)\n\n\tpa, ok := conf.Paths[\"cam1\"]\n\trequire.Equal(t, true, ok)\n\trequire.Equal(t, \"rtsp://testing\", pa.Source)\n\n\tpa, ok = conf.Paths[\"cam2\"]\n\trequire.Equal(t, true, ok)\n\trequire.Equal(t, false, pa.OverridePublisher)\n}\n\nfunc TestConfFromEnvOnly(t *testing.T) {\n\tt.Setenv(\"MTX_PATHS_CAM1_SOURCE\", \"rtsp://testing\")\n\n\tconf, confPath, err := Load(\"\", nil, nil)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"\", confPath)\n\n\tpa, ok := conf.Paths[\"cam1\"]\n\trequire.Equal(t, true, ok)\n\trequire.Equal(t, \"rtsp://testing\", pa.Source)\n}\n\nfunc TestConfEncryption(t *testing.T) {\n\tkey := \"testing123testin\"\n\tplaintext := \"paths:\\n\" +\n\t\t\"  path1:\\n\" +\n\t\t\"  path2:\\n\"\n\n\tencryptedConf := func() string {\n\t\tvar secretKey [32]byte\n\t\tcopy(secretKey[:], key)\n\n\t\tvar nonce [24]byte\n\t\t_, err := io.ReadFull(rand.Reader, nonce[:])\n\t\trequire.NoError(t, err)\n\n\t\tencrypted := secretbox.Seal(nonce[:], []byte(plaintext), &nonce, &secretKey)\n\t\treturn base64.StdEncoding.EncodeToString(encrypted)\n\t}()\n\n\tt.Setenv(\"RTSP_CONFKEY\", key)\n\n\ttmpf, err := createTempFile([]byte(encryptedConf))\n\trequire.NoError(t, err)\n\tdefer os.Remove(tmpf)\n\n\tconf, confPath, err := Load(tmpf, nil, nil)\n\trequire.NoError(t, err)\n\trequire.Equal(t, tmpf, confPath)\n\n\t_, ok := conf.Paths[\"path1\"]\n\trequire.Equal(t, true, ok)\n\n\t_, ok = conf.Paths[\"path2\"]\n\trequire.Equal(t, true, ok)\n}\n\nfunc TestConfDeprecatedAuth(t *testing.T) {\n\ttmpf, err := createTempFile([]byte(\n\t\t\"paths:\\n\" +\n\t\t\t\"  cam:\\n\" +\n\t\t\t\"    readUser: myuser\\n\" +\n\t\t\t\"    readPass: mypass\\n\"))\n\trequire.NoError(t, err)\n\tdefer os.Remove(tmpf)\n\n\tconf, _, err := Load(tmpf, nil, nil)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, []AuthInternalUser{\n\t\t{\n\t\t\tUser: \"any\",\n\t\t\tPermissions: []AuthInternalUserPermission{\n\t\t\t\t{\n\t\t\t\t\tAction: AuthActionPlayback,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tUser: \"any\",\n\t\t\tIPs:  IPNetworks{mustParseCIDR(\"127.0.0.1/32\"), mustParseCIDR(\"::1/128\")},\n\t\t\tPermissions: []AuthInternalUserPermission{\n\t\t\t\t{\n\t\t\t\t\tAction: AuthActionAPI,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tAction: AuthActionMetrics,\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tAction: AuthActionPprof,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tUser: \"any\",\n\t\t\tIPs:  IPNetworks{mustParseCIDR(\"0.0.0.0/0\")},\n\t\t\tPermissions: []AuthInternalUserPermission{\n\t\t\t\t{\n\t\t\t\t\tAction: AuthActionPublish,\n\t\t\t\t\tPath:   \"cam\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tUser: \"myuser\",\n\t\t\tPass: \"mypass\",\n\t\t\tIPs:  IPNetworks{mustParseCIDR(\"0.0.0.0/0\")},\n\t\t\tPermissions: []AuthInternalUserPermission{\n\t\t\t\t{\n\t\t\t\t\tAction: AuthActionRead,\n\t\t\t\t\tPath:   \"cam\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}, conf.AuthInternalUsers)\n}\n\nfunc TestConfErrors(t *testing.T) {\n\tfor _, ca := range []struct {\n\t\tname string\n\t\tconf string\n\t\terr  string\n\t}{\n\t\t{\n\t\t\t\"duplicate parameter\",\n\t\t\t\"paths:\\n\" +\n\t\t\t\t\"paths:\\n\",\n\t\t\t\"[2:1] mapping key \\\"paths\\\" already defined at [1:1]\\n   1 |  null\\n>  2 | paths:\\n       ^\\n\",\n\t\t},\n\t\t{\n\t\t\t\"non existent parameter\",\n\t\t\t`invalid: param`,\n\t\t\t\"json: unknown field \\\"invalid\\\"\",\n\t\t},\n\t\t{\n\t\t\t\"invalid readTimeout\",\n\t\t\t\"readTimeout: 0s\\n\",\n\t\t\t\"'readTimeout' must be greater than zero\",\n\t\t},\n\t\t{\n\t\t\t\"invalid writeTimeout\",\n\t\t\t\"writeTimeout: 0s\\n\",\n\t\t\t\"'writeTimeout' must be greater than zero\",\n\t\t},\n\t\t{\n\t\t\t\"invalid writeQueueSize 1\",\n\t\t\t\"writeQueueSize: 0\\n\",\n\t\t\t\"'writeQueueSize' must be greater than zero\",\n\t\t},\n\t\t{\n\t\t\t\"invalid writeQueueSize 2\",\n\t\t\t\"writeQueueSize: 1001\\n\",\n\t\t\t\"'writeQueueSize' must be a power of two\",\n\t\t},\n\t\t{\n\t\t\t\"invalid udpMaxPayloadSize\",\n\t\t\t\"udpMaxPayloadSize: 5000\\n\",\n\t\t\t\"'udpMaxPayloadSize' must be less than 1472\",\n\t\t},\n\t\t{\n\t\t\t\"invalid ICE server\",\n\t\t\t\"webrtcICEServers: [testing]\\n\",\n\t\t\t\"invalid ICE server: 'testing'\",\n\t\t},\n\t\t{\n\t\t\t\"non existent parameter in path\",\n\t\t\t\"paths:\\n\" +\n\t\t\t\t\"  mypath:\\n\" +\n\t\t\t\t\"    invalid: parameter\\n\",\n\t\t\t\"json: unknown field \\\"invalid\\\"\",\n\t\t},\n\t\t{\n\t\t\t\"non existent parameter in auth\",\n\t\t\t\"authInternalUsers:\\n\" +\n\t\t\t\t\"- users: test\\n\",\n\t\t\t\"json: unknown field \\\"authInternalUsers[0].users\\\"\",\n\t\t},\n\t\t{\n\t\t\t\"invalid path name\",\n\t\t\t\"paths:\\n\" +\n\t\t\t\t\"  '':\\n\" +\n\t\t\t\t\"    source: publisher\\n\",\n\t\t\t\"invalid path name '': cannot be empty\",\n\t\t},\n\t\t{\n\t\t\t\"double raspberry pi camera\",\n\t\t\t\"paths:\\n\" +\n\t\t\t\t\"  cam1:\\n\" +\n\t\t\t\t\"    source: rpiCamera\\n\" +\n\t\t\t\t\"  cam2:\\n\" +\n\t\t\t\t\"    source: rpiCamera\\n\",\n\t\t\t\"'rpiCamera' with same camera ID 0 is used as source in two paths, 'cam1' and 'cam2'\",\n\t\t},\n\t\t{\n\t\t\t\"invalid srt publish passphrase\",\n\t\t\t\"paths:\\n\" +\n\t\t\t\t\"  mypath:\\n\" +\n\t\t\t\t\"    srtPublishPassphrase: a\\n\",\n\t\t\t`invalid 'srtPublishPassphrase': must be between 10 and 79 characters`,\n\t\t},\n\t\t{\n\t\t\t\"invalid srt read passphrase\",\n\t\t\t\"paths:\\n\" +\n\t\t\t\t\"  mypath:\\n\" +\n\t\t\t\t\"    srtReadPassphrase: a\\n\",\n\t\t\t`invalid 'readRTPassphrase': must be between 10 and 79 characters`,\n\t\t},\n\t\t{\n\t\t\t\"all_others aliases\",\n\t\t\t\"paths:\\n\" +\n\t\t\t\t\"  all:\\n\" +\n\t\t\t\t\"  all_others:\\n\",\n\t\t\t`all_others, all and '~^.*$' are aliases`,\n\t\t},\n\t\t{\n\t\t\t\"all_others aliases\",\n\t\t\t\"paths:\\n\" +\n\t\t\t\t\"  all_others:\\n\" +\n\t\t\t\t\"  ~^.*$:\\n\",\n\t\t\t`all_others, all and '~^.*$' are aliases`,\n\t\t},\n\t\t{\n\t\t\t\"jwt jwks empty\",\n\t\t\t\"authMethod: jwt\\n\" +\n\t\t\t\t\"authJWTJWKS: \\\"\\\"\\n\" +\n\t\t\t\t\"authJWTClaimKey: test\",\n\t\t\t\"'authJWTJWKS' is empty\",\n\t\t},\n\t\t{\n\t\t\t\"invalid jwt jwks url\",\n\t\t\t\"authMethod: jwt\\n\" +\n\t\t\t\t\"authJWTJWKS: ftp://invalid\\n\" +\n\t\t\t\t\"authJWTClaimKey: test\",\n\t\t\t\"'authJWTJWKS' must be a HTTP URL\",\n\t\t},\n\t\t{\n\t\t\t\"jwt claim key empty\",\n\t\t\t\"authMethod: jwt\\n\" +\n\t\t\t\t\"authJWTJWKS: https://not-real.com\\n\" +\n\t\t\t\t\"authJWTClaimKey: \\\"\\\"\",\n\t\t\t\"'authJWTClaimKey' is empty\",\n\t\t},\n\t\t{\n\t\t\t\"http auth address empty\",\n\t\t\t\"authMethod: http\\n\" +\n\t\t\t\t\"authHTTPAddress: \\\"\\\"\",\n\t\t\t\"'authHTTPAddress' is empty\",\n\t\t},\n\t\t{\n\t\t\t\"invalid http auth address\",\n\t\t\t\"authMethod: http\\n\" +\n\t\t\t\t\"authHTTPAddress: ftp://invalid\",\n\t\t\t\"'externalAuthenticationURL' must be a HTTP URL\",\n\t\t},\n\t\t{\n\t\t\t\"invalid rtsp auth methods\",\n\t\t\t\"rtspAuthMethods: []\",\n\t\t\t\"at least one 'rtspAuthMethods' must be provided\",\n\t\t},\n\t\t{\n\t\t\t\"rtsp digest with non-internal auth\",\n\t\t\t\"authMethod: http\\n\" +\n\t\t\t\t\"authHTTPAddress: http://localhost:9000\\n\" +\n\t\t\t\t\"rtspAuthMethods: [digest]\\n\",\n\t\t\t\"when RTSP digest is enabled, the only supported auth method is 'internal'\",\n\t\t},\n\t\t{\n\t\t\t\"rtsp digest with hashed credentials\",\n\t\t\t\"rtspAuthMethods: [digest]\\n\" +\n\t\t\t\t\"authInternalUsers:\\n\" +\n\t\t\t\t\"- user: sha256:test\\n\" +\n\t\t\t\t\"  pass: test\\n\" +\n\t\t\t\t\"  permissions:\\n\" +\n\t\t\t\t\"  - action: publish\\n\",\n\t\t\t\"when RTSP digest is enabled, hashed credentials cannot be used\",\n\t\t},\n\t\t{\n\t\t\t\"invalid fallback\",\n\t\t\t\"paths:\\n\" +\n\t\t\t\t\"  my_path:\\n\" +\n\t\t\t\t\"    fallback: invalid://invalid\",\n\t\t\t`'invalid://invalid' is not a valid RTSP URL`,\n\t\t},\n\t\t{\n\t\t\t\"invalid source redirect\",\n\t\t\t\"paths:\\n\" +\n\t\t\t\t\"  my_path:\\n\" +\n\t\t\t\t\"    source: redirect\\n\" +\n\t\t\t\t\"    sourceRedirect: invalid://invalid\",\n\t\t\t`'invalid://invalid' is not a valid RTSP URL`,\n\t\t},\n\t\t{\n\t\t\t\"useless source redirect\",\n\t\t\t\"paths:\\n\" +\n\t\t\t\t\"  my_path:\\n\" +\n\t\t\t\t\"    sourceRedirect: invalid://invalid\",\n\t\t\t`'sourceRedirect' is useless when source is not 'redirect'`,\n\t\t},\n\t\t{\n\t\t\t\"invalid user\",\n\t\t\t\"authInternalUsers:\\n\" +\n\t\t\t\t\"- user:\\n\" +\n\t\t\t\t\"  pass: test\\n\" +\n\t\t\t\t\"  permissions:\\n\" +\n\t\t\t\t\"  - action: publish\\n\",\n\t\t\t\"empty usernames are not supported\",\n\t\t},\n\t\t{\n\t\t\t\"invalid pass\",\n\t\t\t\"authInternalUsers:\\n\" +\n\t\t\t\t\"- user: any\\n\" +\n\t\t\t\t\"  pass: test\\n\" +\n\t\t\t\t\"  permissions:\\n\" +\n\t\t\t\t\"  - action: publish\\n\",\n\t\t\t`using a password with 'any' user is not supported`,\n\t\t},\n\t\t{\n\t\t\t\"invalid record path 1\",\n\t\t\t\"paths:\\n\" +\n\t\t\t\t\"  my_path:\\n\" +\n\t\t\t\t\"    recordPath: invalid\\n\",\n\t\t\t`'recordPath' must contain %path`,\n\t\t},\n\t\t{\n\t\t\t\"invalid record path 2\",\n\t\t\t\"paths:\\n\" +\n\t\t\t\t\"  my_path:\\n\" +\n\t\t\t\t\"    recordPath: '%path/invalid'\\n\",\n\t\t\t`'recordPath' must contain either %s or %Y %m %d %H %M %S`,\n\t\t},\n\t\t{\n\t\t\t\"invalid record path 3\",\n\t\t\t\"playback: true\\n\" +\n\t\t\t\t\"paths:\\n\" +\n\t\t\t\t\"  my_path:\\n\" +\n\t\t\t\t\"    recordPath: '%path/%s'\\n\",\n\t\t\t`'recordPath' must contain %f`,\n\t\t},\n\t\t{\n\t\t\t\"invalid record delete after\",\n\t\t\t\"paths:\\n\" +\n\t\t\t\t\"  my_path:\\n\" +\n\t\t\t\t\"    recordSegmentDuration: 30m\\n\" +\n\t\t\t\t\"    recordDeleteAfter: 20m\\n\",\n\t\t\t`'recordDeleteAfter' cannot be lower than 'recordSegmentDuration'`,\n\t\t},\n\t\t{\n\t\t\t\"missing rtpAddress with UDP and no encryption\",\n\t\t\t\"rtspEncryption: \\\"no\\\"\\n\" +\n\t\t\t\t\"rtspTransports: [udp]\\n\" +\n\t\t\t\t\"rtpAddress: ''\\n\",\n\t\t\t\"'rtpAddress' must be set when UDP is enabled and RTSP encryption is 'no' or 'optional'\",\n\t\t},\n\t\t{\n\t\t\t\"missing rtcpAddress with UDP and no encryption\",\n\t\t\t\"rtspEncryption: \\\"no\\\"\\n\" +\n\t\t\t\t\"rtspTransports: [udp]\\n\" +\n\t\t\t\t\"rtpAddress: ':8000'\\n\" +\n\t\t\t\t\"rtcpAddress: ''\\n\",\n\t\t\t\"'rtcpAddress' must be set when UDP is enabled and RTSP encryption is 'no' or 'optional'\",\n\t\t},\n\t\t{\n\t\t\t\"missing rtpAddress with UDP and optional encryption\",\n\t\t\t\"rtspEncryption: optional\\n\" +\n\t\t\t\t\"rtspTransports: [udp]\\n\" +\n\t\t\t\t\"rtpAddress: ''\\n\",\n\t\t\t\"'rtpAddress' must be set when UDP is enabled and RTSP encryption is 'no' or 'optional'\",\n\t\t},\n\t\t{\n\t\t\t\"missing rtcpAddress with UDP and optional encryption\",\n\t\t\t\"rtspEncryption: optional\\n\" +\n\t\t\t\t\"rtspTransports: [udp]\\n\" +\n\t\t\t\t\"rtpAddress: ':8000'\\n\" +\n\t\t\t\t\"rtcpAddress: ''\\n\",\n\t\t\t\"'rtcpAddress' must be set when UDP is enabled and RTSP encryption is 'no' or 'optional'\",\n\t\t},\n\t\t{\n\t\t\t\"missing multicastIPRange with UDP multicast and no encryption\",\n\t\t\t\"rtspEncryption: \\\"no\\\"\\n\" +\n\t\t\t\t\"rtspTransports: [multicast]\\n\" +\n\t\t\t\t\"multicastIPRange: ''\\n\",\n\t\t\t\"'multicastIPRange' must be set when UDP multicast is enabled and RTSP encryption is 'no' or 'optional'\",\n\t\t},\n\t\t{\n\t\t\t\"missing multicastRTPPort with UDP multicast and no encryption\",\n\t\t\t\"rtspEncryption: \\\"no\\\"\\n\" +\n\t\t\t\t\"rtspTransports: [multicast]\\n\" +\n\t\t\t\t\"multicastIPRange: '224.1.0.0/16'\\n\" +\n\t\t\t\t\"multicastRTPPort: 0\\n\",\n\t\t\t\"'multicastRTPPort' must be set when UDP multicast is enabled and RTSP encryption is 'no' or 'optional'\",\n\t\t},\n\t\t{\n\t\t\t\"missing multicastRTCPPort with UDP multicast and no encryption\",\n\t\t\t\"rtspEncryption: \\\"no\\\"\\n\" +\n\t\t\t\t\"rtspTransports: [multicast]\\n\" +\n\t\t\t\t\"multicastIPRange: '224.1.0.0/16'\\n\" +\n\t\t\t\t\"multicastRTPPort: 8002\\n\" +\n\t\t\t\t\"multicastRTCPPort: 0\\n\",\n\t\t\t\"'multicastRTCPPort' must be set when UDP multicast is enabled and RTSP encryption is 'no' or 'optional'\",\n\t\t},\n\t\t{\n\t\t\t\"missing multicastIPRange with UDP multicast and optional encryption\",\n\t\t\t\"rtspEncryption: optional\\n\" +\n\t\t\t\t\"rtspTransports: [multicast]\\n\" +\n\t\t\t\t\"multicastIPRange: ''\\n\",\n\t\t\t\"'multicastIPRange' must be set when UDP multicast is enabled and RTSP encryption is 'no' or 'optional'\",\n\t\t},\n\t\t{\n\t\t\t\"missing multicastRTPPort with UDP multicast and optional encryption\",\n\t\t\t\"rtspEncryption: optional\\n\" +\n\t\t\t\t\"rtspTransports: [multicast]\\n\" +\n\t\t\t\t\"multicastIPRange: '224.1.0.0/16'\\n\" +\n\t\t\t\t\"multicastRTPPort: 0\\n\",\n\t\t\t\"'multicastRTPPort' must be set when UDP multicast is enabled and RTSP encryption is 'no' or 'optional'\",\n\t\t},\n\t\t{\n\t\t\t\"missing multicastRTCPPort with UDP multicast and optional encryption\",\n\t\t\t\"rtspEncryption: optional\\n\" +\n\t\t\t\t\"rtspTransports: [multicast]\\n\" +\n\t\t\t\t\"multicastIPRange: '224.1.0.0/16'\\n\" +\n\t\t\t\t\"multicastRTPPort: 8002\\n\" +\n\t\t\t\t\"multicastRTCPPort: 0\\n\",\n\t\t\t\"'multicastRTCPPort' must be set when UDP multicast is enabled and RTSP encryption is 'no' or 'optional'\",\n\t\t},\n\t\t{\n\t\t\t\"missing srtpAddress with UDP and optional encryption\",\n\t\t\t\"rtspEncryption: optional\\n\" +\n\t\t\t\t\"rtspTransports: [udp]\\n\" +\n\t\t\t\t\"rtpAddress: ':8000'\\n\" +\n\t\t\t\t\"rtcpAddress: ':8001'\\n\" +\n\t\t\t\t\"srtpAddress: ''\\n\",\n\t\t\t\"'srtpAddress' must be set when UDP is enabled and RTSP encryption is 'optional' or 'strict'\",\n\t\t},\n\t\t{\n\t\t\t\"missing srtcpAddress with UDP and optional encryption\",\n\t\t\t\"rtspEncryption: optional\\n\" +\n\t\t\t\t\"rtspTransports: [udp]\\n\" +\n\t\t\t\t\"rtpAddress: ':8000'\\n\" +\n\t\t\t\t\"rtcpAddress: ':8001'\\n\" +\n\t\t\t\t\"srtpAddress: ':8004'\\n\" +\n\t\t\t\t\"srtcpAddress: ''\\n\",\n\t\t\t\"'srtcpAddress' must be set when UDP is enabled and RTSP encryption is 'optional' or 'strict'\",\n\t\t},\n\t\t{\n\t\t\t\"missing srtpAddress with UDP and strict encryption\",\n\t\t\t\"rtspEncryption: strict\\n\" +\n\t\t\t\t\"rtspTransports: [udp]\\n\" +\n\t\t\t\t\"srtpAddress: ''\\n\",\n\t\t\t\"'srtpAddress' must be set when UDP is enabled and RTSP encryption is 'optional' or 'strict'\",\n\t\t},\n\t\t{\n\t\t\t\"missing srtcpAddress with UDP and strict encryption\",\n\t\t\t\"rtspEncryption: strict\\n\" +\n\t\t\t\t\"rtspTransports: [udp]\\n\" +\n\t\t\t\t\"srtpAddress: ':8004'\\n\" +\n\t\t\t\t\"srtcpAddress: ''\\n\",\n\t\t\t\"'srtcpAddress' must be set when UDP is enabled and RTSP encryption is 'optional' or 'strict'\",\n\t\t},\n\t\t{\n\t\t\t\"missing multicastIPRange with UDP multicast and optional encryption second check\",\n\t\t\t\"rtspEncryption: optional\\n\" +\n\t\t\t\t\"rtspTransports: [multicast]\\n\" +\n\t\t\t\t\"rtpAddress: ':8000'\\n\" +\n\t\t\t\t\"rtcpAddress: ':8001'\\n\" +\n\t\t\t\t\"multicastIPRange: ''\\n\",\n\t\t\t\"'multicastIPRange' must be set when UDP multicast is enabled and RTSP encryption is 'no' or 'optional'\",\n\t\t},\n\t\t{\n\t\t\t\"missing multicastSRTPPort with UDP multicast and optional encryption\",\n\t\t\t\"rtspEncryption: optional\\n\" +\n\t\t\t\t\"rtspTransports: [multicast]\\n\" +\n\t\t\t\t\"rtpAddress: ':8000'\\n\" +\n\t\t\t\t\"rtcpAddress: ':8001'\\n\" +\n\t\t\t\t\"multicastIPRange: '224.1.0.0/16'\\n\" +\n\t\t\t\t\"multicastRTPPort: 8002\\n\" +\n\t\t\t\t\"multicastRTCPPort: 8003\\n\" +\n\t\t\t\t\"srtpAddress: ':8004'\\n\" +\n\t\t\t\t\"srtcpAddress: ':8005'\\n\" +\n\t\t\t\t\"multicastSRTPPort: 0\\n\",\n\t\t\t\"'multicastSRTPPort' must be set when UDP multicast is enabled and RTSP encryption is 'optional' or 'strict'\",\n\t\t},\n\t\t{\n\t\t\t\"missing multicastSRTCPPort with UDP multicast and optional encryption\",\n\t\t\t\"rtspEncryption: optional\\n\" +\n\t\t\t\t\"rtspTransports: [multicast]\\n\" +\n\t\t\t\t\"rtpAddress: ':8000'\\n\" +\n\t\t\t\t\"rtcpAddress: ':8001'\\n\" +\n\t\t\t\t\"multicastIPRange: '224.1.0.0/16'\\n\" +\n\t\t\t\t\"multicastRTPPort: 8002\\n\" +\n\t\t\t\t\"multicastRTCPPort: 8003\\n\" +\n\t\t\t\t\"srtpAddress: ':8004'\\n\" +\n\t\t\t\t\"srtcpAddress: ':8005'\\n\" +\n\t\t\t\t\"multicastSRTPPort: 8006\\n\" +\n\t\t\t\t\"multicastSRTCPPort: 0\\n\",\n\t\t\t\"'multicastSRTCPPort' must be set when UDP multicast is enabled and RTSP encryption is 'optional' or 'strict'\",\n\t\t},\n\t\t{\n\t\t\t\"missing multicastIPRange with UDP multicast and strict encryption\",\n\t\t\t\"rtspEncryption: strict\\n\" +\n\t\t\t\t\"rtspTransports: [multicast]\\n\" +\n\t\t\t\t\"multicastIPRange: ''\\n\",\n\t\t\t\"'multicastIPRange' must be set when UDP multicast is enabled and RTSP encryption is 'optional' or 'strict'\",\n\t\t},\n\t\t{\n\t\t\t\"missing multicastSRTPPort with UDP multicast and strict encryption\",\n\t\t\t\"rtspEncryption: strict\\n\" +\n\t\t\t\t\"rtspTransports: [multicast]\\n\" +\n\t\t\t\t\"multicastIPRange: '224.1.0.0/16'\\n\" +\n\t\t\t\t\"multicastSRTPPort: 0\\n\",\n\t\t\t\"'multicastSRTPPort' must be set when UDP multicast is enabled and RTSP encryption is 'optional' or 'strict'\",\n\t\t},\n\t\t{\n\t\t\t\"missing multicastSRTCPPort with UDP multicast and strict encryption\",\n\t\t\t\"rtspEncryption: strict\\n\" +\n\t\t\t\t\"rtspTransports: [multicast]\\n\" +\n\t\t\t\t\"multicastIPRange: '224.1.0.0/16'\\n\" +\n\t\t\t\t\"multicastSRTPPort: 8006\\n\" +\n\t\t\t\t\"multicastSRTCPPort: 0\\n\",\n\t\t\t\"'multicastSRTCPPort' must be set when UDP multicast is enabled and RTSP encryption is 'optional' or 'strict'\",\n\t\t},\n\t\t{\n\t\t\t\"missing rtspAddress with RTSP enabled and no encryption\",\n\t\t\t\"rtsp: yes\\n\" +\n\t\t\t\t\"rtspEncryption: \\\"no\\\"\\n\" +\n\t\t\t\t\"rtspAddress: ''\\n\",\n\t\t\t\"'rtspAddress' must be set when RTSP is enabled and RTSP encryption is 'no' or 'optional'\",\n\t\t},\n\t\t{\n\t\t\t\"missing rtspsAddress with RTSP enabled and strict encryption\",\n\t\t\t\"rtsp: yes\\n\" +\n\t\t\t\t\"rtspEncryption: strict\\n\" +\n\t\t\t\t\"rtspsAddress: ''\\n\",\n\t\t\t\"'rtspsAddress' must be set when RTSP is enabled and RTSP encryption is 'optional' or 'strict'\",\n\t\t},\n\t\t{\n\t\t\t\"missing rtmpAddress with RTMP enabled\",\n\t\t\t\"rtmp: yes\\n\" +\n\t\t\t\t\"rtmpAddress: ''\\n\",\n\t\t\t\"'rtmpAddress' must be set when RTMP is enabled\",\n\t\t},\n\t\t{\n\t\t\t\"missing hlsAddress with HLS enabled\",\n\t\t\t\"hls: yes\\n\" +\n\t\t\t\t\"hlsAddress: ''\\n\",\n\t\t\t\"'hlsAddress' must be set when HLS is enabled\",\n\t\t},\n\t\t{\n\t\t\t\"missing webrtcAddress with WebRTC enabled\",\n\t\t\t\"webrtc: yes\\n\" +\n\t\t\t\t\"webrtcAddress: ''\\n\",\n\t\t\t\"'webrtcAddress' must be set when WebRTC is enabled\",\n\t\t},\n\t\t{\n\t\t\t\"webrtc missing local addresses and ice servers\",\n\t\t\t\"webrtc: yes\\n\" +\n\t\t\t\t\"webrtcLocalUDPAddress: ''\\n\" +\n\t\t\t\t\"webrtcLocalTCPAddress: ''\\n\" +\n\t\t\t\t\"webrtcICEServers2: []\\n\",\n\t\t\t\"at least one between 'webrtcLocalUDPAddress', 'webrtcLocalTCPAddress' or 'webrtcICEServers2' must be filled\",\n\t\t},\n\t\t{\n\t\t\t\"webrtc missing ips config\",\n\t\t\t\"webrtc: yes\\n\" +\n\t\t\t\t\"webrtcLocalUDPAddress: ':8189'\\n\" +\n\t\t\t\t\"webrtcIPsFromInterfaces: false\\n\" +\n\t\t\t\t\"webrtcAdditionalHosts: []\\n\",\n\t\t\t\"at least one between 'webrtcIPsFromInterfaces' or 'webrtcAdditionalHosts' must be filled\",\n\t\t},\n\t\t{\n\t\t\t\"missing apiAddress with API enabled\",\n\t\t\t\"api: yes\\n\" +\n\t\t\t\t\"apiAddress: ''\\n\",\n\t\t\t\"'apiAddress' must be set when API is enabled\",\n\t\t},\n\t\t{\n\t\t\t\"missing metricsAddress with metrics enabled\",\n\t\t\t\"metrics: yes\\n\" +\n\t\t\t\t\"metricsAddress: ''\\n\",\n\t\t\t\"'metricsAddress' must be set when metrics are enabled\",\n\t\t},\n\t\t{\n\t\t\t\"missing pprofAddress with pprof enabled\",\n\t\t\t\"pprof: yes\\n\" +\n\t\t\t\t\"pprofAddress: ''\\n\",\n\t\t\t\"'pprofAddress' must be set when pprof is enabled\",\n\t\t},\n\t\t{\n\t\t\t\"missing playbackAddress with playback enabled\",\n\t\t\t\"playback: yes\\n\" +\n\t\t\t\t\"playbackAddress: ''\\n\",\n\t\t\t\"'playbackAddress' must be set when playback is enabled\",\n\t\t},\n\t\t{\n\t\t\t\"alwaysAvailableTracks and alwaysAvailableFile together\",\n\t\t\t\"paths:\\n\" +\n\t\t\t\t\"  mypath:\\n\" +\n\t\t\t\t\"    alwaysAvailable: yes\\n\" +\n\t\t\t\t\"    alwaysAvailableTracks:\\n\" +\n\t\t\t\t\"    - codec: H264\\n\" +\n\t\t\t\t\"    alwaysAvailableFile: /path/to/file.mp4\\n\",\n\t\t\t\"'alwaysAvailableFile' and 'alwaysAvailableTracks' cannot be used together\",\n\t\t},\n\t} {\n\t\tt.Run(ca.name, func(t *testing.T) {\n\t\t\ttmpf, err := createTempFile([]byte(ca.conf))\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer os.Remove(tmpf)\n\n\t\t\t_, _, err = Load(tmpf, nil, nil)\n\t\t\trequire.EqualError(t, err, ca.err)\n\t\t})\n\t}\n}\n\nfunc TestAlwaysAvailableFileErrorMagicBytes(t *testing.T) {\n\ttmpf, err := createTempFile([]byte(\"ABCDEFGHI\"))\n\trequire.NoError(t, err)\n\tdefer os.Remove(tmpf)\n\n\ttmpConf, err := createTempFile([]byte(\"paths:\\n\" +\n\t\t\"  mypath:\\n\" +\n\t\t\"    alwaysAvailable: yes\\n\" +\n\t\t\"    alwaysAvailableFile: \" + tmpf + \"\\n\"))\n\trequire.NoError(t, err)\n\tdefer os.Remove(tmpConf)\n\n\t_, _, err = Load(tmpConf, nil, nil)\n\trequire.EqualError(t, err, \"invalid 'alwaysAvailableFile': file is not MP4, magic bytes are [69 70 71 72]\")\n}\n\nfunc TestSampleConfFile(t *testing.T) {\n\tfunc() {\n\t\tconf1, confPath1, err := Load(\"../../mediamtx.yml\", nil, nil)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"../../mediamtx.yml\", confPath1)\n\t\tconf1.Paths = make(map[string]*Path)\n\t\tconf1.OptionalPaths = nil\n\n\t\tconf2, confPath2, err := Load(\"\", nil, nil)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"\", confPath2)\n\n\t\trequire.Equal(t, conf1, conf2)\n\t}()\n\n\tfunc() {\n\t\tconf1, confPath1, err := Load(\"../../mediamtx.yml\", nil, nil)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, \"../../mediamtx.yml\", confPath1)\n\n\t\ttmpf, err := createTempFile([]byte(\"paths:\\n  all_others:\"))\n\t\trequire.NoError(t, err)\n\t\tdefer os.Remove(tmpf)\n\n\t\tconf2, confPath2, err := Load(tmpf, nil, nil)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, tmpf, confPath2)\n\n\t\trequire.Equal(t, conf1.Paths, conf2.Paths)\n\t}()\n}\n\nfunc TestClone(t *testing.T) {\n\tconf1, _, err := Load(\"\", nil, nil)\n\trequire.NoError(t, err)\n\n\tconf2 := conf1.Clone()\n\trequire.Equal(t, conf1, conf2)\n}\n"
  },
  {
    "path": "internal/conf/credential.go",
    "content": "package conf\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper\"\n\t\"github.com/matthewhartstonge/argon2\"\n)\n\nvar (\n\trePlainCredential = regexp.MustCompile(`^[a-zA-Z0-9!\\$\\(\\)\\*\\+\\.;<=>\\[\\]\\^_\\-\\{\\}@#&]+$`)\n\treBase64          = regexp.MustCompile(`^sha256:[a-zA-Z0-9\\+/=]+$`)\n)\n\nconst plainCredentialSupportedChars = \"A-Z,0-9,!,$,(,),*,+,.,;,<,=,>,[,],^,_,-,\\\",\\\",@,#,&\"\n\nfunc sha256Base64(in string) string {\n\th := sha256.New()\n\th.Write([]byte(in))\n\treturn base64.StdEncoding.EncodeToString(h.Sum(nil))\n}\n\n// Credential is a parameter that is used as username or password.\ntype Credential string\n\n// UnmarshalJSON implements json.Unmarshaler.\nfunc (d *Credential) UnmarshalJSON(b []byte) error {\n\tvar in string\n\tif err := jsonwrapper.Unmarshal(b, &in); err != nil {\n\t\treturn err\n\t}\n\n\t*d = Credential(in)\n\n\treturn d.validate()\n}\n\n// UnmarshalEnv implements env.Unmarshaler.\nfunc (d *Credential) UnmarshalEnv(_ string, v string) error {\n\treturn d.UnmarshalJSON([]byte(`\"` + v + `\"`))\n}\n\n// IsSha256 returns true if the credential is a sha256 hash.\nfunc (d Credential) IsSha256() bool {\n\treturn strings.HasPrefix(string(d), \"sha256:\")\n}\n\n// IsArgon2 returns true if the credential is an argon2 hash.\nfunc (d Credential) IsArgon2() bool {\n\treturn strings.HasPrefix(string(d), \"argon2:\")\n}\n\n// IsHashed returns true if the credential is a sha256 or argon2 hash.\nfunc (d Credential) IsHashed() bool {\n\treturn d.IsSha256() || d.IsArgon2()\n}\n\n// Check returns true if the given value matches the credential.\nfunc (d Credential) Check(guess string) bool {\n\tif d.IsSha256() {\n\t\treturn string(d)[len(\"sha256:\"):] == sha256Base64(guess)\n\t}\n\n\tif d.IsArgon2() {\n\t\t// TODO: remove matthewhartstonge/argon2 when this PR gets merged into mainline Go:\n\t\t// https://go-review.googlesource.com/c/crypto/+/502515\n\t\tok, err := argon2.VerifyEncoded([]byte(guess), []byte(string(d)[len(\"argon2:\"):]))\n\t\treturn ok && err == nil\n\t}\n\n\tif d != \"\" {\n\t\treturn string(d) == guess\n\t}\n\n\treturn true\n}\n\nfunc (d Credential) validate() error {\n\tif d != \"\" {\n\t\tswitch {\n\t\tcase d.IsSha256():\n\t\t\tif !reBase64.MatchString(string(d)) {\n\t\t\t\treturn fmt.Errorf(\"credential contains unsupported characters, sha256 hash must be base64 encoded\")\n\t\t\t}\n\t\tcase d.IsArgon2():\n\t\t\t// TODO: remove matthewhartstonge/argon2 when this PR gets merged into mainline Go:\n\t\t\t// https://go-review.googlesource.com/c/crypto/+/502515\n\t\t\t_, err := argon2.Decode([]byte(string(d)[len(\"argon2:\"):]))\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid argon2 hash: %w\", err)\n\t\t\t}\n\t\tdefault:\n\t\t\tif !rePlainCredential.MatchString(string(d)) {\n\t\t\t\treturn fmt.Errorf(\"credential contains unsupported characters. Supported are: %s\", plainCredentialSupportedChars)\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/conf/credential_test.go",
    "content": "package conf\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestCredential(t *testing.T) {\n\tt.Run(\"UnmarshalJSON\", func(t *testing.T) {\n\t\texpectedCred := Credential(\"password\")\n\t\tjsonData := []byte(`\"password\"`)\n\t\tvar actualCred Credential\n\t\terr := actualCred.UnmarshalJSON(jsonData)\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, expectedCred, actualCred)\n\t})\n\n\tt.Run(\"UnmarshalEnv\", func(t *testing.T) {\n\t\tcred := Credential(\"\")\n\t\terr := cred.UnmarshalEnv(\"\", \"password\")\n\t\tassert.NoError(t, err)\n\t\tassert.Equal(t, Credential(\"password\"), cred)\n\t})\n\n\tt.Run(\"IsSha256\", func(t *testing.T) {\n\t\tcred := Credential(\"\")\n\t\tassert.False(t, cred.IsSha256())\n\t\tassert.False(t, cred.IsHashed())\n\n\t\tcred = \"sha256:j1tsRqDEw9xvq/D7/9tMx6Jh/jMhk3UfjwIB2f1zgMo=\"\n\t\tassert.True(t, cred.IsSha256())\n\t\tassert.True(t, cred.IsHashed())\n\n\t\tcred = \"argon2:$argon2id$v=19$m=65536,t=1,\" +\n\t\t\t\"p=4$WXJGqwIB2qd+pRmxMOw9Dg$X4gvR0ZB2DtQoN8vOnJPR2SeFdUhH9TyVzfV98sfWeE\"\n\t\tassert.False(t, cred.IsSha256())\n\t\tassert.True(t, cred.IsHashed())\n\t})\n\n\tt.Run(\"IsArgon2\", func(t *testing.T) {\n\t\tcred := Credential(\"\")\n\t\tassert.False(t, cred.IsArgon2())\n\t\tassert.False(t, cred.IsHashed())\n\n\t\tcred = \"sha256:j1tsRqDEw9xvq/D7/9tMx6Jh/jMhk3UfjwIB2f1zgMo=\"\n\t\tassert.False(t, cred.IsArgon2())\n\t\tassert.True(t, cred.IsHashed())\n\n\t\tcred = \"argon2:$argon2id$v=19$m=65536,t=1,\" +\n\t\t\t\"p=4$WXJGqwIB2qd+pRmxMOw9Dg$X4gvR0ZB2DtQoN8vOnJPR2SeFdUhH9TyVzfV98sfWeE\"\n\t\tassert.True(t, cred.IsArgon2())\n\t\tassert.True(t, cred.IsHashed())\n\t})\n\n\tt.Run(\"Check-plain\", func(t *testing.T) {\n\t\tcred := Credential(\"password\")\n\t\tassert.True(t, cred.Check(\"password\"))\n\t\tassert.False(t, cred.Check(\"wrongpassword\"))\n\t})\n\n\tt.Run(\"Check-sha256\", func(t *testing.T) {\n\t\tcred := Credential(\"password\")\n\t\tassert.True(t, cred.Check(\"password\"))\n\t\tassert.False(t, cred.Check(\"wrongpassword\"))\n\t})\n\n\tt.Run(\"Check-sha256\", func(t *testing.T) {\n\t\tcred := Credential(\"sha256:rl3rgi4NcZkpAEcacZnQ2VuOfJ0FxAqCRaKB/SwdZoQ=\")\n\t\tassert.True(t, cred.Check(\"testuser\"))\n\t\tassert.False(t, cred.Check(\"notestuser\"))\n\t})\n\n\tt.Run(\"Check-argon2\", func(t *testing.T) {\n\t\tcred := Credential(\"argon2:$argon2id$v=19$m=4096,t=3,\" +\n\t\t\t\"p=1$MTIzNDU2Nzg$Ux/LWeTgJQPyfMMJo1myR64+o8rALHoPmlE1i/TR+58\")\n\t\tassert.True(t, cred.Check(\"testuser\"))\n\t\tassert.False(t, cred.Check(\"notestuser\"))\n\t})\n\n\tt.Run(\"validate\", func(t *testing.T) {\n\t\ttests := []struct {\n\t\t\tname    string\n\t\t\tcred    Credential\n\t\t\twantErr bool\n\t\t}{\n\t\t\t{\n\t\t\t\tname:    \"Empty credential\",\n\t\t\t\tcred:    Credential(\"\"),\n\t\t\t\twantErr: false,\n\t\t\t},\n\t\t\t{\n\t\t\t\tname:    \"Valid plain credential\",\n\t\t\t\tcred:    Credential(\"validPlain123\"),\n\t\t\t\twantErr: false,\n\t\t\t},\n\t\t\t{\n\t\t\t\tname:    \"Invalid plain credential\",\n\t\t\t\tcred:    Credential(\"invalid/Plain\"),\n\t\t\t\twantErr: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tname:    \"Valid sha256 credential\",\n\t\t\t\tcred:    Credential(\"sha256:validBase64EncodedHash==\"),\n\t\t\t\twantErr: false,\n\t\t\t},\n\t\t\t{\n\t\t\t\tname:    \"Invalid sha256 credential\",\n\t\t\t\tcred:    Credential(\"sha256:inval*idBase64\"),\n\t\t\t\twantErr: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: \"Valid Argon2 credential\",\n\t\t\t\tcred: Credential(\"argon2:$argon2id$v=19$m=4096,\" +\n\t\t\t\t\t\"t=3,p=1$MTIzNDU2Nzg$zarsL19s86GzUWlAkvwt4gJBFuU/A9CVuCjNI4fksow\"),\n\t\t\t\twantErr: false,\n\t\t\t},\n\t\t\t{\n\t\t\t\tname:    \"Invalid Argon2 credential\",\n\t\t\t\tcred:    Credential(\"argon2:invalid\"),\n\t\t\t\twantErr: true,\n\t\t\t},\n\t\t\t{\n\t\t\t\tname: \"Invalid Argon2 credential\",\n\t\t\t\t// testing argon2d errors, because it's not supported\n\t\t\t\tcred: Credential(\"$argon2d$v=19$m=4096,t=3,\" +\n\t\t\t\t\t\"p=1$MTIzNDU2Nzg$Xqyd4R7LzXvvAEHaVU12+Nzf5OkHoYcwIEIIYJUDpz0\"),\n\t\t\t\twantErr: true,\n\t\t\t},\n\t\t}\n\n\t\tfor _, tt := range tests {\n\t\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\t\terr := tt.cred.validate()\n\t\t\t\tif tt.wantErr {\n\t\t\t\t\tassert.Error(t, err)\n\t\t\t\t} else {\n\t\t\t\t\tassert.NoError(t, err)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/conf/decrypt/decrypt.go",
    "content": "// Package decrypt contains the Decrypt function.\npackage decrypt\n\nimport (\n\t\"encoding/base64\"\n\t\"fmt\"\n\n\t\"golang.org/x/crypto/nacl/secretbox\"\n)\n\n// Decrypt decrypts the configuration with the given key.\nfunc Decrypt(key string, byts []byte) ([]byte, error) {\n\tenc, err := base64.StdEncoding.DecodeString(string(byts))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar secretKey [32]byte\n\tcopy(secretKey[:], key)\n\n\tvar decryptNonce [24]byte\n\tcopy(decryptNonce[:], enc[:24])\n\tdecrypted, ok := secretbox.Open(nil, enc[24:], &decryptNonce, &secretKey)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"decryption error\")\n\t}\n\n\treturn decrypted, nil\n}\n"
  },
  {
    "path": "internal/conf/duration.go",
    "content": "package conf\n\nimport (\n\t\"encoding/json\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper\"\n)\n\nvar reDays = regexp.MustCompile(\"^(-?[0-9]+)d\")\n\n// Duration is a duration. It differs from the standard duration in these ways:\n// - it is unmarshaled/marshaled from/to a string (instead of a number)\n// - it supports days\ntype Duration time.Duration\n\nfunc (d Duration) marshalInternal() string {\n\tnegative := false\n\tif d < 0 {\n\t\tnegative = true\n\t\td = -d\n\t}\n\n\tday := Duration(86400 * time.Second)\n\tdays := d / day\n\tnonDays := d % day\n\n\tret := \"\"\n\tif negative {\n\t\tret += \"-\"\n\t}\n\n\tif days > 0 {\n\t\tret += strconv.FormatInt(int64(days), 10) + \"d\"\n\t}\n\n\tif nonDays != 0 {\n\t\tret += time.Duration(nonDays).String()\n\t}\n\n\treturn ret\n}\n\n// MarshalJSON implements json.Marshaler.\nfunc (d Duration) MarshalJSON() ([]byte, error) {\n\treturn json.Marshal(d.marshalInternal())\n}\n\nfunc (d *Duration) unmarshalInternal(in string) error {\n\tnegative := false\n\tdays := int64(0)\n\n\tm := reDays.FindStringSubmatch(in)\n\tif m != nil {\n\t\tdays, _ = strconv.ParseInt(m[1], 10, 64)\n\t\tif days < 0 {\n\t\t\tnegative = true\n\t\t\tdays = -days\n\t\t}\n\n\t\tin = in[len(m[0]):]\n\t}\n\n\tvar nonDays time.Duration\n\n\tif len(in) != 0 {\n\t\tvar err error\n\t\tnonDays, err = time.ParseDuration(in)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tnonDays += time.Duration(days) * 24 * time.Hour\n\tif negative {\n\t\tnonDays = -nonDays\n\t}\n\n\t*d = Duration(nonDays)\n\treturn nil\n}\n\n// UnmarshalJSON implements json.Unmarshaler.\nfunc (d *Duration) UnmarshalJSON(b []byte) error {\n\tvar in string\n\tif err := jsonwrapper.Unmarshal(b, &in); err != nil {\n\t\treturn err\n\t}\n\n\terr := d.unmarshalInternal(in)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// UnmarshalEnv implements env.Unmarshaler.\nfunc (d *Duration) UnmarshalEnv(_ string, v string) error {\n\treturn d.UnmarshalJSON([]byte(`\"` + v + `\"`))\n}\n"
  },
  {
    "path": "internal/conf/duration_test.go",
    "content": "package conf\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar casesDuration = []struct {\n\tname string\n\tdec  Duration\n\tenc  string\n}{\n\t{\n\t\t\"standard\",\n\t\tDuration(13456 * time.Second),\n\t\t`\"3h44m16s\"`,\n\t},\n\t{\n\t\t\"days\",\n\t\tDuration(50 * 13456 * time.Second),\n\t\t`\"7d18h53m20s\"`,\n\t},\n\t{\n\t\t\"days negative\",\n\t\tDuration(-50 * 13456 * time.Second),\n\t\t`\"-7d18h53m20s\"`,\n\t},\n\t{\n\t\t\"days even\",\n\t\tDuration(7 * 24 * time.Hour),\n\t\t`\"7d\"`,\n\t},\n}\n\nfunc TestDurationUnmarshal(t *testing.T) {\n\tfor _, ca := range casesDuration {\n\t\tt.Run(ca.name, func(t *testing.T) {\n\t\t\tvar dec Duration\n\t\t\terr := dec.UnmarshalJSON([]byte(ca.enc))\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, ca.dec, dec)\n\t\t})\n\t}\n}\n\nfunc TestDurationMarshal(t *testing.T) {\n\tfor _, ca := range casesDuration {\n\t\tt.Run(ca.name, func(t *testing.T) {\n\t\t\tenc, err := ca.dec.MarshalJSON()\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, ca.enc, string(enc))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/conf/encryption.go",
    "content": "package conf\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper\"\n)\n\n// Encryption is the rtspEncryption / rtmpEncryption parameter.\ntype Encryption string\n\n// values.\nconst (\n\tEncryptionNo       Encryption = \"no\"\n\tEncryptionOptional Encryption = \"optional\"\n\tEncryptionStrict   Encryption = \"strict\"\n)\n\n// UnmarshalJSON implements json.Unmarshaler.\nfunc (d *Encryption) UnmarshalJSON(b []byte) error {\n\ttype alias Encryption\n\tif err := jsonwrapper.Unmarshal(b, (*alias)(d)); err != nil {\n\t\treturn err\n\t}\n\n\tswitch *d {\n\tcase \"false\":\n\t\t*d = EncryptionNo\n\tcase \"true\", \"yes\":\n\t\t*d = EncryptionStrict\n\t}\n\n\tswitch *d {\n\tcase EncryptionNo, EncryptionOptional, EncryptionStrict:\n\n\tdefault:\n\t\treturn fmt.Errorf(\"invalid encryption: '%s'\", *d)\n\t}\n\n\treturn nil\n}\n\n// UnmarshalEnv implements env.Unmarshaler.\nfunc (d *Encryption) UnmarshalEnv(_ string, v string) error {\n\treturn d.UnmarshalJSON([]byte(`\"` + v + `\"`))\n}\n"
  },
  {
    "path": "internal/conf/env/env.go",
    "content": "// Package env contains a function to load configuration from environment.\npackage env\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n// Unmarshaler can be implemented to override the unmarshaling process.\ntype Unmarshaler interface {\n\tUnmarshalEnv(prefix string, v string) error\n}\n\nfunc envHasAtLeastAKeyWithPrefix(env map[string]string, prefix string) bool {\n\tfor key := range env {\n\t\tif strings.HasPrefix(key, prefix) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc loadEnvInternal(env map[string]string, prefix string, prv reflect.Value) error {\n\tif prv.Kind() != reflect.Pointer {\n\t\treturn loadEnvInternal(env, prefix, prv.Addr())\n\t}\n\n\trt := prv.Type().Elem()\n\n\tif i, ok := prv.Interface().(Unmarshaler); ok {\n\t\tif ev, ok2 := env[prefix]; ok2 {\n\t\t\tif prv.IsNil() {\n\t\t\t\tprv.Set(reflect.New(rt))\n\t\t\t\ti = prv.Interface().(Unmarshaler)\n\t\t\t}\n\t\t\terr := i.UnmarshalEnv(prefix, ev)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"%s: %w\", prefix, err)\n\t\t\t}\n\t\t} else if envHasAtLeastAKeyWithPrefix(env, prefix) {\n\t\t\terr := i.UnmarshalEnv(prefix, \"\")\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"%s: %w\", prefix, err)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tswitch rt {\n\tcase reflect.TypeOf(\"\"):\n\t\tif ev, ok := env[prefix]; ok {\n\t\t\tif prv.IsNil() {\n\t\t\t\tprv.Set(reflect.New(rt))\n\t\t\t}\n\t\t\tprv.Elem().SetString(ev)\n\t\t}\n\t\treturn nil\n\n\tcase reflect.TypeOf(int(0)):\n\t\tif ev, ok := env[prefix]; ok {\n\t\t\tif prv.IsNil() {\n\t\t\t\tprv.Set(reflect.New(rt))\n\t\t\t}\n\t\t\tiv, err := strconv.ParseInt(ev, 10, 32)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"%s: %w\", prefix, err)\n\t\t\t}\n\t\t\tprv.Elem().SetInt(iv)\n\t\t}\n\t\treturn nil\n\n\tcase reflect.TypeOf(uint(0)):\n\t\tif ev, ok := env[prefix]; ok {\n\t\t\tif prv.IsNil() {\n\t\t\t\tprv.Set(reflect.New(rt))\n\t\t\t}\n\t\t\tiv, err := strconv.ParseUint(ev, 10, 32)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"%s: %w\", prefix, err)\n\t\t\t}\n\t\t\tprv.Elem().SetUint(iv)\n\t\t}\n\t\treturn nil\n\n\tcase reflect.TypeOf(float64(0)):\n\t\tif ev, ok := env[prefix]; ok {\n\t\t\tif prv.IsNil() {\n\t\t\t\tprv.Set(reflect.New(rt))\n\t\t\t}\n\t\t\tiv, err := strconv.ParseFloat(ev, 64)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"%s: %w\", prefix, err)\n\t\t\t}\n\t\t\tprv.Elem().SetFloat(iv)\n\t\t}\n\t\treturn nil\n\n\tcase reflect.TypeOf(bool(false)):\n\t\tif ev, ok := env[prefix]; ok {\n\t\t\tif prv.IsNil() {\n\t\t\t\tprv.Set(reflect.New(rt))\n\t\t\t}\n\t\t\tswitch strings.ToLower(ev) {\n\t\t\tcase \"yes\", \"true\":\n\t\t\t\tprv.Elem().SetBool(true)\n\n\t\t\tcase \"no\", \"false\":\n\t\t\t\tprv.Elem().SetBool(false)\n\n\t\t\tdefault:\n\t\t\t\treturn fmt.Errorf(\"%s: invalid value '%s'\", prefix, ev)\n\t\t\t}\n\t\t}\n\t\treturn nil\n\t}\n\n\tswitch rt.Kind() {\n\tcase reflect.Map:\n\t\tfor k := range env {\n\t\t\tif !strings.HasPrefix(k, prefix+\"_\") {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tmapKey := strings.Split(k[len(prefix+\"_\"):], \"_\")[0]\n\t\t\tif len(mapKey) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// allow only keys in uppercase\n\t\t\tif mapKey != strings.ToUpper(mapKey) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// initialize only if there's at least one key\n\t\t\tif prv.Elem().IsNil() {\n\t\t\t\tprv.Elem().Set(reflect.MakeMap(rt))\n\t\t\t}\n\n\t\t\tmapKeyLower := strings.ToLower(mapKey)\n\t\t\tnv := prv.Elem().MapIndex(reflect.ValueOf(mapKeyLower))\n\t\t\tzero := reflect.Value{}\n\t\t\tif nv == zero {\n\t\t\t\tnv = reflect.New(rt.Elem().Elem())\n\t\t\t\tprv.Elem().SetMapIndex(reflect.ValueOf(mapKeyLower), nv)\n\t\t\t}\n\n\t\t\terr := loadEnvInternal(env, prefix+\"_\"+mapKey, nv.Elem())\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\n\tcase reflect.Struct:\n\t\tflen := rt.NumField()\n\t\tfor i := range flen {\n\t\t\tf := rt.Field(i)\n\t\t\tjsonTag := f.Tag.Get(\"json\")\n\n\t\t\t// load only public fields\n\t\t\tif jsonTag == \"-\" {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\terr := loadEnvInternal(env, prefix+\"_\"+\n\t\t\t\tstrings.ToUpper(strings.TrimSuffix(jsonTag, \",omitempty\")), prv.Elem().Field(i))\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\n\tcase reflect.Slice:\n\t\tswitch {\n\t\tcase rt.Elem() == reflect.TypeOf(\"\"):\n\t\t\tif ev, ok := env[prefix]; ok {\n\t\t\t\tif ev == \"\" {\n\t\t\t\t\tprv.Elem().Set(reflect.MakeSlice(prv.Elem().Type(), 0, 0))\n\t\t\t\t} else {\n\t\t\t\t\tif prv.IsNil() {\n\t\t\t\t\t\tprv.Set(reflect.New(rt))\n\t\t\t\t\t}\n\t\t\t\t\tprv.Elem().Set(reflect.ValueOf(strings.Split(ev, \",\")))\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil\n\n\t\tcase rt.Elem() == reflect.TypeOf(uint(0)):\n\t\t\tif ev, ok := env[prefix]; ok {\n\t\t\t\tif ev == \"\" {\n\t\t\t\t\tprv.Elem().Set(reflect.MakeSlice(prv.Elem().Type(), 0, 0))\n\t\t\t\t} else {\n\t\t\t\t\tif prv.IsNil() {\n\t\t\t\t\t\tprv.Set(reflect.New(rt))\n\t\t\t\t\t}\n\n\t\t\t\t\traw := strings.Split(ev, \",\")\n\t\t\t\t\tvals := make([]uint, len(raw))\n\n\t\t\t\t\tfor i, v := range raw {\n\t\t\t\t\t\ttmp, err := strconv.ParseUint(v, 10, 64)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t\tvals[i] = uint(tmp)\n\t\t\t\t\t}\n\n\t\t\t\t\tprv.Elem().Set(reflect.ValueOf(vals))\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil\n\n\t\tcase rt.Elem() == reflect.TypeOf(float64(0)):\n\t\t\tif ev, ok := env[prefix]; ok {\n\t\t\t\tif ev == \"\" {\n\t\t\t\t\tprv.Elem().Set(reflect.MakeSlice(prv.Elem().Type(), 0, 0))\n\t\t\t\t} else {\n\t\t\t\t\tif prv.IsNil() {\n\t\t\t\t\t\tprv.Set(reflect.New(rt))\n\t\t\t\t\t}\n\n\t\t\t\t\traw := strings.Split(ev, \",\")\n\t\t\t\t\tvals := make([]float64, len(raw))\n\n\t\t\t\t\tfor i, v := range raw {\n\t\t\t\t\t\ttmp, err := strconv.ParseFloat(v, 64)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t\tvals[i] = tmp\n\t\t\t\t\t}\n\n\t\t\t\t\tprv.Elem().Set(reflect.ValueOf(vals))\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil\n\n\t\tcase rt.Elem().Kind() == reflect.Struct:\n\t\t\tif ev, ok := env[prefix]; ok && ev == \"\" { // special case: empty list\n\t\t\t\tprv.Elem().Set(reflect.MakeSlice(prv.Elem().Type(), 0, 0))\n\t\t\t} else {\n\t\t\t\tfor i := 0; ; i++ {\n\t\t\t\t\titemPrefix := prefix + \"_\" + strconv.FormatInt(int64(i), 10)\n\t\t\t\t\tif !envHasAtLeastAKeyWithPrefix(env, itemPrefix) && (prv.IsZero() || prv.Elem().Len() <= i) {\n\t\t\t\t\t\tbreak\n\t\t\t\t\t}\n\n\t\t\t\t\tvar elem reflect.Value\n\n\t\t\t\t\tif !prv.IsZero() && prv.Elem().Len() > i {\n\t\t\t\t\t\telem = prv.Elem().Index(i).Addr()\n\t\t\t\t\t} else {\n\t\t\t\t\t\telem = reflect.New(rt.Elem())\n\t\t\t\t\t}\n\n\t\t\t\t\terr := loadEnvInternal(env, itemPrefix, elem.Elem())\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\tif !prv.IsZero() && prv.Elem().Len() > i {\n\t\t\t\t\t\tprv.Elem().Index(i).Set(elem.Elem())\n\t\t\t\t\t} else {\n\t\t\t\t\t\tif prv.IsZero() {\n\t\t\t\t\t\t\tprv.Set(reflect.New(rt))\n\t\t\t\t\t\t}\n\t\t\t\t\t\tprv.Elem().Set(reflect.Append(prv.Elem(), elem.Elem()))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn fmt.Errorf(\"unsupported type: %v\", rt)\n}\n\nfunc loadWithEnv(env map[string]string, prefix string, v any) error {\n\treturn loadEnvInternal(env, prefix, reflect.ValueOf(v).Elem())\n}\n\nfunc envToMap() map[string]string {\n\tenv := make(map[string]string)\n\tfor _, kv := range os.Environ() {\n\t\ttmp := strings.SplitN(kv, \"=\", 2)\n\t\tenv[tmp[0]] = tmp[1]\n\t}\n\treturn env\n}\n\n// Load loads the configuration from the environment.\nfunc Load(prefix string, v any) error {\n\treturn loadWithEnv(envToMap(), prefix, v)\n}\n"
  },
  {
    "path": "internal/conf/env/env_test.go",
    "content": "package env\n\nimport (\n\t\"encoding/json\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc ptrOf[T any](v T) *T {\n\tp := new(T)\n\t*p = v\n\treturn p\n}\n\ntype myDuration time.Duration\n\nfunc (d *myDuration) UnmarshalJSON(b []byte) error {\n\tvar in string\n\tif err := json.Unmarshal(b, &in); err != nil {\n\t\treturn err\n\t}\n\n\tdu, err := time.ParseDuration(in)\n\tif err != nil {\n\t\treturn err\n\t}\n\t*d = myDuration(du)\n\n\treturn nil\n}\n\n// UnmarshalEnv implements env.Unmarshaler.\nfunc (d *myDuration) UnmarshalEnv(_ string, v string) error {\n\treturn d.UnmarshalJSON([]byte(`\"` + v + `\"`))\n}\n\nfunc TestLoadPrimitives(t *testing.T) {\n\ttype subStruct2 struct {\n\t\tMyParam int `json:\"myParam\"`\n\t}\n\n\ttype mapEntry struct {\n\t\tMyValue  string     `json:\"myValue\"`\n\t\tMyStruct subStruct2 `json:\"myStruct\"`\n\t}\n\n\ttype testStruct struct {\n\t\tMyString           string               `json:\"myString\"`\n\t\tMyStringOpt        *string              `json:\"myStringOpt\"`\n\t\tMyInt              int                  `json:\"myInt\"`\n\t\tMyIntOpt           *int                 `json:\"myIntOpt\"`\n\t\tMyUint             uint                 `json:\"myUint\"`\n\t\tMyUintOpt          *uint                `json:\"myUintOpt\"`\n\t\tMyFloat            float64              `json:\"myFloat\"`\n\t\tMyFloatOpt         *float64             `json:\"myFloatOpt\"`\n\t\tMyBool             bool                 `json:\"myBool\"`\n\t\tMyBoolOpt          *bool                `json:\"myBoolOpt\"`\n\t\tMyDuration         myDuration           `json:\"myDuration\"`\n\t\tMyDurationOpt      *myDuration          `json:\"myDurationOpt\"`\n\t\tMyDurationOptUnset *myDuration          `json:\"myDurationOptUnset\"`\n\t\tMyMap              map[string]*mapEntry `json:\"myMap\"`\n\t\tUnset              *bool                `json:\"unset\"`\n\t}\n\n\tvar s testStruct\n\n\tt.Setenv(\"MYPREFIX_MYSTRING\", \"testcontent\")\n\tt.Setenv(\"MYPREFIX_MYSTRINGOPT\", \"testcontent2\")\n\tt.Setenv(\"MYPREFIX_MYINT\", \"123\")\n\tt.Setenv(\"MYPREFIX_MYINTOPT\", \"456\")\n\tt.Setenv(\"MYPREFIX_MYUINT\", \"8910\")\n\tt.Setenv(\"MYPREFIX_MYUINTOPT\", \"112313\")\n\tt.Setenv(\"MYPREFIX_MYFLOAT\", \"15.2\")\n\tt.Setenv(\"MYPREFIX_MYFLOATOPT\", \"16.2\")\n\tt.Setenv(\"MYPREFIX_MYBOOL\", \"yes\")\n\tt.Setenv(\"MYPREFIX_MYBOOLOPT\", \"false\")\n\tt.Setenv(\"MYPREFIX_MYDURATION\", \"22s\")\n\tt.Setenv(\"MYPREFIX_MYDURATIONOPT\", \"30s\")\n\tt.Setenv(\"MYPREFIX_MYMAP_MYKEY\", \"\")\n\tt.Setenv(\"MYPREFIX_MYMAP_MYKEY2_MYVALUE\", \"asd\")\n\tt.Setenv(\"MYPREFIX_MYMAP_MYKEY2_MYSTRUCT_MYPARAM\", \"456\")\n\n\terr := Load(\"MYPREFIX\", &s)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, testStruct{\n\t\tMyString:      \"testcontent\",\n\t\tMyStringOpt:   ptrOf(\"testcontent2\"),\n\t\tMyInt:         123,\n\t\tMyIntOpt:      ptrOf(456),\n\t\tMyUint:        8910,\n\t\tMyUintOpt:     ptrOf(uint(112313)),\n\t\tMyFloat:       15.2,\n\t\tMyFloatOpt:    ptrOf(16.2),\n\t\tMyBool:        true,\n\t\tMyBoolOpt:     ptrOf(false),\n\t\tMyDuration:    22000000000,\n\t\tMyDurationOpt: ptrOf(myDuration(30000000000)),\n\t\tMyMap: map[string]*mapEntry{\n\t\t\t\"mykey\": {\n\t\t\t\tMyValue: \"\",\n\t\t\t\tMyStruct: subStruct2{\n\t\t\t\t\tMyParam: 0,\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"mykey2\": {\n\t\t\t\tMyValue: \"asd\",\n\t\t\t\tMyStruct: subStruct2{\n\t\t\t\t\tMyParam: 456,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}, s)\n}\n\nfunc TestLoadSlice(t *testing.T) {\n\ttype testStruct struct {\n\t\tMySliceFloat          []float64 `json:\"mySliceFloat\"`\n\t\tMySliceString         []string  `json:\"mySliceString\"`\n\t\tMySliceStringEmpty    []string  `json:\"mySliceStringEmpty\"`\n\t\tMySliceStringOpt      *[]string `json:\"mySliceStringOpt\"`\n\t\tMySliceStringOptUnset *[]string `json:\"mySliceStringOptUnset\"`\n\t}\n\n\tvar s testStruct\n\n\tt.Setenv(\"MYPREFIX_MYSLICEFLOAT\", \"0.5,0.5\")\n\tt.Setenv(\"MYPREFIX_MYSLICESTRING\", \"val1,val2\")\n\tt.Setenv(\"MYPREFIX_MYSLICESTRINGEMPTY\", \"\")\n\tt.Setenv(\"MYPREFIX_MYSLICESTRINGOPT\", \"aa\")\n\n\terr := Load(\"MYPREFIX\", &s)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, testStruct{\n\t\tMySliceFloat: []float64{0.5, 0.5},\n\t\tMySliceString: []string{\n\t\t\t\"val1\",\n\t\t\t\"val2\",\n\t\t},\n\t\tMySliceStringEmpty: []string{},\n\t\tMySliceStringOpt:   &[]string{\"aa\"},\n\t}, s)\n}\n\nfunc TestLoadSliceStruct(t *testing.T) {\n\ttype subStruct struct {\n\t\tURL      string `json:\"url\"`\n\t\tUsername string `json:\"username\"`\n\t\tPassword string `json:\"password\"`\n\t\tMyInt2   int    `json:\"myInt2\"`\n\t}\n\n\ttype testStruct struct {\n\t\tMySliceSubStruct         []subStruct  `json:\"mySliceSubStruct\"`\n\t\tMySliceSubStructOpt      *[]subStruct `json:\"mySliceSubStructOpt\"`\n\t\tMySliceSubStructOptUnset *[]subStruct `json:\"mySliceSubStructOptUnset\"`\n\t}\n\n\tvar s testStruct\n\n\tt.Setenv(\"MYPREFIX_MYSLICESUBSTRUCT_0_URL\", \"url1\")\n\tt.Setenv(\"MYPREFIX_MYSLICESUBSTRUCT_0_USERNAME\", \"user1\")\n\tt.Setenv(\"MYPREFIX_MYSLICESUBSTRUCT_0_PASSWORD\", \"pass1\")\n\tt.Setenv(\"MYPREFIX_MYSLICESUBSTRUCT_1_URL\", \"url2\")\n\tt.Setenv(\"MYPREFIX_MYSLICESUBSTRUCT_1_PASSWORD\", \"pass2\")\n\tt.Setenv(\"MYPREFIX_MYSLICESUBSTRUCTOPT_0_PASSWORD\", \"pwd\")\n\n\terr := Load(\"MYPREFIX\", &s)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, testStruct{\n\t\tMySliceSubStruct: []subStruct{\n\t\t\t{\n\t\t\t\tURL:      \"url1\",\n\t\t\t\tUsername: \"user1\",\n\t\t\t\tPassword: \"pass1\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tURL:      \"url2\",\n\t\t\t\tUsername: \"\",\n\t\t\t\tPassword: \"pass2\",\n\t\t\t},\n\t\t},\n\t\tMySliceSubStructOpt: &[]subStruct{\n\t\t\t{\n\t\t\t\tPassword: \"pwd\",\n\t\t\t},\n\t\t},\n\t}, s)\n}\n\nfunc TestLoadEmptySliceStruct(t *testing.T) {\n\ttype subStruct struct {\n\t\tURL      string `json:\"url\"`\n\t\tUsername string `json:\"username\"`\n\t\tPassword string `json:\"password\"`\n\t\tMyInt2   int    `json:\"myInt2\"`\n\t}\n\n\ttype testStruct struct {\n\t\tMySliceSubStructEmpty []subStruct `json:\"mySliceSubStructEmpty\"`\n\t}\n\n\tvar s testStruct\n\n\tt.Setenv(\"MYPREFIX_MYSLICESUBSTRUCTEMPTY\", \"\")\n\n\terr := Load(\"MYPREFIX\", &s)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, testStruct{\n\t\tMySliceSubStructEmpty: []subStruct{},\n\t}, s)\n}\n\nfunc TestLoadPreloadedSliceStruct(t *testing.T) {\n\ttype subStruct struct {\n\t\tURL      string `json:\"url\"`\n\t\tUsername string `json:\"username\"`\n\t\tPassword string `json:\"password\"`\n\t\tMyInt2   int    `json:\"myInt2\"`\n\t}\n\n\ttype testStruct struct {\n\t\tMySliceSubStructPreloaded  []subStruct `json:\"mySliceSubStructPreloaded\"`\n\t\tMySliceSubStructPreloaded2 []subStruct `json:\"mySliceSubStructPreloaded2\"`\n\t}\n\n\ts := testStruct{\n\t\tMySliceSubStructPreloaded: []subStruct{\n\t\t\t{\n\t\t\t\tURL:      \"val1\",\n\t\t\t\tUsername: \"val2\",\n\t\t\t},\n\t\t},\n\t\tMySliceSubStructPreloaded2: []subStruct{\n\t\t\t{\n\t\t\t\tURL:      \"val3\",\n\t\t\t\tUsername: \"val4\",\n\t\t\t},\n\t\t},\n\t}\n\n\tt.Setenv(\"MYPREFIX_MYSLICESUBSTRUCTPRELOADED_0_URL\", \"newurl\")\n\tt.Setenv(\"MYPREFIX_MYSLICESUBSTRUCTPRELOADED2_1_URL\", \"newurl2\")\n\n\terr := Load(\"MYPREFIX\", &s)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, testStruct{\n\t\tMySliceSubStructPreloaded: []subStruct{\n\t\t\t{\n\t\t\t\tURL:      \"newurl\",\n\t\t\t\tUsername: \"val2\",\n\t\t\t},\n\t\t},\n\t\tMySliceSubStructPreloaded2: []subStruct{\n\t\t\t{\n\t\t\t\tURL:      \"val3\",\n\t\t\t\tUsername: \"val4\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tURL: \"newurl2\",\n\t\t\t},\n\t\t},\n\t}, s)\n}\n"
  },
  {
    "path": "internal/conf/global.go",
    "content": "package conf\n\nimport (\n\t\"encoding/json\"\n\t\"reflect\"\n)\n\nvar globalValuesType = func() reflect.Type {\n\tvar fields []reflect.StructField\n\trt := reflect.TypeOf(Conf{})\n\tnf := rt.NumField()\n\n\tfor i := range nf {\n\t\tf := rt.Field(i)\n\t\tj := f.Tag.Get(\"json\")\n\n\t\tif j != \"-\" && j != \"pathDefaults\" && j != \"paths\" {\n\t\t\tfields = append(fields, reflect.StructField{\n\t\t\t\tName: f.Name,\n\t\t\t\tType: f.Type,\n\t\t\t\tTag:  f.Tag,\n\t\t\t})\n\t\t}\n\t}\n\n\treturn reflect.StructOf(fields)\n}()\n\nfunc newGlobalValues() any {\n\treturn reflect.New(globalValuesType).Interface()\n}\n\n// Global is the global part of Conf.\ntype Global struct {\n\tValues any\n}\n\n// MarshalJSON implements json.Marshaler.\nfunc (p *Global) MarshalJSON() ([]byte, error) {\n\treturn json.Marshal(p.Values)\n}\n"
  },
  {
    "path": "internal/conf/hls_variant.go",
    "content": "package conf\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/bluenviron/gohlslib/v2\"\n\t\"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper\"\n)\n\n// HLSVariant is the hlsVariant parameter.\ntype HLSVariant gohlslib.MuxerVariant\n\n// MarshalJSON implements json.Marshaler.\nfunc (d HLSVariant) MarshalJSON() ([]byte, error) {\n\tvar out string\n\n\tswitch d {\n\tcase HLSVariant(gohlslib.MuxerVariantMPEGTS):\n\t\tout = \"mpegts\"\n\n\tcase HLSVariant(gohlslib.MuxerVariantFMP4):\n\t\tout = \"fmp4\"\n\n\tdefault:\n\t\tout = \"lowLatency\"\n\t}\n\n\treturn json.Marshal(out)\n}\n\n// UnmarshalJSON implements json.Unmarshaler.\nfunc (d *HLSVariant) UnmarshalJSON(b []byte) error {\n\tvar in string\n\tif err := jsonwrapper.Unmarshal(b, &in); err != nil {\n\t\treturn err\n\t}\n\n\tswitch in {\n\tcase \"mpegts\":\n\t\t*d = HLSVariant(gohlslib.MuxerVariantMPEGTS)\n\n\tcase \"fmp4\":\n\t\t*d = HLSVariant(gohlslib.MuxerVariantFMP4)\n\n\tcase \"lowLatency\":\n\t\t*d = HLSVariant(gohlslib.MuxerVariantLowLatency)\n\n\tdefault:\n\t\treturn fmt.Errorf(\"invalid HLS variant: '%s'\", in)\n\t}\n\n\treturn nil\n}\n\n// UnmarshalEnv implements env.Unmarshaler.\nfunc (d *HLSVariant) UnmarshalEnv(_ string, v string) error {\n\treturn d.UnmarshalJSON([]byte(`\"` + v + `\"`))\n}\n"
  },
  {
    "path": "internal/conf/ip_network.go",
    "content": "package conf\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper\"\n)\n\n// IPNetwork represents an IP network.\ntype IPNetwork net.IPNet\n\n// MarshalJSON implements json.Marshaler.\nfunc (n IPNetwork) MarshalJSON() ([]byte, error) {\n\treturn json.Marshal(n.String())\n}\n\n// UnmarshalJSON implements json.Unmarshaler.\nfunc (n *IPNetwork) UnmarshalJSON(b []byte) error {\n\tvar t string\n\tif err := jsonwrapper.Unmarshal(b, &t); err != nil {\n\t\treturn err\n\t}\n\n\tif _, ipnet, err := net.ParseCIDR(t); err == nil {\n\t\tif ipv4 := ipnet.IP.To4(); ipv4 != nil {\n\t\t\t*n = IPNetwork{IP: ipv4, Mask: ipnet.Mask[len(ipnet.Mask)-4 : len(ipnet.Mask)]}\n\t\t} else {\n\t\t\t*n = IPNetwork(*ipnet)\n\t\t}\n\t} else if ip := net.ParseIP(t); ip != nil {\n\t\tif ipv4 := ip.To4(); ipv4 != nil {\n\t\t\t*n = IPNetwork{IP: ipv4, Mask: net.CIDRMask(32, 32)}\n\t\t} else {\n\t\t\t*n = IPNetwork{IP: ip, Mask: net.CIDRMask(128, 128)}\n\t\t}\n\t} else {\n\t\treturn fmt.Errorf(\"unable to parse IP/CIDR '%s'\", t)\n\t}\n\n\treturn nil\n}\n\n// String implements fmt.Stringer.\nfunc (n IPNetwork) String() string {\n\tipnet := net.IPNet(n)\n\treturn ipnet.String()\n}\n\n// Contains checks whether the IP is part of the network.\nfunc (n IPNetwork) Contains(ip net.IP) bool {\n\tipnet := net.IPNet(n)\n\treturn ipnet.Contains(ip)\n}\n"
  },
  {
    "path": "internal/conf/ip_networks.go",
    "content": "package conf\n\nimport (\n\t\"encoding/json\"\n\t\"net\"\n\t\"strings\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper\"\n)\n\n// IPNetworks is a parameter that contains a list of IP networks.\ntype IPNetworks []IPNetwork\n\n// UnmarshalEnv implements env.Unmarshaler.\nfunc (d *IPNetworks) UnmarshalEnv(_ string, v string) error {\n\tbyts, _ := json.Marshal(strings.Split(v, \",\"))\n\treturn jsonwrapper.Unmarshal(byts, d)\n}\n\n// ToTrustedProxies converts IPNetworks into a string slice for SetTrustedProxies.\nfunc (d *IPNetworks) ToTrustedProxies() []string {\n\tret := make([]string, len(*d))\n\tfor i, entry := range *d {\n\t\tret[i] = entry.String()\n\t}\n\treturn ret\n}\n\n// Contains checks whether the IP is part of one of the networks.\nfunc (d IPNetworks) Contains(ip net.IP) bool {\n\tfor _, network := range d {\n\t\tif network.Contains(ip) {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "internal/conf/jsonwrapper/testdata/fuzz/FuzzUnmarshal/297c43aee5d30530",
    "content": "go test fuzz v1\n[]byte(\"[0\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\\n\")\n"
  },
  {
    "path": "internal/conf/jsonwrapper/testdata/fuzz/FuzzUnmarshal/7f71a2d116d5afde",
    "content": "go test fuzz v1\n[]byte(\"null\")\n"
  },
  {
    "path": "internal/conf/jsonwrapper/unmarshal.go",
    "content": "// Package jsonwrapper contains a JSON unmarshaler.\npackage jsonwrapper\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"reflect\"\n\t\"strings\"\n)\n\n// differences with respect to the standard package:\n// - JSON cannot contain unknown fields\n// - existing elements of slices are never used, fixing https://github.com/golang/go/issues/21092\n// - slices cannot be set to nil\n\nfunc jsonFieldKey(f reflect.StructField) string {\n\ttag := f.Tag.Get(\"json\")\n\tif tag == \"\" || tag == \"-\" {\n\t\treturn \"\"\n\t}\n\treturn strings.Split(tag, \",\")[0]\n}\n\nfunc isJSONNull(raw json.RawMessage) bool {\n\treturn string(bytes.TrimSpace(raw)) == \"null\"\n}\n\nfunc needsCustomDecode(t reflect.Type) bool {\n\tfor t.Kind() == reflect.Ptr {\n\t\tt = t.Elem()\n\t}\n\treturn t.Kind() == reflect.Struct || t.Kind() == reflect.Slice\n}\n\nfunc checkForUnknownFields(rawMap map[string]json.RawMessage, known map[string]int, path string) error {\n\tfor k := range rawMap {\n\t\tif _, ok := known[k]; !ok {\n\t\t\tif path != \"\" {\n\t\t\t\treturn fmt.Errorf(\"json: unknown field %q\", path+\".\"+k)\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"json: unknown field %q\", k)\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc decode(v reflect.Value, raw json.RawMessage, path string) error {\n\tfor v.Kind() == reflect.Ptr {\n\t\tif isJSONNull(raw) {\n\t\t\tv.Set(reflect.Zero(v.Type()))\n\t\t\treturn nil\n\t\t}\n\n\t\tif v.IsNil() {\n\t\t\tv.Set(reflect.New(v.Type().Elem()))\n\t\t}\n\n\t\tv = v.Elem()\n\t}\n\n\tif unm, ok := v.Addr().Interface().(json.Unmarshaler); ok {\n\t\treturn unm.UnmarshalJSON(raw)\n\t}\n\n\tswitch v.Kind() {\n\tcase reflect.Struct:\n\t\tvar rawMap map[string]json.RawMessage\n\t\terr := json.Unmarshal(raw, &rawMap)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tvType := v.Type()\n\t\tknown := make(map[string]int, v.NumField())\n\n\t\tfor i := 0; i < v.NumField(); i++ {\n\t\t\tif key := jsonFieldKey(vType.Field(i)); key != \"\" {\n\t\t\t\tknown[key] = i\n\t\t\t}\n\t\t}\n\n\t\terr = checkForUnknownFields(rawMap, known, path)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfor key, fieldIdx := range known {\n\t\t\trawVal, ok := rawMap[key]\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfieldPath := key\n\t\t\tif path != \"\" {\n\t\t\t\tfieldPath = path + \".\" + key\n\t\t\t}\n\n\t\t\terr = decode(v.Field(fieldIdx), rawVal, fieldPath)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t\treturn nil\n\n\tcase reflect.Slice:\n\t\tif isJSONNull(raw) {\n\t\t\tif path != \"\" {\n\t\t\t\treturn fmt.Errorf(\"cannot set slice %q to nil\", path)\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"cannot set slice to nil\")\n\t\t}\n\n\t\tif !v.IsNil() {\n\t\t\tv.Set(reflect.Zero(v.Type()))\n\t\t}\n\n\t\telemType := v.Type().Elem()\n\n\t\tif needsCustomDecode(elemType) {\n\t\t\tvar rawElems []json.RawMessage\n\t\t\tif err := json.Unmarshal(raw, &rawElems); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tslice := reflect.MakeSlice(v.Type(), len(rawElems), len(rawElems))\n\t\t\tfor i, re := range rawElems {\n\t\t\t\terr := decode(slice.Index(i), re, fmt.Sprintf(\"%s[%d]\", path, i))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\tv.Set(slice)\n\n\t\t\treturn nil\n\t\t}\n\n\t\treturn json.Unmarshal(raw, v.Addr().Interface())\n\n\tdefault:\n\t\treturn json.Unmarshal(raw, v.Addr().Interface())\n\t}\n}\n\n// Unmarshal decodes JSON.\nfunc Unmarshal(buf []byte, dest any) error {\n\treturn Decode(bytes.NewReader(buf), dest)\n}\n\n// Decode decodes JSON.\nfunc Decode(r io.Reader, dest any) error {\n\tbuf, err := io.ReadAll(r)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn decode(reflect.ValueOf(dest).Elem(), buf, \"\")\n}\n"
  },
  {
    "path": "internal/conf/jsonwrapper/unmarshal_test.go",
    "content": "package jsonwrapper\n\nimport (\n\t\"encoding/json\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype testStruct struct {\n\tField1 string `json:\"field1\"`\n\tField2 int    `json:\"field2\"`\n}\n\nfunc TestUnmarshalUnknownFields(t *testing.T) {\n\tinput := strings.NewReader(`{\"field1\": \"test\", \"unknownField\": \"value\", \"field2\": 456}`)\n\tvar result testStruct\n\terr := Decode(input, &result)\n\trequire.Error(t, err)\n\trequire.EqualError(t, err, \"json: unknown field \\\"unknownField\\\"\")\n}\n\nfunc TestUnmarshalPreventSliceReuse(t *testing.T) {\n\tt.Run(\"a\", func(t *testing.T) {\n\t\ttype Person struct {\n\t\t\tName string `json:\"name\"`\n\t\t\tAge  int    `json:\"age\"`\n\t\t}\n\n\t\tslice := []Person{\n\t\t\t{Name: \"John\", Age: 30},\n\t\t\t{Name: \"Jane\", Age: 25},\n\t\t}\n\n\t\tjson := []byte(`[{\"name\": \"Bob\"}]`)\n\t\terr := Unmarshal(json, &slice)\n\t\trequire.NoError(t, err)\n\n\t\trequire.Equal(t, []Person{{\n\t\t\tName: \"Bob\",\n\t\t\tAge:  0,\n\t\t}}, slice)\n\t})\n\n\tt.Run(\"b\", func(t *testing.T) {\n\t\ttype Config struct {\n\t\t\tName    string `json:\"name\"`\n\t\t\tEnabled bool   `json:\"enabled\"`\n\t\t\tPort    int    `json:\"port\"`\n\t\t}\n\t\ttype Settings struct {\n\t\t\tConfigs []Config `json:\"configs\"`\n\t\t}\n\n\t\tsettings := Settings{\n\t\t\tConfigs: []Config{\n\t\t\t\t{Name: \"old1\", Enabled: true, Port: 8080},\n\t\t\t\t{Name: \"old2\", Enabled: false, Port: 9090},\n\t\t\t\t{Name: \"old3\", Enabled: true, Port: 7070},\n\t\t\t},\n\t\t}\n\n\t\tjson := []byte(`{\"configs\": [{\"name\": \"new1\"}, {\"name\": \"new2\"}]}`)\n\t\terr := Unmarshal(json, &settings)\n\t\trequire.NoError(t, err)\n\n\t\trequire.Equal(t, Settings{\n\t\t\tConfigs: []Config{\n\t\t\t\t{Name: \"new1\", Enabled: false, Port: 0},\n\t\t\t\t{Name: \"new2\", Enabled: false, Port: 0},\n\t\t\t},\n\t\t}, settings)\n\t})\n}\n\nfunc TestUnmarshalSetSliceToNil(t *testing.T) {\n\tt.Run(\"top level\", func(t *testing.T) {\n\t\ttype Data struct {\n\t\t\tItems []string `json:\"items\"`\n\t\t}\n\n\t\tvar data Data\n\n\t\tjson := []byte(`{\"items\": null}`)\n\t\terr := Unmarshal(json, &data)\n\t\trequire.EqualError(t, err, \"cannot set slice \\\"items\\\" to nil\")\n\n\t\tdata = Data{Items: []string{\"a\", \"b\"}}\n\n\t\tjson = []byte(`{\"items\": null}`)\n\t\terr = Unmarshal(json, &data)\n\t\trequire.EqualError(t, err, \"cannot set slice \\\"items\\\" to nil\")\n\t})\n\n\tt.Run(\"nested\", func(t *testing.T) {\n\t\ttype Inner struct {\n\t\t\tValues []int `json:\"values\"`\n\t\t}\n\t\ttype Outer struct {\n\t\t\tInner Inner `json:\"inner\"`\n\t\t}\n\n\t\tvar data Outer\n\t\tjson := []byte(`{\"inner\": {\"values\": null}}`)\n\t\terr := Unmarshal(json, &data)\n\t\trequire.EqualError(t, err, \"cannot set slice \\\"inner.values\\\" to nil\")\n\t})\n}\n\nfunc TestUnmarshalSetNullableSliceToNil(t *testing.T) {\n\ttype Data struct {\n\t\tItems *[]string `json:\"items\"`\n\t}\n\n\tvar data Data\n\n\tjson := []byte(`{\"items\": null}`)\n\terr := Unmarshal(json, &data)\n\trequire.NoError(t, err)\n\n\tdata = Data{Items: &[]string{\"a\", \"b\"}}\n\n\tjson = []byte(`{\"items\": null}`)\n\terr = Unmarshal(json, &data)\n\trequire.NoError(t, err)\n}\n\ntype testStructWithUnmarshaler struct {\n\tField1 string `json:\"field1\"`\n}\n\nfunc (s *testStructWithUnmarshaler) UnmarshalJSON(b []byte) error {\n\tvar t string\n\tif err := json.Unmarshal(b, &t); err != nil {\n\t\treturn err\n\t}\n\n\ts.Field1 = t\n\treturn nil\n}\n\nfunc TestUnmarshalStructWithCustomUnmarshalerFromString(t *testing.T) {\n\tvar data testStructWithUnmarshaler\n\n\tjson := []byte(`\"testing\"`)\n\terr := Unmarshal(json, &data)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, &testStructWithUnmarshaler{Field1: \"testing\"}, &data)\n}\n\nfunc FuzzUnmarshal(f *testing.F) {\n\tf.Fuzz(func(_ *testing.T, buf []byte) {\n\t\tvar dest any\n\t\tUnmarshal(buf, &dest) //nolint:errcheck\n\t})\n}\n"
  },
  {
    "path": "internal/conf/log_destination.go",
    "content": "package conf\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n)\n\n// LogDestination represents a log destination.\ntype LogDestination logger.Destination\n\n// MarshalJSON implements json.Marshaler.\nfunc (d LogDestination) MarshalJSON() ([]byte, error) {\n\tswitch d {\n\tcase LogDestination(logger.DestinationStdout):\n\t\treturn json.Marshal(\"stdout\")\n\n\tcase LogDestination(logger.DestinationFile):\n\t\treturn json.Marshal(\"file\")\n\n\tdefault:\n\t\treturn json.Marshal(\"syslog\")\n\t}\n}\n\n// UnmarshalJSON implements json.Unmarshaler.\nfunc (d *LogDestination) UnmarshalJSON(b []byte) error {\n\tvar in string\n\tif err := jsonwrapper.Unmarshal(b, &in); err != nil {\n\t\treturn err\n\t}\n\n\tswitch in {\n\tcase \"stdout\":\n\t\t*d = LogDestination(logger.DestinationStdout)\n\n\tcase \"file\":\n\t\t*d = LogDestination(logger.DestinationFile)\n\n\tcase \"syslog\":\n\t\t*d = LogDestination(logger.DestinationSyslog)\n\n\tdefault:\n\t\treturn fmt.Errorf(\"invalid log destination: %s\", in)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/conf/log_destinations.go",
    "content": "package conf\n\nimport (\n\t\"encoding/json\"\n\t\"strings\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n)\n\n// LogDestinations is the logDestionations parameter.\ntype LogDestinations []LogDestination\n\n// UnmarshalEnv implements env.Unmarshaler.\nfunc (d *LogDestinations) UnmarshalEnv(_ string, v string) error {\n\tbyts, _ := json.Marshal(strings.Split(v, \",\"))\n\treturn jsonwrapper.Unmarshal(byts, d)\n}\n\n// ToDestinations converts to logger.Destination slice.\nfunc (d LogDestinations) ToDestinations() []logger.Destination {\n\tout := make([]logger.Destination, len(d))\n\tfor i, v := range d {\n\t\tout[i] = logger.Destination(v)\n\t}\n\treturn out\n}\n"
  },
  {
    "path": "internal/conf/log_level.go",
    "content": "package conf\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n)\n\n// LogLevel is the logLevel parameter.\ntype LogLevel logger.Level\n\n// MarshalJSON implements json.Marshaler.\nfunc (d LogLevel) MarshalJSON() ([]byte, error) {\n\tvar out string\n\n\tswitch d {\n\tcase LogLevel(logger.Error):\n\t\tout = \"error\"\n\n\tcase LogLevel(logger.Warn):\n\t\tout = \"warn\"\n\n\tcase LogLevel(logger.Info):\n\t\tout = \"info\"\n\n\tdefault:\n\t\tout = \"debug\"\n\t}\n\n\treturn json.Marshal(out)\n}\n\n// UnmarshalJSON implements json.Unmarshaler.\nfunc (d *LogLevel) UnmarshalJSON(b []byte) error {\n\tvar in string\n\tif err := jsonwrapper.Unmarshal(b, &in); err != nil {\n\t\treturn err\n\t}\n\n\tswitch in {\n\tcase \"error\":\n\t\t*d = LogLevel(logger.Error)\n\n\tcase \"warn\":\n\t\t*d = LogLevel(logger.Warn)\n\n\tcase \"info\":\n\t\t*d = LogLevel(logger.Info)\n\n\tcase \"debug\":\n\t\t*d = LogLevel(logger.Debug)\n\n\tdefault:\n\t\treturn fmt.Errorf(\"invalid log level: '%s'\", in)\n\t}\n\n\treturn nil\n}\n\n// UnmarshalEnv implements env.Unmarshaler.\nfunc (d *LogLevel) UnmarshalEnv(_ string, v string) error {\n\treturn d.UnmarshalJSON([]byte(`\"` + v + `\"`))\n}\n"
  },
  {
    "path": "internal/conf/optional_global.go",
    "content": "package conf\n\nimport (\n\t\"encoding/json\"\n\t\"reflect\"\n\t\"strings\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper\"\n)\n\nvar optionalGlobalValuesType = func() reflect.Type {\n\tvar fields []reflect.StructField\n\trt := reflect.TypeOf(Conf{})\n\tnf := rt.NumField()\n\n\tfor i := range nf {\n\t\tf := rt.Field(i)\n\t\tj := f.Tag.Get(\"json\")\n\n\t\tif j != \"-\" && j != \"pathDefaults\" && j != \"paths\" {\n\t\t\tif !strings.Contains(j, \",omitempty\") {\n\t\t\t\tj += \",omitempty\"\n\t\t\t}\n\n\t\t\ttyp := f.Type\n\t\t\tif typ.Kind() != reflect.Pointer {\n\t\t\t\ttyp = reflect.PointerTo(typ)\n\t\t\t}\n\n\t\t\tfields = append(fields, reflect.StructField{\n\t\t\t\tName: f.Name,\n\t\t\t\tType: typ,\n\t\t\t\tTag:  reflect.StructTag(`json:\"` + j + `\"`),\n\t\t\t})\n\t\t}\n\t}\n\n\treturn reflect.StructOf(fields)\n}()\n\nfunc newOptionalGlobalValues() any {\n\treturn reflect.New(optionalGlobalValuesType).Interface()\n}\n\n// OptionalGlobal is a Conf whose values can all be optional.\ntype OptionalGlobal struct {\n\tValues any\n}\n\n// UnmarshalJSON implements json.Unmarshaler.\nfunc (p *OptionalGlobal) UnmarshalJSON(b []byte) error {\n\tp.Values = newOptionalGlobalValues()\n\treturn jsonwrapper.Unmarshal(b, p.Values)\n}\n\n// MarshalJSON implements json.Marshaler.\nfunc (p *OptionalGlobal) MarshalJSON() ([]byte, error) {\n\treturn json.Marshal(p.Values)\n}\n"
  },
  {
    "path": "internal/conf/optional_path.go",
    "content": "package conf\n\nimport (\n\t\"encoding/json\"\n\t\"reflect\"\n\t\"strings\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf/env\"\n\t\"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper\"\n)\n\nvar optionalPathValuesType = func() reflect.Type {\n\tvar fields []reflect.StructField\n\trt := reflect.TypeOf(Path{})\n\tnf := rt.NumField()\n\n\tfor i := range nf {\n\t\tf := rt.Field(i)\n\t\tj := f.Tag.Get(\"json\")\n\n\t\tif j != \"-\" {\n\t\t\tif !strings.Contains(j, \",omitempty\") {\n\t\t\t\tj += \",omitempty\"\n\t\t\t}\n\n\t\t\ttyp := f.Type\n\t\t\tif typ.Kind() != reflect.Pointer {\n\t\t\t\ttyp = reflect.PointerTo(typ)\n\t\t\t}\n\n\t\t\tfields = append(fields, reflect.StructField{\n\t\t\t\tName: f.Name,\n\t\t\t\tType: typ,\n\t\t\t\tTag:  reflect.StructTag(`json:\"` + j + `\"`),\n\t\t\t})\n\t\t}\n\t}\n\n\treturn reflect.StructOf(fields)\n}()\n\nfunc newOptionalPathValues() any {\n\treturn reflect.New(optionalPathValuesType).Interface()\n}\n\n// OptionalPath is a Path whose values can all be optional.\ntype OptionalPath struct {\n\tValues any\n}\n\n// UnmarshalJSON implements json.Unmarshaler.\nfunc (p *OptionalPath) UnmarshalJSON(b []byte) error {\n\tp.Values = newOptionalPathValues()\n\treturn jsonwrapper.Unmarshal(b, p.Values)\n}\n\n// UnmarshalEnv implements env.Unmarshaler.\nfunc (p *OptionalPath) UnmarshalEnv(prefix string, _ string) error {\n\tif p.Values == nil {\n\t\tp.Values = newOptionalPathValues()\n\t}\n\treturn env.Load(prefix, p.Values)\n}\n\n// MarshalJSON implements json.Marshaler.\nfunc (p *OptionalPath) MarshalJSON() ([]byte, error) {\n\treturn json.Marshal(p.Values)\n}\n"
  },
  {
    "path": "internal/conf/path.go",
    "content": "package conf\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/url\"\n\t\"os\"\n\t\"reflect\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/base\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/mp4/codecs\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/pmp4\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n)\n\nvar rePathName = regexp.MustCompile(`^[0-9a-zA-Z_\\-/\\.~:]+$`)\n\n// IsValidPathName checks whether the path name is valid.\nfunc IsValidPathName(name string) error {\n\tif name == \"\" {\n\t\treturn fmt.Errorf(\"cannot be empty\")\n\t}\n\n\tif name[0] == '/' {\n\t\treturn fmt.Errorf(\"can't begin with a slash\")\n\t}\n\n\tif name[len(name)-1] == '/' {\n\t\treturn fmt.Errorf(\"can't end with a slash\")\n\t}\n\n\tif !rePathName.MatchString(name) {\n\t\treturn fmt.Errorf(\"can contain only alphanumeric characters, underscore, dot, tilde, minus, slash, colon\")\n\t}\n\n\treturn nil\n}\n\nfunc checkSRTPassphrase(passphrase string) error {\n\tswitch {\n\tcase len(passphrase) < 10 || len(passphrase) > 79:\n\t\treturn fmt.Errorf(\"must be between 10 and 79 characters\")\n\n\tdefault:\n\t\treturn nil\n\t}\n}\n\nfunc checkRedirect(v string) error {\n\tif strings.HasPrefix(v, \"/\") {\n\t\terr := IsValidPathName(v[1:])\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"'%s': %w\", v, err)\n\t\t}\n\t} else {\n\t\t_, err := base.ParseURL(v)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"'%s' is not a valid RTSP URL\", v)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc checkMP4MagicBytes(f io.ReadSeeker) error {\n\tmagicBytes := make([]byte, 4)\n\n\t_, err := f.Seek(4, io.SeekStart)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = io.ReadFull(f, magicBytes)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !bytes.Equal(magicBytes, []byte(\"ftyp\")) {\n\t\treturn fmt.Errorf(\"file is not MP4, magic bytes are %v\", magicBytes)\n\t}\n\n\t_, err = f.Seek(0, io.SeekStart)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc checkAlwaysAvailableFile(fpath string) error {\n\tf, err := os.Open(fpath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer f.Close()\n\n\terr = checkMP4MagicBytes(f)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar presentation pmp4.Presentation\n\terr = presentation.Unmarshal(f)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif len(presentation.Tracks) == 0 {\n\t\treturn fmt.Errorf(\"file does not contain any track\")\n\t}\n\n\tfor _, track := range presentation.Tracks {\n\t\tswitch track.Codec.(type) {\n\t\tcase *codecs.AV1, *codecs.VP9, *codecs.H265, *codecs.H264, *codecs.Opus, *codecs.MPEG4Audio, *codecs.LPCM:\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"unsupported codec %T\", track.Codec)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// FindPathConf returns the configuration corresponding to the given path name.\nfunc FindPathConf(pathConfs map[string]*Path, name string) (*Path, []string, error) {\n\t// normal path\n\tif pathConf, ok := pathConfs[name]; ok {\n\t\treturn pathConf, nil, nil\n\t}\n\n\terr := IsValidPathName(name)\n\tif err != nil {\n\t\treturn nil, nil, fmt.Errorf(\"invalid path name: %w (%s)\", err, name)\n\t}\n\n\t// gather and sort all regexp-based path configs\n\tvar regexpPathConfs []*Path\n\tfor _, pathConf := range pathConfs {\n\t\tif pathConf.Regexp != nil {\n\t\t\tregexpPathConfs = append(regexpPathConfs, pathConf)\n\t\t}\n\t}\n\tsort.Slice(regexpPathConfs, func(i, j int) bool {\n\t\t// keep all and all_others at the end\n\t\tif regexpPathConfs[i].Name == \"all\" || regexpPathConfs[i].Name == \"all_others\" {\n\t\t\treturn false\n\t\t}\n\t\tif regexpPathConfs[j].Name == \"all\" || regexpPathConfs[j].Name == \"all_others\" {\n\t\t\treturn true\n\t\t}\n\t\treturn regexpPathConfs[i].Name < regexpPathConfs[j].Name\n\t})\n\n\t// check path against regexp-based path configs\n\tfor _, pathConf := range regexpPathConfs {\n\t\tm := pathConf.Regexp.FindStringSubmatch(name)\n\t\tif m != nil {\n\t\t\treturn pathConf, m, nil\n\t\t}\n\t}\n\n\treturn nil, nil, fmt.Errorf(\"path '%s' is not configured\", name)\n}\n\n// Path is a path configuration.\ntype Path struct {\n\tRegexp *regexp.Regexp `json:\"-\"`    // filled by Validate()\n\tName   string         `json:\"name\"` // filled by Validate()\n\n\t// General\n\tSource                     string   `json:\"source\"`\n\tSourceFingerprint          string   `json:\"sourceFingerprint\"`\n\tSourceOnDemand             bool     `json:\"sourceOnDemand\"`\n\tSourceOnDemandStartTimeout Duration `json:\"sourceOnDemandStartTimeout\"`\n\tSourceOnDemandCloseAfter   Duration `json:\"sourceOnDemandCloseAfter\"`\n\tMaxReaders                 int      `json:\"maxReaders\"`\n\tSRTReadPassphrase          string   `json:\"srtReadPassphrase\"`\n\tFallback                   *string  `json:\"fallback,omitempty\" deprecated:\"true\"`\n\tUseAbsoluteTimestamp       bool     `json:\"useAbsoluteTimestamp\"`\n\n\t// Always available\n\tAlwaysAvailable       bool                   `json:\"alwaysAvailable\"`\n\tAlwaysAvailableTracks []AlwaysAvailableTrack `json:\"alwaysAvailableTracks\"`\n\tAlwaysAvailableFile   string                 `json:\"alwaysAvailableFile\"`\n\n\t// Record\n\tRecord                bool         `json:\"record\"`\n\tPlayback              *bool        `json:\"playback,omitempty\" deprecated:\"true\"`\n\tRecordPath            string       `json:\"recordPath\"`\n\tRecordFormat          RecordFormat `json:\"recordFormat\"`\n\tRecordPartDuration    Duration     `json:\"recordPartDuration\"`\n\tRecordMaxPartSize     StringSize   `json:\"recordMaxPartSize\"`\n\tRecordSegmentDuration Duration     `json:\"recordSegmentDuration\"`\n\tRecordDeleteAfter     Duration     `json:\"recordDeleteAfter\"`\n\n\t// Authentication (deprecated)\n\tPublishUser *Credential `json:\"publishUser,omitempty\" deprecated:\"true\"`\n\tPublishPass *Credential `json:\"publishPass,omitempty\" deprecated:\"true\"`\n\tPublishIPs  *IPNetworks `json:\"publishIPs,omitempty\" deprecated:\"true\"`\n\tReadUser    *Credential `json:\"readUser,omitempty\" deprecated:\"true\"`\n\tReadPass    *Credential `json:\"readPass,omitempty\" deprecated:\"true\"`\n\tReadIPs     *IPNetworks `json:\"readIPs,omitempty\" deprecated:\"true\"`\n\n\t// Publisher source\n\tOverridePublisher        bool   `json:\"overridePublisher\"`\n\tDisablePublisherOverride *bool  `json:\"disablePublisherOverride,omitempty\" deprecated:\"true\"`\n\tSRTPublishPassphrase     string `json:\"srtPublishPassphrase\"`\n\tRTSPDemuxMpegts          bool   `json:\"rtspDemuxMpegts\"`\n\n\t// RTSP source\n\tRTSPTransport          RTSPTransport  `json:\"rtspTransport\"`\n\tRTSPAnyPort            bool           `json:\"rtspAnyPort\"`\n\tSourceProtocol         *RTSPTransport `json:\"sourceProtocol,omitempty\" deprecated:\"true\"`\n\tSourceAnyPortEnable    *bool          `json:\"sourceAnyPortEnable,omitempty\" deprecated:\"true\"`\n\tRTSPRangeType          RTSPRangeType  `json:\"rtspRangeType\"`\n\tRTSPRangeStart         string         `json:\"rtspRangeStart\"`\n\tRTSPUDPReadBufferSize  *uint          `json:\"rtspUDPReadBufferSize,omitempty\" deprecated:\"true\"`\n\tRTSPUDPSourcePortRange []uint         `json:\"rtspUDPSourcePortRange\"`\n\n\t// MPEG-TS source\n\tMPEGTSUDPReadBufferSize *uint `json:\"mpegtsUDPReadBufferSize,omitempty\" deprecated:\"true\"`\n\n\t// RTP source\n\tRTPSDP               string `json:\"rtpSDP\"`\n\tRTPUDPReadBufferSize *uint  `json:\"rtpUDPReadBufferSize,omitempty\" deprecated:\"true\"`\n\n\t// WHEP source\n\tWHEPBearerToken        string   `json:\"whepBearerToken\"`\n\tWHEPSTUNGatherTimeout  Duration `json:\"whepSTUNGatherTimeout\"`\n\tWHEPHandshakeTimeout   Duration `json:\"whepHandshakeTimeout\"`\n\tWHEPTrackGatherTimeout Duration `json:\"whepTrackGatherTimeout\"`\n\n\t// Redirect source\n\tSourceRedirect string `json:\"sourceRedirect\"`\n\n\t// Raspberry Pi Camera source\n\tRPICameraCamID                 uint      `json:\"rpiCameraCamID\"`\n\tRPICameraSecondary             bool      `json:\"rpiCameraSecondary\"`\n\tRPICameraWidth                 uint      `json:\"rpiCameraWidth\"`\n\tRPICameraHeight                uint      `json:\"rpiCameraHeight\"`\n\tRPICameraHFlip                 bool      `json:\"rpiCameraHFlip\"`\n\tRPICameraVFlip                 bool      `json:\"rpiCameraVFlip\"`\n\tRPICameraBrightness            float64   `json:\"rpiCameraBrightness\"`\n\tRPICameraContrast              float64   `json:\"rpiCameraContrast\"`\n\tRPICameraSaturation            float64   `json:\"rpiCameraSaturation\"`\n\tRPICameraSharpness             float64   `json:\"rpiCameraSharpness\"`\n\tRPICameraExposure              string    `json:\"rpiCameraExposure\"`\n\tRPICameraAWB                   string    `json:\"rpiCameraAWB\"`\n\tRPICameraAWBGains              []float64 `json:\"rpiCameraAWBGains\"`\n\tRPICameraDenoise               string    `json:\"rpiCameraDenoise\"`\n\tRPICameraShutter               uint      `json:\"rpiCameraShutter\"`\n\tRPICameraMetering              string    `json:\"rpiCameraMetering\"`\n\tRPICameraGain                  float64   `json:\"rpiCameraGain\"`\n\tRPICameraEV                    float64   `json:\"rpiCameraEV\"`\n\tRPICameraROI                   string    `json:\"rpiCameraROI\"`\n\tRPICameraHDR                   bool      `json:\"rpiCameraHDR\"`\n\tRPICameraTuningFile            string    `json:\"rpiCameraTuningFile\"`\n\tRPICameraMode                  string    `json:\"rpiCameraMode\"`\n\tRPICameraFPS                   float64   `json:\"rpiCameraFPS\"`\n\tRPICameraAfMode                string    `json:\"rpiCameraAfMode\"`\n\tRPICameraAfRange               string    `json:\"rpiCameraAfRange\"`\n\tRPICameraAfSpeed               string    `json:\"rpiCameraAfSpeed\"`\n\tRPICameraLensPosition          float64   `json:\"rpiCameraLensPosition\"`\n\tRPICameraAfWindow              string    `json:\"rpiCameraAfWindow\"`\n\tRPICameraFlickerPeriod         uint      `json:\"rpiCameraFlickerPeriod\"`\n\tRPICameraTextOverlayEnable     bool      `json:\"rpiCameraTextOverlayEnable\"`\n\tRPICameraTextOverlay           string    `json:\"rpiCameraTextOverlay\"`\n\tRPICameraCodec                 string    `json:\"rpiCameraCodec\"`\n\tRPICameraIDRPeriod             uint      `json:\"rpiCameraIDRPeriod\"`\n\tRPICameraBitrate               uint      `json:\"rpiCameraBitrate\"`\n\tRPICameraProfile               *string   `json:\"rpiCameraProfile,omitempty\" deprecated:\"true\"`\n\tRPICameraLevel                 *string   `json:\"rpiCameraLevel,omitempty\" deprecated:\"true\"`\n\tRPICameraHardwareH264Profile   string    `json:\"rpiCameraHardwareH264Profile\"`\n\tRPICameraHardwareH264Level     string    `json:\"rpiCameraHardwareH264Level\"`\n\tRPICameraSoftwareH264Profile   string    `json:\"rpiCameraSoftwareH264Profile\"`\n\tRPICameraSoftwareH264Level     string    `json:\"rpiCameraSoftwareH264Level\"`\n\tRPICameraJPEGQuality           *uint     `json:\"rpiCameraJPEGQuality,omitempty\" deprecated:\"true\"`\n\tRPICameraMJPEGQuality          uint      `json:\"rpiCameraMJPEGQuality\"`\n\tRPICameraPrimaryName           string    `json:\"-\"` // filled by Validate()\n\tRPICameraSecondaryWidth        uint      `json:\"-\"` // filled by Validate()\n\tRPICameraSecondaryHeight       uint      `json:\"-\"` // filled by Validate()\n\tRPICameraSecondaryFPS          float64   `json:\"-\"` // filled by Validate()\n\tRPICameraSecondaryMJPEGQuality uint      `json:\"-\"` // filled by Validate()\n\n\t// Hooks\n\tRunOnInit                  string   `json:\"runOnInit\"`\n\tRunOnInitRestart           bool     `json:\"runOnInitRestart\"`\n\tRunOnDemand                string   `json:\"runOnDemand\"`\n\tRunOnDemandRestart         bool     `json:\"runOnDemandRestart\"`\n\tRunOnDemandStartTimeout    Duration `json:\"runOnDemandStartTimeout\"`\n\tRunOnDemandCloseAfter      Duration `json:\"runOnDemandCloseAfter\"`\n\tRunOnUnDemand              string   `json:\"runOnUnDemand\"`\n\tRunOnReady                 string   `json:\"runOnReady\"`\n\tRunOnReadyRestart          bool     `json:\"runOnReadyRestart\"`\n\tRunOnNotReady              string   `json:\"runOnNotReady\"`\n\tRunOnRead                  string   `json:\"runOnRead\"`\n\tRunOnReadRestart           bool     `json:\"runOnReadRestart\"`\n\tRunOnUnread                string   `json:\"runOnUnread\"`\n\tRunOnRecordSegmentCreate   string   `json:\"runOnRecordSegmentCreate\"`\n\tRunOnRecordSegmentComplete string   `json:\"runOnRecordSegmentComplete\"`\n}\n\nfunc (pconf *Path) setDefaults() {\n\t// General\n\tpconf.Source = \"publisher\"\n\tpconf.SourceOnDemandStartTimeout = 10 * Duration(time.Second)\n\tpconf.SourceOnDemandCloseAfter = 10 * Duration(time.Second)\n\n\t// Record\n\tpconf.RecordPath = \"./recordings/%path/%Y-%m-%d_%H-%M-%S-%f\"\n\tpconf.RecordFormat = RecordFormatFMP4\n\tpconf.RecordPartDuration = Duration(1 * time.Second)\n\tpconf.RecordMaxPartSize = 50 * 1024 * 1024\n\tpconf.RecordSegmentDuration = 3600 * Duration(time.Second)\n\tpconf.RecordDeleteAfter = 24 * 3600 * Duration(time.Second)\n\n\t// Publisher source\n\tpconf.OverridePublisher = true\n\n\t// RTSP source\n\tpconf.RTSPUDPSourcePortRange = []uint{10000, 65535}\n\n\t// WHEP source\n\tpconf.WHEPSTUNGatherTimeout = Duration(5 * time.Second)\n\tpconf.WHEPHandshakeTimeout = Duration(10 * time.Second)\n\tpconf.WHEPTrackGatherTimeout = Duration(2 * time.Second)\n\n\t// Raspberry Pi Camera source\n\tpconf.RPICameraWidth = 1920\n\tpconf.RPICameraHeight = 1080\n\tpconf.RPICameraContrast = 1\n\tpconf.RPICameraSaturation = 1\n\tpconf.RPICameraSharpness = 1\n\tpconf.RPICameraExposure = \"normal\"\n\tpconf.RPICameraAWB = \"auto\"\n\tpconf.RPICameraAWBGains = []float64{0, 0}\n\tpconf.RPICameraDenoise = \"off\"\n\tpconf.RPICameraMetering = \"centre\"\n\tpconf.RPICameraFPS = 30\n\tpconf.RPICameraAfMode = \"continuous\"\n\tpconf.RPICameraAfRange = \"normal\"\n\tpconf.RPICameraAfSpeed = \"normal\"\n\tpconf.RPICameraTextOverlay = \"%Y-%m-%d %H:%M:%S - MediaMTX\"\n\tpconf.RPICameraCodec = \"auto\"\n\tpconf.RPICameraIDRPeriod = 60\n\tpconf.RPICameraBitrate = 5000000\n\tpconf.RPICameraHardwareH264Profile = \"main\"\n\tpconf.RPICameraHardwareH264Level = \"4.1\"\n\tpconf.RPICameraSoftwareH264Profile = \"baseline\"\n\tpconf.RPICameraSoftwareH264Level = \"4.1\"\n\tpconf.RPICameraMJPEGQuality = 60\n\n\t// Hooks\n\tpconf.RunOnDemandStartTimeout = 10 * Duration(time.Second)\n\tpconf.RunOnDemandCloseAfter = 10 * Duration(time.Second)\n}\n\nfunc newPath(defaults *Path, partial *OptionalPath) *Path {\n\tpconf := &Path{}\n\tcopyStructFields(pconf, defaults)\n\tcopyStructFields(pconf, partial.Values)\n\treturn pconf\n}\n\n// Clone clones the configuration.\nfunc (pconf Path) Clone() *Path {\n\tcloned := deepClone(reflect.ValueOf(pconf)).Interface().(Path)\n\treturn &cloned\n}\n\nfunc (pconf *Path) validate(\n\tconf *Conf,\n\tname string,\n\tdeprecatedCredentialsMode bool,\n\tl logger.Writer,\n) error {\n\tpconf.Name = name\n\n\tswitch {\n\tcase name == \"all_others\", name == \"all\":\n\t\tpconf.Regexp = regexp.MustCompile(\"^.*$\")\n\n\tcase name == \"\" || name[0] != '~': // normal path\n\t\terr := IsValidPathName(name)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"invalid path name '%s': %w\", name, err)\n\t\t}\n\n\tdefault: // regular expression-based path\n\t\tregexp, err := regexp.Compile(name[1:])\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"invalid regular expression: %s\", name[1:])\n\t\t}\n\t\tpconf.Regexp = regexp\n\t}\n\n\t// common configuration errors\n\n\tif pconf.SRTPublishPassphrase != \"\" && pconf.Source != \"publisher\" {\n\t\treturn fmt.Errorf(\"'srtPublishPassphase' can only be used when source is 'publisher'\")\n\t}\n\n\tif pconf.Source != \"redirect\" && pconf.SourceRedirect != \"\" {\n\t\treturn fmt.Errorf(\"'sourceRedirect' is useless when source is not 'redirect'\")\n\t}\n\n\t// General\n\n\tswitch {\n\tcase pconf.Source == \"publisher\":\n\t\tif pconf.DisablePublisherOverride != nil {\n\t\t\tl.Log(logger.Warn, \"parameter 'disablePublisherOverride' is deprecated \"+\n\t\t\t\t\"and has been replaced with 'overridePublisher'\")\n\t\t\tpconf.OverridePublisher = !*pconf.DisablePublisherOverride\n\t\t}\n\n\t\tif pconf.SRTPublishPassphrase != \"\" {\n\t\t\terr := checkSRTPassphrase(pconf.SRTPublishPassphrase)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid 'srtPublishPassphrase': %w\", err)\n\t\t\t}\n\t\t}\n\n\tcase strings.HasPrefix(pconf.Source, \"rtsp://\") ||\n\t\tstrings.HasPrefix(pconf.Source, \"rtsps://\") ||\n\t\tstrings.HasPrefix(pconf.Source, \"rtsp+http://\") ||\n\t\tstrings.HasPrefix(pconf.Source, \"rtsps+http://\") ||\n\t\tstrings.HasPrefix(pconf.Source, \"rtsp+ws://\") ||\n\t\tstrings.HasPrefix(pconf.Source, \"rtsps+ws://\"):\n\t\t_, err := url.Parse(pconf.Source)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"'%s' is not a valid URL\", pconf.Source)\n\t\t}\n\n\t\tif pconf.SourceProtocol != nil {\n\t\t\tl.Log(logger.Warn, \"parameter 'sourceProtocol' is deprecated and has been replaced with 'rtspTransport'\")\n\t\t\tpconf.RTSPTransport = *pconf.SourceProtocol\n\t\t}\n\n\t\tif pconf.SourceAnyPortEnable != nil {\n\t\t\tl.Log(logger.Warn, \"parameter 'sourceAnyPortEnable' is deprecated and has been replaced with 'rtspAnyPort'\")\n\t\t\tpconf.RTSPAnyPort = *pconf.SourceAnyPortEnable\n\t\t}\n\n\tcase strings.HasPrefix(pconf.Source, \"rtmp://\") ||\n\t\tstrings.HasPrefix(pconf.Source, \"rtmps://\"):\n\t\tu, err := url.Parse(pconf.Source)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"'%s' is not a valid URL\", pconf.Source)\n\t\t}\n\n\t\tif u.User != nil {\n\t\t\tpass, _ := u.User.Password()\n\t\t\tuser := u.User.Username()\n\t\t\tif user != \"\" && pass == \"\" ||\n\t\t\t\tuser == \"\" && pass != \"\" {\n\t\t\t\treturn fmt.Errorf(\"username and password must be both provided\")\n\t\t\t}\n\t\t}\n\n\tcase strings.HasPrefix(pconf.Source, \"http://\") ||\n\t\tstrings.HasPrefix(pconf.Source, \"https://\"):\n\t\tu, err := url.Parse(pconf.Source)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"'%s' is not a valid URL\", pconf.Source)\n\t\t}\n\n\t\tif u.User != nil {\n\t\t\tpass, _ := u.User.Password()\n\t\t\tuser := u.User.Username()\n\t\t\tif user != \"\" && pass == \"\" ||\n\t\t\t\tuser == \"\" && pass != \"\" {\n\t\t\t\treturn fmt.Errorf(\"username and password must be both provided\")\n\t\t\t}\n\t\t}\n\n\tcase strings.HasPrefix(pconf.Source, \"udp://\"):\n\t\t_, _, err := net.SplitHostPort(pconf.Source[len(\"udp://\"):])\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"'%s' is not a valid UDP+MPEGTS URL\", pconf.Source)\n\t\t}\n\n\tcase strings.HasPrefix(pconf.Source, \"udp+mpegts://\"):\n\t\t_, _, err := net.SplitHostPort(pconf.Source[len(\"udp+mpegts://\"):])\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"'%s' is not a valid UDP+MPEGTS URL\", pconf.Source)\n\t\t}\n\n\tcase strings.HasPrefix(pconf.Source, \"unix+mpegts://\"):\n\n\tcase strings.HasPrefix(pconf.Source, \"udp+rtp://\"):\n\t\t_, _, err := net.SplitHostPort(pconf.Source[len(\"udp+rtp://\"):])\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"'%s' is not a valid UDP+RTP URL\", pconf.Source)\n\t\t}\n\n\t\tif pconf.RTPSDP == \"\" {\n\t\t\treturn fmt.Errorf(\"`rtpSDP` was not provided\")\n\t\t}\n\n\tcase strings.HasPrefix(pconf.Source, \"unix+rtp://\"):\n\t\tl.Log(logger.Warn, \"source 'unix+rtp' is deprecated due to intrinsic instability, use 'udp+rtp' instead\")\n\t\tif pconf.RTPSDP == \"\" {\n\t\t\treturn fmt.Errorf(\"`rtpSDP` was not provided\")\n\t\t}\n\n\tcase strings.HasPrefix(pconf.Source, \"srt://\"):\n\t\t_, err := url.Parse(pconf.Source)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"'%s' is not a valid URL\", pconf.Source)\n\t\t}\n\n\tcase strings.HasPrefix(pconf.Source, \"whep://\") ||\n\t\tstrings.HasPrefix(pconf.Source, \"wheps://\"):\n\t\t_, err := url.Parse(pconf.Source)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"'%s' is not a valid URL\", pconf.Source)\n\t\t}\n\n\tcase pconf.Source == \"redirect\":\n\t\tif pconf.SourceRedirect == \"\" {\n\t\t\treturn fmt.Errorf(\"source redirect must be filled\")\n\t\t}\n\n\t\terr := checkRedirect(pconf.SourceRedirect)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\tcase pconf.Source == \"rpiCamera\":\n\n\t\tif pconf.RPICameraWidth == 0 {\n\t\t\treturn fmt.Errorf(\"invalid 'rpiCameraWidth' value\")\n\t\t}\n\n\t\tif pconf.RPICameraHeight == 0 {\n\t\t\treturn fmt.Errorf(\"invalid 'rpiCameraHeight' value\")\n\t\t}\n\n\t\tswitch pconf.RPICameraExposure {\n\t\tcase \"normal\", \"short\", \"long\", \"custom\":\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"invalid 'rpiCameraExposure' value\")\n\t\t}\n\n\t\tswitch pconf.RPICameraAWB {\n\t\tcase \"auto\", \"incandescent\", \"tungsten\", \"fluorescent\", \"indoor\", \"daylight\", \"cloudy\", \"custom\":\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"invalid 'rpiCameraAWB' value\")\n\t\t}\n\n\t\tif len(pconf.RPICameraAWBGains) != 2 {\n\t\t\treturn fmt.Errorf(\"invalid 'rpiCameraAWBGains' value\")\n\t\t}\n\n\t\tswitch pconf.RPICameraDenoise {\n\t\tcase \"off\", \"cdn_off\", \"cdn_fast\", \"cdn_hq\":\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"invalid 'rpiCameraDenoise' value\")\n\t\t}\n\n\t\tswitch pconf.RPICameraMetering {\n\t\tcase \"centre\", \"spot\", \"matrix\", \"custom\":\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"invalid 'rpiCameraMetering' value\")\n\t\t}\n\n\t\tswitch pconf.RPICameraAfMode {\n\t\tcase \"auto\", \"manual\", \"continuous\":\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"invalid 'rpiCameraAfMode' value\")\n\t\t}\n\n\t\tswitch pconf.RPICameraAfRange {\n\t\tcase \"normal\", \"macro\", \"full\":\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"invalid 'rpiCameraAfRange' value\")\n\t\t}\n\n\t\tswitch pconf.RPICameraAfSpeed {\n\t\tcase \"normal\", \"fast\":\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"invalid 'rpiCameraAfSpeed' value\")\n\t\t}\n\n\t\tif pconf.RPICameraProfile != nil {\n\t\t\tl.Log(logger.Warn, \"parameter 'rpiCameraProfile' is deprecated\"+\n\t\t\t\t\" and has been replaced with 'rpiCameraHardwareH264Profile'\")\n\t\t\tpconf.RPICameraHardwareH264Profile = *pconf.RPICameraProfile\n\t\t}\n\n\t\tif pconf.RPICameraLevel != nil {\n\t\t\tl.Log(logger.Warn, \"parameter 'rpiCameraLevel' is deprecated\"+\n\t\t\t\t\" and has been replaced with 'rpiCameraHardwareH264Level'\")\n\t\t\tpconf.RPICameraHardwareH264Level = *pconf.RPICameraLevel\n\t\t}\n\n\t\tswitch pconf.RPICameraHardwareH264Profile {\n\t\tcase \"baseline\", \"main\", \"high\":\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"invalid 'rpiCameraHardwareH264Profile' value\")\n\t\t}\n\n\t\tswitch pconf.RPICameraHardwareH264Level {\n\t\tcase \"4.0\", \"4.1\", \"4.2\":\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"invalid 'rpiCameraHardwareH264Level' value\")\n\t\t}\n\n\t\tswitch pconf.RPICameraSoftwareH264Profile {\n\t\tcase \"baseline\", \"main\", \"high\":\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"invalid 'rpiCameraSoftwareH264Profile' value\")\n\t\t}\n\n\t\tswitch pconf.RPICameraSoftwareH264Level {\n\t\tcase \"4.0\", \"4.1\", \"4.2\":\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"invalid 'rpiCameraSoftwareH264Level' value\")\n\t\t}\n\n\t\tif pconf.RPICameraJPEGQuality != nil {\n\t\t\tl.Log(logger.Warn, \"parameter 'rpiCameraJPEGQuality' is deprecated\"+\n\t\t\t\t\" and has been replaced with 'rpiCameraMJPEGQuality'\")\n\t\t\tpconf.RPICameraMJPEGQuality = *pconf.RPICameraJPEGQuality\n\t\t}\n\n\t\tif !pconf.RPICameraSecondary {\n\t\t\tswitch pconf.RPICameraCodec {\n\t\t\tcase \"auto\", \"hardwareH264\", \"softwareH264\":\n\t\t\tdefault:\n\t\t\t\treturn fmt.Errorf(\"supported codecs for a primary RPI Camera stream are auto, hardwareH264, softwareH264\")\n\t\t\t}\n\n\t\t\tfor otherName, otherPath := range conf.Paths {\n\t\t\t\tif otherPath != pconf &&\n\t\t\t\t\totherPath != nil &&\n\t\t\t\t\totherPath.Source == \"rpiCamera\" &&\n\t\t\t\t\totherPath.RPICameraCamID == pconf.RPICameraCamID &&\n\t\t\t\t\t!otherPath.RPICameraSecondary {\n\t\t\t\t\treturn fmt.Errorf(\"'rpiCamera' with same camera ID %d is used as source in two paths, '%s' and '%s'\",\n\t\t\t\t\t\tpconf.RPICameraCamID, name, otherName)\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tswitch pconf.RPICameraCodec {\n\t\t\tcase \"auto\", \"mjpeg\":\n\t\t\tdefault:\n\t\t\t\treturn fmt.Errorf(\"supported codecs for a secondary RPI Camera stream are auto, mjpeg\")\n\t\t\t}\n\n\t\t\tvar primaryName string\n\t\t\tvar primary *Path\n\n\t\t\tfor otherPathName, otherPath := range conf.Paths {\n\t\t\t\tif otherPath != pconf &&\n\t\t\t\t\totherPath != nil &&\n\t\t\t\t\totherPath.Source == \"rpiCamera\" &&\n\t\t\t\t\totherPath.RPICameraCamID == pconf.RPICameraCamID &&\n\t\t\t\t\t!otherPath.RPICameraSecondary {\n\t\t\t\t\tprimaryName = otherPathName\n\t\t\t\t\tprimary = otherPath\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif primary == nil {\n\t\t\t\treturn fmt.Errorf(\"cannot find a primary RPI Camera stream to associate with the secondary stream\")\n\t\t\t}\n\n\t\t\tif primary.RPICameraSecondaryWidth != 0 {\n\t\t\t\treturn fmt.Errorf(\"a primary RPI Camera stream is associated with multiple secondary streams\")\n\t\t\t}\n\n\t\t\tpconf.RPICameraPrimaryName = primaryName\n\t\t\tprimary.RPICameraSecondaryWidth = pconf.RPICameraWidth\n\t\t\tprimary.RPICameraSecondaryHeight = pconf.RPICameraHeight\n\t\t\tprimary.RPICameraSecondaryFPS = pconf.RPICameraFPS\n\t\t\tprimary.RPICameraSecondaryMJPEGQuality = pconf.RPICameraMJPEGQuality\n\t\t}\n\n\tdefault:\n\t\treturn fmt.Errorf(\"invalid source: '%s'\", pconf.Source)\n\t}\n\n\tif pconf.SourceOnDemand {\n\t\tif pconf.Source == \"publisher\" {\n\t\t\treturn fmt.Errorf(\"'sourceOnDemand' is useless when source is 'publisher'\")\n\t\t}\n\t} else {\n\t\tif pconf.Source != \"publisher\" && pconf.Source != \"redirect\" && pconf.Regexp != nil {\n\t\t\treturn fmt.Errorf(\"a path with a regular expression (or path 'all') and a static source\" +\n\t\t\t\t\" must have 'sourceOnDemand' set to true\")\n\t\t}\n\t}\n\n\tif pconf.SRTReadPassphrase != \"\" {\n\t\terr := checkSRTPassphrase(pconf.SRTReadPassphrase)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"invalid 'readRTPassphrase': %w\", err)\n\t\t}\n\t}\n\n\tif pconf.Fallback != nil {\n\t\tl.Log(logger.Warn, \"the 'fallback' feature is deprecated, use 'alwaysAvailable' instead\")\n\t\terr := checkRedirect(*pconf.Fallback)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// Always available\n\n\tif pconf.AlwaysAvailable {\n\t\tif pconf.Regexp != nil {\n\t\t\treturn fmt.Errorf(\"'alwaysAvailable' cannot be used in a path with a regular expression (or path 'all')\")\n\t\t}\n\n\t\tif pconf.SourceOnDemand {\n\t\t\treturn fmt.Errorf(\"'sourceOnDemand' is not compatible with 'alwaysAvailable'\")\n\t\t}\n\n\t\tif pconf.RunOnDemand != \"\" || pconf.RunOnUnDemand != \"\" {\n\t\t\treturn fmt.Errorf(\"'runOnDemand' and 'runOnUnDemand' cannot be used with 'alwaysAvailable'\")\n\t\t}\n\n\t\tif pconf.AlwaysAvailableFile != \"\" {\n\t\t\tif len(pconf.AlwaysAvailableTracks) != 0 {\n\t\t\t\treturn fmt.Errorf(\"'alwaysAvailableFile' and 'alwaysAvailableTracks' cannot be used together\")\n\t\t\t}\n\n\t\t\terr := checkAlwaysAvailableFile(pconf.AlwaysAvailableFile)\n\t\t\tif err != nil {\n\t\t\t\treturn fmt.Errorf(\"invalid 'alwaysAvailableFile': %w\", err)\n\t\t\t}\n\t\t} else if len(pconf.AlwaysAvailableTracks) == 0 {\n\t\t\treturn fmt.Errorf(\"'alwaysAvailableTracks' must contain at least one track\")\n\t\t}\n\n\t\tif pconf.UseAbsoluteTimestamp {\n\t\t\treturn fmt.Errorf(\"'useAbsoluteTimestamp' cannot be used with 'alwaysAvailable'\")\n\t\t}\n\t}\n\n\t// Record\n\n\tif pconf.Playback != nil {\n\t\tl.Log(logger.Warn, \"parameter 'playback' is deprecated and has no effect\")\n\t}\n\n\tif !strings.Contains(pconf.RecordPath, \"%path\") {\n\t\treturn fmt.Errorf(\"'recordPath' must contain %%path\")\n\t}\n\n\tif !strings.Contains(pconf.RecordPath, \"%s\") &&\n\t\t(!strings.Contains(pconf.RecordPath, \"%Y\") ||\n\t\t\t!strings.Contains(pconf.RecordPath, \"%m\") ||\n\t\t\t!strings.Contains(pconf.RecordPath, \"%d\") ||\n\t\t\t!strings.Contains(pconf.RecordPath, \"%H\") ||\n\t\t\t!strings.Contains(pconf.RecordPath, \"%M\") ||\n\t\t\t!strings.Contains(pconf.RecordPath, \"%S\")) {\n\t\treturn fmt.Errorf(\"'recordPath' must contain either %%s or %%Y %%m %%d %%H %%M %%S\")\n\t}\n\n\tif conf.Playback && !strings.Contains(pconf.RecordPath, \"%f\") {\n\t\treturn fmt.Errorf(\"'recordPath' must contain %%f\")\n\t}\n\n\tif pconf.RecordSegmentDuration > Duration(24*time.Hour) { // avoid overflowing DurationV0 of mvhd\n\t\treturn fmt.Errorf(\"maximum segment duration is 1 day\")\n\t}\n\n\tif pconf.RecordDeleteAfter != 0 && pconf.RecordDeleteAfter < pconf.RecordSegmentDuration {\n\t\treturn fmt.Errorf(\"'recordDeleteAfter' cannot be lower than 'recordSegmentDuration'\")\n\t}\n\n\t// Authentication (deprecated)\n\n\tif deprecatedCredentialsMode {\n\t\tfunc() {\n\t\t\tvar user Credential = \"any\"\n\t\t\tif pconf.PublishUser != nil && *pconf.PublishUser != \"\" {\n\t\t\t\tuser = *pconf.PublishUser\n\t\t\t}\n\n\t\t\tvar pass Credential\n\t\t\tif pconf.PublishPass != nil && *pconf.PublishPass != \"\" {\n\t\t\t\tpass = *pconf.PublishPass\n\t\t\t}\n\n\t\t\tips := IPNetworks{mustParseCIDR(\"0.0.0.0/0\")}\n\t\t\tif pconf.PublishIPs != nil && len(*pconf.PublishIPs) != 0 {\n\t\t\t\tips = *pconf.PublishIPs\n\t\t\t}\n\n\t\t\tpathName := name\n\t\t\tif name == \"all_others\" || name == \"all\" {\n\t\t\t\tpathName = \"~^.*$\"\n\t\t\t}\n\n\t\t\tconf.AuthInternalUsers = append(conf.AuthInternalUsers, AuthInternalUser{\n\t\t\t\tUser: user,\n\t\t\t\tPass: pass,\n\t\t\t\tIPs:  ips,\n\t\t\t\tPermissions: []AuthInternalUserPermission{{\n\t\t\t\t\tAction: AuthActionPublish,\n\t\t\t\t\tPath:   pathName,\n\t\t\t\t}},\n\t\t\t})\n\t\t}()\n\n\t\tfunc() {\n\t\t\tvar user Credential = \"any\"\n\t\t\tif pconf.ReadUser != nil && *pconf.ReadUser != \"\" {\n\t\t\t\tuser = *pconf.ReadUser\n\t\t\t}\n\n\t\t\tvar pass Credential\n\t\t\tif pconf.ReadPass != nil && *pconf.ReadPass != \"\" {\n\t\t\t\tpass = *pconf.ReadPass\n\t\t\t}\n\n\t\t\tips := IPNetworks{mustParseCIDR(\"0.0.0.0/0\")}\n\t\t\tif pconf.ReadIPs != nil && len(*pconf.ReadIPs) != 0 {\n\t\t\t\tips = *pconf.ReadIPs\n\t\t\t}\n\n\t\t\tpathName := name\n\t\t\tif name == \"all_others\" || name == \"all\" {\n\t\t\t\tpathName = \"~^.*$\"\n\t\t\t}\n\n\t\t\tconf.AuthInternalUsers = append(conf.AuthInternalUsers, AuthInternalUser{\n\t\t\t\tUser: user,\n\t\t\t\tPass: pass,\n\t\t\t\tIPs:  ips,\n\t\t\t\tPermissions: []AuthInternalUserPermission{{\n\t\t\t\t\tAction: AuthActionRead,\n\t\t\t\t\tPath:   pathName,\n\t\t\t\t}},\n\t\t\t})\n\t\t}()\n\t}\n\n\t// Hooks\n\n\tif pconf.RunOnInit != \"\" && pconf.Regexp != nil {\n\t\treturn fmt.Errorf(\"a path with a regular expression (or path 'all')\" +\n\t\t\t\" does not support option 'runOnInit'; use another path\")\n\t}\n\n\tif (pconf.RunOnDemand != \"\" || pconf.RunOnUnDemand != \"\") && pconf.Source != \"publisher\" {\n\t\treturn fmt.Errorf(\"'runOnDemand' and 'runOnUnDemand' can be used only when source is 'publisher'\")\n\t}\n\n\treturn nil\n}\n\n// Equal checks whether two Paths are equal.\nfunc (pconf *Path) Equal(other *Path) bool {\n\treturn reflect.DeepEqual(pconf, other)\n}\n\n// HasStaticSource checks whether the path has a static source.\nfunc (pconf Path) HasStaticSource() bool {\n\treturn pconf.Source != \"publisher\" && pconf.Source != \"redirect\"\n}\n\n// HasOnDemandStaticSource checks whether the path has a on demand static source.\nfunc (pconf Path) HasOnDemandStaticSource() bool {\n\treturn pconf.HasStaticSource() && pconf.SourceOnDemand\n}\n\n// HasOnDemandPublisher checks whether the path has a on-demand publisher.\nfunc (pconf Path) HasOnDemandPublisher() bool {\n\treturn pconf.RunOnDemand != \"\"\n}\n"
  },
  {
    "path": "internal/conf/path_test.go",
    "content": "package conf\n\nimport (\n\t\"testing\"\n\n\t\"github.com/bluenviron/gortsplib/v5\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPathClone(t *testing.T) {\n\toriginal := &Path{\n\t\tName:                \"example\",\n\t\tRTSPTransport:       RTSPTransport{ptrOf(gortsplib.ProtocolUDP)},\n\t\tSourceAnyPortEnable: ptrOf(true),\n\t\tRecordPath:          \"/var/recordings\",\n\t}\n\n\tclone := original.Clone()\n\trequire.Equal(t, original, clone)\n}\n"
  },
  {
    "path": "internal/conf/record_format.go",
    "content": "package conf\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper\"\n)\n\n// RecordFormat is the recordFormat parameter.\ntype RecordFormat string\n\n// supported values.\nconst (\n\tRecordFormatFMP4   RecordFormat = \"fmp4\"\n\tRecordFormatMPEGTS RecordFormat = \"mpegts\"\n)\n\n// UnmarshalJSON implements json.Unmarshaler.\nfunc (d *RecordFormat) UnmarshalJSON(b []byte) error {\n\ttype alias RecordFormat\n\tif err := jsonwrapper.Unmarshal(b, (*alias)(d)); err != nil {\n\t\treturn err\n\t}\n\n\tswitch *d {\n\tcase RecordFormatFMP4, RecordFormatMPEGTS:\n\n\tdefault:\n\t\treturn fmt.Errorf(\"invalid record format '%s'\", *d)\n\t}\n\n\treturn nil\n}\n\n// UnmarshalEnv implements env.Unmarshaler.\nfunc (d *RecordFormat) UnmarshalEnv(_ string, v string) error {\n\treturn d.UnmarshalJSON([]byte(`\"` + v + `\"`))\n}\n"
  },
  {
    "path": "internal/conf/rtsp_auth_method.go",
    "content": "package conf\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/auth\"\n\t\"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper\"\n)\n\n// RTSPAuthMethod represents an RTSP authentication method.\ntype RTSPAuthMethod auth.VerifyMethod\n\n// MarshalJSON implements json.Marshaler.\nfunc (d RTSPAuthMethod) MarshalJSON() ([]byte, error) {\n\tswitch d {\n\tcase RTSPAuthMethod(auth.VerifyMethodBasic):\n\t\treturn json.Marshal(\"basic\")\n\n\tdefault:\n\t\treturn json.Marshal(\"digest\")\n\t}\n}\n\n// UnmarshalJSON implements json.Unmarshaler.\nfunc (d *RTSPAuthMethod) UnmarshalJSON(b []byte) error {\n\tvar in string\n\tif err := jsonwrapper.Unmarshal(b, &in); err != nil {\n\t\treturn err\n\t}\n\n\tswitch in {\n\tcase \"basic\":\n\t\t*d = RTSPAuthMethod(auth.VerifyMethodBasic)\n\n\tcase \"digest\":\n\t\t*d = RTSPAuthMethod(auth.VerifyMethodDigestMD5)\n\n\tdefault:\n\t\treturn fmt.Errorf(\"invalid authentication method: '%s'\", in)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/conf/rtsp_auth_methods.go",
    "content": "package conf\n\nimport (\n\t\"encoding/json\"\n\t\"strings\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/auth\"\n\t\"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper\"\n)\n\n// RTSPAuthMethods is the rtspAuthMethods parameter.\ntype RTSPAuthMethods []RTSPAuthMethod\n\n// UnmarshalEnv implements env.Unmarshaler.\nfunc (d *RTSPAuthMethods) UnmarshalEnv(_ string, v string) error {\n\tbyts, _ := json.Marshal(strings.Split(v, \",\"))\n\treturn jsonwrapper.Unmarshal(byts, d)\n}\n\n// ToAuthMethods converts to auth.VerifyMethod slice.\nfunc (d RTSPAuthMethods) ToAuthMethods() []auth.VerifyMethod {\n\tout := make([]auth.VerifyMethod, len(d))\n\tfor i, v := range d {\n\t\tout[i] = auth.VerifyMethod(v)\n\t}\n\treturn out\n}\n"
  },
  {
    "path": "internal/conf/rtsp_range_type.go",
    "content": "package conf\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper\"\n)\n\n// RTSPRangeType is the type used in the Range header.\ntype RTSPRangeType string\n\n// supported values.\nconst (\n\tRTSPRangeTypeUndefined RTSPRangeType = \"\"\n\tRTSPRangeTypeClock     RTSPRangeType = \"clock\"\n\tRTSPRangeTypeNPT       RTSPRangeType = \"npt\"\n\tRTSPRangeTypeSMPTE     RTSPRangeType = \"smpte\"\n)\n\n// UnmarshalJSON implements json.Unmarshaler.\nfunc (d *RTSPRangeType) UnmarshalJSON(b []byte) error {\n\ttype alias RTSPRangeType\n\tif err := jsonwrapper.Unmarshal(b, (*alias)(d)); err != nil {\n\t\treturn err\n\t}\n\n\tswitch *d {\n\tcase RTSPRangeTypeUndefined, RTSPRangeTypeClock, RTSPRangeTypeNPT, RTSPRangeTypeSMPTE:\n\n\tdefault:\n\t\treturn fmt.Errorf(\"invalid rtsp range type: '%s'\", *d)\n\t}\n\n\treturn nil\n}\n\n// UnmarshalEnv implements env.Unmarshaler.\nfunc (d *RTSPRangeType) UnmarshalEnv(_ string, v string) error {\n\treturn d.UnmarshalJSON([]byte(`\"` + v + `\"`))\n}\n"
  },
  {
    "path": "internal/conf/rtsp_transport.go",
    "content": "package conf\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/bluenviron/gortsplib/v5\"\n\t\"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper\"\n)\n\nfunc ptrOf[T any](v T) *T {\n\tp := new(T)\n\t*p = v\n\treturn p\n}\n\n// RTSPTransport is the rtspTransport parameter.\ntype RTSPTransport struct {\n\t*gortsplib.Protocol\n}\n\n// MarshalJSON implements json.Marshaler.\nfunc (d RTSPTransport) MarshalJSON() ([]byte, error) {\n\tvar out string\n\n\tif d.Protocol == nil {\n\t\tout = \"automatic\"\n\t} else {\n\t\tswitch *d.Protocol {\n\t\tcase gortsplib.ProtocolUDP:\n\t\t\tout = \"udp\"\n\n\t\tcase gortsplib.ProtocolUDPMulticast:\n\t\t\tout = \"multicast\"\n\n\t\tdefault:\n\t\t\tout = \"tcp\"\n\t\t}\n\t}\n\n\treturn json.Marshal(out)\n}\n\n// UnmarshalJSON implements json.Unmarshaler.\nfunc (d *RTSPTransport) UnmarshalJSON(b []byte) error {\n\tvar in string\n\tif err := jsonwrapper.Unmarshal(b, &in); err != nil {\n\t\treturn err\n\t}\n\n\tswitch in {\n\tcase \"udp\":\n\t\td.Protocol = ptrOf(gortsplib.ProtocolUDP)\n\n\tcase \"multicast\":\n\t\td.Protocol = ptrOf(gortsplib.ProtocolUDPMulticast)\n\n\tcase \"tcp\":\n\t\td.Protocol = ptrOf(gortsplib.ProtocolTCP)\n\n\tcase \"automatic\":\n\t\td.Protocol = nil\n\n\tdefault:\n\t\treturn fmt.Errorf(\"invalid transport '%s'\", in)\n\t}\n\n\treturn nil\n}\n\n// UnmarshalEnv implements env.Unmarshaler.\nfunc (d *RTSPTransport) UnmarshalEnv(_ string, v string) error {\n\treturn d.UnmarshalJSON([]byte(`\"` + v + `\"`))\n}\n"
  },
  {
    "path": "internal/conf/rtsp_transports.go",
    "content": "package conf\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/bluenviron/gortsplib/v5\"\n\t\"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper\"\n)\n\n// RTSPTransports is the rtspTransports parameter.\ntype RTSPTransports map[gortsplib.Protocol]struct{}\n\n// MarshalJSON implements json.Marshaler.\nfunc (d RTSPTransports) MarshalJSON() ([]byte, error) {\n\tout := make([]string, len(d))\n\ti := 0\n\n\tfor p := range d {\n\t\tvar v string\n\n\t\tswitch p {\n\t\tcase gortsplib.ProtocolUDP:\n\t\t\tv = \"udp\"\n\n\t\tcase gortsplib.ProtocolUDPMulticast:\n\t\t\tv = \"multicast\"\n\n\t\tdefault:\n\t\t\tv = \"tcp\"\n\t\t}\n\n\t\tout[i] = v\n\t\ti++\n\t}\n\n\tsort.Strings(out)\n\n\treturn json.Marshal(out)\n}\n\n// UnmarshalJSON implements json.Unmarshaler.\nfunc (d *RTSPTransports) UnmarshalJSON(b []byte) error {\n\tvar in []string\n\tif err := jsonwrapper.Unmarshal(b, &in); err != nil {\n\t\treturn err\n\t}\n\n\t*d = make(RTSPTransports)\n\n\tfor _, proto := range in {\n\t\tswitch proto {\n\t\tcase \"udp\":\n\t\t\t(*d)[gortsplib.ProtocolUDP] = struct{}{}\n\n\t\tcase \"multicast\":\n\t\t\t(*d)[gortsplib.ProtocolUDPMulticast] = struct{}{}\n\n\t\tcase \"tcp\":\n\t\t\t(*d)[gortsplib.ProtocolTCP] = struct{}{}\n\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"invalid transport: %s\", proto)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// UnmarshalEnv implements env.Unmarshaler.\nfunc (d *RTSPTransports) UnmarshalEnv(_ string, v string) error {\n\tbyts, _ := json.Marshal(strings.Split(v, \",\"))\n\treturn d.UnmarshalJSON(byts)\n}\n"
  },
  {
    "path": "internal/conf/string_size.go",
    "content": "package conf\n\nimport (\n\t\"code.cloudfoundry.org/bytefmt\"\n\t\"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper\"\n)\n\n// StringSize is a size that is unmarshaled from a string.\ntype StringSize uint64\n\n// MarshalJSON implements json.Marshaler.\nfunc (s StringSize) MarshalJSON() ([]byte, error) {\n\treturn []byte(`\"` + bytefmt.ByteSize(uint64(s)) + `\"`), nil\n}\n\n// UnmarshalJSON implements json.Unmarshaler.\nfunc (s *StringSize) UnmarshalJSON(b []byte) error {\n\tvar in string\n\tif err := jsonwrapper.Unmarshal(b, &in); err != nil {\n\t\treturn err\n\t}\n\n\tv, err := bytefmt.ToBytes(in)\n\tif err != nil {\n\t\treturn err\n\t}\n\t*s = StringSize(v)\n\n\treturn nil\n}\n\n// UnmarshalEnv implements env.Unmarshaler.\nfunc (s *StringSize) UnmarshalEnv(_ string, v string) error {\n\treturn s.UnmarshalJSON([]byte(`\"` + v + `\"`))\n}\n"
  },
  {
    "path": "internal/conf/webrtc_ice_server.go",
    "content": "package conf\n\n// WebRTCICEServer is a WebRTC ICE Server.\ntype WebRTCICEServer struct {\n\tURL        string `json:\"url\"`\n\tUsername   string `json:\"username\"`\n\tPassword   string `json:\"password\"`\n\tClientOnly bool   `json:\"clientOnly\"`\n}\n"
  },
  {
    "path": "internal/conf/yamlwrapper/testdata/fuzz/FuzzUnmarshal/90b5d1b18dd9f8ac",
    "content": "go test fuzz v1\n[]byte(\"*0\")\n"
  },
  {
    "path": "internal/conf/yamlwrapper/testdata/fuzz/FuzzUnmarshal/dc806ec658c460fc",
    "content": "go test fuzz v1\n[]byte(\"...\")\n"
  },
  {
    "path": "internal/conf/yamlwrapper/unmarshal.go",
    "content": "// Package yamlwrapper contains a YAML unmarshaler.\npackage yamlwrapper\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf/jsonwrapper\"\n\t\"github.com/goccy/go-yaml\"\n\t\"github.com/goccy/go-yaml/ast\"\n\t\"github.com/goccy/go-yaml/parser\"\n\t\"github.com/goccy/go-yaml/token\"\n)\n\n// differences with respect to the standard package:\n// - some legacy YAML 1.1 boolean values (yes, no) are supported\n// - all differences of jsonwrapper are inherited\n\nfunc convertLegacyBools(node ast.Node) ast.Node {\n\tif node != nil {\n\t\tswitch n := node.(type) {\n\t\tcase *ast.MappingNode:\n\t\t\tfor _, value := range n.Values {\n\t\t\t\tconvertLegacyBools(value)\n\t\t\t}\n\n\t\tcase *ast.MappingValueNode:\n\t\t\tn.Key = convertLegacyBools(n.Key).(ast.MapKeyNode)\n\t\t\tn.Value = convertLegacyBools(n.Value)\n\n\t\tcase *ast.SequenceNode:\n\t\t\tfor i, value := range n.Values {\n\t\t\t\tn.Values[i] = convertLegacyBools(value)\n\t\t\t}\n\n\t\tcase *ast.DocumentNode:\n\t\t\tn.Body = convertLegacyBools(n.Body)\n\n\t\tcase *ast.StringNode:\n\t\t\tif n.Token.Type == token.StringType {\n\t\t\t\tvar boolVal bool\n\t\t\t\tshouldConvert := false\n\n\t\t\t\tswitch n.Token.Value {\n\t\t\t\tcase \"yes\", \"Yes\", \"YES\", \"on\", \"On\", \"ON\":\n\t\t\t\t\tboolVal = true\n\t\t\t\t\tshouldConvert = true\n\n\t\t\t\tcase \"no\", \"No\", \"NO\", \"off\", \"Off\", \"OFF\":\n\t\t\t\t\tboolVal = false\n\t\t\t\t\tshouldConvert = true\n\t\t\t\t}\n\n\t\t\t\tif shouldConvert {\n\t\t\t\t\tnewToken := &token.Token{\n\t\t\t\t\t\tType:  token.BoolType,\n\t\t\t\t\t\tValue: n.Token.Value,\n\t\t\t\t\t}\n\n\t\t\t\t\tif boolVal {\n\t\t\t\t\t\tnewToken.Value = \"true\"\n\t\t\t\t\t} else {\n\t\t\t\t\t\tnewToken.Value = \"false\"\n\t\t\t\t\t}\n\n\t\t\t\t\tboolNode := ast.Bool(newToken)\n\t\t\t\t\tboolNode.Value = boolVal\n\t\t\t\t\treturn boolNode\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn node\n}\n\n// Unmarshal loads the configuration from YAML.\nfunc Unmarshal(buf []byte, dest any) error {\n\tfile, err := parser.ParseBytes(buf, parser.ParseComments)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif len(file.Docs) != 1 {\n\t\treturn fmt.Errorf(\"invalid YAML\")\n\t}\n\n\tfile.Docs[0] = convertLegacyBools(file.Docs[0]).(*ast.DocumentNode)\n\n\tvar temp any\n\tif file.Docs[0].Body != nil {\n\t\terr = yaml.NodeToValue(file.Docs[0].Body, &temp)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// convert the generic map into JSON\n\tbuf, err = json.Marshal(temp)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// load JSON into destination\n\treturn jsonwrapper.Unmarshal(buf, dest)\n}\n"
  },
  {
    "path": "internal/conf/yamlwrapper/unmarshal_test.go",
    "content": "package yamlwrapper\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestUnmarshalIntegerMapKey(t *testing.T) {\n\tbuf := []byte(`\n1: value\ntest: value2\n`)\n\n\tvar dest any\n\terr := Unmarshal(buf, &dest)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, map[string]any{\n\t\t\"1\":    \"value\",\n\t\t\"test\": \"value2\",\n\t}, dest)\n}\n\nfunc TestUnmarshalDuplicateKey(t *testing.T) {\n\tbuf := []byte(`\nkey: value1\nkey: value2\n`)\n\n\terr := Unmarshal(buf, &map[string]string{})\n\trequire.EqualError(t, err, \"[3:1] mapping key \\\"key\\\" already defined at [2:1]\"+\n\t\t\"\\n   2 | key: value1\\n>  3 | key: value2\\n       ^\\n\")\n}\n\nfunc TestUnmarshalUnknownFields(t *testing.T) {\n\ttype testStruct struct {\n\t\tField1 string `json:\"field1\"`\n\t\tField2 int    `json:\"field2\"`\n\t}\n\n\tinput := []byte(`field1: test\nunknownField: value\nfield2: 456`)\n\n\tvar result testStruct\n\terr := Unmarshal(input, &result)\n\trequire.Error(t, err)\n\trequire.EqualError(t, err, \"json: unknown field \\\"unknownField\\\"\")\n}\n\nfunc TestUnmarshalLegacyBools(t *testing.T) {\n\ttype testStruct struct {\n\t\tField1 bool   `json:\"field1\"`\n\t\tField2 string `json:\"field2\"`\n\t}\n\n\tinput := []byte(\"field1: yes\\n\" +\n\t\t\"field2: \\\"yes\\\"\\n\")\n\n\tvar result testStruct\n\terr := Unmarshal(input, &result)\n\trequire.NoError(t, err)\n\trequire.Equal(t, true, result.Field1)\n}\n\nfunc TestUnmarshalEmpty(t *testing.T) {\n\tinput := []byte(``)\n\n\tvar result any\n\terr := Unmarshal(input, &result)\n\trequire.NoError(t, err)\n}\n\nfunc FuzzUnmarshal(f *testing.F) {\n\tf.Fuzz(func(_ *testing.T, buf []byte) {\n\t\tvar dest any\n\t\tUnmarshal(buf, &dest) //nolint:errcheck\n\t})\n}\n"
  },
  {
    "path": "internal/confwatcher/confwatcher.go",
    "content": "// Package confwatcher contains a configuration watcher.\npackage confwatcher\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/fsnotify/fsnotify\"\n)\n\nconst (\n\tminInterval    = 1 * time.Second\n\tadditionalWait = 10 * time.Millisecond\n)\n\n// ConfWatcher is a configuration file watcher.\ntype ConfWatcher struct {\n\tFilePath string\n\n\tinner        *fsnotify.Watcher\n\tabsolutePath string\n\n\t// in\n\tterminate chan struct{}\n\n\t// out\n\tsignal chan struct{}\n\tdone   chan struct{}\n}\n\n// Initialize initializes a ConfWatcher.\nfunc (w *ConfWatcher) Initialize() error {\n\tif _, err := os.Stat(w.FilePath); err != nil {\n\t\treturn err\n\t}\n\n\tvar err error\n\tw.inner, err = fsnotify.NewWatcher()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// use absolute paths to support Darwin\n\tw.absolutePath, _ = filepath.Abs(w.FilePath)\n\tparentPath := filepath.Dir(w.absolutePath)\n\n\terr = w.inner.Add(parentPath)\n\tif err != nil {\n\t\tw.inner.Close() //nolint:errcheck\n\t\treturn err\n\t}\n\n\tw.terminate = make(chan struct{})\n\tw.signal = make(chan struct{})\n\tw.done = make(chan struct{})\n\n\tgo w.run()\n\n\treturn nil\n}\n\n// Close closes a ConfWatcher.\nfunc (w *ConfWatcher) Close() {\n\tclose(w.terminate)\n\t<-w.done\n}\n\nfunc (w *ConfWatcher) run() {\n\tdefer close(w.done)\n\n\tvar lastCalled time.Time\n\tpreviousWatchedPath, _ := filepath.EvalSymlinks(w.absolutePath)\n\nouter:\n\tfor {\n\t\tselect {\n\t\tcase event := <-w.inner.Events:\n\t\t\tif time.Since(lastCalled) < minInterval {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcurrentWatchedPath, _ := filepath.EvalSymlinks(w.absolutePath)\n\t\t\teventPath, _ := filepath.Abs(event.Name)\n\t\t\teventPath, _ = filepath.EvalSymlinks(eventPath)\n\n\t\t\tif currentWatchedPath == \"\" {\n\t\t\t\t// watched file was removed; wait for write event to trigger reload\n\t\t\t\tpreviousWatchedPath = \"\"\n\t\t\t} else if currentWatchedPath != previousWatchedPath ||\n\t\t\t\t(eventPath == currentWatchedPath &&\n\t\t\t\t\t((event.Op&fsnotify.Write) == fsnotify.Write ||\n\t\t\t\t\t\t(event.Op&fsnotify.Create) == fsnotify.Create)) {\n\t\t\t\t// wait some additional time to allow the writer to complete its job\n\t\t\t\ttime.Sleep(additionalWait)\n\t\t\t\tpreviousWatchedPath = currentWatchedPath\n\n\t\t\t\tlastCalled = time.Now()\n\n\t\t\t\tselect {\n\t\t\t\tcase w.signal <- struct{}{}:\n\t\t\t\tcase <-w.terminate:\n\t\t\t\t\tbreak outer\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase <-w.inner.Errors:\n\t\t\tbreak outer\n\n\t\tcase <-w.terminate:\n\t\t\tbreak outer\n\t\t}\n\t}\n\n\tclose(w.signal)\n\tw.inner.Close() //nolint:errcheck\n}\n\n// Watch returns a channel that is called after the configuration file has changed.\nfunc (w *ConfWatcher) Watch() chan struct{} {\n\treturn w.signal\n}\n"
  },
  {
    "path": "internal/confwatcher/confwatcher_test.go",
    "content": "package confwatcher\n\nimport (\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNoFile(t *testing.T) {\n\tw := &ConfWatcher{FilePath: \"/nonexistent\"}\n\terr := w.Initialize()\n\trequire.Error(t, err)\n}\n\nfunc TestWrite(t *testing.T) {\n\tfpath, err := test.CreateTempFile([]byte(\"{}\"))\n\trequire.NoError(t, err)\n\n\tw := &ConfWatcher{FilePath: fpath}\n\terr = w.Initialize()\n\trequire.NoError(t, err)\n\tdefer w.Close()\n\n\tfunc() {\n\t\tvar f *os.File\n\t\tf, err = os.Create(fpath)\n\t\trequire.NoError(t, err)\n\t\tdefer f.Close()\n\n\t\t_, err = f.Write([]byte(\"{}\"))\n\t\trequire.NoError(t, err)\n\t}()\n\n\tselect {\n\tcase <-w.Watch():\n\tcase <-time.After(500 * time.Millisecond):\n\t\tt.Errorf(\"timed out\")\n\t\treturn\n\t}\n}\n\nfunc TestWriteMultipleTimes(t *testing.T) {\n\tfpath, err := test.CreateTempFile([]byte(\"{}\"))\n\trequire.NoError(t, err)\n\n\tw := &ConfWatcher{FilePath: fpath}\n\terr = w.Initialize()\n\trequire.NoError(t, err)\n\tdefer w.Close()\n\n\tfunc() {\n\t\tf, err2 := os.Create(fpath)\n\t\trequire.NoError(t, err2)\n\t\tdefer f.Close()\n\n\t\t_, err2 = f.Write([]byte(\"{}\"))\n\t\trequire.NoError(t, err2)\n\t}()\n\n\ttime.Sleep(10 * time.Millisecond)\n\n\tfunc() {\n\t\tf, err2 := os.Create(fpath)\n\t\trequire.NoError(t, err2)\n\t\tdefer f.Close()\n\n\t\t_, err2 = f.Write([]byte(\"{}\"))\n\t\trequire.NoError(t, err2)\n\t}()\n\n\tselect {\n\tcase <-w.Watch():\n\tcase <-time.After(500 * time.Millisecond):\n\t\tt.Errorf(\"timed out\")\n\t\treturn\n\t}\n\n\tselect {\n\tcase <-time.After(500 * time.Millisecond):\n\tcase <-w.Watch():\n\t\tt.Errorf(\"should not happen\")\n\t\treturn\n\t}\n}\n\nfunc TestDeleteCreate(t *testing.T) {\n\tfpath, err := test.CreateTempFile([]byte(\"{}\"))\n\trequire.NoError(t, err)\n\n\tw := &ConfWatcher{FilePath: fpath}\n\terr = w.Initialize()\n\trequire.NoError(t, err)\n\tdefer w.Close()\n\n\tos.Remove(fpath)\n\ttime.Sleep(10 * time.Millisecond)\n\n\tfunc() {\n\t\tvar f *os.File\n\t\tf, err = os.Create(fpath)\n\t\trequire.NoError(t, err)\n\t\tdefer f.Close()\n\n\t\t_, err = f.Write([]byte(\"{}\"))\n\t\trequire.NoError(t, err)\n\t}()\n\n\tselect {\n\tcase <-w.Watch():\n\tcase <-time.After(500 * time.Millisecond):\n\t\tt.Errorf(\"timed out\")\n\t\treturn\n\t}\n}\n\nfunc TestSymlinkDeleteCreate(t *testing.T) {\n\tfpath, err := test.CreateTempFile([]byte(\"{}\"))\n\trequire.NoError(t, err)\n\n\terr = os.Symlink(fpath, fpath+\"-sym\")\n\trequire.NoError(t, err)\n\n\tw := &ConfWatcher{FilePath: fpath + \"-sym\"}\n\terr = w.Initialize()\n\trequire.NoError(t, err)\n\tdefer w.Close()\n\n\tos.Remove(fpath)\n\n\tfunc() {\n\t\tf, err2 := os.Create(fpath)\n\t\trequire.NoError(t, err2)\n\t\tdefer f.Close()\n\n\t\t_, err2 = f.Write([]byte(\"{}\"))\n\t\trequire.NoError(t, err2)\n\t}()\n\n\tselect {\n\tcase <-w.Watch():\n\tcase <-time.After(500 * time.Millisecond):\n\t\tt.Errorf(\"timed out\")\n\t\treturn\n\t}\n}\n"
  },
  {
    "path": "internal/core/api_test.go",
    "content": "//nolint:dupl,lll\npackage core\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortmplib\"\n\trtmpcodecs \"github.com/bluenviron/gortmplib/pkg/codecs\"\n\t\"github.com/bluenviron/gortsplib/v5\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts\"\n\ttscodecs \"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts/codecs\"\n\tsrt \"github.com/datarhei/gosrt\"\n\t\"github.com/google/uuid\"\n\t\"github.com/pion/rtp\"\n\tpwebrtc \"github.com/pion/webrtc/v4\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/webrtc\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/whip\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n)\n\nfunc checkClose(t *testing.T, closeFunc func() error) {\n\trequire.NoError(t, closeFunc())\n}\n\nfunc httpRequest(t *testing.T, hc *http.Client, method string, ur string, in any, out any) {\n\tbuf := func() io.Reader {\n\t\tif in == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tbyts, err := json.Marshal(in)\n\t\trequire.NoError(t, err)\n\n\t\treturn bytes.NewBuffer(byts)\n\t}()\n\n\treq, err := http.NewRequest(method, ur, buf)\n\trequire.NoError(t, err)\n\n\tres, err := hc.Do(req)\n\trequire.NoError(t, err)\n\tdefer res.Body.Close()\n\n\tif res.StatusCode != http.StatusOK {\n\t\tt.Errorf(\"bad status code: %d\", res.StatusCode)\n\t}\n\n\tif out == nil {\n\t\treturn\n\t}\n\n\terr = json.NewDecoder(res.Body).Decode(out)\n\trequire.NoError(t, err)\n}\n\nfunc checkError(t *testing.T, msg string, body io.Reader) {\n\tvar resErr map[string]any\n\terr := json.NewDecoder(body).Decode(&resErr)\n\trequire.NoError(t, err)\n\trequire.Equal(t, map[string]any{\"status\": \"error\", \"error\": msg}, resErr)\n}\n\nfunc TestAPIPathsList(t *testing.T) {\n\ttype pathSource struct {\n\t\tType string `json:\"type\"`\n\t}\n\n\ttype path struct {\n\t\tName                 string                   `json:\"name\"`\n\t\tSource               pathSource               `json:\"source\"`\n\t\tReady                bool                     `json:\"ready\"`\n\t\tTracks               []defs.APIPathTrackCodec `json:\"tracks\"`\n\t\tInboundBytes         uint64                   `json:\"inboundBytes\"`\n\t\tOutboundBytes        uint64                   `json:\"outboundBytes\"`\n\t\tInboundFramesInError uint64                   `json:\"inboundFramesInError\"`\n\t\tBytesReceived        uint64                   `json:\"bytesReceived\"`\n\t\tBytesSent            uint64                   `json:\"bytesSent\"`\n\t}\n\n\ttype pathList struct {\n\t\tItemCount int    `json:\"itemCount\"`\n\t\tPageCount int    `json:\"pageCount\"`\n\t\tItems     []path `json:\"items\"`\n\t}\n\n\tt.Run(\"rtsp session\", func(t *testing.T) {\n\t\tp, ok := newInstance(\"api: yes\\n\" +\n\t\t\t\"paths:\\n\" +\n\t\t\t\"  mypath:\\n\")\n\t\trequire.Equal(t, true, ok)\n\t\tdefer p.Close()\n\n\t\ttr := &http.Transport{}\n\t\tdefer tr.CloseIdleConnections()\n\t\thc := &http.Client{Transport: tr}\n\n\t\tmedia0 := test.UniqueMediaH264()\n\n\t\tsource := gortsplib.Client{}\n\t\terr := source.StartRecording(\n\t\t\t\"rtsp://localhost:8554/mypath\",\n\t\t\t&description.Session{Medias: []*description.Media{\n\t\t\t\tmedia0,\n\t\t\t\ttest.MediaMPEG4Audio,\n\t\t\t}})\n\t\trequire.NoError(t, err)\n\t\tdefer source.Close()\n\n\t\terr = source.WritePacketRTP(media0, &rtp.Packet{\n\t\t\tHeader: rtp.Header{\n\t\t\t\tVersion:     2,\n\t\t\t\tPayloadType: 96,\n\t\t\t},\n\t\t\tPayload: []byte{5, 1, 2, 3, 4},\n\t\t})\n\t\trequire.NoError(t, err)\n\n\t\tvar out pathList\n\t\thttpRequest(t, hc, http.MethodGet, \"http://localhost:9997/v3/paths/list\", nil, &out)\n\t\trequire.Equal(t, pathList{\n\t\t\tItemCount: 1,\n\t\t\tPageCount: 1,\n\t\t\tItems: []path{{\n\t\t\t\tName: \"mypath\",\n\t\t\t\tSource: pathSource{\n\t\t\t\t\tType: \"rtspSession\",\n\t\t\t\t},\n\t\t\t\tReady:                true,\n\t\t\t\tTracks:               []defs.APIPathTrackCodec{defs.APIPathTrackCodecH264, defs.APIPathTrackCodecMPEG4Audio},\n\t\t\t\tInboundBytes:         17,\n\t\t\t\tInboundFramesInError: 0,\n\t\t\t\tBytesReceived:        17,\n\t\t\t}},\n\t\t}, out)\n\t})\n\n\tt.Run(\"rtsps session\", func(t *testing.T) {\n\t\tserverCertFpath, err := test.CreateTempFile(test.TLSCertPub)\n\t\trequire.NoError(t, err)\n\t\tdefer os.Remove(serverCertFpath)\n\n\t\tserverKeyFpath, err := test.CreateTempFile(test.TLSCertKey)\n\t\trequire.NoError(t, err)\n\t\tdefer os.Remove(serverKeyFpath)\n\n\t\tp, ok := newInstance(\"api: yes\\n\" +\n\t\t\t\"rtspEncryption: optional\\n\" +\n\t\t\t\"rtspServerCert: \" + serverCertFpath + \"\\n\" +\n\t\t\t\"rtspServerKey: \" + serverKeyFpath + \"\\n\" +\n\t\t\t\"paths:\\n\" +\n\t\t\t\"  mypath:\\n\")\n\t\trequire.Equal(t, true, ok)\n\t\tdefer p.Close()\n\n\t\ttr := &http.Transport{}\n\t\tdefer tr.CloseIdleConnections()\n\t\thc := &http.Client{Transport: tr}\n\n\t\tsource := gortsplib.Client{TLSConfig: &tls.Config{InsecureSkipVerify: true}}\n\t\terr = source.StartRecording(\"rtsps://localhost:8322/mypath\",\n\t\t\t&description.Session{Medias: []*description.Media{\n\t\t\t\ttest.UniqueMediaH264(),\n\t\t\t\ttest.UniqueMediaMPEG4Audio(),\n\t\t\t}})\n\t\trequire.NoError(t, err)\n\t\tdefer source.Close()\n\n\t\tvar out pathList\n\t\thttpRequest(t, hc, http.MethodGet, \"http://localhost:9997/v3/paths/list\", nil, &out)\n\t\trequire.Equal(t, pathList{\n\t\t\tItemCount: 1,\n\t\t\tPageCount: 1,\n\t\t\tItems: []path{{\n\t\t\t\tName: \"mypath\",\n\t\t\t\tSource: pathSource{\n\t\t\t\t\tType: \"rtspsSession\",\n\t\t\t\t},\n\t\t\t\tReady:  true,\n\t\t\t\tTracks: []defs.APIPathTrackCodec{defs.APIPathTrackCodecH264, defs.APIPathTrackCodecMPEG4Audio},\n\t\t\t}},\n\t\t}, out)\n\t})\n\n\tt.Run(\"rtsp source\", func(t *testing.T) {\n\t\tp, ok := newInstance(\"api: yes\\n\" +\n\t\t\t\"paths:\\n\" +\n\t\t\t\"  mypath:\\n\" +\n\t\t\t\"    source: rtsp://127.0.0.1:1234/mypath\\n\" +\n\t\t\t\"    sourceOnDemand: yes\\n\")\n\t\trequire.Equal(t, true, ok)\n\t\tdefer p.Close()\n\n\t\ttr := &http.Transport{}\n\t\tdefer tr.CloseIdleConnections()\n\t\thc := &http.Client{Transport: tr}\n\n\t\tvar out pathList\n\t\thttpRequest(t, hc, http.MethodGet, \"http://localhost:9997/v3/paths/list\", nil, &out)\n\t\trequire.Equal(t, pathList{\n\t\t\tItemCount: 1,\n\t\t\tPageCount: 1,\n\t\t\tItems: []path{{\n\t\t\t\tName: \"mypath\",\n\t\t\t\tSource: pathSource{\n\t\t\t\t\tType: \"rtspSource\",\n\t\t\t\t},\n\t\t\t\tReady:  false,\n\t\t\t\tTracks: []defs.APIPathTrackCodec{},\n\t\t\t}},\n\t\t}, out)\n\t})\n\n\tt.Run(\"rtmp source\", func(t *testing.T) {\n\t\tp, ok := newInstance(\"api: yes\\n\" +\n\t\t\t\"paths:\\n\" +\n\t\t\t\"  mypath:\\n\" +\n\t\t\t\"    source: rtmp://127.0.0.1:1234/mypath\\n\" +\n\t\t\t\"    sourceOnDemand: yes\\n\")\n\t\trequire.Equal(t, true, ok)\n\t\tdefer p.Close()\n\n\t\ttr := &http.Transport{}\n\t\tdefer tr.CloseIdleConnections()\n\t\thc := &http.Client{Transport: tr}\n\n\t\tvar out pathList\n\t\thttpRequest(t, hc, http.MethodGet, \"http://localhost:9997/v3/paths/list\", nil, &out)\n\t\trequire.Equal(t, pathList{\n\t\t\tItemCount: 1,\n\t\t\tPageCount: 1,\n\t\t\tItems: []path{{\n\t\t\t\tName: \"mypath\",\n\t\t\t\tSource: pathSource{\n\t\t\t\t\tType: \"rtmpSource\",\n\t\t\t\t},\n\t\t\t\tReady:  false,\n\t\t\t\tTracks: []defs.APIPathTrackCodec{},\n\t\t\t}},\n\t\t}, out)\n\t})\n\n\tt.Run(\"hls source\", func(t *testing.T) {\n\t\tp, ok := newInstance(\"api: yes\\n\" +\n\t\t\t\"paths:\\n\" +\n\t\t\t\"  mypath:\\n\" +\n\t\t\t\"    source: http://127.0.0.1:1234/mypath\\n\" +\n\t\t\t\"    sourceOnDemand: yes\\n\")\n\t\trequire.Equal(t, true, ok)\n\t\tdefer p.Close()\n\n\t\ttr := &http.Transport{}\n\t\tdefer tr.CloseIdleConnections()\n\t\thc := &http.Client{Transport: tr}\n\n\t\tvar out pathList\n\t\thttpRequest(t, hc, http.MethodGet, \"http://localhost:9997/v3/paths/list\", nil, &out)\n\t\trequire.Equal(t, pathList{\n\t\t\tItemCount: 1,\n\t\t\tPageCount: 1,\n\t\t\tItems: []path{{\n\t\t\t\tName: \"mypath\",\n\t\t\t\tSource: pathSource{\n\t\t\t\t\tType: \"hlsSource\",\n\t\t\t\t},\n\t\t\t\tReady:  false,\n\t\t\t\tTracks: []defs.APIPathTrackCodec{},\n\t\t\t}},\n\t\t}, out)\n\t})\n}\n\nfunc TestAPIPathsGet(t *testing.T) {\n\tp, ok := newInstance(\"api: yes\\n\" +\n\t\t\"paths:\\n\" +\n\t\t\"  all_others:\\n\")\n\trequire.Equal(t, true, ok)\n\tdefer p.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\tfor _, ca := range []string{\"ok\", \"ok-nested\", \"not found\"} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\ttype pathSource struct {\n\t\t\t\tType string `json:\"type\"`\n\t\t\t}\n\n\t\t\ttype path struct {\n\t\t\t\tName                 string                   `json:\"name\"`\n\t\t\t\tSource               pathSource               `json:\"source\"`\n\t\t\t\tReady                bool                     `json:\"Ready\"`\n\t\t\t\tTracks               []defs.APIPathTrackCodec `json:\"tracks\"`\n\t\t\t\tInboundBytes         uint64                   `json:\"inboundBytes\"`\n\t\t\t\tOutboundBytes        uint64                   `json:\"outboundBytes\"`\n\t\t\t\tInboundFramesInError uint64                   `json:\"inboundFramesInError\"`\n\t\t\t\tBytesReceived        uint64                   `json:\"bytesReceived\"`\n\t\t\t\tBytesSent            uint64                   `json:\"bytesSent\"`\n\t\t\t}\n\n\t\t\tvar pathName string\n\n\t\t\tswitch ca {\n\t\t\tcase \"ok\":\n\t\t\t\tpathName = \"mypath\"\n\t\t\tcase \"ok-nested\":\n\t\t\t\tpathName = \"my/nested/path\"\n\t\t\tcase \"not found\":\n\t\t\t\tpathName = \"nonexisting\"\n\t\t\t}\n\n\t\t\tif ca == \"ok\" || ca == \"ok-nested\" {\n\t\t\t\tsource := gortsplib.Client{}\n\t\t\t\terr := source.StartRecording(\"rtsp://localhost:8554/\"+pathName,\n\t\t\t\t\t&description.Session{Medias: []*description.Media{test.UniqueMediaH264()}})\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer source.Close()\n\n\t\t\t\tvar out path\n\t\t\t\thttpRequest(t, hc, http.MethodGet, \"http://localhost:9997/v3/paths/get/\"+pathName, nil, &out)\n\t\t\t\trequire.Equal(t, path{\n\t\t\t\t\tName: pathName,\n\t\t\t\t\tSource: pathSource{\n\t\t\t\t\t\tType: \"rtspSession\",\n\t\t\t\t\t},\n\t\t\t\t\tReady:  true,\n\t\t\t\t\tTracks: []defs.APIPathTrackCodec{defs.APIPathTrackCodecH264},\n\t\t\t\t}, out)\n\t\t\t} else {\n\t\t\t\tres, err := hc.Get(\"http://localhost:9997/v3/paths/get/\" + pathName)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer res.Body.Close()\n\n\t\t\t\trequire.Equal(t, http.StatusNotFound, res.StatusCode)\n\t\t\t\tcheckError(t, \"path not found\", res.Body)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestAPIProtocolListGet(t *testing.T) {\n\tserverCertFpath, err := test.CreateTempFile(test.TLSCertPub)\n\trequire.NoError(t, err)\n\tdefer os.Remove(serverCertFpath)\n\n\tserverKeyFpath, err := test.CreateTempFile(test.TLSCertKey)\n\trequire.NoError(t, err)\n\tdefer os.Remove(serverKeyFpath)\n\n\tfor _, ca := range []string{\n\t\t\"rtsp conns\",\n\t\t\"rtsp sessions\",\n\t\t\"rtsps conns\",\n\t\t\"rtsps sessions\",\n\t\t\"rtmp\",\n\t\t\"rtmps\",\n\t\t\"hls\",\n\t\t\"webrtc\",\n\t\t\"srt\",\n\t} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tcnf := \"api: yes\\n\"\n\n\t\t\tswitch ca {\n\t\t\tcase \"rtsps conns\", \"rtsps sessions\":\n\t\t\t\tcnf += \"rtspEncryption: strict\\n\" +\n\t\t\t\t\t\"rtspServerCert: \" + serverCertFpath + \"\\n\" +\n\t\t\t\t\t\"rtspServerKey: \" + serverKeyFpath + \"\\n\"\n\n\t\t\tcase \"rtmps\":\n\t\t\t\tcnf += \"rtmpEncryption: strict\\n\" +\n\t\t\t\t\t\"rtmpServerCert: \" + serverCertFpath + \"\\n\" +\n\t\t\t\t\t\"rtmpServerKey: \" + serverKeyFpath + \"\\n\"\n\t\t\t}\n\n\t\t\tcnf += \"paths:\\n\" +\n\t\t\t\t\"  all_others:\\n\"\n\n\t\t\tp, ok := newInstance(cnf)\n\t\t\trequire.Equal(t, true, ok)\n\t\t\tdefer p.Close()\n\n\t\t\ttr := &http.Transport{}\n\t\t\tdefer tr.CloseIdleConnections()\n\t\t\thc := &http.Client{Transport: tr}\n\n\t\t\tmedi := test.UniqueMediaH264()\n\n\t\t\tswitch ca { //nolint:dupl\n\t\t\tcase \"rtsp conns\", \"rtsp sessions\":\n\t\t\t\tsource := gortsplib.Client{}\n\n\t\t\t\terr = source.StartRecording(\"rtsp://localhost:8554/mypath?key=val\",\n\t\t\t\t\t&description.Session{Medias: []*description.Media{medi}})\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer source.Close()\n\n\t\t\tcase \"rtsps conns\", \"rtsps sessions\":\n\t\t\t\tsource := gortsplib.Client{\n\t\t\t\t\tTLSConfig: &tls.Config{InsecureSkipVerify: true},\n\t\t\t\t}\n\n\t\t\t\terr = source.StartRecording(\"rtsps://localhost:8322/mypath?key=val\",\n\t\t\t\t\t&description.Session{Medias: []*description.Media{medi}})\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer source.Close()\n\n\t\t\tcase \"rtmp\", \"rtmps\":\n\t\t\t\tvar port string\n\t\t\t\tif ca == \"rtmp\" {\n\t\t\t\t\tport = \"1935\"\n\t\t\t\t} else {\n\t\t\t\t\tport = \"1936\"\n\t\t\t\t}\n\n\t\t\t\tvar rawURL string\n\n\t\t\t\tif ca == \"rtmps\" {\n\t\t\t\t\trawURL = \"rtmps://\"\n\t\t\t\t} else {\n\t\t\t\t\trawURL = \"rtmp://\"\n\t\t\t\t}\n\n\t\t\t\trawURL += \"127.0.0.1:\" + port + \"/mypath?key=val\"\n\n\t\t\t\tvar u *url.URL\n\t\t\t\tu, err = url.Parse(rawURL)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tconn := &gortmplib.Client{\n\t\t\t\t\tURL:       u,\n\t\t\t\t\tTLSConfig: &tls.Config{InsecureSkipVerify: true},\n\t\t\t\t\tPublish:   true,\n\t\t\t\t}\n\t\t\t\terr = conn.Initialize(context.Background())\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer conn.Close()\n\n\t\t\t\ttrack := &gortmplib.Track{\n\t\t\t\t\tCodec: &rtmpcodecs.H264{\n\t\t\t\t\t\tSPS: test.FormatH264.SPS,\n\t\t\t\t\t\tPPS: test.FormatH264.PPS,\n\t\t\t\t\t},\n\t\t\t\t}\n\n\t\t\t\tw := &gortmplib.Writer{\n\t\t\t\t\tConn:   conn,\n\t\t\t\t\tTracks: []*gortmplib.Track{track},\n\t\t\t\t}\n\t\t\t\terr = w.Initialize()\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\terr = w.WriteH264(track, 2*time.Second, 2*time.Second, [][]byte{{5, 2, 3, 4}})\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\ttime.Sleep(500 * time.Millisecond)\n\n\t\t\tcase \"hls\":\n\t\t\t\tsource := gortsplib.Client{}\n\t\t\t\terr = source.StartRecording(\"rtsp://localhost:8554/mypath\",\n\t\t\t\t\t&description.Session{Medias: []*description.Media{medi}})\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer source.Close()\n\n\t\t\t\tgo func() {\n\t\t\t\t\ttime.Sleep(500 * time.Millisecond)\n\n\t\t\t\t\tfor i := range 3 {\n\t\t\t\t\t\t/*source.WritePacketRTP(medi, &rtp.Packet{\n\t\t\t\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\t\t\t\tVersion:        2,\n\t\t\t\t\t\t\t\tMarker:         true,\n\t\t\t\t\t\t\t\tPayloadType:    96,\n\t\t\t\t\t\t\t\tSequenceNumber: 123 + uint16(i),\n\t\t\t\t\t\t\t\tTimestamp:      45343 + uint32(i)*90000,\n\t\t\t\t\t\t\t\tSSRC:           563423,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tPayload: []byte{\n\t\t\t\t\t\t\t\ttestSPS,\n\t\t\t\t\t\t\t\t0x05,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t})\n\n\t\t\t\t\t\t[]byte{ // 1920x1080 baseline\n\t\t\t\t\t\t\t0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02,\n\t\t\t\t\t\t\t0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04,\n\t\t\t\t\t\t\t0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, 0x20,\n\t\t\t\t\t\t},*/\n\n\t\t\t\t\t\terr2 := source.WritePacketRTP(medi, &rtp.Packet{\n\t\t\t\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\t\t\t\tVersion:        2,\n\t\t\t\t\t\t\t\tMarker:         true,\n\t\t\t\t\t\t\t\tPayloadType:    96,\n\t\t\t\t\t\t\t\tSequenceNumber: 123 + uint16(i),\n\t\t\t\t\t\t\t\tTimestamp:      45343 + uint32(i)*90000,\n\t\t\t\t\t\t\t\tSSRC:           563423,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tPayload: []byte{\n\t\t\t\t\t\t\t\t// testSPS,\n\t\t\t\t\t\t\t\t0x05,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t})\n\t\t\t\t\t\trequire.NoError(t, err2)\n\t\t\t\t\t}\n\t\t\t\t}()\n\n\t\t\t\tfunc() {\n\t\t\t\t\tres, err2 := hc.Get(\"http://localhost:8888/mypath/index.m3u8\")\n\t\t\t\t\trequire.NoError(t, err2)\n\t\t\t\t\tdefer res.Body.Close()\n\t\t\t\t\trequire.Equal(t, 200, res.StatusCode)\n\t\t\t\t}()\n\n\t\t\tcase \"webrtc\":\n\t\t\t\tsource := gortsplib.Client{}\n\t\t\t\terr = source.StartRecording(\"rtsp://localhost:8554/mypath\",\n\t\t\t\t\t&description.Session{Medias: []*description.Media{medi}})\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer source.Close()\n\n\t\t\t\tvar u *url.URL\n\t\t\t\tu, err = url.Parse(\"http://localhost:8889/mypath/whep?key=val\")\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tgo func() {\n\t\t\t\t\ttime.Sleep(500 * time.Millisecond)\n\n\t\t\t\t\terr2 := source.WritePacketRTP(medi, &rtp.Packet{\n\t\t\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\t\t\tVersion:        2,\n\t\t\t\t\t\t\tMarker:         true,\n\t\t\t\t\t\t\tPayloadType:    96,\n\t\t\t\t\t\t\tSequenceNumber: 123,\n\t\t\t\t\t\t\tTimestamp:      45343,\n\t\t\t\t\t\t\tSSRC:           563423,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPayload: []byte{5, 1, 2, 3, 4},\n\t\t\t\t\t})\n\t\t\t\t\trequire.NoError(t, err2)\n\t\t\t\t}()\n\n\t\t\t\tc := &whip.Client{\n\t\t\t\t\tHTTPClient: hc,\n\t\t\t\t\tURL:        u,\n\t\t\t\t\tLog:        test.NilLogger,\n\t\t\t\t}\n\n\t\t\t\terr = c.Initialize(context.Background())\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer checkClose(t, c.Close)\n\n\t\t\tcase \"srt\":\n\t\t\t\tconf := srt.DefaultConfig()\n\t\t\t\tconf.StreamId = \"publish:mypath:::key=val\"\n\n\t\t\t\tvar conn srt.Conn\n\t\t\t\tconn, err = srt.Dial(\"srt\", \"localhost:8890\", conf)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer conn.Close()\n\n\t\t\t\ttrack := &mpegts.Track{\n\t\t\t\t\tCodec: &tscodecs.H264{},\n\t\t\t\t}\n\n\t\t\t\tbw := bufio.NewWriter(conn)\n\t\t\t\tw := &mpegts.Writer{W: bw, Tracks: []*mpegts.Track{track}}\n\t\t\t\terr = w.Initialize()\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\terr = w.WriteH264(track, 0, 0, [][]byte{{1}})\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\terr = bw.Flush()\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\ttime.Sleep(500 * time.Millisecond)\n\t\t\t}\n\n\t\t\tvar pa string\n\t\t\tswitch ca {\n\t\t\tcase \"rtsp conns\":\n\t\t\t\tpa = \"rtspconns\"\n\n\t\t\tcase \"rtsp sessions\":\n\t\t\t\tpa = \"rtspsessions\"\n\n\t\t\tcase \"rtsps conns\":\n\t\t\t\tpa = \"rtspsconns\"\n\n\t\t\tcase \"rtsps sessions\":\n\t\t\t\tpa = \"rtspssessions\"\n\n\t\t\tcase \"rtmp\":\n\t\t\t\tpa = \"rtmpconns\"\n\n\t\t\tcase \"rtmps\":\n\t\t\t\tpa = \"rtmpsconns\"\n\n\t\t\tcase \"hls\":\n\t\t\t\tpa = \"hlsmuxers\"\n\n\t\t\tcase \"webrtc\":\n\t\t\t\tpa = \"webrtcsessions\"\n\n\t\t\tcase \"srt\":\n\t\t\t\tpa = \"srtconns\"\n\t\t\t}\n\n\t\t\tvar out1 any\n\t\t\thttpRequest(t, hc, http.MethodGet, \"http://localhost:9997/v3/\"+pa+\"/list\", nil, &out1)\n\n\t\t\tswitch ca {\n\t\t\tcase \"rtsp conns\":\n\t\t\t\trequire.Equal(t, map[string]any{\n\t\t\t\t\t\"pageCount\": float64(1),\n\t\t\t\t\t\"itemCount\": float64(1),\n\t\t\t\t\t\"items\": []any{\n\t\t\t\t\t\tmap[string]any{\n\t\t\t\t\t\t\t\"inboundBytes\":  out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundBytes\"],\n\t\t\t\t\t\t\t\"outboundBytes\": out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"outboundBytes\"],\n\t\t\t\t\t\t\t\"bytesReceived\": out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"bytesReceived\"],\n\t\t\t\t\t\t\t\"bytesSent\":     out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"bytesSent\"],\n\t\t\t\t\t\t\t\"created\":       out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"created\"],\n\t\t\t\t\t\t\t\"id\":            out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"id\"],\n\t\t\t\t\t\t\t\"remoteAddr\":    out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"remoteAddr\"],\n\t\t\t\t\t\t\t\"session\":       out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"session\"],\n\t\t\t\t\t\t\t\"tunnel\":        \"none\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}, out1)\n\n\t\t\tcase \"rtsp sessions\":\n\t\t\t\trequire.Equal(t, map[string]any{\n\t\t\t\t\t\"pageCount\": float64(1),\n\t\t\t\t\t\"itemCount\": float64(1),\n\t\t\t\t\t\"items\": []any{\n\t\t\t\t\t\tmap[string]any{\n\t\t\t\t\t\t\t\"inboundBytes\":                   out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundBytes\"],\n\t\t\t\t\t\t\t\"inboundRTPPackets\":              out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundRTPPackets\"],\n\t\t\t\t\t\t\t\"inboundRTPPacketsLost\":          out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundRTPPacketsLost\"],\n\t\t\t\t\t\t\t\"inboundRTPPacketsInError\":       out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundRTPPacketsInError\"],\n\t\t\t\t\t\t\t\"inboundRTPPacketsJitter\":        out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundRTPPacketsJitter\"],\n\t\t\t\t\t\t\t\"inboundRTCPPackets\":             out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundRTCPPackets\"],\n\t\t\t\t\t\t\t\"inboundRTCPPacketsInError\":      out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundRTCPPacketsInError\"],\n\t\t\t\t\t\t\t\"outboundBytes\":                  out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"outboundBytes\"],\n\t\t\t\t\t\t\t\"outboundRTPPackets\":             out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"outboundRTPPackets\"],\n\t\t\t\t\t\t\t\"outboundRTPPacketsReportedLost\": out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"outboundRTPPacketsReportedLost\"],\n\t\t\t\t\t\t\t\"outboundRTPPacketsDiscarded\":    out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"outboundRTPPacketsDiscarded\"],\n\t\t\t\t\t\t\t\"outboundRTCPPackets\":            out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"outboundRTCPPackets\"],\n\t\t\t\t\t\t\t\"bytesReceived\":                  out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"bytesReceived\"],\n\t\t\t\t\t\t\t\"bytesSent\":                      out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"bytesSent\"],\n\t\t\t\t\t\t\t\"created\":                        out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"created\"],\n\t\t\t\t\t\t\t\"id\":                             out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"id\"],\n\t\t\t\t\t\t\t\"path\":                           \"mypath\",\n\t\t\t\t\t\t\t\"query\":                          \"key=val\",\n\t\t\t\t\t\t\t\"user\":                           \"\",\n\t\t\t\t\t\t\t\"remoteAddr\":                     out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"remoteAddr\"],\n\t\t\t\t\t\t\t\"state\":                          \"publish\",\n\t\t\t\t\t\t\t\"transport\":                      \"UDP\",\n\t\t\t\t\t\t\t\"profile\":                        \"AVP\",\n\t\t\t\t\t\t\t\"rtpPacketsReceived\":             float64(0),\n\t\t\t\t\t\t\t\"rtpPacketsSent\":                 float64(0),\n\t\t\t\t\t\t\t\"rtpPacketsLost\":                 float64(0),\n\t\t\t\t\t\t\t\"rtpPacketsInError\":              float64(0),\n\t\t\t\t\t\t\t\"rtpPacketsJitter\":               float64(0),\n\t\t\t\t\t\t\t\"rtcpPacketsReceived\":            float64(0),\n\t\t\t\t\t\t\t\"rtcpPacketsSent\":                float64(0),\n\t\t\t\t\t\t\t\"rtcpPacketsInError\":             float64(0),\n\t\t\t\t\t\t\t\"conns\":                          out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"conns\"],\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}, out1)\n\n\t\t\tcase \"rtsps conns\":\n\t\t\t\trequire.Equal(t, map[string]any{\n\t\t\t\t\t\"pageCount\": float64(1),\n\t\t\t\t\t\"itemCount\": float64(1),\n\t\t\t\t\t\"items\": []any{\n\t\t\t\t\t\tmap[string]any{\n\t\t\t\t\t\t\t\"inboundBytes\":  out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundBytes\"],\n\t\t\t\t\t\t\t\"outboundBytes\": out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"outboundBytes\"],\n\t\t\t\t\t\t\t\"bytesReceived\": out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"bytesReceived\"],\n\t\t\t\t\t\t\t\"bytesSent\":     out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"bytesSent\"],\n\t\t\t\t\t\t\t\"created\":       out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"created\"],\n\t\t\t\t\t\t\t\"id\":            out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"id\"],\n\t\t\t\t\t\t\t\"remoteAddr\":    out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"remoteAddr\"],\n\t\t\t\t\t\t\t\"session\":       out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"session\"],\n\t\t\t\t\t\t\t\"tunnel\":        \"none\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}, out1)\n\n\t\t\tcase \"rtsps sessions\":\n\t\t\t\trequire.Equal(t, map[string]any{\n\t\t\t\t\t\"pageCount\": float64(1),\n\t\t\t\t\t\"itemCount\": float64(1),\n\t\t\t\t\t\"items\": []any{\n\t\t\t\t\t\tmap[string]any{\n\t\t\t\t\t\t\t\"inboundBytes\":                   out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundBytes\"],\n\t\t\t\t\t\t\t\"inboundRTPPackets\":              out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundRTPPackets\"],\n\t\t\t\t\t\t\t\"inboundRTPPacketsLost\":          out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundRTPPacketsLost\"],\n\t\t\t\t\t\t\t\"inboundRTPPacketsInError\":       out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundRTPPacketsInError\"],\n\t\t\t\t\t\t\t\"inboundRTPPacketsJitter\":        out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundRTPPacketsJitter\"],\n\t\t\t\t\t\t\t\"inboundRTCPPackets\":             out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundRTCPPackets\"],\n\t\t\t\t\t\t\t\"inboundRTCPPacketsInError\":      out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundRTCPPacketsInError\"],\n\t\t\t\t\t\t\t\"outboundBytes\":                  out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"outboundBytes\"],\n\t\t\t\t\t\t\t\"outboundRTPPackets\":             out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"outboundRTPPackets\"],\n\t\t\t\t\t\t\t\"outboundRTPPacketsReportedLost\": out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"outboundRTPPacketsReportedLost\"],\n\t\t\t\t\t\t\t\"outboundRTPPacketsDiscarded\":    out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"outboundRTPPacketsDiscarded\"],\n\t\t\t\t\t\t\t\"outboundRTCPPackets\":            out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"outboundRTCPPackets\"],\n\t\t\t\t\t\t\t\"bytesReceived\":                  out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"bytesReceived\"],\n\t\t\t\t\t\t\t\"bytesSent\":                      out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"bytesSent\"],\n\t\t\t\t\t\t\t\"created\":                        out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"created\"],\n\t\t\t\t\t\t\t\"id\":                             out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"id\"],\n\t\t\t\t\t\t\t\"path\":                           \"mypath\",\n\t\t\t\t\t\t\t\"query\":                          \"key=val\",\n\t\t\t\t\t\t\t\"user\":                           \"\",\n\t\t\t\t\t\t\t\"remoteAddr\":                     out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"remoteAddr\"],\n\t\t\t\t\t\t\t\"state\":                          \"publish\",\n\t\t\t\t\t\t\t\"transport\":                      \"UDP\",\n\t\t\t\t\t\t\t\"profile\":                        \"SAVP\",\n\t\t\t\t\t\t\t\"rtpPacketsReceived\":             float64(0),\n\t\t\t\t\t\t\t\"rtpPacketsSent\":                 float64(0),\n\t\t\t\t\t\t\t\"rtpPacketsLost\":                 float64(0),\n\t\t\t\t\t\t\t\"rtpPacketsInError\":              float64(0),\n\t\t\t\t\t\t\t\"rtpPacketsJitter\":               float64(0),\n\t\t\t\t\t\t\t\"rtcpPacketsReceived\":            float64(0),\n\t\t\t\t\t\t\t\"rtcpPacketsSent\":                float64(0),\n\t\t\t\t\t\t\t\"rtcpPacketsInError\":             float64(0),\n\t\t\t\t\t\t\t\"conns\":                          out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"conns\"],\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}, out1)\n\n\t\t\tcase \"rtmp\":\n\t\t\t\trequire.Equal(t, map[string]any{\n\t\t\t\t\t\"pageCount\": float64(1),\n\t\t\t\t\t\"itemCount\": float64(1),\n\t\t\t\t\t\"items\": []any{\n\t\t\t\t\t\tmap[string]any{\n\t\t\t\t\t\t\t\"inboundBytes\":            out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundBytes\"],\n\t\t\t\t\t\t\t\"outboundBytes\":           out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"outboundBytes\"],\n\t\t\t\t\t\t\t\"outboundFramesDiscarded\": out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"outboundFramesDiscarded\"],\n\t\t\t\t\t\t\t\"bytesReceived\":           out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"bytesReceived\"],\n\t\t\t\t\t\t\t\"bytesSent\":               out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"bytesSent\"],\n\t\t\t\t\t\t\t\"created\":                 out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"created\"],\n\t\t\t\t\t\t\t\"id\":                      out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"id\"],\n\t\t\t\t\t\t\t\"path\":                    \"mypath\",\n\t\t\t\t\t\t\t\"query\":                   \"key=val\",\n\t\t\t\t\t\t\t\"user\":                    \"\",\n\t\t\t\t\t\t\t\"remoteAddr\":              out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"remoteAddr\"],\n\t\t\t\t\t\t\t\"state\":                   \"publish\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}, out1)\n\n\t\t\tcase \"rtmps\":\n\t\t\t\trequire.Equal(t, map[string]any{\n\t\t\t\t\t\"pageCount\": float64(1),\n\t\t\t\t\t\"itemCount\": float64(1),\n\t\t\t\t\t\"items\": []any{\n\t\t\t\t\t\tmap[string]any{\n\t\t\t\t\t\t\t\"inboundBytes\":            out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundBytes\"],\n\t\t\t\t\t\t\t\"outboundBytes\":           out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"outboundBytes\"],\n\t\t\t\t\t\t\t\"outboundFramesDiscarded\": out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"outboundFramesDiscarded\"],\n\t\t\t\t\t\t\t\"bytesReceived\":           out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"bytesReceived\"],\n\t\t\t\t\t\t\t\"bytesSent\":               out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"bytesSent\"],\n\t\t\t\t\t\t\t\"created\":                 out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"created\"],\n\t\t\t\t\t\t\t\"id\":                      out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"id\"],\n\t\t\t\t\t\t\t\"path\":                    \"mypath\",\n\t\t\t\t\t\t\t\"query\":                   \"key=val\",\n\t\t\t\t\t\t\t\"user\":                    \"\",\n\t\t\t\t\t\t\t\"remoteAddr\":              out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"remoteAddr\"],\n\t\t\t\t\t\t\t\"state\":                   \"publish\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}, out1)\n\n\t\t\tcase \"hls\":\n\t\t\t\trequire.Equal(t, map[string]any{\n\t\t\t\t\t\"itemCount\": float64(1),\n\t\t\t\t\t\"pageCount\": float64(1),\n\t\t\t\t\t\"items\": []any{\n\t\t\t\t\t\tmap[string]any{\n\t\t\t\t\t\t\t\"outboundBytes\":           out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"outboundBytes\"],\n\t\t\t\t\t\t\t\"outboundFramesDiscarded\": out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"outboundFramesDiscarded\"],\n\t\t\t\t\t\t\t\"bytesSent\":               out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"bytesSent\"],\n\t\t\t\t\t\t\t\"created\":                 out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"created\"],\n\t\t\t\t\t\t\t\"lastRequest\":             out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"lastRequest\"],\n\t\t\t\t\t\t\t\"path\":                    \"mypath\",\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}, out1)\n\n\t\t\tcase \"webrtc\":\n\t\t\t\trequire.Equal(t, map[string]any{\n\t\t\t\t\t\"itemCount\": float64(1),\n\t\t\t\t\t\"pageCount\": float64(1),\n\t\t\t\t\t\"items\": []any{\n\t\t\t\t\t\tmap[string]any{\n\t\t\t\t\t\t\t\"inboundBytes\":              out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundBytes\"],\n\t\t\t\t\t\t\t\"inboundRTPPackets\":         out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundRTPPackets\"],\n\t\t\t\t\t\t\t\"inboundRTPPacketsLost\":     out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundRTPPacketsLost\"],\n\t\t\t\t\t\t\t\"inboundRTPPacketsJitter\":   out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundRTPPacketsJitter\"],\n\t\t\t\t\t\t\t\"inboundRTCPPackets\":        out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundRTCPPackets\"],\n\t\t\t\t\t\t\t\"outboundBytes\":             out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"outboundBytes\"],\n\t\t\t\t\t\t\t\"outboundRTPPackets\":        out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"outboundRTPPackets\"],\n\t\t\t\t\t\t\t\"outboundRTCPPackets\":       out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"outboundRTCPPackets\"],\n\t\t\t\t\t\t\t\"outboundFramesDiscarded\":   out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"outboundFramesDiscarded\"],\n\t\t\t\t\t\t\t\"bytesReceived\":             out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"bytesReceived\"],\n\t\t\t\t\t\t\t\"bytesSent\":                 out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"bytesSent\"],\n\t\t\t\t\t\t\t\"created\":                   out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"created\"],\n\t\t\t\t\t\t\t\"id\":                        out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"id\"],\n\t\t\t\t\t\t\t\"localCandidate\":            out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"localCandidate\"],\n\t\t\t\t\t\t\t\"path\":                      \"mypath\",\n\t\t\t\t\t\t\t\"peerConnectionEstablished\": true,\n\t\t\t\t\t\t\t\"query\":                     \"key=val\",\n\t\t\t\t\t\t\t\"remoteAddr\":                out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"remoteAddr\"],\n\t\t\t\t\t\t\t\"remoteCandidate\":           out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"remoteCandidate\"],\n\t\t\t\t\t\t\t\"state\":                     \"read\",\n\t\t\t\t\t\t\t\"user\":                      \"\",\n\t\t\t\t\t\t\t\"rtcpPacketsReceived\":       float64(0),\n\t\t\t\t\t\t\t\"rtcpPacketsSent\":           float64(2),\n\t\t\t\t\t\t\t\"rtpPacketsJitter\":          float64(0),\n\t\t\t\t\t\t\t\"rtpPacketsLost\":            float64(0),\n\t\t\t\t\t\t\t\"rtpPacketsReceived\":        float64(0),\n\t\t\t\t\t\t\t\"rtpPacketsSent\":            float64(1),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}, out1)\n\n\t\t\tcase \"srt\":\n\t\t\t\trequire.Equal(t, map[string]any{\n\t\t\t\t\t\"itemCount\": float64(1),\n\t\t\t\t\t\"pageCount\": float64(1),\n\t\t\t\t\t\"items\": []any{\n\t\t\t\t\t\tmap[string]any{\n\t\t\t\t\t\t\t\"byteMSS\":                       float64(1500),\n\t\t\t\t\t\t\t\"bytesAvailReceiveBuf\":          float64(0),\n\t\t\t\t\t\t\t\"bytesAvailSendBuf\":             float64(0),\n\t\t\t\t\t\t\t\"bytesReceiveBuf\":               float64(0),\n\t\t\t\t\t\t\t\"bytesReceived\":                 float64(628),\n\t\t\t\t\t\t\t\"bytesReceivedBelated\":          float64(0),\n\t\t\t\t\t\t\t\"bytesReceivedDrop\":             float64(0),\n\t\t\t\t\t\t\t\"bytesReceivedLoss\":             float64(0),\n\t\t\t\t\t\t\t\"bytesReceivedRetrans\":          float64(0),\n\t\t\t\t\t\t\t\"bytesReceivedUndecrypt\":        float64(0),\n\t\t\t\t\t\t\t\"outboundFramesDiscarded\":       out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"outboundFramesDiscarded\"],\n\t\t\t\t\t\t\t\"bytesReceivedUnique\":           float64(628),\n\t\t\t\t\t\t\t\"bytesRetrans\":                  float64(0),\n\t\t\t\t\t\t\t\"bytesSendBuf\":                  float64(0),\n\t\t\t\t\t\t\t\"bytesSendDrop\":                 float64(0),\n\t\t\t\t\t\t\t\"bytesSent\":                     float64(0),\n\t\t\t\t\t\t\t\"bytesSentUnique\":               float64(0),\n\t\t\t\t\t\t\t\"created\":                       out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"created\"],\n\t\t\t\t\t\t\t\"id\":                            out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"id\"],\n\t\t\t\t\t\t\t\"mbpsLinkCapacity\":              float64(0),\n\t\t\t\t\t\t\t\"mbpsMaxBW\":                     float64(-1),\n\t\t\t\t\t\t\t\"mbpsReceiveRate\":               float64(0),\n\t\t\t\t\t\t\t\"mbpsSendRate\":                  float64(0),\n\t\t\t\t\t\t\t\"msRTT\":                         out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"msRTT\"],\n\t\t\t\t\t\t\t\"msReceiveBuf\":                  float64(0),\n\t\t\t\t\t\t\t\"msReceiveTsbPdDelay\":           float64(120),\n\t\t\t\t\t\t\t\"msSendBuf\":                     float64(0),\n\t\t\t\t\t\t\t\"msSendTsbPdDelay\":              float64(120),\n\t\t\t\t\t\t\t\"packetsFlightSize\":             float64(0),\n\t\t\t\t\t\t\t\"packetsFlowWindow\":             float64(25600),\n\t\t\t\t\t\t\t\"packetsReceiveBuf\":             float64(0),\n\t\t\t\t\t\t\t\"packetsReceived\":               float64(1),\n\t\t\t\t\t\t\t\"packetsReceivedACK\":            out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"packetsReceivedACK\"],\n\t\t\t\t\t\t\t\"packetsReceivedAvgBelatedTime\": float64(0),\n\t\t\t\t\t\t\t\"packetsReceivedBelated\":        float64(0),\n\t\t\t\t\t\t\t\"packetsReceivedDrop\":           float64(0),\n\t\t\t\t\t\t\t\"packetsReceivedKM\":             float64(0),\n\t\t\t\t\t\t\t\"packetsReceivedLoss\":           float64(0),\n\t\t\t\t\t\t\t\"packetsReceivedLossRate\":       float64(0),\n\t\t\t\t\t\t\t\"packetsReceivedNAK\":            float64(0),\n\t\t\t\t\t\t\t\"packetsReceivedRetrans\":        float64(0),\n\t\t\t\t\t\t\t\"packetsReceivedUndecrypt\":      float64(0),\n\t\t\t\t\t\t\t\"packetsReceivedUnique\":         float64(1),\n\t\t\t\t\t\t\t\"packetsReorderTolerance\":       float64(0),\n\t\t\t\t\t\t\t\"packetsRetrans\":                float64(0),\n\t\t\t\t\t\t\t\"packetsSendBuf\":                float64(0),\n\t\t\t\t\t\t\t\"packetsSendDrop\":               float64(0),\n\t\t\t\t\t\t\t\"packetsSendLoss\":               float64(0),\n\t\t\t\t\t\t\t\"packetsSendLossRate\":           float64(0),\n\t\t\t\t\t\t\t\"packetsSent\":                   float64(0),\n\t\t\t\t\t\t\t\"packetsSentACK\":                out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"packetsSentACK\"],\n\t\t\t\t\t\t\t\"packetsSentKM\":                 float64(0),\n\t\t\t\t\t\t\t\"packetsSentNAK\":                float64(0),\n\t\t\t\t\t\t\t\"packetsSentUnique\":             float64(0),\n\t\t\t\t\t\t\t\"path\":                          \"mypath\",\n\t\t\t\t\t\t\t\"query\":                         \"key=val\",\n\t\t\t\t\t\t\t\"remoteAddr\":                    out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"remoteAddr\"],\n\t\t\t\t\t\t\t\"state\":                         \"publish\",\n\t\t\t\t\t\t\t\"user\":                          \"\",\n\t\t\t\t\t\t\t\"usPacketsSendPeriod\":           float64(10.967254638671875),\n\t\t\t\t\t\t\t\"usSndDuration\":                 float64(0),\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}, out1)\n\t\t\t}\n\n\t\t\tvar out2 any\n\n\t\t\tif ca == \"hls\" {\n\t\t\t\thttpRequest(t, hc, http.MethodGet, \"http://localhost:9997/v3/\"+pa+\"/get/\"+\n\t\t\t\t\tout1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"path\"].(string),\n\t\t\t\t\tnil, &out2)\n\t\t\t} else {\n\t\t\t\thttpRequest(t, hc, http.MethodGet, \"http://localhost:9997/v3/\"+pa+\"/get/\"+\n\t\t\t\t\tout1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"id\"].(string),\n\t\t\t\t\tnil, &out2)\n\t\t\t}\n\n\t\t\tswitch ca {\n\t\t\tcase \"rtsp conns\", \"rtsps conns\":\n\t\t\t\tout2.(map[string]any)[\"inboundBytes\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundBytes\"]\n\t\t\t\tout2.(map[string]any)[\"outboundBytes\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"outboundBytes\"]\n\t\t\t\tout2.(map[string]any)[\"bytesReceived\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"bytesReceived\"]\n\t\t\t\tout2.(map[string]any)[\"bytesSent\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"bytesSent\"]\n\n\t\t\tcase \"rtsp sessions\", \"rtsps sessions\":\n\t\t\t\tout2.(map[string]any)[\"inboundBytes\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundBytes\"]\n\t\t\t\tout2.(map[string]any)[\"inboundRTPPackets\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundRTPPackets\"]\n\t\t\t\tout2.(map[string]any)[\"inboundRTPPacketsLost\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundRTPPacketsLost\"]\n\t\t\t\tout2.(map[string]any)[\"inboundRTPPacketsInError\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundRTPPacketsInError\"]\n\t\t\t\tout2.(map[string]any)[\"inboundRTPPacketsJitter\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundRTPPacketsJitter\"]\n\t\t\t\tout2.(map[string]any)[\"inboundRTCPPackets\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundRTCPPackets\"]\n\t\t\t\tout2.(map[string]any)[\"inboundRTCPPacketsInError\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundRTCPPacketsInError\"]\n\t\t\t\tout2.(map[string]any)[\"outboundBytes\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"outboundBytes\"]\n\t\t\t\tout2.(map[string]any)[\"outboundRTPPackets\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"outboundRTPPackets\"]\n\t\t\t\tout2.(map[string]any)[\"outboundRTPPacketsReportedLost\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"outboundRTPPacketsReportedLost\"]\n\t\t\t\tout2.(map[string]any)[\"outboundRTPPacketsDiscarded\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"outboundRTPPacketsDiscarded\"]\n\t\t\t\tout2.(map[string]any)[\"outboundRTCPPackets\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"outboundRTCPPackets\"]\n\t\t\t\tout2.(map[string]any)[\"bytesReceived\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"bytesReceived\"]\n\t\t\t\tout2.(map[string]any)[\"bytesSent\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"bytesSent\"]\n\t\t\t\tout2.(map[string]any)[\"rtpPacketsReceived\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"rtpPacketsReceived\"]\n\t\t\t\tout2.(map[string]any)[\"rtpPacketsSent\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"rtpPacketsSent\"]\n\t\t\t\tout2.(map[string]any)[\"rtpPacketsLost\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"rtpPacketsLost\"]\n\t\t\t\tout2.(map[string]any)[\"rtpPacketsInError\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"rtpPacketsInError\"]\n\t\t\t\tout2.(map[string]any)[\"rtpPacketsJitter\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"rtpPacketsJitter\"]\n\t\t\t\tout2.(map[string]any)[\"rtcpPacketsReceived\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"rtcpPacketsReceived\"]\n\t\t\t\tout2.(map[string]any)[\"rtcpPacketsSent\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"rtcpPacketsSent\"]\n\t\t\t\tout2.(map[string]any)[\"rtcpPacketsInError\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"rtcpPacketsInError\"]\n\n\t\t\tcase \"webrtc\":\n\t\t\t\tout2.(map[string]any)[\"inboundBytes\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundBytes\"]\n\t\t\t\tout2.(map[string]any)[\"inboundRTPPackets\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundRTPPackets\"]\n\t\t\t\tout2.(map[string]any)[\"inboundRTPPacketsLost\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundRTPPacketsLost\"]\n\t\t\t\tout2.(map[string]any)[\"inboundRTPPacketsJitter\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundRTPPacketsJitter\"]\n\t\t\t\tout2.(map[string]any)[\"inboundRTCPPackets\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"inboundRTCPPackets\"]\n\t\t\t\tout2.(map[string]any)[\"outboundBytes\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"outboundBytes\"]\n\t\t\t\tout2.(map[string]any)[\"outboundRTPPackets\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"outboundRTPPackets\"]\n\t\t\t\tout2.(map[string]any)[\"outboundRTCPPackets\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"outboundRTCPPackets\"]\n\t\t\t\tout2.(map[string]any)[\"outboundFramesDiscarded\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"outboundFramesDiscarded\"]\n\t\t\t\tout2.(map[string]any)[\"bytesReceived\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"bytesReceived\"]\n\t\t\t\tout2.(map[string]any)[\"bytesSent\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"bytesSent\"]\n\t\t\t\tout2.(map[string]any)[\"rtpPacketsReceived\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"rtpPacketsReceived\"]\n\t\t\t\tout2.(map[string]any)[\"rtpPacketsSent\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"rtpPacketsSent\"]\n\t\t\t\tout2.(map[string]any)[\"rtpPacketsLost\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"rtpPacketsLost\"]\n\t\t\t\tout2.(map[string]any)[\"rtpPacketsJitter\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"rtpPacketsJitter\"]\n\t\t\t\tout2.(map[string]any)[\"rtcpPacketsReceived\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"rtcpPacketsReceived\"]\n\t\t\t\tout2.(map[string]any)[\"rtcpPacketsSent\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"rtcpPacketsSent\"]\n\n\t\t\tcase \"hls\":\n\t\t\t\tout2.(map[string]any)[\"lastRequest\"] = out1.(map[string]any)[\"items\"].([]any)[0].(map[string]any)[\"lastRequest\"]\n\t\t\t}\n\n\t\t\trequire.Equal(t, out1.(map[string]any)[\"items\"].([]any)[0], out2)\n\t\t})\n\t}\n}\n\nfunc TestAPIProtocolGetNotFound(t *testing.T) {\n\tserverCertFpath, err := test.CreateTempFile(test.TLSCertPub)\n\trequire.NoError(t, err)\n\tdefer os.Remove(serverCertFpath)\n\n\tserverKeyFpath, err := test.CreateTempFile(test.TLSCertKey)\n\trequire.NoError(t, err)\n\tdefer os.Remove(serverKeyFpath)\n\n\tfor _, ca := range []string{\n\t\t\"rtsp conns\",\n\t\t\"rtsp sessions\",\n\t\t\"rtsps conns\",\n\t\t\"rtsps sessions\",\n\t\t\"rtmp\",\n\t\t\"rtmps\",\n\t\t\"hls\",\n\t\t\"webrtc\",\n\t\t\"srt\",\n\t} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tcnf := \"api: yes\\n\"\n\n\t\t\tswitch ca {\n\t\t\tcase \"rtsps conns\", \"rtsps sessions\":\n\t\t\t\tcnf += \"rtspTransports: [tcp]\\n\" +\n\t\t\t\t\t\"rtspEncryption: strict\\n\" +\n\t\t\t\t\t\"rtspServerCert: \" + serverCertFpath + \"\\n\" +\n\t\t\t\t\t\"rtspServerKey: \" + serverKeyFpath + \"\\n\"\n\n\t\t\tcase \"rtmps\":\n\t\t\t\tcnf += \"rtmpEncryption: strict\\n\" +\n\t\t\t\t\t\"rtmpServerCert: \" + serverCertFpath + \"\\n\" +\n\t\t\t\t\t\"rtmpServerKey: \" + serverKeyFpath + \"\\n\"\n\t\t\t}\n\n\t\t\tcnf += \"paths:\\n\" +\n\t\t\t\t\"  all_others:\\n\"\n\n\t\t\tp, ok := newInstance(cnf)\n\t\t\trequire.Equal(t, true, ok)\n\t\t\tdefer p.Close()\n\n\t\t\ttr := &http.Transport{}\n\t\t\tdefer tr.CloseIdleConnections()\n\t\t\thc := &http.Client{Transport: tr}\n\n\t\t\tvar pa string\n\t\t\tswitch ca {\n\t\t\tcase \"rtsp conns\":\n\t\t\t\tpa = \"rtspconns\"\n\n\t\t\tcase \"rtsp sessions\":\n\t\t\t\tpa = \"rtspsessions\"\n\n\t\t\tcase \"rtsps conns\":\n\t\t\t\tpa = \"rtspsconns\"\n\n\t\t\tcase \"rtsps sessions\":\n\t\t\t\tpa = \"rtspssessions\"\n\n\t\t\tcase \"rtmp\":\n\t\t\t\tpa = \"rtmpconns\"\n\n\t\t\tcase \"rtmps\":\n\t\t\t\tpa = \"rtmpsconns\"\n\n\t\t\tcase \"hls\":\n\t\t\t\tpa = \"hlsmuxers\"\n\n\t\t\tcase \"webrtc\":\n\t\t\t\tpa = \"webrtcsessions\"\n\n\t\t\tcase \"srt\":\n\t\t\t\tpa = \"srtconns\"\n\t\t\t}\n\n\t\t\tfunc() {\n\t\t\t\tvar req *http.Request\n\t\t\t\treq, err = http.NewRequest(http.MethodGet, \"http://localhost:9997/v3/\"+pa+\"/get/\"+uuid.New().String(), nil)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tvar res *http.Response\n\t\t\t\tres, err = hc.Do(req)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer res.Body.Close()\n\n\t\t\t\trequire.Equal(t, http.StatusNotFound, res.StatusCode)\n\n\t\t\t\tswitch ca {\n\t\t\t\tcase \"rtsp conns\", \"rtsps conns\", \"rtmp\", \"rtmps\", \"srt\":\n\t\t\t\t\tcheckError(t, \"connection not found\", res.Body)\n\n\t\t\t\tcase \"rtsp sessions\", \"rtsps sessions\", \"webrtc\":\n\t\t\t\t\tcheckError(t, \"session not found\", res.Body)\n\n\t\t\t\tcase \"hls\":\n\t\t\t\t\tcheckError(t, \"muxer not found\", res.Body)\n\t\t\t\t}\n\t\t\t}()\n\t\t})\n\t}\n}\n\nfunc TestAPIProtocolKick(t *testing.T) {\n\tserverCertFpath, err := test.CreateTempFile(test.TLSCertPub)\n\trequire.NoError(t, err)\n\tdefer os.Remove(serverCertFpath)\n\n\tserverKeyFpath, err := test.CreateTempFile(test.TLSCertKey)\n\trequire.NoError(t, err)\n\tdefer os.Remove(serverKeyFpath)\n\n\tfor _, ca := range []string{\n\t\t\"rtsp\",\n\t\t\"rtsps\",\n\t\t\"rtmp\",\n\t\t\"webrtc\",\n\t\t\"srt\",\n\t} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tcnf := \"api: yes\\n\"\n\n\t\t\tif ca == \"rtsps\" {\n\t\t\t\tcnf += \"rtspTransports: [tcp]\\n\" +\n\t\t\t\t\t\"rtspEncryption: strict\\n\" +\n\t\t\t\t\t\"rtspServerCert: \" + serverCertFpath + \"\\n\" +\n\t\t\t\t\t\"rtspServerKey: \" + serverKeyFpath + \"\\n\"\n\t\t\t}\n\n\t\t\tcnf += \"paths:\\n\" +\n\t\t\t\t\"  all_others:\\n\"\n\n\t\t\tp, ok := newInstance(cnf)\n\t\t\trequire.Equal(t, true, ok)\n\t\t\tdefer p.Close()\n\n\t\t\ttr := &http.Transport{}\n\t\t\tdefer tr.CloseIdleConnections()\n\t\t\thc := &http.Client{Transport: tr}\n\n\t\t\tmedi := test.MediaH264\n\n\t\t\tswitch ca {\n\t\t\tcase \"rtsp\":\n\t\t\t\tsource := gortsplib.Client{}\n\n\t\t\t\terr = source.StartRecording(\"rtsp://localhost:8554/mypath\",\n\t\t\t\t\t&description.Session{Medias: []*description.Media{medi}})\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer source.Close()\n\n\t\t\tcase \"rtsps\":\n\t\t\t\tsource := gortsplib.Client{\n\t\t\t\t\tTLSConfig: &tls.Config{InsecureSkipVerify: true},\n\t\t\t\t}\n\n\t\t\t\terr = source.StartRecording(\"rtsps://localhost:8322/mypath\",\n\t\t\t\t\t&description.Session{Medias: []*description.Media{medi}})\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer source.Close()\n\n\t\t\tcase \"rtmp\":\n\t\t\t\tvar u *url.URL\n\t\t\t\tu, err = url.Parse(\"rtmp://localhost:1935/mypath\")\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tconn := &gortmplib.Client{\n\t\t\t\t\tURL:     u,\n\t\t\t\t\tPublish: true,\n\t\t\t\t}\n\t\t\t\terr = conn.Initialize(context.Background())\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer conn.Close()\n\n\t\t\t\ttrack := &gortmplib.Track{\n\t\t\t\t\tCodec: &rtmpcodecs.H264{\n\t\t\t\t\t\tSPS: test.FormatH264.SPS,\n\t\t\t\t\t\tPPS: test.FormatH264.PPS,\n\t\t\t\t\t},\n\t\t\t\t}\n\n\t\t\t\tw := &gortmplib.Writer{\n\t\t\t\t\tConn:   conn,\n\t\t\t\t\tTracks: []*gortmplib.Track{track},\n\t\t\t\t}\n\t\t\t\terr = w.Initialize()\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\terr = w.WriteH264(track, 2*time.Second, 2*time.Second, [][]byte{{5, 2, 3, 4}})\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\tcase \"webrtc\":\n\t\t\t\tvar u *url.URL\n\t\t\t\tu, err = url.Parse(\"http://localhost:8889/mypath/whip\")\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\ttrack := &webrtc.OutgoingTrack{\n\t\t\t\t\tCaps: pwebrtc.RTPCodecCapability{\n\t\t\t\t\t\tMimeType:    pwebrtc.MimeTypeH264,\n\t\t\t\t\t\tClockRate:   90000,\n\t\t\t\t\t\tSDPFmtpLine: \"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\",\n\t\t\t\t\t},\n\t\t\t\t}\n\n\t\t\t\tc := &whip.Client{\n\t\t\t\t\tHTTPClient:     hc,\n\t\t\t\t\tURL:            u,\n\t\t\t\t\tLog:            test.NilLogger,\n\t\t\t\t\tPublish:        true,\n\t\t\t\t\tOutgoingTracks: []*webrtc.OutgoingTrack{track},\n\t\t\t\t}\n\n\t\t\t\terr = c.Initialize(context.Background())\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer func() {\n\t\t\t\t\trequire.Error(t, c.Close())\n\t\t\t\t}()\n\n\t\t\tcase \"srt\":\n\t\t\t\tconf := srt.DefaultConfig()\n\t\t\t\tconf.StreamId = \"publish:mypath\"\n\n\t\t\t\tvar conn srt.Conn\n\t\t\t\tconn, err = srt.Dial(\"srt\", \"localhost:8890\", conf)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer conn.Close()\n\n\t\t\t\ttrack := &mpegts.Track{\n\t\t\t\t\tCodec: &tscodecs.H264{},\n\t\t\t\t}\n\n\t\t\t\tbw := bufio.NewWriter(conn)\n\t\t\t\tw := &mpegts.Writer{W: bw, Tracks: []*mpegts.Track{track}}\n\t\t\t\terr = w.Initialize()\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\terr = w.WriteH264(track, 0, 0, [][]byte{{1}})\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\terr = bw.Flush()\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\tvar pa string\n\t\t\tswitch ca {\n\t\t\tcase \"rtsp\":\n\t\t\t\tpa = \"rtspsessions\"\n\n\t\t\tcase \"rtsps\":\n\t\t\t\tpa = \"rtspssessions\"\n\n\t\t\tcase \"rtmp\":\n\t\t\t\tpa = \"rtmpconns\"\n\n\t\t\tcase \"webrtc\":\n\t\t\t\tpa = \"webrtcsessions\"\n\n\t\t\tcase \"srt\":\n\t\t\t\tpa = \"srtconns\"\n\t\t\t}\n\n\t\t\tvar out1 struct {\n\t\t\t\tItems []struct {\n\t\t\t\t\tID string `json:\"id\"`\n\t\t\t\t} `json:\"items\"`\n\t\t\t}\n\t\t\thttpRequest(t, hc, http.MethodGet, \"http://localhost:9997/v3/\"+pa+\"/list\", nil, &out1)\n\n\t\t\thttpRequest(t, hc, http.MethodPost, \"http://localhost:9997/v3/\"+pa+\"/kick/\"+out1.Items[0].ID, nil, nil)\n\n\t\t\tvar out2 struct {\n\t\t\t\tItems []struct {\n\t\t\t\t\tID string `json:\"id\"`\n\t\t\t\t} `json:\"items\"`\n\t\t\t}\n\t\t\thttpRequest(t, hc, http.MethodGet, \"http://localhost:9997/v3/\"+pa+\"/list\", nil, &out2)\n\t\t\trequire.Empty(t, out2.Items)\n\t\t})\n\t}\n}\n\nfunc TestAPIProtocolKickNotFound(t *testing.T) {\n\tserverCertFpath, err := test.CreateTempFile(test.TLSCertPub)\n\trequire.NoError(t, err)\n\tdefer os.Remove(serverCertFpath)\n\n\tserverKeyFpath, err := test.CreateTempFile(test.TLSCertKey)\n\trequire.NoError(t, err)\n\tdefer os.Remove(serverKeyFpath)\n\n\tfor _, ca := range []string{\n\t\t\"rtsp\",\n\t\t\"rtsps\",\n\t\t\"rtmp\",\n\t\t\"webrtc\",\n\t\t\"srt\",\n\t} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tcnf := \"api: yes\\n\"\n\n\t\t\tif ca == \"rtsps\" {\n\t\t\t\tcnf += \"rtspTransports: [tcp]\\n\" +\n\t\t\t\t\t\"rtspEncryption: strict\\n\" +\n\t\t\t\t\t\"rtspServerCert: \" + serverCertFpath + \"\\n\" +\n\t\t\t\t\t\"rtspServerKey: \" + serverKeyFpath + \"\\n\"\n\t\t\t}\n\n\t\t\tcnf += \"paths:\\n\" +\n\t\t\t\t\"  all_others:\\n\"\n\n\t\t\tp, ok := newInstance(cnf)\n\t\t\trequire.Equal(t, true, ok)\n\t\t\tdefer p.Close()\n\n\t\t\ttr := &http.Transport{}\n\t\t\tdefer tr.CloseIdleConnections()\n\t\t\thc := &http.Client{Transport: tr}\n\n\t\t\tvar pa string\n\t\t\tswitch ca {\n\t\t\tcase \"rtsp\":\n\t\t\t\tpa = \"rtspsessions\"\n\n\t\t\tcase \"rtsps\":\n\t\t\t\tpa = \"rtspssessions\"\n\n\t\t\tcase \"rtmp\":\n\t\t\t\tpa = \"rtmpconns\"\n\n\t\t\tcase \"webrtc\":\n\t\t\t\tpa = \"webrtcsessions\"\n\n\t\t\tcase \"srt\":\n\t\t\t\tpa = \"srtconns\"\n\t\t\t}\n\n\t\t\tfunc() {\n\t\t\t\tvar req *http.Request\n\t\t\t\treq, err = http.NewRequest(http.MethodPost, \"http://localhost:9997/v3/\"+pa+\"/kick/\"+uuid.New().String(), nil)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tvar res *http.Response\n\t\t\t\tres, err = hc.Do(req)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer res.Body.Close()\n\n\t\t\t\trequire.Equal(t, http.StatusNotFound, res.StatusCode)\n\n\t\t\t\tswitch ca {\n\t\t\t\tcase \"rtsp conns\", \"rtsps conns\", \"rtmp\", \"rtmps\", \"srt\":\n\t\t\t\t\tcheckError(t, \"connection not found\", res.Body)\n\n\t\t\t\tcase \"rtsp sessions\", \"rtsps sessions\", \"webrtc\":\n\t\t\t\t\tcheckError(t, \"session not found\", res.Body)\n\n\t\t\t\tcase \"hls\":\n\t\t\t\t\tcheckError(t, \"muxer not found\", res.Body)\n\t\t\t\t}\n\t\t\t}()\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/core/core.go",
    "content": "// Package core contains the main struct of the software.\npackage core\n\nimport (\n\t\"context\"\n\t_ \"embed\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/signal\"\n\t\"path/filepath\"\n\t\"reflect\"\n\t\"runtime\"\n\t\"runtime/debug\"\n\t\"slices\"\n\t\"strings\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/alecthomas/kong\"\n\t\"github.com/gin-gonic/gin\"\n\n\t\"github.com/bluenviron/mediamtx/internal/api\"\n\t\"github.com/bluenviron/mediamtx/internal/auth\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/confwatcher\"\n\t\"github.com/bluenviron/mediamtx/internal/externalcmd\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/metrics\"\n\t\"github.com/bluenviron/mediamtx/internal/playback\"\n\t\"github.com/bluenviron/mediamtx/internal/pprof\"\n\t\"github.com/bluenviron/mediamtx/internal/recordcleaner\"\n\t\"github.com/bluenviron/mediamtx/internal/rlimit\"\n\t\"github.com/bluenviron/mediamtx/internal/servers/hls\"\n\t\"github.com/bluenviron/mediamtx/internal/servers/rtmp\"\n\t\"github.com/bluenviron/mediamtx/internal/servers/rtsp\"\n\t\"github.com/bluenviron/mediamtx/internal/servers/srt\"\n\t\"github.com/bluenviron/mediamtx/internal/servers/webrtc\"\n)\n\n//go:generate go run ./versiongetter\n\n//go:embed VERSION\nvar version []byte\n\nvar started = time.Now()\n\nvar defaultConfPaths = []string{\n\t\"rtsp-simple-server.yml\",\n\t\"mediamtx.yml\",\n}\n\nvar defaultConfPathsNotWin = []string{\n\t\"/usr/local/etc/mediamtx.yml\",\n\t\"/usr/etc/mediamtx.yml\",\n\t\"/etc/mediamtx/mediamtx.yml\",\n}\n\nfunc goArm() string {\n\tbi, _ := debug.ReadBuildInfo()\n\tfor _, bs := range bi.Settings {\n\t\tif bs.Key == \"GOARM\" {\n\t\t\treturn bs.Value\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc getArch() string {\n\tvar arch string\n\tif runtime.GOARCH == \"arm\" {\n\t\tarch = \"armv\" + goArm()\n\t} else {\n\t\tarch = runtime.GOARCH\n\t}\n\treturn arch\n}\n\nfunc atLeastOneRecordDeleteAfter(pathConfs map[string]*conf.Path) bool {\n\tfor _, e := range pathConfs {\n\t\tif e.RecordDeleteAfter != 0 {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc getRTPMaxPayloadSize(udpMaxPayloadSize int, rtspEncryption conf.Encryption) int {\n\t// UDP max payload size - 12 (RTP header)\n\tv := udpMaxPayloadSize - 12\n\n\t// 10 (SRTP HMAC SHA1 authentication tag)\n\tif rtspEncryption == conf.EncryptionOptional || rtspEncryption == conf.EncryptionStrict {\n\t\tv -= 10\n\t}\n\n\treturn v\n}\n\nvar cli struct {\n\tConfpath string `arg:\"\" default:\"\"`\n\tVersion  bool   `help:\"print version\"`\n\tUpgrade  bool   `help:\"upgrade executable to the latest version\"`\n}\n\n// Core is an instance of MediaMTX.\ntype Core struct {\n\tctx             context.Context\n\tctxCancel       func()\n\tconfPath        string\n\tconf            *conf.Conf\n\tlogger          *logger.Logger\n\texternalCmdPool *externalcmd.Pool\n\tauthManager     *auth.Manager\n\tmetrics         *metrics.Metrics\n\tpprof           *pprof.PPROF\n\trecordCleaner   *recordcleaner.Cleaner\n\tplaybackServer  *playback.Server\n\tpathManager     *pathManager\n\trtspServer      *rtsp.Server\n\trtspsServer     *rtsp.Server\n\trtmpServer      *rtmp.Server\n\trtmpsServer     *rtmp.Server\n\thlsServer       *hls.Server\n\twebRTCServer    *webrtc.Server\n\tsrtServer       *srt.Server\n\tapi             *api.API\n\tconfWatcher     *confwatcher.ConfWatcher\n\n\t// in\n\tchAPIConfigSet chan *conf.Conf\n\n\t// out\n\tdone chan struct{}\n}\n\n// New allocates a Core.\nfunc New(args []string) (*Core, bool) {\n\tparser, err := kong.New(&cli,\n\t\tkong.Description(\"MediaMTX \"+string(version)+\", \"+runtime.GOOS+\", \"+getArch()),\n\t\tkong.UsageOnError(),\n\t\tkong.ValueFormatter(func(value *kong.Value) string {\n\t\t\tswitch value.Name {\n\t\t\tcase \"confpath\":\n\t\t\t\treturn \"path to a config file. The default is mediamtx.yml.\"\n\n\t\t\tdefault:\n\t\t\t\treturn kong.DefaultHelpValueFormatter(value)\n\t\t\t}\n\t\t}))\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = parser.Parse(args)\n\tparser.FatalIfErrorf(err)\n\n\tif cli.Version {\n\t\tfmt.Println(string(version))\n\t\tos.Exit(0)\n\t}\n\n\tif cli.Upgrade {\n\t\terr = upgrade() //nolint:staticcheck\n\t\tif err != nil { //nolint:staticcheck\n\t\t\tfmt.Printf(\"ERR: %v\\n\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t\tos.Exit(0)\n\t}\n\n\tctx, ctxCancel := context.WithCancel(context.Background())\n\n\tp := &Core{\n\t\tctx:            ctx,\n\t\tctxCancel:      ctxCancel,\n\t\tchAPIConfigSet: make(chan *conf.Conf),\n\t\tdone:           make(chan struct{}),\n\t}\n\n\ttempLogger := &logger.Logger{\n\t\tLevel:        logger.Warn,\n\t\tDestinations: []logger.Destination{logger.DestinationStdout},\n\t\tStructured:   false,\n\t\tFile:         \"\",\n\t\tSysLogPrefix: \"\",\n\t}\n\ttempLogger.Initialize() //nolint:errcheck\n\n\tconfPaths := append([]string(nil), defaultConfPaths...)\n\tif runtime.GOOS != \"windows\" {\n\t\tconfPaths = append(confPaths, defaultConfPathsNotWin...)\n\t}\n\n\tp.conf, p.confPath, err = conf.Load(cli.Confpath, confPaths, tempLogger)\n\tif err != nil {\n\t\tfmt.Printf(\"ERR: %s\\n\", err)\n\t\treturn nil, false\n\t}\n\n\terr = p.createResources(true)\n\tif err != nil {\n\t\tif p.logger != nil {\n\t\t\tp.Log(logger.Error, \"%s\", err)\n\t\t} else {\n\t\t\tfmt.Printf(\"ERR: %s\\n\", err)\n\t\t}\n\t\tp.closeResources(nil, false)\n\t\treturn nil, false\n\t}\n\n\tgo p.run()\n\n\treturn p, true\n}\n\n// Close closes Core and waits for all goroutines to return.\nfunc (p *Core) Close() {\n\tp.ctxCancel()\n\t<-p.done\n}\n\n// Wait waits for the Core to exit.\nfunc (p *Core) Wait() {\n\t<-p.done\n}\n\n// Log implements logger.Writer.\nfunc (p *Core) Log(level logger.Level, format string, args ...any) {\n\tp.logger.Log(level, format, args...)\n}\n\nfunc (p *Core) run() {\n\tdefer close(p.done)\n\n\tconfChanged := func() chan struct{} {\n\t\tif p.confWatcher != nil {\n\t\t\treturn p.confWatcher.Watch()\n\t\t}\n\t\treturn make(chan struct{})\n\t}()\n\n\tinterrupt := make(chan os.Signal, 1)\n\tsignal.Notify(interrupt, os.Interrupt)\n\tif runtime.GOOS == \"linux\" {\n\t\tsignal.Notify(interrupt, syscall.SIGTERM)\n\t}\n\nouter:\n\tfor {\n\t\tselect {\n\t\tcase <-confChanged:\n\t\t\tp.Log(logger.Info, \"reloading configuration (file changed)\")\n\n\t\t\tnewConf, _, err := conf.Load(p.confPath, nil, p.logger)\n\t\t\tif err != nil {\n\t\t\t\tp.Log(logger.Error, \"%s\", err)\n\t\t\t\tbreak outer\n\t\t\t}\n\n\t\t\terr = p.reloadConf(newConf, false)\n\t\t\tif err != nil {\n\t\t\t\tp.Log(logger.Error, \"%s\", err)\n\t\t\t\tbreak outer\n\t\t\t}\n\n\t\tcase newConf := <-p.chAPIConfigSet:\n\t\t\tp.Log(logger.Info, \"reloading configuration (API request)\")\n\n\t\t\terr := p.reloadConf(newConf, true)\n\t\t\tif err != nil {\n\t\t\t\tp.Log(logger.Error, \"%s\", err)\n\t\t\t\tbreak outer\n\t\t\t}\n\n\t\tcase <-interrupt:\n\t\t\tp.Log(logger.Info, \"shutting down gracefully\")\n\t\t\tbreak outer\n\n\t\tcase <-p.ctx.Done():\n\t\t\tbreak outer\n\t\t}\n\t}\n\n\tp.ctxCancel()\n\n\tp.closeResources(nil, false)\n}\n\nfunc (p *Core) createResources(initial bool) error {\n\tvar err error\n\n\tif p.logger == nil {\n\t\ti := &logger.Logger{\n\t\t\tLevel:        logger.Level(p.conf.LogLevel),\n\t\t\tDestinations: p.conf.LogDestinations.ToDestinations(),\n\t\t\tStructured:   p.conf.LogStructured,\n\t\t\tFile:         p.conf.LogFile,\n\t\t\tSysLogPrefix: p.conf.SysLogPrefix,\n\t\t}\n\t\terr = i.Initialize()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tp.logger = i\n\t}\n\n\tif initial {\n\t\tp.Log(logger.Info, \"MediaMTX %s, %s, %s\", string(version), runtime.GOOS, getArch())\n\n\t\tif p.confPath != \"\" {\n\t\t\ta, _ := filepath.Abs(p.confPath)\n\t\t\tp.Log(logger.Info, \"configuration loaded from %s\", a)\n\t\t} else {\n\t\t\tlist := make([]string, len(defaultConfPaths))\n\t\t\tfor i, pa := range defaultConfPaths {\n\t\t\t\ta, _ := filepath.Abs(pa)\n\t\t\t\tlist[i] = a\n\t\t\t}\n\n\t\t\tp.Log(logger.Warn,\n\t\t\t\t\"configuration file not found (looked in %s), using an empty configuration\",\n\t\t\t\tstrings.Join(list, \", \"))\n\t\t}\n\n\t\t// on Linux, try to raise the number of file descriptors that can be opened\n\t\t// to allow the maximum possible number of clients.\n\t\trlimit.Raise() //nolint:errcheck\n\n\t\tgin.SetMode(gin.ReleaseMode)\n\n\t\tp.externalCmdPool = &externalcmd.Pool{}\n\t\tp.externalCmdPool.Initialize()\n\t}\n\n\tif p.authManager == nil {\n\t\tp.authManager = &auth.Manager{\n\t\t\tMethod:             p.conf.AuthMethod,\n\t\t\tInternalUsers:      p.conf.AuthInternalUsers,\n\t\t\tHTTPAddress:        p.conf.AuthHTTPAddress,\n\t\t\tHTTPFingerprint:    p.conf.AuthHTTPFingerprint,\n\t\t\tHTTPExclude:        p.conf.AuthHTTPExclude,\n\t\t\tJWTJWKS:            p.conf.AuthJWTJWKS,\n\t\t\tJWTJWKSFingerprint: p.conf.AuthJWTJWKSFingerprint,\n\t\t\tJWTClaimKey:        p.conf.AuthJWTClaimKey,\n\t\t\tJWTExclude:         p.conf.AuthJWTExclude,\n\t\t\tJWTInHTTPQuery:     p.conf.AuthJWTInHTTPQuery,\n\t\t\tJWTIssuer:          p.conf.AuthJWTIssuer,\n\t\t\tJWTAudience:        p.conf.AuthJWTAudience,\n\t\t\tReadTimeout:        time.Duration(p.conf.ReadTimeout),\n\t\t}\n\t}\n\n\tif p.conf.Metrics &&\n\t\tp.metrics == nil {\n\t\ti := &metrics.Metrics{\n\t\t\tAddress:        p.conf.MetricsAddress,\n\t\t\tDumpPackets:    p.conf.DumpPackets,\n\t\t\tEncryption:     p.conf.MetricsEncryption,\n\t\t\tServerKey:      p.conf.MetricsServerKey,\n\t\t\tServerCert:     p.conf.MetricsServerCert,\n\t\t\tAllowOrigins:   p.conf.MetricsAllowOrigins,\n\t\t\tTrustedProxies: p.conf.MetricsTrustedProxies,\n\t\t\tReadTimeout:    p.conf.ReadTimeout,\n\t\t\tWriteTimeout:   p.conf.WriteTimeout,\n\t\t\tAuthManager:    p.authManager,\n\t\t\tParent:         p,\n\t\t}\n\t\terr = i.Initialize()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tp.metrics = i\n\t}\n\n\tif p.conf.PPROF &&\n\t\tp.pprof == nil {\n\t\ti := &pprof.PPROF{\n\t\t\tAddress:        p.conf.PPROFAddress,\n\t\t\tDumpPackets:    p.conf.DumpPackets,\n\t\t\tEncryption:     p.conf.PPROFEncryption,\n\t\t\tServerKey:      p.conf.PPROFServerKey,\n\t\t\tServerCert:     p.conf.PPROFServerCert,\n\t\t\tAllowOrigins:   p.conf.PPROFAllowOrigins,\n\t\t\tTrustedProxies: p.conf.PPROFTrustedProxies,\n\t\t\tReadTimeout:    p.conf.ReadTimeout,\n\t\t\tWriteTimeout:   p.conf.WriteTimeout,\n\t\t\tAuthManager:    p.authManager,\n\t\t\tParent:         p,\n\t\t}\n\t\terr = i.Initialize()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tp.pprof = i\n\t}\n\n\tif p.recordCleaner == nil &&\n\t\tatLeastOneRecordDeleteAfter(p.conf.Paths) {\n\t\tp.recordCleaner = &recordcleaner.Cleaner{\n\t\t\tPathConfs: p.conf.Paths,\n\t\t\tParent:    p,\n\t\t}\n\t\tp.recordCleaner.Initialize()\n\t}\n\n\tif p.conf.Playback &&\n\t\tp.playbackServer == nil {\n\t\ti := &playback.Server{\n\t\t\tAddress:        p.conf.PlaybackAddress,\n\t\t\tDumpPackets:    p.conf.DumpPackets,\n\t\t\tEncryption:     p.conf.PlaybackEncryption,\n\t\t\tServerKey:      p.conf.PlaybackServerKey,\n\t\t\tServerCert:     p.conf.PlaybackServerCert,\n\t\t\tAllowOrigins:   p.conf.PlaybackAllowOrigins,\n\t\t\tTrustedProxies: p.conf.PlaybackTrustedProxies,\n\t\t\tReadTimeout:    p.conf.ReadTimeout,\n\t\t\tWriteTimeout:   p.conf.WriteTimeout,\n\t\t\tPathConfs:      p.conf.Paths,\n\t\t\tAuthManager:    p.authManager,\n\t\t\tParent:         p,\n\t\t}\n\t\terr = i.Initialize()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tp.playbackServer = i\n\t}\n\n\tif p.pathManager == nil {\n\t\trtpMaxPayloadSize := getRTPMaxPayloadSize(p.conf.UDPMaxPayloadSize, p.conf.RTSPEncryption)\n\n\t\tp.pathManager = &pathManager{\n\t\t\tlogLevel:          p.conf.LogLevel,\n\t\t\tdumpPackets:       p.conf.DumpPackets,\n\t\t\trtspAddress:       p.conf.RTSPAddress,\n\t\t\treadTimeout:       p.conf.ReadTimeout,\n\t\t\twriteTimeout:      p.conf.WriteTimeout,\n\t\t\twriteQueueSize:    p.conf.WriteQueueSize,\n\t\t\tudpReadBufferSize: p.conf.UDPReadBufferSize,\n\t\t\trtpMaxPayloadSize: rtpMaxPayloadSize,\n\t\t\tpathConfs:         p.conf.Paths,\n\t\t\tauthManager:       p.authManager,\n\t\t\texternalCmdPool:   p.externalCmdPool,\n\t\t\tmetrics:           p.metrics,\n\t\t\tparent:            p,\n\t\t}\n\t\tp.pathManager.initialize()\n\t}\n\n\tif p.conf.RTSP &&\n\t\t(p.conf.RTSPEncryption == conf.EncryptionNo ||\n\t\t\tp.conf.RTSPEncryption == conf.EncryptionOptional) &&\n\t\tp.rtspServer == nil {\n\t\tudpReadBufferSize := p.conf.UDPReadBufferSize\n\t\tif p.conf.RTSPUDPReadBufferSize != nil {\n\t\t\tudpReadBufferSize = *p.conf.RTSPUDPReadBufferSize\n\t\t}\n\n\t\ti := &rtsp.Server{\n\t\t\tAddress:             p.conf.RTSPAddress,\n\t\t\tAuthMethods:         p.conf.RTSPAuthMethods.ToAuthMethods(),\n\t\t\tDumpPackets:         p.conf.DumpPackets,\n\t\t\tUDPReadBufferSize:   udpReadBufferSize,\n\t\t\tReadTimeout:         p.conf.ReadTimeout,\n\t\t\tWriteTimeout:        p.conf.WriteTimeout,\n\t\t\tWriteQueueSize:      p.conf.WriteQueueSize,\n\t\t\tRTSPTransports:      p.conf.RTSPTransports,\n\t\t\tRTPAddress:          p.conf.RTPAddress,\n\t\t\tRTCPAddress:         p.conf.RTCPAddress,\n\t\t\tMulticastIPRange:    p.conf.MulticastIPRange,\n\t\t\tMulticastRTPPort:    p.conf.MulticastRTPPort,\n\t\t\tMulticastRTCPPort:   p.conf.MulticastRTCPPort,\n\t\t\tIsTLS:               false,\n\t\t\tServerCert:          \"\",\n\t\t\tServerKey:           \"\",\n\t\t\tRTSPAddress:         p.conf.RTSPAddress,\n\t\t\tTransports:          p.conf.RTSPTransports,\n\t\t\tRunOnConnect:        p.conf.RunOnConnect,\n\t\t\tRunOnConnectRestart: p.conf.RunOnConnectRestart,\n\t\t\tRunOnDisconnect:     p.conf.RunOnDisconnect,\n\t\t\tExternalCmdPool:     p.externalCmdPool,\n\t\t\tMetrics:             p.metrics,\n\t\t\tPathManager:         p.pathManager,\n\t\t\tParent:              p,\n\t\t}\n\t\terr = i.Initialize()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tp.rtspServer = i\n\t}\n\n\tif p.conf.RTSP &&\n\t\t(p.conf.RTSPEncryption == conf.EncryptionStrict ||\n\t\t\tp.conf.RTSPEncryption == conf.EncryptionOptional) &&\n\t\tp.rtspsServer == nil {\n\t\tudpReadBufferSize := p.conf.UDPReadBufferSize\n\t\tif p.conf.RTSPUDPReadBufferSize != nil {\n\t\t\tudpReadBufferSize = *p.conf.RTSPUDPReadBufferSize\n\t\t}\n\n\t\ti := &rtsp.Server{\n\t\t\tAddress:             p.conf.RTSPSAddress,\n\t\t\tAuthMethods:         p.conf.RTSPAuthMethods.ToAuthMethods(),\n\t\t\tDumpPackets:         p.conf.DumpPackets,\n\t\t\tUDPReadBufferSize:   udpReadBufferSize,\n\t\t\tReadTimeout:         p.conf.ReadTimeout,\n\t\t\tWriteTimeout:        p.conf.WriteTimeout,\n\t\t\tWriteQueueSize:      p.conf.WriteQueueSize,\n\t\t\tRTSPTransports:      p.conf.RTSPTransports,\n\t\t\tRTPAddress:          p.conf.SRTPAddress,\n\t\t\tRTCPAddress:         p.conf.SRTCPAddress,\n\t\t\tMulticastIPRange:    p.conf.MulticastIPRange,\n\t\t\tMulticastRTPPort:    p.conf.MulticastSRTPPort,\n\t\t\tMulticastRTCPPort:   p.conf.MulticastSRTCPPort,\n\t\t\tIsTLS:               true,\n\t\t\tServerCert:          p.conf.RTSPServerCert,\n\t\t\tServerKey:           p.conf.RTSPServerKey,\n\t\t\tRTSPAddress:         p.conf.RTSPAddress,\n\t\t\tTransports:          p.conf.RTSPTransports,\n\t\t\tRunOnConnect:        p.conf.RunOnConnect,\n\t\t\tRunOnConnectRestart: p.conf.RunOnConnectRestart,\n\t\t\tRunOnDisconnect:     p.conf.RunOnDisconnect,\n\t\t\tExternalCmdPool:     p.externalCmdPool,\n\t\t\tMetrics:             p.metrics,\n\t\t\tPathManager:         p.pathManager,\n\t\t\tParent:              p,\n\t\t}\n\t\terr = i.Initialize()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tp.rtspsServer = i\n\t}\n\n\tif p.conf.RTMP &&\n\t\t(p.conf.RTMPEncryption == conf.EncryptionNo ||\n\t\t\tp.conf.RTMPEncryption == conf.EncryptionOptional) &&\n\t\tp.rtmpServer == nil {\n\t\ti := &rtmp.Server{\n\t\t\tAddress:             p.conf.RTMPAddress,\n\t\t\tDumpPackets:         p.conf.DumpPackets,\n\t\t\tReadTimeout:         p.conf.ReadTimeout,\n\t\t\tWriteTimeout:        p.conf.WriteTimeout,\n\t\t\tIsTLS:               false,\n\t\t\tServerCert:          \"\",\n\t\t\tServerKey:           \"\",\n\t\t\tRTSPAddress:         p.conf.RTSPAddress,\n\t\t\tRunOnConnect:        p.conf.RunOnConnect,\n\t\t\tRunOnConnectRestart: p.conf.RunOnConnectRestart,\n\t\t\tRunOnDisconnect:     p.conf.RunOnDisconnect,\n\t\t\tExternalCmdPool:     p.externalCmdPool,\n\t\t\tMetrics:             p.metrics,\n\t\t\tPathManager:         p.pathManager,\n\t\t\tParent:              p,\n\t\t}\n\t\terr = i.Initialize()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tp.rtmpServer = i\n\t}\n\n\tif p.conf.RTMP &&\n\t\t(p.conf.RTMPEncryption == conf.EncryptionStrict ||\n\t\t\tp.conf.RTMPEncryption == conf.EncryptionOptional) &&\n\t\tp.rtmpsServer == nil {\n\t\ti := &rtmp.Server{\n\t\t\tAddress:             p.conf.RTMPSAddress,\n\t\t\tReadTimeout:         p.conf.ReadTimeout,\n\t\t\tWriteTimeout:        p.conf.WriteTimeout,\n\t\t\tIsTLS:               true,\n\t\t\tServerCert:          p.conf.RTMPServerCert,\n\t\t\tServerKey:           p.conf.RTMPServerKey,\n\t\t\tDumpPackets:         p.conf.DumpPackets,\n\t\t\tRTSPAddress:         p.conf.RTSPAddress,\n\t\t\tRunOnConnect:        p.conf.RunOnConnect,\n\t\t\tRunOnConnectRestart: p.conf.RunOnConnectRestart,\n\t\t\tRunOnDisconnect:     p.conf.RunOnDisconnect,\n\t\t\tExternalCmdPool:     p.externalCmdPool,\n\t\t\tMetrics:             p.metrics,\n\t\t\tPathManager:         p.pathManager,\n\t\t\tParent:              p,\n\t\t}\n\t\terr = i.Initialize()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tp.rtmpsServer = i\n\t}\n\n\tif p.conf.HLS &&\n\t\tp.hlsServer == nil {\n\t\ti := &hls.Server{\n\t\t\tAddress:         p.conf.HLSAddress,\n\t\t\tDumpPackets:     p.conf.DumpPackets,\n\t\t\tEncryption:      p.conf.HLSEncryption,\n\t\t\tServerKey:       p.conf.HLSServerKey,\n\t\t\tServerCert:      p.conf.HLSServerCert,\n\t\t\tAllowOrigins:    p.conf.HLSAllowOrigins,\n\t\t\tTrustedProxies:  p.conf.HLSTrustedProxies,\n\t\t\tAlwaysRemux:     p.conf.HLSAlwaysRemux,\n\t\t\tVariant:         p.conf.HLSVariant,\n\t\t\tSegmentCount:    p.conf.HLSSegmentCount,\n\t\t\tSegmentDuration: p.conf.HLSSegmentDuration,\n\t\t\tPartDuration:    p.conf.HLSPartDuration,\n\t\t\tSegmentMaxSize:  p.conf.HLSSegmentMaxSize,\n\t\t\tDirectory:       p.conf.HLSDirectory,\n\t\t\tReadTimeout:     p.conf.ReadTimeout,\n\t\t\tWriteTimeout:    p.conf.WriteTimeout,\n\t\t\tMuxerCloseAfter: p.conf.HLSMuxerCloseAfter,\n\t\t\tMetrics:         p.metrics,\n\t\t\tPathManager:     p.pathManager,\n\t\t\tParent:          p,\n\t\t}\n\t\terr = i.Initialize()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tp.hlsServer = i\n\t}\n\n\tif p.conf.WebRTC &&\n\t\tp.webRTCServer == nil {\n\t\ti := &webrtc.Server{\n\t\t\tAddress:               p.conf.WebRTCAddress,\n\t\t\tDumpPackets:           p.conf.DumpPackets,\n\t\t\tEncryption:            p.conf.WebRTCEncryption,\n\t\t\tServerKey:             p.conf.WebRTCServerKey,\n\t\t\tServerCert:            p.conf.WebRTCServerCert,\n\t\t\tAllowOrigins:          p.conf.WebRTCAllowOrigins,\n\t\t\tTrustedProxies:        p.conf.WebRTCTrustedProxies,\n\t\t\tReadTimeout:           p.conf.ReadTimeout,\n\t\t\tWriteTimeout:          p.conf.WriteTimeout,\n\t\t\tUDPReadBufferSize:     p.conf.UDPReadBufferSize,\n\t\t\tLocalUDPAddress:       p.conf.WebRTCLocalUDPAddress,\n\t\t\tLocalTCPAddress:       p.conf.WebRTCLocalTCPAddress,\n\t\t\tIPsFromInterfaces:     p.conf.WebRTCIPsFromInterfaces,\n\t\t\tIPsFromInterfacesList: p.conf.WebRTCIPsFromInterfacesList,\n\t\t\tAdditionalHosts:       p.conf.WebRTCAdditionalHosts,\n\t\t\tICEServers:            p.conf.WebRTCICEServers2,\n\t\t\tSTUNGatherTimeout:     p.conf.WebRTCSTUNGatherTimeout,\n\t\t\tHandshakeTimeout:      p.conf.WebRTCHandshakeTimeout,\n\t\t\tTrackGatherTimeout:    p.conf.WebRTCTrackGatherTimeout,\n\t\t\tExternalCmdPool:       p.externalCmdPool,\n\t\t\tMetrics:               p.metrics,\n\t\t\tPathManager:           p.pathManager,\n\t\t\tParent:                p,\n\t\t}\n\t\terr = i.Initialize()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tp.webRTCServer = i\n\t}\n\n\tif p.conf.SRT &&\n\t\tp.srtServer == nil {\n\t\ti := &srt.Server{\n\t\t\tAddress:             p.conf.SRTAddress,\n\t\t\tRTSPAddress:         p.conf.RTSPAddress,\n\t\t\tReadTimeout:         p.conf.ReadTimeout,\n\t\t\tWriteTimeout:        p.conf.WriteTimeout,\n\t\t\tUDPMaxPayloadSize:   p.conf.UDPMaxPayloadSize,\n\t\t\tRunOnConnect:        p.conf.RunOnConnect,\n\t\t\tRunOnConnectRestart: p.conf.RunOnConnectRestart,\n\t\t\tRunOnDisconnect:     p.conf.RunOnDisconnect,\n\t\t\tExternalCmdPool:     p.externalCmdPool,\n\t\t\tMetrics:             p.metrics,\n\t\t\tPathManager:         p.pathManager,\n\t\t\tParent:              p,\n\t\t}\n\t\terr = i.Initialize()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tp.srtServer = i\n\t}\n\n\tif p.conf.API &&\n\t\tp.api == nil {\n\t\ti := &api.API{\n\t\t\tVersion:        string(version),\n\t\t\tStarted:        started,\n\t\t\tAddress:        p.conf.APIAddress,\n\t\t\tDumpPackets:    p.conf.DumpPackets,\n\t\t\tEncryption:     p.conf.APIEncryption,\n\t\t\tServerKey:      p.conf.APIServerKey,\n\t\t\tServerCert:     p.conf.APIServerCert,\n\t\t\tAllowOrigins:   p.conf.APIAllowOrigins,\n\t\t\tTrustedProxies: p.conf.APITrustedProxies,\n\t\t\tReadTimeout:    p.conf.ReadTimeout,\n\t\t\tWriteTimeout:   p.conf.WriteTimeout,\n\t\t\tConf:           p.conf,\n\t\t\tAuthManager:    p.authManager,\n\t\t\tPathManager:    p.pathManager,\n\t\t\tRTSPServer:     p.rtspServer,\n\t\t\tRTSPSServer:    p.rtspsServer,\n\t\t\tRTMPServer:     p.rtmpServer,\n\t\t\tRTMPSServer:    p.rtmpsServer,\n\t\t\tHLSServer:      p.hlsServer,\n\t\t\tWebRTCServer:   p.webRTCServer,\n\t\t\tSRTServer:      p.srtServer,\n\t\t\tParent:         p,\n\t\t}\n\t\terr = i.Initialize()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tp.api = i\n\t}\n\n\tif initial && p.confPath != \"\" {\n\t\tcf := &confwatcher.ConfWatcher{FilePath: p.confPath}\n\t\terr = cf.Initialize()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tp.confWatcher = cf\n\t}\n\n\treturn nil\n}\n\nfunc (p *Core) closeResources(newConf *conf.Conf, calledByAPI bool) {\n\tcloseLogger := newConf == nil ||\n\t\tnewConf.LogLevel != p.conf.LogLevel ||\n\t\t!reflect.DeepEqual(newConf.LogDestinations, p.conf.LogDestinations) ||\n\t\tnewConf.LogFile != p.conf.LogFile ||\n\t\tnewConf.SysLogPrefix != p.conf.SysLogPrefix ||\n\t\tnewConf.LogStructured != p.conf.LogStructured\n\n\tcloseAuthManager := newConf == nil ||\n\t\tnewConf.AuthMethod != p.conf.AuthMethod ||\n\t\tnewConf.AuthHTTPAddress != p.conf.AuthHTTPAddress ||\n\t\tnewConf.AuthHTTPFingerprint != p.conf.AuthHTTPFingerprint ||\n\t\t!reflect.DeepEqual(newConf.AuthHTTPExclude, p.conf.AuthHTTPExclude) ||\n\t\tnewConf.AuthJWTJWKS != p.conf.AuthJWTJWKS ||\n\t\tnewConf.AuthJWTJWKSFingerprint != p.conf.AuthJWTJWKSFingerprint ||\n\t\tnewConf.AuthJWTClaimKey != p.conf.AuthJWTClaimKey ||\n\t\t!reflect.DeepEqual(newConf.AuthJWTExclude, p.conf.AuthJWTExclude) ||\n\t\tnewConf.AuthJWTInHTTPQuery != p.conf.AuthJWTInHTTPQuery ||\n\t\tnewConf.AuthJWTIssuer != p.conf.AuthJWTIssuer ||\n\t\tnewConf.AuthJWTAudience != p.conf.AuthJWTAudience ||\n\t\tnewConf.ReadTimeout != p.conf.ReadTimeout\n\tif !closeAuthManager && !reflect.DeepEqual(newConf.AuthInternalUsers, p.conf.AuthInternalUsers) {\n\t\tp.authManager.ReloadInternalUsers(newConf.AuthInternalUsers)\n\t}\n\n\tcloseMetrics := newConf == nil ||\n\t\tnewConf.Metrics != p.conf.Metrics ||\n\t\tnewConf.MetricsAddress != p.conf.MetricsAddress ||\n\t\tnewConf.MetricsEncryption != p.conf.MetricsEncryption ||\n\t\tnewConf.MetricsServerKey != p.conf.MetricsServerKey ||\n\t\tnewConf.MetricsServerCert != p.conf.MetricsServerCert ||\n\t\t!slices.Equal(newConf.MetricsAllowOrigins, p.conf.MetricsAllowOrigins) ||\n\t\t!reflect.DeepEqual(newConf.MetricsTrustedProxies, p.conf.MetricsTrustedProxies) ||\n\t\tnewConf.ReadTimeout != p.conf.ReadTimeout ||\n\t\tnewConf.WriteTimeout != p.conf.WriteTimeout ||\n\t\tnewConf.DumpPackets != p.conf.DumpPackets ||\n\t\tcloseAuthManager ||\n\t\tcloseLogger\n\n\tclosePPROF := newConf == nil ||\n\t\tnewConf.PPROF != p.conf.PPROF ||\n\t\tnewConf.PPROFAddress != p.conf.PPROFAddress ||\n\t\tnewConf.PPROFEncryption != p.conf.PPROFEncryption ||\n\t\tnewConf.PPROFServerKey != p.conf.PPROFServerKey ||\n\t\tnewConf.PPROFServerCert != p.conf.PPROFServerCert ||\n\t\t!slices.Equal(newConf.PPROFAllowOrigins, p.conf.PPROFAllowOrigins) ||\n\t\t!reflect.DeepEqual(newConf.PPROFTrustedProxies, p.conf.PPROFTrustedProxies) ||\n\t\tnewConf.ReadTimeout != p.conf.ReadTimeout ||\n\t\tnewConf.WriteTimeout != p.conf.WriteTimeout ||\n\t\tnewConf.DumpPackets != p.conf.DumpPackets ||\n\t\tcloseAuthManager ||\n\t\tcloseLogger\n\n\tcloseRecorderCleaner := newConf == nil ||\n\t\tatLeastOneRecordDeleteAfter(newConf.Paths) != atLeastOneRecordDeleteAfter(p.conf.Paths) ||\n\t\tcloseLogger\n\tif !closeRecorderCleaner && p.recordCleaner != nil && !reflect.DeepEqual(newConf.Paths, p.conf.Paths) {\n\t\tp.recordCleaner.ReloadPathConfs(newConf.Paths)\n\t}\n\n\tclosePlaybackServer := newConf == nil ||\n\t\tnewConf.Playback != p.conf.Playback ||\n\t\tnewConf.PlaybackAddress != p.conf.PlaybackAddress ||\n\t\tnewConf.PlaybackEncryption != p.conf.PlaybackEncryption ||\n\t\tnewConf.PlaybackServerKey != p.conf.PlaybackServerKey ||\n\t\tnewConf.PlaybackServerCert != p.conf.PlaybackServerCert ||\n\t\t!slices.Equal(newConf.PlaybackAllowOrigins, p.conf.PlaybackAllowOrigins) ||\n\t\t!reflect.DeepEqual(newConf.PlaybackTrustedProxies, p.conf.PlaybackTrustedProxies) ||\n\t\tnewConf.ReadTimeout != p.conf.ReadTimeout ||\n\t\tnewConf.WriteTimeout != p.conf.WriteTimeout ||\n\t\tnewConf.DumpPackets != p.conf.DumpPackets ||\n\t\tcloseAuthManager ||\n\t\tcloseLogger\n\tif !closePlaybackServer && p.playbackServer != nil && !reflect.DeepEqual(newConf.Paths, p.conf.Paths) {\n\t\tp.playbackServer.ReloadPathConfs(newConf.Paths)\n\t}\n\n\tclosePathManager := newConf == nil ||\n\t\tnewConf.LogLevel != p.conf.LogLevel ||\n\t\tnewConf.DumpPackets != p.conf.DumpPackets ||\n\t\tnewConf.RTSPAddress != p.conf.RTSPAddress ||\n\t\tnewConf.ReadTimeout != p.conf.ReadTimeout ||\n\t\tnewConf.WriteTimeout != p.conf.WriteTimeout ||\n\t\tnewConf.WriteQueueSize != p.conf.WriteQueueSize ||\n\t\tnewConf.UDPReadBufferSize != p.conf.UDPReadBufferSize ||\n\t\tnewConf.UDPMaxPayloadSize != p.conf.UDPMaxPayloadSize ||\n\t\tnewConf.RTSPEncryption != p.conf.RTSPEncryption ||\n\t\tcloseMetrics ||\n\t\tcloseAuthManager ||\n\t\tcloseLogger\n\tif !closePathManager && !reflect.DeepEqual(newConf.Paths, p.conf.Paths) {\n\t\tp.pathManager.ReloadPathConfs(newConf.Paths)\n\t}\n\n\tcloseRTSPServer := newConf == nil ||\n\t\tnewConf.RTSP != p.conf.RTSP ||\n\t\tnewConf.RTSPEncryption != p.conf.RTSPEncryption ||\n\t\tnewConf.RTSPAddress != p.conf.RTSPAddress ||\n\t\t!reflect.DeepEqual(newConf.RTSPAuthMethods, p.conf.RTSPAuthMethods) ||\n\t\tnewConf.RTSPUDPReadBufferSize != p.conf.RTSPUDPReadBufferSize ||\n\t\tnewConf.DumpPackets != p.conf.DumpPackets ||\n\t\tnewConf.UDPReadBufferSize != p.conf.UDPReadBufferSize ||\n\t\tnewConf.ReadTimeout != p.conf.ReadTimeout ||\n\t\tnewConf.WriteTimeout != p.conf.WriteTimeout ||\n\t\tnewConf.WriteQueueSize != p.conf.WriteQueueSize ||\n\t\tnewConf.RTPAddress != p.conf.RTPAddress ||\n\t\tnewConf.RTCPAddress != p.conf.RTCPAddress ||\n\t\tnewConf.MulticastIPRange != p.conf.MulticastIPRange ||\n\t\tnewConf.MulticastRTPPort != p.conf.MulticastRTPPort ||\n\t\tnewConf.MulticastRTCPPort != p.conf.MulticastRTCPPort ||\n\t\t!reflect.DeepEqual(newConf.RTSPTransports, p.conf.RTSPTransports) ||\n\t\tnewConf.RunOnConnect != p.conf.RunOnConnect ||\n\t\tnewConf.RunOnConnectRestart != p.conf.RunOnConnectRestart ||\n\t\tnewConf.RunOnDisconnect != p.conf.RunOnDisconnect ||\n\t\tcloseMetrics ||\n\t\tclosePathManager ||\n\t\tcloseLogger\n\n\tcloseRTSPSServer := newConf == nil ||\n\t\tnewConf.RTSP != p.conf.RTSP ||\n\t\tnewConf.RTSPEncryption != p.conf.RTSPEncryption ||\n\t\tnewConf.RTSPSAddress != p.conf.RTSPSAddress ||\n\t\t!reflect.DeepEqual(newConf.RTSPAuthMethods, p.conf.RTSPAuthMethods) ||\n\t\tnewConf.RTSPUDPReadBufferSize != p.conf.RTSPUDPReadBufferSize ||\n\t\tnewConf.DumpPackets != p.conf.DumpPackets ||\n\t\tnewConf.UDPReadBufferSize != p.conf.UDPReadBufferSize ||\n\t\tnewConf.ReadTimeout != p.conf.ReadTimeout ||\n\t\tnewConf.WriteTimeout != p.conf.WriteTimeout ||\n\t\tnewConf.WriteQueueSize != p.conf.WriteQueueSize ||\n\t\tnewConf.RTSPServerCert != p.conf.RTSPServerCert ||\n\t\tnewConf.RTSPServerKey != p.conf.RTSPServerKey ||\n\t\tnewConf.RTSPAddress != p.conf.RTSPAddress ||\n\t\t!reflect.DeepEqual(newConf.RTSPTransports, p.conf.RTSPTransports) ||\n\t\tnewConf.RunOnConnect != p.conf.RunOnConnect ||\n\t\tnewConf.RunOnConnectRestart != p.conf.RunOnConnectRestart ||\n\t\tnewConf.RunOnDisconnect != p.conf.RunOnDisconnect ||\n\t\tcloseMetrics ||\n\t\tclosePathManager ||\n\t\tcloseLogger\n\n\tcloseRTMPServer := newConf == nil ||\n\t\tnewConf.RTMP != p.conf.RTMP ||\n\t\tnewConf.RTMPEncryption != p.conf.RTMPEncryption ||\n\t\tnewConf.RTMPAddress != p.conf.RTMPAddress ||\n\t\tnewConf.DumpPackets != p.conf.DumpPackets ||\n\t\tnewConf.ReadTimeout != p.conf.ReadTimeout ||\n\t\tnewConf.WriteTimeout != p.conf.WriteTimeout ||\n\t\tnewConf.RTSPAddress != p.conf.RTSPAddress ||\n\t\tnewConf.RunOnConnect != p.conf.RunOnConnect ||\n\t\tnewConf.RunOnConnectRestart != p.conf.RunOnConnectRestart ||\n\t\tnewConf.RunOnDisconnect != p.conf.RunOnDisconnect ||\n\t\tcloseMetrics ||\n\t\tclosePathManager ||\n\t\tcloseLogger\n\n\tcloseRTMPSServer := newConf == nil ||\n\t\tnewConf.RTMP != p.conf.RTMP ||\n\t\tnewConf.RTMPEncryption != p.conf.RTMPEncryption ||\n\t\tnewConf.RTMPSAddress != p.conf.RTMPSAddress ||\n\t\tnewConf.DumpPackets != p.conf.DumpPackets ||\n\t\tnewConf.ReadTimeout != p.conf.ReadTimeout ||\n\t\tnewConf.WriteTimeout != p.conf.WriteTimeout ||\n\t\tnewConf.RTMPServerCert != p.conf.RTMPServerCert ||\n\t\tnewConf.RTMPServerKey != p.conf.RTMPServerKey ||\n\t\tnewConf.RTSPAddress != p.conf.RTSPAddress ||\n\t\tnewConf.RunOnConnect != p.conf.RunOnConnect ||\n\t\tnewConf.RunOnConnectRestart != p.conf.RunOnConnectRestart ||\n\t\tnewConf.RunOnDisconnect != p.conf.RunOnDisconnect ||\n\t\tcloseMetrics ||\n\t\tclosePathManager ||\n\t\tcloseLogger\n\n\tcloseHLSServer := newConf == nil ||\n\t\tnewConf.HLS != p.conf.HLS ||\n\t\tnewConf.HLSAddress != p.conf.HLSAddress ||\n\t\tnewConf.HLSEncryption != p.conf.HLSEncryption ||\n\t\tnewConf.HLSServerKey != p.conf.HLSServerKey ||\n\t\tnewConf.HLSServerCert != p.conf.HLSServerCert ||\n\t\t!slices.Equal(newConf.HLSAllowOrigins, p.conf.HLSAllowOrigins) ||\n\t\t!reflect.DeepEqual(newConf.HLSTrustedProxies, p.conf.HLSTrustedProxies) ||\n\t\tnewConf.HLSAlwaysRemux != p.conf.HLSAlwaysRemux ||\n\t\tnewConf.HLSVariant != p.conf.HLSVariant ||\n\t\tnewConf.HLSSegmentCount != p.conf.HLSSegmentCount ||\n\t\tnewConf.HLSSegmentDuration != p.conf.HLSSegmentDuration ||\n\t\tnewConf.HLSPartDuration != p.conf.HLSPartDuration ||\n\t\tnewConf.HLSSegmentMaxSize != p.conf.HLSSegmentMaxSize ||\n\t\tnewConf.HLSDirectory != p.conf.HLSDirectory ||\n\t\tnewConf.ReadTimeout != p.conf.ReadTimeout ||\n\t\tnewConf.WriteTimeout != p.conf.WriteTimeout ||\n\t\tnewConf.HLSMuxerCloseAfter != p.conf.HLSMuxerCloseAfter ||\n\t\tnewConf.DumpPackets != p.conf.DumpPackets ||\n\t\tclosePathManager ||\n\t\tcloseMetrics ||\n\t\tcloseLogger\n\n\tcloseWebRTCServer := newConf == nil ||\n\t\tnewConf.WebRTC != p.conf.WebRTC ||\n\t\tnewConf.WebRTCAddress != p.conf.WebRTCAddress ||\n\t\tnewConf.WebRTCEncryption != p.conf.WebRTCEncryption ||\n\t\tnewConf.WebRTCServerKey != p.conf.WebRTCServerKey ||\n\t\tnewConf.WebRTCServerCert != p.conf.WebRTCServerCert ||\n\t\t!slices.Equal(newConf.WebRTCAllowOrigins, p.conf.WebRTCAllowOrigins) ||\n\t\t!reflect.DeepEqual(newConf.WebRTCTrustedProxies, p.conf.WebRTCTrustedProxies) ||\n\t\tnewConf.ReadTimeout != p.conf.ReadTimeout ||\n\t\tnewConf.WriteTimeout != p.conf.WriteTimeout ||\n\t\tnewConf.UDPReadBufferSize != p.conf.UDPReadBufferSize ||\n\t\tnewConf.WebRTCLocalUDPAddress != p.conf.WebRTCLocalUDPAddress ||\n\t\tnewConf.WebRTCLocalTCPAddress != p.conf.WebRTCLocalTCPAddress ||\n\t\tnewConf.WebRTCIPsFromInterfaces != p.conf.WebRTCIPsFromInterfaces ||\n\t\t!reflect.DeepEqual(newConf.WebRTCIPsFromInterfacesList, p.conf.WebRTCIPsFromInterfacesList) ||\n\t\t!reflect.DeepEqual(newConf.WebRTCAdditionalHosts, p.conf.WebRTCAdditionalHosts) ||\n\t\t!reflect.DeepEqual(newConf.WebRTCICEServers2, p.conf.WebRTCICEServers2) ||\n\t\tnewConf.WebRTCSTUNGatherTimeout != p.conf.WebRTCSTUNGatherTimeout ||\n\t\tnewConf.WebRTCHandshakeTimeout != p.conf.WebRTCHandshakeTimeout ||\n\t\tnewConf.WebRTCTrackGatherTimeout != p.conf.WebRTCTrackGatherTimeout ||\n\t\tnewConf.DumpPackets != p.conf.DumpPackets ||\n\t\tcloseMetrics ||\n\t\tclosePathManager ||\n\t\tcloseLogger\n\n\tcloseSRTServer := newConf == nil ||\n\t\tnewConf.SRT != p.conf.SRT ||\n\t\tnewConf.SRTAddress != p.conf.SRTAddress ||\n\t\tnewConf.RTSPAddress != p.conf.RTSPAddress ||\n\t\tnewConf.ReadTimeout != p.conf.ReadTimeout ||\n\t\tnewConf.WriteTimeout != p.conf.WriteTimeout ||\n\t\tnewConf.UDPMaxPayloadSize != p.conf.UDPMaxPayloadSize ||\n\t\tnewConf.RunOnConnect != p.conf.RunOnConnect ||\n\t\tnewConf.RunOnConnectRestart != p.conf.RunOnConnectRestart ||\n\t\tnewConf.RunOnDisconnect != p.conf.RunOnDisconnect ||\n\t\tclosePathManager ||\n\t\tcloseLogger\n\n\tcloseAPI := newConf == nil ||\n\t\tnewConf.API != p.conf.API ||\n\t\tnewConf.APIAddress != p.conf.APIAddress ||\n\t\tnewConf.APIEncryption != p.conf.APIEncryption ||\n\t\tnewConf.APIServerKey != p.conf.APIServerKey ||\n\t\tnewConf.APIServerCert != p.conf.APIServerCert ||\n\t\t!slices.Equal(newConf.APIAllowOrigins, p.conf.APIAllowOrigins) ||\n\t\t!reflect.DeepEqual(newConf.APITrustedProxies, p.conf.APITrustedProxies) ||\n\t\tnewConf.ReadTimeout != p.conf.ReadTimeout ||\n\t\tnewConf.WriteTimeout != p.conf.WriteTimeout ||\n\t\tnewConf.DumpPackets != p.conf.DumpPackets ||\n\t\tcloseAuthManager ||\n\t\tclosePathManager ||\n\t\tcloseRTSPServer ||\n\t\tcloseRTSPSServer ||\n\t\tcloseRTMPServer ||\n\t\tcloseHLSServer ||\n\t\tcloseWebRTCServer ||\n\t\tcloseSRTServer ||\n\t\tcloseLogger\n\n\tif newConf == nil && p.confWatcher != nil {\n\t\tp.confWatcher.Close()\n\t\tp.confWatcher = nil\n\t}\n\n\tif p.api != nil {\n\t\tif closeAPI {\n\t\t\tp.api.Close()\n\t\t\tp.api = nil\n\t\t} else if !calledByAPI { // avoid a loop\n\t\t\tp.api.ReloadConf(newConf)\n\t\t}\n\t}\n\n\tif closeSRTServer && p.srtServer != nil {\n\t\tp.srtServer.Close()\n\t\tp.srtServer = nil\n\t}\n\n\tif closeWebRTCServer && p.webRTCServer != nil {\n\t\tp.webRTCServer.Close()\n\t\tp.webRTCServer = nil\n\t}\n\n\tif closeHLSServer && p.hlsServer != nil {\n\t\tp.hlsServer.Close()\n\t\tp.hlsServer = nil\n\t}\n\n\tif closeRTMPSServer && p.rtmpsServer != nil {\n\t\tp.rtmpsServer.Close()\n\t\tp.rtmpsServer = nil\n\t}\n\n\tif closeRTMPServer && p.rtmpServer != nil {\n\t\tp.rtmpServer.Close()\n\t\tp.rtmpServer = nil\n\t}\n\n\tif closeRTSPSServer && p.rtspsServer != nil {\n\t\tp.rtspsServer.Close()\n\t\tp.rtspsServer = nil\n\t}\n\n\tif closeRTSPServer && p.rtspServer != nil {\n\t\tp.rtspServer.Close()\n\t\tp.rtspServer = nil\n\t}\n\n\tif closePathManager && p.pathManager != nil {\n\t\tp.pathManager.close()\n\t\tp.pathManager = nil\n\t}\n\n\tif closePlaybackServer && p.playbackServer != nil {\n\t\tp.playbackServer.Close()\n\t\tp.playbackServer = nil\n\t}\n\n\tif closeRecorderCleaner && p.recordCleaner != nil {\n\t\tp.recordCleaner.Close()\n\t\tp.recordCleaner = nil\n\t}\n\n\tif closePPROF && p.pprof != nil {\n\t\tp.pprof.Close()\n\t\tp.pprof = nil\n\t}\n\n\tif closeMetrics && p.metrics != nil {\n\t\tp.metrics.Close()\n\t\tp.metrics = nil\n\t}\n\n\tif closeAuthManager && p.authManager != nil {\n\t\tp.authManager = nil\n\t}\n\n\tif newConf == nil && p.externalCmdPool != nil {\n\t\tp.Log(logger.Info, \"waiting for running hooks\")\n\t\tp.externalCmdPool.Close()\n\t}\n\n\tif closeLogger && p.logger != nil {\n\t\tif newConf == nil {\n\t\t\tp.logger.Close()\n\t\t}\n\t\tp.logger = nil\n\t}\n}\n\nfunc (p *Core) reloadConf(newConf *conf.Conf, calledByAPI bool) error {\n\toldLogger := p.logger\n\n\tp.closeResources(newConf, calledByAPI)\n\n\tp.conf = newConf\n\n\terr := p.createResources(false)\n\tif err != nil {\n\t\tp.logger = oldLogger\n\t\treturn err\n\t}\n\n\tif p.logger != oldLogger {\n\t\toldLogger.Close()\n\t}\n\n\treturn nil\n}\n\n// APIConfigSet is called by api.\nfunc (p *Core) APIConfigSet(conf *conf.Conf) {\n\tselect {\n\tcase p.chAPIConfigSet <- conf:\n\tcase <-p.ctx.Done():\n\t}\n}\n"
  },
  {
    "path": "internal/core/core_test.go",
    "content": "package core\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc newInstance(conf string) (*Core, bool) {\n\tif conf == \"\" {\n\t\treturn New([]string{})\n\t}\n\n\ttmpf, err := test.CreateTempFile([]byte(conf))\n\tif err != nil {\n\t\treturn nil, false\n\t}\n\tdefer os.Remove(tmpf)\n\n\treturn New([]string{tmpf})\n}\n\nfunc TestCoreErrors(t *testing.T) {\n\tfor _, ca := range []struct {\n\t\tname string\n\t\tconf string\n\t}{\n\t\t{\n\t\t\t\"logger\",\n\t\t\t\"logDestinations: [file]\\n\" +\n\t\t\t\t\"logFile: /nonexisting/nonexist\\n\" +\n\t\t\t\t\"sysLogPrefix: /mediamtx\\n\",\n\t\t},\n\t\t{\n\t\t\t\"metrics\",\n\t\t\t\"metrics: yes\\n\" +\n\t\t\t\t\"metricsAddress: invalid\\n\",\n\t\t},\n\t\t{\n\t\t\t\"pprof\",\n\t\t\t\"pprof: yes\\n\" +\n\t\t\t\t\"pprofAddress: invalid\\n\",\n\t\t},\n\t\t{\n\t\t\t\"playback\",\n\t\t\t\"playback: yes\\n\" +\n\t\t\t\t\"playbackAddress: invalid\\n\",\n\t\t},\n\t\t{\n\t\t\t\"rtsp\",\n\t\t\t\"rtspAddress: invalid\\n\",\n\t\t},\n\t\t{\n\t\t\t\"rtsps\",\n\t\t\t\"rtspEncryption: strict\\n\" +\n\t\t\t\t\"rtspAddress: invalid\\n\",\n\t\t},\n\t\t{\n\t\t\t\"rtmp\",\n\t\t\t\"rtmpAddress: invalid\\n\",\n\t\t},\n\t\t{\n\t\t\t\"rtmps\",\n\t\t\t\"rtmpEncryption: strict\\n\" +\n\t\t\t\t\"rtmpAddress: invalid\\n\",\n\t\t},\n\t\t{\n\t\t\t\"hls\",\n\t\t\t\"hlsAddress: invalid\\n\",\n\t\t},\n\t\t{\n\t\t\t\"webrtc\",\n\t\t\t\"webrtcAddress: invalid\\n\",\n\t\t},\n\t\t{\n\t\t\t\"srt\",\n\t\t\t\"srtAddress: invalid\\n\",\n\t\t},\n\t\t{\n\t\t\t\"api\",\n\t\t\t\"api: yes\\n\" +\n\t\t\t\t\"apiAddress: invalid\\n\",\n\t\t},\n\t} {\n\t\tt.Run(ca.name, func(t *testing.T) {\n\t\t\t_, ok := newInstance(ca.conf)\n\t\t\trequire.Equal(t, false, ok)\n\t\t})\n\t}\n}\n\nfunc TestCoreHotReloading(t *testing.T) {\n\tconfPath := filepath.Join(os.TempDir(), \"rtsp-conf\")\n\n\terr := os.WriteFile(confPath, []byte(\"paths:\\n\"+\n\t\t\"  test1:\\n\"+\n\t\t\"    publishUser: myuser\\n\"+\n\t\t\"    publishPass: mypass\\n\"),\n\t\t0o644)\n\trequire.NoError(t, err)\n\tdefer os.Remove(confPath)\n\n\tp, ok := New([]string{confPath})\n\trequire.Equal(t, true, ok)\n\tdefer p.Close()\n\n\tfunc() {\n\t\tc := gortsplib.Client{}\n\t\terr = c.StartRecording(\"rtsp://localhost:8554/test1\",\n\t\t\t&description.Session{Medias: []*description.Media{test.UniqueMediaH264()}})\n\t\trequire.EqualError(t, err, \"bad status code: 401 (Unauthorized)\")\n\t}()\n\n\terr = os.WriteFile(confPath, []byte(\"paths:\\n\"+\n\t\t\"  test1:\\n\"),\n\t\t0o644)\n\trequire.NoError(t, err)\n\n\ttime.Sleep(1 * time.Second)\n\n\tfunc() {\n\t\tconn := gortsplib.Client{}\n\t\terr = conn.StartRecording(\"rtsp://localhost:8554/test1\",\n\t\t\t&description.Session{Medias: []*description.Media{test.UniqueMediaH264()}})\n\t\trequire.NoError(t, err)\n\t\tdefer conn.Close()\n\t}()\n}\n\nfunc TestCoreHotReloadingAndLoggerError(t *testing.T) {\n\tconfPath := filepath.Join(os.TempDir(), \"rtsp-conf\")\n\n\terr := os.WriteFile(confPath, []byte(\"\"),\n\t\t0o644)\n\trequire.NoError(t, err)\n\tdefer os.Remove(confPath)\n\n\tp, ok := New([]string{confPath})\n\trequire.Equal(t, true, ok)\n\tdefer p.Close()\n\n\terr = os.WriteFile(confPath, []byte(\"logDestinations: [file]\\n\"+\n\t\t\"logFile: /nonexisting/nonexist\\n\"),\n\t\t0o644)\n\trequire.NoError(t, err)\n\n\tp.Wait()\n}\n"
  },
  {
    "path": "internal/core/metrics_test.go",
    "content": "package core\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"sync\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortmplib\"\n\trtmpcodecs \"github.com/bluenviron/gortmplib/pkg/codecs\"\n\t\"github.com/bluenviron/gortsplib/v5\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts\"\n\ttscodecs \"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts/codecs\"\n\tsrt \"github.com/datarhei/gosrt\"\n\t\"github.com/pion/rtp\"\n\tpwebrtc \"github.com/pion/webrtc/v4\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/bluenviron/mediamtx/internal/protocols/webrtc\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/whip\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n)\n\nfunc httpPullFile(t *testing.T, hc *http.Client, u string) []byte {\n\tres, err := hc.Get(u)\n\trequire.NoError(t, err)\n\tdefer res.Body.Close()\n\n\tif res.StatusCode != http.StatusOK {\n\t\tt.Errorf(\"bad status code: %v\", res.StatusCode)\n\t}\n\n\tbyts, err := io.ReadAll(res.Body)\n\trequire.NoError(t, err)\n\n\treturn byts\n}\n\nfunc TestMetrics(t *testing.T) {\n\tserverCertFpath, err := test.CreateTempFile(test.TLSCertPub)\n\trequire.NoError(t, err)\n\tdefer os.Remove(serverCertFpath)\n\n\tserverKeyFpath, err := test.CreateTempFile(test.TLSCertKey)\n\trequire.NoError(t, err)\n\tdefer os.Remove(serverKeyFpath)\n\n\tp, ok := newInstance(\"api: yes\\n\" +\n\t\t\"hlsAlwaysRemux: yes\\n\" +\n\t\t\"metrics: yes\\n\" +\n\t\t\"webrtcServerCert: \" + serverCertFpath + \"\\n\" +\n\t\t\"webrtcServerKey: \" + serverKeyFpath + \"\\n\" +\n\t\t\"rtspEncryption: optional\\n\" +\n\t\t\"rtspServerCert: \" + serverCertFpath + \"\\n\" +\n\t\t\"rtspServerKey: \" + serverKeyFpath + \"\\n\" +\n\t\t\"rtmpEncryption: optional\\n\" +\n\t\t\"rtmpServerCert: \" + serverCertFpath + \"\\n\" +\n\t\t\"rtmpServerKey: \" + serverKeyFpath + \"\\n\" +\n\t\t\"paths:\\n\" +\n\t\t\"  all_others:\\n\")\n\trequire.Equal(t, true, ok)\n\tdefer p.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\tt.Run(\"initial\", func(t *testing.T) {\n\t\tbo := httpPullFile(t, hc, \"http://localhost:9998/metrics\")\n\n\t\trequire.Equal(t, `paths 0\npaths_inbound_bytes 0\npaths_outbound_bytes 0\npaths_inbound_frames_in_error 0\npaths_bytes_received 0\npaths_bytes_sent 0\npaths_readers 0\nhls_muxers 0\nhls_muxers_outbound_bytes 0\nhls_muxers_outbound_frames_discarded 0\nhls_muxers_bytes_sent 0\nrtsp_conns 0\nrtsp_conns_inbound_bytes 0\nrtsp_conns_outbound_bytes 0\nrtsp_conns_bytes_received 0\nrtsp_conns_bytes_sent 0\nrtsp_sessions 0\nrtsp_sessions_inbound_bytes 0\nrtsp_sessions_inbound_rtp_packets 0\nrtsp_sessions_inbound_rtp_packets_lost 0\nrtsp_sessions_inbound_rtp_packets_in_error 0\nrtsp_sessions_inbound_rtp_packets_jitter 0\nrtsp_sessions_inbound_rtcp_packets 0\nrtsp_sessions_inbound_rtcp_packets_in_error 0\nrtsp_sessions_outbound_bytes 0\nrtsp_sessions_outbound_rtp_packets 0\nrtsp_sessions_outbound_rtp_packets_reported_lost 0\nrtsp_sessions_outbound_rtp_packets_discarded 0\nrtsp_sessions_outbound_rtcp_packets 0\nrtsp_sessions_bytes_received 0\nrtsp_sessions_bytes_sent 0\nrtsp_sessions_rtp_packets_received 0\nrtsp_sessions_rtp_packets_sent 0\nrtsp_sessions_rtp_packets_lost 0\nrtsp_sessions_rtp_packets_in_error 0\nrtsp_sessions_rtp_packets_jitter 0\nrtsp_sessions_rtcp_packets_received 0\nrtsp_sessions_rtcp_packets_sent 0\nrtsp_sessions_rtcp_packets_in_error 0\nrtsps_conns 0\nrtsps_conns_inbound_bytes 0\nrtsps_conns_outbound_bytes 0\nrtsps_conns_bytes_received 0\nrtsps_conns_bytes_sent 0\nrtsps_sessions 0\nrtsps_sessions_inbound_bytes 0\nrtsps_sessions_inbound_rtp_packets 0\nrtsps_sessions_inbound_rtp_packets_lost 0\nrtsps_sessions_inbound_rtp_packets_in_error 0\nrtsps_sessions_inbound_rtp_packets_jitter 0\nrtsps_sessions_inbound_rtcp_packets 0\nrtsps_sessions_inbound_rtcp_packets_in_error 0\nrtsps_sessions_outbound_bytes 0\nrtsps_sessions_outbound_rtp_packets 0\nrtsps_sessions_outbound_rtp_packets_reported_lost 0\nrtsps_sessions_outbound_rtp_packets_discarded 0\nrtsps_sessions_outbound_rtcp_packets 0\nrtsps_sessions_bytes_received 0\nrtsps_sessions_bytes_sent 0\nrtsps_sessions_rtp_packets_received 0\nrtsps_sessions_rtp_packets_sent 0\nrtsps_sessions_rtp_packets_lost 0\nrtsps_sessions_rtp_packets_in_error 0\nrtsps_sessions_rtp_packets_jitter 0\nrtsps_sessions_rtcp_packets_received 0\nrtsps_sessions_rtcp_packets_sent 0\nrtsps_sessions_rtcp_packets_in_error 0\nrtmp_conns 0\nrtmp_conns_inbound_bytes 0\nrtmp_conns_outbound_bytes 0\nrtmp_conns_outbound_frames_discarded 0\nrtmp_conns_bytes_received 0\nrtmp_conns_bytes_sent 0\nrtmps_conns 0\nrtmps_conns_inbound_bytes 0\nrtmps_conns_outbound_bytes 0\nrtmps_conns_outbound_frames_discarded 0\nrtmps_conns_bytes_received 0\nrtmps_conns_bytes_sent 0\nsrt_conns 0\nsrt_conns_packets_sent 0\nsrt_conns_packets_received 0\nsrt_conns_packets_sent_unique 0\nsrt_conns_packets_received_unique 0\nsrt_conns_packets_send_loss 0\nsrt_conns_packets_received_loss 0\nsrt_conns_packets_retrans 0\nsrt_conns_packets_received_retrans 0\nsrt_conns_packets_sent_ack 0\nsrt_conns_packets_received_ack 0\nsrt_conns_packets_sent_nak 0\nsrt_conns_packets_received_nak 0\nsrt_conns_packets_sent_km 0\nsrt_conns_packets_received_km 0\nsrt_conns_us_snd_duration 0\nsrt_conns_packets_received_belated 0\nsrt_conns_packets_send_drop 0\nsrt_conns_packets_received_drop 0\nsrt_conns_packets_received_undecrypt 0\nsrt_conns_bytes_sent 0\nsrt_conns_bytes_received 0\nsrt_conns_bytes_sent_unique 0\nsrt_conns_bytes_received_unique 0\nsrt_conns_bytes_received_loss 0\nsrt_conns_bytes_retrans 0\nsrt_conns_bytes_received_retrans 0\nsrt_conns_bytes_received_belated 0\nsrt_conns_bytes_send_drop 0\nsrt_conns_bytes_received_drop 0\nsrt_conns_bytes_received_undecrypt 0\nsrt_conns_us_packets_send_period 0\nsrt_conns_packets_flow_window 0\nsrt_conns_packets_flight_size 0\nsrt_conns_ms_rtt 0\nsrt_conns_mbps_send_rate 0\nsrt_conns_mbps_receive_rate 0\nsrt_conns_mbps_link_capacity 0\nsrt_conns_bytes_avail_send_buf 0\nsrt_conns_bytes_avail_receive_buf 0\nsrt_conns_mbps_max_bw 0\nsrt_conns_bytes_mss 0\nsrt_conns_packets_send_buf 0\nsrt_conns_bytes_send_buf 0\nsrt_conns_ms_send_buf 0\nsrt_conns_ms_send_tsb_pd_delay 0\nsrt_conns_packets_receive_buf 0\nsrt_conns_bytes_receive_buf 0\nsrt_conns_ms_receive_buf 0\nsrt_conns_ms_receive_tsb_pd_delay 0\nsrt_conns_packets_reorder_tolerance 0\nsrt_conns_packets_received_avg_belated_time 0\nsrt_conns_packets_send_loss_rate 0\nsrt_conns_packets_received_loss_rate 0\nsrt_conns_outbound_frames_discarded 0\nwebrtc_sessions 0\nwebrtc_sessions_inbound_bytes 0\nwebrtc_sessions_inbound_rtp_packets 0\nwebrtc_sessions_inbound_rtp_packets_lost 0\nwebrtc_sessions_inbound_rtp_packets_jitter 0\nwebrtc_sessions_inbound_rtcp_packets 0\nwebrtc_sessions_outbound_bytes 0\nwebrtc_sessions_outbound_rtp_packets 0\nwebrtc_sessions_outbound_rtcp_packets 0\nwebrtc_sessions_outbound_frames_discarded 0\nwebrtc_sessions_bytes_received 0\nwebrtc_sessions_bytes_sent 0\nwebrtc_sessions_rtp_packets_received 0\nwebrtc_sessions_rtp_packets_sent 0\nwebrtc_sessions_rtp_packets_lost 0\nwebrtc_sessions_rtp_packets_jitter 0\nwebrtc_sessions_rtcp_packets_received 0\nwebrtc_sessions_rtcp_packets_sent 0\n`, string(bo))\n\t})\n\n\tt.Run(\"with data\", func(t *testing.T) {\n\t\tterminate := make(chan struct{})\n\t\tvar wg sync.WaitGroup\n\t\twg.Add(6)\n\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tsource := gortsplib.Client{}\n\t\t\terr2 := source.StartRecording(\"rtsp://localhost:8554/rtsp_path\",\n\t\t\t\t&description.Session{Medias: []*description.Media{test.UniqueMediaH264()}})\n\t\t\trequire.NoError(t, err2)\n\t\t\tdefer source.Close()\n\t\t\t<-terminate\n\t\t}()\n\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\t\t\tsource2 := gortsplib.Client{TLSConfig: &tls.Config{InsecureSkipVerify: true}}\n\t\t\terr2 := source2.StartRecording(\"rtsps://localhost:8322/rtsps_path\",\n\t\t\t\t&description.Session{Medias: []*description.Media{test.UniqueMediaH264()}})\n\t\t\trequire.NoError(t, err2)\n\t\t\tdefer source2.Close()\n\t\t\t<-terminate\n\t\t}()\n\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\n\t\t\tu, err2 := url.Parse(\"rtmp://localhost:1935/rtmp_path\")\n\t\t\trequire.NoError(t, err2)\n\n\t\t\tconn := &gortmplib.Client{\n\t\t\t\tURL:     u,\n\t\t\t\tPublish: true,\n\t\t\t}\n\t\t\terr2 = conn.Initialize(context.Background())\n\t\t\trequire.NoError(t, err2)\n\t\t\tdefer conn.Close()\n\n\t\t\ttrack := &gortmplib.Track{\n\t\t\t\tCodec: &rtmpcodecs.H264{\n\t\t\t\t\tSPS: test.FormatH264.SPS,\n\t\t\t\t\tPPS: test.FormatH264.PPS,\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tw := &gortmplib.Writer{\n\t\t\t\tConn:   conn,\n\t\t\t\tTracks: []*gortmplib.Track{track},\n\t\t\t}\n\t\t\terr2 = w.Initialize()\n\t\t\trequire.NoError(t, err2)\n\n\t\t\terr2 = w.WriteH264(track, 2*time.Second, 2*time.Second, [][]byte{{5, 2, 3, 4}})\n\t\t\trequire.NoError(t, err2)\n\n\t\t\t<-terminate\n\t\t}()\n\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\n\t\t\tu, err2 := url.Parse(\"rtmps://localhost:1936/rtmps_path\")\n\t\t\trequire.NoError(t, err2)\n\n\t\t\tconn := &gortmplib.Client{\n\t\t\t\tURL:       u,\n\t\t\t\tTLSConfig: &tls.Config{InsecureSkipVerify: true},\n\t\t\t\tPublish:   true,\n\t\t\t}\n\t\t\terr2 = conn.Initialize(context.Background())\n\t\t\trequire.NoError(t, err2)\n\t\t\tdefer conn.Close()\n\n\t\t\ttrack := &gortmplib.Track{\n\t\t\t\tCodec: &rtmpcodecs.H264{\n\t\t\t\t\tSPS: test.FormatH264.SPS,\n\t\t\t\t\tPPS: test.FormatH264.PPS,\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tw := &gortmplib.Writer{\n\t\t\t\tConn:   conn,\n\t\t\t\tTracks: []*gortmplib.Track{track},\n\t\t\t}\n\t\t\terr2 = w.Initialize()\n\t\t\trequire.NoError(t, err2)\n\n\t\t\terr2 = w.WriteH264(track, 2*time.Second, 2*time.Second, [][]byte{{5, 2, 3, 4}})\n\t\t\trequire.NoError(t, err2)\n\n\t\t\t<-terminate\n\t\t}()\n\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\n\t\t\tsu, err2 := url.Parse(\"http://localhost:8889/webrtc_path/whip\")\n\t\t\trequire.NoError(t, err2)\n\n\t\t\ttr2 := &http.Transport{}\n\t\t\tdefer tr2.CloseIdleConnections()\n\t\t\thc2 := &http.Client{Transport: tr2}\n\n\t\t\ttrack := &webrtc.OutgoingTrack{\n\t\t\t\tCaps: pwebrtc.RTPCodecCapability{\n\t\t\t\t\tMimeType:    pwebrtc.MimeTypeH264,\n\t\t\t\t\tClockRate:   90000,\n\t\t\t\t\tSDPFmtpLine: \"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\",\n\t\t\t\t},\n\t\t\t}\n\n\t\t\ts := &whip.Client{\n\t\t\t\tHTTPClient:     hc2,\n\t\t\t\tURL:            su,\n\t\t\t\tLog:            test.NilLogger,\n\t\t\t\tPublish:        true,\n\t\t\t\tOutgoingTracks: []*webrtc.OutgoingTrack{track},\n\t\t\t}\n\n\t\t\terr2 = s.Initialize(context.Background())\n\t\t\trequire.NoError(t, err2)\n\t\t\tdefer checkClose(t, s.Close)\n\n\t\t\terr2 = track.WriteRTP(&rtp.Packet{\n\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\tVersion:        2,\n\t\t\t\t\tMarker:         true,\n\t\t\t\t\tPayloadType:    96,\n\t\t\t\t\tSequenceNumber: 123,\n\t\t\t\t\tTimestamp:      45343,\n\t\t\t\t\tSSRC:           563423,\n\t\t\t\t},\n\t\t\t\tPayload: []byte{1},\n\t\t\t})\n\t\t\trequire.NoError(t, err2)\n\t\t\t<-terminate\n\t\t}()\n\n\t\tgo func() {\n\t\t\tdefer wg.Done()\n\n\t\t\tsrtConf := srt.DefaultConfig()\n\t\t\taddress, err2 := srtConf.UnmarshalURL(\"srt://localhost:8890?streamid=publish:srt_path\")\n\t\t\trequire.NoError(t, err2)\n\n\t\t\terr2 = srtConf.Validate()\n\t\t\trequire.NoError(t, err2)\n\n\t\t\tpublisher, err2 := srt.Dial(\"srt\", address, srtConf)\n\t\t\trequire.NoError(t, err2)\n\t\t\tdefer publisher.Close()\n\n\t\t\ttrack := &mpegts.Track{\n\t\t\t\tCodec: &tscodecs.H264{},\n\t\t\t}\n\n\t\t\tbw := bufio.NewWriter(publisher)\n\t\t\tw := &mpegts.Writer{W: bw, Tracks: []*mpegts.Track{track}}\n\t\t\terr2 = w.Initialize()\n\t\t\trequire.NoError(t, err2)\n\n\t\t\terr2 = w.WriteH264(track, 0, 0, [][]byte{\n\t\t\t\ttest.FormatH264.SPS,\n\t\t\t\ttest.FormatH264.PPS,\n\t\t\t\t{0x05, 1}, // IDR\n\t\t\t})\n\t\t\trequire.NoError(t, err2)\n\n\t\t\terr2 = bw.Flush()\n\t\t\trequire.NoError(t, err2)\n\t\t\t<-terminate\n\t\t}()\n\n\t\ttime.Sleep(500*time.Millisecond + 2*time.Second)\n\n\t\tbo := httpPullFile(t, hc, \"http://localhost:9998/metrics\")\n\n\t\trequire.Regexp(t,\n\t\t\t`^paths\\{name=\".*?\",state=\"ready\"\\} 1`+\"\\n\"+\n\t\t\t\t`paths_inbound_bytes\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths_outbound_bytes\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths_inbound_frames_in_error\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths_bytes_received\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths_bytes_sent\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths_readers\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths\\{name=\".*?\",state=\"ready\"\\} 1`+\"\\n\"+\n\t\t\t\t`paths_inbound_bytes\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths_outbound_bytes\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths_inbound_frames_in_error\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths_bytes_received\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths_bytes_sent\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths_readers\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths\\{name=\".*?\",state=\"ready\"\\} 1`+\"\\n\"+\n\t\t\t\t`paths_inbound_bytes\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths_outbound_bytes\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths_inbound_frames_in_error\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths_bytes_received\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths_bytes_sent\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths_readers\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths\\{name=\".*?\",state=\"ready\"\\} 1`+\"\\n\"+\n\t\t\t\t`paths_inbound_bytes\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths_outbound_bytes\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths_inbound_frames_in_error\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths_bytes_received\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths_bytes_sent\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths_readers\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths\\{name=\".*?\",state=\"ready\"\\} 1`+\"\\n\"+\n\t\t\t\t`paths_inbound_bytes\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths_outbound_bytes\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths_inbound_frames_in_error\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths_bytes_received\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths_bytes_sent\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths_readers\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths\\{name=\".*?\",state=\"ready\"\\} 1`+\"\\n\"+\n\t\t\t\t`paths_inbound_bytes\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths_outbound_bytes\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths_inbound_frames_in_error\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths_bytes_received\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths_bytes_sent\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`paths_readers\\{name=\".*?\",state=\"ready\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`hls_muxers\\{name=\".*?\"\\} 1`+\"\\n\"+\n\t\t\t\t`hls_muxers_outbound_bytes\\{name=\".*?\"\\} 0`+\"\\n\"+\n\t\t\t\t`hls_muxers_outbound_frames_discarded\\{name=\".*?\"\\} 0`+\"\\n\"+\n\t\t\t\t`hls_muxers_bytes_sent\\{name=\".*?\"\\} 0`+\"\\n\"+\n\t\t\t\t`hls_muxers\\{name=\".*?\"\\} 1`+\"\\n\"+\n\t\t\t\t`hls_muxers_outbound_bytes\\{name=\".*?\"\\} 0`+\"\\n\"+\n\t\t\t\t`hls_muxers_outbound_frames_discarded\\{name=\".*?\"\\} 0`+\"\\n\"+\n\t\t\t\t`hls_muxers_bytes_sent\\{name=\".*?\"\\} 0`+\"\\n\"+\n\t\t\t\t`hls_muxers\\{name=\".*?\"\\} 1`+\"\\n\"+\n\t\t\t\t`hls_muxers_outbound_bytes\\{name=\".*?\"\\} 0`+\"\\n\"+\n\t\t\t\t`hls_muxers_outbound_frames_discarded\\{name=\".*?\"\\} 0`+\"\\n\"+\n\t\t\t\t`hls_muxers_bytes_sent\\{name=\".*?\"\\} 0`+\"\\n\"+\n\t\t\t\t`hls_muxers\\{name=\".*?\"\\} 1`+\"\\n\"+\n\t\t\t\t`hls_muxers_outbound_bytes\\{name=\".*?\"\\} 0`+\"\\n\"+\n\t\t\t\t`hls_muxers_outbound_frames_discarded\\{name=\".*?\"\\} 0`+\"\\n\"+\n\t\t\t\t`hls_muxers_bytes_sent\\{name=\".*?\"\\} 0`+\"\\n\"+\n\t\t\t\t`hls_muxers\\{name=\".*?\"\\} 1`+\"\\n\"+\n\t\t\t\t`hls_muxers_outbound_bytes\\{name=\".*?\"\\} 0`+\"\\n\"+\n\t\t\t\t`hls_muxers_outbound_frames_discarded\\{name=\".*?\"\\} 0`+\"\\n\"+\n\t\t\t\t`hls_muxers_bytes_sent\\{name=\".*?\"\\} 0`+\"\\n\"+\n\t\t\t\t`hls_muxers\\{name=\".*?\"\\} 1`+\"\\n\"+\n\t\t\t\t`hls_muxers_outbound_bytes\\{name=\".*?\"\\} 0`+\"\\n\"+\n\t\t\t\t`hls_muxers_outbound_frames_discarded\\{name=\".*?\"\\} 0`+\"\\n\"+\n\t\t\t\t`hls_muxers_bytes_sent\\{name=\".*?\"\\} 0`+\"\\n\"+\n\t\t\t\t`rtsp_conns\\{id=\".*?\"\\} 1`+\"\\n\"+\n\t\t\t\t`rtsp_conns_inbound_bytes\\{id=\".*?\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsp_conns_outbound_bytes\\{id=\".*?\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsp_conns_bytes_received\\{id=\".*?\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsp_conns_bytes_sent\\{id=\".*?\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsp_sessions\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} 1`+\"\\n\"+\n\t\t\t\t`rtsp_sessions_inbound_bytes\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsp_sessions_inbound_rtp_packets\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsp_sessions_inbound_rtp_packets_lost\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsp_sessions_inbound_rtp_packets_in_error\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsp_sessions_inbound_rtp_packets_jitter\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsp_sessions_inbound_rtcp_packets\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsp_sessions_inbound_rtcp_packets_in_error\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsp_sessions_outbound_bytes\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsp_sessions_outbound_rtp_packets\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsp_sessions_outbound_rtp_packets_reported_lost\\{id=\".*?\",path=\".*?\",`+\n\t\t\t\t`remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsp_sessions_outbound_rtp_packets_discarded\\{id=\".*?\",path=\".*?\",`+\n\t\t\t\t`remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsp_sessions_outbound_rtcp_packets\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsp_sessions_bytes_received\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} 0`+\"\\n\"+\n\t\t\t\t`rtsp_sessions_bytes_sent\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsp_sessions_rtp_packets_received\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsp_sessions_rtp_packets_sent\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsp_sessions_rtp_packets_lost\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsp_sessions_rtp_packets_in_error\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsp_sessions_rtp_packets_jitter\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsp_sessions_rtcp_packets_received\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsp_sessions_rtcp_packets_sent\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsp_sessions_rtcp_packets_in_error\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsps_conns\\{id=\".*?\"\\} 1`+\"\\n\"+\n\t\t\t\t`rtsps_conns_inbound_bytes\\{id=\".*?\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsps_conns_outbound_bytes\\{id=\".*?\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsps_conns_bytes_received\\{id=\".*?\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsps_conns_bytes_sent\\{id=\".*?\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsps_sessions\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} 1`+\"\\n\"+\n\t\t\t\t`rtsps_sessions_inbound_bytes\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsps_sessions_inbound_rtp_packets\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsps_sessions_inbound_rtp_packets_lost\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsps_sessions_inbound_rtp_packets_in_error\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsps_sessions_inbound_rtp_packets_jitter\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsps_sessions_inbound_rtcp_packets\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsps_sessions_inbound_rtcp_packets_in_error\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsps_sessions_outbound_bytes\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsps_sessions_outbound_rtp_packets\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsps_sessions_outbound_rtp_packets_reported_lost\\{id=\".*?\",path=\".*?\",`+\n\t\t\t\t`remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsps_sessions_outbound_rtp_packets_discarded\\{id=\".*?\",path=\".*?\",`+\n\t\t\t\t`remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsps_sessions_outbound_rtcp_packets\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsps_sessions_bytes_received\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} 0`+\"\\n\"+\n\t\t\t\t`rtsps_sessions_bytes_sent\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsps_sessions_rtp_packets_received\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsps_sessions_rtp_packets_sent\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsps_sessions_rtp_packets_lost\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsps_sessions_rtp_packets_in_error\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsps_sessions_rtp_packets_jitter\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsps_sessions_rtcp_packets_received\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsps_sessions_rtcp_packets_sent\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtsps_sessions_rtcp_packets_in_error\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtmp_conns\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} 1`+\"\\n\"+\n\t\t\t\t`rtmp_conns_inbound_bytes\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtmp_conns_outbound_bytes\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtmp_conns_outbound_frames_discarded\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtmp_conns_bytes_received\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtmp_conns_bytes_sent\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtmps_conns\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} 1`+\"\\n\"+\n\t\t\t\t`rtmps_conns_inbound_bytes\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtmps_conns_outbound_bytes\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtmps_conns_outbound_frames_discarded\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtmps_conns_bytes_received\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`rtmps_conns_bytes_sent\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} 1`+\"\\n\"+\n\t\t\t\t`srt_conns_packets_sent\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_packets_received\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_packets_sent_unique\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_packets_received_unique\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} 1`+\"\\n\"+\n\t\t\t\t`srt_conns_packets_send_loss\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_packets_received_loss\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_packets_retrans\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_packets_received_retrans\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_packets_sent_ack\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_packets_received_ack\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_packets_sent_nak\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_packets_received_nak\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_packets_sent_km\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_packets_received_km\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_us_snd_duration\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_packets_received_belated\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_packets_send_drop\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_packets_received_drop\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_packets_received_undecrypt\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_bytes_sent\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} 0`+\"\\n\"+\n\t\t\t\t`srt_conns_bytes_received\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_bytes_sent_unique\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_bytes_received_unique\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_bytes_received_loss\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_bytes_retrans\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_bytes_received_retrans\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_bytes_received_belated\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_bytes_send_drop\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_bytes_received_drop\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_bytes_received_undecrypt\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_us_packets_send_period\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} \\d+\\.\\d+`+\"\\n\"+\n\t\t\t\t`srt_conns_packets_flow_window\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_packets_flight_size\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_ms_rtt\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} \\d+\\.\\d+`+\"\\n\"+\n\t\t\t\t`srt_conns_mbps_send_rate\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_mbps_receive_rate\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_mbps_link_capacity\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_bytes_avail_send_buf\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_bytes_avail_receive_buf\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_mbps_max_bw\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} -1`+\"\\n\"+\n\t\t\t\t`srt_conns_bytes_mss\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_packets_send_buf\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_bytes_send_buf\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_ms_send_buf\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_ms_send_tsb_pd_delay\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_packets_receive_buf\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_bytes_receive_buf\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_ms_receive_buf\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_ms_receive_tsb_pd_delay\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_packets_reorder_tolerance\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_packets_received_avg_belated_time\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_packets_send_loss_rate\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_packets_received_loss_rate\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`srt_conns_outbound_frames_discarded\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`webrtc_sessions\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} 1`+\"\\n\"+\n\t\t\t\t`webrtc_sessions_inbound_bytes\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`webrtc_sessions_inbound_rtp_packets\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`webrtc_sessions_inbound_rtp_packets_lost\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`webrtc_sessions_inbound_rtp_packets_jitter\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`webrtc_sessions_inbound_rtcp_packets\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`webrtc_sessions_outbound_bytes\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`webrtc_sessions_outbound_rtp_packets\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`webrtc_sessions_outbound_rtcp_packets\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`webrtc_sessions_outbound_frames_discarded\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`webrtc_sessions_bytes_received\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`webrtc_sessions_bytes_sent\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`webrtc_sessions_rtp_packets_received\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`webrtc_sessions_rtp_packets_sent\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`webrtc_sessions_rtp_packets_lost\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`webrtc_sessions_rtp_packets_jitter\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`webrtc_sessions_rtcp_packets_received\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t`webrtc_sessions_rtcp_packets_sent\\{id=\".*?\",path=\".*?\",remoteAddr=\".*?\",state=\"publish\"\\} [0-9]+`+\"\\n\"+\n\t\t\t\t\"$\",\n\t\t\tstring(bo))\n\n\t\tclose(terminate)\n\t\twg.Wait()\n\t})\n\n\tt.Run(\"servers disabled\", func(t *testing.T) {\n\t\thttpRequest(t, hc, http.MethodPatch, \"http://localhost:9997/v3/config/global/patch\", map[string]any{\n\t\t\t\"rtsp\":   false,\n\t\t\t\"rtmp\":   false,\n\t\t\t\"srt\":    false,\n\t\t\t\"hls\":    false,\n\t\t\t\"webrtc\": false,\n\t\t}, nil)\n\n\t\ttime.Sleep(500 * time.Millisecond)\n\n\t\tbo := httpPullFile(t, hc, \"http://localhost:9998/metrics\")\n\n\t\trequire.Equal(t, \"paths 0\\n\"+\n\t\t\t\"paths_inbound_bytes 0\\n\"+\n\t\t\t\"paths_outbound_bytes 0\\n\"+\n\t\t\t\"paths_inbound_frames_in_error 0\\n\"+\n\t\t\t\"paths_bytes_received 0\\n\"+\n\t\t\t\"paths_bytes_sent 0\\n\"+\n\t\t\t\"paths_readers 0\\n\",\n\t\t\tstring(bo))\n\t})\n}\n"
  },
  {
    "path": "internal/core/path.go",
    "content": "package core\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"strconv\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/externalcmd\"\n\t\"github.com/bluenviron/mediamtx/internal/hooks\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/recorder\"\n\t\"github.com/bluenviron/mediamtx/internal/staticsources\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n)\n\nfunc emptyTimer() *time.Timer {\n\tt := time.NewTimer(0)\n\t<-t.C\n\treturn t\n}\n\ntype pathParent interface {\n\tlogger.Writer\n\tsetPathReady(*path)\n\tsetPathNotReady(*path)\n\tclosePathIfIdle(*path)\n\tremovePath(*path)\n\tAddReader(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error)\n}\n\ntype pathOnDemandState int\n\nconst (\n\tpathOnDemandStateInitial pathOnDemandState = iota\n\tpathOnDemandStateWaitingReady\n\tpathOnDemandStateReady\n\tpathOnDemandStateClosing\n)\n\ntype pathAPIPathsListRes struct {\n\tdata  *defs.APIPathList\n\tpaths map[string]*path\n}\n\ntype pathAPIPathsListReq struct {\n\tres chan pathAPIPathsListRes\n}\n\ntype pathAPIPathsGetRes struct {\n\tpath *path\n\tdata *defs.APIPath\n\terr  error\n}\n\ntype pathAPIPathsGetReq struct {\n\tname string\n\tres  chan pathAPIPathsGetRes\n}\n\ntype path struct {\n\tparentCtx         context.Context\n\tlogLevel          conf.LogLevel\n\tdumpPackets       bool\n\trtspAddress       string\n\treadTimeout       conf.Duration\n\twriteTimeout      conf.Duration\n\twriteQueueSize    int\n\tudpReadBufferSize uint\n\trtpMaxPayloadSize int\n\tconf              *conf.Path\n\tname              string\n\tmatches           []string\n\twg                *sync.WaitGroup\n\texternalCmdPool   *externalcmd.Pool\n\tparent            pathParent\n\n\t// accessed by pathManager only\n\tready    bool\n\tconfName string\n\n\tctx                            context.Context\n\tctxCancel                      func()\n\tpendingRequests                *int64\n\tconfMutex                      sync.RWMutex\n\tsource                         defs.Source\n\tstream                         *stream.Stream\n\trecorder                       *recorder.Recorder\n\tavailableTime                  time.Time\n\tonlineTime                     time.Time\n\tonUnDemandHook                 func(string)\n\tonNotReadyHook                 func()\n\treaders                        map[defs.Reader]struct{}\n\tdescribeRequestsOnHold         []defs.PathDescribeReq\n\treaderAddRequestsOnHold        []defs.PathAddReaderReq\n\tonDemandStaticSourceState      pathOnDemandState\n\tonDemandStaticSourceReadyTimer *time.Timer\n\tonDemandStaticSourceCloseTimer *time.Timer\n\tonDemandPublisherState         pathOnDemandState\n\tonDemandPublisherReadyTimer    *time.Timer\n\tonDemandPublisherCloseTimer    *time.Timer\n\n\t// in\n\tchReloadConf              chan *conf.Path\n\tchStaticSourceSetReady    chan defs.PathSourceStaticSetReadyReq\n\tchStaticSourceSetNotReady chan defs.PathSourceStaticSetNotReadyReq\n\tchDescribe                chan defs.PathDescribeReq\n\tchAddPublisher            chan defs.PathAddPublisherReq\n\tchRemovePublisher         chan defs.PathRemovePublisherReq\n\tchAddReader               chan defs.PathAddReaderReq\n\tchRemoveReader            chan defs.PathRemoveReaderReq\n\tchAPIPathsGet             chan pathAPIPathsGetReq\n\n\t// out\n\tdone chan struct{}\n}\n\nfunc (pa *path) initialize() {\n\tctx, ctxCancel := context.WithCancel(pa.parentCtx)\n\n\tpa.confName = pa.conf.Name\n\tpa.ctx = ctx\n\tpa.ctxCancel = ctxCancel\n\tpa.pendingRequests = new(int64)\n\tpa.readers = make(map[defs.Reader]struct{})\n\tpa.onDemandStaticSourceReadyTimer = emptyTimer()\n\tpa.onDemandStaticSourceCloseTimer = emptyTimer()\n\tpa.onDemandPublisherReadyTimer = emptyTimer()\n\tpa.onDemandPublisherCloseTimer = emptyTimer()\n\tpa.chReloadConf = make(chan *conf.Path)\n\tpa.chStaticSourceSetReady = make(chan defs.PathSourceStaticSetReadyReq)\n\tpa.chStaticSourceSetNotReady = make(chan defs.PathSourceStaticSetNotReadyReq)\n\tpa.chDescribe = make(chan defs.PathDescribeReq)\n\tpa.chAddPublisher = make(chan defs.PathAddPublisherReq)\n\tpa.chRemovePublisher = make(chan defs.PathRemovePublisherReq)\n\tpa.chAddReader = make(chan defs.PathAddReaderReq)\n\tpa.chRemoveReader = make(chan defs.PathRemoveReaderReq)\n\tpa.chAPIPathsGet = make(chan pathAPIPathsGetReq)\n\tpa.done = make(chan struct{})\n\n\tpa.Log(logger.Debug, \"created\")\n\n\tpa.wg.Add(1)\n\tgo pa.run()\n}\n\nfunc (pa *path) close() {\n\tpa.ctxCancel()\n}\n\nfunc (pa *path) wait() {\n\t<-pa.done\n}\n\n// Log implements logger.Writer.\nfunc (pa *path) Log(level logger.Level, format string, args ...any) {\n\tpa.parent.Log(level, \"[path \"+pa.name+\"] \"+format, args...)\n}\n\nfunc (pa *path) Name() string {\n\treturn pa.name\n}\n\nfunc (pa *path) isAvailable() bool {\n\treturn pa.stream != nil\n}\n\nfunc (pa *path) isOnline() bool {\n\treturn pa.source != nil\n}\n\nfunc (pa *path) run() {\n\tdefer close(pa.done)\n\tdefer pa.wg.Done()\n\n\tif pa.conf.AlwaysAvailable {\n\t\terr := pa.setAvailable(nil, \"\", nil, true)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\n\tif pa.conf.Source == \"redirect\" {\n\t\tpa.source = &sourceRedirect{}\n\t} else if pa.conf.HasStaticSource() {\n\t\tpa.source = &staticsources.Handler{\n\t\t\tConf:              pa.conf,\n\t\t\tLogLevel:          pa.logLevel,\n\t\t\tDumpPackets:       pa.dumpPackets,\n\t\t\tReadTimeout:       pa.readTimeout,\n\t\t\tWriteTimeout:      pa.writeTimeout,\n\t\t\tWriteQueueSize:    pa.writeQueueSize,\n\t\t\tUDPReadBufferSize: pa.udpReadBufferSize,\n\t\t\tRTPMaxPayloadSize: pa.rtpMaxPayloadSize,\n\t\t\tMatches:           pa.matches,\n\t\t\tPathManager:       pa.parent,\n\t\t\tParent:            pa,\n\t\t}\n\t\tpa.source.(*staticsources.Handler).Initialize()\n\n\t\tif !pa.conf.SourceOnDemand {\n\t\t\tpa.source.(*staticsources.Handler).Start(false, \"\")\n\t\t}\n\t}\n\n\tonUnInitHook := hooks.OnInit(hooks.OnInitParams{\n\t\tLogger:          pa,\n\t\tExternalCmdPool: pa.externalCmdPool,\n\t\tConf:            pa.conf,\n\t\tExternalCmdEnv:  pa.ExternalCmdEnv(),\n\t})\n\n\terr := pa.runInner()\n\n\t// call before destroying context\n\tpa.parent.removePath(pa)\n\n\tpa.ctxCancel()\n\n\tpa.onDemandStaticSourceReadyTimer.Stop()\n\tpa.onDemandStaticSourceCloseTimer.Stop()\n\tpa.onDemandPublisherReadyTimer.Stop()\n\tpa.onDemandPublisherCloseTimer.Stop()\n\n\tonUnInitHook()\n\n\tfor _, req := range pa.describeRequestsOnHold {\n\t\treq.Res <- defs.PathDescribeRes{Err: fmt.Errorf(\"terminated\")}\n\t}\n\n\tfor _, req := range pa.readerAddRequestsOnHold {\n\t\treq.Res <- defs.PathAddReaderRes{Err: fmt.Errorf(\"terminated\")}\n\t}\n\n\tif pa.stream != nil {\n\t\tpa.setNotAvailable()\n\t}\n\n\tif pa.source != nil {\n\t\tif source, ok := pa.source.(*staticsources.Handler); ok {\n\t\t\tif !pa.conf.SourceOnDemand || pa.onDemandStaticSourceState != pathOnDemandStateInitial {\n\t\t\t\tsource.Close(\"path is closing\")\n\t\t\t}\n\t\t} else if source, ok2 := pa.source.(defs.Publisher); ok2 {\n\t\t\tsource.Close()\n\t\t}\n\t}\n\n\tif pa.onUnDemandHook != nil {\n\t\tpa.onUnDemandHook(\"path destroyed\")\n\t}\n\n\tpa.Log(logger.Debug, \"destroyed: %v\", err)\n}\n\nfunc (pa *path) runInner() error {\n\tfor {\n\t\tselect {\n\t\tcase <-pa.onDemandStaticSourceReadyTimer.C:\n\t\t\tpa.doOnDemandStaticSourceReadyTimer()\n\n\t\t\tif pa.shouldClose() {\n\t\t\t\tpa.parent.closePathIfIdle(pa)\n\t\t\t}\n\n\t\tcase <-pa.onDemandStaticSourceCloseTimer.C:\n\t\t\tpa.doOnDemandStaticSourceCloseTimer()\n\n\t\t\tif pa.shouldClose() {\n\t\t\t\tpa.parent.closePathIfIdle(pa)\n\t\t\t}\n\n\t\tcase <-pa.onDemandPublisherReadyTimer.C:\n\t\t\tpa.doOnDemandPublisherReadyTimer()\n\n\t\t\tif pa.shouldClose() {\n\t\t\t\tpa.parent.closePathIfIdle(pa)\n\t\t\t}\n\n\t\tcase <-pa.onDemandPublisherCloseTimer.C:\n\t\t\tpa.doOnDemandPublisherCloseTimer()\n\n\t\tcase newConf := <-pa.chReloadConf:\n\t\t\tpa.doReloadConf(newConf)\n\n\t\tcase req := <-pa.chStaticSourceSetReady:\n\t\t\tpa.doSourceStaticSetReady(req)\n\n\t\tcase req := <-pa.chStaticSourceSetNotReady:\n\t\t\tpa.doSourceStaticSetNotReady(req)\n\n\t\t\tif pa.shouldClose() {\n\t\t\t\tpa.parent.closePathIfIdle(pa)\n\t\t\t}\n\n\t\tcase req := <-pa.chDescribe:\n\t\t\tpa.doDescribe(req)\n\n\t\t\tatomic.AddInt64(pa.pendingRequests, -1)\n\n\t\t\tif pa.shouldClose() {\n\t\t\t\tpa.parent.closePathIfIdle(pa)\n\t\t\t}\n\n\t\tcase req := <-pa.chAddPublisher:\n\t\t\tpa.doAddPublisher(req)\n\n\t\t\tatomic.AddInt64(pa.pendingRequests, -1)\n\n\t\t\tif pa.shouldClose() {\n\t\t\t\tpa.parent.closePathIfIdle(pa)\n\t\t\t}\n\n\t\tcase req := <-pa.chRemovePublisher:\n\t\t\tpa.doRemovePublisher(req)\n\n\t\t\tif pa.shouldClose() {\n\t\t\t\tpa.parent.closePathIfIdle(pa)\n\t\t\t}\n\n\t\tcase req := <-pa.chAddReader:\n\t\t\tpa.doAddReader(req)\n\n\t\t\tatomic.AddInt64(pa.pendingRequests, -1)\n\n\t\t\tif pa.shouldClose() {\n\t\t\t\tpa.parent.closePathIfIdle(pa)\n\t\t\t}\n\n\t\tcase req := <-pa.chRemoveReader:\n\t\t\tpa.doRemoveReader(req)\n\n\t\tcase req := <-pa.chAPIPathsGet:\n\t\t\tpa.doAPIPathsGet(req)\n\n\t\tcase <-pa.ctx.Done():\n\t\t\treturn fmt.Errorf(\"terminated\")\n\t\t}\n\t}\n}\n\nfunc (pa *path) doOnDemandStaticSourceReadyTimer() {\n\tfor _, req := range pa.describeRequestsOnHold {\n\t\treq.Res <- defs.PathDescribeRes{Err: fmt.Errorf(\"source of path '%s' has timed out\", pa.name)}\n\t}\n\tpa.describeRequestsOnHold = nil\n\n\tfor _, req := range pa.readerAddRequestsOnHold {\n\t\treq.Res <- defs.PathAddReaderRes{Err: fmt.Errorf(\"source of path '%s' has timed out\", pa.name)}\n\t}\n\tpa.readerAddRequestsOnHold = nil\n\n\tpa.onDemandStaticSourceStop(\"timed out\")\n}\n\nfunc (pa *path) doOnDemandStaticSourceCloseTimer() {\n\tif pa.conf.AlwaysAvailable {\n\t\tpanic(\"should not happen\")\n\t}\n\tpa.setNotAvailable()\n\tpa.onDemandStaticSourceStop(\"not needed by anyone\")\n}\n\nfunc (pa *path) doOnDemandPublisherReadyTimer() {\n\tfor _, req := range pa.describeRequestsOnHold {\n\t\treq.Res <- defs.PathDescribeRes{Err: fmt.Errorf(\"source of path '%s' has timed out\", pa.name)}\n\t}\n\tpa.describeRequestsOnHold = nil\n\n\tfor _, req := range pa.readerAddRequestsOnHold {\n\t\treq.Res <- defs.PathAddReaderRes{Err: fmt.Errorf(\"source of path '%s' has timed out\", pa.name)}\n\t}\n\tpa.readerAddRequestsOnHold = nil\n\n\tpa.onDemandPublisherStop(\"timed out\")\n}\n\nfunc (pa *path) doOnDemandPublisherCloseTimer() {\n\tpa.onDemandPublisherStop(\"not needed by anyone\")\n}\n\nfunc (pa *path) doReloadConf(newConf *conf.Path) {\n\tpa.confMutex.Lock()\n\toldConf := pa.conf\n\tpa.conf = newConf\n\tpa.confMutex.Unlock()\n\n\tif pa.conf.HasStaticSource() {\n\t\tpa.source.(*staticsources.Handler).ReloadConf(newConf)\n\t}\n\n\tif pa.recorder != nil &&\n\t\t(newConf.Record != oldConf.Record ||\n\t\t\tnewConf.RecordPath != oldConf.RecordPath ||\n\t\t\tnewConf.RecordFormat != oldConf.RecordFormat ||\n\t\t\tnewConf.RecordPartDuration != oldConf.RecordPartDuration ||\n\t\t\tnewConf.RecordMaxPartSize != oldConf.RecordMaxPartSize ||\n\t\t\tnewConf.RecordSegmentDuration != oldConf.RecordSegmentDuration ||\n\t\t\tnewConf.RecordDeleteAfter != oldConf.RecordDeleteAfter) {\n\t\tpa.recorder.Close()\n\t\tpa.recorder = nil\n\t}\n\n\tif newConf.Record && pa.stream != nil && pa.recorder == nil {\n\t\tpa.startRecording()\n\t}\n}\n\nfunc (pa *path) doSourceStaticSetReady(req defs.PathSourceStaticSetReadyReq) {\n\tif !pa.conf.AlwaysAvailable {\n\t\terr := pa.setAvailable(pa.source, \"\", req.Desc, req.ReplaceNTP)\n\t\tif err != nil {\n\t\t\treq.Res <- defs.PathSourceStaticSetReadyRes{Err: err}\n\t\t\treturn\n\t\t}\n\t}\n\n\tsubStream := &stream.SubStream{\n\t\tStream:        pa.stream,\n\t\tUseRTPPackets: req.UseRTPPackets,\n\t}\n\tif pa.conf.AlwaysAvailable {\n\t\tsubStream.CurDesc = req.Desc\n\t}\n\terr := subStream.Initialize()\n\tif err != nil {\n\t\treq.Res <- defs.PathSourceStaticSetReadyRes{Err: err}\n\t\treturn\n\t}\n\n\tif pa.conf.AlwaysAvailable {\n\t\tpa.onlineTime = time.Now()\n\t}\n\n\tif pa.conf.HasOnDemandStaticSource() {\n\t\tpa.onDemandStaticSourceReadyTimer.Stop()\n\t\tpa.onDemandStaticSourceReadyTimer = emptyTimer()\n\t\tpa.onDemandStaticSourceScheduleClose()\n\t}\n\n\tpa.consumeOnHoldRequests()\n\n\treq.Res <- defs.PathSourceStaticSetReadyRes{SubStream: subStream}\n}\n\nfunc (pa *path) doSourceStaticSetNotReady(req defs.PathSourceStaticSetNotReadyReq) {\n\tif !pa.conf.AlwaysAvailable {\n\t\tpa.setNotAvailable()\n\t} else {\n\t\terr := pa.stream.StartOfflineSubStream()\n\t\tif err != nil {\n\t\t\tpanic(\"should not happen\")\n\t\t}\n\t}\n\n\t// send response before calling onDemandStaticSourceStop()\n\t// in order to avoid a deadlock due to staticsources.Handler.stop()\n\tclose(req.Res)\n\n\tif pa.conf.HasOnDemandStaticSource() && pa.onDemandStaticSourceState != pathOnDemandStateInitial {\n\t\tpa.onDemandStaticSourceStop(\"an error occurred\")\n\t}\n}\n\nfunc (pa *path) doDescribe(req defs.PathDescribeReq) {\n\tif _, ok := pa.source.(*sourceRedirect); ok {\n\t\treq.Res <- defs.PathDescribeRes{\n\t\t\tRedirect: pa.conf.SourceRedirect,\n\t\t}\n\t\treturn\n\t}\n\n\tif pa.stream != nil {\n\t\treq.Res <- defs.PathDescribeRes{\n\t\t\tStream: pa.stream,\n\t\t}\n\t\treturn\n\t}\n\n\tif pa.conf.HasOnDemandStaticSource() {\n\t\tif pa.onDemandStaticSourceState == pathOnDemandStateInitial {\n\t\t\tpa.onDemandStaticSourceStart(req.AccessRequest.Query)\n\t\t}\n\t\tpa.describeRequestsOnHold = append(pa.describeRequestsOnHold, req)\n\t\treturn\n\t}\n\n\tif pa.conf.HasOnDemandPublisher() {\n\t\tif pa.onDemandPublisherState == pathOnDemandStateInitial {\n\t\t\tpa.onDemandPublisherStart(req.AccessRequest.Query)\n\t\t}\n\t\tpa.describeRequestsOnHold = append(pa.describeRequestsOnHold, req)\n\t\treturn\n\t}\n\n\tif pa.conf.Fallback != nil {\n\t\treq.Res <- defs.PathDescribeRes{Redirect: *pa.conf.Fallback}\n\t\treturn\n\t}\n\n\treq.Res <- defs.PathDescribeRes{Err: defs.PathNoStreamAvailableError{PathName: pa.name}}\n}\n\nfunc (pa *path) doRemovePublisher(req defs.PathRemovePublisherReq) {\n\tif pa.source == req.Author {\n\t\tpa.executeRemovePublisher()\n\t}\n\tclose(req.Res)\n}\n\nfunc (pa *path) doAddPublisher(req defs.PathAddPublisherReq) {\n\tif pa.conf.Source != \"publisher\" {\n\t\treq.Res <- defs.PathAddPublisherRes{\n\t\t\tErr: fmt.Errorf(\"can't publish to path '%s' since 'source' is not 'publisher'\", pa.name),\n\t\t}\n\t\treturn\n\t}\n\n\tif pa.source != nil {\n\t\tif !pa.conf.OverridePublisher {\n\t\t\treq.Res <- defs.PathAddPublisherRes{Err: fmt.Errorf(\"someone is already publishing to path '%s'\", pa.name)}\n\t\t\treturn\n\t\t}\n\n\t\tpa.Log(logger.Info, \"closing existing publisher\")\n\t\tpa.source.(defs.Publisher).Close()\n\t\tpa.executeRemovePublisher()\n\t}\n\n\tif !pa.conf.AlwaysAvailable {\n\t\terr := pa.setAvailable(req.Author, req.AccessRequest.Query, req.Desc, req.ReplaceNTP)\n\t\tif err != nil {\n\t\t\treq.Res <- defs.PathAddPublisherRes{Err: err}\n\t\t\treturn\n\t\t}\n\t}\n\n\tsubStream := &stream.SubStream{\n\t\tStream:        pa.stream,\n\t\tUseRTPPackets: req.UseRTPPackets,\n\t}\n\tif pa.conf.AlwaysAvailable {\n\t\tsubStream.CurDesc = req.Desc\n\t}\n\terr := subStream.Initialize()\n\tif err != nil {\n\t\treq.Res <- defs.PathAddPublisherRes{Err: err}\n\t\treturn\n\t}\n\n\tpa.source = req.Author\n\n\treq.Author.Log(logger.Info, \"is publishing to path '%s'\",\n\t\tpa.name)\n\n\tif pa.conf.AlwaysAvailable {\n\t\tpa.onlineTime = time.Now()\n\t}\n\n\tif pa.conf.HasOnDemandPublisher() && pa.onDemandPublisherState != pathOnDemandStateInitial {\n\t\tpa.onDemandPublisherReadyTimer.Stop()\n\t\tpa.onDemandPublisherReadyTimer = emptyTimer()\n\t\tpa.onDemandPublisherScheduleClose()\n\t}\n\n\tpa.consumeOnHoldRequests()\n\n\treq.Res <- defs.PathAddPublisherRes{SubStream: subStream}\n}\n\nfunc (pa *path) doAddReader(req defs.PathAddReaderReq) {\n\tif pa.stream != nil {\n\t\tpa.addReaderPost(req)\n\t\treturn\n\t}\n\n\tif pa.conf.HasOnDemandStaticSource() {\n\t\tif pa.onDemandStaticSourceState == pathOnDemandStateInitial {\n\t\t\tpa.onDemandStaticSourceStart(req.AccessRequest.Query)\n\t\t}\n\t\tpa.readerAddRequestsOnHold = append(pa.readerAddRequestsOnHold, req)\n\t\treturn\n\t}\n\n\tif pa.conf.HasOnDemandPublisher() {\n\t\tif pa.onDemandPublisherState == pathOnDemandStateInitial {\n\t\t\tpa.onDemandPublisherStart(req.AccessRequest.Query)\n\t\t}\n\t\tpa.readerAddRequestsOnHold = append(pa.readerAddRequestsOnHold, req)\n\t\treturn\n\t}\n\n\treq.Res <- defs.PathAddReaderRes{Err: defs.PathNoStreamAvailableError{PathName: pa.name}}\n}\n\nfunc (pa *path) doRemoveReader(req defs.PathRemoveReaderReq) {\n\tif _, ok := pa.readers[req.Author]; ok {\n\t\tpa.executeRemoveReader(req.Author)\n\t}\n\tclose(req.Res)\n\n\tif len(pa.readers) == 0 {\n\t\tif pa.conf.HasOnDemandStaticSource() {\n\t\t\tif pa.onDemandStaticSourceState == pathOnDemandStateReady {\n\t\t\t\tpa.onDemandStaticSourceScheduleClose()\n\t\t\t}\n\t\t} else if pa.conf.HasOnDemandPublisher() {\n\t\t\tif pa.onDemandPublisherState == pathOnDemandStateReady {\n\t\t\t\tpa.onDemandPublisherScheduleClose()\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (pa *path) doAPIPathsGet(req pathAPIPathsGetReq) {\n\treq.res <- pathAPIPathsGetRes{\n\t\tdata: &defs.APIPath{\n\t\t\tName:     pa.name,\n\t\t\tConfName: pa.conf.Name,\n\t\t\tReady:    pa.isAvailable(),\n\t\t\tReadyTime: func() *time.Time {\n\t\t\t\tif !pa.isAvailable() {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\tv := pa.availableTime\n\t\t\t\treturn &v\n\t\t\t}(),\n\t\t\tAvailable: pa.isAvailable(),\n\t\t\tAvailableTime: func() *time.Time {\n\t\t\t\tif !pa.isAvailable() {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\tv := pa.availableTime\n\t\t\t\treturn &v\n\t\t\t}(),\n\t\t\tOnline: pa.isOnline(),\n\t\t\tOnlineTime: func() *time.Time {\n\t\t\t\tif !pa.isOnline() {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\tv := pa.onlineTime\n\t\t\t\treturn &v\n\t\t\t}(),\n\t\t\tSource: func() *defs.APIPathSource {\n\t\t\t\tif pa.source == nil {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t\tv := pa.source.APISourceDescribe()\n\t\t\t\treturn v\n\t\t\t}(),\n\t\t\tTracks: func() []defs.APIPathTrackCodec {\n\t\t\t\tif !pa.isAvailable() {\n\t\t\t\t\treturn []defs.APIPathTrackCodec{}\n\t\t\t\t}\n\t\t\t\treturn defs.MediasToCodecs(pa.stream.Desc.Medias)\n\t\t\t}(),\n\t\t\tTracks2: func() []defs.APIPathTrack {\n\t\t\t\tif !pa.isAvailable() {\n\t\t\t\t\treturn []defs.APIPathTrack{}\n\t\t\t\t}\n\t\t\t\treturn defs.MediasToTracks(pa.stream.Desc.Medias)\n\t\t\t}(),\n\t\t\tInboundBytes: func() uint64 {\n\t\t\t\tif !pa.isAvailable() {\n\t\t\t\t\treturn 0\n\t\t\t\t}\n\t\t\t\treturn pa.stream.InboundBytes()\n\t\t\t}(),\n\t\t\tOutboundBytes: func() uint64 {\n\t\t\t\tif !pa.isAvailable() {\n\t\t\t\t\treturn 0\n\t\t\t\t}\n\t\t\t\treturn pa.stream.OutboundBytes()\n\t\t\t}(),\n\t\t\tInboundFramesInError: func() uint64 {\n\t\t\t\tif !pa.isAvailable() {\n\t\t\t\t\treturn 0\n\t\t\t\t}\n\t\t\t\treturn pa.stream.InboundFramesInError()\n\t\t\t}(),\n\t\t\tBytesReceived: func() uint64 {\n\t\t\t\tif !pa.isAvailable() {\n\t\t\t\t\treturn 0\n\t\t\t\t}\n\t\t\t\treturn pa.stream.InboundBytes()\n\t\t\t}(),\n\t\t\tBytesSent: func() uint64 {\n\t\t\t\tif !pa.isAvailable() {\n\t\t\t\t\treturn 0\n\t\t\t\t}\n\t\t\t\treturn pa.stream.OutboundBytes()\n\t\t\t}(),\n\t\t\tReaders: func() []defs.APIPathReader {\n\t\t\t\tret := make([]defs.APIPathReader, len(pa.readers))\n\t\t\t\ti := 0\n\t\t\t\tfor r := range pa.readers {\n\t\t\t\t\tret[i] = *r.APIReaderDescribe()\n\t\t\t\t\ti++\n\t\t\t\t}\n\t\t\t\treturn ret\n\t\t\t}(),\n\t\t},\n\t}\n}\n\nfunc (pa *path) SafeConf() *conf.Path {\n\tpa.confMutex.RLock()\n\tdefer pa.confMutex.RUnlock()\n\treturn pa.conf\n}\n\nfunc (pa *path) ExternalCmdEnv() externalcmd.Environment {\n\t_, port, _ := net.SplitHostPort(pa.rtspAddress)\n\tenv := externalcmd.Environment{\n\t\t\"MTX_PATH\":  pa.name,\n\t\t\"RTSP_PATH\": pa.name, // deprecated\n\t\t\"RTSP_PORT\": port,\n\t}\n\n\tif len(pa.matches) > 1 {\n\t\tfor i, ma := range pa.matches[1:] {\n\t\t\tenv[\"G\"+strconv.FormatInt(int64(i+1), 10)] = ma\n\t\t}\n\t}\n\n\treturn env\n}\n\nfunc (pa *path) shouldClose() bool {\n\treturn pa.conf.Regexp != nil &&\n\t\tpa.source == nil &&\n\t\tlen(pa.readers) == 0 &&\n\t\tlen(pa.describeRequestsOnHold) == 0 &&\n\t\tlen(pa.readerAddRequestsOnHold) == 0\n}\n\nfunc (pa *path) onDemandStaticSourceStart(query string) {\n\tpa.source.(*staticsources.Handler).Start(true, query)\n\n\tpa.onDemandStaticSourceReadyTimer.Stop()\n\tpa.onDemandStaticSourceReadyTimer = time.NewTimer(time.Duration(pa.conf.SourceOnDemandStartTimeout))\n\n\tpa.onDemandStaticSourceState = pathOnDemandStateWaitingReady\n}\n\nfunc (pa *path) onDemandStaticSourceScheduleClose() {\n\tpa.onDemandStaticSourceCloseTimer.Stop()\n\tpa.onDemandStaticSourceCloseTimer = time.NewTimer(time.Duration(pa.conf.SourceOnDemandCloseAfter))\n\n\tpa.onDemandStaticSourceState = pathOnDemandStateClosing\n}\n\nfunc (pa *path) onDemandStaticSourceStop(reason string) {\n\tif pa.onDemandStaticSourceState == pathOnDemandStateClosing {\n\t\tpa.onDemandStaticSourceCloseTimer.Stop()\n\t\tpa.onDemandStaticSourceCloseTimer = emptyTimer()\n\t}\n\n\tpa.onDemandStaticSourceState = pathOnDemandStateInitial\n\n\tpa.source.(*staticsources.Handler).Stop(reason)\n}\n\nfunc (pa *path) onDemandPublisherStart(query string) {\n\tpa.onUnDemandHook = hooks.OnDemand(hooks.OnDemandParams{\n\t\tLogger:          pa,\n\t\tExternalCmdPool: pa.externalCmdPool,\n\t\tConf:            pa.conf,\n\t\tExternalCmdEnv:  pa.ExternalCmdEnv(),\n\t\tQuery:           query,\n\t})\n\n\tpa.onDemandPublisherReadyTimer.Stop()\n\tpa.onDemandPublisherReadyTimer = time.NewTimer(time.Duration(pa.conf.RunOnDemandStartTimeout))\n\n\tpa.onDemandPublisherState = pathOnDemandStateWaitingReady\n}\n\nfunc (pa *path) onDemandPublisherScheduleClose() {\n\tpa.onDemandPublisherCloseTimer.Stop()\n\tpa.onDemandPublisherCloseTimer = time.NewTimer(time.Duration(pa.conf.RunOnDemandCloseAfter))\n\n\tpa.onDemandPublisherState = pathOnDemandStateClosing\n}\n\nfunc (pa *path) onDemandPublisherStop(reason string) {\n\tif pa.onDemandPublisherState == pathOnDemandStateClosing {\n\t\tpa.onDemandPublisherCloseTimer.Stop()\n\t\tpa.onDemandPublisherCloseTimer = emptyTimer()\n\t}\n\n\tpa.onUnDemandHook(reason)\n\tpa.onUnDemandHook = nil\n\n\tpa.onDemandPublisherState = pathOnDemandStateInitial\n}\n\nfunc (pa *path) setAvailable(\n\tsource defs.Source,\n\tpublisherQuery string,\n\tdesc *description.Session,\n\treplaceNTP bool,\n) error {\n\tpa.stream = &stream.Stream{\n\t\tDesc:                  desc,\n\t\tAlwaysAvailable:       pa.conf.AlwaysAvailable,\n\t\tAlwaysAvailableTracks: pa.conf.AlwaysAvailableTracks,\n\t\tAlwaysAvailableFile:   pa.conf.AlwaysAvailableFile,\n\t\tWriteQueueSize:        pa.writeQueueSize,\n\t\tRTPMaxPayloadSize:     pa.rtpMaxPayloadSize,\n\t\tReplaceNTP:            replaceNTP,\n\t\tParent:                pa,\n\t}\n\terr := pa.stream.Initialize()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpa.availableTime = time.Now()\n\n\tif !pa.conf.AlwaysAvailable {\n\t\tpa.onlineTime = time.Now()\n\t}\n\n\tif pa.conf.Record {\n\t\tpa.startRecording()\n\t}\n\n\tvar sourceDesc *defs.APIPathSource\n\tif source != nil {\n\t\tsourceDesc = source.APISourceDescribe()\n\t}\n\n\tpa.onNotReadyHook = hooks.OnReady(hooks.OnReadyParams{\n\t\tLogger:          pa,\n\t\tExternalCmdPool: pa.externalCmdPool,\n\t\tConf:            pa.conf,\n\t\tExternalCmdEnv:  pa.ExternalCmdEnv(),\n\t\tDesc:            sourceDesc,\n\t\tQuery:           publisherQuery,\n\t})\n\n\tif pa.conf.AlwaysAvailable {\n\t\tpa.Log(logger.Info, \"stream is available, %s\", defs.MediasInfo(pa.stream.Desc.Medias))\n\t} else {\n\t\tpa.Log(logger.Info, \"stream is available and online, %s\", defs.MediasInfo(pa.stream.Desc.Medias))\n\t}\n\n\tpa.parent.setPathReady(pa)\n\n\treturn nil\n}\n\nfunc (pa *path) consumeOnHoldRequests() {\n\tfor _, req := range pa.describeRequestsOnHold {\n\t\treq.Res <- defs.PathDescribeRes{\n\t\t\tStream: pa.stream,\n\t\t}\n\t}\n\tpa.describeRequestsOnHold = nil\n\n\tfor _, req := range pa.readerAddRequestsOnHold {\n\t\tpa.addReaderPost(req)\n\t}\n\tpa.readerAddRequestsOnHold = nil\n}\n\nfunc (pa *path) setNotAvailable() {\n\tpa.parent.setPathNotReady(pa)\n\n\tfor r := range pa.readers {\n\t\tpa.executeRemoveReader(r)\n\t\tr.Close()\n\t}\n\n\tpa.onNotReadyHook()\n\n\tif pa.recorder != nil {\n\t\tpa.recorder.Close()\n\t\tpa.recorder = nil\n\t}\n\n\tif pa.stream != nil {\n\t\tpa.stream.Close()\n\t\tpa.stream = nil\n\t}\n}\n\nfunc (pa *path) startRecording() {\n\tpa.recorder = &recorder.Recorder{\n\t\tPathFormat:      pa.conf.RecordPath,\n\t\tFormat:          pa.conf.RecordFormat,\n\t\tPartDuration:    time.Duration(pa.conf.RecordPartDuration),\n\t\tMaxPartSize:     pa.conf.RecordMaxPartSize,\n\t\tSegmentDuration: time.Duration(pa.conf.RecordSegmentDuration),\n\t\tPathName:        pa.name,\n\t\tStream:          pa.stream,\n\t\tOnSegmentCreate: func(segmentPath string) {\n\t\t\tif pa.conf.RunOnRecordSegmentCreate != \"\" {\n\t\t\t\tenv := pa.ExternalCmdEnv()\n\t\t\t\tenv[\"MTX_SEGMENT_PATH\"] = segmentPath\n\n\t\t\t\tpa.Log(logger.Info, \"runOnRecordSegmentCreate command launched\")\n\t\t\t\texternalcmd.NewCmd(\n\t\t\t\t\tpa.externalCmdPool,\n\t\t\t\t\tpa.conf.RunOnRecordSegmentCreate,\n\t\t\t\t\tfalse,\n\t\t\t\t\tenv,\n\t\t\t\t\tnil)\n\t\t\t}\n\t\t},\n\t\tOnSegmentComplete: func(segmentPath string, segmentDuration time.Duration) {\n\t\t\tif pa.conf.RunOnRecordSegmentComplete != \"\" {\n\t\t\t\tenv := pa.ExternalCmdEnv()\n\t\t\t\tenv[\"MTX_SEGMENT_PATH\"] = segmentPath\n\t\t\t\tenv[\"MTX_SEGMENT_DURATION\"] = strconv.FormatFloat(segmentDuration.Seconds(), 'f', -1, 64)\n\n\t\t\t\tpa.Log(logger.Info, \"runOnRecordSegmentComplete command launched\")\n\t\t\t\texternalcmd.NewCmd(\n\t\t\t\t\tpa.externalCmdPool,\n\t\t\t\t\tpa.conf.RunOnRecordSegmentComplete,\n\t\t\t\t\tfalse,\n\t\t\t\t\tenv,\n\t\t\t\t\tnil)\n\t\t\t}\n\t\t},\n\t\tParent: pa,\n\t}\n\tpa.recorder.Initialize()\n}\n\nfunc (pa *path) executeRemoveReader(r defs.Reader) {\n\tdelete(pa.readers, r)\n}\n\nfunc (pa *path) executeRemovePublisher() {\n\tif !pa.conf.AlwaysAvailable {\n\t\tpa.setNotAvailable()\n\t} else {\n\t\terr := pa.stream.StartOfflineSubStream()\n\t\tif err != nil {\n\t\t\tpanic(\"should not happen\")\n\t\t}\n\t}\n\tpa.source = nil\n}\n\nfunc (pa *path) addReaderPost(req defs.PathAddReaderReq) {\n\tif _, ok := pa.readers[req.Author]; ok {\n\t\treq.Res <- defs.PathAddReaderRes{Stream: pa.stream}\n\t\treturn\n\t}\n\n\tif pa.conf.MaxReaders != 0 && len(pa.readers) >= pa.conf.MaxReaders {\n\t\treq.Res <- defs.PathAddReaderRes{Err: fmt.Errorf(\"maximum reader count reached\")}\n\t\treturn\n\t}\n\n\tpa.readers[req.Author] = struct{}{}\n\n\tif pa.conf.HasOnDemandStaticSource() {\n\t\tif pa.onDemandStaticSourceState == pathOnDemandStateClosing {\n\t\t\tpa.onDemandStaticSourceState = pathOnDemandStateReady\n\t\t\tpa.onDemandStaticSourceCloseTimer.Stop()\n\t\t\tpa.onDemandStaticSourceCloseTimer = emptyTimer()\n\t\t}\n\t} else if pa.conf.HasOnDemandPublisher() {\n\t\tif pa.onDemandPublisherState == pathOnDemandStateClosing {\n\t\t\tpa.onDemandPublisherState = pathOnDemandStateReady\n\t\t\tpa.onDemandPublisherCloseTimer.Stop()\n\t\t\tpa.onDemandPublisherCloseTimer = emptyTimer()\n\t\t}\n\t}\n\n\treq.Res <- defs.PathAddReaderRes{Stream: pa.stream}\n}\n\n// reloadConf is called by pathManager.\nfunc (pa *path) reloadConf(newConf *conf.Path) {\n\tselect {\n\tcase pa.chReloadConf <- newConf:\n\tcase <-pa.ctx.Done():\n\t}\n}\n\n// StaticSourceHandlerSetReady is called by staticsources.Handler.\nfunc (pa *path) StaticSourceHandlerSetReady(\n\tctx context.Context, req defs.PathSourceStaticSetReadyReq,\n) {\n\tselect {\n\tcase pa.chStaticSourceSetReady <- req:\n\n\tcase <-pa.ctx.Done():\n\t\treq.Res <- defs.PathSourceStaticSetReadyRes{Err: fmt.Errorf(\"terminated\")}\n\n\t// this avoids:\n\t// - invalid requests sent after the source has been terminated\n\t// - deadlocks caused by <-done inside stop()\n\tcase <-ctx.Done():\n\t\treq.Res <- defs.PathSourceStaticSetReadyRes{Err: fmt.Errorf(\"terminated\")}\n\t}\n}\n\n// StaticSourceHandlerSetNotReady is called by staticsources.Handler.\nfunc (pa *path) StaticSourceHandlerSetNotReady(\n\tctx context.Context, req defs.PathSourceStaticSetNotReadyReq,\n) {\n\tselect {\n\tcase pa.chStaticSourceSetNotReady <- req:\n\n\tcase <-pa.ctx.Done():\n\t\tclose(req.Res)\n\n\t// this avoids:\n\t// - invalid requests sent after the source has been terminated\n\t// - deadlocks caused by <-done inside stop()\n\tcase <-ctx.Done():\n\t\tclose(req.Res)\n\t}\n}\n\n// describe is called by a reader or publisher through pathManager.\nfunc (pa *path) describe(req defs.PathDescribeReq) defs.PathDescribeRes {\n\tselect {\n\tcase pa.chDescribe <- req:\n\t\treturn <-req.Res\n\tcase <-pa.ctx.Done():\n\t\treturn defs.PathDescribeRes{Err: fmt.Errorf(\"terminated\")}\n\t}\n}\n\n// addPublisher is called by a publisher through pathManager.\nfunc (pa *path) addPublisher(req defs.PathAddPublisherReq) (*defs.PathAddPublisherRes, error) {\n\tselect {\n\tcase pa.chAddPublisher <- req:\n\t\tres := <-req.Res\n\t\treturn &res, res.Err\n\tcase <-pa.ctx.Done():\n\t\treturn nil, fmt.Errorf(\"terminated\")\n\t}\n}\n\n// RemovePublisher is called by a publisher.\nfunc (pa *path) RemovePublisher(req defs.PathRemovePublisherReq) {\n\treq.Res = make(chan struct{})\n\tselect {\n\tcase pa.chRemovePublisher <- req:\n\t\t<-req.Res\n\tcase <-pa.ctx.Done():\n\t}\n}\n\n// addReader is called by a reader through pathManager.\nfunc (pa *path) addReader(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error) {\n\tselect {\n\tcase pa.chAddReader <- req:\n\t\tres := <-req.Res\n\t\treturn &res, res.Err\n\tcase <-pa.ctx.Done():\n\t\treturn nil, fmt.Errorf(\"terminated\")\n\t}\n}\n\n// RemoveReader is called by a reader.\nfunc (pa *path) RemoveReader(req defs.PathRemoveReaderReq) {\n\treq.Res = make(chan struct{})\n\tselect {\n\tcase pa.chRemoveReader <- req:\n\t\t<-req.Res\n\tcase <-pa.ctx.Done():\n\t}\n}\n\n// APIPathsGet is called by api.\nfunc (pa *path) APIPathsGet(req pathAPIPathsGetReq) (*defs.APIPath, error) {\n\treq.res = make(chan pathAPIPathsGetRes)\n\tselect {\n\tcase pa.chAPIPathsGet <- req:\n\t\tres := <-req.res\n\t\treturn res.data, res.err\n\n\tcase <-pa.ctx.Done():\n\t\treturn nil, fmt.Errorf(\"terminated\")\n\t}\n}\n"
  },
  {
    "path": "internal/core/path_manager.go",
    "content": "package core\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"maps\"\n\t\"sort\"\n\t\"sync\"\n\t\"sync/atomic\"\n\n\t\"github.com/bluenviron/mediamtx/internal/auth\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/externalcmd\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/metrics\"\n\t\"github.com/bluenviron/mediamtx/internal/servers/hls\"\n)\n\nfunc pathConfCanBeUpdated(oldPathConf *conf.Path, newPathConf *conf.Path) bool {\n\tclone := oldPathConf.Clone()\n\n\tclone.Name = newPathConf.Name\n\tclone.Regexp = newPathConf.Regexp\n\n\tclone.Record = newPathConf.Record\n\tclone.RecordPath = newPathConf.RecordPath\n\tclone.RecordFormat = newPathConf.RecordFormat\n\tclone.RecordPartDuration = newPathConf.RecordPartDuration\n\tclone.RecordMaxPartSize = newPathConf.RecordMaxPartSize\n\tclone.RecordSegmentDuration = newPathConf.RecordSegmentDuration\n\tclone.RecordDeleteAfter = newPathConf.RecordDeleteAfter\n\n\tclone.RPICameraBrightness = newPathConf.RPICameraBrightness\n\tclone.RPICameraContrast = newPathConf.RPICameraContrast\n\tclone.RPICameraSaturation = newPathConf.RPICameraSaturation\n\tclone.RPICameraSharpness = newPathConf.RPICameraSharpness\n\tclone.RPICameraExposure = newPathConf.RPICameraExposure\n\tclone.RPICameraFlickerPeriod = newPathConf.RPICameraFlickerPeriod\n\tclone.RPICameraAWB = newPathConf.RPICameraAWB\n\tclone.RPICameraAWBGains = newPathConf.RPICameraAWBGains\n\tclone.RPICameraDenoise = newPathConf.RPICameraDenoise\n\tclone.RPICameraShutter = newPathConf.RPICameraShutter\n\tclone.RPICameraMetering = newPathConf.RPICameraMetering\n\tclone.RPICameraGain = newPathConf.RPICameraGain\n\tclone.RPICameraEV = newPathConf.RPICameraEV\n\tclone.RPICameraFPS = newPathConf.RPICameraFPS\n\tclone.RPICameraIDRPeriod = newPathConf.RPICameraIDRPeriod\n\tclone.RPICameraBitrate = newPathConf.RPICameraBitrate\n\n\treturn newPathConf.Equal(clone)\n}\n\ntype pathSetHLSServerRes struct {\n\treadyPaths []defs.Path\n}\n\ntype pathSetHLSServerReq struct {\n\ts   *hls.Server\n\tres chan pathSetHLSServerRes\n}\n\ntype pathManagerAuthManager interface {\n\tAuthenticate(req *auth.Request) (string, *auth.Error)\n}\n\ntype pathManagerParent interface {\n\tlogger.Writer\n}\n\ntype pathManager struct {\n\tlogLevel          conf.LogLevel\n\trtspAddress       string\n\tdumpPackets       bool\n\treadTimeout       conf.Duration\n\twriteTimeout      conf.Duration\n\twriteQueueSize    int\n\tudpReadBufferSize uint\n\trtpMaxPayloadSize int\n\tpathConfs         map[string]*conf.Path\n\tauthManager       pathManagerAuthManager\n\texternalCmdPool   *externalcmd.Pool\n\tmetrics           *metrics.Metrics\n\tparent            pathManagerParent\n\n\tctx       context.Context\n\tctxCancel func()\n\twg        sync.WaitGroup\n\thlsServer *hls.Server\n\tpaths     map[string]*path\n\n\t// in\n\tchReloadConf      chan map[string]*conf.Path\n\tchSetHLSServer    chan pathSetHLSServerReq\n\tchRemovePath      chan *path\n\tchClosePathIfIdle chan *path\n\tchSetPathReady    chan *path\n\tchSetPathNotReady chan *path\n\tchFindPathConf    chan defs.PathFindPathConfReq\n\tchDescribe        chan defs.PathDescribeReq\n\tchAddReader       chan defs.PathAddReaderReq\n\tchAddPublisher    chan defs.PathAddPublisherReq\n\tchAPIPathsList    chan pathAPIPathsListReq\n\tchAPIPathsGet     chan pathAPIPathsGetReq\n}\n\nfunc (pm *pathManager) initialize() {\n\tctx, ctxCancel := context.WithCancel(context.Background())\n\n\tpm.ctx = ctx\n\tpm.ctxCancel = ctxCancel\n\tpm.paths = make(map[string]*path)\n\tpm.chReloadConf = make(chan map[string]*conf.Path)\n\tpm.chSetHLSServer = make(chan pathSetHLSServerReq)\n\tpm.chRemovePath = make(chan *path)\n\tpm.chClosePathIfIdle = make(chan *path)\n\tpm.chSetPathReady = make(chan *path)\n\tpm.chSetPathNotReady = make(chan *path)\n\tpm.chFindPathConf = make(chan defs.PathFindPathConfReq)\n\tpm.chDescribe = make(chan defs.PathDescribeReq)\n\tpm.chAddReader = make(chan defs.PathAddReaderReq)\n\tpm.chAddPublisher = make(chan defs.PathAddPublisherReq)\n\tpm.chAPIPathsList = make(chan pathAPIPathsListReq)\n\tpm.chAPIPathsGet = make(chan pathAPIPathsGetReq)\n\n\tfor _, pathConf := range pm.pathConfs {\n\t\tif pathConf.Regexp == nil {\n\t\t\tpm.createPath(pathConf, pathConf.Name, nil)\n\t\t}\n\t}\n\n\tpm.Log(logger.Debug, \"path manager created\")\n\n\tpm.wg.Add(1)\n\tgo pm.run()\n\n\tif pm.metrics != nil {\n\t\tpm.metrics.SetPathManager(pm)\n\t}\n}\n\nfunc (pm *pathManager) close() {\n\tpm.Log(logger.Debug, \"path manager is shutting down\")\n\n\tif pm.metrics != nil {\n\t\tpm.metrics.SetPathManager(nil)\n\t}\n\n\tpm.ctxCancel()\n\tpm.wg.Wait()\n}\n\n// Log implements logger.Writer.\nfunc (pm *pathManager) Log(level logger.Level, format string, args ...any) {\n\tpm.parent.Log(level, format, args...)\n}\n\nfunc (pm *pathManager) run() {\n\tdefer pm.wg.Done()\n\nouter:\n\tfor {\n\t\tselect {\n\t\tcase newPaths := <-pm.chReloadConf:\n\t\t\tpm.doReloadConf(newPaths)\n\n\t\tcase req := <-pm.chSetHLSServer:\n\t\t\treadyPaths := pm.doSetHLSServer(req.s)\n\t\t\treq.res <- pathSetHLSServerRes{readyPaths: readyPaths}\n\n\t\tcase pa := <-pm.chRemovePath:\n\t\t\tif pa2, ok := pm.paths[pa.name]; ok && pa2 == pa {\n\t\t\t\tdelete(pm.paths, pa.name)\n\t\t\t}\n\n\t\tcase pa := <-pm.chClosePathIfIdle:\n\t\t\tif atomic.LoadInt64(pa.pendingRequests) == 0 {\n\t\t\t\tpm.doClosePath(pa)\n\t\t\t}\n\n\t\tcase pa := <-pm.chSetPathReady:\n\t\t\tpm.doSetPathReady(pa)\n\n\t\tcase pa := <-pm.chSetPathNotReady:\n\t\t\tpm.doSetPathNotReady(pa)\n\n\t\tcase req := <-pm.chFindPathConf:\n\t\t\tpm.doFindPathConf(req)\n\n\t\tcase req := <-pm.chDescribe:\n\t\t\tpm.doDescribe(req)\n\n\t\tcase req := <-pm.chAddReader:\n\t\t\tpm.doAddReader(req)\n\n\t\tcase req := <-pm.chAddPublisher:\n\t\t\tpm.doAddPublisher(req)\n\n\t\tcase req := <-pm.chAPIPathsList:\n\t\t\tpm.doAPIPathsList(req)\n\n\t\tcase req := <-pm.chAPIPathsGet:\n\t\t\tpm.doAPIPathsGet(req)\n\n\t\tcase <-pm.ctx.Done():\n\t\t\tbreak outer\n\t\t}\n\t}\n\n\tpm.ctxCancel()\n}\n\nfunc (pm *pathManager) doReloadConf(newPaths map[string]*conf.Path) {\n\tconfsToRecreate := make(map[string]struct{})\n\tconfsToReload := make(map[string]struct{})\n\n\tfor confName, pathConf := range pm.pathConfs {\n\t\tif newPath, ok := newPaths[confName]; ok {\n\t\t\tif !newPath.Equal(pathConf) {\n\t\t\t\tif pathConfCanBeUpdated(pathConf, newPath) {\n\t\t\t\t\tconfsToReload[confName] = struct{}{}\n\t\t\t\t} else {\n\t\t\t\t\tconfsToRecreate[confName] = struct{}{}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// process existing paths\n\tfor pathName, pa := range pm.paths {\n\t\tnewPathConf, _, err := conf.FindPathConf(newPaths, pathName)\n\t\t// path does not have a config anymore: delete it\n\t\tif err != nil {\n\t\t\tpm.doClosePath(pa)\n\t\t\tcontinue\n\t\t}\n\n\t\t// path now belongs to a different config\n\t\tif newPathConf.Name != pa.confName {\n\t\t\t// path config can be hot reloaded\n\t\t\toldPathConf := pm.pathConfs[pa.confName]\n\t\t\tif pathConfCanBeUpdated(oldPathConf, newPathConf) {\n\t\t\t\tpa.confName = newPathConf.Name\n\t\t\t\tgo pa.reloadConf(newPathConf)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Configuration cannot be hot reloaded: delete the path\n\t\t\tpm.doClosePath(pa)\n\t\t\tcontinue\n\t\t}\n\n\t\t// path configuration has changed and cannot be hot reloaded: delete path\n\t\tif _, ok := confsToRecreate[newPathConf.Name]; ok {\n\t\t\tpm.doClosePath(pa)\n\t\t\tcontinue\n\t\t}\n\n\t\t// path configuration has changed but can be hot reloaded: reload it\n\t\tif _, ok := confsToReload[newPathConf.Name]; ok {\n\t\t\tgo pa.reloadConf(newPathConf)\n\t\t}\n\t}\n\n\tpm.pathConfs = newPaths\n\n\t// create new static paths\n\tfor pathConfName, pathConf := range newPaths {\n\t\tif pathConf.Regexp == nil {\n\t\t\tif _, ok := pm.paths[pathConfName]; !ok {\n\t\t\t\tpm.createPath(pathConf, pathConfName, nil)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (pm *pathManager) doClosePath(pa *path) {\n\tdelete(pm.paths, pa.name)\n\tpa.close()\n\tpa.wait() // avoid conflicts between sources\n}\n\nfunc (pm *pathManager) doSetHLSServer(m *hls.Server) []defs.Path {\n\tpm.hlsServer = m\n\n\tvar ret []defs.Path\n\n\tfor _, pa := range pm.paths {\n\t\tif pa.ready {\n\t\t\tret = append(ret, pa)\n\t\t}\n\t}\n\n\treturn ret\n}\n\nfunc (pm *pathManager) doSetPathReady(pa *path) {\n\tif pa2, ok := pm.paths[pa.name]; !ok || pa2 != pa {\n\t\treturn\n\t}\n\n\tpm.paths[pa.name].ready = true\n\n\tif pm.hlsServer != nil {\n\t\tpm.hlsServer.PathReady(pa)\n\t}\n}\n\nfunc (pm *pathManager) doSetPathNotReady(pa *path) {\n\tif pa2, ok := pm.paths[pa.name]; !ok || pa2 != pa {\n\t\treturn\n\t}\n\n\tpm.paths[pa.name].ready = false\n\n\tif pm.hlsServer != nil {\n\t\tpm.hlsServer.PathNotReady(pa)\n\t}\n}\n\nfunc (pm *pathManager) doFindPathConf(req defs.PathFindPathConfReq) {\n\tpathConf, _, err := conf.FindPathConf(pm.pathConfs, req.AccessRequest.Name)\n\tif err != nil {\n\t\treq.Res <- defs.PathFindPathConfRes{Err: err}\n\t\treturn\n\t}\n\n\tuser, err2 := pm.authManager.Authenticate(req.AccessRequest.ToAuthRequest())\n\tif err2 != nil {\n\t\treq.Res <- defs.PathFindPathConfRes{Err: err2}\n\t\treturn\n\t}\n\n\treq.Res <- defs.PathFindPathConfRes{\n\t\tConf: pathConf,\n\t\tUser: user,\n\t}\n}\n\nfunc (pm *pathManager) doDescribe(req defs.PathDescribeReq) {\n\tpathConf, pathMatches, err := conf.FindPathConf(pm.pathConfs, req.AccessRequest.Name)\n\tif err != nil {\n\t\treq.Res <- defs.PathDescribeRes{Err: err}\n\t\treturn\n\t}\n\n\t_, err2 := pm.authManager.Authenticate(req.AccessRequest.ToAuthRequest())\n\tif err2 != nil {\n\t\treq.Res <- defs.PathDescribeRes{Err: err2}\n\t\treturn\n\t}\n\n\t// create path if it doesn't exist\n\tif _, ok := pm.paths[req.AccessRequest.Name]; !ok {\n\t\tpm.createPath(pathConf, req.AccessRequest.Name, pathMatches)\n\t}\n\n\tpa := pm.paths[req.AccessRequest.Name]\n\n\tatomic.AddInt64(pa.pendingRequests, 1)\n\n\treq.Res <- defs.PathDescribeRes{Path: pa}\n}\n\nfunc (pm *pathManager) doAddReader(req defs.PathAddReaderReq) {\n\tpathConf, pathMatches, err := conf.FindPathConf(pm.pathConfs, req.AccessRequest.Name)\n\tif err != nil {\n\t\treq.Res <- defs.PathAddReaderRes{Err: err}\n\t\treturn\n\t}\n\n\tvar user string\n\n\tif !req.AccessRequest.SkipAuth {\n\t\tvar authErr *auth.Error\n\t\tuser, authErr = pm.authManager.Authenticate(req.AccessRequest.ToAuthRequest())\n\t\tif authErr != nil {\n\t\t\treq.Res <- defs.PathAddReaderRes{Err: authErr}\n\t\t\treturn\n\t\t}\n\t}\n\n\t// create path if it doesn't exist\n\tif _, ok := pm.paths[req.AccessRequest.Name]; !ok {\n\t\tpm.createPath(pathConf, req.AccessRequest.Name, pathMatches)\n\t}\n\n\tpa := pm.paths[req.AccessRequest.Name]\n\n\tatomic.AddInt64(pa.pendingRequests, 1)\n\n\treq.Res <- defs.PathAddReaderRes{\n\t\tPath: pa,\n\t\tUser: user,\n\t}\n}\n\nfunc (pm *pathManager) doAddPublisher(req defs.PathAddPublisherReq) {\n\tpathConf, pathMatches, err := conf.FindPathConf(pm.pathConfs, req.AccessRequest.Name)\n\tif err != nil {\n\t\treq.Res <- defs.PathAddPublisherRes{Err: err}\n\t\treturn\n\t}\n\n\tif req.ConfToCompare != nil && !pathConf.Equal(req.ConfToCompare) {\n\t\treq.Res <- defs.PathAddPublisherRes{Err: fmt.Errorf(\"configuration has changed\")}\n\t\treturn\n\t}\n\n\tvar user string\n\n\tif !req.AccessRequest.SkipAuth {\n\t\tvar authErr *auth.Error\n\t\tuser, authErr = pm.authManager.Authenticate(req.AccessRequest.ToAuthRequest())\n\t\tif authErr != nil {\n\t\t\treq.Res <- defs.PathAddPublisherRes{Err: authErr}\n\t\t\treturn\n\t\t}\n\t}\n\n\t// create path if it doesn't exist\n\tif _, ok := pm.paths[req.AccessRequest.Name]; !ok {\n\t\tpm.createPath(pathConf, req.AccessRequest.Name, pathMatches)\n\t}\n\n\tpa := pm.paths[req.AccessRequest.Name]\n\n\tatomic.AddInt64(pa.pendingRequests, 1)\n\n\treq.Res <- defs.PathAddPublisherRes{\n\t\tPath: pa,\n\t\tUser: user,\n\t}\n}\n\nfunc (pm *pathManager) doAPIPathsList(req pathAPIPathsListReq) {\n\tpaths := make(map[string]*path)\n\tmaps.Copy(paths, pm.paths)\n\n\treq.res <- pathAPIPathsListRes{paths: paths}\n}\n\nfunc (pm *pathManager) doAPIPathsGet(req pathAPIPathsGetReq) {\n\tpa, ok := pm.paths[req.name]\n\tif !ok {\n\t\treq.res <- pathAPIPathsGetRes{err: conf.ErrPathNotFound}\n\t\treturn\n\t}\n\n\treq.res <- pathAPIPathsGetRes{path: pa}\n}\n\nfunc (pm *pathManager) createPath(\n\tpathConf *conf.Path,\n\tname string,\n\tmatches []string,\n) {\n\tpa := &path{\n\t\tparentCtx:         pm.ctx,\n\t\tlogLevel:          pm.logLevel,\n\t\tdumpPackets:       pm.dumpPackets,\n\t\trtspAddress:       pm.rtspAddress,\n\t\treadTimeout:       pm.readTimeout,\n\t\twriteTimeout:      pm.writeTimeout,\n\t\twriteQueueSize:    pm.writeQueueSize,\n\t\tudpReadBufferSize: pm.udpReadBufferSize,\n\t\trtpMaxPayloadSize: pm.rtpMaxPayloadSize,\n\t\tconf:              pathConf,\n\t\tname:              name,\n\t\tmatches:           matches,\n\t\twg:                &pm.wg,\n\t\texternalCmdPool:   pm.externalCmdPool,\n\t\tparent:            pm,\n\t}\n\tpa.initialize()\n\tpm.paths[name] = pa\n}\n\n// ReloadPathConfs is called by core.\nfunc (pm *pathManager) ReloadPathConfs(pathConfs map[string]*conf.Path) {\n\tselect {\n\tcase pm.chReloadConf <- pathConfs:\n\tcase <-pm.ctx.Done():\n\t}\n}\n\n// setPathReady is called by path.\nfunc (pm *pathManager) setPathReady(pa *path) {\n\tselect {\n\tcase pm.chSetPathReady <- pa:\n\tcase <-pm.ctx.Done():\n\tcase <-pa.ctx.Done(): // in case pathManager is blocked by path.wait()\n\t}\n}\n\n// setPathNotReady is called by path.\nfunc (pm *pathManager) setPathNotReady(pa *path) {\n\tselect {\n\tcase pm.chSetPathNotReady <- pa:\n\tcase <-pm.ctx.Done():\n\tcase <-pa.ctx.Done(): // in case pathManager is blocked by path.wait()\n\t}\n}\n\n// removePath is called by path.\nfunc (pm *pathManager) removePath(pa *path) {\n\tselect {\n\tcase pm.chRemovePath <- pa:\n\tcase <-pm.ctx.Done():\n\tcase <-pa.ctx.Done(): // in case pathManager is blocked by path.wait()\n\t}\n}\n\n// closePath is called by path.\nfunc (pm *pathManager) closePathIfIdle(pa *path) {\n\tselect {\n\tcase pm.chClosePathIfIdle <- pa:\n\tcase <-pm.ctx.Done():\n\tcase <-pa.ctx.Done(): // in case pathManager is blocked by path.wait()\n\t}\n}\n\n// FindPathConf is called by a reader or publisher.\nfunc (pm *pathManager) FindPathConf(req defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error) {\n\treq.Res = make(chan defs.PathFindPathConfRes)\n\tselect {\n\tcase pm.chFindPathConf <- req:\n\t\tres := <-req.Res\n\t\treturn &res, res.Err\n\n\tcase <-pm.ctx.Done():\n\t\treturn nil, fmt.Errorf(\"terminated\")\n\t}\n}\n\n// Describe is called by a reader or publisher.\nfunc (pm *pathManager) Describe(req defs.PathDescribeReq) defs.PathDescribeRes {\n\treq.Res = make(chan defs.PathDescribeRes)\n\tselect {\n\tcase pm.chDescribe <- req:\n\t\tres1 := <-req.Res\n\t\tif res1.Err != nil {\n\t\t\treturn res1\n\t\t}\n\n\t\tres2 := res1.Path.(*path).describe(req)\n\t\tif res2.Err != nil {\n\t\t\treturn res2\n\t\t}\n\n\t\tres2.Path = res1.Path\n\t\treturn res2\n\n\tcase <-pm.ctx.Done():\n\t\treturn defs.PathDescribeRes{Err: fmt.Errorf(\"terminated\")}\n\t}\n}\n\n// AddPublisher is called by a publisher.\nfunc (pm *pathManager) AddPublisher(req defs.PathAddPublisherReq) (*defs.PathAddPublisherRes, error) {\n\treq.Res = make(chan defs.PathAddPublisherRes)\n\tselect {\n\tcase pm.chAddPublisher <- req:\n\t\tres1 := <-req.Res\n\t\tif res1.Err != nil {\n\t\t\treturn nil, res1.Err\n\t\t}\n\n\t\tres2, err := res1.Path.(*path).addPublisher(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tres2.Path = res1.Path\n\t\tres2.User = res1.User\n\n\t\treturn res2, nil\n\n\tcase <-pm.ctx.Done():\n\t\treturn nil, fmt.Errorf(\"terminated\")\n\t}\n}\n\n// AddReader is called by a reader.\nfunc (pm *pathManager) AddReader(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error) {\n\treq.Res = make(chan defs.PathAddReaderRes)\n\tselect {\n\tcase pm.chAddReader <- req:\n\t\tres1 := <-req.Res\n\t\tif res1.Err != nil {\n\t\t\treturn nil, res1.Err\n\t\t}\n\n\t\tres2, err := res1.Path.(*path).addReader(req)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tres2.Path = res1.Path\n\t\tres2.User = res1.User\n\n\t\treturn res2, nil\n\n\tcase <-pm.ctx.Done():\n\t\treturn nil, fmt.Errorf(\"terminated\")\n\t}\n}\n\n// SetHLSServer is called by hls.Server.\nfunc (pm *pathManager) SetHLSServer(s *hls.Server) []defs.Path {\n\treq := pathSetHLSServerReq{\n\t\ts:   s,\n\t\tres: make(chan pathSetHLSServerRes),\n\t}\n\n\tselect {\n\tcase pm.chSetHLSServer <- req:\n\t\tres := <-req.res\n\t\treturn res.readyPaths\n\n\tcase <-pm.ctx.Done():\n\t\treturn nil\n\t}\n}\n\n// APIPathsList is called by api.\nfunc (pm *pathManager) APIPathsList() (*defs.APIPathList, error) {\n\treq := pathAPIPathsListReq{\n\t\tres: make(chan pathAPIPathsListRes),\n\t}\n\n\tselect {\n\tcase pm.chAPIPathsList <- req:\n\t\tres := <-req.res\n\n\t\tres.data = &defs.APIPathList{\n\t\t\tItems: []defs.APIPath{},\n\t\t}\n\n\t\tfor _, pa := range res.paths {\n\t\t\titem, err := pa.APIPathsGet(pathAPIPathsGetReq{})\n\t\t\tif err == nil {\n\t\t\t\tres.data.Items = append(res.data.Items, *item)\n\t\t\t}\n\t\t}\n\n\t\tsort.Slice(res.data.Items, func(i, j int) bool {\n\t\t\treturn res.data.Items[i].Name < res.data.Items[j].Name\n\t\t})\n\n\t\treturn res.data, nil\n\n\tcase <-pm.ctx.Done():\n\t\treturn nil, fmt.Errorf(\"terminated\")\n\t}\n}\n\n// APIPathsGet is called by api.\nfunc (pm *pathManager) APIPathsGet(name string) (*defs.APIPath, error) {\n\treq := pathAPIPathsGetReq{\n\t\tname: name,\n\t\tres:  make(chan pathAPIPathsGetRes),\n\t}\n\n\tselect {\n\tcase pm.chAPIPathsGet <- req:\n\t\tres := <-req.res\n\t\tif res.err != nil {\n\t\t\treturn nil, res.err\n\t\t}\n\n\t\tdata, err := res.path.APIPathsGet(req)\n\t\treturn data, err\n\n\tcase <-pm.ctx.Done():\n\t\treturn nil, fmt.Errorf(\"terminated\")\n\t}\n}\n"
  },
  {
    "path": "internal/core/path_manager_test.go",
    "content": "package core\n\nimport (\n\t\"net/http\"\n\t\"regexp\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/pion/rtp\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n)\n\ntype dummyPublisher struct{}\n\nfunc (d *dummyPublisher) Close() {}\n\nfunc (d *dummyPublisher) Log(_ logger.Level, _ string, _ ...any) {}\n\nfunc (d *dummyPublisher) APISourceDescribe() *defs.APIPathSource {\n\treturn nil\n}\n\ntype dummyReader struct{}\n\nfunc (d *dummyReader) Close() {}\n\nfunc (d *dummyReader) Log(_ logger.Level, _ string, _ ...any) {}\n\nfunc (d *dummyReader) APIReaderDescribe() *defs.APIPathReader {\n\treturn nil\n}\n\nfunc TestPathManagerDynamicPathAutoDeletion(t *testing.T) {\n\tfor _, ca := range []string{\"describe\", \"add reader\"} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tpathConfs := map[string]*conf.Path{\n\t\t\t\t\"all_others\": {\n\t\t\t\t\tRegexp: regexp.MustCompile(\"^.*$\"),\n\t\t\t\t\tName:   \"all_others\",\n\t\t\t\t\tSource: \"publisher\",\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tpm := &pathManager{\n\t\t\t\tauthManager: test.NilAuthManager,\n\t\t\t\tpathConfs:   pathConfs,\n\t\t\t\tparent:      test.NilLogger,\n\t\t\t}\n\t\t\tpm.initialize()\n\t\t\tdefer pm.close()\n\n\t\t\tfunc() {\n\t\t\t\tif ca == \"describe\" {\n\t\t\t\t\tres := pm.Describe(defs.PathDescribeReq{\n\t\t\t\t\t\tAccessRequest: defs.PathAccessRequest{\n\t\t\t\t\t\t\tName: \"mypath\",\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t\trequire.EqualError(t, res.Err, \"no stream is available on path 'mypath'\")\n\t\t\t\t} else {\n\t\t\t\t\t_, err := pm.AddReader(defs.PathAddReaderReq{\n\t\t\t\t\t\tAuthor: &dummyReader{},\n\t\t\t\t\t\tAccessRequest: defs.PathAccessRequest{\n\t\t\t\t\t\t\tName: \"mypath\",\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t\trequire.EqualError(t, err, \"no stream is available on path 'mypath'\")\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\t\tdata, err := pm.APIPathsList()\n\t\t\trequire.NoError(t, err)\n\n\t\t\trequire.Empty(t, data.Items)\n\t\t})\n\t}\n}\n\nfunc TestPathManagerDynamicPathDescribeAndPublish(t *testing.T) {\n\tpathConfs := map[string]*conf.Path{\n\t\t\"all_others\": {\n\t\t\tRegexp: regexp.MustCompile(\"^.*$\"),\n\t\t\tName:   \"all_others\",\n\t\t\tSource: \"publisher\",\n\t\t},\n\t}\n\n\tpm := &pathManager{\n\t\tauthManager: test.NilAuthManager,\n\t\tpathConfs:   pathConfs,\n\t\tparent:      test.NilLogger,\n\t}\n\tpm.initialize()\n\tdefer pm.close()\n\n\tgo func() {\n\t\tfor range 10 {\n\t\t\tpm.Describe(defs.PathDescribeReq{\n\t\t\t\tAccessRequest: defs.PathAccessRequest{\n\t\t\t\t\tName: \"mypath\",\n\t\t\t\t},\n\t\t\t})\n\t\t}\n\t}()\n\n\t_, err := pm.AddPublisher(defs.PathAddPublisherReq{\n\t\tAuthor: &dummyPublisher{},\n\t\tDesc:   &description.Session{},\n\t\tAccessRequest: defs.PathAccessRequest{\n\t\t\tName: \"mypath\",\n\t\t},\n\t})\n\trequire.NoError(t, err)\n}\n\nfunc TestPathManagerConfigHotReload(t *testing.T) {\n\t// Start MediaMTX with basic configuration\n\tp, ok := newInstance(\"api: yes\\n\" +\n\t\t\"paths:\\n\" +\n\t\t\"  all:\\n\" +\n\t\t\"    record: no\\n\")\n\trequire.Equal(t, true, ok)\n\tdefer p.Close()\n\n\t// Set up HTTP client for API calls\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\t// Create a publisher that will use the \"all\" configuration\n\tmedia0 := test.UniqueMediaH264()\n\tsource := gortsplib.Client{}\n\terr := source.StartRecording(\n\t\t\"rtsp://localhost:8554/undefined_stream\",\n\t\t&description.Session{Medias: []*description.Media{media0}})\n\trequire.NoError(t, err)\n\tdefer source.Close()\n\n\t// Send some data to establish the stream\n\terr = source.WritePacketRTP(media0, &rtp.Packet{\n\t\tHeader: rtp.Header{\n\t\t\tVersion:     2,\n\t\t\tPayloadType: 96,\n\t\t},\n\t\tPayload: []byte{5, 1, 2, 3, 4},\n\t})\n\trequire.NoError(t, err)\n\n\ttime.Sleep(100 * time.Millisecond)\n\n\t// Verify the path exists and is using the \"all\" configuration\n\tpathData, err := p.pathManager.APIPathsGet(\"undefined_stream\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"undefined_stream\", pathData.Name)\n\trequire.Equal(t, \"all\", pathData.ConfName)\n\n\t// Check the current configuration via API\n\tvar allConfig map[string]any\n\thttpRequest(t, hc, http.MethodGet, \"http://localhost:9997/v3/config/paths/get/all\", nil, &allConfig)\n\trequire.Equal(t, false, allConfig[\"record\"]) // Should be false from \"all\" config\n\n\t// Add a new specific configuration for \"undefined_stream\" with record enabled\n\thttpRequest(t, hc, http.MethodPost, \"http://localhost:9997/v3/config/paths/add/undefined_stream\",\n\t\tmap[string]any{\n\t\t\t\"record\": true,\n\t\t}, nil)\n\n\t// Give the system time to process the configuration change\n\ttime.Sleep(200 * time.Millisecond)\n\n\t// Verify the path now uses the new specific configuration\n\tpathData, err = p.pathManager.APIPathsGet(\"undefined_stream\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"undefined_stream\", pathData.Name)\n\trequire.Equal(t, \"undefined_stream\", pathData.ConfName) // Should now use the specific config\n\n\t// Check the new configuration via API\n\tvar newConfig map[string]any\n\thttpRequest(t, hc, http.MethodGet, \"http://localhost:9997/v3/config/paths/get/undefined_stream\", nil, &newConfig)\n\trequire.Equal(t, true, newConfig[\"record\"]) // Should be true from new config\n\n\t// Verify the stream is still active and working\n\terr = source.WritePacketRTP(media0, &rtp.Packet{\n\t\tHeader: rtp.Header{\n\t\t\tVersion:        2,\n\t\t\tPayloadType:    96,\n\t\t\tSequenceNumber: 2,\n\t\t},\n\t\tPayload: []byte{5, 1, 2, 3, 4},\n\t})\n\trequire.NoError(t, err)\n\n\t// Verify the path is still ready and functional\n\trequire.Equal(t, true, pathData.Ready)\n\n\t// revert configuration\n\thttpRequest(t, hc, http.MethodDelete, \"http://localhost:9997/v3/config/paths/delete/undefined_stream\",\n\t\tnil, nil)\n\n\t// Give the system time to process the configuration change\n\ttime.Sleep(200 * time.Millisecond)\n\n\t// Verify the path now uses the old configuration\n\tpathData, err = p.pathManager.APIPathsGet(\"undefined_stream\")\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"undefined_stream\", pathData.Name)\n\trequire.Equal(t, \"all\", pathData.ConfName)\n}\n"
  },
  {
    "path": "internal/core/path_test.go",
    "content": "package core\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortmplib\"\n\t\"github.com/bluenviron/gortsplib/v5\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/base\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/headers\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/sdp\"\n\tsrt \"github.com/datarhei/gosrt\"\n\t\"github.com/pion/rtp\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/whip\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n)\n\ntype testServer struct {\n\tonDescribe func(*gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error)\n\tonSetup    func(*gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error)\n\tonPlay     func(*gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error)\n}\n\nfunc (sh *testServer) OnDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,\n) (*base.Response, *gortsplib.ServerStream, error) {\n\treturn sh.onDescribe(ctx)\n}\n\nfunc (sh *testServer) OnSetup(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) {\n\treturn sh.onSetup(ctx)\n}\n\nfunc (sh *testServer) OnPlay(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) {\n\treturn sh.onPlay(ctx)\n}\n\nvar _ defs.Path = &path{}\n\nfunc TestPathRunOnDemand(t *testing.T) {\n\tonDemand := filepath.Join(os.TempDir(), \"on_demand\")\n\tonUnDemand := filepath.Join(os.TempDir(), \"on_undemand\")\n\n\tfor _, ca := range []string{\"describe\", \"setup\", \"describe and setup\"} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tdefer os.Remove(onDemand)\n\t\t\tdefer os.Remove(onUnDemand)\n\n\t\t\tp1, ok := newInstance(fmt.Sprintf(\"rtmp: no\\n\"+\n\t\t\t\t\"hls: no\\n\"+\n\t\t\t\t\"webrtc: no\\n\"+\n\t\t\t\t\"paths:\\n\"+\n\t\t\t\t\"  '~^(on)demand$':\\n\"+\n\t\t\t\t\"    runOnDemand: sh -c \\\"ON_DEMAND=%s go run ./test_on_demand/main.go\\\"\\n\"+\n\t\t\t\t\"    runOnDemandCloseAfter: 1s\\n\"+\n\t\t\t\t\"    runOnUnDemand: touch %s\\n\", onDemand, onUnDemand))\n\t\t\trequire.Equal(t, true, ok)\n\t\t\tdefer p1.Close()\n\n\t\t\tvar control string\n\n\t\t\tfunc() {\n\t\t\t\tconn, err := net.Dial(\"tcp\", \"localhost:8554\")\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer conn.Close()\n\t\t\t\tbr := bufio.NewReader(conn)\n\n\t\t\t\tif ca == \"describe\" || ca == \"describe and setup\" {\n\t\t\t\t\tvar u *base.URL\n\t\t\t\t\tu, err = base.ParseURL(\"rtsp://localhost:8554/ondemand?param=value\")\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\tbyts, _ := base.Request{\n\t\t\t\t\t\tMethod: base.Describe,\n\t\t\t\t\t\tURL:    u,\n\t\t\t\t\t\tHeader: base.Header{\n\t\t\t\t\t\t\t\"CSeq\": base.HeaderValue{\"1\"},\n\t\t\t\t\t\t},\n\t\t\t\t\t}.Marshal()\n\t\t\t\t\t_, err = conn.Write(byts)\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\tvar res base.Response\n\t\t\t\t\terr = res.Unmarshal(br)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.Equal(t, base.StatusOK, res.StatusCode)\n\n\t\t\t\t\tvar desc sdp.SessionDescription\n\t\t\t\t\terr = desc.Unmarshal(res.Body)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\tcontrol, _ = desc.MediaDescriptions[0].Attribute(\"control\")\n\t\t\t\t\tcontrol = \"rtsp://localhost:8554/ondemand?param=value/\" + control\n\t\t\t\t} else {\n\t\t\t\t\tcontrol = \"rtsp://localhost:8554/ondemand?param=value/\"\n\t\t\t\t}\n\n\t\t\t\tif ca == \"setup\" || ca == \"describe and setup\" {\n\t\t\t\t\tvar u *base.URL\n\t\t\t\t\tu, err = base.ParseURL(control)\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\tbyts, _ := base.Request{\n\t\t\t\t\t\tMethod: base.Setup,\n\t\t\t\t\t\tURL:    u,\n\t\t\t\t\t\tHeader: base.Header{\n\t\t\t\t\t\t\t\"CSeq\": base.HeaderValue{\"2\"},\n\t\t\t\t\t\t\t\"Transport\": headers.Transport{\n\t\t\t\t\t\t\t\tMode: func() *headers.TransportMode {\n\t\t\t\t\t\t\t\t\tv := headers.TransportModePlay\n\t\t\t\t\t\t\t\t\treturn &v\n\t\t\t\t\t\t\t\t}(),\n\t\t\t\t\t\t\t\tProtocol:       headers.TransportProtocolTCP,\n\t\t\t\t\t\t\t\tInterleavedIDs: &[2]int{0, 1},\n\t\t\t\t\t\t\t}.Marshal(),\n\t\t\t\t\t\t},\n\t\t\t\t\t}.Marshal()\n\t\t\t\t\t_, err = conn.Write(byts)\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\tvar res base.Response\n\t\t\t\t\terr = res.Unmarshal(br)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\trequire.Equal(t, base.StatusOK, res.StatusCode)\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\tfor {\n\t\t\t\t_, err := os.Stat(onUnDemand)\n\t\t\t\tif err == nil {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t\t}\n\n\t\t\t_, err := os.Stat(onDemand)\n\t\t\trequire.NoError(t, err)\n\t\t})\n\t}\n}\n\nfunc TestPathRunOnConnect(t *testing.T) {\n\tserverCertFpath, err := test.CreateTempFile(test.TLSCertPub)\n\trequire.NoError(t, err)\n\tdefer os.Remove(serverCertFpath)\n\n\tserverKeyFpath, err := test.CreateTempFile(test.TLSCertKey)\n\trequire.NoError(t, err)\n\tdefer os.Remove(serverKeyFpath)\n\n\tfor _, ca := range []string{\"rtsp\", \"rtsps\", \"rtmp\", \"rtmps\", \"srt\"} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tonConnect := filepath.Join(os.TempDir(), \"on_connect\")\n\t\t\tdefer os.Remove(onConnect)\n\n\t\t\tonDisconnect := filepath.Join(os.TempDir(), \"on_disconnect\")\n\t\t\tdefer os.Remove(onDisconnect)\n\n\t\t\tconnType := \"\"\n\n\t\t\tfunc() {\n\t\t\t\tp, ok := newInstance(fmt.Sprintf(\n\t\t\t\t\t\"rtspEncryption: optional\\n\"+\n\t\t\t\t\t\t\"rtspServerCert: \"+serverCertFpath+\"\\n\"+\n\t\t\t\t\t\t\"rtspServerKey: \"+serverKeyFpath+\"\\n\"+\n\t\t\t\t\t\t\"rtmpEncryption: optional\\n\"+\n\t\t\t\t\t\t\"rtmpServerCert: \"+serverCertFpath+\"\\n\"+\n\t\t\t\t\t\t\"rtmpServerKey: \"+serverKeyFpath+\"\\n\"+\n\t\t\t\t\t\t\"paths:\\n\"+\n\t\t\t\t\t\t\"  test:\\n\"+\n\t\t\t\t\t\t\"runOnConnect: sh -c 'echo \\\"$MTX_CONN_TYPE $MTX_CONN_ID $RTSP_PORT\\\" > %s'\\n\"+\n\t\t\t\t\t\t\"runOnDisconnect: sh -c 'echo \\\"$MTX_CONN_TYPE $MTX_CONN_ID $RTSP_PORT\\\" > %s'\\n\",\n\t\t\t\t\tonConnect, onDisconnect))\n\t\t\t\trequire.Equal(t, true, ok)\n\t\t\t\tdefer p.Close()\n\n\t\t\t\tswitch ca {\n\t\t\t\tcase \"rtsp\":\n\t\t\t\t\tconnType = \"rtspConn\"\n\n\t\t\t\t\tc := gortsplib.Client{}\n\n\t\t\t\t\terr = c.StartRecording(\n\t\t\t\t\t\t\"rtsp://localhost:8554/test\",\n\t\t\t\t\t\t&description.Session{Medias: []*description.Media{test.UniqueMediaH264()}})\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\tdefer c.Close()\n\n\t\t\t\tcase \"rtsps\":\n\t\t\t\t\tconnType = \"rtspsConn\"\n\n\t\t\t\t\tc := gortsplib.Client{TLSConfig: &tls.Config{InsecureSkipVerify: true}}\n\n\t\t\t\t\terr = c.StartRecording(\n\t\t\t\t\t\t\"rtsps://localhost:8322/test\",\n\t\t\t\t\t\t&description.Session{Medias: []*description.Media{test.UniqueMediaH264()}})\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\tdefer c.Close()\n\n\t\t\t\tcase \"rtmp\":\n\t\t\t\t\tconnType = \"rtmpConn\"\n\n\t\t\t\t\tvar u *url.URL\n\t\t\t\t\tu, err = url.Parse(\"rtmp://127.0.0.1:1935/test\")\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\tconn := &gortmplib.Client{\n\t\t\t\t\t\tURL:     u,\n\t\t\t\t\t\tPublish: true,\n\t\t\t\t\t}\n\t\t\t\t\terr = conn.Initialize(context.Background())\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\tdefer conn.Close()\n\n\t\t\t\tcase \"rtmps\":\n\t\t\t\t\tconnType = \"rtmpsConn\"\n\n\t\t\t\t\tvar u *url.URL\n\t\t\t\t\tu, err = url.Parse(\"rtmps://127.0.0.1:1936/test\")\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\tconn := &gortmplib.Client{\n\t\t\t\t\t\tURL:       u,\n\t\t\t\t\t\tPublish:   true,\n\t\t\t\t\t\tTLSConfig: &tls.Config{InsecureSkipVerify: true},\n\t\t\t\t\t}\n\t\t\t\t\terr = conn.Initialize(context.Background())\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\tdefer conn.Close()\n\n\t\t\t\tcase \"srt\":\n\t\t\t\t\tconnType = \"srtConn\"\n\n\t\t\t\t\tconf := srt.DefaultConfig()\n\t\t\t\t\tvar address string\n\t\t\t\t\taddress, err = conf.UnmarshalURL(\"srt://localhost:8890?streamid=publish:test\")\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\terr = conf.Validate()\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\tvar c srt.Conn\n\t\t\t\t\tc, err = srt.Dial(\"srt\", address, conf)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\tdefer c.Close()\n\t\t\t\t}\n\n\t\t\t\ttime.Sleep(500 * time.Millisecond)\n\t\t\t}()\n\n\t\t\tvar byts []byte\n\t\t\tbyts, err = os.ReadFile(onConnect)\n\t\t\trequire.NoError(t, err)\n\t\t\tfields := strings.Split(string(byts[:len(byts)-1]), \" \")\n\t\t\trequire.Equal(t, connType, fields[0])\n\t\t\trequire.NotEmpty(t, fields[1])\n\t\t\trequire.Equal(t, \"8554\", fields[2])\n\n\t\t\tbyts, err = os.ReadFile(onDisconnect)\n\t\t\trequire.NoError(t, err)\n\t\t\tfields = strings.Split(string(byts[:len(byts)-1]), \" \")\n\t\t\trequire.Equal(t, connType, fields[0])\n\t\t\trequire.NotEmpty(t, fields[1])\n\t\t\trequire.Equal(t, \"8554\", fields[2])\n\t\t})\n\t}\n}\n\nfunc TestPathRunOnReady(t *testing.T) {\n\tonReady := filepath.Join(os.TempDir(), \"on_ready\")\n\tdefer os.Remove(onReady)\n\n\tonNotReady := filepath.Join(os.TempDir(), \"on_unready\")\n\tdefer os.Remove(onNotReady)\n\n\tfunc() {\n\t\tp, ok := newInstance(fmt.Sprintf(\"rtmp: no\\n\"+\n\t\t\t\"hls: no\\n\"+\n\t\t\t\"webrtc: no\\n\"+\n\t\t\t\"paths:\\n\"+\n\t\t\t\"  ~te(st):\\n\"+\n\t\t\t\"    runOnReady: sh -c 'echo \\\"$MTX_PATH $MTX_QUERY $MTX_SOURCE_TYPE $MTX_SOURCE_ID $RTSP_PORT $G1\\\" > %s'\\n\"+\n\t\t\t\"    runOnNotReady: sh -c 'echo \\\"$MTX_PATH $MTX_QUERY $MTX_SOURCE_TYPE $MTX_SOURCE_ID $RTSP_PORT $G1\\\" > %s'\\n\",\n\t\t\tonReady, onNotReady))\n\t\trequire.Equal(t, true, ok)\n\t\tdefer p.Close()\n\n\t\tc := gortsplib.Client{}\n\n\t\terr := c.StartRecording(\n\t\t\t\"rtsp://localhost:8554/test?query=value\",\n\t\t\t&description.Session{Medias: []*description.Media{test.UniqueMediaH264()}})\n\t\trequire.NoError(t, err)\n\t\tdefer c.Close()\n\n\t\ttime.Sleep(500 * time.Millisecond)\n\t}()\n\n\tbyts, err := os.ReadFile(onReady)\n\trequire.NoError(t, err)\n\tfields := strings.Split(string(byts[:len(byts)-1]), \" \")\n\trequire.Equal(t, \"test\", fields[0])\n\trequire.Equal(t, \"query=value\", fields[1])\n\trequire.Equal(t, \"rtspSession\", fields[2])\n\trequire.NotEmpty(t, fields[3])\n\trequire.Equal(t, \"8554\", fields[4])\n\trequire.Equal(t, \"st\", fields[5])\n\n\tbyts, err = os.ReadFile(onNotReady)\n\trequire.NoError(t, err)\n\tfields = strings.Split(string(byts[:len(byts)-1]), \" \")\n\trequire.Equal(t, \"test\", fields[0])\n\trequire.Equal(t, \"query=value\", fields[1])\n\trequire.Equal(t, \"rtspSession\", fields[2])\n\trequire.NotEmpty(t, fields[3])\n\trequire.Equal(t, \"8554\", fields[4])\n\trequire.Equal(t, \"st\", fields[5])\n}\n\nfunc TestPathRunOnRead(t *testing.T) {\n\tserverCertFpath, err := test.CreateTempFile(test.TLSCertPub)\n\trequire.NoError(t, err)\n\tdefer os.Remove(serverCertFpath)\n\n\tserverKeyFpath, err := test.CreateTempFile(test.TLSCertKey)\n\trequire.NoError(t, err)\n\tdefer os.Remove(serverKeyFpath)\n\n\tfor _, ca := range []string{\"rtsp\", \"rtsps\", \"rtmp\", \"rtmps\", \"srt\", \"webrtc\"} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tonRead := filepath.Join(os.TempDir(), \"on_read\")\n\t\t\tdefer os.Remove(onRead)\n\n\t\t\tonUnread := filepath.Join(os.TempDir(), \"on_unread\")\n\t\t\tdefer os.Remove(onUnread)\n\n\t\t\tfunc() {\n\t\t\t\tp, ok := newInstance(fmt.Sprintf(\n\t\t\t\t\t\"rtspEncryption: optional\\n\"+\n\t\t\t\t\t\t\"rtspServerCert: \"+serverCertFpath+\"\\n\"+\n\t\t\t\t\t\t\"rtspServerKey: \"+serverKeyFpath+\"\\n\"+\n\t\t\t\t\t\t\"rtmpEncryption: optional\\n\"+\n\t\t\t\t\t\t\"rtmpServerCert: \"+serverCertFpath+\"\\n\"+\n\t\t\t\t\t\t\"rtmpServerKey: \"+serverKeyFpath+\"\\n\"+\n\t\t\t\t\t\t\"paths:\\n\"+\n\t\t\t\t\t\t\"  ~te(st):\\n\"+\n\t\t\t\t\t\t\"    runOnRead: sh -c 'echo \\\"$MTX_PATH $MTX_QUERY $MTX_READER_TYPE $MTX_READER_ID $RTSP_PORT $G1\\\" > %s'\\n\"+\n\t\t\t\t\t\t\"    runOnUnread: sh -c 'echo \\\"$MTX_PATH $MTX_QUERY $MTX_READER_TYPE $MTX_READER_ID $RTSP_PORT $G1\\\" > %s'\\n\",\n\t\t\t\t\tonRead, onUnread))\n\t\t\t\trequire.Equal(t, true, ok)\n\t\t\t\tdefer p.Close()\n\n\t\t\t\tmedia0 := test.UniqueMediaH264()\n\n\t\t\t\tsource := gortsplib.Client{}\n\n\t\t\t\terr = source.StartRecording(\n\t\t\t\t\t\"rtsp://localhost:8554/test\",\n\t\t\t\t\t&description.Session{Medias: []*description.Media{media0}})\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer source.Close()\n\n\t\t\t\twriterDone := make(chan struct{})\n\t\t\t\tdefer func() { <-writerDone }()\n\n\t\t\t\twriterTerminate := make(chan struct{})\n\t\t\t\tdefer close(writerTerminate)\n\n\t\t\t\tgo func() {\n\t\t\t\t\tdefer close(writerDone)\n\t\t\t\t\ti := 0\n\t\t\t\t\tfor {\n\t\t\t\t\t\tselect {\n\t\t\t\t\t\tcase <-time.After(100 * time.Millisecond):\n\t\t\t\t\t\tcase <-writerTerminate:\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t\terr2 := source.WritePacketRTP(media0, &rtp.Packet{\n\t\t\t\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\t\t\t\tVersion:        2,\n\t\t\t\t\t\t\t\tMarker:         true,\n\t\t\t\t\t\t\t\tPayloadType:    96,\n\t\t\t\t\t\t\t\tSequenceNumber: uint16(123 + i),\n\t\t\t\t\t\t\t\tTimestamp:      uint32(45343 + i*90000),\n\t\t\t\t\t\t\t\tSSRC:           563423,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tPayload: []byte{5},\n\t\t\t\t\t\t})\n\t\t\t\t\t\trequire.NoError(t, err2)\n\t\t\t\t\t\ti++\n\t\t\t\t\t}\n\t\t\t\t}()\n\n\t\t\t\tswitch ca {\n\t\t\t\tcase \"rtsp\":\n\t\t\t\t\tvar u *base.URL\n\t\t\t\t\tu, err = base.ParseURL(\"rtsp://127.0.0.1:8554/test?query=value\")\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\treader := gortsplib.Client{\n\t\t\t\t\t\tScheme: u.Scheme,\n\t\t\t\t\t\tHost:   u.Host,\n\t\t\t\t\t}\n\n\t\t\t\t\terr = reader.Start()\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\tdefer reader.Close()\n\n\t\t\t\t\tvar desc *description.Session\n\t\t\t\t\tdesc, _, err = reader.Describe(u)\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\terr = reader.SetupAll(desc.BaseURL, desc.Medias)\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\t_, err = reader.Play(nil)\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tcase \"rtsps\":\n\t\t\t\t\tvar u *base.URL\n\t\t\t\t\tu, err = base.ParseURL(\"rtsps://127.0.0.1:8322/test?query=value\")\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\treader := gortsplib.Client{\n\t\t\t\t\t\tScheme:    u.Scheme,\n\t\t\t\t\t\tHost:      u.Host,\n\t\t\t\t\t\tTLSConfig: &tls.Config{InsecureSkipVerify: true},\n\t\t\t\t\t}\n\n\t\t\t\t\terr = reader.Start()\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\tdefer reader.Close()\n\n\t\t\t\t\tvar desc *description.Session\n\t\t\t\t\tdesc, _, err = reader.Describe(u)\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\terr = reader.SetupAll(desc.BaseURL, desc.Medias)\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\t_, err = reader.Play(nil)\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tcase \"rtmp\":\n\t\t\t\t\tvar u *url.URL\n\t\t\t\t\tu, err = url.Parse(\"rtmp://127.0.0.1:1935/test?query=value\")\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\tconn := &gortmplib.Client{\n\t\t\t\t\t\tURL:     u,\n\t\t\t\t\t\tPublish: false,\n\t\t\t\t\t}\n\t\t\t\t\terr = conn.Initialize(context.Background())\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\tdefer conn.Close()\n\n\t\t\t\t\tr := &gortmplib.Reader{\n\t\t\t\t\t\tConn: conn,\n\t\t\t\t\t}\n\t\t\t\t\terr = r.Initialize()\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tcase \"rtmps\":\n\t\t\t\t\tvar u *url.URL\n\t\t\t\t\tu, err = url.Parse(\"rtmps://127.0.0.1:1936/test?query=value\")\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\tconn := &gortmplib.Client{\n\t\t\t\t\t\tURL:       u,\n\t\t\t\t\t\tPublish:   false,\n\t\t\t\t\t\tTLSConfig: &tls.Config{InsecureSkipVerify: true},\n\t\t\t\t\t}\n\t\t\t\t\terr = conn.Initialize(context.Background())\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\tdefer conn.Close()\n\n\t\t\t\t\tgo func() {\n\t\t\t\t\t\tfor i := range uint16(3) {\n\t\t\t\t\t\t\terr2 := source.WritePacketRTP(media0, &rtp.Packet{\n\t\t\t\t\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\t\t\t\t\tVersion:        2,\n\t\t\t\t\t\t\t\t\tMarker:         true,\n\t\t\t\t\t\t\t\t\tPayloadType:    96,\n\t\t\t\t\t\t\t\t\tSequenceNumber: 123 + i,\n\t\t\t\t\t\t\t\t\tTimestamp:      45343 + uint32(i)*2*90000,\n\t\t\t\t\t\t\t\t\tSSRC:           563423,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tPayload: []byte{5},\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\trequire.NoError(t, err2)\n\t\t\t\t\t\t}\n\t\t\t\t\t}()\n\n\t\t\t\t\tr := &gortmplib.Reader{\n\t\t\t\t\t\tConn: conn,\n\t\t\t\t\t}\n\t\t\t\t\terr = r.Initialize()\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tcase \"srt\":\n\t\t\t\t\tconf := srt.DefaultConfig()\n\t\t\t\t\tvar address string\n\t\t\t\t\taddress, err = conf.UnmarshalURL(\"srt://localhost:8890?streamid=read:test:query=value\")\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\terr = conf.Validate()\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\tvar reader srt.Conn\n\t\t\t\t\treader, err = srt.Dial(\"srt\", address, conf)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\tdefer reader.Close()\n\n\t\t\t\tcase \"webrtc\":\n\t\t\t\t\ttr := &http.Transport{}\n\t\t\t\t\tdefer tr.CloseIdleConnections()\n\t\t\t\t\thc := &http.Client{Transport: tr}\n\n\t\t\t\t\tvar u *url.URL\n\t\t\t\t\tu, err = url.Parse(\"http://localhost:8889/test/whep?query=value\")\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\tc := &whip.Client{\n\t\t\t\t\t\tHTTPClient: hc,\n\t\t\t\t\t\tURL:        u,\n\t\t\t\t\t\tLog:        test.NilLogger,\n\t\t\t\t\t}\n\n\t\t\t\t\terr = c.Initialize(context.Background())\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\tdefer checkClose(t, c.Close)\n\t\t\t\t}\n\n\t\t\t\ttime.Sleep(500 * time.Millisecond)\n\t\t\t}()\n\n\t\t\tvar readerType string\n\n\t\t\tswitch ca {\n\t\t\tcase \"rtsp\":\n\t\t\t\treaderType = \"rtspSession\"\n\t\t\tcase \"rtsps\":\n\t\t\t\treaderType = \"rtspsSession\"\n\t\t\tcase \"rtmp\":\n\t\t\t\treaderType = \"rtmpConn\"\n\t\t\tcase \"rtmps\":\n\t\t\t\treaderType = \"rtmpsConn\"\n\t\t\tcase \"srt\":\n\t\t\t\treaderType = \"srtConn\"\n\t\t\tcase \"webrtc\":\n\t\t\t\treaderType = \"webRTCSession\"\n\t\t\t}\n\n\t\t\tvar byts []byte\n\t\t\tbyts, err = os.ReadFile(onRead)\n\t\t\trequire.NoError(t, err)\n\t\t\tfields := strings.Split(string(byts[:len(byts)-1]), \" \")\n\t\t\trequire.Equal(t, \"test\", fields[0])\n\t\t\trequire.Equal(t, \"query=value\", fields[1])\n\t\t\trequire.Equal(t, readerType, fields[2])\n\t\t\trequire.NotEmpty(t, fields[3])\n\t\t\trequire.Equal(t, \"8554\", fields[4])\n\t\t\trequire.Equal(t, \"st\", fields[5])\n\n\t\t\tbyts, err = os.ReadFile(onUnread)\n\t\t\trequire.NoError(t, err)\n\t\t\tfields = strings.Split(string(byts[:len(byts)-1]), \" \")\n\t\t\trequire.Equal(t, \"test\", fields[0])\n\t\t\trequire.Equal(t, \"query=value\", fields[1])\n\t\t\trequire.Equal(t, readerType, fields[2])\n\t\t\trequire.NotEmpty(t, fields[3])\n\t\t\trequire.Equal(t, \"8554\", fields[4])\n\t\t\trequire.Equal(t, \"st\", fields[5])\n\t\t})\n\t}\n}\n\nfunc TestPathRunOnRecordSegment(t *testing.T) {\n\tonRecordSegmentCreate := filepath.Join(os.TempDir(), \"on_record_segment_create\")\n\tdefer os.Remove(onRecordSegmentCreate)\n\n\tonRecordSegmentComplete := filepath.Join(os.TempDir(), \"on_record_segment_complete\")\n\tdefer os.Remove(onRecordSegmentComplete)\n\n\trecordDir, err := os.MkdirTemp(\"\", \"rtsp-path-record\")\n\trequire.NoError(t, err)\n\tdefer os.RemoveAll(recordDir)\n\n\tfunc() {\n\t\tp, ok := newInstance(fmt.Sprintf(\"record: yes\\n\"+\n\t\t\t\"recordPath: %s\\n\"+\n\t\t\t\"paths:\\n\"+\n\t\t\t\"  test:\\n\"+\n\t\t\t\"    runOnRecordSegmentCreate: sh -c 'echo \\\"$MTX_SEGMENT_PATH $RTSP_PORT\\\" > %s'\\n\"+\n\t\t\t\"    runOnRecordSegmentComplete: sh -c 'echo \\\"$MTX_SEGMENT_PATH $MTX_SEGMENT_DURATION $RTSP_PORT\\\" > %s'\\n\",\n\t\t\tfilepath.Join(recordDir, \"%path/%Y-%m-%d_%H-%M-%S-%f\"),\n\t\t\tonRecordSegmentCreate,\n\t\t\tonRecordSegmentComplete,\n\t\t))\n\t\trequire.Equal(t, true, ok)\n\t\tdefer p.Close()\n\n\t\tmedia0 := test.UniqueMediaH264()\n\n\t\tsource := gortsplib.Client{}\n\n\t\terr = source.StartRecording(\n\t\t\t\"rtsp://localhost:8554/test\",\n\t\t\t&description.Session{Medias: []*description.Media{media0}})\n\t\trequire.NoError(t, err)\n\t\tdefer source.Close()\n\n\t\tfor i := range 4 {\n\t\t\terr = source.WritePacketRTP(media0, &rtp.Packet{\n\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\tVersion:        2,\n\t\t\t\t\tMarker:         true,\n\t\t\t\t\tPayloadType:    96,\n\t\t\t\t\tSequenceNumber: 1123 + uint16(i),\n\t\t\t\t\tTimestamp:      45343 + 90000*uint32(i),\n\t\t\t\t\tSSRC:           563423,\n\t\t\t\t},\n\t\t\t\tPayload: []byte{5},\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t}\n\n\t\ttime.Sleep(500 * time.Millisecond)\n\t}()\n\n\tbyts, err := os.ReadFile(onRecordSegmentCreate)\n\trequire.NoError(t, err)\n\tfields := strings.Split(string(byts[:len(byts)-1]), \" \")\n\trequire.True(t, strings.HasPrefix(fields[0], recordDir))\n\trequire.Equal(t, \"8554\", fields[1])\n\n\tbyts, err = os.ReadFile(onRecordSegmentComplete)\n\trequire.NoError(t, err)\n\tfields = strings.Split(string(byts[:len(byts)-1]), \" \")\n\trequire.True(t, strings.HasPrefix(fields[0], recordDir))\n\trequire.Equal(t, \"3\", fields[1])\n\trequire.Equal(t, \"8554\", fields[2])\n}\n\nfunc TestPathMaxReaders(t *testing.T) {\n\tp, ok := newInstance(\"paths:\\n\" +\n\t\t\"  all_others:\\n\" +\n\t\t\"    maxReaders: 1\\n\")\n\trequire.Equal(t, true, ok)\n\tdefer p.Close()\n\n\tsource := gortsplib.Client{}\n\n\terr := source.StartRecording(\n\t\t\"rtsp://localhost:8554/mystream\",\n\t\t&description.Session{Medias: []*description.Media{\n\t\t\ttest.UniqueMediaH264(),\n\t\t\ttest.UniqueMediaMPEG4Audio(),\n\t\t}})\n\trequire.NoError(t, err)\n\tdefer source.Close()\n\n\tfor i := range 2 {\n\t\tvar u *base.URL\n\t\tu, err = base.ParseURL(\"rtsp://127.0.0.1:8554/mystream\")\n\t\trequire.NoError(t, err)\n\n\t\treader := gortsplib.Client{\n\t\t\tScheme: u.Scheme,\n\t\t\tHost:   u.Host,\n\t\t}\n\n\t\terr = reader.Start()\n\t\trequire.NoError(t, err)\n\t\tdefer reader.Close()\n\n\t\tvar desc *description.Session\n\t\tdesc, _, err = reader.Describe(u)\n\t\trequire.NoError(t, err)\n\n\t\terr = reader.SetupAll(desc.BaseURL, desc.Medias)\n\t\tif i != 1 {\n\t\t\trequire.NoError(t, err)\n\t\t} else {\n\t\t\trequire.Error(t, err)\n\t\t}\n\t}\n}\n\nfunc TestPathRecord(t *testing.T) {\n\tdir, err := os.MkdirTemp(\"\", \"rtsp-path-record\")\n\trequire.NoError(t, err)\n\tdefer os.RemoveAll(dir)\n\n\tp, ok := newInstance(\"api: yes\\n\" +\n\t\t\"record: yes\\n\" +\n\t\t\"recordPath: \" + filepath.Join(dir, \"%path/%Y-%m-%d_%H-%M-%S-%f\") + \"\\n\" +\n\t\t\"paths:\\n\" +\n\t\t\"  all_others:\\n\" +\n\t\t\"    record: yes\\n\")\n\trequire.Equal(t, true, ok)\n\tdefer p.Close()\n\n\tmedia0 := test.UniqueMediaH264()\n\n\tsource := gortsplib.Client{}\n\n\terr = source.StartRecording(\n\t\t\"rtsp://localhost:8554/mystream\",\n\t\t&description.Session{Medias: []*description.Media{media0}})\n\trequire.NoError(t, err)\n\tdefer source.Close()\n\n\tfor i := range 4 {\n\t\terr = source.WritePacketRTP(media0, &rtp.Packet{\n\t\t\tHeader: rtp.Header{\n\t\t\t\tVersion:        2,\n\t\t\t\tMarker:         true,\n\t\t\t\tPayloadType:    96,\n\t\t\t\tSequenceNumber: 1123 + uint16(i),\n\t\t\t\tTimestamp:      45343 + 90000*uint32(i),\n\t\t\t\tSSRC:           563423,\n\t\t\t},\n\t\t\tPayload: []byte{5},\n\t\t})\n\t\trequire.NoError(t, err)\n\t}\n\n\ttime.Sleep(500 * time.Millisecond)\n\n\tfiles, err := os.ReadDir(filepath.Join(dir, \"mystream\"))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 1, len(files))\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\thttpRequest(t, hc, http.MethodPatch, \"http://localhost:9997/v3/config/paths/patch/all_others\", map[string]any{\n\t\t\"record\": false,\n\t}, nil)\n\n\ttime.Sleep(500 * time.Millisecond)\n\n\thttpRequest(t, hc, http.MethodPatch, \"http://localhost:9997/v3/config/paths/patch/all_others\", map[string]any{\n\t\t\"record\": true,\n\t}, nil)\n\n\ttime.Sleep(500 * time.Millisecond)\n\n\tfor i := 4; i < 8; i++ {\n\t\terr = source.WritePacketRTP(media0, &rtp.Packet{\n\t\t\tHeader: rtp.Header{\n\t\t\t\tVersion:        2,\n\t\t\t\tMarker:         true,\n\t\t\t\tPayloadType:    96,\n\t\t\t\tSequenceNumber: 1123 + uint16(i),\n\t\t\t\tTimestamp:      45343 + 90000*uint32(i),\n\t\t\t\tSSRC:           563423,\n\t\t\t},\n\t\t\tPayload: []byte{5},\n\t\t})\n\t\trequire.NoError(t, err)\n\t}\n\n\ttime.Sleep(500 * time.Millisecond)\n\n\tfiles, err = os.ReadDir(filepath.Join(dir, \"mystream\"))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 2, len(files))\n}\n\nfunc TestPathFallback(t *testing.T) {\n\tfor _, ca := range []string{\n\t\t\"absolute\",\n\t\t\"relative\",\n\t\t\"source\",\n\t} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tvar cnf string\n\n\t\t\tswitch ca {\n\t\t\tcase \"absolute\":\n\t\t\t\tcnf = \"paths:\\n\" +\n\t\t\t\t\t\"  path1:\\n\" +\n\t\t\t\t\t\"    fallback: rtsp://localhost:8554/path2\\n\" +\n\t\t\t\t\t\"  path2:\\n\"\n\n\t\t\tcase \"relative\":\n\t\t\t\tcnf = \"paths:\\n\" +\n\t\t\t\t\t\"  path1:\\n\" +\n\t\t\t\t\t\"    fallback: /path2\\n\" +\n\t\t\t\t\t\"  path2:\\n\"\n\n\t\t\tcase \"source\":\n\t\t\t\tcnf = \"paths:\\n\" +\n\t\t\t\t\t\"  path1:\\n\" +\n\t\t\t\t\t\"    fallback: /path2\\n\" +\n\t\t\t\t\t\"    source: rtsp://localhost:3333/nonexistent\\n\" +\n\t\t\t\t\t\"  path2:\\n\"\n\t\t\t}\n\n\t\t\tp1, ok := newInstance(cnf)\n\t\t\trequire.Equal(t, true, ok)\n\t\t\tdefer p1.Close()\n\n\t\t\tsource := gortsplib.Client{}\n\t\t\terr := source.StartRecording(\"rtsp://localhost:8554/path2\",\n\t\t\t\t&description.Session{Medias: []*description.Media{test.UniqueMediaH264()}})\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer source.Close()\n\n\t\t\tu, err := base.ParseURL(\"rtsp://localhost:8554/path1\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\tdest := gortsplib.Client{\n\t\t\t\tScheme: u.Scheme,\n\t\t\t\tHost:   u.Host,\n\t\t\t}\n\n\t\t\terr = dest.Start()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer dest.Close()\n\n\t\t\tdesc, _, err := dest.Describe(u)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, 1, len(desc.Medias))\n\t\t})\n\t}\n}\n\nfunc TestPathResolveSource(t *testing.T) {\n\tvar strm *gortsplib.ServerStream\n\n\ts := gortsplib.Server{\n\t\tHandler: &testServer{\n\t\t\tonDescribe: func(ctx *gortsplib.ServerHandlerOnDescribeCtx,\n\t\t\t) (*base.Response, *gortsplib.ServerStream, error) {\n\t\t\t\trequire.Equal(t, \"key=val\", ctx.Query)\n\t\t\t\trequire.Equal(t, \"/a\", ctx.Path)\n\t\t\t\treturn &base.Response{\n\t\t\t\t\tStatusCode: base.StatusOK,\n\t\t\t\t}, strm, nil\n\t\t\t},\n\t\t\tonSetup: func(_ *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) {\n\t\t\t\treturn &base.Response{\n\t\t\t\t\tStatusCode: base.StatusOK,\n\t\t\t\t}, strm, nil\n\t\t\t},\n\t\t\tonPlay: func(_ *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) {\n\t\t\t\treturn &base.Response{\n\t\t\t\t\tStatusCode: base.StatusOK,\n\t\t\t\t}, nil\n\t\t\t},\n\t\t},\n\t\tRTSPAddress: \"127.0.0.1:8555\",\n\t}\n\n\terr := s.Start()\n\trequire.NoError(t, err)\n\tdefer s.Close()\n\n\tstrm = &gortsplib.ServerStream{\n\t\tServer: &s,\n\t\tDesc:   &description.Session{Medias: []*description.Media{test.MediaH264}},\n\t}\n\terr = strm.Initialize()\n\trequire.NoError(t, err)\n\tdefer strm.Close()\n\n\tp, ok := newInstance(\n\t\t\"paths:\\n\" +\n\t\t\t\"  '~^test_(.+)$':\\n\" +\n\t\t\t\"    source: rtsp://127.0.0.1:8555/$G1?$MTX_QUERY\\n\" +\n\t\t\t\"    sourceOnDemand: yes\\n\" +\n\t\t\t\"  'all':\\n\")\n\trequire.Equal(t, true, ok)\n\tdefer p.Close()\n\n\tu, err := base.ParseURL(\"rtsp://127.0.0.1:8554/test_a?key=val\")\n\trequire.NoError(t, err)\n\n\treader := gortsplib.Client{\n\t\tScheme: u.Scheme,\n\t\tHost:   u.Host,\n\t}\n\n\terr = reader.Start()\n\trequire.NoError(t, err)\n\tdefer reader.Close()\n\n\t_, _, err = reader.Describe(u)\n\trequire.NoError(t, err)\n}\n\nfunc TestPathOverridePublisher(t *testing.T) {\n\tfor _, ca := range []string{\n\t\t\"enabled\",\n\t\t\"disabled\",\n\t} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tcnf := \"rtmp: no\\n\" +\n\t\t\t\t\"paths:\\n\" +\n\t\t\t\t\"  all_others:\\n\"\n\n\t\t\tif ca == \"disabled\" {\n\t\t\t\tcnf += \"    overridePublisher: no\\n\"\n\t\t\t}\n\n\t\t\tp, ok := newInstance(cnf)\n\t\t\trequire.Equal(t, true, ok)\n\t\t\tdefer p.Close()\n\n\t\t\tmedi := test.UniqueMediaH264()\n\n\t\t\ts1 := gortsplib.Client{}\n\n\t\t\terr := s1.StartRecording(\"rtsp://localhost:8554/teststream\",\n\t\t\t\t&description.Session{Medias: []*description.Media{medi}})\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer s1.Close()\n\n\t\t\ts2 := gortsplib.Client{}\n\n\t\t\terr = s2.StartRecording(\"rtsp://localhost:8554/teststream\",\n\t\t\t\t&description.Session{Medias: []*description.Media{medi}})\n\t\t\tif ca == \"enabled\" {\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer s2.Close()\n\t\t\t} else {\n\t\t\t\trequire.Error(t, err)\n\t\t\t}\n\n\t\t\tframeRecv := make(chan struct{})\n\n\t\t\tu, err := base.ParseURL(\"rtsp://localhost:8554/teststream\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\tc := gortsplib.Client{\n\t\t\t\tScheme: u.Scheme,\n\t\t\t\tHost:   u.Host,\n\t\t\t}\n\n\t\t\terr = c.Start()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer c.Close()\n\n\t\t\tdesc, _, err := c.Describe(u)\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = c.SetupAll(desc.BaseURL, desc.Medias)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tc.OnPacketRTP(desc.Medias[0], desc.Medias[0].Formats[0], func(pkt *rtp.Packet) {\n\t\t\t\tif ca == \"enabled\" {\n\t\t\t\t\trequire.Equal(t, []byte{5, 15, 16, 17, 18}, pkt.Payload)\n\t\t\t\t} else {\n\t\t\t\t\trequire.Equal(t, []byte{5, 11, 12, 13, 14}, pkt.Payload)\n\t\t\t\t}\n\t\t\t\tclose(frameRecv)\n\t\t\t})\n\n\t\t\t_, err = c.Play(nil)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tif ca == \"enabled\" {\n\t\t\t\terr = s1.Wait()\n\t\t\t\trequire.EqualError(t, err, \"EOF\")\n\n\t\t\t\terr = s2.WritePacketRTP(medi, &rtp.Packet{\n\t\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\t\tVersion:        0x02,\n\t\t\t\t\t\tPayloadType:    96,\n\t\t\t\t\t\tSequenceNumber: 57899,\n\t\t\t\t\t\tTimestamp:      345234345,\n\t\t\t\t\t\tSSRC:           978651231,\n\t\t\t\t\t\tMarker:         true,\n\t\t\t\t\t},\n\t\t\t\t\tPayload: []byte{5, 15, 16, 17, 18},\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\t\t\t} else {\n\t\t\t\terr = s1.WritePacketRTP(medi, &rtp.Packet{\n\t\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\t\tVersion:        0x02,\n\t\t\t\t\t\tPayloadType:    96,\n\t\t\t\t\t\tSequenceNumber: 57899,\n\t\t\t\t\t\tTimestamp:      345234345,\n\t\t\t\t\t\tSSRC:           978651231,\n\t\t\t\t\t\tMarker:         true,\n\t\t\t\t\t},\n\t\t\t\t\tPayload: []byte{5, 11, 12, 13, 14},\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\t<-frameRecv\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/core/source_redirect.go",
    "content": "package core\n\nimport (\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n)\n\n// sourceRedirect is a source that redirects to another one.\ntype sourceRedirect struct{}\n\nfunc (*sourceRedirect) Log(logger.Level, string, ...any) {\n}\n\n// APISourceDescribe implements source.\nfunc (*sourceRedirect) APISourceDescribe() *defs.APIPathSource {\n\treturn &defs.APIPathSource{\n\t\tType: defs.APIPathSourceTypeRedirect,\n\t\tID:   \"\",\n\t}\n}\n"
  },
  {
    "path": "internal/core/test_on_demand/main.go",
    "content": "// This is used for testing purposes.\npackage main\n\nimport (\n\t\"os\"\n\t\"os/signal\"\n\t\"syscall\"\n\n\t\"github.com/bluenviron/gortsplib/v5\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n)\n\nfunc main() {\n\tif os.Getenv(\"MTX_QUERY\") != \"param=value\" {\n\t\tpanic(\"unexpected MTX_QUERY\")\n\t}\n\tif os.Getenv(\"G1\") != \"on\" {\n\t\tpanic(\"unexpected G1\")\n\t}\n\n\tmedi := &description.Media{\n\t\tType: description.MediaTypeVideo,\n\t\tFormats: []format.Format{&format.H264{\n\t\t\tPayloadTyp: 96,\n\t\t\tSPS: []byte{\n\t\t\t\t0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02,\n\t\t\t\t0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04,\n\t\t\t\t0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, 0x20,\n\t\t\t},\n\t\t\tPPS:               []byte{0x01, 0x02, 0x03, 0x04},\n\t\t\tPacketizationMode: 1,\n\t\t}},\n\t}\n\n\tsource := gortsplib.Client{}\n\n\terr := source.StartRecording(\n\t\t\"rtsp://localhost:\"+os.Getenv(\"RTSP_PORT\")+\"/\"+os.Getenv(\"MTX_PATH\"),\n\t\t&description.Session{Medias: []*description.Media{medi}})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer source.Close()\n\n\tc := make(chan os.Signal, 1)\n\tsignal.Notify(c, syscall.SIGINT)\n\t<-c\n\n\terr = os.WriteFile(os.Getenv(\"ON_DEMAND\"), []byte(\"\"), 0o644)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n"
  },
  {
    "path": "internal/core/upgrade.go",
    "content": "//go:build enable_upgrade\n\npackage core\n\nimport (\n\t\"archive/tar\"\n\t\"archive/zip\"\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"runtime\"\n\t\"sort\"\n\n\t\"github.com/Masterminds/semver/v3\"\n\t\"github.com/go-git/go-git/v5\"\n\t\"github.com/go-git/go-git/v5/config\"\n\t\"github.com/go-git/go-git/v5/storage/memory\"\n\t\"github.com/minio/selfupdate\"\n)\n\nconst (\n\tgitRepo     = \"https://github.com/bluenviron/mediamtx\"\n\tdownloadURL = \"https://github.com/bluenviron/mediamtx/releases/download/%s/mediamtx_%s_%s_%s.%s\"\n\texecutable  = \"mediamtx\"\n)\n\nvar (\n\ttagsRegexp    = regexp.MustCompile(`^refs/tags/(v1\\.[0-9]+\\.[0-9]+)$`)\n\tcurrentRegexp = regexp.MustCompile(`^(v1\\.[0-9]+\\.[0-9]+)$`)\n)\n\nfunc latestRemoteVersion() (*semver.Version, error) {\n\trem := git.NewRemote(memory.NewStorage(), &config.RemoteConfig{\n\t\tURLs: []string{gitRepo},\n\t})\n\n\trefs, err := rem.List(&git.ListOptions{})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar versions []*semver.Version\n\n\tfor _, ref := range refs {\n\t\tmatches := tagsRegexp.FindStringSubmatch(ref.Name().String())\n\t\tif matches != nil {\n\t\t\tv, _ := semver.NewVersion(matches[1])\n\t\t\tversions = append(versions, v)\n\t\t}\n\t}\n\n\tif len(versions) == 0 {\n\t\treturn nil, fmt.Errorf(\"no versions found\")\n\t}\n\n\tsort.Sort(sort.Reverse(semver.Collection(versions)))\n\n\treturn versions[0], nil\n}\n\nfunc extractExecutable(r io.Reader) ([]byte, error) {\n\tgzReader, err := gzip.NewReader(r)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer gzReader.Close()\n\n\ttarReader := tar.NewReader(gzReader)\n\n\tfor {\n\t\theader, err := tarReader.Next()\n\t\tif err == io.EOF {\n\t\t\treturn nil, fmt.Errorf(\"executable not found\")\n\t\t}\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif header.Name == executable {\n\t\t\tbuf, err := io.ReadAll(tarReader)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\treturn buf, nil\n\t\t}\n\t}\n}\n\nfunc extractExecutableWin(r io.Reader) ([]byte, error) {\n\tdata, err := io.ReadAll(r)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tzipReader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, file := range zipReader.File {\n\t\tif file.Name == executable+\".exe\" {\n\t\t\trc, err := file.Open()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tdefer rc.Close()\n\n\t\t\tbuf, err := io.ReadAll(rc)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\treturn buf, nil\n\t\t}\n\t}\n\n\treturn nil, fmt.Errorf(\"executable not found\")\n}\n\nfunc upgrade() error {\n\tif !currentRegexp.MatchString(string(version)) {\n\t\treturn fmt.Errorf(\"current version (%v) is not official and cannot be upgraded\", string(version))\n\t}\n\n\tfmt.Println(\"getting latest version...\")\n\n\tlatest, err := latestRemoteVersion()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcurrent, _ := semver.NewVersion(string(version))\n\n\tif current.GreaterThanEqual(latest) {\n\t\tfmt.Printf(\"current version (%v) is up to date\\n\", \"v\"+current.String())\n\t\treturn nil\n\t}\n\n\tfmt.Printf(\"downloading version %v...\\n\", \"v\"+latest.String())\n\n\tvar extension string\n\tif runtime.GOOS == \"windows\" {\n\t\textension = \"zip\"\n\t} else {\n\t\textension = \"tar.gz\"\n\t}\n\n\tur := fmt.Sprintf(downloadURL, \"v\"+latest.String(), \"v\"+latest.String(), runtime.GOOS, getArch(), extension)\n\n\tres, err := http.Get(ur)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer res.Body.Close()\n\n\tif res.StatusCode != http.StatusOK {\n\t\treturn fmt.Errorf(\"bad status code: %v\", res.StatusCode)\n\t}\n\n\tvar exe []byte\n\tif runtime.GOOS == \"windows\" {\n\t\texe, err = extractExecutableWin(res.Body)\n\t} else {\n\t\texe, err = extractExecutable(res.Body)\n\t}\n\n\terr = selfupdate.Apply(bytes.NewReader(exe), selfupdate.Options{})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfmt.Printf(\"MediaMTX upgraded successfully from %v to %v.\\n\", \"v\"+current.String(), \"v\"+latest.String())\n\treturn nil\n}\n"
  },
  {
    "path": "internal/core/upgrade_disabled.go",
    "content": "//go:build !enable_upgrade\n\npackage core\n\nimport \"fmt\"\n\nfunc upgrade() error {\n\treturn fmt.Errorf(\"upgrade command is not available\")\n}\n"
  },
  {
    "path": "internal/core/versiongetter/main.go",
    "content": "// Package main contains an utility to get the server version\npackage main\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/go-git/go-billy/v5/osfs\"\n\t\"github.com/go-git/go-git/v5\"\n\t\"github.com/go-git/go-git/v5/plumbing\"\n\t\"github.com/go-git/go-git/v5/plumbing/cache\"\n\t\"github.com/go-git/go-git/v5/plumbing/object\"\n\t\"github.com/go-git/go-git/v5/storage/filesystem\"\n)\n\n// Golang version of git describe --tags\nfunc gitDescribeTags(repo *git.Repository) (string, error) {\n\thead, err := repo.Head()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get HEAD: %w\", err)\n\t}\n\n\ttagIterator, err := repo.Tags()\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get tags: %w\", err)\n\t}\n\tdefer tagIterator.Close()\n\n\ttags := make(map[plumbing.Hash]*plumbing.Reference)\n\n\terr = tagIterator.ForEach(func(t *plumbing.Reference) error {\n\t\tif to, err2 := repo.TagObject(t.Hash()); err2 == nil {\n\t\t\ttags[to.Target] = t\n\t\t} else {\n\t\t\ttags[t.Hash()] = t\n\t\t}\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to iterate tags: %w\", err)\n\t}\n\n\tcIter, err := repo.Log(&git.LogOptions{From: head.Hash()})\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to get log: %w\", err)\n\t}\n\n\ti := 0\n\n\tfor {\n\t\tvar commit *object.Commit\n\t\tcommit, err = cIter.Next()\n\t\tif err != nil {\n\t\t\treturn \"\", fmt.Errorf(\"failed to get next commit: %w\", err)\n\t\t}\n\n\t\tif str, ok := tags[commit.Hash]; ok {\n\t\t\tlabel := strings.TrimPrefix(string(str.Name()), \"refs/tags/\")\n\n\t\t\tif i != 0 {\n\t\t\t\tlabel += \"-\" + strconv.FormatInt(int64(i), 10) + \"-\" + head.Hash().String()[:8]\n\t\t\t}\n\n\t\t\treturn label, nil\n\t\t}\n\n\t\ti++\n\t}\n}\n\nfunc tagFromGit() error {\n\t// [git.PlainOpen] uses a ChrootOS that limits filesystem access to the .git directory only.\n\t//\n\t// Unfortunately, this can cause issues with package build environments such as Arch Linux's,\n\t// where .git/objects/info/alternates points to a directory outside of the .git directory.\n\t//\n\t// To work around this, specify an AlternatesFS that allows access to the entire filesystem.\n\tdotGitAbs, _ := filepath.Abs(\"../../.git\")\n\tstorerFs := osfs.New(dotGitAbs, osfs.WithBoundOS())\n\tstorer := filesystem.NewStorageWithOptions(storerFs, cache.NewObjectLRUDefault(), filesystem.Options{\n\t\tAlternatesFS: osfs.New(\"/\", osfs.WithBoundOS()),\n\t})\n\tworkTreeAbs, _ := filepath.Abs(\"../../\")\n\tworktreeFs := osfs.New(workTreeAbs, osfs.WithBoundOS())\n\trepo, err := git.Open(storer, worktreeFs)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to open repository: %w\", err)\n\t}\n\n\tversion, err := gitDescribeTags(repo)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get version: %w\", err)\n\t}\n\n\twt, err := repo.Worktree()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get worktree: %w\", err)\n\t}\n\n\tstatus, err := wt.Status()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to get status: %w\", err)\n\t}\n\n\tif !status.IsClean() {\n\t\tversion += \"-dirty\"\n\t}\n\n\terr = os.WriteFile(\"VERSION\", []byte(version), 0o644)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to write version file: %w\", err)\n\t}\n\n\tlog.Printf(\"ok (%s)\", version)\n\treturn nil\n}\n\nfunc do() error {\n\tlog.Println(\"getting mediamtx version...\")\n\n\terr := tagFromGit()\n\tif err != nil {\n\t\tlog.Println(\"WARN: cannot get tag from .git folder, using v0.0.0 as version\")\n\t\terr = os.WriteFile(\"VERSION\", []byte(\"v0.0.0\"), 0o644)\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to write version file: %w\", err)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc main() {\n\terr := do()\n\tif err != nil {\n\t\tlog.Printf(\"ERR: %v\", err)\n\t\tos.Exit(1)\n\t}\n}\n"
  },
  {
    "path": "internal/counterdumper/dumper.go",
    "content": "// Package counterdumper contains a counter that that periodically invokes a callback if the counter is not zero.\npackage counterdumper\n\nimport (\n\t\"sync\"\n\t\"time\"\n)\n\nconst (\n\tcallbackPeriod = 1 * time.Second\n)\n\n// Dumper is a counter that periodically invokes a callback if the counter is not zero.\ntype Dumper struct {\n\tOnReport func(v uint64)\n\n\tmutex      sync.Mutex\n\tcounter    uint64\n\tabsCounter uint64\n\n\tterminate chan struct{}\n\tdone      chan struct{}\n}\n\n// Start starts the counter.\nfunc (c *Dumper) Start() {\n\tc.terminate = make(chan struct{})\n\tc.done = make(chan struct{})\n\n\tgo c.run()\n}\n\n// Stop stops the counter.\nfunc (c *Dumper) Stop() {\n\tclose(c.terminate)\n\t<-c.done\n}\n\n// Increase increases the counter value by 1.\nfunc (c *Dumper) Increase() {\n\tc.mutex.Lock()\n\tdefer c.mutex.Unlock()\n\tc.counter++\n\tc.absCounter++\n}\n\n// Add adds value to the counter.\nfunc (c *Dumper) Add(v uint64) {\n\tc.mutex.Lock()\n\tdefer c.mutex.Unlock()\n\tc.counter += v\n\tc.absCounter += v\n}\n\n// Get returns the counter value.\nfunc (c *Dumper) Get() uint64 {\n\tc.mutex.Lock()\n\tdefer c.mutex.Unlock()\n\treturn c.absCounter\n}\n\nfunc (c *Dumper) run() {\n\tdefer close(c.done)\n\n\tt := time.NewTicker(callbackPeriod)\n\tdefer t.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-c.terminate:\n\t\t\treturn\n\n\t\tcase <-t.C:\n\t\t\tc.mutex.Lock()\n\t\t\tvar v uint64\n\t\t\tv, c.counter = c.counter, 0\n\t\t\tc.mutex.Unlock()\n\n\t\t\tif v != 0 {\n\t\t\t\tc.OnReport(v)\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/counterdumper/dumper_test.go",
    "content": "package counterdumper\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDumperReport(t *testing.T) {\n\tdone := make(chan struct{})\n\n\tc := &Dumper{\n\t\tOnReport: func(v uint64) {\n\t\t\trequire.Equal(t, uint64(3), v)\n\t\t\tclose(done)\n\t\t},\n\t}\n\tc.Start()\n\tdefer c.Stop()\n\n\tc.Add(2)\n\tc.Increase()\n\n\tselect {\n\tcase <-done:\n\tcase <-time.After(2 * time.Second):\n\t\tt.Errorf(\"should not happen\")\n\t}\n}\n\nfunc TestDumperDoNotReport(t *testing.T) {\n\tc := &Dumper{\n\t\tOnReport: func(_ uint64) {\n\t\t\tt.Errorf(\"should not happen\")\n\t\t},\n\t}\n\tc.Start()\n\tdefer c.Stop()\n\n\t<-time.After(2 * time.Second)\n}\n"
  },
  {
    "path": "internal/defs/api.go",
    "content": "package defs\n\nimport (\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n)\n\n// APIOKStatus is the status of a successful response.\ntype APIOKStatus string\n\n// statuses.\nconst (\n\tAPIOKStatusOK APIOKStatus = \"ok\"\n)\n\n// APIErrorStatus is the status of an error response.\ntype APIErrorStatus string\n\n// statuses.\nconst (\n\tAPIErrorStatusError APIErrorStatus = \"error\"\n)\n\n// APIOK is returned on success.\ntype APIOK struct {\n\tStatus APIOKStatus `json:\"status\"`\n}\n\n// APIError is a generic error.\ntype APIError struct {\n\tStatus APIErrorStatus `json:\"status\"`\n\tError  string         `json:\"error\"`\n}\n\n// APIInfo is a info response.\ntype APIInfo struct {\n\tVersion string    `json:\"version\"`\n\tStarted time.Time `json:\"started\"`\n}\n\n// APIPathConfList is a list of path configurations.\ntype APIPathConfList struct {\n\tItemCount int         `json:\"itemCount\"`\n\tPageCount int         `json:\"pageCount\"`\n\tItems     []conf.Path `json:\"items\"`\n}\n"
  },
  {
    "path": "internal/defs/api_hls.go",
    "content": "package defs\n\nimport \"time\"\n\n// APIHLSServer contains methods used by the API and Metrics server.\ntype APIHLSServer interface {\n\tAPIMuxersList() (*APIHLSMuxerList, error)\n\tAPIMuxersGet(string) (*APIHLSMuxer, error)\n}\n\n// APIHLSMuxer is an HLS muxer.\ntype APIHLSMuxer struct {\n\tPath                    string    `json:\"path\"`\n\tCreated                 time.Time `json:\"created\"`\n\tLastRequest             time.Time `json:\"lastRequest\"`\n\tOutboundBytes           uint64    `json:\"outboundBytes\"`\n\tOutboundFramesDiscarded uint64    `json:\"outboundFramesDiscarded\"`\n\t// deprecated\n\tBytesSent uint64 `json:\"bytesSent\" deprecated:\"true\"`\n}\n\n// APIHLSMuxerList is a list of HLS muxers.\ntype APIHLSMuxerList struct {\n\tItemCount int           `json:\"itemCount\"`\n\tPageCount int           `json:\"pageCount\"`\n\tItems     []APIHLSMuxer `json:\"items\"`\n}\n"
  },
  {
    "path": "internal/defs/api_path.go",
    "content": "package defs\n\nimport (\n\t\"time\"\n)\n\n// APIPathManager contains methods used by the API and Metrics server.\ntype APIPathManager interface {\n\tAPIPathsList() (*APIPathList, error)\n\tAPIPathsGet(string) (*APIPath, error)\n}\n\n// APIPathSourceType is the type of a path source.\ntype APIPathSourceType string\n\n// source types.\nconst (\n\tAPIPathSourceTypeHLSSource       APIPathSourceType = \"hlsSource\"\n\tAPIPathSourceTypeRedirect        APIPathSourceType = \"redirect\"\n\tAPIPathSourceTypeRPICameraSource APIPathSourceType = \"rpiCameraSource\"\n\tAPIPathSourceTypeRTMPConn        APIPathSourceType = \"rtmpConn\"\n\tAPIPathSourceTypeRTMPSConn       APIPathSourceType = \"rtmpsConn\"\n\tAPIPathSourceTypeRTMPSource      APIPathSourceType = \"rtmpSource\"\n\tAPIPathSourceTypeRTSPSession     APIPathSourceType = \"rtspSession\"\n\tAPIPathSourceTypeRTSPSource      APIPathSourceType = \"rtspSource\"\n\tAPIPathSourceTypeRTSPSSession    APIPathSourceType = \"rtspsSession\"\n\tAPIPathSourceTypeSRTConn         APIPathSourceType = \"srtConn\"\n\tAPIPathSourceTypeSRTSource       APIPathSourceType = \"srtSource\"\n\tAPIPathSourceTypeMPEGTSSource    APIPathSourceType = \"mpegtsSource\"\n\tAPIPathSourceTypeRTPSource       APIPathSourceType = \"rtpSource\"\n\tAPIPathSourceTypeWebRTCSession   APIPathSourceType = \"webRTCSession\"\n\tAPIPathSourceTypeWebRTCSource    APIPathSourceType = \"webRTCSource\"\n)\n\n// APIPathSource is a source.\ntype APIPathSource struct {\n\tType APIPathSourceType `json:\"type\"`\n\tID   string            `json:\"id\"`\n}\n\n// APIPathReaderType is the type of a path reader.\ntype APIPathReaderType string\n\n// reader types.\nconst (\n\tAPIPathReaderTypeHLSMuxer           APIPathReaderType = \"hlsMuxer\"\n\tAPIPathReaderTypeRTMPConn           APIPathReaderType = \"rtmpConn\"\n\tAPIPathReaderTypeRTMPSConn          APIPathReaderType = \"rtmpsConn\"\n\tAPIPathReaderTypeRTSPConn           APIPathReaderType = \"rtspConn\"\n\tAPIPathReaderTypeRPICameraSecondary APIPathReaderType = \"rpiCameraSecondary\"\n\tAPIPathReaderTypeRTSPSession        APIPathReaderType = \"rtspSession\"\n\tAPIPathReaderTypeRTSPSConn          APIPathReaderType = \"rtspsConn\"\n\tAPIPathReaderTypeRTSPSSession       APIPathReaderType = \"rtspsSession\"\n\tAPIPathReaderTypeSRTConn            APIPathReaderType = \"srtConn\"\n\tAPIPathReaderTypeWebRTCSession      APIPathReaderType = \"webRTCSession\"\n)\n\n// APIPathReader is a reader.\ntype APIPathReader struct {\n\tType APIPathReaderType `json:\"type\"`\n\tID   string            `json:\"id\"`\n}\n\n// APIPath is a path.\ntype APIPath struct {\n\tName                 string              `json:\"name\"`\n\tConfName             string              `json:\"confName\"`\n\tReady                bool                `json:\"ready\" deprecated:\"true\"`\n\tReadyTime            *time.Time          `json:\"readyTime\" deprecated:\"true\"`\n\tAvailable            bool                `json:\"available\"`\n\tAvailableTime        *time.Time          `json:\"availableTime\"`\n\tOnline               bool                `json:\"online\"`\n\tOnlineTime           *time.Time          `json:\"onlineTime\"`\n\tSource               *APIPathSource      `json:\"source\"`\n\tTracks               []APIPathTrackCodec `json:\"tracks\" deprecated:\"true\"`\n\tTracks2              []APIPathTrack      `json:\"tracks2\"`\n\tReaders              []APIPathReader     `json:\"readers\"`\n\tInboundBytes         uint64              `json:\"inboundBytes\"`\n\tOutboundBytes        uint64              `json:\"outboundBytes\"`\n\tInboundFramesInError uint64              `json:\"inboundFramesInError\"`\n\t// deprecated\n\tBytesReceived uint64 `json:\"bytesReceived\" deprecated:\"true\"`\n\tBytesSent     uint64 `json:\"bytesSent\" deprecated:\"true\"`\n}\n\n// APIPathList is a list of paths.\ntype APIPathList struct {\n\tItemCount int       `json:\"itemCount\"`\n\tPageCount int       `json:\"pageCount\"`\n\tItems     []APIPath `json:\"items\"`\n}\n"
  },
  {
    "path": "internal/defs/api_path_track.go",
    "content": "package defs\n\n// APIPathTrack is a track.\ntype APIPathTrack struct {\n\tCodec      APIPathTrackCodec      `json:\"codec\"`\n\tCodecProps APIPathTrackCodecProps `json:\"codecProps\"`\n}\n"
  },
  {
    "path": "internal/defs/api_path_track_codec.go",
    "content": "package defs\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\tcodecsh264 \"github.com/bluenviron/mediacommon/v2/pkg/codecs/h264\"\n\tcodecsh265 \"github.com/bluenviron/mediacommon/v2/pkg/codecs/h265\"\n)\n\n// APIPathTrackCodec is a path track codec.\ntype APIPathTrackCodec string\n\n// path track codecs.\nconst (\n\t// video\n\tAPIPathTrackCodecAV1        APIPathTrackCodec = \"AV1\"\n\tAPIPathTrackCodecVP9        APIPathTrackCodec = \"VP9\"\n\tAPIPathTrackCodecVP8        APIPathTrackCodec = \"VP8\"\n\tAPIPathTrackCodecH265       APIPathTrackCodec = \"H265\"\n\tAPIPathTrackCodecH264       APIPathTrackCodec = \"H264\"\n\tAPIPathTrackCodecMPEG4Video APIPathTrackCodec = \"MPEG-4 Video\"\n\tAPIPathTrackCodecMPEG1Video APIPathTrackCodec = \"MPEG-1 Video\"\n\tAPIPathTrackCodecMJPEG      APIPathTrackCodec = \"MJPEG\"\n\t// audio\n\tAPIPathTrackCodecOpus           APIPathTrackCodec = \"Opus\"\n\tAPIPathTrackCodecVorbis         APIPathTrackCodec = \"Vorbis\"\n\tAPIPathTrackCodecMPEG4Audio     APIPathTrackCodec = \"MPEG-4 Audio\"\n\tAPIPathTrackCodecMPEG4AudioLATM APIPathTrackCodec = \"MPEG-4 Audio LATM\"\n\tAPIPathTrackCodecMPEG1Audio     APIPathTrackCodec = \"MPEG-1 Audio\"\n\tAPIPathTrackCodecAC3            APIPathTrackCodec = \"AC3\"\n\tAPIPathTrackCodecSpeex          APIPathTrackCodec = \"Speex\"\n\tAPIPathTrackCodecG726           APIPathTrackCodec = \"G726\"\n\tAPIPathTrackCodecG722           APIPathTrackCodec = \"G722\"\n\tAPIPathTrackCodecG711           APIPathTrackCodec = \"G711\"\n\tAPIPathTrackCodecLPCM           APIPathTrackCodec = \"LPCM\"\n\t// other\n\tAPIPathTrackCodecMPEGTS  APIPathTrackCodec = \"MPEG-TS\"\n\tAPIPathTrackCodecKLV     APIPathTrackCodec = \"KLV\"\n\tAPIPathTrackCodecGeneric APIPathTrackCodec = \"Generic\"\n)\n\nfunc formatToCodec(forma format.Format) APIPathTrackCodec {\n\tswitch forma.(type) {\n\t// video\n\tcase *format.AV1:\n\t\treturn APIPathTrackCodecAV1\n\tcase *format.VP9:\n\t\treturn APIPathTrackCodecVP9\n\tcase *format.VP8:\n\t\treturn APIPathTrackCodecVP8\n\tcase *format.H265:\n\t\treturn APIPathTrackCodecH265\n\tcase *format.H264:\n\t\treturn APIPathTrackCodecH264\n\tcase *format.MPEG4Video:\n\t\treturn APIPathTrackCodecMPEG4Video\n\tcase *format.MPEG1Video:\n\t\treturn APIPathTrackCodecMPEG1Video\n\tcase *format.MJPEG:\n\t\treturn APIPathTrackCodecMJPEG\n\t// audio\n\tcase *format.Opus:\n\t\treturn APIPathTrackCodecOpus\n\tcase *format.Vorbis:\n\t\treturn APIPathTrackCodecVorbis\n\tcase *format.MPEG4Audio:\n\t\treturn APIPathTrackCodecMPEG4Audio\n\tcase *format.MPEG4AudioLATM:\n\t\treturn APIPathTrackCodecMPEG4AudioLATM\n\tcase *format.MPEG1Audio:\n\t\treturn APIPathTrackCodecMPEG1Audio\n\tcase *format.AC3:\n\t\treturn APIPathTrackCodecAC3\n\tcase *format.Speex:\n\t\treturn APIPathTrackCodecSpeex\n\tcase *format.G726:\n\t\treturn APIPathTrackCodecG726\n\tcase *format.G722:\n\t\treturn APIPathTrackCodecG722\n\tcase *format.G711:\n\t\treturn APIPathTrackCodecG711\n\tcase *format.LPCM:\n\t\treturn APIPathTrackCodecLPCM\n\t// other\n\tcase *format.MPEGTS:\n\t\treturn APIPathTrackCodecMPEGTS\n\tcase *format.KLV:\n\t\treturn APIPathTrackCodecKLV\n\tdefault:\n\t\treturn APIPathTrackCodecGeneric\n\t}\n}\n\nfunc h264ProfileString(profileIdc uint8) string {\n\tswitch profileIdc {\n\tcase 66:\n\t\treturn \"Baseline\"\n\tcase 77:\n\t\treturn \"Main\"\n\tcase 88:\n\t\treturn \"Extended\"\n\tcase 100:\n\t\treturn \"High\"\n\tcase 110:\n\t\treturn \"High 10\"\n\tcase 122:\n\t\treturn \"High 4:2:2\"\n\tcase 244:\n\t\treturn \"High 4:4:4 Predictive\"\n\tdefault:\n\t\treturn strconv.Itoa(int(profileIdc))\n\t}\n}\n\nfunc h264LevelString(levelIdc uint8) string {\n\tmajor := levelIdc / 10\n\tminor := levelIdc % 10\n\tif minor == 0 {\n\t\treturn fmt.Sprintf(\"%d\", major)\n\t}\n\treturn fmt.Sprintf(\"%d.%d\", major, minor)\n}\n\nfunc h265ProfileString(profileIdc uint8) string {\n\tswitch profileIdc {\n\tcase 1:\n\t\treturn \"Main\"\n\tcase 2:\n\t\treturn \"Main 10\"\n\tcase 3:\n\t\treturn \"Main Still Picture\"\n\tcase 4:\n\t\treturn \"Range Extensions\"\n\tcase 5:\n\t\treturn \"High Throughput\"\n\tcase 6:\n\t\treturn \"Multiview Main\"\n\tcase 7:\n\t\treturn \"Scalable Main\"\n\tcase 8:\n\t\treturn \"3D Main\"\n\tcase 9:\n\t\treturn \"Screen Content Coding Extensions\"\n\tcase 10:\n\t\treturn \"Scalable Range Extensions\"\n\tdefault:\n\t\treturn strconv.Itoa(int(profileIdc))\n\t}\n}\n\nfunc h265LevelString(levelIdc uint8) string {\n\t// H.265 level_idc = 30 * level, so 3.0 = 90, 4.0 = 120, 4.1 = 123\n\tlevel := float64(levelIdc) / 30.0\n\t// Check if it's a clean level like 3.0, 4.0\n\tif levelIdc%30 == 0 {\n\t\treturn fmt.Sprintf(\"%.0f\", level)\n\t}\n\treturn fmt.Sprintf(\"%.1f\", level)\n}\n\n// FormatsToCodecs returns codecs of given formats.\nfunc FormatsToCodecs(formats []format.Format) []APIPathTrackCodec {\n\tret := make([]APIPathTrackCodec, len(formats))\n\tfor i, forma := range formats {\n\t\tret[i] = formatToCodec(forma)\n\t}\n\treturn ret\n}\n\nfunc formatToTrack(forma format.Format) APIPathTrack {\n\treturn APIPathTrack{\n\t\tCodec:      formatToCodec(forma),\n\t\tCodecProps: formatToTrackCodecProps(forma),\n\t}\n}\n\nfunc formatToTrackCodecProps(forma format.Format) APIPathTrackCodecProps {\n\tswitch forma := forma.(type) {\n\tcase *format.AV1:\n\t\tprops := &APIPathTrackCodecPropsAV1{}\n\n\t\tif forma.Profile != nil {\n\t\t\tprops.Profile = *forma.Profile\n\t\t}\n\t\tif forma.LevelIdx != nil {\n\t\t\tprops.Level = *forma.LevelIdx\n\t\t}\n\t\tif forma.Tier != nil {\n\t\t\tprops.Tier = *forma.Tier\n\t\t}\n\n\t\treturn props\n\n\tcase *format.VP9:\n\t\tprops := &APIPathTrackCodecPropsVP9{}\n\n\t\tif forma.ProfileID != nil {\n\t\t\tprops.Profile = *forma.ProfileID\n\t\t}\n\n\t\treturn props\n\n\tcase *format.H265:\n\t\tprops := &APIPathTrackCodecPropsH265{}\n\n\t\t_, sps, _ := forma.SafeParams()\n\t\tif sps != nil {\n\t\t\tvar s codecsh265.SPS\n\t\t\tif err := s.Unmarshal(sps); err == nil {\n\t\t\t\tprops.Width = s.Width()\n\t\t\t\tprops.Height = s.Height()\n\t\t\t\tprops.Profile = h265ProfileString(s.ProfileTierLevel.GeneralProfileIdc)\n\t\t\t\tprops.Level = h265LevelString(s.ProfileTierLevel.GeneralLevelIdc)\n\t\t\t}\n\t\t}\n\n\t\treturn props\n\n\tcase *format.H264:\n\t\tprops := &APIPathTrackCodecPropsH264{}\n\n\t\tsps, _ := forma.SafeParams()\n\t\tif sps != nil {\n\t\t\tvar s codecsh264.SPS\n\t\t\tif err := s.Unmarshal(sps); err == nil {\n\t\t\t\tprops.Width = s.Width()\n\t\t\t\tprops.Height = s.Height()\n\t\t\t\tprops.Profile = h264ProfileString(s.ProfileIdc)\n\t\t\t\tprops.Level = h264LevelString(s.LevelIdc)\n\t\t\t}\n\t\t}\n\n\t\treturn props\n\n\tcase *format.Opus:\n\t\treturn &APIPathTrackCodecPropsOpus{\n\t\t\tChannelCount: forma.ChannelCount,\n\t\t}\n\n\tcase *format.MPEG4Audio:\n\t\tprops := &APIPathTrackCodecPropsMPEG4Audio{}\n\n\t\tif forma.Config != nil {\n\t\t\tprops.SampleRate = forma.Config.SampleRate\n\t\t\tprops.ChannelCount = int(forma.Config.ChannelConfig)\n\t\t}\n\n\t\treturn props\n\n\tcase *format.AC3:\n\t\treturn &APIPathTrackCodecPropsAC3{\n\t\t\tSampleRate:   forma.SampleRate,\n\t\t\tChannelCount: forma.ChannelCount,\n\t\t}\n\n\tcase *format.G711:\n\t\treturn &APIPathTrackCodecPropsG711{\n\t\t\tMULaw:        forma.MULaw,\n\t\t\tSampleRate:   forma.SampleRate,\n\t\t\tChannelCount: forma.ChannelCount,\n\t\t}\n\n\tcase *format.LPCM:\n\t\treturn &APIPathTrackCodecPropsLPCM{\n\t\t\tBitDepth:     forma.BitDepth,\n\t\t\tSampleRate:   forma.SampleRate,\n\t\t\tChannelCount: forma.ChannelCount,\n\t\t}\n\n\tdefault:\n\t\treturn nil\n\t}\n}\n\nfunc gatherFormats(medias []*description.Media) []format.Format {\n\tn := 0\n\tfor _, media := range medias {\n\t\tn += len(media.Formats)\n\t}\n\n\tif n == 0 {\n\t\treturn nil\n\t}\n\n\tformats := make([]format.Format, n)\n\tn = 0\n\n\tfor _, media := range medias {\n\t\tn += copy(formats[n:], media.Formats)\n\t}\n\n\treturn formats\n}\n\n// MediasToCodecs returns codecs of given medias.\nfunc MediasToCodecs(medias []*description.Media) []APIPathTrackCodec {\n\treturn FormatsToCodecs(gatherFormats(medias))\n}\n\n// FormatsToTracks returns tracks of given formats.\nfunc FormatsToTracks(formats []format.Format) []APIPathTrack {\n\tret := make([]APIPathTrack, len(formats))\n\n\tfor i, forma := range formats {\n\t\tret[i] = formatToTrack(forma)\n\t}\n\n\treturn ret\n}\n\n// MediasToTracks returns tracks of given medias.\nfunc MediasToTracks(medias []*description.Media) []APIPathTrack {\n\treturn FormatsToTracks(gatherFormats(medias))\n}\n"
  },
  {
    "path": "internal/defs/api_path_track_codec_props.go",
    "content": "package defs\n\n// APIPathTrackCodecProps contains codec-specific properties.\ntype APIPathTrackCodecProps interface {\n\tapiPathTrackCodecProps()\n}\n\n// APIPathTrackCodecPropsAV1 contains codec-specific properties of AV1.\ntype APIPathTrackCodecPropsAV1 struct {\n\tWidth   int `json:\"width\"`\n\tHeight  int `json:\"height\"`\n\tProfile int `json:\"profile\"`\n\tLevel   int `json:\"level\"`\n\tTier    int `json:\"tier\"`\n}\n\nfunc (*APIPathTrackCodecPropsAV1) apiPathTrackCodecProps() {}\n\n// APIPathTrackCodecPropsVP9 contains codec-specific properties of VP9.\ntype APIPathTrackCodecPropsVP9 struct {\n\tProfile int `json:\"profile\"`\n}\n\nfunc (*APIPathTrackCodecPropsVP9) apiPathTrackCodecProps() {}\n\n// APIPathTrackCodecPropsH265 contains codec-specific properties of H265.\ntype APIPathTrackCodecPropsH265 struct {\n\tWidth   int    `json:\"width\"`\n\tHeight  int    `json:\"height\"`\n\tProfile string `json:\"profile\"`\n\tLevel   string `json:\"level\"`\n}\n\nfunc (*APIPathTrackCodecPropsH265) apiPathTrackCodecProps() {}\n\n// APIPathTrackCodecPropsH264 contains codec-specific properties of H264.\ntype APIPathTrackCodecPropsH264 struct {\n\tWidth   int    `json:\"width\"`\n\tHeight  int    `json:\"height\"`\n\tProfile string `json:\"profile\"`\n\tLevel   string `json:\"level\"`\n}\n\nfunc (*APIPathTrackCodecPropsH264) apiPathTrackCodecProps() {}\n\n// APIPathTrackCodecPropsOpus contains codec-specific properties of Opus.\ntype APIPathTrackCodecPropsOpus struct {\n\tChannelCount int `json:\"channelCount\"`\n}\n\nfunc (*APIPathTrackCodecPropsOpus) apiPathTrackCodecProps() {}\n\n// APIPathTrackCodecPropsMPEG4Audio contains codec-specific properties of MPEG4Audio.\ntype APIPathTrackCodecPropsMPEG4Audio struct {\n\tSampleRate   int `json:\"sampleRate\"`\n\tChannelCount int `json:\"channelCount\"`\n}\n\nfunc (*APIPathTrackCodecPropsMPEG4Audio) apiPathTrackCodecProps() {}\n\n// APIPathTrackCodecPropsAC3 contains codec-specific properties of AC3.\ntype APIPathTrackCodecPropsAC3 struct {\n\tSampleRate   int `json:\"sampleRate\"`\n\tChannelCount int `json:\"channelCount\"`\n}\n\nfunc (*APIPathTrackCodecPropsAC3) apiPathTrackCodecProps() {}\n\n// APIPathTrackCodecPropsG711 contains codec-specific properties of G711.\ntype APIPathTrackCodecPropsG711 struct {\n\tMULaw        bool `json:\"muLaw\"`\n\tSampleRate   int  `json:\"sampleRate\"`\n\tChannelCount int  `json:\"channelCount\"`\n}\n\nfunc (*APIPathTrackCodecPropsG711) apiPathTrackCodecProps() {}\n\n// APIPathTrackCodecPropsLPCM contains codec-specific properties of LPCM.\ntype APIPathTrackCodecPropsLPCM struct {\n\tBitDepth     int `json:\"bitDepth\"`\n\tSampleRate   int `json:\"sampleRate\"`\n\tChannelCount int `json:\"channelCount\"`\n}\n\nfunc (*APIPathTrackCodecPropsLPCM) apiPathTrackCodecProps() {}\n"
  },
  {
    "path": "internal/defs/api_path_track_codec_test.go",
    "content": "package defs\n\nimport (\n\t\"testing\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc ptr[T any](v T) *T {\n\treturn &v\n}\n\nfunc testFormatH264() *format.H264 {\n\treturn &format.H264{\n\t\tPayloadTyp: 96,\n\t\tSPS: []byte{\n\t\t\t0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02,\n\t\t\t0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04,\n\t\t\t0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, 0x20,\n\t\t},\n\t\tPPS:               []byte{0x08, 0x06, 0x07, 0x08},\n\t\tPacketizationMode: 1,\n\t}\n}\n\nfunc testFormatMPEG4Audio() *format.MPEG4Audio {\n\treturn &format.MPEG4Audio{\n\t\tPayloadTyp: 96,\n\t\tConfig: &mpeg4audio.AudioSpecificConfig{\n\t\t\tType:          2,\n\t\t\tSampleRate:    44100,\n\t\t\tChannelCount:  2,\n\t\t\tChannelConfig: 2,\n\t\t},\n\t\tSizeLength:       13,\n\t\tIndexLength:      3,\n\t\tIndexDeltaLength: 3,\n\t}\n}\n\nfunc testFormatH265() *format.H265 {\n\treturn &format.H265{\n\t\tPayloadTyp: 96,\n\t\tVPS: []byte{\n\t\t\t0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x02, 0x20,\n\t\t\t0x00, 0x00, 0x03, 0x00, 0xb0, 0x00, 0x00, 0x03,\n\t\t\t0x00, 0x00, 0x03, 0x00, 0x7b, 0x18, 0xb0, 0x24,\n\t\t},\n\t\tSPS: []byte{\n\t\t\t0x42, 0x01, 0x01, 0x02, 0x20, 0x00, 0x00, 0x03,\n\t\t\t0x00, 0xb0, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03,\n\t\t\t0x00, 0x7b, 0xa0, 0x07, 0x82, 0x00, 0x88, 0x7d,\n\t\t\t0xb6, 0x71, 0x8b, 0x92, 0x44, 0x80, 0x53, 0x88,\n\t\t\t0x88, 0x92, 0xcf, 0x24, 0xa6, 0x92, 0x72, 0xc9,\n\t\t\t0x12, 0x49, 0x22, 0xdc, 0x91, 0xaa, 0x48, 0xfc,\n\t\t\t0xa2, 0x23, 0xff, 0x00, 0x01, 0x00, 0x01, 0x6a,\n\t\t\t0x02, 0x02, 0x02, 0x01,\n\t\t},\n\t\tPPS: []byte{\n\t\t\t0x44, 0x01, 0xc0, 0x25, 0x2f, 0x05, 0x32, 0x40,\n\t\t},\n\t}\n}\n\nfunc TestFormatsToCodecs(t *testing.T) {\n\tcodecs := FormatsToCodecs([]format.Format{\n\t\t&format.AV1{},\n\t\t&format.VP9{},\n\t\t&format.VP8{},\n\t\t&format.H265{},\n\t\t&format.H264{},\n\t\t&format.MPEG4Video{},\n\t\t&format.MPEG1Video{},\n\t\t&format.MJPEG{},\n\t\t&format.Opus{},\n\t\t&format.Vorbis{},\n\t\t&format.MPEG4Audio{},\n\t\t&format.MPEG4AudioLATM{},\n\t\t&format.MPEG1Audio{},\n\t\t&format.AC3{},\n\t\t&format.Speex{},\n\t\t&format.G726{},\n\t\t&format.G722{},\n\t\t&format.G711{},\n\t\t&format.LPCM{},\n\t\t&format.MPEGTS{},\n\t\t&format.KLV{},\n\t\t&format.Generic{},\n\t})\n\n\trequire.Equal(t, []APIPathTrackCodec{\n\t\tAPIPathTrackCodecAV1,\n\t\tAPIPathTrackCodecVP9,\n\t\tAPIPathTrackCodecVP8,\n\t\tAPIPathTrackCodecH265,\n\t\tAPIPathTrackCodecH264,\n\t\tAPIPathTrackCodecMPEG4Video,\n\t\tAPIPathTrackCodecMPEG1Video,\n\t\tAPIPathTrackCodecMJPEG,\n\t\tAPIPathTrackCodecOpus,\n\t\tAPIPathTrackCodecVorbis,\n\t\tAPIPathTrackCodecMPEG4Audio,\n\t\tAPIPathTrackCodecMPEG4AudioLATM,\n\t\tAPIPathTrackCodecMPEG1Audio,\n\t\tAPIPathTrackCodecAC3,\n\t\tAPIPathTrackCodecSpeex,\n\t\tAPIPathTrackCodecG726,\n\t\tAPIPathTrackCodecG722,\n\t\tAPIPathTrackCodecG711,\n\t\tAPIPathTrackCodecLPCM,\n\t\tAPIPathTrackCodecMPEGTS,\n\t\tAPIPathTrackCodecKLV,\n\t\tAPIPathTrackCodecGeneric,\n\t}, codecs)\n}\n\nfunc TestMediasToTracks(t *testing.T) {\n\ttracks := MediasToTracks([]*description.Media{\n\t\t{Formats: []format.Format{testFormatH264()}},\n\t\t{Formats: []format.Format{testFormatH265()}},\n\t\t{\n\t\t\tFormats: []format.Format{\n\t\t\t\t&format.AV1{Profile: ptr(1), LevelIdx: ptr(9), Tier: ptr(0)},\n\t\t\t\t&format.VP9{ProfileID: ptr(2)},\n\t\t\t\t&format.MJPEG{},\n\t\t\t\t&format.Opus{ChannelCount: 2},\n\t\t\t\t&format.G711{MULaw: true, SampleRate: 8000, ChannelCount: 1},\n\t\t\t\t&format.LPCM{BitDepth: 16, SampleRate: 48000, ChannelCount: 2},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tFormats: []format.Format{testFormatMPEG4Audio()},\n\t\t},\n\t})\n\n\trequire.Equal(t, []APIPathTrack{\n\t\t{\n\t\t\tCodec: APIPathTrackCodecH264,\n\t\t\tCodecProps: &APIPathTrackCodecPropsH264{\n\t\t\t\tWidth:   1920,\n\t\t\t\tHeight:  1080,\n\t\t\t\tProfile: \"Baseline\",\n\t\t\t\tLevel:   \"4\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tCodec: APIPathTrackCodecH265,\n\t\t\tCodecProps: &APIPathTrackCodecPropsH265{\n\t\t\t\tWidth:   960,\n\t\t\t\tHeight:  540,\n\t\t\t\tProfile: \"Main 10\",\n\t\t\t\tLevel:   \"4.1\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tCodec: APIPathTrackCodecAV1,\n\t\t\tCodecProps: &APIPathTrackCodecPropsAV1{\n\t\t\t\tProfile: 1,\n\t\t\t\tLevel:   9,\n\t\t\t\tTier:    0,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tCodec: APIPathTrackCodecVP9,\n\t\t\tCodecProps: &APIPathTrackCodecPropsVP9{\n\t\t\t\tProfile: 2,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tCodec:      APIPathTrackCodecMJPEG,\n\t\t\tCodecProps: nil,\n\t\t},\n\t\t{\n\t\t\tCodec: APIPathTrackCodecOpus,\n\t\t\tCodecProps: &APIPathTrackCodecPropsOpus{\n\t\t\t\tChannelCount: 2,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tCodec: APIPathTrackCodecG711,\n\t\t\tCodecProps: &APIPathTrackCodecPropsG711{\n\t\t\t\tMULaw:        true,\n\t\t\t\tSampleRate:   8000,\n\t\t\t\tChannelCount: 1,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tCodec: APIPathTrackCodecLPCM,\n\t\t\tCodecProps: &APIPathTrackCodecPropsLPCM{\n\t\t\t\tBitDepth:     16,\n\t\t\t\tSampleRate:   48000,\n\t\t\t\tChannelCount: 2,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tCodec: APIPathTrackCodecMPEG4Audio,\n\t\t\tCodecProps: &APIPathTrackCodecPropsMPEG4Audio{\n\t\t\t\tSampleRate:   44100,\n\t\t\t\tChannelCount: 2,\n\t\t\t},\n\t\t},\n\t}, tracks)\n}\n"
  },
  {
    "path": "internal/defs/api_recording.go",
    "content": "package defs\n\nimport \"time\"\n\n// APIRecordingSegment is a recording segment.\ntype APIRecordingSegment struct {\n\tStart time.Time `json:\"start\"`\n}\n\n// APIRecording is a recording.\ntype APIRecording struct {\n\tName     string                `json:\"name\"`\n\tSegments []APIRecordingSegment `json:\"segments\"`\n}\n\n// APIRecordingList is a list of recordings.\ntype APIRecordingList struct {\n\tItemCount int            `json:\"itemCount\"`\n\tPageCount int            `json:\"pageCount\"`\n\tItems     []APIRecording `json:\"items\"`\n}\n"
  },
  {
    "path": "internal/defs/api_rtmp.go",
    "content": "package defs\n\nimport (\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n)\n\n// APIRTMPServer contains methods used by the API and Metrics server.\ntype APIRTMPServer interface {\n\tAPIConnsList() (*APIRTMPConnList, error)\n\tAPIConnsGet(uuid.UUID) (*APIRTMPConn, error)\n\tAPIConnsKick(uuid.UUID) error\n}\n\n// APIRTMPConnState is the state of a RTMP connection.\ntype APIRTMPConnState string\n\n// states.\nconst (\n\tAPIRTMPConnStateIdle    APIRTMPConnState = \"idle\"\n\tAPIRTMPConnStateRead    APIRTMPConnState = \"read\"\n\tAPIRTMPConnStatePublish APIRTMPConnState = \"publish\"\n)\n\n// APIRTMPConn is a RTMP connection.\ntype APIRTMPConn struct {\n\tID                      uuid.UUID        `json:\"id\"`\n\tCreated                 time.Time        `json:\"created\"`\n\tRemoteAddr              string           `json:\"remoteAddr\"`\n\tState                   APIRTMPConnState `json:\"state\"`\n\tPath                    string           `json:\"path\"`\n\tQuery                   string           `json:\"query\"`\n\tUser                    string           `json:\"user\"`\n\tInboundBytes            uint64           `json:\"inboundBytes\"`\n\tOutboundBytes           uint64           `json:\"outboundBytes\"`\n\tOutboundFramesDiscarded uint64           `json:\"outboundFramesDiscarded\"`\n\t// deprecated\n\tBytesReceived uint64 `json:\"bytesReceived\" deprecated:\"true\"`\n\tBytesSent     uint64 `json:\"bytesSent\" deprecated:\"true\"`\n}\n\n// APIRTMPConnList is a list of RTMP connections.\ntype APIRTMPConnList struct {\n\tItemCount int           `json:\"itemCount\"`\n\tPageCount int           `json:\"pageCount\"`\n\tItems     []APIRTMPConn `json:\"items\"`\n}\n"
  },
  {
    "path": "internal/defs/api_rtsp.go",
    "content": "package defs\n\nimport (\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n)\n\n// APIRTSPServer contains methods used by the API and Metrics server.\ntype APIRTSPServer interface {\n\tAPIConnsList() (*APIRTSPConnsList, error)\n\tAPIConnsGet(uuid.UUID) (*APIRTSPConn, error)\n\tAPISessionsList() (*APIRTSPSessionList, error)\n\tAPISessionsGet(uuid.UUID) (*APIRTSPSession, error)\n\tAPISessionsKick(uuid.UUID) error\n}\n\n// APIRTSPConn is a RTSP connection.\ntype APIRTSPConn struct {\n\tID            uuid.UUID  `json:\"id\"`\n\tCreated       time.Time  `json:\"created\"`\n\tRemoteAddr    string     `json:\"remoteAddr\"`\n\tSession       *uuid.UUID `json:\"session\"`\n\tTunnel        string     `json:\"tunnel\"`\n\tInboundBytes  uint64     `json:\"inboundBytes\"`\n\tOutboundBytes uint64     `json:\"outboundBytes\"`\n\tBytesReceived uint64     `json:\"bytesReceived\" deprecated:\"true\"`\n\tBytesSent     uint64     `json:\"bytesSent\" deprecated:\"true\"`\n}\n\n// APIRTSPConnsList is a list of RTSP connections.\ntype APIRTSPConnsList struct {\n\tItemCount int           `json:\"itemCount\"`\n\tPageCount int           `json:\"pageCount\"`\n\tItems     []APIRTSPConn `json:\"items\"`\n}\n\n// APIRTSPSessionState is the state of a RTSP session.\ntype APIRTSPSessionState string\n\n// states.\nconst (\n\tAPIRTSPSessionStateIdle    APIRTSPSessionState = \"idle\"\n\tAPIRTSPSessionStateRead    APIRTSPSessionState = \"read\"\n\tAPIRTSPSessionStatePublish APIRTSPSessionState = \"publish\"\n)\n\n// APIRTSPSession is a RTSP session.\ntype APIRTSPSession struct {\n\tID                             uuid.UUID           `json:\"id\"`\n\tCreated                        time.Time           `json:\"created\"`\n\tRemoteAddr                     string              `json:\"remoteAddr\"`\n\tState                          APIRTSPSessionState `json:\"state\"`\n\tPath                           string              `json:\"path\"`\n\tQuery                          string              `json:\"query\"`\n\tUser                           string              `json:\"user\"`\n\tTransport                      *string             `json:\"transport\"`\n\tProfile                        *string             `json:\"profile\"`\n\tConns                          []uuid.UUID         `json:\"conns\"`\n\tInboundBytes                   uint64              `json:\"inboundBytes\"`\n\tInboundRTPPackets              uint64              `json:\"inboundRTPPackets\"`\n\tInboundRTPPacketsLost          uint64              `json:\"inboundRTPPacketsLost\"`\n\tInboundRTPPacketsInError       uint64              `json:\"inboundRTPPacketsInError\"`\n\tInboundRTPPacketsJitter        float64             `json:\"inboundRTPPacketsJitter\"`\n\tInboundRTCPPackets             uint64              `json:\"inboundRTCPPackets\"`\n\tInboundRTCPPacketsInError      uint64              `json:\"inboundRTCPPacketsInError\"`\n\tOutboundBytes                  uint64              `json:\"outboundBytes\"`\n\tOutboundRTPPackets             uint64              `json:\"outboundRTPPackets\"`\n\tOutboundRTPPacketsReportedLost uint64              `json:\"outboundRTPPacketsReportedLost\"`\n\tOutboundRTPPacketsDiscarded    uint64              `json:\"outboundRTPPacketsDiscarded\"`\n\tOutboundRTCPPackets            uint64              `json:\"outboundRTCPPackets\"`\n\t// deprecated\n\tBytesReceived       uint64  `json:\"bytesReceived\" deprecated:\"true\"`\n\tBytesSent           uint64  `json:\"bytesSent\" deprecated:\"true\"`\n\tRTPPacketsReceived  uint64  `json:\"rtpPacketsReceived\" deprecated:\"true\"`\n\tRTPPacketsSent      uint64  `json:\"rtpPacketsSent\" deprecated:\"true\"`\n\tRTPPacketsLost      uint64  `json:\"rtpPacketsLost\" deprecated:\"true\"`\n\tRTPPacketsInError   uint64  `json:\"rtpPacketsInError\" deprecated:\"true\"`\n\tRTPPacketsJitter    float64 `json:\"rtpPacketsJitter\" deprecated:\"true\"`\n\tRTCPPacketsReceived uint64  `json:\"rtcpPacketsReceived\" deprecated:\"true\"`\n\tRTCPPacketsSent     uint64  `json:\"rtcpPacketsSent\" deprecated:\"true\"`\n\tRTCPPacketsInError  uint64  `json:\"rtcpPacketsInError\" deprecated:\"true\"`\n}\n\n// APIRTSPSessionList is a list of RTSP sessions.\ntype APIRTSPSessionList struct {\n\tItemCount int              `json:\"itemCount\"`\n\tPageCount int              `json:\"pageCount\"`\n\tItems     []APIRTSPSession `json:\"items\"`\n}\n"
  },
  {
    "path": "internal/defs/api_srt.go",
    "content": "package defs\n\nimport (\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n)\n\n// APISRTServer contains methods used by the API and Metrics server.\ntype APISRTServer interface {\n\tAPIConnsList() (*APISRTConnList, error)\n\tAPIConnsGet(uuid.UUID) (*APISRTConn, error)\n\tAPIConnsKick(uuid.UUID) error\n}\n\n// APISRTConnState is the state of a SRT connection.\ntype APISRTConnState string\n\n// states.\nconst (\n\tAPISRTConnStateIdle    APISRTConnState = \"idle\"\n\tAPISRTConnStateRead    APISRTConnState = \"read\"\n\tAPISRTConnStatePublish APISRTConnState = \"publish\"\n)\n\n// APISRTConn is a SRT connection.\ntype APISRTConn struct {\n\tID         uuid.UUID       `json:\"id\"`\n\tCreated    time.Time       `json:\"created\"`\n\tRemoteAddr string          `json:\"remoteAddr\"`\n\tState      APISRTConnState `json:\"state\"`\n\tPath       string          `json:\"path\"`\n\tQuery      string          `json:\"query\"`\n\tUser       string          `json:\"user\"`\n\n\t// The metric names/comments are pulled from GoSRT\n\n\t// The total number of sent DATA packets, including retransmitted packets\n\tPacketsSent uint64 `json:\"packetsSent\"`\n\t// The total number of received DATA packets, including retransmitted packets\n\tPacketsReceived uint64 `json:\"packetsReceived\"`\n\t// The total number of unique DATA packets sent by the SRT sender\n\tPacketsSentUnique uint64 `json:\"packetsSentUnique\"`\n\t// The total number of unique original, retransmitted or recovered by the packet filter DATA packets\n\t// received in time, decrypted without errors and, as a result, scheduled for delivery to the\n\t// upstream application by the SRT receiver.\n\tPacketsReceivedUnique uint64 `json:\"packetsReceivedUnique\"`\n\t// The total number of data packets considered or reported as lost at the sender side.\n\t// Does not correspond to the packets detected as lost at the receiver side.\n\tPacketsSendLoss uint64 `json:\"packetsSendLoss\"`\n\t// The total number of SRT DATA packets detected as presently missing (either reordered or lost) at the receiver side\n\tPacketsReceivedLoss uint64 `json:\"packetsReceivedLoss\"`\n\t// The total number of retransmitted packets sent by the SRT sender\n\tPacketsRetrans uint64 `json:\"packetsRetrans\"`\n\t// The total number of retransmitted packets registered at the receiver side\n\tPacketsReceivedRetrans uint64 `json:\"packetsReceivedRetrans\"`\n\t// The total number of sent ACK (Acknowledgement) control packets\n\tPacketsSentACK uint64 `json:\"packetsSentACK\"`\n\t// The total number of received ACK (Acknowledgement) control packets\n\tPacketsReceivedACK uint64 `json:\"packetsReceivedACK\"`\n\t// The total number of sent NAK (Negative Acknowledgement) control packets\n\tPacketsSentNAK uint64 `json:\"packetsSentNAK\"`\n\t// The total number of received NAK (Negative Acknowledgement) control packets\n\tPacketsReceivedNAK uint64 `json:\"packetsReceivedNAK\"`\n\t// The total number of sent KM (Key Material) control packets\n\tPacketsSentKM uint64 `json:\"packetsSentKM\"`\n\t// The total number of received KM (Key Material) control packets\n\tPacketsReceivedKM uint64 `json:\"packetsReceivedKM\"`\n\t// The total accumulated time in microseconds, during which the SRT sender has some data to transmit,\n\t// including packets that have been sent, but not yet acknowledged\n\tUsSndDuration uint64 `json:\"usSndDuration\"`\n\t// ??\n\tPacketsReceivedBelated uint64 `json:\"packetsReceivedBelated\"`\n\t// The total number of dropped by the SRT sender DATA packets that have no chance to be delivered in time\n\tPacketsSendDrop uint64 `json:\"packetsSendDrop\"`\n\t// The total number of dropped by the SRT receiver and, as a result,\n\t// not delivered to the upstream application DATA packets\n\tPacketsReceivedDrop uint64 `json:\"packetsReceivedDrop\"`\n\t// The total number of packets that failed to be decrypted at the receiver side\n\tPacketsReceivedUndecrypt uint64 `json:\"packetsReceivedUndecrypt\"`\n\n\t// Same as packetsReceived, but expressed in bytes, including payload and all the headers (IP, TCP, SRT)\n\tBytesReceived uint64 `json:\"bytesReceived\"`\n\t// Same as packetsSent, but expressed in bytes, including payload and all the headers (IP, TCP, SRT)\n\tBytesSent uint64 `json:\"bytesSent\"`\n\t// Same as packetsSentUnique, but expressed in bytes, including payload and all the headers (IP, TCP, SRT)\n\tBytesSentUnique uint64 `json:\"bytesSentUnique\"`\n\t// Same as packetsReceivedUnique, but expressed in bytes, including payload and all the headers (IP, TCP, SRT)\n\tBytesReceivedUnique uint64 `json:\"bytesReceivedUnique\"`\n\t// Same as packetsReceivedLoss, but expressed in bytes, including payload and all the headers (IP, TCP, SRT),\n\t// bytes for the presently missing (either reordered or lost) packets' payloads are estimated\n\t// based on the average packet size\n\tBytesReceivedLoss uint64 `json:\"bytesReceivedLoss\"`\n\t// Same as packetsRetrans, but expressed in bytes, including payload and all the headers (IP, TCP, SRT)\n\tBytesRetrans uint64 `json:\"bytesRetrans\"`\n\t// Same as packetsReceivedRetrans, but expressed in bytes, including payload and all the headers (IP, TCP, SRT)\n\tBytesReceivedRetrans uint64 `json:\"bytesReceivedRetrans\"`\n\t// Same as PacketsReceivedBelated, but expressed in bytes, including payload and all the headers (IP, TCP, SRT)\n\tBytesReceivedBelated uint64 `json:\"bytesReceivedBelated\"`\n\t// Same as packetsSendDrop, but expressed in bytes, including payload and all the headers (IP, TCP, SRT)\n\tBytesSendDrop uint64 `json:\"bytesSendDrop\"`\n\t// Same as packetsReceivedDrop, but expressed in bytes, including payload and all the headers (IP, TCP, SRT)\n\tBytesReceivedDrop uint64 `json:\"bytesReceivedDrop\"`\n\t// Same as packetsReceivedUndecrypt, but expressed in bytes, including payload and all the headers (IP, TCP, SRT)\n\tBytesReceivedUndecrypt uint64 `json:\"bytesReceivedUndecrypt\"`\n\n\t// Current minimum time interval between which consecutive packets are sent, in microseconds\n\tUsPacketsSendPeriod float64 `json:\"usPacketsSendPeriod\"`\n\t// The maximum number of packets that can be \"in flight\"\n\tPacketsFlowWindow uint64 `json:\"packetsFlowWindow\"`\n\t// The number of packets in flight\n\tPacketsFlightSize uint64 `json:\"packetsFlightSize\"`\n\t// Smoothed round-trip time (SRTT), an exponentially-weighted moving average (EWMA)\n\t// of an endpoint's RTT samples, in milliseconds\n\tMsRTT float64 `json:\"msRTT\"`\n\t// Current transmission bandwidth, in Mbps\n\tMbpsSendRate float64 `json:\"mbpsSendRate\"`\n\t// Current receiving bandwidth, in Mbps\n\tMbpsReceiveRate float64 `json:\"mbpsReceiveRate\"`\n\t// Estimated capacity of the network link, in Mbps\n\tMbpsLinkCapacity float64 `json:\"mbpsLinkCapacity\"`\n\t// The available space in the sender's buffer, in bytes\n\tBytesAvailSendBuf uint64 `json:\"bytesAvailSendBuf\"`\n\t// The available space in the receiver's buffer, in bytes\n\tBytesAvailReceiveBuf uint64 `json:\"bytesAvailReceiveBuf\"`\n\t// Transmission bandwidth limit, in Mbps\n\tMbpsMaxBW float64 `json:\"mbpsMaxBW\"`\n\t// Maximum Segment Size (MSS), in bytes\n\tByteMSS uint64 `json:\"byteMSS\"`\n\t// The number of packets in the sender's buffer that are already scheduled\n\t// for sending or even possibly sent, but not yet acknowledged\n\tPacketsSendBuf uint64 `json:\"packetsSendBuf\"`\n\t// Instantaneous (current) value of packetsSndBuf, but expressed in bytes,\n\t// including payload and all headers (IP, TCP, SRT)\n\tBytesSendBuf uint64 `json:\"bytesSendBuf\"`\n\t// The timespan (msec) of packets in the sender's buffer (unacknowledged packets)\n\tMsSendBuf uint64 `json:\"msSendBuf\"`\n\t// Timestamp-based Packet Delivery Delay value of the peer\n\tMsSendTsbPdDelay uint64 `json:\"msSendTsbPdDelay\"`\n\t// The number of acknowledged packets in receiver's buffer\n\tPacketsReceiveBuf uint64 `json:\"packetsReceiveBuf\"`\n\t// Instantaneous (current) value of packetsRcvBuf, expressed in bytes, including payload and all headers (IP, TCP, SRT)\n\tBytesReceiveBuf uint64 `json:\"bytesReceiveBuf\"`\n\t// The timespan (msec) of acknowledged packets in the receiver's buffer\n\tMsReceiveBuf uint64 `json:\"msReceiveBuf\"`\n\t// Timestamp-based Packet Delivery Delay value set on the socket via SRTO_RCVLATENCY or SRTO_LATENCY\n\tMsReceiveTsbPdDelay uint64 `json:\"msReceiveTsbPdDelay\"`\n\t// Instant value of the packet reorder tolerance\n\tPacketsReorderTolerance uint64 `json:\"packetsReorderTolerance\"`\n\t// Accumulated difference between the current time and the time-to-play of a packet that is received late\n\tPacketsReceivedAvgBelatedTime uint64 `json:\"packetsReceivedAvgBelatedTime\"`\n\t// Percentage of resent data vs. sent data\n\tPacketsSendLossRate float64 `json:\"packetsSendLossRate\"`\n\t// Percentage of retransmitted data vs. received data\n\tPacketsReceivedLossRate float64 `json:\"packetsReceivedLossRate\"`\n\n\tOutboundFramesDiscarded uint64 `json:\"outboundFramesDiscarded\"`\n}\n\n// APISRTConnList is a list of SRT connections.\ntype APISRTConnList struct {\n\tItemCount int          `json:\"itemCount\"`\n\tPageCount int          `json:\"pageCount\"`\n\tItems     []APISRTConn `json:\"items\"`\n}\n"
  },
  {
    "path": "internal/defs/api_webrtc.go",
    "content": "package defs\n\nimport (\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n)\n\n// APIWebRTCServer contains methods used by the API and Metrics server.\ntype APIWebRTCServer interface {\n\tAPISessionsList() (*APIWebRTCSessionList, error)\n\tAPISessionsGet(uuid.UUID) (*APIWebRTCSession, error)\n\tAPISessionsKick(uuid.UUID) error\n}\n\n// APIWebRTCSessionState is the state of a WebRTC connection.\ntype APIWebRTCSessionState string\n\n// states.\nconst (\n\tAPIWebRTCSessionStateRead    APIWebRTCSessionState = \"read\"\n\tAPIWebRTCSessionStatePublish APIWebRTCSessionState = \"publish\"\n)\n\n// APIWebRTCSession is a WebRTC session.\ntype APIWebRTCSession struct {\n\tID                        uuid.UUID             `json:\"id\"`\n\tCreated                   time.Time             `json:\"created\"`\n\tRemoteAddr                string                `json:\"remoteAddr\"`\n\tPeerConnectionEstablished bool                  `json:\"peerConnectionEstablished\"`\n\tLocalCandidate            string                `json:\"localCandidate\"`\n\tRemoteCandidate           string                `json:\"remoteCandidate\"`\n\tState                     APIWebRTCSessionState `json:\"state\"`\n\tPath                      string                `json:\"path\"`\n\tQuery                     string                `json:\"query\"`\n\tUser                      string                `json:\"user\"`\n\tInboundBytes              uint64                `json:\"inboundBytes\"`\n\tInboundRTPPackets         uint64                `json:\"inboundRTPPackets\"`\n\tInboundRTPPacketsLost     uint64                `json:\"inboundRTPPacketsLost\"`\n\tInboundRTPPacketsJitter   float64               `json:\"inboundRTPPacketsJitter\"`\n\tInboundRTCPPackets        uint64                `json:\"inboundRTCPPackets\"`\n\tOutboundBytes             uint64                `json:\"outboundBytes\"`\n\tOutboundRTPPackets        uint64                `json:\"outboundRTPPackets\"`\n\tOutboundRTCPPackets       uint64                `json:\"outboundRTCPPackets\"`\n\tOutboundFramesDiscarded   uint64                `json:\"outboundFramesDiscarded\"`\n\t// deprecated\n\tBytesReceived       uint64  `json:\"bytesReceived\" deprecated:\"true\"`\n\tBytesSent           uint64  `json:\"bytesSent\" deprecated:\"true\"`\n\tRTPPacketsReceived  uint64  `json:\"rtpPacketsReceived\" deprecated:\"true\"`\n\tRTPPacketsSent      uint64  `json:\"rtpPacketsSent\" deprecated:\"true\"`\n\tRTPPacketsLost      uint64  `json:\"rtpPacketsLost\" deprecated:\"true\"`\n\tRTPPacketsJitter    float64 `json:\"rtpPacketsJitter\" deprecated:\"true\"`\n\tRTCPPacketsReceived uint64  `json:\"rtcpPacketsReceived\" deprecated:\"true\"`\n\tRTCPPacketsSent     uint64  `json:\"rtcpPacketsSent\" deprecated:\"true\"`\n}\n\n// APIWebRTCSessionList is a list of WebRTC sessions.\ntype APIWebRTCSessionList struct {\n\tItemCount int                `json:\"itemCount\"`\n\tPageCount int                `json:\"pageCount\"`\n\tItems     []APIWebRTCSession `json:\"items\"`\n}\n"
  },
  {
    "path": "internal/defs/defs.go",
    "content": "// Package defs contains shared definitions.\npackage defs\n"
  },
  {
    "path": "internal/defs/path.go",
    "content": "package defs\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/externalcmd\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n)\n\n// PathNoStreamAvailableError is returned when no one is publishing.\ntype PathNoStreamAvailableError struct {\n\tPathName string\n}\n\n// Error implements the error interface.\nfunc (e PathNoStreamAvailableError) Error() string {\n\treturn fmt.Sprintf(\"no stream is available on path '%s'\", e.PathName)\n}\n\n// Path is a path.\ntype Path interface {\n\tName() string\n\tSafeConf() *conf.Path\n\tExternalCmdEnv() externalcmd.Environment\n\tRemovePublisher(req PathRemovePublisherReq)\n\tRemoveReader(req PathRemoveReaderReq)\n}\n\n// PathFindPathConfRes contains the response of FindPathConf().\ntype PathFindPathConfRes struct {\n\tConf *conf.Path\n\tUser string\n\tErr  error\n}\n\n// PathFindPathConfReq contains arguments of FindPathConf().\ntype PathFindPathConfReq struct {\n\tAccessRequest PathAccessRequest\n\tRes           chan PathFindPathConfRes\n}\n\n// PathDescribeRes contains the response of Describe().\ntype PathDescribeRes struct {\n\tPath     Path\n\tStream   *stream.Stream\n\tRedirect string\n\tErr      error\n}\n\n// PathDescribeReq contains arguments of Describe().\ntype PathDescribeReq struct {\n\tAccessRequest PathAccessRequest\n\tRes           chan PathDescribeRes\n}\n\n// PathAddPublisherRes contains the response of AddPublisher().\ntype PathAddPublisherRes struct {\n\tPath      Path\n\tUser      string\n\tSubStream *stream.SubStream\n\tErr       error\n}\n\n// PathAddPublisherReq contains arguments of AddPublisher().\ntype PathAddPublisherReq struct {\n\tAuthor        Publisher\n\tDesc          *description.Session\n\tUseRTPPackets bool\n\tReplaceNTP    bool\n\tConfToCompare *conf.Path\n\tAccessRequest PathAccessRequest\n\tRes           chan PathAddPublisherRes\n}\n\n// PathRemovePublisherReq contains arguments of RemovePublisher().\ntype PathRemovePublisherReq struct {\n\tAuthor Publisher\n\tRes    chan struct{}\n}\n\n// PathAddReaderRes contains the response of AddReader().\ntype PathAddReaderRes struct {\n\tPath   Path\n\tUser   string\n\tStream *stream.Stream\n\tErr    error\n}\n\n// PathAddReaderReq contains arguments of AddReader().\ntype PathAddReaderReq struct {\n\tAuthor        Reader\n\tAccessRequest PathAccessRequest\n\tRes           chan PathAddReaderRes\n}\n\n// PathRemoveReaderReq contains arguments of RemoveReader().\ntype PathRemoveReaderReq struct {\n\tAuthor Reader\n\tRes    chan struct{}\n}\n\n// PathSourceStaticSetReadyRes contains the response of SetReady().\ntype PathSourceStaticSetReadyRes struct {\n\tSubStream *stream.SubStream\n\tErr       error\n}\n\n// PathSourceStaticSetReadyReq contains arguments of SetReady().\ntype PathSourceStaticSetReadyReq struct {\n\tDesc          *description.Session\n\tUseRTPPackets bool\n\tReplaceNTP    bool\n\tRes           chan PathSourceStaticSetReadyRes\n}\n\n// PathSourceStaticSetNotReadyReq contains arguments of SetNotReady().\ntype PathSourceStaticSetNotReadyReq struct {\n\tRes chan struct{}\n}\n"
  },
  {
    "path": "internal/defs/path_access_request.go",
    "content": "package defs\n\nimport (\n\t\"net\"\n\n\t\"github.com/bluenviron/mediamtx/internal/auth\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/google/uuid\"\n)\n\n// PathAccessRequest is a path access request.\ntype PathAccessRequest struct {\n\tName     string\n\tQuery    string\n\tPublish  bool\n\tSkipAuth bool\n\n\t// only if skipAuth = false\n\tProto            auth.Protocol\n\tID               *uuid.UUID\n\tCredentials      *auth.Credentials\n\tIP               net.IP\n\tCustomVerifyFunc func(expectedUser string, expectedPass string) bool\n}\n\n// ToAuthRequest converts a path access request into an authentication request.\nfunc (r *PathAccessRequest) ToAuthRequest() *auth.Request {\n\treturn &auth.Request{\n\t\tAction: func() conf.AuthAction {\n\t\t\tif r.Publish {\n\t\t\t\treturn conf.AuthActionPublish\n\t\t\t}\n\t\t\treturn conf.AuthActionRead\n\t\t}(),\n\t\tPath:             r.Name,\n\t\tQuery:            r.Query,\n\t\tProtocol:         r.Proto,\n\t\tID:               r.ID,\n\t\tCredentials:      r.Credentials,\n\t\tIP:               r.IP,\n\t\tCustomVerifyFunc: r.CustomVerifyFunc,\n\t}\n}\n"
  },
  {
    "path": "internal/defs/publisher.go",
    "content": "package defs\n\n// Publisher is an entity that can publish a stream.\ntype Publisher interface {\n\tSource\n\tClose()\n}\n"
  },
  {
    "path": "internal/defs/reader.go",
    "content": "package defs\n\n// Reader is an entity that can read a stream.\ntype Reader interface {\n\tClose()\n\tAPIReaderDescribe() *APIPathReader\n}\n"
  },
  {
    "path": "internal/defs/source.go",
    "content": "package defs\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n)\n\n// Source is an entity that can provide a stream.\n// it can be:\n// - Publisher\n// - staticsources.Handler\n// - core.sourceRedirect\ntype Source interface {\n\tlogger.Writer\n\tAPISourceDescribe() *APIPathSource\n}\n\n// FormatsInfo returns a description of formats.\nfunc FormatsInfo(formats []format.Format) string {\n\tcodecs := FormatsToCodecs(formats)\n\tcodecNames := make([]string, len(codecs))\n\tfor i, codec := range codecs {\n\t\tcodecNames[i] = string(codec)\n\t}\n\n\treturn fmt.Sprintf(\"%d %s (%s)\",\n\t\tlen(formats),\n\t\tfunc() string {\n\t\t\tif len(formats) == 1 {\n\t\t\t\treturn \"track\"\n\t\t\t}\n\t\t\treturn \"tracks\"\n\t\t}(),\n\t\tstrings.Join(codecNames, \", \"))\n}\n\n// MediasInfo returns a description of medias.\nfunc MediasInfo(medias []*description.Media) string {\n\treturn FormatsInfo(gatherFormats(medias))\n}\n"
  },
  {
    "path": "internal/defs/static_source.go",
    "content": "package defs\n\nimport (\n\t\"context\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n)\n\n// StaticSourceRunParams is the set of params passed to Run().\ntype StaticSourceRunParams struct {\n\tContext        context.Context\n\tResolvedSource string\n\tConf           *conf.Path\n\tReloadConf     chan *conf.Path\n}\n"
  },
  {
    "path": "internal/errordumper/dumper.go",
    "content": "// Package errordumper contains a counter that that periodically invokes a callback if the counter is not zero.\npackage errordumper\n\nimport (\n\t\"sync\"\n\t\"time\"\n)\n\nconst (\n\tcallbackPeriod = 1 * time.Second\n)\n\n// Dumper is a counter that periodically invokes a callback if errors were added.\ntype Dumper struct {\n\tOnReport func(v uint64, last error)\n\n\tmutex      sync.Mutex\n\tcounter    uint64\n\tabsCounter uint64\n\tlast       error\n\n\tterminate chan struct{}\n\tdone      chan struct{}\n}\n\n// Start starts the counter.\nfunc (c *Dumper) Start() {\n\tc.terminate = make(chan struct{})\n\tc.done = make(chan struct{})\n\n\tgo c.run()\n}\n\n// Stop stops the counter.\nfunc (c *Dumper) Stop() {\n\tclose(c.terminate)\n\t<-c.done\n}\n\n// Add adds an error to the counter.\nfunc (c *Dumper) Add(err error) {\n\tc.mutex.Lock()\n\tdefer c.mutex.Unlock()\n\tc.counter++\n\tc.absCounter++\n\tc.last = err\n}\n\n// Get returns the total number of errors.\nfunc (c *Dumper) Get() uint64 {\n\tc.mutex.Lock()\n\tdefer c.mutex.Unlock()\n\treturn c.absCounter\n}\n\nfunc (c *Dumper) run() {\n\tdefer close(c.done)\n\n\tt := time.NewTicker(callbackPeriod)\n\tdefer t.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-c.terminate:\n\t\t\treturn\n\n\t\tcase <-t.C:\n\t\t\tc.mutex.Lock()\n\t\t\tvar counter uint64\n\t\t\tcounter, c.counter = c.counter, 0\n\t\t\tlast := c.last\n\t\t\tc.mutex.Unlock()\n\n\t\t\tif counter != 0 {\n\t\t\t\tc.OnReport(counter, last)\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/errordumper/dumper_test.go",
    "content": "package errordumper\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDumperReport(t *testing.T) {\n\tdone := make(chan struct{})\n\n\tc := &Dumper{\n\t\tOnReport: func(v uint64, last error) {\n\t\t\trequire.Equal(t, uint64(1), v)\n\t\t\trequire.EqualError(t, last, \"test error\")\n\t\t\tclose(done)\n\t\t},\n\t}\n\tc.Start()\n\tdefer c.Stop()\n\n\tc.Add(fmt.Errorf(\"test error\"))\n\n\tselect {\n\tcase <-done:\n\tcase <-time.After(2 * time.Second):\n\t\tt.Errorf(\"should not happen\")\n\t}\n}\n\nfunc TestDumperDoNotReport(t *testing.T) {\n\tc := &Dumper{\n\t\tOnReport: func(_ uint64, _ error) {\n\t\t\tt.Errorf(\"should not happen\")\n\t\t},\n\t}\n\tc.Start()\n\tdefer c.Stop()\n\n\t<-time.After(2 * time.Second)\n}\n"
  },
  {
    "path": "internal/externalcmd/cmd.go",
    "content": "// Package externalcmd allows to launch external commands.\npackage externalcmd\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n)\n\nconst (\n\trestartPause = 5 * time.Second\n)\n\nvar errTerminated = errors.New(\"terminated\")\n\n// OnExitFunc is the prototype of onExit.\ntype OnExitFunc func(error)\n\n// Environment is a Cmd environment.\ntype Environment map[string]string\n\n// Cmd is an external command.\ntype Cmd struct {\n\tpool    *Pool\n\tcmdstr  string\n\trestart bool\n\tenv     Environment\n\tonExit  func(error)\n\n\t// in\n\tterminate chan struct{}\n}\n\n// NewCmd allocates a Cmd.\nfunc NewCmd(\n\tpool *Pool,\n\tcmdstr string,\n\trestart bool,\n\tenv Environment,\n\tonExit OnExitFunc,\n) *Cmd {\n\t// replace variables in both Linux and Windows, in order to allow using the\n\t// same commands on both of them.\n\tcmdstr = os.Expand(cmdstr, func(variable string) string {\n\t\tif value, ok := env[variable]; ok {\n\t\t\treturn value\n\t\t}\n\t\treturn os.Getenv(variable)\n\t})\n\n\tif onExit == nil {\n\t\tonExit = func(_ error) {}\n\t}\n\n\te := &Cmd{\n\t\tpool:      pool,\n\t\tcmdstr:    cmdstr,\n\t\trestart:   restart,\n\t\tenv:       env,\n\t\tonExit:    onExit,\n\t\tterminate: make(chan struct{}),\n\t}\n\n\tpool.wg.Add(1)\n\n\tgo e.run()\n\n\treturn e\n}\n\n// Close closes the command. It doesn't wait for the command to exit.\nfunc (e *Cmd) Close() {\n\tclose(e.terminate)\n}\n\nfunc (e *Cmd) run() {\n\tdefer e.pool.wg.Done()\n\n\tenv := append([]string(nil), os.Environ()...)\n\tfor key, val := range e.env {\n\t\tenv = append(env, key+\"=\"+val)\n\t}\n\n\tfor {\n\t\terr := e.runOSSpecific(env)\n\t\tif errors.Is(err, errTerminated) {\n\t\t\treturn\n\t\t}\n\n\t\tif !e.restart {\n\t\t\tif err != nil {\n\t\t\t\te.onExit(err)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\tif err != nil {\n\t\t\te.onExit(err)\n\t\t} else {\n\t\t\te.onExit(fmt.Errorf(\"command exited with code 0\"))\n\t\t}\n\n\t\tselect {\n\t\tcase <-time.After(restartPause):\n\t\tcase <-e.terminate:\n\t\t\treturn\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/externalcmd/cmd_unix.go",
    "content": "//go:build !windows\n\npackage externalcmd\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"syscall\"\n\n\t\"github.com/kballard/go-shellquote\"\n)\n\nfunc (e *Cmd) runOSSpecific(env []string) error {\n\tcmdParts, err := shellquote.Split(e.cmdstr)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcmd := exec.Command(cmdParts[0], cmdParts[1:]...)\n\n\tcmd.Env = env\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\t// set process group in order to allow killing subprocesses\n\tcmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}\n\n\terr = cmd.Start()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcmdDone := make(chan int)\n\tgo func() {\n\t\tcmdDone <- func() int {\n\t\t\terr2 := cmd.Wait()\n\t\t\tif err2 == nil {\n\t\t\t\treturn 0\n\t\t\t}\n\t\t\tvar ee *exec.ExitError\n\t\t\tif errors.As(err2, &ee) {\n\t\t\t\tee.ExitCode()\n\t\t\t}\n\t\t\treturn 0\n\t\t}()\n\t}()\n\n\tselect {\n\tcase <-e.terminate:\n\t\t// the minus is needed to kill all subprocesses\n\t\tsyscall.Kill(-cmd.Process.Pid, syscall.SIGINT) //nolint:errcheck\n\t\t<-cmdDone\n\t\treturn errTerminated\n\n\tcase c := <-cmdDone:\n\t\tif c != 0 {\n\t\t\treturn fmt.Errorf(\"command exited with code %d\", c)\n\t\t}\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "internal/externalcmd/cmd_win.go",
    "content": "//go:build windows\n\npackage externalcmd\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n\t\"syscall\"\n\t\"unsafe\"\n\n\t\"github.com/kballard/go-shellquote\"\n\t\"golang.org/x/sys/windows\"\n)\n\n// taken from\n// https://gist.github.com/hallazzang/76f3970bfc949831808bbebc8ca15209\nfunc createProcessGroup() (windows.Handle, error) {\n\th, err := windows.CreateJobObject(nil, nil)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tinfo := windows.JOBOBJECT_EXTENDED_LIMIT_INFORMATION{\n\t\tBasicLimitInformation: windows.JOBOBJECT_BASIC_LIMIT_INFORMATION{\n\t\t\tLimitFlags: windows.JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,\n\t\t},\n\t}\n\t_, err = windows.SetInformationJobObject(\n\t\th,\n\t\twindows.JobObjectExtendedLimitInformation,\n\t\tuintptr(unsafe.Pointer(&info)),\n\t\tuint32(unsafe.Sizeof(info)))\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn h, nil\n}\n\nfunc closeProcessGroup(h windows.Handle) error {\n\treturn windows.CloseHandle(h)\n}\n\nfunc addProcessToGroup(h windows.Handle, p *os.Process) error {\n\t// Combine the required access rights\n\taccess := uint32(windows.PROCESS_SET_QUOTA | windows.PROCESS_TERMINATE)\n\n\tprocessHandle, err := windows.OpenProcess(access, false, uint32(p.Pid))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to open process: %v\", err)\n\t}\n\tdefer windows.CloseHandle(processHandle)\n\n\terr = windows.AssignProcessToJobObject(h, processHandle)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to assign process to job object: %v\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (e *Cmd) runOSSpecific(env []string) error {\n\tvar cmd *exec.Cmd\n\n\t// from Golang documentation:\n\t// On Windows, processes receive the whole command line as a single string and do their own parsing.\n\t// Command combines and quotes Args into a command line string with an algorithm compatible with\n\t// applications using CommandLineToArgvW (which is the most common way). Notable exceptions are\n\t// msiexec.exe and cmd.exe (and thus, all batch files), which have a different unquoting algorithm.\n\t// In these or other similar cases, you can do the quoting yourself and provide the full command\n\t// line in SysProcAttr.CmdLine, leaving Args empty.\n\tif strings.HasPrefix(e.cmdstr, \"cmd \") || strings.HasPrefix(e.cmdstr, \"cmd.exe \") {\n\t\targs := strings.TrimPrefix(strings.TrimPrefix(e.cmdstr, \"cmd \"), \"cmd.exe \")\n\n\t\tcmd = exec.Command(\"cmd.exe\")\n\t\tcmd.SysProcAttr = &syscall.SysProcAttr{\n\t\t\tCmdLine: args,\n\t\t}\n\t} else {\n\t\tcmdParts, err := shellquote.Split(e.cmdstr)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tcmd = exec.Command(cmdParts[0], cmdParts[1:]...)\n\t}\n\n\tcmd.Env = env\n\tcmd.Stdout = os.Stdout\n\tcmd.Stderr = os.Stderr\n\n\t// create a process group to kill all subprocesses\n\tg, err := createProcessGroup()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = cmd.Start()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = addProcessToGroup(g, cmd.Process)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcmdDone := make(chan int)\n\tgo func() {\n\t\tcmdDone <- func() int {\n\t\t\terr := cmd.Wait()\n\t\t\tif err == nil {\n\t\t\t\treturn 0\n\t\t\t}\n\t\t\tee, ok := err.(*exec.ExitError)\n\t\t\tif !ok {\n\t\t\t\treturn 0\n\t\t\t}\n\t\t\treturn ee.ExitCode()\n\t\t}()\n\t}()\n\n\tselect {\n\tcase <-e.terminate:\n\t\tcloseProcessGroup(g)\n\t\t<-cmdDone\n\t\treturn errTerminated\n\n\tcase c := <-cmdDone:\n\t\tcloseProcessGroup(g)\n\t\tif c != 0 {\n\t\t\treturn fmt.Errorf(\"command exited with code %d\", c)\n\t\t}\n\t\treturn nil\n\t}\n}\n"
  },
  {
    "path": "internal/externalcmd/pool.go",
    "content": "package externalcmd\n\nimport (\n\t\"sync\"\n)\n\n// Pool is a pool of external commands.\ntype Pool struct {\n\twg sync.WaitGroup\n}\n\n// Initialize initializes a Pool.\nfunc (p *Pool) Initialize() {\n}\n\n// Close waits for all external commands to exit.\nfunc (p *Pool) Close() {\n\tp.wg.Wait()\n}\n"
  },
  {
    "path": "internal/hooks/hooks.go",
    "content": "// Package hooks contains hook implementations.\npackage hooks\n"
  },
  {
    "path": "internal/hooks/on_connect.go",
    "content": "package hooks\n\nimport (\n\t\"net\"\n\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/externalcmd\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n)\n\n// OnConnectParams are the parameters of OnConnect.\ntype OnConnectParams struct {\n\tLogger              logger.Writer\n\tExternalCmdPool     *externalcmd.Pool\n\tRunOnConnect        string\n\tRunOnConnectRestart bool\n\tRunOnDisconnect     string\n\tRTSPAddress         string\n\tDesc                defs.APIPathReader\n}\n\n// OnConnect is the OnConnect hook.\nfunc OnConnect(params OnConnectParams) func() {\n\tvar env externalcmd.Environment\n\tvar onConnectCmd *externalcmd.Cmd\n\n\tif params.RunOnConnect != \"\" || params.RunOnDisconnect != \"\" {\n\t\t_, port, _ := net.SplitHostPort(params.RTSPAddress)\n\t\tenv = externalcmd.Environment{\n\t\t\t\"RTSP_PORT\":     port,\n\t\t\t\"MTX_CONN_TYPE\": string(params.Desc.Type),\n\t\t\t\"MTX_CONN_ID\":   params.Desc.ID,\n\t\t}\n\t}\n\n\tif params.RunOnConnect != \"\" {\n\t\tparams.Logger.Log(logger.Info, \"runOnConnect command started\")\n\n\t\tonConnectCmd = externalcmd.NewCmd(\n\t\t\tparams.ExternalCmdPool,\n\t\t\tparams.RunOnConnect,\n\t\t\tparams.RunOnConnectRestart,\n\t\t\tenv,\n\t\t\tfunc(err error) {\n\t\t\t\tparams.Logger.Log(logger.Info, \"runOnConnect command exited: %v\", err)\n\t\t\t})\n\t}\n\n\treturn func() {\n\t\tif onConnectCmd != nil {\n\t\t\tonConnectCmd.Close()\n\t\t\tparams.Logger.Log(logger.Info, \"runOnConnect command stopped\")\n\t\t}\n\n\t\tif params.RunOnDisconnect != \"\" {\n\t\t\tparams.Logger.Log(logger.Info, \"runOnDisconnect command launched\")\n\t\t\texternalcmd.NewCmd(\n\t\t\t\tparams.ExternalCmdPool,\n\t\t\t\tparams.RunOnDisconnect,\n\t\t\t\tfalse,\n\t\t\t\tenv,\n\t\t\t\tnil)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/hooks/on_demand.go",
    "content": "package hooks\n\nimport (\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/externalcmd\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n)\n\n// OnDemandParams are the parameters of OnDemand.\ntype OnDemandParams struct {\n\tLogger          logger.Writer\n\tExternalCmdPool *externalcmd.Pool\n\tConf            *conf.Path\n\tExternalCmdEnv  externalcmd.Environment\n\tQuery           string\n}\n\n// OnDemand is the OnDemand hook.\nfunc OnDemand(params OnDemandParams) func(string) {\n\tvar env externalcmd.Environment\n\tvar onDemandCmd *externalcmd.Cmd\n\n\tif params.Conf.RunOnDemand != \"\" || params.Conf.RunOnUnDemand != \"\" {\n\t\tenv = params.ExternalCmdEnv\n\t\tenv[\"MTX_QUERY\"] = params.Query\n\t}\n\n\tif params.Conf.RunOnDemand != \"\" {\n\t\tparams.Logger.Log(logger.Info, \"runOnDemand command started\")\n\n\t\tonDemandCmd = externalcmd.NewCmd(\n\t\t\tparams.ExternalCmdPool,\n\t\t\tparams.Conf.RunOnDemand,\n\t\t\tparams.Conf.RunOnDemandRestart,\n\t\t\tenv,\n\t\t\tfunc(err error) {\n\t\t\t\tparams.Logger.Log(logger.Info, \"runOnDemand command exited: %v\", err)\n\t\t\t})\n\t}\n\n\treturn func(reason string) {\n\t\tif onDemandCmd != nil {\n\t\t\tonDemandCmd.Close()\n\t\t\tparams.Logger.Log(logger.Info, \"runOnDemand command stopped: %v\", reason)\n\t\t}\n\n\t\tif params.Conf.RunOnUnDemand != \"\" {\n\t\t\tparams.Logger.Log(logger.Info, \"runOnUnDemand command launched\")\n\t\t\texternalcmd.NewCmd(\n\t\t\t\tparams.ExternalCmdPool,\n\t\t\t\tparams.Conf.RunOnUnDemand,\n\t\t\t\tfalse,\n\t\t\t\tenv,\n\t\t\t\tnil)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/hooks/on_init.go",
    "content": "package hooks\n\nimport (\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/externalcmd\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n)\n\n// OnInitParams are the parameters of OnInit.\ntype OnInitParams struct {\n\tLogger          logger.Writer\n\tExternalCmdPool *externalcmd.Pool\n\tConf            *conf.Path\n\tExternalCmdEnv  externalcmd.Environment\n}\n\n// OnInit is the OnInit hook.\nfunc OnInit(params OnInitParams) func() {\n\tvar onInitCmd *externalcmd.Cmd\n\n\tif params.Conf.RunOnInit != \"\" {\n\t\tparams.Logger.Log(logger.Info, \"runOnInit command started\")\n\t\tonInitCmd = externalcmd.NewCmd(\n\t\t\tparams.ExternalCmdPool,\n\t\t\tparams.Conf.RunOnInit,\n\t\t\tparams.Conf.RunOnInitRestart,\n\t\t\tparams.ExternalCmdEnv,\n\t\t\tfunc(err error) {\n\t\t\t\tparams.Logger.Log(logger.Info, \"runOnInit command exited: %v\", err)\n\t\t\t})\n\t}\n\n\treturn func() {\n\t\tif onInitCmd != nil {\n\t\t\tonInitCmd.Close()\n\t\t\tparams.Logger.Log(logger.Info, \"runOnInit command stopped\")\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/hooks/on_read.go",
    "content": "package hooks\n\nimport (\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/externalcmd\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n)\n\n// OnReadParams are the parameters of OnRead.\ntype OnReadParams struct {\n\tLogger          logger.Writer\n\tExternalCmdPool *externalcmd.Pool\n\tConf            *conf.Path\n\tExternalCmdEnv  externalcmd.Environment\n\tReader          defs.APIPathReader\n\tQuery           string\n}\n\n// OnRead is the OnRead hook.\nfunc OnRead(params OnReadParams) func() {\n\tvar env externalcmd.Environment\n\tvar onReadCmd *externalcmd.Cmd\n\n\tif params.Conf.RunOnRead != \"\" || params.Conf.RunOnUnread != \"\" {\n\t\tenv = params.ExternalCmdEnv\n\t\tdesc := params.Reader\n\t\tenv[\"MTX_QUERY\"] = params.Query\n\t\tenv[\"MTX_READER_TYPE\"] = string(desc.Type)\n\t\tenv[\"MTX_READER_ID\"] = desc.ID\n\t}\n\n\tif params.Conf.RunOnRead != \"\" {\n\t\tparams.Logger.Log(logger.Info, \"runOnRead command started\")\n\t\tonReadCmd = externalcmd.NewCmd(\n\t\t\tparams.ExternalCmdPool,\n\t\t\tparams.Conf.RunOnRead,\n\t\t\tparams.Conf.RunOnReadRestart,\n\t\t\tenv,\n\t\t\tfunc(err error) {\n\t\t\t\tparams.Logger.Log(logger.Info, \"runOnRead command exited: %v\", err)\n\t\t\t})\n\t}\n\n\treturn func() {\n\t\tif onReadCmd != nil {\n\t\t\tonReadCmd.Close()\n\t\t\tparams.Logger.Log(logger.Info, \"runOnRead command stopped\")\n\t\t}\n\n\t\tif params.Conf.RunOnUnread != \"\" {\n\t\t\tparams.Logger.Log(logger.Info, \"runOnUnread command launched\")\n\t\t\texternalcmd.NewCmd(\n\t\t\t\tparams.ExternalCmdPool,\n\t\t\t\tparams.Conf.RunOnUnread,\n\t\t\t\tfalse,\n\t\t\t\tenv,\n\t\t\t\tnil)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/hooks/on_ready.go",
    "content": "package hooks\n\nimport (\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/externalcmd\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n)\n\n// OnReadyParams are the parameters of OnReady.\ntype OnReadyParams struct {\n\tLogger          logger.Writer\n\tExternalCmdPool *externalcmd.Pool\n\tConf            *conf.Path\n\tExternalCmdEnv  externalcmd.Environment\n\tDesc            *defs.APIPathSource\n\tQuery           string\n}\n\n// OnReady is the OnReady hook.\nfunc OnReady(params OnReadyParams) func() {\n\tvar env externalcmd.Environment\n\tvar onReadyCmd *externalcmd.Cmd\n\n\tif params.Conf.RunOnReady != \"\" || params.Conf.RunOnNotReady != \"\" {\n\t\tenv = params.ExternalCmdEnv\n\t\tenv[\"MTX_QUERY\"] = params.Query\n\t\tif params.Desc != nil {\n\t\t\tenv[\"MTX_SOURCE_TYPE\"] = string(params.Desc.Type)\n\t\t\tenv[\"MTX_SOURCE_ID\"] = params.Desc.ID\n\t\t}\n\t}\n\n\tif params.Conf.RunOnReady != \"\" {\n\t\tparams.Logger.Log(logger.Info, \"runOnReady command started\")\n\t\tonReadyCmd = externalcmd.NewCmd(\n\t\t\tparams.ExternalCmdPool,\n\t\t\tparams.Conf.RunOnReady,\n\t\t\tparams.Conf.RunOnReadyRestart,\n\t\t\tenv,\n\t\t\tfunc(err error) {\n\t\t\t\tparams.Logger.Log(logger.Info, \"runOnReady command exited: %v\", err)\n\t\t\t})\n\t}\n\n\treturn func() {\n\t\tif onReadyCmd != nil {\n\t\t\tonReadyCmd.Close()\n\t\t\tparams.Logger.Log(logger.Info, \"runOnReady command stopped\")\n\t\t}\n\n\t\tif params.Conf.RunOnNotReady != \"\" {\n\t\t\tparams.Logger.Log(logger.Info, \"runOnNotReady command launched\")\n\t\t\texternalcmd.NewCmd(\n\t\t\t\tparams.ExternalCmdPool,\n\t\t\t\tparams.Conf.RunOnNotReady,\n\t\t\t\tfalse,\n\t\t\t\tenv,\n\t\t\t\tnil)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/linters/conf/conf_test.go",
    "content": "//go:build enable_linters\n\npackage conf\n\nimport (\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/goccy/go-yaml/ast\"\n\t\"github.com/goccy/go-yaml/parser\"\n\t\"github.com/goccy/go-yaml/token\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc checkBooleans(t *testing.T, keys []string, node ast.Node) {\n\tswitch n := node.(type) {\n\tcase *ast.StringNode:\n\t\tif n.Token.Type == token.StringType {\n\t\t\tval := strings.ToLower(n.Token.Value)\n\t\t\tif val == \"yes\" || val == \"no\" || val == \"on\" || val == \"off\" || val == \"y\" || val == \"n\" {\n\t\t\t\tt.Errorf(\"deprecated bool value '%v: %v'\", strings.Join(keys, \".\"), val)\n\t\t\t}\n\t\t}\n\n\tcase *ast.MappingNode:\n\t\tfor _, value := range n.Values {\n\t\t\tcheckBooleans(t, append(keys, value.Key.(*ast.StringNode).Token.Value), value.Value)\n\t\t}\n\t}\n}\n\nfunc TestConf(t *testing.T) {\n\tbuf, err := os.ReadFile(\"../../../mediamtx.yml\")\n\trequire.NoError(t, err)\n\n\tfile, err := parser.ParseBytes(buf, 0)\n\trequire.NoError(t, err)\n\n\tcheckBooleans(t, nil, file.Docs[0].Body)\n}\n"
  },
  {
    "path": "internal/linters/go2api/go2api_test.go",
    "content": "//go:build enable_linters\n\npackage main\n\nimport (\n\t\"os\"\n\t\"reflect\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/goccy/go-yaml\"\n\t\"github.com/google/uuid\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype openAPIProperty struct {\n\tRef        string            `yaml:\"$ref\"`\n\tType       string            `yaml:\"type\"`\n\tFormat     string            `yaml:\"format\"`\n\tAllOf      []openAPIProperty `yaml:\"allOf\"`\n\tNullable   bool              `yaml:\"nullable\"`\n\tDeprecated bool              `yaml:\"deprecated\"`\n\tEnum       []string          `yaml:\"enum\"`\n\tItems      *openAPIProperty  `yaml:\"items\"`\n}\n\nfunc wrapRef(rt reflect.Type, p openAPIProperty) openAPIProperty {\n\tif p.Ref == \"\" {\n\t\treturn p\n\t}\n\n\tif _, ok := goEnumToApi(rt); ok {\n\t\tp.Type = \"string\"\n\t} else if rt.Kind() == reflect.Struct {\n\t\tp.Type = \"object\"\n\t}\n\n\tp.AllOf = []openAPIProperty{{Ref: p.Ref}}\n\tp.Ref = \"\"\n\treturn p\n}\n\ntype openAPISchema struct {\n\tType       string                     `yaml:\"type\"`\n\tEnum       []string                   `yaml:\"enum\"`\n\tProperties map[string]openAPIProperty `yaml:\"properties\"`\n}\n\ntype openAPI struct {\n\tComponents struct {\n\t\tSchemas map[string]openAPISchema `yaml:\"schemas\"`\n\t} `yaml:\"components\"`\n}\n\nfunc schemaName(rt reflect.Type) string {\n\tname := strings.TrimPrefix(rt.Name(), \"API\")\n\n\tif rt.PkgPath() == \"github.com/bluenviron/mediamtx/internal/conf\" && name == \"Path\" {\n\t\treturn \"PathConf\"\n\t}\n\n\treturn name\n}\n\nfunc goStructToApi(t *testing.T, rt reflect.Type) openAPIProperty {\n\tif rt.Kind() == reflect.Pointer {\n\t\tprop := goStructToApi(t, rt.Elem())\n\t\tprop = wrapRef(rt.Elem(), prop)\n\t\tprop.Nullable = true\n\t\treturn prop\n\t}\n\n\tif _, ok := goEnumToApi(rt); ok {\n\t\treturn openAPIProperty{Ref: \"#/components/schemas/\" + schemaName(rt)}\n\t}\n\n\tif rt == reflect.TypeOf((*defs.APIPathTrackCodecProps)(nil)).Elem() {\n\t\treturn openAPIProperty{\n\t\t\tType:     \"object\",\n\t\t\tAllOf:    []openAPIProperty{{Ref: \"#/components/schemas/\" + schemaName(rt)}},\n\t\t\tNullable: true,\n\t\t}\n\t}\n\n\tswitch {\n\tcase rt == reflect.TypeOf(\"\"):\n\t\treturn openAPIProperty{Type: \"string\"}\n\n\tcase rt == reflect.TypeOf(int(0)):\n\t\treturn openAPIProperty{Type: \"integer\", Format: \"int64\"}\n\n\tcase rt == reflect.TypeOf(uint(0)):\n\t\treturn openAPIProperty{Type: \"integer\", Format: \"uint64\"}\n\n\tcase rt == reflect.TypeOf(uint64(0)):\n\t\treturn openAPIProperty{Type: \"integer\", Format: \"uint64\"}\n\n\tcase rt == reflect.TypeOf(float64(0)):\n\t\treturn openAPIProperty{Type: \"number\", Format: \"double\"}\n\n\tcase rt == reflect.TypeOf(false):\n\t\treturn openAPIProperty{Type: \"boolean\"}\n\n\tcase rt == reflect.TypeOf(uuid.UUID{}):\n\t\treturn openAPIProperty{Type: \"string\", Format: \"uuid\"}\n\n\tcase rt == reflect.TypeOf(time.Time{}) ||\n\t\trt == reflect.TypeOf(conf.Duration(0)) ||\n\t\trt == reflect.TypeOf(conf.IPNetwork{}) ||\n\t\trt == reflect.TypeOf(conf.Credential(\"\")) ||\n\t\trt == reflect.TypeOf(conf.StringSize(0)):\n\t\treturn openAPIProperty{Type: \"string\"}\n\n\tcase rt == reflect.TypeOf(conf.RTSPTransports{}):\n\t\treturn openAPIProperty{\n\t\t\tType: \"array\",\n\t\t\tItems: &openAPIProperty{\n\t\t\t\tType: \"string\",\n\t\t\t\tEnum: []string{\"udp\", \"multicast\", \"tcp\"},\n\t\t\t},\n\t\t}\n\n\tcase rt.Kind() == reflect.Struct:\n\t\treturn openAPIProperty{\n\t\t\tRef: \"#/components/schemas/\" + schemaName(rt),\n\t\t}\n\n\tcase rt.Kind() == reflect.Slice:\n\t\titems := goStructToApi(t, rt.Elem())\n\t\treturn openAPIProperty{\n\t\t\tType:  \"array\",\n\t\t\tItems: &items,\n\t\t}\n\n\tdefault:\n\t\tt.Errorf(\"unhandled type: %v\", rt)\n\t\treturn openAPIProperty{}\n\t}\n}\n\nfunc goEnumToApi(rt reflect.Type) (openAPISchema, bool) {\n\tswitch rt {\n\tcase reflect.TypeOf(defs.APIOKStatus(\"\")):\n\t\treturn openAPISchema{Type: \"string\", Enum: []string{\"ok\"}}, true\n\n\tcase reflect.TypeOf(defs.APIErrorStatus(\"\")):\n\t\treturn openAPISchema{Type: \"string\", Enum: []string{\"error\"}}, true\n\n\tcase reflect.TypeOf(defs.APIPathSourceType(\"\")):\n\t\treturn openAPISchema{Type: \"string\", Enum: []string{\n\t\t\t\"hlsSource\",\n\t\t\t\"redirect\",\n\t\t\t\"rpiCameraSource\",\n\t\t\t\"rtmpConn\",\n\t\t\t\"rtmpsConn\",\n\t\t\t\"rtmpSource\",\n\t\t\t\"rtspSession\",\n\t\t\t\"rtspSource\",\n\t\t\t\"rtspsSession\",\n\t\t\t\"srtConn\",\n\t\t\t\"srtSource\",\n\t\t\t\"mpegtsSource\",\n\t\t\t\"rtpSource\",\n\t\t\t\"webRTCSession\",\n\t\t\t\"webRTCSource\",\n\t\t}}, true\n\n\tcase reflect.TypeOf(defs.APIPathReaderType(\"\")):\n\t\treturn openAPISchema{Type: \"string\", Enum: []string{\n\t\t\t\"hlsMuxer\",\n\t\t\t\"rpiCameraSecondary\",\n\t\t\t\"rtmpConn\",\n\t\t\t\"rtmpsConn\",\n\t\t\t\"rtspConn\",\n\t\t\t\"rtspSession\",\n\t\t\t\"rtspsConn\",\n\t\t\t\"rtspsSession\",\n\t\t\t\"srtConn\",\n\t\t\t\"webRTCSession\",\n\t\t}}, true\n\n\tcase reflect.TypeOf(defs.APIPathTrackCodec(\"\")):\n\t\treturn openAPISchema{Type: \"string\", Enum: []string{\n\t\t\t\"AV1\",\n\t\t\t\"VP9\",\n\t\t\t\"VP8\",\n\t\t\t\"H265\",\n\t\t\t\"H264\",\n\t\t\t\"MPEG-4 Video\",\n\t\t\t\"MPEG-1 Video\",\n\t\t\t\"MJPEG\",\n\t\t\t\"Opus\",\n\t\t\t\"Vorbis\",\n\t\t\t\"MPEG-4 Audio\",\n\t\t\t\"MPEG-4 Audio LATM\",\n\t\t\t\"MPEG-1 Audio\",\n\t\t\t\"AC3\",\n\t\t\t\"Speex\",\n\t\t\t\"G726\",\n\t\t\t\"G722\",\n\t\t\t\"G711\",\n\t\t\t\"LPCM\",\n\t\t\t\"MPEG-TS\",\n\t\t\t\"KLV\",\n\t\t\t\"Generic\",\n\t\t}}, true\n\n\tcase reflect.TypeOf(conf.AlwaysAvailableTrackCodec(\"\")):\n\t\treturn openAPISchema{Type: \"string\", Enum: []string{\n\t\t\t\"AV1\",\n\t\t\t\"VP9\",\n\t\t\t\"H265\",\n\t\t\t\"H264\",\n\t\t\t\"MPEG4Audio\",\n\t\t\t\"Opus\",\n\t\t\t\"G711\",\n\t\t\t\"LPCM\",\n\t\t}}, true\n\n\tcase reflect.TypeOf(conf.AuthAction(\"\")):\n\t\treturn openAPISchema{Type: \"string\", Enum: []string{\n\t\t\t\"publish\",\n\t\t\t\"read\",\n\t\t\t\"playback\",\n\t\t\t\"api\",\n\t\t\t\"metrics\",\n\t\t\t\"pprof\",\n\t\t}}, true\n\n\tcase reflect.TypeOf(conf.AuthMethod(\"\")):\n\t\treturn openAPISchema{Type: \"string\", Enum: []string{\n\t\t\t\"internal\",\n\t\t\t\"http\",\n\t\t\t\"jwt\",\n\t\t}}, true\n\n\tcase reflect.TypeOf(conf.Encryption(\"\")):\n\t\treturn openAPISchema{Type: \"string\", Enum: []string{\n\t\t\t\"no\",\n\t\t\t\"optional\",\n\t\t\t\"strict\",\n\t\t}}, true\n\n\tcase reflect.TypeOf(conf.HLSVariant(0)):\n\t\treturn openAPISchema{Type: \"string\", Enum: []string{\n\t\t\t\"mpegts\",\n\t\t\t\"fmp4\",\n\t\t\t\"lowLatency\",\n\t\t}}, true\n\n\tcase reflect.TypeOf(conf.LogDestination(0)):\n\t\treturn openAPISchema{Type: \"string\", Enum: []string{\n\t\t\t\"stdout\",\n\t\t\t\"file\",\n\t\t\t\"syslog\",\n\t\t}}, true\n\n\tcase reflect.TypeOf(conf.LogLevel(0)):\n\t\treturn openAPISchema{Type: \"string\", Enum: []string{\n\t\t\t\"error\",\n\t\t\t\"warn\",\n\t\t\t\"info\",\n\t\t\t\"debug\",\n\t\t}}, true\n\n\tcase reflect.TypeOf(conf.RecordFormat(\"\")):\n\t\treturn openAPISchema{Type: \"string\", Enum: []string{\n\t\t\t\"fmp4\",\n\t\t\t\"mpegts\",\n\t\t}}, true\n\n\tcase reflect.TypeOf(conf.RTSPAuthMethod(0)):\n\t\treturn openAPISchema{Type: \"string\", Enum: []string{\n\t\t\t\"basic\",\n\t\t\t\"digest\",\n\t\t}}, true\n\n\tcase reflect.TypeOf(conf.RTSPRangeType(\"\")):\n\t\treturn openAPISchema{Type: \"string\", Enum: []string{\n\t\t\t\"\",\n\t\t\t\"clock\",\n\t\t\t\"npt\",\n\t\t\t\"smpte\",\n\t\t}}, true\n\n\tcase reflect.TypeOf(conf.RTSPTransport{}):\n\t\treturn openAPISchema{Type: \"string\", Enum: []string{\n\t\t\t\"udp\",\n\t\t\t\"multicast\",\n\t\t\t\"tcp\",\n\t\t\t\"automatic\",\n\t\t}}, true\n\n\tcase reflect.TypeOf(defs.APIRTMPConnState(\"\")):\n\t\treturn openAPISchema{Type: \"string\", Enum: []string{\"idle\", \"read\", \"publish\"}}, true\n\n\tcase reflect.TypeOf(defs.APIWebRTCSessionState(\"\")):\n\t\treturn openAPISchema{Type: \"string\", Enum: []string{\"read\", \"publish\"}}, true\n\n\tcase reflect.TypeOf(defs.APISRTConnState(\"\")):\n\t\treturn openAPISchema{Type: \"string\", Enum: []string{\"idle\", \"read\", \"publish\"}}, true\n\n\tcase reflect.TypeOf(defs.APIRTSPSessionState(\"\")):\n\t\treturn openAPISchema{Type: \"string\", Enum: []string{\"idle\", \"read\", \"publish\"}}, true\n\t}\n\n\treturn openAPISchema{}, false\n}\n\nfunc TestGo2API(t *testing.T) {\n\tbyts, err := os.ReadFile(\"../../../api/openapi.yaml\")\n\trequire.NoError(t, err)\n\n\tvar doc openAPI\n\terr = yaml.Unmarshal(byts, &doc)\n\trequire.NoError(t, err)\n\n\tt.Run(\"structs\", func(t *testing.T) {\n\t\tfor _, ca := range []struct {\n\t\t\topenAPIKey string\n\t\t\tgoStruct   any\n\t\t}{\n\t\t\t{\n\t\t\t\t\"AlwaysAvailableTrack\",\n\t\t\t\tconf.AlwaysAvailableTrack{},\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"AuthInternalUser\",\n\t\t\t\tconf.AuthInternalUser{},\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"AuthInternalUserPermission\",\n\t\t\t\tconf.AuthInternalUserPermission{},\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"GlobalConf\",\n\t\t\t\tconf.Conf{},\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"HLSMuxer\",\n\t\t\t\tdefs.APIHLSMuxer{},\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"HLSMuxerList\",\n\t\t\t\tdefs.APIHLSMuxerList{},\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"Info\",\n\t\t\t\tdefs.APIInfo{},\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"Path\",\n\t\t\t\tdefs.APIPath{},\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"PathConf\",\n\t\t\t\tconf.Path{},\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"PathConfList\",\n\t\t\t\tdefs.APIPathConfList{},\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"PathList\",\n\t\t\t\tdefs.APIPathList{},\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"PathReader\",\n\t\t\t\tdefs.APIPathReader{},\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"PathSource\",\n\t\t\t\tdefs.APIPathSource{},\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"PathTrack\",\n\t\t\t\tdefs.APIPathTrack{},\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"Recording\",\n\t\t\t\tdefs.APIRecording{},\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"RecordingList\",\n\t\t\t\tdefs.APIRecordingList{},\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"RecordingSegment\",\n\t\t\t\tdefs.APIRecordingSegment{},\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"RTMPConn\",\n\t\t\t\tdefs.APIRTMPConn{},\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"RTMPConnList\",\n\t\t\t\tdefs.APIRTMPConnList{},\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"RTSPConn\",\n\t\t\t\tdefs.APIRTSPConn{},\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"RTSPConnList\",\n\t\t\t\tdefs.APIRTSPConnsList{},\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"RTSPSession\",\n\t\t\t\tdefs.APIRTSPSession{},\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"RTSPSessionList\",\n\t\t\t\tdefs.APIRTSPSessionList{},\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"SRTConn\",\n\t\t\t\tdefs.APISRTConn{},\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"SRTConnList\",\n\t\t\t\tdefs.APISRTConnList{},\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"WebRTCSession\",\n\t\t\t\tdefs.APIWebRTCSession{},\n\t\t\t},\n\t\t\t{\n\t\t\t\t\"WebRTCSessionList\",\n\t\t\t\tdefs.APIWebRTCSessionList{},\n\t\t\t},\n\t\t} {\n\t\t\tt.Run(ca.openAPIKey, func(t *testing.T) {\n\t\t\t\tcontent1 := doc.Components.Schemas[ca.openAPIKey]\n\n\t\t\t\tcontent2 := openAPISchema{\n\t\t\t\t\tType:       \"object\",\n\t\t\t\t\tProperties: make(map[string]openAPIProperty),\n\t\t\t\t}\n\n\t\t\t\tty := reflect.TypeOf(ca.goStruct)\n\n\t\t\t\tfor i := range ty.NumField() {\n\t\t\t\t\tsf := ty.Field(i)\n\t\t\t\t\tjs := sf.Tag.Get(\"json\")\n\t\t\t\t\tname, _, _ := strings.Cut(js, \",\")\n\t\t\t\t\tdeprecated := sf.Tag.Get(\"deprecated\") == \"true\"\n\n\t\t\t\t\tif name != \"\" && name != \"-\" && name != \"paths\" && name != \"pathDefaults\" &&\n\t\t\t\t\t\t(!strings.Contains(js, \",omitempty\") || deprecated) {\n\t\t\t\t\t\tprop := goStructToApi(t, sf.Type)\n\t\t\t\t\t\tprop.Deprecated = deprecated\n\t\t\t\t\t\tif deprecated {\n\t\t\t\t\t\t\tprop = wrapRef(sf.Type, prop)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tcontent2.Properties[name] = prop\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\trequire.Equal(t, content2, content1)\n\t\t\t})\n\t\t}\n\t})\n\n\tt.Run(\"enums\", func(t *testing.T) {\n\t\tfor _, rt := range []reflect.Type{\n\t\t\treflect.TypeOf(defs.APIOKStatus(\"\")),\n\t\t\treflect.TypeOf(defs.APIErrorStatus(\"\")),\n\t\t\treflect.TypeOf(defs.APIPathSourceType(\"\")),\n\t\t\treflect.TypeOf(defs.APIPathReaderType(\"\")),\n\t\t\treflect.TypeOf(defs.APIPathTrackCodec(\"\")),\n\t\t\treflect.TypeOf(conf.AlwaysAvailableTrackCodec(\"\")),\n\t\t\treflect.TypeOf(conf.AuthAction(\"\")),\n\t\t\treflect.TypeOf(conf.AuthMethod(\"\")),\n\t\t\treflect.TypeOf(conf.Encryption(\"\")),\n\t\t\treflect.TypeOf(conf.HLSVariant(0)),\n\t\t\treflect.TypeOf(conf.LogDestination(0)),\n\t\t\treflect.TypeOf(conf.LogLevel(0)),\n\t\t\treflect.TypeOf(conf.RecordFormat(\"\")),\n\t\t\treflect.TypeOf(conf.RTSPAuthMethod(0)),\n\t\t\treflect.TypeOf(conf.RTSPRangeType(\"\")),\n\t\t\treflect.TypeOf(conf.RTSPTransport{}),\n\t\t\treflect.TypeOf(defs.APIRTMPConnState(\"\")),\n\t\t\treflect.TypeOf(defs.APIRTSPSessionState(\"\")),\n\t\t\treflect.TypeOf(defs.APISRTConnState(\"\")),\n\t\t\treflect.TypeOf(defs.APIWebRTCSessionState(\"\")),\n\t\t} {\n\t\t\tt.Run(rt.Name(), func(t *testing.T) {\n\t\t\t\tcontent1 := doc.Components.Schemas[schemaName(rt)]\n\t\t\t\tcontent2, ok := goEnumToApi(rt)\n\t\t\t\trequire.True(t, ok)\n\t\t\t\trequire.Equal(t, content2, content1)\n\t\t\t})\n\t\t}\n\t})\n}\n"
  },
  {
    "path": "internal/logger/destination.go",
    "content": "package logger\n\nimport (\n\t\"time\"\n)\n\n// Destination is a log destination.\ntype Destination int\n\nconst (\n\t// DestinationStdout writes logs to the standard output.\n\tDestinationStdout Destination = iota\n\n\t// DestinationFile writes logs to a file.\n\tDestinationFile\n\n\t// DestinationSyslog writes logs to the system logger.\n\tDestinationSyslog\n)\n\ntype destination interface {\n\tlog(time.Time, Level, string, ...any)\n\tclose()\n}\n"
  },
  {
    "path": "internal/logger/destination_file.go",
    "content": "package logger\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"os\"\n\t\"strconv\"\n\t\"time\"\n)\n\ntype destinationFile struct {\n\tstructured bool\n\tfile       *os.File\n\tbuf        bytes.Buffer\n}\n\nfunc newDestinationFile(structured bool, filePath string) (destination, error) {\n\tf, err := os.OpenFile(filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &destinationFile{\n\t\tstructured: structured,\n\t\tfile:       f,\n\t}, nil\n}\n\nfunc (d *destinationFile) log(t time.Time, level Level, format string, args ...any) {\n\td.buf.Reset()\n\n\tif d.structured {\n\t\td.buf.WriteString(`{\"timestamp\":\"`)\n\t\td.buf.WriteString(t.Format(time.RFC3339Nano))\n\t\td.buf.WriteString(`\",\"level\":\"`)\n\t\twriteLevel(&d.buf, level, false)\n\t\td.buf.WriteString(`\",\"message\":`)\n\t\td.buf.WriteString(strconv.Quote(fmt.Sprintf(format, args...)))\n\t\td.buf.WriteString(`}`)\n\t\td.buf.WriteByte('\\n')\n\t} else {\n\t\twritePlainTime(&d.buf, t, false)\n\t\twriteLevel(&d.buf, level, false)\n\t\td.buf.WriteByte(' ')\n\t\tfmt.Fprintf(&d.buf, format, args...)\n\t\td.buf.WriteByte('\\n')\n\t}\n\n\td.file.Write(d.buf.Bytes()) //nolint:errcheck\n}\n\nfunc (d *destinationFile) close() {\n\td.file.Close()\n}\n"
  },
  {
    "path": "internal/logger/destination_stdout.go",
    "content": "package logger\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"golang.org/x/term\"\n)\n\ntype destinationStdout struct {\n\tstructured bool\n\tstdout     io.Writer\n\tuseColor   bool\n\tbuf        bytes.Buffer\n}\n\nfunc newDestionationStdout(structured bool, stdout io.Writer) destination {\n\treturn &destinationStdout{\n\t\tstructured: structured,\n\t\tstdout:     stdout,\n\t\tuseColor:   term.IsTerminal(int(os.Stdout.Fd())),\n\t}\n}\n\nfunc (d *destinationStdout) log(t time.Time, level Level, format string, args ...any) {\n\td.buf.Reset()\n\n\tif d.structured {\n\t\td.buf.WriteString(`{\"timestamp\":\"`)\n\t\td.buf.WriteString(t.Format(time.RFC3339Nano))\n\t\td.buf.WriteString(`\",\"level\":\"`)\n\t\twriteLevel(&d.buf, level, false)\n\t\td.buf.WriteString(`\",\"message\":`)\n\t\td.buf.WriteString(strconv.Quote(fmt.Sprintf(format, args...)))\n\t\td.buf.WriteString(`}`)\n\t\td.buf.WriteByte('\\n')\n\t} else {\n\t\twritePlainTime(&d.buf, t, d.useColor)\n\t\twriteLevel(&d.buf, level, d.useColor)\n\t\td.buf.WriteByte(' ')\n\t\tfmt.Fprintf(&d.buf, format, args...)\n\t\td.buf.WriteByte('\\n')\n\t}\n\n\td.stdout.Write(d.buf.Bytes()) //nolint:errcheck\n}\n\nfunc (d *destinationStdout) close() {\n}\n"
  },
  {
    "path": "internal/logger/destination_syslog.go",
    "content": "//go:build !darwin && !windows\n\npackage logger\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"log/syslog\"\n\t\"time\"\n)\n\ntype destinationSysLog struct {\n\tsyslog *syslog.Writer\n\tbuf    bytes.Buffer\n}\n\nfunc newDestinationSyslog(prefix string) (destination, error) {\n\tsyslog, err := syslog.New(syslog.LOG_DAEMON, prefix)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &destinationSysLog{\n\t\tsyslog: syslog,\n\t}, nil\n}\n\nfunc (d *destinationSysLog) log(_ time.Time, level Level, format string, args ...any) {\n\td.buf.Reset()\n\n\tfmt.Fprintf(&d.buf, format, args...)\n\n\tswitch level {\n\tcase Debug:\n\t\td.syslog.Debug(d.buf.String()) //nolint:errcheck\n\tcase Info:\n\t\td.syslog.Info(d.buf.String()) //nolint:errcheck\n\tcase Warn:\n\t\td.syslog.Warning(d.buf.String()) //nolint:errcheck\n\tcase Error:\n\t\td.syslog.Err(d.buf.String()) //nolint:errcheck\n\t}\n}\n\nfunc (d *destinationSysLog) close() {\n\td.syslog.Close() //nolint:errcheck\n}\n"
  },
  {
    "path": "internal/logger/destination_syslog_disabled.go",
    "content": "//go:build darwin || windows\n\npackage logger\n\nimport \"fmt\"\n\nfunc newDestinationSyslog(_ string) (destination, error) {\n\treturn nil, fmt.Errorf(\"syslog is not available on macOS and Windows\")\n}\n"
  },
  {
    "path": "internal/logger/level.go",
    "content": "package logger\n\n// Level is a log level.\ntype Level int\n\n// Log levels.\nconst (\n\tDebug Level = iota + 1\n\tInfo\n\tWarn\n\tError\n)\n"
  },
  {
    "path": "internal/logger/logger.go",
    "content": "// Package logger contains a logger implementation.\npackage logger\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"os\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gookit/color\"\n)\n\n// Logger is a log handler.\ntype Logger struct {\n\tLevel        Level\n\tDestinations []Destination\n\tStructured   bool\n\tFile         string\n\tSysLogPrefix string\n\n\ttimeNow      func() time.Time\n\tstdout       io.Writer\n\tdestinations []destination\n\tmutex        sync.Mutex\n}\n\n// Initialize initializes Logger.\nfunc (l *Logger) Initialize() error {\n\tif l.timeNow == nil {\n\t\tl.timeNow = time.Now\n\t}\n\tif l.stdout == nil {\n\t\tl.stdout = os.Stdout\n\t}\n\n\tfor _, destType := range l.Destinations {\n\t\tswitch destType {\n\t\tcase DestinationStdout:\n\t\t\tl.destinations = append(l.destinations, newDestionationStdout(l.Structured, l.stdout))\n\n\t\tcase DestinationFile:\n\t\t\tdest, err := newDestinationFile(l.Structured, l.File)\n\t\t\tif err != nil {\n\t\t\t\tl.Close()\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tl.destinations = append(l.destinations, dest)\n\n\t\tcase DestinationSyslog:\n\t\t\tdest, err := newDestinationSyslog(l.SysLogPrefix)\n\t\t\tif err != nil {\n\t\t\t\tl.Close()\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tl.destinations = append(l.destinations, dest)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Close closes a log handler.\nfunc (l *Logger) Close() {\n\tfor _, dest := range l.destinations {\n\t\tdest.close()\n\t}\n}\n\n// https://golang.org/src/log/log.go#L78\nfunc itoa(i int, wid int) []byte {\n\t// Assemble decimal in reverse order.\n\tvar b [20]byte\n\tbp := len(b) - 1\n\tfor i >= 10 || wid > 1 {\n\t\twid--\n\t\tq := i / 10\n\t\tb[bp] = byte('0' + i - q*10)\n\t\tbp--\n\t\ti = q\n\t}\n\t// i < 10\n\tb[bp] = byte('0' + i)\n\treturn b[bp:]\n}\n\nfunc writePlainTime(buf *bytes.Buffer, t time.Time, useColor bool) {\n\tvar intbuf bytes.Buffer\n\n\t// date\n\tyear, month, day := t.Date()\n\tintbuf.Write(itoa(year, 4))\n\tintbuf.WriteByte('/')\n\tintbuf.Write(itoa(int(month), 2))\n\tintbuf.WriteByte('/')\n\tintbuf.Write(itoa(day, 2))\n\tintbuf.WriteByte(' ')\n\n\t// time\n\thour, minute, sec := t.Clock()\n\tintbuf.Write(itoa(hour, 2))\n\tintbuf.WriteByte(':')\n\tintbuf.Write(itoa(minute, 2))\n\tintbuf.WriteByte(':')\n\tintbuf.Write(itoa(sec, 2))\n\tintbuf.WriteByte(' ')\n\n\tif useColor {\n\t\tbuf.WriteString(color.RenderString(color.Gray.Code(), intbuf.String()))\n\t} else {\n\t\tbuf.WriteString(intbuf.String())\n\t}\n}\n\nfunc writeLevel(buf *bytes.Buffer, level Level, useColor bool) {\n\tswitch level {\n\tcase Debug:\n\t\tif useColor {\n\t\t\tbuf.WriteString(color.RenderString(color.Debug.Code(), \"DEB\"))\n\t\t} else {\n\t\t\tbuf.WriteString(\"DEB\")\n\t\t}\n\n\tcase Info:\n\t\tif useColor {\n\t\t\tbuf.WriteString(color.RenderString(color.Green.Code(), \"INF\"))\n\t\t} else {\n\t\t\tbuf.WriteString(\"INF\")\n\t\t}\n\n\tcase Warn:\n\t\tif useColor {\n\t\t\tbuf.WriteString(color.RenderString(color.Warn.Code(), \"WAR\"))\n\t\t} else {\n\t\t\tbuf.WriteString(\"WAR\")\n\t\t}\n\n\tcase Error:\n\t\tif useColor {\n\t\t\tbuf.WriteString(color.RenderString(color.Error.Code(), \"ERR\"))\n\t\t} else {\n\t\t\tbuf.WriteString(\"ERR\")\n\t\t}\n\t}\n}\n\n// Log writes a log entry.\nfunc (l *Logger) Log(level Level, format string, args ...any) {\n\tif level < l.Level {\n\t\treturn\n\t}\n\n\tl.mutex.Lock()\n\tdefer l.mutex.Unlock()\n\n\tt := l.timeNow()\n\n\tfor _, dest := range l.destinations {\n\t\tdest.log(t, level, format, args...)\n\t}\n}\n"
  },
  {
    "path": "internal/logger/logger_test.go",
    "content": "package logger\n\nimport (\n\t\"bytes\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestLoggerToStdout(t *testing.T) {\n\tfor _, ca := range []string{\n\t\t\"plain\",\n\t\t\"structured\",\n\t} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tvar buf bytes.Buffer\n\n\t\t\tl := &Logger{\n\t\t\t\tDestinations: []Destination{DestinationStdout},\n\t\t\t\tStructured:   (ca == \"structured\"),\n\t\t\t\ttimeNow:      func() time.Time { return time.Date(2003, 11, 4, 23, 15, 8, 431232, time.UTC) },\n\t\t\t\tstdout:       &buf,\n\t\t\t}\n\t\t\terr := l.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer l.Close()\n\n\t\t\tl.Log(Info, \"test format %d\", 123)\n\n\t\t\tif ca == \"plain\" {\n\t\t\t\trequire.Equal(t, \"2003/11/04 23:15:08 INF test format 123\\n\", buf.String())\n\t\t\t} else {\n\t\t\t\trequire.Equal(t, `{\"timestamp\":\"2003-11-04T23:15:08.000431232Z\",`+\n\t\t\t\t\t`\"level\":\"INF\",\"message\":\"test format 123\"}`+\"\\n\", buf.String())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestLoggerToFile(t *testing.T) {\n\tfor _, ca := range []string{\n\t\t\"plain\",\n\t\t\"structured\",\n\t} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\ttempFile, err := os.CreateTemp(os.TempDir(), \"mtx-logger-\")\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer os.Remove(tempFile.Name())\n\t\t\tdefer tempFile.Close()\n\n\t\t\tl := &Logger{\n\t\t\t\tLevel:        Debug,\n\t\t\t\tDestinations: []Destination{DestinationFile},\n\t\t\t\tStructured:   ca == \"structured\",\n\t\t\t\tFile:         tempFile.Name(),\n\t\t\t\ttimeNow:      func() time.Time { return time.Date(2003, 11, 4, 23, 15, 8, 0, time.UTC) },\n\t\t\t}\n\t\t\terr = l.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer l.Close()\n\n\t\t\tl.Log(Info, \"test format %d\", 123)\n\n\t\t\tbuf, err := os.ReadFile(tempFile.Name())\n\t\t\trequire.NoError(t, err)\n\n\t\t\tif ca == \"plain\" {\n\t\t\t\trequire.Equal(t, \"2003/11/04 23:15:08 INF test format 123\\n\", string(buf))\n\t\t\t} else {\n\t\t\t\trequire.Equal(t, `{\"timestamp\":\"2003-11-04T23:15:08Z\",`+\n\t\t\t\t\t`\"level\":\"INF\",\"message\":\"test format 123\"}`+\"\\n\", string(buf))\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/logger/writer.go",
    "content": "package logger\n\n// Writer is an object that provides a log method.\ntype Writer interface {\n\tLog(Level, string, ...any)\n}\n"
  },
  {
    "path": "internal/metrics/metrics.go",
    "content": "// Package metrics contains the metrics provider.\npackage metrics //nolint:revive\n\nimport (\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"reflect\"\n\t\"sort\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/gin-gonic/gin\"\n\n\t\"github.com/bluenviron/mediamtx/internal/auth\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/httpp\"\n)\n\nfunc interfaceIsEmpty(i any) bool {\n\treturn reflect.ValueOf(i).Kind() != reflect.Pointer || reflect.ValueOf(i).IsNil()\n}\n\nfunc sortedKeys(paths map[string]string) []string {\n\tret := make([]string, len(paths))\n\ti := 0\n\tfor name := range paths {\n\t\tret[i] = name\n\t\ti++\n\t}\n\tsort.Strings(ret)\n\treturn ret\n}\n\nfunc tags(m map[string]string) string {\n\to := \"{\"\n\n\tfirst := true\n\tfor _, k := range sortedKeys(m) {\n\t\tif first {\n\t\t\tfirst = false\n\t\t} else {\n\t\t\to += \",\"\n\t\t}\n\t\to += k + \"=\\\"\" + m[k] + \"\\\"\"\n\t}\n\n\to += \"}\"\n\treturn o\n}\n\nfunc metric(key string, tags string, value int64) string {\n\treturn key + tags + \" \" + strconv.FormatInt(value, 10) + \"\\n\"\n}\n\nfunc metricFloat(key string, tags string, value float64) string {\n\treturn key + tags + \" \" + strconv.FormatFloat(value, 'f', -1, 64) + \"\\n\"\n}\n\ntype metricsAuthManager interface {\n\tAuthenticate(req *auth.Request) (string, *auth.Error)\n}\n\ntype metricsParent interface {\n\tlogger.Writer\n}\n\n// Metrics is a metrics provider.\ntype Metrics struct {\n\tAddress        string\n\tDumpPackets    bool\n\tEncryption     bool\n\tServerKey      string\n\tServerCert     string\n\tAllowOrigins   []string\n\tTrustedProxies conf.IPNetworks\n\tReadTimeout    conf.Duration\n\tWriteTimeout   conf.Duration\n\tAuthManager    metricsAuthManager\n\tParent         metricsParent\n\n\thttpServer   *httpp.Server\n\tmutex        sync.Mutex\n\tpathManager  defs.APIPathManager\n\thlsServer    defs.APIHLSServer\n\trtspServer   defs.APIRTSPServer\n\trtspsServer  defs.APIRTSPServer\n\trtmpServer   defs.APIRTMPServer\n\trtmpsServer  defs.APIRTMPServer\n\tsrtServer    defs.APISRTServer\n\twebRTCServer defs.APIWebRTCServer\n}\n\n// Initialize initializes metrics.\nfunc (m *Metrics) Initialize() error {\n\trouter := gin.New()\n\trouter.SetTrustedProxies(m.TrustedProxies.ToTrustedProxies()) //nolint:errcheck\n\n\trouter.Use(m.middlewarePreflightRequests)\n\trouter.Use(m.middlewareAuth)\n\n\trouter.GET(\"/metrics\", m.onMetrics)\n\n\tm.httpServer = &httpp.Server{\n\t\tAddress:           m.Address,\n\t\tAllowOrigins:      m.AllowOrigins,\n\t\tDumpPackets:       m.DumpPackets,\n\t\tDumpPacketsPrefix: \"metrics_server_conn\",\n\t\tReadTimeout:       time.Duration(m.ReadTimeout),\n\t\tWriteTimeout:      time.Duration(m.WriteTimeout),\n\t\tEncryption:        m.Encryption,\n\t\tServerCert:        m.ServerCert,\n\t\tServerKey:         m.ServerKey,\n\t\tHandler:           router,\n\t\tParent:            m,\n\t}\n\terr := m.httpServer.Initialize()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tm.Log(logger.Info, \"listener opened on \"+m.Address)\n\n\treturn nil\n}\n\n// Close closes Metrics.\nfunc (m *Metrics) Close() {\n\tm.Log(logger.Info, \"listener is closing\")\n\tm.httpServer.Close()\n}\n\n// Log implements logger.Writer.\nfunc (m *Metrics) Log(level logger.Level, format string, args ...any) {\n\tm.Parent.Log(level, \"[metrics] \"+format, args...)\n}\n\nfunc (m *Metrics) middlewarePreflightRequests(ctx *gin.Context) {\n\tif ctx.Request.Method == http.MethodOptions &&\n\t\tctx.Request.Header.Get(\"Access-Control-Request-Method\") != \"\" {\n\t\tctx.Header(\"Access-Control-Allow-Methods\", \"OPTIONS, GET\")\n\t\tctx.Header(\"Access-Control-Allow-Headers\", \"Authorization\")\n\t\tctx.AbortWithStatus(http.StatusNoContent)\n\t\treturn\n\t}\n}\n\nfunc (m *Metrics) middlewareAuth(ctx *gin.Context) {\n\treq := &auth.Request{\n\t\tAction:      conf.AuthActionMetrics,\n\t\tQuery:       ctx.Request.URL.RawQuery,\n\t\tCredentials: httpp.Credentials(ctx.Request),\n\t\tIP:          net.ParseIP(ctx.ClientIP()),\n\t}\n\n\t_, err := m.AuthManager.Authenticate(req)\n\tif err != nil {\n\t\tif err.AskCredentials {\n\t\t\tctx.Header(\"WWW-Authenticate\", `Basic realm=\"mediamtx\"`)\n\t\t\tctx.AbortWithStatusJSON(http.StatusUnauthorized, &defs.APIError{\n\t\t\t\tStatus: defs.APIErrorStatusError,\n\t\t\t\tError:  \"authentication error\",\n\t\t\t})\n\t\t\treturn\n\t\t}\n\n\t\tm.Log(logger.Info, \"connection %v failed to authenticate: %v\", httpp.RemoteAddr(ctx), err.Wrapped)\n\n\t\t// wait some seconds to delay brute force attacks\n\t\t<-time.After(auth.PauseAfterError)\n\n\t\tctx.AbortWithStatusJSON(http.StatusUnauthorized, &defs.APIError{\n\t\t\tStatus: defs.APIErrorStatusError,\n\t\t\tError:  \"authentication error\",\n\t\t})\n\t\treturn\n\t}\n}\n\nfunc (m *Metrics) onMetrics(ctx *gin.Context) {\n\ttyp := ctx.Query(\"type\")\n\tpathFilter := ctx.Query(\"path\")\n\thlsMuxerFilter := ctx.Query(\"hls_muxer\")\n\trtspConnFilter := ctx.Query(\"rtsp_conn\")\n\trtspSessionFilter := ctx.Query(\"rtsp_session\")\n\trtspsConnFilter := ctx.Query(\"rtsps_conn\")\n\trtspsSessionFilter := ctx.Query(\"rtsps_session\")\n\trtmpConnFilter := ctx.Query(\"rtmp_conn\")\n\trtmpsConnFilter := ctx.Query(\"rtmps_conn\")\n\tsrtConnFilter := ctx.Query(\"srt_conn\")\n\twebrtcSessionFilter := ctx.Query(\"webrtc_session\")\n\n\tanyFilterActive := pathFilter != \"\" ||\n\t\thlsMuxerFilter != \"\" ||\n\t\trtspConnFilter != \"\" ||\n\t\trtspSessionFilter != \"\" ||\n\t\trtspsConnFilter != \"\" ||\n\t\trtspsSessionFilter != \"\" ||\n\t\trtmpConnFilter != \"\" ||\n\t\trtmpsConnFilter != \"\" ||\n\t\tsrtConnFilter != \"\" ||\n\t\twebrtcSessionFilter != \"\"\n\n\tout := \"\"\n\n\tif (typ == \"\" || typ == \"paths\") && (!anyFilterActive || pathFilter != \"\") {\n\t\tdata, err := m.pathManager.APIPathsList()\n\t\tif err == nil && len(data.Items) != 0 {\n\t\t\tfor _, i := range data.Items {\n\t\t\t\tif pathFilter == \"\" || pathFilter == i.Name {\n\t\t\t\t\tvar state string\n\t\t\t\t\tif i.Ready {\n\t\t\t\t\t\tstate = \"ready\"\n\t\t\t\t\t} else {\n\t\t\t\t\t\tstate = \"notReady\"\n\t\t\t\t\t}\n\n\t\t\t\t\tta := tags(map[string]string{\n\t\t\t\t\t\t\"name\":  i.Name,\n\t\t\t\t\t\t\"state\": state,\n\t\t\t\t\t})\n\t\t\t\t\tout += metric(\"paths\", ta, 1)\n\t\t\t\t\tout += metric(\"paths_inbound_bytes\", ta, int64(i.InboundBytes))\n\t\t\t\t\tout += metric(\"paths_outbound_bytes\", ta, int64(i.OutboundBytes))\n\t\t\t\t\tout += metric(\"paths_inbound_frames_in_error\", ta, int64(i.InboundFramesInError))\n\t\t\t\t\t// deprecated\n\t\t\t\t\tout += metric(\"paths_bytes_received\", ta, int64(i.BytesReceived))\n\t\t\t\t\tout += metric(\"paths_bytes_sent\", ta, int64(i.BytesSent))\n\t\t\t\t\tout += metric(\"paths_readers\", ta, int64(len(i.Readers)))\n\t\t\t\t}\n\t\t\t}\n\t\t} else if pathFilter == \"\" {\n\t\t\tout += metric(\"paths\", \"\", 0)\n\t\t\tout += metric(\"paths_inbound_bytes\", \"\", 0)\n\t\t\tout += metric(\"paths_outbound_bytes\", \"\", 0)\n\t\t\tout += metric(\"paths_inbound_frames_in_error\", \"\", 0)\n\t\t\t// deprecated\n\t\t\tout += metric(\"paths_bytes_received\", \"\", 0)\n\t\t\tout += metric(\"paths_bytes_sent\", \"\", 0)\n\t\t\tout += metric(\"paths_readers\", \"\", 0)\n\t\t}\n\t}\n\n\tif !interfaceIsEmpty(m.hlsServer) &&\n\t\t(typ == \"\" || typ == \"hls_muxers\") &&\n\t\t(!anyFilterActive || hlsMuxerFilter != \"\") {\n\t\tvar data *defs.APIHLSMuxerList\n\t\tdata, err := m.hlsServer.APIMuxersList()\n\t\tif err == nil && len(data.Items) != 0 {\n\t\t\tfor _, i := range data.Items {\n\t\t\t\tif hlsMuxerFilter == \"\" || hlsMuxerFilter == i.Path {\n\t\t\t\t\tta := tags(map[string]string{\n\t\t\t\t\t\t\"name\": i.Path,\n\t\t\t\t\t})\n\t\t\t\t\tout += metric(\"hls_muxers\", ta, 1)\n\t\t\t\t\tout += metric(\"hls_muxers_outbound_bytes\", ta, int64(i.OutboundBytes))\n\t\t\t\t\tout += metric(\"hls_muxers_outbound_frames_discarded\", ta, int64(i.OutboundFramesDiscarded))\n\t\t\t\t\t// deprecated\n\t\t\t\t\tout += metric(\"hls_muxers_bytes_sent\", ta, int64(i.BytesSent))\n\t\t\t\t}\n\t\t\t}\n\t\t} else if hlsMuxerFilter == \"\" {\n\t\t\tout += metric(\"hls_muxers\", \"\", 0)\n\t\t\tout += metric(\"hls_muxers_outbound_bytes\", \"\", 0)\n\t\t\tout += metric(\"hls_muxers_outbound_frames_discarded\", \"\", 0)\n\t\t\t// deprecated\n\t\t\tout += metric(\"hls_muxers_bytes_sent\", \"\", 0)\n\t\t}\n\t}\n\n\tif !interfaceIsEmpty(m.rtspServer) { //nolint:dupl\n\t\tif (typ == \"\" || typ == \"rtsp_conns\") && (!anyFilterActive || rtspConnFilter != \"\") {\n\t\t\tvar data *defs.APIRTSPConnsList\n\t\t\tdata, err := m.rtspServer.APIConnsList()\n\t\t\tif err == nil && len(data.Items) != 0 {\n\t\t\t\tfor _, i := range data.Items {\n\t\t\t\t\tif rtspConnFilter == \"\" || rtspConnFilter == i.ID.String() {\n\t\t\t\t\t\tta := tags(map[string]string{\n\t\t\t\t\t\t\t\"id\": i.ID.String(),\n\t\t\t\t\t\t})\n\t\t\t\t\t\tout += metric(\"rtsp_conns\", ta, 1)\n\t\t\t\t\t\tout += metric(\"rtsp_conns_inbound_bytes\", ta, int64(i.InboundBytes))\n\t\t\t\t\t\tout += metric(\"rtsp_conns_outbound_bytes\", ta, int64(i.OutboundBytes))\n\t\t\t\t\t\t// deprecated\n\t\t\t\t\t\tout += metric(\"rtsp_conns_bytes_received\", ta, int64(i.BytesReceived))\n\t\t\t\t\t\tout += metric(\"rtsp_conns_bytes_sent\", ta, int64(i.BytesSent))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if rtspConnFilter == \"\" {\n\t\t\t\tout += metric(\"rtsp_conns\", \"\", 0)\n\t\t\t\tout += metric(\"rtsp_conns_inbound_bytes\", \"\", 0)\n\t\t\t\tout += metric(\"rtsp_conns_outbound_bytes\", \"\", 0)\n\t\t\t\t// deprecated\n\t\t\t\tout += metric(\"rtsp_conns_bytes_received\", \"\", 0)\n\t\t\t\tout += metric(\"rtsp_conns_bytes_sent\", \"\", 0)\n\t\t\t}\n\t\t}\n\n\t\tif (typ == \"\" || typ == \"rtsp_sessions\") && (!anyFilterActive || rtspSessionFilter != \"\") {\n\t\t\tvar data *defs.APIRTSPSessionList\n\t\t\tdata, err := m.rtspServer.APISessionsList()\n\t\t\tif err == nil && len(data.Items) != 0 {\n\t\t\t\tfor _, i := range data.Items {\n\t\t\t\t\tif rtspSessionFilter == \"\" || rtspSessionFilter == i.ID.String() {\n\t\t\t\t\t\tta := tags(map[string]string{\n\t\t\t\t\t\t\t\"id\":         i.ID.String(),\n\t\t\t\t\t\t\t\"state\":      string(i.State),\n\t\t\t\t\t\t\t\"path\":       i.Path,\n\t\t\t\t\t\t\t\"remoteAddr\": i.RemoteAddr,\n\t\t\t\t\t\t})\n\t\t\t\t\t\tout += metric(\"rtsp_sessions\", ta, 1)\n\t\t\t\t\t\tout += metric(\"rtsp_sessions_inbound_bytes\", ta, int64(i.InboundBytes))\n\t\t\t\t\t\tout += metric(\"rtsp_sessions_inbound_rtp_packets\", ta, int64(i.InboundRTPPackets))\n\t\t\t\t\t\tout += metric(\"rtsp_sessions_inbound_rtp_packets_lost\", ta, int64(i.InboundRTPPacketsLost))\n\t\t\t\t\t\tout += metric(\"rtsp_sessions_inbound_rtp_packets_in_error\", ta, int64(i.InboundRTPPacketsInError))\n\t\t\t\t\t\tout += metricFloat(\"rtsp_sessions_inbound_rtp_packets_jitter\", ta, i.InboundRTPPacketsJitter)\n\t\t\t\t\t\tout += metric(\"rtsp_sessions_inbound_rtcp_packets\", ta, int64(i.InboundRTCPPackets))\n\t\t\t\t\t\tout += metric(\"rtsp_sessions_inbound_rtcp_packets_in_error\", ta, int64(i.InboundRTCPPacketsInError))\n\t\t\t\t\t\tout += metric(\"rtsp_sessions_outbound_bytes\", ta, int64(i.OutboundBytes))\n\t\t\t\t\t\tout += metric(\"rtsp_sessions_outbound_rtp_packets\", ta, int64(i.OutboundRTPPackets))\n\t\t\t\t\t\tout += metric(\"rtsp_sessions_outbound_rtp_packets_reported_lost\", ta, int64(i.OutboundRTPPacketsReportedLost))\n\t\t\t\t\t\tout += metric(\"rtsp_sessions_outbound_rtp_packets_discarded\", ta, int64(i.OutboundRTPPacketsDiscarded))\n\t\t\t\t\t\tout += metric(\"rtsp_sessions_outbound_rtcp_packets\", ta, int64(i.OutboundRTCPPackets))\n\t\t\t\t\t\t// deprecated\n\t\t\t\t\t\tout += metric(\"rtsp_sessions_bytes_received\", ta, int64(i.BytesReceived))\n\t\t\t\t\t\tout += metric(\"rtsp_sessions_bytes_sent\", ta, int64(i.BytesSent))\n\t\t\t\t\t\tout += metric(\"rtsp_sessions_rtp_packets_received\", ta, int64(i.RTPPacketsReceived))\n\t\t\t\t\t\tout += metric(\"rtsp_sessions_rtp_packets_sent\", ta, int64(i.RTPPacketsSent))\n\t\t\t\t\t\tout += metric(\"rtsp_sessions_rtp_packets_lost\", ta, int64(i.RTPPacketsLost))\n\t\t\t\t\t\tout += metric(\"rtsp_sessions_rtp_packets_in_error\", ta, int64(i.RTPPacketsInError))\n\t\t\t\t\t\tout += metricFloat(\"rtsp_sessions_rtp_packets_jitter\", ta, i.RTPPacketsJitter)\n\t\t\t\t\t\tout += metric(\"rtsp_sessions_rtcp_packets_received\", ta, int64(i.RTCPPacketsReceived))\n\t\t\t\t\t\tout += metric(\"rtsp_sessions_rtcp_packets_sent\", ta, int64(i.RTCPPacketsSent))\n\t\t\t\t\t\tout += metric(\"rtsp_sessions_rtcp_packets_in_error\", ta, int64(i.RTCPPacketsInError))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if rtspSessionFilter == \"\" {\n\t\t\t\tout += metric(\"rtsp_sessions\", \"\", 0)\n\t\t\t\tout += metric(\"rtsp_sessions_inbound_bytes\", \"\", 0)\n\t\t\t\tout += metric(\"rtsp_sessions_inbound_rtp_packets\", \"\", 0)\n\t\t\t\tout += metric(\"rtsp_sessions_inbound_rtp_packets_lost\", \"\", 0)\n\t\t\t\tout += metric(\"rtsp_sessions_inbound_rtp_packets_in_error\", \"\", 0)\n\t\t\t\tout += metricFloat(\"rtsp_sessions_inbound_rtp_packets_jitter\", \"\", 0)\n\t\t\t\tout += metric(\"rtsp_sessions_inbound_rtcp_packets\", \"\", 0)\n\t\t\t\tout += metric(\"rtsp_sessions_inbound_rtcp_packets_in_error\", \"\", 0)\n\t\t\t\tout += metric(\"rtsp_sessions_outbound_bytes\", \"\", 0)\n\t\t\t\tout += metric(\"rtsp_sessions_outbound_rtp_packets\", \"\", 0)\n\t\t\t\tout += metric(\"rtsp_sessions_outbound_rtp_packets_reported_lost\", \"\", 0)\n\t\t\t\tout += metric(\"rtsp_sessions_outbound_rtp_packets_discarded\", \"\", 0)\n\t\t\t\tout += metric(\"rtsp_sessions_outbound_rtcp_packets\", \"\", 0)\n\t\t\t\t// deprecated\n\t\t\t\tout += metric(\"rtsp_sessions_bytes_received\", \"\", 0)\n\t\t\t\tout += metric(\"rtsp_sessions_bytes_sent\", \"\", 0)\n\t\t\t\tout += metric(\"rtsp_sessions_rtp_packets_received\", \"\", 0)\n\t\t\t\tout += metric(\"rtsp_sessions_rtp_packets_sent\", \"\", 0)\n\t\t\t\tout += metric(\"rtsp_sessions_rtp_packets_lost\", \"\", 0)\n\t\t\t\tout += metric(\"rtsp_sessions_rtp_packets_in_error\", \"\", 0)\n\t\t\t\tout += metricFloat(\"rtsp_sessions_rtp_packets_jitter\", \"\", 0)\n\t\t\t\tout += metric(\"rtsp_sessions_rtcp_packets_received\", \"\", 0)\n\t\t\t\tout += metric(\"rtsp_sessions_rtcp_packets_sent\", \"\", 0)\n\t\t\t\tout += metric(\"rtsp_sessions_rtcp_packets_in_error\", \"\", 0)\n\t\t\t}\n\t\t}\n\t}\n\n\tif !interfaceIsEmpty(m.rtspsServer) { //nolint:dupl\n\t\tif (typ == \"\" || typ == \"rtsps_conns\") && (!anyFilterActive || rtspsConnFilter != \"\") {\n\t\t\tvar data *defs.APIRTSPConnsList\n\t\t\tdata, err := m.rtspsServer.APIConnsList()\n\t\t\tif err == nil && len(data.Items) != 0 {\n\t\t\t\tfor _, i := range data.Items {\n\t\t\t\t\tif rtspsConnFilter == \"\" || rtspsConnFilter == i.ID.String() {\n\t\t\t\t\t\tta := tags(map[string]string{\n\t\t\t\t\t\t\t\"id\": i.ID.String(),\n\t\t\t\t\t\t})\n\t\t\t\t\t\tout += metric(\"rtsps_conns\", ta, 1)\n\t\t\t\t\t\tout += metric(\"rtsps_conns_inbound_bytes\", ta, int64(i.InboundBytes))\n\t\t\t\t\t\tout += metric(\"rtsps_conns_outbound_bytes\", ta, int64(i.OutboundBytes))\n\t\t\t\t\t\t// deprecated\n\t\t\t\t\t\tout += metric(\"rtsps_conns_bytes_received\", ta, int64(i.BytesReceived))\n\t\t\t\t\t\tout += metric(\"rtsps_conns_bytes_sent\", ta, int64(i.BytesSent))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if rtspsConnFilter == \"\" {\n\t\t\t\tout += metric(\"rtsps_conns\", \"\", 0)\n\t\t\t\tout += metric(\"rtsps_conns_inbound_bytes\", \"\", 0)\n\t\t\t\tout += metric(\"rtsps_conns_outbound_bytes\", \"\", 0)\n\t\t\t\t// deprecated\n\t\t\t\tout += metric(\"rtsps_conns_bytes_received\", \"\", 0)\n\t\t\t\tout += metric(\"rtsps_conns_bytes_sent\", \"\", 0)\n\t\t\t}\n\t\t}\n\n\t\tif (typ == \"\" || typ == \"rtsps_sessions\") && (!anyFilterActive || rtspsSessionFilter != \"\") {\n\t\t\tvar data *defs.APIRTSPSessionList\n\t\t\tdata, err := m.rtspsServer.APISessionsList()\n\t\t\tif err == nil && len(data.Items) != 0 {\n\t\t\t\tfor _, i := range data.Items {\n\t\t\t\t\tif rtspsSessionFilter == \"\" || rtspsSessionFilter == i.ID.String() {\n\t\t\t\t\t\tta := tags(map[string]string{\n\t\t\t\t\t\t\t\"id\":         i.ID.String(),\n\t\t\t\t\t\t\t\"state\":      string(i.State),\n\t\t\t\t\t\t\t\"path\":       i.Path,\n\t\t\t\t\t\t\t\"remoteAddr\": i.RemoteAddr,\n\t\t\t\t\t\t})\n\t\t\t\t\t\tout += metric(\"rtsps_sessions\", ta, 1)\n\t\t\t\t\t\tout += metric(\"rtsps_sessions_inbound_bytes\", ta, int64(i.InboundBytes))\n\t\t\t\t\t\tout += metric(\"rtsps_sessions_inbound_rtp_packets\", ta, int64(i.InboundRTPPackets))\n\t\t\t\t\t\tout += metric(\"rtsps_sessions_inbound_rtp_packets_lost\", ta, int64(i.InboundRTPPacketsLost))\n\t\t\t\t\t\tout += metric(\"rtsps_sessions_inbound_rtp_packets_in_error\", ta, int64(i.InboundRTPPacketsInError))\n\t\t\t\t\t\tout += metricFloat(\"rtsps_sessions_inbound_rtp_packets_jitter\", ta, i.InboundRTPPacketsJitter)\n\t\t\t\t\t\tout += metric(\"rtsps_sessions_inbound_rtcp_packets\", ta, int64(i.InboundRTCPPackets))\n\t\t\t\t\t\tout += metric(\"rtsps_sessions_inbound_rtcp_packets_in_error\", ta, int64(i.InboundRTCPPacketsInError))\n\t\t\t\t\t\tout += metric(\"rtsps_sessions_outbound_bytes\", ta, int64(i.OutboundBytes))\n\t\t\t\t\t\tout += metric(\"rtsps_sessions_outbound_rtp_packets\", ta, int64(i.OutboundRTPPackets))\n\t\t\t\t\t\tout += metric(\"rtsps_sessions_outbound_rtp_packets_reported_lost\", ta, int64(i.OutboundRTPPacketsReportedLost))\n\t\t\t\t\t\tout += metric(\"rtsps_sessions_outbound_rtp_packets_discarded\", ta, int64(i.OutboundRTPPacketsDiscarded))\n\t\t\t\t\t\tout += metric(\"rtsps_sessions_outbound_rtcp_packets\", ta, int64(i.OutboundRTCPPackets))\n\t\t\t\t\t\t// deprecated\n\t\t\t\t\t\tout += metric(\"rtsps_sessions_bytes_received\", ta, int64(i.BytesReceived))\n\t\t\t\t\t\tout += metric(\"rtsps_sessions_bytes_sent\", ta, int64(i.BytesSent))\n\t\t\t\t\t\tout += metric(\"rtsps_sessions_rtp_packets_received\", ta, int64(i.RTPPacketsReceived))\n\t\t\t\t\t\tout += metric(\"rtsps_sessions_rtp_packets_sent\", ta, int64(i.RTPPacketsSent))\n\t\t\t\t\t\tout += metric(\"rtsps_sessions_rtp_packets_lost\", ta, int64(i.RTPPacketsLost))\n\t\t\t\t\t\tout += metric(\"rtsps_sessions_rtp_packets_in_error\", ta, int64(i.RTPPacketsInError))\n\t\t\t\t\t\tout += metricFloat(\"rtsps_sessions_rtp_packets_jitter\", ta, i.RTPPacketsJitter)\n\t\t\t\t\t\tout += metric(\"rtsps_sessions_rtcp_packets_received\", ta, int64(i.RTCPPacketsReceived))\n\t\t\t\t\t\tout += metric(\"rtsps_sessions_rtcp_packets_sent\", ta, int64(i.RTCPPacketsSent))\n\t\t\t\t\t\tout += metric(\"rtsps_sessions_rtcp_packets_in_error\", ta, int64(i.RTCPPacketsInError))\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else if rtspsSessionFilter == \"\" {\n\t\t\t\tout += metric(\"rtsps_sessions\", \"\", 0)\n\t\t\t\tout += metric(\"rtsps_sessions_inbound_bytes\", \"\", 0)\n\t\t\t\tout += metric(\"rtsps_sessions_inbound_rtp_packets\", \"\", 0)\n\t\t\t\tout += metric(\"rtsps_sessions_inbound_rtp_packets_lost\", \"\", 0)\n\t\t\t\tout += metric(\"rtsps_sessions_inbound_rtp_packets_in_error\", \"\", 0)\n\t\t\t\tout += metricFloat(\"rtsps_sessions_inbound_rtp_packets_jitter\", \"\", 0)\n\t\t\t\tout += metric(\"rtsps_sessions_inbound_rtcp_packets\", \"\", 0)\n\t\t\t\tout += metric(\"rtsps_sessions_inbound_rtcp_packets_in_error\", \"\", 0)\n\t\t\t\tout += metric(\"rtsps_sessions_outbound_bytes\", \"\", 0)\n\t\t\t\tout += metric(\"rtsps_sessions_outbound_rtp_packets\", \"\", 0)\n\t\t\t\tout += metric(\"rtsps_sessions_outbound_rtp_packets_reported_lost\", \"\", 0)\n\t\t\t\tout += metric(\"rtsps_sessions_outbound_rtp_packets_discarded\", \"\", 0)\n\t\t\t\tout += metric(\"rtsps_sessions_outbound_rtcp_packets\", \"\", 0)\n\t\t\t\t// deprecated\n\t\t\t\tout += metric(\"rtsps_sessions_bytes_received\", \"\", 0)\n\t\t\t\tout += metric(\"rtsps_sessions_bytes_sent\", \"\", 0)\n\t\t\t\tout += metric(\"rtsps_sessions_rtp_packets_received\", \"\", 0)\n\t\t\t\tout += metric(\"rtsps_sessions_rtp_packets_sent\", \"\", 0)\n\t\t\t\tout += metric(\"rtsps_sessions_rtp_packets_lost\", \"\", 0)\n\t\t\t\tout += metric(\"rtsps_sessions_rtp_packets_in_error\", \"\", 0)\n\t\t\t\tout += metricFloat(\"rtsps_sessions_rtp_packets_jitter\", \"\", 0)\n\t\t\t\tout += metric(\"rtsps_sessions_rtcp_packets_received\", \"\", 0)\n\t\t\t\tout += metric(\"rtsps_sessions_rtcp_packets_sent\", \"\", 0)\n\t\t\t\tout += metric(\"rtsps_sessions_rtcp_packets_in_error\", \"\", 0)\n\t\t\t}\n\t\t}\n\t}\n\n\tif !interfaceIsEmpty(m.rtmpServer) && //nolint:dupl\n\t\t(typ == \"\" || typ == \"rtmp_conns\") &&\n\t\t(!anyFilterActive || rtmpConnFilter != \"\") {\n\t\tvar data *defs.APIRTMPConnList\n\t\tdata, err := m.rtmpServer.APIConnsList()\n\t\tif err == nil && len(data.Items) != 0 {\n\t\t\tfor _, i := range data.Items {\n\t\t\t\tif rtmpConnFilter == \"\" || rtmpConnFilter == i.ID.String() {\n\t\t\t\t\tta := tags(map[string]string{\n\t\t\t\t\t\t\"id\":         i.ID.String(),\n\t\t\t\t\t\t\"state\":      string(i.State),\n\t\t\t\t\t\t\"path\":       i.Path,\n\t\t\t\t\t\t\"remoteAddr\": i.RemoteAddr,\n\t\t\t\t\t})\n\t\t\t\t\tout += metric(\"rtmp_conns\", ta, 1)\n\t\t\t\t\tout += metric(\"rtmp_conns_inbound_bytes\", ta, int64(i.InboundBytes))\n\t\t\t\t\tout += metric(\"rtmp_conns_outbound_bytes\", ta, int64(i.OutboundBytes))\n\t\t\t\t\tout += metric(\"rtmp_conns_outbound_frames_discarded\", ta, int64(i.OutboundFramesDiscarded))\n\t\t\t\t\t// deprecated\n\t\t\t\t\tout += metric(\"rtmp_conns_bytes_received\", ta, int64(i.BytesReceived))\n\t\t\t\t\tout += metric(\"rtmp_conns_bytes_sent\", ta, int64(i.BytesSent))\n\t\t\t\t}\n\t\t\t}\n\t\t} else if rtmpConnFilter == \"\" {\n\t\t\tout += metric(\"rtmp_conns\", \"\", 0)\n\t\t\tout += metric(\"rtmp_conns_inbound_bytes\", \"\", 0)\n\t\t\tout += metric(\"rtmp_conns_outbound_bytes\", \"\", 0)\n\t\t\tout += metric(\"rtmp_conns_outbound_frames_discarded\", \"\", 0)\n\t\t\t// deprecated\n\t\t\tout += metric(\"rtmp_conns_bytes_received\", \"\", 0)\n\t\t\tout += metric(\"rtmp_conns_bytes_sent\", \"\", 0)\n\t\t}\n\t}\n\n\tif !interfaceIsEmpty(m.rtmpsServer) && //nolint:dupl\n\t\t(typ == \"\" || typ == \"rtmp_conns\") &&\n\t\t(!anyFilterActive || rtmpsConnFilter != \"\") {\n\t\tvar data *defs.APIRTMPConnList\n\t\tdata, err := m.rtmpsServer.APIConnsList()\n\t\tif err == nil && len(data.Items) != 0 {\n\t\t\tfor _, i := range data.Items {\n\t\t\t\tif rtmpsConnFilter == \"\" || rtmpsConnFilter == i.ID.String() {\n\t\t\t\t\tta := tags(map[string]string{\n\t\t\t\t\t\t\"id\":         i.ID.String(),\n\t\t\t\t\t\t\"state\":      string(i.State),\n\t\t\t\t\t\t\"path\":       i.Path,\n\t\t\t\t\t\t\"remoteAddr\": i.RemoteAddr,\n\t\t\t\t\t})\n\t\t\t\t\tout += metric(\"rtmps_conns\", ta, 1)\n\t\t\t\t\tout += metric(\"rtmps_conns_inbound_bytes\", ta, int64(i.InboundBytes))\n\t\t\t\t\tout += metric(\"rtmps_conns_outbound_bytes\", ta, int64(i.OutboundBytes))\n\t\t\t\t\tout += metric(\"rtmps_conns_outbound_frames_discarded\", ta, int64(i.OutboundFramesDiscarded))\n\t\t\t\t\t// deprecated\n\t\t\t\t\tout += metric(\"rtmps_conns_bytes_received\", ta, int64(i.BytesReceived))\n\t\t\t\t\tout += metric(\"rtmps_conns_bytes_sent\", ta, int64(i.BytesSent))\n\t\t\t\t}\n\t\t\t}\n\t\t} else if rtmpsConnFilter == \"\" {\n\t\t\tout += metric(\"rtmps_conns\", \"\", 0)\n\t\t\tout += metric(\"rtmps_conns_inbound_bytes\", \"\", 0)\n\t\t\tout += metric(\"rtmps_conns_outbound_bytes\", \"\", 0)\n\t\t\tout += metric(\"rtmps_conns_outbound_frames_discarded\", \"\", 0)\n\t\t\t// deprecated\n\t\t\tout += metric(\"rtmps_conns_bytes_received\", \"\", 0)\n\t\t\tout += metric(\"rtmps_conns_bytes_sent\", \"\", 0)\n\t\t}\n\t}\n\n\tif !interfaceIsEmpty(m.srtServer) &&\n\t\t(typ == \"\" || typ == \"srt_conns\") &&\n\t\t(!anyFilterActive || srtConnFilter != \"\") {\n\t\tvar data *defs.APISRTConnList\n\t\tdata, err := m.srtServer.APIConnsList()\n\t\tif err == nil && len(data.Items) != 0 {\n\t\t\tfor _, i := range data.Items {\n\t\t\t\tif srtConnFilter == \"\" || srtConnFilter == i.ID.String() {\n\t\t\t\t\tta := tags(map[string]string{\n\t\t\t\t\t\t\"id\":         i.ID.String(),\n\t\t\t\t\t\t\"state\":      string(i.State),\n\t\t\t\t\t\t\"path\":       i.Path,\n\t\t\t\t\t\t\"remoteAddr\": i.RemoteAddr,\n\t\t\t\t\t})\n\t\t\t\t\tout += metric(\"srt_conns\", ta, 1)\n\t\t\t\t\tout += metric(\"srt_conns_packets_sent\", ta, int64(i.PacketsSent))\n\t\t\t\t\tout += metric(\"srt_conns_packets_received\", ta, int64(i.PacketsReceived))\n\t\t\t\t\tout += metric(\"srt_conns_packets_sent_unique\", ta, int64(i.PacketsSentUnique))\n\t\t\t\t\tout += metric(\"srt_conns_packets_received_unique\", ta, int64(i.PacketsReceivedUnique))\n\t\t\t\t\tout += metric(\"srt_conns_packets_send_loss\", ta, int64(i.PacketsSendLoss))\n\t\t\t\t\tout += metric(\"srt_conns_packets_received_loss\", ta, int64(i.PacketsReceivedLoss))\n\t\t\t\t\tout += metric(\"srt_conns_packets_retrans\", ta, int64(i.PacketsRetrans))\n\t\t\t\t\tout += metric(\"srt_conns_packets_received_retrans\", ta, int64(i.PacketsReceivedRetrans))\n\t\t\t\t\tout += metric(\"srt_conns_packets_sent_ack\", ta, int64(i.PacketsSentACK))\n\t\t\t\t\tout += metric(\"srt_conns_packets_received_ack\", ta, int64(i.PacketsReceivedACK))\n\t\t\t\t\tout += metric(\"srt_conns_packets_sent_nak\", ta, int64(i.PacketsSentNAK))\n\t\t\t\t\tout += metric(\"srt_conns_packets_received_nak\", ta, int64(i.PacketsReceivedNAK))\n\t\t\t\t\tout += metric(\"srt_conns_packets_sent_km\", ta, int64(i.PacketsSentKM))\n\t\t\t\t\tout += metric(\"srt_conns_packets_received_km\", ta, int64(i.PacketsReceivedKM))\n\t\t\t\t\tout += metric(\"srt_conns_us_snd_duration\", ta, int64(i.UsSndDuration))\n\t\t\t\t\tout += metric(\"srt_conns_packets_received_belated\", ta, int64(i.PacketsReceivedBelated))\n\t\t\t\t\tout += metric(\"srt_conns_packets_send_drop\", ta, int64(i.PacketsSendDrop))\n\t\t\t\t\tout += metric(\"srt_conns_packets_received_drop\", ta, int64(i.PacketsReceivedDrop))\n\t\t\t\t\tout += metric(\"srt_conns_packets_received_undecrypt\", ta, int64(i.PacketsReceivedUndecrypt))\n\t\t\t\t\tout += metric(\"srt_conns_bytes_sent\", ta, int64(i.BytesSent))\n\t\t\t\t\tout += metric(\"srt_conns_bytes_received\", ta, int64(i.BytesReceived))\n\t\t\t\t\tout += metric(\"srt_conns_bytes_sent_unique\", ta, int64(i.BytesSentUnique))\n\t\t\t\t\tout += metric(\"srt_conns_bytes_received_unique\", ta, int64(i.BytesReceivedUnique))\n\t\t\t\t\tout += metric(\"srt_conns_bytes_received_loss\", ta, int64(i.BytesReceivedLoss))\n\t\t\t\t\tout += metric(\"srt_conns_bytes_retrans\", ta, int64(i.BytesRetrans))\n\t\t\t\t\tout += metric(\"srt_conns_bytes_received_retrans\", ta, int64(i.BytesReceivedRetrans))\n\t\t\t\t\tout += metric(\"srt_conns_bytes_received_belated\", ta, int64(i.BytesReceivedBelated))\n\t\t\t\t\tout += metric(\"srt_conns_bytes_send_drop\", ta, int64(i.BytesSendDrop))\n\t\t\t\t\tout += metric(\"srt_conns_bytes_received_drop\", ta, int64(i.BytesReceivedDrop))\n\t\t\t\t\tout += metric(\"srt_conns_bytes_received_undecrypt\", ta, int64(i.BytesReceivedUndecrypt))\n\t\t\t\t\tout += metricFloat(\"srt_conns_us_packets_send_period\", ta, i.UsPacketsSendPeriod)\n\t\t\t\t\tout += metric(\"srt_conns_packets_flow_window\", ta, int64(i.PacketsFlowWindow))\n\t\t\t\t\tout += metric(\"srt_conns_packets_flight_size\", ta, int64(i.PacketsFlightSize))\n\t\t\t\t\tout += metricFloat(\"srt_conns_ms_rtt\", ta, i.MsRTT)\n\t\t\t\t\tout += metricFloat(\"srt_conns_mbps_send_rate\", ta, i.MbpsSendRate)\n\t\t\t\t\tout += metricFloat(\"srt_conns_mbps_receive_rate\", ta, i.MbpsReceiveRate)\n\t\t\t\t\tout += metricFloat(\"srt_conns_mbps_link_capacity\", ta, i.MbpsLinkCapacity)\n\t\t\t\t\tout += metric(\"srt_conns_bytes_avail_send_buf\", ta, int64(i.BytesAvailSendBuf))\n\t\t\t\t\tout += metric(\"srt_conns_bytes_avail_receive_buf\", ta, int64(i.BytesAvailReceiveBuf))\n\t\t\t\t\tout += metricFloat(\"srt_conns_mbps_max_bw\", ta, i.MbpsMaxBW)\n\t\t\t\t\tout += metric(\"srt_conns_bytes_mss\", ta, int64(i.ByteMSS))\n\t\t\t\t\tout += metric(\"srt_conns_packets_send_buf\", ta, int64(i.PacketsSendBuf))\n\t\t\t\t\tout += metric(\"srt_conns_bytes_send_buf\", ta, int64(i.BytesSendBuf))\n\t\t\t\t\tout += metric(\"srt_conns_ms_send_buf\", ta, int64(i.MsSendBuf))\n\t\t\t\t\tout += metric(\"srt_conns_ms_send_tsb_pd_delay\", ta, int64(i.MsSendTsbPdDelay))\n\t\t\t\t\tout += metric(\"srt_conns_packets_receive_buf\", ta, int64(i.PacketsReceiveBuf))\n\t\t\t\t\tout += metric(\"srt_conns_bytes_receive_buf\", ta, int64(i.BytesReceiveBuf))\n\t\t\t\t\tout += metric(\"srt_conns_ms_receive_buf\", ta, int64(i.MsReceiveBuf))\n\t\t\t\t\tout += metric(\"srt_conns_ms_receive_tsb_pd_delay\", ta, int64(i.MsReceiveTsbPdDelay))\n\t\t\t\t\tout += metric(\"srt_conns_packets_reorder_tolerance\", ta, int64(i.PacketsReorderTolerance))\n\t\t\t\t\tout += metric(\"srt_conns_packets_received_avg_belated_time\", ta, int64(i.PacketsReceivedAvgBelatedTime))\n\t\t\t\t\tout += metricFloat(\"srt_conns_packets_send_loss_rate\", ta, i.PacketsSendLossRate)\n\t\t\t\t\tout += metricFloat(\"srt_conns_packets_received_loss_rate\", ta, i.PacketsReceivedLossRate)\n\t\t\t\t\tout += metric(\"srt_conns_outbound_frames_discarded\", ta, int64(i.OutboundFramesDiscarded))\n\t\t\t\t}\n\t\t\t}\n\t\t} else if srtConnFilter == \"\" {\n\t\t\tout += metric(\"srt_conns\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_packets_sent\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_packets_received\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_packets_sent_unique\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_packets_received_unique\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_packets_send_loss\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_packets_received_loss\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_packets_retrans\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_packets_received_retrans\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_packets_sent_ack\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_packets_received_ack\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_packets_sent_nak\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_packets_received_nak\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_packets_sent_km\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_packets_received_km\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_us_snd_duration\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_packets_received_belated\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_packets_send_drop\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_packets_received_drop\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_packets_received_undecrypt\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_bytes_sent\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_bytes_received\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_bytes_sent_unique\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_bytes_received_unique\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_bytes_received_loss\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_bytes_retrans\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_bytes_received_retrans\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_bytes_received_belated\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_bytes_send_drop\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_bytes_received_drop\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_bytes_received_undecrypt\", \"\", 0)\n\t\t\tout += metricFloat(\"srt_conns_us_packets_send_period\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_packets_flow_window\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_packets_flight_size\", \"\", 0)\n\t\t\tout += metricFloat(\"srt_conns_ms_rtt\", \"\", 0)\n\t\t\tout += metricFloat(\"srt_conns_mbps_send_rate\", \"\", 0)\n\t\t\tout += metricFloat(\"srt_conns_mbps_receive_rate\", \"\", 0)\n\t\t\tout += metricFloat(\"srt_conns_mbps_link_capacity\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_bytes_avail_send_buf\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_bytes_avail_receive_buf\", \"\", 0)\n\t\t\tout += metricFloat(\"srt_conns_mbps_max_bw\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_bytes_mss\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_packets_send_buf\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_bytes_send_buf\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_ms_send_buf\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_ms_send_tsb_pd_delay\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_packets_receive_buf\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_bytes_receive_buf\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_ms_receive_buf\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_ms_receive_tsb_pd_delay\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_packets_reorder_tolerance\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_packets_received_avg_belated_time\", \"\", 0)\n\t\t\tout += metricFloat(\"srt_conns_packets_send_loss_rate\", \"\", 0)\n\t\t\tout += metricFloat(\"srt_conns_packets_received_loss_rate\", \"\", 0)\n\t\t\tout += metric(\"srt_conns_outbound_frames_discarded\", \"\", 0)\n\t\t}\n\t}\n\n\tif !interfaceIsEmpty(m.webRTCServer) &&\n\t\t(typ == \"\" || typ == \"webrtc_sessions\") &&\n\t\t(!anyFilterActive || webrtcSessionFilter != \"\") {\n\t\tvar data *defs.APIWebRTCSessionList\n\t\tdata, err := m.webRTCServer.APISessionsList()\n\t\tif err == nil && len(data.Items) != 0 {\n\t\t\tfor _, i := range data.Items {\n\t\t\t\tif webrtcSessionFilter == \"\" || webrtcSessionFilter == i.ID.String() {\n\t\t\t\t\tta := tags(map[string]string{\n\t\t\t\t\t\t\"id\":         i.ID.String(),\n\t\t\t\t\t\t\"state\":      string(i.State),\n\t\t\t\t\t\t\"path\":       i.Path,\n\t\t\t\t\t\t\"remoteAddr\": i.RemoteAddr,\n\t\t\t\t\t})\n\t\t\t\t\tout += metric(\"webrtc_sessions\", ta, 1)\n\t\t\t\t\tout += metric(\"webrtc_sessions_inbound_bytes\", ta, int64(i.InboundBytes))\n\t\t\t\t\tout += metric(\"webrtc_sessions_inbound_rtp_packets\", ta, int64(i.InboundRTPPackets))\n\t\t\t\t\tout += metric(\"webrtc_sessions_inbound_rtp_packets_lost\", ta, int64(i.InboundRTPPacketsLost))\n\t\t\t\t\tout += metricFloat(\"webrtc_sessions_inbound_rtp_packets_jitter\", ta, i.InboundRTPPacketsJitter)\n\t\t\t\t\tout += metric(\"webrtc_sessions_inbound_rtcp_packets\", ta, int64(i.InboundRTCPPackets))\n\t\t\t\t\tout += metric(\"webrtc_sessions_outbound_bytes\", ta, int64(i.OutboundBytes))\n\t\t\t\t\tout += metric(\"webrtc_sessions_outbound_rtp_packets\", ta, int64(i.OutboundRTPPackets))\n\t\t\t\t\tout += metric(\"webrtc_sessions_outbound_rtcp_packets\", ta, int64(i.OutboundRTCPPackets))\n\t\t\t\t\tout += metric(\"webrtc_sessions_outbound_frames_discarded\", ta, int64(i.OutboundFramesDiscarded))\n\t\t\t\t\t// deprecated\n\t\t\t\t\tout += metric(\"webrtc_sessions_bytes_received\", ta, int64(i.BytesReceived))\n\t\t\t\t\tout += metric(\"webrtc_sessions_bytes_sent\", ta, int64(i.BytesSent))\n\t\t\t\t\tout += metric(\"webrtc_sessions_rtp_packets_received\", ta, int64(i.RTPPacketsReceived))\n\t\t\t\t\tout += metric(\"webrtc_sessions_rtp_packets_sent\", ta, int64(i.RTPPacketsSent))\n\t\t\t\t\tout += metric(\"webrtc_sessions_rtp_packets_lost\", ta, int64(i.RTPPacketsLost))\n\t\t\t\t\tout += metricFloat(\"webrtc_sessions_rtp_packets_jitter\", ta, i.RTPPacketsJitter)\n\t\t\t\t\tout += metric(\"webrtc_sessions_rtcp_packets_received\", ta, int64(i.RTCPPacketsReceived))\n\t\t\t\t\tout += metric(\"webrtc_sessions_rtcp_packets_sent\", ta, int64(i.RTCPPacketsSent))\n\t\t\t\t}\n\t\t\t}\n\t\t} else if webrtcSessionFilter == \"\" {\n\t\t\tout += metric(\"webrtc_sessions\", \"\", 0)\n\t\t\tout += metric(\"webrtc_sessions_inbound_bytes\", \"\", 0)\n\t\t\tout += metric(\"webrtc_sessions_inbound_rtp_packets\", \"\", 0)\n\t\t\tout += metric(\"webrtc_sessions_inbound_rtp_packets_lost\", \"\", 0)\n\t\t\tout += metricFloat(\"webrtc_sessions_inbound_rtp_packets_jitter\", \"\", 0)\n\t\t\tout += metric(\"webrtc_sessions_inbound_rtcp_packets\", \"\", 0)\n\t\t\tout += metric(\"webrtc_sessions_outbound_bytes\", \"\", 0)\n\t\t\tout += metric(\"webrtc_sessions_outbound_rtp_packets\", \"\", 0)\n\t\t\tout += metric(\"webrtc_sessions_outbound_rtcp_packets\", \"\", 0)\n\t\t\tout += metric(\"webrtc_sessions_outbound_frames_discarded\", \"\", 0)\n\t\t\t// deprecated\n\t\t\tout += metric(\"webrtc_sessions_bytes_received\", \"\", 0)\n\t\t\tout += metric(\"webrtc_sessions_bytes_sent\", \"\", 0)\n\t\t\tout += metric(\"webrtc_sessions_rtp_packets_received\", \"\", 0)\n\t\t\tout += metric(\"webrtc_sessions_rtp_packets_sent\", \"\", 0)\n\t\t\tout += metric(\"webrtc_sessions_rtp_packets_lost\", \"\", 0)\n\t\t\tout += metricFloat(\"webrtc_sessions_rtp_packets_jitter\", \"\", 0)\n\t\t\tout += metric(\"webrtc_sessions_rtcp_packets_received\", \"\", 0)\n\t\t\tout += metric(\"webrtc_sessions_rtcp_packets_sent\", \"\", 0)\n\t\t}\n\t}\n\n\tctx.Writer.WriteHeader(http.StatusOK)\n\tio.WriteString(ctx.Writer, out) //nolint:errcheck\n}\n\n// SetPathManager is called by core.\nfunc (m *Metrics) SetPathManager(s defs.APIPathManager) {\n\tm.mutex.Lock()\n\tdefer m.mutex.Unlock()\n\tm.pathManager = s\n}\n\n// SetHLSServer is called by core.\nfunc (m *Metrics) SetHLSServer(s defs.APIHLSServer) {\n\tm.mutex.Lock()\n\tdefer m.mutex.Unlock()\n\tm.hlsServer = s\n}\n\n// SetRTSPServer is called by core.\nfunc (m *Metrics) SetRTSPServer(s defs.APIRTSPServer) {\n\tm.mutex.Lock()\n\tdefer m.mutex.Unlock()\n\tm.rtspServer = s\n}\n\n// SetRTSPSServer is called by core.\nfunc (m *Metrics) SetRTSPSServer(s defs.APIRTSPServer) {\n\tm.mutex.Lock()\n\tdefer m.mutex.Unlock()\n\tm.rtspsServer = s\n}\n\n// SetRTMPServer is called by core.\nfunc (m *Metrics) SetRTMPServer(s defs.APIRTMPServer) {\n\tm.mutex.Lock()\n\tdefer m.mutex.Unlock()\n\tm.rtmpServer = s\n}\n\n// SetRTMPSServer is called by core.\nfunc (m *Metrics) SetRTMPSServer(s defs.APIRTMPServer) {\n\tm.mutex.Lock()\n\tdefer m.mutex.Unlock()\n\tm.rtmpsServer = s\n}\n\n// SetSRTServer is called by core.\nfunc (m *Metrics) SetSRTServer(s defs.APISRTServer) {\n\tm.mutex.Lock()\n\tdefer m.mutex.Unlock()\n\tm.srtServer = s\n}\n\n// SetWebRTCServer is called by core.\nfunc (m *Metrics) SetWebRTCServer(s defs.APIWebRTCServer) {\n\tm.mutex.Lock()\n\tdefer m.mutex.Unlock()\n\tm.webRTCServer = s\n}\n"
  },
  {
    "path": "internal/metrics/metrics_test.go",
    "content": "package metrics //nolint:revive\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/auth\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/google/uuid\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc ptrOf[T any](v T) *T {\n\tp := new(T)\n\t*p = v\n\treturn p\n}\n\nfunc requireMetricsLines(t *testing.T, body []byte, lines []string) {\n\tt.Helper()\n\n\tgot := string(body)\n\tfor _, line := range lines {\n\t\trequire.Contains(t, got, line+\"\\n\")\n\t}\n\n\tnonEmptyLines := strings.Count(strings.TrimSpace(got), \"\\n\") + 1\n\trequire.GreaterOrEqual(t, nonEmptyLines, len(lines))\n}\n\ntype dummyPathManager struct{}\n\nfunc (dummyPathManager) APIPathsList() (*defs.APIPathList, error) {\n\treturn &defs.APIPathList{\n\t\tItemCount: 1,\n\t\tPageCount: 1,\n\t\tItems: []defs.APIPath{{\n\t\t\tName:     \"mypath\",\n\t\t\tConfName: \"mypathconf\",\n\t\t\tSource: &defs.APIPathSource{\n\t\t\t\tType: defs.APIPathSourceTypeRTSPSession,\n\t\t\t\tID:   \"123324354\",\n\t\t\t},\n\t\t\tReady:                true,\n\t\t\tReadyTime:            ptrOf(time.Date(2003, 11, 4, 23, 15, 7, 0, time.UTC)),\n\t\t\tTracks:               []defs.APIPathTrackCodec{defs.APIPathTrackCodecH264, defs.APIPathTrackCodecH265},\n\t\t\tInboundBytes:         123,\n\t\t\tOutboundBytes:        456,\n\t\t\tInboundFramesInError: 7,\n\t\t\tBytesReceived:        123,\n\t\t\tBytesSent:            456,\n\t\t\tReaders: []defs.APIPathReader{\n\t\t\t\t{\n\t\t\t\t\tType: defs.APIPathReaderTypeRTSPSession,\n\t\t\t\t\tID:   \"345234423\",\n\t\t\t\t},\n\t\t\t},\n\t\t}},\n\t}, nil\n}\n\nfunc (dummyPathManager) APIPathsGet(string) (*defs.APIPath, error) {\n\tpanic(\"unused\")\n}\n\ntype dummyHLSServer struct{}\n\nfunc (dummyHLSServer) APIMuxersList() (*defs.APIHLSMuxerList, error) {\n\treturn &defs.APIHLSMuxerList{\n\t\tItemCount: 1,\n\t\tPageCount: 1,\n\t\tItems: []defs.APIHLSMuxer{{\n\t\t\tPath:                    \"mypath\",\n\t\t\tCreated:                 time.Date(2003, 11, 4, 23, 15, 7, 0, time.UTC),\n\t\t\tLastRequest:             time.Date(2003, 11, 4, 23, 15, 7, 0, time.UTC),\n\t\t\tOutboundBytes:           789,\n\t\t\tOutboundFramesDiscarded: 12,\n\t\t\tBytesSent:               789,\n\t\t}},\n\t}, nil\n}\n\nfunc (dummyHLSServer) APIMuxersGet(string) (*defs.APIHLSMuxer, error) {\n\tpanic(\"unused\")\n}\n\ntype dummyRTSPServer struct{}\n\nfunc (dummyRTSPServer) APIConnsList() (*defs.APIRTSPConnsList, error) {\n\treturn &defs.APIRTSPConnsList{\n\t\tItemCount: 1,\n\t\tPageCount: 1,\n\t\tItems: []defs.APIRTSPConn{{\n\t\t\tID:            uuid.MustParse(\"18294761-f9d1-4ea9-9a35-fe265b62eb41\"),\n\t\t\tCreated:       time.Date(2003, 11, 4, 23, 15, 7, 0, time.UTC),\n\t\t\tRemoteAddr:    \"124.5.5.5:34542\",\n\t\t\tInboundBytes:  123,\n\t\t\tOutboundBytes: 456,\n\t\t\tBytesReceived: 123,\n\t\t\tBytesSent:     456,\n\t\t\tSession:       nil,\n\t\t}},\n\t}, nil\n}\n\nfunc (dummyRTSPServer) APIConnsGet(uuid.UUID) (*defs.APIRTSPConn, error) {\n\tpanic(\"unused\")\n}\n\nfunc (dummyRTSPServer) APISessionsList() (*defs.APIRTSPSessionList, error) {\n\treturn &defs.APIRTSPSessionList{\n\t\tItemCount: 1,\n\t\tPageCount: 1,\n\t\tItems: []defs.APIRTSPSession{{\n\t\t\tID:                             uuid.MustParse(\"124b22ce-9c34-4387-b045-44caf98049f7\"),\n\t\t\tCreated:                        time.Date(2003, 11, 4, 23, 15, 7, 0, time.UTC),\n\t\t\tRemoteAddr:                     \"124.5.5.5:34542\",\n\t\t\tState:                          defs.APIRTSPSessionStatePublish,\n\t\t\tPath:                           \"mypath\",\n\t\t\tQuery:                          \"myquery\",\n\t\t\tTransport:                      nil,\n\t\t\tInboundBytes:                   123,\n\t\t\tInboundRTPPackets:              789,\n\t\t\tInboundRTPPacketsLost:          456,\n\t\t\tInboundRTPPacketsInError:       789,\n\t\t\tInboundRTPPacketsJitter:        123,\n\t\t\tInboundRTCPPackets:             456,\n\t\t\tInboundRTCPPacketsInError:      456,\n\t\t\tOutboundBytes:                  456,\n\t\t\tOutboundRTPPackets:             123,\n\t\t\tOutboundRTPPacketsReportedLost: 321,\n\t\t\tOutboundRTPPacketsDiscarded:    111,\n\t\t\tOutboundRTCPPackets:            789,\n\t\t\tBytesReceived:                  123,\n\t\t\tBytesSent:                      456,\n\t\t\tRTPPacketsReceived:             789,\n\t\t\tRTPPacketsSent:                 123,\n\t\t\tRTPPacketsLost:                 456,\n\t\t\tRTPPacketsInError:              789,\n\t\t\tRTPPacketsJitter:               123,\n\t\t\tRTCPPacketsReceived:            456,\n\t\t\tRTCPPacketsSent:                789,\n\t\t\tRTCPPacketsInError:             456,\n\t\t}},\n\t}, nil\n}\n\nfunc (dummyRTSPServer) APISessionsGet(uuid.UUID) (*defs.APIRTSPSession, error) {\n\tpanic(\"unused\")\n}\n\nfunc (dummyRTSPServer) APISessionsKick(uuid.UUID) error {\n\tpanic(\"unused\")\n}\n\ntype dummyRTMPServer struct{}\n\nfunc (dummyRTMPServer) APIConnsList() (*defs.APIRTMPConnList, error) {\n\treturn &defs.APIRTMPConnList{\n\t\tItemCount: 1,\n\t\tPageCount: 1,\n\t\tItems: []defs.APIRTMPConn{{\n\t\t\tID:                      uuid.MustParse(\"9a07afe4-fc07-4c9b-be6e-6255720c36d0\"),\n\t\t\tCreated:                 time.Date(2003, 11, 4, 23, 15, 7, 0, time.UTC),\n\t\t\tRemoteAddr:              \"3.3.3.3:5678\",\n\t\t\tState:                   defs.APIRTMPConnStateRead,\n\t\t\tPath:                    \"mypath\",\n\t\t\tQuery:                   \"myquery\",\n\t\t\tInboundBytes:            123,\n\t\t\tOutboundBytes:           456,\n\t\t\tOutboundFramesDiscarded: 12,\n\t\t\tBytesReceived:           123,\n\t\t\tBytesSent:               456,\n\t\t}},\n\t}, nil\n}\n\nfunc (dummyRTMPServer) APIConnsGet(uuid.UUID) (*defs.APIRTMPConn, error) {\n\tpanic(\"unused\")\n}\n\nfunc (dummyRTMPServer) APIConnsKick(uuid.UUID) error {\n\tpanic(\"unused\")\n}\n\ntype dummyWebRTCServer struct{}\n\nfunc (dummyWebRTCServer) APISessionsList() (*defs.APIWebRTCSessionList, error) {\n\treturn &defs.APIWebRTCSessionList{\n\t\tItemCount: 1,\n\t\tPageCount: 1,\n\t\tItems: []defs.APIWebRTCSession{{\n\t\t\tID:                        uuid.MustParse(\"f47ac10b-58cc-4372-a567-0e02b2c3d479\"),\n\t\t\tCreated:                   time.Date(2003, 11, 4, 23, 15, 7, 0, time.UTC),\n\t\t\tRemoteAddr:                \"127.0.0.1:3455\",\n\t\t\tPeerConnectionEstablished: true,\n\t\t\tLocalCandidate:            \"local\",\n\t\t\tRemoteCandidate:           \"remote\",\n\t\t\tState:                     defs.APIWebRTCSessionStateRead,\n\t\t\tPath:                      \"mypath\",\n\t\t\tQuery:                     \"myquery\",\n\t\t\tInboundBytes:              123,\n\t\t\tInboundRTPPackets:         789,\n\t\t\tInboundRTPPacketsLost:     456,\n\t\t\tInboundRTPPacketsJitter:   789,\n\t\t\tInboundRTCPPackets:        123,\n\t\t\tOutboundBytes:             456,\n\t\t\tOutboundRTPPackets:        123,\n\t\t\tOutboundRTCPPackets:       456,\n\t\t\tOutboundFramesDiscarded:   12,\n\t\t\tBytesReceived:             123,\n\t\t\tBytesSent:                 456,\n\t\t\tRTPPacketsReceived:        789,\n\t\t\tRTPPacketsSent:            123,\n\t\t\tRTPPacketsLost:            456,\n\t\t\tRTPPacketsJitter:          789,\n\t\t\tRTCPPacketsReceived:       123,\n\t\t\tRTCPPacketsSent:           456,\n\t\t}},\n\t}, nil\n}\n\nfunc (dummyWebRTCServer) APISessionsGet(uuid.UUID) (*defs.APIWebRTCSession, error) {\n\tpanic(\"unused\")\n}\n\nfunc (dummyWebRTCServer) APISessionsKick(uuid.UUID) error {\n\tpanic(\"unused\")\n}\n\nfunc TestPreflightRequest(t *testing.T) {\n\tm := Metrics{\n\t\tAddress:      \"localhost:9998\",\n\t\tAllowOrigins: []string{\"*\"},\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tAuthManager:  test.NilAuthManager,\n\t\tParent:       test.NilLogger,\n\t}\n\terr := m.Initialize()\n\trequire.NoError(t, err)\n\tdefer m.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\treq, err := http.NewRequest(http.MethodOptions, \"http://localhost:9998\", nil)\n\trequire.NoError(t, err)\n\n\treq.Header.Add(\"Access-Control-Request-Method\", \"GET\")\n\n\tres, err := hc.Do(req)\n\trequire.NoError(t, err)\n\tdefer res.Body.Close()\n\n\trequire.Equal(t, http.StatusNoContent, res.StatusCode)\n\n\tbyts, err := io.ReadAll(res.Body)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, \"*\", res.Header.Get(\"Access-Control-Allow-Origin\"))\n\trequire.Equal(t, \"true\", res.Header.Get(\"Access-Control-Allow-Credentials\"))\n\trequire.Equal(t, \"OPTIONS, GET\", res.Header.Get(\"Access-Control-Allow-Methods\"))\n\trequire.Equal(t, \"Authorization\", res.Header.Get(\"Access-Control-Allow-Headers\"))\n\trequire.Equal(t, byts, []byte{})\n}\n\nfunc TestMetrics(t *testing.T) {\n\tchecked := false\n\n\tm := Metrics{\n\t\tAddress:      \"localhost:9998\",\n\t\tAllowOrigins: []string{\"*\"},\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tAuthManager: &test.AuthManager{\n\t\t\tAuthenticateImpl: func(req *auth.Request) (string, *auth.Error) {\n\t\t\t\trequire.Equal(t, conf.AuthActionMetrics, req.Action)\n\t\t\t\trequire.Equal(t, \"myuser\", req.Credentials.User)\n\t\t\t\trequire.Equal(t, \"mypass\", req.Credentials.Pass)\n\t\t\t\tchecked = true\n\t\t\t\treturn req.Credentials.User, nil\n\t\t\t},\n\t\t},\n\t\tParent: test.NilLogger,\n\t}\n\terr := m.Initialize()\n\trequire.NoError(t, err)\n\tdefer m.Close()\n\n\tm.SetPathManager(&dummyPathManager{})\n\tm.SetHLSServer(&dummyHLSServer{})\n\tm.SetRTSPServer(&dummyRTSPServer{})\n\tm.SetRTSPSServer(&dummyRTSPServer{})\n\tm.SetRTMPServer(&dummyRTMPServer{})\n\tm.SetRTMPSServer(&dummyRTMPServer{})\n\tm.SetWebRTCServer(&dummyWebRTCServer{})\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\tres, err := hc.Get(\"http://myuser:mypass@localhost:9998/metrics\")\n\trequire.NoError(t, err)\n\tdefer res.Body.Close()\n\n\trequire.Equal(t, http.StatusOK, res.StatusCode)\n\n\tbyts, err := io.ReadAll(res.Body)\n\trequire.NoError(t, err)\n\n\trequireMetricsLines(t, byts, []string{\n\t\t`paths{name=\"mypath\",state=\"ready\"} 1`,\n\t\t`paths_inbound_bytes{name=\"mypath\",state=\"ready\"} 123`,\n\t\t`paths_outbound_bytes{name=\"mypath\",state=\"ready\"} 456`,\n\t\t`paths_inbound_frames_in_error{name=\"mypath\",state=\"ready\"} 7`,\n\t\t`paths_bytes_received{name=\"mypath\",state=\"ready\"} 123`,\n\t\t`paths_bytes_sent{name=\"mypath\",state=\"ready\"} 456`,\n\t\t`paths_readers{name=\"mypath\",state=\"ready\"} 1`,\n\t\t`hls_muxers{name=\"mypath\"} 1`,\n\t\t`hls_muxers_outbound_bytes{name=\"mypath\"} 789`,\n\t\t`hls_muxers_outbound_frames_discarded{name=\"mypath\"} 12`,\n\t\t`rtsp_conns_inbound_bytes{id=\"18294761-f9d1-4ea9-9a35-fe265b62eb41\"} 123`,\n\t\t`rtsp_sessions_outbound_rtp_packets_discarded{id=\"124b22ce-9c34-4387-b045-44caf98049f7\",` +\n\t\t\t`path=\"mypath\",remoteAddr=\"124.5.5.5:34542\",state=\"publish\"} 111`,\n\t\t`rtsps_sessions_outbound_rtp_packets_discarded{id=\"124b22ce-9c34-4387-b045-44caf98049f7\",` +\n\t\t\t`path=\"mypath\",remoteAddr=\"124.5.5.5:34542\",state=\"publish\"} 111`,\n\t\t`rtmp_conns_inbound_bytes{id=\"9a07afe4-fc07-4c9b-be6e-6255720c36d0\",` +\n\t\t\t`path=\"mypath\",remoteAddr=\"3.3.3.3:5678\",state=\"read\"} 123`,\n\t\t`rtmp_conns_outbound_bytes{id=\"9a07afe4-fc07-4c9b-be6e-6255720c36d0\",` +\n\t\t\t`path=\"mypath\",remoteAddr=\"3.3.3.3:5678\",state=\"read\"} 456`,\n\t\t`rtmp_conns_outbound_frames_discarded{id=\"9a07afe4-fc07-4c9b-be6e-6255720c36d0\",` +\n\t\t\t`path=\"mypath\",remoteAddr=\"3.3.3.3:5678\",state=\"read\"} 12`,\n\t\t`rtmp_conns_bytes_received{id=\"9a07afe4-fc07-4c9b-be6e-6255720c36d0\",` +\n\t\t\t`path=\"mypath\",remoteAddr=\"3.3.3.3:5678\",state=\"read\"} 123`,\n\t\t`webrtc_sessions_outbound_bytes{id=\"f47ac10b-58cc-4372-a567-0e02b2c3d479\",` +\n\t\t\t`path=\"mypath\",remoteAddr=\"127.0.0.1:3455\",state=\"read\"} 456`,\n\t\t`webrtc_sessions_outbound_frames_discarded{id=\"f47ac10b-58cc-4372-a567-0e02b2c3d479\",` +\n\t\t\t`path=\"mypath\",remoteAddr=\"127.0.0.1:3455\",state=\"read\"} 12`,\n\t})\n\n\trequire.True(t, checked)\n}\n\nfunc TestAuthError(t *testing.T) {\n\tn := 0\n\n\tm := Metrics{\n\t\tAddress:      \"localhost:9998\",\n\t\tAllowOrigins: []string{\"*\"},\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tAuthManager: &test.AuthManager{\n\t\t\tAuthenticateImpl: func(req *auth.Request) (string, *auth.Error) {\n\t\t\t\tif req.Credentials.User == \"\" {\n\t\t\t\t\treturn \"\", &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf(\"auth error\")}\n\t\t\t\t}\n\t\t\t\treturn \"\", &auth.Error{Wrapped: fmt.Errorf(\"auth error\")}\n\t\t\t},\n\t\t},\n\t\tParent: test.Logger(func(l logger.Level, s string, i ...any) {\n\t\t\tif l == logger.Info {\n\t\t\t\tif n == 1 {\n\t\t\t\t\trequire.Regexp(t, \"failed to authenticate: auth error$\", fmt.Sprintf(s, i...))\n\t\t\t\t}\n\t\t\t\tn++\n\t\t\t}\n\t\t}),\n\t}\n\terr := m.Initialize()\n\trequire.NoError(t, err)\n\tdefer m.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\tres, err := hc.Get(\"http://localhost:9998/metrics\")\n\trequire.NoError(t, err)\n\tdefer res.Body.Close()\n\n\trequire.Equal(t, http.StatusUnauthorized, res.StatusCode)\n\trequire.Equal(t, `Basic realm=\"mediamtx\"`, res.Header.Get(\"WWW-Authenticate\"))\n\n\tres, err = hc.Get(\"http://myuser:mypass@localhost:9998/metrics\")\n\trequire.NoError(t, err)\n\tdefer res.Body.Close()\n\n\trequire.Equal(t, http.StatusUnauthorized, res.StatusCode)\n\n\trequire.Equal(t, 2, n)\n}\n\nfunc TestFilter(t *testing.T) {\n\tfor _, ca := range []string{\n\t\t\"path\",\n\t\t\"hls_muxer\",\n\t\t\"rtsp_conn\",\n\t\t\"rtsp_session\",\n\t\t// \"rtsps_conn\",\n\t\t// \"rtsps_session\",\n\t\t// \"rtmp_conn\",\n\t\t// \"rtmps_conn\",\n\t\t// \"srt_conn\",\n\t\t// \"webrtc_session\",\n\t} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tm := Metrics{\n\t\t\t\tAddress:      \"localhost:9998\",\n\t\t\t\tAllowOrigins: []string{\"*\"},\n\t\t\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\t\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\t\t\tAuthManager:  test.NilAuthManager,\n\t\t\t\tParent:       test.NilLogger,\n\t\t\t}\n\t\t\terr := m.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer m.Close()\n\n\t\t\tm.SetPathManager(&dummyPathManager{})\n\t\t\tm.SetHLSServer(&dummyHLSServer{})\n\t\t\tm.SetRTSPServer(&dummyRTSPServer{})\n\t\t\tm.SetWebRTCServer(&dummyWebRTCServer{})\n\n\t\t\ttr := &http.Transport{}\n\t\t\tdefer tr.CloseIdleConnections()\n\t\t\thc := &http.Client{Transport: tr}\n\n\t\t\tu := \"http://localhost:9998/metrics\"\n\n\t\t\tswitch ca {\n\t\t\tcase \"path\":\n\t\t\t\tu += \"?path=mypath\"\n\t\t\tcase \"hls_muxer\":\n\t\t\t\tu += \"?hls_muxer=mypath\"\n\t\t\tcase \"rtsp_conn\":\n\t\t\t\tu += \"?rtsp_conn=18294761-f9d1-4ea9-9a35-fe265b62eb41\"\n\t\t\tcase \"rtsp_session\":\n\t\t\t\tu += \"?rtsp_session=124b22ce-9c34-4387-b045-44caf98049f7\"\n\t\t\t}\n\n\t\t\tres, err := hc.Get(u)\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer res.Body.Close()\n\n\t\t\tbyts, err := io.ReadAll(res.Body)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tswitch ca {\n\t\t\tcase \"path\":\n\t\t\t\trequire.Equal(t,\n\t\t\t\t\t`paths{name=\"mypath\",state=\"ready\"} 1`+\"\\n\"+\n\t\t\t\t\t\t`paths_inbound_bytes{name=\"mypath\",state=\"ready\"} 123`+\"\\n\"+\n\t\t\t\t\t\t`paths_outbound_bytes{name=\"mypath\",state=\"ready\"} 456`+\"\\n\"+\n\t\t\t\t\t\t`paths_inbound_frames_in_error{name=\"mypath\",state=\"ready\"} 7`+\"\\n\"+\n\t\t\t\t\t\t`paths_bytes_received{name=\"mypath\",state=\"ready\"} 123`+\"\\n\"+\n\t\t\t\t\t\t`paths_bytes_sent{name=\"mypath\",state=\"ready\"} 456`+\"\\n\"+\n\t\t\t\t\t\t`paths_readers{name=\"mypath\",state=\"ready\"} 1`+\"\\n\",\n\t\t\t\t\tstring(byts))\n\n\t\t\tcase \"hls_muxer\":\n\t\t\t\trequire.Equal(t,\n\t\t\t\t\t`hls_muxers{name=\"mypath\"} 1`+\"\\n\"+\n\t\t\t\t\t\t`hls_muxers_outbound_bytes{name=\"mypath\"} 789`+\"\\n\"+\n\t\t\t\t\t\t`hls_muxers_outbound_frames_discarded{name=\"mypath\"} 12`+\"\\n\"+\n\t\t\t\t\t\t`hls_muxers_bytes_sent{name=\"mypath\"} 789`+\"\\n\",\n\t\t\t\t\tstring(byts))\n\n\t\t\tcase \"rtsp_conn\":\n\t\t\t\trequire.Equal(t,\n\t\t\t\t\t`rtsp_conns{id=\"18294761-f9d1-4ea9-9a35-fe265b62eb41\"} 1`+\"\\n\"+\n\t\t\t\t\t\t`rtsp_conns_inbound_bytes{id=\"18294761-f9d1-4ea9-9a35-fe265b62eb41\"} 123`+\"\\n\"+\n\t\t\t\t\t\t`rtsp_conns_outbound_bytes{id=\"18294761-f9d1-4ea9-9a35-fe265b62eb41\"} 456`+\"\\n\"+\n\t\t\t\t\t\t`rtsp_conns_bytes_received{id=\"18294761-f9d1-4ea9-9a35-fe265b62eb41\"} 123`+\"\\n\"+\n\t\t\t\t\t\t`rtsp_conns_bytes_sent{id=\"18294761-f9d1-4ea9-9a35-fe265b62eb41\"} 456`+\"\\n\",\n\t\t\t\t\tstring(byts))\n\n\t\t\tcase \"rtsp_session\":\n\t\t\t\trequire.Equal(t,\n\t\t\t\t\t`rtsp_sessions{id=\"124b22ce-9c34-4387-b045-44caf98049f7\",`+\n\t\t\t\t\t\t`path=\"mypath\",remoteAddr=\"124.5.5.5:34542\",state=\"publish\"} 1`+\"\\n\"+\n\t\t\t\t\t\t`rtsp_sessions_inbound_bytes{id=\"124b22ce-9c34-4387-b045-44caf98049f7\",`+\n\t\t\t\t\t\t`path=\"mypath\",remoteAddr=\"124.5.5.5:34542\",state=\"publish\"} 123`+\"\\n\"+\n\t\t\t\t\t\t`rtsp_sessions_inbound_rtp_packets{id=\"124b22ce-9c34-4387-b045-44caf98049f7\",`+\n\t\t\t\t\t\t`path=\"mypath\",remoteAddr=\"124.5.5.5:34542\",state=\"publish\"} 789`+\"\\n\"+\n\t\t\t\t\t\t`rtsp_sessions_inbound_rtp_packets_lost{id=\"124b22ce-9c34-4387-b045-44caf98049f7\",`+\n\t\t\t\t\t\t`path=\"mypath\",remoteAddr=\"124.5.5.5:34542\",state=\"publish\"} 456`+\"\\n\"+\n\t\t\t\t\t\t`rtsp_sessions_inbound_rtp_packets_in_error{id=\"124b22ce-9c34-4387-b045-44caf98049f7\",`+\n\t\t\t\t\t\t`path=\"mypath\",remoteAddr=\"124.5.5.5:34542\",state=\"publish\"} 789`+\"\\n\"+\n\t\t\t\t\t\t`rtsp_sessions_inbound_rtp_packets_jitter{id=\"124b22ce-9c34-4387-b045-44caf98049f7\",`+\n\t\t\t\t\t\t`path=\"mypath\",remoteAddr=\"124.5.5.5:34542\",state=\"publish\"} 123`+\"\\n\"+\n\t\t\t\t\t\t`rtsp_sessions_inbound_rtcp_packets{id=\"124b22ce-9c34-4387-b045-44caf98049f7\",`+\n\t\t\t\t\t\t`path=\"mypath\",remoteAddr=\"124.5.5.5:34542\",state=\"publish\"} 456`+\"\\n\"+\n\t\t\t\t\t\t`rtsp_sessions_inbound_rtcp_packets_in_error{id=\"124b22ce-9c34-4387-b045-44caf98049f7\",`+\n\t\t\t\t\t\t`path=\"mypath\",remoteAddr=\"124.5.5.5:34542\",state=\"publish\"} 456`+\"\\n\"+\n\t\t\t\t\t\t`rtsp_sessions_outbound_bytes{id=\"124b22ce-9c34-4387-b045-44caf98049f7\",`+\n\t\t\t\t\t\t`path=\"mypath\",remoteAddr=\"124.5.5.5:34542\",state=\"publish\"} 456`+\"\\n\"+\n\t\t\t\t\t\t`rtsp_sessions_outbound_rtp_packets{id=\"124b22ce-9c34-4387-b045-44caf98049f7\",`+\n\t\t\t\t\t\t`path=\"mypath\",remoteAddr=\"124.5.5.5:34542\",state=\"publish\"} 123`+\"\\n\"+\n\t\t\t\t\t\t`rtsp_sessions_outbound_rtp_packets_reported_lost{id=\"124b22ce-9c34-4387-b045-44caf98049f7\",`+\n\t\t\t\t\t\t`path=\"mypath\",remoteAddr=\"124.5.5.5:34542\",state=\"publish\"} 321`+\"\\n\"+\n\t\t\t\t\t\t`rtsp_sessions_outbound_rtp_packets_discarded{id=\"124b22ce-9c34-4387-b045-44caf98049f7\",`+\n\t\t\t\t\t\t`path=\"mypath\",remoteAddr=\"124.5.5.5:34542\",state=\"publish\"} 111`+\"\\n\"+\n\t\t\t\t\t\t`rtsp_sessions_outbound_rtcp_packets{id=\"124b22ce-9c34-4387-b045-44caf98049f7\",`+\n\t\t\t\t\t\t`path=\"mypath\",remoteAddr=\"124.5.5.5:34542\",state=\"publish\"} 789`+\"\\n\"+\n\t\t\t\t\t\t`rtsp_sessions_bytes_received{id=\"124b22ce-9c34-4387-b045-44caf98049f7\",`+\n\t\t\t\t\t\t`path=\"mypath\",remoteAddr=\"124.5.5.5:34542\",state=\"publish\"} 123`+\"\\n\"+\n\t\t\t\t\t\t`rtsp_sessions_bytes_sent{id=\"124b22ce-9c34-4387-b045-44caf98049f7\",`+\n\t\t\t\t\t\t`path=\"mypath\",remoteAddr=\"124.5.5.5:34542\",state=\"publish\"} 456`+\"\\n\"+\n\t\t\t\t\t\t`rtsp_sessions_rtp_packets_received{id=\"124b22ce-9c34-4387-b045-44caf98049f7\",`+\n\t\t\t\t\t\t`path=\"mypath\",remoteAddr=\"124.5.5.5:34542\",state=\"publish\"} 789`+\"\\n\"+\n\t\t\t\t\t\t`rtsp_sessions_rtp_packets_sent{id=\"124b22ce-9c34-4387-b045-44caf98049f7\",`+\n\t\t\t\t\t\t`path=\"mypath\",remoteAddr=\"124.5.5.5:34542\",state=\"publish\"} 123`+\"\\n\"+\n\t\t\t\t\t\t`rtsp_sessions_rtp_packets_lost{id=\"124b22ce-9c34-4387-b045-44caf98049f7\",`+\n\t\t\t\t\t\t`path=\"mypath\",remoteAddr=\"124.5.5.5:34542\",state=\"publish\"} 456`+\"\\n\"+\n\t\t\t\t\t\t`rtsp_sessions_rtp_packets_in_error{id=\"124b22ce-9c34-4387-b045-44caf98049f7\",`+\n\t\t\t\t\t\t`path=\"mypath\",remoteAddr=\"124.5.5.5:34542\",state=\"publish\"} 789`+\"\\n\"+\n\t\t\t\t\t\t`rtsp_sessions_rtp_packets_jitter{id=\"124b22ce-9c34-4387-b045-44caf98049f7\",`+\n\t\t\t\t\t\t`path=\"mypath\",remoteAddr=\"124.5.5.5:34542\",state=\"publish\"} 123`+\"\\n\"+\n\t\t\t\t\t\t`rtsp_sessions_rtcp_packets_received{id=\"124b22ce-9c34-4387-b045-44caf98049f7\",`+\n\t\t\t\t\t\t`path=\"mypath\",remoteAddr=\"124.5.5.5:34542\",state=\"publish\"} 456`+\"\\n\"+\n\t\t\t\t\t\t`rtsp_sessions_rtcp_packets_sent{id=\"124b22ce-9c34-4387-b045-44caf98049f7\",`+\n\t\t\t\t\t\t`path=\"mypath\",remoteAddr=\"124.5.5.5:34542\",state=\"publish\"} 789`+\"\\n\"+\n\t\t\t\t\t\t`rtsp_sessions_rtcp_packets_in_error{id=\"124b22ce-9c34-4387-b045-44caf98049f7\",`+\n\t\t\t\t\t\t`path=\"mypath\",remoteAddr=\"124.5.5.5:34542\",state=\"publish\"} 456`+\"\\n\",\n\t\t\t\t\tstring(byts))\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/ntpestimator/estimator.go",
    "content": "// Package ntpestimator contains a NTP estimator.\npackage ntpestimator\n\nimport (\n\t\"time\"\n)\n\nconst (\n\tmaxTimeDifference = 5 * time.Second\n)\n\nvar timeNow = time.Now\n\nfunc multiplyAndDivide(v, m, d time.Duration) time.Duration {\n\tsecs := v / d\n\tdec := v % d\n\treturn (secs*m + dec*m/d)\n}\n\n// Estimator is a NTP estimator.\ntype Estimator struct {\n\tClockRate int\n\n\trefNTP time.Time\n\trefPTS int64\n}\n\nvar zero = time.Time{}\n\n// Estimate returns estimated NTP.\nfunc (e *Estimator) Estimate(pts int64) time.Time {\n\tnow := timeNow()\n\n\t// do not store monotonic clock, in order to include\n\t// system clock changes into time differences\n\tnow = now.Round(0)\n\n\tif e.refNTP.Equal(zero) {\n\t\te.refNTP = now\n\t\te.refPTS = pts\n\t\treturn now\n\t}\n\n\tcomputed := e.refNTP.Add((multiplyAndDivide(time.Duration(pts-e.refPTS), time.Second, time.Duration(e.ClockRate))))\n\n\tif computed.After(now) || computed.Before(now.Add(-maxTimeDifference)) {\n\t\te.refNTP = now\n\t\te.refPTS = pts\n\t\treturn now\n\t}\n\n\treturn computed\n}\n"
  },
  {
    "path": "internal/ntpestimator/estimator_test.go",
    "content": "package ntpestimator\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestEstimator(t *testing.T) {\n\te := &Estimator{ClockRate: 90000}\n\n\ttimeNow = func() time.Time { return time.Date(2003, 11, 4, 23, 15, 7, 0, time.UTC) }\n\tntp := e.Estimate(90000)\n\trequire.Equal(t, time.Date(2003, 11, 4, 23, 15, 7, 0, time.UTC), ntp)\n\n\ttimeNow = func() time.Time { return time.Date(2003, 11, 4, 23, 15, 8, 0, time.UTC) }\n\tntp = e.Estimate(2 * 90000)\n\trequire.Equal(t, time.Date(2003, 11, 4, 23, 15, 8, 0, time.UTC), ntp)\n\n\ttimeNow = func() time.Time { return time.Date(2003, 11, 4, 23, 15, 10, 0, time.UTC) }\n\tntp = e.Estimate(3 * 90000)\n\trequire.Equal(t, time.Date(2003, 11, 4, 23, 15, 9, 0, time.UTC), ntp)\n\n\ttimeNow = func() time.Time { return time.Date(2003, 11, 4, 23, 15, 9, 0, time.UTC) }\n\tntp = e.Estimate(4 * 90000)\n\trequire.Equal(t, time.Date(2003, 11, 4, 23, 15, 9, 0, time.UTC), ntp)\n\n\ttimeNow = func() time.Time { return time.Date(2003, 11, 4, 23, 15, 15, 0, time.UTC) }\n\tntp = e.Estimate(5 * 90000)\n\trequire.Equal(t, time.Date(2003, 11, 4, 23, 15, 10, 0, time.UTC), ntp)\n\n\ttimeNow = func() time.Time { return time.Date(2003, 11, 4, 23, 15, 20, 0, time.UTC) }\n\tntp = e.Estimate(6 * 90000)\n\trequire.Equal(t, time.Date(2003, 11, 4, 23, 15, 20, 0, time.UTC), ntp)\n}\n"
  },
  {
    "path": "internal/packetdumper/conn.go",
    "content": "// Package packetdumper provides utilities to dump packets to disk.\npackage packetdumper\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/google/gopacket\"\n\t\"github.com/google/gopacket/layers\"\n\t\"github.com/google/gopacket/pcapgo\"\n\t\"github.com/google/uuid\"\n)\n\nvar _ net.Conn = (*Conn)(nil)\n\ntype direction int\n\nconst (\n\tdirRead direction = iota\n\tdirWrite\n\tdirHandshake\n)\n\ntype dumpEntry struct {\n\tntp       time.Time\n\tdata      []byte\n\tdirection direction\n}\n\n// Conn is a wrapper around net.Conn that dumps packets to disk.\ntype Conn struct {\n\tPrefix     string\n\tConn       net.Conn\n\tServerSide bool\n\n\tf    *os.File\n\tpw   *pcapgo.NgWriter\n\tonce sync.Once\n\n\tqueue      chan dumpEntry\n\tterminated chan struct{}\n\tdone       chan struct{}\n}\n\n// Initialize initializes Conn.\nfunc (c *Conn) Initialize() error {\n\tvar err error\n\tc.f, err = os.Create(fmt.Sprintf(\"%s_%d_%s.pcapng\", c.Prefix, time.Now().UnixNano(), uuid.New().String()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tc.pw, err = pcapgo.NewNgWriter(c.f, layers.LinkTypeEthernet)\n\tif err != nil {\n\t\tc.f.Close()\n\t\treturn err\n\t}\n\n\tc.queue = make(chan dumpEntry, 64)\n\tc.terminated = make(chan struct{})\n\tc.done = make(chan struct{})\n\n\tgo c.run()\n\n\tc.enqueue(dumpEntry{ntp: time.Now(), direction: dirHandshake})\n\n\treturn nil\n}\n\n// Close implements net.Conn.\nfunc (c *Conn) Close() error {\n\tc.once.Do(func() {\n\t\tclose(c.terminated)\n\t})\n\t<-c.done\n\treturn c.Conn.Close()\n}\n\nfunc (c *Conn) run() {\n\tdefer close(c.done)\n\tdefer c.f.Close()\n\n\tlocal := c.Conn.LocalAddr().(*net.TCPAddr)\n\tremote := c.Conn.RemoteAddr().(*net.TCPAddr)\n\n\tnextLocalSequence := uint32(1000)\n\tnextRemoteSequence := uint32(2000)\n\n\tfor {\n\t\tselect {\n\t\tcase e := <-c.queue:\n\t\t\tc.processEntry(e, local, remote, &nextLocalSequence, &nextRemoteSequence)\n\n\t\tcase <-c.terminated:\n\t\t\t// Drain anything already in the queue before exiting.\n\t\t\tfor {\n\t\t\t\tselect {\n\t\t\t\tcase e := <-c.queue:\n\t\t\t\t\tc.processEntry(e, local, remote, &nextLocalSequence, &nextRemoteSequence)\n\t\t\t\tdefault:\n\t\t\t\t\tc.pw.Flush() //nolint:errcheck\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (c *Conn) processEntry(\n\te dumpEntry,\n\tlocal, remote *net.TCPAddr,\n\tnextLocalSequence, nextRemoteSequence *uint32,\n) {\n\tswitch e.direction {\n\tcase dirHandshake:\n\t\tclientAddr, serverAddr := local, remote // client side: local initiates\n\t\tclientSeq, serverSeq := nextLocalSequence, nextRemoteSequence\n\t\tif c.ServerSide {\n\t\t\tclientAddr, serverAddr = remote, local // server side: remote initiated\n\t\t\tclientSeq, serverSeq = nextRemoteSequence, nextLocalSequence\n\t\t}\n\n\t\t// SYN (client -> server)\n\t\tc.writePacket(e.ntp, clientAddr, serverAddr,\n\t\t\tlayers.TCP{SYN: true, Window: 65535, Seq: *clientSeq, Ack: 0}, nil)\n\t\t*clientSeq++\n\n\t\t// SYN-ACK (server -> client)\n\t\tc.writePacket(e.ntp, serverAddr, clientAddr,\n\t\t\tlayers.TCP{SYN: true, ACK: true, Window: 65535, Seq: *serverSeq, Ack: *clientSeq}, nil)\n\t\t*serverSeq++\n\n\t\t// ACK (client -> server)\n\t\tc.writePacket(e.ntp, clientAddr, serverAddr,\n\t\t\tlayers.TCP{ACK: true, Window: 65535, Seq: *clientSeq, Ack: *serverSeq}, nil)\n\n\tcase dirRead:\n\t\ttcpFlags := layers.TCP{\n\t\t\tPSH:    true,\n\t\t\tACK:    true,\n\t\t\tWindow: 14600,\n\t\t\tSeq:    *nextRemoteSequence,\n\t\t\tAck:    *nextLocalSequence,\n\t\t}\n\t\tc.writePacket(e.ntp, remote, local, tcpFlags, e.data)\n\t\t*nextRemoteSequence += uint32(len(e.data))\n\n\tcase dirWrite:\n\t\ttcpFlags := layers.TCP{\n\t\t\tPSH:    true,\n\t\t\tACK:    true,\n\t\t\tWindow: 14600,\n\t\t\tSeq:    *nextLocalSequence,\n\t\t\tAck:    *nextRemoteSequence,\n\t\t}\n\t\tc.writePacket(e.ntp, local, remote, tcpFlags, e.data)\n\t\t*nextLocalSequence += uint32(len(e.data))\n\t}\n}\n\nfunc (c *Conn) writePacket(\n\tntp time.Time,\n\tsrc, dst *net.TCPAddr,\n\ttcpFlags layers.TCP,\n\tpayload []byte,\n) {\n\teth := &layers.Ethernet{\n\t\tSrcMAC:       net.HardwareAddr{0, 0, 0, 0, 0, 0},\n\t\tDstMAC:       net.HardwareAddr{0, 0, 0, 0, 0, 0},\n\t\tEthernetType: layers.EthernetTypeIPv6,\n\t}\n\n\tipv6 := &layers.IPv6{\n\t\tVersion:    6,\n\t\tSrcIP:      src.IP.To16(),\n\t\tDstIP:      dst.IP.To16(),\n\t\tNextHeader: layers.IPProtocolTCP,\n\t\tHopLimit:   64,\n\t}\n\n\ttcp := &layers.TCP{\n\t\tSrcPort: layers.TCPPort(src.Port),\n\t\tDstPort: layers.TCPPort(dst.Port),\n\t\tSeq:     tcpFlags.Seq,\n\t\tAck:     tcpFlags.Ack,\n\t\tWindow:  tcpFlags.Window,\n\t\tSYN:     tcpFlags.SYN,\n\t\tACK:     tcpFlags.ACK,\n\t\tPSH:     tcpFlags.PSH,\n\t\tFIN:     tcpFlags.FIN,\n\t}\n\ttcp.SetNetworkLayerForChecksum(ipv6) //nolint:errcheck\n\n\tbuf := gopacket.NewSerializeBuffer()\n\topts := gopacket.SerializeOptions{FixLengths: true, ComputeChecksums: true}\n\tgopacket.SerializeLayers(buf, opts, eth, ipv6, tcp, gopacket.Payload(payload)) //nolint:errcheck\n\n\traw := buf.Bytes()\n\n\tc.pw.WritePacket(gopacket.CaptureInfo{ //nolint:errcheck\n\t\tTimestamp:     ntp,\n\t\tCaptureLength: len(raw),\n\t\tLength:        len(raw),\n\t}, raw)\n}\n\nfunc (c *Conn) enqueue(e dumpEntry) {\n\tselect {\n\tcase c.queue <- e:\n\tcase <-c.terminated:\n\t}\n}\n\nfunc (c *Conn) Read(p []byte) (n int, err error) {\n\tn, err = c.Conn.Read(p)\n\n\tif n != 0 {\n\t\tc.enqueue(dumpEntry{\n\t\t\tntp:       time.Now(),\n\t\t\tdata:      append([]byte(nil), p[:n]...),\n\t\t\tdirection: dirRead,\n\t\t})\n\t}\n\n\treturn n, err\n}\n\nfunc (c *Conn) Write(p []byte) (n int, err error) {\n\tn, err = c.Conn.Write(p)\n\n\tif err == nil {\n\t\tc.enqueue(dumpEntry{\n\t\t\tntp:       time.Now(),\n\t\t\tdata:      append([]byte(nil), p...),\n\t\t\tdirection: dirWrite,\n\t\t})\n\t}\n\n\treturn n, err\n}\n\n// LocalAddr implements net.Conn.\nfunc (c *Conn) LocalAddr() net.Addr { return c.Conn.LocalAddr() }\n\n// RemoteAddr implements net.Conn.\nfunc (c *Conn) RemoteAddr() net.Addr { return c.Conn.RemoteAddr() }\n\n// SetDeadline implements net.Conn.\nfunc (c *Conn) SetDeadline(t time.Time) error { return c.Conn.SetDeadline(t) }\n\n// SetReadDeadline implements net.Conn.\nfunc (c *Conn) SetReadDeadline(t time.Time) error { return c.Conn.SetReadDeadline(t) }\n\n// SetWriteDeadline implements net.Conn.\nfunc (c *Conn) SetWriteDeadline(t time.Time) error { return c.Conn.SetWriteDeadline(t) }\n"
  },
  {
    "path": "internal/packetdumper/conn_test.go",
    "content": "package packetdumper\n\nimport (\n\t\"io\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\n// startTCPPair dials a local TCP listener and returns both ends of the connection.\nfunc startTCPPair(t *testing.T) (client, server net.Conn) {\n\tt.Helper()\n\n\tln, err := net.Listen(\"tcp\", \"127.0.0.1:0\")\n\trequire.NoError(t, err)\n\n\tserverCh := make(chan net.Conn, 1)\n\tgo func() {\n\t\tconn, err2 := ln.Accept()\n\t\tif err2 == nil {\n\t\t\tserverCh <- conn\n\t\t}\n\t}()\n\n\tclient, err = net.Dial(\"tcp\", ln.Addr().String())\n\trequire.NoError(t, err)\n\n\tt.Cleanup(func() { ln.Close() })\n\n\tselect {\n\tcase server = <-serverCh:\n\tcase <-time.After(2 * time.Second):\n\t\tt.Fatal(\"timed out waiting for server connection\")\n\t}\n\n\treturn client, server\n}\n\nfunc cleanupPcapng(t *testing.T, prefix string) {\n\tt.Helper()\n\n\tmatches, err := filepath.Glob(prefix + \"_*.pcapng\")\n\trequire.NoError(t, err, \"glob for pcapng files\")\n\trequire.NotEmpty(t, matches, \"expected at least one pcapng file to have been created\")\n\n\tfor _, f := range matches {\n\t\trequire.NoError(t, os.Remove(f), \"removing pcapng file %s\", f)\n\t}\n}\n\nfunc TestConnInitialize_CreatesFile(t *testing.T) {\n\tclient, server := startTCPPair(t)\n\tdefer server.Close()\n\n\tprefix := filepath.Join(t.TempDir(), \"capture\")\n\tc := &Conn{Prefix: prefix, Conn: client}\n\trequire.NoError(t, c.Initialize())\n\n\tdefer cleanupPcapng(t, prefix)\n\tdefer c.Close() //nolint:errcheck\n}\n\nfunc TestConnWrite(t *testing.T) {\n\tclient, server := startTCPPair(t)\n\tdefer server.Close()\n\n\tprefix := filepath.Join(t.TempDir(), \"capture\")\n\tc := &Conn{Prefix: prefix, Conn: client}\n\trequire.NoError(t, c.Initialize())\n\n\tdefer cleanupPcapng(t, prefix)\n\tdefer c.Close() //nolint:errcheck\n\n\tn, err := c.Write([]byte(\"hello world\"))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 11, n)\n\n\tbuf := make([]byte, 11)\n\t_, err = io.ReadFull(server, buf)\n\trequire.NoError(t, err)\n\trequire.Equal(t, []byte(\"hello world\"), buf)\n}\n\nfunc TestConnRead(t *testing.T) {\n\tclient, server := startTCPPair(t)\n\tdefer server.Close()\n\n\tprefix := filepath.Join(t.TempDir(), \"capture\")\n\tc := &Conn{Prefix: prefix, Conn: client}\n\trequire.NoError(t, c.Initialize())\n\n\tdefer cleanupPcapng(t, prefix)\n\tdefer c.Close() //nolint:errcheck\n\n\t_, err := server.Write([]byte(\"incoming data\"))\n\trequire.NoError(t, err)\n\n\tbuf := make([]byte, 32)\n\tn, err := c.Read(buf)\n\trequire.NoError(t, err)\n\trequire.Equal(t, []byte(\"incoming data\"), buf[:n])\n}\n\nfunc TestConnServerSide(t *testing.T) {\n\tclient, server := startTCPPair(t)\n\tdefer client.Close()\n\n\tprefix := filepath.Join(t.TempDir(), \"capture\")\n\tc := &Conn{Prefix: prefix, Conn: server, ServerSide: true}\n\trequire.NoError(t, c.Initialize())\n\n\tdefer cleanupPcapng(t, prefix)\n\tdefer c.Close() //nolint:errcheck\n\n\tn, err := c.Write([]byte(\"server response\"))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 15, n)\n\n\tbuf := make([]byte, 15)\n\t_, err = io.ReadFull(client, buf)\n\trequire.NoError(t, err)\n\trequire.Equal(t, []byte(\"server response\"), buf)\n}\n\nfunc TestConnMultipleWriteRead(t *testing.T) {\n\tclient, server := startTCPPair(t)\n\tdefer server.Close()\n\n\tprefix := filepath.Join(t.TempDir(), \"capture\")\n\tc := &Conn{Prefix: prefix, Conn: client}\n\trequire.NoError(t, c.Initialize())\n\n\tdefer cleanupPcapng(t, prefix)\n\tdefer c.Close() //nolint:errcheck\n\n\tfor _, msg := range []string{\"foo\", \"bar\", \"baz\"} {\n\t\tn, err := c.Write([]byte(msg))\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, len(msg), n)\n\t}\n\n\tbuf := make([]byte, len(\"foobarbaz\"))\n\t_, err := io.ReadFull(server, buf)\n\trequire.NoError(t, err)\n\trequire.Equal(t, []byte(\"foobarbaz\"), buf)\n\n\t_, err = server.Write([]byte(\"abcde\"))\n\trequire.NoError(t, err)\n\t_, err = server.Write([]byte(\"fghij\"))\n\trequire.NoError(t, err)\n\n\treadBuf := make([]byte, 10)\n\t_, err = io.ReadFull(c, readBuf)\n\trequire.NoError(t, err)\n\trequire.Equal(t, []byte(\"abcdefghij\"), readBuf)\n}\n\nfunc TestConnCloseIdempotent(t *testing.T) {\n\tclient, server := startTCPPair(t)\n\tdefer server.Close()\n\n\tprefix := filepath.Join(t.TempDir(), \"capture\")\n\tc := &Conn{Prefix: prefix, Conn: client}\n\trequire.NoError(t, c.Initialize())\n\n\tdefer cleanupPcapng(t, prefix)\n\n\tdefer c.Close() //nolint:errcheck\n\tdefer c.Close() //nolint:errcheck\n}\n\nfunc TestConnDelegatesAddrMethods(t *testing.T) {\n\tclient, server := startTCPPair(t)\n\tdefer server.Close()\n\n\tprefix := filepath.Join(t.TempDir(), \"capture\")\n\tc := &Conn{Prefix: prefix, Conn: client}\n\trequire.NoError(t, c.Initialize())\n\n\tdefer cleanupPcapng(t, prefix)\n\tdefer c.Close() //nolint:errcheck\n\n\trequire.Equal(t, client.LocalAddr(), c.LocalAddr())\n\trequire.Equal(t, client.RemoteAddr(), c.RemoteAddr())\n\n\trequire.NoError(t, c.SetDeadline(time.Now().Add(time.Second)))\n\trequire.NoError(t, c.SetReadDeadline(time.Now().Add(time.Second)))\n\trequire.NoError(t, c.SetWriteDeadline(time.Now().Add(time.Second)))\n}\n"
  },
  {
    "path": "internal/packetdumper/dial_context.go",
    "content": "package packetdumper\n\nimport (\n\t\"context\"\n\t\"net\"\n)\n\n// DialContext is a wrapper around net.Dialer.DialContext that dumps packets to disk.\ntype DialContext struct {\n\tPrefix      string\n\tDialContext func(ctx context.Context, network, address string) (net.Conn, error)\n}\n\n// Do mimics net.Dialer.DialContext.\nfunc (d *DialContext) Do(ctx context.Context, network, address string) (net.Conn, error) {\n\tconn, err := d.DialContext(ctx, network, address)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tc := &Conn{\n\t\tPrefix: d.Prefix,\n\t\tConn:   conn,\n\t}\n\terr = c.Initialize()\n\tif err != nil {\n\t\tconn.Close()\n\t\treturn nil, err\n\t}\n\n\treturn c, nil\n}\n"
  },
  {
    "path": "internal/packetdumper/listen.go",
    "content": "package packetdumper\n\nimport (\n\t\"net\"\n)\n\n// Listen is a wrapper around net.Listen that dumps packets to disk.\ntype Listen struct {\n\tPrefix string\n\tListen func(network, address string) (net.Listener, error)\n}\n\n// Do mimics net.Listen.\nfunc (l *Listen) Do(network, address string) (net.Listener, error) {\n\tln, err := l.Listen(network, address)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Listener{\n\t\tPrefix:   l.Prefix,\n\t\tListener: ln,\n\t}, nil\n}\n"
  },
  {
    "path": "internal/packetdumper/listen_packet.go",
    "content": "package packetdumper\n\nimport (\n\t\"net\"\n)\n\n// ListenPacket is a wrapper around net.ListenPacket that dumps packets to disk.\ntype ListenPacket struct {\n\tPrefix       string\n\tListenPacket func(network, address string) (net.PacketConn, error)\n}\n\n// Do mimics net.ListenPacket\nfunc (l *ListenPacket) Do(network, address string) (net.PacketConn, error) {\n\tpc, err := l.ListenPacket(network, address)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\td := &PacketConn{\n\t\tPrefix:     l.Prefix,\n\t\tPacketConn: pc,\n\t}\n\terr = d.Initialize()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn d, nil\n}\n"
  },
  {
    "path": "internal/packetdumper/listener.go",
    "content": "package packetdumper\n\nimport \"net\"\n\nvar _ net.Listener = (*Listener)(nil)\n\n// Listener is a wrapper around net.Listener that dumps packets to disk.\ntype Listener struct {\n\tPrefix   string\n\tListener net.Listener\n}\n\n// Accept implements net.Listener.\nfunc (l *Listener) Accept() (net.Conn, error) {\n\tconn, err := l.Listener.Accept()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcd := &Conn{\n\t\tPrefix:     l.Prefix,\n\t\tConn:       conn,\n\t\tServerSide: true,\n\t}\n\terr = cd.Initialize()\n\tif err != nil {\n\t\tconn.Close() //nolint:errcheck\n\t\treturn nil, err\n\t}\n\n\treturn cd, nil\n}\n\n// Close implements net.Listener.\nfunc (l *Listener) Close() error {\n\treturn l.Listener.Close()\n}\n\n// Addr implements net.Listener.\nfunc (l *Listener) Addr() net.Addr {\n\treturn l.Listener.Addr()\n}\n"
  },
  {
    "path": "internal/packetdumper/packet_conn.go",
    "content": "package packetdumper\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"sync\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/google/gopacket\"\n\t\"github.com/google/gopacket/layers\"\n\t\"github.com/google/gopacket/pcapgo\"\n\t\"github.com/google/uuid\"\n)\n\nvar _ net.PacketConn = (*PacketConn)(nil)\n\ntype extendedPacketConn interface {\n\tnet.PacketConn\n\tSetReadBuffer(bytes int) error\n\tSyscallConn() (syscall.RawConn, error)\n}\n\ntype packetDumpEntry struct {\n\tntp      time.Time\n\tdata     []byte\n\tsrc, dst *net.UDPAddr\n}\n\n// PacketConn is a wrapper around net.PacketConn that dumps packets to disk.\ntype PacketConn struct {\n\tPrefix     string\n\tPacketConn net.PacketConn\n\n\tf    *os.File\n\tpw   *pcapgo.NgWriter\n\tonce sync.Once\n\n\tqueue      chan packetDumpEntry\n\tterminated chan struct{}\n\tdone       chan struct{}\n}\n\n// Initialize initializes PacketConn.\nfunc (c *PacketConn) Initialize() error {\n\tvar err error\n\tc.f, err = os.Create(fmt.Sprintf(\"%s_%d_%s.pcapng\", c.Prefix, time.Now().UnixNano(), uuid.New().String()))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tc.pw, err = pcapgo.NewNgWriter(c.f, layers.LinkTypeEthernet)\n\tif err != nil {\n\t\tc.f.Close()\n\t\treturn err\n\t}\n\n\tc.queue = make(chan packetDumpEntry, 64)\n\tc.terminated = make(chan struct{})\n\tc.done = make(chan struct{})\n\n\tgo c.run()\n\n\treturn nil\n}\n\n// Close implements net.PacketConn.\nfunc (c *PacketConn) Close() error {\n\tc.once.Do(func() {\n\t\tclose(c.terminated)\n\t})\n\t<-c.done\n\treturn c.PacketConn.Close()\n}\n\nfunc (c *PacketConn) run() {\n\tdefer close(c.done)\n\tdefer c.f.Close()\n\n\tfor {\n\t\tselect {\n\t\tcase e := <-c.queue:\n\t\t\tc.writePacket(e.ntp, e.src, e.dst, e.data)\n\n\t\tcase <-c.terminated:\n\t\t\tfor {\n\t\t\t\tselect {\n\t\t\t\tcase e := <-c.queue:\n\t\t\t\t\tc.writePacket(e.ntp, e.src, e.dst, e.data)\n\t\t\t\tdefault:\n\t\t\t\t\tc.pw.Flush() //nolint:errcheck\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (c *PacketConn) writePacket(ntp time.Time, src, dst *net.UDPAddr, payload []byte) {\n\teth := &layers.Ethernet{\n\t\tSrcMAC:       net.HardwareAddr{0, 0, 0, 0, 0, 0},\n\t\tDstMAC:       net.HardwareAddr{0, 0, 0, 0, 0, 0},\n\t\tEthernetType: layers.EthernetTypeIPv6,\n\t}\n\n\tipv6 := &layers.IPv6{\n\t\tVersion:    6,\n\t\tSrcIP:      src.IP.To16(),\n\t\tDstIP:      dst.IP.To16(),\n\t\tNextHeader: layers.IPProtocolUDP,\n\t\tHopLimit:   64,\n\t}\n\n\tudp := &layers.UDP{\n\t\tSrcPort: layers.UDPPort(src.Port),\n\t\tDstPort: layers.UDPPort(dst.Port),\n\t}\n\tudp.SetNetworkLayerForChecksum(ipv6) //nolint:errcheck\n\n\tbuf := gopacket.NewSerializeBuffer()\n\topts := gopacket.SerializeOptions{FixLengths: true, ComputeChecksums: true}\n\tgopacket.SerializeLayers(buf, opts, eth, ipv6, udp, gopacket.Payload(payload)) //nolint:errcheck\n\n\traw := buf.Bytes()\n\tc.pw.WritePacket(gopacket.CaptureInfo{ //nolint:errcheck\n\t\tTimestamp:     ntp,\n\t\tCaptureLength: len(raw),\n\t\tLength:        len(raw),\n\t}, raw)\n}\n\nfunc (c *PacketConn) enqueue(e packetDumpEntry) {\n\tselect {\n\tcase c.queue <- e:\n\tcase <-c.terminated:\n\t}\n}\n\n// ReadFrom implements net.PacketConn.\nfunc (c *PacketConn) ReadFrom(p []byte) (n int, addr net.Addr, err error) {\n\tn, addr, err = c.PacketConn.ReadFrom(p)\n\n\tif n != 0 {\n\t\tlocal := c.PacketConn.LocalAddr().(*net.UDPAddr)\n\t\tremote := addr.(*net.UDPAddr)\n\n\t\tc.enqueue(packetDumpEntry{\n\t\t\tntp:  time.Now(),\n\t\t\tdata: append([]byte(nil), p[:n]...),\n\t\t\tsrc:  remote,\n\t\t\tdst:  local,\n\t\t})\n\t}\n\n\treturn n, addr, err\n}\n\n// WriteTo implements net.PacketConn.\nfunc (c *PacketConn) WriteTo(p []byte, addr net.Addr) (n int, err error) {\n\tn, err = c.PacketConn.WriteTo(p, addr)\n\n\tif err == nil {\n\t\tlocal := c.PacketConn.LocalAddr().(*net.UDPAddr)\n\t\tremote := addr.(*net.UDPAddr)\n\n\t\tc.enqueue(packetDumpEntry{\n\t\t\tntp:  time.Now(),\n\t\t\tdata: append([]byte(nil), p...),\n\t\t\tsrc:  local,\n\t\t\tdst:  remote,\n\t\t})\n\t}\n\n\treturn n, err\n}\n\n// LocalAddr implements net.PacketConn.\nfunc (c *PacketConn) LocalAddr() net.Addr { return c.PacketConn.LocalAddr() }\n\n// SetDeadline implements net.PacketConn.\nfunc (c *PacketConn) SetDeadline(t time.Time) error { return c.PacketConn.SetDeadline(t) }\n\n// SetReadDeadline implements net.PacketConn.\nfunc (c *PacketConn) SetReadDeadline(t time.Time) error { return c.PacketConn.SetReadDeadline(t) }\n\n// SetWriteDeadline implements net.PacketConn.\nfunc (c *PacketConn) SetWriteDeadline(t time.Time) error { return c.PacketConn.SetWriteDeadline(t) }\n\n// SetReadBuffer implements extendedPacketConn.\nfunc (c *PacketConn) SetReadBuffer(bytes int) error {\n\treturn c.PacketConn.(extendedPacketConn).SetReadBuffer(bytes)\n}\n\n// SyscallConn implements extendedPacketConn.\nfunc (c *PacketConn) SyscallConn() (syscall.RawConn, error) {\n\treturn c.PacketConn.(extendedPacketConn).SyscallConn()\n}\n"
  },
  {
    "path": "internal/packetdumper/packet_conn_test.go",
    "content": "package packetdumper\n\nimport (\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\n// startUDPPair creates a pair of UDP connections and returns both ends.\nfunc startUDPPair(t *testing.T) (client, server *net.UDPConn) {\n\tt.Helper()\n\n\tserverAddr, err := net.ResolveUDPAddr(\"udp\", \"127.0.0.1:0\")\n\trequire.NoError(t, err)\n\n\tserver, err = net.ListenUDP(\"udp\", serverAddr)\n\trequire.NoError(t, err)\n\n\tclientAddr, err := net.ResolveUDPAddr(\"udp\", \"127.0.0.1:0\")\n\trequire.NoError(t, err)\n\n\tclient, err = net.ListenUDP(\"udp\", clientAddr)\n\trequire.NoError(t, err)\n\n\tt.Cleanup(func() {\n\t\tserver.Close() //nolint:errcheck\n\t\tclient.Close() //nolint:errcheck\n\t})\n\n\treturn client, server\n}\n\nfunc cleanupPcapngPacket(t *testing.T, prefix string) {\n\tt.Helper()\n\n\tmatches, err := filepath.Glob(prefix + \"_*.pcapng\")\n\trequire.NoError(t, err, \"glob for pcapng files\")\n\trequire.NotEmpty(t, matches, \"expected at least one pcapng file to have been created\")\n\n\tfor _, f := range matches {\n\t\trequire.NoError(t, os.Remove(f), \"removing pcapng file %s\", f)\n\t}\n}\n\nfunc TestPacketConnInitialize_CreatesFile(t *testing.T) {\n\tclient, server := startUDPPair(t)\n\tdefer server.Close() //nolint:errcheck\n\n\tprefix := filepath.Join(t.TempDir(), \"capture\")\n\tc := &PacketConn{Prefix: prefix, PacketConn: client}\n\trequire.NoError(t, c.Initialize())\n\n\tdefer cleanupPcapngPacket(t, prefix)\n\tdefer c.Close() //nolint:errcheck\n}\n\nfunc TestPacketConnWriteTo(t *testing.T) {\n\tclient, server := startUDPPair(t)\n\tdefer server.Close() //nolint:errcheck\n\n\tprefix := filepath.Join(t.TempDir(), \"capture\")\n\tc := &PacketConn{Prefix: prefix, PacketConn: client}\n\trequire.NoError(t, c.Initialize())\n\n\tdefer cleanupPcapngPacket(t, prefix)\n\tdefer c.Close() //nolint:errcheck\n\n\tn, err := c.WriteTo([]byte(\"hello world\"), server.LocalAddr())\n\trequire.NoError(t, err)\n\trequire.Equal(t, 11, n)\n\n\tbuf := make([]byte, 32)\n\tserver.SetReadDeadline(time.Now().Add(2 * time.Second)) //nolint:errcheck\n\trn, _, err := server.ReadFromUDP(buf)\n\trequire.NoError(t, err)\n\trequire.Equal(t, []byte(\"hello world\"), buf[:rn])\n}\n\nfunc TestPacketConnReadFrom(t *testing.T) {\n\tclient, server := startUDPPair(t)\n\tdefer server.Close() //nolint:errcheck\n\n\tprefix := filepath.Join(t.TempDir(), \"capture\")\n\tc := &PacketConn{Prefix: prefix, PacketConn: client}\n\trequire.NoError(t, c.Initialize())\n\n\tdefer cleanupPcapngPacket(t, prefix)\n\tdefer c.Close() //nolint:errcheck\n\n\t_, err := server.WriteTo([]byte(\"incoming data\"), client.LocalAddr())\n\trequire.NoError(t, err)\n\n\tbuf := make([]byte, 32)\n\tc.SetReadDeadline(time.Now().Add(2 * time.Second)) //nolint:errcheck\n\tn, addr, err := c.ReadFrom(buf)\n\trequire.NoError(t, err)\n\trequire.Equal(t, []byte(\"incoming data\"), buf[:n])\n\trequire.NotNil(t, addr)\n}\n\nfunc TestPacketConnMultipleWriteRead(t *testing.T) {\n\tclient, server := startUDPPair(t)\n\tdefer server.Close() //nolint:errcheck\n\n\tprefix := filepath.Join(t.TempDir(), \"capture\")\n\tc := &PacketConn{Prefix: prefix, PacketConn: client}\n\trequire.NoError(t, c.Initialize())\n\n\tdefer cleanupPcapngPacket(t, prefix)\n\tdefer c.Close() //nolint:errcheck\n\n\tserverAddr := server.LocalAddr()\n\tfor _, msg := range []string{\"foo\", \"bar\", \"baz\"} {\n\t\tn, err := c.WriteTo([]byte(msg), serverAddr)\n\t\trequire.NoError(t, err)\n\t\trequire.Equal(t, len(msg), n)\n\t}\n\n\tserver.SetReadDeadline(time.Now().Add(2 * time.Second)) //nolint:errcheck\n\tbuf := make([]byte, 32)\n\treceived := make([]byte, 0, 9)\n\tfor range 3 {\n\t\tn, _, err := server.ReadFromUDP(buf)\n\t\trequire.NoError(t, err)\n\t\treceived = append(received, buf[:n]...)\n\t}\n\trequire.Equal(t, []byte(\"foobarbaz\"), received)\n\n\tfor _, msg := range []string{\"abcde\", \"fghij\"} {\n\t\t_, err := server.WriteTo([]byte(msg), client.LocalAddr())\n\t\trequire.NoError(t, err)\n\t}\n\n\tc.SetReadDeadline(time.Now().Add(2 * time.Second)) //nolint:errcheck\n\treadReceived := make([]byte, 0, 10)\n\tfor range 2 {\n\t\tn, _, err := c.ReadFrom(buf)\n\t\trequire.NoError(t, err)\n\t\treadReceived = append(readReceived, buf[:n]...)\n\t}\n\trequire.Equal(t, []byte(\"abcdefghij\"), readReceived)\n}\n\nfunc TestPacketConnCloseIdempotent(t *testing.T) {\n\tclient, server := startUDPPair(t)\n\tdefer server.Close() //nolint:errcheck\n\n\tprefix := filepath.Join(t.TempDir(), \"capture\")\n\tc := &PacketConn{Prefix: prefix, PacketConn: client}\n\trequire.NoError(t, c.Initialize())\n\n\tdefer cleanupPcapngPacket(t, prefix)\n\n\tdefer c.Close() //nolint:errcheck\n\tdefer c.Close() //nolint:errcheck\n}\n\nfunc TestPacketConnDelegatesAddrMethods(t *testing.T) {\n\tclient, server := startUDPPair(t)\n\tdefer server.Close() //nolint:errcheck\n\n\tprefix := filepath.Join(t.TempDir(), \"capture\")\n\tc := &PacketConn{Prefix: prefix, PacketConn: client}\n\trequire.NoError(t, c.Initialize())\n\n\tdefer cleanupPcapngPacket(t, prefix)\n\tdefer c.Close() //nolint:errcheck\n\n\trequire.Equal(t, client.LocalAddr(), c.LocalAddr())\n\n\trequire.NoError(t, c.SetDeadline(time.Now().Add(time.Second)))\n\trequire.NoError(t, c.SetReadDeadline(time.Now().Add(time.Second)))\n\trequire.NoError(t, c.SetWriteDeadline(time.Now().Add(time.Second)))\n}\n\nfunc TestPacketConnReadFromRecordsSource(t *testing.T) {\n\tclient, server := startUDPPair(t)\n\tdefer server.Close() //nolint:errcheck\n\n\tprefix := filepath.Join(t.TempDir(), \"capture\")\n\tc := &PacketConn{Prefix: prefix, PacketConn: client}\n\trequire.NoError(t, c.Initialize())\n\n\tdefer cleanupPcapngPacket(t, prefix)\n\tdefer c.Close() //nolint:errcheck\n\n\t_, err := server.WriteTo([]byte(\"ping\"), client.LocalAddr())\n\trequire.NoError(t, err)\n\n\tbuf := make([]byte, 32)\n\tc.SetReadDeadline(time.Now().Add(2 * time.Second)) //nolint:errcheck\n\tn, addr, err := c.ReadFrom(buf)\n\trequire.NoError(t, err)\n\trequire.Equal(t, []byte(\"ping\"), buf[:n])\n\n\t// The reported source address should match the server's address.\n\trequire.Equal(t, server.LocalAddr().String(), addr.String())\n}\n"
  },
  {
    "path": "internal/playback/muxer.go",
    "content": "package playback\n\nimport \"github.com/bluenviron/mediacommon/v2/pkg/formats/fmp4\"\n\ntype muxer interface {\n\twriteInit(init *fmp4.Init)\n\tsetTrack(trackID int)\n\twriteSample(\n\t\tdts int64,\n\t\tptsOffset int32,\n\t\tisNonSyncSample bool,\n\t\tpayloadSize uint32,\n\t\tgetPayload func() ([]byte, error),\n\t) error\n\twriteFinalDTS(dts int64)\n\tflush() error\n}\n"
  },
  {
    "path": "internal/playback/muxer_fmp4.go",
    "content": "package playback\n\nimport (\n\t\"io\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/fmp4\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/fmp4/seekablebuffer\"\n\t\"github.com/bluenviron/mediamtx/internal/recordstore\"\n)\n\nconst (\n\tpartDuration = 1 * time.Second\n)\n\ntype muxerFMP4Track struct {\n\tid        int\n\ttimeScale uint32\n\tfirstDTS  int64\n\tlastDTS   int64\n\tsamples   []*fmp4.Sample\n}\n\nfunc findTrack(tracks []*muxerFMP4Track, id int) *muxerFMP4Track {\n\tfor _, track := range tracks {\n\t\tif track.id == id {\n\t\t\treturn track\n\t\t}\n\t}\n\treturn nil\n}\n\ntype muxerFMP4 struct {\n\tw io.Writer\n\n\tinit               *fmp4.Init\n\tnextSequenceNumber uint32\n\ttracks             []*muxerFMP4Track\n\tcurTrack           *muxerFMP4Track\n\toutBuf             seekablebuffer.Buffer\n}\n\nfunc (w *muxerFMP4) writeInit(init *fmp4.Init) {\n\tw.init = init\n\n\tw.tracks = make([]*muxerFMP4Track, len(init.Tracks))\n\n\tfor i, track := range init.Tracks {\n\t\tw.tracks[i] = &muxerFMP4Track{\n\t\t\tid:        track.ID,\n\t\t\ttimeScale: track.TimeScale,\n\t\t\tfirstDTS:  -1,\n\t\t}\n\t}\n}\n\nfunc (w *muxerFMP4) setTrack(trackID int) {\n\tw.curTrack = findTrack(w.tracks, trackID)\n}\n\nfunc (w *muxerFMP4) writeSample(\n\tdts int64,\n\tptsOffset int32,\n\tisNonSyncSample bool,\n\t_ uint32,\n\tgetPayload func() ([]byte, error),\n) error {\n\tpl, err := getPayload()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif dts >= 0 {\n\t\t// this is the first visible sample of this track\n\t\tif w.curTrack.firstDTS < 0 {\n\t\t\tw.curTrack.firstDTS = dts\n\n\t\t\t// if sample is a IDR, remove previous GOP\n\t\t\tif !isNonSyncSample {\n\t\t\t\tw.curTrack.samples = w.curTrack.samples[:0]\n\t\t\t}\n\t\t} else {\n\t\t\tduration := max(dts-w.curTrack.lastDTS, 0)\n\t\t\tw.curTrack.samples[len(w.curTrack.samples)-1].Duration = uint32(duration)\n\t\t}\n\n\t\tw.curTrack.samples = append(w.curTrack.samples, &fmp4.Sample{\n\t\t\tPTSOffset:       ptsOffset,\n\t\t\tIsNonSyncSample: isNonSyncSample,\n\t\t\tPayload:         pl,\n\t\t})\n\t\tw.curTrack.lastDTS = dts\n\n\t\tpartDurationMP4 := durationGoToMp4(partDuration, w.curTrack.timeScale)\n\n\t\tif (w.curTrack.lastDTS - w.curTrack.firstDTS) >= partDurationMP4 {\n\t\t\terr = w.innerFlush(false)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t} else {\n\t\tif !isNonSyncSample { // sample is IDR\n\t\t\t// create a new GOP that starts from this sample.\n\t\t\t// set sample duration to zero\n\t\t\tw.curTrack.samples = w.curTrack.samples[:0]\n\t\t\tw.curTrack.samples = append(w.curTrack.samples, &fmp4.Sample{\n\t\t\t\tIsNonSyncSample: isNonSyncSample,\n\t\t\t\tPayload:         pl,\n\t\t\t\tPTSOffset:       ptsOffset,\n\t\t\t})\n\t\t} else { // sample is not IDR\n\t\t\t// append sample to current GOP\n\t\t\t// set sample duration to zero\n\t\t\tw.curTrack.samples = append(w.curTrack.samples, &fmp4.Sample{\n\t\t\t\tIsNonSyncSample: isNonSyncSample,\n\t\t\t\tPayload:         pl,\n\t\t\t\tPTSOffset:       ptsOffset,\n\t\t\t})\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (w *muxerFMP4) writeFinalDTS(dts int64) {\n\tif len(w.curTrack.samples) != 0 && w.curTrack.firstDTS >= 0 {\n\t\tduration := max(dts-w.curTrack.lastDTS, 0)\n\t\tw.curTrack.samples[len(w.curTrack.samples)-1].Duration = uint32(duration)\n\t}\n}\n\nfunc (w *muxerFMP4) innerFlush(final bool) error {\n\tvar part fmp4.Part\n\n\tfor _, track := range w.tracks {\n\t\tif track.firstDTS >= 0 && (len(track.samples) > 1 || (final && len(track.samples) != 0)) {\n\t\t\t// do not write the final sample\n\t\t\t// in order to allow changing its duration to compensate NTP-DTS differences\n\t\t\tvar samples []*fmp4.Sample\n\t\t\tif !final {\n\t\t\t\tsamples = track.samples[:len(track.samples)-1]\n\t\t\t} else {\n\t\t\t\tsamples = track.samples\n\t\t\t}\n\n\t\t\tpart.Tracks = append(part.Tracks, &fmp4.PartTrack{\n\t\t\t\tID:       track.id,\n\t\t\t\tBaseTime: uint64(track.firstDTS),\n\t\t\t\tSamples:  samples,\n\t\t\t})\n\n\t\t\tif !final {\n\t\t\t\ttrack.samples = track.samples[len(track.samples)-1:]\n\t\t\t\ttrack.firstDTS = track.lastDTS\n\t\t\t}\n\t\t}\n\t}\n\n\t// no samples to write\n\tif part.Tracks == nil {\n\t\t// if no samples has been written before, return an error\n\t\tif w.init != nil {\n\t\t\treturn recordstore.ErrNoSegmentsFound\n\t\t}\n\t\treturn nil\n\t}\n\n\tpart.SequenceNumber = w.nextSequenceNumber\n\tw.nextSequenceNumber++\n\n\tif w.init != nil {\n\t\terr := w.init.Marshal(&w.outBuf)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t_, err = w.w.Write(w.outBuf.Bytes())\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tw.init = nil\n\t\tw.outBuf.Reset()\n\t}\n\n\terr := part.Marshal(&w.outBuf)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = w.w.Write(w.outBuf.Bytes())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tw.outBuf.Reset()\n\n\treturn nil\n}\n\nfunc (w *muxerFMP4) flush() error {\n\treturn w.innerFlush(true)\n}\n"
  },
  {
    "path": "internal/playback/muxer_mp4.go",
    "content": "package playback\n\nimport (\n\t\"io\"\n\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/fmp4\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/pmp4\"\n\t\"github.com/bluenviron/mediamtx/internal/recordstore\"\n)\n\ntype muxerMP4Track struct {\n\tpmp4.Track\n\tlastDTS int64\n}\n\nfunc findTrackMP4(tracks []*muxerMP4Track, id int) *muxerMP4Track {\n\tfor _, track := range tracks {\n\t\tif track.ID == id {\n\t\t\treturn track\n\t\t}\n\t}\n\treturn nil\n}\n\ntype muxerMP4 struct {\n\tw io.Writer\n\n\ttracks   []*muxerMP4Track\n\tcurTrack *muxerMP4Track\n}\n\nfunc (w *muxerMP4) writeInit(init *fmp4.Init) {\n\tw.tracks = make([]*muxerMP4Track, len(init.Tracks))\n\n\tfor i, track := range init.Tracks {\n\t\tw.tracks[i] = &muxerMP4Track{\n\t\t\tTrack: pmp4.Track{\n\t\t\t\tID:        track.ID,\n\t\t\t\tTimeScale: track.TimeScale,\n\t\t\t\tCodec:     track.Codec,\n\t\t\t},\n\t\t}\n\t}\n}\n\nfunc (w *muxerMP4) setTrack(trackID int) {\n\tw.curTrack = findTrackMP4(w.tracks, trackID)\n}\n\nfunc (w *muxerMP4) writeSample(\n\tdts int64,\n\tptsOffset int32,\n\tisNonSyncSample bool,\n\tpayloadSize uint32,\n\tgetPayload func() ([]byte, error),\n) error {\n\t// remove GOPs before the GOP of the first sample\n\tif (dts < 0 || (dts >= 0 && w.curTrack.lastDTS < 0)) && !isNonSyncSample {\n\t\tw.curTrack.Samples = w.curTrack.Samples[:0]\n\t}\n\n\tif len(w.curTrack.Samples) == 0 {\n\t\tw.curTrack.TimeOffset = int32(dts)\n\t} else {\n\t\tduration := max(dts-w.curTrack.lastDTS, 0)\n\t\tw.curTrack.Samples[len(w.curTrack.Samples)-1].Duration = uint32(duration)\n\t}\n\n\t// prevent warning \"edit list: 1 Missing key frame while searching for timestamp: 0\"\n\tif !isNonSyncSample {\n\t\tptsOffset = 0\n\t}\n\n\tw.curTrack.Samples = append(w.curTrack.Samples, &pmp4.Sample{\n\t\tPTSOffset:       ptsOffset,\n\t\tIsNonSyncSample: isNonSyncSample,\n\t\tPayloadSize:     payloadSize,\n\t\tGetPayload:      getPayload,\n\t})\n\tw.curTrack.lastDTS = dts\n\n\treturn nil\n}\n\nfunc (w *muxerMP4) writeFinalDTS(dts int64) {\n\tif len(w.curTrack.Samples) != 0 {\n\t\tduration := max(dts-w.curTrack.lastDTS, 0)\n\t\tw.curTrack.Samples[len(w.curTrack.Samples)-1].Duration = uint32(duration)\n\t}\n}\n\nfunc (w *muxerMP4) flush() error {\n\tif len(w.curTrack.Samples) == 0 || w.curTrack.lastDTS < 0 {\n\t\treturn recordstore.ErrNoSegmentsFound\n\t}\n\n\tvar tracks []*pmp4.Track\n\tfor _, track := range w.tracks {\n\t\tif len(track.Samples) != 0 {\n\t\t\ttracks = append(tracks, &track.Track)\n\t\t}\n\t}\n\n\th := pmp4.Presentation{\n\t\tTracks: tracks,\n\t}\n\n\treturn h.Marshal(w.w)\n}\n"
  },
  {
    "path": "internal/playback/muxer_mp4_test.go",
    "content": "package playback\n\nimport (\n\t\"bytes\"\n\t\"testing\"\n\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/fmp4\"\n\tmcodecs \"github.com/bluenviron/mediacommon/v2/pkg/formats/mp4/codecs\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/stretchr/testify/require\"\n)\n\n// Test that tracks with no samples are not included in the output\nfunc TestMuxerMP4EmptyTracks(t *testing.T) {\n\tvar buf bytes.Buffer\n\n\tmux := &muxerMP4{\n\t\tw: &buf,\n\t}\n\n\tinit := &fmp4.Init{\n\t\tTracks: []*fmp4.InitTrack{\n\t\t\t{\n\t\t\t\tID:        1,\n\t\t\t\tTimeScale: 90000,\n\t\t\t\tCodec: &mcodecs.H264{\n\t\t\t\t\tSPS: test.FormatH264.SPS,\n\t\t\t\t\tPPS: test.FormatH264.PPS,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tID:        2,\n\t\t\t\tTimeScale: 48000,\n\t\t\t\tCodec:     &mcodecs.Opus{},\n\t\t\t},\n\t\t},\n\t}\n\n\tmux.writeInit(init)\n\n\t// Only write samples to the first track\n\tmux.setTrack(1)\n\terr := mux.writeSample(0, 0, false, 5, func() ([]byte, error) {\n\t\treturn []byte{0x01, 0x02, 0x03, 0x04, 0x05}, nil\n\t})\n\trequire.NoError(t, err)\n\n\terr = mux.writeSample(90000, 0, true, 5, func() ([]byte, error) {\n\t\treturn []byte{0x06, 0x07, 0x08, 0x09, 0x0a}, nil\n\t})\n\trequire.NoError(t, err)\n\n\tmux.writeFinalDTS(180000)\n\n\terr = mux.flush()\n\trequire.NoError(t, err)\n\n\trequire.Greater(t, buf.Len(), 0)\n}\n"
  },
  {
    "path": "internal/playback/on_get.go",
    "content": "package playback\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/fmp4\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/recordstore\"\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype writerWrapper struct {\n\tctx     *gin.Context\n\twritten bool\n}\n\nfunc (w *writerWrapper) Write(p []byte) (int, error) {\n\tif !w.written {\n\t\tw.written = true\n\t\tw.ctx.Header(\"Accept-Ranges\", \"none\")\n\t\tw.ctx.Header(\"Content-Type\", \"video/mp4\")\n\t}\n\treturn w.ctx.Writer.Write(p)\n}\n\nfunc parseDuration(raw string) (time.Duration, error) {\n\t// seconds\n\tif secs, err := strconv.ParseFloat(raw, 64); err == nil {\n\t\treturn time.Duration(secs * float64(time.Second)), nil\n\t}\n\n\t// deprecated, golang format\n\treturn time.ParseDuration(raw)\n}\n\nfunc seekAndMux(\n\trecordFormat conf.RecordFormat,\n\tsegments []*recordstore.Segment,\n\tstart time.Time,\n\tduration time.Duration,\n\tm muxer,\n) error {\n\tif recordFormat == conf.RecordFormatFMP4 {\n\t\tf, err := os.Open(segments[0].Fpath)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tdefer f.Close()\n\n\t\tfirstInit, _, err := segmentFMP4ReadHeader(f)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tm.writeInit(&fmp4.Init{\n\t\t\tTracks: firstInit.Tracks,\n\t\t})\n\n\t\tfirstMtxi := findMtxi(firstInit.UserData)\n\t\tstartOffset := segments[0].Start.Sub(start) // this is negative\n\t\tdts := startOffset\n\t\tprevInit := firstInit\n\n\t\tsegmentDuration, err := segmentFMP4MuxParts(f, dts, duration, firstInit.Tracks, m)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tsegmentEnd := segments[0].Start.Add(segmentDuration)\n\n\t\tfor _, seg := range segments[1:] {\n\t\t\tf, err = os.Open(seg.Fpath)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tdefer f.Close()\n\n\t\t\tvar init *fmp4.Init\n\t\t\tinit, _, err = segmentFMP4ReadHeader(f)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif !segmentFMP4CanBeConcatenated(prevInit, segmentEnd, init, seg.Start) {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tif firstMtxi != nil {\n\t\t\t\tmtxi := findMtxi(init.UserData)\n\t\t\t\tdts = time.Duration(mtxi.DTS-firstMtxi.DTS) + startOffset\n\t\t\t} else { // legacy method\n\t\t\t\tdts = seg.Start.Sub(start) // this is positive\n\t\t\t}\n\n\t\t\tsegmentDuration, err = segmentFMP4MuxParts(f, dts, duration, firstInit.Tracks, m)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tsegmentEnd = seg.Start.Add(segmentDuration)\n\t\t\tprevInit = init\n\t\t}\n\n\t\terr = m.flush()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\treturn nil\n\t}\n\n\treturn fmt.Errorf(\"MPEG-TS format is not supported yet\")\n}\n\nfunc (s *Server) onGet(ctx *gin.Context) {\n\tpathName := ctx.Query(\"path\")\n\n\tif !s.doAuth(ctx, pathName) {\n\t\treturn\n\t}\n\n\tstart, err := time.Parse(time.RFC3339, ctx.Query(\"start\"))\n\tif err != nil {\n\t\ts.writeError(ctx, http.StatusBadRequest, fmt.Errorf(\"invalid start: %w\", err))\n\t\treturn\n\t}\n\n\tduration, err := parseDuration(ctx.Query(\"duration\"))\n\tif err != nil {\n\t\ts.writeError(ctx, http.StatusBadRequest, fmt.Errorf(\"invalid duration: %w\", err))\n\t\treturn\n\t}\n\n\tww := &writerWrapper{ctx: ctx}\n\tvar m muxer\n\n\tformat := ctx.Query(\"format\")\n\tswitch format {\n\tcase \"\", \"fmp4\":\n\t\tm = &muxerFMP4{w: ww}\n\n\tcase \"mp4\":\n\t\tm = &muxerMP4{w: ww}\n\n\tdefault:\n\t\ts.writeError(ctx, http.StatusBadRequest, fmt.Errorf(\"invalid format: %s\", format))\n\t\treturn\n\t}\n\n\tpathConf, err := s.safeFindPathConf(pathName)\n\tif err != nil {\n\t\ts.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\tend := start.Add(duration)\n\tsegments, err := recordstore.FindSegments(pathConf, pathName, &start, &end)\n\tif err != nil {\n\t\tif errors.Is(err, recordstore.ErrNoSegmentsFound) {\n\t\t\ts.writeError(ctx, http.StatusNotFound, err)\n\t\t} else {\n\t\t\ts.writeError(ctx, http.StatusBadRequest, err)\n\t\t}\n\t\treturn\n\t}\n\n\terr = seekAndMux(pathConf.RecordFormat, segments, start, duration, m)\n\tif err != nil {\n\t\t// user aborted the download\n\t\tvar neterr *net.OpError\n\t\tif errors.As(err, &neterr) {\n\t\t\treturn\n\t\t}\n\n\t\t// nothing has been written yet; send back JSON\n\t\tif !ww.written {\n\t\t\tif errors.Is(err, recordstore.ErrNoSegmentsFound) {\n\t\t\t\ts.writeError(ctx, http.StatusNotFound, err)\n\t\t\t} else {\n\t\t\t\ts.writeError(ctx, http.StatusBadRequest, err)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\t// something has already been written: abort and write logs only\n\t\ts.Log(logger.Error, err.Error())\n\t\treturn\n\t}\n}\n"
  },
  {
    "path": "internal/playback/on_get_test.go",
    "content": "package playback\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\tamp4 \"github.com/abema/go-mp4\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/fmp4\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/fmp4/seekablebuffer\"\n\tmcodecs \"github.com/bluenviron/mediacommon/v2/pkg/formats/mp4/codecs\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/pmp4\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/recordstore\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/google/uuid\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc writeSegment1(t *testing.T, fpath string) {\n\tinit := fmp4.Init{\n\t\tTracks: []*fmp4.InitTrack{\n\t\t\t{\n\t\t\t\tID:        1,\n\t\t\t\tTimeScale: 90000,\n\t\t\t\tCodec: &mcodecs.H264{\n\t\t\t\t\tSPS: test.FormatH264.SPS,\n\t\t\t\t\tPPS: test.FormatH264.PPS,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tID:        2,\n\t\t\t\tTimeScale: 48000,\n\t\t\t\tCodec: &mcodecs.MPEG4Audio{\n\t\t\t\t\tConfig: mpeg4audio.AudioSpecificConfig{\n\t\t\t\t\t\tType:         mpeg4audio.ObjectTypeAACLC,\n\t\t\t\t\t\tSampleRate:   48000,\n\t\t\t\t\t\tChannelCount: 2,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tvar buf1 seekablebuffer.Buffer\n\terr := init.Marshal(&buf1)\n\trequire.NoError(t, err)\n\n\tvar buf2 seekablebuffer.Buffer\n\tparts := fmp4.Parts{\n\t\t{\n\t\t\tTracks: []*fmp4.PartTrack{\n\t\t\t\t{\n\t\t\t\t\tID:       1,\n\t\t\t\t\tBaseTime: 30 * 90000,\n\t\t\t\t\tSamples: []*fmp4.Sample{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tDuration: 30 * 90000,\n\t\t\t\t\t\t\tPayload:  []byte{1, 2},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tDuration:  1 * 90000,\n\t\t\t\t\t\t\tPTSOffset: 90000,\n\t\t\t\t\t\t\tPayload:   []byte{3, 4},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tDuration:        1 * 90000,\n\t\t\t\t\t\t\tPTSOffset:       -90000,\n\t\t\t\t\t\t\tIsNonSyncSample: true,\n\t\t\t\t\t\t\tPayload:         []byte{5, 6},\n\t\t\t\t\t\t}, // 62 secs\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tID:       2,\n\t\t\t\t\tBaseTime: 31 * 48000,\n\t\t\t\t\tSamples: []*fmp4.Sample{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tDuration: 29 * 48000,\n\t\t\t\t\t\t\tPayload:  []byte{1, 2},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tDuration: 1 * 48000,\n\t\t\t\t\t\t\tPayload:  []byte{3, 4},\n\t\t\t\t\t\t}, // 61 secs\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\terr = parts.Marshal(&buf2)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(fpath, append(buf1.Bytes(), buf2.Bytes()...), 0o644)\n\trequire.NoError(t, err)\n}\n\nfunc writeSegment2(t *testing.T, fpath string) {\n\tinit := fmp4.Init{\n\t\tTracks: []*fmp4.InitTrack{\n\t\t\t{\n\t\t\t\tID:        1,\n\t\t\t\tTimeScale: 90000,\n\t\t\t\tCodec: &mcodecs.H264{\n\t\t\t\t\tSPS: test.FormatH264.SPS,\n\t\t\t\t\tPPS: test.FormatH264.PPS,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tID:        2,\n\t\t\t\tTimeScale: 48000,\n\t\t\t\tCodec: &mcodecs.MPEG4Audio{\n\t\t\t\t\tConfig: mpeg4audio.AudioSpecificConfig{\n\t\t\t\t\t\tType:         mpeg4audio.ObjectTypeAACLC,\n\t\t\t\t\t\tSampleRate:   48000,\n\t\t\t\t\t\tChannelCount: 2,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tvar buf1 seekablebuffer.Buffer\n\terr := init.Marshal(&buf1)\n\trequire.NoError(t, err)\n\n\tvar buf2 seekablebuffer.Buffer\n\tparts := fmp4.Parts{ //nolint:dupl\n\t\t{\n\t\t\tTracks: []*fmp4.PartTrack{{\n\t\t\t\tID:       1,\n\t\t\t\tBaseTime: 0,\n\t\t\t\tSamples: []*fmp4.Sample{\n\t\t\t\t\t{\n\t\t\t\t\t\tDuration: 1 * 90000,\n\t\t\t\t\t\tPayload:  []byte{7, 8},\n\t\t\t\t\t}, // 1 sec\n\t\t\t\t},\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tTracks: []*fmp4.PartTrack{{\n\t\t\t\tID:       2,\n\t\t\t\tBaseTime: 0,\n\t\t\t\tSamples: []*fmp4.Sample{\n\t\t\t\t\t{\n\t\t\t\t\t\tDuration: 1 * 48000,\n\t\t\t\t\t\tPayload:  []byte{5, 6},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tDuration: 1 * 48000,\n\t\t\t\t\t\tPayload:  []byte{7, 8},\n\t\t\t\t\t}, // 2 secs\n\t\t\t\t},\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tSequenceNumber: 5,\n\t\t\tTracks: []*fmp4.PartTrack{{\n\t\t\t\tID:       1,\n\t\t\t\tBaseTime: 1 * 90000,\n\t\t\t\tSamples: []*fmp4.Sample{\n\t\t\t\t\t{\n\t\t\t\t\t\tDuration: 1 * 90000,\n\t\t\t\t\t\tPayload:  []byte{9, 10},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tDuration: 1 * 90000,\n\t\t\t\t\t\tPayload:  []byte{11, 12},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tDuration: 1 * 90000,\n\t\t\t\t\t\tPayload:  []byte{13, 14},\n\t\t\t\t\t}, // 4 secs\n\t\t\t\t},\n\t\t\t}},\n\t\t},\n\t}\n\terr = parts.Marshal(&buf2)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(fpath, append(buf1.Bytes(), buf2.Bytes()...), 0o644)\n\trequire.NoError(t, err)\n}\n\nfunc writeSegment3(t *testing.T, fpath string) {\n\tinit := fmp4.Init{\n\t\tTracks: []*fmp4.InitTrack{\n\t\t\t{\n\t\t\t\tID:        1,\n\t\t\t\tTimeScale: 90000,\n\t\t\t\tCodec: &mcodecs.H264{\n\t\t\t\t\tSPS: test.FormatH264.SPS,\n\t\t\t\t\tPPS: test.FormatH264.PPS,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tvar buf1 seekablebuffer.Buffer\n\terr := init.Marshal(&buf1)\n\trequire.NoError(t, err)\n\n\tvar buf2 seekablebuffer.Buffer\n\tparts := fmp4.Parts{\n\t\t{\n\t\t\tTracks: []*fmp4.PartTrack{{\n\t\t\t\tID:       1,\n\t\t\t\tBaseTime: 0,\n\t\t\t\tSamples: []*fmp4.Sample{\n\t\t\t\t\t{\n\t\t\t\t\t\tDuration: 1 * 90000,\n\t\t\t\t\t\tPayload:  []byte{13, 14},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}},\n\t\t},\n\t}\n\terr = parts.Marshal(&buf2)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(fpath, append(buf1.Bytes(), buf2.Bytes()...), 0o644)\n\trequire.NoError(t, err)\n}\n\nfunc TestOnGet(t *testing.T) {\n\tfor _, format := range []string{\n\t\t\"fmp4\",\n\t\t\"mp4\",\n\t} {\n\t\tfor _, mode := range []string{\n\t\t\t\"no_mtxi\",\n\t\t\t\"mtxi\",\n\t\t} {\n\t\t\tt.Run(format+\"_\"+mode, func(t *testing.T) {\n\t\t\t\tdir, err := os.MkdirTemp(\"\", \"mediamtx-playback\")\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer os.RemoveAll(dir)\n\n\t\t\t\terr = os.Mkdir(filepath.Join(dir, \"mypath\"), 0o755)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tinit := fmp4.Init{\n\t\t\t\t\tTracks: []*fmp4.InitTrack{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        1,\n\t\t\t\t\t\t\tTimeScale: 90000,\n\t\t\t\t\t\t\tCodec: &mcodecs.H264{\n\t\t\t\t\t\t\t\tSPS: test.FormatH264.SPS,\n\t\t\t\t\t\t\t\tPPS: test.FormatH264.PPS,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        2,\n\t\t\t\t\t\t\tTimeScale: 48000,\n\t\t\t\t\t\t\tCodec: &mcodecs.MPEG4Audio{\n\t\t\t\t\t\t\t\tConfig: mpeg4audio.AudioSpecificConfig{\n\t\t\t\t\t\t\t\t\tType:         mpeg4audio.ObjectTypeAACLC,\n\t\t\t\t\t\t\t\t\tSampleRate:   48000,\n\t\t\t\t\t\t\t\t\tChannelCount: 2,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}\n\n\t\t\t\t// segment 1\n\t\t\t\tfunc() {\n\t\t\t\t\tif mode == \"mtxi\" {\n\t\t\t\t\t\tinit.UserData = []amp4.IBox{&recordstore.Mtxi{\n\t\t\t\t\t\t\tStreamID:      uuid.MustParse(\"31564107-9e7e-4923-bf2f-631371a35397\"),\n\t\t\t\t\t\t\tSegmentNumber: 4,\n\t\t\t\t\t\t\tDTS:           int64(11 * time.Second),\n\t\t\t\t\t\t}}\n\t\t\t\t\t}\n\n\t\t\t\t\tvar buf1 seekablebuffer.Buffer\n\t\t\t\t\terr = init.Marshal(&buf1)\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\tvar buf2 seekablebuffer.Buffer\n\t\t\t\t\tparts := fmp4.Parts{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tTracks: []*fmp4.PartTrack{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tID:       1,\n\t\t\t\t\t\t\t\t\tBaseTime: 30 * 90000,\n\t\t\t\t\t\t\t\t\tSamples: []*fmp4.Sample{\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\tDuration: 30 * 90000,\n\t\t\t\t\t\t\t\t\t\t\tPayload:  []byte{1, 2},\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\tDuration:  1 * 90000,\n\t\t\t\t\t\t\t\t\t\t\tPTSOffset: 90000,\n\t\t\t\t\t\t\t\t\t\t\tPayload:   []byte{3, 4},\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\tDuration:        1 * 90000,\n\t\t\t\t\t\t\t\t\t\t\tPTSOffset:       -90000,\n\t\t\t\t\t\t\t\t\t\t\tIsNonSyncSample: true,\n\t\t\t\t\t\t\t\t\t\t\tPayload:         []byte{5, 6},\n\t\t\t\t\t\t\t\t\t\t}, // 62 secs\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tID:       2,\n\t\t\t\t\t\t\t\t\tBaseTime: 31 * 48000,\n\t\t\t\t\t\t\t\t\tSamples: []*fmp4.Sample{\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\tDuration: 29 * 48000,\n\t\t\t\t\t\t\t\t\t\t\tPayload:  []byte{1, 2},\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\tDuration: 1 * 48000,\n\t\t\t\t\t\t\t\t\t\t\tPayload:  []byte{3, 4},\n\t\t\t\t\t\t\t\t\t\t}, // 61 secs\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t\terr = parts.Marshal(&buf2)\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\terr = os.WriteFile(filepath.Join(dir, \"mypath\", \"2008-11-07_11-22-00-500000.mp4\"),\n\t\t\t\t\t\tappend(buf1.Bytes(), buf2.Bytes()...), 0o644)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t}()\n\n\t\t\t\t// segment 2\n\t\t\t\tfunc() { //nolint:dupl\n\t\t\t\t\tif mode == \"mtxi\" {\n\t\t\t\t\t\tinit.UserData = []amp4.IBox{&recordstore.Mtxi{\n\t\t\t\t\t\t\tStreamID:      uuid.MustParse(\"31564107-9e7e-4923-bf2f-631371a35397\"),\n\t\t\t\t\t\t\tSegmentNumber: 5,\n\t\t\t\t\t\t\tDTS:           int64(73 * time.Second),\n\t\t\t\t\t\t}}\n\t\t\t\t\t}\n\n\t\t\t\t\tvar buf1 seekablebuffer.Buffer\n\t\t\t\t\terr = init.Marshal(&buf1)\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\tvar buf2 seekablebuffer.Buffer\n\t\t\t\t\tparts := fmp4.Parts{ //nolint:dupl\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tTracks: []*fmp4.PartTrack{{\n\t\t\t\t\t\t\t\tID:       1,\n\t\t\t\t\t\t\t\tBaseTime: 0,\n\t\t\t\t\t\t\t\tSamples: []*fmp4.Sample{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tDuration: 1 * 90000,\n\t\t\t\t\t\t\t\t\t\tPayload:  []byte{7, 8},\n\t\t\t\t\t\t\t\t\t}, // 1 sec\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tTracks: []*fmp4.PartTrack{{\n\t\t\t\t\t\t\t\tID:       2,\n\t\t\t\t\t\t\t\tBaseTime: 0,\n\t\t\t\t\t\t\t\tSamples: []*fmp4.Sample{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tDuration: 1 * 48000,\n\t\t\t\t\t\t\t\t\t\tPayload:  []byte{5, 6},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tDuration: 1 * 48000,\n\t\t\t\t\t\t\t\t\t\tPayload:  []byte{7, 8},\n\t\t\t\t\t\t\t\t\t}, // 2 secs\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tSequenceNumber: 5,\n\t\t\t\t\t\t\tTracks: []*fmp4.PartTrack{{\n\t\t\t\t\t\t\t\tID:       1,\n\t\t\t\t\t\t\t\tBaseTime: 1 * 90000,\n\t\t\t\t\t\t\t\tSamples: []*fmp4.Sample{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tDuration: 1 * 90000,\n\t\t\t\t\t\t\t\t\t\tPayload:  []byte{9, 10},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tDuration: 1 * 90000,\n\t\t\t\t\t\t\t\t\t\tPayload:  []byte{11, 12},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tDuration: 1 * 90000,\n\t\t\t\t\t\t\t\t\t\tPayload:  []byte{13, 14},\n\t\t\t\t\t\t\t\t\t}, // 4 secs\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}},\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t\terr = parts.Marshal(&buf2)\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\terr = os.WriteFile(filepath.Join(dir, \"mypath\", \"2008-11-07_11-23-02-500000.mp4\"),\n\t\t\t\t\t\tappend(buf1.Bytes(), buf2.Bytes()...), 0o644)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t}()\n\n\t\t\t\t// segment 3\n\t\t\t\tfunc() { //nolint:dupl\n\t\t\t\t\tif mode == \"mtxi\" {\n\t\t\t\t\t\tinit.UserData = []amp4.IBox{&recordstore.Mtxi{\n\t\t\t\t\t\t\tStreamID:      uuid.MustParse(\"31564107-9e7e-4923-bf2f-631371a35397\"),\n\t\t\t\t\t\t\tSegmentNumber: 6,\n\t\t\t\t\t\t\tDTS:           int64(75 * time.Second),\n\t\t\t\t\t\t}}\n\t\t\t\t\t}\n\n\t\t\t\t\tvar buf1 seekablebuffer.Buffer\n\t\t\t\t\terr = init.Marshal(&buf1)\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\tvar buf2 seekablebuffer.Buffer\n\t\t\t\t\tparts := fmp4.Parts{ //nolint:dupl\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tTracks: []*fmp4.PartTrack{{\n\t\t\t\t\t\t\t\tID:       1,\n\t\t\t\t\t\t\t\tBaseTime: 0,\n\t\t\t\t\t\t\t\tSamples: []*fmp4.Sample{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tDuration: 1 * 90000,\n\t\t\t\t\t\t\t\t\t\tPayload:  []byte{7, 8},\n\t\t\t\t\t\t\t\t\t}, // 1 sec\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tTracks: []*fmp4.PartTrack{{\n\t\t\t\t\t\t\t\tID:       2,\n\t\t\t\t\t\t\t\tBaseTime: 0,\n\t\t\t\t\t\t\t\tSamples: []*fmp4.Sample{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tDuration: 1 * 48000,\n\t\t\t\t\t\t\t\t\t\tPayload:  []byte{5, 6},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tDuration: 1 * 48000,\n\t\t\t\t\t\t\t\t\t\tPayload:  []byte{7, 8},\n\t\t\t\t\t\t\t\t\t}, // 2 secs\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tSequenceNumber: 5,\n\t\t\t\t\t\t\tTracks: []*fmp4.PartTrack{{\n\t\t\t\t\t\t\t\tID:       1,\n\t\t\t\t\t\t\t\tBaseTime: 1 * 90000,\n\t\t\t\t\t\t\t\tSamples: []*fmp4.Sample{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tDuration: 1 * 90000,\n\t\t\t\t\t\t\t\t\t\tPayload:  []byte{9, 10},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tDuration: 1 * 90000,\n\t\t\t\t\t\t\t\t\t\tPayload:  []byte{11, 12},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tDuration: 1 * 90000,\n\t\t\t\t\t\t\t\t\t\tPayload:  []byte{13, 14},\n\t\t\t\t\t\t\t\t\t}, // 4 secs\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}},\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t\terr = parts.Marshal(&buf2)\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\terr = os.WriteFile(filepath.Join(dir, \"mypath\", \"2008-11-07_11-23-04-500000.mp4\"),\n\t\t\t\t\t\tappend(buf1.Bytes(), buf2.Bytes()...), 0o644)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t}()\n\n\t\t\t\ts := &Server{\n\t\t\t\t\tAddress:      \"127.0.0.1:9996\",\n\t\t\t\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\t\t\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\t\t\t\tPathConfs: map[string]*conf.Path{\n\t\t\t\t\t\t\"mypath\": {\n\t\t\t\t\t\t\tName:         \"mypath\",\n\t\t\t\t\t\t\tRecordPath:   filepath.Join(dir, \"%path/%Y-%m-%d_%H-%M-%S-%f\"),\n\t\t\t\t\t\t\tRecordFormat: conf.RecordFormatFMP4,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tAuthManager: test.NilAuthManager,\n\t\t\t\t\tParent:      test.NilLogger,\n\t\t\t\t}\n\t\t\t\terr = s.Initialize()\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer s.Close()\n\n\t\t\t\tu, err := url.Parse(\"http://myuser:mypass@localhost:9996/get\")\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tv := url.Values{}\n\t\t\t\tv.Set(\"path\", \"mypath\")\n\t\t\t\tv.Set(\"start\", time.Date(2008, 11, 7, 11, 23, 1, 500000000, time.Local).Format(time.RFC3339Nano))\n\t\t\t\tv.Set(\"duration\", \"3\")\n\t\t\t\tv.Set(\"format\", format)\n\t\t\t\tu.RawQuery = v.Encode()\n\n\t\t\t\treq, err := http.NewRequest(http.MethodGet, u.String(), nil)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tres, err := http.DefaultClient.Do(req)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer res.Body.Close()\n\n\t\t\t\trequire.Equal(t, http.StatusOK, res.StatusCode)\n\n\t\t\t\tbuf, err := io.ReadAll(res.Body)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tswitch format {\n\t\t\t\tcase \"fmp4\":\n\t\t\t\t\tvar parts fmp4.Parts\n\t\t\t\t\terr = parts.Unmarshal(buf)\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\trequire.Equal(t, fmp4.Parts{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tSequenceNumber: 0,\n\t\t\t\t\t\t\tTracks: []*fmp4.PartTrack{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tID: 1,\n\t\t\t\t\t\t\t\t\tSamples: []*fmp4.Sample{\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\tDuration:  0,\n\t\t\t\t\t\t\t\t\t\t\tPTSOffset: 90000,\n\t\t\t\t\t\t\t\t\t\t\tPayload:   []byte{3, 4},\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\tDuration:        90000,\n\t\t\t\t\t\t\t\t\t\t\tPTSOffset:       -90000,\n\t\t\t\t\t\t\t\t\t\t\tIsNonSyncSample: true,\n\t\t\t\t\t\t\t\t\t\t\tPayload:         []byte{5, 6},\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tSequenceNumber: 1,\n\t\t\t\t\t\t\tTracks: []*fmp4.PartTrack{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tID:       2,\n\t\t\t\t\t\t\t\t\tBaseTime: 48000,\n\t\t\t\t\t\t\t\t\tSamples: []*fmp4.Sample{\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\tDuration: 48000,\n\t\t\t\t\t\t\t\t\t\t\tPayload:  []byte{5, 6},\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tSequenceNumber: 2,\n\t\t\t\t\t\t\tTracks: []*fmp4.PartTrack{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tID:       1,\n\t\t\t\t\t\t\t\t\tBaseTime: 90000,\n\t\t\t\t\t\t\t\t\tSamples: []*fmp4.Sample{\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\tDuration: 90000,\n\t\t\t\t\t\t\t\t\t\t\tPayload:  []byte{7, 8},\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tSequenceNumber: 3,\n\t\t\t\t\t\t\tTracks: []*fmp4.PartTrack{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tID:       1,\n\t\t\t\t\t\t\t\t\tBaseTime: 2 * 90000,\n\t\t\t\t\t\t\t\t\tSamples: []*fmp4.Sample{\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\tDuration: 90000,\n\t\t\t\t\t\t\t\t\t\t\tPayload:  []byte{9, 10},\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tID:       2,\n\t\t\t\t\t\t\t\t\tBaseTime: 2 * 48000,\n\t\t\t\t\t\t\t\t\tSamples: []*fmp4.Sample{\n\t\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\t\tDuration: 48000,\n\t\t\t\t\t\t\t\t\t\t\tPayload:  []byte{7, 8},\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t}, parts)\n\n\t\t\t\tcase \"mp4\":\n\t\t\t\t\tvar p pmp4.Presentation\n\t\t\t\t\terr = p.Unmarshal(bytes.NewReader(buf))\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\tsampleData := make(map[int][][]byte)\n\t\t\t\t\tfor _, track := range p.Tracks {\n\t\t\t\t\t\tsamples := make([][]byte, len(track.Samples))\n\t\t\t\t\t\tfor i, sample := range track.Samples {\n\t\t\t\t\t\t\tbuf, err = sample.GetPayload()\n\t\t\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\t\t\tsamples[i] = buf\n\t\t\t\t\t\t\tsample.GetPayload = nil\n\t\t\t\t\t\t}\n\t\t\t\t\t\tsampleData[track.ID] = samples\n\t\t\t\t\t}\n\n\t\t\t\t\trequire.Equal(t, pmp4.Presentation{\n\t\t\t\t\t\tTracks: []*pmp4.Track{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tID:         1,\n\t\t\t\t\t\t\t\tTimeScale:  90000,\n\t\t\t\t\t\t\t\tTimeOffset: -90000,\n\t\t\t\t\t\t\t\tCodec: &mcodecs.H264{\n\t\t\t\t\t\t\t\t\tSPS: test.FormatH264.SPS,\n\t\t\t\t\t\t\t\t\tPPS: test.FormatH264.PPS,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tSamples: []*pmp4.Sample{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tDuration:    90000,\n\t\t\t\t\t\t\t\t\t\tPayloadSize: 2,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tDuration:        90000,\n\t\t\t\t\t\t\t\t\t\tPTSOffset:       -90000,\n\t\t\t\t\t\t\t\t\t\tIsNonSyncSample: true,\n\t\t\t\t\t\t\t\t\t\tPayloadSize:     2,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tDuration:    90000,\n\t\t\t\t\t\t\t\t\t\tPayloadSize: 2,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tDuration:    90000,\n\t\t\t\t\t\t\t\t\t\tPayloadSize: 2,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tID:         2,\n\t\t\t\t\t\t\t\tTimeScale:  48000,\n\t\t\t\t\t\t\t\tTimeOffset: 48000,\n\t\t\t\t\t\t\t\tCodec: &mcodecs.MPEG4Audio{\n\t\t\t\t\t\t\t\t\tConfig: mpeg4audio.AudioSpecificConfig{\n\t\t\t\t\t\t\t\t\t\tType:          mpeg4audio.ObjectTypeAACLC,\n\t\t\t\t\t\t\t\t\t\tSampleRate:    48000,\n\t\t\t\t\t\t\t\t\t\tChannelCount:  2,\n\t\t\t\t\t\t\t\t\t\tChannelConfig: 2,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tSamples: []*pmp4.Sample{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tDuration:    48000,\n\t\t\t\t\t\t\t\t\t\tPayloadSize: 2,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tDuration:    48000,\n\t\t\t\t\t\t\t\t\t\tPayloadSize: 2,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t}, p)\n\n\t\t\t\t\trequire.Equal(t, map[int][][]byte{\n\t\t\t\t\t\t1: {\n\t\t\t\t\t\t\t{3, 4},\n\t\t\t\t\t\t\t{5, 6},\n\t\t\t\t\t\t\t{7, 8},\n\t\t\t\t\t\t\t{9, 10},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t2: {\n\t\t\t\t\t\t\t{5, 6},\n\t\t\t\t\t\t\t{7, 8},\n\t\t\t\t\t\t},\n\t\t\t\t\t}, sampleData)\n\t\t\t\t}\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc TestOnGetDifferentInit(t *testing.T) {\n\tdir, err := os.MkdirTemp(\"\", \"mediamtx-playback\")\n\trequire.NoError(t, err)\n\tdefer os.RemoveAll(dir)\n\n\terr = os.Mkdir(filepath.Join(dir, \"mypath\"), 0o755)\n\trequire.NoError(t, err)\n\n\twriteSegment1(t, filepath.Join(dir, \"mypath\", \"2008-11-07_11-22-00-500000.mp4\"))\n\twriteSegment3(t, filepath.Join(dir, \"mypath\", \"2008-11-07_11-23-02-500000.mp4\"))\n\n\ts := &Server{\n\t\tAddress:      \"127.0.0.1:9996\",\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tPathConfs: map[string]*conf.Path{\n\t\t\t\"mypath\": {\n\t\t\t\tName:         \"mypath\",\n\t\t\t\tRecordPath:   filepath.Join(dir, \"%path/%Y-%m-%d_%H-%M-%S-%f\"),\n\t\t\t\tRecordFormat: conf.RecordFormatFMP4,\n\t\t\t},\n\t\t},\n\t\tAuthManager: test.NilAuthManager,\n\t\tParent:      test.NilLogger,\n\t}\n\terr = s.Initialize()\n\trequire.NoError(t, err)\n\tdefer s.Close()\n\n\tu, err := url.Parse(\"http://myuser:mypass@localhost:9996/get\")\n\trequire.NoError(t, err)\n\n\tv := url.Values{}\n\tv.Set(\"path\", \"mypath\")\n\tv.Set(\"start\", time.Date(2008, 11, 7, 11, 23, 1, 500000000, time.Local).Format(time.RFC3339Nano))\n\tv.Set(\"duration\", \"2\")\n\tv.Set(\"format\", \"fmp4\")\n\tu.RawQuery = v.Encode()\n\n\treq, err := http.NewRequest(http.MethodGet, u.String(), nil)\n\trequire.NoError(t, err)\n\n\tres, err := http.DefaultClient.Do(req)\n\trequire.NoError(t, err)\n\tdefer res.Body.Close()\n\n\trequire.Equal(t, http.StatusOK, res.StatusCode)\n\n\tbuf, err := io.ReadAll(res.Body)\n\trequire.NoError(t, err)\n\n\tvar parts fmp4.Parts\n\terr = parts.Unmarshal(buf)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, fmp4.Parts{\n\t\t{\n\t\t\tSequenceNumber: 0,\n\t\t\tTracks: []*fmp4.PartTrack{\n\t\t\t\t{\n\t\t\t\t\tID: 1,\n\t\t\t\t\tSamples: []*fmp4.Sample{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tDuration:  0,\n\t\t\t\t\t\t\tPTSOffset: 90000,\n\t\t\t\t\t\t\tPayload:   []byte{3, 4},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tDuration:        90000,\n\t\t\t\t\t\t\tPTSOffset:       -90000,\n\t\t\t\t\t\t\tIsNonSyncSample: true,\n\t\t\t\t\t\t\tPayload:         []byte{5, 6},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}, parts)\n}\n\nfunc TestOnGetInMiddleOfLastSample(t *testing.T) {\n\tfor _, format := range []string{\"fmp4\", \"mp4\"} {\n\t\tt.Run(format, func(t *testing.T) {\n\t\t\tdir, err := os.MkdirTemp(\"\", \"mediamtx-playback\")\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer os.RemoveAll(dir)\n\n\t\t\terr = os.Mkdir(filepath.Join(dir, \"mypath\"), 0o755)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tinit := fmp4.Init{\n\t\t\t\tTracks: []*fmp4.InitTrack{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:        1,\n\t\t\t\t\t\tTimeScale: 90000,\n\t\t\t\t\t\tCodec: &mcodecs.H264{\n\t\t\t\t\t\t\tSPS: test.FormatH264.SPS,\n\t\t\t\t\t\t\tPPS: test.FormatH264.PPS,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tfunc() {\n\t\t\t\tfpath := filepath.Join(dir, \"mypath\", \"2008-11-07_11-22-00-000000.mp4\")\n\n\t\t\t\tvar buf1 seekablebuffer.Buffer\n\t\t\t\terr = init.Marshal(&buf1)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tvar buf2 seekablebuffer.Buffer\n\t\t\t\tparts := fmp4.Parts{\n\t\t\t\t\t{\n\t\t\t\t\t\tTracks: []*fmp4.PartTrack{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tID: 1,\n\t\t\t\t\t\t\t\tSamples: []*fmp4.Sample{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tDuration:        1 * 90000,\n\t\t\t\t\t\t\t\t\t\tIsNonSyncSample: false,\n\t\t\t\t\t\t\t\t\t\tPayload:         []byte{1, 2},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\terr = parts.Marshal(&buf2)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\terr = os.WriteFile(fpath, append(buf1.Bytes(), buf2.Bytes()...), 0o644)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}()\n\n\t\t\ts := &Server{\n\t\t\t\tAddress:      \"127.0.0.1:9996\",\n\t\t\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\t\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\t\t\tPathConfs: map[string]*conf.Path{\n\t\t\t\t\t\"mypath\": {\n\t\t\t\t\t\tName:         \"mypath\",\n\t\t\t\t\t\tRecordPath:   filepath.Join(dir, \"%path/%Y-%m-%d_%H-%M-%S-%f\"),\n\t\t\t\t\t\tRecordFormat: conf.RecordFormatFMP4,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tAuthManager: test.NilAuthManager,\n\t\t\t\tParent:      test.NilLogger,\n\t\t\t}\n\t\t\terr = s.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer s.Close()\n\n\t\t\tu, err := url.Parse(\"http://myuser:mypass@localhost:9996/get\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\tv := url.Values{}\n\t\t\tv.Set(\"path\", \"mypath\")\n\t\t\tv.Set(\"start\", time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano))\n\t\t\tv.Set(\"duration\", \"3\")\n\t\t\tv.Set(\"format\", format)\n\t\t\tu.RawQuery = v.Encode()\n\n\t\t\treq, err := http.NewRequest(http.MethodGet, u.String(), nil)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tres, err := http.DefaultClient.Do(req)\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer res.Body.Close()\n\n\t\t\trequire.Equal(t, http.StatusNotFound, res.StatusCode)\n\t\t})\n\t}\n}\n\nfunc TestOnGetBetweenSegments(t *testing.T) {\n\tfor _, ca := range []string{\n\t\t\"idr before\",\n\t\t\"idr after\",\n\t} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tdir, err := os.MkdirTemp(\"\", \"mediamtx-playback\")\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer os.RemoveAll(dir)\n\n\t\t\terr = os.Mkdir(filepath.Join(dir, \"mypath\"), 0o755)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tinit := fmp4.Init{\n\t\t\t\tTracks: []*fmp4.InitTrack{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:        1,\n\t\t\t\t\t\tTimeScale: 90000,\n\t\t\t\t\t\tCodec: &mcodecs.H264{\n\t\t\t\t\t\t\tSPS: test.FormatH264.SPS,\n\t\t\t\t\t\t\tPPS: test.FormatH264.PPS,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tfunc() {\n\t\t\t\tfpath := filepath.Join(dir, \"mypath\", \"2008-11-07_11-22-00-000000.mp4\")\n\n\t\t\t\tvar buf1 seekablebuffer.Buffer\n\t\t\t\terr = init.Marshal(&buf1)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tvar buf2 seekablebuffer.Buffer\n\t\t\t\tparts := fmp4.Parts{\n\t\t\t\t\t{\n\t\t\t\t\t\tTracks: []*fmp4.PartTrack{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tID: 1,\n\t\t\t\t\t\t\t\tSamples: []*fmp4.Sample{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tDuration:        1 * 90000,\n\t\t\t\t\t\t\t\t\t\tIsNonSyncSample: false,\n\t\t\t\t\t\t\t\t\t\tPayload:         []byte{1, 2},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\terr = parts.Marshal(&buf2)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\terr = os.WriteFile(fpath, append(buf1.Bytes(), buf2.Bytes()...), 0o644)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}()\n\n\t\t\tfunc() {\n\t\t\t\tfpath := filepath.Join(dir, \"mypath\", \"2008-11-07_11-22-01-000000.mp4\")\n\n\t\t\t\tvar buf1 seekablebuffer.Buffer\n\t\t\t\terr = init.Marshal(&buf1)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tvar buf2 seekablebuffer.Buffer\n\t\t\t\tparts := fmp4.Parts{\n\t\t\t\t\t{\n\t\t\t\t\t\tTracks: []*fmp4.PartTrack{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tID: 1,\n\t\t\t\t\t\t\t\tSamples: []*fmp4.Sample{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tDuration:        1 * 90000,\n\t\t\t\t\t\t\t\t\t\tIsNonSyncSample: (ca == \"idr before\"),\n\t\t\t\t\t\t\t\t\t\tPayload:         []byte{3, 4},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t\terr = parts.Marshal(&buf2)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\terr = os.WriteFile(fpath, append(buf1.Bytes(), buf2.Bytes()...), 0o644)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}()\n\n\t\t\ts := &Server{\n\t\t\t\tAddress:      \"127.0.0.1:9996\",\n\t\t\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\t\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\t\t\tPathConfs: map[string]*conf.Path{\n\t\t\t\t\t\"mypath\": {\n\t\t\t\t\t\tName:         \"mypath\",\n\t\t\t\t\t\tRecordPath:   filepath.Join(dir, \"%path/%Y-%m-%d_%H-%M-%S-%f\"),\n\t\t\t\t\t\tRecordFormat: conf.RecordFormatFMP4,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tAuthManager: test.NilAuthManager,\n\t\t\t\tParent:      test.NilLogger,\n\t\t\t}\n\t\t\terr = s.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer s.Close()\n\n\t\t\tu, err := url.Parse(\"http://myuser:mypass@localhost:9996/get\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\tv := url.Values{}\n\t\t\tv.Set(\"path\", \"mypath\")\n\t\t\tv.Set(\"start\", time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano))\n\t\t\tv.Set(\"duration\", \"3\")\n\t\t\tv.Set(\"format\", \"fmp4\")\n\t\t\tu.RawQuery = v.Encode()\n\n\t\t\treq, err := http.NewRequest(http.MethodGet, u.String(), nil)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tres, err := http.DefaultClient.Do(req)\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer res.Body.Close()\n\n\t\t\trequire.Equal(t, http.StatusOK, res.StatusCode)\n\n\t\t\tbuf, err := io.ReadAll(res.Body)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tvar parts fmp4.Parts\n\t\t\terr = parts.Unmarshal(buf)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tswitch ca {\n\t\t\tcase \"idr before\":\n\t\t\t\trequire.Equal(t, fmp4.Parts{\n\t\t\t\t\t{\n\t\t\t\t\t\tSequenceNumber: 0,\n\t\t\t\t\t\tTracks: []*fmp4.PartTrack{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tID:       1,\n\t\t\t\t\t\t\t\tBaseTime: 45000,\n\t\t\t\t\t\t\t\tSamples: []*fmp4.Sample{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tDuration: 0,\n\t\t\t\t\t\t\t\t\t\tPayload:  []byte{1, 2},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tDuration:        90000,\n\t\t\t\t\t\t\t\t\t\tPayload:         []byte{3, 4},\n\t\t\t\t\t\t\t\t\t\tIsNonSyncSample: true,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}, parts)\n\n\t\t\tcase \"idr after\":\n\t\t\t\trequire.Equal(t, fmp4.Parts{\n\t\t\t\t\t{\n\t\t\t\t\t\tSequenceNumber: 0,\n\t\t\t\t\t\tTracks: []*fmp4.PartTrack{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tID:       1,\n\t\t\t\t\t\t\t\tBaseTime: 45000,\n\t\t\t\t\t\t\t\tSamples: []*fmp4.Sample{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tDuration: 90000,\n\t\t\t\t\t\t\t\t\t\tPayload:  []byte{3, 4},\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}, parts)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/playback/on_list.go",
    "content": "package playback\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/fmp4\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/recordstore\"\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype listEntryDuration time.Duration\n\nfunc (d listEntryDuration) MarshalJSON() ([]byte, error) {\n\treturn json.Marshal(time.Duration(d).Seconds())\n}\n\ntype parsedSegment struct {\n\tstart    time.Time\n\tinit     *fmp4.Init\n\tduration time.Duration\n}\n\nfunc parseSegment(seg *recordstore.Segment) (*parsedSegment, error) {\n\tf, err := os.Open(seg.Fpath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer f.Close()\n\n\tinit, duration, err := segmentFMP4ReadHeader(f)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// if duration is not present in the header, compute it\n\t// by parsing each part\n\tif duration == 0 {\n\t\tduration, err = segmentFMP4ReadDurationFromParts(f, init)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn &parsedSegment{\n\t\tstart:    seg.Start,\n\t\tinit:     init,\n\t\tduration: duration,\n\t}, nil\n}\n\nfunc parseSegments(segments []*recordstore.Segment) ([]*parsedSegment, error) {\n\tparsed := make([]*parsedSegment, len(segments))\n\tch := make(chan error)\n\n\t// process segments in parallel.\n\t// parallel random access should improve performance in most cases.\n\t// ref: https://pkolaczk.github.io/disk-parallelism/\n\tfor i, seg := range segments {\n\t\tgo func(i int, seg *recordstore.Segment) {\n\t\t\tvar err error\n\t\t\tparsed[i], err = parseSegment(seg)\n\t\t\tch <- err\n\t\t}(i, seg)\n\t}\n\n\tvar err error\n\n\tfor range segments {\n\t\terr2 := <-ch\n\t\tif err2 != nil {\n\t\t\terr = err2\n\t\t}\n\t}\n\n\treturn parsed, err\n}\n\nfunc urlScheme(ctx *gin.Context, trustedProxies conf.IPNetworks, encryption bool) string {\n\tif trustedProxies.Contains(net.ParseIP(ctx.RemoteIP())) {\n\t\txForwardedProto := ctx.Request.Header.Get(\"X-Forwarded-Proto\")\n\t\tif xForwardedProto != \"\" {\n\t\t\treturn xForwardedProto\n\t\t}\n\t}\n\n\tif encryption {\n\t\treturn \"https\"\n\t}\n\n\treturn \"http\"\n}\n\ntype listEntry struct {\n\tStart    time.Time         `json:\"start\"`\n\tDuration listEntryDuration `json:\"duration\"`\n\tURL      string            `json:\"url\"`\n}\n\nfunc concatenateSegments(parsed []*parsedSegment) []listEntry {\n\tout := []listEntry{}\n\tvar prevInit *fmp4.Init\n\n\tfor _, parsed := range parsed {\n\t\tif len(out) != 0 && segmentFMP4CanBeConcatenated(\n\t\t\tprevInit,\n\t\t\tout[len(out)-1].Start.Add(time.Duration(out[len(out)-1].Duration)),\n\t\t\tparsed.init,\n\t\t\tparsed.start) {\n\t\t\tprevStart := out[len(out)-1].Start\n\t\t\tcurEnd := parsed.start.Add(parsed.duration)\n\t\t\tout[len(out)-1].Duration = listEntryDuration(curEnd.Sub(prevStart))\n\t\t} else {\n\t\t\tout = append(out, listEntry{\n\t\t\t\tStart:    parsed.start,\n\t\t\t\tDuration: listEntryDuration(parsed.duration),\n\t\t\t})\n\t\t}\n\n\t\tprevInit = parsed.init\n\t}\n\n\treturn out\n}\n\nfunc parseAndConcatenate(\n\trecordFormat conf.RecordFormat,\n\tsegments []*recordstore.Segment,\n) ([]listEntry, error) {\n\tif recordFormat == conf.RecordFormatFMP4 {\n\t\tparsed, err := parseSegments(segments)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tout := concatenateSegments(parsed)\n\t\treturn out, nil\n\t}\n\n\treturn nil, fmt.Errorf(\"MPEG-TS format is not supported yet\")\n}\n\nfunc (s *Server) onList(ctx *gin.Context) {\n\tpathName := ctx.Query(\"path\")\n\n\tif !s.doAuth(ctx, pathName) {\n\t\treturn\n\t}\n\n\tpathConf, err := s.safeFindPathConf(pathName)\n\tif err != nil {\n\t\ts.writeError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\tvar start *time.Time\n\trawStart := ctx.Query(\"start\")\n\tif rawStart != \"\" {\n\t\tvar tmp time.Time\n\t\ttmp, err = time.Parse(time.RFC3339, rawStart)\n\t\tif err != nil {\n\t\t\ts.writeError(ctx, http.StatusBadRequest, fmt.Errorf(\"invalid start: %w\", err))\n\t\t\treturn\n\t\t}\n\t\tstart = &tmp\n\t}\n\n\tvar end *time.Time\n\trawEnd := ctx.Query(\"end\")\n\tif rawEnd != \"\" {\n\t\tvar tmp time.Time\n\t\ttmp, err = time.Parse(time.RFC3339, rawEnd)\n\t\tif err != nil {\n\t\t\ts.writeError(ctx, http.StatusBadRequest, fmt.Errorf(\"invalid end: %w\", err))\n\t\t\treturn\n\t\t}\n\t\tend = &tmp\n\t}\n\n\tsegments, err := recordstore.FindSegments(pathConf, pathName, start, end)\n\tif err != nil {\n\t\tif errors.Is(err, recordstore.ErrNoSegmentsFound) {\n\t\t\ts.writeError(ctx, http.StatusNotFound, err)\n\t\t} else {\n\t\t\ts.writeError(ctx, http.StatusBadRequest, err)\n\t\t}\n\t\treturn\n\t}\n\n\tentries, err := parseAndConcatenate(pathConf.RecordFormat, segments)\n\tif err != nil {\n\t\ts.writeError(ctx, http.StatusInternalServerError, err)\n\t\treturn\n\t}\n\n\tif start != nil {\n\t\tfirstEntry := entries[0]\n\n\t\t// when start is placed in a gap between the first and second segment,\n\t\t// or when there's no second segment,\n\t\t// the first segment is erroneously included with a negative duration.\n\t\t// remove it.\n\t\tif firstEntry.Start.Add(time.Duration(firstEntry.Duration)).Before(*start) {\n\t\t\tentries = entries[1:]\n\n\t\t\tif len(entries) == 0 {\n\t\t\t\ts.writeError(ctx, http.StatusNotFound, recordstore.ErrNoSegmentsFound)\n\t\t\t\treturn\n\t\t\t}\n\t\t} else if firstEntry.Start.Before(*start) {\n\t\t\tentries[0].Duration -= listEntryDuration(start.Sub(firstEntry.Start))\n\t\t\tentries[0].Start = *start\n\t\t}\n\t}\n\n\tif end != nil {\n\t\tlastEntry := entries[len(entries)-1]\n\t\tif lastEntry.Start.Add(time.Duration(lastEntry.Duration)).After(*end) {\n\t\t\tentries[len(entries)-1].Duration = listEntryDuration(end.Sub(lastEntry.Start))\n\t\t}\n\t}\n\n\tscheme := urlScheme(ctx, s.TrustedProxies, s.Encryption)\n\n\tfor i := range entries {\n\t\tv := url.Values{}\n\t\tv.Add(\"path\", pathName)\n\t\tv.Add(\"start\", entries[i].Start.Format(time.RFC3339Nano))\n\t\tv.Add(\"duration\", strconv.FormatFloat(time.Duration(entries[i].Duration).Seconds(), 'f', -1, 64))\n\t\tu := &url.URL{\n\t\t\tScheme:   scheme,\n\t\t\tHost:     ctx.Request.Host,\n\t\t\tPath:     \"/get\",\n\t\t\tRawQuery: v.Encode(),\n\t\t}\n\t\tentries[i].URL = u.String()\n\t}\n\n\tctx.JSON(http.StatusOK, entries)\n}\n"
  },
  {
    "path": "internal/playback/on_list_test.go",
    "content": "package playback\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\tamp4 \"github.com/abema/go-mp4\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/fmp4\"\n\tmcodecs \"github.com/bluenviron/mediacommon/v2/pkg/formats/mp4/codecs\"\n\t\"github.com/bluenviron/mediamtx/internal/auth\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestOnList(t *testing.T) {\n\tfor _, ca := range []string{\n\t\t\"unfiltered\",\n\t\t\"filtered\",\n\t\t\"filtered and gap\",\n\t\t\"different init\",\n\t\t\"start after duration\",\n\t\t\"start before first\",\n\t} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tdir, err := os.MkdirTemp(\"\", \"mediamtx-playback\")\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer os.RemoveAll(dir)\n\n\t\t\terr = os.Mkdir(filepath.Join(dir, \"mypath\"), 0o755)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tswitch ca {\n\t\t\tcase \"unfiltered\", \"filtered\", \"start before first\":\n\t\t\t\twriteSegment1(t, filepath.Join(dir, \"mypath\", \"2008-11-07_11-22-00-500000.mp4\"))\n\t\t\t\twriteSegment2(t, filepath.Join(dir, \"mypath\", \"2008-11-07_11-23-02-500000.mp4\"))\n\t\t\t\twriteSegment2(t, filepath.Join(dir, \"mypath\", \"2009-11-07_11-23-02-500000.mp4\"))\n\n\t\t\tcase \"filtered and gap\":\n\t\t\t\twriteSegment1(t, filepath.Join(dir, \"mypath\", \"2008-11-07_11-22-00-500000.mp4\"))\n\t\t\t\twriteSegment2(t, filepath.Join(dir, \"mypath\", \"2008-11-07_11-24-02-500000.mp4\"))\n\n\t\t\tcase \"different init\":\n\t\t\t\twriteSegment1(t, filepath.Join(dir, \"mypath\", \"2008-11-07_11-22-00-500000.mp4\"))\n\t\t\t\twriteSegment3(t, filepath.Join(dir, \"mypath\", \"2008-11-07_11-23-02-500000.mp4\"))\n\n\t\t\tcase \"start after duration\":\n\t\t\t\twriteSegment1(t, filepath.Join(dir, \"mypath\", \"2008-11-07_11-22-00-500000.mp4\"))\n\t\t\t}\n\n\t\t\tchecked := false\n\n\t\t\ts := &Server{\n\t\t\t\tAddress:      \"127.0.0.1:9996\",\n\t\t\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\t\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\t\t\tPathConfs: map[string]*conf.Path{\n\t\t\t\t\t\"mypath\": {\n\t\t\t\t\t\tName:         \"mypath\",\n\t\t\t\t\t\tRecordPath:   filepath.Join(dir, \"%path/%Y-%m-%d_%H-%M-%S-%f\"),\n\t\t\t\t\t\tRecordFormat: conf.RecordFormatFMP4,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tAuthManager: &test.AuthManager{\n\t\t\t\t\tAuthenticateImpl: func(req *auth.Request) (string, *auth.Error) {\n\t\t\t\t\t\trequire.Equal(t, conf.AuthActionPlayback, req.Action)\n\t\t\t\t\t\trequire.Equal(t, \"myuser\", req.Credentials.User)\n\t\t\t\t\t\trequire.Equal(t, \"mypass\", req.Credentials.Pass)\n\t\t\t\t\t\tchecked = true\n\t\t\t\t\t\treturn req.Credentials.User, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tParent: test.NilLogger,\n\t\t\t}\n\t\t\terr = s.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer s.Close()\n\n\t\t\tu, err := url.Parse(\"http://myuser:mypass@localhost:9996/list?start=\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\tv := url.Values{}\n\t\t\tv.Set(\"path\", \"mypath\")\n\n\t\t\tswitch ca {\n\t\t\tcase \"filtered\":\n\t\t\t\tv.Set(\"start\", time.Date(2008, 11, 7, 11, 22, 1, 500000000, time.Local).Format(time.RFC3339Nano))\n\t\t\t\tv.Set(\"end\", time.Date(2009, 11, 7, 11, 23, 4, 500000000, time.Local).Format(time.RFC3339Nano))\n\n\t\t\tcase \"filtered and gap\":\n\t\t\t\tv.Set(\"start\", time.Date(2008, 11, 7, 11, 23, 20, 500000000, time.Local).Format(time.RFC3339Nano))\n\t\t\t\tv.Set(\"end\", time.Date(2009, 11, 7, 11, 23, 4, 500000000, time.Local).Format(time.RFC3339Nano))\n\n\t\t\tcase \"start after duration\":\n\t\t\t\tv.Set(\"start\", time.Date(2010, 11, 7, 11, 23, 20, 500000000, time.Local).Format(time.RFC3339Nano))\n\n\t\t\tcase \"start before first\":\n\t\t\t\tv.Set(\"start\", time.Date(2007, 11, 7, 11, 23, 20, 500000000, time.Local).Format(time.RFC3339Nano))\n\t\t\t}\n\n\t\t\tu.RawQuery = v.Encode()\n\n\t\t\treq, err := http.NewRequest(http.MethodGet, u.String(), nil)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tres, err := http.DefaultClient.Do(req)\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer res.Body.Close()\n\n\t\t\tif ca == \"start after duration\" {\n\t\t\t\trequire.Equal(t, http.StatusNotFound, res.StatusCode)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\trequire.Equal(t, http.StatusOK, res.StatusCode)\n\n\t\t\tvar out any\n\t\t\terr = json.NewDecoder(res.Body).Decode(&out)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tswitch ca {\n\t\t\tcase \"unfiltered\", \"start before first\":\n\t\t\t\trequire.Equal(t, []any{\n\t\t\t\t\tmap[string]any{\n\t\t\t\t\t\t\"duration\": float64(66),\n\t\t\t\t\t\t\"start\":    time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano),\n\t\t\t\t\t\t\"url\": \"http://localhost:9996/get?duration=66&path=mypath&start=\" +\n\t\t\t\t\t\t\turl.QueryEscape(time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano)),\n\t\t\t\t\t},\n\t\t\t\t\tmap[string]any{\n\t\t\t\t\t\t\"duration\": float64(4),\n\t\t\t\t\t\t\"start\":    time.Date(2009, 11, 7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano),\n\t\t\t\t\t\t\"url\": \"http://localhost:9996/get?duration=4&path=mypath&start=\" +\n\t\t\t\t\t\t\turl.QueryEscape(time.Date(2009, 11, 7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano)),\n\t\t\t\t\t},\n\t\t\t\t}, out)\n\n\t\t\tcase \"filtered\":\n\t\t\t\trequire.Equal(t, []any{\n\t\t\t\t\tmap[string]any{\n\t\t\t\t\t\t\"duration\": float64(65),\n\t\t\t\t\t\t\"start\":    time.Date(2008, 11, 7, 11, 22, 1, 500000000, time.Local).Format(time.RFC3339Nano),\n\t\t\t\t\t\t\"url\": \"http://localhost:9996/get?duration=65&path=mypath&start=\" +\n\t\t\t\t\t\t\turl.QueryEscape(time.Date(2008, 11, 7, 11, 22, 1, 500000000, time.Local).Format(time.RFC3339Nano)),\n\t\t\t\t\t},\n\t\t\t\t\tmap[string]any{\n\t\t\t\t\t\t\"duration\": float64(2),\n\t\t\t\t\t\t\"start\":    time.Date(2009, 11, 7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano),\n\t\t\t\t\t\t\"url\": \"http://localhost:9996/get?duration=2&path=mypath&start=\" +\n\t\t\t\t\t\t\turl.QueryEscape(time.Date(2009, 11, 7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano)),\n\t\t\t\t\t},\n\t\t\t\t}, out)\n\n\t\t\tcase \"filtered and gap\":\n\t\t\t\trequire.Equal(t, []any{\n\t\t\t\t\tmap[string]any{\n\t\t\t\t\t\t\"duration\": float64(4),\n\t\t\t\t\t\t\"start\":    time.Date(2008, 11, 7, 11, 24, 2, 500000000, time.Local).Format(time.RFC3339Nano),\n\t\t\t\t\t\t\"url\": \"http://localhost:9996/get?duration=4&path=mypath&start=\" +\n\t\t\t\t\t\t\turl.QueryEscape(time.Date(2008, 11, 7, 11, 24, 2, 500000000, time.Local).Format(time.RFC3339Nano)),\n\t\t\t\t\t},\n\t\t\t\t}, out)\n\n\t\t\tcase \"different init\":\n\t\t\t\trequire.Equal(t, []any{\n\t\t\t\t\tmap[string]any{\n\t\t\t\t\t\t\"duration\": float64(62),\n\t\t\t\t\t\t\"start\":    time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano),\n\t\t\t\t\t\t\"url\": \"http://localhost:9996/get?duration=62&path=mypath&start=\" +\n\t\t\t\t\t\t\turl.QueryEscape(time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano)),\n\t\t\t\t\t},\n\t\t\t\t\tmap[string]any{\n\t\t\t\t\t\t\"duration\": float64(1),\n\t\t\t\t\t\t\"start\":    time.Date(2008, 11, 7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano),\n\t\t\t\t\t\t\"url\": \"http://localhost:9996/get?duration=1&path=mypath&start=\" +\n\t\t\t\t\t\t\turl.QueryEscape(time.Date(2008, 11, 7, 11, 23, 2, 500000000, time.Local).Format(time.RFC3339Nano)),\n\t\t\t\t\t},\n\t\t\t\t}, out)\n\t\t\t}\n\n\t\t\trequire.True(t, checked)\n\t\t})\n\t}\n}\n\nfunc writeDuration(f io.ReadWriteSeeker, d time.Duration) error {\n\t_, err := f.Seek(0, io.SeekStart)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// check and skip ftyp header and content\n\n\tbuf := make([]byte, 8)\n\t_, err = io.ReadFull(f, buf)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !bytes.Equal(buf[4:], []byte{'f', 't', 'y', 'p'}) {\n\t\treturn fmt.Errorf(\"ftyp box not found\")\n\t}\n\n\tftypSize := uint32(buf[0])<<24 | uint32(buf[1])<<16 | uint32(buf[2])<<8 | uint32(buf[3])\n\n\t_, err = f.Seek(int64(ftypSize), io.SeekStart)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// check and skip moov header\n\n\t_, err = io.ReadFull(f, buf)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !bytes.Equal(buf[4:], []byte{'m', 'o', 'o', 'v'}) {\n\t\treturn fmt.Errorf(\"moov box not found\")\n\t}\n\n\tmoovSize := uint32(buf[0])<<24 | uint32(buf[1])<<16 | uint32(buf[2])<<8 | uint32(buf[3])\n\n\tmoovPos, err := f.Seek(8, io.SeekCurrent)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar mvhd amp4.Mvhd\n\t_, err = amp4.Unmarshal(f, uint64(moovSize-8), &mvhd, amp4.Context{})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tmvhd.DurationV0 = uint32(d / time.Millisecond)\n\n\t_, err = f.Seek(moovPos, io.SeekStart)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = amp4.Marshal(f, &mvhd, amp4.Context{})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc TestOnListCachedDuration(t *testing.T) {\n\tdir, err := os.MkdirTemp(\"\", \"mediamtx-playback\")\n\trequire.NoError(t, err)\n\tdefer os.RemoveAll(dir)\n\n\terr = os.Mkdir(filepath.Join(dir, \"mypath\"), 0o755)\n\trequire.NoError(t, err)\n\n\tfunc() {\n\t\tvar f *os.File\n\t\tf, err = os.Create(filepath.Join(dir, \"mypath\", \"2008-11-07_11-22-00-500000.mp4\"))\n\t\trequire.NoError(t, err)\n\t\tdefer f.Close()\n\n\t\tinit := fmp4.Init{\n\t\t\tTracks: []*fmp4.InitTrack{\n\t\t\t\t{\n\t\t\t\t\tID:        1,\n\t\t\t\t\tTimeScale: 90000,\n\t\t\t\t\tCodec: &mcodecs.H264{\n\t\t\t\t\t\tSPS: test.FormatH264.SPS,\n\t\t\t\t\t\tPPS: test.FormatH264.PPS,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\terr = init.Marshal(f)\n\t\trequire.NoError(t, err)\n\n\t\terr = writeDuration(f, 50*time.Second)\n\t\trequire.NoError(t, err)\n\t}()\n\n\ts := &Server{\n\t\tAddress:      \"127.0.0.1:9996\",\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tPathConfs: map[string]*conf.Path{\n\t\t\t\"mypath\": {\n\t\t\t\tName:         \"mypath\",\n\t\t\t\tRecordPath:   filepath.Join(dir, \"%path/%Y-%m-%d_%H-%M-%S-%f\"),\n\t\t\t\tRecordFormat: conf.RecordFormatFMP4,\n\t\t\t},\n\t\t},\n\t\tAuthManager: test.NilAuthManager,\n\t\tParent:      test.NilLogger,\n\t}\n\terr = s.Initialize()\n\trequire.NoError(t, err)\n\tdefer s.Close()\n\n\tu, err := url.Parse(\"http://myuser:mypass@localhost:9996/list\")\n\trequire.NoError(t, err)\n\n\tv := url.Values{}\n\tv.Set(\"path\", \"mypath\")\n\tu.RawQuery = v.Encode()\n\n\treq, err := http.NewRequest(http.MethodGet, u.String(), nil)\n\trequire.NoError(t, err)\n\n\tres, err := http.DefaultClient.Do(req)\n\trequire.NoError(t, err)\n\tdefer res.Body.Close()\n\n\trequire.Equal(t, http.StatusOK, res.StatusCode)\n\n\tvar out any\n\terr = json.NewDecoder(res.Body).Decode(&out)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, []any{\n\t\tmap[string]any{\n\t\t\t\"duration\": float64(50),\n\t\t\t\"start\":    time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano),\n\t\t\t\"url\": \"http://localhost:9996/get?duration=50&path=mypath&start=\" +\n\t\t\t\turl.QueryEscape(time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano)),\n\t\t},\n\t}, out)\n}\n\nfunc TestOnListXForwardedProto(t *testing.T) {\n\tdir, err := os.MkdirTemp(\"\", \"mediamtx-playback\")\n\trequire.NoError(t, err)\n\tdefer os.RemoveAll(dir)\n\n\terr = os.Mkdir(filepath.Join(dir, \"mypath\"), 0o755)\n\trequire.NoError(t, err)\n\n\twriteSegment1(t, filepath.Join(dir, \"mypath\", \"2008-11-07_11-22-00-500000.mp4\"))\n\n\tvar trustedProxies conf.IPNetworks\n\terr = json.Unmarshal([]byte(`[\"127.0.0.0/8\"]`), &trustedProxies)\n\trequire.NoError(t, err)\n\n\ts := &Server{\n\t\tAddress:        \"127.0.0.1:9996\",\n\t\tReadTimeout:    conf.Duration(10 * time.Second),\n\t\tWriteTimeout:   conf.Duration(10 * time.Second),\n\t\tTrustedProxies: trustedProxies,\n\t\tPathConfs: map[string]*conf.Path{\n\t\t\t\"mypath\": {\n\t\t\t\tName:         \"mypath\",\n\t\t\t\tRecordPath:   filepath.Join(dir, \"%path/%Y-%m-%d_%H-%M-%S-%f\"),\n\t\t\t\tRecordFormat: conf.RecordFormatFMP4,\n\t\t\t},\n\t\t},\n\t\tAuthManager: test.NilAuthManager,\n\t\tParent:      test.NilLogger,\n\t}\n\terr = s.Initialize()\n\trequire.NoError(t, err)\n\tdefer s.Close()\n\n\tu, err := url.Parse(\"http://localhost:9996/list\")\n\trequire.NoError(t, err)\n\n\tv := url.Values{}\n\tv.Set(\"path\", \"mypath\")\n\tu.RawQuery = v.Encode()\n\n\treq, err := http.NewRequest(http.MethodGet, u.String(), nil)\n\trequire.NoError(t, err)\n\n\treq.Header.Set(\"X-Forwarded-Proto\", \"https\")\n\n\tres, err := http.DefaultClient.Do(req)\n\trequire.NoError(t, err)\n\tdefer res.Body.Close()\n\n\trequire.Equal(t, http.StatusOK, res.StatusCode)\n\n\tvar out any\n\terr = json.NewDecoder(res.Body).Decode(&out)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, []any{\n\t\tmap[string]any{\n\t\t\t\"duration\": float64(62),\n\t\t\t\"start\":    time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano),\n\t\t\t\"url\": \"https://localhost:9996/get?duration=62&path=mypath&start=\" +\n\t\t\t\turl.QueryEscape(time.Date(2008, 11, 7, 11, 22, 0, 500000000, time.Local).Format(time.RFC3339Nano)),\n\t\t},\n\t}, out)\n}\n"
  },
  {
    "path": "internal/playback/segment_fmp4.go",
    "content": "package playback\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"reflect\"\n\t\"time\"\n\n\tamp4 \"github.com/abema/go-mp4\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/fmp4\"\n\t\"github.com/bluenviron/mediamtx/internal/recordstore\"\n)\n\nconst (\n\tsampleFlagIsNonSyncSample = 1 << 16\n\tconcatenationTolerance    = 1 * time.Second\n)\n\nvar errTerminated = errors.New(\"terminated\")\n\ntype readSeekerAt interface {\n\tio.Reader\n\tio.Seeker\n\tio.ReaderAt\n}\n\nfunc durationGoToMp4(v time.Duration, timeScale uint32) int64 {\n\ttimeScale64 := int64(timeScale)\n\tsecs := v / time.Second\n\tdec := v % time.Second\n\treturn int64(secs)*timeScale64 + int64(dec)*timeScale64/int64(time.Second)\n}\n\nfunc durationMp4ToGo(v int64, timeScale uint32) time.Duration {\n\ttimeScale64 := int64(timeScale)\n\tsecs := v / timeScale64\n\tdec := v % timeScale64\n\treturn time.Duration(secs)*time.Second + time.Duration(dec)*time.Second/time.Duration(timeScale64)\n}\n\nfunc findInitTrack(tracks []*fmp4.InitTrack, id int) *fmp4.InitTrack {\n\tfor _, track := range tracks {\n\t\tif track.ID == id {\n\t\t\treturn track\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc findMtxi(userData []amp4.IBox) *recordstore.Mtxi {\n\tfor _, box := range userData {\n\t\tif i, ok := box.(*recordstore.Mtxi); ok {\n\t\t\treturn i\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc segmentFMP4TracksAreEqual(tracks1 []*fmp4.InitTrack, tracks2 []*fmp4.InitTrack) bool {\n\tif len(tracks1) != len(tracks2) {\n\t\treturn false\n\t}\n\n\tfor i, track1 := range tracks1 {\n\t\ttrack2 := tracks2[i]\n\n\t\tif track1.ID != track2.ID ||\n\t\t\ttrack1.TimeScale != track2.TimeScale ||\n\t\t\treflect.TypeOf(track1.Codec) != reflect.TypeOf(track2.Codec) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\nfunc segmentFMP4CanBeConcatenated(\n\tprevInit *fmp4.Init,\n\tprevEnd time.Time,\n\tcurInit *fmp4.Init,\n\tcurStart time.Time,\n) bool {\n\tmtxi1 := findMtxi(prevInit.UserData)\n\tmtxi2 := findMtxi(curInit.UserData)\n\n\tswitch {\n\tcase mtxi1 == nil && mtxi2 != nil:\n\t\treturn false\n\n\tcase mtxi1 != nil && mtxi2 == nil:\n\t\treturn false\n\n\tcase mtxi1 == nil && mtxi2 == nil: // legacy method\n\t\treturn segmentFMP4TracksAreEqual(prevInit.Tracks, curInit.Tracks) &&\n\t\t\t!curStart.Before(prevEnd.Add(-concatenationTolerance)) &&\n\t\t\t!curStart.After(prevEnd.Add(concatenationTolerance))\n\n\tdefault:\n\t\treturn bytes.Equal(mtxi1.StreamID[:], mtxi2.StreamID[:]) &&\n\t\t\t(mtxi1.SegmentNumber+1) == mtxi2.SegmentNumber\n\t}\n}\n\nfunc segmentFMP4ReadHeader(r io.ReadSeeker) (*fmp4.Init, time.Duration, error) {\n\t// check and skip ftyp\n\n\tbuf := make([]byte, 8)\n\t_, err := io.ReadFull(r, buf)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\tif !bytes.Equal(buf[4:], []byte{'f', 't', 'y', 'p'}) {\n\t\treturn nil, 0, fmt.Errorf(\"ftyp box not found\")\n\t}\n\n\tftypSize := uint32(buf[0])<<24 | uint32(buf[1])<<16 | uint32(buf[2])<<8 | uint32(buf[3])\n\n\t_, err = r.Seek(int64(ftypSize), io.SeekStart)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\t// check moov\n\n\t_, err = io.ReadFull(r, buf)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\tif !bytes.Equal(buf[4:], []byte{'m', 'o', 'o', 'v'}) {\n\t\treturn nil, 0, fmt.Errorf(\"moov box not found\")\n\t}\n\n\tmoovSize := uint32(buf[0])<<24 | uint32(buf[1])<<16 | uint32(buf[2])<<8 | uint32(buf[3])\n\n\t// skip moov header\n\n\t_, err = r.Seek(8, io.SeekCurrent)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\t// read mvhd\n\n\tvar mvhd amp4.Mvhd\n\t_, err = amp4.Unmarshal(r, uint64(moovSize-8), &mvhd, amp4.Context{})\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\td := time.Duration(mvhd.DurationV0) * time.Second / time.Duration(mvhd.Timescale)\n\n\t// read ftyp and moov\n\n\t_, err = r.Seek(0, io.SeekStart)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\tbuf = make([]byte, uint64(ftypSize+moovSize))\n\n\t_, err = io.ReadFull(r, buf)\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\t// pass ftyp and moov to fmp4.Init\n\n\tvar init fmp4.Init\n\terr = init.Unmarshal(bytes.NewReader(buf))\n\tif err != nil {\n\t\treturn nil, 0, err\n\t}\n\n\treturn &init, d, nil\n}\n\nfunc segmentFMP4ReadDurationFromParts(\n\tr io.ReadSeeker,\n\tinit *fmp4.Init,\n) (time.Duration, error) {\n\t_, err := r.Seek(0, io.SeekStart)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\t// check and skip ftyp\n\n\tbuf := make([]byte, 8)\n\t_, err = io.ReadFull(r, buf)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tif !bytes.Equal(buf[4:], []byte{'f', 't', 'y', 'p'}) {\n\t\treturn 0, fmt.Errorf(\"ftyp box not found\")\n\t}\n\n\tftypSize := uint32(buf[0])<<24 | uint32(buf[1])<<16 | uint32(buf[2])<<8 | uint32(buf[3])\n\n\t_, err = r.Seek(int64(ftypSize), io.SeekStart)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\t// check and skip moov\n\n\t_, err = io.ReadFull(r, buf)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tif !bytes.Equal(buf[4:], []byte{'m', 'o', 'o', 'v'}) {\n\t\treturn 0, fmt.Errorf(\"moov box not found\")\n\t}\n\n\tmoovSize := uint32(buf[0])<<24 | uint32(buf[1])<<16 | uint32(buf[2])<<8 | uint32(buf[3])\n\n\t_, err = r.Seek(int64(moovSize)-8, io.SeekCurrent)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\t// find last valid moof and mdat\n\n\tlastMoofPos := int64(-1)\n\n\tfor {\n\t\tvar moofPos int64\n\t\tmoofPos, err = r.Seek(0, io.SeekCurrent)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\n\t\t_, err = io.ReadFull(r, buf)\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\n\t\tif !bytes.Equal(buf[4:], []byte{'m', 'o', 'o', 'f'}) {\n\t\t\tbreak\n\t\t}\n\n\t\tmoofSize := uint32(buf[0])<<24 | uint32(buf[1])<<16 | uint32(buf[2])<<8 | uint32(buf[3])\n\n\t\t_, err = r.Seek(int64(moofSize)-8, io.SeekCurrent)\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\n\t\t_, err = io.ReadFull(r, buf)\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\n\t\tif !bytes.Equal(buf[4:], []byte{'m', 'd', 'a', 't'}) {\n\t\t\tbreak\n\t\t}\n\n\t\tmdatSize := uint32(buf[0])<<24 | uint32(buf[1])<<16 | uint32(buf[2])<<8 | uint32(buf[3])\n\n\t\t_, err = r.Seek(int64(mdatSize)-8, io.SeekCurrent)\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\n\t\tlastMoofPos = moofPos\n\t}\n\n\tif lastMoofPos < 0 {\n\t\treturn 0, fmt.Errorf(\"no moof boxes found\")\n\t}\n\n\t// open last moof\n\n\t_, err = r.Seek(lastMoofPos+8, io.SeekStart)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\t_, err = io.ReadFull(r, buf)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\t// skip mfhd\n\n\tif !bytes.Equal(buf[4:], []byte{'m', 'f', 'h', 'd'}) {\n\t\treturn 0, fmt.Errorf(\"mfhd box not found\")\n\t}\n\n\t_, err = r.Seek(8, io.SeekCurrent)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tvar maxElapsed time.Duration\n\n\t// foreach traf\n\nouter:\n\tfor {\n\t\t_, err = io.ReadFull(r, buf)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\n\t\tswitch {\n\t\tcase bytes.Equal(buf[4:], []byte{'t', 'r', 'a', 'f'}):\n\t\tcase bytes.Equal(buf[4:], []byte{'m', 'd', 'a', 't'}):\n\t\t\tbreak outer\n\t\tdefault:\n\t\t\treturn 0, fmt.Errorf(\"unexpected box %x\", buf[4:8])\n\t\t}\n\n\t\t// parse tfhd\n\n\t\t_, err = io.ReadFull(r, buf)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\n\t\tif !bytes.Equal(buf[4:], []byte{'t', 'f', 'h', 'd'}) {\n\t\t\treturn 0, fmt.Errorf(\"tfhd box not found\")\n\t\t}\n\n\t\ttfhdSize := uint32(buf[0])<<24 | uint32(buf[1])<<16 | uint32(buf[2])<<8 | uint32(buf[3])\n\n\t\tbuf2 := make([]byte, tfhdSize-8)\n\n\t\t_, err = io.ReadFull(r, buf2)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\n\t\tvar tfhd amp4.Tfhd\n\t\t_, err = amp4.Unmarshal(bytes.NewReader(buf2), uint64(len(buf2)), &tfhd, amp4.Context{})\n\t\tif err != nil {\n\t\t\treturn 0, fmt.Errorf(\"invalid tfhd box: %w\", err)\n\t\t}\n\n\t\ttrack := findInitTrack(init.Tracks, int(tfhd.TrackID))\n\t\tif track == nil {\n\t\t\treturn 0, fmt.Errorf(\"invalid track ID: %v\", tfhd.TrackID)\n\t\t}\n\n\t\t// parse tfdt\n\n\t\t_, err = io.ReadFull(r, buf)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\n\t\tif !bytes.Equal(buf[4:], []byte{'t', 'f', 'd', 't'}) {\n\t\t\treturn 0, fmt.Errorf(\"tfdt box not found\")\n\t\t}\n\n\t\ttfdtSize := uint32(buf[0])<<24 | uint32(buf[1])<<16 | uint32(buf[2])<<8 | uint32(buf[3])\n\n\t\tbuf2 = make([]byte, tfdtSize-8)\n\n\t\t_, err = io.ReadFull(r, buf2)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\n\t\tvar tfdt amp4.Tfdt\n\t\t_, err = amp4.Unmarshal(bytes.NewReader(buf2), uint64(len(buf2)), &tfdt, amp4.Context{})\n\t\tif err != nil {\n\t\t\treturn 0, fmt.Errorf(\"invalid tfdt box: %w\", err)\n\t\t}\n\n\t\t// parse trun\n\n\t\t_, err = io.ReadFull(r, buf)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\n\t\tif !bytes.Equal(buf[4:], []byte{'t', 'r', 'u', 'n'}) {\n\t\t\treturn 0, fmt.Errorf(\"trun box not found\")\n\t\t}\n\n\t\ttrunSize := uint32(buf[0])<<24 | uint32(buf[1])<<16 | uint32(buf[2])<<8 | uint32(buf[3])\n\n\t\tbuf2 = make([]byte, trunSize-8)\n\n\t\t_, err = io.ReadFull(r, buf2)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\n\t\tvar trun amp4.Trun\n\t\t_, err = amp4.Unmarshal(bytes.NewReader(buf2), uint64(len(buf2)), &trun, amp4.Context{})\n\t\tif err != nil {\n\t\t\treturn 0, fmt.Errorf(\"invalid trun box: %w\", err)\n\t\t}\n\n\t\telapsed := int64(tfdt.BaseMediaDecodeTimeV1)\n\n\t\tfor _, entry := range trun.Entries {\n\t\t\telapsed += int64(entry.SampleDuration)\n\t\t}\n\n\t\telapsedGo := durationMp4ToGo(elapsed, track.TimeScale)\n\n\t\tif elapsedGo > maxElapsed {\n\t\t\tmaxElapsed = elapsedGo\n\t\t}\n\t}\n\n\treturn maxElapsed, nil\n}\n\nfunc segmentFMP4MuxParts(\n\tr readSeekerAt,\n\tstartDTS time.Duration,\n\tduration time.Duration,\n\ttracks []*fmp4.InitTrack,\n\tm muxer,\n) (time.Duration, error) {\n\tvar startDTSMP4 int64\n\tvar durationMP4 int64\n\tmoofOffset := uint64(0)\n\tvar tfhd *amp4.Tfhd\n\tvar tfdt *amp4.Tfdt\n\tvar timeScale uint32\n\tvar segmentDuration time.Duration\n\tbreakAtNextMdat := false\n\n\t_, err := amp4.ReadBoxStructure(r, func(h *amp4.ReadHandle) (any, error) {\n\t\tswitch h.BoxInfo.Type.String() {\n\t\tcase \"moof\":\n\t\t\tmoofOffset = h.BoxInfo.Offset\n\t\t\treturn h.Expand()\n\n\t\tcase \"traf\":\n\t\t\treturn h.Expand()\n\n\t\tcase \"tfhd\":\n\t\t\tbox, _, err := h.ReadPayload()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\ttfhd = box.(*amp4.Tfhd)\n\n\t\tcase \"tfdt\":\n\t\t\tbox, _, err := h.ReadPayload()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\ttfdt = box.(*amp4.Tfdt)\n\n\t\t\ttrack := findInitTrack(tracks, int(tfhd.TrackID))\n\t\t\tif track == nil {\n\t\t\t\treturn nil, fmt.Errorf(\"invalid track ID: %v\", tfhd.TrackID)\n\t\t\t}\n\n\t\t\tm.setTrack(int(tfhd.TrackID))\n\t\t\ttimeScale = track.TimeScale\n\t\t\tstartDTSMP4 = durationGoToMp4(startDTS, track.TimeScale)\n\t\t\tdurationMP4 = durationGoToMp4(duration, track.TimeScale)\n\n\t\tcase \"trun\":\n\t\t\tbox, _, err := h.ReadPayload()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\ttrun := box.(*amp4.Trun)\n\n\t\t\tdataOffset := moofOffset + uint64(trun.DataOffset)\n\t\t\tdts := int64(tfdt.BaseMediaDecodeTimeV1) + startDTSMP4\n\n\t\t\tfor _, e := range trun.Entries {\n\t\t\t\tif dts >= durationMP4 {\n\t\t\t\t\tbreakAtNextMdat = true\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tsampleOffset := dataOffset\n\t\t\t\tsampleSize := e.SampleSize\n\n\t\t\t\terr = m.writeSample(\n\t\t\t\t\tdts,\n\t\t\t\t\te.SampleCompositionTimeOffsetV1,\n\t\t\t\t\t(e.SampleFlags&sampleFlagIsNonSyncSample) != 0,\n\t\t\t\t\te.SampleSize,\n\t\t\t\t\tfunc() ([]byte, error) {\n\t\t\t\t\t\tpayload := make([]byte, sampleSize)\n\t\t\t\t\t\tn, err2 := r.ReadAt(payload, int64(sampleOffset))\n\t\t\t\t\t\tif err2 != nil {\n\t\t\t\t\t\t\treturn nil, err2\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif n != int(sampleSize) {\n\t\t\t\t\t\t\treturn nil, fmt.Errorf(\"partial read\")\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn payload, nil\n\t\t\t\t\t},\n\t\t\t\t)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tdataOffset += uint64(e.SampleSize)\n\t\t\t\tdts += int64(e.SampleDuration)\n\t\t\t}\n\n\t\t\tm.writeFinalDTS(dts)\n\n\t\t\tsegmentElapsed := durationMp4ToGo(dts-startDTSMP4, timeScale)\n\n\t\t\tif segmentElapsed > segmentDuration {\n\t\t\t\tsegmentDuration = segmentElapsed\n\t\t\t}\n\n\t\tcase \"mdat\":\n\t\t\tif breakAtNextMdat {\n\t\t\t\treturn nil, errTerminated\n\t\t\t}\n\t\t}\n\t\treturn nil, nil\n\t})\n\tif err != nil && !errors.Is(err, errTerminated) {\n\t\treturn 0, err\n\t}\n\n\treturn segmentDuration, nil\n}\n"
  },
  {
    "path": "internal/playback/segment_fmp4_test.go",
    "content": "package playback\n\nimport (\n\t\"io\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\tamp4 \"github.com/abema/go-mp4\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/fmp4\"\n\tmcodecs \"github.com/bluenviron/mediacommon/v2/pkg/formats/mp4/codecs\"\n\t\"github.com/bluenviron/mediamtx/internal/recordstore\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc writeBenchInit(f io.WriteSeeker) {\n\tinit := fmp4.Init{\n\t\tTracks: []*fmp4.InitTrack{\n\t\t\t{\n\t\t\t\tID:        1,\n\t\t\t\tTimeScale: 90000,\n\t\t\t\tCodec: &mcodecs.H264{\n\t\t\t\t\tSPS: test.FormatH264.SPS,\n\t\t\t\t\tPPS: test.FormatH264.PPS,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tID:        2,\n\t\t\t\tTimeScale: 90000,\n\t\t\t\tCodec: &mcodecs.MPEG4Audio{\n\t\t\t\t\tConfig: mpeg4audio.AudioSpecificConfig{\n\t\t\t\t\t\tType:         mpeg4audio.ObjectTypeAACLC,\n\t\t\t\t\t\tSampleRate:   48000,\n\t\t\t\t\t\tChannelCount: 2,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\terr := init.Marshal(f)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t_, err = f.Write([]byte{\n\t\t0x00, 0x00, 0x00, 0x10, 'm', 'o', 'o', 'f',\n\t})\n\tif err != nil {\n\t\tpanic(err)\n\t}\n}\n\nfunc BenchmarkFMP4ReadHeader(b *testing.B) {\n\tf, err := os.CreateTemp(os.TempDir(), \"mediamtx-playback-fmp4-\")\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\tdefer os.Remove(f.Name())\n\n\twriteBenchInit(f)\n\tf.Close()\n\n\tfor b.Loop() {\n\t\tfunc() {\n\t\t\tf, err = os.Open(f.Name())\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\tdefer f.Close()\n\n\t\t\t_, _, err = segmentFMP4ReadHeader(f)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t}()\n\t}\n}\n\nfunc TestSegmentFMP4CanBeConcatenated(t *testing.T) {\n\tbaseTime := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)\n\tstreamID1 := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}\n\tstreamID2 := [16]byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 17}\n\n\tbaseTracks := []*fmp4.InitTrack{\n\t\t{\n\t\t\tID:        1,\n\t\t\tTimeScale: 90000,\n\t\t\tCodec: &mcodecs.H264{\n\t\t\t\tSPS: test.FormatH264.SPS,\n\t\t\t\tPPS: test.FormatH264.PPS,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tID:        2,\n\t\t\tTimeScale: 48000,\n\t\t\tCodec: &mcodecs.MPEG4Audio{\n\t\t\t\tConfig: mpeg4audio.AudioSpecificConfig{\n\t\t\t\t\tType:         mpeg4audio.ObjectTypeAACLC,\n\t\t\t\t\tSampleRate:   48000,\n\t\t\t\t\tChannelCount: 2,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tdifferentTracks := []*fmp4.InitTrack{\n\t\t{\n\t\t\tID:        1,\n\t\t\tTimeScale: 90000,\n\t\t\tCodec: &mcodecs.H264{\n\t\t\t\tSPS: test.FormatH264.SPS,\n\t\t\t\tPPS: test.FormatH264.PPS,\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tt := range []struct {\n\t\tname     string\n\t\tprevInit *fmp4.Init\n\t\tprevEnd  time.Time\n\t\tcurInit  *fmp4.Init\n\t\tcurStart time.Time\n\t\twant     bool\n\t}{\n\t\t{\n\t\t\tname: \"with mtxi - consecutive segments, same stream\",\n\t\t\tprevInit: &fmp4.Init{\n\t\t\t\tTracks: baseTracks,\n\t\t\t\tUserData: []amp4.IBox{\n\t\t\t\t\t&recordstore.Mtxi{\n\t\t\t\t\t\tStreamID:      streamID1,\n\t\t\t\t\t\tSegmentNumber: 1,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tprevEnd: baseTime,\n\t\t\tcurInit: &fmp4.Init{\n\t\t\t\tTracks: baseTracks,\n\t\t\t\tUserData: []amp4.IBox{\n\t\t\t\t\t&recordstore.Mtxi{\n\t\t\t\t\t\tStreamID:      streamID1,\n\t\t\t\t\t\tSegmentNumber: 2,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcurStart: baseTime.Add(5 * time.Second),\n\t\t\twant:     true,\n\t\t},\n\t\t{\n\t\t\tname: \"with mtxi - non-consecutive segments, same stream\",\n\t\t\tprevInit: &fmp4.Init{\n\t\t\t\tTracks: baseTracks,\n\t\t\t\tUserData: []amp4.IBox{\n\t\t\t\t\t&recordstore.Mtxi{\n\t\t\t\t\t\tStreamID:      streamID1,\n\t\t\t\t\t\tSegmentNumber: 1,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tprevEnd: baseTime,\n\t\t\tcurInit: &fmp4.Init{\n\t\t\t\tTracks: baseTracks,\n\t\t\t\tUserData: []amp4.IBox{\n\t\t\t\t\t&recordstore.Mtxi{\n\t\t\t\t\t\tStreamID:      streamID1,\n\t\t\t\t\t\tSegmentNumber: 3,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcurStart: baseTime.Add(5 * time.Second),\n\t\t\twant:     false,\n\t\t},\n\t\t{\n\t\t\tname: \"with mtxi - consecutive segments, different streams\",\n\t\t\tprevInit: &fmp4.Init{\n\t\t\t\tTracks: baseTracks,\n\t\t\t\tUserData: []amp4.IBox{\n\t\t\t\t\t&recordstore.Mtxi{\n\t\t\t\t\t\tStreamID:      streamID1,\n\t\t\t\t\t\tSegmentNumber: 1,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tprevEnd: baseTime,\n\t\t\tcurInit: &fmp4.Init{\n\t\t\t\tTracks: baseTracks,\n\t\t\t\tUserData: []amp4.IBox{\n\t\t\t\t\t&recordstore.Mtxi{\n\t\t\t\t\t\tStreamID:      streamID2,\n\t\t\t\t\t\tSegmentNumber: 2,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcurStart: baseTime.Add(5 * time.Second),\n\t\t\twant:     false,\n\t\t},\n\t\t{\n\t\t\tname: \"prev has mtxi, current does not\",\n\t\t\tprevInit: &fmp4.Init{\n\t\t\t\tTracks: baseTracks,\n\t\t\t\tUserData: []amp4.IBox{\n\t\t\t\t\t&recordstore.Mtxi{\n\t\t\t\t\t\tStreamID:      streamID1,\n\t\t\t\t\t\tSegmentNumber: 1,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tprevEnd: baseTime,\n\t\t\tcurInit: &fmp4.Init{\n\t\t\t\tTracks:   baseTracks,\n\t\t\t\tUserData: []amp4.IBox{},\n\t\t\t},\n\t\t\tcurStart: baseTime.Add(5 * time.Second),\n\t\t\twant:     false,\n\t\t},\n\t\t{\n\t\t\tname: \"prev does not have mtxi, current has mtxi\",\n\t\t\tprevInit: &fmp4.Init{\n\t\t\t\tTracks:   baseTracks,\n\t\t\t\tUserData: []amp4.IBox{},\n\t\t\t},\n\t\t\tprevEnd: baseTime,\n\t\t\tcurInit: &fmp4.Init{\n\t\t\t\tTracks: baseTracks,\n\t\t\t\tUserData: []amp4.IBox{\n\t\t\t\t\t&recordstore.Mtxi{\n\t\t\t\t\t\tStreamID:      streamID1,\n\t\t\t\t\t\tSegmentNumber: 1,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tcurStart: baseTime.Add(5 * time.Second),\n\t\t\twant:     false,\n\t\t},\n\t\t{\n\t\t\tname: \"legacy mode - same tracks, within time tolerance\",\n\t\t\tprevInit: &fmp4.Init{\n\t\t\t\tTracks:   baseTracks,\n\t\t\t\tUserData: []amp4.IBox{},\n\t\t\t},\n\t\t\tprevEnd: baseTime.Add(10 * time.Second),\n\t\t\tcurInit: &fmp4.Init{\n\t\t\t\tTracks:   baseTracks,\n\t\t\t\tUserData: []amp4.IBox{},\n\t\t\t},\n\t\t\tcurStart: baseTime.Add(10 * time.Second),\n\t\t\twant:     true,\n\t\t},\n\t\t{\n\t\t\tname: \"legacy mode - same tracks, exactly at tolerance boundary (before)\",\n\t\t\tprevInit: &fmp4.Init{\n\t\t\t\tTracks:   baseTracks,\n\t\t\t\tUserData: []amp4.IBox{},\n\t\t\t},\n\t\t\tprevEnd: baseTime.Add(10 * time.Second),\n\t\t\tcurInit: &fmp4.Init{\n\t\t\t\tTracks:   baseTracks,\n\t\t\t\tUserData: []amp4.IBox{},\n\t\t\t},\n\t\t\tcurStart: baseTime.Add(9 * time.Second),\n\t\t\twant:     true,\n\t\t},\n\t\t{\n\t\t\tname: \"legacy mode - same tracks, exactly at tolerance boundary (after)\",\n\t\t\tprevInit: &fmp4.Init{\n\t\t\t\tTracks:   baseTracks,\n\t\t\t\tUserData: []amp4.IBox{},\n\t\t\t},\n\t\t\tprevEnd: baseTime.Add(10 * time.Second),\n\t\t\tcurInit: &fmp4.Init{\n\t\t\t\tTracks:   baseTracks,\n\t\t\t\tUserData: []amp4.IBox{},\n\t\t\t},\n\t\t\tcurStart: baseTime.Add(11 * time.Second),\n\t\t\twant:     true,\n\t\t},\n\t\t{\n\t\t\tname: \"legacy mode - same tracks, outside time tolerance (too early)\",\n\t\t\tprevInit: &fmp4.Init{\n\t\t\t\tTracks:   baseTracks,\n\t\t\t\tUserData: []amp4.IBox{},\n\t\t\t},\n\t\t\tprevEnd: baseTime.Add(10 * time.Second),\n\t\t\tcurInit: &fmp4.Init{\n\t\t\t\tTracks:   baseTracks,\n\t\t\t\tUserData: []amp4.IBox{},\n\t\t\t},\n\t\t\tcurStart: baseTime.Add(8*time.Second + 999*time.Millisecond),\n\t\t\twant:     false,\n\t\t},\n\t\t{\n\t\t\tname: \"legacy mode - same tracks, outside time tolerance (too late)\",\n\t\t\tprevInit: &fmp4.Init{\n\t\t\t\tTracks:   baseTracks,\n\t\t\t\tUserData: []amp4.IBox{},\n\t\t\t},\n\t\t\tprevEnd: baseTime.Add(10 * time.Second),\n\t\t\tcurInit: &fmp4.Init{\n\t\t\t\tTracks:   baseTracks,\n\t\t\t\tUserData: []amp4.IBox{},\n\t\t\t},\n\t\t\tcurStart: baseTime.Add(11*time.Second + 1*time.Millisecond),\n\t\t\twant:     false,\n\t\t},\n\t\t{\n\t\t\tname: \"legacy mode - different number of tracks\",\n\t\t\tprevInit: &fmp4.Init{\n\t\t\t\tTracks:   baseTracks,\n\t\t\t\tUserData: []amp4.IBox{},\n\t\t\t},\n\t\t\tprevEnd: baseTime,\n\t\t\tcurInit: &fmp4.Init{\n\t\t\t\tTracks:   differentTracks,\n\t\t\t\tUserData: []amp4.IBox{},\n\t\t\t},\n\t\t\tcurStart: baseTime.Add(5 * time.Second),\n\t\t\twant:     false,\n\t\t},\n\t\t{\n\t\t\tname: \"legacy mode - different track IDs\",\n\t\t\tprevInit: &fmp4.Init{\n\t\t\t\tTracks: []*fmp4.InitTrack{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:        1,\n\t\t\t\t\t\tTimeScale: 90000,\n\t\t\t\t\t\tCodec: &mcodecs.H264{\n\t\t\t\t\t\t\tSPS: test.FormatH264.SPS,\n\t\t\t\t\t\t\tPPS: test.FormatH264.PPS,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tUserData: []amp4.IBox{},\n\t\t\t},\n\t\t\tprevEnd: baseTime,\n\t\t\tcurInit: &fmp4.Init{\n\t\t\t\tTracks: []*fmp4.InitTrack{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:        2,\n\t\t\t\t\t\tTimeScale: 90000,\n\t\t\t\t\t\tCodec: &mcodecs.H264{\n\t\t\t\t\t\t\tSPS: test.FormatH264.SPS,\n\t\t\t\t\t\t\tPPS: test.FormatH264.PPS,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tUserData: []amp4.IBox{},\n\t\t\t},\n\t\t\tcurStart: baseTime.Add(5 * time.Second),\n\t\t\twant:     false,\n\t\t},\n\t\t{\n\t\t\tname: \"legacy mode - different time scales\",\n\t\t\tprevInit: &fmp4.Init{\n\t\t\t\tTracks: []*fmp4.InitTrack{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:        1,\n\t\t\t\t\t\tTimeScale: 90000,\n\t\t\t\t\t\tCodec: &mcodecs.H264{\n\t\t\t\t\t\t\tSPS: test.FormatH264.SPS,\n\t\t\t\t\t\t\tPPS: test.FormatH264.PPS,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tUserData: []amp4.IBox{},\n\t\t\t},\n\t\t\tprevEnd: baseTime,\n\t\t\tcurInit: &fmp4.Init{\n\t\t\t\tTracks: []*fmp4.InitTrack{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:        1,\n\t\t\t\t\t\tTimeScale: 48000,\n\t\t\t\t\t\tCodec: &mcodecs.H264{\n\t\t\t\t\t\t\tSPS: test.FormatH264.SPS,\n\t\t\t\t\t\t\tPPS: test.FormatH264.PPS,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tUserData: []amp4.IBox{},\n\t\t\t},\n\t\t\tcurStart: baseTime.Add(5 * time.Second),\n\t\t\twant:     false,\n\t\t},\n\t\t{\n\t\t\tname: \"legacy mode - different codec types\",\n\t\t\tprevInit: &fmp4.Init{\n\t\t\t\tTracks: []*fmp4.InitTrack{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:        1,\n\t\t\t\t\t\tTimeScale: 90000,\n\t\t\t\t\t\tCodec: &mcodecs.H264{\n\t\t\t\t\t\t\tSPS: test.FormatH264.SPS,\n\t\t\t\t\t\t\tPPS: test.FormatH264.PPS,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tUserData: []amp4.IBox{},\n\t\t\t},\n\t\t\tprevEnd: baseTime,\n\t\t\tcurInit: &fmp4.Init{\n\t\t\t\tTracks: []*fmp4.InitTrack{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:        1,\n\t\t\t\t\t\tTimeScale: 90000,\n\t\t\t\t\t\tCodec: &mcodecs.MPEG4Audio{\n\t\t\t\t\t\t\tConfig: mpeg4audio.AudioSpecificConfig{\n\t\t\t\t\t\t\t\tType:         mpeg4audio.ObjectTypeAACLC,\n\t\t\t\t\t\t\t\tSampleRate:   48000,\n\t\t\t\t\t\t\t\tChannelCount: 2,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tUserData: []amp4.IBox{},\n\t\t\t},\n\t\t\tcurStart: baseTime.Add(5 * time.Second),\n\t\t\twant:     false,\n\t\t},\n\t} {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tgot := segmentFMP4CanBeConcatenated(tt.prevInit, tt.prevEnd, tt.curInit, tt.curStart)\n\t\t\trequire.Equal(t, tt.want, got)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/playback/server.go",
    "content": "// Package playback contains the playback server.\npackage playback\n\nimport (\n\t\"net\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/auth\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/httpp\"\n\t\"github.com/gin-gonic/gin\"\n)\n\ntype serverAuthManager interface {\n\tAuthenticate(req *auth.Request) (string, *auth.Error)\n}\n\n// Server is the playback server.\ntype Server struct {\n\tAddress        string\n\tDumpPackets    bool\n\tEncryption     bool\n\tServerKey      string\n\tServerCert     string\n\tAllowOrigins   []string\n\tTrustedProxies conf.IPNetworks\n\tReadTimeout    conf.Duration\n\tWriteTimeout   conf.Duration\n\tPathConfs      map[string]*conf.Path\n\tAuthManager    serverAuthManager\n\tParent         logger.Writer\n\n\thttpServer *httpp.Server\n\tmutex      sync.RWMutex\n}\n\n// Initialize initializes Server.\nfunc (s *Server) Initialize() error {\n\trouter := gin.New()\n\trouter.SetTrustedProxies(s.TrustedProxies.ToTrustedProxies()) //nolint:errcheck\n\n\trouter.Use(s.middlewarePreflightRequests)\n\n\trouter.GET(\"/list\", s.onList)\n\trouter.GET(\"/get\", s.onGet)\n\n\ts.httpServer = &httpp.Server{\n\t\tAddress:           s.Address,\n\t\tAllowOrigins:      s.AllowOrigins,\n\t\tDumpPackets:       s.DumpPackets,\n\t\tDumpPacketsPrefix: \"playback_server_conn\",\n\t\tReadTimeout:       time.Duration(s.ReadTimeout),\n\t\tWriteTimeout:      time.Duration(s.WriteTimeout),\n\t\tEncryption:        s.Encryption,\n\t\tServerCert:        s.ServerCert,\n\t\tServerKey:         s.ServerKey,\n\t\tHandler:           router,\n\t\tParent:            s,\n\t}\n\terr := s.httpServer.Initialize()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ts.Log(logger.Info, \"listener opened on \"+s.Address)\n\n\treturn nil\n}\n\n// Close closes Server.\nfunc (s *Server) Close() {\n\ts.Log(logger.Info, \"listener is closing\")\n\ts.httpServer.Close()\n}\n\n// Log implements logger.Writer.\nfunc (s *Server) Log(level logger.Level, format string, args ...any) {\n\ts.Parent.Log(level, \"[playback] \"+format, args...)\n}\n\n// ReloadPathConfs is called by core.Core.\nfunc (s *Server) ReloadPathConfs(pathConfs map[string]*conf.Path) {\n\ts.mutex.Lock()\n\tdefer s.mutex.Unlock()\n\ts.PathConfs = pathConfs\n}\n\nfunc (s *Server) writeError(ctx *gin.Context, status int, err error) {\n\t// show error in logs\n\ts.Log(logger.Error, err.Error())\n\n\t// add error to response\n\tctx.String(status, err.Error())\n}\n\nfunc (s *Server) safeFindPathConf(name string) (*conf.Path, error) {\n\ts.mutex.RLock()\n\tdefer s.mutex.RUnlock()\n\n\tpathConf, _, err := conf.FindPathConf(s.PathConfs, name)\n\treturn pathConf, err\n}\n\nfunc (s *Server) middlewarePreflightRequests(ctx *gin.Context) {\n\tif ctx.Request.Method == http.MethodOptions &&\n\t\tctx.Request.Header.Get(\"Access-Control-Request-Method\") != \"\" {\n\t\tctx.Header(\"Access-Control-Allow-Methods\", \"OPTIONS, GET\")\n\t\tctx.Header(\"Access-Control-Allow-Headers\", \"Authorization\")\n\t\tctx.AbortWithStatus(http.StatusNoContent)\n\t\treturn\n\t}\n}\n\nfunc (s *Server) doAuth(ctx *gin.Context, pathName string) bool {\n\treq := &auth.Request{\n\t\tAction:      conf.AuthActionPlayback,\n\t\tPath:        pathName,\n\t\tQuery:       ctx.Request.URL.RawQuery,\n\t\tCredentials: httpp.Credentials(ctx.Request),\n\t\tIP:          net.ParseIP(ctx.ClientIP()),\n\t}\n\n\t_, err := s.AuthManager.Authenticate(req)\n\tif err != nil {\n\t\tif err.AskCredentials {\n\t\t\tctx.Header(\"WWW-Authenticate\", `Basic realm=\"mediamtx\"`)\n\t\t\tctx.AbortWithStatusJSON(http.StatusUnauthorized, &defs.APIError{\n\t\t\t\tStatus: defs.APIErrorStatusError,\n\t\t\t\tError:  \"authentication error\",\n\t\t\t})\n\t\t\treturn false\n\t\t}\n\n\t\ts.Log(logger.Info, \"connection %v failed to authenticate: %v\",\n\t\t\thttpp.RemoteAddr(ctx), err.Wrapped)\n\n\t\t// wait some seconds to delay brute force attacks\n\t\t<-time.After(auth.PauseAfterError)\n\n\t\tctx.AbortWithStatusJSON(http.StatusUnauthorized, &defs.APIError{\n\t\t\tStatus: defs.APIErrorStatusError,\n\t\t\tError:  \"authentication error\",\n\t\t})\n\t\treturn false\n\t}\n\n\treturn true\n}\n"
  },
  {
    "path": "internal/playback/server_test.go",
    "content": "package playback\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/auth\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPreflightRequest(t *testing.T) {\n\ts := &Server{\n\t\tAddress:      \"127.0.0.1:9996\",\n\t\tAllowOrigins: []string{\"*\"},\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tParent:       test.NilLogger,\n\t}\n\terr := s.Initialize()\n\trequire.NoError(t, err)\n\tdefer s.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\treq, err := http.NewRequest(http.MethodOptions, \"http://localhost:9996\", nil)\n\trequire.NoError(t, err)\n\n\treq.Header.Add(\"Access-Control-Request-Method\", \"GET\")\n\n\tres, err := hc.Do(req)\n\trequire.NoError(t, err)\n\tdefer res.Body.Close()\n\n\trequire.Equal(t, http.StatusNoContent, res.StatusCode)\n\n\tbyts, err := io.ReadAll(res.Body)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, \"*\", res.Header.Get(\"Access-Control-Allow-Origin\"))\n\trequire.Equal(t, \"true\", res.Header.Get(\"Access-Control-Allow-Credentials\"))\n\trequire.Equal(t, \"OPTIONS, GET\", res.Header.Get(\"Access-Control-Allow-Methods\"))\n\trequire.Equal(t, \"Authorization\", res.Header.Get(\"Access-Control-Allow-Headers\"))\n\trequire.Equal(t, byts, []byte{})\n}\n\nfunc TestAuthError(t *testing.T) {\n\tn := 0\n\n\ts := &Server{\n\t\tAddress:      \"127.0.0.1:9996\",\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tAuthManager: &test.AuthManager{\n\t\t\tAuthenticateImpl: func(req *auth.Request) (string, *auth.Error) {\n\t\t\t\tif req.Credentials.User == \"\" {\n\t\t\t\t\treturn \"\", &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf(\"auth error\")}\n\t\t\t\t}\n\t\t\t\treturn \"\", &auth.Error{Wrapped: fmt.Errorf(\"auth error\")}\n\t\t\t},\n\t\t},\n\t\tParent: test.Logger(func(l logger.Level, s string, i ...any) {\n\t\t\tif l == logger.Info {\n\t\t\t\tif n == 1 {\n\t\t\t\t\trequire.Regexp(t, \"failed to authenticate: auth error$\", fmt.Sprintf(s, i...))\n\t\t\t\t}\n\t\t\t\tn++\n\t\t\t}\n\t\t}),\n\t}\n\terr := s.Initialize()\n\trequire.NoError(t, err)\n\tdefer s.Close()\n\n\tu, err := url.Parse(\"http://localhost:9996/list\")\n\trequire.NoError(t, err)\n\n\tv := url.Values{}\n\tv.Set(\"path\", \"mypath\")\n\tu.RawQuery = v.Encode()\n\n\treq, err := http.NewRequest(http.MethodGet, u.String(), nil)\n\trequire.NoError(t, err)\n\n\tres, err := http.DefaultClient.Do(req)\n\trequire.NoError(t, err)\n\tdefer res.Body.Close()\n\n\trequire.Equal(t, http.StatusUnauthorized, res.StatusCode)\n\trequire.Equal(t, `Basic realm=\"mediamtx\"`, res.Header.Get(\"WWW-Authenticate\"))\n\n\tu, err = url.Parse(\"http://myuser:mypass@localhost:9996/list\")\n\trequire.NoError(t, err)\n\n\tv = url.Values{}\n\tv.Set(\"path\", \"mypath\")\n\tu.RawQuery = v.Encode()\n\n\treq, err = http.NewRequest(http.MethodGet, u.String(), nil)\n\trequire.NoError(t, err)\n\n\tstart := time.Now()\n\n\tres, err = http.DefaultClient.Do(req)\n\trequire.NoError(t, err)\n\tdefer res.Body.Close()\n\n\trequire.Greater(t, time.Since(start), 2*time.Second)\n\n\trequire.Equal(t, http.StatusUnauthorized, res.StatusCode)\n\n\trequire.Equal(t, 2, n)\n}\n"
  },
  {
    "path": "internal/pprof/pprof.go",
    "content": "// Package pprof contains a pprof exporter.\npackage pprof //nolint:revive\n\nimport (\n\t\"net\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/gin-contrib/pprof\"\n\t\"github.com/gin-gonic/gin\"\n\n\t\"github.com/bluenviron/mediamtx/internal/auth\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/httpp\"\n)\n\ntype pprofAuthManager interface {\n\tAuthenticate(req *auth.Request) (string, *auth.Error)\n}\n\ntype pprofParent interface {\n\tlogger.Writer\n}\n\n// PPROF is a pprof exporter.\ntype PPROF struct {\n\tAddress        string\n\tDumpPackets    bool\n\tEncryption     bool\n\tServerKey      string\n\tServerCert     string\n\tAllowOrigins   []string\n\tTrustedProxies conf.IPNetworks\n\tReadTimeout    conf.Duration\n\tWriteTimeout   conf.Duration\n\tAuthManager    pprofAuthManager\n\tParent         pprofParent\n\n\thttpServer *httpp.Server\n}\n\n// Initialize initializes PPROF.\nfunc (pp *PPROF) Initialize() error {\n\trouter := gin.New()\n\trouter.SetTrustedProxies(pp.TrustedProxies.ToTrustedProxies()) //nolint:errcheck\n\n\trouter.Use(pp.middlewarePreflightRequests)\n\trouter.Use(pp.middlewareAuth)\n\n\tpprof.Register(router)\n\n\tpp.httpServer = &httpp.Server{\n\t\tAddress:           pp.Address,\n\t\tDumpPackets:       pp.DumpPackets,\n\t\tAllowOrigins:      pp.AllowOrigins,\n\t\tDumpPacketsPrefix: \"pprof_server_conn\",\n\t\tReadTimeout:       time.Duration(pp.ReadTimeout),\n\t\tWriteTimeout:      time.Duration(pp.WriteTimeout),\n\t\tEncryption:        pp.Encryption,\n\t\tServerCert:        pp.ServerCert,\n\t\tServerKey:         pp.ServerKey,\n\t\tHandler:           router,\n\t\tParent:            pp,\n\t}\n\terr := pp.httpServer.Initialize()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpp.Log(logger.Info, \"listener opened on \"+pp.Address)\n\n\treturn nil\n}\n\n// Close closes PPROF.\nfunc (pp *PPROF) Close() {\n\tpp.Log(logger.Info, \"listener is closing\")\n\tpp.httpServer.Close()\n}\n\n// Log implements logger.Writer.\nfunc (pp *PPROF) Log(level logger.Level, format string, args ...any) {\n\tpp.Parent.Log(level, \"[pprof] \"+format, args...)\n}\n\nfunc (pp *PPROF) middlewarePreflightRequests(ctx *gin.Context) {\n\tif ctx.Request.Method == http.MethodOptions &&\n\t\tctx.Request.Header.Get(\"Access-Control-Request-Method\") != \"\" {\n\t\tctx.Header(\"Access-Control-Allow-Methods\", \"OPTIONS, GET\")\n\t\tctx.Header(\"Access-Control-Allow-Headers\", \"Authorization\")\n\t\tctx.AbortWithStatus(http.StatusNoContent)\n\t\treturn\n\t}\n}\n\nfunc (pp *PPROF) middlewareAuth(ctx *gin.Context) {\n\treq := &auth.Request{\n\t\tAction:      conf.AuthActionPprof,\n\t\tQuery:       ctx.Request.URL.RawQuery,\n\t\tCredentials: httpp.Credentials(ctx.Request),\n\t\tIP:          net.ParseIP(ctx.ClientIP()),\n\t}\n\n\t_, err := pp.AuthManager.Authenticate(req)\n\tif err != nil {\n\t\tif err.AskCredentials {\n\t\t\tctx.Header(\"WWW-Authenticate\", `Basic realm=\"mediamtx\"`)\n\t\t\tctx.AbortWithStatusJSON(http.StatusUnauthorized, &defs.APIError{\n\t\t\t\tStatus: defs.APIErrorStatusError,\n\t\t\t\tError:  \"authentication error\",\n\t\t\t})\n\t\t\treturn\n\t\t}\n\n\t\tpp.Log(logger.Info, \"connection %v failed to authenticate: %v\", httpp.RemoteAddr(ctx), err.Wrapped)\n\n\t\t// wait some seconds to delay brute force attacks\n\t\t<-time.After(auth.PauseAfterError)\n\n\t\tctx.AbortWithStatusJSON(http.StatusUnauthorized, &defs.APIError{\n\t\t\tStatus: defs.APIErrorStatusError,\n\t\t\tError:  \"authentication error\",\n\t\t})\n\t\treturn\n\t}\n}\n"
  },
  {
    "path": "internal/pprof/pprof_test.go",
    "content": "package pprof //nolint:revive\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/auth\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPreflightRequest(t *testing.T) {\n\ts := &PPROF{\n\t\tAddress:      \"127.0.0.1:9999\",\n\t\tAllowOrigins: []string{\"*\"},\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tParent:       test.NilLogger,\n\t}\n\terr := s.Initialize()\n\trequire.NoError(t, err)\n\tdefer s.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\treq, err := http.NewRequest(http.MethodOptions, \"http://localhost:9999\", nil)\n\trequire.NoError(t, err)\n\n\treq.Header.Add(\"Access-Control-Request-Method\", \"GET\")\n\n\tres, err := hc.Do(req)\n\trequire.NoError(t, err)\n\tdefer res.Body.Close()\n\n\trequire.Equal(t, http.StatusNoContent, res.StatusCode)\n\n\tbyts, err := io.ReadAll(res.Body)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, \"*\", res.Header.Get(\"Access-Control-Allow-Origin\"))\n\trequire.Equal(t, \"true\", res.Header.Get(\"Access-Control-Allow-Credentials\"))\n\trequire.Equal(t, \"OPTIONS, GET\", res.Header.Get(\"Access-Control-Allow-Methods\"))\n\trequire.Equal(t, \"Authorization\", res.Header.Get(\"Access-Control-Allow-Headers\"))\n\trequire.Equal(t, byts, []byte{})\n}\n\nfunc TestPprof(t *testing.T) {\n\tchecked := false\n\n\ts := &PPROF{\n\t\tAddress:      \"127.0.0.1:9999\",\n\t\tAllowOrigins: []string{\"*\"},\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tAuthManager: &test.AuthManager{\n\t\t\tAuthenticateImpl: func(req *auth.Request) (string, *auth.Error) {\n\t\t\t\trequire.Equal(t, conf.AuthActionPprof, req.Action)\n\t\t\t\trequire.Equal(t, \"myuser\", req.Credentials.User)\n\t\t\t\trequire.Equal(t, \"mypass\", req.Credentials.Pass)\n\t\t\t\tchecked = true\n\t\t\t\treturn req.Credentials.User, nil\n\t\t\t},\n\t\t},\n\t\tParent: test.NilLogger,\n\t}\n\terr := s.Initialize()\n\trequire.NoError(t, err)\n\tdefer s.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\treq, err := http.NewRequest(http.MethodGet, \"http://myuser:mypass@127.0.0.1:9999/debug/pprof/heap\", nil)\n\trequire.NoError(t, err)\n\n\tres, err := hc.Do(req)\n\trequire.NoError(t, err)\n\tdefer res.Body.Close()\n\n\trequire.Equal(t, http.StatusOK, res.StatusCode)\n\n\tbyts, err := io.ReadAll(res.Body)\n\trequire.NoError(t, err)\n\trequire.NotEmpty(t, byts)\n\n\trequire.True(t, checked)\n}\n\nfunc TestAuthError(t *testing.T) {\n\tn := 0\n\n\ts := &PPROF{\n\t\tAddress:      \"127.0.0.1:9999\",\n\t\tAllowOrigins: []string{\"*\"},\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tAuthManager: &test.AuthManager{\n\t\t\tAuthenticateImpl: func(req *auth.Request) (string, *auth.Error) {\n\t\t\t\tif req.Credentials.User == \"\" {\n\t\t\t\t\treturn \"\", &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf(\"auth error\")}\n\t\t\t\t}\n\t\t\t\treturn \"\", &auth.Error{Wrapped: fmt.Errorf(\"auth error\")}\n\t\t\t},\n\t\t},\n\t\tParent: test.Logger(func(l logger.Level, s string, i ...any) {\n\t\t\tif l == logger.Info {\n\t\t\t\tif n == 1 {\n\t\t\t\t\trequire.Regexp(t, \"failed to authenticate: auth error$\", fmt.Sprintf(s, i...))\n\t\t\t\t}\n\t\t\t\tn++\n\t\t\t}\n\t\t}),\n\t}\n\terr := s.Initialize()\n\trequire.NoError(t, err)\n\tdefer s.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\treq, err := http.NewRequest(http.MethodGet, \"http://127.0.0.1:9999/debug/pprof/heap\", nil)\n\trequire.NoError(t, err)\n\n\tres, err := hc.Do(req)\n\trequire.NoError(t, err)\n\tdefer res.Body.Close()\n\n\trequire.Equal(t, http.StatusUnauthorized, res.StatusCode)\n\trequire.Equal(t, `Basic realm=\"mediamtx\"`, res.Header.Get(\"WWW-Authenticate\"))\n\n\treq, err = http.NewRequest(http.MethodGet, \"http://myuser:mypass@127.0.0.1:9999/debug/pprof/heap\", nil)\n\trequire.NoError(t, err)\n\n\tres, err = hc.Do(req)\n\trequire.NoError(t, err)\n\tdefer res.Body.Close()\n\n\trequire.Equal(t, http.StatusUnauthorized, res.StatusCode)\n\n\trequire.Equal(t, 2, n)\n}\n"
  },
  {
    "path": "internal/protocols/hls/from_stream.go",
    "content": "// Package hls contains HLS utilities.\npackage hls\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"slices\"\n\n\t\"github.com/bluenviron/gohlslib/v2\"\n\t\"github.com/bluenviron/gohlslib/v2/pkg/codecs\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n)\n\n// ErrNoSupportedCodecs is returned by FromStream when there are no supported codecs.\nvar ErrNoSupportedCodecs = errors.New(\n\t\"the stream doesn't contain any supported codec, which are currently AV1, VP9, H265, H264, Opus, MPEG-4 Audio\")\n\nfunc setupVideoTrack(\n\tdesc *description.Session,\n\tr *stream.Reader,\n\tmuxer *gohlslib.Muxer,\n) {\n\taddTrack := func(\n\t\tmedia *description.Media,\n\t\tforma format.Format,\n\t\ttrack *gohlslib.Track,\n\t\tonData stream.OnDataFunc,\n\t) {\n\t\tmuxer.Tracks = append(muxer.Tracks, track)\n\t\tr.OnData(media, forma, onData)\n\t}\n\n\tvar videoFormatAV1 *format.AV1\n\tvideoMedia := desc.FindFormat(&videoFormatAV1)\n\n\tif videoFormatAV1 != nil {\n\t\ttrack := &gohlslib.Track{\n\t\t\tCodec:     &codecs.AV1{},\n\t\t\tClockRate: videoFormatAV1.ClockRate(),\n\t\t}\n\n\t\taddTrack(\n\t\t\tvideoMedia,\n\t\t\tvideoFormatAV1,\n\t\t\ttrack,\n\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\tif u.NilPayload() {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\terr := muxer.WriteAV1(\n\t\t\t\t\ttrack,\n\t\t\t\t\tu.NTP,\n\t\t\t\t\tu.PTS, // no conversion is needed since we set gohlslib.Track.ClockRate = format.ClockRate\n\t\t\t\t\tu.Payload.(unit.PayloadAV1))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"muxer error: %w\", err)\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\treturn\n\t}\n\n\tvar videoFormatVP9 *format.VP9\n\tvideoMedia = desc.FindFormat(&videoFormatVP9)\n\n\tif videoFormatVP9 != nil {\n\t\ttrack := &gohlslib.Track{\n\t\t\tCodec:     &codecs.VP9{},\n\t\t\tClockRate: videoFormatVP9.ClockRate(),\n\t\t}\n\n\t\taddTrack(\n\t\t\tvideoMedia,\n\t\t\tvideoFormatVP9,\n\t\t\ttrack,\n\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\tif u.NilPayload() {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\terr := muxer.WriteVP9(\n\t\t\t\t\ttrack,\n\t\t\t\t\tu.NTP,\n\t\t\t\t\tu.PTS, // no conversion is needed since we set gohlslib.Track.ClockRate = format.ClockRate\n\t\t\t\t\tu.Payload.(unit.PayloadVP9))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"muxer error: %w\", err)\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\treturn\n\t}\n\n\tvar videoFormatH265 *format.H265\n\tvideoMedia = desc.FindFormat(&videoFormatH265)\n\n\tif videoFormatH265 != nil {\n\t\tvps, sps, pps := videoFormatH265.SafeParams()\n\t\ttrack := &gohlslib.Track{\n\t\t\tCodec: &codecs.H265{\n\t\t\t\tVPS: vps,\n\t\t\t\tSPS: sps,\n\t\t\t\tPPS: pps,\n\t\t\t},\n\t\t\tClockRate: videoFormatH265.ClockRate(),\n\t\t}\n\n\t\taddTrack(\n\t\t\tvideoMedia,\n\t\t\tvideoFormatH265,\n\t\t\ttrack,\n\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\tif u.NilPayload() {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\terr := muxer.WriteH265(\n\t\t\t\t\ttrack,\n\t\t\t\t\tu.NTP,\n\t\t\t\t\tu.PTS, // no conversion is needed since we set gohlslib.Track.ClockRate = format.ClockRate\n\t\t\t\t\tu.Payload.(unit.PayloadH265))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"muxer error: %w\", err)\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\treturn\n\t}\n\n\tvar videoFormatH264 *format.H264\n\tvideoMedia = desc.FindFormat(&videoFormatH264)\n\n\tif videoFormatH264 != nil {\n\t\tsps, pps := videoFormatH264.SafeParams()\n\t\ttrack := &gohlslib.Track{\n\t\t\tCodec: &codecs.H264{\n\t\t\t\tSPS: sps,\n\t\t\t\tPPS: pps,\n\t\t\t},\n\t\t\tClockRate: videoFormatH264.ClockRate(),\n\t\t}\n\n\t\taddTrack(\n\t\t\tvideoMedia,\n\t\t\tvideoFormatH264,\n\t\t\ttrack,\n\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\tif u.NilPayload() {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\terr := muxer.WriteH264(\n\t\t\t\t\ttrack,\n\t\t\t\t\tu.NTP,\n\t\t\t\t\tu.PTS, // no conversion is needed since we set gohlslib.Track.ClockRate = format.ClockRate\n\t\t\t\t\tu.Payload.(unit.PayloadH264))\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"muxer error: %w\", err)\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\treturn\n\t}\n}\n\nfunc setupAudioTracks(\n\tdesc *description.Session,\n\tr *stream.Reader,\n\tmuxer *gohlslib.Muxer,\n) {\n\taddTrack := func(\n\t\tmedi *description.Media,\n\t\tforma format.Format,\n\t\ttrack *gohlslib.Track,\n\t\tonData stream.OnDataFunc,\n\t) {\n\t\tmuxer.Tracks = append(muxer.Tracks, track)\n\t\tr.OnData(medi, forma, onData)\n\t}\n\n\tfor _, media := range desc.Medias {\n\t\tfor _, forma := range media.Formats {\n\t\t\tswitch forma := forma.(type) {\n\t\t\tcase *format.Opus:\n\t\t\t\ttrack := &gohlslib.Track{\n\t\t\t\t\tCodec: &codecs.Opus{\n\t\t\t\t\t\tChannelCount: forma.ChannelCount,\n\t\t\t\t\t},\n\t\t\t\t\tClockRate: forma.ClockRate(),\n\t\t\t\t}\n\n\t\t\t\taddTrack(\n\t\t\t\t\tmedia,\n\t\t\t\t\tforma,\n\t\t\t\t\ttrack,\n\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\terr := muxer.WriteOpus(\n\t\t\t\t\t\t\ttrack,\n\t\t\t\t\t\t\tu.NTP,\n\t\t\t\t\t\t\tu.PTS, // no conversion is needed since we set gohlslib.Track.ClockRate = format.ClockRate\n\t\t\t\t\t\t\tu.Payload.(unit.PayloadOpus))\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"muxer error: %w\", err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t})\n\n\t\t\tcase *format.MPEG4Audio:\n\t\t\t\ttrack := &gohlslib.Track{\n\t\t\t\t\tCodec: &codecs.MPEG4Audio{\n\t\t\t\t\t\tConfig: *forma.Config,\n\t\t\t\t\t},\n\t\t\t\t\tClockRate: forma.ClockRate(),\n\t\t\t\t}\n\n\t\t\t\taddTrack(\n\t\t\t\t\tmedia,\n\t\t\t\t\tforma,\n\t\t\t\t\ttrack,\n\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\terr := muxer.WriteMPEG4Audio(\n\t\t\t\t\t\t\ttrack,\n\t\t\t\t\t\t\tu.NTP,\n\t\t\t\t\t\t\tu.PTS, // no conversion is needed since we set gohlslib.Track.ClockRate = format.ClockRate\n\t\t\t\t\t\t\tu.Payload.(unit.PayloadMPEG4Audio))\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"muxer error: %w\", err)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t})\n\n\t\t\tcase *format.MPEG4AudioLATM:\n\t\t\t\tif !forma.CPresent {\n\t\t\t\t\ttrack := &gohlslib.Track{\n\t\t\t\t\t\tCodec: &codecs.MPEG4Audio{\n\t\t\t\t\t\t\tConfig: *forma.StreamMuxConfig.Programs[0].Layers[0].AudioSpecificConfig,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tClockRate: forma.ClockRate(),\n\t\t\t\t\t}\n\n\t\t\t\t\taddTrack(\n\t\t\t\t\t\tmedia,\n\t\t\t\t\t\tforma,\n\t\t\t\t\t\ttrack,\n\t\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tvar ame mpeg4audio.AudioMuxElement\n\t\t\t\t\t\t\tame.StreamMuxConfig = forma.StreamMuxConfig\n\t\t\t\t\t\t\terr := ame.Unmarshal(u.Payload.(unit.PayloadMPEG4AudioLATM))\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\treturn muxer.WriteMPEG4Audio(\n\t\t\t\t\t\t\t\ttrack,\n\t\t\t\t\t\t\t\tu.NTP,\n\t\t\t\t\t\t\t\tu.PTS, // no conversion is needed since we set gohlslib.Track.ClockRate = format.ClockRate\n\t\t\t\t\t\t\t\t[][]byte{ame.Payloads[0][0][0]})\n\t\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\n// FromStream maps a MediaMTX stream to a HLS muxer.\nfunc FromStream(\n\tdesc *description.Session,\n\tr *stream.Reader,\n\tmuxer *gohlslib.Muxer,\n) error {\n\tsetupVideoTrack(\n\t\tdesc,\n\t\tr,\n\t\tmuxer,\n\t)\n\n\tsetupAudioTracks(\n\t\tdesc,\n\t\tr,\n\t\tmuxer,\n\t)\n\n\tif len(muxer.Tracks) == 0 {\n\t\treturn ErrNoSupportedCodecs\n\t}\n\n\tsetuppedFormats := r.Formats()\n\n\tn := 1\n\tfor _, media := range desc.Medias {\n\t\tfor _, forma := range media.Formats {\n\t\t\tif !slices.Contains(setuppedFormats, forma) {\n\t\t\t\tr.Parent.Log(logger.Warn, \"skipping track %d (%s)\", n, forma.Codec())\n\t\t\t}\n\t\t\tn++\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/protocols/hls/from_stream_test.go",
    "content": "package hls\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/bluenviron/gohlslib/v2\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestFromStreamNoSupportedCodecs(t *testing.T) {\n\tdesc := &description.Session{Medias: []*description.Media{{\n\t\tType:    description.MediaTypeVideo,\n\t\tFormats: []format.Format{&format.VP8{}},\n\t}}}\n\n\tr := &stream.Reader{\n\t\tParent: test.Logger(func(logger.Level, string, ...any) {\n\t\t\tt.Error(\"should not happen\")\n\t\t}),\n\t}\n\n\tm := &gohlslib.Muxer{}\n\n\terr := FromStream(desc, r, m)\n\trequire.Equal(t, ErrNoSupportedCodecs, err)\n}\n\nfunc TestFromStreamSkipUnsupportedTracks(t *testing.T) {\n\tdesc := &description.Session{Medias: []*description.Media{\n\t\t{\n\t\t\tType:    description.MediaTypeVideo,\n\t\t\tFormats: []format.Format{&format.VP9{}},\n\t\t},\n\t\t{\n\t\t\tType:    description.MediaTypeVideo,\n\t\t\tFormats: []format.Format{&format.VP8{}},\n\t\t},\n\t\t{\n\t\t\tType:    description.MediaTypeAudio,\n\t\t\tFormats: []format.Format{&format.MPEG1Audio{}},\n\t\t},\n\t}}\n\n\tm := &gohlslib.Muxer{}\n\n\tn := 0\n\n\tr := &stream.Reader{\n\t\tParent: test.Logger(func(l logger.Level, format string, args ...any) {\n\t\t\trequire.Equal(t, logger.Warn, l)\n\t\t\tswitch n {\n\t\t\tcase 0:\n\t\t\t\trequire.Equal(t, \"skipping track 2 (VP8)\", fmt.Sprintf(format, args...))\n\t\t\tcase 1:\n\t\t\t\trequire.Equal(t, \"skipping track 3 (MPEG-1/2 Audio)\", fmt.Sprintf(format, args...))\n\t\t\t}\n\t\t\tn++\n\t\t}),\n\t}\n\n\terr := FromStream(desc, r, m)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, 2, n)\n}\n"
  },
  {
    "path": "internal/protocols/hls/to_stream.go",
    "content": "package hls\n\nimport (\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gohlslib/v2\"\n\t\"github.com/bluenviron/gohlslib/v2/pkg/codecs\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/ntpestimator\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n)\n\ntype ntpState int\n\nconst (\n\tntpStateInitial ntpState = iota\n\tntpStateUnavailable\n\tntpStateAvailable\n\tntpStateReplace\n)\n\nfunc multiplyAndDivide(v, m, d int64) int64 {\n\tsecs := v / d\n\tdec := v % d\n\treturn (secs*m + dec*m/d)\n}\n\n// ToStream maps a HLS stream to a MediaMTX stream.\nfunc ToStream(\n\tc *gohlslib.Client,\n\ttracks []*gohlslib.Track,\n\tpathConf *conf.Path,\n\tsubStream **stream.SubStream,\n) ([]*description.Media, error) {\n\tvar ntpStat ntpState\n\tvar ntpStatMutex sync.Mutex\n\n\tif !pathConf.UseAbsoluteTimestamp {\n\t\tntpStat = ntpStateReplace\n\t}\n\n\tvar medias []*description.Media //nolint:prealloc\n\n\tfor _, track := range tracks {\n\t\tctrack := track\n\t\tntpEstimator := &ntpestimator.Estimator{ClockRate: track.ClockRate}\n\n\t\thandleNTP := func(pts int64) time.Time {\n\t\t\tntpStatMutex.Lock()\n\t\t\tdefer ntpStatMutex.Unlock()\n\n\t\t\tswitch ntpStat {\n\t\t\tcase ntpStateInitial:\n\t\t\t\tntp, avail := c.AbsoluteTime(ctrack)\n\t\t\t\tif !avail {\n\t\t\t\t\tntpStat = ntpStateUnavailable\n\t\t\t\t\treturn ntpEstimator.Estimate(pts)\n\t\t\t\t}\n\t\t\t\tntpStat = ntpStateAvailable\n\t\t\t\treturn ntp\n\n\t\t\tcase ntpStateAvailable:\n\t\t\t\tntp, avail := c.AbsoluteTime(ctrack)\n\t\t\t\tif !avail {\n\t\t\t\t\tpanic(\"should not happen\")\n\t\t\t\t}\n\t\t\t\treturn ntp\n\n\t\t\tcase ntpStateUnavailable:\n\t\t\t\t_, avail := c.AbsoluteTime(ctrack)\n\t\t\t\tif avail {\n\t\t\t\t\t// absolute timestamp appeared after stream started, we are not using it\n\t\t\t\t\tntpStat = ntpStateReplace\n\t\t\t\t}\n\t\t\t\treturn ntpEstimator.Estimate(pts)\n\n\t\t\tdefault: // ntpStateReplace\n\t\t\t\treturn ntpEstimator.Estimate(pts)\n\t\t\t}\n\t\t}\n\n\t\tvar medi *description.Media\n\n\t\tswitch tcodec := ctrack.Codec.(type) {\n\t\tcase *codecs.AV1:\n\t\t\tmedi = &description.Media{\n\t\t\t\tType: description.MediaTypeVideo,\n\t\t\t\tFormats: []format.Format{&format.AV1{\n\t\t\t\t\tPayloadTyp: 96,\n\t\t\t\t}},\n\t\t\t}\n\t\t\tnewClockRate := medi.Formats[0].ClockRate()\n\n\t\t\tc.OnDataAV1(ctrack, func(pts int64, tu [][]byte) {\n\t\t\t\t(*subStream).WriteUnit(medi, medi.Formats[0], &unit.Unit{\n\t\t\t\t\tNTP:     handleNTP(pts),\n\t\t\t\t\tPTS:     multiplyAndDivide(pts, int64(newClockRate), int64(ctrack.ClockRate)),\n\t\t\t\t\tPayload: unit.PayloadAV1(tu),\n\t\t\t\t})\n\t\t\t})\n\n\t\tcase *codecs.VP9:\n\t\t\tmedi = &description.Media{\n\t\t\t\tType: description.MediaTypeVideo,\n\t\t\t\tFormats: []format.Format{&format.VP9{\n\t\t\t\t\tPayloadTyp: 96,\n\t\t\t\t}},\n\t\t\t}\n\t\t\tnewClockRate := medi.Formats[0].ClockRate()\n\n\t\t\tc.OnDataVP9(ctrack, func(pts int64, frame []byte) {\n\t\t\t\t(*subStream).WriteUnit(medi, medi.Formats[0], &unit.Unit{\n\t\t\t\t\tNTP:     handleNTP(pts),\n\t\t\t\t\tPTS:     multiplyAndDivide(pts, int64(newClockRate), int64(ctrack.ClockRate)),\n\t\t\t\t\tPayload: unit.PayloadVP9(frame),\n\t\t\t\t})\n\t\t\t})\n\n\t\tcase *codecs.H265:\n\t\t\tmedi = &description.Media{\n\t\t\t\tType: description.MediaTypeVideo,\n\t\t\t\tFormats: []format.Format{&format.H265{\n\t\t\t\t\tPayloadTyp: 96,\n\t\t\t\t\tVPS:        tcodec.VPS,\n\t\t\t\t\tSPS:        tcodec.SPS,\n\t\t\t\t\tPPS:        tcodec.PPS,\n\t\t\t\t}},\n\t\t\t}\n\t\t\tnewClockRate := medi.Formats[0].ClockRate()\n\n\t\t\tc.OnDataH26x(ctrack, func(pts int64, _ int64, au [][]byte) {\n\t\t\t\t(*subStream).WriteUnit(medi, medi.Formats[0], &unit.Unit{\n\t\t\t\t\tNTP:     handleNTP(pts),\n\t\t\t\t\tPTS:     multiplyAndDivide(pts, int64(newClockRate), int64(ctrack.ClockRate)),\n\t\t\t\t\tPayload: unit.PayloadH265(au),\n\t\t\t\t})\n\t\t\t})\n\n\t\tcase *codecs.H264:\n\t\t\tmedi = &description.Media{\n\t\t\t\tType: description.MediaTypeVideo,\n\t\t\t\tFormats: []format.Format{&format.H264{\n\t\t\t\t\tPayloadTyp:        96,\n\t\t\t\t\tPacketizationMode: 1,\n\t\t\t\t\tSPS:               tcodec.SPS,\n\t\t\t\t\tPPS:               tcodec.PPS,\n\t\t\t\t}},\n\t\t\t}\n\t\t\tnewClockRate := medi.Formats[0].ClockRate()\n\n\t\t\tc.OnDataH26x(ctrack, func(pts int64, _ int64, au [][]byte) {\n\t\t\t\t(*subStream).WriteUnit(medi, medi.Formats[0], &unit.Unit{\n\t\t\t\t\tNTP:     handleNTP(pts),\n\t\t\t\t\tPTS:     multiplyAndDivide(pts, int64(newClockRate), int64(ctrack.ClockRate)),\n\t\t\t\t\tPayload: unit.PayloadH264(au),\n\t\t\t\t})\n\t\t\t})\n\n\t\tcase *codecs.Opus:\n\t\t\tmedi = &description.Media{\n\t\t\t\tType: description.MediaTypeAudio,\n\t\t\t\tFormats: []format.Format{&format.Opus{\n\t\t\t\t\tPayloadTyp:   96,\n\t\t\t\t\tChannelCount: tcodec.ChannelCount,\n\t\t\t\t}},\n\t\t\t}\n\t\t\tnewClockRate := medi.Formats[0].ClockRate()\n\n\t\t\tc.OnDataOpus(ctrack, func(pts int64, packets [][]byte) {\n\t\t\t\t(*subStream).WriteUnit(medi, medi.Formats[0], &unit.Unit{\n\t\t\t\t\tNTP:     handleNTP(pts),\n\t\t\t\t\tPTS:     multiplyAndDivide(pts, int64(newClockRate), int64(ctrack.ClockRate)),\n\t\t\t\t\tPayload: unit.PayloadOpus(packets),\n\t\t\t\t})\n\t\t\t})\n\n\t\tcase *codecs.MPEG4Audio:\n\t\t\tmedi = &description.Media{\n\t\t\t\tType: description.MediaTypeAudio,\n\t\t\t\tFormats: []format.Format{&format.MPEG4Audio{\n\t\t\t\t\tPayloadTyp:       96,\n\t\t\t\t\tSizeLength:       13,\n\t\t\t\t\tIndexLength:      3,\n\t\t\t\t\tIndexDeltaLength: 3,\n\t\t\t\t\tConfig:           &tcodec.Config,\n\t\t\t\t}},\n\t\t\t}\n\t\t\tnewClockRate := medi.Formats[0].ClockRate()\n\n\t\t\tc.OnDataMPEG4Audio(ctrack, func(pts int64, aus [][]byte) {\n\t\t\t\t(*subStream).WriteUnit(medi, medi.Formats[0], &unit.Unit{\n\t\t\t\t\tNTP:     handleNTP(pts),\n\t\t\t\t\tPTS:     multiplyAndDivide(pts, int64(newClockRate), int64(ctrack.ClockRate)),\n\t\t\t\t\tPayload: unit.PayloadMPEG4Audio(aus),\n\t\t\t\t})\n\t\t\t})\n\n\t\tdefault:\n\t\t\tpanic(\"should not happen\")\n\t\t}\n\n\t\tmedias = append(medias, medi)\n\t}\n\n\tif len(medias) == 0 {\n\t\treturn nil, ErrNoSupportedCodecs\n\t}\n\n\treturn medias, nil\n}\n"
  },
  {
    "path": "internal/protocols/hls/to_stream_test.go",
    "content": "package hls\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gohlslib/v2\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts\"\n\ttscodecs \"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts/codecs\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestToStreamNoSupportedCodecs(t *testing.T) {\n\t_, err := ToStream(nil, []*gohlslib.Track{}, &conf.Path{}, nil)\n\trequire.Equal(t, ErrNoSupportedCodecs, err)\n}\n\n// this is impossible to test since currently we support all gohlslib.Tracks.\n// func TestToStreamSkipUnsupportedTracks(t *testing.T)\n\nfunc TestToStream(t *testing.T) {\n\ttrack1 := &mpegts.Track{\n\t\tCodec: &tscodecs.H264{},\n\t}\n\n\ts := &http.Server{\n\t\tHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tswitch {\n\t\t\tcase r.Method == http.MethodGet && r.URL.Path == \"/stream.m3u8\":\n\t\t\t\tw.Header().Set(\"Content-Type\", `application/vnd.apple.mpegurl`)\n\t\t\t\tw.Write([]byte(\"#EXTM3U\\n\" +\n\t\t\t\t\t\"#EXT-X-VERSION:3\\n\" +\n\t\t\t\t\t\"#EXT-X-ALLOW-CACHE:NO\\n\" +\n\t\t\t\t\t\"#EXT-X-TARGETDURATION:2\\n\" +\n\t\t\t\t\t\"#EXT-X-MEDIA-SEQUENCE:0\\n\" +\n\t\t\t\t\t\"#EXT-X-PROGRAM-DATE-TIME:2018-05-20T08:17:15Z\\n\" +\n\t\t\t\t\t\"#EXTINF:2,\\n\" +\n\t\t\t\t\t\"segment1.ts\\n\" +\n\t\t\t\t\t\"#EXTINF:2,\\n\" +\n\t\t\t\t\t\"segment2.ts\\n\" +\n\t\t\t\t\t\"#EXTINF:2,\\n\" +\n\t\t\t\t\t\"segment3.ts\\n\" +\n\t\t\t\t\t\"#EXT-X-ENDLIST\\n\"))\n\n\t\t\tcase r.Method == http.MethodGet && r.URL.Path == \"/segment1.ts\":\n\t\t\t\tw.Header().Set(\"Content-Type\", `video/MP2T`)\n\n\t\t\t\tw := &mpegts.Writer{W: w, Tracks: []*mpegts.Track{track1}}\n\t\t\t\terr := w.Initialize()\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\terr = w.WriteH264(track1, 2*90000, 2*90000, [][]byte{\n\t\t\t\t\t{7, 1, 2, 3}, // SPS\n\t\t\t\t\t{8},          // PPS\n\t\t\t\t\t{5, 1},\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\tcase r.Method == http.MethodGet && r.URL.Path == \"/segment2.ts\":\n\t\t\t\tw.Header().Set(\"Content-Type\", `video/MP2T`)\n\n\t\t\t\tw := &mpegts.Writer{W: w, Tracks: []*mpegts.Track{track1}}\n\t\t\t\terr := w.Initialize()\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\terr = w.WriteH264(track1, 2*90000, 2*90000, [][]byte{\n\t\t\t\t\t{5, 2},\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t}),\n\t}\n\n\tln, err := net.Listen(\"tcp\", \"localhost:5781\")\n\trequire.NoError(t, err)\n\n\tgo s.Serve(ln)\n\tdefer s.Shutdown(context.Background())\n\n\tvar strm *stream.Stream\n\tvar subStream *stream.SubStream\n\tdone := make(chan struct{})\n\n\tr := &stream.Reader{Parent: test.NilLogger}\n\n\tvar c *gohlslib.Client\n\tc = &gohlslib.Client{\n\t\tURI: \"http://localhost:5781/stream.m3u8\",\n\t\tOnTracks: func(tracks []*gohlslib.Track) error {\n\t\t\tmedias, err2 := ToStream(c, tracks, &conf.Path{\n\t\t\t\tUseAbsoluteTimestamp: true,\n\t\t\t}, &subStream)\n\t\t\trequire.NoError(t, err2)\n\n\t\t\trequire.Equal(t, []*description.Media{{\n\t\t\t\tType: description.MediaTypeVideo,\n\t\t\t\tFormats: []format.Format{&format.H264{\n\t\t\t\t\tPayloadTyp:        96,\n\t\t\t\t\tPacketizationMode: 1,\n\t\t\t\t}},\n\t\t\t}}, medias)\n\n\t\t\tstrm = &stream.Stream{\n\t\t\t\tDesc:              &description.Session{Medias: medias},\n\t\t\t\tWriteQueueSize:    512,\n\t\t\t\tRTPMaxPayloadSize: 1450,\n\t\t\t\tParent:            test.NilLogger,\n\t\t\t}\n\t\t\terr2 = strm.Initialize()\n\t\t\trequire.NoError(t, err2)\n\n\t\t\tsubStream = &stream.SubStream{\n\t\t\t\tStream:        strm,\n\t\t\t\tUseRTPPackets: false,\n\t\t\t}\n\t\t\terr2 = subStream.Initialize()\n\t\t\trequire.NoError(t, err2)\n\n\t\t\tn := 0\n\n\t\t\tr.OnData(\n\t\t\t\tmedias[0],\n\t\t\t\tmedias[0].Formats[0],\n\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\tswitch n {\n\t\t\t\t\tcase 0:\n\t\t\t\t\t\trequire.Equal(t, unit.PayloadH264{\n\t\t\t\t\t\t\t{7, 1, 2, 3},\n\t\t\t\t\t\t\t{8},\n\t\t\t\t\t\t\t{5, 1},\n\t\t\t\t\t\t}, u.Payload)\n\t\t\t\t\t\trequire.Equal(t, time.Date(2018, 0o5, 20, 8, 17, 15, 0, time.UTC), u.NTP)\n\t\t\t\t\tcase 1:\n\t\t\t\t\t\trequire.Equal(t, unit.PayloadH264{\n\t\t\t\t\t\t\t{7, 1, 2, 3},\n\t\t\t\t\t\t\t{8},\n\t\t\t\t\t\t\t{5, 2},\n\t\t\t\t\t\t}, u.Payload)\n\t\t\t\t\t\trequire.Equal(t, time.Date(2018, 0o5, 20, 8, 17, 15, 0, time.UTC), u.NTP)\n\t\t\t\t\t\tclose(done)\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tt.Error(\"should not happen\")\n\t\t\t\t\t}\n\t\t\t\t\tn++\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\n\t\t\tstrm.AddReader(r)\n\n\t\t\treturn nil\n\t\t},\n\t}\n\terr = c.Start()\n\trequire.NoError(t, err)\n\tdefer c.Close()\n\n\t<-done\n\n\tstrm.RemoveReader(r)\n\tstrm.Close()\n}\n"
  },
  {
    "path": "internal/protocols/httpp/content_type.go",
    "content": "package httpp\n\nimport \"strings\"\n\n// ParseContentType parses a Content-Type header and returns the content type.\nfunc ParseContentType(v string) string {\n\treturn strings.TrimSpace(strings.Split(v, \";\")[0])\n}\n"
  },
  {
    "path": "internal/protocols/httpp/credentials.go",
    "content": "package httpp\n\nimport (\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/bluenviron/mediamtx/internal/auth\"\n)\n\n// Credentials extracts credentials from a HTTP request.\nfunc Credentials(h *http.Request) *auth.Credentials {\n\tc := &auth.Credentials{}\n\n\tfor _, auth := range h.Header[\"Authorization\"] {\n\t\tif strings.HasPrefix(auth, \"Bearer \") {\n\t\t\t// user:pass in Authorization Bearer\n\t\t\tif parts := strings.Split(auth[len(\"Bearer \"):], \":\"); len(parts) == 2 {\n\t\t\t\tc.User = parts[0]\n\t\t\t\tc.Pass = parts[1]\n\t\t\t\treturn c\n\t\t\t}\n\n\t\t\t// JWT in Authorization Bearer\n\t\t\tc.Token = auth[len(\"Bearer \"):]\n\t\t\treturn c\n\t\t}\n\t}\n\n\t// user:pass in Authorization Basic\n\tc.User, c.Pass, _ = h.BasicAuth()\n\n\treturn c\n}\n"
  },
  {
    "path": "internal/protocols/httpp/credentials_test.go",
    "content": "package httpp\n\nimport (\n\t\"net/http\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/bluenviron/mediamtx/internal/auth\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCredentials(t *testing.T) {\n\tt.Run(\"user and pass in basic\", func(t *testing.T) {\n\t\th := &http.Request{\n\t\t\tURL: &url.URL{},\n\t\t\tHeader: http.Header{\n\t\t\t\t\"Authorization\": []string{\n\t\t\t\t\t\"Basic bXl1c2VyOm15cGFzcw==\",\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tc := Credentials(h)\n\n\t\trequire.Equal(t, &auth.Credentials{\n\t\t\tUser: \"myuser\",\n\t\t\tPass: \"mypass\",\n\t\t}, c)\n\t})\n\n\tt.Run(\"user and pass in bearer\", func(t *testing.T) {\n\t\th := &http.Request{\n\t\t\tURL: &url.URL{},\n\t\t\tHeader: http.Header{\n\t\t\t\t\"Authorization\": []string{\n\t\t\t\t\t\"Bearer myuser:mypass\",\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tc := Credentials(h)\n\n\t\trequire.Equal(t, &auth.Credentials{\n\t\t\tUser: \"myuser\",\n\t\t\tPass: \"mypass\",\n\t\t}, c)\n\t})\n\n\tt.Run(\"token in bearer\", func(t *testing.T) {\n\t\th := &http.Request{\n\t\t\tURL: &url.URL{},\n\t\t\tHeader: http.Header{\n\t\t\t\t\"Authorization\": []string{\n\t\t\t\t\t\"Bearer testing123\",\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tc := Credentials(h)\n\n\t\trequire.Equal(t, &auth.Credentials{\n\t\t\tToken: \"testing123\",\n\t\t}, c)\n\t})\n\n\tt.Run(\"user and pass and token\", func(t *testing.T) {\n\t\th := &http.Request{\n\t\t\tURL: &url.URL{},\n\t\t\tHeader: http.Header{\n\t\t\t\t\"Authorization\": []string{\n\t\t\t\t\t\"Basic bXl1c2VyOm15cGFzcw==\",\n\t\t\t\t\t\"Bearer testing123\",\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tc := Credentials(h)\n\n\t\trequire.Equal(t, &auth.Credentials{\n\t\t\tToken: \"testing123\",\n\t\t}, c)\n\t})\n}\n"
  },
  {
    "path": "internal/protocols/httpp/handler_exit_on_panic.go",
    "content": "package httpp\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"runtime\"\n)\n\n// exit when there's a panic inside the HTTP handler.\n// https://github.com/golang/go/issues/16542\ntype handlerExitOnPanic struct {\n\th http.Handler\n}\n\nfunc (h *handlerExitOnPanic) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tdefer func() {\n\t\terr := recover()\n\t\tif err != nil {\n\t\t\tbuf := make([]byte, 1<<20)\n\t\t\tn := runtime.Stack(buf, true)\n\t\t\tfmt.Fprintf(os.Stderr, \"panic: %v\\n\\n%s\", err, buf[:n])\n\t\t\tos.Exit(1)\n\t\t}\n\t}()\n\th.h.ServeHTTP(w, r)\n}\n"
  },
  {
    "path": "internal/protocols/httpp/handler_filter_requests.go",
    "content": "package httpp\n\nimport (\n\t\"net/http\"\n)\n\n// reject requests with empty paths.\ntype handlerFilterRequests struct {\n\th http.Handler\n}\n\nfunc (h *handlerFilterRequests) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tif r.URL.Path == \"\" || r.URL.Path[0] != '/' {\n\t\tw.WriteHeader(http.StatusBadRequest)\n\t\treturn\n\t}\n\th.h.ServeHTTP(w, r)\n}\n"
  },
  {
    "path": "internal/protocols/httpp/handler_filter_requests_test.go",
    "content": "package httpp\n\nimport (\n\t\"net\"\n\t\"net/http\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestHandlerFilterRequests(t *testing.T) {\n\ts := &Server{\n\t\tAddress:      \"localhost:4555\",\n\t\tReadTimeout:  10 * time.Second,\n\t\tWriteTimeout: 10 * time.Second,\n\t\tParent:       test.NilLogger,\n\t\tHandler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t}),\n\t}\n\terr := s.Initialize()\n\trequire.NoError(t, err)\n\tdefer s.Close()\n\n\tconn, err := net.Dial(\"tcp\", \"localhost:4555\")\n\trequire.NoError(t, err)\n\tdefer conn.Close()\n\n\t_, err = conn.Write([]byte(\"OPTIONS / HTTP/1.1\\n\" +\n\t\t\"Host: localhost:8889\\n\\n\"))\n\trequire.NoError(t, err)\n\n\tbuf := make([]byte, 200)\n\tn, err := conn.Read(buf)\n\trequire.NoError(t, err)\n\n\tres := strings.Split(string(buf[:n]), \"\\r\\n\")\n\trequire.Equal(t, \"HTTP/1.1 200 OK\", res[0])\n}\n"
  },
  {
    "path": "internal/protocols/httpp/handler_logger.go",
    "content": "package httpp\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n)\n\ntype loggerWriter struct {\n\tw      http.ResponseWriter\n\tstatus int\n\tsize   int\n}\n\nfunc (w *loggerWriter) Header() http.Header {\n\treturn w.w.Header()\n}\n\nfunc (w *loggerWriter) Write(b []byte) (int, error) {\n\tif w.status == 0 {\n\t\tw.status = http.StatusOK\n\t}\n\tw.size += len(b)\n\treturn w.w.Write(b)\n}\n\nfunc (w *loggerWriter) WriteHeader(statusCode int) {\n\tw.status = statusCode\n\tw.w.WriteHeader(statusCode)\n}\n\nfunc (w *loggerWriter) dump() string {\n\tvar buf bytes.Buffer\n\tfmt.Fprintf(&buf, \"%s %d %s\\n\", \"HTTP/1.1\", w.status, http.StatusText(w.status))\n\tw.w.Header().Write(&buf) //nolint:errcheck\n\tbuf.Write([]byte(\"\\n\"))\n\tif w.size > 0 {\n\t\tfmt.Fprintf(&buf, \"(body of %d bytes)\", w.size)\n\t}\n\treturn buf.String()\n}\n\n// log requests and responses.\ntype handlerLogger struct {\n\th   http.Handler\n\tlog logger.Writer\n}\n\nfunc (h *handlerLogger) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tbyts, _ := httputil.DumpRequest(r, true)\n\th.log.Log(logger.Debug, \"[conn %v] [c->s] %s\", r.RemoteAddr, string(byts))\n\n\tlogw := &loggerWriter{w: w}\n\n\th.h.ServeHTTP(logw, r)\n\n\th.log.Log(logger.Debug, \"[conn %v] [s->c] %s\", r.RemoteAddr, logw.dump())\n}\n"
  },
  {
    "path": "internal/protocols/httpp/handler_origin.go",
    "content": "package httpp\n\nimport (\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n)\n\nfunc isOriginAllowed(origin string, allowOrigins []string) (string, bool) {\n\tif len(allowOrigins) == 0 {\n\t\treturn \"\", false\n\t}\n\n\tfor _, o := range allowOrigins {\n\t\tif o == \"*\" {\n\t\t\treturn o, true\n\t\t}\n\t}\n\n\tif origin == \"\" {\n\t\treturn \"\", false\n\t}\n\n\toriginURL, err := url.Parse(origin)\n\tif err != nil || originURL.Scheme == \"\" {\n\t\treturn \"\", false\n\t}\n\n\tif originURL.Port() == \"\" && originURL.Scheme != \"\" {\n\t\tswitch originURL.Scheme {\n\t\tcase \"http\":\n\t\t\toriginURL.Host = net.JoinHostPort(originURL.Host, \"80\")\n\t\tcase \"https\":\n\t\t\toriginURL.Host = net.JoinHostPort(originURL.Host, \"443\")\n\t\t}\n\t}\n\n\tfor _, o := range allowOrigins {\n\t\tallowedURL, errAllowed := url.Parse(o)\n\t\tif errAllowed != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tif allowedURL.Port() == \"\" {\n\t\t\tswitch allowedURL.Scheme {\n\t\t\tcase \"http\":\n\t\t\t\tallowedURL.Host = net.JoinHostPort(allowedURL.Host, \"80\")\n\t\t\tcase \"https\":\n\t\t\t\tallowedURL.Host = net.JoinHostPort(allowedURL.Host, \"443\")\n\t\t\t}\n\t\t}\n\n\t\tif allowedURL.Scheme == originURL.Scheme &&\n\t\t\tallowedURL.Host == originURL.Host &&\n\t\t\tallowedURL.Port() == originURL.Port() {\n\t\t\treturn origin, true\n\t\t}\n\n\t\tif strings.Contains(allowedURL.Host, \"*\") {\n\t\t\tpattern := strings.ReplaceAll(allowedURL.Host, \"*.\", \"(.*\\\\.)?\")\n\t\t\tpattern = strings.ReplaceAll(pattern, \"*\", \".*\")\n\t\t\tmatched, errMatched := regexp.MatchString(\"^\"+pattern+\"$\", originURL.Host)\n\t\t\tif errMatched == nil && matched {\n\t\t\t\treturn origin, true\n\t\t\t}\n\t\t}\n\t}\n\n\treturn \"\", false\n}\n\n// add Access-Control-Allow-Origin and Access-Control-Allow-Credentials headers.\ntype handlerOrigin struct {\n\th            http.Handler\n\tallowOrigins []string\n}\n\nfunc (h *handlerOrigin) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\torigin, ok := isOriginAllowed(r.Header.Get(\"Origin\"), h.allowOrigins)\n\tif ok {\n\t\tw.Header().Set(\"Access-Control-Allow-Origin\", origin)\n\t\tw.Header().Set(\"Access-Control-Allow-Credentials\", \"true\")\n\t}\n\n\th.h.ServeHTTP(w, r)\n}\n"
  },
  {
    "path": "internal/protocols/httpp/handler_origin_test.go",
    "content": "package httpp\n\nimport (\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestHandlerOrigin(t *testing.T) {\n\tfor _, ca := range []struct {\n\t\tname           string\n\t\torigin         string\n\t\tallowedOrigins []string\n\t\texpected       string\n\t}{\n\t\t{\n\t\t\t\"empty\",\n\t\t\t\"\",\n\t\t\t[]string{},\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"not allowed\",\n\t\t\t\"http://another.com\",\n\t\t\t[]string{\"http://example.com\"},\n\t\t\t\"\",\n\t\t},\n\t\t{\n\t\t\t\"everything allowed, no origin\",\n\t\t\t\"\",\n\t\t\t[]string{\"*\"},\n\t\t\t\"*\",\n\t\t},\n\t\t{\n\t\t\t\"everything allowed, with origin\",\n\t\t\t\"https://example.com\",\n\t\t\t[]string{\"*\"},\n\t\t\t\"*\",\n\t\t},\n\t\t{\n\t\t\t\"allowed\",\n\t\t\t\"https://example.org\",\n\t\t\t[]string{\"http://example.com\", \"https://example.org\"},\n\t\t\t\"https://example.org\",\n\t\t},\n\t\t{\n\t\t\t\"wildcard\",\n\t\t\t\"https://test.example.org\",\n\t\t\t[]string{\"https://*.example.org\"},\n\t\t\t\"https://test.example.org\",\n\t\t},\n\t} {\n\t\tt.Run(ca.name, func(t *testing.T) {\n\t\t\ts := &Server{\n\t\t\t\tAddress:      \"localhost:4555\",\n\t\t\t\tAllowOrigins: ca.allowedOrigins,\n\t\t\t\tReadTimeout:  10 * time.Second,\n\t\t\t\tWriteTimeout: 10 * time.Second,\n\t\t\t\tParent:       test.NilLogger,\n\t\t\t\tHandler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\t\t}),\n\t\t\t}\n\t\t\terr := s.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer s.Close()\n\n\t\t\ttr := &http.Transport{}\n\t\t\tdefer tr.CloseIdleConnections()\n\t\t\thc := &http.Client{Transport: tr}\n\n\t\t\treq, err := http.NewRequest(http.MethodGet, \"http://localhost:4555\", nil)\n\t\t\trequire.NoError(t, err)\n\n\t\t\treq.Header.Set(\"Origin\", ca.origin)\n\n\t\t\tres, err := hc.Do(req)\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer res.Body.Close()\n\n\t\t\trequire.Equal(t, ca.expected, res.Header.Get(\"Access-Control-Allow-Origin\"))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/protocols/httpp/handler_server_header.go",
    "content": "package httpp\n\nimport (\n\t\"net/http\"\n)\n\n// set the Server header.\ntype handlerServerHeader struct {\n\th http.Handler\n}\n\nfunc (h *handlerServerHeader) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tw.Header().Set(\"Server\", \"mediamtx\")\n\th.h.ServeHTTP(w, r)\n}\n"
  },
  {
    "path": "internal/protocols/httpp/handler_tracker.go",
    "content": "package httpp\n\nimport (\n\t\"net/http\"\n\t\"sync\"\n)\n\ntype handlerTracker struct {\n\th http.Handler\n\n\tmutex  sync.Mutex\n\twg     sync.WaitGroup\n\tclosed bool\n}\n\nfunc (h *handlerTracker) close() {\n\th.mutex.Lock()\n\th.closed = true\n\th.mutex.Unlock()\n\th.wg.Wait()\n}\n\nfunc (h *handlerTracker) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\th.mutex.Lock()\n\tif h.closed {\n\t\th.mutex.Unlock()\n\t\treturn\n\t}\n\n\th.wg.Add(1)\n\th.mutex.Unlock()\n\n\tdefer h.wg.Done()\n\n\th.h.ServeHTTP(w, r)\n}\n"
  },
  {
    "path": "internal/protocols/httpp/handler_tracker_test.go",
    "content": "package httpp\n\nimport (\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestHandlerTracker(t *testing.T) {\n\trequestReceived := make(chan struct{})\n\n\ts := &Server{\n\t\tAddress:      \"localhost:4667\",\n\t\tReadTimeout:  10 * time.Second,\n\t\tWriteTimeout: 10 * time.Second,\n\t\tParent:       test.NilLogger,\n\t\tHandler: http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {\n\t\t\tclose(requestReceived)\n\t\t\ttime.Sleep(1 * time.Second)\n\t\t}),\n\t}\n\terr := s.Initialize()\n\trequire.NoError(t, err)\n\n\tgo func() {\n\t\ttr := &http.Transport{}\n\t\tdefer tr.CloseIdleConnections()\n\t\thc := &http.Client{Transport: tr}\n\n\t\t_, err2 := hc.Get(\"http://localhost:4667/test\") //nolint:bodyclose\n\t\trequire.Error(t, err2)\n\t}()\n\n\t<-requestReceived\n\n\tbeforeClose := time.Now()\n\n\ts.Close()\n\n\trequire.Greater(t, time.Since(beforeClose), 1*time.Second)\n}\n"
  },
  {
    "path": "internal/protocols/httpp/handler_write_timeout.go",
    "content": "package httpp\n\nimport (\n\t\"net/http\"\n\t\"time\"\n)\n\ntype writeTimeoutWriter struct {\n\tw       http.ResponseWriter\n\trc      *http.ResponseController\n\ttimeout time.Duration\n}\n\nfunc (w *writeTimeoutWriter) Header() http.Header {\n\treturn w.w.Header()\n}\n\nfunc (w *writeTimeoutWriter) Write(p []byte) (int, error) {\n\tw.rc.SetWriteDeadline(time.Now().Add(w.timeout)) //nolint:errcheck\n\treturn w.w.Write(p)\n}\n\nfunc (w *writeTimeoutWriter) WriteHeader(statusCode int) {\n\tw.rc.SetWriteDeadline(time.Now().Add(w.timeout)) //nolint:errcheck\n\tw.w.WriteHeader(statusCode)\n}\n\n// apply write deadline before every Write() call.\n// this allows to write long responses, splitted in chunks,\n// without causing timeouts.\ntype handlerWriteTimeout struct {\n\th       http.Handler\n\ttimeout time.Duration\n}\n\nfunc (h *handlerWriteTimeout) ServeHTTP(w http.ResponseWriter, r *http.Request) {\n\tww := &writeTimeoutWriter{\n\t\tw:       w,\n\t\trc:      http.NewResponseController(w),\n\t\ttimeout: h.timeout,\n\t}\n\n\th.h.ServeHTTP(ww, r)\n}\n"
  },
  {
    "path": "internal/protocols/httpp/remote_addr.go",
    "content": "package httpp\n\nimport (\n\t\"net\"\n\n\t\"github.com/gin-gonic/gin\"\n)\n\n// RemoteAddr returns the remote address of an HTTP client,\n// with the IP replaced by the real IP passed by any proxy in between.\nfunc RemoteAddr(ctx *gin.Context) string {\n\tip := ctx.ClientIP()\n\t_, port, _ := net.SplitHostPort(ctx.Request.RemoteAddr)\n\treturn net.JoinHostPort(ip, port)\n}\n"
  },
  {
    "path": "internal/protocols/httpp/server.go",
    "content": "// Package httpp contains HTTP utilities.\npackage httpp\n\nimport (\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"log\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/certloader\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/packetdumper\"\n\t\"github.com/bluenviron/mediamtx/internal/restrictnetwork\"\n)\n\ntype nilWriter struct{}\n\nfunc (nilWriter) Write(p []byte) (int, error) {\n\treturn len(p), nil\n}\n\n// Server is a wrapper around http.Server that provides:\n// - net.Listener allocation and closure\n// - TLS allocation\n// - exit on panic\n// - logging\n// - server header\n// - filtering of invalid requests\ntype Server struct {\n\tAddress           string\n\tAllowOrigins      []string\n\tDumpPackets       bool\n\tDumpPacketsPrefix string\n\tReadTimeout       time.Duration\n\tWriteTimeout      time.Duration\n\tEncryption        bool\n\tServerCert        string\n\tServerKey         string\n\tHandler           http.Handler\n\tParent            logger.Writer\n\n\tln      net.Listener\n\tinner   *http.Server\n\tloader  *certloader.CertLoader\n\ttracker *handlerTracker\n}\n\n// Initialize initializes a Server.\nfunc (s *Server) Initialize() error {\n\tif s.ReadTimeout == 0 {\n\t\treturn fmt.Errorf(\"invalid ReadTimeout\")\n\t}\n\tif s.WriteTimeout == 0 {\n\t\treturn fmt.Errorf(\"invalid WriteTimeout\")\n\t}\n\n\tvar tlsConfig *tls.Config\n\tif s.Encryption {\n\t\tif s.ServerCert == \"\" {\n\t\t\treturn fmt.Errorf(\"server cert is missing\")\n\t\t}\n\n\t\ts.loader = &certloader.CertLoader{\n\t\t\tCertPath: s.ServerCert,\n\t\t\tKeyPath:  s.ServerKey,\n\t\t\tParent:   s.Parent,\n\t\t}\n\t\terr := s.loader.Initialize()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\ttlsConfig = &tls.Config{\n\t\t\tGetCertificate: s.loader.GetCertificate(),\n\t\t}\n\t}\n\n\tvar network string\n\tvar address string\n\n\tif strings.HasPrefix(s.Address, \"unix://\") {\n\t\tnetwork = \"unix\"\n\t\taddress = s.Address[len(\"unix://\"):]\n\t} else {\n\t\tnetwork, address = restrictnetwork.Restrict(\"tcp\", s.Address)\n\t}\n\n\tif network == \"unix\" {\n\t\tos.Remove(address)\n\t}\n\n\tvar err error\n\ts.ln, err = net.Listen(network, address)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif s.DumpPackets {\n\t\ts.ln = &packetdumper.Listener{\n\t\t\tPrefix:   s.DumpPacketsPrefix,\n\t\t\tListener: s.ln,\n\t\t}\n\t}\n\n\tif network == \"unix\" {\n\t\tos.Chmod(address, 0o755) //nolint:errcheck\n\t}\n\n\th := s.Handler\n\th = &handlerOrigin{h, s.AllowOrigins}\n\th = &handlerServerHeader{h}\n\th = &handlerFilterRequests{h}\n\th = &handlerLogger{h, s.Parent}\n\th = &handlerExitOnPanic{h}\n\th = &handlerWriteTimeout{h, s.WriteTimeout}\n\ts.tracker = &handlerTracker{h: h}\n\th = s.tracker\n\n\ts.inner = &http.Server{\n\t\tHandler:   h,\n\t\tTLSConfig: tlsConfig,\n\n\t\t// applied before reading any request\n\t\tReadTimeout: s.ReadTimeout,\n\n\t\t// applied after HTTP handler has returned\n\t\tIdleTimeout: 30 * time.Second,\n\n\t\tErrorLog: log.New(&nilWriter{}, \"\", 0),\n\t}\n\n\tif tlsConfig != nil {\n\t\tgo s.inner.ServeTLS(s.ln, \"\", \"\")\n\t} else {\n\t\tgo s.inner.Serve(s.ln)\n\t}\n\n\treturn nil\n}\n\n// Close closes all resources and waits for all routines to return.\nfunc (s *Server) Close() {\n\ts.ln.Close()\n\ts.inner.Close() //nolint:errcheck\n\ts.tracker.close()\n\n\tif s.loader != nil {\n\t\ts.loader.Close()\n\t}\n}\n"
  },
  {
    "path": "internal/protocols/httpp/server_test.go",
    "content": "package httpp\n\nimport (\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n)\n\nfunc TestUnixSocket(t *testing.T) {\n\ts := &Server{\n\t\tAddress:      \"unix://http.sock\",\n\t\tReadTimeout:  10 * time.Second,\n\t\tWriteTimeout: 10 * time.Second,\n\t\tParent:       test.NilLogger,\n\t\tHandler: http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t}),\n\t}\n\terr := s.Initialize()\n\trequire.NoError(t, err)\n\n\t_, err = os.Stat(\"http.sock\")\n\trequire.NoError(t, err)\n\n\tconn, err := net.Dial(\"unix\", \"http.sock\")\n\trequire.NoError(t, err)\n\n\t_, err = conn.Write([]byte(\"OPTIONS / HTTP/1.1\\n\" +\n\t\t\"Host: localhost:8889\\n\\n\"))\n\trequire.NoError(t, err)\n\n\tbuf := make([]byte, 200)\n\tn, err := conn.Read(buf)\n\trequire.NoError(t, err)\n\n\tres := strings.Split(string(buf[:n]), \"\\r\\n\")\n\trequire.Equal(t, \"HTTP/1.1 200 OK\", res[0])\n\n\tconn.Close()\n\ts.Close()\n\n\t_, err = os.Stat(\"http.sock\")\n\trequire.EqualError(t, err, \"stat http.sock: no such file or directory\")\n}\n"
  },
  {
    "path": "internal/protocols/mpegts/enhanced_reader.go",
    "content": "package mpegts\n\nimport (\n\t\"io\"\n\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio\"\n\tmcmpegts \"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts\"\n\ttscodecs \"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts/codecs\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/rewindablereader\"\n)\n\n// EnhancedReader is a mpegts.Reader wrapper\n// That provides additional informations that are needed in order\n// to perform conversion to RTSP.\ntype EnhancedReader struct {\n\tR io.Reader\n\n\t*mcmpegts.Reader\n\n\tlatmConfigs map[uint16]*mpeg4audio.StreamMuxConfig\n}\n\n// Initialize initializes EnhancedReader.\nfunc (r *EnhancedReader) Initialize() error {\n\trr := &rewindablereader.Reader{R: r.R}\n\tmr := &mcmpegts.Reader{R: rr}\n\terr := mr.Initialize()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tr.latmConfigs = make(map[uint16]*mpeg4audio.StreamMuxConfig)\n\ttracksToParse := 0\n\n\tfor _, track := range mr.Tracks() {\n\t\tif _, ok := track.Codec.(*tscodecs.MPEG4AudioLATM); ok {\n\t\t\tcpid := track.PID\n\t\t\tdone := false\n\t\t\ttracksToParse++\n\n\t\t\tmr.OnDataMPEG4AudioLATM(track, func(_ int64, els [][]byte) error {\n\t\t\t\tif done {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tvar ame mpeg4audio.AudioMuxElement\n\t\t\t\tame.MuxConfigPresent = true\n\t\t\t\terr2 := ame.Unmarshal(els[0])\n\t\t\t\tif err2 != nil {\n\t\t\t\t\treturn nil //nolint:nilerr\n\t\t\t\t}\n\n\t\t\t\tif ame.MuxConfigPresent {\n\t\t\t\t\tr.latmConfigs[cpid] = ame.StreamMuxConfig\n\t\t\t\t\ttracksToParse--\n\t\t\t\t\tdone = true\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t})\n\t\t}\n\t}\n\n\tfor tracksToParse > 0 {\n\t\terr = mr.Read()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\trr.Rewind()\n\tr.Reader = &mcmpegts.Reader{R: rr}\n\terr = r.Reader.Initialize()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn err\n}\n"
  },
  {
    "path": "internal/protocols/mpegts/from_stream.go",
    "content": "package mpegts\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/ac3\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/h264\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/h265\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio\"\n\tmcmpegts \"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts\"\n\ttscodecs \"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts/codecs\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts/substructs\"\n\tsrt \"github.com/datarhei/gosrt\"\n\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n)\n\nfunc multiplyAndDivide(v, m, d int64) int64 {\n\tsecs := v / d\n\tdec := v % d\n\treturn (secs*m + dec*m/d)\n}\n\n// FromStream maps a MediaMTX stream to a MPEG-TS writer.\nfunc FromStream(\n\tdesc *description.Session,\n\tr *stream.Reader,\n\tbw *bufio.Writer,\n\tsconn srt.Conn,\n\twriteTimeout time.Duration,\n) error {\n\tvar w *mcmpegts.Writer\n\tvar tracks []*mcmpegts.Track\n\n\taddTrack := func(\n\t\tmedia *description.Media,\n\t\tforma format.Format,\n\t\ttrack *mcmpegts.Track,\n\t\tonData stream.OnDataFunc,\n\t) {\n\t\ttracks = append(tracks, track)\n\t\tr.OnData(media, forma, onData)\n\t}\n\n\tfor _, media := range desc.Medias {\n\t\tfor _, forma := range media.Formats {\n\t\t\tclockRate := forma.ClockRate()\n\n\t\t\tswitch forma := forma.(type) {\n\t\t\tcase *format.H265: //nolint:dupl\n\t\t\t\ttrack := &mcmpegts.Track{Codec: &tscodecs.H265{}}\n\n\t\t\t\tvar dtsExtractor *h265.DTSExtractor\n\n\t\t\t\taddTrack(\n\t\t\t\t\tmedia,\n\t\t\t\t\tforma,\n\t\t\t\t\ttrack,\n\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif dtsExtractor == nil {\n\t\t\t\t\t\t\tif !h265.IsRandomAccess(u.Payload.(unit.PayloadH265)) {\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tdtsExtractor = &h265.DTSExtractor{}\n\t\t\t\t\t\t\tdtsExtractor.Initialize()\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tdts, err := dtsExtractor.Extract(u.Payload.(unit.PayloadH265), u.PTS)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tsconn.SetWriteDeadline(time.Now().Add(writeTimeout))\n\t\t\t\t\t\terr = (*w).WriteH265(\n\t\t\t\t\t\t\ttrack,\n\t\t\t\t\t\t\tu.PTS, // no conversion is needed since clock rate is 90khz in both MPEG-TS and RTSP\n\t\t\t\t\t\t\tdts,\n\t\t\t\t\t\t\tu.Payload.(unit.PayloadH265))\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn bw.Flush()\n\t\t\t\t\t})\n\n\t\t\tcase *format.H264: //nolint:dupl\n\t\t\t\ttrack := &mcmpegts.Track{Codec: &tscodecs.H264{}}\n\n\t\t\t\tvar dtsExtractor *h264.DTSExtractor\n\n\t\t\t\taddTrack(\n\t\t\t\t\tmedia,\n\t\t\t\t\tforma,\n\t\t\t\t\ttrack,\n\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tidrPresent := h264.IsRandomAccess(u.Payload.(unit.PayloadH264))\n\n\t\t\t\t\t\tif dtsExtractor == nil {\n\t\t\t\t\t\t\tif !idrPresent {\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tdtsExtractor = &h264.DTSExtractor{}\n\t\t\t\t\t\t\tdtsExtractor.Initialize()\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tdts, err := dtsExtractor.Extract(u.Payload.(unit.PayloadH264), u.PTS)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tsconn.SetWriteDeadline(time.Now().Add(writeTimeout))\n\t\t\t\t\t\terr = (*w).WriteH264(\n\t\t\t\t\t\t\ttrack,\n\t\t\t\t\t\t\tu.PTS, // no conversion is needed since clock rate is 90khz in both MPEG-TS and RTSP\n\t\t\t\t\t\t\tdts,\n\t\t\t\t\t\t\tu.Payload.(unit.PayloadH264))\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn bw.Flush()\n\t\t\t\t\t})\n\n\t\t\tcase *format.MPEG4Video:\n\t\t\t\ttrack := &mcmpegts.Track{Codec: &tscodecs.MPEG4Video{}}\n\n\t\t\t\tfirstReceived := false\n\t\t\t\tvar lastPTS int64\n\n\t\t\t\taddTrack(\n\t\t\t\t\tmedia,\n\t\t\t\t\tforma,\n\t\t\t\t\ttrack,\n\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif !firstReceived {\n\t\t\t\t\t\t\tfirstReceived = true\n\t\t\t\t\t\t} else if u.PTS < lastPTS {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"MPEG-4 Video streams with B-frames are not supported (yet)\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tlastPTS = u.PTS\n\n\t\t\t\t\t\tsconn.SetWriteDeadline(time.Now().Add(writeTimeout))\n\t\t\t\t\t\terr := (*w).WriteMPEG4Video(\n\t\t\t\t\t\t\ttrack,\n\t\t\t\t\t\t\tu.PTS, // no conversion is needed since clock rate is 90khz in both MPEG-TS and RTSP\n\t\t\t\t\t\t\tu.Payload.(unit.PayloadMPEG4Video))\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn bw.Flush()\n\t\t\t\t\t})\n\n\t\t\tcase *format.MPEG1Video:\n\t\t\t\ttrack := &mcmpegts.Track{Codec: &tscodecs.MPEG1Video{}}\n\n\t\t\t\tfirstReceived := false\n\t\t\t\tvar lastPTS int64\n\n\t\t\t\taddTrack(\n\t\t\t\t\tmedia,\n\t\t\t\t\tforma,\n\t\t\t\t\ttrack,\n\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif !firstReceived {\n\t\t\t\t\t\t\tfirstReceived = true\n\t\t\t\t\t\t} else if u.PTS < lastPTS {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"MPEG-1 Video streams with B-frames are not supported (yet)\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tlastPTS = u.PTS\n\n\t\t\t\t\t\tsconn.SetWriteDeadline(time.Now().Add(writeTimeout))\n\t\t\t\t\t\terr := (*w).WriteMPEG1Video(\n\t\t\t\t\t\t\ttrack,\n\t\t\t\t\t\t\tu.PTS, // no conversion is needed since clock rate is 90khz in both MPEG-TS and RTSP\n\t\t\t\t\t\t\tu.Payload.(unit.PayloadMPEG1Video))\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn bw.Flush()\n\t\t\t\t\t})\n\n\t\t\tcase *format.Opus:\n\t\t\t\ttrack := &mcmpegts.Track{Codec: &tscodecs.Opus{\n\t\t\t\t\tDesc: &substructs.OpusAudioDescriptor{\n\t\t\t\t\t\tChannelConfigCode: uint8(forma.ChannelCount),\n\t\t\t\t\t},\n\t\t\t\t}}\n\n\t\t\t\taddTrack(\n\t\t\t\t\tmedia,\n\t\t\t\t\tforma,\n\t\t\t\t\ttrack,\n\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tsconn.SetWriteDeadline(time.Now().Add(writeTimeout))\n\t\t\t\t\t\terr := (*w).WriteOpus(\n\t\t\t\t\t\t\ttrack,\n\t\t\t\t\t\t\tmultiplyAndDivide(u.PTS, 90000, int64(clockRate)),\n\t\t\t\t\t\t\tu.Payload.(unit.PayloadOpus))\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn bw.Flush()\n\t\t\t\t\t})\n\n\t\t\tcase *format.KLV:\n\t\t\t\ttrack := &mcmpegts.Track{\n\t\t\t\t\tCodec: &tscodecs.KLV{\n\t\t\t\t\t\tSynchronous: true,\n\t\t\t\t\t},\n\t\t\t\t}\n\n\t\t\t\taddTrack(\n\t\t\t\t\tmedia,\n\t\t\t\t\tforma,\n\t\t\t\t\ttrack,\n\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tsconn.SetWriteDeadline(time.Now().Add(writeTimeout))\n\t\t\t\t\t\terr := (*w).WriteKLV(track, multiplyAndDivide(u.PTS, 90000, 90000), u.Payload.(unit.PayloadKLV))\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn bw.Flush()\n\t\t\t\t\t})\n\n\t\t\tcase *format.MPEG4Audio:\n\t\t\t\ttrack := &mcmpegts.Track{Codec: &tscodecs.MPEG4Audio{\n\t\t\t\t\tConfig: *forma.Config,\n\t\t\t\t}}\n\n\t\t\t\taddTrack(\n\t\t\t\t\tmedia,\n\t\t\t\t\tforma,\n\t\t\t\t\ttrack,\n\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tsconn.SetWriteDeadline(time.Now().Add(writeTimeout))\n\t\t\t\t\t\terr := (*w).WriteMPEG4Audio(\n\t\t\t\t\t\t\ttrack,\n\t\t\t\t\t\t\tmultiplyAndDivide(u.PTS, 90000, int64(clockRate)),\n\t\t\t\t\t\t\tu.Payload.(unit.PayloadMPEG4Audio))\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn bw.Flush()\n\t\t\t\t\t})\n\n\t\t\tcase *format.MPEG4AudioLATM:\n\t\t\t\ttrack := &mcmpegts.Track{Codec: &tscodecs.MPEG4AudioLATM{}}\n\n\t\t\t\tif !forma.CPresent {\n\t\t\t\t\taddTrack(\n\t\t\t\t\t\tmedia,\n\t\t\t\t\t\tforma,\n\t\t\t\t\t\ttrack,\n\t\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tvar elIn mpeg4audio.AudioMuxElement\n\t\t\t\t\t\t\telIn.MuxConfigPresent = false\n\t\t\t\t\t\t\telIn.StreamMuxConfig = forma.StreamMuxConfig\n\t\t\t\t\t\t\terr := elIn.Unmarshal(u.Payload.(unit.PayloadMPEG4AudioLATM))\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tvar elOut mpeg4audio.AudioMuxElement\n\t\t\t\t\t\t\telOut.MuxConfigPresent = true\n\t\t\t\t\t\t\telOut.StreamMuxConfig = forma.StreamMuxConfig\n\t\t\t\t\t\t\telOut.UseSameStreamMux = false\n\t\t\t\t\t\t\telOut.Payloads = elIn.Payloads\n\t\t\t\t\t\t\tbuf, err := elOut.Marshal()\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tsconn.SetWriteDeadline(time.Now().Add(writeTimeout))\n\t\t\t\t\t\t\terr = (*w).WriteMPEG4AudioLATM(\n\t\t\t\t\t\t\t\ttrack,\n\t\t\t\t\t\t\t\tmultiplyAndDivide(u.PTS, 90000, int64(clockRate)),\n\t\t\t\t\t\t\t\t[][]byte{buf})\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn bw.Flush()\n\t\t\t\t\t\t})\n\t\t\t\t} else {\n\t\t\t\t\taddTrack(\n\t\t\t\t\t\tmedia,\n\t\t\t\t\t\tforma,\n\t\t\t\t\t\ttrack,\n\t\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tsconn.SetWriteDeadline(time.Now().Add(writeTimeout))\n\t\t\t\t\t\t\terr := (*w).WriteMPEG4AudioLATM(\n\t\t\t\t\t\t\t\ttrack,\n\t\t\t\t\t\t\t\tmultiplyAndDivide(u.PTS, 90000, int64(clockRate)),\n\t\t\t\t\t\t\t\t[][]byte{u.Payload.(unit.PayloadMPEG4AudioLATM)})\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\treturn bw.Flush()\n\t\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\tcase *format.MPEG1Audio:\n\t\t\t\ttrack := &mcmpegts.Track{Codec: &tscodecs.MPEG1Audio{}}\n\n\t\t\t\taddTrack(\n\t\t\t\t\tmedia,\n\t\t\t\t\tforma,\n\t\t\t\t\ttrack,\n\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tsconn.SetWriteDeadline(time.Now().Add(writeTimeout))\n\t\t\t\t\t\terr := (*w).WriteMPEG1Audio(\n\t\t\t\t\t\t\ttrack,\n\t\t\t\t\t\t\tu.PTS, // no conversion is needed since clock rate is 90khz in both MPEG-TS and RTSP\n\t\t\t\t\t\t\tu.Payload.(unit.PayloadMPEG1Audio))\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn bw.Flush()\n\t\t\t\t\t})\n\n\t\t\tcase *format.AC3:\n\t\t\t\ttrack := &mcmpegts.Track{Codec: &tscodecs.AC3{}}\n\n\t\t\t\taddTrack(\n\t\t\t\t\tmedia,\n\t\t\t\t\tforma,\n\t\t\t\t\ttrack,\n\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tfor i, frame := range u.Payload.(unit.PayloadAC3) {\n\t\t\t\t\t\t\tframePTS := u.PTS + int64(i)*ac3.SamplesPerFrame\n\n\t\t\t\t\t\t\tsconn.SetWriteDeadline(time.Now().Add(writeTimeout))\n\t\t\t\t\t\t\terr := (*w).WriteAC3(\n\t\t\t\t\t\t\t\ttrack,\n\t\t\t\t\t\t\t\tmultiplyAndDivide(framePTS, 90000, int64(clockRate)),\n\t\t\t\t\t\t\t\tframe)\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn bw.Flush()\n\t\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(tracks) == 0 {\n\t\treturn errNoSupportedCodecs\n\t}\n\n\tsetuppedFormats := r.Formats()\n\n\tn := 1\n\tfor _, medi := range desc.Medias {\n\t\tfor _, forma := range medi.Formats {\n\t\t\tif !slices.Contains(setuppedFormats, forma) {\n\t\t\t\tr.Parent.Log(logger.Warn, \"skipping track %d (%s)\", n, forma.Codec())\n\t\t\t}\n\t\t\tn++\n\t\t}\n\t}\n\n\tw = &mcmpegts.Writer{W: bw, Tracks: tracks}\n\treturn w.Initialize()\n}\n"
  },
  {
    "path": "internal/protocols/mpegts/from_stream_test.go",
    "content": "package mpegts\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestFromStreamNoSupportedCodecs(t *testing.T) {\n\tdesc := &description.Session{Medias: []*description.Media{{\n\t\tType:    description.MediaTypeVideo,\n\t\tFormats: []format.Format{&format.VP8{}},\n\t}}}\n\n\tr := &stream.Reader{\n\t\tParent: test.Logger(func(logger.Level, string, ...any) {\n\t\t\tt.Error(\"should not happen\")\n\t\t}),\n\t}\n\n\terr := FromStream(desc, r, nil, nil, 0)\n\trequire.Equal(t, errNoSupportedCodecs, err)\n}\n\nfunc TestFromStreamSkipUnsupportedTracks(t *testing.T) {\n\tdesc := &description.Session{Medias: []*description.Media{\n\t\t{\n\t\t\tType:    description.MediaTypeVideo,\n\t\t\tFormats: []format.Format{&format.H265{}},\n\t\t},\n\t\t{\n\t\t\tType:    description.MediaTypeVideo,\n\t\t\tFormats: []format.Format{&format.VP8{}},\n\t\t},\n\t}}\n\n\tn := 0\n\n\tr := &stream.Reader{\n\t\tParent: test.Logger(func(l logger.Level, format string, args ...any) {\n\t\t\trequire.Equal(t, logger.Warn, l)\n\t\t\tif n == 0 {\n\t\t\t\trequire.Equal(t, \"skipping track 2 (VP8)\", fmt.Sprintf(format, args...))\n\t\t\t}\n\t\t\tn++\n\t\t}),\n\t}\n\n\terr := FromStream(desc, r, nil, nil, 0)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, 1, n)\n}\n"
  },
  {
    "path": "internal/protocols/mpegts/to_stream.go",
    "content": "// Package mpegts contains MPEG-ts utilities.\npackage mpegts\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"reflect\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts\"\n\ttscodecs \"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts/codecs\"\n\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n)\n\nvar errNoSupportedCodecs = errors.New(\n\t\"the stream doesn't contain any supported codec, which are currently \" +\n\t\t\"H265, H264, MPEG-4 Video, MPEG-1/2 Video, Opus, MPEG-4 Audio, MPEG-1 Audio, AC-3\")\n\n// ToStream maps a MPEG-TS stream to a MediaMTX stream.\nfunc ToStream(\n\tr *EnhancedReader,\n\tsubStream **stream.SubStream,\n\tl logger.Writer,\n) ([]*description.Media, error) {\n\tvar medias []*description.Media //nolint:prealloc\n\tvar unsupportedTracks []int\n\n\ttd := &mpegts.TimeDecoder{}\n\ttd.Initialize()\n\n\tfor i, track := range r.Tracks() { //nolint:dupl\n\t\tvar medi *description.Media\n\n\t\tswitch codec := track.Codec.(type) {\n\t\tcase *tscodecs.H265:\n\t\t\tmedi = &description.Media{\n\t\t\t\tType: description.MediaTypeVideo,\n\t\t\t\tFormats: []format.Format{&format.H265{\n\t\t\t\t\tPayloadTyp: 96,\n\t\t\t\t}},\n\t\t\t}\n\n\t\t\tr.OnDataH265(track, func(pts int64, _ int64, au [][]byte) error {\n\t\t\t\tpts = td.Decode(pts)\n\n\t\t\t\t(*subStream).WriteUnit(medi, medi.Formats[0], &unit.Unit{\n\t\t\t\t\tPTS:     pts, // no conversion is needed since clock rate is 90khz in both MPEG-TS and RTSP\n\t\t\t\t\tPayload: unit.PayloadH265(au),\n\t\t\t\t})\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\tcase *tscodecs.H264:\n\t\t\tmedi = &description.Media{\n\t\t\t\tType: description.MediaTypeVideo,\n\t\t\t\tFormats: []format.Format{&format.H264{\n\t\t\t\t\tPayloadTyp:        96,\n\t\t\t\t\tPacketizationMode: 1,\n\t\t\t\t}},\n\t\t\t}\n\n\t\t\tr.OnDataH264(track, func(pts int64, _ int64, au [][]byte) error {\n\t\t\t\tpts = td.Decode(pts)\n\n\t\t\t\t(*subStream).WriteUnit(medi, medi.Formats[0], &unit.Unit{\n\t\t\t\t\tPTS:     pts, // no conversion is needed since clock rate is 90khz in both MPEG-TS and RTSP\n\t\t\t\t\tPayload: unit.PayloadH264(au),\n\t\t\t\t})\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\tcase *tscodecs.MPEG4Video:\n\t\t\tmedi = &description.Media{\n\t\t\t\tType: description.MediaTypeVideo,\n\t\t\t\tFormats: []format.Format{&format.MPEG4Video{\n\t\t\t\t\tPayloadTyp: 96,\n\t\t\t\t}},\n\t\t\t}\n\n\t\t\tr.OnDataMPEGxVideo(track, func(pts int64, frame []byte) error {\n\t\t\t\tpts = td.Decode(pts)\n\n\t\t\t\t(*subStream).WriteUnit(medi, medi.Formats[0], &unit.Unit{\n\t\t\t\t\tPTS:     pts, // no conversion is needed since clock rate is 90khz in both MPEG-TS and RTSP\n\t\t\t\t\tPayload: unit.PayloadMPEG4Video(frame),\n\t\t\t\t})\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\tcase *tscodecs.MPEG1Video:\n\t\t\tmedi = &description.Media{\n\t\t\t\tType:    description.MediaTypeVideo,\n\t\t\t\tFormats: []format.Format{&format.MPEG1Video{}},\n\t\t\t}\n\n\t\t\tr.OnDataMPEGxVideo(track, func(pts int64, frame []byte) error {\n\t\t\t\tpts = td.Decode(pts)\n\n\t\t\t\t(*subStream).WriteUnit(medi, medi.Formats[0], &unit.Unit{\n\t\t\t\t\tPTS:     pts, // no conversion is needed since clock rate is 90khz in both MPEG-TS and RTSP\n\t\t\t\t\tPayload: unit.PayloadMPEG1Video(frame),\n\t\t\t\t})\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\tcase *tscodecs.Opus:\n\t\t\tmedi = &description.Media{\n\t\t\t\tType: description.MediaTypeAudio,\n\t\t\t\tFormats: []format.Format{&format.Opus{\n\t\t\t\t\tPayloadTyp:   96,\n\t\t\t\t\tChannelCount: codec.Desc.ChannelCount(),\n\t\t\t\t}},\n\t\t\t}\n\n\t\t\tr.OnDataOpus(track, func(pts int64, packets [][]byte) error {\n\t\t\t\tpts = td.Decode(pts)\n\n\t\t\t\t(*subStream).WriteUnit(medi, medi.Formats[0], &unit.Unit{\n\t\t\t\t\tPTS:     multiplyAndDivide(pts, int64(medi.Formats[0].ClockRate()), 90000),\n\t\t\t\t\tPayload: unit.PayloadOpus(packets),\n\t\t\t\t})\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\tcase *tscodecs.KLV:\n\t\t\tmedi = &description.Media{\n\t\t\t\tType: description.MediaTypeApplication,\n\t\t\t\tFormats: []format.Format{&format.KLV{\n\t\t\t\t\tPayloadTyp: 96,\n\t\t\t\t}},\n\t\t\t}\n\t\t\tr.OnDataKLV(track, func(pts int64, uni []byte) error {\n\t\t\t\tpts = td.Decode(pts)\n\n\t\t\t\t(*subStream).WriteUnit(medi, medi.Formats[0], &unit.Unit{\n\t\t\t\t\tPTS:     pts,\n\t\t\t\t\tPayload: unit.PayloadKLV(uni),\n\t\t\t\t})\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\tcase *tscodecs.MPEG4Audio:\n\t\t\tmedi = &description.Media{\n\t\t\t\tType: description.MediaTypeAudio,\n\t\t\t\tFormats: []format.Format{&format.MPEG4Audio{\n\t\t\t\t\tPayloadTyp:       96,\n\t\t\t\t\tSizeLength:       13,\n\t\t\t\t\tIndexLength:      3,\n\t\t\t\t\tIndexDeltaLength: 3,\n\t\t\t\t\tConfig:           &codec.Config,\n\t\t\t\t}},\n\t\t\t}\n\n\t\t\tr.OnDataMPEG4Audio(track, func(pts int64, aus [][]byte) error {\n\t\t\t\tpts = td.Decode(pts)\n\n\t\t\t\t(*subStream).WriteUnit(medi, medi.Formats[0], &unit.Unit{\n\t\t\t\t\tPTS:     multiplyAndDivide(pts, int64(medi.Formats[0].ClockRate()), 90000),\n\t\t\t\t\tPayload: unit.PayloadMPEG4Audio(aus),\n\t\t\t\t})\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\tcase *tscodecs.MPEG4AudioLATM:\n\t\t\t// We are dealing with a LATM stream with in-band configuration.\n\t\t\t// Although in theory this can be streamed with RTSP (RFC6416 with cpresent=1),\n\t\t\t// in practice there is no player that supports it.\n\t\t\t// Therefore, convert the stream to a LATM stream with out-of-band configuration.\n\t\t\tstreamMuxConfig := r.latmConfigs[track.PID]\n\t\t\tmedi = &description.Media{\n\t\t\t\tType: description.MediaTypeAudio,\n\t\t\t\tFormats: []format.Format{&format.MPEG4AudioLATM{\n\t\t\t\t\tPayloadTyp:      96,\n\t\t\t\t\tCPresent:        false,\n\t\t\t\t\tProfileLevelID:  30,\n\t\t\t\t\tStreamMuxConfig: streamMuxConfig,\n\t\t\t\t}},\n\t\t\t}\n\t\t\tclockRate := medi.Formats[0].ClockRate()\n\n\t\t\tr.OnDataMPEG4AudioLATM(track, func(pts int64, els [][]byte) error {\n\t\t\t\tpts = td.Decode(pts)\n\n\t\t\t\tpts = multiplyAndDivide(pts, int64(clockRate), 90000)\n\n\t\t\t\tfor _, el := range els {\n\t\t\t\t\tvar elIn mpeg4audio.AudioMuxElement\n\t\t\t\t\telIn.MuxConfigPresent = true\n\t\t\t\t\telIn.StreamMuxConfig = streamMuxConfig\n\t\t\t\t\terr := elIn.Unmarshal(el)\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\tif !reflect.DeepEqual(elIn.StreamMuxConfig, streamMuxConfig) {\n\t\t\t\t\t\treturn fmt.Errorf(\"dynamic stream mux config is not supported\")\n\t\t\t\t\t}\n\n\t\t\t\t\tvar elOut mpeg4audio.AudioMuxElement\n\t\t\t\t\telOut.MuxConfigPresent = false\n\t\t\t\t\telOut.StreamMuxConfig = streamMuxConfig\n\t\t\t\t\telOut.Payloads = elIn.Payloads\n\t\t\t\t\tbuf, err := elOut.Marshal()\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\t(*subStream).WriteUnit(medi, medi.Formats[0], &unit.Unit{\n\t\t\t\t\t\tPTS:     pts,\n\t\t\t\t\t\tPayload: unit.PayloadMPEG4AudioLATM(buf),\n\t\t\t\t\t})\n\n\t\t\t\t\tpts += mpeg4audio.SamplesPerAccessUnit\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\tcase *tscodecs.MPEG1Audio:\n\t\t\tmedi = &description.Media{\n\t\t\t\tType:    description.MediaTypeAudio,\n\t\t\t\tFormats: []format.Format{&format.MPEG1Audio{}},\n\t\t\t}\n\n\t\t\tr.OnDataMPEG1Audio(track, func(pts int64, frames [][]byte) error {\n\t\t\t\tpts = td.Decode(pts)\n\n\t\t\t\t(*subStream).WriteUnit(medi, medi.Formats[0], &unit.Unit{\n\t\t\t\t\tPTS:     pts, // no conversion is needed since clock rate is 90khz in both MPEG-TS and RTSP\n\t\t\t\t\tPayload: unit.PayloadMPEG1Audio(frames),\n\t\t\t\t})\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\tcase *tscodecs.AC3:\n\t\t\tmedi = &description.Media{\n\t\t\t\tType: description.MediaTypeAudio,\n\t\t\t\tFormats: []format.Format{&format.AC3{\n\t\t\t\t\tPayloadTyp:   96,\n\t\t\t\t\tSampleRate:   codec.SampleRate,\n\t\t\t\t\tChannelCount: codec.ChannelCount,\n\t\t\t\t}},\n\t\t\t}\n\n\t\t\tr.OnDataAC3(track, func(pts int64, frame []byte) error {\n\t\t\t\tpts = td.Decode(pts)\n\n\t\t\t\t(*subStream).WriteUnit(medi, medi.Formats[0], &unit.Unit{\n\t\t\t\t\tPTS:     multiplyAndDivide(pts, int64(medi.Formats[0].ClockRate()), 90000),\n\t\t\t\t\tPayload: unit.PayloadAC3{frame},\n\t\t\t\t})\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\tdefault:\n\t\t\tunsupportedTracks = append(unsupportedTracks, i+1)\n\t\t\tcontinue\n\t\t}\n\n\t\tmedias = append(medias, medi)\n\t}\n\n\tif len(medias) == 0 {\n\t\treturn nil, errNoSupportedCodecs\n\t}\n\n\tfor _, id := range unsupportedTracks {\n\t\tl.Log(logger.Warn, \"skipping track %d (unsupported codec)\", id)\n\t}\n\n\treturn medias, nil\n}\n"
  },
  {
    "path": "internal/protocols/mpegts/to_stream_test.go",
    "content": "package mpegts\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/asticode/go-astits\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestToStream(t *testing.T) {\n\tfor _, ca := range []string{\n\t\t\"h265\",\n\t\t\"h264\",\n\t\t\"mpeg-4 audio latm\",\n\t} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tvar buf bytes.Buffer\n\t\t\tmux := astits.NewMuxer(context.Background(), &buf)\n\n\t\t\tswitch ca {\n\t\t\tcase \"h265\":\n\t\t\t\terr := mux.AddElementaryStream(astits.PMTElementaryStream{\n\t\t\t\t\tElementaryPID: 122,\n\t\t\t\t\tStreamType:    astits.StreamTypeH265Video,\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tmux.SetPCRPID(122)\n\n\t\t\t\t_, err = mux.WriteTables()\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\tcase \"h264\":\n\t\t\t\terr := mux.AddElementaryStream(astits.PMTElementaryStream{\n\t\t\t\t\tElementaryPID: 122,\n\t\t\t\t\tStreamType:    astits.StreamTypeH264Video,\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tmux.SetPCRPID(122)\n\n\t\t\t\t_, err = mux.WriteTables()\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\tcase \"mpeg-4 audio latm\":\n\t\t\t\terr := mux.AddElementaryStream(astits.PMTElementaryStream{\n\t\t\t\t\tElementaryPID: 122,\n\t\t\t\t\tStreamType:    astits.StreamTypeAACLATMAudio,\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tmux.SetPCRPID(122)\n\n\t\t\t\tenc1, err := mpeg4audio.AudioMuxElement{\n\t\t\t\t\tMuxConfigPresent: true,\n\t\t\t\t\tStreamMuxConfig: &mpeg4audio.StreamMuxConfig{\n\t\t\t\t\t\tPrograms: []*mpeg4audio.StreamMuxConfigProgram{{\n\t\t\t\t\t\t\tLayers: []*mpeg4audio.StreamMuxConfigLayer{{\n\t\t\t\t\t\t\t\tAudioSpecificConfig: &mpeg4audio.AudioSpecificConfig{\n\t\t\t\t\t\t\t\t\tType:         2,\n\t\t\t\t\t\t\t\t\tSampleRate:   48000,\n\t\t\t\t\t\t\t\t\tChannelCount: 2,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tLatmBufferFullness: 255,\n\t\t\t\t\t\t\t}},\n\t\t\t\t\t\t}},\n\t\t\t\t\t},\n\t\t\t\t\tPayloads: [][][][]byte{{{{1, 2, 3, 4}}}},\n\t\t\t\t}.Marshal()\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tenc2, err := mpeg4audio.AudioSyncStream{\n\t\t\t\t\tAudioMuxElements: [][]byte{enc1},\n\t\t\t\t}.Marshal()\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t_, err = mux.WriteData(&astits.MuxerData{\n\t\t\t\t\tPID: 122,\n\t\t\t\t\tPES: &astits.PESData{\n\t\t\t\t\t\tHeader: &astits.PESHeader{\n\t\t\t\t\t\t\tOptionalHeader: &astits.PESOptionalHeader{\n\t\t\t\t\t\t\t\tMarkerBits:      2,\n\t\t\t\t\t\t\t\tPTSDTSIndicator: astits.PTSDTSIndicatorOnlyPTS,\n\t\t\t\t\t\t\t\tPTS:             &astits.ClockReference{Base: 90000},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tStreamID: 192,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tData: enc2,\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\tr := &EnhancedReader{R: &buf}\n\t\t\terr := r.Initialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tdesc, err := ToStream(r, nil, nil)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tswitch ca {\n\t\t\tcase \"h265\":\n\t\t\t\trequire.Equal(t, []*description.Media{{\n\t\t\t\t\tType: description.MediaTypeVideo,\n\t\t\t\t\tFormats: []format.Format{&format.H265{\n\t\t\t\t\t\tPayloadTyp: 96,\n\t\t\t\t\t}},\n\t\t\t\t}}, desc)\n\n\t\t\tcase \"h264\":\n\t\t\t\trequire.Equal(t, []*description.Media{{\n\t\t\t\t\tType: description.MediaTypeVideo,\n\t\t\t\t\tFormats: []format.Format{&format.H264{\n\t\t\t\t\t\tPayloadTyp:        96,\n\t\t\t\t\t\tPacketizationMode: 1,\n\t\t\t\t\t}},\n\t\t\t\t}}, desc)\n\n\t\t\tcase \"mpeg-4 audio latm\":\n\t\t\t\trequire.Equal(t, []*description.Media{{\n\t\t\t\t\tType: description.MediaTypeAudio,\n\t\t\t\t\tFormats: []format.Format{&format.MPEG4AudioLATM{\n\t\t\t\t\t\tPayloadTyp:     96,\n\t\t\t\t\t\tProfileLevelID: 30,\n\t\t\t\t\t\tStreamMuxConfig: &mpeg4audio.StreamMuxConfig{\n\t\t\t\t\t\t\tPrograms: []*mpeg4audio.StreamMuxConfigProgram{{\n\t\t\t\t\t\t\t\tLayers: []*mpeg4audio.StreamMuxConfigLayer{{\n\t\t\t\t\t\t\t\t\tAudioSpecificConfig: &mpeg4audio.AudioSpecificConfig{\n\t\t\t\t\t\t\t\t\t\tType:          2,\n\t\t\t\t\t\t\t\t\t\tSampleRate:    48000,\n\t\t\t\t\t\t\t\t\t\tChannelCount:  2,\n\t\t\t\t\t\t\t\t\t\tChannelConfig: 2,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tLatmBufferFullness: 255,\n\t\t\t\t\t\t\t\t}},\n\t\t\t\t\t\t\t}},\n\t\t\t\t\t\t},\n\t\t\t\t\t}},\n\t\t\t\t}}, desc)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestToStreamNoSupportedCodecs(t *testing.T) {\n\tvar buf bytes.Buffer\n\tmux := astits.NewMuxer(context.Background(), &buf)\n\n\terr := mux.AddElementaryStream(astits.PMTElementaryStream{\n\t\tElementaryPID: 122,\n\t\tStreamType:    astits.StreamTypeDTSAudio,\n\t})\n\trequire.NoError(t, err)\n\n\tmux.SetPCRPID(122)\n\n\t_, err = mux.WriteTables()\n\trequire.NoError(t, err)\n\n\tr := &EnhancedReader{R: &buf}\n\terr = r.Initialize()\n\trequire.NoError(t, err)\n\n\tl := test.Logger(func(logger.Level, string, ...any) {\n\t\tt.Error(\"should not happen\")\n\t})\n\t_, err = ToStream(r, nil, l)\n\trequire.Equal(t, errNoSupportedCodecs, err)\n}\n\nfunc TestToStreamSkipUnsupportedTracks(t *testing.T) {\n\tvar buf bytes.Buffer\n\tmux := astits.NewMuxer(context.Background(), &buf)\n\n\terr := mux.AddElementaryStream(astits.PMTElementaryStream{\n\t\tElementaryPID: 122,\n\t\tStreamType:    astits.StreamTypeDTSAudio,\n\t})\n\trequire.NoError(t, err)\n\n\terr = mux.AddElementaryStream(astits.PMTElementaryStream{\n\t\tElementaryPID: 123,\n\t\tStreamType:    astits.StreamTypeH264Video,\n\t})\n\trequire.NoError(t, err)\n\n\tmux.SetPCRPID(122)\n\n\t_, err = mux.WriteTables()\n\trequire.NoError(t, err)\n\n\tr := &EnhancedReader{R: &buf}\n\terr = r.Initialize()\n\trequire.NoError(t, err)\n\n\tn := 0\n\n\tl := test.Logger(func(l logger.Level, format string, args ...any) {\n\t\trequire.Equal(t, logger.Warn, l)\n\t\tif n == 0 {\n\t\t\trequire.Equal(t, \"skipping track 1 (unsupported codec)\", fmt.Sprintf(format, args...))\n\t\t}\n\t\tn++\n\t})\n\n\t_, err = ToStream(r, nil, l)\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "internal/protocols/rtmp/from_stream.go",
    "content": "// Package rtmp provides RTMP utilities.\npackage rtmp\n\nimport (\n\t\"errors\"\n\t\"net\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortmplib\"\n\t\"github.com/bluenviron/gortmplib/pkg/codecs\"\n\t\"github.com/bluenviron/gortmplib/pkg/message\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/ac3\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/h264\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/h265\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg1audio\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/opus\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n)\n\nvar errNoSupportedCodecsFrom = errors.New(\n\t\"the stream doesn't contain any supported codec, which are currently \" +\n\t\t\"AV1, VP9, H265, H264, Opus, MPEG-4 Audio, MPEG-1/2 Audio, AC-3, G711, LPCM\")\n\nfunc multiplyAndDivide2(v, m, d time.Duration) time.Duration {\n\tsecs := v / d\n\tdec := v % d\n\treturn (secs*m + dec*m/d)\n}\n\nfunc timestampToDuration(t int64, clockRate int) time.Duration {\n\treturn multiplyAndDivide2(time.Duration(t), time.Second, time.Duration(clockRate))\n}\n\n// FromStream maps a MediaMTX stream to a RTMP stream.\nfunc FromStream(\n\tdesc *description.Session,\n\tr *stream.Reader,\n\tconn *gortmplib.ServerConn,\n\tnconn net.Conn,\n\twriteTimeout time.Duration,\n) error {\n\tvar tracks []*gortmplib.Track\n\tvar w *gortmplib.Writer\n\n\tisEnhanced := len(conn.FourCcList) != 0\n\tlegacyVideoTrackCount := 0\n\tlegacyAudioTrackCount := 0\n\n\tfor _, media := range desc.Medias {\n\t\tfor _, forma := range media.Formats {\n\t\t\tswitch forma := forma.(type) {\n\t\t\tcase *format.AV1:\n\t\t\t\tif slices.Contains(conn.FourCcList, any(fourCCToString(message.FourCCAV1))) {\n\t\t\t\t\ttrack := &gortmplib.Track{\n\t\t\t\t\t\tCodec: &codecs.AV1{},\n\t\t\t\t\t}\n\t\t\t\t\ttracks = append(tracks, track)\n\n\t\t\t\t\tr.OnData(\n\t\t\t\t\t\tmedia,\n\t\t\t\t\t\tforma,\n\t\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tnconn.SetWriteDeadline(time.Now().Add(writeTimeout))\n\t\t\t\t\t\t\treturn (*w).WriteAV1(\n\t\t\t\t\t\t\t\ttrack,\n\t\t\t\t\t\t\t\ttimestampToDuration(u.PTS, forma.ClockRate()),\n\t\t\t\t\t\t\t\tu.Payload.(unit.PayloadAV1))\n\t\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\tcase *format.VP9:\n\t\t\t\tif slices.Contains(conn.FourCcList, any(fourCCToString(message.FourCCVP9))) {\n\t\t\t\t\ttrack := &gortmplib.Track{\n\t\t\t\t\t\tCodec: &codecs.VP9{},\n\t\t\t\t\t}\n\t\t\t\t\ttracks = append(tracks, track)\n\n\t\t\t\t\tr.OnData(\n\t\t\t\t\t\tmedia,\n\t\t\t\t\t\tforma,\n\t\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tnconn.SetWriteDeadline(time.Now().Add(writeTimeout))\n\t\t\t\t\t\t\treturn (*w).WriteVP9(\n\t\t\t\t\t\t\t\ttrack,\n\t\t\t\t\t\t\t\ttimestampToDuration(u.PTS, forma.ClockRate()),\n\t\t\t\t\t\t\t\tu.Payload.(unit.PayloadVP9))\n\t\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\tcase *format.H265:\n\t\t\t\tif slices.Contains(conn.FourCcList, any(fourCCToString(message.FourCCHEVC))) {\n\t\t\t\t\tvps, sps, pps := forma.SafeParams()\n\t\t\t\t\ttrack := &gortmplib.Track{\n\t\t\t\t\t\tCodec: &codecs.H265{\n\t\t\t\t\t\t\tVPS: vps,\n\t\t\t\t\t\t\tSPS: sps,\n\t\t\t\t\t\t\tPPS: pps,\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t\ttracks = append(tracks, track)\n\n\t\t\t\t\tvar videoDTSExtractor *h265.DTSExtractor\n\n\t\t\t\t\tr.OnData(\n\t\t\t\t\t\tmedia,\n\t\t\t\t\t\tforma,\n\t\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif videoDTSExtractor == nil {\n\t\t\t\t\t\t\t\tif !h265.IsRandomAccess(u.Payload.(unit.PayloadH265)) {\n\t\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\tvideoDTSExtractor = &h265.DTSExtractor{}\n\t\t\t\t\t\t\t\tvideoDTSExtractor.Initialize()\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tdts, err := videoDTSExtractor.Extract(u.Payload.(unit.PayloadH265), u.PTS)\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tnconn.SetWriteDeadline(time.Now().Add(writeTimeout))\n\t\t\t\t\t\t\treturn (*w).WriteH265(\n\t\t\t\t\t\t\t\ttrack,\n\t\t\t\t\t\t\t\ttimestampToDuration(u.PTS, forma.ClockRate()),\n\t\t\t\t\t\t\t\ttimestampToDuration(dts, forma.ClockRate()),\n\t\t\t\t\t\t\t\tu.Payload.(unit.PayloadH265))\n\t\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\tcase *format.H264:\n\t\t\t\tif isEnhanced || legacyVideoTrackCount == 0 {\n\t\t\t\t\tlegacyVideoTrackCount++\n\t\t\t\t\tsps, pps := forma.SafeParams()\n\t\t\t\t\ttrack := &gortmplib.Track{\n\t\t\t\t\t\tCodec: &codecs.H264{\n\t\t\t\t\t\t\tSPS: sps,\n\t\t\t\t\t\t\tPPS: pps,\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t\ttracks = append(tracks, track)\n\n\t\t\t\t\tvar videoDTSExtractor *h264.DTSExtractor\n\n\t\t\t\t\tr.OnData(\n\t\t\t\t\t\tmedia,\n\t\t\t\t\t\tforma,\n\t\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tidrPresent := false\n\t\t\t\t\t\t\tnonIDRPresent := false\n\n\t\t\t\t\t\t\tfor _, nalu := range u.Payload.(unit.PayloadH264) {\n\t\t\t\t\t\t\t\ttyp := h264.NALUType(nalu[0] & 0x1F)\n\t\t\t\t\t\t\t\tswitch typ {\n\t\t\t\t\t\t\t\tcase h264.NALUTypeIDR:\n\t\t\t\t\t\t\t\t\tidrPresent = true\n\n\t\t\t\t\t\t\t\tcase h264.NALUTypeNonIDR:\n\t\t\t\t\t\t\t\t\tnonIDRPresent = true\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t// wait until we receive an IDR\n\t\t\t\t\t\t\tif videoDTSExtractor == nil {\n\t\t\t\t\t\t\t\tif !idrPresent {\n\t\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tvideoDTSExtractor = &h264.DTSExtractor{}\n\t\t\t\t\t\t\t\tvideoDTSExtractor.Initialize()\n\t\t\t\t\t\t\t} else if !idrPresent && !nonIDRPresent {\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tdts, err := videoDTSExtractor.Extract(u.Payload.(unit.PayloadH264), u.PTS)\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tnconn.SetWriteDeadline(time.Now().Add(writeTimeout))\n\t\t\t\t\t\t\treturn (*w).WriteH264(\n\t\t\t\t\t\t\t\ttrack,\n\t\t\t\t\t\t\t\ttimestampToDuration(u.PTS, forma.ClockRate()),\n\t\t\t\t\t\t\t\ttimestampToDuration(dts, forma.ClockRate()),\n\t\t\t\t\t\t\t\tu.Payload.(unit.PayloadH264))\n\t\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\tcase *format.Opus:\n\t\t\t\tif slices.Contains(conn.FourCcList, any(fourCCToString(message.FourCCOpus))) {\n\t\t\t\t\ttrack := &gortmplib.Track{\n\t\t\t\t\t\tCodec: &codecs.Opus{\n\t\t\t\t\t\t\tChannelCount: forma.ChannelCount,\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t\ttracks = append(tracks, track)\n\n\t\t\t\t\tr.OnData(\n\t\t\t\t\t\tmedia,\n\t\t\t\t\t\tforma,\n\t\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tpts := u.PTS\n\n\t\t\t\t\t\t\tfor _, pkt := range u.Payload.(unit.PayloadOpus) {\n\t\t\t\t\t\t\t\tnconn.SetWriteDeadline(time.Now().Add(writeTimeout))\n\t\t\t\t\t\t\t\terr := (*w).WriteOpus(\n\t\t\t\t\t\t\t\t\ttrack,\n\t\t\t\t\t\t\t\t\ttimestampToDuration(pts, forma.ClockRate()),\n\t\t\t\t\t\t\t\t\tpkt,\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tpts += opus.PacketDuration2(pkt)\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\tcase *format.MPEG4Audio:\n\t\t\t\tif isEnhanced || legacyAudioTrackCount == 0 {\n\t\t\t\t\tlegacyAudioTrackCount++\n\t\t\t\t\ttrack := &gortmplib.Track{\n\t\t\t\t\t\tCodec: &codecs.MPEG4Audio{\n\t\t\t\t\t\t\tConfig: forma.Config,\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t\ttracks = append(tracks, track)\n\n\t\t\t\t\tr.OnData(\n\t\t\t\t\t\tmedia,\n\t\t\t\t\t\tforma,\n\t\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tfor i, au := range u.Payload.(unit.PayloadMPEG4Audio) {\n\t\t\t\t\t\t\t\tpts := u.PTS + int64(i)*mpeg4audio.SamplesPerAccessUnit\n\n\t\t\t\t\t\t\t\tnconn.SetWriteDeadline(time.Now().Add(writeTimeout))\n\t\t\t\t\t\t\t\terr := (*w).WriteMPEG4Audio(\n\t\t\t\t\t\t\t\t\ttrack,\n\t\t\t\t\t\t\t\t\ttimestampToDuration(pts, forma.ClockRate()),\n\t\t\t\t\t\t\t\t\tau,\n\t\t\t\t\t\t\t\t)\n\t\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\tcase *format.MPEG4AudioLATM:\n\t\t\t\tif !forma.CPresent && (isEnhanced || legacyAudioTrackCount == 0) {\n\t\t\t\t\tlegacyAudioTrackCount++\n\t\t\t\t\ttrack := &gortmplib.Track{\n\t\t\t\t\t\tCodec: &codecs.MPEG4Audio{\n\t\t\t\t\t\t\tConfig: forma.StreamMuxConfig.Programs[0].Layers[0].AudioSpecificConfig,\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t\ttracks = append(tracks, track)\n\n\t\t\t\t\tr.OnData(\n\t\t\t\t\t\tmedia,\n\t\t\t\t\t\tforma,\n\t\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tvar ame mpeg4audio.AudioMuxElement\n\t\t\t\t\t\t\tame.StreamMuxConfig = forma.StreamMuxConfig\n\t\t\t\t\t\t\terr := ame.Unmarshal(u.Payload.(unit.PayloadMPEG4AudioLATM))\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tnconn.SetWriteDeadline(time.Now().Add(writeTimeout))\n\t\t\t\t\t\t\treturn (*w).WriteMPEG4Audio(\n\t\t\t\t\t\t\t\ttrack,\n\t\t\t\t\t\t\t\ttimestampToDuration(u.PTS, forma.ClockRate()),\n\t\t\t\t\t\t\t\tame.Payloads[0][0][0],\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\tcase *format.MPEG1Audio:\n\t\t\t\tif isEnhanced || legacyAudioTrackCount == 0 {\n\t\t\t\t\tlegacyAudioTrackCount++\n\t\t\t\t\ttrack := &gortmplib.Track{\n\t\t\t\t\t\tCodec: &codecs.MPEG1Audio{},\n\t\t\t\t\t}\n\t\t\t\t\ttracks = append(tracks, track)\n\n\t\t\t\t\tr.OnData(\n\t\t\t\t\t\tmedia,\n\t\t\t\t\t\tforma,\n\t\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tpts := u.PTS\n\n\t\t\t\t\t\t\tfor _, frame := range u.Payload.(unit.PayloadMPEG1Audio) {\n\t\t\t\t\t\t\t\tvar h mpeg1audio.FrameHeader\n\t\t\t\t\t\t\t\terr := h.Unmarshal(frame)\n\t\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tnconn.SetWriteDeadline(time.Now().Add(writeTimeout))\n\t\t\t\t\t\t\t\terr = (*w).WriteMPEG1Audio(\n\t\t\t\t\t\t\t\t\ttrack,\n\t\t\t\t\t\t\t\t\ttimestampToDuration(pts, forma.ClockRate()),\n\t\t\t\t\t\t\t\t\tframe)\n\t\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\tpts += int64(h.SampleCount()) *\n\t\t\t\t\t\t\t\t\tint64(forma.ClockRate()) / int64(h.SampleRate)\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\tcase *format.AC3:\n\t\t\t\tif slices.Contains(conn.FourCcList, any(fourCCToString(message.FourCCAC3))) {\n\t\t\t\t\ttrack := &gortmplib.Track{\n\t\t\t\t\t\tCodec: &codecs.AC3{\n\t\t\t\t\t\t\tSampleRate:   forma.SampleRate,\n\t\t\t\t\t\t\tChannelCount: forma.ChannelCount,\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t\ttracks = append(tracks, track)\n\n\t\t\t\t\tr.OnData(\n\t\t\t\t\t\tmedia,\n\t\t\t\t\t\tforma,\n\t\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tfor i, frame := range u.Payload.(unit.PayloadAC3) {\n\t\t\t\t\t\t\t\tpts := u.PTS + int64(i)*ac3.SamplesPerFrame\n\n\t\t\t\t\t\t\t\tnconn.SetWriteDeadline(time.Now().Add(writeTimeout))\n\t\t\t\t\t\t\t\terr := (*w).WriteAC3(\n\t\t\t\t\t\t\t\t\ttrack,\n\t\t\t\t\t\t\t\t\ttimestampToDuration(pts, forma.ClockRate()),\n\t\t\t\t\t\t\t\t\tframe)\n\t\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\tcase *format.G711:\n\t\t\t\tif forma.SampleRate == 8000 && (isEnhanced || legacyAudioTrackCount == 0) {\n\t\t\t\t\tlegacyAudioTrackCount++\n\t\t\t\t\ttrack := &gortmplib.Track{\n\t\t\t\t\t\tCodec: &codecs.G711{\n\t\t\t\t\t\t\tMULaw:        forma.MULaw,\n\t\t\t\t\t\t\tChannelCount: forma.ChannelCount,\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t\ttracks = append(tracks, track)\n\n\t\t\t\t\tr.OnData(\n\t\t\t\t\t\tmedia,\n\t\t\t\t\t\tforma,\n\t\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tnconn.SetWriteDeadline(time.Now().Add(writeTimeout))\n\t\t\t\t\t\t\treturn (*w).WriteG711(\n\t\t\t\t\t\t\t\ttrack,\n\t\t\t\t\t\t\t\ttimestampToDuration(u.PTS, forma.ClockRate()),\n\t\t\t\t\t\t\t\tu.Payload.(unit.PayloadG711),\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\tcase *format.LPCM:\n\t\t\t\tif (forma.ChannelCount == 1 || forma.ChannelCount == 2) &&\n\t\t\t\t\t(forma.SampleRate == 5512 ||\n\t\t\t\t\t\tforma.SampleRate == 11025 ||\n\t\t\t\t\t\tforma.SampleRate == 22050 ||\n\t\t\t\t\t\tforma.SampleRate == 44100) &&\n\t\t\t\t\t(isEnhanced || legacyAudioTrackCount == 0) {\n\t\t\t\t\tlegacyAudioTrackCount++\n\t\t\t\t\ttrack := &gortmplib.Track{\n\t\t\t\t\t\tCodec: &codecs.LPCM{\n\t\t\t\t\t\t\tBitDepth:     forma.BitDepth,\n\t\t\t\t\t\t\tSampleRate:   forma.SampleRate,\n\t\t\t\t\t\t\tChannelCount: forma.ChannelCount,\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t\ttracks = append(tracks, track)\n\n\t\t\t\t\tr.OnData(\n\t\t\t\t\t\tmedia,\n\t\t\t\t\t\tforma,\n\t\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tnconn.SetWriteDeadline(time.Now().Add(writeTimeout))\n\t\t\t\t\t\t\treturn (*w).WriteLPCM(\n\t\t\t\t\t\t\t\ttrack,\n\t\t\t\t\t\t\t\ttimestampToDuration(u.PTS, forma.ClockRate()),\n\t\t\t\t\t\t\t\tu.Payload.(unit.PayloadLPCM),\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(tracks) == 0 {\n\t\treturn errNoSupportedCodecsFrom\n\t}\n\n\tw = &gortmplib.Writer{\n\t\tConn:   conn,\n\t\tTracks: tracks,\n\t}\n\terr := w.Initialize()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tsetuppedFormats := r.Formats()\n\n\tn := 1\n\tfor _, media := range desc.Medias {\n\t\tfor _, forma := range media.Formats {\n\t\t\tif !slices.Contains(setuppedFormats, forma) {\n\t\t\t\tr.Parent.Log(logger.Warn, \"skipping track %d (%s)\", n, forma.Codec())\n\t\t\t}\n\t\t\tn++\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/protocols/rtmp/from_stream_test.go",
    "content": "package rtmp\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/url\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortmplib\"\n\t\"github.com/bluenviron/gortmplib/pkg/codecs\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar (\n\th265DefaultVPS = []byte{\n\t\t0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x02, 0x20,\n\t\t0x00, 0x00, 0x03, 0x00, 0xb0, 0x00, 0x00, 0x03,\n\t\t0x00, 0x00, 0x03, 0x00, 0x7b, 0x18, 0xb0, 0x24,\n\t}\n\n\th265DefaultSPS = []byte{\n\t\t0x42, 0x01, 0x01, 0x02, 0x20, 0x00, 0x00, 0x03,\n\t\t0x00, 0xb0, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03,\n\t\t0x00, 0x7b, 0xa0, 0x07, 0x82, 0x00, 0x88, 0x7d,\n\t\t0xb6, 0x71, 0x8b, 0x92, 0x44, 0x80, 0x53, 0x88,\n\t\t0x88, 0x92, 0xcf, 0x24, 0xa6, 0x92, 0x72, 0xc9,\n\t\t0x12, 0x49, 0x22, 0xdc, 0x91, 0xaa, 0x48, 0xfc,\n\t\t0xa2, 0x23, 0xff, 0x00, 0x01, 0x00, 0x01, 0x6a,\n\t\t0x02, 0x02, 0x02, 0x01,\n\t}\n\n\th265DefaultPPS = []byte{\n\t\t0x44, 0x01, 0xc0, 0x25, 0x2f, 0x05, 0x32, 0x40,\n\t}\n\n\th264DefaultSPS = []byte{ // 1920x1080 baseline\n\t\t0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02,\n\t\t0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04,\n\t\t0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, 0x20,\n\t}\n\n\th264DefaultPPS = []byte{0x08, 0x06, 0x07, 0x08}\n)\n\nfunc TestFromStream(t *testing.T) {\n\th265VPS := []byte{\n\t\t0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x60,\n\t\t0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03,\n\t\t0x00, 0x00, 0x03, 0x00, 0x78, 0xba, 0x02, 0x40,\n\t}\n\th265SPS := []byte{\n\t\t0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03,\n\t\t0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03,\n\t\t0x00, 0x78, 0xa0, 0x03, 0xc0, 0x80, 0x11, 0x07,\n\t\t0xcb, 0x96, 0xe9, 0x29, 0x30, 0xbc, 0x05, 0xa0,\n\t\t0x20, 0x00, 0x00, 0x03, 0x00, 0x20, 0x00, 0x00,\n\t\t0x03, 0x03, 0xc1,\n\t}\n\th265PPS := []byte{\n\t\t0x44, 0x01, 0xc0, 0x73, 0xc1, 0x89,\n\t}\n\n\tcases := []struct {\n\t\tname           string\n\t\tmedias         []*description.Media\n\t\texpectedTracks []*gortmplib.Track\n\t\twriteUnits     func([]*description.Media, *stream.SubStream)\n\t}{\n\t\t{\n\t\t\tname: \"h264 + aac\",\n\t\t\tmedias: []*description.Media{\n\t\t\t\t{\n\t\t\t\t\tFormats: []format.Format{test.FormatH264},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tFormats: []format.Format{test.FormatMPEG4Audio},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedTracks: []*gortmplib.Track{\n\t\t\t\t{Codec: &codecs.H264{\n\t\t\t\t\tSPS: test.FormatH264.SPS,\n\t\t\t\t\tPPS: test.FormatH264.PPS,\n\t\t\t\t}},\n\t\t\t\t{Codec: &codecs.MPEG4Audio{\n\t\t\t\t\tConfig: test.FormatMPEG4Audio.Config,\n\t\t\t\t}},\n\t\t\t},\n\t\t\twriteUnits: func(medias []*description.Media, subStream *stream.SubStream) {\n\t\t\t\tsubStream.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{\n\t\t\t\t\tPTS: 0,\n\t\t\t\t\tPayload: unit.PayloadH264{\n\t\t\t\t\t\t{5, 2}, // IDR\n\t\t\t\t\t},\n\t\t\t\t})\n\n\t\t\t\tsubStream.WriteUnit(medias[1], medias[1].Formats[0], &unit.Unit{\n\t\t\t\t\tPTS: 90000 * 5,\n\t\t\t\t\tPayload: unit.PayloadMPEG4Audio{\n\t\t\t\t\t\t{3, 4},\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"av1\",\n\t\t\tmedias: []*description.Media{\n\t\t\t\t{\n\t\t\t\t\tFormats: []format.Format{&format.AV1{\n\t\t\t\t\t\tPayloadTyp: 96,\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedTracks: []*gortmplib.Track{\n\t\t\t\t{Codec: &codecs.AV1{}},\n\t\t\t},\n\t\t\twriteUnits: func(medias []*description.Media, subStream *stream.SubStream) {\n\t\t\t\tfor i := range 2 {\n\t\t\t\t\tsubStream.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{\n\t\t\t\t\t\tPTS: 90000 * 2 * int64(i),\n\t\t\t\t\t\tPayload: unit.PayloadAV1{{\n\t\t\t\t\t\t\t0x0a, 0x0e, 0x00, 0x00, 0x00, 0x4a, 0xab, 0xbf,\n\t\t\t\t\t\t\t0xc3, 0x77, 0x6b, 0xe4, 0x40, 0x40, 0x40, 0x41,\n\t\t\t\t\t\t}},\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"vp9\",\n\t\t\tmedias: []*description.Media{\n\t\t\t\t{\n\t\t\t\t\tFormats: []format.Format{&format.VP9{\n\t\t\t\t\t\tPayloadTyp: 96,\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedTracks: []*gortmplib.Track{\n\t\t\t\t{Codec: &codecs.VP9{}},\n\t\t\t},\n\t\t\twriteUnits: func(medias []*description.Media, subStream *stream.SubStream) {\n\t\t\t\tfor i := range 2 {\n\t\t\t\t\tsubStream.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{\n\t\t\t\t\t\tPTS:     90000 * 2 * int64(i),\n\t\t\t\t\t\tPayload: unit.PayloadVP9{1, 2},\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"h265\",\n\t\t\tmedias: []*description.Media{\n\t\t\t\t{\n\t\t\t\t\tFormats: []format.Format{\n\t\t\t\t\t\t&format.H265{\n\t\t\t\t\t\t\tPayloadTyp: 96,\n\t\t\t\t\t\t\tVPS:        h265VPS,\n\t\t\t\t\t\t\tSPS:        h265SPS,\n\t\t\t\t\t\t\tPPS:        h265PPS,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedTracks: []*gortmplib.Track{\n\t\t\t\t{Codec: &codecs.H265{\n\t\t\t\t\tVPS: h265VPS,\n\t\t\t\t\tSPS: h265SPS,\n\t\t\t\t\tPPS: h265PPS,\n\t\t\t\t}},\n\t\t\t},\n\t\t\twriteUnits: func(medias []*description.Media, subStream *stream.SubStream) {\n\t\t\t\tfor i := range 2 {\n\t\t\t\t\tsubStream.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{\n\t\t\t\t\t\tPTS: 90000 * 2 * int64(i),\n\t\t\t\t\t\tPayload: unit.PayloadH265{{\n\t\t\t\t\t\t\t0x2a, 0x01, 0xad, 0xe0, 0xf5, 0x34, 0x11, 0x0b,\n\t\t\t\t\t\t\t0x41, 0xe8,\n\t\t\t\t\t\t}},\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"h264\",\n\t\t\tmedias: []*description.Media{\n\t\t\t\t{\n\t\t\t\t\tFormats: []format.Format{test.FormatH264},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedTracks: []*gortmplib.Track{\n\t\t\t\t{Codec: &codecs.H264{\n\t\t\t\t\tSPS: test.FormatH264.SPS,\n\t\t\t\t\tPPS: test.FormatH264.PPS,\n\t\t\t\t}},\n\t\t\t},\n\t\t\twriteUnits: func(medias []*description.Media, subStream *stream.SubStream) {\n\t\t\t\tfor i := range 2 {\n\t\t\t\t\tsubStream.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{\n\t\t\t\t\t\tPTS: 90000 * 2 * int64(i),\n\t\t\t\t\t\tPayload: unit.PayloadH264{\n\t\t\t\t\t\t\t{5, 2}, // IDR\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"opus\",\n\t\t\tmedias: []*description.Media{\n\t\t\t\t{\n\t\t\t\t\tFormats: []format.Format{&format.Opus{\n\t\t\t\t\t\tPayloadTyp:   96,\n\t\t\t\t\t\tChannelCount: 2,\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedTracks: []*gortmplib.Track{\n\t\t\t\t{Codec: &codecs.Opus{\n\t\t\t\t\tChannelCount: 2,\n\t\t\t\t}},\n\t\t\t},\n\t\t\twriteUnits: func(medias []*description.Media, subStream *stream.SubStream) {\n\t\t\t\tfor i := range 2 {\n\t\t\t\t\tsubStream.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{\n\t\t\t\t\t\tPTS: 90000 * 5 * int64(i),\n\t\t\t\t\t\tPayload: unit.PayloadOpus{\n\t\t\t\t\t\t\t{3, 4},\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"aac\",\n\t\t\tmedias: []*description.Media{\n\t\t\t\t{\n\t\t\t\t\tFormats: []format.Format{test.FormatMPEG4Audio},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedTracks: []*gortmplib.Track{\n\t\t\t\t{Codec: &codecs.MPEG4Audio{\n\t\t\t\t\tConfig: test.FormatMPEG4Audio.Config,\n\t\t\t\t}},\n\t\t\t},\n\t\t\twriteUnits: func(medias []*description.Media, subStream *stream.SubStream) {\n\t\t\t\tfor i := range 2 {\n\t\t\t\t\tsubStream.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{\n\t\t\t\t\t\tPTS: 90000 * 5 * int64(i),\n\t\t\t\t\t\tPayload: unit.PayloadMPEG4Audio{\n\t\t\t\t\t\t\t{3, 4},\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"mp3\",\n\t\t\tmedias: []*description.Media{\n\t\t\t\t{\n\t\t\t\t\tFormats: []format.Format{&format.MPEG1Audio{}},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedTracks: []*gortmplib.Track{\n\t\t\t\t{Codec: &codecs.MPEG1Audio{}},\n\t\t\t},\n\t\t\twriteUnits: func(medias []*description.Media, subStream *stream.SubStream) {\n\t\t\t\tfor i := range 2 {\n\t\t\t\t\tsubStream.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{\n\t\t\t\t\t\tPTS: 90000 * 5 * int64(i),\n\t\t\t\t\t\tPayload: unit.PayloadMPEG1Audio{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t0xff, 0xfa, 0x52, 0x04, 0x00,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"ac-3\",\n\t\t\tmedias: []*description.Media{\n\t\t\t\t{\n\t\t\t\t\tFormats: []format.Format{&format.AC3{\n\t\t\t\t\t\tSampleRate:   44100,\n\t\t\t\t\t\tChannelCount: 2,\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedTracks: []*gortmplib.Track{\n\t\t\t\t{Codec: &codecs.AC3{\n\t\t\t\t\tSampleRate:   48000,\n\t\t\t\t\tChannelCount: 1,\n\t\t\t\t}},\n\t\t\t},\n\t\t\twriteUnits: func(medias []*description.Media, subStream *stream.SubStream) {\n\t\t\t\tfor i := range 2 {\n\t\t\t\t\tsubStream.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{\n\t\t\t\t\t\tPTS: 90000 * 5 * int64(i),\n\t\t\t\t\t\tPayload: unit.PayloadAC3{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t0x0b, 0x77, 0x47, 0x11, 0x0c, 0x40, 0x2f, 0x84,\n\t\t\t\t\t\t\t\t0x2b, 0xc1, 0x07, 0x7a, 0xb0, 0xfa, 0xbb, 0xea,\n\t\t\t\t\t\t\t\t0xef, 0x9f, 0x57, 0x7c, 0xf9, 0xf3, 0xf7, 0xcf,\n\t\t\t\t\t\t\t\t0x9f, 0x3e, 0x32, 0xfe, 0xd5, 0xc1, 0x50, 0xde,\n\t\t\t\t\t\t\t\t0xc5, 0x1e, 0x73, 0xd2, 0x6c, 0xa6, 0x94, 0x46,\n\t\t\t\t\t\t\t\t0x4e, 0x92, 0x8c, 0x0f, 0xb9, 0xcf, 0xad, 0x07,\n\t\t\t\t\t\t\t\t0x54, 0x4a, 0x2e, 0xf3, 0x7d, 0x07, 0x2e, 0xa4,\n\t\t\t\t\t\t\t\t0x2f, 0xba, 0xbf, 0x39, 0xb5, 0xc9, 0x92, 0xa6,\n\t\t\t\t\t\t\t\t0xe1, 0xb4, 0x70, 0xc5, 0xc4, 0xb5, 0xe6, 0x5d,\n\t\t\t\t\t\t\t\t0x0f, 0xa8, 0x71, 0xa4, 0xcc, 0xc5, 0xbc, 0x75,\n\t\t\t\t\t\t\t\t0x67, 0x92, 0x52, 0x4f, 0x7e, 0x62, 0x1c, 0xa9,\n\t\t\t\t\t\t\t\t0xd9, 0xb5, 0x19, 0x6a, 0xd7, 0xb0, 0x44, 0x92,\n\t\t\t\t\t\t\t\t0x30, 0x3b, 0xf7, 0x61, 0xd6, 0x49, 0x96, 0x66,\n\t\t\t\t\t\t\t\t0x98, 0x28, 0x1a, 0x95, 0xa9, 0x42, 0xad, 0xb7,\n\t\t\t\t\t\t\t\t0x50, 0x90, 0xad, 0x1c, 0x34, 0x80, 0xe2, 0xef,\n\t\t\t\t\t\t\t\t0xcd, 0x41, 0x0b, 0xf0, 0x9d, 0x57, 0x62, 0x78,\n\t\t\t\t\t\t\t\t0xfd, 0xc6, 0xc2, 0x19, 0x9e, 0x26, 0x31, 0xca,\n\t\t\t\t\t\t\t\t0x1e, 0x75, 0xb1, 0x7a, 0x8e, 0xb5, 0x51, 0x3a,\n\t\t\t\t\t\t\t\t0xfe, 0xe4, 0xf1, 0x0b, 0x4f, 0x14, 0x90, 0xdb,\n\t\t\t\t\t\t\t\t0x9f, 0x44, 0x50, 0xbb, 0xef, 0x74, 0x00, 0x8c,\n\t\t\t\t\t\t\t\t0x1f, 0x97, 0xa1, 0xa2, 0xfa, 0x72, 0x16, 0x47,\n\t\t\t\t\t\t\t\t0xc6, 0xc0, 0xe5, 0xfe, 0x67, 0x03, 0x9c, 0xfe,\n\t\t\t\t\t\t\t\t0x62, 0x01, 0xa1, 0x00, 0x5d, 0xff, 0xa5, 0x03,\n\t\t\t\t\t\t\t\t0x59, 0xfa, 0xa8, 0x25, 0x5f, 0x6b, 0x83, 0x51,\n\t\t\t\t\t\t\t\t0xf2, 0xc0, 0x44, 0xff, 0x2d, 0x05, 0x4b, 0xee,\n\t\t\t\t\t\t\t\t0xe0, 0x54, 0x9e, 0xae, 0x86, 0x45, 0xf3, 0xbd,\n\t\t\t\t\t\t\t\t0x0e, 0x42, 0xf2, 0xbf, 0x0f, 0x7f, 0xc6, 0x09,\n\t\t\t\t\t\t\t\t0x07, 0xdc, 0x22, 0x11, 0x77, 0xbe, 0x31, 0x27,\n\t\t\t\t\t\t\t\t0x5b, 0xa4, 0x13, 0x47, 0x07, 0x32, 0x9f, 0x1f,\n\t\t\t\t\t\t\t\t0xcb, 0xb0, 0xdf, 0x3e, 0x7d, 0x0d, 0xf3, 0xe7,\n\t\t\t\t\t\t\t\t0xcf, 0x9f, 0x3e, 0xae, 0xf9, 0xf3, 0xe7, 0xcf,\n\t\t\t\t\t\t\t\t0x9f, 0x3e, 0x85, 0x5d, 0xf3, 0xe7, 0xcf, 0x9f,\n\t\t\t\t\t\t\t\t0x3e, 0x7c, 0xf9, 0xf3, 0xe7, 0xcf, 0x9f, 0x3f,\n\t\t\t\t\t\t\t\t0x53, 0x5d, 0xf3, 0xe7, 0xcf, 0x9f, 0x3e, 0x7c,\n\t\t\t\t\t\t\t\t0xf9, 0xf3, 0xe7, 0xcf, 0x9f, 0x3e, 0x7c, 0xf9,\n\t\t\t\t\t\t\t\t0xf3, 0xe7, 0xcf, 0x9f, 0x3e, 0x7c, 0xf9, 0xf3,\n\t\t\t\t\t\t\t\t0xe7, 0xcf, 0x9f, 0x3e, 0x00, 0x46, 0x28, 0x26,\n\t\t\t\t\t\t\t\t0x20, 0x4a, 0x5a, 0xc0, 0x8a, 0xc5, 0xae, 0xa0,\n\t\t\t\t\t\t\t\t0x55, 0x78, 0x82, 0x7a, 0x38, 0x10, 0x09, 0xc9,\n\t\t\t\t\t\t\t\t0xb8, 0x0c, 0xfa, 0x5b, 0xc9, 0xd2, 0xec, 0x44,\n\t\t\t\t\t\t\t\t0x25, 0xf8, 0x20, 0xf2, 0xc8, 0x8a, 0xe9, 0x40,\n\t\t\t\t\t\t\t\t0x18, 0x06, 0xc6, 0x2b, 0xc8, 0xed, 0x8f, 0x33,\n\t\t\t\t\t\t\t\t0x09, 0x92, 0x28, 0x1e, 0xc4, 0x24, 0xd8, 0x33,\n\t\t\t\t\t\t\t\t0xa5, 0x00, 0xf5, 0xea, 0x18, 0xfa, 0x90, 0x97,\n\t\t\t\t\t\t\t\t0x97, 0xe8, 0x39, 0x6a, 0xcf, 0xf1, 0xdd, 0xff,\n\t\t\t\t\t\t\t\t0x9e, 0x8e, 0x04, 0x02, 0xae, 0x65, 0x87, 0x5c,\n\t\t\t\t\t\t\t\t0x4e, 0x72, 0xfd, 0x3c, 0x01, 0x86, 0xfe, 0x56,\n\t\t\t\t\t\t\t\t0x59, 0x74, 0x44, 0x3a, 0x40, 0x00, 0xec, 0xfc,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"pcma\",\n\t\t\tmedias: []*description.Media{\n\t\t\t\t{\n\t\t\t\t\tFormats: []format.Format{&format.G711{\n\t\t\t\t\t\tMULaw:        false,\n\t\t\t\t\t\tSampleRate:   8000,\n\t\t\t\t\t\tChannelCount: 1,\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedTracks: []*gortmplib.Track{\n\t\t\t\t{Codec: &codecs.G711{\n\t\t\t\t\tMULaw:        false,\n\t\t\t\t\tChannelCount: 1,\n\t\t\t\t}},\n\t\t\t},\n\t\t\twriteUnits: func(medias []*description.Media, subStream *stream.SubStream) {\n\t\t\t\tfor i := range 2 {\n\t\t\t\t\tsubStream.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{\n\t\t\t\t\t\tPTS: 90000 * 5 * int64(i),\n\t\t\t\t\t\tPayload: unit.PayloadG711{\n\t\t\t\t\t\t\t3, 4,\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"pcmu\",\n\t\t\tmedias: []*description.Media{\n\t\t\t\t{\n\t\t\t\t\tFormats: []format.Format{&format.G711{\n\t\t\t\t\t\tMULaw:        true,\n\t\t\t\t\t\tSampleRate:   8000,\n\t\t\t\t\t\tChannelCount: 1,\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedTracks: []*gortmplib.Track{\n\t\t\t\t{Codec: &codecs.G711{\n\t\t\t\t\tMULaw:        true,\n\t\t\t\t\tChannelCount: 1,\n\t\t\t\t}},\n\t\t\t},\n\t\t\twriteUnits: func(medias []*description.Media, subStream *stream.SubStream) {\n\t\t\t\tfor i := range 2 {\n\t\t\t\t\tsubStream.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{\n\t\t\t\t\t\tPTS: 90000 * 5 * int64(i),\n\t\t\t\t\t\tPayload: unit.PayloadG711{\n\t\t\t\t\t\t\t3, 4,\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"lpcm\",\n\t\t\tmedias: []*description.Media{\n\t\t\t\t{\n\t\t\t\t\tFormats: []format.Format{&format.LPCM{\n\t\t\t\t\t\tBitDepth:     16,\n\t\t\t\t\t\tSampleRate:   44100,\n\t\t\t\t\t\tChannelCount: 2,\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedTracks: []*gortmplib.Track{\n\t\t\t\t{Codec: &codecs.LPCM{\n\t\t\t\t\tBitDepth:     16,\n\t\t\t\t\tSampleRate:   44100,\n\t\t\t\t\tChannelCount: 2,\n\t\t\t\t}},\n\t\t\t},\n\t\t\twriteUnits: func(medias []*description.Media, subStream *stream.SubStream) {\n\t\t\t\tfor i := range 2 {\n\t\t\t\t\tsubStream.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{\n\t\t\t\t\t\tPTS: 90000 * 5 * int64(i),\n\t\t\t\t\t\tPayload: unit.PayloadLPCM{\n\t\t\t\t\t\t\t3, 4, 5, 6,\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname: \"h265 + h264 + vp9 + av1 + opus + aac\",\n\t\t\tmedias: []*description.Media{\n\t\t\t\t{\n\t\t\t\t\tFormats: []format.Format{&format.H265{}},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tFormats: []format.Format{&format.H264{}},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tFormats: []format.Format{&format.VP9{}},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tFormats: []format.Format{&format.AV1{}},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tFormats: []format.Format{&format.Opus{\n\t\t\t\t\t\tPayloadTyp:   96,\n\t\t\t\t\t\tChannelCount: 2,\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tFormats: []format.Format{&format.MPEG4Audio{\n\t\t\t\t\t\tPayloadTyp:       96,\n\t\t\t\t\t\tConfig:           test.FormatMPEG4Audio.Config,\n\t\t\t\t\t\tSizeLength:       13,\n\t\t\t\t\t\tIndexLength:      3,\n\t\t\t\t\t\tIndexDeltaLength: 3,\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t},\n\t\t\texpectedTracks: []*gortmplib.Track{\n\t\t\t\t{Codec: &codecs.H265{\n\t\t\t\t\tVPS: h265DefaultVPS,\n\t\t\t\t\tSPS: h265DefaultSPS,\n\t\t\t\t\tPPS: h265DefaultPPS,\n\t\t\t\t}},\n\t\t\t\t{Codec: &codecs.H264{\n\t\t\t\t\tSPS: h264DefaultSPS,\n\t\t\t\t\tPPS: h264DefaultPPS,\n\t\t\t\t}},\n\t\t\t\t{Codec: &codecs.VP9{}},\n\t\t\t\t{Codec: &codecs.AV1{}},\n\t\t\t\t{Codec: &codecs.Opus{\n\t\t\t\t\tChannelCount: 2,\n\t\t\t\t}},\n\t\t\t\t{Codec: &codecs.MPEG4Audio{\n\t\t\t\t\tConfig: test.FormatMPEG4Audio.Config,\n\t\t\t\t}},\n\t\t\t},\n\t\t\twriteUnits: func(medias []*description.Media, subStream *stream.SubStream) {\n\t\t\t\tsubStream.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{\n\t\t\t\t\tPayload: unit.PayloadH265{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x60,\n\t\t\t\t\t\t\t0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03,\n\t\t\t\t\t\t\t0x00, 0x00, 0x03, 0x00, 0x78, 0xba, 0x02, 0x40,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03,\n\t\t\t\t\t\t\t0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03,\n\t\t\t\t\t\t\t0x00, 0x78, 0xa0, 0x03, 0xc0, 0x80, 0x11, 0x07,\n\t\t\t\t\t\t\t0xcb, 0x96, 0xe9, 0x29, 0x30, 0xbc, 0x05, 0xa0,\n\t\t\t\t\t\t\t0x20, 0x00, 0x00, 0x03, 0x00, 0x20, 0x00, 0x00,\n\t\t\t\t\t\t\t0x03, 0x03, 0xc1,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t0x44, 0x01, 0xc0, 0x73, 0xc1, 0x89,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\t0x2a, 0x01, 0xad, 0xe0, 0xf5, 0x34, 0x11, 0x0b,\n\t\t\t\t\t\t\t0x41, 0xe8,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t})\n\n\t\t\t\tsubStream.WriteUnit(medias[1], medias[1].Formats[0], &unit.Unit{\n\t\t\t\t\tPayload: unit.PayloadH264{\n\t\t\t\t\t\th264DefaultSPS,\n\t\t\t\t\t\th264DefaultPPS,\n\t\t\t\t\t\t{5, 2}, // IDR\n\t\t\t\t\t},\n\t\t\t\t})\n\n\t\t\t\tsubStream.WriteUnit(medias[2], medias[2].Formats[0], &unit.Unit{\n\t\t\t\t\tPayload: unit.PayloadVP9{1, 2},\n\t\t\t\t})\n\n\t\t\t\tsubStream.WriteUnit(medias[3], medias[3].Formats[0], &unit.Unit{\n\t\t\t\t\tPayload: unit.PayloadAV1{{\n\t\t\t\t\t\t0x0a, 0x0e, 0x00, 0x00, 0x00, 0x4a, 0xab, 0xbf,\n\t\t\t\t\t\t0xc3, 0x77, 0x6b, 0xe4, 0x40, 0x40, 0x40, 0x41,\n\t\t\t\t\t}},\n\t\t\t\t})\n\n\t\t\t\tsubStream.WriteUnit(medias[4], medias[4].Formats[0], &unit.Unit{\n\t\t\t\t\tPayload: unit.PayloadOpus{\n\t\t\t\t\t\t{3, 4},\n\t\t\t\t\t},\n\t\t\t\t})\n\n\t\t\t\tsubStream.WriteUnit(medias[5], medias[5].Formats[0], &unit.Unit{\n\t\t\t\t\tPTS: 90000 * 5,\n\t\t\t\t\tPayload: unit.PayloadMPEG4Audio{\n\t\t\t\t\t\t{3, 4},\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t},\n\t\t},\n\t}\n\n\tfor _, tc := range cases {\n\t\tt.Run(tc.name, func(t *testing.T) {\n\t\t\tmedias := tc.medias\n\n\t\t\tstrm := &stream.Stream{\n\t\t\t\tDesc:              &description.Session{Medias: medias},\n\t\t\t\tWriteQueueSize:    512,\n\t\t\t\tRTPMaxPayloadSize: 1450,\n\t\t\t\tParent:            test.NilLogger,\n\t\t\t}\n\t\t\terr := strm.Initialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsubStream := &stream.SubStream{\n\t\t\t\tStream:        strm,\n\t\t\t\tUseRTPPackets: false,\n\t\t\t}\n\t\t\terr = subStream.Initialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tln, err := net.Listen(\"tcp\", \"127.0.0.1:9121\")\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer ln.Close()\n\n\t\t\tdone := make(chan struct{})\n\n\t\t\tgo func() {\n\t\t\t\tu, err2 := url.Parse(\"rtmp://127.0.0.1:9121/stream\")\n\t\t\t\trequire.NoError(t, err2)\n\n\t\t\t\tc := &gortmplib.Client{\n\t\t\t\t\tURL: u,\n\t\t\t\t}\n\t\t\t\terr2 = c.Initialize(context.Background())\n\t\t\t\trequire.NoError(t, err2)\n\n\t\t\t\tr := &gortmplib.Reader{\n\t\t\t\t\tConn: c,\n\t\t\t\t}\n\t\t\t\terr2 = r.Initialize()\n\t\t\t\trequire.NoError(t, err2)\n\n\t\t\t\trequire.Equal(t, tc.expectedTracks, r.Tracks())\n\n\t\t\t\tclose(done)\n\t\t\t}()\n\n\t\t\tnconn, err := ln.Accept()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer nconn.Close()\n\n\t\t\tconn := &gortmplib.ServerConn{\n\t\t\t\tRW: nconn,\n\t\t\t}\n\t\t\terr = conn.Initialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = conn.Accept()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tr := &stream.Reader{Parent: test.NilLogger}\n\n\t\t\terr = FromStream(strm.Desc, r, conn, nconn, 10*time.Second)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tstrm.AddReader(r)\n\t\t\tdefer strm.RemoveReader(r)\n\n\t\t\ttc.writeUnits(medias, subStream)\n\n\t\t\t<-done\n\t\t})\n\t}\n}\n\nfunc TestFromStreamLegacyClientMultipleTracks(t *testing.T) {\n\t// Test that legacy RTMP clients (without enhanced RTMP)\n\t// only receive one H264 track and one MPEG4-audio track\n\t// when multiple tracks of each type are available\n\n\th264SPS1 := []byte{\n\t\t0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02,\n\t\t0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04,\n\t\t0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, 0x20,\n\t}\n\th264PPS1 := []byte{0x08, 0x06, 0x07, 0x08}\n\n\th264SPS2 := []byte{\n\t\t0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02,\n\t\t0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04,\n\t\t0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, 0x21,\n\t}\n\th264PPS2 := []byte{0x08, 0x06, 0x07, 0x09}\n\n\taacConfig1 := test.FormatMPEG4Audio.Config\n\taacConfig2 := &mpeg4audio.AudioSpecificConfig{\n\t\tType:          2, // MPEG4-AAC LC\n\t\tSampleRate:    48000,\n\t\tChannelCount:  2,\n\t\tChannelConfig: 2,\n\t}\n\n\tmedias := []*description.Media{\n\t\t{\n\t\t\tFormats: []format.Format{&format.H264{\n\t\t\t\tPayloadTyp:        96,\n\t\t\t\tPacketizationMode: 1,\n\t\t\t\tSPS:               h264SPS1,\n\t\t\t\tPPS:               h264PPS1,\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tFormats: []format.Format{&format.H264{\n\t\t\t\tPayloadTyp:        97,\n\t\t\t\tPacketizationMode: 1,\n\t\t\t\tSPS:               h264SPS2,\n\t\t\t\tPPS:               h264PPS2,\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tFormats: []format.Format{&format.MPEG4Audio{\n\t\t\t\tPayloadTyp:       98,\n\t\t\t\tConfig:           aacConfig1,\n\t\t\t\tSizeLength:       13,\n\t\t\t\tIndexLength:      3,\n\t\t\t\tIndexDeltaLength: 3,\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tFormats: []format.Format{&format.MPEG4Audio{\n\t\t\t\tPayloadTyp:       99,\n\t\t\t\tConfig:           aacConfig2,\n\t\t\t\tSizeLength:       13,\n\t\t\t\tIndexLength:      3,\n\t\t\t\tIndexDeltaLength: 3,\n\t\t\t}},\n\t\t},\n\t}\n\n\tstrm := &stream.Stream{\n\t\tDesc:              &description.Session{Medias: medias},\n\t\tWriteQueueSize:    512,\n\t\tRTPMaxPayloadSize: 1450,\n\t\tParent:            test.NilLogger,\n\t}\n\terr := strm.Initialize()\n\trequire.NoError(t, err)\n\n\tsubStream := &stream.SubStream{\n\t\tStream:        strm,\n\t\tUseRTPPackets: false,\n\t}\n\terr = subStream.Initialize()\n\trequire.NoError(t, err)\n\n\tln, err := net.Listen(\"tcp\", \"127.0.0.1:9121\")\n\trequire.NoError(t, err)\n\tdefer ln.Close()\n\n\tdone := make(chan struct{})\n\n\tgo func() {\n\t\tu, err2 := url.Parse(\"rtmp://127.0.0.1:9121/stream\")\n\t\trequire.NoError(t, err2)\n\n\t\tc := &gortmplib.Client{\n\t\t\tURL: u,\n\t\t}\n\t\terr2 = c.Initialize(context.Background())\n\t\trequire.NoError(t, err2)\n\n\t\tr := &gortmplib.Reader{\n\t\t\tConn: c,\n\t\t}\n\t\terr2 = r.Initialize()\n\t\trequire.NoError(t, err2)\n\n\t\trequire.Equal(t, []*gortmplib.Track{\n\t\t\t{Codec: &codecs.H264{\n\t\t\t\tSPS: h264SPS1,\n\t\t\t\tPPS: h264PPS1,\n\t\t\t}},\n\t\t\t{Codec: &codecs.MPEG4Audio{\n\t\t\t\tConfig: aacConfig1,\n\t\t\t}},\n\t\t}, r.Tracks())\n\n\t\tclose(done)\n\t}()\n\n\tnconn, err := ln.Accept()\n\trequire.NoError(t, err)\n\tdefer nconn.Close()\n\n\tconn := &gortmplib.ServerConn{\n\t\tRW: nconn,\n\t}\n\terr = conn.Initialize()\n\trequire.NoError(t, err)\n\n\terr = conn.Accept()\n\trequire.NoError(t, err)\n\n\t// Simulate a legacy client by clearing the FourCcList\n\tconn.FourCcList = []any{}\n\n\tr := &stream.Reader{Parent: test.NilLogger}\n\n\terr = FromStream(strm.Desc, r, conn, nconn, 10*time.Second)\n\trequire.NoError(t, err)\n\n\tstrm.AddReader(r)\n\tdefer strm.RemoveReader(r)\n\n\t// Write units to trigger track setup\n\tsubStream.WriteUnit(medias[0], medias[0].Formats[0], &unit.Unit{\n\t\tPTS: 0,\n\t\tPayload: unit.PayloadH264{\n\t\t\t{5, 1}, // IDR\n\t\t},\n\t})\n\n\tsubStream.WriteUnit(medias[2], medias[2].Formats[0], &unit.Unit{\n\t\tPTS: 90000,\n\t\tPayload: unit.PayloadMPEG4Audio{\n\t\t\t{3, 4},\n\t\t},\n\t})\n\n\t<-done\n}\n\nfunc TestFromStreamNoSupportedCodecs(t *testing.T) {\n\tdesc := &description.Session{Medias: []*description.Media{{\n\t\tType:    description.MediaTypeVideo,\n\t\tFormats: []format.Format{&format.VP8{}},\n\t}}}\n\n\tr := &stream.Reader{\n\t\tParent: test.Logger(func(logger.Level, string, ...any) {\n\t\t\tt.Error(\"should not happen\")\n\t\t}),\n\t}\n\n\tconn := &gortmplib.ServerConn{}\n\n\terr := FromStream(desc, r, conn, nil, 0)\n\trequire.Equal(t, errNoSupportedCodecsFrom, err)\n}\n\nfunc TestFromStreamSkipUnsupportedTracks(t *testing.T) {\n\tdesc := &description.Session{Medias: []*description.Media{\n\t\t{\n\t\t\tType:    description.MediaTypeVideo,\n\t\t\tFormats: []format.Format{&format.VP8{}},\n\t\t},\n\t\t{\n\t\t\tType:    description.MediaTypeVideo,\n\t\t\tFormats: []format.Format{&format.H264{}},\n\t\t},\n\t}}\n\n\tn := 0\n\n\tr := &stream.Reader{\n\t\tParent: test.Logger(func(l logger.Level, format string, args ...any) {\n\t\t\trequire.Equal(t, logger.Warn, l)\n\t\t\tif n == 0 {\n\t\t\t\trequire.Equal(t, \"skipping track 1 (VP8)\", fmt.Sprintf(format, args...))\n\t\t\t}\n\t\t\tn++\n\t\t}),\n\t}\n\n\tln, err := net.Listen(\"tcp\", \"127.0.0.1:9121\")\n\trequire.NoError(t, err)\n\tdefer ln.Close()\n\n\tgo func() {\n\t\tu, err2 := url.Parse(\"rtmp://127.0.0.1:9121/stream\")\n\t\trequire.NoError(t, err2)\n\n\t\tc := &gortmplib.Client{\n\t\t\tURL: u,\n\t\t}\n\t\terr2 = c.Initialize(context.Background())\n\t\trequire.NoError(t, err2)\n\t}()\n\n\tnconn, err := ln.Accept()\n\trequire.NoError(t, err)\n\tdefer nconn.Close()\n\n\tconn := &gortmplib.ServerConn{\n\t\tRW: nconn,\n\t}\n\terr = conn.Initialize()\n\trequire.NoError(t, err)\n\n\terr = conn.Accept()\n\trequire.NoError(t, err)\n\n\terr = FromStream(desc, r, conn, nil, 0)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, 1, n)\n}\n"
  },
  {
    "path": "internal/protocols/rtmp/to_stream.go",
    "content": "package rtmp\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortmplib\"\n\t\"github.com/bluenviron/gortmplib/pkg/codecs\"\n\t\"github.com/bluenviron/gortmplib/pkg/message\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n)\n\nvar errNoSupportedCodecsTo = errors.New(\n\t\"the stream doesn't contain any supported codec, which are currently \" +\n\t\t\"AV1, VP9, H265, H264, MPEG-4 Audio, MPEG-1/2 Audio, G711, LPCM\")\n\nfunc multiplyAndDivide(v, m, d int64) int64 {\n\tsecs := v / d\n\tdec := v % d\n\treturn (secs*m + dec*m/d)\n}\n\nfunc durationToTimestamp(d time.Duration, clockRate int) int64 {\n\treturn multiplyAndDivide(int64(d), int64(clockRate), int64(time.Second))\n}\n\nfunc fourCCToString(c message.FourCC) string {\n\treturn string([]byte{byte(c >> 24), byte(c >> 16), byte(c >> 8), byte(c)})\n}\n\n// ToStream maps a RTMP stream to a MediaMTX stream.\nfunc ToStream(\n\tr *gortmplib.Reader,\n\tsubStream **stream.SubStream,\n) ([]*description.Media, error) {\n\tvar medias []*description.Media\n\n\tfor _, track := range r.Tracks() {\n\t\tswitch codec := track.Codec.(type) {\n\t\tcase *codecs.AV1:\n\t\t\tforma := &format.AV1{\n\t\t\t\tPayloadTyp: 96,\n\t\t\t}\n\t\t\tmedi := &description.Media{\n\t\t\t\tType:    description.MediaTypeVideo,\n\t\t\t\tFormats: []format.Format{forma},\n\t\t\t}\n\t\t\tmedias = append(medias, medi)\n\n\t\t\tr.OnDataAV1(track, func(pts time.Duration, tu [][]byte) {\n\t\t\t\t(*subStream).WriteUnit(medi, forma, &unit.Unit{\n\t\t\t\t\tPTS:     durationToTimestamp(pts, forma.ClockRate()),\n\t\t\t\t\tPayload: unit.PayloadAV1(tu),\n\t\t\t\t})\n\t\t\t})\n\n\t\tcase *codecs.VP9:\n\t\t\tforma := &format.VP9{\n\t\t\t\tPayloadTyp: 96,\n\t\t\t}\n\t\t\tmedi := &description.Media{\n\t\t\t\tType:    description.MediaTypeVideo,\n\t\t\t\tFormats: []format.Format{forma},\n\t\t\t}\n\t\t\tmedias = append(medias, medi)\n\n\t\t\tr.OnDataVP9(track, func(pts time.Duration, frame []byte) {\n\t\t\t\t(*subStream).WriteUnit(medi, forma, &unit.Unit{\n\t\t\t\t\tPTS:     durationToTimestamp(pts, forma.ClockRate()),\n\t\t\t\t\tPayload: unit.PayloadVP9(frame),\n\t\t\t\t})\n\t\t\t})\n\n\t\tcase *codecs.H265:\n\t\t\tforma := &format.H265{\n\t\t\t\tPayloadTyp: 96,\n\t\t\t\tVPS:        codec.VPS,\n\t\t\t\tSPS:        codec.SPS,\n\t\t\t\tPPS:        codec.PPS,\n\t\t\t}\n\t\t\tmedi := &description.Media{\n\t\t\t\tType:    description.MediaTypeVideo,\n\t\t\t\tFormats: []format.Format{forma},\n\t\t\t}\n\t\t\tmedias = append(medias, medi)\n\n\t\t\tr.OnDataH265(track, func(pts time.Duration, _ time.Duration, au [][]byte) {\n\t\t\t\t(*subStream).WriteUnit(medi, forma, &unit.Unit{\n\t\t\t\t\tPTS:     durationToTimestamp(pts, forma.ClockRate()),\n\t\t\t\t\tPayload: unit.PayloadH265(au),\n\t\t\t\t})\n\t\t\t})\n\n\t\tcase *codecs.H264:\n\t\t\tforma := &format.H264{\n\t\t\t\tPayloadTyp:        96,\n\t\t\t\tSPS:               codec.SPS,\n\t\t\t\tPPS:               codec.PPS,\n\t\t\t\tPacketizationMode: 1,\n\t\t\t}\n\t\t\tmedi := &description.Media{\n\t\t\t\tType:    description.MediaTypeVideo,\n\t\t\t\tFormats: []format.Format{forma},\n\t\t\t}\n\t\t\tmedias = append(medias, medi)\n\n\t\t\tr.OnDataH264(track, func(pts time.Duration, _ time.Duration, au [][]byte) {\n\t\t\t\t(*subStream).WriteUnit(medi, forma, &unit.Unit{\n\t\t\t\t\tPTS:     durationToTimestamp(pts, forma.ClockRate()),\n\t\t\t\t\tPayload: unit.PayloadH264(au),\n\t\t\t\t})\n\t\t\t})\n\n\t\tcase *codecs.Opus:\n\t\t\tforma := &format.Opus{\n\t\t\t\tPayloadTyp:   96,\n\t\t\t\tChannelCount: codec.ChannelCount,\n\t\t\t}\n\t\t\tmedi := &description.Media{\n\t\t\t\tType:    description.MediaTypeAudio,\n\t\t\t\tFormats: []format.Format{forma},\n\t\t\t}\n\t\t\tmedias = append(medias, medi)\n\n\t\t\tr.OnDataOpus(track, func(pts time.Duration, packet []byte) {\n\t\t\t\t(*subStream).WriteUnit(medi, forma, &unit.Unit{\n\t\t\t\t\tPTS:     durationToTimestamp(pts, forma.ClockRate()),\n\t\t\t\t\tPayload: unit.PayloadOpus{packet},\n\t\t\t\t})\n\t\t\t})\n\n\t\tcase *codecs.MPEG4Audio:\n\t\t\tforma := &format.MPEG4Audio{\n\t\t\t\tPayloadTyp:       96,\n\t\t\t\tConfig:           codec.Config,\n\t\t\t\tSizeLength:       13,\n\t\t\t\tIndexLength:      3,\n\t\t\t\tIndexDeltaLength: 3,\n\t\t\t}\n\t\t\tmedi := &description.Media{\n\t\t\t\tType:    description.MediaTypeAudio,\n\t\t\t\tFormats: []format.Format{forma},\n\t\t\t}\n\t\t\tmedias = append(medias, medi)\n\n\t\t\tr.OnDataMPEG4Audio(track, func(pts time.Duration, au []byte) {\n\t\t\t\t(*subStream).WriteUnit(medi, forma, &unit.Unit{\n\t\t\t\t\tPTS:     durationToTimestamp(pts, forma.ClockRate()),\n\t\t\t\t\tPayload: unit.PayloadMPEG4Audio{au},\n\t\t\t\t})\n\t\t\t})\n\n\t\tcase *codecs.MPEG1Audio:\n\t\t\tforma := &format.MPEG1Audio{}\n\t\t\tmedi := &description.Media{\n\t\t\t\tType:    description.MediaTypeAudio,\n\t\t\t\tFormats: []format.Format{forma},\n\t\t\t}\n\t\t\tmedias = append(medias, medi)\n\n\t\t\tr.OnDataMPEG1Audio(track, func(pts time.Duration, frame []byte) {\n\t\t\t\t(*subStream).WriteUnit(medi, forma, &unit.Unit{\n\t\t\t\t\tPTS:     durationToTimestamp(pts, forma.ClockRate()),\n\t\t\t\t\tPayload: unit.PayloadMPEG1Audio{frame},\n\t\t\t\t})\n\t\t\t})\n\n\t\tcase *codecs.AC3:\n\t\t\tforma := &format.AC3{\n\t\t\t\tPayloadTyp:   96,\n\t\t\t\tSampleRate:   codec.SampleRate,\n\t\t\t\tChannelCount: codec.ChannelCount,\n\t\t\t}\n\t\t\tmedi := &description.Media{\n\t\t\t\tType:    description.MediaTypeAudio,\n\t\t\t\tFormats: []format.Format{forma},\n\t\t\t}\n\t\t\tmedias = append(medias, medi)\n\n\t\t\tr.OnDataAC3(track, func(pts time.Duration, frame []byte) {\n\t\t\t\t(*subStream).WriteUnit(medi, forma, &unit.Unit{\n\t\t\t\t\tPTS:     durationToTimestamp(pts, forma.ClockRate()),\n\t\t\t\t\tPayload: unit.PayloadAC3{frame},\n\t\t\t\t})\n\t\t\t})\n\n\t\tcase *codecs.G711:\n\t\t\tforma := &format.G711{\n\t\t\t\tPayloadTyp: func() uint8 {\n\t\t\t\t\tswitch {\n\t\t\t\t\tcase codec.ChannelCount == 1 && codec.MULaw:\n\t\t\t\t\t\treturn 0\n\t\t\t\t\tcase codec.ChannelCount == 1 && !codec.MULaw:\n\t\t\t\t\t\treturn 8\n\t\t\t\t\tdefault:\n\t\t\t\t\t\treturn 96\n\t\t\t\t\t}\n\t\t\t\t}(),\n\t\t\t\tMULaw:        codec.MULaw,\n\t\t\t\tSampleRate:   8000,\n\t\t\t\tChannelCount: codec.ChannelCount,\n\t\t\t}\n\t\t\tmedi := &description.Media{\n\t\t\t\tType:    description.MediaTypeAudio,\n\t\t\t\tFormats: []format.Format{forma},\n\t\t\t}\n\t\t\tmedias = append(medias, medi)\n\n\t\t\tr.OnDataG711(track, func(pts time.Duration, samples []byte) {\n\t\t\t\t(*subStream).WriteUnit(medi, forma, &unit.Unit{\n\t\t\t\t\tPTS:     durationToTimestamp(pts, forma.ClockRate()),\n\t\t\t\t\tPayload: unit.PayloadG711(samples),\n\t\t\t\t})\n\t\t\t})\n\n\t\tcase *codecs.LPCM:\n\t\t\tforma := &format.LPCM{\n\t\t\t\tPayloadTyp:   96,\n\t\t\t\tBitDepth:     codec.BitDepth,\n\t\t\t\tSampleRate:   codec.SampleRate,\n\t\t\t\tChannelCount: codec.ChannelCount,\n\t\t\t}\n\t\t\tmedi := &description.Media{\n\t\t\t\tType:    description.MediaTypeAudio,\n\t\t\t\tFormats: []format.Format{forma},\n\t\t\t}\n\t\t\tmedias = append(medias, medi)\n\n\t\t\tr.OnDataLPCM(track, func(pts time.Duration, samples []byte) {\n\t\t\t\t(*subStream).WriteUnit(medi, forma, &unit.Unit{\n\t\t\t\t\tPTS:     durationToTimestamp(pts, forma.ClockRate()),\n\t\t\t\t\tPayload: unit.PayloadLPCM(samples),\n\t\t\t\t})\n\t\t\t})\n\n\t\tdefault:\n\t\t\tpanic(\"should not happen\")\n\t\t}\n\t}\n\n\tif len(medias) == 0 {\n\t\treturn nil, errNoSupportedCodecsTo\n\t}\n\n\treturn medias, nil\n}\n"
  },
  {
    "path": "internal/protocols/rtmp/to_stream_test.go",
    "content": "package rtmp\n\nimport (\n\t\"testing\"\n\n\t\"github.com/bluenviron/gortmplib\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestToStreamNoSupportedCodecs(t *testing.T) {\n\tr := &gortmplib.Reader{}\n\n\t_, err := ToStream(r, nil)\n\trequire.Equal(t, errNoSupportedCodecsTo, err)\n}\n\n// this is impossible to test since currently we support all RTMP tracks.\n// func TestToStreamSkipUnsupportedTracks(t *testing.T)\n"
  },
  {
    "path": "internal/protocols/rtsp/credentials.go",
    "content": "package rtsp\n\nimport (\n\t\"github.com/bluenviron/gortsplib/v5/pkg/base\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/headers\"\n\t\"github.com/bluenviron/mediamtx/internal/auth\"\n)\n\n// Credentials extracts credentials from a RTSP request.\nfunc Credentials(rt *base.Request) *auth.Credentials {\n\tc := &auth.Credentials{}\n\n\tvar rtspAuthHeader headers.Authorization\n\terr := rtspAuthHeader.Unmarshal(rt.Header[\"Authorization\"])\n\tif err == nil {\n\t\tc.User = rtspAuthHeader.Username\n\t\tif rtspAuthHeader.Method == headers.AuthMethodBasic {\n\t\t\tc.Pass = rtspAuthHeader.BasicPass\n\t\t}\n\t}\n\n\treturn c\n}\n"
  },
  {
    "path": "internal/protocols/rtsp/credentials_test.go",
    "content": "package rtsp\n\nimport (\n\t\"testing\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/base\"\n\t\"github.com/bluenviron/mediamtx/internal/auth\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCredentials(t *testing.T) {\n\trr := &base.Request{\n\t\tHeader: base.Header{\n\t\t\t\"Authorization\": []string{\n\t\t\t\t\"Basic bXl1c2VyOm15cGFzcw==\",\n\t\t\t},\n\t\t},\n\t}\n\n\tc := Credentials(rr)\n\n\trequire.Equal(t, &auth.Credentials{\n\t\tUser: \"myuser\",\n\t\tPass: \"mypass\",\n\t}, c)\n}\n"
  },
  {
    "path": "internal/protocols/rtsp/to_stream.go",
    "content": "// Package rtsp provides RTSP utilities.\npackage rtsp\n\nimport (\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n\t\"github.com/pion/rtp\"\n)\n\ntype ntpState int\n\nconst (\n\tntpStateInitial ntpState = iota\n\tntpStateReplace\n\tntpStateAvailable\n)\n\ntype rtspSource interface {\n\tPacketPTS(*description.Media, *rtp.Packet) (int64, bool)\n\tPacketNTP(*description.Media, *rtp.Packet) (time.Time, bool)\n\tOnPacketRTP(*description.Media, format.Format, gortsplib.OnPacketRTPFunc)\n}\n\n// ToStream maps a RTSP stream to a MediaMTX stream.\nfunc ToStream(\n\tsource rtspSource,\n\tmedias []*description.Media,\n\tpathConf *conf.Path,\n\tsubStream **stream.SubStream,\n\tlog logger.Writer,\n) {\n\tfor _, medi := range medias {\n\t\tfor _, forma := range medi.Formats {\n\t\t\tcmedi := medi\n\t\t\tcforma := forma\n\n\t\t\tvar ntpStat ntpState\n\n\t\t\tif !pathConf.UseAbsoluteTimestamp {\n\t\t\t\tntpStat = ntpStateReplace\n\t\t\t}\n\n\t\t\thandleNTP := func(pkt *rtp.Packet) (time.Time, bool) {\n\t\t\t\tswitch ntpStat {\n\t\t\t\tcase ntpStateReplace:\n\t\t\t\t\treturn time.Time{}, true\n\n\t\t\t\tcase ntpStateInitial:\n\t\t\t\t\tntp, avail := source.PacketNTP(cmedi, pkt)\n\t\t\t\t\tif !avail {\n\t\t\t\t\t\tlog.Log(logger.Warn, \"received RTP packet without absolute time, skipping it\")\n\t\t\t\t\t\treturn time.Time{}, false\n\t\t\t\t\t}\n\n\t\t\t\t\tntpStat = ntpStateAvailable\n\t\t\t\t\treturn ntp, true\n\n\t\t\t\tdefault: // ntpStateAvailable\n\t\t\t\t\tntp, avail := source.PacketNTP(cmedi, pkt)\n\t\t\t\t\tif !avail {\n\t\t\t\t\t\tpanic(\"should not happen\")\n\t\t\t\t\t}\n\n\t\t\t\t\treturn ntp, true\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tsource.OnPacketRTP(cmedi, cforma, func(pkt *rtp.Packet) {\n\t\t\t\tpts, ok := source.PacketPTS(cmedi, pkt)\n\t\t\t\tif !ok {\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tntp, ok := handleNTP(pkt)\n\t\t\t\tif !ok {\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\t(*subStream).WriteUnit(cmedi, cforma, &unit.Unit{\n\t\t\t\t\tPTS:        pts,\n\t\t\t\t\tNTP:        ntp,\n\t\t\t\t\tRTPPackets: []*rtp.Packet{pkt},\n\t\t\t\t})\n\t\t\t})\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/protocols/tls/make_config.go",
    "content": "// Package tls contains TLS utilities.\npackage tls //nolint:revive\n\nimport (\n\t\"crypto/sha256\"\n\t\"crypto/tls\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"strings\"\n)\n\n// MakeConfig returns a tls.Config with:\n// - server name indicator (SNI) support\n// - fingerprint support\nfunc MakeConfig(serverName string, fingerprint string) *tls.Config {\n\tconf := &tls.Config{\n\t\tServerName: serverName,\n\t}\n\n\tif fingerprint != \"\" {\n\t\tfingerprintLower := strings.ToLower(fingerprint)\n\t\tconf.InsecureSkipVerify = true\n\t\tconf.VerifyConnection = func(cs tls.ConnectionState) error {\n\t\t\th := sha256.New()\n\t\t\th.Write(cs.PeerCertificates[0].Raw)\n\t\t\thstr := hex.EncodeToString(h.Sum(nil))\n\n\t\t\tif hstr != fingerprintLower {\n\t\t\t\treturn fmt.Errorf(\"source fingerprint does not match: expected %s, got %s\",\n\t\t\t\t\tfingerprintLower, hstr)\n\t\t\t}\n\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn conf\n}\n"
  },
  {
    "path": "internal/protocols/tls/make_config_test.go",
    "content": "package tls //nolint:revive\n\nimport (\n\t\"crypto/tls\"\n\t\"net\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar testTLSCertPub = []byte(`-----BEGIN CERTIFICATE-----\nMIIDazCCAlOgAwIBAgIUXw1hEC3LFpTsllv7D3ARJyEq7sIwDQYJKoZIhvcNAQEL\nBQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM\nGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDEyMTMxNzQ0NThaFw0zMDEy\nMTExNzQ0NThaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw\nHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB\nAQUAA4IBDwAwggEKAoIBAQDG8DyyS51810GsGwgWr5rjJK7OE1kTTLSNEEKax8Bj\nzOyiaz8rA2JGl2VUEpi2UjDr9Cm7nd+YIEVs91IIBOb7LGqObBh1kGF3u5aZxLkv\nNJE+HrLVvUhaDobK2NU+Wibqc/EI3DfUkt1rSINvv9flwTFu1qHeuLWhoySzDKEp\nOzYxpFhwjVSokZIjT4Red3OtFz7gl2E6OAWe2qoh5CwLYVdMWtKR0Xuw3BkDPk9I\nqkQKx3fqv97LPEzhyZYjDT5WvGrgZ1WDAN3booxXF3oA1H3GHQc4m/vcLatOtb8e\nnI59gMQLEbnp08cl873bAuNuM95EZieXTHNbwUnq5iybAgMBAAGjUzBRMB0GA1Ud\nDgQWBBQBKhJh8eWu0a4au9X/2fKhkFX2vjAfBgNVHSMEGDAWgBQBKhJh8eWu0a4a\nu9X/2fKhkFX2vjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBj\n3aCW0YPKukYgVK9cwN0IbVy/D0C1UPT4nupJcy/E0iC7MXPZ9D/SZxYQoAkdptdO\nxfI+RXkpQZLdODNx9uvV+cHyZHZyjtE5ENu/i5Rer2cWI/mSLZm5lUQyx+0KZ2Yu\ntEI1bsebDK30msa8QSTn0WidW9XhFnl3gRi4wRdimcQapOWYVs7ih+nAlSvng7NI\nXpAyRs8PIEbpDDBMWnldrX4TP6EWYUi49gCp8OUDRREKX3l6Ls1vZ02F34yHIt/7\n7IV/XSKG096bhW+icKBWV0IpcEsgTzPK1J1hMxgjhzIMxGboAeUU+kidthOob6Sd\nXQxaORfgM//NzX9LhUPk\n-----END CERTIFICATE-----\n`)\n\nvar testTLSCertKey = []byte(`-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAxvA8skudfNdBrBsIFq+a4ySuzhNZE0y0jRBCmsfAY8zsoms/\nKwNiRpdlVBKYtlIw6/Qpu53fmCBFbPdSCATm+yxqjmwYdZBhd7uWmcS5LzSRPh6y\n1b1IWg6GytjVPlom6nPxCNw31JLda0iDb7/X5cExbtah3ri1oaMkswyhKTs2MaRY\ncI1UqJGSI0+EXndzrRc+4JdhOjgFntqqIeQsC2FXTFrSkdF7sNwZAz5PSKpECsd3\n6r/eyzxM4cmWIw0+Vrxq4GdVgwDd26KMVxd6ANR9xh0HOJv73C2rTrW/HpyOfYDE\nCxG56dPHJfO92wLjbjPeRGYnl0xzW8FJ6uYsmwIDAQABAoIBACi0BKcyQ3HElSJC\nkaAao+Uvnzh4yvPg8Nwf5JDIp/uDdTMyIEWLtrLczRWrjGVZYbsVROinP5VfnPTT\nkYwkfKINj2u+gC6lsNuPnRuvHXikF8eO/mYvCTur1zZvsQnF5kp4GGwIqr+qoPUP\nbB0UMndG1PdpoMryHe+JcrvTrLHDmCeH10TqOwMsQMLHYLkowvxwJWsmTY7/Qr5S\nWm3PPpOcW2i0uyPVuyuv4yD1368fqnqJ8QFsQp1K6QtYsNnJ71Hut1/IoxK/e6hj\n5Z+byKtHVtmcLnABuoOT7BhleJNFBksX9sh83jid4tMBgci+zXNeGmgqo2EmaWAb\nagQslkECgYEA8B1rzjOHVQx/vwSzDa4XOrpoHQRfyElrGNz9JVBvnoC7AorezBXQ\nM9WTHQIFTGMjzD8pb+YJGi3gj93VN51r0SmJRxBaBRh1ZZI9kFiFzngYev8POgD3\nygmlS3kTHCNxCK/CJkB+/jMBgtPj5ygDpCWVcTSuWlQFphePkW7jaaECgYEA1Blz\nulqgAyJHZaqgcbcCsI2q6m527hVr9pjzNjIVmkwu38yS9RTCgdlbEVVDnS0hoifl\n+jVMEGXjF3xjyMvL50BKbQUH+KAa+V4n1WGlnZOxX9TMny8MBjEuSX2+362vQ3BX\n4vOlX00gvoc+sY+lrzvfx/OdPCHQGVYzoKCxhLsCgYA07HcviuIAV/HsO2/vyvhp\nxF5gTu+BqNUHNOZDDDid+ge+Jre2yfQLCL8VPLXIQW3Jff53IH/PGl+NtjphuLvj\n7UDJvgvpZZuymIojP6+2c3gJ3CASC9aR3JBnUzdoE1O9s2eaoMqc4scpe+SWtZYf\n3vzSZ+cqF6zrD/Rf/M35IQKBgHTU4E6ShPm09CcoaeC5sp2WK8OevZw/6IyZi78a\nr5Oiy18zzO97U/k6xVMy6F+38ILl/2Rn31JZDVJujniY6eSkIVsUHmPxrWoXV1HO\ny++U32uuSFiXDcSLarfIsE992MEJLSAynbF1Rsgsr3gXbGiuToJRyxbIeVy7gwzD\n94TpAoGAY4/PejWQj9psZfAhyk5dRGra++gYRQ/gK1IIc1g+Dd2/BxbT/RHr05GK\n6vwrfjsoRyMWteC1SsNs/CurjfQ/jqCfHNP5XPvxgd5Ec8sRJIiV7V5RTuWJsPu1\n+3K6cnKEyg+0ekYmLertRFIY6SwWmY1fyKgTvxudMcsBY7dC4xs=\n-----END RSA PRIVATE KEY-----\n`)\n\nfunc TestMakeConfigSNI(t *testing.T) {\n\tl, err := net.Listen(\"tcp\", \"localhost:8556\")\n\trequire.NoError(t, err)\n\tdefer l.Close()\n\n\tserverDone := make(chan struct{})\n\tdefer func() { <-serverDone }()\n\n\tgo func() {\n\t\tdefer close(serverDone)\n\n\t\tnconn, err2 := l.Accept()\n\t\trequire.NoError(t, err2)\n\t\tdefer nconn.Close()\n\n\t\tcert, err2 := tls.X509KeyPair(testTLSCertPub, testTLSCertKey)\n\t\trequire.NoError(t, err2)\n\n\t\ttnconn := tls.Server(nconn, &tls.Config{\n\t\t\tCertificates:       []tls.Certificate{cert},\n\t\t\tInsecureSkipVerify: true,\n\t\t\tVerifyConnection: func(cs tls.ConnectionState) error {\n\t\t\t\trequire.Equal(t, \"myhost\", cs.ServerName)\n\t\t\t\treturn nil\n\t\t\t},\n\t\t})\n\n\t\terr2 = tnconn.Handshake()\n\t\trequire.EqualError(t, err2, \"remote error: tls: bad certificate\")\n\t}()\n\n\tconf := MakeConfig(\"myhost\", \"\")\n\n\t_, err = tls.Dial(\"tcp\", \"localhost:8556\", conf)\n\trequire.EqualError(t, err, \"tls: failed to verify certificate: x509: \"+\n\t\t\"certificate is not valid for any names, but wanted to match myhost\")\n}\n\nfunc TestMakeConfigFingerprint(t *testing.T) {\n\tl, err := net.Listen(\"tcp\", \"localhost:8556\")\n\trequire.NoError(t, err)\n\tdefer l.Close()\n\n\tserverDone := make(chan struct{})\n\tdefer func() { <-serverDone }()\n\n\tgo func() {\n\t\tdefer close(serverDone)\n\n\t\tnconn, err2 := l.Accept()\n\t\trequire.NoError(t, err2)\n\t\tdefer nconn.Close()\n\n\t\tcert, err2 := tls.X509KeyPair(testTLSCertPub, testTLSCertKey)\n\t\trequire.NoError(t, err2)\n\n\t\ttnconn := tls.Server(nconn, &tls.Config{\n\t\t\tCertificates:       []tls.Certificate{cert},\n\t\t\tInsecureSkipVerify: true,\n\t\t\tVerifyConnection: func(cs tls.ConnectionState) error {\n\t\t\t\trequire.Equal(t, \"myhost\", cs.ServerName)\n\t\t\t\treturn nil\n\t\t\t},\n\t\t})\n\n\t\terr2 = tnconn.Handshake()\n\t\trequire.NoError(t, err2)\n\t}()\n\n\tconf := MakeConfig(\"myhost\", \"33949e05fffb5ff3e8aa16f8213a6251b4d9363804ba53233c4da9a46d6f2739\")\n\n\tconn, err := tls.Dial(\"tcp\", \"localhost:8556\", conf)\n\trequire.NoError(t, err)\n\tdefer conn.Close() //nolint:errcheck\n}\n"
  },
  {
    "path": "internal/protocols/udp/listener.go",
    "content": "// Package udp contains utilities to work with the UDP protocol.\npackage udp\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/multicast\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/readbuffer\"\n\t\"github.com/bluenviron/mediamtx/internal/restrictnetwork\"\n)\n\ntype packetConn interface {\n\tnet.PacketConn\n\tSetReadBuffer(bytes int) error\n\tSyscallConn() (syscall.RawConn, error)\n}\n\nfunc defaultInterfaceForMulticast(multicastAddr *net.UDPAddr) (*net.Interface, error) {\n\tconn, err := net.Dial(\"udp4\", multicastAddr.String())\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tlocalAddr := conn.LocalAddr().(*net.UDPAddr)\n\tconn.Close()\n\n\tinterfaces, err := net.Interfaces()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor _, iface := range interfaces {\n\t\tvar addrs []net.Addr\n\t\taddrs, err = iface.Addrs()\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, addr := range addrs {\n\t\t\tvar ip net.IP\n\t\t\tswitch v := addr.(type) {\n\t\t\tcase *net.IPNet:\n\t\t\t\tip = v.IP\n\t\t\tcase *net.IPAddr:\n\t\t\t\tip = v.IP\n\t\t\t}\n\n\t\t\tif ip != nil && ip.Equal(localAddr.IP) {\n\t\t\t\treturn &iface, nil\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil, fmt.Errorf(\"could not find any interface for using multicast address %s\", multicastAddr)\n}\n\n// Listener is a listener on a UDP socket.\ntype Listener struct {\n\tAddress           string\n\tSource            string\n\tIntfName          string\n\tUDPReadBufferSize int\n\tListenPacket      func(network, address string) (net.PacketConn, error)\n\n\tpc       packetConn\n\tsourceIP net.IP\n}\n\n// Initialize initializes the listener.\nfunc (l *Listener) Initialize() error {\n\tif l.ListenPacket == nil {\n\t\tl.ListenPacket = net.ListenPacket\n\t}\n\n\tif l.Source != \"\" {\n\t\tl.sourceIP = net.ParseIP(l.Source)\n\t\tif l.sourceIP == nil {\n\t\t\treturn fmt.Errorf(\"invalid source IP\")\n\t\t}\n\t}\n\n\taddr, err := net.ResolveUDPAddr(\"udp\", l.Address)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif ip4 := addr.IP.To4(); ip4 != nil && addr.IP.IsMulticast() {\n\t\tvar intf *net.Interface\n\n\t\tif l.IntfName != \"\" {\n\t\t\tintf, err = net.InterfaceByName(l.IntfName)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\tintf, err = defaultInterfaceForMulticast(addr)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tl.pc, err = multicast.NewSingleConn(intf, addr.String(), l.ListenPacket)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\tvar tmp net.PacketConn\n\t\ttmp, err = l.ListenPacket(restrictnetwork.Restrict(\"udp\", addr.String()))\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tl.pc = tmp.(packetConn)\n\t}\n\n\tif l.UDPReadBufferSize != 0 {\n\t\terr = readbuffer.SetReadBuffer(l.pc, l.UDPReadBufferSize)\n\t\tif err != nil {\n\t\t\tl.pc.Close()\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Close closes the listener.\nfunc (l *Listener) Close() error {\n\treturn l.pc.Close()\n}\n\n// Read implements net.Conn.\nfunc (l *Listener) Read(p []byte) (int, error) {\n\tfor {\n\t\tn, addr, err := l.pc.ReadFrom(p)\n\n\t\tif l.sourceIP != nil && addr != nil && !addr.(*net.UDPAddr).IP.Equal(l.sourceIP) {\n\t\t\tcontinue\n\t\t}\n\n\t\treturn n, err\n\t}\n}\n\n// Write implements net.Conn.\nfunc (l *Listener) Write(_ []byte) (int, error) {\n\tpanic(\"unimplemented\")\n}\n\n// LocalAddr implements net.Conn.\nfunc (l *Listener) LocalAddr() net.Addr {\n\tpanic(\"unimplemented\")\n}\n\n// RemoteAddr implements net.Conn.\nfunc (l *Listener) RemoteAddr() net.Addr {\n\tpanic(\"unimplemented\")\n}\n\n// SetDeadline implements net.Conn.\nfunc (l *Listener) SetDeadline(_ time.Time) error {\n\tpanic(\"unimplemented\")\n}\n\n// SetReadDeadline implements net.Conn.\nfunc (l *Listener) SetReadDeadline(t time.Time) error {\n\treturn l.pc.SetReadDeadline(t)\n}\n\n// SetWriteDeadline implements net.Conn.\nfunc (l *Listener) SetWriteDeadline(_ time.Time) error {\n\tpanic(\"unimplemented\")\n}\n"
  },
  {
    "path": "internal/protocols/udp/listener_test.go",
    "content": "package udp\n\nimport (\n\t\"net\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestListen(t *testing.T) {\n\tl := &Listener{\n\t\tAddress:           \"127.0.0.1:0\",\n\t\tUDPReadBufferSize: 4096,\n\t}\n\terr := l.Initialize()\n\trequire.NoError(t, err)\n\tdefer l.Close() //nolint:errcheck\n\n\tdone := make(chan struct{})\n\n\tgo func() {\n\t\tdefer close(done)\n\n\t\terr2 := l.SetReadDeadline(time.Now().Add(2 * time.Second))\n\t\trequire.NoError(t, err2)\n\n\t\tbuf := make([]byte, 1024)\n\t\tn, err2 := l.Read(buf)\n\t\trequire.NoError(t, err2)\n\n\t\trequire.Equal(t, []byte(\"testing\"), buf[:n])\n\t}()\n\n\tlocalAddr := l.pc.LocalAddr().(*net.UDPAddr)\n\n\tclientConn, err := net.DialUDP(\"udp\", nil, localAddr)\n\trequire.NoError(t, err)\n\tdefer clientConn.Close() //nolint:errcheck\n\n\t_, err = clientConn.Write([]byte(\"testing\"))\n\trequire.NoError(t, err)\n\n\t<-done\n}\n"
  },
  {
    "path": "internal/protocols/udp/params.go",
    "content": "package udp\n\nimport \"net/url\"\n\n// Params are the parameters of a UDP listener.\ntype Params struct {\n\tAddress  string\n\tSource   string\n\tIntfName string\n}\n\n// URLToParams converts a URL to Params.\nfunc URLToParams(u *url.URL) *Params {\n\treturn &Params{\n\t\tAddress:  u.Host,\n\t\tSource:   u.Query().Get(\"source\"),\n\t\tIntfName: u.Query().Get(\"interface\"),\n\t}\n}\n"
  },
  {
    "path": "internal/protocols/unix/listener.go",
    "content": "// Package unix contains utilities to work with Unix sockets.\npackage unix\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"os\"\n\t\"sync\"\n\t\"time\"\n)\n\n// Listener is a listener on a Unix socket.\ntype Listener struct {\n\tPath string\n\n\tl        net.Listener\n\tc        net.Conn\n\tmutex    sync.Mutex\n\tclosed   bool\n\tdeadline time.Time\n}\n\n// Initialize initializes the listener.\nfunc (l *Listener) Initialize() error {\n\tif l.Path == \"\" {\n\t\treturn fmt.Errorf(\"invalid unix path\")\n\t}\n\n\tos.Remove(l.Path)\n\n\tvar err error\n\tl.l, err = net.Listen(\"unix\", l.Path)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// Close closes the listener.\nfunc (l *Listener) Close() error {\n\tl.mutex.Lock()\n\tdefer l.mutex.Unlock()\n\n\tl.closed = true\n\n\tl.l.Close()\n\n\tif l.c != nil {\n\t\tl.c.Close()\n\t}\n\n\treturn nil\n}\n\nfunc (l *Listener) acceptWithDeadline() (net.Conn, error) {\n\tdone := make(chan struct{})\n\tdefer func() { <-done }()\n\n\tterminate := make(chan struct{})\n\tdefer close(terminate)\n\n\tgo func() {\n\t\tdefer close(done)\n\t\tselect {\n\t\tcase <-time.After(time.Until(l.deadline)):\n\t\t\tl.l.Close()\n\t\tcase <-terminate:\n\t\t\treturn\n\t\t}\n\t}()\n\n\tc, err := l.l.Accept()\n\tif err != nil {\n\t\tif time.Now().After(l.deadline) {\n\t\t\treturn nil, fmt.Errorf(\"deadline exceeded\")\n\t\t}\n\t\treturn nil, err\n\t}\n\treturn c, nil\n}\n\nfunc (l *Listener) setConn(c net.Conn) error {\n\tl.mutex.Lock()\n\tdefer l.mutex.Unlock()\n\n\tif l.closed {\n\t\treturn fmt.Errorf(\"closed\")\n\t}\n\n\tl.c = c\n\treturn nil\n}\n\nfunc (l *Listener) Read(p []byte) (int, error) {\n\tif l.c == nil {\n\t\tc, err := l.acceptWithDeadline()\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\n\t\terr = l.setConn(c)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t}\n\n\tl.c.SetReadDeadline(l.deadline)\n\treturn l.c.Read(p)\n}\n\n// Write implements net.Conn.\nfunc (l *Listener) Write(_ []byte) (int, error) {\n\tpanic(\"unimplemented\")\n}\n\n// LocalAddr implements net.Conn.\nfunc (l *Listener) LocalAddr() net.Addr {\n\tpanic(\"unimplemented\")\n}\n\n// RemoteAddr implements net.Conn.\nfunc (l *Listener) RemoteAddr() net.Addr {\n\tpanic(\"unimplemented\")\n}\n\n// SetDeadline implements net.Conn.\nfunc (l *Listener) SetDeadline(_ time.Time) error {\n\tpanic(\"unimplemented\")\n}\n\n// SetReadDeadline implements net.Conn.\nfunc (l *Listener) SetReadDeadline(t time.Time) error {\n\tl.deadline = t\n\treturn nil\n}\n\n// SetWriteDeadline implements net.Conn.\nfunc (l *Listener) SetWriteDeadline(_ time.Time) error {\n\tpanic(\"unimplemented\")\n}\n"
  },
  {
    "path": "internal/protocols/unix/listener_test.go",
    "content": "package unix\n\nimport (\n\t\"net\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestListen(t *testing.T) {\n\tsocket, err := os.CreateTemp(os.TempDir(), \"mtx-unix-\")\n\trequire.NoError(t, err)\n\tsocket.Close()\n\tdefer os.Remove(socket.Name())\n\n\tl := &Listener{\n\t\tPath: socket.Name(),\n\t}\n\terr = l.Initialize()\n\trequire.NoError(t, err)\n\tdefer l.Close() //nolint:errcheck\n\n\tdone := make(chan struct{})\n\n\tgo func() {\n\t\tdefer close(done)\n\n\t\tbuf := make([]byte, 1024)\n\t\tl.SetReadDeadline(time.Now().Add(2 * time.Second)) //nolint:errcheck\n\t\tn, err2 := l.Read(buf)\n\t\trequire.NoError(t, err2)\n\t\trequire.Equal(t, []byte(\"testing\"), buf[:n])\n\t}()\n\n\tclientAddr, err := net.ResolveUnixAddr(\"unix\", socket.Name())\n\trequire.NoError(t, err)\n\n\tclientConn, err := net.DialUnix(\"unix\", nil, clientAddr)\n\trequire.NoError(t, err)\n\tdefer clientConn.Close() //nolint:errcheck\n\n\t_, err = clientConn.Write([]byte(\"testing\"))\n\trequire.NoError(t, err)\n\n\t<-done\n}\n"
  },
  {
    "path": "internal/protocols/unix/params.go",
    "content": "package unix\n\nimport \"net/url\"\n\n// Params are the parameters of a unix listener.\ntype Params struct {\n\tPath string\n}\n\n// URLToParams converts a URL to Params.\nfunc URLToParams(u *url.URL) *Params {\n\tvar pa string\n\tif u.Path != \"\" {\n\t\tpa = u.Path\n\t} else {\n\t\tpa = u.Host\n\t}\n\n\treturn &Params{\n\t\tPath: pa,\n\t}\n}\n"
  },
  {
    "path": "internal/protocols/webrtc/from_stream.go",
    "content": "package webrtc\n\nimport (\n\t\"crypto/rand\"\n\t\"errors\"\n\t\"fmt\"\n\t\"slices\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtpav1\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtph264\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtph265\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtplpcm\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtpvp8\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtpvp9\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/g711\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/opus\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n\t\"github.com/pion/rtp\"\n\t\"github.com/pion/webrtc/v4\"\n)\n\nconst (\n\twebrtcPayloadMaxSize = 1188 // 1200 - 12 (RTP header)\n)\n\nvar multichannelOpusSDP = map[int]string{\n\t3: \"channel_mapping=0,2,1;num_streams=2;coupled_streams=1\",\n\t4: \"channel_mapping=0,1,2,3;num_streams=2;coupled_streams=2\",\n\t5: \"channel_mapping=0,4,1,2,3;num_streams=3;coupled_streams=2\",\n\t6: \"channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2\",\n\t7: \"channel_mapping=0,4,1,2,3,5,6;num_streams=4;coupled_streams=4\",\n\t8: \"channel_mapping=0,6,1,4,5,2,3,7;num_streams=5;coupled_streams=4\",\n}\n\nvar errNoSupportedCodecsFrom = errors.New(\n\t\"the stream doesn't contain any supported codec, which are currently \" +\n\t\t\"AV1, VP9, VP8, H265, H264, Opus, G722, G711, LPCM\")\n\nfunc ptrOf[T any](v T) *T {\n\tp := new(T)\n\t*p = v\n\treturn p\n}\n\nfunc randUint32() (uint32, error) {\n\tvar b [4]byte\n\t_, err := rand.Read(b[:])\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3]), nil\n}\n\nfunc multiplyAndDivide2(v, m, d time.Duration) time.Duration {\n\tsecs := v / d\n\tdec := v % d\n\treturn (secs*m + dec*m/d)\n}\n\nfunc timestampToDuration(t int64, clockRate int) time.Duration {\n\treturn multiplyAndDivide2(time.Duration(t), time.Second, time.Duration(clockRate))\n}\n\nfunc setupVideoTrack(\n\tdesc *description.Session,\n\tr *stream.Reader,\n) (*OutgoingTrack, error) {\n\tvar av1Format *format.AV1\n\tmedia := desc.FindFormat(&av1Format)\n\n\tif av1Format != nil { //nolint:dupl\n\t\ttrack := &OutgoingTrack{\n\t\t\tCaps: webrtc.RTPCodecCapability{\n\t\t\t\tMimeType:  webrtc.MimeTypeAV1,\n\t\t\t\tClockRate: 90000,\n\t\t\t},\n\t\t}\n\n\t\tencoder := &rtpav1.Encoder{\n\t\t\tPayloadType:    105,\n\t\t\tPayloadMaxSize: webrtcPayloadMaxSize,\n\t\t}\n\t\terr := encoder.Init()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tr.OnData(\n\t\t\tmedia,\n\t\t\tav1Format,\n\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\tif u.NilPayload() {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tpackets, err2 := encoder.Encode(u.Payload.(unit.PayloadAV1))\n\t\t\t\tif err2 != nil {\n\t\t\t\t\treturn nil //nolint:nilerr\n\t\t\t\t}\n\n\t\t\t\tfor _, pkt := range packets {\n\t\t\t\t\tntp := u.NTP.Add(timestampToDuration(int64(pkt.Timestamp), 90000))\n\t\t\t\t\tpkt.Timestamp += u.RTPPackets[0].Timestamp\n\t\t\t\t\ttrack.WriteRTPWithNTP(pkt, ntp) //nolint:errcheck\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\treturn track, nil\n\t}\n\n\tvar vp9Format *format.VP9\n\tmedia = desc.FindFormat(&vp9Format)\n\n\tif vp9Format != nil {\n\t\ttrack := &OutgoingTrack{\n\t\t\tCaps: webrtc.RTPCodecCapability{\n\t\t\t\tMimeType:    webrtc.MimeTypeVP9,\n\t\t\t\tClockRate:   90000,\n\t\t\t\tSDPFmtpLine: \"profile-id=0\",\n\t\t\t},\n\t\t}\n\n\t\tencoder := &rtpvp9.Encoder{\n\t\t\tPayloadType:      96,\n\t\t\tPayloadMaxSize:   webrtcPayloadMaxSize,\n\t\t\tInitialPictureID: ptrOf(uint16(8445)),\n\t\t}\n\t\terr := encoder.Init()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tr.OnData(\n\t\t\tmedia,\n\t\t\tvp9Format,\n\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\tif u.NilPayload() {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tpackets, err2 := encoder.Encode(u.Payload.(unit.PayloadVP9))\n\t\t\t\tif err2 != nil {\n\t\t\t\t\treturn nil //nolint:nilerr\n\t\t\t\t}\n\n\t\t\t\tfor _, pkt := range packets {\n\t\t\t\t\tntp := u.NTP.Add(timestampToDuration(int64(pkt.Timestamp), 90000))\n\t\t\t\t\tpkt.Timestamp += u.RTPPackets[0].Timestamp\n\t\t\t\t\ttrack.WriteRTPWithNTP(pkt, ntp) //nolint:errcheck\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\treturn track, nil\n\t}\n\n\tvar vp8Format *format.VP8\n\tmedia = desc.FindFormat(&vp8Format)\n\n\tif vp8Format != nil { //nolint:dupl\n\t\ttrack := &OutgoingTrack{\n\t\t\tCaps: webrtc.RTPCodecCapability{\n\t\t\t\tMimeType:  webrtc.MimeTypeVP8,\n\t\t\t\tClockRate: 90000,\n\t\t\t},\n\t\t}\n\n\t\tencoder := &rtpvp8.Encoder{\n\t\t\tPayloadType:    96,\n\t\t\tPayloadMaxSize: webrtcPayloadMaxSize,\n\t\t}\n\t\terr := encoder.Init()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tr.OnData(\n\t\t\tmedia,\n\t\t\tvp8Format,\n\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\tif u.NilPayload() {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tpackets, err2 := encoder.Encode(u.Payload.(unit.PayloadVP8))\n\t\t\t\tif err2 != nil {\n\t\t\t\t\treturn nil //nolint:nilerr\n\t\t\t\t}\n\n\t\t\t\tfor _, pkt := range packets {\n\t\t\t\t\tntp := u.NTP.Add(timestampToDuration(int64(pkt.Timestamp), 90000))\n\t\t\t\t\tpkt.Timestamp += u.RTPPackets[0].Timestamp\n\t\t\t\t\ttrack.WriteRTPWithNTP(pkt, ntp) //nolint:errcheck\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\treturn track, nil\n\t}\n\n\tvar h265Format *format.H265\n\tmedia = desc.FindFormat(&h265Format)\n\n\tif h265Format != nil { //nolint:dupl\n\t\ttrack := &OutgoingTrack{\n\t\t\tCaps: webrtc.RTPCodecCapability{\n\t\t\t\tMimeType:    webrtc.MimeTypeH265,\n\t\t\t\tClockRate:   90000,\n\t\t\t\tSDPFmtpLine: \"level-id=93;profile-id=1;tier-flag=0;tx-mode=SRST\",\n\t\t\t},\n\t\t}\n\n\t\tencoder := &rtph265.Encoder{\n\t\t\tPayloadType:    96,\n\t\t\tPayloadMaxSize: webrtcPayloadMaxSize,\n\t\t}\n\t\terr := encoder.Init()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tfirstReceived := false\n\t\tvar lastPTS int64\n\n\t\tr.OnData(\n\t\t\tmedia,\n\t\t\th265Format,\n\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\tif u.NilPayload() {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tif !firstReceived {\n\t\t\t\t\tfirstReceived = true\n\t\t\t\t} else if u.PTS < lastPTS {\n\t\t\t\t\treturn fmt.Errorf(\"WebRTC doesn't support H265 streams with B-frames\")\n\t\t\t\t}\n\t\t\t\tlastPTS = u.PTS\n\n\t\t\t\tpackets, err2 := encoder.Encode(u.Payload.(unit.PayloadH265))\n\t\t\t\tif err2 != nil {\n\t\t\t\t\treturn nil //nolint:nilerr\n\t\t\t\t}\n\n\t\t\t\tfor _, pkt := range packets {\n\t\t\t\t\tntp := u.NTP.Add(timestampToDuration(int64(pkt.Timestamp), 90000))\n\t\t\t\t\tpkt.Timestamp += u.RTPPackets[0].Timestamp\n\t\t\t\t\ttrack.WriteRTPWithNTP(pkt, ntp) //nolint:errcheck\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\treturn track, nil\n\t}\n\n\tvar h264Format *format.H264\n\tmedia = desc.FindFormat(&h264Format)\n\n\tif h264Format != nil { //nolint:dupl\n\t\ttrack := &OutgoingTrack{\n\t\t\tCaps: webrtc.RTPCodecCapability{\n\t\t\t\tMimeType:    webrtc.MimeTypeH264,\n\t\t\t\tClockRate:   90000,\n\t\t\t\tSDPFmtpLine: \"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\",\n\t\t\t},\n\t\t}\n\n\t\tencoder := &rtph264.Encoder{\n\t\t\tPayloadType:    96,\n\t\t\tPayloadMaxSize: webrtcPayloadMaxSize,\n\t\t}\n\t\terr := encoder.Init()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tfirstReceived := false\n\t\tvar lastPTS int64\n\n\t\tr.OnData(\n\t\t\tmedia,\n\t\t\th264Format,\n\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\tif u.NilPayload() {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tif !firstReceived {\n\t\t\t\t\tfirstReceived = true\n\t\t\t\t} else if u.PTS < lastPTS {\n\t\t\t\t\treturn fmt.Errorf(\"WebRTC doesn't support H264 streams with B-frames\")\n\t\t\t\t}\n\t\t\t\tlastPTS = u.PTS\n\n\t\t\t\tpackets, err2 := encoder.Encode(u.Payload.(unit.PayloadH264))\n\t\t\t\tif err2 != nil {\n\t\t\t\t\treturn nil //nolint:nilerr\n\t\t\t\t}\n\n\t\t\t\tfor _, pkt := range packets {\n\t\t\t\t\tntp := u.NTP.Add(timestampToDuration(int64(pkt.Timestamp), 90000))\n\t\t\t\t\tpkt.Timestamp += u.RTPPackets[0].Timestamp\n\t\t\t\t\ttrack.WriteRTPWithNTP(pkt, ntp) //nolint:errcheck\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\treturn track, nil\n\t}\n\n\treturn nil, nil\n}\n\nfunc setupAudioTrack(\n\tdesc *description.Session,\n\tr *stream.Reader,\n) (*OutgoingTrack, error) {\n\tvar opusFormat *format.Opus\n\tmedia := desc.FindFormat(&opusFormat)\n\n\tif opusFormat != nil {\n\t\tvar caps webrtc.RTPCodecCapability\n\n\t\tswitch opusFormat.ChannelCount {\n\t\tcase 1, 2:\n\t\t\tcaps = webrtc.RTPCodecCapability{\n\t\t\t\tMimeType:  webrtc.MimeTypeOpus,\n\t\t\t\tClockRate: 48000,\n\t\t\t\tChannels:  2,\n\t\t\t\tSDPFmtpLine: func() string {\n\t\t\t\t\ts := \"minptime=10;useinbandfec=1\"\n\t\t\t\t\tif opusFormat.ChannelCount == 2 {\n\t\t\t\t\t\ts += \";stereo=1;sprop-stereo=1\"\n\t\t\t\t\t}\n\t\t\t\t\treturn s\n\t\t\t\t}(),\n\t\t\t}\n\n\t\tcase 3, 4, 5, 6, 7, 8:\n\t\t\tcaps = webrtc.RTPCodecCapability{\n\t\t\t\tMimeType:    mimeTypeMultiopus,\n\t\t\t\tClockRate:   48000,\n\t\t\t\tChannels:    uint16(opusFormat.ChannelCount),\n\t\t\t\tSDPFmtpLine: multichannelOpusSDP[opusFormat.ChannelCount],\n\t\t\t}\n\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"unsupported channel count: %d\", opusFormat.ChannelCount)\n\t\t}\n\n\t\ttrack := &OutgoingTrack{\n\t\t\tCaps: caps,\n\t\t}\n\n\t\tcurTimestamp, err := randUint32()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tr.OnData(\n\t\t\tmedia,\n\t\t\topusFormat,\n\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\tfor _, orig := range u.RTPPackets {\n\t\t\t\t\tpkt := &rtp.Packet{\n\t\t\t\t\t\tHeader:  orig.Header,\n\t\t\t\t\t\tPayload: orig.Payload,\n\t\t\t\t\t}\n\n\t\t\t\t\t// recompute timestamp from scratch.\n\t\t\t\t\t// Chrome requires a precise timestamp that FFmpeg doesn't provide.\n\t\t\t\t\tpkt.Timestamp = curTimestamp\n\t\t\t\t\tcurTimestamp += uint32(opus.PacketDuration2(pkt.Payload))\n\n\t\t\t\t\tntp := u.NTP.Add(timestampToDuration(int64(pkt.Timestamp-u.RTPPackets[0].Timestamp), 48000))\n\t\t\t\t\ttrack.WriteRTPWithNTP(pkt, ntp) //nolint:errcheck\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\treturn track, nil\n\t}\n\n\tvar g722Format *format.G722\n\tmedia = desc.FindFormat(&g722Format)\n\n\tif g722Format != nil {\n\t\ttrack := &OutgoingTrack{\n\t\t\tCaps: webrtc.RTPCodecCapability{\n\t\t\t\tMimeType:  webrtc.MimeTypeG722,\n\t\t\t\tClockRate: 8000,\n\t\t\t},\n\t\t}\n\n\t\tr.OnData(\n\t\t\tmedia,\n\t\t\tg722Format,\n\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\tfor _, pkt := range u.RTPPackets {\n\t\t\t\t\tntp := u.NTP.Add(timestampToDuration(int64(pkt.Timestamp-u.RTPPackets[0].Timestamp), 8000))\n\t\t\t\t\ttrack.WriteRTPWithNTP(pkt, ntp) //nolint:errcheck\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\treturn track, nil\n\t}\n\n\tvar g711Format *format.G711\n\tmedia = desc.FindFormat(&g711Format)\n\n\tif g711Format != nil {\n\t\t// These are the sample rates and channels supported by Chrome.\n\t\t// Different sample rates and channels can be streamed too but we don't want compatibility issues.\n\t\t// https://webrtc.googlesource.com/src/+/refs/heads/main/modules/audio_coding/codecs/pcm16b/audio_decoder_pcm16b.cc#23\n\t\tif g711Format.ClockRate() != 8000 && g711Format.ClockRate() != 16000 &&\n\t\t\tg711Format.ClockRate() != 32000 && g711Format.ClockRate() != 48000 {\n\t\t\treturn nil, fmt.Errorf(\"unsupported clock rate: %d\", g711Format.ClockRate())\n\t\t}\n\t\tif g711Format.ChannelCount != 1 && g711Format.ChannelCount != 2 {\n\t\t\treturn nil, fmt.Errorf(\"unsupported channel count: %d\", g711Format.ChannelCount)\n\t\t}\n\n\t\tvar caps webrtc.RTPCodecCapability\n\n\t\tif g711Format.ClockRate() == 8000 {\n\t\t\tif g711Format.MULaw {\n\t\t\t\tif g711Format.ChannelCount != 1 {\n\t\t\t\t\tcaps = webrtc.RTPCodecCapability{\n\t\t\t\t\t\tMimeType:  webrtc.MimeTypePCMU,\n\t\t\t\t\t\tClockRate: uint32(g711Format.ClockRate()),\n\t\t\t\t\t\tChannels:  uint16(g711Format.ChannelCount),\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tcaps = webrtc.RTPCodecCapability{\n\t\t\t\t\t\tMimeType:  webrtc.MimeTypePCMU,\n\t\t\t\t\t\tClockRate: 8000,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif g711Format.ChannelCount != 1 {\n\t\t\t\t\tcaps = webrtc.RTPCodecCapability{\n\t\t\t\t\t\tMimeType:  webrtc.MimeTypePCMA,\n\t\t\t\t\t\tClockRate: uint32(g711Format.ClockRate()),\n\t\t\t\t\t\tChannels:  uint16(g711Format.ChannelCount),\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tcaps = webrtc.RTPCodecCapability{\n\t\t\t\t\t\tMimeType:  webrtc.MimeTypePCMA,\n\t\t\t\t\t\tClockRate: 8000,\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tcaps = webrtc.RTPCodecCapability{\n\t\t\t\tMimeType:  mimeTypeL16,\n\t\t\t\tClockRate: uint32(g711Format.ClockRate()),\n\t\t\t\tChannels:  uint16(g711Format.ChannelCount),\n\t\t\t}\n\t\t}\n\n\t\ttrack := &OutgoingTrack{\n\t\t\tCaps: caps,\n\t\t}\n\n\t\tif g711Format.ClockRate() == 8000 {\n\t\t\tcurTimestamp, err := randUint32()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tr.OnData(\n\t\t\t\tmedia,\n\t\t\t\tg711Format,\n\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\tfor _, orig := range u.RTPPackets {\n\t\t\t\t\t\tpkt := &rtp.Packet{\n\t\t\t\t\t\t\tHeader:  orig.Header,\n\t\t\t\t\t\t\tPayload: orig.Payload,\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\t// recompute timestamp from scratch.\n\t\t\t\t\t\t// Chrome requires a precise timestamp that FFmpeg doesn't provide.\n\t\t\t\t\t\tpkt.Timestamp = curTimestamp\n\t\t\t\t\t\tcurTimestamp += uint32(len(pkt.Payload)) / uint32(g711Format.ChannelCount)\n\n\t\t\t\t\t\tntp := u.NTP.Add(timestampToDuration(int64(pkt.Timestamp-u.RTPPackets[0].Timestamp), 8000))\n\t\t\t\t\t\ttrack.WriteRTPWithNTP(pkt, ntp) //nolint:errcheck\n\t\t\t\t\t}\n\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\t\t} else {\n\t\t\tencoder := &rtplpcm.Encoder{\n\t\t\t\tPayloadType:    96,\n\t\t\t\tPayloadMaxSize: webrtcPayloadMaxSize,\n\t\t\t\tBitDepth:       16,\n\t\t\t\tChannelCount:   g711Format.ChannelCount,\n\t\t\t}\n\t\t\terr := encoder.Init()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tcurTimestamp, err := randUint32()\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tr.OnData(\n\t\t\t\tmedia,\n\t\t\t\tg711Format,\n\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t}\n\n\t\t\t\t\tvar lpcm []byte\n\t\t\t\t\tif g711Format.MULaw {\n\t\t\t\t\t\tvar mu g711.Mulaw\n\t\t\t\t\t\tmu.Unmarshal(u.Payload.(unit.PayloadG711))\n\t\t\t\t\t\tlpcm = mu\n\t\t\t\t\t} else {\n\t\t\t\t\t\tvar al g711.Alaw\n\t\t\t\t\t\tal.Unmarshal(u.Payload.(unit.PayloadG711))\n\t\t\t\t\t\tlpcm = al\n\t\t\t\t\t}\n\n\t\t\t\t\tpackets, err2 := encoder.Encode(lpcm)\n\t\t\t\t\tif err2 != nil {\n\t\t\t\t\t\treturn nil //nolint:nilerr\n\t\t\t\t\t}\n\n\t\t\t\t\tfor _, pkt := range packets {\n\t\t\t\t\t\t// recompute timestamp from scratch.\n\t\t\t\t\t\t// Chrome requires a precise timestamp that FFmpeg doesn't provide.\n\t\t\t\t\t\tpkt.Timestamp = curTimestamp\n\t\t\t\t\t\tcurTimestamp += uint32(len(pkt.Payload)) / 2 / uint32(g711Format.ChannelCount)\n\n\t\t\t\t\t\tntp := u.NTP.Add(timestampToDuration(int64(pkt.Timestamp-u.RTPPackets[0].Timestamp),\n\t\t\t\t\t\t\tg711Format.ClockRate()))\n\t\t\t\t\t\ttrack.WriteRTPWithNTP(pkt, ntp) //nolint:errcheck\n\t\t\t\t\t}\n\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\t\t}\n\n\t\treturn track, nil\n\t}\n\n\tvar lpcmFormat *format.LPCM\n\tmedia = desc.FindFormat(&lpcmFormat)\n\n\tif lpcmFormat != nil {\n\t\tif lpcmFormat.BitDepth != 16 {\n\t\t\treturn nil, fmt.Errorf(\"unsupported LPCM bit depth: %d\", lpcmFormat.BitDepth)\n\t\t}\n\n\t\t// These are the sample rates and channels supported by Chrome.\n\t\t// Different sample rates and channels can be streamed too but we don't want compatibility issues.\n\t\t// https://webrtc.googlesource.com/src/+/refs/heads/main/modules/audio_coding/codecs/pcm16b/audio_decoder_pcm16b.cc#23\n\t\tif lpcmFormat.ClockRate() != 8000 && lpcmFormat.ClockRate() != 16000 &&\n\t\t\tlpcmFormat.ClockRate() != 32000 && lpcmFormat.ClockRate() != 48000 {\n\t\t\treturn nil, fmt.Errorf(\"unsupported clock rate: %d\", lpcmFormat.ClockRate())\n\t\t}\n\t\tif lpcmFormat.ChannelCount != 1 && lpcmFormat.ChannelCount != 2 {\n\t\t\treturn nil, fmt.Errorf(\"unsupported channel count: %d\", lpcmFormat.ChannelCount)\n\t\t}\n\n\t\ttrack := &OutgoingTrack{\n\t\t\tCaps: webrtc.RTPCodecCapability{\n\t\t\t\tMimeType:  mimeTypeL16,\n\t\t\t\tClockRate: uint32(lpcmFormat.ClockRate()),\n\t\t\t\tChannels:  uint16(lpcmFormat.ChannelCount),\n\t\t\t},\n\t\t}\n\n\t\tencoder := &rtplpcm.Encoder{\n\t\t\tPayloadType:    96,\n\t\t\tBitDepth:       16,\n\t\t\tChannelCount:   lpcmFormat.ChannelCount,\n\t\t\tPayloadMaxSize: webrtcPayloadMaxSize,\n\t\t}\n\t\terr := encoder.Init()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tcurTimestamp, err := randUint32()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tr.OnData(\n\t\t\tmedia,\n\t\t\tlpcmFormat,\n\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\tif u.NilPayload() {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tpackets, err2 := encoder.Encode(u.Payload.(unit.PayloadLPCM))\n\t\t\t\tif err2 != nil {\n\t\t\t\t\treturn nil //nolint:nilerr\n\t\t\t\t}\n\n\t\t\t\tfor _, pkt := range packets {\n\t\t\t\t\t// recompute timestamp from scratch.\n\t\t\t\t\t// Chrome requires a precise timestamp that FFmpeg doesn't provide.\n\t\t\t\t\tpkt.Timestamp = curTimestamp\n\t\t\t\t\tcurTimestamp += uint32(len(pkt.Payload)) / 2 / uint32(lpcmFormat.ChannelCount)\n\n\t\t\t\t\tntp := u.NTP.Add(timestampToDuration(int64(pkt.Timestamp-u.RTPPackets[0].Timestamp),\n\t\t\t\t\t\tlpcmFormat.ClockRate()))\n\t\t\t\t\ttrack.WriteRTPWithNTP(pkt, ntp) //nolint:errcheck\n\t\t\t\t}\n\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\treturn track, nil\n\t}\n\n\treturn nil, nil\n}\n\nfunc setupKLVDataChannel(\n\tdesc *description.Session,\n\tr *stream.Reader,\n) (*OutgoingDataChannel, error) {\n\tvar klvFormat *format.KLV\n\tmedia := desc.FindFormat(&klvFormat)\n\n\tif klvFormat != nil {\n\t\tdataChan := &OutgoingDataChannel{\n\t\t\tLabel: \"KLV\",\n\t\t}\n\n\t\tr.OnData(\n\t\t\tmedia,\n\t\t\tklvFormat,\n\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\tif u.NilPayload() {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\tdataChan.Write(u.Payload.(unit.PayloadKLV))\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\treturn dataChan, nil\n\t}\n\n\treturn nil, nil\n}\n\n// FromStream maps a MediaMTX stream to a WebRTC connection\nfunc FromStream(\n\tdesc *description.Session,\n\tr *stream.Reader,\n\tpc *PeerConnection,\n) error {\n\tvideoTrack, err := setupVideoTrack(desc, r)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif videoTrack != nil {\n\t\tpc.OutgoingTracks = append(pc.OutgoingTracks, videoTrack)\n\t}\n\n\taudioTrack, err := setupAudioTrack(desc, r)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif audioTrack != nil {\n\t\tpc.OutgoingTracks = append(pc.OutgoingTracks, audioTrack)\n\t}\n\n\tklvDataChan, err := setupKLVDataChannel(desc, r)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif klvDataChan != nil {\n\t\tpc.OutgoingDataChannels = append(pc.OutgoingDataChannels, klvDataChan)\n\t}\n\n\tif len(pc.OutgoingTracks) == 0 && len(pc.OutgoingDataChannels) == 0 {\n\t\treturn errNoSupportedCodecsFrom\n\t}\n\n\tsetuppedFormats := r.Formats()\n\n\tn := 1\n\tfor _, media := range desc.Medias {\n\t\tfor _, forma := range media.Formats {\n\t\t\tif !slices.Contains(setuppedFormats, forma) {\n\t\t\t\tr.Parent.Log(logger.Warn, \"skipping track %d (%s)\", n, forma.Codec())\n\t\t\t}\n\t\t\tn++\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/protocols/webrtc/from_stream_test.go",
    "content": "package webrtc\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n\t\"github.com/pion/rtp\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestFromStreamNoSupportedCodecs(t *testing.T) {\n\tdesc := &description.Session{Medias: []*description.Media{{\n\t\tType:    description.MediaTypeVideo,\n\t\tFormats: []format.Format{&format.MJPEG{}},\n\t}}}\n\n\tr := &stream.Reader{\n\t\tParent: test.Logger(func(logger.Level, string, ...any) {\n\t\t\tt.Error(\"should not happen\")\n\t\t}),\n\t}\n\n\tpc := &PeerConnection{}\n\n\terr := FromStream(desc, r, pc)\n\trequire.Equal(t, errNoSupportedCodecsFrom, err)\n}\n\nfunc TestFromStreamSkipUnsupportedTracks(t *testing.T) {\n\tdesc := &description.Session{Medias: []*description.Media{\n\t\t{\n\t\t\tType:    description.MediaTypeVideo,\n\t\t\tFormats: []format.Format{&format.H264{}},\n\t\t},\n\t\t{\n\t\t\tType:    description.MediaTypeVideo,\n\t\t\tFormats: []format.Format{&format.MJPEG{}},\n\t\t},\n\t}}\n\n\tn := 0\n\n\tr := &stream.Reader{\n\t\tParent: test.Logger(func(l logger.Level, format string, args ...any) {\n\t\t\trequire.Equal(t, logger.Warn, l)\n\t\t\tif n == 0 {\n\t\t\t\trequire.Equal(t, \"skipping track 2 (M-JPEG)\", fmt.Sprintf(format, args...))\n\t\t\t}\n\t\t\tn++\n\t\t}),\n\t}\n\n\tpc := &PeerConnection{}\n\n\terr := FromStream(desc, r, pc)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, 1, n)\n}\n\nfunc TestFromStream(t *testing.T) {\n\tfor _, ca := range toFromStreamCases {\n\t\tt.Run(ca.name, func(t *testing.T) {\n\t\t\tdesc := &description.Session{\n\t\t\t\tMedias: []*description.Media{{\n\t\t\t\t\tFormats: []format.Format{ca.in},\n\t\t\t\t}},\n\t\t\t}\n\n\t\t\tpc := &PeerConnection{}\n\t\t\tr := &stream.Reader{Parent: test.NilLogger}\n\n\t\t\terr := FromStream(desc, r, pc)\n\t\t\trequire.NoError(t, err)\n\n\t\t\trequire.Equal(t, ca.webrtcCaps, pc.OutgoingTracks[0].Caps)\n\t\t})\n\t}\n}\n\nfunc TestFromStreamResampleOpus(t *testing.T) {\n\tstrm := &stream.Stream{\n\t\tDesc: &description.Session{Medias: []*description.Media{\n\t\t\t{\n\t\t\t\tType: description.MediaTypeAudio,\n\t\t\t\tFormats: []format.Format{&format.Opus{\n\t\t\t\t\tChannelCount: 2,\n\t\t\t\t}},\n\t\t\t},\n\t\t}},\n\t\tWriteQueueSize:    512,\n\t\tRTPMaxPayloadSize: 1450,\n\t\tReplaceNTP:        false,\n\t\tParent:            test.NilLogger,\n\t}\n\terr := strm.Initialize()\n\trequire.NoError(t, err)\n\n\tsubStream := &stream.SubStream{\n\t\tStream:        strm,\n\t\tUseRTPPackets: true,\n\t}\n\terr = subStream.Initialize()\n\trequire.NoError(t, err)\n\n\tpc1 := &PeerConnection{\n\t\tLocalRandomUDP:    true,\n\t\tIPsFromInterfaces: true,\n\t\tPublish:           false,\n\t\tLog:               test.NilLogger,\n\t}\n\terr = pc1.Start()\n\trequire.NoError(t, err)\n\tdefer pc1.Close()\n\n\tpc2 := &PeerConnection{\n\t\tLocalRandomUDP:    true,\n\t\tIPsFromInterfaces: true,\n\t\tPublish:           true,\n\t\tLog:               test.NilLogger,\n\t}\n\n\tr := &stream.Reader{Parent: nil}\n\n\terr = FromStream(strm.Desc, r, pc2)\n\trequire.NoError(t, err)\n\n\terr = pc2.Start()\n\trequire.NoError(t, err)\n\tdefer pc2.Close()\n\n\toffer, err := pc1.CreatePartialOffer()\n\trequire.NoError(t, err)\n\n\tanswer, err := pc2.CreateFullAnswer(offer)\n\trequire.NoError(t, err)\n\n\terr = pc1.SetAnswer(answer)\n\trequire.NoError(t, err)\n\n\terr = pc1.WaitUntilConnected(10 * time.Second)\n\trequire.NoError(t, err)\n\n\terr = pc2.WaitUntilConnected(10 * time.Second)\n\trequire.NoError(t, err)\n\n\tstrm.AddReader(r)\n\tdefer strm.RemoveReader(r)\n\n\tsubStream.WriteUnit(strm.Desc.Medias[0], strm.Desc.Medias[0].Formats[0], &unit.Unit{\n\t\tPTS: 0,\n\t\tNTP: time.Now(),\n\t\tRTPPackets: []*rtp.Packet{{\n\t\t\tHeader: rtp.Header{\n\t\t\t\tVersion:        2,\n\t\t\t\tMarker:         true,\n\t\t\t\tPayloadType:    111,\n\t\t\t\tSequenceNumber: 1123,\n\t\t\t\tTimestamp:      45343,\n\t\t\t\tSSRC:           563424,\n\t\t\t},\n\t\t\tPayload: []byte{1},\n\t\t}},\n\t})\n\n\tsubStream.WriteUnit(strm.Desc.Medias[0], strm.Desc.Medias[0].Formats[0], &unit.Unit{\n\t\tPTS: 0,\n\t\tNTP: time.Now(),\n\t\tRTPPackets: []*rtp.Packet{{\n\t\t\tHeader: rtp.Header{\n\t\t\t\tVersion:        2,\n\t\t\t\tMarker:         true,\n\t\t\t\tPayloadType:    111,\n\t\t\t\tSequenceNumber: 1124,\n\t\t\t\tTimestamp:      45343,\n\t\t\t\tSSRC:           563424,\n\t\t\t},\n\t\t\tPayload: []byte{1},\n\t\t}},\n\t})\n\n\terr = pc1.GatherIncomingTracks(2 * time.Second)\n\trequire.NoError(t, err)\n\n\ttracks := pc1.IncomingTracks()\n\n\tdone := make(chan struct{})\n\tn := 0\n\tvar ts uint32\n\n\ttracks[0].OnPacketRTP = func(pkt *rtp.Packet) {\n\t\tn++\n\n\t\tswitch n {\n\t\tcase 1:\n\t\t\tts = pkt.Timestamp\n\n\t\tcase 2:\n\t\t\trequire.Equal(t, uint32(960), pkt.Timestamp-ts)\n\t\t\tclose(done)\n\t\t}\n\t}\n\n\tpc1.StartReading()\n\n\t<-done\n}\n"
  },
  {
    "path": "internal/protocols/webrtc/incoming_track.go",
    "content": "package webrtc\n\nimport (\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/rtpreceiver\"\n\t\"github.com/pion/rtcp\"\n\t\"github.com/pion/rtp\"\n\t\"github.com/pion/webrtc/v4\"\n\n\t\"github.com/bluenviron/mediamtx/internal/counterdumper\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n)\n\nconst (\n\tkeyFrameInterval  = 2 * time.Second\n\tmimeTypeMultiopus = \"audio/multiopus\"\n\tmimeTypeL16       = \"audio/L16\"\n)\n\nvar incomingVideoCodecs = []webrtc.RTPCodecParameters{\n\t{\n\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\tMimeType:    webrtc.MimeTypeAV1,\n\t\t\tClockRate:   90000,\n\t\t\tSDPFmtpLine: \"profile=1\",\n\t\t},\n\t\tPayloadType: 96,\n\t},\n\t{\n\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\tMimeType:  webrtc.MimeTypeAV1,\n\t\t\tClockRate: 90000,\n\t\t},\n\t\tPayloadType: 97,\n\t},\n\t{\n\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\tMimeType:    webrtc.MimeTypeVP9,\n\t\t\tClockRate:   90000,\n\t\t\tSDPFmtpLine: \"profile-id=3\",\n\t\t},\n\t\tPayloadType: 98,\n\t},\n\t{\n\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\tMimeType:    webrtc.MimeTypeVP9,\n\t\t\tClockRate:   90000,\n\t\t\tSDPFmtpLine: \"profile-id=2\",\n\t\t},\n\t\tPayloadType: 99,\n\t},\n\t{\n\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\tMimeType:    webrtc.MimeTypeVP9,\n\t\t\tClockRate:   90000,\n\t\t\tSDPFmtpLine: \"profile-id=1\",\n\t\t},\n\t\tPayloadType: 100,\n\t},\n\t{\n\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\tMimeType:    webrtc.MimeTypeVP9,\n\t\t\tClockRate:   90000,\n\t\t\tSDPFmtpLine: \"profile-id=0\",\n\t\t},\n\t\tPayloadType: 101,\n\t},\n\t{\n\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\tMimeType:  webrtc.MimeTypeVP8,\n\t\t\tClockRate: 90000,\n\t\t},\n\t\tPayloadType: 102,\n\t},\n\t{\n\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\tMimeType:    webrtc.MimeTypeH265,\n\t\t\tClockRate:   90000,\n\t\t\tSDPFmtpLine: \"level-id=93;profile-id=2;tier-flag=0;tx-mode=SRST\",\n\t\t},\n\t\tPayloadType: 103,\n\t},\n\t{\n\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\tMimeType:    webrtc.MimeTypeH265,\n\t\t\tClockRate:   90000,\n\t\t\tSDPFmtpLine: \"level-id=93;profile-id=1;tier-flag=0;tx-mode=SRST\",\n\t\t},\n\t\tPayloadType: 104,\n\t},\n\t{\n\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\tMimeType:    webrtc.MimeTypeH264,\n\t\t\tClockRate:   90000,\n\t\t\tSDPFmtpLine: \"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f\",\n\t\t},\n\t\tPayloadType: 105,\n\t},\n\t{\n\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\tMimeType:    webrtc.MimeTypeH264,\n\t\t\tClockRate:   90000,\n\t\t\tSDPFmtpLine: \"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\",\n\t\t},\n\t\tPayloadType: 106,\n\t},\n}\n\nvar incomingAudioCodecs = []webrtc.RTPCodecParameters{\n\t{\n\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\tMimeType:    mimeTypeMultiopus,\n\t\t\tClockRate:   48000,\n\t\t\tChannels:    3,\n\t\t\tSDPFmtpLine: \"channel_mapping=0,2,1;num_streams=2;coupled_streams=1\",\n\t\t},\n\t\tPayloadType: 112,\n\t},\n\t{\n\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\tMimeType:    mimeTypeMultiopus,\n\t\t\tClockRate:   48000,\n\t\t\tChannels:    4,\n\t\t\tSDPFmtpLine: \"channel_mapping=0,1,2,3;num_streams=2;coupled_streams=2\",\n\t\t},\n\t\tPayloadType: 113,\n\t},\n\t{\n\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\tMimeType:    mimeTypeMultiopus,\n\t\t\tClockRate:   48000,\n\t\t\tChannels:    5,\n\t\t\tSDPFmtpLine: \"channel_mapping=0,4,1,2,3;num_streams=3;coupled_streams=2\",\n\t\t},\n\t\tPayloadType: 114,\n\t},\n\t{\n\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\tMimeType:    mimeTypeMultiopus,\n\t\t\tClockRate:   48000,\n\t\t\tChannels:    6,\n\t\t\tSDPFmtpLine: \"channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2\",\n\t\t},\n\t\tPayloadType: 115,\n\t},\n\t{\n\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\tMimeType:    mimeTypeMultiopus,\n\t\t\tClockRate:   48000,\n\t\t\tChannels:    7,\n\t\t\tSDPFmtpLine: \"channel_mapping=0,4,1,2,3,5,6;num_streams=4;coupled_streams=4\",\n\t\t},\n\t\tPayloadType: 116,\n\t},\n\t{\n\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\tMimeType:    mimeTypeMultiopus,\n\t\t\tClockRate:   48000,\n\t\t\tChannels:    8,\n\t\t\tSDPFmtpLine: \"channel_mapping=0,6,1,4,5,2,3,7;num_streams=5;coupled_streams=4\",\n\t\t},\n\t\tPayloadType: 117,\n\t},\n\t{\n\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\tMimeType:    webrtc.MimeTypeOpus,\n\t\t\tClockRate:   48000,\n\t\t\tChannels:    2,\n\t\t\tSDPFmtpLine: \"minptime=10;useinbandfec=1;stereo=1;sprop-stereo=1\",\n\t\t},\n\t\tPayloadType: 111,\n\t},\n\t{\n\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\tMimeType:  webrtc.MimeTypeG722,\n\t\t\tClockRate: 8000,\n\t\t},\n\t\tPayloadType: 9,\n\t},\n\t{\n\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\tMimeType:  webrtc.MimeTypePCMU,\n\t\t\tClockRate: 8000,\n\t\t\tChannels:  2,\n\t\t},\n\t\tPayloadType: 118,\n\t},\n\t{\n\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\tMimeType:  webrtc.MimeTypePCMA,\n\t\t\tClockRate: 8000,\n\t\t\tChannels:  2,\n\t\t},\n\t\tPayloadType: 119,\n\t},\n\t{\n\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\tMimeType:  webrtc.MimeTypePCMU,\n\t\t\tClockRate: 8000,\n\t\t},\n\t\tPayloadType: 0,\n\t},\n\t{\n\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\tMimeType:  webrtc.MimeTypePCMA,\n\t\t\tClockRate: 8000,\n\t\t},\n\t\tPayloadType: 8,\n\t},\n\t{\n\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\tMimeType:  mimeTypeL16,\n\t\t\tClockRate: 8000,\n\t\t\tChannels:  2,\n\t\t},\n\t\tPayloadType: 120,\n\t},\n\t{\n\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\tMimeType:  mimeTypeL16,\n\t\t\tClockRate: 16000,\n\t\t\tChannels:  2,\n\t\t},\n\t\tPayloadType: 121,\n\t},\n\t{\n\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\tMimeType:  mimeTypeL16,\n\t\t\tClockRate: 48000,\n\t\t\tChannels:  2,\n\t\t},\n\t\tPayloadType: 122,\n\t},\n}\n\n// IncomingTrack is an incoming track.\ntype IncomingTrack struct {\n\tOnPacketRTP func(*rtp.Packet)\n\n\ttrack     *webrtc.TrackRemote\n\treceiver  *webrtc.RTPReceiver\n\trid       string\n\twriteRTCP func([]rtcp.Packet) error\n\tlog       logger.Writer\n\n\tinboundRTPPacketsLost *counterdumper.Dumper\n\trtpReceiver           *rtpreceiver.Receiver\n}\n\nfunc (t *IncomingTrack) initialize() {\n\tt.OnPacketRTP = func(*rtp.Packet) {}\n}\n\n// Codec returns the track codec.\nfunc (t *IncomingTrack) Codec() webrtc.RTPCodecParameters {\n\treturn t.track.Codec()\n}\n\n// ClockRate returns the clock rate. Needed by rtptime.GlobalDecoder\nfunc (t *IncomingTrack) ClockRate() int {\n\treturn int(t.track.Codec().ClockRate)\n}\n\n// PTSEqualsDTS returns whether PTS equals DTS. Needed by rtptime.GlobalDecoder\nfunc (*IncomingTrack) PTSEqualsDTS(*rtp.Packet) bool {\n\treturn true\n}\n\nfunc (t *IncomingTrack) start() {\n\tt.inboundRTPPacketsLost = &counterdumper.Dumper{\n\t\tOnReport: func(val uint64) {\n\t\t\tt.log.Log(logger.Warn, \"%d RTP %s lost\",\n\t\t\t\tval,\n\t\t\t\tfunc() string {\n\t\t\t\t\tif val == 1 {\n\t\t\t\t\t\treturn \"packet\"\n\t\t\t\t\t}\n\t\t\t\t\treturn \"packets\"\n\t\t\t\t}())\n\t\t},\n\t}\n\tt.inboundRTPPacketsLost.Start()\n\n\tt.rtpReceiver = &rtpreceiver.Receiver{\n\t\tClockRate:            int(t.track.Codec().ClockRate),\n\t\tUnrealiableTransport: true,\n\t\tPeriod:               1 * time.Second,\n\t\tWritePacketRTCP: func(p rtcp.Packet) {\n\t\t\tt.writeRTCP([]rtcp.Packet{p}) //nolint:errcheck\n\t\t},\n\t}\n\terr := t.rtpReceiver.Initialize()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\t// read incoming RTCP packets.\n\t// incoming RTCP packets must always be read to make interceptors work.\n\tgo func() {\n\t\tbuf := make([]byte, 1500)\n\t\tfor {\n\t\t\tn, _, err2 := t.receiver.ReadSimulcast(buf, t.rid)\n\t\t\tif err2 != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tpkts, err2 := rtcp.Unmarshal(buf[:n])\n\t\t\tif err2 != nil {\n\t\t\t\tpanic(err2)\n\t\t\t}\n\n\t\t\tfor _, pkt := range pkts {\n\t\t\t\tif sr, ok := pkt.(*rtcp.SenderReport); ok {\n\t\t\t\t\tt.rtpReceiver.ProcessSenderReport(sr, time.Now())\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}()\n\n\t// send period key frame requests\n\tif t.track.Kind() == webrtc.RTPCodecTypeVideo {\n\t\tgo func() {\n\t\t\tkeyframeTicker := time.NewTicker(keyFrameInterval)\n\t\t\tdefer keyframeTicker.Stop()\n\n\t\t\tfor range keyframeTicker.C {\n\t\t\t\terr2 := t.writeRTCP([]rtcp.Packet{\n\t\t\t\t\t&rtcp.PictureLossIndication{\n\t\t\t\t\t\tMediaSSRC: uint32(t.track.SSRC()),\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t\tif err2 != nil {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t}()\n\t}\n\n\t// read incoming RTP packets.\n\tgo func() {\n\t\tfor {\n\t\t\tpkt, _, err2 := t.track.ReadRTP()\n\t\t\tif err2 != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tpackets, lost := t.rtpReceiver.ProcessPacket2(pkt, time.Now(), true)\n\n\t\t\tif lost != 0 {\n\t\t\t\tt.inboundRTPPacketsLost.Add(lost)\n\t\t\t\t// do not return\n\t\t\t}\n\n\t\t\tfor _, pkt := range packets {\n\t\t\t\t// sometimes Chrome sends empty RTP packets. ignore them.\n\t\t\t\tif len(pkt.Payload) == 0 {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tt.OnPacketRTP(pkt)\n\t\t\t}\n\t\t}\n\t}()\n}\n\n// PacketNTP returns the packet NTP.\nfunc (t *IncomingTrack) PacketNTP(pkt *rtp.Packet) (time.Time, bool) {\n\treturn t.rtpReceiver.PacketNTP(pkt.Timestamp)\n}\n\nfunc (t *IncomingTrack) close() {\n\tif t.inboundRTPPacketsLost != nil {\n\t\tt.inboundRTPPacketsLost.Stop()\n\t}\n\tif t.rtpReceiver != nil {\n\t\tt.rtpReceiver.Close()\n\t}\n}\n"
  },
  {
    "path": "internal/protocols/webrtc/net.go",
    "content": "package webrtc\n\nimport (\n\t\"net\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/readbuffer\"\n\t\"github.com/pion/transport/v4\"\n\t\"github.com/pion/transport/v4/stdnet\"\n)\n\ntype webrtcNet struct {\n\tudpReadBufferSize int\n\n\t*stdnet.Net\n}\n\nfunc (n *webrtcNet) initialize() error {\n\tvar err error\n\tn.Net, err = stdnet.NewNet()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (n *webrtcNet) ListenUDP(network string, laddr *net.UDPAddr) (transport.UDPConn, error) {\n\tconn, err := n.Net.ListenUDP(network, laddr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif n.udpReadBufferSize != 0 {\n\t\terr = readbuffer.SetReadBuffer(conn.(*net.UDPConn), n.udpReadBufferSize)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn conn, nil\n}\n"
  },
  {
    "path": "internal/protocols/webrtc/outgoing_data_channel.go",
    "content": "package webrtc\n\nimport (\n\t\"github.com/pion/webrtc/v4\"\n)\n\n// OutgoingDataChannel is an outgoing data channel.\ntype OutgoingDataChannel struct {\n\tLabel string\n\n\tdataChan *webrtc.DataChannel\n}\n\nfunc (c *OutgoingDataChannel) setup(p *PeerConnection) error {\n\tvar err error\n\tc.dataChan, err = p.wr.CreateDataChannel(c.Label, &webrtc.DataChannelInit{\n\t\tOrdered: ptrOf(false),\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// Write writes data to the channel.\nfunc (c *OutgoingDataChannel) Write(data []byte) {\n\tc.dataChan.Send(data) //nolint:errcheck\n}\n"
  },
  {
    "path": "internal/protocols/webrtc/outgoing_track.go",
    "content": "package webrtc\n\nimport (\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/rtpsender\"\n\t\"github.com/pion/rtcp\"\n\t\"github.com/pion/rtp\"\n\t\"github.com/pion/webrtc/v4\"\n)\n\n// OutgoingTrack is an outgoing track.\ntype OutgoingTrack struct {\n\tCaps webrtc.RTPCodecCapability\n\n\ttrack      *webrtc.TrackLocalStaticRTP\n\tssrc       uint32\n\trtcpSender *rtpsender.Sender\n}\n\nfunc (t *OutgoingTrack) isVideo() bool {\n\treturn strings.Split(t.Caps.MimeType, \"/\")[0] == \"video\"\n}\n\nfunc (t *OutgoingTrack) setup(p *PeerConnection) error {\n\tvar trackID string\n\tif t.isVideo() {\n\t\ttrackID = \"video\"\n\t} else {\n\t\ttrackID = \"audio\"\n\t}\n\n\tvar err error\n\tt.track, err = webrtc.NewTrackLocalStaticRTP(\n\t\tt.Caps,\n\t\ttrackID,\n\t\twebrtcStreamID,\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tsender, err := p.wr.AddTrack(t.track)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tt.ssrc = uint32(sender.GetParameters().Encodings[0].SSRC)\n\n\tt.rtcpSender = &rtpsender.Sender{\n\t\tClockRate: int(t.track.Codec().ClockRate),\n\t\tPeriod:    1 * time.Second,\n\t\tTimeNow:   time.Now,\n\t\tWritePacketRTCP: func(pkt rtcp.Packet) {\n\t\t\tp.wr.WriteRTCP([]rtcp.Packet{pkt}) //nolint:errcheck\n\t\t},\n\t}\n\tt.rtcpSender.Initialize()\n\n\t// incoming RTCP packets must always be read to make interceptors work\n\tgo func() {\n\t\tbuf := make([]byte, 1500)\n\t\tfor {\n\t\t\tn, _, err2 := sender.Read(buf)\n\t\t\tif err2 != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t_, err2 = rtcp.Unmarshal(buf[:n])\n\t\t\tif err2 != nil {\n\t\t\t\tpanic(err2)\n\t\t\t}\n\t\t}\n\t}()\n\n\treturn nil\n}\n\nfunc (t *OutgoingTrack) close() {\n\tif t.rtcpSender != nil {\n\t\tt.rtcpSender.Close()\n\t}\n}\n\n// WriteRTP writes a RTP packet.\nfunc (t *OutgoingTrack) WriteRTP(pkt *rtp.Packet) error {\n\treturn t.WriteRTPWithNTP(pkt, time.Now())\n}\n\n// WriteRTPWithNTP writes a RTP packet.\nfunc (t *OutgoingTrack) WriteRTPWithNTP(pkt *rtp.Packet, ntp time.Time) error {\n\t// use right SSRC in packet to make rtcpSender work\n\tpkt.SSRC = t.ssrc\n\n\tt.rtcpSender.ProcessPacket(pkt, ntp, true)\n\n\treturn t.track.WriteRTP(pkt)\n}\n"
  },
  {
    "path": "internal/protocols/webrtc/peer_connection.go",
    "content": "// Package webrtc contains WebRTC utilities.\npackage webrtc\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/pion/ice/v4\"\n\t\"github.com/pion/interceptor\"\n\t\"github.com/pion/sdp/v3\"\n\t\"github.com/pion/webrtc/v4\"\n\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n)\n\nconst (\n\twebrtcStreamID = \"mediamtx\"\n)\n\nfunc interfaceIPs(interfaceList []string) ([]string, error) {\n\tintfs, err := net.Interfaces()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar ips []string\n\n\tfor _, intf := range intfs {\n\t\tif len(interfaceList) == 0 || slices.Contains(interfaceList, intf.Name) {\n\t\t\tvar addrs []net.Addr\n\t\t\taddrs, err = intf.Addrs()\n\t\t\tif err == nil {\n\t\t\t\tfor _, addr := range addrs {\n\t\t\t\t\tvar ip net.IP\n\n\t\t\t\t\tswitch v := addr.(type) {\n\t\t\t\t\tcase *net.IPNet:\n\t\t\t\t\t\tip = v.IP\n\t\t\t\t\tcase *net.IPAddr:\n\t\t\t\t\t\tip = v.IP\n\t\t\t\t\t}\n\n\t\t\t\t\tif ip != nil {\n\t\t\t\t\t\tips = append(ips, ip.String())\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn ips, nil\n}\n\nfunc maxTrackCount(medias []*sdp.MediaDescription) int {\n\ttotal := 0\n\tfor _, media := range medias {\n\t\tridCount := 0\n\n\t\tfor _, attr := range media.Attributes {\n\t\t\tif attr.Key == \"rid\" {\n\t\t\t\tridCount++\n\t\t\t}\n\t\t}\n\n\t\tif ridCount == 0 {\n\t\t\tridCount = 1\n\t\t}\n\t\ttotal += ridCount\n\t}\n\treturn total\n}\n\n// * skip ConfigureRTCPReports\n// * add statsInterceptor\nfunc registerInterceptors(\n\tmediaEngine *webrtc.MediaEngine,\n\tinterceptorRegistry *interceptor.Registry,\n\tonStatsInterceptor func(s *statsInterceptor),\n) error {\n\terr := webrtc.ConfigureNack(mediaEngine, interceptorRegistry)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = webrtc.ConfigureSimulcastExtensionHeaders(mediaEngine)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = webrtc.ConfigureTWCCSender(mediaEngine, interceptorRegistry)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tinterceptorRegistry.Add(&statsInterceptorFactory{\n\t\tonCreate: onStatsInterceptor,\n\t})\n\n\treturn nil\n}\n\nfunc candidateLabel(c *webrtc.ICECandidate) string {\n\treturn c.Typ.String() + \"/\" + c.Protocol.String() + \"/\" +\n\t\tc.Address + \"/\" + strconv.FormatInt(int64(c.Port), 10)\n}\n\n// TracksAreValid checks whether tracks in the SDP are valid\nfunc TracksAreValid(medias []*sdp.MediaDescription) error {\n\tvideoTrack := false\n\taudioTrack := false\n\n\tfor _, media := range medias {\n\t\tswitch media.MediaName.Media {\n\t\tcase \"video\":\n\t\t\tif videoTrack {\n\t\t\t\treturn fmt.Errorf(\"only a single video and a single audio track are supported\")\n\t\t\t}\n\t\t\tvideoTrack = true\n\n\t\tcase \"audio\":\n\t\t\tif audioTrack {\n\t\t\t\treturn fmt.Errorf(\"only a single video and a single audio track are supported\")\n\t\t\t}\n\t\t\taudioTrack = true\n\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"unsupported media '%s'\", media.MediaName.Media)\n\t\t}\n\t}\n\n\tif !videoTrack && !audioTrack {\n\t\treturn fmt.Errorf(\"no valid tracks found\")\n\t}\n\n\treturn nil\n}\n\ntype trackRecvPair struct {\n\ttrack    *webrtc.TrackRemote\n\treceiver *webrtc.RTPReceiver\n}\n\n// PeerConnection is a wrapper around webrtc.PeerConnection.\ntype PeerConnection struct {\n\tUDPReadBufferSize     uint\n\tLocalRandomUDP        bool\n\tICEUDPMux             ice.UDPMux\n\tICETCPMux             *TCPMuxWrapper\n\tICEServers            []webrtc.ICEServer\n\tIPsFromInterfaces     bool\n\tIPsFromInterfacesList []string\n\tAdditionalHosts       []string\n\tSTUNGatherTimeout     time.Duration\n\tPublish               bool\n\tOutgoingTracks        []*OutgoingTrack\n\tOutgoingDataChannels  []*OutgoingDataChannel\n\tLog                   logger.Writer\n\n\twr               *webrtc.PeerConnection\n\tctx              context.Context\n\tctxCancel        context.CancelFunc\n\treadingStarted   *int64\n\tincomingTracks   []*IncomingTrack\n\tstatsInterceptor *statsInterceptor\n\n\tnewLocalCandidate chan *webrtc.ICECandidateInit\n\tincomingTrack     chan trackRecvPair\n\tconnected         chan struct{}\n\tfailed            chan struct{}\n\tclosed            chan struct{}\n\tgatheringDone     chan struct{}\n\tdone              chan struct{}\n\tchStartReading    chan struct{}\n}\n\n// Start starts the peer connection.\nfunc (co *PeerConnection) Start() error {\n\tif co.STUNGatherTimeout == 0 {\n\t\tco.STUNGatherTimeout = 5 * time.Second\n\t}\n\n\tsettingsEngine := webrtc.SettingEngine{}\n\n\tsettingsEngine.SetIncludeLoopbackCandidate(true)\n\n\t// always enable TCP since we might be the client of a remote TCP listener\n\tnetworkTypes := []webrtc.NetworkType{\n\t\twebrtc.NetworkTypeTCP4,\n\t\twebrtc.NetworkTypeTCP6,\n\t}\n\n\tif co.LocalRandomUDP || co.ICEUDPMux != nil || len(co.ICEServers) != 0 {\n\t\tnetworkTypes = append(networkTypes, webrtc.NetworkTypeUDP4, webrtc.NetworkTypeUDP6)\n\t}\n\n\tsettingsEngine.SetNetworkTypes(networkTypes)\n\n\tif co.ICEUDPMux != nil {\n\t\tsettingsEngine.SetICEUDPMux(co.ICEUDPMux)\n\t}\n\n\tif co.ICETCPMux != nil {\n\t\tsettingsEngine.SetICETCPMux(co.ICETCPMux.Mux)\n\t}\n\n\tsettingsEngine.SetSTUNGatherTimeout(co.STUNGatherTimeout)\n\n\twebrtcNet := &webrtcNet{\n\t\tudpReadBufferSize: int(co.UDPReadBufferSize),\n\t}\n\terr := webrtcNet.initialize()\n\tif err != nil {\n\t\treturn err\n\t}\n\tsettingsEngine.SetNet(webrtcNet)\n\n\tmediaEngine := &webrtc.MediaEngine{}\n\n\tif co.Publish {\n\t\tvideoSetupped := false\n\t\taudioSetupped := false\n\t\tfor _, tr := range co.OutgoingTracks {\n\t\t\tif tr.isVideo() {\n\t\t\t\tvideoSetupped = true\n\t\t\t} else {\n\t\t\t\taudioSetupped = true\n\t\t\t}\n\t\t}\n\n\t\t// When audio is not used, a track has to be present anyway,\n\t\t// otherwise video is not displayed on Firefox and Chrome.\n\t\tif !audioSetupped {\n\t\t\tco.OutgoingTracks = append(co.OutgoingTracks, &OutgoingTrack{\n\t\t\t\tCaps: webrtc.RTPCodecCapability{\n\t\t\t\t\tMimeType:  webrtc.MimeTypePCMU,\n\t\t\t\t\tClockRate: 8000,\n\t\t\t\t},\n\t\t\t})\n\t\t}\n\n\t\tfor i, tr := range co.OutgoingTracks {\n\t\t\tvar codecType webrtc.RTPCodecType\n\t\t\tif tr.isVideo() {\n\t\t\t\tcodecType = webrtc.RTPCodecTypeVideo\n\t\t\t} else {\n\t\t\t\tcodecType = webrtc.RTPCodecTypeAudio\n\t\t\t}\n\n\t\t\terr = mediaEngine.RegisterCodec(webrtc.RTPCodecParameters{\n\t\t\t\tRTPCodecCapability: tr.Caps,\n\t\t\t\tPayloadType:        webrtc.PayloadType(96 + i),\n\t\t\t}, codecType)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\t// When video is not used, a track must not be added but a codec has to present.\n\t\t// Otherwise audio is muted on Firefox and Chrome.\n\t\tif !videoSetupped {\n\t\t\terr = mediaEngine.RegisterCodec(webrtc.RTPCodecParameters{\n\t\t\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\t\t\tMimeType:  webrtc.MimeTypeVP8,\n\t\t\t\t\tClockRate: 90000,\n\t\t\t\t},\n\t\t\t\tPayloadType: 96,\n\t\t\t}, webrtc.RTPCodecTypeVideo)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfor _, codec := range incomingVideoCodecs {\n\t\t\terr = mediaEngine.RegisterCodec(codec, webrtc.RTPCodecTypeVideo)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tfor _, codec := range incomingAudioCodecs {\n\t\t\terr = mediaEngine.RegisterCodec(codec, webrtc.RTPCodecTypeAudio)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\tinterceptorRegistry := &interceptor.Registry{}\n\n\terr = registerInterceptors(\n\t\tmediaEngine,\n\t\tinterceptorRegistry,\n\t\tfunc(s *statsInterceptor) {\n\t\t\tco.statsInterceptor = s\n\t\t},\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tapi := webrtc.NewAPI(\n\t\twebrtc.WithSettingEngine(settingsEngine),\n\t\twebrtc.WithMediaEngine(mediaEngine),\n\t\twebrtc.WithInterceptorRegistry(interceptorRegistry))\n\n\tco.wr, err = api.NewPeerConnection(webrtc.Configuration{\n\t\tICEServers: co.ICEServers,\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tco.ctx, co.ctxCancel = context.WithCancel(context.Background())\n\n\tco.readingStarted = new(int64)\n\n\tco.newLocalCandidate = make(chan *webrtc.ICECandidateInit)\n\tco.connected = make(chan struct{})\n\tco.failed = make(chan struct{})\n\tco.closed = make(chan struct{})\n\tco.gatheringDone = make(chan struct{})\n\tco.incomingTrack = make(chan trackRecvPair)\n\tco.done = make(chan struct{})\n\tco.chStartReading = make(chan struct{})\n\n\tif co.Publish {\n\t\tfor _, tr := range co.OutgoingTracks {\n\t\t\terr = tr.setup(co)\n\t\t\tif err != nil {\n\t\t\t\tco.wr.GracefulClose() //nolint:errcheck\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tfor _, dc := range co.OutgoingDataChannels {\n\t\t\terr = dc.setup(co)\n\t\t\tif err != nil {\n\t\t\t\tco.wr.GracefulClose() //nolint:errcheck\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t} else {\n\t\t_, err = co.wr.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo, webrtc.RTPTransceiverInit{\n\t\t\tDirection: webrtc.RTPTransceiverDirectionRecvonly,\n\t\t})\n\t\tif err != nil {\n\t\t\tco.wr.GracefulClose() //nolint:errcheck\n\t\t\treturn err\n\t\t}\n\n\t\t_, err = co.wr.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio, webrtc.RTPTransceiverInit{\n\t\t\tDirection: webrtc.RTPTransceiverDirectionRecvonly,\n\t\t})\n\t\tif err != nil {\n\t\t\tco.wr.GracefulClose() //nolint:errcheck\n\t\t\treturn err\n\t\t}\n\n\t\tco.wr.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {\n\t\t\tselect {\n\t\t\tcase co.incomingTrack <- trackRecvPair{track, receiver}:\n\t\t\tcase <-co.ctx.Done():\n\t\t\t}\n\t\t})\n\t}\n\n\tvar stateChangeMutex sync.Mutex\n\n\tco.wr.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {\n\t\tstateChangeMutex.Lock()\n\t\tdefer stateChangeMutex.Unlock()\n\n\t\tselect {\n\t\tcase <-co.closed:\n\t\t\treturn\n\t\tdefault:\n\t\t}\n\n\t\tco.Log.Log(logger.Debug, \"peer connection state: \"+state.String())\n\n\t\tswitch state {\n\t\tcase webrtc.PeerConnectionStateConnected:\n\t\t\t// PeerConnectionStateConnected can arrive twice, since state can\n\t\t\t// switch from \"disconnected\" to \"connected\".\n\t\t\t// contrarily, we're interested into emitting \"connected\" once.\n\t\t\tselect {\n\t\t\tcase <-co.connected:\n\t\t\t\treturn\n\t\t\tdefault:\n\t\t\t}\n\n\t\t\tco.Log.Log(logger.Info, \"peer connection established, local candidate: %v, remote candidate: %v\",\n\t\t\t\tco.LocalCandidate(), co.RemoteCandidate())\n\n\t\t\tclose(co.connected)\n\n\t\tcase webrtc.PeerConnectionStateFailed:\n\t\t\tclose(co.failed)\n\n\t\tcase webrtc.PeerConnectionStateClosed:\n\t\t\t// \"closed\" can arrive before \"failed\" and without\n\t\t\t// the Close() method being called at all.\n\t\t\t// It happens when the other peer sends a termination\n\t\t\t// message like a DTLS CloseNotify.\n\t\t\tselect {\n\t\t\tcase <-co.failed:\n\t\t\tdefault:\n\t\t\t\tclose(co.failed)\n\t\t\t}\n\n\t\t\tclose(co.closed)\n\t\t}\n\t})\n\n\tco.wr.OnICECandidate(func(i *webrtc.ICECandidate) {\n\t\tif i != nil {\n\t\t\tv := i.ToJSON()\n\t\t\tselect {\n\t\t\tcase co.newLocalCandidate <- &v:\n\t\t\tcase <-co.connected:\n\t\t\tcase <-co.ctx.Done():\n\t\t\t}\n\t\t} else {\n\t\t\tclose(co.gatheringDone)\n\t\t}\n\t})\n\n\tgo co.run()\n\n\treturn nil\n}\n\n// Close closes the connection.\nfunc (co *PeerConnection) Close() {\n\tco.ctxCancel()\n\t<-co.done\n}\n\nfunc (co *PeerConnection) run() {\n\tdefer close(co.done)\n\n\tdefer func() {\n\t\tfor _, track := range co.incomingTracks {\n\t\t\ttrack.close()\n\t\t}\n\t\tfor _, track := range co.OutgoingTracks {\n\t\t\ttrack.close()\n\t\t}\n\n\t\tco.wr.GracefulClose() //nolint:errcheck\n\n\t\t// even if GracefulClose() should wait for any goroutine to return,\n\t\t// we have to wait for OnConnectionStateChange to return anyway,\n\t\t// since it is executed in an uncontrolled goroutine.\n\t\t// https://github.com/pion/webrtc/blob/v4.2.8/peerconnection.go#L529\n\t\t<-co.closed\n\t}()\n\n\tfor {\n\t\tselect {\n\t\tcase <-co.chStartReading:\n\t\t\tfor _, track := range co.incomingTracks {\n\t\t\t\ttrack.start()\n\t\t\t}\n\t\t\tatomic.StoreInt64(co.readingStarted, 1)\n\n\t\tcase <-co.ctx.Done():\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (co *PeerConnection) removeUnwantedCandidates(firstMedia *sdp.MediaDescription) error {\n\tvar allowedIPs []string\n\tif co.IPsFromInterfaces {\n\t\tvar err error\n\t\tallowedIPs, err = interfaceIPs(co.IPsFromInterfacesList)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tvar newAttributes []sdp.Attribute //nolint:prealloc\n\n\tfor _, attr := range firstMedia.Attributes {\n\t\tif attr.Key == \"candidate\" {\n\t\t\tparts := strings.Split(attr.Value, \" \")\n\n\t\t\t// hide random UDP candidates\n\t\t\tif !co.LocalRandomUDP && co.ICEUDPMux == nil && parts[2] == \"udp\" && parts[7] == \"host\" {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// hide disallowed IPs\n\t\t\tif parts[7] == \"host\" && !slices.Contains(allowedIPs, parts[4]) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tnewAttributes = append(newAttributes, attr)\n\t}\n\n\tfirstMedia.Attributes = newAttributes\n\n\treturn nil\n}\n\nfunc (co *PeerConnection) addAdditionalCandidates(firstMedia *sdp.MediaDescription) error {\n\ti := 0\n\tfor _, attr := range firstMedia.Attributes {\n\t\tif attr.Key == \"end-of-candidates\" {\n\t\t\tbreak\n\t\t}\n\t\ti++\n\t}\n\n\tfor _, host := range co.AdditionalHosts {\n\t\tvar ips []string\n\t\tif net.ParseIP(host) != nil {\n\t\t\tips = []string{host}\n\t\t} else {\n\t\t\ttmp, err := net.LookupIP(host)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tips = make([]string, len(tmp))\n\t\t\tfor i, e := range tmp {\n\t\t\t\tips[i] = e.String()\n\t\t\t}\n\t\t}\n\n\t\tfor _, ip := range ips {\n\t\t\tnewAttrs := append([]sdp.Attribute(nil), firstMedia.Attributes[:i]...)\n\n\t\t\tif co.ICEUDPMux != nil {\n\t\t\t\tport := strconv.FormatInt(int64(co.ICEUDPMux.GetListenAddresses()[0].(*net.UDPAddr).Port), 10)\n\n\t\t\t\ttmp, err := randUint32()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tid := strconv.FormatInt(int64(tmp), 10)\n\n\t\t\t\tnewAttrs = append(newAttrs, sdp.Attribute{\n\t\t\t\t\tKey:   \"candidate\",\n\t\t\t\t\tValue: id + \" 1 udp 2130706431 \" + ip + \" \" + port + \" typ host\",\n\t\t\t\t})\n\t\t\t\tnewAttrs = append(newAttrs, sdp.Attribute{\n\t\t\t\t\tKey:   \"candidate\",\n\t\t\t\t\tValue: id + \" 2 udp 2130706431 \" + ip + \" \" + port + \" typ host\",\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif co.ICETCPMux != nil {\n\t\t\t\tport := strconv.FormatInt(int64(co.ICETCPMux.Ln.Addr().(*net.TCPAddr).Port), 10)\n\n\t\t\t\ttmp, err := randUint32()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\tid := strconv.FormatInt(int64(tmp), 10)\n\n\t\t\t\tnewAttrs = append(newAttrs, sdp.Attribute{\n\t\t\t\t\tKey:   \"candidate\",\n\t\t\t\t\tValue: id + \" 1 tcp 1671430143 \" + ip + \" \" + port + \" typ host tcptype passive\",\n\t\t\t\t})\n\t\t\t\tnewAttrs = append(newAttrs, sdp.Attribute{\n\t\t\t\t\tKey:   \"candidate\",\n\t\t\t\t\tValue: id + \" 2 tcp 1671430143 \" + ip + \" \" + port + \" typ host tcptype passive\",\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tnewAttrs = append(newAttrs, firstMedia.Attributes[i:]...)\n\t\t\tfirstMedia.Attributes = newAttrs\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (co *PeerConnection) filterLocalDescription(desc *webrtc.SessionDescription) (*webrtc.SessionDescription, error) {\n\tvar psdp sdp.SessionDescription\n\tpsdp.Unmarshal([]byte(desc.SDP)) //nolint:errcheck\n\n\tfirstMedia := psdp.MediaDescriptions[0]\n\n\terr := co.removeUnwantedCandidates(firstMedia)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\terr = co.addAdditionalCandidates(firstMedia)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tout, _ := psdp.Marshal()\n\tdesc.SDP = string(out)\n\n\treturn desc, nil\n}\n\n// CreatePartialOffer creates a partial offer.\nfunc (co *PeerConnection) CreatePartialOffer() (*webrtc.SessionDescription, error) {\n\ttmp, err := co.wr.CreateOffer(nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\toffer := &tmp\n\n\terr = co.wr.SetLocalDescription(*offer)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\toffer, err = co.filterLocalDescription(offer)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn offer, nil\n}\n\n// SetAnswer sets the answer.\nfunc (co *PeerConnection) SetAnswer(answer *webrtc.SessionDescription) error {\n\treturn co.wr.SetRemoteDescription(*answer)\n}\n\n// AddRemoteCandidate adds a remote candidate.\nfunc (co *PeerConnection) AddRemoteCandidate(candidate *webrtc.ICECandidateInit) error {\n\treturn co.wr.AddICECandidate(*candidate)\n}\n\n// CreateFullAnswer creates a full answer.\nfunc (co *PeerConnection) CreateFullAnswer(offer *webrtc.SessionDescription) (*webrtc.SessionDescription, error) {\n\terr := co.wr.SetRemoteDescription(*offer)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttmp, err := co.wr.CreateAnswer(nil)\n\tif err != nil {\n\t\tif errors.Is(err, webrtc.ErrSenderWithNoCodecs) {\n\t\t\treturn nil, fmt.Errorf(\"codecs not supported by client\")\n\t\t}\n\t\treturn nil, err\n\t}\n\tanswer := &tmp\n\n\terr = co.wr.SetLocalDescription(*answer)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\terr = co.waitGatheringDone()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tanswer = co.wr.LocalDescription()\n\n\tanswer, err = co.filterLocalDescription(answer)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn answer, nil\n}\n\nfunc (co *PeerConnection) waitGatheringDone() error {\n\tfor {\n\t\tselect {\n\t\tcase <-co.NewLocalCandidate():\n\t\tcase <-co.GatheringDone():\n\t\t\treturn nil\n\t\tcase <-co.ctx.Done():\n\t\t\treturn fmt.Errorf(\"terminated\")\n\t\t}\n\t}\n}\n\n// WaitUntilConnected waits until connection is established.\nfunc (co *PeerConnection) WaitUntilConnected(timeout time.Duration) error {\n\tt := time.NewTimer(timeout)\n\tdefer t.Stop()\n\nouter:\n\tfor {\n\t\tselect {\n\t\tcase <-t.C:\n\t\t\treturn fmt.Errorf(\"deadline exceeded while waiting connection\")\n\n\t\tcase <-co.connected:\n\t\t\tbreak outer\n\n\t\tcase <-co.ctx.Done():\n\t\t\treturn fmt.Errorf(\"terminated\")\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// GatherIncomingTracks gathers incoming tracks.\nfunc (co *PeerConnection) GatherIncomingTracks(timeout time.Duration) error {\n\tvar sdp sdp.SessionDescription\n\tsdp.Unmarshal([]byte(co.wr.RemoteDescription().SDP)) //nolint:errcheck\n\n\tmaxTrackCount := maxTrackCount(sdp.MediaDescriptions)\n\n\tt := time.NewTimer(timeout)\n\tdefer t.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-t.C:\n\t\t\tif len(co.incomingTracks) != 0 {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\treturn fmt.Errorf(\"deadline exceeded while waiting tracks\")\n\n\t\tcase pair := <-co.incomingTrack:\n\t\t\tt := &IncomingTrack{\n\t\t\t\ttrack:     pair.track,\n\t\t\t\treceiver:  pair.receiver,\n\t\t\t\trid:       pair.track.RID(),\n\t\t\t\twriteRTCP: co.wr.WriteRTCP,\n\t\t\t\tlog:       co.Log,\n\t\t\t}\n\t\t\tt.initialize()\n\t\t\tco.incomingTracks = append(co.incomingTracks, t)\n\n\t\t\tif len(co.incomingTracks) >= maxTrackCount {\n\t\t\t\treturn nil\n\t\t\t}\n\n\t\tcase <-co.Failed():\n\t\t\treturn fmt.Errorf(\"peer connection closed\")\n\n\t\tcase <-co.ctx.Done():\n\t\t\treturn fmt.Errorf(\"terminated\")\n\t\t}\n\t}\n}\n\n// Connected returns when connected.\nfunc (co *PeerConnection) Connected() <-chan struct{} {\n\treturn co.connected\n}\n\n// Failed returns when failed.\nfunc (co *PeerConnection) Failed() <-chan struct{} {\n\treturn co.failed\n}\n\n// NewLocalCandidate returns when there's a new local candidate.\nfunc (co *PeerConnection) NewLocalCandidate() <-chan *webrtc.ICECandidateInit {\n\treturn co.newLocalCandidate\n}\n\n// GatheringDone returns when candidate gathering is complete.\nfunc (co *PeerConnection) GatheringDone() <-chan struct{} {\n\treturn co.gatheringDone\n}\n\n// IncomingTracks returns incoming tracks.\nfunc (co *PeerConnection) IncomingTracks() []*IncomingTrack {\n\treturn co.incomingTracks\n}\n\n// StartReading starts reading incoming tracks.\nfunc (co *PeerConnection) StartReading() {\n\tselect {\n\tcase co.chStartReading <- struct{}{}:\n\tcase <-co.ctx.Done():\n\t}\n}\n\n// LocalCandidate returns the local candidate.\nfunc (co *PeerConnection) LocalCandidate() string {\n\treceivers := co.wr.GetReceivers()\n\tif len(receivers) < 1 {\n\t\treturn \"\"\n\t}\n\n\tcp, err := receivers[0].Transport().ICETransport().GetSelectedCandidatePair()\n\tif err != nil || cp == nil {\n\t\treturn \"\"\n\t}\n\n\treturn candidateLabel(cp.Local)\n}\n\n// RemoteCandidate returns the remote candidate.\nfunc (co *PeerConnection) RemoteCandidate() string {\n\treceivers := co.wr.GetReceivers()\n\tif len(receivers) < 1 {\n\t\treturn \"\"\n\t}\n\n\tcp, err := receivers[0].Transport().ICETransport().GetSelectedCandidatePair()\n\tif err != nil || cp == nil {\n\t\treturn \"\"\n\t}\n\n\treturn candidateLabel(cp.Remote)\n}\n\nfunc bytesStats(wr *webrtc.PeerConnection) (uint64, uint64) {\n\tfor _, stats := range wr.GetStats() {\n\t\tif tstats, ok := stats.(webrtc.TransportStats); ok {\n\t\t\tif tstats.ID == \"iceTransport\" {\n\t\t\t\treturn tstats.BytesReceived, tstats.BytesSent\n\t\t\t}\n\t\t}\n\t}\n\treturn 0, 0\n}\n\n// Stats returns statistics.\nfunc (co *PeerConnection) Stats() *Stats {\n\tbytesReceived, bytesSent := bytesStats(co.wr)\n\n\tv := float64(0)\n\tn := float64(0)\n\tpacketsReceived := uint64(0)\n\tpacketsSent := uint64(0)\n\tpacketsLost := uint64(0)\n\n\tif atomic.LoadInt64(co.readingStarted) == 1 {\n\t\tfor _, tr := range co.incomingTracks {\n\t\t\tif recvStats := tr.rtpReceiver.Stats(); recvStats != nil {\n\t\t\t\tv += recvStats.Jitter\n\t\t\t\tn++\n\t\t\t\tpacketsReceived += recvStats.Received\n\t\t\t\tpacketsLost += recvStats.Lost\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, tr := range co.OutgoingTracks {\n\t\tif sentStats := tr.rtcpSender.Stats(); sentStats != nil {\n\t\t\tpacketsSent += sentStats.Sent\n\t\t}\n\t}\n\n\tvar rtpPacketsJitter float64\n\tif n != 0 {\n\t\trtpPacketsJitter = v / n\n\t} else {\n\t\trtpPacketsJitter = 0\n\t}\n\n\treturn &Stats{\n\t\tBytesReceived:       bytesReceived,\n\t\tBytesSent:           bytesSent,\n\t\tRTPPacketsReceived:  packetsReceived,\n\t\tRTPPacketsSent:      packetsSent,\n\t\tRTPPacketsLost:      packetsLost,\n\t\tRTPPacketsJitter:    rtpPacketsJitter,\n\t\tRTCPPacketsReceived: atomic.LoadUint64(co.statsInterceptor.rtcpPacketsReceived),\n\t\tRTCPPacketsSent:     atomic.LoadUint64(co.statsInterceptor.rtcpPacketsSent),\n\t}\n}\n"
  },
  {
    "path": "internal/protocols/webrtc/peer_connection_test.go",
    "content": "package webrtc\n\nimport (\n\t\"net\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strings\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/pion/ice/v4\"\n\t\"github.com/pion/logging\"\n\t\"github.com/pion/rtcp\"\n\t\"github.com/pion/rtp\"\n\t\"github.com/pion/sdp/v3\"\n\t\"github.com/pion/webrtc/v4\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype nilWriter struct{}\n\nfunc (nilWriter) Write(p []byte) (int, error) {\n\treturn len(p), nil\n}\n\nvar webrtcNilLogger = logging.NewDefaultLeveledLoggerForScope(\"\", 0, &nilWriter{})\n\nfunc gatherCodecs(tracks []*IncomingTrack) []webrtc.RTPCodecParameters {\n\tcodecs := make([]webrtc.RTPCodecParameters, len(tracks))\n\tfor i, track := range tracks {\n\t\tcodecs[i] = track.Codec()\n\t}\n\treturn codecs\n}\n\nfunc TestPeerConnectionCloseImmediately(t *testing.T) {\n\tpc := &PeerConnection{\n\t\tLocalRandomUDP:    true,\n\t\tIPsFromInterfaces: true,\n\t\tPublish:           false,\n\t\tLog:               test.NilLogger,\n\t}\n\terr := pc.Start()\n\trequire.NoError(t, err)\n\tdefer pc.Close()\n}\n\nfunc TestPeerConnectionCloseImmediately2(t *testing.T) {\n\tpc := &PeerConnection{\n\t\tLocalRandomUDP:    true,\n\t\tIPsFromInterfaces: true,\n\t\tPublish:           false,\n\t\tLog:               test.NilLogger,\n\t}\n\terr := pc.Start()\n\trequire.NoError(t, err)\n\tdefer pc.Close()\n\n\t_, err = pc.CreatePartialOffer()\n\trequire.NoError(t, err)\n\n\t// wait for ICE candidates to be generated\n\ttime.Sleep(500 * time.Millisecond)\n}\n\nfunc TestPeerConnectionCandidates(t *testing.T) {\n\tfor _, ca := range []string{\n\t\t\"udp random\",\n\t\t\"udp\",\n\t\t\"tcp\",\n\t\t\"stun\",\n\t\t\"udp+stun\",\n\t\t\"udp random+stun\",\n\t} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tpc2, err := webrtc.NewPeerConnection(webrtc.Configuration{})\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer pc2.Close() //nolint:errcheck\n\n\t\t\ttrack, err := webrtc.NewTrackLocalStaticRTP(\n\t\t\t\twebrtc.RTPCodecCapability{\n\t\t\t\t\tMimeType:  webrtc.MimeTypeVP8,\n\t\t\t\t\tClockRate: 90000,\n\t\t\t\t},\n\t\t\t\t\"video\",\n\t\t\t\t\"publisher\",\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\n\t\t\t_, err = pc2.AddTrack(track)\n\t\t\trequire.NoError(t, err)\n\n\t\t\toffer, err := pc2.CreateOffer(nil)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tvar udpMux ice.UDPMux\n\t\t\tif ca == \"udp\" || ca == \"udp+stun\" {\n\t\t\t\tvar ln net.PacketConn\n\t\t\t\tln, err = net.ListenPacket(\"udp\", \":3454\")\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer ln.Close()\n\t\t\t\tudpMux = webrtc.NewICEUDPMux(webrtcNilLogger, ln)\n\t\t\t}\n\n\t\t\tvar tcpMux *TCPMuxWrapper\n\t\t\tif ca == \"tcp\" {\n\t\t\t\tvar ln net.Listener\n\t\t\t\tln, err = net.Listen(\"tcp\", \":3454\")\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer ln.Close()\n\t\t\t\ttcpMux = &TCPMuxWrapper{\n\t\t\t\t\tMux: webrtc.NewICETCPMux(webrtcNilLogger, ln, 8),\n\t\t\t\t\tLn:  ln,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tpc := &PeerConnection{\n\t\t\t\tLocalRandomUDP:        (ca == \"udp random\" || ca == \"udp random+stun\"),\n\t\t\t\tICEUDPMux:             udpMux,\n\t\t\t\tICETCPMux:             tcpMux,\n\t\t\t\tIPsFromInterfaces:     true,\n\t\t\t\tIPsFromInterfacesList: []string{\"lo\"},\n\t\t\t\tLog:                   test.NilLogger,\n\t\t\t}\n\n\t\t\tif ca == \"stun\" || ca == \"udp+stun\" || ca == \"udp random+stun\" {\n\t\t\t\tpc.ICEServers = []webrtc.ICEServer{{\n\t\t\t\t\tURLs: []string{\"stun:stun.l.google.com:19302\"},\n\t\t\t\t}}\n\t\t\t}\n\n\t\t\terr = pc.Start()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer pc.Close()\n\n\t\t\tanswer, err := pc.CreateFullAnswer(&offer)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tn := len(regexp.MustCompile(\"(?m)^a=candidate:.+? udp .+? typ host\").FindAllString(answer.SDP, -1))\n\t\t\tif ca == \"udp\" || ca == \"udp random\" || ca == \"udp+stun\" || ca == \"udp random+stun\" {\n\t\t\t\trequire.Equal(t, 2, n)\n\t\t\t} else {\n\t\t\t\trequire.Equal(t, 0, n)\n\t\t\t}\n\n\t\t\tn = len(regexp.MustCompile(\"(?m)^a=candidate:.+? tcp .+? typ host tcptype passive\").FindAllString(answer.SDP, -1))\n\t\t\tif ca == \"tcp\" {\n\t\t\t\trequire.Equal(t, 2, n)\n\t\t\t} else {\n\t\t\t\trequire.Equal(t, 0, n)\n\t\t\t}\n\n\t\t\tn = len(regexp.MustCompile(\"(?m)^a=candidate:.+? udp .+? typ srflx\").FindAllString(answer.SDP, -1))\n\t\t\tif ca == \"stun\" || ca == \"udp+stun\" || ca == \"udp random+stun\" {\n\t\t\t\trequire.Equal(t, 2, n)\n\t\t\t} else {\n\t\t\t\trequire.Equal(t, 0, n)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestPeerConnectionConnectivity(t *testing.T) {\n\tfor _, mode := range []string{\n\t\t\"passive udp\",\n\t\t\"passive tcp\",\n\t\t\"active udp\",\n\t\t\"active udp + stun\",\n\t} {\n\t\tfor _, ip := range []string{\n\t\t\t\"from interfaces\",\n\t\t\t\"additional hosts\",\n\t\t} {\n\t\t\t// LocalRandomUDP doesn't work with AdditionalHosts\n\t\t\t// we don't care since we are not currently using them together\n\t\t\tif mode == \"active udp\" && ip == \"additional hosts\" {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tt.Run(mode+\"_\"+ip, func(t *testing.T) {\n\t\t\t\tvar iceServers []webrtc.ICEServer\n\n\t\t\t\tif mode == \"active udp + stun\" {\n\t\t\t\t\ticeServers = []webrtc.ICEServer{{\n\t\t\t\t\t\tURLs: []string{\"stun:stun.l.google.com:19302\"},\n\t\t\t\t\t}}\n\t\t\t\t}\n\n\t\t\t\tclientPC := &PeerConnection{\n\t\t\t\t\tLocalRandomUDP:        (mode == \"passive udp\" || mode == \"active udp\"),\n\t\t\t\t\tIPsFromInterfaces:     true,\n\t\t\t\t\tIPsFromInterfacesList: []string{\"lo\"},\n\t\t\t\t\tICEServers:            iceServers,\n\t\t\t\t\tLog:                   test.NilLogger,\n\t\t\t\t}\n\t\t\t\terr := clientPC.Start()\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer clientPC.Close()\n\n\t\t\t\tvar udpMux ice.UDPMux\n\t\t\t\tvar tcpMux *TCPMuxWrapper\n\n\t\t\t\tswitch mode {\n\t\t\t\tcase \"passive udp\":\n\t\t\t\t\tvar ln net.PacketConn\n\t\t\t\t\tln, err = net.ListenPacket(\"udp4\", \":4458\")\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\tdefer ln.Close()\n\t\t\t\t\tudpMux = webrtc.NewICEUDPMux(webrtcNilLogger, ln)\n\n\t\t\t\tcase \"passive tcp\":\n\t\t\t\t\tvar ln net.Listener\n\t\t\t\t\tln, err = net.Listen(\"tcp4\", \":4458\")\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\tdefer ln.Close()\n\t\t\t\t\ttcpMux = &TCPMuxWrapper{\n\t\t\t\t\t\tMux: webrtc.NewICETCPMux(webrtcNilLogger, ln, 8),\n\t\t\t\t\t\tLn:  ln,\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tserverPC := &PeerConnection{\n\t\t\t\t\tLocalRandomUDP: (mode == \"active udp\"),\n\t\t\t\t\tICEUDPMux:      udpMux,\n\t\t\t\t\tICETCPMux:      tcpMux,\n\t\t\t\t\tICEServers:     iceServers,\n\t\t\t\t\tPublish:        true,\n\t\t\t\t\tOutgoingTracks: []*OutgoingTrack{{\n\t\t\t\t\t\tCaps: webrtc.RTPCodecCapability{\n\t\t\t\t\t\t\tMimeType:  webrtc.MimeTypeAV1,\n\t\t\t\t\t\t\tClockRate: 90000,\n\t\t\t\t\t\t},\n\t\t\t\t\t}},\n\t\t\t\t\tLog: test.NilLogger,\n\t\t\t\t}\n\n\t\t\t\tif ip == \"from interfaces\" {\n\t\t\t\t\tserverPC.IPsFromInterfaces = true\n\t\t\t\t\tserverPC.IPsFromInterfacesList = []string{\"lo\"}\n\t\t\t\t} else {\n\t\t\t\t\tserverPC.AdditionalHosts = []string{\"127.0.0.1\"}\n\t\t\t\t}\n\n\t\t\t\terr = serverPC.Start()\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer serverPC.Close()\n\n\t\t\t\toffer, err := clientPC.CreatePartialOffer()\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tanswer, err := serverPC.CreateFullAnswer(offer)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\trequire.Equal(t, 2, strings.Count(answer.SDP, \"a=candidate:\"))\n\n\t\t\t\terr = clientPC.SetAnswer(answer)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tgo func() {\n\t\t\t\t\tfor {\n\t\t\t\t\t\tselect {\n\t\t\t\t\t\tcase cd := <-clientPC.NewLocalCandidate():\n\t\t\t\t\t\t\terr2 := serverPC.AddRemoteCandidate(cd)\n\t\t\t\t\t\t\trequire.NoError(t, err2)\n\n\t\t\t\t\t\tcase <-clientPC.Failed():\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}()\n\n\t\t\t\terr = serverPC.WaitUntilConnected(10 * time.Second)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t})\n\t\t}\n\t}\n}\n\nfunc TestPeerConnectionRead(t *testing.T) {\n\tpub, err := webrtc.NewPeerConnection(webrtc.Configuration{})\n\trequire.NoError(t, err)\n\tdefer pub.Close() //nolint:errcheck\n\n\tvideoTrack, err := webrtc.NewTrackLocalStaticRTP(\n\t\twebrtc.RTPCodecCapability{\n\t\t\tMimeType:  webrtc.MimeTypeVP8,\n\t\t\tClockRate: 90000,\n\t\t},\n\t\t\"video\",\n\t\t\"publisher\")\n\trequire.NoError(t, err)\n\n\tvideoSender, err := pub.AddTrack(videoTrack)\n\trequire.NoError(t, err)\n\n\taudioTrack, err := webrtc.NewTrackLocalStaticRTP(\n\t\twebrtc.RTPCodecCapability{\n\t\t\tMimeType:  webrtc.MimeTypeOpus,\n\t\t\tClockRate: 48000,\n\t\t},\n\t\t\"audio\",\n\t\t\"publisher\")\n\trequire.NoError(t, err)\n\n\taudioSender, err := pub.AddTrack(audioTrack)\n\trequire.NoError(t, err)\n\n\treader := &PeerConnection{\n\t\tLocalRandomUDP:    true,\n\t\tIPsFromInterfaces: true,\n\t\tPublish:           false,\n\t\tLog:               test.NilLogger,\n\t}\n\terr = reader.Start()\n\trequire.NoError(t, err)\n\tdefer reader.Close()\n\n\toffer, err := pub.CreateOffer(nil)\n\trequire.NoError(t, err)\n\n\terr = pub.SetLocalDescription(offer)\n\trequire.NoError(t, err)\n\n\tanswer, err := reader.CreateFullAnswer(&offer)\n\trequire.NoError(t, err)\n\n\terr = pub.SetRemoteDescription(*answer)\n\trequire.NoError(t, err)\n\n\terr = reader.WaitUntilConnected(10 * time.Second)\n\trequire.NoError(t, err)\n\n\tgo func() {\n\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\terr2 := videoTrack.WriteRTP(&rtp.Packet{\n\t\t\tHeader: rtp.Header{\n\t\t\t\tVersion:        2,\n\t\t\t\tMarker:         true,\n\t\t\t\tPayloadType:    111,\n\t\t\t\tSequenceNumber: 1123,\n\t\t\t\tTimestamp:      45343,\n\t\t\t\tSSRC:           563424,\n\t\t\t},\n\t\t\tPayload: []byte{5, 2},\n\t\t})\n\t\trequire.NoError(t, err2)\n\n\t\terr2 = audioTrack.WriteRTP(&rtp.Packet{\n\t\t\tHeader: rtp.Header{\n\t\t\t\tVersion:        2,\n\t\t\t\tMarker:         true,\n\t\t\t\tPayloadType:    111,\n\t\t\t\tSequenceNumber: 1123,\n\t\t\t\tTimestamp:      45343,\n\t\t\t\tSSRC:           563424,\n\t\t\t},\n\t\t\tPayload: []byte{5, 2},\n\t\t})\n\t\trequire.NoError(t, err2)\n\t}()\n\n\terr = reader.GatherIncomingTracks(2 * time.Second)\n\trequire.NoError(t, err)\n\n\tcodecs := gatherCodecs(reader.IncomingTracks())\n\n\tsort.Slice(codecs, func(i, j int) bool {\n\t\treturn codecs[i].PayloadType < codecs[j].PayloadType\n\t})\n\n\trequire.Equal(t, []webrtc.RTPCodecParameters{\n\t\t{\n\t\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\t\tMimeType:     webrtc.MimeTypeVP8,\n\t\t\t\tClockRate:    90000,\n\t\t\t\tRTCPFeedback: codecs[0].RTCPFeedback,\n\t\t\t},\n\t\t\tPayloadType: 96,\n\t\t},\n\t\t{\n\t\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\t\tMimeType:     webrtc.MimeTypeOpus,\n\t\t\t\tClockRate:    48000,\n\t\t\t\tChannels:     2,\n\t\t\t\tSDPFmtpLine:  \"minptime=10;useinbandfec=1\",\n\t\t\t\tRTCPFeedback: codecs[1].RTCPFeedback,\n\t\t\t},\n\t\t\tPayloadType: 111,\n\t\t},\n\t}, codecs)\n\n\treader.StartReading()\n\n\tpkts, _, err := videoSender.ReadRTCP()\n\trequire.NoError(t, err)\n\trequire.Equal(t, []rtcp.Packet{\n\t\t&rtcp.ReceiverReport{\n\t\t\tSSRC: pkts[0].(*rtcp.ReceiverReport).SSRC,\n\t\t\tReports: []rtcp.ReceptionReport{{\n\t\t\t\tSSRC:               uint32(videoSender.GetParameters().Encodings[0].SSRC),\n\t\t\t\tLastSequenceNumber: pkts[0].(*rtcp.ReceiverReport).Reports[0].LastSequenceNumber,\n\t\t\t\tLastSenderReport:   pkts[0].(*rtcp.ReceiverReport).Reports[0].LastSenderReport,\n\t\t\t\tDelay:              pkts[0].(*rtcp.ReceiverReport).Reports[0].Delay,\n\t\t\t}},\n\t\t\tProfileExtensions: []byte{},\n\t\t},\n\t}, pkts)\n\n\tpkts, _, err = audioSender.ReadRTCP()\n\trequire.NoError(t, err)\n\trequire.Equal(t, []rtcp.Packet{\n\t\t&rtcp.ReceiverReport{\n\t\t\tSSRC: pkts[0].(*rtcp.ReceiverReport).SSRC,\n\t\t\tReports: []rtcp.ReceptionReport{{\n\t\t\t\tSSRC:               uint32(audioSender.GetParameters().Encodings[0].SSRC),\n\t\t\t\tLastSequenceNumber: pkts[0].(*rtcp.ReceiverReport).Reports[0].LastSequenceNumber,\n\t\t\t\tLastSenderReport:   pkts[0].(*rtcp.ReceiverReport).Reports[0].LastSenderReport,\n\t\t\t\tDelay:              pkts[0].(*rtcp.ReceiverReport).Reports[0].Delay,\n\t\t\t}},\n\t\t\tProfileExtensions: []byte{},\n\t\t},\n\t}, pkts)\n}\n\nfunc TestPeerConnectionReadSimulcast(t *testing.T) {\n\tpub, err := webrtc.NewPeerConnection(webrtc.Configuration{})\n\trequire.NoError(t, err)\n\tdefer pub.Close() //nolint:errcheck\n\n\tvideoTrackL, err := webrtc.NewTrackLocalStaticRTP(\n\t\twebrtc.RTPCodecCapability{\n\t\t\tMimeType:  webrtc.MimeTypeVP8,\n\t\t\tClockRate: 90000,\n\t\t},\n\t\t\"video\", \"publisher\",\n\t\twebrtc.WithRTPStreamID(\"l\"),\n\t)\n\trequire.NoError(t, err)\n\n\tvideoTrackM, err := webrtc.NewTrackLocalStaticRTP(\n\t\twebrtc.RTPCodecCapability{\n\t\t\tMimeType:  webrtc.MimeTypeVP8,\n\t\t\tClockRate: 90000,\n\t\t},\n\t\t\"video\", \"publisher\",\n\t\twebrtc.WithRTPStreamID(\"m\"),\n\t)\n\trequire.NoError(t, err)\n\n\tvideoTrackH, err := webrtc.NewTrackLocalStaticRTP(\n\t\twebrtc.RTPCodecCapability{\n\t\t\tMimeType:  webrtc.MimeTypeVP8,\n\t\t\tClockRate: 90000,\n\t\t},\n\t\t\"video\", \"publisher\",\n\t\twebrtc.WithRTPStreamID(\"h\"),\n\t)\n\trequire.NoError(t, err)\n\n\ttransceiver, err := pub.AddTransceiverFromTrack(videoTrackL, webrtc.RTPTransceiverInit{\n\t\tDirection: webrtc.RTPTransceiverDirectionSendonly,\n\t\tSendEncodings: []webrtc.RTPEncodingParameters{\n\t\t\t{RTPCodingParameters: webrtc.RTPCodingParameters{RID: \"l\"}},\n\t\t\t{RTPCodingParameters: webrtc.RTPCodingParameters{RID: \"m\"}},\n\t\t\t{RTPCodingParameters: webrtc.RTPCodingParameters{RID: \"h\"}},\n\t\t},\n\t})\n\trequire.NoError(t, err)\n\n\terr = transceiver.Sender().AddEncoding(videoTrackM)\n\trequire.NoError(t, err)\n\n\terr = transceiver.Sender().AddEncoding(videoTrackH)\n\trequire.NoError(t, err)\n\n\treader := &PeerConnection{\n\t\tLocalRandomUDP:    true,\n\t\tIPsFromInterfaces: true,\n\t\tPublish:           false,\n\t\tLog:               test.NilLogger,\n\t}\n\terr = reader.Start()\n\trequire.NoError(t, err)\n\tdefer reader.Close()\n\n\toffer, err := pub.CreateOffer(nil)\n\trequire.NoError(t, err)\n\n\terr = pub.SetLocalDescription(offer)\n\trequire.NoError(t, err)\n\n\tanswer, err := reader.CreateFullAnswer(&offer)\n\trequire.NoError(t, err)\n\n\terr = pub.SetRemoteDescription(*answer)\n\trequire.NoError(t, err)\n\n\terr = reader.WaitUntilConnected(10 * time.Second)\n\trequire.NoError(t, err)\n\n\tvar midExtID, ridExtID uint8\n\tfor _, ext := range transceiver.Sender().GetParameters().HeaderExtensions {\n\t\tswitch ext.URI {\n\t\tcase sdp.SDESMidURI:\n\t\t\tmidExtID = uint8(ext.ID)\n\t\tcase sdp.SDESRTPStreamIDURI:\n\t\t\tridExtID = uint8(ext.ID)\n\t\t}\n\t}\n\trequire.NotZero(t, midExtID)\n\trequire.NotZero(t, ridExtID)\n\n\tmid := transceiver.Mid()\n\n\tgo func() {\n\t\ttime.Sleep(200 * time.Millisecond)\n\n\t\tlayers := []struct {\n\t\t\ttrack  *webrtc.TrackLocalStaticRTP\n\t\t\trid    string\n\t\t\tssrc   uint32\n\t\t\tseqNum uint16\n\t\t}{\n\t\t\t{videoTrackL, \"l\", 100001, 1000},\n\t\t\t{videoTrackM, \"m\", 100002, 2000},\n\t\t\t{videoTrackH, \"h\", 100003, 3000},\n\t\t}\n\n\t\tfor i, layer := range layers {\n\t\t\tpkt := &rtp.Packet{\n\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\tVersion:        2,\n\t\t\t\t\tMarker:         true,\n\t\t\t\t\tPayloadType:    96,\n\t\t\t\t\tSequenceNumber: layer.seqNum,\n\t\t\t\t\tTimestamp:      45343,\n\t\t\t\t\tSSRC:           layer.ssrc,\n\t\t\t\t},\n\t\t\t\tPayload: []byte{5, 2},\n\t\t\t}\n\n\t\t\tpkt.ExtensionProfile = 0xBEDE\n\t\t\trequire.NoError(t, pkt.SetExtension(midExtID, []byte(mid)))\n\t\t\trequire.NoError(t, pkt.SetExtension(ridExtID, []byte(layer.rid)))\n\n\t\t\terr2 := layer.track.WriteRTP(pkt)\n\t\t\tif err2 != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tlayers[i].seqNum++\n\t\t}\n\t}()\n\n\terr = reader.GatherIncomingTracks(5 * time.Second)\n\trequire.NoError(t, err)\n\n\ttracks := reader.IncomingTracks()\n\tcodecs := gatherCodecs(tracks)\n\n\trequire.Equal(t, 3, len(codecs))\n\n\tfor _, codec := range codecs {\n\t\trequire.Equal(t, webrtc.RTPCodecCapability{\n\t\t\tMimeType:     webrtc.MimeTypeVP8,\n\t\t\tClockRate:    90000,\n\t\t\tRTCPFeedback: codec.RTCPFeedback,\n\t\t}, codec.RTPCodecCapability)\n\t}\n\n\trids := make([]string, len(tracks))\n\tfor i, track := range tracks {\n\t\trids[i] = track.track.RID()\n\t}\n\tsort.Strings(rids)\n\trequire.Equal(t, []string{\"h\", \"l\", \"m\"}, rids)\n}\n\nfunc TestPeerConnectionPublishRead(t *testing.T) {\n\tpc1 := &PeerConnection{\n\t\tLocalRandomUDP:    true,\n\t\tIPsFromInterfaces: true,\n\t\tPublish:           false,\n\t\tLog:               test.NilLogger,\n\t}\n\terr := pc1.Start()\n\trequire.NoError(t, err)\n\tdefer pc1.Close()\n\n\tpc2 := &PeerConnection{\n\t\tLocalRandomUDP:    true,\n\t\tIPsFromInterfaces: true,\n\t\tPublish:           true,\n\t\tOutgoingTracks: []*OutgoingTrack{\n\t\t\t{\n\t\t\t\tCaps: webrtc.RTPCodecCapability{\n\t\t\t\t\tMimeType:  webrtc.MimeTypeH264,\n\t\t\t\t\tClockRate: 90000,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tCaps: webrtc.RTPCodecCapability{\n\t\t\t\t\tMimeType:  webrtc.MimeTypeOpus,\n\t\t\t\t\tClockRate: 48000,\n\t\t\t\t\tChannels:  2,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tLog: test.NilLogger,\n\t}\n\terr = pc2.Start()\n\trequire.NoError(t, err)\n\tdefer pc2.Close()\n\n\toffer, err := pc1.CreatePartialOffer()\n\trequire.NoError(t, err)\n\n\tanswer, err := pc2.CreateFullAnswer(offer)\n\trequire.NoError(t, err)\n\n\terr = pc1.SetAnswer(answer)\n\trequire.NoError(t, err)\n\n\terr = pc1.WaitUntilConnected(10 * time.Second)\n\trequire.NoError(t, err)\n\n\terr = pc2.WaitUntilConnected(10 * time.Second)\n\trequire.NoError(t, err)\n\n\tfor _, track := range pc2.OutgoingTracks {\n\t\terr = track.WriteRTP(&rtp.Packet{\n\t\t\tHeader: rtp.Header{\n\t\t\t\tVersion:        2,\n\t\t\t\tMarker:         true,\n\t\t\t\tPayloadType:    111,\n\t\t\t\tSequenceNumber: 1123,\n\t\t\t\tTimestamp:      45343,\n\t\t\t\tSSRC:           563424,\n\t\t\t},\n\t\t\tPayload: []byte{5, 2},\n\t\t})\n\t\trequire.NoError(t, err)\n\t}\n\n\terr = pc1.GatherIncomingTracks(2 * time.Second)\n\trequire.NoError(t, err)\n\n\tcodecs := gatherCodecs(pc1.IncomingTracks())\n\n\tsort.Slice(codecs, func(i, j int) bool {\n\t\treturn codecs[i].PayloadType < codecs[j].PayloadType\n\t})\n\n\trequire.Equal(t, []webrtc.RTPCodecParameters{\n\t\t{\n\t\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\t\tMimeType:     webrtc.MimeTypeH264,\n\t\t\t\tClockRate:    90000,\n\t\t\t\tSDPFmtpLine:  \"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f\",\n\t\t\t\tRTCPFeedback: codecs[0].RTCPFeedback,\n\t\t\t},\n\t\t\tPayloadType: 105,\n\t\t},\n\t\t{\n\t\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\t\tMimeType:     webrtc.MimeTypeOpus,\n\t\t\t\tClockRate:    48000,\n\t\t\t\tChannels:     2,\n\t\t\t\tSDPFmtpLine:  \"minptime=10;useinbandfec=1;stereo=1;sprop-stereo=1\",\n\t\t\t\tRTCPFeedback: codecs[1].RTCPFeedback,\n\t\t\t},\n\t\t\tPayloadType: 111,\n\t\t},\n\t}, codecs)\n}\n\n// test that an audio codec is present regardless of the fact that an audio track is.\nfunc TestPeerConnectionFallbackCodecs(t *testing.T) {\n\tpc1 := &PeerConnection{\n\t\tLocalRandomUDP:    true,\n\t\tIPsFromInterfaces: true,\n\t\tPublish:           false,\n\t\tLog:               test.NilLogger,\n\t}\n\terr := pc1.Start()\n\trequire.NoError(t, err)\n\tdefer pc1.Close()\n\n\tpc2 := &PeerConnection{\n\t\tLocalRandomUDP:    true,\n\t\tIPsFromInterfaces: true,\n\t\tPublish:           true,\n\t\tOutgoingTracks: []*OutgoingTrack{{\n\t\t\tCaps: webrtc.RTPCodecCapability{\n\t\t\t\tMimeType:  webrtc.MimeTypeAV1,\n\t\t\t\tClockRate: 90000,\n\t\t\t},\n\t\t}},\n\t\tLog: test.NilLogger,\n\t}\n\terr = pc2.Start()\n\trequire.NoError(t, err)\n\tdefer pc2.Close()\n\n\toffer, err := pc1.CreatePartialOffer()\n\trequire.NoError(t, err)\n\n\tanswer, err := pc2.CreateFullAnswer(offer)\n\trequire.NoError(t, err)\n\n\tvar s sdp.SessionDescription\n\terr = s.Unmarshal([]byte(answer.SDP))\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, []*sdp.MediaDescription{\n\t\t{\n\t\t\tMediaName: sdp.MediaName{\n\t\t\t\tMedia:   \"video\",\n\t\t\t\tPort:    sdp.RangedPort{Value: 9},\n\t\t\t\tProtos:  []string{\"UDP\", \"TLS\", \"RTP\", \"SAVPF\"},\n\t\t\t\tFormats: []string{\"97\"},\n\t\t\t},\n\t\t\tConnectionInformation: s.MediaDescriptions[0].ConnectionInformation,\n\t\t\tAttributes:            s.MediaDescriptions[0].Attributes,\n\t\t},\n\t\t{\n\t\t\tMediaName: sdp.MediaName{\n\t\t\t\tMedia:   \"audio\",\n\t\t\t\tPort:    sdp.RangedPort{Value: 9},\n\t\t\t\tProtos:  []string{\"UDP\", \"TLS\", \"RTP\", \"SAVPF\"},\n\t\t\t\tFormats: []string{\"0\"},\n\t\t\t},\n\t\t\tConnectionInformation: s.MediaDescriptions[1].ConnectionInformation,\n\t\t\tAttributes:            s.MediaDescriptions[1].Attributes,\n\t\t},\n\t}, s.MediaDescriptions)\n}\n\nfunc TestPeerConnectionPublishDataChannel(t *testing.T) {\n\tpc1, err := webrtc.NewPeerConnection(webrtc.Configuration{})\n\trequire.NoError(t, err)\n\tdefer pc1.Close() //nolint:errcheck\n\n\t_, err = pc1.CreateDataChannel(\"\", nil)\n\trequire.NoError(t, err)\n\n\tdataChanCreated := make(chan struct{})\n\tdataReceived := make(chan struct{})\n\n\tpc1.OnDataChannel(func(dc *webrtc.DataChannel) {\n\t\tclose(dataChanCreated)\n\n\t\tdc.OnMessage(func(msg webrtc.DataChannelMessage) {\n\t\t\trequire.Equal(t, []byte(\"test data\"), msg.Data)\n\t\t\tclose(dataReceived)\n\t\t})\n\t})\n\n\toffer, err := pc1.CreateOffer(nil)\n\trequire.NoError(t, err)\n\n\terr = pc1.SetLocalDescription(offer)\n\trequire.NoError(t, err)\n\n\tpc2 := &PeerConnection{\n\t\tLocalRandomUDP:    true,\n\t\tIPsFromInterfaces: true,\n\t\tPublish:           true,\n\t\tOutgoingDataChannels: []*OutgoingDataChannel{\n\t\t\t{\n\t\t\t\tLabel: \"test-channel\",\n\t\t\t},\n\t\t},\n\t\tLog: test.NilLogger,\n\t}\n\terr = pc2.Start()\n\trequire.NoError(t, err)\n\tdefer pc2.Close()\n\n\tanswer, err := pc2.CreateFullAnswer(&offer)\n\trequire.NoError(t, err)\n\n\terr = pc1.SetRemoteDescription(*answer)\n\trequire.NoError(t, err)\n\n\terr = pc2.WaitUntilConnected(10 * time.Second)\n\trequire.NoError(t, err)\n\n\t<-dataChanCreated\n\n\tpc2.OutgoingDataChannels[0].Write([]byte(\"test data\"))\n\n\t<-dataReceived\n}\n"
  },
  {
    "path": "internal/protocols/webrtc/stats.go",
    "content": "package webrtc\n\n// Stats are WebRTC statistics.\ntype Stats struct {\n\tBytesReceived       uint64\n\tBytesSent           uint64\n\tRTPPacketsReceived  uint64\n\tRTPPacketsSent      uint64\n\tRTPPacketsLost      uint64\n\tRTPPacketsJitter    float64\n\tRTCPPacketsReceived uint64\n\tRTCPPacketsSent     uint64\n}\n"
  },
  {
    "path": "internal/protocols/webrtc/stats_interceptor.go",
    "content": "package webrtc\n\nimport (\n\t\"sync/atomic\"\n\n\t\"github.com/pion/interceptor\"\n\t\"github.com/pion/rtcp\"\n)\n\ntype statsInterceptor struct {\n\trtcpPacketsSent     *uint64\n\trtcpPacketsReceived *uint64\n}\n\nfunc (*statsInterceptor) Close() error {\n\treturn nil\n}\n\nfunc (s *statsInterceptor) BindRTCPReader(reader interceptor.RTCPReader) interceptor.RTCPReader {\n\treturn interceptor.RTCPReaderFunc(func(bytes []byte,\n\t\tattributes interceptor.Attributes,\n\t) (int, interceptor.Attributes, error) {\n\t\tn, attrs, err := reader.Read(bytes, attributes)\n\n\t\tpkts, err2 := attrs.GetRTCPPackets(bytes)\n\t\tif err2 == nil {\n\t\t\tatomic.AddUint64(s.rtcpPacketsReceived, uint64(len(pkts)))\n\t\t}\n\n\t\treturn n, attrs, err\n\t})\n}\n\nfunc (s *statsInterceptor) BindRTCPWriter(writer interceptor.RTCPWriter) interceptor.RTCPWriter {\n\treturn interceptor.RTCPWriterFunc(func(pkts []rtcp.Packet, attributes interceptor.Attributes) (int, error) {\n\t\tatomic.AddUint64(s.rtcpPacketsSent, uint64(len(pkts)))\n\t\treturn writer.Write(pkts, attributes)\n\t})\n}\n\nfunc (s *statsInterceptor) BindLocalStream(_ *interceptor.StreamInfo,\n\twriter interceptor.RTPWriter,\n) interceptor.RTPWriter {\n\treturn writer\n}\n\nfunc (*statsInterceptor) UnbindLocalStream(_ *interceptor.StreamInfo) {}\n\nfunc (s *statsInterceptor) BindRemoteStream(_ *interceptor.StreamInfo,\n\treader interceptor.RTPReader,\n) interceptor.RTPReader {\n\treturn reader\n}\n\nfunc (*statsInterceptor) UnbindRemoteStream(_ *interceptor.StreamInfo) {}\n\ntype statsInterceptorFactory struct {\n\tonCreate func(s *statsInterceptor)\n}\n\nfunc (f *statsInterceptorFactory) NewInterceptor(_ string) (interceptor.Interceptor, error) {\n\ts := &statsInterceptor{\n\t\trtcpPacketsSent:     new(uint64),\n\t\trtcpPacketsReceived: new(uint64),\n\t}\n\n\tf.onCreate(s)\n\n\treturn s, nil\n}\n"
  },
  {
    "path": "internal/protocols/webrtc/tcp_mux_wrapper.go",
    "content": "package webrtc\n\nimport (\n\t\"net\"\n\n\t\"github.com/pion/ice/v4\"\n)\n\n// TCPMuxWrapper is a wrapper around ice.TCPMux.\ntype TCPMuxWrapper struct {\n\tMux ice.TCPMux\n\tLn  net.Listener\n}\n"
  },
  {
    "path": "internal/protocols/webrtc/to_stream.go",
    "content": "package webrtc\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/rtptime\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n\t\"github.com/pion/rtp\"\n\t\"github.com/pion/webrtc/v4\"\n)\n\ntype ntpState int\n\nconst (\n\tntpStateInitial ntpState = iota\n\tntpStateReplace\n\tntpStateAvailable\n)\n\nvar errNoSupportedCodecsTo = errors.New(\n\t\"the stream doesn't contain any supported codec, which are currently \" +\n\t\t\"AV1, VP9, VP8, H265, H264, Opus, G722, G711, LPCM\")\n\n// ToStream maps a WebRTC connection to a MediaMTX stream.\nfunc ToStream(\n\tpc *PeerConnection,\n\tpathConf *conf.Path,\n\tsubStream **stream.SubStream,\n\tlog logger.Writer,\n) ([]*description.Media, error) {\n\tvar medias []*description.Media //nolint:prealloc\n\ttimeDecoder := &rtptime.GlobalDecoder{}\n\ttimeDecoder.Initialize()\n\n\tfor _, track := range pc.incomingTracks {\n\t\tvar typ description.MediaType\n\t\tvar forma format.Format\n\n\t\tswitch strings.ToLower(track.track.Codec().MimeType) {\n\t\tcase strings.ToLower(webrtc.MimeTypeAV1):\n\t\t\ttyp = description.MediaTypeVideo\n\t\t\tforma = &format.AV1{\n\t\t\t\tPayloadTyp: 96,\n\t\t\t}\n\n\t\tcase strings.ToLower(webrtc.MimeTypeVP9):\n\t\t\ttyp = description.MediaTypeVideo\n\t\t\tforma = &format.VP9{\n\t\t\t\tPayloadTyp: 96,\n\t\t\t}\n\n\t\tcase strings.ToLower(webrtc.MimeTypeVP8):\n\t\t\ttyp = description.MediaTypeVideo\n\t\t\tforma = &format.VP8{\n\t\t\t\tPayloadTyp: 96,\n\t\t\t}\n\n\t\tcase strings.ToLower(webrtc.MimeTypeH265):\n\t\t\ttyp = description.MediaTypeVideo\n\t\t\tforma = &format.H265{\n\t\t\t\tPayloadTyp: 96,\n\t\t\t}\n\n\t\tcase strings.ToLower(webrtc.MimeTypeH264):\n\t\t\ttyp = description.MediaTypeVideo\n\t\t\tforma = &format.H264{\n\t\t\t\tPayloadTyp:        96,\n\t\t\t\tPacketizationMode: 1,\n\t\t\t}\n\n\t\tcase strings.ToLower(mimeTypeMultiopus):\n\t\t\ttyp = description.MediaTypeAudio\n\t\t\tforma = &format.Opus{\n\t\t\t\tPayloadTyp:   96,\n\t\t\t\tChannelCount: int(track.track.Codec().Channels),\n\t\t\t}\n\n\t\tcase strings.ToLower(webrtc.MimeTypeOpus):\n\t\t\ttyp = description.MediaTypeAudio\n\t\t\tforma = &format.Opus{\n\t\t\t\tPayloadTyp: 96,\n\t\t\t\tChannelCount: func() int {\n\t\t\t\t\tif strings.Contains(track.track.Codec().SDPFmtpLine, \"stereo=1\") {\n\t\t\t\t\t\treturn 2\n\t\t\t\t\t}\n\t\t\t\t\treturn 1\n\t\t\t\t}(),\n\t\t\t}\n\n\t\tcase strings.ToLower(webrtc.MimeTypeG722):\n\t\t\ttyp = description.MediaTypeAudio\n\t\t\tforma = &format.G722{}\n\n\t\tcase strings.ToLower(webrtc.MimeTypePCMU):\n\t\t\tchannels := int(track.track.Codec().Channels)\n\t\t\tif channels == 0 {\n\t\t\t\tchannels = 1\n\t\t\t}\n\n\t\t\ttyp = description.MediaTypeAudio\n\t\t\tforma = &format.G711{\n\t\t\t\tPayloadTyp: func() uint8 {\n\t\t\t\t\tif channels > 1 {\n\t\t\t\t\t\treturn 96\n\t\t\t\t\t}\n\t\t\t\t\treturn 0\n\t\t\t\t}(),\n\t\t\t\tMULaw:        true,\n\t\t\t\tSampleRate:   8000,\n\t\t\t\tChannelCount: channels,\n\t\t\t}\n\n\t\tcase strings.ToLower(webrtc.MimeTypePCMA):\n\t\t\tchannels := int(track.track.Codec().Channels)\n\t\t\tif channels == 0 {\n\t\t\t\tchannels = 1\n\t\t\t}\n\n\t\t\ttyp = description.MediaTypeAudio\n\t\t\tforma = &format.G711{\n\t\t\t\tPayloadTyp: func() uint8 {\n\t\t\t\t\tif channels > 1 {\n\t\t\t\t\t\treturn 96\n\t\t\t\t\t}\n\t\t\t\t\treturn 8\n\t\t\t\t}(),\n\t\t\t\tMULaw:        false,\n\t\t\t\tSampleRate:   8000,\n\t\t\t\tChannelCount: channels,\n\t\t\t}\n\n\t\tcase strings.ToLower(mimeTypeL16):\n\t\t\ttyp = description.MediaTypeAudio\n\t\t\tforma = &format.LPCM{\n\t\t\t\tPayloadTyp:   96,\n\t\t\t\tBitDepth:     16,\n\t\t\t\tSampleRate:   int(track.track.Codec().ClockRate),\n\t\t\t\tChannelCount: int(track.track.Codec().Channels),\n\t\t\t}\n\n\t\tdefault:\n\t\t\treturn nil, fmt.Errorf(\"unsupported codec: %+v\", track.track.Codec().RTPCodecCapability)\n\t\t}\n\n\t\tmedi := &description.Media{\n\t\t\tType:    typ,\n\t\t\tFormats: []format.Format{forma},\n\t\t}\n\n\t\tvar ntpStat ntpState\n\n\t\tif !pathConf.UseAbsoluteTimestamp {\n\t\t\tntpStat = ntpStateReplace\n\t\t}\n\n\t\thandleNTP := func(pkt *rtp.Packet) (time.Time, bool) {\n\t\t\tswitch ntpStat {\n\t\t\tcase ntpStateReplace:\n\t\t\t\treturn time.Time{}, true\n\n\t\t\tcase ntpStateInitial:\n\t\t\t\tntp, avail := track.PacketNTP(pkt)\n\t\t\t\tif !avail {\n\t\t\t\t\tlog.Log(logger.Warn, \"received RTP packet without absolute time, skipping it\")\n\t\t\t\t\treturn time.Time{}, false\n\t\t\t\t}\n\n\t\t\t\tntpStat = ntpStateAvailable\n\t\t\t\treturn ntp, true\n\n\t\t\tdefault: // ntpStateAvailable\n\t\t\t\tntp, avail := track.PacketNTP(pkt)\n\t\t\t\tif !avail {\n\t\t\t\t\tpanic(\"should not happen\")\n\t\t\t\t}\n\n\t\t\t\treturn ntp, true\n\t\t\t}\n\t\t}\n\n\t\ttrack.OnPacketRTP = func(pkt *rtp.Packet) {\n\t\t\tpkt.PayloadType = forma.PayloadType()\n\n\t\t\tpts, ok := timeDecoder.Decode(track, pkt)\n\t\t\tif !ok {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tntp, ok := handleNTP(pkt)\n\t\t\tif !ok {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t(*subStream).WriteUnit(medi, forma, &unit.Unit{\n\t\t\t\tPTS:        pts,\n\t\t\t\tNTP:        ntp,\n\t\t\t\tRTPPackets: []*rtp.Packet{pkt},\n\t\t\t})\n\t\t}\n\n\t\tmedias = append(medias, medi)\n\t}\n\n\tif len(medias) == 0 {\n\t\treturn nil, errNoSupportedCodecsTo\n\t}\n\n\treturn medias, nil\n}\n"
  },
  {
    "path": "internal/protocols/webrtc/to_stream_test.go",
    "content": "package webrtc\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/pion/rtp\"\n\t\"github.com/pion/webrtc/v4\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestToStreamNoSupportedCodecs(t *testing.T) {\n\tpc := &PeerConnection{}\n\t_, err := ToStream(pc, &conf.Path{}, nil, nil)\n\trequire.Equal(t, errNoSupportedCodecsTo, err)\n}\n\n// this is impossible to test since unsupported tracks cause an error\n// as they are not included inside incomingVideoCodecs or incomingAudioCodecs\n// func TestToStreamSkipUnsupportedTracks(t *testing.T)\n\nvar toFromStreamCases = []struct {\n\tname       string\n\tin         format.Format\n\twebrtcCaps webrtc.RTPCodecCapability\n\tout        format.Format\n}{\n\t{\n\t\t\"av1\",\n\t\t&format.AV1{\n\t\t\tPayloadTyp: 96,\n\t\t},\n\t\twebrtc.RTPCodecCapability{\n\t\t\tMimeType:  \"video/AV1\",\n\t\t\tClockRate: 90000,\n\t\t},\n\t\t&format.AV1{\n\t\t\tPayloadTyp: 96,\n\t\t},\n\t},\n\t{\n\t\t\"vp9\",\n\t\t&format.VP9{\n\t\t\tPayloadTyp: 96,\n\t\t},\n\t\twebrtc.RTPCodecCapability{\n\t\t\tMimeType:    \"video/VP9\",\n\t\t\tClockRate:   90000,\n\t\t\tSDPFmtpLine: \"profile-id=0\",\n\t\t},\n\t\t&format.VP9{\n\t\t\tPayloadTyp: 96,\n\t\t},\n\t},\n\t{\n\t\t\"vp8\",\n\t\t&format.VP8{\n\t\t\tPayloadTyp: 96,\n\t\t},\n\t\twebrtc.RTPCodecCapability{\n\t\t\tMimeType:  \"video/VP8\",\n\t\t\tClockRate: 90000,\n\t\t},\n\t\t&format.VP8{\n\t\t\tPayloadTyp: 96,\n\t\t},\n\t},\n\t{\n\t\t\"h265\",\n\t\t&format.H265{\n\t\t\tPayloadTyp: 96,\n\t\t},\n\t\twebrtc.RTPCodecCapability{\n\t\t\tMimeType:    \"video/H265\",\n\t\t\tClockRate:   90000,\n\t\t\tSDPFmtpLine: \"level-id=93;profile-id=1;tier-flag=0;tx-mode=SRST\",\n\t\t},\n\t\t&format.H265{\n\t\t\tPayloadTyp: 96,\n\t\t},\n\t},\n\t{\n\t\t\"h264\",\n\t\ttest.FormatH264,\n\t\twebrtc.RTPCodecCapability{\n\t\t\tMimeType:    \"video/H264\",\n\t\t\tClockRate:   90000,\n\t\t\tSDPFmtpLine: \"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\",\n\t\t},\n\t\t&format.H264{\n\t\t\tPayloadTyp:        96,\n\t\t\tPacketizationMode: 1,\n\t\t},\n\t},\n\t{\n\t\t\"opus multichannel\",\n\t\t&format.Opus{\n\t\t\tPayloadTyp:   96,\n\t\t\tChannelCount: 6,\n\t\t},\n\t\twebrtc.RTPCodecCapability{\n\t\t\tMimeType:    \"audio/multiopus\",\n\t\t\tClockRate:   48000,\n\t\t\tChannels:    6,\n\t\t\tSDPFmtpLine: \"channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2\",\n\t\t},\n\t\t&format.Opus{\n\t\t\tPayloadTyp:   96,\n\t\t\tChannelCount: 6,\n\t\t},\n\t},\n\t{\n\t\t\"opus stereo\",\n\t\t&format.Opus{\n\t\t\tPayloadTyp:   96,\n\t\t\tChannelCount: 2,\n\t\t},\n\t\twebrtc.RTPCodecCapability{\n\t\t\tMimeType:    \"audio/opus\",\n\t\t\tClockRate:   48000,\n\t\t\tChannels:    2,\n\t\t\tSDPFmtpLine: \"minptime=10;useinbandfec=1;stereo=1;sprop-stereo=1\",\n\t\t},\n\t\t&format.Opus{\n\t\t\tPayloadTyp:   96,\n\t\t\tChannelCount: 2,\n\t\t},\n\t},\n\t{\n\t\t\"opus mono\",\n\t\t&format.Opus{\n\t\t\tPayloadTyp:   96,\n\t\t\tChannelCount: 1,\n\t\t},\n\t\twebrtc.RTPCodecCapability{\n\t\t\tMimeType:    \"audio/opus\",\n\t\t\tClockRate:   48000,\n\t\t\tChannels:    2,\n\t\t\tSDPFmtpLine: \"minptime=10;useinbandfec=1\",\n\t\t},\n\t\t&format.Opus{\n\t\t\tPayloadTyp:   96,\n\t\t\tChannelCount: 1,\n\t\t},\n\t},\n\t{\n\t\t\"g722\",\n\t\t&format.G722{},\n\t\twebrtc.RTPCodecCapability{\n\t\t\tMimeType:  \"audio/G722\",\n\t\t\tClockRate: 8000,\n\t\t},\n\t\t&format.G722{},\n\t},\n\t{\n\t\t\"g711 pcma 8khz mono\",\n\t\t&format.G711{\n\t\t\tPayloadTyp:   8,\n\t\t\tSampleRate:   8000,\n\t\t\tChannelCount: 1,\n\t\t},\n\t\twebrtc.RTPCodecCapability{\n\t\t\tMimeType:  \"audio/PCMA\",\n\t\t\tClockRate: 8000,\n\t\t},\n\t\t&format.G711{\n\t\t\tPayloadTyp:   8,\n\t\t\tSampleRate:   8000,\n\t\t\tChannelCount: 1,\n\t\t},\n\t},\n\t{\n\t\t\"g711 pcmu 8khz mono\",\n\t\t&format.G711{\n\t\t\tMULaw:        true,\n\t\t\tPayloadTyp:   0,\n\t\t\tSampleRate:   8000,\n\t\t\tChannelCount: 1,\n\t\t},\n\t\twebrtc.RTPCodecCapability{\n\t\t\tMimeType:  \"audio/PCMU\",\n\t\t\tClockRate: 8000,\n\t\t},\n\t\t&format.G711{\n\t\t\tMULaw:        true,\n\t\t\tPayloadTyp:   0,\n\t\t\tSampleRate:   8000,\n\t\t\tChannelCount: 1,\n\t\t},\n\t},\n\t{\n\t\t\"g711 pcma 8khz stereo\",\n\t\t&format.G711{\n\t\t\tPayloadTyp:   96,\n\t\t\tSampleRate:   8000,\n\t\t\tChannelCount: 2,\n\t\t},\n\t\twebrtc.RTPCodecCapability{\n\t\t\tMimeType:  \"audio/PCMA\",\n\t\t\tClockRate: 8000,\n\t\t\tChannels:  2,\n\t\t},\n\t\t&format.G711{\n\t\t\tPayloadTyp:   96,\n\t\t\tSampleRate:   8000,\n\t\t\tChannelCount: 2,\n\t\t},\n\t},\n\t{\n\t\t\"g711 pcmu 8khz stereo\",\n\t\t&format.G711{\n\t\t\tMULaw:        true,\n\t\t\tPayloadTyp:   96,\n\t\t\tSampleRate:   8000,\n\t\t\tChannelCount: 2,\n\t\t},\n\t\twebrtc.RTPCodecCapability{\n\t\t\tMimeType:  \"audio/PCMU\",\n\t\t\tClockRate: 8000,\n\t\t\tChannels:  2,\n\t\t},\n\t\t&format.G711{\n\t\t\tMULaw:        true,\n\t\t\tPayloadTyp:   96,\n\t\t\tSampleRate:   8000,\n\t\t\tChannelCount: 2,\n\t\t},\n\t},\n\t{\n\t\t\"g711 pcma 16khz stereo\",\n\t\t&format.G711{\n\t\t\tPayloadTyp:   96,\n\t\t\tSampleRate:   16000,\n\t\t\tChannelCount: 2,\n\t\t},\n\t\twebrtc.RTPCodecCapability{\n\t\t\tMimeType:  \"audio/L16\",\n\t\t\tClockRate: 16000,\n\t\t\tChannels:  2,\n\t\t},\n\t\t&format.LPCM{\n\t\t\tPayloadTyp:   96,\n\t\t\tBitDepth:     16,\n\t\t\tSampleRate:   16000,\n\t\t\tChannelCount: 2,\n\t\t},\n\t},\n\t{\n\t\t\"g711 pcmu 16khz stereo\",\n\t\t&format.G711{\n\t\t\tMULaw:        true,\n\t\t\tPayloadTyp:   96,\n\t\t\tSampleRate:   16000,\n\t\t\tChannelCount: 2,\n\t\t},\n\t\twebrtc.RTPCodecCapability{\n\t\t\tMimeType:  \"audio/L16\",\n\t\t\tClockRate: 16000,\n\t\t\tChannels:  2,\n\t\t},\n\t\t&format.LPCM{\n\t\t\tPayloadTyp:   96,\n\t\t\tBitDepth:     16,\n\t\t\tSampleRate:   16000,\n\t\t\tChannelCount: 2,\n\t\t},\n\t},\n\t{\n\t\t\"l16 8khz stereo\",\n\t\t&format.LPCM{\n\t\t\tPayloadTyp:   96,\n\t\t\tBitDepth:     16,\n\t\t\tSampleRate:   8000,\n\t\t\tChannelCount: 2,\n\t\t},\n\t\twebrtc.RTPCodecCapability{\n\t\t\tMimeType:  \"audio/L16\",\n\t\t\tClockRate: 8000,\n\t\t\tChannels:  2,\n\t\t},\n\t\t&format.LPCM{\n\t\t\tPayloadTyp:   96,\n\t\t\tBitDepth:     16,\n\t\t\tSampleRate:   8000,\n\t\t\tChannelCount: 2,\n\t\t},\n\t},\n\t{\n\t\t\"l16 16khz stereo\",\n\t\t&format.LPCM{\n\t\t\tPayloadTyp:   96,\n\t\t\tBitDepth:     16,\n\t\t\tSampleRate:   16000,\n\t\t\tChannelCount: 2,\n\t\t},\n\t\twebrtc.RTPCodecCapability{\n\t\t\tMimeType:  \"audio/L16\",\n\t\t\tClockRate: 16000,\n\t\t\tChannels:  2,\n\t\t},\n\t\t&format.LPCM{\n\t\t\tPayloadTyp:   96,\n\t\t\tBitDepth:     16,\n\t\t\tSampleRate:   16000,\n\t\t\tChannelCount: 2,\n\t\t},\n\t},\n\t{\n\t\t\"l16 48khz stereo\",\n\t\t&format.LPCM{\n\t\t\tPayloadTyp:   96,\n\t\t\tBitDepth:     16,\n\t\t\tSampleRate:   48000,\n\t\t\tChannelCount: 2,\n\t\t},\n\t\twebrtc.RTPCodecCapability{\n\t\t\tMimeType:  \"audio/L16\",\n\t\t\tClockRate: 48000,\n\t\t\tChannels:  2,\n\t\t},\n\t\t&format.LPCM{\n\t\t\tPayloadTyp:   96,\n\t\t\tBitDepth:     16,\n\t\t\tSampleRate:   48000,\n\t\t\tChannelCount: 2,\n\t\t},\n\t},\n}\n\nfunc TestToStream(t *testing.T) {\n\tfor _, ca := range toFromStreamCases {\n\t\tt.Run(ca.name, func(t *testing.T) {\n\t\t\tpc1 := &PeerConnection{\n\t\t\t\tLocalRandomUDP:    true,\n\t\t\t\tIPsFromInterfaces: true,\n\t\t\t\tPublish:           true,\n\t\t\t\tOutgoingTracks: []*OutgoingTrack{{\n\t\t\t\t\tCaps: ca.webrtcCaps,\n\t\t\t\t}},\n\t\t\t\tLog: test.NilLogger,\n\t\t\t}\n\t\t\terr := pc1.Start()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer pc1.Close()\n\n\t\t\tpc2 := &PeerConnection{\n\t\t\t\tLocalRandomUDP:    true,\n\t\t\t\tIPsFromInterfaces: true,\n\t\t\t\tPublish:           false,\n\t\t\t\tLog:               test.NilLogger,\n\t\t\t}\n\t\t\terr = pc2.Start()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer pc2.Close()\n\n\t\t\toffer, err := pc1.CreatePartialOffer()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tanswer, err := pc2.CreateFullAnswer(offer)\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = pc1.SetAnswer(answer)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tgo func() {\n\t\t\t\tfor {\n\t\t\t\t\tselect {\n\t\t\t\t\tcase cnd := <-pc1.NewLocalCandidate():\n\t\t\t\t\t\terr2 := pc2.AddRemoteCandidate(cnd)\n\t\t\t\t\t\trequire.NoError(t, err2)\n\n\t\t\t\t\tcase <-pc1.Connected():\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\terr = pc1.WaitUntilConnected(10 * time.Second)\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = pc2.WaitUntilConnected(10 * time.Second)\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = pc1.OutgoingTracks[0].WriteRTP(&rtp.Packet{\n\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\tVersion:        2,\n\t\t\t\t\tMarker:         true,\n\t\t\t\t\tPayloadType:    111,\n\t\t\t\t\tSequenceNumber: 1123,\n\t\t\t\t\tTimestamp:      45343,\n\t\t\t\t\tSSRC:           563424,\n\t\t\t\t},\n\t\t\t\tPayload: []byte{5, 2},\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = pc2.GatherIncomingTracks(2 * time.Second)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tvar subStream *stream.SubStream\n\t\t\tmedias, err := ToStream(pc2, &conf.Path{}, &subStream, nil)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, ca.out, medias[0].Formats[0])\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/protocols/websocket/serverconn.go",
    "content": "// Package websocket provides WebSocket connectivity.\npackage websocket\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/gorilla/websocket\"\n)\n\nvar (\n\tpingInterval = 30 * time.Second\n\tpingTimeout  = 5 * time.Second\n\twriteTimeout = 2 * time.Second\n)\n\nvar upgrader = websocket.Upgrader{\n\tCheckOrigin: func(_ *http.Request) bool {\n\t\treturn true\n\t},\n}\n\n// ServerConn is a server-side WebSocket connection with\n// automatic, periodic ping-pong\ntype ServerConn struct {\n\twc *websocket.Conn\n\n\t// in\n\tterminate chan struct{}\n\twrite     chan []byte\n\n\t// out\n\twriteErr chan error\n}\n\n// NewServerConn allocates a ServerConn.\nfunc NewServerConn(w http.ResponseWriter, req *http.Request) (*ServerConn, error) {\n\twc, err := upgrader.Upgrade(w, req, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tc := &ServerConn{\n\t\twc:        wc,\n\t\tterminate: make(chan struct{}),\n\t\twrite:     make(chan []byte),\n\t\twriteErr:  make(chan error),\n\t}\n\n\tgo c.run()\n\n\treturn c, nil\n}\n\n// Close closes a ServerConn.\nfunc (c *ServerConn) Close() {\n\tc.wc.Close() //nolint:errcheck\n\tclose(c.terminate)\n}\n\n// RemoteAddr returns the remote address.\nfunc (c *ServerConn) RemoteAddr() net.Addr {\n\treturn c.wc.RemoteAddr()\n}\n\nfunc (c *ServerConn) run() {\n\tc.wc.SetReadDeadline(time.Now().Add(pingInterval + pingTimeout)) //nolint:errcheck\n\n\tc.wc.SetPongHandler(func(string) error {\n\t\tc.wc.SetReadDeadline(time.Now().Add(pingInterval + pingTimeout)) //nolint:errcheck\n\t\treturn nil\n\t})\n\n\tpingTicker := time.NewTicker(pingInterval)\n\tdefer pingTicker.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase byts := <-c.write:\n\t\t\tc.wc.SetWriteDeadline(time.Now().Add(writeTimeout)) //nolint:errcheck\n\t\t\terr := c.wc.WriteMessage(websocket.TextMessage, byts)\n\t\t\tc.writeErr <- err\n\n\t\tcase <-pingTicker.C:\n\t\t\tc.wc.SetWriteDeadline(time.Now().Add(writeTimeout)) //nolint:errcheck\n\t\t\tc.wc.WriteMessage(websocket.PingMessage, nil)       //nolint:errcheck\n\n\t\tcase <-c.terminate:\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// ReadJSON reads a JSON object.\nfunc (c *ServerConn) ReadJSON(in any) error {\n\treturn c.wc.ReadJSON(in)\n}\n\n// WriteJSON writes a JSON object.\nfunc (c *ServerConn) WriteJSON(in any) error {\n\tbyts, err := json.Marshal(in)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tselect {\n\tcase c.write <- byts:\n\t\treturn <-c.writeErr\n\tcase <-c.terminate:\n\t\treturn fmt.Errorf(\"terminated\")\n\t}\n}\n"
  },
  {
    "path": "internal/protocols/websocket/serverconn_test.go",
    "content": "package websocket\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/gorilla/websocket\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestServerConn(t *testing.T) {\n\tpingReceived := make(chan struct{})\n\tpingInterval = 100 * time.Millisecond\n\n\ts := &http.Server{\n\t\tHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tc, err := NewServerConn(w, r)\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer c.Close()\n\n\t\t\terr = c.WriteJSON(\"testing\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\t<-pingReceived\n\t\t}),\n\t}\n\n\tln, err := net.Listen(\"tcp\", \"localhost:6344\")\n\trequire.NoError(t, err)\n\n\tgo s.Serve(ln)\n\tdefer s.Shutdown(context.Background())\n\n\tc, res, err := websocket.DefaultDialer.Dial(\"ws://localhost:6344/\", nil)\n\trequire.NoError(t, err)\n\tdefer res.Body.Close()\n\tdefer c.Close() //nolint:errcheck\n\n\tc.SetPingHandler(func(_ string) error {\n\t\tclose(pingReceived)\n\t\treturn nil\n\t})\n\n\tvar msg string\n\terr = c.ReadJSON(&msg)\n\trequire.NoError(t, err)\n\trequire.Equal(t, \"testing\", msg)\n\n\t_, _, err = c.ReadMessage()\n\trequire.Error(t, err)\n\n\t<-pingReceived\n}\n"
  },
  {
    "path": "internal/protocols/whip/client.go",
    "content": "// Package whip contains a WHIP/WHEP client.\npackage whip\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/pion/sdp/v3\"\n\tpwebrtc \"github.com/pion/webrtc/v4\"\n\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/httpp\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/webrtc\"\n)\n\n// Client is a WHIP client.\ntype Client struct {\n\tURL                *url.URL\n\tPublish            bool\n\tOutgoingTracks     []*webrtc.OutgoingTrack\n\tHTTPClient         *http.Client\n\tBearerToken        string\n\tUDPReadBufferSize  uint\n\tSTUNGatherTimeout  time.Duration\n\tHandshakeTimeout   time.Duration\n\tTrackGatherTimeout time.Duration\n\tLog                logger.Writer\n\n\tpc               *webrtc.PeerConnection\n\tpatchIsSupported bool\n}\n\n// Initialize initializes the Client.\nfunc (c *Client) Initialize(ctx context.Context) error {\n\tif c.STUNGatherTimeout == 0 {\n\t\tc.STUNGatherTimeout = 5 * time.Second\n\t}\n\tif c.HandshakeTimeout == 0 {\n\t\tc.HandshakeTimeout = 10 * time.Second\n\t}\n\tif c.TrackGatherTimeout == 0 {\n\t\tc.TrackGatherTimeout = 2 * time.Second\n\t}\n\n\ticeServers, err := c.optionsICEServers(ctx)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tc.pc = &webrtc.PeerConnection{\n\t\tUDPReadBufferSize: c.UDPReadBufferSize,\n\t\tLocalRandomUDP:    true,\n\t\tICEServers:        iceServers,\n\t\tIPsFromInterfaces: true,\n\t\tPublish:           c.Publish,\n\t\tSTUNGatherTimeout: c.STUNGatherTimeout,\n\t\tOutgoingTracks:    c.OutgoingTracks,\n\t\tLog:               c.Log,\n\t}\n\terr = c.pc.Start()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tinitializeRes := make(chan error)\n\n\tgo func() {\n\t\tinitializeRes <- c.initializeInner(ctx)\n\t}()\n\n\tselect {\n\tcase <-ctx.Done():\n\t\tc.pc.Close()\n\t\t<-initializeRes\n\t\treturn fmt.Errorf(\"terminated\")\n\n\tcase err = <-initializeRes:\n\t}\n\n\tif err != nil {\n\t\tc.pc.Close()\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) initializeInner(ctx context.Context) error {\n\toffer, err := c.pc.CreatePartialOffer()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tres, err := c.postOffer(ctx, offer)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tc.URL, err = c.URL.Parse(res.Location)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !c.Publish {\n\t\tvar sdp sdp.SessionDescription\n\t\terr = sdp.Unmarshal([]byte(res.Answer.SDP))\n\t\tif err != nil {\n\t\t\tc.deleteSession(context.Background()) //nolint:errcheck\n\t\t\treturn err\n\t\t}\n\n\t\terr = webrtc.TracksAreValid(sdp.MediaDescriptions)\n\t\tif err != nil {\n\t\t\tc.deleteSession(context.Background()) //nolint:errcheck\n\t\t\treturn err\n\t\t}\n\t}\n\n\terr = c.pc.SetAnswer(res.Answer)\n\tif err != nil {\n\t\tc.deleteSession(context.Background()) //nolint:errcheck\n\t\treturn err\n\t}\n\n\tt := time.NewTimer(c.HandshakeTimeout)\n\tdefer t.Stop()\n\nouter:\n\tfor {\n\t\tselect {\n\t\tcase ca := <-c.pc.NewLocalCandidate():\n\t\t\terr = c.patchCandidate(ctx, offer, res.ETag, ca)\n\t\t\tif err != nil {\n\t\t\t\tc.deleteSession(context.Background()) //nolint:errcheck\n\t\t\t\treturn err\n\t\t\t}\n\n\t\tcase <-c.pc.GatheringDone():\n\n\t\tcase <-c.pc.Connected():\n\t\t\tbreak outer\n\n\t\tcase <-t.C:\n\t\t\tc.deleteSession(context.Background()) //nolint:errcheck\n\t\t\treturn fmt.Errorf(\"deadline exceeded while waiting connection\")\n\t\t}\n\t}\n\n\tif !c.Publish {\n\t\terr = c.pc.GatherIncomingTracks(c.TrackGatherTimeout)\n\t\tif err != nil {\n\t\t\tc.deleteSession(context.Background()) //nolint:errcheck\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// PeerConnection returns the underlying peer connection.\nfunc (c *Client) PeerConnection() *webrtc.PeerConnection {\n\treturn c.pc\n}\n\n// IncomingTracks returns incoming tracks.\nfunc (c *Client) IncomingTracks() []*webrtc.IncomingTrack {\n\treturn c.pc.IncomingTracks()\n}\n\n// StartReading starts reading all incoming tracks.\nfunc (c *Client) StartReading() {\n\tc.pc.StartReading()\n}\n\n// Close closes the client.\nfunc (c *Client) Close() error {\n\terr := c.deleteSession(context.Background())\n\tc.pc.Close()\n\treturn err\n}\n\n// Wait waits until a fatal error.\nfunc (c *Client) Wait() error {\n\t<-c.pc.Failed()\n\treturn fmt.Errorf(\"peer connection closed\")\n}\n\nfunc (c *Client) optionsICEServers(\n\tctx context.Context,\n) ([]pwebrtc.ICEServer, error) {\n\treq, err := http.NewRequestWithContext(ctx, http.MethodOptions, c.URL.String(), nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif c.BearerToken != \"\" {\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+c.BearerToken)\n\t}\n\n\tres, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer res.Body.Close()\n\n\tif res.StatusCode != http.StatusOK && res.StatusCode != http.StatusNoContent {\n\t\treturn nil, fmt.Errorf(\"bad status code: %v\", res.StatusCode)\n\t}\n\n\treturn LinkHeaderUnmarshal(res.Header[\"Link\"])\n}\n\ntype whipPostOfferResponse struct {\n\tAnswer   *pwebrtc.SessionDescription\n\tLocation string\n\tETag     string\n}\n\nfunc (c *Client) postOffer(\n\tctx context.Context,\n\toffer *pwebrtc.SessionDescription,\n) (*whipPostOfferResponse, error) {\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPost, c.URL.String(), bytes.NewReader([]byte(offer.SDP)))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif c.BearerToken != \"\" {\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+c.BearerToken)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/sdp\")\n\n\tres, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer res.Body.Close()\n\n\tif res.StatusCode != http.StatusCreated {\n\t\treturn nil, fmt.Errorf(\"bad status code: %v\", res.StatusCode)\n\t}\n\n\tcontentType := httpp.ParseContentType(req.Header.Get(\"Content-Type\"))\n\tif contentType != \"application/sdp\" {\n\t\treturn nil, fmt.Errorf(\"bad Content-Type: expected 'application/sdp', got '%s'\", contentType)\n\t}\n\n\tc.patchIsSupported = (res.Header.Get(\"Accept-Patch\") == \"application/trickle-ice-sdpfrag\")\n\n\tLocation := res.Header.Get(\"Location\")\n\n\tetag := res.Header.Get(\"ETag\")\n\tif etag == \"\" {\n\t\treturn nil, fmt.Errorf(\"ETag is missing\")\n\t}\n\n\tsdp, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tanswer := &pwebrtc.SessionDescription{\n\t\tType: pwebrtc.SDPTypeAnswer,\n\t\tSDP:  string(sdp),\n\t}\n\n\treturn &whipPostOfferResponse{\n\t\tAnswer:   answer,\n\t\tLocation: Location,\n\t\tETag:     etag,\n\t}, nil\n}\n\nfunc (c *Client) patchCandidate(\n\tctx context.Context,\n\toffer *pwebrtc.SessionDescription,\n\tetag string,\n\tcandidate *pwebrtc.ICECandidateInit,\n) error {\n\tif !c.patchIsSupported {\n\t\treturn nil\n\t}\n\n\tfrag, err := ICEFragmentMarshal(offer.SDP, []*pwebrtc.ICECandidateInit{candidate})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treq, err := http.NewRequestWithContext(ctx, http.MethodPatch, c.URL.String(), bytes.NewReader(frag))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif c.BearerToken != \"\" {\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+c.BearerToken)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/trickle-ice-sdpfrag\")\n\treq.Header.Set(\"If-Match\", etag)\n\n\tres, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer res.Body.Close()\n\n\tif res.StatusCode != http.StatusNoContent {\n\t\treturn fmt.Errorf(\"bad status code: %v\", res.StatusCode)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) deleteSession(\n\tctx context.Context,\n) error {\n\treq, err := http.NewRequestWithContext(ctx, http.MethodDelete, c.URL.String(), nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif c.BearerToken != \"\" {\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+c.BearerToken)\n\t}\n\n\tres, err := c.HTTPClient.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer res.Body.Close()\n\n\tif res.StatusCode != http.StatusOK {\n\t\treturn fmt.Errorf(\"bad status code: %v\", res.StatusCode)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/protocols/whip/client_test.go",
    "content": "package whip\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"sort\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/protocols/webrtc\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/pion/rtp\"\n\tpwebrtc \"github.com/pion/webrtc/v4\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc whipOffer(body []byte) *pwebrtc.SessionDescription {\n\treturn &pwebrtc.SessionDescription{\n\t\tType: pwebrtc.SDPTypeOffer,\n\t\tSDP:  string(body),\n\t}\n}\n\nfunc gatherCodecs(tracks []*webrtc.IncomingTrack) []pwebrtc.RTPCodecParameters {\n\tcodecs := make([]pwebrtc.RTPCodecParameters, len(tracks))\n\tfor i, track := range tracks {\n\t\tcodecs[i] = track.Codec()\n\t}\n\treturn codecs\n}\n\nfunc TestClientRead(t *testing.T) {\n\tfor _, ca := range []string{\n\t\t\"audio\",\n\t\t\"video+audio\",\n\t} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tvar outgoingTracks []*webrtc.OutgoingTrack\n\n\t\t\tswitch ca {\n\t\t\tcase \"audio\":\n\t\t\t\toutgoingTracks = []*webrtc.OutgoingTrack{{\n\t\t\t\t\tCaps: pwebrtc.RTPCodecCapability{\n\t\t\t\t\t\tMimeType:  \"audio/opus\",\n\t\t\t\t\t\tClockRate: 48000,\n\t\t\t\t\t\tChannels:  2,\n\t\t\t\t\t},\n\t\t\t\t}}\n\n\t\t\tcase \"video+audio\":\n\t\t\t\toutgoingTracks = []*webrtc.OutgoingTrack{\n\t\t\t\t\t{\n\t\t\t\t\t\tCaps: pwebrtc.RTPCodecCapability{\n\t\t\t\t\t\t\tMimeType:  \"video/H264\",\n\t\t\t\t\t\t\tClockRate: 90000,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tCaps: pwebrtc.RTPCodecCapability{\n\t\t\t\t\t\t\tMimeType:  \"audio/opus\",\n\t\t\t\t\t\t\tClockRate: 48000,\n\t\t\t\t\t\t\tChannels:  2,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tpc := &webrtc.PeerConnection{\n\t\t\t\tLocalRandomUDP:    true,\n\t\t\t\tIPsFromInterfaces: true,\n\t\t\t\tPublish:           true,\n\t\t\t\tOutgoingTracks:    outgoingTracks,\n\t\t\t\tLog:               test.NilLogger,\n\t\t\t}\n\t\t\terr := pc.Start()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer pc.Close()\n\n\t\t\tstate := 0\n\n\t\t\thttpServ := &http.Server{\n\t\t\t\tHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tswitch state {\n\t\t\t\t\tcase 0:\n\t\t\t\t\t\trequire.Equal(t, http.MethodOptions, r.Method)\n\t\t\t\t\t\trequire.Equal(t, \"/my/resource\", r.URL.Path)\n\n\t\t\t\t\t\tw.Header().Set(\"Access-Control-Allow-Methods\", \"OPTIONS, GET, POST, PATCH\")\n\t\t\t\t\t\tw.Header().Set(\"Access-Control-Allow-Headers\", \"Authorization, Content-Type, If-Match\")\n\t\t\t\t\t\tw.WriteHeader(http.StatusNoContent)\n\n\t\t\t\t\tcase 1:\n\t\t\t\t\t\trequire.Equal(t, http.MethodPost, r.Method)\n\t\t\t\t\t\trequire.Equal(t, \"/my/resource\", r.URL.Path)\n\t\t\t\t\t\trequire.Equal(t, \"application/sdp\", r.Header.Get(\"Content-Type\"))\n\n\t\t\t\t\t\tbody, err2 := io.ReadAll(r.Body)\n\t\t\t\t\t\trequire.NoError(t, err2)\n\t\t\t\t\t\toffer := whipOffer(body)\n\n\t\t\t\t\t\tanswer, err2 := pc.CreateFullAnswer(offer)\n\t\t\t\t\t\trequire.NoError(t, err2)\n\n\t\t\t\t\t\tw.Header().Set(\"Content-Type\", \"application/sdp\")\n\t\t\t\t\t\tw.Header().Set(\"Accept-Patch\", \"application/trickle-ice-sdpfrag\")\n\t\t\t\t\t\tw.Header().Set(\"ETag\", \"test_etag\")\n\t\t\t\t\t\tw.Header().Set(\"Location\", \"/my/resource/sessionid\")\n\t\t\t\t\t\tw.WriteHeader(http.StatusCreated)\n\t\t\t\t\t\tw.Write([]byte(answer.SDP))\n\n\t\t\t\t\t\tgo func() {\n\t\t\t\t\t\t\terr3 := pc.WaitUntilConnected(10 * time.Second)\n\t\t\t\t\t\t\trequire.NoError(t, err3)\n\n\t\t\t\t\t\t\tfor _, track := range outgoingTracks {\n\t\t\t\t\t\t\t\terr3 = track.WriteRTP(&rtp.Packet{\n\t\t\t\t\t\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\t\t\t\t\t\tVersion:        2,\n\t\t\t\t\t\t\t\t\t\tMarker:         true,\n\t\t\t\t\t\t\t\t\t\tPayloadType:    111,\n\t\t\t\t\t\t\t\t\t\tSequenceNumber: 1123,\n\t\t\t\t\t\t\t\t\t\tTimestamp:      45343,\n\t\t\t\t\t\t\t\t\t\tSSRC:           563424,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\tPayload: []byte{5, 2},\n\t\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\t\trequire.NoError(t, err3)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}()\n\n\t\t\t\t\tdefault:\n\t\t\t\t\t\trequire.Equal(t, \"/my/resource/sessionid\", r.URL.Path)\n\n\t\t\t\t\t\tswitch r.Method {\n\t\t\t\t\t\tcase http.MethodPatch:\n\t\t\t\t\t\t\tw.WriteHeader(http.StatusNoContent)\n\n\t\t\t\t\t\tcase http.MethodDelete:\n\t\t\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tt.Errorf(\"should not happen\")\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tstate++\n\t\t\t\t}),\n\t\t\t}\n\n\t\t\tln, err := net.Listen(\"tcp\", \"localhost:9005\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\tgo httpServ.Serve(ln)\n\t\t\tdefer httpServ.Shutdown(context.Background())\n\n\t\t\tu, err := url.Parse(\"http://localhost:9005/my/resource\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\tcl := &Client{\n\t\t\t\tURL:        u,\n\t\t\t\tHTTPClient: &http.Client{},\n\t\t\t\tLog:        test.NilLogger,\n\t\t\t}\n\t\t\terr = cl.Initialize(context.Background())\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer cl.Close() //nolint:errcheck\n\n\t\t\tcodecs := gatherCodecs(cl.IncomingTracks())\n\n\t\t\tswitch ca {\n\t\t\tcase \"audio\":\n\t\t\t\trequire.Equal(t, []pwebrtc.RTPCodecParameters{\n\t\t\t\t\t{\n\t\t\t\t\t\tRTPCodecCapability: pwebrtc.RTPCodecCapability{\n\t\t\t\t\t\t\tMimeType:    pwebrtc.MimeTypeOpus,\n\t\t\t\t\t\t\tClockRate:   48000,\n\t\t\t\t\t\t\tChannels:    2,\n\t\t\t\t\t\t\tSDPFmtpLine: \"minptime=10;useinbandfec=1;stereo=1;sprop-stereo=1\",\n\t\t\t\t\t\t\tRTCPFeedback: []pwebrtc.RTCPFeedback{{\n\t\t\t\t\t\t\t\tType: \"transport-cc\",\n\t\t\t\t\t\t\t}},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPayloadType: 111,\n\t\t\t\t\t},\n\t\t\t\t}, codecs)\n\n\t\t\tcase \"video+audio\":\n\t\t\t\tsort.Slice(codecs, func(i, j int) bool {\n\t\t\t\t\treturn codecs[i].PayloadType < codecs[j].PayloadType\n\t\t\t\t})\n\n\t\t\t\trequire.Equal(t, []pwebrtc.RTPCodecParameters{\n\t\t\t\t\t{\n\t\t\t\t\t\tRTPCodecCapability: pwebrtc.RTPCodecCapability{\n\t\t\t\t\t\t\tMimeType:     pwebrtc.MimeTypeH264,\n\t\t\t\t\t\t\tClockRate:    90000,\n\t\t\t\t\t\t\tSDPFmtpLine:  \"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f\",\n\t\t\t\t\t\t\tRTCPFeedback: codecs[0].RTCPFeedback,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPayloadType: 105,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tRTPCodecCapability: pwebrtc.RTPCodecCapability{\n\t\t\t\t\t\t\tMimeType:     pwebrtc.MimeTypeOpus,\n\t\t\t\t\t\t\tClockRate:    48000,\n\t\t\t\t\t\t\tChannels:     2,\n\t\t\t\t\t\t\tSDPFmtpLine:  \"minptime=10;useinbandfec=1;stereo=1;sprop-stereo=1\",\n\t\t\t\t\t\t\tRTCPFeedback: codecs[1].RTCPFeedback,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPayloadType: 111,\n\t\t\t\t\t},\n\t\t\t\t}, codecs)\n\t\t\t}\n\n\t\t\trecv := make([]chan struct{}, len(outgoingTracks))\n\t\t\tfor i := range outgoingTracks {\n\t\t\t\trecv[i] = make(chan struct{})\n\t\t\t}\n\n\t\t\tfor i, track := range cl.IncomingTracks() {\n\t\t\t\tci := i\n\t\t\t\ttrack.OnPacketRTP = func(_ *rtp.Packet) {\n\t\t\t\t\tclose(recv[ci])\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tcl.StartReading()\n\n\t\t\tfor _, rv := range recv {\n\t\t\t\t<-rv\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClientPublish(t *testing.T) {\n\tfor _, ca := range []string{\"audio\", \"video+audio\"} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tpc := &webrtc.PeerConnection{\n\t\t\t\tLocalRandomUDP:    true,\n\t\t\t\tIPsFromInterfaces: true,\n\t\t\t\tLog:               test.NilLogger,\n\t\t\t}\n\t\t\terr := pc.Start()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer pc.Close()\n\n\t\t\tstate := 0\n\t\t\tvar recv []chan struct{}\n\n\t\t\thttpServ := &http.Server{\n\t\t\t\tHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\t\t\tswitch state {\n\t\t\t\t\tcase 0:\n\t\t\t\t\t\trequire.Equal(t, http.MethodOptions, r.Method)\n\t\t\t\t\t\trequire.Equal(t, \"/my/resource\", r.URL.Path)\n\n\t\t\t\t\t\tw.Header().Set(\"Access-Control-Allow-Methods\", \"OPTIONS, GET, POST, PATCH\")\n\t\t\t\t\t\tw.Header().Set(\"Access-Control-Allow-Headers\", \"Authorization, Content-Type, If-Match\")\n\t\t\t\t\t\tw.WriteHeader(http.StatusNoContent)\n\n\t\t\t\t\tcase 1:\n\t\t\t\t\t\trequire.Equal(t, http.MethodPost, r.Method)\n\t\t\t\t\t\trequire.Equal(t, \"/my/resource\", r.URL.Path)\n\t\t\t\t\t\trequire.Equal(t, \"application/sdp\", r.Header.Get(\"Content-Type\"))\n\n\t\t\t\t\t\tbody, err2 := io.ReadAll(r.Body)\n\t\t\t\t\t\trequire.NoError(t, err2)\n\t\t\t\t\t\toffer := whipOffer(body)\n\n\t\t\t\t\t\tanswer, err2 := pc.CreateFullAnswer(offer)\n\t\t\t\t\t\trequire.NoError(t, err2)\n\n\t\t\t\t\t\tw.Header().Set(\"Content-Type\", \"application/sdp\")\n\t\t\t\t\t\tw.Header().Set(\"Accept-Patch\", \"application/trickle-ice-sdpfrag\")\n\t\t\t\t\t\tw.Header().Set(\"ETag\", \"test_etag\")\n\t\t\t\t\t\tw.Header().Set(\"Location\", \"/my/resource/sessionid\")\n\t\t\t\t\t\tw.WriteHeader(http.StatusCreated)\n\t\t\t\t\t\tw.Write([]byte(answer.SDP))\n\n\t\t\t\t\t\tgo func() {\n\t\t\t\t\t\t\terr3 := pc.WaitUntilConnected(10 * time.Second)\n\t\t\t\t\t\t\trequire.NoError(t, err3)\n\n\t\t\t\t\t\t\terr3 = pc.GatherIncomingTracks(2 * time.Second)\n\t\t\t\t\t\t\trequire.NoError(t, err3)\n\n\t\t\t\t\t\t\tcodecs := gatherCodecs(pc.IncomingTracks())\n\n\t\t\t\t\t\t\tswitch ca {\n\t\t\t\t\t\t\tcase \"audio\":\n\t\t\t\t\t\t\t\trequire.Equal(t, []pwebrtc.RTPCodecParameters{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tRTPCodecCapability: pwebrtc.RTPCodecCapability{\n\t\t\t\t\t\t\t\t\t\t\tMimeType:     pwebrtc.MimeTypeOpus,\n\t\t\t\t\t\t\t\t\t\t\tClockRate:    48000,\n\t\t\t\t\t\t\t\t\t\t\tChannels:     2,\n\t\t\t\t\t\t\t\t\t\t\tSDPFmtpLine:  \"\",\n\t\t\t\t\t\t\t\t\t\t\tRTCPFeedback: codecs[0].RTCPFeedback,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tPayloadType: 96,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t}, codecs)\n\n\t\t\t\t\t\t\tcase \"video+audio\":\n\t\t\t\t\t\t\t\tsort.Slice(codecs, func(i, j int) bool {\n\t\t\t\t\t\t\t\t\treturn codecs[i].PayloadType < codecs[j].PayloadType\n\t\t\t\t\t\t\t\t})\n\n\t\t\t\t\t\t\t\trequire.Equal(t, []pwebrtc.RTPCodecParameters{\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tRTPCodecCapability: pwebrtc.RTPCodecCapability{\n\t\t\t\t\t\t\t\t\t\t\tMimeType:     pwebrtc.MimeTypeH264,\n\t\t\t\t\t\t\t\t\t\t\tClockRate:    90000,\n\t\t\t\t\t\t\t\t\t\t\tRTCPFeedback: codecs[0].RTCPFeedback,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tPayloadType: 96,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tRTPCodecCapability: pwebrtc.RTPCodecCapability{\n\t\t\t\t\t\t\t\t\t\t\tMimeType:     pwebrtc.MimeTypeOpus,\n\t\t\t\t\t\t\t\t\t\t\tClockRate:    48000,\n\t\t\t\t\t\t\t\t\t\t\tChannels:     2,\n\t\t\t\t\t\t\t\t\t\t\tSDPFmtpLine:  \"\",\n\t\t\t\t\t\t\t\t\t\t\tRTCPFeedback: codecs[1].RTCPFeedback,\n\t\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t\t\tPayloadType: 97,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t}, codecs)\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tfor i, track := range pc.IncomingTracks() {\n\t\t\t\t\t\t\t\tci := i\n\t\t\t\t\t\t\t\ttrack.OnPacketRTP = func(_ *rtp.Packet) {\n\t\t\t\t\t\t\t\t\tclose(recv[ci])\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tpc.StartReading()\n\t\t\t\t\t\t}()\n\n\t\t\t\t\tdefault:\n\t\t\t\t\t\trequire.Equal(t, \"/my/resource/sessionid\", r.URL.Path)\n\n\t\t\t\t\t\tswitch r.Method {\n\t\t\t\t\t\tcase http.MethodPatch:\n\t\t\t\t\t\t\tw.WriteHeader(http.StatusNoContent)\n\n\t\t\t\t\t\tcase http.MethodDelete:\n\t\t\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\tt.Errorf(\"should not happen\")\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tstate++\n\t\t\t\t}),\n\t\t\t}\n\n\t\t\tln, err := net.Listen(\"tcp\", \"localhost:9005\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\tgo httpServ.Serve(ln)\n\t\t\tdefer httpServ.Shutdown(context.Background())\n\n\t\t\tu, err := url.Parse(\"http://localhost:9005/my/resource\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\tvar outgoingTracks []*webrtc.OutgoingTrack\n\n\t\t\tswitch ca {\n\t\t\tcase \"audio\":\n\t\t\t\toutgoingTracks = []*webrtc.OutgoingTrack{{\n\t\t\t\t\tCaps: pwebrtc.RTPCodecCapability{\n\t\t\t\t\t\tMimeType:  \"audio/opus\",\n\t\t\t\t\t\tClockRate: 48000,\n\t\t\t\t\t\tChannels:  2,\n\t\t\t\t\t},\n\t\t\t\t}}\n\n\t\t\tcase \"video+audio\":\n\t\t\t\toutgoingTracks = []*webrtc.OutgoingTrack{\n\t\t\t\t\t{\n\t\t\t\t\t\tCaps: pwebrtc.RTPCodecCapability{\n\t\t\t\t\t\t\tMimeType:  \"video/H264\",\n\t\t\t\t\t\t\tClockRate: 90000,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tCaps: pwebrtc.RTPCodecCapability{\n\t\t\t\t\t\t\tMimeType:  \"audio/opus\",\n\t\t\t\t\t\t\tClockRate: 48000,\n\t\t\t\t\t\t\tChannels:  2,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}\n\t\t\t}\n\n\t\t\trecv = make([]chan struct{}, len(outgoingTracks))\n\t\t\tfor i := range outgoingTracks {\n\t\t\t\trecv[i] = make(chan struct{})\n\t\t\t}\n\n\t\t\tcl := &Client{\n\t\t\t\tURL:            u,\n\t\t\t\tPublish:        true,\n\t\t\t\tOutgoingTracks: outgoingTracks,\n\t\t\t\tHTTPClient:     &http.Client{},\n\t\t\t\tLog:            test.NilLogger,\n\t\t\t}\n\t\t\terr = cl.Initialize(context.Background())\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer cl.Close() //nolint:errcheck\n\n\t\t\tfor _, track := range cl.OutgoingTracks {\n\t\t\t\terr = track.WriteRTP(&rtp.Packet{\n\t\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\t\tVersion:        2,\n\t\t\t\t\t\tMarker:         true,\n\t\t\t\t\t\tPayloadType:    111,\n\t\t\t\t\t\tSequenceNumber: 1123,\n\t\t\t\t\t\tTimestamp:      45343,\n\t\t\t\t\t\tSSRC:           563424,\n\t\t\t\t\t},\n\t\t\t\t\tPayload: []byte{5, 2},\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\tfor _, rv := range recv {\n\t\t\t\t<-rv\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestClientBearerToken(t *testing.T) {\n\tpc := &webrtc.PeerConnection{\n\t\tLocalRandomUDP:    true,\n\t\tIPsFromInterfaces: true,\n\t\tLog:               test.NilLogger,\n\t}\n\terr := pc.Start()\n\trequire.NoError(t, err)\n\tdefer pc.Close()\n\n\thttpServ := &http.Server{\n\t\tHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\trequire.Equal(t, \"Bearer my_secret_token\", r.Header.Get(\"Authorization\"))\n\n\t\t\tswitch r.Method {\n\t\t\tcase http.MethodOptions:\n\t\t\t\tw.WriteHeader(http.StatusNoContent)\n\n\t\t\tcase http.MethodPost:\n\t\t\t\tbody, err2 := io.ReadAll(r.Body)\n\t\t\t\trequire.NoError(t, err2)\n\t\t\t\toffer := whipOffer(body)\n\n\t\t\t\tanswer, err2 := pc.CreateFullAnswer(offer)\n\t\t\t\trequire.NoError(t, err2)\n\n\t\t\t\tw.Header().Set(\"Content-Type\", \"application/sdp\")\n\t\t\t\tw.Header().Set(\"ETag\", \"test_etag\")\n\t\t\t\tw.WriteHeader(http.StatusCreated)\n\t\t\t\tw.Write([]byte(answer.SDP))\n\t\t\t}\n\t\t}),\n\t}\n\n\tln, err := net.Listen(\"tcp\", \"localhost:9005\")\n\trequire.NoError(t, err)\n\n\tgo httpServ.Serve(ln)\n\tdefer httpServ.Shutdown(context.Background())\n\n\tu, err := url.Parse(\"http://localhost:9005/my/resource\")\n\trequire.NoError(t, err)\n\n\toutgoingTracks := []*webrtc.OutgoingTrack{{\n\t\tCaps: pwebrtc.RTPCodecCapability{\n\t\t\tMimeType:  \"audio/opus\",\n\t\t\tClockRate: 48000,\n\t\t\tChannels:  2,\n\t\t},\n\t}}\n\n\tcl := &Client{\n\t\tURL:            u,\n\t\tHTTPClient:     &http.Client{},\n\t\tBearerToken:    \"my_secret_token\",\n\t\tLog:            test.NilLogger,\n\t\tPublish:        true,\n\t\tOutgoingTracks: outgoingTracks,\n\t}\n\terr = cl.Initialize(context.Background())\n\trequire.NoError(t, err)\n\tdefer cl.Close() //nolint:errcheck\n}\n"
  },
  {
    "path": "internal/protocols/whip/ice_fragment.go",
    "content": "package whip\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\n\t\"github.com/pion/sdp/v3\"\n\t\"github.com/pion/webrtc/v4\"\n)\n\n// ICEFragmentUnmarshal decodes an ICE fragment.\nfunc ICEFragmentUnmarshal(buf []byte) ([]*webrtc.ICECandidateInit, error) {\n\tbuf = append([]byte(\"v=0\\r\\no=- 0 0 IN IP4 0.0.0.0\\r\\ns=-\\r\\nt=0 0\\r\\n\"), buf...)\n\n\tvar sdp sdp.SessionDescription\n\terr := sdp.Unmarshal(buf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar ret []*webrtc.ICECandidateInit\n\n\tfor _, media := range sdp.MediaDescriptions {\n\t\tmid, ok := media.Attribute(\"mid\")\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"mid attribute is missing\")\n\t\t}\n\n\t\tvar tmp uint64\n\t\ttmp, err = strconv.ParseUint(mid, 10, 16)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid mid attribute\")\n\t\t}\n\t\tmidNum := uint16(tmp)\n\n\t\tfor _, attr := range media.Attributes {\n\t\t\tif attr.Key == \"candidate\" {\n\t\t\t\tret = append(ret, &webrtc.ICECandidateInit{\n\t\t\t\t\tCandidate:     attr.Value,\n\t\t\t\t\tSDPMid:        &mid,\n\t\t\t\t\tSDPMLineIndex: &midNum,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\treturn ret, nil\n}\n\n// ICEFragmentMarshal encodes an ICE fragment.\nfunc ICEFragmentMarshal(offer string, candidates []*webrtc.ICECandidateInit) ([]byte, error) {\n\tvar sdp sdp.SessionDescription\n\terr := sdp.Unmarshal([]byte(offer))\n\tif err != nil || len(sdp.MediaDescriptions) == 0 {\n\t\treturn nil, err\n\t}\n\n\tfirstMedia := sdp.MediaDescriptions[0]\n\ticeUfrag, _ := firstMedia.Attribute(\"ice-ufrag\")\n\ticePwd, _ := firstMedia.Attribute(\"ice-pwd\")\n\n\tcandidatesByMedia := make(map[uint16][]*webrtc.ICECandidateInit)\n\tfor _, candidate := range candidates {\n\t\tif candidate.SDPMLineIndex == nil {\n\t\t\treturn nil, fmt.Errorf(\"sdpMLineIndex is null\")\n\t\t}\n\t\tmid := *candidate.SDPMLineIndex\n\t\tcandidatesByMedia[mid] = append(candidatesByMedia[mid], candidate)\n\t}\n\n\tfrag := \"a=ice-ufrag:\" + iceUfrag + \"\\r\\n\" +\n\t\t\"a=ice-pwd:\" + icePwd + \"\\r\\n\"\n\n\tfor mid, media := range sdp.MediaDescriptions {\n\t\tcbm, ok := candidatesByMedia[uint16(mid)]\n\t\tif ok {\n\t\t\tfrag += \"m=\" + media.MediaName.String() + \"\\r\\n\" +\n\t\t\t\t\"a=mid:\" + strconv.FormatUint(uint64(mid), 10) + \"\\r\\n\"\n\n\t\t\tfor _, candidate := range cbm {\n\t\t\t\tfrag += \"a=candidate:\" + candidate.Candidate + \"\\r\\n\"\n\t\t\t}\n\t\t}\n\t}\n\n\treturn []byte(frag), nil\n}\n"
  },
  {
    "path": "internal/protocols/whip/ice_fragment_test.go",
    "content": "package whip\n\nimport (\n\t\"testing\"\n\n\t\"github.com/pion/webrtc/v4\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc ptrOf[T any](v T) *T {\n\tp := new(T)\n\t*p = v\n\treturn p\n}\n\nvar iceFragmentCases = []struct {\n\tname       string\n\toffer      string\n\tcandidates []*webrtc.ICECandidateInit\n\tenc        string\n}{\n\t{\n\t\t\"a\",\n\t\t\"v=0\\n\" +\n\t\t\t\"o=- 8429658789122714282 1690995382 IN IP4 0.0.0.0\\n\" +\n\t\t\t\"s=-\\n\" +\n\t\t\t\"t=0 0\\n\" +\n\t\t\t\"a=fingerprint:sha-256 EA:05:9D:04:8F:56:41:92:3E:D5:2B:55:03:\" +\n\t\t\t\"1B:5A:2C:3D:D8:B3:FB:1B:D9:F7:1F:DA:77:0E:B9:E0:3D:B6:FF\\n\" +\n\t\t\t\"a=extmap-allow-mixed\\n\" +\n\t\t\t\"a=group:BUNDLE 0\\n\" +\n\t\t\t\"m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 121 127 120 125 107 108 109 123 118 45 46 116\\n\" +\n\t\t\t\"c=IN IP4 0.0.0.0\\n\" +\n\t\t\t\"a=setup:actpass\\n\" +\n\t\t\t\"a=mid:0\\n\" +\n\t\t\t\"a=ice-ufrag:tUQMzoQAVLzlvBys\\n\" +\n\t\t\t\"a=ice-pwd:pimyGfJcjjRwvUjnmGOODSjtIxyDljQj\\n\" +\n\t\t\t\"a=rtcp-mux\\n\" +\n\t\t\t\"a=rtcp-rsize\\n\" +\n\t\t\t\"a=rtpmap:96 VP8/90000\\n\" +\n\t\t\t\"a=rtcp-fb:96 goog-remb \\n\" +\n\t\t\t\"a=rtcp-fb:96 ccm fir\\n\" +\n\t\t\t\"a=rtcp-fb:96 nack \\n\" +\n\t\t\t\"a=rtcp-fb:96 nack pli\\n\" +\n\t\t\t\"a=rtcp-fb:96 nack \\n\" +\n\t\t\t\"a=rtcp-fb:96 nack pli\\n\" +\n\t\t\t\"a=rtcp-fb:96 transport-cc \\n\" +\n\t\t\t\"a=rtpmap:97 rtx/90000\\n\" +\n\t\t\t\"a=fmtp:97 apt=96\\n\" +\n\t\t\t\"a=rtcp-fb:97 nack \\n\" +\n\t\t\t\"a=rtcp-fb:97 nack pli\\n\" +\n\t\t\t\"a=rtcp-fb:97 transport-cc \\n\" +\n\t\t\t\"a=rtpmap:98 VP9/90000\\n\" +\n\t\t\t\"a=fmtp:98 profile-id=0\\n\" +\n\t\t\t\"a=rtcp-fb:98 goog-remb \\n\" +\n\t\t\t\"a=rtcp-fb:98 ccm fir\\n\" +\n\t\t\t\"a=rtcp-fb:98 nack \\n\" +\n\t\t\t\"a=rtcp-fb:98 nack pli\\n\" +\n\t\t\t\"a=rtcp-fb:98 nack \\n\" +\n\t\t\t\"a=rtcp-fb:98 nack pli\\n\" +\n\t\t\t\"a=rtcp-fb:98 transport-cc \\n\" +\n\t\t\t\"a=rtpmap:99 rtx/90000\\n\" +\n\t\t\t\"a=fmtp:99 apt=98\\n\" +\n\t\t\t\"a=rtcp-fb:99 nack \\n\" +\n\t\t\t\"a=rtcp-fb:99 nack pli\\n\" +\n\t\t\t\"a=rtcp-fb:99 transport-cc \\n\" +\n\t\t\t\"a=rtpmap:100 VP9/90000\\n\" +\n\t\t\t\"a=fmtp:100 profile-id=1\\n\" +\n\t\t\t\"a=rtcp-fb:100 goog-remb \\n\" +\n\t\t\t\"a=rtcp-fb:100 ccm fir\\n\" +\n\t\t\t\"a=rtcp-fb:100 nack \\n\" +\n\t\t\t\"a=rtcp-fb:100 nack pli\\n\" +\n\t\t\t\"a=rtcp-fb:100 nack \\n\" +\n\t\t\t\"a=rtcp-fb:100 nack pli\\n\" +\n\t\t\t\"a=rtcp-fb:100 transport-cc \\n\" +\n\t\t\t\"a=rtpmap:101 rtx/90000\\n\" +\n\t\t\t\"a=fmtp:101 apt=100\\n\" +\n\t\t\t\"a=rtcp-fb:101 nack \\n\" +\n\t\t\t\"a=rtcp-fb:101 nack pli\\n\" +\n\t\t\t\"a=rtcp-fb:101 transport-cc \\n\" +\n\t\t\t\"a=rtpmap:102 H264/90000\\n\" +\n\t\t\t\"a=fmtp:102 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f\\n\" +\n\t\t\t\"a=rtcp-fb:102 goog-remb \\n\" +\n\t\t\t\"a=rtcp-fb:102 ccm fir\\n\" +\n\t\t\t\"a=rtcp-fb:102 nack \\n\" +\n\t\t\t\"a=rtcp-fb:102 nack pli\\n\" +\n\t\t\t\"a=rtcp-fb:102 nack \\n\" +\n\t\t\t\"a=rtcp-fb:102 nack pli\\n\" +\n\t\t\t\"a=rtcp-fb:102 transport-cc \\n\" +\n\t\t\t\"a=rtpmap:121 rtx/90000\\n\" +\n\t\t\t\"a=fmtp:121 apt=102\\n\" +\n\t\t\t\"a=rtcp-fb:121 nack \\n\" +\n\t\t\t\"a=rtcp-fb:121 nack pli\\n\" +\n\t\t\t\"a=rtcp-fb:121 transport-cc \\n\" +\n\t\t\t\"a=rtpmap:127 H264/90000\\n\" +\n\t\t\t\"a=fmtp:127 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f\\n\" +\n\t\t\t\"a=rtcp-fb:127 goog-remb \\n\" +\n\t\t\t\"a=rtcp-fb:127 ccm fir\\n\" +\n\t\t\t\"a=rtcp-fb:127 nack \\n\" +\n\t\t\t\"a=rtcp-fb:127 nack pli\\n\" +\n\t\t\t\"a=rtcp-fb:127 nack \\n\" +\n\t\t\t\"a=rtcp-fb:127 nack pli\\n\" +\n\t\t\t\"a=rtcp-fb:127 transport-cc \\n\" +\n\t\t\t\"a=rtpmap:120 rtx/90000\\n\" +\n\t\t\t\"a=fmtp:120 apt=127\\n\" +\n\t\t\t\"a=rtcp-fb:120 nack \\n\" +\n\t\t\t\"a=rtcp-fb:120 nack pli\\n\" +\n\t\t\t\"a=rtcp-fb:120 transport-cc \\n\" +\n\t\t\t\"a=rtpmap:125 H264/90000\\n\" +\n\t\t\t\"a=fmtp:125 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\\n\" +\n\t\t\t\"a=rtcp-fb:125 goog-remb \\n\" +\n\t\t\t\"a=rtcp-fb:125 ccm fir\\n\" +\n\t\t\t\"a=rtcp-fb:125 nack \\n\" +\n\t\t\t\"a=rtcp-fb:125 nack pli\\n\" +\n\t\t\t\"a=rtcp-fb:125 nack \\n\" +\n\t\t\t\"a=rtcp-fb:125 nack pli\\n\" +\n\t\t\t\"a=rtcp-fb:125 transport-cc \\n\" +\n\t\t\t\"a=rtpmap:107 rtx/90000\\n\" +\n\t\t\t\"a=fmtp:107 apt=125\\n\" +\n\t\t\t\"a=rtcp-fb:107 nack \\n\" +\n\t\t\t\"a=rtcp-fb:107 nack pli\\n\" +\n\t\t\t\"a=rtcp-fb:107 transport-cc \\n\" +\n\t\t\t\"a=rtpmap:108 H264/90000\\n\" +\n\t\t\t\"a=fmtp:108 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f\\n\" +\n\t\t\t\"a=rtcp-fb:108 goog-remb \\n\" +\n\t\t\t\"a=rtcp-fb:108 ccm fir\\n\" +\n\t\t\t\"a=rtcp-fb:108 nack \\n\" +\n\t\t\t\"a=rtcp-fb:108 nack pli\\n\" +\n\t\t\t\"a=rtcp-fb:108 nack \\n\" +\n\t\t\t\"a=rtcp-fb:108 nack pli\\n\" +\n\t\t\t\"a=rtcp-fb:108 transport-cc \\n\" +\n\t\t\t\"a=rtpmap:109 rtx/90000\\n\" +\n\t\t\t\"a=fmtp:109 apt=108\\n\" +\n\t\t\t\"a=rtcp-fb:109 nack \\n\" +\n\t\t\t\"a=rtcp-fb:109 nack pli\\n\" +\n\t\t\t\"a=rtcp-fb:109 transport-cc \\n\" +\n\t\t\t\"a=rtpmap:123 H264/90000\\n\" +\n\t\t\t\"a=fmtp:123 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032\\n\" +\n\t\t\t\"a=rtcp-fb:123 goog-remb \\n\" +\n\t\t\t\"a=rtcp-fb:123 ccm fir\\n\" +\n\t\t\t\"a=rtcp-fb:123 nack \\n\" +\n\t\t\t\"a=rtcp-fb:123 nack pli\\n\" +\n\t\t\t\"a=rtcp-fb:123 nack \\n\" +\n\t\t\t\"a=rtcp-fb:123 nack pli\\n\" +\n\t\t\t\"a=rtcp-fb:123 transport-cc \\n\" +\n\t\t\t\"a=rtpmap:118 rtx/90000\\n\" +\n\t\t\t\"a=fmtp:118 apt=123\\n\" +\n\t\t\t\"a=rtcp-fb:118 nack \\n\" +\n\t\t\t\"a=rtcp-fb:118 nack pli\\n\" +\n\t\t\t\"a=rtcp-fb:118 transport-cc \\n\" +\n\t\t\t\"a=rtpmap:45 AV1/90000\\n\" +\n\t\t\t\"a=rtcp-fb:45 goog-remb \\n\" +\n\t\t\t\"a=rtcp-fb:45 ccm fir\\n\" +\n\t\t\t\"a=rtcp-fb:45 nack \\n\" +\n\t\t\t\"a=rtcp-fb:45 nack pli\\n\" +\n\t\t\t\"a=rtcp-fb:45 nack \\n\" +\n\t\t\t\"a=rtcp-fb:45 nack pli\\n\" +\n\t\t\t\"a=rtcp-fb:45 transport-cc \\n\" +\n\t\t\t\"a=rtpmap:46 rtx/90000\\n\" +\n\t\t\t\"a=fmtp:46 apt=45\\n\" +\n\t\t\t\"a=rtcp-fb:46 nack \\n\" +\n\t\t\t\"a=rtcp-fb:46 nack pli\\n\" +\n\t\t\t\"a=rtcp-fb:46 transport-cc \\n\" +\n\t\t\t\"a=rtpmap:116 ulpfec/90000\\n\" +\n\t\t\t\"a=rtcp-fb:116 nack \\n\" +\n\t\t\t\"a=rtcp-fb:116 nack pli\\n\" +\n\t\t\t\"a=rtcp-fb:116 transport-cc \\n\" +\n\t\t\t\"a=extmap:1 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\\n\" +\n\t\t\t\"a=ssrc:3421396091 cname:BmFVQDtOlcBwXZCl\\n\" +\n\t\t\t\"a=ssrc:3421396091 msid:BmFVQDtOlcBwXZCl CLgunVCazXXKLyEx\\n\" +\n\t\t\t\"a=ssrc:3421396091 mslabel:BmFVQDtOlcBwXZCl\\n\" +\n\t\t\t\"a=ssrc:3421396091 label:CLgunVCazXXKLyEx\\n\" +\n\t\t\t\"a=msid:BmFVQDtOlcBwXZCl CLgunVCazXXKLyEx\\n\" +\n\t\t\t\"a=sendrecv\\n\",\n\t\t[]*webrtc.ICECandidateInit{{\n\t\t\tCandidate:     \"3628911098 1 udp 2130706431 192.168.3.218 49462 typ host\",\n\t\t\tSDPMid:        ptrOf(\"0\"),\n\t\t\tSDPMLineIndex: ptrOf(uint16(0)),\n\t\t}},\n\t\t\"a=ice-ufrag:tUQMzoQAVLzlvBys\\r\\n\" +\n\t\t\t\"a=ice-pwd:pimyGfJcjjRwvUjnmGOODSjtIxyDljQj\\r\\n\" +\n\t\t\t\"m=video 9 UDP/TLS/RTP/SAVPF 96 97 98 99 100 101 102 121 127 120 125 107 108 109 123 118 45 46 116\\r\\n\" +\n\t\t\t\"a=mid:0\\r\\n\" +\n\t\t\t\"a=candidate:3628911098 1 udp 2130706431 192.168.3.218 49462 typ host\\r\\n\",\n\t},\n}\n\nfunc TestICEFragmentUnmarshal(t *testing.T) {\n\tfor _, ca := range iceFragmentCases {\n\t\tt.Run(ca.name, func(t *testing.T) {\n\t\t\tcandidates, err := ICEFragmentUnmarshal([]byte(ca.enc))\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, ca.candidates, candidates)\n\t\t})\n\t}\n}\n\nfunc TestICEFragmentMarshal(t *testing.T) {\n\tfor _, ca := range iceFragmentCases {\n\t\tt.Run(ca.name, func(t *testing.T) {\n\t\t\tbyts, err := ICEFragmentMarshal(ca.offer, ca.candidates)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, ca.enc, string(byts))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/protocols/whip/link_header.go",
    "content": "package whip\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"regexp\"\n\n\t\"github.com/pion/webrtc/v4\"\n)\n\nfunc quoteCredential(v string) string {\n\tb, _ := json.Marshal(v)\n\ts := string(b)\n\treturn s[1 : len(s)-1]\n}\n\nfunc unquoteCredential(v string) string {\n\tvar s string\n\tjson.Unmarshal([]byte(\"\\\"\"+v+\"\\\"\"), &s) //nolint:errcheck\n\treturn s\n}\n\n// LinkHeaderMarshal encodes a link header.\nfunc LinkHeaderMarshal(iceServers []webrtc.ICEServer) []string {\n\tret := make([]string, len(iceServers))\n\n\tfor i, server := range iceServers {\n\t\tlink := \"<\" + server.URLs[0] + \">; rel=\\\"ice-server\\\"\"\n\t\tif server.Username != \"\" {\n\t\t\tlink += \"; username=\\\"\" + quoteCredential(server.Username) + \"\\\"\" +\n\t\t\t\t\"; credential=\\\"\" + quoteCredential(server.Credential.(string)) + \"\\\"; credential-type=\\\"password\\\"\"\n\t\t}\n\t\tret[i] = link\n\t}\n\n\treturn ret\n}\n\nvar reLink = regexp.MustCompile(`^<(.+?)>; rel=\"ice-server\"(; username=\"(.+?)\"` +\n\t`; credential=\"(.+?)\"; credential-type=\"password\")?`)\n\n// LinkHeaderUnmarshal decodes a link header.\nfunc LinkHeaderUnmarshal(link []string) ([]webrtc.ICEServer, error) {\n\tret := make([]webrtc.ICEServer, len(link))\n\n\tfor i, li := range link {\n\t\tm := reLink.FindStringSubmatch(li)\n\t\tif m == nil {\n\t\t\treturn nil, fmt.Errorf(\"invalid link header: '%s'\", li)\n\t\t}\n\n\t\ts := webrtc.ICEServer{\n\t\t\tURLs: []string{m[1]},\n\t\t}\n\n\t\tif m[3] != \"\" {\n\t\t\ts.Username = unquoteCredential(m[3])\n\t\t\ts.Credential = unquoteCredential(m[4])\n\t\t\ts.CredentialType = webrtc.ICECredentialTypePassword\n\t\t}\n\n\t\tret[i] = s\n\t}\n\n\treturn ret, nil\n}\n"
  },
  {
    "path": "internal/protocols/whip/link_header_test.go",
    "content": "package whip\n\nimport (\n\t\"testing\"\n\n\t\"github.com/pion/webrtc/v4\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar linkHeaderCases = []struct {\n\tname string\n\tenc  []string\n\tdec  []webrtc.ICEServer\n}{\n\t{\n\t\t\"a\",\n\t\t[]string{\n\t\t\t`<stun:stun.l.google.com:19302>; rel=\"ice-server\"`,\n\t\t\t`<turns:turn.example.com>; rel=\"ice-server\"; username=\"myuser\\\"a?2;B\"; ` +\n\t\t\t\t`credential=\"mypwd\"; credential-type=\"password\"`,\n\t\t},\n\t\t[]webrtc.ICEServer{\n\t\t\t{\n\t\t\t\tURLs: []string{\"stun:stun.l.google.com:19302\"},\n\t\t\t},\n\t\t\t{\n\t\t\t\tURLs:       []string{\"turns:turn.example.com\"},\n\t\t\t\tUsername:   \"myuser\\\"a?2;B\",\n\t\t\t\tCredential: \"mypwd\",\n\t\t\t},\n\t\t},\n\t},\n}\n\nfunc TestLinkHeaderUnmarshal(t *testing.T) {\n\tfor _, ca := range linkHeaderCases {\n\t\tt.Run(ca.name, func(t *testing.T) {\n\t\t\tdec, err := LinkHeaderUnmarshal(ca.enc)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, ca.dec, dec)\n\t\t})\n\t}\n}\n\nfunc TestLinkHeaderMarshal(t *testing.T) {\n\tfor _, ca := range linkHeaderCases {\n\t\tt.Run(ca.name, func(t *testing.T) {\n\t\t\tenc := LinkHeaderMarshal(ca.dec)\n\t\t\trequire.Equal(t, ca.enc, enc)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/recordcleaner/cleaner.go",
    "content": "// Package recordcleaner contains the recording cleaner.\npackage recordcleaner\n\nimport (\n\t\"context\"\n\t\"io/fs\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/recordstore\"\n)\n\nvar timeNow = time.Now\n\n// Cleaner removes expired recording segments from disk.\ntype Cleaner struct {\n\tPathConfs map[string]*conf.Path\n\tParent    logger.Writer\n\n\tctx       context.Context\n\tctxCancel func()\n\n\tchReloadConf chan map[string]*conf.Path\n\tdone         chan struct{}\n}\n\n// Initialize initializes a Cleaner.\nfunc (c *Cleaner) Initialize() {\n\tc.ctx, c.ctxCancel = context.WithCancel(context.Background())\n\tc.chReloadConf = make(chan map[string]*conf.Path)\n\tc.done = make(chan struct{})\n\n\tgo c.run()\n}\n\n// Close closes the Cleaner.\nfunc (c *Cleaner) Close() {\n\tc.ctxCancel()\n\t<-c.done\n}\n\n// Log implements logger.Writer.\nfunc (c *Cleaner) Log(level logger.Level, format string, args ...any) {\n\tc.Parent.Log(level, \"[record cleaner]\"+format, args...)\n}\n\n// ReloadPathConfs is called by core.Core.\nfunc (c *Cleaner) ReloadPathConfs(pathConfs map[string]*conf.Path) {\n\tselect {\n\tcase c.chReloadConf <- pathConfs:\n\tcase <-c.ctx.Done():\n\t}\n}\n\nfunc (c *Cleaner) run() {\n\tdefer close(c.done)\n\n\tc.doRun() //nolint:errcheck\n\n\tfor {\n\t\tselect {\n\t\tcase <-time.After(c.cleanInterval()):\n\t\t\tc.doRun()\n\n\t\tcase cnf := <-c.chReloadConf:\n\t\t\tc.PathConfs = cnf\n\n\t\tcase <-c.ctx.Done():\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (c *Cleaner) cleanInterval() time.Duration {\n\tinterval := 30 * 60 * time.Second\n\n\tfor _, e := range c.PathConfs {\n\t\tif e.RecordDeleteAfter != 0 &&\n\t\t\tinterval > (time.Duration(e.RecordDeleteAfter)/2) {\n\t\t\tinterval = time.Duration(e.RecordDeleteAfter) / 2\n\t\t}\n\t}\n\n\treturn interval\n}\n\nfunc (c *Cleaner) doRun() {\n\tnow := timeNow()\n\n\tpathNames := recordstore.FindAllPathsWithSegments(c.PathConfs)\n\n\tfor _, pathName := range pathNames {\n\t\tc.processPath(now, pathName) //nolint:errcheck\n\t}\n}\n\nfunc (c *Cleaner) processPath(now time.Time, pathName string) error {\n\tpathConf, _, err := conf.FindPathConf(c.PathConfs, pathName)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif pathConf.RecordDeleteAfter == 0 {\n\t\treturn nil\n\t}\n\n\terr = c.deleteExpiredSegments(now, pathName, pathConf)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tc.deleteEmptyDirs(pathConf)\n\n\treturn nil\n}\n\nfunc (c *Cleaner) deleteExpiredSegments(now time.Time, pathName string, pathConf *conf.Path) error {\n\tend := now.Add(-time.Duration(pathConf.RecordDeleteAfter))\n\tsegments, err := recordstore.FindSegments(pathConf, pathName, nil, &end)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, seg := range segments {\n\t\tc.Log(logger.Debug, \"removing %s\", seg.Fpath)\n\t\tos.Remove(seg.Fpath)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Cleaner) deleteEmptyDirs(pathConf *conf.Path) {\n\trecordPath := strings.ReplaceAll(pathConf.RecordPath, \"%path\", pathConf.Name)\n\tcommonPath := recordstore.CommonPath(recordPath)\n\n\tfilepath.WalkDir(commonPath, func(fpath string, info fs.DirEntry, err error) error { //nolint:errcheck\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif info.IsDir() {\n\t\t\tos.Remove(fpath)\n\t\t}\n\n\t\treturn nil\n\t})\n}\n"
  },
  {
    "path": "internal/recordcleaner/cleaner_test.go",
    "content": "package recordcleaner\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestCleaner(t *testing.T) {\n\ttimeNow = func() time.Time {\n\t\treturn time.Date(2009, 5, 20, 22, 15, 25, 427000, time.Local)\n\t}\n\n\tdir, err := os.MkdirTemp(\"\", \"mediamtx-cleaner\")\n\trequire.NoError(t, err)\n\tdefer os.RemoveAll(dir)\n\n\tconst specialChars = \"_-+*?^$()[]{}|\"\n\n\terr = os.Mkdir(filepath.Join(dir, specialChars+\"_mypath\"), 0o755)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(dir, specialChars+\"_mypath\", \"2008-05-20_22-15-25-000125.mp4\"), []byte{1}, 0o644)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(dir, specialChars+\"_mypath\", \"2009-05-20_22-15-25-000427.mp4\"), []byte{1}, 0o644)\n\trequire.NoError(t, err)\n\n\tc := &Cleaner{\n\t\tPathConfs: map[string]*conf.Path{\n\t\t\t\"~^.*$\": {\n\t\t\t\tName:              \"~^.*$\",\n\t\t\t\tRegexp:            regexp.MustCompile(\"^.*$\"),\n\t\t\t\tRecordPath:        filepath.Join(dir, specialChars+\"_%path/%Y-%m-%d_%H-%M-%S-%f\"),\n\t\t\t\tRecordFormat:      conf.RecordFormatFMP4,\n\t\t\t\tRecordDeleteAfter: conf.Duration(10 * time.Second),\n\t\t\t},\n\t\t},\n\t\tParent: test.NilLogger,\n\t}\n\tc.Initialize()\n\tdefer c.Close()\n\n\ttime.Sleep(500 * time.Millisecond)\n\n\t_, err = os.Stat(filepath.Join(dir, specialChars+\"_mypath\", \"2008-05-20_22-15-25-000125.mp4\"))\n\trequire.Error(t, err)\n\n\t_, err = os.Stat(filepath.Join(dir, specialChars+\"_mypath\", \"2009-05-20_22-15-25-000427.mp4\"))\n\trequire.NoError(t, err)\n}\n\nfunc TestCleanerMultipleEntriesSamePath(t *testing.T) {\n\ttimeNow = func() time.Time {\n\t\treturn time.Date(2009, 5, 20, 22, 15, 25, 427000, time.Local)\n\t}\n\n\tdir, err := os.MkdirTemp(\"\", \"mediamtx-cleaner\")\n\trequire.NoError(t, err)\n\tdefer os.RemoveAll(dir)\n\n\terr = os.Mkdir(filepath.Join(dir, \"path1\"), 0o755)\n\trequire.NoError(t, err)\n\n\terr = os.Mkdir(filepath.Join(dir, \"path2\"), 0o755)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(dir, \"path1\", \"2009-05-19_22-15-25-000427.mp4\"), []byte{1}, 0o644)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(dir, \"path2\", \"2009-05-19_22-15-25-000427.mp4\"), []byte{1}, 0o644)\n\trequire.NoError(t, err)\n\n\tc := &Cleaner{\n\t\tPathConfs: map[string]*conf.Path{\n\t\t\t\"path1\": {\n\t\t\t\tName:              \"path1\",\n\t\t\t\tRecordPath:        filepath.Join(dir, \"%path/%Y-%m-%d_%H-%M-%S-%f\"),\n\t\t\t\tRecordFormat:      conf.RecordFormatFMP4,\n\t\t\t\tRecordDeleteAfter: conf.Duration(10 * time.Second),\n\t\t\t},\n\t\t\t\"path2\": {\n\t\t\t\tName:              \"path2\",\n\t\t\t\tRecordPath:        filepath.Join(dir, \"%path/%Y-%m-%d_%H-%M-%S-%f\"),\n\t\t\t\tRecordFormat:      conf.RecordFormatFMP4,\n\t\t\t\tRecordDeleteAfter: conf.Duration(10 * 24 * time.Hour),\n\t\t\t},\n\t\t},\n\t\tParent: test.NilLogger,\n\t}\n\tc.Initialize()\n\tdefer c.Close()\n\n\ttime.Sleep(500 * time.Millisecond)\n\n\t_, err = os.Stat(filepath.Join(dir, \"path1\", \"2009-05-19_22-15-25-000427.mp4\"))\n\trequire.Error(t, err)\n\n\t_, err = os.Stat(filepath.Join(dir, \"path1\"))\n\trequire.Error(t, err, \"testing\")\n\n\t_, err = os.Stat(filepath.Join(dir, \"path2\", \"2009-05-19_22-15-25-000427.mp4\"))\n\trequire.NoError(t, err)\n}\n"
  },
  {
    "path": "internal/recorder/format.go",
    "content": "package recorder\n\ntype format interface {\n\tinitialize() bool\n\tclose()\n}\n"
  },
  {
    "path": "internal/recorder/format_fmp4.go",
    "content": "package recorder\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"slices\"\n\t\"time\"\n\n\trtspformat \"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/ac3\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/av1\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/g711\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/h264\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/h265\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/jpeg\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg1audio\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4video\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/opus\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/vp9\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/fmp4\"\n\tmcodecs \"github.com/bluenviron/mediacommon/v2/pkg/formats/mp4/codecs\"\n\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n)\n\nvar (\n\tav1DefaultSequenceHeader = []byte{\n\t\t8, 0, 0, 0, 66, 167, 191, 228, 96, 13, 0, 64,\n\t}\n\n\th265DefaultVPS = []byte{\n\t\t0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x02, 0x20,\n\t\t0x00, 0x00, 0x03, 0x00, 0xb0, 0x00, 0x00, 0x03,\n\t\t0x00, 0x00, 0x03, 0x00, 0x7b, 0x18, 0xb0, 0x24,\n\t}\n\n\th265DefaultSPS = []byte{\n\t\t0x42, 0x01, 0x01, 0x02, 0x20, 0x00, 0x00, 0x03,\n\t\t0x00, 0xb0, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03,\n\t\t0x00, 0x7b, 0xa0, 0x07, 0x82, 0x00, 0x88, 0x7d,\n\t\t0xb6, 0x71, 0x8b, 0x92, 0x44, 0x80, 0x53, 0x88,\n\t\t0x88, 0x92, 0xcf, 0x24, 0xa6, 0x92, 0x72, 0xc9,\n\t\t0x12, 0x49, 0x22, 0xdc, 0x91, 0xaa, 0x48, 0xfc,\n\t\t0xa2, 0x23, 0xff, 0x00, 0x01, 0x00, 0x01, 0x6a,\n\t\t0x02, 0x02, 0x02, 0x01,\n\t}\n\n\th265DefaultPPS = []byte{\n\t\t0x44, 0x01, 0xc0, 0x25, 0x2f, 0x05, 0x32, 0x40,\n\t}\n\n\th264DefaultSPS = []byte{ // 1920x1080 baseline\n\t\t0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02,\n\t\t0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04,\n\t\t0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, 0x20,\n\t}\n\n\th264DefaultPPS = []byte{0x08, 0x06, 0x07, 0x08}\n\n\tmpeg4VideoDefaultConfig = []byte{\n\t\t0x00, 0x00, 0x01, 0xb0, 0x01, 0x00, 0x00, 0x01,\n\t\t0xb5, 0x89, 0x13, 0x00, 0x00, 0x01, 0x00, 0x00,\n\t\t0x00, 0x01, 0x20, 0x00, 0xc4, 0x8d, 0x88, 0x00,\n\t\t0xf5, 0x3c, 0x04, 0x87, 0x14, 0x63, 0x00, 0x00,\n\t\t0x01, 0xb2, 0x4c, 0x61, 0x76, 0x63, 0x35, 0x38,\n\t\t0x2e, 0x31, 0x33, 0x34, 0x2e, 0x31, 0x30, 0x30,\n\t}\n\n\tmpeg1VideoDefaultConfig = []byte{\n\t\t0x00, 0x00, 0x01, 0xb3, 0x78, 0x04, 0x38, 0x35,\n\t\t0xff, 0xff, 0xe0, 0x18, 0x00, 0x00, 0x01, 0xb5,\n\t\t0x14, 0x4a, 0x00, 0x01, 0x00, 0x00,\n\t}\n)\n\nfunc mpeg1audioChannelCount(cm mpeg1audio.ChannelMode) int {\n\tswitch cm {\n\tcase mpeg1audio.ChannelModeStereo,\n\t\tmpeg1audio.ChannelModeJointStereo,\n\t\tmpeg1audio.ChannelModeDualChannel:\n\t\treturn 2\n\n\tdefault:\n\t\treturn 1\n\t}\n}\n\nfunc jpegExtractSize(image []byte) (int, int, error) {\n\tl := len(image)\n\tif l < 2 || image[0] != 0xFF || image[1] != jpeg.MarkerStartOfImage {\n\t\treturn 0, 0, fmt.Errorf(\"invalid header\")\n\t}\n\n\timage = image[2:]\n\n\tfor {\n\t\tif len(image) < 2 {\n\t\t\treturn 0, 0, fmt.Errorf(\"not enough bits\")\n\t\t}\n\n\t\th0, h1 := image[0], image[1]\n\t\timage = image[2:]\n\n\t\tif h0 != 0xFF {\n\t\t\treturn 0, 0, fmt.Errorf(\"invalid image\")\n\t\t}\n\n\t\tswitch h1 {\n\t\tcase 0xE0, 0xE1, 0xE2, // JFIF\n\t\t\tjpeg.MarkerDefineHuffmanTable,\n\t\t\tjpeg.MarkerComment,\n\t\t\tjpeg.MarkerDefineQuantizationTable,\n\t\t\tjpeg.MarkerDefineRestartInterval:\n\t\t\tmlen := int(image[0])<<8 | int(image[1])\n\t\t\tif len(image) < mlen {\n\t\t\t\treturn 0, 0, fmt.Errorf(\"not enough bits\")\n\t\t\t}\n\t\t\timage = image[mlen:]\n\n\t\tcase jpeg.MarkerStartOfFrame1:\n\t\t\tmlen := int(image[0])<<8 | int(image[1])\n\t\t\tif len(image) < mlen {\n\t\t\t\treturn 0, 0, fmt.Errorf(\"not enough bits\")\n\t\t\t}\n\n\t\t\tvar sof jpeg.StartOfFrame1\n\t\t\terr := sof.Unmarshal(image[2:mlen])\n\t\t\tif err != nil {\n\t\t\t\treturn 0, 0, err\n\t\t\t}\n\n\t\t\treturn sof.Width, sof.Height, nil\n\n\t\tcase jpeg.MarkerStartOfScan:\n\t\t\treturn 0, 0, fmt.Errorf(\"SOF not found\")\n\n\t\tdefault:\n\t\t\treturn 0, 0, fmt.Errorf(\"unknown marker: 0x%.2x\", h1)\n\t\t}\n\t}\n}\n\ntype formatFMP4Sample struct {\n\t*fmp4.Sample\n\tdts int64\n\tntp time.Time\n}\n\ntype formatFMP4 struct {\n\tri *recorderInstance\n\n\ttracks            []*formatFMP4Track\n\thasVideo          bool\n\tcurrentSegment    *formatFMP4Segment\n\tnextSegmentNumber uint64\n}\n\nfunc (f *formatFMP4) initialize() bool {\n\tnextID := 1\n\n\taddTrack := func(format rtspformat.Format, codec mcodecs.Codec) *formatFMP4Track {\n\t\ttrack := &formatFMP4Track{\n\t\t\tf:         f,\n\t\t\tid:        nextID,\n\t\t\tclockRate: uint32(format.ClockRate()),\n\t\t\tcodec:     codec,\n\t\t}\n\t\ttrack.initialize()\n\n\t\tnextID++\n\t\tf.tracks = append(f.tracks, track)\n\t\treturn track\n\t}\n\n\tfor _, media := range f.ri.stream.Desc.Medias {\n\t\tfor _, forma := range media.Formats {\n\t\t\tclockRate := forma.ClockRate()\n\n\t\t\tswitch forma := forma.(type) {\n\t\t\tcase *rtspformat.AV1:\n\t\t\t\tcodec := &mcodecs.AV1{\n\t\t\t\t\tSequenceHeader: av1DefaultSequenceHeader,\n\t\t\t\t}\n\t\t\t\ttrack := addTrack(forma, codec)\n\n\t\t\t\tfirstReceived := false\n\n\t\t\t\tf.ri.reader.OnData(\n\t\t\t\t\tmedia,\n\t\t\t\t\tforma,\n\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\trandomAccess := false\n\t\t\t\t\t\tparamsChanged := false\n\n\t\t\t\t\t\tfor _, obu := range u.Payload.(unit.PayloadAV1) {\n\t\t\t\t\t\t\ttyp := av1.OBUType((obu[0] >> 3) & 0b1111)\n\n\t\t\t\t\t\t\tif typ == av1.OBUTypeSequenceHeader {\n\t\t\t\t\t\t\t\tif !bytes.Equal(codec.SequenceHeader, obu) {\n\t\t\t\t\t\t\t\t\tcodec.SequenceHeader = obu\n\t\t\t\t\t\t\t\t\tparamsChanged = true\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\trandomAccess = true\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif paramsChanged {\n\t\t\t\t\t\t\tf.updateCodecParams()\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif !firstReceived {\n\t\t\t\t\t\t\tif !randomAccess {\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tfirstReceived = true\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tvar sampl fmp4.Sample\n\t\t\t\t\t\terr := sampl.FillAV1(u.Payload.(unit.PayloadAV1))\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn track.write(&formatFMP4Sample{\n\t\t\t\t\t\t\tSample: &sampl,\n\t\t\t\t\t\t\tdts:    u.PTS,\n\t\t\t\t\t\t\tntp:    u.NTP,\n\t\t\t\t\t\t})\n\t\t\t\t\t})\n\n\t\t\tcase *rtspformat.VP9:\n\t\t\t\tcodec := &mcodecs.VP9{\n\t\t\t\t\tWidth:             1280,\n\t\t\t\t\tHeight:            720,\n\t\t\t\t\tProfile:           1,\n\t\t\t\t\tBitDepth:          8,\n\t\t\t\t\tChromaSubsampling: 1,\n\t\t\t\t\tColorRange:        false,\n\t\t\t\t}\n\t\t\t\ttrack := addTrack(forma, codec)\n\n\t\t\t\tfirstReceived := false\n\n\t\t\t\tf.ri.reader.OnData(\n\t\t\t\t\tmedia,\n\t\t\t\t\tforma,\n\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tvar h vp9.Header\n\t\t\t\t\t\terr := h.Unmarshal(u.Payload.(unit.PayloadVP9))\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\trandomAccess := false\n\t\t\t\t\t\tparamsChanged := false\n\n\t\t\t\t\t\tif !h.NonKeyFrame {\n\t\t\t\t\t\t\trandomAccess = true\n\n\t\t\t\t\t\t\tif w := h.Width(); codec.Width != w {\n\t\t\t\t\t\t\t\tcodec.Width = w\n\t\t\t\t\t\t\t\tparamsChanged = true\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif h := h.Width(); codec.Height != h {\n\t\t\t\t\t\t\t\tcodec.Height = h\n\t\t\t\t\t\t\t\tparamsChanged = true\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif codec.Profile != h.Profile {\n\t\t\t\t\t\t\t\tcodec.Profile = h.Profile\n\t\t\t\t\t\t\t\tparamsChanged = true\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif codec.BitDepth != h.ColorConfig.BitDepth {\n\t\t\t\t\t\t\t\tcodec.BitDepth = h.ColorConfig.BitDepth\n\t\t\t\t\t\t\t\tparamsChanged = true\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif c := h.ChromaSubsampling(); codec.ChromaSubsampling != c {\n\t\t\t\t\t\t\t\tcodec.ChromaSubsampling = c\n\t\t\t\t\t\t\t\tparamsChanged = true\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tif codec.ColorRange != h.ColorConfig.ColorRange {\n\t\t\t\t\t\t\t\tcodec.ColorRange = h.ColorConfig.ColorRange\n\t\t\t\t\t\t\t\tparamsChanged = true\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif paramsChanged {\n\t\t\t\t\t\t\tf.updateCodecParams()\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif !firstReceived {\n\t\t\t\t\t\t\tif !randomAccess {\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tfirstReceived = true\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn track.write(&formatFMP4Sample{\n\t\t\t\t\t\t\tSample: &fmp4.Sample{\n\t\t\t\t\t\t\t\tIsNonSyncSample: !randomAccess,\n\t\t\t\t\t\t\t\tPayload:         u.Payload.(unit.PayloadVP9),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tdts: u.PTS,\n\t\t\t\t\t\t\tntp: u.NTP,\n\t\t\t\t\t\t})\n\t\t\t\t\t})\n\n\t\t\tcase *rtspformat.VP8:\n\t\t\t\t// TODO\n\n\t\t\tcase *rtspformat.H265:\n\t\t\t\tvps, sps, pps := forma.SafeParams()\n\t\t\t\tif vps == nil || sps == nil || pps == nil {\n\t\t\t\t\tvps = h265DefaultVPS\n\t\t\t\t\tsps = h265DefaultSPS\n\t\t\t\t\tpps = h265DefaultPPS\n\t\t\t\t}\n\n\t\t\t\tcodec := &mcodecs.H265{\n\t\t\t\t\tVPS: vps,\n\t\t\t\t\tSPS: sps,\n\t\t\t\t\tPPS: pps,\n\t\t\t\t}\n\t\t\t\ttrack := addTrack(forma, codec)\n\n\t\t\t\tvar dtsExtractor *h265.DTSExtractor\n\n\t\t\t\tf.ri.reader.OnData(\n\t\t\t\t\tmedia,\n\t\t\t\t\tforma,\n\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\trandomAccess := false\n\t\t\t\t\t\tparamsChanged := false\n\n\t\t\t\t\t\tfor _, nalu := range u.Payload.(unit.PayloadH265) {\n\t\t\t\t\t\t\ttyp := h265.NALUType((nalu[0] >> 1) & 0b111111)\n\n\t\t\t\t\t\t\tswitch typ {\n\t\t\t\t\t\t\tcase h265.NALUType_VPS_NUT:\n\t\t\t\t\t\t\t\tif !bytes.Equal(codec.VPS, nalu) {\n\t\t\t\t\t\t\t\t\tcodec.VPS = nalu\n\t\t\t\t\t\t\t\t\tparamsChanged = true\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tcase h265.NALUType_SPS_NUT:\n\t\t\t\t\t\t\t\tif !bytes.Equal(codec.SPS, nalu) {\n\t\t\t\t\t\t\t\t\tcodec.SPS = nalu\n\t\t\t\t\t\t\t\t\tparamsChanged = true\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tcase h265.NALUType_PPS_NUT:\n\t\t\t\t\t\t\t\tif !bytes.Equal(codec.PPS, nalu) {\n\t\t\t\t\t\t\t\t\tcodec.PPS = nalu\n\t\t\t\t\t\t\t\t\tparamsChanged = true\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tcase h265.NALUType_IDR_W_RADL, h265.NALUType_IDR_N_LP, h265.NALUType_CRA_NUT:\n\t\t\t\t\t\t\t\trandomAccess = true\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif paramsChanged {\n\t\t\t\t\t\t\tf.updateCodecParams()\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif dtsExtractor == nil {\n\t\t\t\t\t\t\tif !randomAccess {\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tdtsExtractor = &h265.DTSExtractor{}\n\t\t\t\t\t\t\tdtsExtractor.Initialize()\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tdts, err := dtsExtractor.Extract(u.Payload.(unit.PayloadH265), u.PTS)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tvar sampl fmp4.Sample\n\t\t\t\t\t\terr = sampl.FillH265(int32(u.PTS-dts), u.Payload.(unit.PayloadH265))\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn track.write(&formatFMP4Sample{\n\t\t\t\t\t\t\tSample: &sampl,\n\t\t\t\t\t\t\tdts:    dts,\n\t\t\t\t\t\t\tntp:    u.NTP,\n\t\t\t\t\t\t})\n\t\t\t\t\t})\n\n\t\t\tcase *rtspformat.H264:\n\t\t\t\tsps, pps := forma.SafeParams()\n\t\t\t\tif sps == nil || pps == nil {\n\t\t\t\t\tsps = h264DefaultSPS\n\t\t\t\t\tpps = h264DefaultPPS\n\t\t\t\t}\n\n\t\t\t\tcodec := &mcodecs.H264{\n\t\t\t\t\tSPS: sps,\n\t\t\t\t\tPPS: pps,\n\t\t\t\t}\n\t\t\t\ttrack := addTrack(forma, codec)\n\n\t\t\t\tvar dtsExtractor *h264.DTSExtractor\n\n\t\t\t\tf.ri.reader.OnData(\n\t\t\t\t\tmedia,\n\t\t\t\t\tforma,\n\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\trandomAccess := false\n\t\t\t\t\t\tparamsChanged := false\n\n\t\t\t\t\t\tfor _, nalu := range u.Payload.(unit.PayloadH264) {\n\t\t\t\t\t\t\ttyp := h264.NALUType(nalu[0] & 0x1F)\n\t\t\t\t\t\t\tswitch typ {\n\t\t\t\t\t\t\tcase h264.NALUTypeSPS:\n\t\t\t\t\t\t\t\tif !bytes.Equal(codec.SPS, nalu) {\n\t\t\t\t\t\t\t\t\tcodec.SPS = nalu\n\t\t\t\t\t\t\t\t\tparamsChanged = true\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tcase h264.NALUTypePPS:\n\t\t\t\t\t\t\t\tif !bytes.Equal(codec.PPS, nalu) {\n\t\t\t\t\t\t\t\t\tcodec.PPS = nalu\n\t\t\t\t\t\t\t\t\tparamsChanged = true\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tcase h264.NALUTypeIDR:\n\t\t\t\t\t\t\t\trandomAccess = true\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif paramsChanged {\n\t\t\t\t\t\t\tf.updateCodecParams()\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif dtsExtractor == nil {\n\t\t\t\t\t\t\tif !randomAccess {\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tdtsExtractor = &h264.DTSExtractor{}\n\t\t\t\t\t\t\tdtsExtractor.Initialize()\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tdts, err := dtsExtractor.Extract(u.Payload.(unit.PayloadH264), u.PTS)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tvar sampl fmp4.Sample\n\t\t\t\t\t\terr = sampl.FillH264(int32(u.PTS-dts), u.Payload.(unit.PayloadH264))\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn track.write(&formatFMP4Sample{\n\t\t\t\t\t\t\tSample: &sampl,\n\t\t\t\t\t\t\tdts:    dts,\n\t\t\t\t\t\t\tntp:    u.NTP,\n\t\t\t\t\t\t})\n\t\t\t\t\t})\n\n\t\t\tcase *rtspformat.MPEG4Video:\n\t\t\t\tconfig := forma.SafeParams()\n\n\t\t\t\tif config == nil {\n\t\t\t\t\tconfig = mpeg4VideoDefaultConfig\n\t\t\t\t}\n\n\t\t\t\tcodec := &mcodecs.MPEG4Video{\n\t\t\t\t\tConfig: config,\n\t\t\t\t}\n\t\t\t\ttrack := addTrack(forma, codec)\n\n\t\t\t\tfirstReceived := false\n\t\t\t\tvar lastPTS int64\n\n\t\t\t\tf.ri.reader.OnData(\n\t\t\t\t\tmedia,\n\t\t\t\t\tforma,\n\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\trandomAccess := bytes.Contains(u.Payload.(unit.PayloadMPEG4Video),\n\t\t\t\t\t\t\t[]byte{0, 0, 1, byte(mpeg4video.GroupOfVOPStartCode)})\n\n\t\t\t\t\t\tif bytes.HasPrefix(u.Payload.(unit.PayloadMPEG4Video),\n\t\t\t\t\t\t\t[]byte{0, 0, 1, byte(mpeg4video.VisualObjectSequenceStartCode)}) {\n\t\t\t\t\t\t\tend := bytes.Index(u.Payload.(unit.PayloadMPEG4Video)[4:],\n\t\t\t\t\t\t\t\t[]byte{0, 0, 1, byte(mpeg4video.GroupOfVOPStartCode)})\n\t\t\t\t\t\t\tif end >= 0 {\n\t\t\t\t\t\t\t\tconfig2 := u.Payload.(unit.PayloadMPEG4Video)[:end+4]\n\n\t\t\t\t\t\t\t\tif !bytes.Equal(codec.Config, config2) {\n\t\t\t\t\t\t\t\t\tcodec.Config = config2\n\t\t\t\t\t\t\t\t\tf.updateCodecParams()\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif !firstReceived {\n\t\t\t\t\t\t\tif !randomAccess {\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tfirstReceived = true\n\t\t\t\t\t\t} else if u.PTS < lastPTS {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"MPEG-4 Video streams with B-frames are not supported (yet)\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tlastPTS = u.PTS\n\n\t\t\t\t\t\treturn track.write(&formatFMP4Sample{\n\t\t\t\t\t\t\tSample: &fmp4.Sample{\n\t\t\t\t\t\t\t\tPayload:         u.Payload.(unit.PayloadMPEG4Video),\n\t\t\t\t\t\t\t\tIsNonSyncSample: !randomAccess,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tdts: u.PTS,\n\t\t\t\t\t\t\tntp: u.NTP,\n\t\t\t\t\t\t})\n\t\t\t\t\t})\n\n\t\t\tcase *rtspformat.MPEG1Video:\n\t\t\t\tcodec := &mcodecs.MPEG1Video{\n\t\t\t\t\tConfig: mpeg1VideoDefaultConfig,\n\t\t\t\t}\n\t\t\t\ttrack := addTrack(forma, codec)\n\n\t\t\t\tfirstReceived := false\n\t\t\t\tvar lastPTS int64\n\n\t\t\t\tf.ri.reader.OnData(\n\t\t\t\t\tmedia,\n\t\t\t\t\tforma,\n\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\trandomAccess := bytes.Contains(u.Payload.(unit.PayloadMPEG1Video), []byte{0, 0, 1, 0xB8})\n\n\t\t\t\t\t\tif bytes.HasPrefix(u.Payload.(unit.PayloadMPEG1Video), []byte{0, 0, 1, 0xB3}) {\n\t\t\t\t\t\t\tend := bytes.Index(u.Payload.(unit.PayloadMPEG1Video)[4:], []byte{0, 0, 1, 0xB8})\n\t\t\t\t\t\t\tif end >= 0 {\n\t\t\t\t\t\t\t\tconfig := u.Payload.(unit.PayloadMPEG1Video)[:end+4]\n\n\t\t\t\t\t\t\t\tif !bytes.Equal(codec.Config, config) {\n\t\t\t\t\t\t\t\t\tcodec.Config = config\n\t\t\t\t\t\t\t\t\tf.updateCodecParams()\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif !firstReceived {\n\t\t\t\t\t\t\tif !randomAccess {\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tfirstReceived = true\n\t\t\t\t\t\t} else if u.PTS < lastPTS {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"MPEG-1 Video streams with B-frames are not supported (yet)\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tlastPTS = u.PTS\n\n\t\t\t\t\t\treturn track.write(&formatFMP4Sample{\n\t\t\t\t\t\t\tSample: &fmp4.Sample{\n\t\t\t\t\t\t\t\tPayload:         u.Payload.(unit.PayloadMPEG1Video),\n\t\t\t\t\t\t\t\tIsNonSyncSample: !randomAccess,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tdts: u.PTS,\n\t\t\t\t\t\t\tntp: u.NTP,\n\t\t\t\t\t\t})\n\t\t\t\t\t})\n\n\t\t\tcase *rtspformat.MJPEG:\n\t\t\t\tcodec := &mcodecs.MJPEG{\n\t\t\t\t\tWidth:  800,\n\t\t\t\t\tHeight: 600,\n\t\t\t\t}\n\t\t\t\ttrack := addTrack(forma, codec)\n\n\t\t\t\tparsed := false\n\n\t\t\t\tf.ri.reader.OnData(\n\t\t\t\t\tmedia,\n\t\t\t\t\tforma,\n\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif !parsed {\n\t\t\t\t\t\t\tparsed = true\n\t\t\t\t\t\t\twidth, height, err := jpegExtractSize(u.Payload.(unit.PayloadMJPEG))\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tcodec.Width = width\n\t\t\t\t\t\t\tcodec.Height = height\n\t\t\t\t\t\t\tf.updateCodecParams()\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn track.write(&formatFMP4Sample{\n\t\t\t\t\t\t\tSample: &fmp4.Sample{\n\t\t\t\t\t\t\t\tPayload: u.Payload.(unit.PayloadMJPEG),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tdts: u.PTS,\n\t\t\t\t\t\t\tntp: u.NTP,\n\t\t\t\t\t\t})\n\t\t\t\t\t})\n\n\t\t\tcase *rtspformat.Opus:\n\t\t\t\tcodec := &mcodecs.Opus{\n\t\t\t\t\tChannelCount: forma.ChannelCount,\n\t\t\t\t}\n\t\t\t\ttrack := addTrack(forma, codec)\n\n\t\t\t\tf.ri.reader.OnData(\n\t\t\t\t\tmedia,\n\t\t\t\t\tforma,\n\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tpts := u.PTS\n\n\t\t\t\t\t\tfor _, packet := range u.Payload.(unit.PayloadOpus) {\n\t\t\t\t\t\t\terr := track.write(&formatFMP4Sample{\n\t\t\t\t\t\t\t\tSample: &fmp4.Sample{\n\t\t\t\t\t\t\t\t\tPayload: packet,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tdts: pts,\n\t\t\t\t\t\t\t\tntp: u.NTP.Add(timestampToDuration(pts-u.PTS, clockRate)),\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tpts += opus.PacketDuration2(packet)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t})\n\n\t\t\tcase *rtspformat.MPEG4Audio:\n\t\t\t\tcodec := &mcodecs.MPEG4Audio{\n\t\t\t\t\tConfig: *forma.Config,\n\t\t\t\t}\n\t\t\t\ttrack := addTrack(forma, codec)\n\n\t\t\t\tf.ri.reader.OnData(\n\t\t\t\t\tmedia,\n\t\t\t\t\tforma,\n\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tfor i, au := range u.Payload.(unit.PayloadMPEG4Audio) {\n\t\t\t\t\t\t\tpts := u.PTS + int64(i)*mpeg4audio.SamplesPerAccessUnit\n\n\t\t\t\t\t\t\terr := track.write(&formatFMP4Sample{\n\t\t\t\t\t\t\t\tSample: &fmp4.Sample{\n\t\t\t\t\t\t\t\t\tPayload: au,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tdts: pts,\n\t\t\t\t\t\t\t\tntp: u.NTP.Add(timestampToDuration(pts-u.PTS, clockRate)),\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t})\n\n\t\t\tcase *rtspformat.MPEG4AudioLATM:\n\t\t\t\tif !forma.CPresent {\n\t\t\t\t\tcodec := &mcodecs.MPEG4Audio{\n\t\t\t\t\t\tConfig: *forma.StreamMuxConfig.Programs[0].Layers[0].AudioSpecificConfig,\n\t\t\t\t\t}\n\t\t\t\t\ttrack := addTrack(forma, codec)\n\n\t\t\t\t\tf.ri.reader.OnData(\n\t\t\t\t\t\tmedia,\n\t\t\t\t\t\tforma,\n\t\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tvar ame mpeg4audio.AudioMuxElement\n\t\t\t\t\t\t\tame.StreamMuxConfig = forma.StreamMuxConfig\n\t\t\t\t\t\t\terr := ame.Unmarshal(u.Payload.(unit.PayloadMPEG4AudioLATM))\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\treturn track.write(&formatFMP4Sample{\n\t\t\t\t\t\t\t\tSample: &fmp4.Sample{\n\t\t\t\t\t\t\t\t\tPayload: ame.Payloads[0][0][0],\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tdts: u.PTS,\n\t\t\t\t\t\t\t\tntp: u.NTP,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\tcase *rtspformat.MPEG1Audio:\n\t\t\t\tcodec := &mcodecs.MPEG1Audio{\n\t\t\t\t\tSampleRate:   32000,\n\t\t\t\t\tChannelCount: 2,\n\t\t\t\t}\n\t\t\t\ttrack := addTrack(forma, codec)\n\n\t\t\t\tparsed := false\n\n\t\t\t\tf.ri.reader.OnData(\n\t\t\t\t\tmedia,\n\t\t\t\t\tforma,\n\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tvar dt time.Duration\n\n\t\t\t\t\t\tfor _, frame := range u.Payload.(unit.PayloadMPEG1Audio) {\n\t\t\t\t\t\t\tvar h mpeg1audio.FrameHeader\n\t\t\t\t\t\t\terr := h.Unmarshal(frame)\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif !parsed {\n\t\t\t\t\t\t\t\tparsed = true\n\t\t\t\t\t\t\t\tcodec.SampleRate = h.SampleRate\n\t\t\t\t\t\t\t\tcodec.ChannelCount = mpeg1audioChannelCount(h.ChannelMode)\n\t\t\t\t\t\t\t\tf.updateCodecParams()\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\terr = track.write(&formatFMP4Sample{\n\t\t\t\t\t\t\t\tSample: &fmp4.Sample{\n\t\t\t\t\t\t\t\t\tPayload: frame,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tdts: u.PTS + u.PTS,\n\t\t\t\t\t\t\t\tntp: u.NTP,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tdt += time.Duration(h.SampleCount()) *\n\t\t\t\t\t\t\t\ttime.Second / time.Duration(h.SampleRate)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t})\n\n\t\t\tcase *rtspformat.AC3:\n\t\t\t\tcodec := &mcodecs.AC3{\n\t\t\t\t\tSampleRate:   forma.SampleRate,\n\t\t\t\t\tChannelCount: forma.ChannelCount,\n\t\t\t\t\tFscod:        0,\n\t\t\t\t\tBsid:         8,\n\t\t\t\t\tBsmod:        0,\n\t\t\t\t\tAcmod:        7,\n\t\t\t\t\tLfeOn:        true,\n\t\t\t\t\tBitRateCode:  7,\n\t\t\t\t}\n\t\t\t\ttrack := addTrack(forma, codec)\n\n\t\t\t\tparsed := false\n\n\t\t\t\tf.ri.reader.OnData(\n\t\t\t\t\tmedia,\n\t\t\t\t\tforma,\n\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tfor i, frame := range u.Payload.(unit.PayloadAC3) {\n\t\t\t\t\t\t\tvar syncInfo ac3.SyncInfo\n\t\t\t\t\t\t\terr := syncInfo.Unmarshal(frame)\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\treturn fmt.Errorf(\"invalid AC-3 frame: %w\", err)\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tvar bsi ac3.BSI\n\t\t\t\t\t\t\terr = bsi.Unmarshal(frame[5:])\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\treturn fmt.Errorf(\"invalid AC-3 frame: %w\", err)\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif !parsed {\n\t\t\t\t\t\t\t\tparsed = true\n\t\t\t\t\t\t\t\tcodec.SampleRate = syncInfo.SampleRate()\n\t\t\t\t\t\t\t\tcodec.ChannelCount = bsi.ChannelCount()\n\t\t\t\t\t\t\t\tcodec.Bsid = bsi.Bsid\n\t\t\t\t\t\t\t\tcodec.Bsmod = bsi.Bsmod\n\t\t\t\t\t\t\t\tcodec.Acmod = bsi.Acmod\n\t\t\t\t\t\t\t\tcodec.LfeOn = bsi.LfeOn\n\t\t\t\t\t\t\t\tcodec.BitRateCode = syncInfo.Frmsizecod >> 1\n\t\t\t\t\t\t\t\tf.updateCodecParams()\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tpts := u.PTS + int64(i)*ac3.SamplesPerFrame\n\n\t\t\t\t\t\t\terr = track.write(&formatFMP4Sample{\n\t\t\t\t\t\t\t\tSample: &fmp4.Sample{\n\t\t\t\t\t\t\t\t\tPayload: frame,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tdts: pts,\n\t\t\t\t\t\t\t\tntp: u.NTP.Add(timestampToDuration(pts-u.PTS, clockRate)),\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn nil\n\t\t\t\t\t})\n\n\t\t\tcase *rtspformat.G722:\n\t\t\t\t// TODO\n\n\t\t\tcase *rtspformat.G711:\n\t\t\t\tcodec := &mcodecs.LPCM{\n\t\t\t\t\tLittleEndian: false,\n\t\t\t\t\tBitDepth:     16,\n\t\t\t\t\tSampleRate:   forma.SampleRate,\n\t\t\t\t\tChannelCount: forma.ChannelCount,\n\t\t\t\t}\n\t\t\t\ttrack := addTrack(forma, codec)\n\n\t\t\t\tf.ri.reader.OnData(\n\t\t\t\t\tmedia,\n\t\t\t\t\tforma,\n\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tvar lpcm []byte\n\t\t\t\t\t\tif forma.MULaw {\n\t\t\t\t\t\t\tvar mu g711.Mulaw\n\t\t\t\t\t\t\tmu.Unmarshal(u.Payload.(unit.PayloadG711))\n\t\t\t\t\t\t\tlpcm = mu\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tvar al g711.Alaw\n\t\t\t\t\t\t\tal.Unmarshal(u.Payload.(unit.PayloadG711))\n\t\t\t\t\t\t\tlpcm = al\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn track.write(&formatFMP4Sample{\n\t\t\t\t\t\t\tSample: &fmp4.Sample{\n\t\t\t\t\t\t\t\tPayload: lpcm,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tdts: u.PTS,\n\t\t\t\t\t\t\tntp: u.NTP,\n\t\t\t\t\t\t})\n\t\t\t\t\t})\n\n\t\t\tcase *rtspformat.LPCM:\n\t\t\t\tcodec := &mcodecs.LPCM{\n\t\t\t\t\tLittleEndian: false,\n\t\t\t\t\tBitDepth:     forma.BitDepth,\n\t\t\t\t\tSampleRate:   forma.SampleRate,\n\t\t\t\t\tChannelCount: forma.ChannelCount,\n\t\t\t\t}\n\t\t\t\ttrack := addTrack(forma, codec)\n\n\t\t\t\tf.ri.reader.OnData(\n\t\t\t\t\tmedia,\n\t\t\t\t\tforma,\n\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn track.write(&formatFMP4Sample{\n\t\t\t\t\t\t\tSample: &fmp4.Sample{\n\t\t\t\t\t\t\t\tPayload: u.Payload.(unit.PayloadLPCM),\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tdts: u.PTS,\n\t\t\t\t\t\t\tntp: u.NTP,\n\t\t\t\t\t\t})\n\t\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(f.tracks) == 0 {\n\t\tf.ri.Log(logger.Warn, \"no supported tracks found, skipping recording\")\n\t\treturn false\n\t}\n\n\tsetuppedFormats := f.ri.reader.Formats()\n\n\tn := 1\n\tfor _, medi := range f.ri.stream.Desc.Medias {\n\t\tfor _, forma := range medi.Formats {\n\t\t\tif !slices.Contains(setuppedFormats, forma) {\n\t\t\t\tf.ri.Log(logger.Warn, \"skipping track %d (%s)\", n, forma.Codec())\n\t\t\t}\n\t\t\tn++\n\t\t}\n\t}\n\n\tf.ri.Log(logger.Info, \"recording %s\",\n\t\tdefs.FormatsInfo(setuppedFormats))\n\n\treturn true\n}\n\nfunc (f *formatFMP4) updateCodecParams() {\n\tf.ri.Log(logger.Debug, \"codec parameters have changed\")\n}\n\nfunc (f *formatFMP4) close() {\n\tif f.currentSegment != nil {\n\t\tf.currentSegment.close() //nolint:errcheck\n\t}\n}\n"
  },
  {
    "path": "internal/recorder/format_fmp4_part.go",
    "content": "package recorder\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/fmp4\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/fmp4/seekablebuffer\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n)\n\nfunc writePart(\n\tf io.Writer,\n\tsequenceNumber uint32,\n\tpartTracks map[*formatFMP4Track]*fmp4.PartTrack,\n) error {\n\tfmp4PartTracks := make([]*fmp4.PartTrack, len(partTracks))\n\ti := 0\n\tfor _, partTrack := range partTracks {\n\t\tfmp4PartTracks[i] = partTrack\n\t\ti++\n\t}\n\n\tpart := &fmp4.Part{\n\t\tSequenceNumber: sequenceNumber,\n\t\tTracks:         fmp4PartTracks,\n\t}\n\n\tvar buf seekablebuffer.Buffer\n\terr := part.Marshal(&buf)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = f.Write(buf.Bytes())\n\treturn err\n}\n\ntype formatFMP4Part struct {\n\tmaxPartSize     conf.StringSize\n\tsegmentStartDTS time.Duration\n\tnumber          uint32\n\tstartDTS        time.Duration\n\n\tpartTracks map[*formatFMP4Track]*fmp4.PartTrack\n\tsize       uint64\n\tendDTS     time.Duration\n}\n\nfunc (p *formatFMP4Part) initialize() {\n\tp.partTracks = make(map[*formatFMP4Track]*fmp4.PartTrack)\n}\n\nfunc (p *formatFMP4Part) close(w io.Writer) error {\n\treturn writePart(w, p.number, p.partTracks)\n}\n\nfunc (p *formatFMP4Part) write(track *formatFMP4Track, sample *formatFMP4Sample, dts time.Duration) error {\n\tsize := uint64(len(sample.Payload))\n\tif (p.size + size) > uint64(p.maxPartSize) {\n\t\treturn fmt.Errorf(\"reached maximum part size\")\n\t}\n\tp.size += size\n\n\tpartTrack, ok := p.partTracks[track]\n\tif !ok {\n\t\tpartTrack = &fmp4.PartTrack{\n\t\t\tID: track.initTrack.ID,\n\t\t\tBaseTime: uint64(multiplyAndDivide(int64(dts-p.segmentStartDTS),\n\t\t\t\tint64(track.initTrack.TimeScale), int64(time.Second))),\n\t\t}\n\t\tp.partTracks[track] = partTrack\n\t}\n\n\tpartTrack.Samples = append(partTrack.Samples, sample.Sample)\n\n\tendDTS := dts + timestampToDuration(int64(sample.Duration), int(track.initTrack.TimeScale))\n\tif endDTS > p.endDTS {\n\t\tp.endDTS = endDTS\n\t}\n\n\treturn nil\n}\n\nfunc (p *formatFMP4Part) duration() time.Duration {\n\treturn p.endDTS - p.startDTS\n}\n"
  },
  {
    "path": "internal/recorder/format_fmp4_segment.go",
    "content": "package recorder\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\tamp4 \"github.com/abema/go-mp4\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/fmp4\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/fmp4/seekablebuffer\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/recordstore\"\n\t\"github.com/google/uuid\"\n)\n\nfunc writeInit(\n\tf io.Writer,\n\tstreamID uuid.UUID,\n\tsegmentNumber uint64,\n\tdts time.Duration,\n\tntp time.Time,\n\ttracks []*formatFMP4Track,\n) error {\n\tfmp4Tracks := make([]*fmp4.InitTrack, len(tracks))\n\tfor i, track := range tracks {\n\t\tfmp4Tracks[i] = track.initTrack\n\t}\n\n\tinit := fmp4.Init{\n\t\tTracks: fmp4Tracks,\n\t\tUserData: []amp4.IBox{\n\t\t\t&recordstore.Mtxi{\n\t\t\t\tFullBox: amp4.FullBox{\n\t\t\t\t\tVersion: 0,\n\t\t\t\t},\n\t\t\t\tStreamID:      streamID,\n\t\t\t\tSegmentNumber: segmentNumber,\n\t\t\t\tDTS:           int64(dts),\n\t\t\t\tNTP:           ntp.UnixNano(),\n\t\t\t},\n\t\t},\n\t}\n\n\tvar buf seekablebuffer.Buffer\n\terr := init.Marshal(&buf)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = f.Write(buf.Bytes())\n\treturn err\n}\n\nfunc writeDuration(f io.ReadWriteSeeker, d time.Duration) error {\n\t_, err := f.Seek(0, io.SeekStart)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// check and skip ftyp header and content\n\n\tbuf := make([]byte, 8)\n\t_, err = io.ReadFull(f, buf)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !bytes.Equal(buf[4:], []byte{'f', 't', 'y', 'p'}) {\n\t\treturn fmt.Errorf(\"ftyp box not found\")\n\t}\n\n\tftypSize := uint32(buf[0])<<24 | uint32(buf[1])<<16 | uint32(buf[2])<<8 | uint32(buf[3])\n\n\t_, err = f.Seek(int64(ftypSize), io.SeekStart)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// check and skip moov header\n\n\t_, err = io.ReadFull(f, buf)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !bytes.Equal(buf[4:], []byte{'m', 'o', 'o', 'v'}) {\n\t\treturn fmt.Errorf(\"moov box not found\")\n\t}\n\n\tmoovSize := uint32(buf[0])<<24 | uint32(buf[1])<<16 | uint32(buf[2])<<8 | uint32(buf[3])\n\n\tmoovPos, err := f.Seek(8, io.SeekCurrent)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar mvhd amp4.Mvhd\n\t_, err = amp4.Unmarshal(f, uint64(moovSize-8), &mvhd, amp4.Context{})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tmvhd.DurationV0 = uint32(d / time.Millisecond)\n\n\t_, err = f.Seek(moovPos, io.SeekStart)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = amp4.Marshal(f, &mvhd, amp4.Context{})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\ntype formatFMP4Segment struct {\n\tf        *formatFMP4\n\tstartDTS time.Duration\n\tstartNTP time.Time\n\tnumber   uint64\n\n\tpath           string\n\tfi             *os.File\n\tcurPart        *formatFMP4Part\n\tendDTS         time.Duration\n\tnextPartNumber uint32\n}\n\nfunc (s *formatFMP4Segment) initialize() {\n\ts.endDTS = s.startDTS\n}\n\nfunc (s *formatFMP4Segment) close() error {\n\tvar err error\n\n\tif s.curPart != nil {\n\t\terr = s.closeCurPart()\n\t}\n\n\tif s.fi != nil {\n\t\ts.f.ri.Log(logger.Debug, \"closing segment %s\", s.path)\n\n\t\t// write overall duration in the header to speed up the playback server\n\t\tduration := s.endDTS - s.startDTS\n\t\terr2 := writeDuration(s.fi, duration)\n\t\tif err == nil {\n\t\t\terr = err2\n\t\t}\n\n\t\terr2 = s.fi.Close()\n\t\tif err == nil {\n\t\t\terr = err2\n\t\t}\n\n\t\tif err2 == nil {\n\t\t\ts.f.ri.onSegmentComplete(s.path, duration)\n\t\t}\n\t}\n\n\treturn err\n}\n\nfunc (s *formatFMP4Segment) closeCurPart() error {\n\tif s.fi == nil {\n\t\ts.path = recordstore.Path{Start: s.startNTP}.Encode(s.f.ri.pathFormat2)\n\t\ts.f.ri.Log(logger.Debug, \"creating segment %s\", s.path)\n\n\t\terr := os.MkdirAll(filepath.Dir(s.path), 0o755)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfi, err := os.Create(s.path)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\ts.f.ri.onSegmentCreate(s.path)\n\n\t\terr = writeInit(\n\t\t\tfi,\n\t\t\ts.f.ri.streamID,\n\t\t\ts.number,\n\t\t\ts.startDTS,\n\t\t\ts.startNTP,\n\t\t\ts.f.tracks)\n\t\tif err != nil {\n\t\t\tfi.Close()\n\t\t\treturn err\n\t\t}\n\n\t\ts.fi = fi\n\t}\n\n\treturn s.curPart.close(s.fi)\n}\n\nfunc (s *formatFMP4Segment) write(track *formatFMP4Track, sample *formatFMP4Sample, dts time.Duration) error {\n\tendDTS := dts + timestampToDuration(int64(sample.Duration), int(track.initTrack.TimeScale))\n\tif endDTS > s.endDTS {\n\t\ts.endDTS = endDTS\n\t}\n\n\tif s.curPart == nil {\n\t\ts.curPart = &formatFMP4Part{\n\t\t\tmaxPartSize:     s.f.ri.maxPartSize,\n\t\t\tsegmentStartDTS: s.startDTS,\n\t\t\tnumber:          s.nextPartNumber,\n\t\t\tstartDTS:        dts,\n\t\t}\n\t\ts.curPart.initialize()\n\t\ts.nextPartNumber++\n\t} else if s.curPart.duration() >= s.f.ri.partDuration {\n\t\terr := s.closeCurPart()\n\t\ts.curPart = nil\n\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\ts.curPart = &formatFMP4Part{\n\t\t\tmaxPartSize:     s.f.ri.maxPartSize,\n\t\t\tsegmentStartDTS: s.startDTS,\n\t\t\tnumber:          s.nextPartNumber,\n\t\t\tstartDTS:        dts,\n\t\t}\n\t\ts.curPart.initialize()\n\t\ts.nextPartNumber++\n\t}\n\n\treturn s.curPart.write(track, sample, dts)\n}\n"
  },
  {
    "path": "internal/recorder/format_fmp4_track.go",
    "content": "package recorder\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/fmp4\"\n\tmcodecs \"github.com/bluenviron/mediacommon/v2/pkg/formats/mp4/codecs\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n)\n\nconst (\n\t// this corresponds to concatenationTolerance\n\tmaxBasetime = 1 * time.Second\n)\n\n// start next segment from the oldest next sample, in order to avoid negative basetimes (impossible) in fMP4.\n// keep starting position within a certain distance from the newest next sample to avoid big basetimes.\nfunc nextSegmentStartingPos(tracks []*formatFMP4Track) (time.Time, time.Duration) {\n\tvar maxDTS time.Duration\n\tfor _, track := range tracks {\n\t\tif track.nextSample != nil {\n\t\t\tdts := timestampToDuration(track.nextSample.dts, int(track.initTrack.TimeScale))\n\t\t\tif dts > maxDTS {\n\t\t\t\tmaxDTS = dts\n\t\t\t}\n\t\t}\n\t}\n\n\tvar oldestNTP time.Time\n\toldestDTS := maxDTS\n\n\tfor _, track := range tracks {\n\t\tif track.nextSample != nil {\n\t\t\tdts := timestampToDuration(track.nextSample.dts, int(track.initTrack.TimeScale))\n\t\t\tif (maxDTS-dts) <= maxBasetime && (dts <= oldestDTS) {\n\t\t\t\toldestNTP = track.nextSample.ntp\n\t\t\t\toldestDTS = dts\n\t\t\t}\n\t\t}\n\t}\n\n\treturn oldestNTP, oldestDTS\n}\n\ntype formatFMP4Track struct {\n\tf         *formatFMP4\n\tid        int\n\tclockRate uint32\n\tcodec     mcodecs.Codec\n\n\tinitTrack        *fmp4.InitTrack\n\tnextSample       *formatFMP4Sample\n\tstartInitialized bool\n\tstartDTS         time.Duration\n\tstartNTP         time.Time\n}\n\nfunc (t *formatFMP4Track) initialize() {\n\tt.initTrack = &fmp4.InitTrack{\n\t\tID:        t.id,\n\t\tTimeScale: t.clockRate,\n\t\tCodec:     t.codec,\n\t}\n}\n\nfunc (t *formatFMP4Track) write(sample *formatFMP4Sample) error {\n\t// wait the first video sample before setting hasVideo\n\tif t.initTrack.Codec.IsVideo() {\n\t\tt.f.hasVideo = true\n\t}\n\n\tsample, t.nextSample = t.nextSample, sample\n\tif sample == nil {\n\t\treturn nil\n\t}\n\n\tduration := t.nextSample.dts - sample.dts\n\tif duration < 0 {\n\t\tt.nextSample.dts = sample.dts\n\t\tduration = 0\n\t}\n\n\tsample.Duration = uint32(duration)\n\n\tdts := timestampToDuration(sample.dts, int(t.initTrack.TimeScale))\n\n\tif !t.startInitialized {\n\t\tt.startDTS = dts\n\t\tt.startNTP = sample.ntp\n\t\tt.startInitialized = true\n\t} else {\n\t\tdrift := sample.ntp.Sub(t.startNTP) - (dts - t.startDTS)\n\t\tif drift < -ntpDriftTolerance || drift > ntpDriftTolerance {\n\t\t\treturn fmt.Errorf(\"detected drift between recording duration and absolute time, resetting\")\n\t\t}\n\t}\n\n\tif t.f.currentSegment == nil {\n\t\tt.f.currentSegment = &formatFMP4Segment{\n\t\t\tf:        t.f,\n\t\t\tstartDTS: dts,\n\t\t\tstartNTP: sample.ntp,\n\t\t\tnumber:   t.f.nextSegmentNumber,\n\t\t}\n\t\tt.f.currentSegment.initialize()\n\t\tt.f.nextSegmentNumber++\n\t} else if (dts - t.f.currentSegment.startDTS) < 0 { // BaseTime is negative, this is not supported by fMP4\n\t\tt.f.ri.Log(logger.Warn, \"sample of track %d received too late, discarding\", t.initTrack.ID)\n\t\treturn nil\n\t}\n\n\terr := t.f.currentSegment.write(t, sample, dts)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tnextDTS := timestampToDuration(t.nextSample.dts, int(t.initTrack.TimeScale))\n\n\tif (!t.f.hasVideo || t.initTrack.Codec.IsVideo()) &&\n\t\t!t.nextSample.IsNonSyncSample &&\n\t\t(nextDTS-t.f.currentSegment.startDTS) >= t.f.ri.segmentDuration {\n\t\terr = t.f.currentSegment.close()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\toldestNTP, oldestDTS := nextSegmentStartingPos(t.f.tracks)\n\n\t\tt.f.currentSegment = &formatFMP4Segment{\n\t\t\tf:        t.f,\n\t\t\tstartDTS: oldestDTS,\n\t\t\tstartNTP: oldestNTP,\n\t\t\tnumber:   t.f.nextSegmentNumber,\n\t\t}\n\t\tt.f.currentSegment.initialize()\n\t\tt.f.nextSegmentNumber++\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/recorder/format_mpegts.go",
    "content": "package recorder\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"io\"\n\t\"slices\"\n\t\"time\"\n\n\trtspformat \"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/ac3\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/h264\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/h265\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4video\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts\"\n\ttscodecs \"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts/codecs\"\n\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n)\n\nconst (\n\tmpegtsBufferSize = 64 * 1024\n)\n\nfunc multiplyAndDivide(v, m, d int64) int64 {\n\tsecs := v / d\n\tdec := v % d\n\treturn (secs*m + dec*m/d)\n}\n\nfunc multiplyAndDivide2(v, m, d time.Duration) time.Duration {\n\tsecs := v / d\n\tdec := v % d\n\treturn (secs*m + dec*m/d)\n}\n\nfunc timestampToDuration(t int64, clockRate int) time.Duration {\n\treturn multiplyAndDivide2(time.Duration(t), time.Second, time.Duration(clockRate))\n}\n\ntype dynamicWriter struct {\n\tw io.Writer\n}\n\nfunc (d *dynamicWriter) Write(p []byte) (int, error) {\n\treturn d.w.Write(p)\n}\n\nfunc (d *dynamicWriter) setTarget(w io.Writer) {\n\td.w = w\n}\n\ntype formatMPEGTS struct {\n\tri *recorderInstance\n\n\tdw             *dynamicWriter\n\tbw             *bufio.Writer\n\tmw             *mpegts.Writer\n\thasVideo       bool\n\tcurrentSegment *formatMPEGTSSegment\n}\n\nfunc (f *formatMPEGTS) initialize() bool {\n\tvar tracks []*mpegts.Track\n\n\taddTrack := func(codec tscodecs.Codec) *formatMPEGTSTrack {\n\t\ttrack := &formatMPEGTSTrack{\n\t\t\tf:     f,\n\t\t\tcodec: codec,\n\t\t}\n\t\ttrack.initialize()\n\n\t\ttracks = append(tracks, track.track)\n\t\treturn track\n\t}\n\n\tfor _, media := range f.ri.stream.Desc.Medias {\n\t\tfor _, forma := range media.Formats {\n\t\t\tclockRate := forma.ClockRate()\n\n\t\t\tswitch forma := forma.(type) {\n\t\t\tcase *rtspformat.H265: //nolint:dupl\n\t\t\t\ttrack := addTrack(&tscodecs.H265{})\n\n\t\t\t\tvar dtsExtractor *h265.DTSExtractor\n\n\t\t\t\tf.ri.reader.OnData(\n\t\t\t\t\tmedia,\n\t\t\t\t\tforma,\n\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\trandomAccess := h265.IsRandomAccess(u.Payload.(unit.PayloadH265))\n\n\t\t\t\t\t\tif dtsExtractor == nil {\n\t\t\t\t\t\t\tif !randomAccess {\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tdtsExtractor = &h265.DTSExtractor{}\n\t\t\t\t\t\t\tdtsExtractor.Initialize()\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tdts, err := dtsExtractor.Extract(u.Payload.(unit.PayloadH265), u.PTS)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn track.write(\n\t\t\t\t\t\t\ttimestampToDuration(dts, clockRate),\n\t\t\t\t\t\t\tu.NTP,\n\t\t\t\t\t\t\trandomAccess,\n\t\t\t\t\t\t\tfunc(mtrack *mpegts.Track) error {\n\t\t\t\t\t\t\t\treturn f.mw.WriteH265(\n\t\t\t\t\t\t\t\t\tmtrack,\n\t\t\t\t\t\t\t\t\tu.PTS, // no conversion is needed since clock rate is 90khz in both MPEG-TS and RTSP\n\t\t\t\t\t\t\t\t\tdts,\n\t\t\t\t\t\t\t\t\tu.Payload.(unit.PayloadH265))\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t)\n\t\t\t\t\t})\n\n\t\t\tcase *rtspformat.H264: //nolint:dupl\n\t\t\t\ttrack := addTrack(&tscodecs.H264{})\n\n\t\t\t\tvar dtsExtractor *h264.DTSExtractor\n\n\t\t\t\tf.ri.reader.OnData(\n\t\t\t\t\tmedia,\n\t\t\t\t\tforma,\n\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\trandomAccess := h264.IsRandomAccess(u.Payload.(unit.PayloadH264))\n\n\t\t\t\t\t\tif dtsExtractor == nil {\n\t\t\t\t\t\t\tif !randomAccess {\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tdtsExtractor = &h264.DTSExtractor{}\n\t\t\t\t\t\t\tdtsExtractor.Initialize()\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tdts, err := dtsExtractor.Extract(u.Payload.(unit.PayloadH264), u.PTS)\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn track.write(\n\t\t\t\t\t\t\ttimestampToDuration(dts, clockRate),\n\t\t\t\t\t\t\tu.NTP,\n\t\t\t\t\t\t\trandomAccess,\n\t\t\t\t\t\t\tfunc(mtrack *mpegts.Track) error {\n\t\t\t\t\t\t\t\treturn f.mw.WriteH264(\n\t\t\t\t\t\t\t\t\tmtrack,\n\t\t\t\t\t\t\t\t\tu.PTS, // no conversion is needed since clock rate is 90khz in both MPEG-TS and RTSP\n\t\t\t\t\t\t\t\t\tdts,\n\t\t\t\t\t\t\t\t\tu.Payload.(unit.PayloadH264))\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t)\n\t\t\t\t\t})\n\n\t\t\tcase *rtspformat.MPEG4Video:\n\t\t\t\ttrack := addTrack(&tscodecs.MPEG4Video{})\n\n\t\t\t\tfirstReceived := false\n\t\t\t\tvar lastPTS int64\n\n\t\t\t\tf.ri.reader.OnData(\n\t\t\t\t\tmedia,\n\t\t\t\t\tforma,\n\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif !firstReceived {\n\t\t\t\t\t\t\tfirstReceived = true\n\t\t\t\t\t\t} else if u.PTS < lastPTS {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"MPEG-4 Video streams with B-frames are not supported (yet)\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tlastPTS = u.PTS\n\n\t\t\t\t\t\trandomAccess := bytes.Contains(u.Payload.(unit.PayloadMPEG4Video),\n\t\t\t\t\t\t\t[]byte{0, 0, 1, byte(mpeg4video.GroupOfVOPStartCode)})\n\n\t\t\t\t\t\treturn track.write(\n\t\t\t\t\t\t\ttimestampToDuration(u.PTS, clockRate),\n\t\t\t\t\t\t\tu.NTP,\n\t\t\t\t\t\t\trandomAccess,\n\t\t\t\t\t\t\tfunc(mtrack *mpegts.Track) error {\n\t\t\t\t\t\t\t\treturn f.mw.WriteMPEG4Video(\n\t\t\t\t\t\t\t\t\tmtrack,\n\t\t\t\t\t\t\t\t\tu.PTS, // no conversion is needed since clock rate is 90khz in both MPEG-TS and RTSP\n\t\t\t\t\t\t\t\t\tu.Payload.(unit.PayloadMPEG4Video))\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t)\n\t\t\t\t\t})\n\n\t\t\tcase *rtspformat.MPEG1Video:\n\t\t\t\ttrack := addTrack(&tscodecs.MPEG1Video{})\n\n\t\t\t\tfirstReceived := false\n\t\t\t\tvar lastPTS int64\n\n\t\t\t\tf.ri.reader.OnData(\n\t\t\t\t\tmedia,\n\t\t\t\t\tforma,\n\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif !firstReceived {\n\t\t\t\t\t\t\tfirstReceived = true\n\t\t\t\t\t\t} else if u.PTS < lastPTS {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"MPEG-1 Video streams with B-frames are not supported (yet)\")\n\t\t\t\t\t\t}\n\t\t\t\t\t\tlastPTS = u.PTS\n\n\t\t\t\t\t\trandomAccess := bytes.Contains(u.Payload.(unit.PayloadMPEG1Video), []byte{0, 0, 1, 0xB8})\n\n\t\t\t\t\t\treturn track.write(\n\t\t\t\t\t\t\ttimestampToDuration(u.PTS, clockRate),\n\t\t\t\t\t\t\tu.NTP,\n\t\t\t\t\t\t\trandomAccess,\n\t\t\t\t\t\t\tfunc(mtrack *mpegts.Track) error {\n\t\t\t\t\t\t\t\treturn f.mw.WriteMPEG1Video(\n\t\t\t\t\t\t\t\t\tmtrack,\n\t\t\t\t\t\t\t\t\tu.PTS, // no conversion is needed since clock rate is 90khz in both MPEG-TS and RTSP\n\t\t\t\t\t\t\t\t\tu.Payload.(unit.PayloadMPEG1Video))\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t)\n\t\t\t\t\t})\n\n\t\t\tcase *rtspformat.Opus:\n\t\t\t\ttrack := addTrack(&tscodecs.Opus{\n\t\t\t\t\tChannelCount: forma.ChannelCount,\n\t\t\t\t})\n\n\t\t\t\tf.ri.reader.OnData(\n\t\t\t\t\tmedia,\n\t\t\t\t\tforma,\n\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn track.write(\n\t\t\t\t\t\t\ttimestampToDuration(u.PTS, clockRate),\n\t\t\t\t\t\t\tu.NTP,\n\t\t\t\t\t\t\ttrue,\n\t\t\t\t\t\t\tfunc(mtrack *mpegts.Track) error {\n\t\t\t\t\t\t\t\treturn f.mw.WriteOpus(\n\t\t\t\t\t\t\t\t\tmtrack,\n\t\t\t\t\t\t\t\t\tmultiplyAndDivide(u.PTS, 90000, int64(clockRate)),\n\t\t\t\t\t\t\t\t\tu.Payload.(unit.PayloadOpus))\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t)\n\t\t\t\t\t})\n\n\t\t\tcase *rtspformat.KLV:\n\t\t\t\ttrack := addTrack(&tscodecs.KLV{\n\t\t\t\t\tSynchronous: true,\n\t\t\t\t})\n\n\t\t\t\tf.ri.reader.OnData(\n\t\t\t\t\tmedia,\n\t\t\t\t\tforma,\n\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn track.write(\n\t\t\t\t\t\t\ttimestampToDuration(u.PTS, 90000),\n\t\t\t\t\t\t\tu.NTP,\n\t\t\t\t\t\t\ttrue,\n\t\t\t\t\t\t\tfunc(mtrack *mpegts.Track) error {\n\t\t\t\t\t\t\t\treturn f.mw.WriteKLV(\n\t\t\t\t\t\t\t\t\tmtrack,\n\t\t\t\t\t\t\t\t\tmultiplyAndDivide(u.PTS, 90000, 90000),\n\t\t\t\t\t\t\t\t\tu.Payload.(unit.PayloadKLV))\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t)\n\t\t\t\t\t})\n\n\t\t\tcase *rtspformat.MPEG4Audio:\n\t\t\t\ttrack := addTrack(&tscodecs.MPEG4Audio{\n\t\t\t\t\tConfig: *forma.Config,\n\t\t\t\t})\n\n\t\t\t\tf.ri.reader.OnData(\n\t\t\t\t\tmedia,\n\t\t\t\t\tforma,\n\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn track.write(\n\t\t\t\t\t\t\ttimestampToDuration(u.PTS, clockRate),\n\t\t\t\t\t\t\tu.NTP,\n\t\t\t\t\t\t\ttrue,\n\t\t\t\t\t\t\tfunc(mtrack *mpegts.Track) error {\n\t\t\t\t\t\t\t\treturn f.mw.WriteMPEG4Audio(\n\t\t\t\t\t\t\t\t\tmtrack,\n\t\t\t\t\t\t\t\t\tmultiplyAndDivide(u.PTS, 90000, int64(clockRate)),\n\t\t\t\t\t\t\t\t\tu.Payload.(unit.PayloadMPEG4Audio))\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t)\n\t\t\t\t\t})\n\n\t\t\tcase *rtspformat.MPEG4AudioLATM:\n\t\t\t\tif !forma.CPresent {\n\t\t\t\t\ttrack := addTrack(&tscodecs.MPEG4Audio{\n\t\t\t\t\t\tConfig: *forma.StreamMuxConfig.Programs[0].Layers[0].AudioSpecificConfig,\n\t\t\t\t\t})\n\n\t\t\t\t\tf.ri.reader.OnData(\n\t\t\t\t\t\tmedia,\n\t\t\t\t\t\tforma,\n\t\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tvar ame mpeg4audio.AudioMuxElement\n\t\t\t\t\t\t\tame.StreamMuxConfig = forma.StreamMuxConfig\n\t\t\t\t\t\t\terr := ame.Unmarshal(u.Payload.(unit.PayloadMPEG4AudioLATM))\n\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\treturn track.write(\n\t\t\t\t\t\t\t\ttimestampToDuration(u.PTS, clockRate),\n\t\t\t\t\t\t\t\tu.NTP,\n\t\t\t\t\t\t\t\ttrue,\n\t\t\t\t\t\t\t\tfunc(mtrack *mpegts.Track) error {\n\t\t\t\t\t\t\t\t\treturn f.mw.WriteMPEG4Audio(\n\t\t\t\t\t\t\t\t\t\tmtrack,\n\t\t\t\t\t\t\t\t\t\tmultiplyAndDivide(u.PTS, 90000, int64(clockRate)),\n\t\t\t\t\t\t\t\t\t\t[][]byte{ame.Payloads[0][0][0]})\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t)\n\t\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\tcase *rtspformat.MPEG1Audio:\n\t\t\t\ttrack := addTrack(&tscodecs.MPEG1Audio{})\n\n\t\t\t\tf.ri.reader.OnData(\n\t\t\t\t\tmedia,\n\t\t\t\t\tforma,\n\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn track.write(\n\t\t\t\t\t\t\ttimestampToDuration(u.PTS, clockRate),\n\t\t\t\t\t\t\tu.NTP,\n\t\t\t\t\t\t\ttrue,\n\t\t\t\t\t\t\tfunc(mtrack *mpegts.Track) error {\n\t\t\t\t\t\t\t\treturn f.mw.WriteMPEG1Audio(\n\t\t\t\t\t\t\t\t\tmtrack,\n\t\t\t\t\t\t\t\t\tu.PTS, // no conversion is needed since clock rate is 90khz in both MPEG-TS and RTSP\n\t\t\t\t\t\t\t\t\tu.Payload.(unit.PayloadMPEG1Audio))\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t)\n\t\t\t\t\t})\n\n\t\t\tcase *rtspformat.AC3:\n\t\t\t\ttrack := addTrack(&tscodecs.AC3{})\n\n\t\t\t\tf.ri.reader.OnData(\n\t\t\t\t\tmedia,\n\t\t\t\t\tforma,\n\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\tif u.NilPayload() {\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn track.write(\n\t\t\t\t\t\t\ttimestampToDuration(u.PTS, clockRate),\n\t\t\t\t\t\t\tu.NTP,\n\t\t\t\t\t\t\ttrue,\n\t\t\t\t\t\t\tfunc(mtrack *mpegts.Track) error {\n\t\t\t\t\t\t\t\tfor i, frame := range u.Payload.(unit.PayloadAC3) {\n\t\t\t\t\t\t\t\t\tframePTS := u.PTS + int64(i)*ac3.SamplesPerFrame\n\n\t\t\t\t\t\t\t\t\terr := f.mw.WriteAC3(\n\t\t\t\t\t\t\t\t\t\tmtrack,\n\t\t\t\t\t\t\t\t\t\tmultiplyAndDivide(framePTS, 90000, int64(clockRate)),\n\t\t\t\t\t\t\t\t\t\tframe)\n\t\t\t\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\t\t\t\treturn err\n\t\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t)\n\t\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(tracks) == 0 {\n\t\tf.ri.Log(logger.Warn, \"no supported tracks found, skipping recording\")\n\t\treturn false\n\t}\n\n\tsetuppedFormats := f.ri.reader.Formats()\n\n\tn := 1\n\tfor _, medi := range f.ri.stream.Desc.Medias {\n\t\tfor _, forma := range medi.Formats {\n\t\t\tif !slices.Contains(setuppedFormats, forma) {\n\t\t\t\tf.ri.Log(logger.Warn, \"skipping track %d (%s)\", n, forma.Codec())\n\t\t\t}\n\t\t\tn++\n\t\t}\n\t}\n\n\tf.dw = &dynamicWriter{}\n\tf.bw = bufio.NewWriterSize(f.dw, mpegtsBufferSize)\n\n\tf.mw = &mpegts.Writer{W: f.bw, Tracks: tracks}\n\terr := f.mw.Initialize()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tf.ri.Log(logger.Info, \"recording %s\",\n\t\tdefs.FormatsInfo(setuppedFormats))\n\n\treturn true\n}\n\nfunc (f *formatMPEGTS) close() {\n\tif f.currentSegment != nil {\n\t\tf.currentSegment.close() //nolint:errcheck\n\t}\n}\n"
  },
  {
    "path": "internal/recorder/format_mpegts_segment.go",
    "content": "package recorder\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/recordstore\"\n)\n\ntype formatMPEGTSSegment struct {\n\tpathFormat2       string\n\tflush             func() error\n\tonSegmentCreate   OnSegmentCreateFunc\n\tonSegmentComplete OnSegmentCompleteFunc\n\tstartDTS          time.Duration\n\tstartNTP          time.Time\n\tlog               logger.Writer\n\n\tpath      string\n\tfi        *os.File\n\tlastFlush time.Duration\n\tlastDTS   time.Duration\n}\n\nfunc (s *formatMPEGTSSegment) initialize() {\n\ts.lastFlush = s.startDTS\n\ts.lastDTS = s.startDTS\n}\n\nfunc (s *formatMPEGTSSegment) close() error {\n\terr := s.flush()\n\n\tif s.fi != nil {\n\t\ts.log.Log(logger.Debug, \"closing segment %s\", s.path)\n\t\terr2 := s.fi.Close()\n\t\tif err == nil {\n\t\t\terr = err2\n\t\t}\n\n\t\tif err2 == nil {\n\t\t\tduration := s.lastDTS - s.startDTS\n\t\t\ts.onSegmentComplete(s.path, duration)\n\t\t}\n\t}\n\n\treturn err\n}\n\nfunc (s *formatMPEGTSSegment) Write(p []byte) (int, error) {\n\tif s.fi == nil {\n\t\ts.path = recordstore.Path{Start: s.startNTP}.Encode(s.pathFormat2)\n\t\ts.log.Log(logger.Debug, \"creating segment %s\", s.path)\n\n\t\terr := os.MkdirAll(filepath.Dir(s.path), 0o755)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\n\t\tfi, err := os.Create(s.path)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\n\t\ts.onSegmentCreate(s.path)\n\n\t\ts.fi = fi\n\t}\n\n\treturn s.fi.Write(p)\n}\n"
  },
  {
    "path": "internal/recorder/format_mpegts_track.go",
    "content": "package recorder\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts\"\n\ttscodecs \"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts/codecs\"\n)\n\ntype formatMPEGTSTrack struct {\n\tf     *formatMPEGTS\n\tcodec tscodecs.Codec\n\n\ttrack            *mpegts.Track\n\tstartInitialized bool\n\tstartDTS         time.Duration\n\tstartNTP         time.Time\n}\n\nfunc (t *formatMPEGTSTrack) initialize() {\n\tt.track = &mpegts.Track{\n\t\tCodec: t.codec,\n\t}\n}\n\nfunc (t *formatMPEGTSTrack) write(\n\tdts time.Duration,\n\tntp time.Time,\n\trandomAccess bool,\n\tcb func(track *mpegts.Track) error,\n) error {\n\tisVideo := t.track.Codec.IsVideo()\n\n\tif isVideo {\n\t\tt.f.hasVideo = true\n\t}\n\n\tif !t.startInitialized {\n\t\tt.startDTS = dts\n\t\tt.startNTP = ntp\n\t\tt.startInitialized = true\n\t} else {\n\t\tdrift := ntp.Sub(t.startNTP) - (dts - t.startDTS)\n\t\tif drift < -ntpDriftTolerance || drift > ntpDriftTolerance {\n\t\t\treturn fmt.Errorf(\"detected drift between recording duration and absolute time, resetting\")\n\t\t}\n\t}\n\n\tswitch {\n\tcase t.f.currentSegment == nil:\n\t\tt.f.currentSegment = &formatMPEGTSSegment{\n\t\t\tpathFormat2:       t.f.ri.pathFormat2,\n\t\t\tflush:             t.f.bw.Flush,\n\t\t\tonSegmentCreate:   t.f.ri.onSegmentCreate,\n\t\t\tonSegmentComplete: t.f.ri.onSegmentComplete,\n\t\t\tstartDTS:          dts,\n\t\t\tstartNTP:          ntp,\n\t\t\tlog:               t.f.ri,\n\t\t}\n\t\tt.f.currentSegment.initialize()\n\t\tt.f.dw.setTarget(t.f.currentSegment)\n\n\tcase (!t.f.hasVideo || isVideo) &&\n\t\trandomAccess &&\n\t\t(dts-t.f.currentSegment.startDTS) >= t.f.ri.segmentDuration:\n\t\tt.f.currentSegment.lastDTS = dts\n\t\terr := t.f.currentSegment.close()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tt.f.currentSegment = &formatMPEGTSSegment{\n\t\t\tpathFormat2:       t.f.ri.pathFormat2,\n\t\t\tflush:             t.f.bw.Flush,\n\t\t\tonSegmentCreate:   t.f.ri.onSegmentCreate,\n\t\t\tonSegmentComplete: t.f.ri.onSegmentComplete,\n\t\t\tstartDTS:          dts,\n\t\t\tstartNTP:          ntp,\n\t\t\tlog:               t.f.ri,\n\t\t}\n\t\tt.f.currentSegment.initialize()\n\t\tt.f.dw.setTarget(t.f.currentSegment)\n\n\tcase (dts - t.f.currentSegment.lastFlush) >= t.f.ri.partDuration:\n\t\terr := t.f.bw.Flush()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tt.f.currentSegment.lastFlush = dts\n\t}\n\n\tt.f.currentSegment.lastDTS = dts\n\n\treturn cb(t.track)\n}\n"
  },
  {
    "path": "internal/recorder/recorder.go",
    "content": "// Package recorder contains the recorder.\npackage recorder\n\nimport (\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n)\n\nconst (\n\tntpDriftTolerance = 5 * time.Second\n)\n\n// OnSegmentCreateFunc is the prototype of the function passed as OnSegmentCreate\ntype OnSegmentCreateFunc = func(path string)\n\n// OnSegmentCompleteFunc is the prototype of the function passed as OnSegmentComplete\ntype OnSegmentCompleteFunc = func(path string, duration time.Duration)\n\n// Recorder writes recordings to disk.\ntype Recorder struct {\n\tPathFormat        string\n\tFormat            conf.RecordFormat\n\tPartDuration      time.Duration\n\tMaxPartSize       conf.StringSize\n\tSegmentDuration   time.Duration\n\tPathName          string\n\tStream            *stream.Stream\n\tOnSegmentCreate   OnSegmentCreateFunc\n\tOnSegmentComplete OnSegmentCompleteFunc\n\tParent            logger.Writer\n\n\trestartPause time.Duration\n\n\tcurrentInstance *recorderInstance\n\n\tterminate chan struct{}\n\tdone      chan struct{}\n}\n\n// Initialize initializes Recorder.\nfunc (r *Recorder) Initialize() {\n\tif r.OnSegmentCreate == nil {\n\t\tr.OnSegmentCreate = func(string) {\n\t\t}\n\t}\n\tif r.OnSegmentComplete == nil {\n\t\tr.OnSegmentComplete = func(string, time.Duration) {\n\t\t}\n\t}\n\tif r.restartPause == 0 {\n\t\tr.restartPause = 2 * time.Second\n\t}\n\n\tr.terminate = make(chan struct{})\n\tr.done = make(chan struct{})\n\n\tr.currentInstance = &recorderInstance{\n\t\tpathFormat:        r.PathFormat,\n\t\tformat:            r.Format,\n\t\tpartDuration:      r.PartDuration,\n\t\tmaxPartSize:       r.MaxPartSize,\n\t\tsegmentDuration:   r.SegmentDuration,\n\t\tpathName:          r.PathName,\n\t\tstream:            r.Stream,\n\t\tonSegmentCreate:   r.OnSegmentCreate,\n\t\tonSegmentComplete: r.OnSegmentComplete,\n\t\tparent:            r,\n\t}\n\tr.currentInstance.initialize()\n\n\tgo r.run()\n}\n\n// Log implements logger.Writer.\nfunc (r *Recorder) Log(level logger.Level, format string, args ...any) {\n\tr.Parent.Log(level, \"[recorder] \"+format, args...)\n}\n\n// Close closes the agent.\nfunc (r *Recorder) Close() {\n\tr.Log(logger.Info, \"recording stopped\")\n\tclose(r.terminate)\n\t<-r.done\n}\n\nfunc (r *Recorder) run() {\n\tdefer close(r.done)\n\n\tfor {\n\t\tselect {\n\t\tcase <-r.currentInstance.done:\n\t\t\tr.currentInstance.close()\n\t\tcase <-r.terminate:\n\t\t\tr.currentInstance.close()\n\t\t\treturn\n\t\t}\n\n\t\tselect {\n\t\tcase <-time.After(r.restartPause):\n\t\tcase <-r.terminate:\n\t\t\treturn\n\t\t}\n\n\t\tr.currentInstance = &recorderInstance{\n\t\t\tpathFormat:        r.PathFormat,\n\t\t\tformat:            r.Format,\n\t\t\tpartDuration:      r.PartDuration,\n\t\t\tmaxPartSize:       r.MaxPartSize,\n\t\t\tsegmentDuration:   r.SegmentDuration,\n\t\t\tpathName:          r.PathName,\n\t\t\tstream:            r.Stream,\n\t\t\tonSegmentCreate:   r.OnSegmentCreate,\n\t\t\tonSegmentComplete: r.OnSegmentComplete,\n\t\t\tparent:            r,\n\t\t}\n\t\tr.currentInstance.initialize()\n\t}\n}\n"
  },
  {
    "path": "internal/recorder/recorder_instance.go",
    "content": "package recorder\n\nimport (\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/recordstore\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n)\n\ntype recorderInstance struct {\n\tpathFormat        string\n\tformat            conf.RecordFormat\n\tpartDuration      time.Duration\n\tmaxPartSize       conf.StringSize\n\tsegmentDuration   time.Duration\n\tpathName          string\n\tstream            *stream.Stream\n\tonSegmentCreate   OnSegmentCreateFunc\n\tonSegmentComplete OnSegmentCompleteFunc\n\tparent            logger.Writer\n\n\tstreamID    uuid.UUID\n\tpathFormat2 string\n\tformat2     format\n\tskip        bool\n\treader      *stream.Reader\n\n\tterminate chan struct{}\n\tdone      chan struct{}\n}\n\n// Log implements logger.Writer.\nfunc (ri *recorderInstance) Log(level logger.Level, format string, args ...any) {\n\tri.parent.Log(level, format, args...)\n}\n\nfunc (ri *recorderInstance) initialize() {\n\tri.streamID = uuid.New()\n\tri.pathFormat2 = ri.pathFormat\n\tri.pathFormat2 = recordstore.PathAddExtension(\n\t\tstrings.ReplaceAll(ri.pathFormat2, \"%path\", ri.pathName),\n\t\tri.format,\n\t)\n\tri.reader = &stream.Reader{\n\t\tSkipBytesSent: true,\n\t\tParent:        ri,\n\t}\n\n\tri.terminate = make(chan struct{})\n\tri.done = make(chan struct{})\n\n\tswitch ri.format {\n\tcase conf.RecordFormatMPEGTS:\n\t\tri.format2 = &formatMPEGTS{\n\t\t\tri: ri,\n\t\t}\n\t\tok := ri.format2.initialize()\n\t\tri.skip = !ok\n\n\tdefault:\n\t\tri.format2 = &formatFMP4{\n\t\t\tri: ri,\n\t\t}\n\t\tok := ri.format2.initialize()\n\t\tri.skip = !ok\n\t}\n\n\tif !ri.skip {\n\t\tri.stream.AddReader(ri.reader)\n\t}\n\n\tgo ri.run()\n}\n\nfunc (ri *recorderInstance) close() {\n\tclose(ri.terminate)\n\t<-ri.done\n}\n\nfunc (ri *recorderInstance) run() {\n\tdefer close(ri.done)\n\n\tif !ri.skip {\n\t\tselect {\n\t\tcase err := <-ri.reader.Error():\n\t\t\tri.Log(logger.Error, err.Error())\n\n\t\tcase <-ri.terminate:\n\t\t}\n\n\t\tri.stream.RemoveReader(ri.reader)\n\t} else {\n\t\t<-ri.terminate\n\t}\n\n\tri.format2.close()\n}\n"
  },
  {
    "path": "internal/recorder/recorder_test.go",
    "content": "package recorder\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\tamp4 \"github.com/abema/go-mp4\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\trtspformat \"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/fmp4\"\n\tmcodecs \"github.com/bluenviron/mediacommon/v2/pkg/formats/mp4/codecs\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/recordstore\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n)\n\nfunc TestRecorder(t *testing.T) {\n\tdesc := &description.Session{Medias: []*description.Media{\n\t\t{\n\t\t\tType: description.MediaTypeVideo,\n\t\t\tFormats: []rtspformat.Format{&rtspformat.H264{\n\t\t\t\tPayloadTyp:        96,\n\t\t\t\tPacketizationMode: 1,\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tType: description.MediaTypeVideo,\n\t\t\tFormats: []rtspformat.Format{&rtspformat.H265{\n\t\t\t\tPayloadTyp: 96,\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tType: description.MediaTypeAudio,\n\t\t\tFormats: []rtspformat.Format{&rtspformat.MPEG4Audio{\n\t\t\t\tPayloadTyp: 96,\n\t\t\t\tConfig: &mpeg4audio.AudioSpecificConfig{\n\t\t\t\t\tType:         2,\n\t\t\t\t\tSampleRate:   44100,\n\t\t\t\t\tChannelCount: 2,\n\t\t\t\t},\n\t\t\t\tSizeLength:       13,\n\t\t\t\tIndexLength:      3,\n\t\t\t\tIndexDeltaLength: 3,\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tType: description.MediaTypeAudio,\n\t\t\tFormats: []rtspformat.Format{&rtspformat.G711{\n\t\t\t\tPayloadTyp:   8,\n\t\t\t\tMULaw:        false,\n\t\t\t\tSampleRate:   8000,\n\t\t\t\tChannelCount: 1,\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tType: description.MediaTypeAudio,\n\t\t\tFormats: []rtspformat.Format{&rtspformat.LPCM{\n\t\t\t\tPayloadTyp:   96,\n\t\t\t\tBitDepth:     16,\n\t\t\t\tSampleRate:   44100,\n\t\t\t\tChannelCount: 2,\n\t\t\t}},\n\t\t},\n\t}}\n\n\twriteToStream := func(subStream *stream.SubStream, startDTS int64, startNTP time.Time) {\n\t\tfor i := range 2 {\n\t\t\tpts := startDTS + int64(i)*100*90000/1000\n\t\t\tntp := startNTP.Add(time.Duration(i*100) * time.Millisecond)\n\n\t\t\tsubStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{\n\t\t\t\tPTS: pts,\n\t\t\t\tNTP: ntp,\n\t\t\t\tPayload: unit.PayloadH264{\n\t\t\t\t\ttest.FormatH264.SPS,\n\t\t\t\t\ttest.FormatH264.PPS,\n\t\t\t\t\t{5}, // IDR\n\t\t\t\t},\n\t\t\t})\n\n\t\t\tsubStream.WriteUnit(desc.Medias[1], desc.Medias[1].Formats[0], &unit.Unit{\n\t\t\t\tPTS: pts,\n\t\t\t\tPayload: unit.PayloadH265{\n\t\t\t\t\t{\n\t\t\t\t\t\t0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x60,\n\t\t\t\t\t\t0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03,\n\t\t\t\t\t\t0x00, 0x00, 0x03, 0x00, 0x78, 0xba, 0x02, 0x40,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03,\n\t\t\t\t\t\t0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03,\n\t\t\t\t\t\t0x00, 0x78, 0xa0, 0x03, 0xc0, 0x80, 0x11, 0x07,\n\t\t\t\t\t\t0xcb, 0x96, 0xe9, 0x29, 0x30, 0xbc, 0x05, 0xa0,\n\t\t\t\t\t\t0x20, 0x00, 0x00, 0x03, 0x00, 0x20, 0x00, 0x00,\n\t\t\t\t\t\t0x03, 0x03, 0xc1,\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\t0x44, 0x01, 0xc0, 0x73, 0xc1, 0x89,\n\t\t\t\t\t},\n\t\t\t\t\t{0x26, 0x1, 0xaf, 0x8, 0x42, 0x23, 0x48, 0x8a, 0x43, 0xe2},\n\t\t\t\t},\n\t\t\t})\n\n\t\t\tsubStream.WriteUnit(desc.Medias[2], desc.Medias[2].Formats[0], &unit.Unit{\n\t\t\t\tPTS:     pts * int64(desc.Medias[2].Formats[0].ClockRate()) / 90000,\n\t\t\t\tPayload: unit.PayloadMPEG4Audio{{1, 2, 3, 4}},\n\t\t\t})\n\n\t\t\tsubStream.WriteUnit(desc.Medias[3], desc.Medias[3].Formats[0], &unit.Unit{\n\t\t\t\tPTS:     pts * int64(desc.Medias[3].Formats[0].ClockRate()) / 90000,\n\t\t\t\tPayload: unit.PayloadG711{1, 2, 3, 4},\n\t\t\t})\n\n\t\t\tsubStream.WriteUnit(desc.Medias[4], desc.Medias[4].Formats[0], &unit.Unit{\n\t\t\t\tPTS:     pts * int64(desc.Medias[4].Formats[0].ClockRate()) / 90000,\n\t\t\t\tPayload: unit.PayloadLPCM{1, 2, 3, 4},\n\t\t\t})\n\t\t}\n\t}\n\n\tfor _, ca := range []string{\"fmp4\", \"mpegts\"} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tstrm := &stream.Stream{\n\t\t\t\tDesc:              desc,\n\t\t\t\tWriteQueueSize:    512,\n\t\t\t\tRTPMaxPayloadSize: 1450,\n\t\t\t\tParent:            test.NilLogger,\n\t\t\t}\n\t\t\terr := strm.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer strm.Close()\n\n\t\t\tsubStream := &stream.SubStream{\n\t\t\t\tStream:        strm,\n\t\t\t\tUseRTPPackets: false,\n\t\t\t}\n\t\t\terr = subStream.Initialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tdir, err := os.MkdirTemp(\"\", \"mediamtx-agent\")\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer os.RemoveAll(dir)\n\n\t\t\trecordPath := filepath.Join(dir, \"%path/%Y-%m-%d_%H-%M-%S-%f\")\n\n\t\t\tsegCreated := make(chan struct{}, 4)\n\t\t\tsegDone := make(chan struct{}, 4)\n\n\t\t\tvar f conf.RecordFormat\n\t\t\tif ca == \"fmp4\" {\n\t\t\t\tf = conf.RecordFormatFMP4\n\t\t\t} else {\n\t\t\t\tf = conf.RecordFormatMPEGTS\n\t\t\t}\n\n\t\t\tvar ext string\n\t\t\tif ca == \"fmp4\" {\n\t\t\t\text = \"mp4\"\n\t\t\t} else {\n\t\t\t\text = \"ts\"\n\t\t\t}\n\n\t\t\tn := 0\n\n\t\t\tw := &Recorder{\n\t\t\t\tPathFormat:      recordPath,\n\t\t\t\tFormat:          f,\n\t\t\t\tPartDuration:    100 * time.Millisecond,\n\t\t\t\tMaxPartSize:     50 * 1024 * 1024,\n\t\t\t\tSegmentDuration: 1 * time.Second,\n\t\t\t\tPathName:        \"mypath\",\n\t\t\t\tStream:          strm,\n\t\t\t\tOnSegmentCreate: func(segPath string) {\n\t\t\t\t\tswitch n {\n\t\t\t\t\tcase 0:\n\t\t\t\t\t\trequire.Equal(t, filepath.Join(dir, \"mypath\", \"2008-05-20_22-15-25-000000.\"+ext), segPath)\n\t\t\t\t\tcase 1:\n\t\t\t\t\t\trequire.Equal(t, filepath.Join(dir, \"mypath\", \"2008-05-20_22-15-27-000000.\"+ext), segPath)\n\t\t\t\t\tdefault:\n\t\t\t\t\t\trequire.Equal(t, filepath.Join(dir, \"mypath\", \"2010-05-20_22-15-25-000000.\"+ext), segPath)\n\t\t\t\t\t}\n\t\t\t\t\tsegCreated <- struct{}{}\n\t\t\t\t},\n\t\t\t\tOnSegmentComplete: func(segPath string, du time.Duration) {\n\t\t\t\t\tswitch n {\n\t\t\t\t\tcase 0:\n\t\t\t\t\t\trequire.Equal(t, filepath.Join(dir, \"mypath\", \"2008-05-20_22-15-25-000000.\"+ext), segPath)\n\t\t\t\t\t\trequire.Equal(t, 2*time.Second, du)\n\t\t\t\t\tcase 1:\n\t\t\t\t\t\trequire.Equal(t, filepath.Join(dir, \"mypath\", \"2008-05-20_22-15-27-000000.\"+ext), segPath)\n\t\t\t\t\t\trequire.Equal(t, 100*time.Millisecond, du)\n\t\t\t\t\tdefault:\n\t\t\t\t\t\trequire.Equal(t, filepath.Join(dir, \"mypath\", \"2010-05-20_22-15-25-000000.\"+ext), segPath)\n\t\t\t\t\t\trequire.Equal(t, 100*time.Millisecond, du)\n\t\t\t\t\t}\n\t\t\t\t\tn++\n\t\t\t\t\tsegDone <- struct{}{}\n\t\t\t\t},\n\t\t\t\tParent:       test.NilLogger,\n\t\t\t\trestartPause: 1 * time.Millisecond,\n\t\t\t}\n\t\t\tw.Initialize()\n\n\t\t\twriteToStream(subStream,\n\t\t\t\t50*90000,\n\t\t\t\ttime.Date(2008, 5, 20, 22, 15, 25, 0, time.UTC))\n\n\t\t\twriteToStream(subStream,\n\t\t\t\t52*90000,\n\t\t\t\ttime.Date(2008, 5, 20, 22, 15, 27, 0, time.UTC))\n\n\t\t\t// simulate a write error\n\t\t\tsubStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{\n\t\t\t\tPTS: 0,\n\t\t\t\tPayload: unit.PayloadH264{\n\t\t\t\t\t{5}, // IDR\n\t\t\t\t},\n\t\t\t})\n\n\t\t\tfor range 2 {\n\t\t\t\t<-segCreated\n\t\t\t\t<-segDone\n\t\t\t}\n\n\t\t\tif ca == \"fmp4\" {\n\t\t\t\tvar init fmp4.Init\n\n\t\t\t\tfunc() {\n\t\t\t\t\tf, err2 := os.Open(filepath.Join(dir, \"mypath\", \"2008-05-20_22-15-25-000000.\"+ext))\n\t\t\t\t\trequire.NoError(t, err2)\n\t\t\t\t\tdefer f.Close()\n\n\t\t\t\t\terr2 = init.Unmarshal(f)\n\t\t\t\t\trequire.NoError(t, err2)\n\t\t\t\t}()\n\n\t\t\t\trequire.Equal(t, fmp4.Init{\n\t\t\t\t\tTracks: []*fmp4.InitTrack{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        1,\n\t\t\t\t\t\t\tTimeScale: 90000,\n\t\t\t\t\t\t\tCodec: &mcodecs.H264{\n\t\t\t\t\t\t\t\tSPS: test.FormatH264.SPS,\n\t\t\t\t\t\t\t\tPPS: test.FormatH264.PPS,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        2,\n\t\t\t\t\t\t\tTimeScale: 90000,\n\t\t\t\t\t\t\tCodec: &mcodecs.H265{\n\t\t\t\t\t\t\t\tVPS: []byte{\n\t\t\t\t\t\t\t\t\t0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x60,\n\t\t\t\t\t\t\t\t\t0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03,\n\t\t\t\t\t\t\t\t\t0x00, 0x00, 0x03, 0x00, 0x78, 0xba, 0x02, 0x40,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tSPS: []byte{\n\t\t\t\t\t\t\t\t\t0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03,\n\t\t\t\t\t\t\t\t\t0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03,\n\t\t\t\t\t\t\t\t\t0x00, 0x78, 0xa0, 0x03, 0xc0, 0x80, 0x11, 0x07,\n\t\t\t\t\t\t\t\t\t0xcb, 0x96, 0xe9, 0x29, 0x30, 0xbc, 0x05, 0xa0,\n\t\t\t\t\t\t\t\t\t0x20, 0x00, 0x00, 0x03, 0x00, 0x20, 0x00, 0x00,\n\t\t\t\t\t\t\t\t\t0x03, 0x03, 0xc1,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tPPS: []byte{\n\t\t\t\t\t\t\t\t\t0x44, 0x01, 0xc0, 0x73, 0xc1, 0x89,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        3,\n\t\t\t\t\t\t\tTimeScale: 44100,\n\t\t\t\t\t\t\tCodec: &mcodecs.MPEG4Audio{\n\t\t\t\t\t\t\t\tConfig: mpeg4audio.AudioSpecificConfig{\n\t\t\t\t\t\t\t\t\tType:          2,\n\t\t\t\t\t\t\t\t\tSampleRate:    44100,\n\t\t\t\t\t\t\t\t\tChannelCount:  2,\n\t\t\t\t\t\t\t\t\tChannelConfig: 2,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        4,\n\t\t\t\t\t\t\tTimeScale: 8000,\n\t\t\t\t\t\t\tCodec: &mcodecs.LPCM{\n\t\t\t\t\t\t\t\tBitDepth:     16,\n\t\t\t\t\t\t\t\tSampleRate:   8000,\n\t\t\t\t\t\t\t\tChannelCount: 1,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        5,\n\t\t\t\t\t\t\tTimeScale: 44100,\n\t\t\t\t\t\t\tCodec: &mcodecs.LPCM{\n\t\t\t\t\t\t\t\tBitDepth:     16,\n\t\t\t\t\t\t\t\tSampleRate:   44100,\n\t\t\t\t\t\t\t\tChannelCount: 2,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t\tUserData: []amp4.IBox{\n\t\t\t\t\t\t&recordstore.Mtxi{\n\t\t\t\t\t\t\tStreamID: init.UserData[0].(*recordstore.Mtxi).StreamID,\n\t\t\t\t\t\t\tDTS:      50000000000,\n\t\t\t\t\t\t\tNTP:      1211321725000000000,\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}, init)\n\n\t\t\t\t_, err = os.Stat(filepath.Join(dir, \"mypath\", \"2008-05-20_22-15-27-000000.\"+ext))\n\t\t\t\trequire.NoError(t, err)\n\t\t\t} else {\n\t\t\t\t_, err = os.Stat(filepath.Join(dir, \"mypath\", \"2008-05-20_22-15-25-000000.\"+ext))\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t_, err = os.Stat(filepath.Join(dir, \"mypath\", \"2008-05-20_22-15-27-000000.\"+ext))\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\ttime.Sleep(50 * time.Millisecond)\n\n\t\t\twriteToStream(subStream,\n\t\t\t\t300*90000,\n\t\t\t\ttime.Date(2010, 5, 20, 22, 15, 25, 0, time.UTC))\n\n\t\t\ttime.Sleep(50 * time.Millisecond)\n\n\t\t\tw.Close()\n\n\t\t\t<-segCreated\n\t\t\t<-segDone\n\n\t\t\t_, err = os.Stat(filepath.Join(dir, \"mypath\", \"2010-05-20_22-15-25-000000.\"+ext))\n\t\t\trequire.NoError(t, err)\n\n\t\t\tif ca == \"fmp4\" {\n\t\t\t\tvar byts []byte\n\t\t\t\tbyts, err = os.ReadFile(filepath.Join(dir, \"mypath\", \"2008-05-20_22-15-25-000000.mp4\"))\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tvar parts fmp4.Parts\n\t\t\t\terr = parts.Unmarshal(byts)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tfor _, part := range parts {\n\t\t\t\t\tfor _, track := range part.Tracks {\n\t\t\t\t\t\ttrack.Samples = nil\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\trequire.Equal(t, fmp4.Parts{\n\t\t\t\t\t{\n\t\t\t\t\t\tTracks: []*fmp4.PartTrack{{\n\t\t\t\t\t\t\tID: 1,\n\t\t\t\t\t\t}},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tSequenceNumber: 1,\n\t\t\t\t\t\tTracks: []*fmp4.PartTrack{{\n\t\t\t\t\t\t\tID: 2,\n\t\t\t\t\t\t}},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tSequenceNumber: 2,\n\t\t\t\t\t\tTracks: []*fmp4.PartTrack{{\n\t\t\t\t\t\t\tID: 3,\n\t\t\t\t\t\t}},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tSequenceNumber: 3,\n\t\t\t\t\t\tTracks: []*fmp4.PartTrack{{\n\t\t\t\t\t\t\tID: 4,\n\t\t\t\t\t\t}},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tSequenceNumber: 4,\n\t\t\t\t\t\tTracks: []*fmp4.PartTrack{{\n\t\t\t\t\t\t\tID: 5,\n\t\t\t\t\t\t}},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tSequenceNumber: 5,\n\t\t\t\t\t\tTracks: []*fmp4.PartTrack{{\n\t\t\t\t\t\t\tID:       1,\n\t\t\t\t\t\t\tBaseTime: 9000,\n\t\t\t\t\t\t}},\n\t\t\t\t\t},\n\t\t\t\t}, parts)\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRecorderFMP4NegativeInitialDTS(t *testing.T) {\n\tdesc := &description.Session{Medias: []*description.Media{\n\t\t{\n\t\t\tType: description.MediaTypeVideo,\n\t\t\tFormats: []rtspformat.Format{&rtspformat.H264{\n\t\t\t\tPayloadTyp:        96,\n\t\t\t\tPacketizationMode: 1,\n\t\t\t}},\n\t\t},\n\t\t{\n\t\t\tType: description.MediaTypeAudio,\n\t\t\tFormats: []rtspformat.Format{&rtspformat.MPEG4Audio{\n\t\t\t\tPayloadTyp: 96,\n\t\t\t\tConfig: &mpeg4audio.AudioSpecificConfig{\n\t\t\t\t\tType:         2,\n\t\t\t\t\tSampleRate:   44100,\n\t\t\t\t\tChannelCount: 2,\n\t\t\t\t},\n\t\t\t\tSizeLength:       13,\n\t\t\t\tIndexLength:      3,\n\t\t\t\tIndexDeltaLength: 3,\n\t\t\t}},\n\t\t},\n\t}}\n\n\tstrm := &stream.Stream{\n\t\tDesc:              desc,\n\t\tWriteQueueSize:    512,\n\t\tRTPMaxPayloadSize: 1450,\n\t\tParent:            test.NilLogger,\n\t}\n\terr := strm.Initialize()\n\trequire.NoError(t, err)\n\tdefer strm.Close()\n\n\tsubStream := &stream.SubStream{\n\t\tStream:        strm,\n\t\tUseRTPPackets: false,\n\t}\n\terr = subStream.Initialize()\n\trequire.NoError(t, err)\n\n\tdir, err := os.MkdirTemp(\"\", \"mediamtx-agent\")\n\trequire.NoError(t, err)\n\tdefer os.RemoveAll(dir)\n\n\trecordPath := filepath.Join(dir, \"%path/%Y-%m-%d_%H-%M-%S-%f\")\n\n\tw := &Recorder{\n\t\tPathFormat:      recordPath,\n\t\tFormat:          conf.RecordFormatFMP4,\n\t\tPartDuration:    100 * time.Millisecond,\n\t\tMaxPartSize:     50 * 1024 * 1024,\n\t\tSegmentDuration: 1 * time.Second,\n\t\tPathName:        \"mypath\",\n\t\tStream:          strm,\n\t\tParent:          test.NilLogger,\n\t}\n\tw.Initialize()\n\n\tfor i := range 3 {\n\t\tsubStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{\n\t\t\tPTS: -50*90000/1000 + (int64(i) * 200 * 90000 / 1000),\n\t\t\tNTP: time.Date(2008, 5, 20, 22, 15, 25, 0, time.UTC),\n\t\t\tPayload: unit.PayloadH264{\n\t\t\t\ttest.FormatH264.SPS,\n\t\t\t\ttest.FormatH264.PPS,\n\t\t\t\t{5}, // IDR\n\t\t\t},\n\t\t})\n\n\t\tsubStream.WriteUnit(desc.Medias[1], desc.Medias[1].Formats[0], &unit.Unit{\n\t\t\tPTS:     -100*44100/1000 + (int64(i) * 200 * 44100 / 1000),\n\t\t\tPayload: unit.PayloadMPEG4Audio{{1, 2, 3, 4}},\n\t\t})\n\t}\n\n\ttime.Sleep(50 * time.Millisecond)\n\n\tw.Close()\n\n\tbyts, err := os.ReadFile(filepath.Join(dir, \"mypath\", \"2008-05-20_22-15-25-000000.mp4\"))\n\trequire.NoError(t, err)\n\n\tvar parts fmp4.Parts\n\terr = parts.Unmarshal(byts)\n\trequire.NoError(t, err)\n\n\tfound := false\n\n\tfor _, part := range parts {\n\t\tfor _, track := range part.Tracks {\n\t\t\tif track.ID == 2 {\n\t\t\t\trequire.Equal(t, uint64(6615), track.BaseTime)\n\t\t\t\tfound = true\n\t\t\t}\n\t\t}\n\t}\n\n\trequire.True(t, found)\n}\n\nfunc TestRecorderFMP4NegativeDTSDiff(t *testing.T) {\n\tdesc := &description.Session{Medias: []*description.Media{\n\t\t{\n\t\t\tType: description.MediaTypeVideo,\n\t\t\tFormats: []rtspformat.Format{&rtspformat.MPEG4Audio{\n\t\t\t\tPayloadTyp: 96,\n\t\t\t\tConfig: &mpeg4audio.AudioSpecificConfig{\n\t\t\t\t\tType:         2,\n\t\t\t\t\tSampleRate:   44100,\n\t\t\t\t\tChannelCount: 2,\n\t\t\t\t},\n\t\t\t\tSizeLength:       13,\n\t\t\t\tIndexLength:      3,\n\t\t\t\tIndexDeltaLength: 3,\n\t\t\t}},\n\t\t},\n\t}}\n\n\tstrm := &stream.Stream{\n\t\tDesc:              desc,\n\t\tWriteQueueSize:    512,\n\t\tRTPMaxPayloadSize: 1450,\n\t\tParent:            test.NilLogger,\n\t}\n\terr := strm.Initialize()\n\trequire.NoError(t, err)\n\tdefer strm.Close()\n\n\tsubStream := &stream.SubStream{\n\t\tStream:        strm,\n\t\tUseRTPPackets: false,\n\t}\n\terr = subStream.Initialize()\n\trequire.NoError(t, err)\n\n\tdir, err := os.MkdirTemp(\"\", \"mediamtx-agent\")\n\trequire.NoError(t, err)\n\tdefer os.RemoveAll(dir)\n\n\trecordPath := filepath.Join(dir, \"%path/%Y-%m-%d_%H-%M-%S-%f\")\n\n\tw := &Recorder{\n\t\tPathFormat:      recordPath,\n\t\tFormat:          conf.RecordFormatFMP4,\n\t\tPartDuration:    100 * time.Millisecond,\n\t\tMaxPartSize:     50 * 1024 * 1024,\n\t\tSegmentDuration: 2 * time.Second,\n\t\tPathName:        \"mypath\",\n\t\tStream:          strm,\n\t\tParent:          test.NilLogger,\n\t}\n\tw.Initialize()\n\n\tsubStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{\n\t\tPTS:     44100,\n\t\tNTP:     time.Date(2008, 5, 20, 22, 15, 25, 0, time.UTC),\n\t\tPayload: unit.PayloadMPEG4Audio{{1, 2}},\n\t})\n\n\tsubStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{\n\t\tPTS:     3 * 44100,\n\t\tNTP:     time.Date(2008, 5, 20, 22, 15, 25, 0, time.UTC),\n\t\tPayload: unit.PayloadMPEG4Audio{{1, 2}},\n\t})\n\n\tsubStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{\n\t\tPTS:     2 * 44100,\n\t\tNTP:     time.Date(2008, 5, 20, 22, 15, 25, 0, time.UTC),\n\t\tPayload: unit.PayloadMPEG4Audio{{1, 2}},\n\t})\n\n\tsubStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{\n\t\tPTS:     4 * 44100,\n\t\tNTP:     time.Date(2008, 5, 20, 22, 15, 25, 0, time.UTC),\n\t\tPayload: unit.PayloadMPEG4Audio{{1, 2}},\n\t})\n\n\ttime.Sleep(50 * time.Millisecond)\n\n\tw.Close()\n\n\tbyts, err := os.ReadFile(filepath.Join(dir, \"mypath\", \"2008-05-20_22-15-25-000000.mp4\"))\n\trequire.NoError(t, err)\n\n\tvar parts fmp4.Parts\n\terr = parts.Unmarshal(byts)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, fmp4.Parts{{\n\t\tTracks: []*fmp4.PartTrack{{\n\t\t\tID: 1,\n\t\t\tSamples: []*fmp4.Sample{\n\t\t\t\t{\n\t\t\t\t\tPayload: []byte{1, 2},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tDuration: 44100,\n\t\t\t\t\tPayload:  []byte{1, 2},\n\t\t\t\t},\n\t\t\t},\n\t\t}},\n\t}}, parts)\n}\n\nfunc TestRecorderSkipTracksPartial(t *testing.T) {\n\tfor _, ca := range []string{\"fmp4\", \"mpegts\"} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tdesc := &description.Session{Medias: []*description.Media{\n\t\t\t\t{\n\t\t\t\t\tType:    description.MediaTypeVideo,\n\t\t\t\t\tFormats: []rtspformat.Format{&rtspformat.H264{}},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tType:    description.MediaTypeVideo,\n\t\t\t\t\tFormats: []rtspformat.Format{&rtspformat.VP8{}},\n\t\t\t\t},\n\t\t\t}}\n\n\t\t\tstrm := &stream.Stream{\n\t\t\t\tDesc:              desc,\n\t\t\t\tWriteQueueSize:    512,\n\t\t\t\tRTPMaxPayloadSize: 1450,\n\t\t\t\tParent:            test.NilLogger,\n\t\t\t}\n\t\t\terr := strm.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer strm.Close()\n\n\t\t\tsubStream := &stream.SubStream{\n\t\t\t\tStream:        strm,\n\t\t\t\tUseRTPPackets: false,\n\t\t\t}\n\t\t\terr = subStream.Initialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tdir, err := os.MkdirTemp(\"\", \"mediamtx-agent\")\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer os.RemoveAll(dir)\n\n\t\t\trecordPath := filepath.Join(dir, \"%path/%Y-%m-%d_%H-%M-%S-%f\")\n\n\t\t\tn := 0\n\n\t\t\tl := test.Logger(func(l logger.Level, format string, args ...any) {\n\t\t\t\tif n == 0 {\n\t\t\t\t\trequire.Equal(t, logger.Warn, l)\n\t\t\t\t\trequire.Equal(t, \"[recorder] skipping track 2 (VP8)\", fmt.Sprintf(format, args...))\n\t\t\t\t}\n\t\t\t\tn++\n\t\t\t})\n\n\t\t\tvar fo conf.RecordFormat\n\t\t\tif ca == \"fmp4\" {\n\t\t\t\tfo = conf.RecordFormatFMP4\n\t\t\t} else {\n\t\t\t\tfo = conf.RecordFormatMPEGTS\n\t\t\t}\n\n\t\t\tw := &Recorder{\n\t\t\t\tPathFormat:      recordPath,\n\t\t\t\tFormat:          fo,\n\t\t\t\tPartDuration:    100 * time.Millisecond,\n\t\t\t\tMaxPartSize:     50 * 1024 * 1024,\n\t\t\t\tSegmentDuration: 1 * time.Second,\n\t\t\t\tPathName:        \"mypath\",\n\t\t\t\tStream:          strm,\n\t\t\t\tParent:          l,\n\t\t\t}\n\t\t\tw.Initialize()\n\t\t\tdefer w.Close()\n\n\t\t\trequire.Equal(t, 2, n)\n\t\t})\n\t}\n}\n\nfunc TestRecorderSkipTracksFull(t *testing.T) {\n\tfor _, ca := range []string{\"fmp4\", \"mpegts\"} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tdesc := &description.Session{Medias: []*description.Media{\n\t\t\t\t{\n\t\t\t\t\tType:    description.MediaTypeVideo,\n\t\t\t\t\tFormats: []rtspformat.Format{&rtspformat.VP8{}},\n\t\t\t\t},\n\t\t\t}}\n\n\t\t\tstrm := &stream.Stream{\n\t\t\t\tDesc:              desc,\n\t\t\t\tWriteQueueSize:    512,\n\t\t\t\tRTPMaxPayloadSize: 1450,\n\t\t\t\tParent:            test.NilLogger,\n\t\t\t}\n\t\t\terr := strm.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer strm.Close()\n\n\t\t\tsubStream := &stream.SubStream{\n\t\t\t\tStream:        strm,\n\t\t\t\tUseRTPPackets: false,\n\t\t\t}\n\t\t\terr = subStream.Initialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tdir, err := os.MkdirTemp(\"\", \"mediamtx-agent\")\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer os.RemoveAll(dir)\n\n\t\t\trecordPath := filepath.Join(dir, \"%path/%Y-%m-%d_%H-%M-%S-%f\")\n\n\t\t\tn := 0\n\n\t\t\tl := test.Logger(func(l logger.Level, format string, args ...any) {\n\t\t\t\tif n == 0 {\n\t\t\t\t\trequire.Equal(t, logger.Warn, l)\n\t\t\t\t\trequire.Equal(t, \"[recorder] no supported tracks found, skipping recording\", fmt.Sprintf(format, args...))\n\t\t\t\t}\n\t\t\t\tn++\n\t\t\t})\n\n\t\t\tvar fo conf.RecordFormat\n\t\t\tif ca == \"fmp4\" {\n\t\t\t\tfo = conf.RecordFormatFMP4\n\t\t\t} else {\n\t\t\t\tfo = conf.RecordFormatMPEGTS\n\t\t\t}\n\n\t\t\tw := &Recorder{\n\t\t\t\tPathFormat:      recordPath,\n\t\t\t\tFormat:          fo,\n\t\t\t\tPartDuration:    100 * time.Millisecond,\n\t\t\t\tMaxPartSize:     50 * 1024 * 1024,\n\t\t\t\tSegmentDuration: 1 * time.Second,\n\t\t\t\tPathName:        \"mypath\",\n\t\t\t\tStream:          strm,\n\t\t\t\tParent:          l,\n\t\t\t}\n\t\t\tw.Initialize()\n\t\t\tdefer w.Close()\n\n\t\t\trequire.Equal(t, 1, n)\n\t\t})\n\t}\n}\n\nfunc TestRecorderFMP4SegmentSwitch(t *testing.T) {\n\tdesc := &description.Session{Medias: []*description.Media{\n\t\t{\n\t\t\tType:    description.MediaTypeVideo,\n\t\t\tFormats: []rtspformat.Format{test.FormatH264},\n\t\t},\n\t\t{\n\t\t\tType:    description.MediaTypeAudio,\n\t\t\tFormats: []rtspformat.Format{test.FormatMPEG4Audio},\n\t\t},\n\t}}\n\n\tstrm := &stream.Stream{\n\t\tDesc:              desc,\n\t\tWriteQueueSize:    512,\n\t\tRTPMaxPayloadSize: 1450,\n\t\tParent:            test.NilLogger,\n\t}\n\terr := strm.Initialize()\n\trequire.NoError(t, err)\n\tdefer strm.Close()\n\n\tsubStream := &stream.SubStream{\n\t\tStream:        strm,\n\t\tUseRTPPackets: false,\n\t}\n\terr = subStream.Initialize()\n\trequire.NoError(t, err)\n\n\tdir, err := os.MkdirTemp(\"\", \"mediamtx-agent\")\n\trequire.NoError(t, err)\n\tdefer os.RemoveAll(dir)\n\n\tn := 0\n\n\tw := &Recorder{\n\t\tPathFormat:      filepath.Join(dir, \"%path/%Y-%m-%d_%H-%M-%S-%f\"),\n\t\tFormat:          conf.RecordFormatFMP4,\n\t\tPartDuration:    100 * time.Millisecond,\n\t\tMaxPartSize:     50 * 1024 * 1024,\n\t\tSegmentDuration: 1 * time.Second,\n\t\tPathName:        \"mypath\",\n\t\tStream:          strm,\n\t\tParent:          test.NilLogger,\n\t\tOnSegmentCreate: func(segPath string) {\n\t\t\tswitch n {\n\t\t\tcase 0:\n\t\t\t\trequire.Equal(t, filepath.Join(dir, \"mypath\", \"2008-05-20_22-15-25-000000.mp4\"), segPath)\n\t\t\tcase 1:\n\t\t\t\trequire.Equal(t, filepath.Join(dir, \"mypath\", \"2008-05-20_22-15-25-700000.mp4\"), segPath) // +0.7s\n\t\t\t}\n\t\t\tn++\n\t\t},\n\t}\n\tw.Initialize()\n\n\tpts := 50 * time.Second\n\tntp := time.Date(2008, 5, 20, 22, 15, 25, 0, time.UTC)\n\n\tsubStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{\n\t\tPTS: int64(pts) * 90000 / int64(time.Second),\n\t\tNTP: ntp,\n\t\tPayload: unit.PayloadH264{\n\t\t\t{5}, // IDR\n\t\t},\n\t})\n\n\tpts += 700 * time.Millisecond\n\tntp = ntp.Add(700 * time.Millisecond)\n\n\tsubStream.WriteUnit(desc.Medias[1], desc.Medias[1].Formats[0], &unit.Unit{ // segment switch should happen here\n\t\tPTS:     int64(pts) * 44100 / int64(time.Second),\n\t\tNTP:     ntp,\n\t\tPayload: unit.PayloadMPEG4Audio{{1, 2}},\n\t})\n\n\tpts += 400 * time.Millisecond\n\tntp = ntp.Add(400 * time.Millisecond)\n\n\tsubStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{\n\t\tPTS: int64(pts) * 90000 / int64(time.Second),\n\t\tNTP: ntp,\n\t\tPayload: unit.PayloadH264{\n\t\t\t{5}, // IDR\n\t\t},\n\t})\n\n\tpts += 100 * time.Millisecond\n\tntp = ntp.Add(100 * time.Millisecond)\n\n\tsubStream.WriteUnit(desc.Medias[1], desc.Medias[1].Formats[0], &unit.Unit{\n\t\tPTS:     int64(pts) * 44100 / int64(time.Second),\n\t\tNTP:     ntp,\n\t\tPayload: unit.PayloadMPEG4Audio{{3, 4}},\n\t})\n\n\tpts += 400 * time.Millisecond\n\tntp = ntp.Add(400 * time.Millisecond)\n\n\tsubStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{\n\t\tPTS: int64(pts) * 90000 / int64(time.Second),\n\t\tNTP: ntp,\n\t\tPayload: unit.PayloadH264{\n\t\t\t{5}, // IDR\n\t\t},\n\t})\n\n\ttime.Sleep(100 * time.Millisecond)\n\n\tw.Close()\n\n\trequire.Equal(t, 2, n)\n}\n\nfunc TestRecorderTimeDriftDetector(t *testing.T) {\n\tfor _, ca := range []string{\"fmp4\", \"mpegts\"} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tdesc := &description.Session{Medias: []*description.Media{\n\t\t\t\t{\n\t\t\t\t\tType: description.MediaTypeVideo,\n\t\t\t\t\tFormats: []rtspformat.Format{&rtspformat.H264{\n\t\t\t\t\t\tPayloadTyp:        96,\n\t\t\t\t\t\tPacketizationMode: 1,\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t\t{\n\t\t\t\t\tType: description.MediaTypeAudio,\n\t\t\t\t\tFormats: []rtspformat.Format{&rtspformat.MPEG4Audio{\n\t\t\t\t\t\tPayloadTyp: 96,\n\t\t\t\t\t\tConfig: &mpeg4audio.AudioSpecificConfig{\n\t\t\t\t\t\t\tType:         2,\n\t\t\t\t\t\t\tSampleRate:   44100,\n\t\t\t\t\t\t\tChannelCount: 2,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tSizeLength:       13,\n\t\t\t\t\t\tIndexLength:      3,\n\t\t\t\t\t\tIndexDeltaLength: 3,\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t}}\n\n\t\t\tstrm := &stream.Stream{\n\t\t\t\tDesc:              desc,\n\t\t\t\tWriteQueueSize:    512,\n\t\t\t\tRTPMaxPayloadSize: 1450,\n\t\t\t\tParent:            test.NilLogger,\n\t\t\t}\n\t\t\terr := strm.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer strm.Close()\n\n\t\t\tsubStream := &stream.SubStream{\n\t\t\t\tStream:        strm,\n\t\t\t\tUseRTPPackets: false,\n\t\t\t}\n\t\t\terr = subStream.Initialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tdir, err := os.MkdirTemp(\"\", \"mediamtx-agent\")\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer os.RemoveAll(dir)\n\n\t\t\trecordPath := filepath.Join(dir, \"%path/%Y-%m-%d_%H-%M-%S-%f\")\n\n\t\t\tvar ext string\n\t\t\tif ca == \"fmp4\" {\n\t\t\t\text = \"mp4\"\n\t\t\t} else {\n\t\t\t\text = \"ts\"\n\t\t\t}\n\n\t\t\tsegCreated := make(chan struct{}, 10)\n\t\t\tsegDone := make(chan struct{}, 10)\n\n\t\t\tvar f conf.RecordFormat\n\t\t\tif ca == \"fmp4\" {\n\t\t\t\tf = conf.RecordFormatFMP4\n\t\t\t} else {\n\t\t\t\tf = conf.RecordFormatMPEGTS\n\t\t\t}\n\n\t\t\tw := &Recorder{\n\t\t\t\tPathFormat:      recordPath,\n\t\t\t\tFormat:          f,\n\t\t\t\tPartDuration:    100 * time.Millisecond,\n\t\t\t\tMaxPartSize:     50 * 1024 * 1024,\n\t\t\t\tSegmentDuration: 1 * time.Second,\n\t\t\t\tPathName:        \"mypath\",\n\t\t\t\tStream:          strm,\n\t\t\t\tOnSegmentCreate: func(_ string) {\n\t\t\t\t\tselect {\n\t\t\t\t\tcase segCreated <- struct{}{}:\n\t\t\t\t\tdefault:\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tOnSegmentComplete: func(_ string, _ time.Duration) {\n\t\t\t\t\tselect {\n\t\t\t\t\tcase segDone <- struct{}{}:\n\t\t\t\t\tdefault:\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tParent:       test.NilLogger,\n\t\t\t\trestartPause: 10 * time.Millisecond,\n\t\t\t}\n\t\t\tw.Initialize()\n\n\t\t\t// Write initial samples with correct timing\n\t\t\tstartDTS := int64(50 * 90000)\n\t\t\tstartNTP := time.Date(2008, 5, 20, 22, 15, 25, 0, time.UTC)\n\n\t\t\tfor i := range 3 {\n\t\t\t\tpts := startDTS + int64(i)*100*90000/1000\n\t\t\t\tntp := startNTP.Add(time.Duration(i*100) * time.Millisecond)\n\n\t\t\t\tsubStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{\n\t\t\t\t\tPTS: pts,\n\t\t\t\t\tNTP: ntp,\n\t\t\t\t\tPayload: unit.PayloadH264{\n\t\t\t\t\t\ttest.FormatH264.SPS,\n\t\t\t\t\t\ttest.FormatH264.PPS,\n\t\t\t\t\t\t{5}, // IDR\n\t\t\t\t\t},\n\t\t\t\t})\n\n\t\t\t\tsubStream.WriteUnit(desc.Medias[1], desc.Medias[1].Formats[0], &unit.Unit{\n\t\t\t\t\tPTS:     pts * int64(desc.Medias[1].Formats[0].ClockRate()) / 90000,\n\t\t\t\t\tNTP:     ntp,\n\t\t\t\t\tPayload: unit.PayloadMPEG4Audio{{1, 2, 3, 4}},\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Wait for first segment to be created\n\t\t\tselect {\n\t\t\tcase <-segCreated:\n\t\t\tcase <-time.After(2 * time.Second):\n\t\t\t\tt.Fatal(\"timeout waiting for first segment\")\n\t\t\t}\n\n\t\t\t// Write more samples to ensure segment has data\n\t\t\tfor i := 3; i < 15; i++ {\n\t\t\t\tpts := startDTS + int64(i)*100*90000/1000\n\t\t\t\tntp := startNTP.Add(time.Duration(i*100) * time.Millisecond)\n\n\t\t\t\tsubStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{\n\t\t\t\t\tPTS: pts,\n\t\t\t\t\tNTP: ntp,\n\t\t\t\t\tPayload: unit.PayloadH264{\n\t\t\t\t\t\ttest.FormatH264.SPS,\n\t\t\t\t\t\ttest.FormatH264.PPS,\n\t\t\t\t\t\t{5}, // IDR\n\t\t\t\t\t},\n\t\t\t\t})\n\n\t\t\t\tsubStream.WriteUnit(desc.Medias[1], desc.Medias[1].Formats[0], &unit.Unit{\n\t\t\t\t\tPTS:     pts * int64(desc.Medias[1].Formats[0].ClockRate()) / 90000,\n\t\t\t\t\tNTP:     ntp,\n\t\t\t\t\tPayload: unit.PayloadMPEG4Audio{{1, 2, 3, 4}},\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Simulate a time drift by advancing NTP time by more than 5 seconds\n\t\t\t// while keeping DTS progression normal (only 100ms forward)\n\t\t\tdriftedPTS := startDTS + 15*100*90000/1000\n\t\t\tdriftedNTP := startNTP.Add(15*100*time.Millisecond + 6*time.Second) // 6 second drift\n\n\t\t\tsubStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{\n\t\t\t\tPTS: driftedPTS,\n\t\t\t\tNTP: driftedNTP,\n\t\t\t\tPayload: unit.PayloadH264{\n\t\t\t\t\ttest.FormatH264.SPS,\n\t\t\t\t\ttest.FormatH264.PPS,\n\t\t\t\t\t{5}, // IDR\n\t\t\t\t},\n\t\t\t})\n\n\t\t\t// Wait for the recorder to detect the drift, complete the segment, and restart\n\t\t\tselect {\n\t\t\tcase <-segDone:\n\t\t\tcase <-time.After(2 * time.Second):\n\t\t\t\tt.Fatal(\"timeout waiting for segment completion after drift\")\n\t\t\t}\n\n\t\t\t// Give the recorder time to restart\n\t\t\ttime.Sleep(100 * time.Millisecond)\n\n\t\t\t// Write samples after restart with corrected timing\n\t\t\trestartDTS := int64(60 * 90000)\n\t\t\trestartNTP := time.Date(2008, 5, 20, 22, 15, 35, 0, time.UTC)\n\n\t\t\tfor i := range 3 {\n\t\t\t\tpts := restartDTS + int64(i)*100*90000/1000\n\t\t\t\tntp := restartNTP.Add(time.Duration(i*100) * time.Millisecond)\n\n\t\t\t\tsubStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{\n\t\t\t\t\tPTS: pts,\n\t\t\t\t\tNTP: ntp,\n\t\t\t\t\tPayload: unit.PayloadH264{\n\t\t\t\t\t\ttest.FormatH264.SPS,\n\t\t\t\t\t\ttest.FormatH264.PPS,\n\t\t\t\t\t\t{5}, // IDR\n\t\t\t\t\t},\n\t\t\t\t})\n\n\t\t\t\tsubStream.WriteUnit(desc.Medias[1], desc.Medias[1].Formats[0], &unit.Unit{\n\t\t\t\t\tPTS:     pts * int64(desc.Medias[1].Formats[0].ClockRate()) / 90000,\n\t\t\t\t\tNTP:     ntp,\n\t\t\t\t\tPayload: unit.PayloadMPEG4Audio{{1, 2, 3, 4}},\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// Wait for second segment to be created after restart\n\t\t\tselect {\n\t\t\tcase <-segCreated:\n\t\t\tcase <-time.After(2 * time.Second):\n\t\t\t\tt.Fatal(\"timeout waiting for segment after restart\")\n\t\t\t}\n\n\t\t\ttime.Sleep(50 * time.Millisecond)\n\n\t\t\tw.Close()\n\n\t\t\t// Wait for final segment to complete\n\t\t\tselect {\n\t\t\tcase <-segDone:\n\t\t\tcase <-time.After(2 * time.Second):\n\t\t\t\t// This is not fatal as the final segment may complete during Close()\n\t\t\t}\n\n\t\t\t// Verify that files were created\n\t\t\tentries, err := os.ReadDir(filepath.Join(dir, \"mypath\"))\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.GreaterOrEqual(t, len(entries), 2, \"expected at least 2 segments (before and after drift)\")\n\n\t\t\t// Verify files have the expected extension\n\t\t\tfor _, entry := range entries {\n\t\t\t\trequire.Equal(t, \".\"+ext, filepath.Ext(entry.Name()))\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/recordstore/mp4_boxes.go",
    "content": "package recordstore\n\nimport (\n\tamp4 \"github.com/abema/go-mp4\"\n)\n\nfunc boxTypeMtxi() amp4.BoxType { return amp4.StrToBoxType(\"mtxi\") }\n\nfunc init() { //nolint:gochecknoinits\n\tamp4.AddBoxDef(&Mtxi{}, 0)\n}\n\n// Mtxi is a MediaMTX segment info.\ntype Mtxi struct {\n\tamp4.FullBox  `mp4:\"0,extend\"`\n\tStreamID      [16]byte `mp4:\"1,size=8\"`\n\tSegmentNumber uint64   `mp4:\"2,size=64\"`\n\tDTS           int64    `mp4:\"3,size=64\"`\n\tNTP           int64    `mp4:\"4,size=64\"`\n}\n\n// GetType implements amp4.IBox.\nfunc (*Mtxi) GetType() amp4.BoxType {\n\treturn boxTypeMtxi()\n}\n"
  },
  {
    "path": "internal/recordstore/path.go",
    "content": "package recordstore\n\nimport (\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n)\n\nfunc leadingZeros(v int, size int) string {\n\tout := strconv.FormatInt(int64(v), 10)\n\tif len(out) >= size {\n\t\treturn out\n\t}\n\n\tout2 := \"\"\n\tfor i := 0; i < (size - len(out)); i++ {\n\t\tout2 += \"0\"\n\t}\n\n\treturn out2 + out\n}\n\nfunc timeLocationEncode(t time.Time) string {\n\t_, off := t.Zone()\n\n\tif off == 0 {\n\t\treturn \"Z\"\n\t}\n\n\tvar ret string\n\n\tif off > 0 {\n\t\tret = \"+\"\n\t} else {\n\t\tret = \"-\"\n\t\toff = -off\n\t}\n\n\tret += leadingZeros(off/60/60, 2)\n\tret += leadingZeros((off/60)%60, 2)\n\n\treturn ret\n}\n\nfunc timeLocationDecode(s string) *time.Location {\n\tif s == \"Z\" {\n\t\treturn time.UTC\n\t}\n\n\tvar sign int\n\tif s[0] == '+' {\n\t\tsign = 1\n\t} else {\n\t\tsign = -1\n\t}\n\n\tv1, _ := strconv.ParseInt(s[1:3], 10, 64)\n\tv2, _ := strconv.ParseInt(s[3:5], 10, 64)\n\n\toff := sign*int(v1)*3600 + int(v2)*3600\n\n\treturn time.FixedZone(\"myzone\", off)\n}\n\n// PathAddExtension adds the file extension to the path.\nfunc PathAddExtension(path string, format conf.RecordFormat) string {\n\tswitch format {\n\tcase conf.RecordFormatMPEGTS:\n\t\treturn path + \".ts\"\n\n\tdefault:\n\t\treturn path + \".mp4\"\n\t}\n}\n\n// CommonPath returns the common path between all segments with given recording path.\nfunc CommonPath(v string) string {\n\tcommon := \"\"\n\tremaining := v\n\n\tfor {\n\t\ti := strings.IndexAny(remaining, \"\\\\/\")\n\t\tif i < 0 {\n\t\t\tbreak\n\t\t}\n\n\t\tvar part string\n\t\tpart, remaining = remaining[:i+1], remaining[i+1:]\n\n\t\tif strings.Contains(part, \"%\") {\n\t\t\tbreak\n\t\t}\n\n\t\tcommon += part\n\t}\n\n\tif len(common) > 0 {\n\t\tcommon = common[:len(common)-1]\n\t}\n\n\treturn common\n}\n\n// Path is a path of a recording segment.\ntype Path struct {\n\tStart time.Time\n\tPath  string\n}\n\n// Decode decodes a Path.\nfunc (p *Path) Decode(format string, v string) bool {\n\tre := format\n\n\tfor _, ch := range []uint8{\n\t\t'\\\\',\n\t\t'.',\n\t\t'+',\n\t\t'*',\n\t\t'?',\n\t\t'^',\n\t\t'$',\n\t\t'(',\n\t\t')',\n\t\t'[',\n\t\t']',\n\t\t'{',\n\t\t'}',\n\t\t'|',\n\t} {\n\t\tre = strings.ReplaceAll(re, string(ch), \"\\\\\"+string(ch))\n\t}\n\n\tre = strings.ReplaceAll(re, \"%path\", \"(.*?)\")\n\tre = strings.ReplaceAll(re, \"%Y\", \"([0-9]{4})\")\n\tre = strings.ReplaceAll(re, \"%m\", \"([0-9]{2})\")\n\tre = strings.ReplaceAll(re, \"%d\", \"([0-9]{2})\")\n\tre = strings.ReplaceAll(re, \"%H\", \"([0-9]{2})\")\n\tre = strings.ReplaceAll(re, \"%M\", \"([0-9]{2})\")\n\tre = strings.ReplaceAll(re, \"%S\", \"([0-9]{2})\")\n\tre = strings.ReplaceAll(re, \"%f\", \"([0-9]{6})\")\n\tre = strings.ReplaceAll(re, \"%z\", \"(Z|\\\\+[0-9]{4}|-[0-9]{4})\")\n\tre = strings.ReplaceAll(re, \"%s\", \"([0-9]{10})\")\n\tr := regexp.MustCompile(re)\n\n\tvar groupMapping []string\n\tcur := format\n\tfor {\n\t\ti := strings.Index(cur, \"%\")\n\t\tif i < 0 {\n\t\t\tbreak\n\t\t}\n\n\t\tcur = cur[i:]\n\n\t\tfor _, va := range []string{\n\t\t\t\"%path\",\n\t\t\t\"%Y\",\n\t\t\t\"%m\",\n\t\t\t\"%d\",\n\t\t\t\"%H\",\n\t\t\t\"%M\",\n\t\t\t\"%S\",\n\t\t\t\"%f\",\n\t\t\t\"%z\",\n\t\t\t\"%s\",\n\t\t} {\n\t\t\tif strings.HasPrefix(cur, va) {\n\t\t\t\tgroupMapping = append(groupMapping, va)\n\t\t\t}\n\t\t}\n\n\t\tcur = cur[1:]\n\t}\n\n\tmatches := r.FindStringSubmatch(v)\n\tif matches == nil {\n\t\treturn false\n\t}\n\n\tvalues := make(map[string]string)\n\n\tfor i, match := range matches[1:] {\n\t\tvalues[groupMapping[i]] = match\n\t}\n\n\tvar year int\n\tvar month time.Month = 1\n\tday := 1\n\tvar hour int\n\tvar minute int\n\tvar second int\n\tvar micros int\n\tvar unixSec int64 = -1\n\tloc := time.Local\n\n\tfor k, v := range values {\n\t\tswitch k {\n\t\tcase \"%path\":\n\t\t\tp.Path = v\n\n\t\tcase \"%Y\":\n\t\t\ttmp, _ := strconv.ParseInt(v, 10, 64)\n\t\t\tyear = int(tmp)\n\n\t\tcase \"%m\":\n\t\t\ttmp, _ := strconv.ParseInt(v, 10, 64)\n\t\t\tmonth = time.Month(int(tmp))\n\n\t\tcase \"%d\":\n\t\t\ttmp, _ := strconv.ParseInt(v, 10, 64)\n\t\t\tday = int(tmp)\n\n\t\tcase \"%H\":\n\t\t\ttmp, _ := strconv.ParseInt(v, 10, 64)\n\t\t\thour = int(tmp)\n\n\t\tcase \"%M\":\n\t\t\ttmp, _ := strconv.ParseInt(v, 10, 64)\n\t\t\tminute = int(tmp)\n\n\t\tcase \"%S\":\n\t\t\ttmp, _ := strconv.ParseInt(v, 10, 64)\n\t\t\tsecond = int(tmp)\n\n\t\tcase \"%f\":\n\t\t\ttmp, _ := strconv.ParseInt(v, 10, 64)\n\t\t\tmicros = int(tmp)\n\n\t\tcase \"%z\":\n\t\t\tloc = timeLocationDecode(v)\n\n\t\tcase \"%s\":\n\t\t\tunixSec, _ = strconv.ParseInt(v, 10, 64)\n\t\t}\n\t}\n\n\tif unixSec > 0 {\n\t\tp.Start = time.Unix(unixSec, int64(micros)*1000)\n\t} else {\n\t\tp.Start = time.Date(year, month, day, hour, minute, second, micros*1000, loc)\n\t}\n\n\treturn true\n}\n\n// Encode encodes a path.\nfunc (p Path) Encode(format string) string {\n\tformat = strings.ReplaceAll(format, \"%path\", p.Path)\n\tformat = strings.ReplaceAll(format, \"%Y\", strconv.FormatInt(int64(p.Start.Year()), 10))\n\tformat = strings.ReplaceAll(format, \"%m\", leadingZeros(int(p.Start.Month()), 2))\n\tformat = strings.ReplaceAll(format, \"%d\", leadingZeros(p.Start.Day(), 2))\n\tformat = strings.ReplaceAll(format, \"%H\", leadingZeros(p.Start.Hour(), 2))\n\tformat = strings.ReplaceAll(format, \"%M\", leadingZeros(p.Start.Minute(), 2))\n\tformat = strings.ReplaceAll(format, \"%S\", leadingZeros(p.Start.Second(), 2))\n\tformat = strings.ReplaceAll(format, \"%f\", leadingZeros(p.Start.Nanosecond()/1000, 6))\n\tformat = strings.ReplaceAll(format, \"%z\", timeLocationEncode(p.Start))\n\tformat = strings.ReplaceAll(format, \"%s\", strconv.FormatInt(p.Start.Unix(), 10))\n\treturn format\n}\n"
  },
  {
    "path": "internal/recordstore/path_test.go",
    "content": "package recordstore\n\nimport (\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nvar pathCases = []struct {\n\tname   string\n\tformat string\n\tdec    Path\n\tenc    string\n}{\n\t{\n\t\t\"standard\",\n\t\t\"%path/%Y-%m-%d_%H-%M-%S-%f.mp4\",\n\t\tPath{\n\t\t\tStart: time.Date(2008, 11, 7, 11, 22, 4, 123456000, time.Local),\n\t\t\tPath:  \"mypath\",\n\t\t},\n\t\t\"mypath/2008-11-07_11-22-04-123456.mp4\",\n\t},\n\t{\n\t\t\"unix seconds\",\n\t\t\"%path/%s.mp4\",\n\t\tPath{\n\t\t\tStart: time.Date(2021, 12, 2, 12, 15, 23, 0, time.UTC).Local(),\n\t\t\tPath:  \"mypath\",\n\t\t},\n\t\t\"mypath/1638447323.mp4\",\n\t},\n\t{\n\t\t\"unix microseconds\",\n\t\t\"%path/%s.%f.mp4\",\n\t\tPath{\n\t\t\tStart: time.Date(2021, 12, 2, 12, 15, 23, 567324000, time.UTC).Local(),\n\t\t\tPath:  \"mypath\",\n\t\t},\n\t\t\"mypath/1638447323.567324.mp4\",\n\t},\n\t{\n\t\t\"timezone utc\",\n\t\t\"%path/%Y-%m-%d_%H-%M-%S-%f_%z.mp4\",\n\t\tPath{\n\t\t\tStart: time.Date(2021, 12, 2, 12, 15, 23, 567324000, time.UTC),\n\t\t\tPath:  \"mypath\",\n\t\t},\n\t\t\"mypath/2021-12-02_12-15-23-567324_Z.mp4\",\n\t},\n\t{\n\t\t\"timezone plus\",\n\t\t\"%path/%Y-%m-%d_%H-%M-%S-%f_%z.mp4\",\n\t\tPath{\n\t\t\tStart: time.Date(2021, 12, 2, 12, 15, 23, 567324000, time.FixedZone(\"myzone\", 7200)),\n\t\t\tPath:  \"mypath\",\n\t\t},\n\t\t\"mypath/2021-12-02_12-15-23-567324_+0200.mp4\",\n\t},\n\t{\n\t\t\"timezone minus\",\n\t\t\"%path/%Y-%m-%d_%H-%M-%S-%f_%z.mp4\",\n\t\tPath{\n\t\t\tStart: time.Date(2021, 12, 2, 12, 15, 23, 567324000, time.FixedZone(\"myzone\", -7200)),\n\t\t\tPath:  \"mypath\",\n\t\t},\n\t\t\"mypath/2021-12-02_12-15-23-567324_-0200.mp4\",\n\t},\n}\n\nfunc TestPathDecode(t *testing.T) {\n\tfor _, ca := range pathCases {\n\t\tt.Run(ca.name, func(t *testing.T) {\n\t\t\tvar dec Path\n\t\t\tok := dec.Decode(ca.format, ca.enc)\n\t\t\trequire.Equal(t, true, ok)\n\t\t\trequire.Equal(t, ca.dec, dec)\n\t\t})\n\t}\n}\n\nfunc TestPathEncode(t *testing.T) {\n\tfor _, ca := range pathCases {\n\t\tt.Run(ca.name, func(t *testing.T) {\n\t\t\trequire.Equal(t, ca.enc, ca.dec.Encode(ca.format))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/recordstore/recordstore.go",
    "content": "// Package recordstore contains utilities to store/retrieve recordings to/from disk.\npackage recordstore\n"
  },
  {
    "path": "internal/recordstore/segment.go",
    "content": "package recordstore\n\nimport (\n\t\"errors\"\n\t\"io/fs\"\n\t\"path/filepath\"\n\t\"sort\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n)\n\n// ErrNoSegmentsFound is returned when no recording segments have been found.\nvar ErrNoSegmentsFound = errors.New(\"no recording segments found\")\n\nvar errFound = errors.New(\"found\")\n\n// Segment is a recording segment.\ntype Segment struct {\n\tFpath string\n\tStart time.Time\n}\n\nfunc fixedPathHasSegments(pathConf *conf.Path) bool {\n\trecordPath := PathAddExtension(\n\t\tstrings.ReplaceAll(pathConf.RecordPath, \"%path\", pathConf.Name),\n\t\tpathConf.RecordFormat,\n\t)\n\n\t// we have to convert to absolute paths\n\t// otherwise, recordPath and fpath inside Walk() won't have common elements\n\trecordPath, _ = filepath.Abs(recordPath)\n\n\tcommonPath := CommonPath(recordPath)\n\n\terr := filepath.WalkDir(commonPath, func(fpath string, info fs.DirEntry, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif !info.IsDir() {\n\t\t\tvar pa Path\n\t\t\tok := pa.Decode(recordPath, fpath)\n\t\t\tif ok {\n\t\t\t\treturn errFound\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t})\n\tif err != nil && !errors.Is(err, errFound) {\n\t\treturn false\n\t}\n\n\treturn errors.Is(err, errFound)\n}\n\nfunc regexpPathFindPathsWithSegments(pathConf *conf.Path) map[string]struct{} {\n\trecordPath := PathAddExtension(\n\t\tpathConf.RecordPath,\n\t\tpathConf.RecordFormat,\n\t)\n\n\t// we have to convert to absolute paths\n\t// otherwise, recordPath and fpath inside Walk() won't have common elements\n\trecordPath, _ = filepath.Abs(recordPath)\n\n\tcommonPath := CommonPath(recordPath)\n\n\tret := make(map[string]struct{})\n\n\tfilepath.WalkDir(commonPath, func(fpath string, info fs.DirEntry, err error) error { //nolint:errcheck\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif !info.IsDir() {\n\t\t\tvar pa Path\n\t\t\tif ok := pa.Decode(recordPath, fpath); ok {\n\t\t\t\tif err = conf.IsValidPathName(pa.Path); err == nil {\n\t\t\t\t\tif pathConf.Regexp.FindStringSubmatch(pa.Path) != nil {\n\t\t\t\t\t\tret[pa.Path] = struct{}{}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t})\n\n\treturn ret\n}\n\n// FindAllPathsWithSegments returns all paths that have at least one segment.\nfunc FindAllPathsWithSegments(pathConfs map[string]*conf.Path) []string {\n\tpathNames := make(map[string]struct{})\n\n\tfor _, pathConf := range pathConfs {\n\t\tif pathConf.Regexp == nil {\n\t\t\tif fixedPathHasSegments(pathConf) {\n\t\t\t\tpathNames[pathConf.Name] = struct{}{}\n\t\t\t}\n\t\t} else {\n\t\t\tfor name := range regexpPathFindPathsWithSegments(pathConf) {\n\t\t\t\tpathNames[name] = struct{}{}\n\t\t\t}\n\t\t}\n\t}\n\n\tout := make([]string, len(pathNames))\n\tn := 0\n\tfor k := range pathNames {\n\t\tout[n] = k\n\t\tn++\n\t}\n\tsort.Strings(out)\n\n\treturn out\n}\n\n// FindSegments returns all segments of a path.\n// Segments can be filtered by start date and end date.\nfunc FindSegments(\n\tpathConf *conf.Path,\n\tpathName string,\n\tstart *time.Time,\n\tend *time.Time,\n) ([]*Segment, error) {\n\trecordPath := PathAddExtension(\n\t\tstrings.ReplaceAll(pathConf.RecordPath, \"%path\", pathName),\n\t\tpathConf.RecordFormat,\n\t)\n\n\t// we have to convert to absolute paths\n\t// otherwise, recordPath and fpath inside Walk() won't have common elements\n\trecordPath, _ = filepath.Abs(recordPath)\n\n\tcommonPath := CommonPath(recordPath)\n\tvar segments []*Segment\n\n\terr := filepath.WalkDir(commonPath, func(fpath string, info fs.DirEntry, err error) error {\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif !info.IsDir() {\n\t\t\tvar pa Path\n\t\t\tok := pa.Decode(recordPath, fpath)\n\n\t\t\t// gather all segments that start before the end of the playback\n\t\t\tif ok && (end == nil || !end.Before(pa.Start)) {\n\t\t\t\tsegments = append(segments, &Segment{\n\t\t\t\t\tFpath: fpath,\n\t\t\t\t\tStart: pa.Start,\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\n\t\treturn nil\n\t})\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif segments == nil {\n\t\treturn nil, ErrNoSegmentsFound\n\t}\n\n\tsort.Slice(segments, func(i, j int) bool {\n\t\treturn segments[i].Start.Before(segments[j].Start)\n\t})\n\n\tif start != nil {\n\t\tif start.Before(segments[0].Start) {\n\t\t\treturn segments, nil\n\t\t}\n\n\t\t// find the segment that may contain the start of the playback and remove all previous ones\n\t\tfound := false\n\t\tfor i := 0; i < len(segments)-1; i++ {\n\t\t\tif !start.Before(segments[i].Start) && start.Before(segments[i+1].Start) {\n\t\t\t\tsegments = segments[i:]\n\t\t\t\tfound = true\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\t// otherwise, keep the last segment only and check if it may contain the start of the playback\n\t\tif !found {\n\t\t\tsegments = segments[len(segments)-1:]\n\t\t\tif segments[len(segments)-1].Start.After(*start) {\n\t\t\t\treturn nil, ErrNoSegmentsFound\n\t\t\t}\n\t\t}\n\t}\n\n\treturn segments, nil\n}\n"
  },
  {
    "path": "internal/recordstore/segment_test.go",
    "content": "package recordstore\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc ptrOf[T any](v T) *T {\n\tp := new(T)\n\t*p = v\n\treturn p\n}\n\nfunc TestFindAllPathsWithSegments(t *testing.T) {\n\tdir, err := os.MkdirTemp(\"\", \"mediamtx-recordstore\")\n\trequire.NoError(t, err)\n\tdefer os.RemoveAll(dir)\n\n\terr = os.Mkdir(filepath.Join(dir, \"path1\"), 0o755)\n\trequire.NoError(t, err)\n\n\terr = os.Mkdir(filepath.Join(dir, \"path2\"), 0o755)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(dir, \"path1\", \"2015-05-19_22-15-25-000427.mp4\"), []byte{1}, 0o644)\n\trequire.NoError(t, err)\n\n\terr = os.WriteFile(filepath.Join(dir, \"path2\", \"2015-07-19_22-15-25-000427.mp4\"), []byte{1}, 0o644)\n\trequire.NoError(t, err)\n\n\tpaths := FindAllPathsWithSegments(map[string]*conf.Path{\n\t\t\"~^.*$\": {\n\t\t\tName:         \"~^.*$\",\n\t\t\tRegexp:       regexp.MustCompile(\"^.*$\"),\n\t\t\tRecordPath:   filepath.Join(dir, \"%path/%Y-%m-%d_%H-%M-%S-%f\"),\n\t\t\tRecordFormat: conf.RecordFormatFMP4,\n\t\t},\n\t\t\"path2\": {\n\t\t\tName:         \"path2\",\n\t\t\tRecordPath:   filepath.Join(dir, \"%path/%Y-%m-%d_%H-%M-%S-%f\"),\n\t\t\tRecordFormat: conf.RecordFormatFMP4,\n\t\t},\n\t})\n\trequire.Equal(t, []string{\"path1\", \"path2\"}, paths)\n}\n\nfunc TestFindAllPathsWithSegmentsInvalidPath(t *testing.T) {\n\tdir, err := os.MkdirTemp(\"\", \"mediamtx-recordstore\")\n\trequire.NoError(t, err)\n\tdefer os.RemoveAll(dir)\n\n\terr = os.WriteFile(filepath.Join(dir, \"_2015-05-19_22-15-25-000427.mp4\"), []byte{1}, 0o644)\n\trequire.NoError(t, err)\n\n\tpaths := FindAllPathsWithSegments(map[string]*conf.Path{\n\t\t\"~^.*$\": {\n\t\t\tName:         \"~^.*$\",\n\t\t\tRegexp:       regexp.MustCompile(\"^.*$\"),\n\t\t\tRecordPath:   filepath.Join(dir, \"%path_%Y-%m-%d_%H-%M-%S-%f\"),\n\t\t\tRecordFormat: conf.RecordFormatFMP4,\n\t\t},\n\t})\n\trequire.Equal(t, []string{}, paths)\n}\n\nfunc TestFindSegments(t *testing.T) {\n\tfor _, ca := range []string{\n\t\t\"no filtering\",\n\t\t\"filtering\",\n\t\t\"start before first\",\n\t} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tdir, err := os.MkdirTemp(\"\", \"mediamtx-recordstore\")\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer os.RemoveAll(dir)\n\n\t\t\terr = os.Mkdir(filepath.Join(dir, \"path1\"), 0o755)\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = os.Mkdir(filepath.Join(dir, \"path2\"), 0o755)\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = os.WriteFile(filepath.Join(dir, \"path1\", \"2015-05-19_22-15-25-000427.mp4\"), []byte{1}, 0o644)\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = os.WriteFile(filepath.Join(dir, \"path1\", \"2016-05-19_22-15-25-000427.mp4\"), []byte{1}, 0o644)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tvar start *time.Time\n\t\t\tvar end *time.Time\n\n\t\t\tswitch ca {\n\t\t\tcase \"no filtering\":\n\n\t\t\tcase \"filtering\":\n\t\t\t\tstart = ptrOf(time.Date(2015, 5, 19, 22, 18, 25, 427000, time.Local))\n\t\t\t\tend = ptrOf(start.Add(60 * time.Minute))\n\n\t\t\tcase \"start before first\":\n\t\t\t\tstart = ptrOf(time.Date(2014, 5, 19, 22, 18, 25, 427000, time.Local))\n\t\t\t}\n\n\t\t\tsegments, err := FindSegments(\n\t\t\t\t&conf.Path{\n\t\t\t\t\tName:         \"~^.*$\",\n\t\t\t\t\tRegexp:       regexp.MustCompile(\"^.*$\"),\n\t\t\t\t\tRecordPath:   filepath.Join(dir, \"%path/%Y-%m-%d_%H-%M-%S-%f\"),\n\t\t\t\t\tRecordFormat: conf.RecordFormatFMP4,\n\t\t\t\t},\n\t\t\t\t\"path1\",\n\t\t\t\tstart,\n\t\t\t\tend,\n\t\t\t)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tswitch ca {\n\t\t\tcase \"no filtering\", \"start before first\":\n\t\t\t\trequire.Equal(t, []*Segment{\n\t\t\t\t\t{\n\t\t\t\t\t\tFpath: filepath.Join(dir, \"path1\", \"2015-05-19_22-15-25-000427.mp4\"),\n\t\t\t\t\t\tStart: time.Date(2015, 5, 19, 22, 15, 25, 427000, time.Local),\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tFpath: filepath.Join(dir, \"path1\", \"2016-05-19_22-15-25-000427.mp4\"),\n\t\t\t\t\t\tStart: time.Date(2016, 5, 19, 22, 15, 25, 427000, time.Local),\n\t\t\t\t\t},\n\t\t\t\t}, segments)\n\n\t\t\tcase \"filtering\":\n\t\t\t\trequire.Equal(t, []*Segment{\n\t\t\t\t\t{\n\t\t\t\t\t\tFpath: filepath.Join(dir, \"path1\", \"2015-05-19_22-15-25-000427.mp4\"),\n\t\t\t\t\t\tStart: time.Date(2015, 5, 19, 22, 15, 25, 427000, time.Local),\n\t\t\t\t\t},\n\t\t\t\t}, segments)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/restrictnetwork/restrict_network.go",
    "content": "// Package restrictnetwork contains Restrict().\npackage restrictnetwork\n\nimport (\n\t\"net\"\n)\n\n// Restrict prevents listening on IPv6 when address is 0.0.0.0.\nfunc Restrict(network string, address string) (string, string) {\n\thost, _, err := net.SplitHostPort(address)\n\tif err == nil {\n\t\tif host == \"0.0.0.0\" {\n\t\t\treturn network + \"4\", address\n\t\t}\n\t}\n\n\treturn network, address\n}\n"
  },
  {
    "path": "internal/rlimit/rlimit_unix.go",
    "content": "//go:build !windows\n\n// Package rlimit contains a function to raise rlimit.\npackage rlimit\n\nimport (\n\t\"syscall\"\n)\n\n// Raise raises the number of file descriptors that can be opened.\nfunc Raise() error {\n\tvar rlim syscall.Rlimit\n\terr := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rlim)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\trlim.Cur = 999999\n\terr = syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rlim)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rlim)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/rlimit/rlimit_win.go",
    "content": "//go:build windows\n\npackage rlimit\n\nfunc Raise() error {\n\treturn nil\n}\n"
  },
  {
    "path": "internal/servers/hls/hlsjsdownloader/HASH",
    "content": "5cd2be2a4f7106b7c82a12bc15af6117f5230af92a44f26f88a0a32b04e57a81\n"
  },
  {
    "path": "internal/servers/hls/hlsjsdownloader/VERSION",
    "content": "v1.6.15\n"
  },
  {
    "path": "internal/servers/hls/hlsjsdownloader/main.go",
    "content": "// Package main contains an utility to download hls.js\npackage main\n\nimport (\n\t\"archive/zip\"\n\t\"bytes\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n)\n\nfunc do() error {\n\tbuf, err := os.ReadFile(\"./hlsjsdownloader/VERSION\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tversion := strings.TrimSpace(string(buf))\n\n\tlog.Printf(\"downloading hls.js %s...\", version)\n\n\tres, err := http.Get(\"https://github.com/video-dev/hls.js/releases/download/\" + version + \"/release.zip\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer res.Body.Close()\n\n\tif res.StatusCode != http.StatusOK {\n\t\treturn fmt.Errorf(\"bad status code: %v\", res.StatusCode)\n\t}\n\n\tzipBuf, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tbuf, err = os.ReadFile(\"./hlsjsdownloader/HASH\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tstr := strings.TrimSpace(string(buf))\n\n\thash, err := hex.DecodeString(str)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif sum := sha256.Sum256(zipBuf); !bytes.Equal(sum[:], hash) {\n\t\treturn fmt.Errorf(\"hash mismatch\")\n\t}\n\n\tz, err := zip.NewReader(bytes.NewReader(zipBuf), int64(len(zipBuf)))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\thls, err := fs.ReadFile(z, \"dist/hls.min.js\")\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err = os.WriteFile(\"hls.min.js\", hls, 0o644); err != nil {\n\t\treturn err\n\t}\n\n\tlog.Println(\"ok\")\n\treturn nil\n}\n\nfunc main() {\n\terr := do()\n\tif err != nil {\n\t\tlog.Printf(\"ERR: %v\", err)\n\t\tos.Exit(1)\n\t}\n}\n"
  },
  {
    "path": "internal/servers/hls/http_server.go",
    "content": "package hls\n\nimport (\n\t_ \"embed\"\n\t\"errors\"\n\t\"net\"\n\t\"net/http\"\n\tgopath \"path\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gin-gonic/gin\"\n\n\t\"github.com/bluenviron/mediamtx/internal/auth\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/httpp\"\n)\n\n//go:generate go run ./hlsjsdownloader\n\n//go:embed index.html\nvar hlsIndex []byte\n\n//go:embed hls.min.js\nvar hlsMinJS []byte\n\nfunc mergePathAndQuery(path string, rawQuery string) string {\n\tres := path\n\tif rawQuery != \"\" {\n\t\tres += \"?\" + rawQuery\n\t}\n\treturn res\n}\n\ntype httpServer struct {\n\taddress        string\n\tdumpPackets    bool\n\tencryption     bool\n\tserverKey      string\n\tserverCert     string\n\tallowOrigins   []string\n\ttrustedProxies conf.IPNetworks\n\treadTimeout    conf.Duration\n\twriteTimeout   conf.Duration\n\tpathManager    serverPathManager\n\tparent         *Server\n\n\tinner *httpp.Server\n}\n\nfunc (s *httpServer) initialize() error {\n\trouter := gin.New()\n\trouter.SetTrustedProxies(s.trustedProxies.ToTrustedProxies()) //nolint:errcheck\n\n\trouter.Use(s.middlewarePreflightRequests)\n\n\trouter.Use(s.onRequest)\n\n\ts.inner = &httpp.Server{\n\t\tAddress:           s.address,\n\t\tAllowOrigins:      s.allowOrigins,\n\t\tDumpPackets:       s.dumpPackets,\n\t\tDumpPacketsPrefix: \"hls_server_conn\",\n\t\tReadTimeout:       time.Duration(s.readTimeout),\n\t\tWriteTimeout:      time.Duration(s.writeTimeout),\n\t\tEncryption:        s.encryption,\n\t\tServerCert:        s.serverCert,\n\t\tServerKey:         s.serverKey,\n\t\tHandler:           router,\n\t\tParent:            s,\n\t}\n\terr := s.inner.Initialize()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// Log implements logger.Writer.\nfunc (s *httpServer) Log(level logger.Level, format string, args ...any) {\n\ts.parent.Log(level, format, args...)\n}\n\nfunc (s *httpServer) close() {\n\ts.inner.Close()\n}\n\nfunc (s *httpServer) middlewarePreflightRequests(ctx *gin.Context) {\n\tif ctx.Request.Method == http.MethodOptions &&\n\t\tctx.Request.Header.Get(\"Access-Control-Request-Method\") != \"\" {\n\t\tctx.Header(\"Access-Control-Allow-Methods\", \"OPTIONS, GET\")\n\t\tctx.Header(\"Access-Control-Allow-Headers\", \"Authorization, Range\")\n\t\tctx.AbortWithStatus(http.StatusNoContent)\n\t\treturn\n\t}\n}\n\nfunc (s *httpServer) onRequest(ctx *gin.Context) {\n\tif ctx.Request.Method != http.MethodGet {\n\t\treturn\n\t}\n\n\t// remove leading prefix\n\tpa := ctx.Request.URL.Path[1:]\n\n\tvar dir string\n\tvar fname string\n\n\tswitch {\n\tcase strings.HasSuffix(pa, \"/hls.min.js\"):\n\t\tctx.Header(\"Cache-Control\", \"max-age=3600\")\n\t\tctx.Header(\"Content-Type\", \"application/javascript\")\n\t\tctx.Writer.WriteHeader(http.StatusOK)\n\t\tctx.Writer.Write(hlsMinJS)\n\t\treturn\n\n\tcase pa == \"\", pa == \"favicon.ico\", strings.HasSuffix(pa, \"/hls.min.js.map\"):\n\t\treturn\n\n\tcase strings.HasSuffix(pa, \".m3u8\") ||\n\t\tstrings.HasSuffix(pa, \".ts\") ||\n\t\tstrings.HasSuffix(pa, \".mp4\") ||\n\t\tstrings.HasSuffix(pa, \".mp\"):\n\t\tdir, fname = gopath.Dir(pa), gopath.Base(pa)\n\n\t\tif strings.HasSuffix(fname, \".mp\") {\n\t\t\tfname += \"4\"\n\t\t}\n\n\tdefault:\n\t\tdir, fname = pa, \"\"\n\n\t\tif !strings.HasSuffix(dir, \"/\") {\n\t\t\tctx.Header(\"Location\", mergePathAndQuery(ctx.Request.URL.Path+\"/\", ctx.Request.URL.RawQuery))\n\t\t\tctx.Writer.WriteHeader(http.StatusMovedPermanently)\n\t\t\treturn\n\t\t}\n\t}\n\n\tdir = strings.TrimSuffix(dir, \"/\")\n\tif dir == \"\" {\n\t\treturn\n\t}\n\n\tres, err := s.pathManager.FindPathConf(defs.PathFindPathConfReq{\n\t\tAccessRequest: defs.PathAccessRequest{\n\t\t\tName:        dir,\n\t\t\tQuery:       ctx.Request.URL.RawQuery,\n\t\t\tPublish:     false,\n\t\t\tProto:       auth.ProtocolHLS,\n\t\t\tCredentials: httpp.Credentials(ctx.Request),\n\t\t\tIP:          net.ParseIP(ctx.ClientIP()),\n\t\t},\n\t})\n\tif err != nil {\n\t\tvar terr *auth.Error\n\t\tif errors.As(err, &terr) {\n\t\t\tif terr.AskCredentials {\n\t\t\t\tctx.Header(\"WWW-Authenticate\", `Basic realm=\"mediamtx\"`)\n\t\t\t\tctx.AbortWithStatusJSON(http.StatusUnauthorized, &defs.APIError{\n\t\t\t\t\tStatus: defs.APIErrorStatusError,\n\t\t\t\t\tError:  \"authentication error\",\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\ts.Log(logger.Info, \"connection %v failed to authenticate: %v\", httpp.RemoteAddr(ctx), terr.Wrapped)\n\n\t\t\t// wait some seconds to delay brute force attacks\n\t\t\t<-time.After(auth.PauseAfterError)\n\n\t\t\tctx.AbortWithStatusJSON(http.StatusUnauthorized, &defs.APIError{\n\t\t\t\tStatus: defs.APIErrorStatusError,\n\t\t\t\tError:  \"authentication error\",\n\t\t\t})\n\t\t\treturn\n\t\t}\n\n\t\tctx.Writer.WriteHeader(http.StatusNotFound)\n\t\treturn\n\t}\n\n\tswitch fname {\n\tcase \"\":\n\t\tctx.Header(\"Cache-Control\", \"max-age=3600\")\n\t\tctx.Header(\"Content-Type\", \"text/html\")\n\t\tctx.Writer.WriteHeader(http.StatusOK)\n\t\tctx.Writer.Write(hlsIndex)\n\n\tdefault:\n\t\tvar mux *muxer\n\t\tmux, err = s.parent.getMuxer(serverGetMuxerReq{\n\t\t\tpath:           dir,\n\t\t\tremoteAddr:     httpp.RemoteAddr(ctx),\n\t\t\tquery:          ctx.Request.URL.RawQuery,\n\t\t\tsourceOnDemand: res.Conf.SourceOnDemand,\n\t\t})\n\t\tif err != nil {\n\t\t\tctx.Writer.WriteHeader(http.StatusNotFound)\n\t\t\treturn\n\t\t}\n\n\t\tctx.Request.URL.Path = fname\n\t\tmux.handleRequest(ctx)\n\t}\n}\n"
  },
  {
    "path": "internal/servers/hls/index.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width\">\n<style>\nhtml, body {\n\tmargin: 0;\n\tpadding: 0;\n\theight: 100%;\n\tfont-family: 'Arial', sans-serif;\n}\n#video {\n\tposition: absolute;\n\ttop: 0;\n\tleft: 0;\n\twidth: 100%;\n\theight: 100%;\n\tbackground: rgb(30, 30, 30);\n}\n#message {\n\tposition: absolute;\n\tleft: 0;\n\ttop: 0;\n\twidth: 100%;\n\theight: 100%;\n\tdisplay: flex;\n\talign-items: center;\n\ttext-align: center;\n\tjustify-content: center;\n\tfont-size: 16px;\n\tfont-weight: bold;\n\tcolor: white;\n\tpointer-events: none;\n\tpadding: 20px;\n\tbox-sizing: border-box;\n\ttext-shadow: 0 0 5px black;\n}\n#lang-icon {\n\tdisplay: none;\n\tposition: absolute;\n\ttop: 20px;\n\tright: 20px;\n\twidth: 30px;\n\theight: 30px;\n\tbackground-image: url(\"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCA1MCA1MCIgZmlsbD0iI2ZmZiIgeG1sbnM6dj0iaHR0cHM6Ly92ZWN0YS5pby9uYW5vIj48cGF0aCBkPSJNMzguNSAzMy45bC0xLjktMS42YzIuNS0yLjkgMy44LTYuMyAzLjgtOS45IDAtMy4xLTEtNi4xLTIuOS04LjhsMi4xLTEuNWMyLjIgMy4xIDMuNCA2LjYgMy40IDEwLjItLjEgNC4zLTEuNiA4LjMtNC41IDExLjZ6TTUuNiAyMy4yaC0zYy0uNSAwLTEgLjUtMSAxLjF2MTAuNWMwIC42LjQgMS4xIDEgMS4xaDNjLjIgMCAuMyAwIC40LjFsMTMuOCA3LjhjLjYuNCAxLjQtLjIgMS40LTFWMTYuM2MwLS44LS44LTEuMy0xLjQtMUw2LjEgMjMuMWMtLjIuMS0uMy4xLS41LjF6bTIxLTE2LjlMMTIuOCAxNGMtLjEuMS0uMy4xLS40LjFoLTNjLS41IDAtMSAuNS0xIDEuMVYyMGwxMi4yLTYuOGExLjM2IDEuMzYgMCAwIDEgMS41IDBjLjUuMy44LjguOCAxLjV2MTcuOWwzLjcgMi4xYy42LjQgMS40LS4yIDEuNC0xVjcuMmMuMS0uOC0uNy0xLjMtMS40LS45em0xNi41IDMwLjJsLTEuOS0xLjZjMy4xLTMuNyA0LjctOCA0LjctMTIuNSAwLTQtMS4zLTcuOC0zLjctMTEuMmwyLjEtMS41YzIuNyAzLjggNC4yIDguMiA0LjIgMTIuNy0uMiA1LjEtMiA5LjktNS40IDE0LjF6TTM1IDMxLjFsLTItMS42YzEuNy0yLjEgMi42LTQuNiAyLjYtNy4yIDAtMi40LS44LTQuNy0yLjItNi43bDItMS41YzEuOCAyLjUgMi43IDUuMyAyLjcgOC4yIDAgMy4yLTEuMSA2LjItMy4xIDguOHoiLz48L3N2Zz4=\");\n\tbackground-size: 80%;\n\tbackground-position: center;\n\tbackground-repeat: no-repeat;\n\tcursor: pointer;\n}\n#lang-list {\n\tdisplay: none;\n\tposition: absolute;\n\ttop: 100%;\n\tright: 0;\n\tbackground: rgb(190, 190, 190);\n\tcolor: black;\n}\n#lang-icon:hover #lang-list {\n\tdisplay: block;\n}\n#lang-list div {\n\tborder-bottom: 1px solid black;\n\tpadding: 5px 15px;\n}\n</style>\n</head>\n<body>\n\n<video id=\"video\"></video>\n<div id=\"message\"></div>\n<div id=\"lang-icon\"><div id=\"lang-list\"></div></div>\n\n<script defer src=\"hls.min.js\"></script>\n\n<script>\n\nconst retryPause = 2000;\n\nconst video = document.getElementById('video');\nconst message = document.getElementById('message');\nconst langIcon = document.getElementById('lang-icon');\nconst langList = document.getElementById('lang-list');\n\nlet defaultControls = false;\n\nconst setMessage = (str) => {\n\tif (str !== '') {\n\t\tvideo.controls = false;\n\t} else {\n\t\tvideo.controls = defaultControls;\n\t}\n\tmessage.innerText = str;\n};\n\nconst isIOS = () => (\n\t/iPad|iPhone|iPod/.test(navigator.platform)\n\t|| (navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1)\n);\n\nconst loadStream = () => {\n\t// Prefer hls.js over native HLS.\n\t// This is because some Android versions support native HLS\n\t// but don't support fMP4s.\n\t// Skip iPad iOS >= 13 and iPhone iOS >= 17,\n\t// which support hls.js but don't support well maxLiveSyncPlaybackRate.\n\tif (Hls.isSupported() && !isIOS()) {\n\t\tconst hls = new Hls({\n\t\t\tmaxLiveSyncPlaybackRate: 1.5,\n\t\t});\n\n\t\thls.on(Hls.Events.ERROR, (evt, data) => {\n\t\t\tif (data.fatal) {\n\t\t\t\thls.destroy();\n\n\t\t\t\tlangIcon.style.display = 'none';\n\t\t\t\tlangList.innerHTML = '';\n\n\t\t\t\tif (data.details === 'manifestIncompatibleCodecsError') {\n\t\t\t\t\tsetMessage('stream makes use of codecs which are not compatible with this browser or operative system');\n\t\t\t\t} else if (data.response && data.response.code === 404) {\n\t\t\t\t\tsetMessage('stream not found, retrying in some seconds');\n\t\t\t\t} else {\n\t\t\t\t\tsetMessage(data.error + ', retrying in some seconds');\n\t\t\t\t}\n\n\t\t\t\tsetTimeout(() => loadStream(video), retryPause);\n\t\t\t}\n\t\t});\n\n\t\thls.on(Hls.Events.MEDIA_ATTACHED, () => {\n\t\t\thls.loadSource('index.m3u8' + window.location.search);\n\t\t});\n\n\t\thls.on(Hls.Events.MANIFEST_LOADED, () => {\n\t\t\tif (hls.audioTracks.length > 1) {\n\t\t\t\tfor (const track of hls.audioTracks) {\n\t\t\t\t\tconst div = document.createElement('DIV');\n\t\t\t\t\tdiv.innerText = track.name;\n\t\t\t\t\tdiv.addEventListener('click', () => {\n\t\t\t\t\t\thls.audioTrack = track.id;\n\t\t\t\t\t});\n\t\t\t\t\tlangList.appendChild(div);\n\t\t\t\t}\n\t\t\t\tlangIcon.style.display = 'block';\n\t\t\t}\n\n\t\t\tsetMessage('');\n\t\t\tvideo.play();\n\t\t});\n\n\t\t// when the video is resumed after a manual or forced pause\n\t\t// (i.e. when the window is minimized), restore live streaming.\n\t\tvideo.onplay = () => {\n\t\t\tvideo.currentTime = hls.liveSyncPosition;\n\t\t};\n\n\t\thls.attachMedia(video);\n\n\t} else if (video.canPlayType('application/vnd.apple.mpegurl')) {\n\t\t// since it's not possible to detect timeout errors in iOS,\n\t\t// wait for the playlist to be available before starting the stream\n\t\tfetch('index.m3u8' + window.location.search)\n\t\t\t.then(() => {\n\t\t\t\tvideo.src = 'index.m3u8' + window.location.search;\n\t\t\t\tvideo.play();\n\t\t\t});\n\t}\n};\n\nconst parseBoolString = (str, defaultVal) => {\n\tstr = (str || '');\n\n\tif (['1', 'yes', 'true'].includes(str.toLowerCase())) {\n\t\treturn true;\n\t}\n\tif (['0', 'no', 'false'].includes(str.toLowerCase())) {\n\t\treturn false;\n\t}\n\treturn defaultVal;\n};\n\nconst loadAttributesFromQuery = () => {\n\tconst params = new URLSearchParams(window.location.search);\n\tvideo.controls = parseBoolString(params.get('controls'), true);\n\tvideo.muted = parseBoolString(params.get('muted'), true);\n\tvideo.autoplay = parseBoolString(params.get('autoplay'), true);\n\tvideo.playsInline = parseBoolString(params.get('playsinline'), true);\n\tvideo.disablepictureinpicture = parseBoolString(params.get('disablepictureinpicture'), false);\n\tdefaultControls = video.controls;\n};\n\n// use load instead of DOMContentLoaded, otherwise, in Firefox,\n// the page gets stuck in the \"loading\" state.\nwindow.addEventListener('load', () => {\n\tloadAttributesFromQuery();\n\tloadStream();\n});\n\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "internal/servers/hls/muxer.go",
    "content": "package hls\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/hls\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n\t\"github.com/gin-gonic/gin\"\n)\n\nconst (\n\tcloseCheckPeriod = 1 * time.Second\n\trecreatePause    = 10 * time.Second\n)\n\nfunc ptrOf[T any](v T) *T {\n\tp := new(T)\n\t*p = v\n\treturn p\n}\n\nfunc emptyTimer() *time.Timer {\n\tt := time.NewTimer(0)\n\t<-t.C\n\treturn t\n}\n\ntype responseWriterWithCounter struct {\n\thttp.ResponseWriter\n\tbytesSent *uint64\n}\n\nfunc (w *responseWriterWithCounter) Write(p []byte) (int, error) {\n\tn, err := w.ResponseWriter.Write(p)\n\tatomic.AddUint64(w.bytesSent, uint64(n))\n\treturn n, err\n}\n\ntype muxerGetInstanceRes struct {\n\tinstance                         *muxerInstance\n\tcumulatedOutboundFramesDiscarded uint64\n}\n\ntype muxerGetInstanceReq struct {\n\tres chan muxerGetInstanceRes\n}\n\ntype muxer struct {\n\tparentCtx       context.Context\n\tremoteAddr      string\n\tvariant         conf.HLSVariant\n\tsegmentCount    int\n\tsegmentDuration conf.Duration\n\tpartDuration    conf.Duration\n\tsegmentMaxSize  conf.StringSize\n\tdirectory       string\n\tcloseAfter      conf.Duration\n\twg              *sync.WaitGroup\n\tpathName        string\n\tpathManager     serverPathManager\n\tparent          *Server\n\tquery           string\n\n\tctx             context.Context\n\tctxCancel       func()\n\tcreated         time.Time\n\tpath            defs.Path\n\tlastRequestTime *int64\n\tbytesSent       *uint64\n\n\t// in\n\tchGetInstance chan muxerGetInstanceReq\n}\n\nfunc (m *muxer) initialize() {\n\tctx, ctxCancel := context.WithCancel(m.parentCtx)\n\n\tm.ctx = ctx\n\tm.ctxCancel = ctxCancel\n\tm.created = time.Now()\n\tm.lastRequestTime = ptrOf(time.Now().UnixNano())\n\tm.bytesSent = new(uint64)\n\tm.chGetInstance = make(chan muxerGetInstanceReq)\n\n\tm.Log(logger.Info, \"created %s\", func() string {\n\t\tif m.remoteAddr == \"\" {\n\t\t\treturn \"automatically\"\n\t\t}\n\t\treturn \"(requested by \" + m.remoteAddr + \")\"\n\t}())\n\n\tm.wg.Add(1)\n\tgo m.run()\n}\n\nfunc (m *muxer) Close() {\n\tm.ctxCancel()\n}\n\n// Log implements logger.Writer.\nfunc (m *muxer) Log(level logger.Level, format string, args ...any) {\n\tm.parent.Log(level, \"[muxer %s] \"+format, append([]any{m.pathName}, args...)...)\n}\n\n// PathName returns the path name.\nfunc (m *muxer) PathName() string {\n\treturn m.pathName\n}\n\nfunc (m *muxer) run() {\n\tdefer m.wg.Done()\n\n\terr := m.runInner()\n\n\tm.ctxCancel()\n\n\tm.parent.closeMuxer(m)\n\n\tm.Log(logger.Info, \"destroyed: %v\", err)\n}\n\nfunc (m *muxer) runInner() error {\n\tres, err := m.pathManager.AddReader(defs.PathAddReaderReq{\n\t\tAuthor: m,\n\t\tAccessRequest: defs.PathAccessRequest{\n\t\t\tName:     m.pathName,\n\t\t\tQuery:    m.query,\n\t\t\tSkipAuth: true,\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tm.path = res.Path\n\n\tdefer m.path.RemoveReader(defs.PathRemoveReaderReq{Author: m})\n\n\tmi, err := m.createInstance(res.Stream)\n\tif err != nil {\n\t\tif m.remoteAddr != \"\" || errors.Is(err, hls.ErrNoSupportedCodecs) {\n\t\t\treturn err\n\t\t}\n\n\t\tm.Log(logger.Error, err.Error())\n\t\tmi = nil\n\t}\n\n\tdefer func() {\n\t\tif mi != nil {\n\t\t\tmi.close()\n\t\t}\n\t}()\n\n\tvar instanceError chan error\n\tvar recreateTimer *time.Timer\n\n\tif mi != nil {\n\t\tinstanceError = mi.errorChan()\n\t\trecreateTimer = emptyTimer()\n\t} else {\n\t\tinstanceError = make(chan error)\n\t\trecreateTimer = time.NewTimer(recreatePause)\n\t}\n\n\tvar activityCheckTimer *time.Timer\n\tif m.remoteAddr != \"\" {\n\t\tactivityCheckTimer = time.NewTimer(closeCheckPeriod)\n\t} else {\n\t\tactivityCheckTimer = emptyTimer()\n\t}\n\n\tcumulatedOutboundFramesDiscarded := uint64(0)\n\n\tfor {\n\t\tselect {\n\t\tcase req := <-m.chGetInstance:\n\t\t\treq.res <- muxerGetInstanceRes{\n\t\t\t\tinstance:                         mi,\n\t\t\t\tcumulatedOutboundFramesDiscarded: cumulatedOutboundFramesDiscarded,\n\t\t\t}\n\n\t\tcase err = <-instanceError:\n\t\t\tif m.remoteAddr != \"\" {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tm.Log(logger.Error, err.Error())\n\t\t\tmi.close()\n\t\t\tcumulatedOutboundFramesDiscarded += mi.reader.OutboundFramesDiscarded()\n\t\t\tmi = nil\n\t\t\tinstanceError = make(chan error)\n\t\t\trecreateTimer = time.NewTimer(recreatePause)\n\n\t\tcase <-recreateTimer.C:\n\t\t\tmi, err = m.createInstance(res.Stream)\n\t\t\tif err != nil {\n\t\t\t\tm.Log(logger.Error, err.Error())\n\t\t\t\tmi = nil\n\t\t\t\trecreateTimer = time.NewTimer(recreatePause)\n\t\t\t} else {\n\t\t\t\tinstanceError = mi.errorChan()\n\t\t\t}\n\n\t\tcase <-activityCheckTimer.C:\n\t\t\tt := time.Unix(0, atomic.LoadInt64(m.lastRequestTime))\n\t\t\tif time.Since(t) >= time.Duration(m.closeAfter) {\n\t\t\t\treturn fmt.Errorf(\"not used anymore\")\n\t\t\t}\n\t\t\tactivityCheckTimer = time.NewTimer(closeCheckPeriod)\n\n\t\tcase <-m.ctx.Done():\n\t\t\treturn errors.New(\"terminated\")\n\t\t}\n\t}\n}\n\nfunc (m *muxer) createInstance(strm *stream.Stream) (*muxerInstance, error) {\n\tmi := &muxerInstance{\n\t\tvariant:         m.variant,\n\t\tsegmentCount:    m.segmentCount,\n\t\tsegmentDuration: m.segmentDuration,\n\t\tpartDuration:    m.partDuration,\n\t\tsegmentMaxSize:  m.segmentMaxSize,\n\t\tdirectory:       m.directory,\n\t\tpathName:        m.pathName,\n\t\tstream:          strm,\n\t\tparent:          m,\n\t}\n\terr := mi.initialize()\n\treturn mi, err\n}\n\nfunc (m *muxer) getInstance() muxerGetInstanceRes {\n\treq := muxerGetInstanceReq{res: make(chan muxerGetInstanceRes)}\n\n\tselect {\n\tcase m.chGetInstance <- req:\n\t\treturn <-req.res\n\n\tcase <-m.ctx.Done():\n\t\treturn muxerGetInstanceRes{}\n\t}\n}\n\n// APIReaderDescribe implements reader.\nfunc (m *muxer) APIReaderDescribe() *defs.APIPathReader {\n\treturn &defs.APIPathReader{\n\t\tType: defs.APIPathReaderTypeHLSMuxer,\n\t\tID:   \"\",\n\t}\n}\n\nfunc (m *muxer) handleRequest(ctx *gin.Context) {\n\tatomic.StoreInt64(m.lastRequestTime, time.Now().UnixNano())\n\n\tres := m.getInstance()\n\tif res.instance == nil {\n\t\tctx.Writer.WriteHeader(http.StatusNotFound)\n\t\treturn\n\t}\n\n\tw := &responseWriterWithCounter{\n\t\tResponseWriter: ctx.Writer,\n\t\tbytesSent:      m.bytesSent,\n\t}\n\n\tres.instance.handleRequest(w, ctx.Request)\n}\n\nfunc (m *muxer) apiItem() *defs.APIHLSMuxer {\n\tres := m.getInstance()\n\n\toutboundFramesDiscarded := res.cumulatedOutboundFramesDiscarded\n\tif res.instance != nil {\n\t\toutboundFramesDiscarded += res.instance.reader.OutboundFramesDiscarded()\n\t}\n\n\treturn &defs.APIHLSMuxer{\n\t\tPath:                    m.pathName,\n\t\tCreated:                 m.created,\n\t\tLastRequest:             time.Unix(0, atomic.LoadInt64(m.lastRequestTime)),\n\t\tOutboundBytes:           atomic.LoadUint64(m.bytesSent),\n\t\tOutboundFramesDiscarded: outboundFramesDiscarded,\n\t\tBytesSent:               atomic.LoadUint64(m.bytesSent),\n\t}\n}\n"
  },
  {
    "path": "internal/servers/hls/muxer_instance.go",
    "content": "package hls\n\nimport (\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gohlslib/v2\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/hls\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n)\n\ntype muxerInstance struct {\n\tvariant         conf.HLSVariant\n\tsegmentCount    int\n\tsegmentDuration conf.Duration\n\tpartDuration    conf.Duration\n\tsegmentMaxSize  conf.StringSize\n\tdirectory       string\n\tpathName        string\n\tstream          *stream.Stream\n\tparent          logger.Writer\n\n\thmuxer *gohlslib.Muxer\n\treader *stream.Reader\n}\n\nfunc (mi *muxerInstance) initialize() error {\n\tvar muxerDirectory string\n\tif mi.directory != \"\" {\n\t\tmuxerDirectory = filepath.Join(mi.directory, mi.pathName)\n\t\tos.MkdirAll(muxerDirectory, 0o755)\n\t}\n\n\tmi.hmuxer = &gohlslib.Muxer{\n\t\tVariant:            gohlslib.MuxerVariant(mi.variant),\n\t\tSegmentCount:       mi.segmentCount,\n\t\tSegmentMinDuration: time.Duration(mi.segmentDuration),\n\t\tPartMinDuration:    time.Duration(mi.partDuration),\n\t\tSegmentMaxSize:     uint64(mi.segmentMaxSize),\n\t\tDirectory:          muxerDirectory,\n\t\tOnEncodeError: func(err error) {\n\t\t\tmi.Log(logger.Warn, err.Error())\n\t\t},\n\t}\n\n\tmi.reader = &stream.Reader{\n\t\tSkipBytesSent: true,\n\t\tParent:        mi,\n\t}\n\n\terr := hls.FromStream(mi.stream.Desc, mi.reader, mi.hmuxer)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = mi.hmuxer.Start()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tmi.Log(logger.Info, \"is converting into HLS, %s\",\n\t\tdefs.FormatsInfo(mi.reader.Formats()))\n\n\tmi.stream.AddReader(mi.reader)\n\n\treturn nil\n}\n\n// Log implements logger.Writer.\nfunc (mi *muxerInstance) Log(level logger.Level, format string, args ...any) {\n\tmi.parent.Log(level, format, args...)\n}\n\nfunc (mi *muxerInstance) close() {\n\tmi.stream.RemoveReader(mi.reader)\n\tmi.hmuxer.Close()\n\tif mi.hmuxer.Directory != \"\" {\n\t\tos.Remove(mi.hmuxer.Directory)\n\t}\n}\n\nfunc (mi *muxerInstance) errorChan() chan error {\n\treturn mi.reader.Error()\n}\n\nfunc (mi *muxerInstance) handleRequest(w http.ResponseWriter, r *http.Request) {\n\tmi.hmuxer.Handle(w, r)\n}\n"
  },
  {
    "path": "internal/servers/hls/server.go",
    "content": "// Package hls contains a HLS server.\npackage hls\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"sort\"\n\t\"sync\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n)\n\n// ErrMuxerNotFound is returned when a muxer is not found.\nvar ErrMuxerNotFound = errors.New(\"muxer not found\")\n\nfunc interfaceIsEmpty(i any) bool {\n\treturn reflect.ValueOf(i).Kind() != reflect.Pointer || reflect.ValueOf(i).IsNil()\n}\n\ntype serverGetMuxerRes struct {\n\tmuxer *muxer\n\terr   error\n}\n\ntype serverGetMuxerReq struct {\n\tpath           string\n\tremoteAddr     string\n\tquery          string\n\tsourceOnDemand bool\n\tres            chan serverGetMuxerRes\n}\n\ntype serverAPIMuxersListRes struct {\n\tdata *defs.APIHLSMuxerList\n\terr  error\n}\n\ntype serverAPIMuxersListReq struct {\n\tres chan serverAPIMuxersListRes\n}\n\ntype serverAPIMuxersGetRes struct {\n\tdata *defs.APIHLSMuxer\n\terr  error\n}\n\ntype serverAPIMuxersGetReq struct {\n\tname string\n\tres  chan serverAPIMuxersGetRes\n}\n\ntype serverMetrics interface {\n\tSetHLSServer(defs.APIHLSServer)\n}\n\ntype serverPathManager interface {\n\tSetHLSServer(*Server) []defs.Path\n\tFindPathConf(req defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error)\n\tAddReader(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error)\n}\n\ntype serverParent interface {\n\tlogger.Writer\n}\n\n// Server is a HLS server.\ntype Server struct {\n\tAddress         string\n\tDumpPackets     bool\n\tEncryption      bool\n\tServerKey       string\n\tServerCert      string\n\tAllowOrigins    []string\n\tTrustedProxies  conf.IPNetworks\n\tAlwaysRemux     bool\n\tVariant         conf.HLSVariant\n\tSegmentCount    int\n\tSegmentDuration conf.Duration\n\tPartDuration    conf.Duration\n\tSegmentMaxSize  conf.StringSize\n\tDirectory       string\n\tReadTimeout     conf.Duration\n\tWriteTimeout    conf.Duration\n\tMuxerCloseAfter conf.Duration\n\tMetrics         serverMetrics\n\tPathManager     serverPathManager\n\tParent          serverParent\n\n\tctx        context.Context\n\tctxCancel  func()\n\twg         sync.WaitGroup\n\thttpServer *httpServer\n\tmuxers     map[string]*muxer\n\n\t// in\n\tchPathReady    chan defs.Path\n\tchPathNotReady chan defs.Path\n\tchGetMuxer     chan serverGetMuxerReq\n\tchCloseMuxer   chan *muxer\n\tchAPIMuxerList chan serverAPIMuxersListReq\n\tchAPIMuxerGet  chan serverAPIMuxersGetReq\n}\n\n// Initialize initializes the server.\nfunc (s *Server) Initialize() error {\n\tctx, ctxCancel := context.WithCancel(context.Background())\n\n\ts.ctx = ctx\n\ts.ctxCancel = ctxCancel\n\ts.muxers = make(map[string]*muxer)\n\ts.chPathReady = make(chan defs.Path)\n\ts.chPathNotReady = make(chan defs.Path)\n\ts.chGetMuxer = make(chan serverGetMuxerReq)\n\ts.chCloseMuxer = make(chan *muxer)\n\ts.chAPIMuxerList = make(chan serverAPIMuxersListReq)\n\ts.chAPIMuxerGet = make(chan serverAPIMuxersGetReq)\n\n\ts.httpServer = &httpServer{\n\t\taddress:        s.Address,\n\t\tdumpPackets:    s.DumpPackets,\n\t\tencryption:     s.Encryption,\n\t\tserverKey:      s.ServerKey,\n\t\tserverCert:     s.ServerCert,\n\t\tallowOrigins:   s.AllowOrigins,\n\t\ttrustedProxies: s.TrustedProxies,\n\t\treadTimeout:    s.ReadTimeout,\n\t\twriteTimeout:   s.WriteTimeout,\n\t\tpathManager:    s.PathManager,\n\t\tparent:         s,\n\t}\n\terr := s.httpServer.initialize()\n\tif err != nil {\n\t\tctxCancel()\n\t\treturn err\n\t}\n\n\ts.Log(logger.Info, \"listener opened on \"+s.Address)\n\n\ts.wg.Add(1)\n\tgo s.run()\n\n\tif !interfaceIsEmpty(s.Metrics) {\n\t\ts.Metrics.SetHLSServer(s)\n\t}\n\n\treturn nil\n}\n\n// Log implements logger.Writer.\nfunc (s *Server) Log(level logger.Level, format string, args ...any) {\n\ts.Parent.Log(level, \"[HLS] \"+format, args...)\n}\n\n// Close closes the server.\nfunc (s *Server) Close() {\n\ts.Log(logger.Info, \"listener is closing\")\n\n\tif !interfaceIsEmpty(s.Metrics) {\n\t\ts.Metrics.SetHLSServer(nil)\n\t}\n\n\ts.ctxCancel()\n\ts.wg.Wait()\n}\n\nfunc (s *Server) run() {\n\tdefer s.wg.Done()\n\n\treadyPaths := s.PathManager.SetHLSServer(s)\n\tdefer s.PathManager.SetHLSServer(nil)\n\n\tif s.AlwaysRemux {\n\t\tfor _, pa := range readyPaths {\n\t\t\tif !pa.SafeConf().SourceOnDemand {\n\t\t\t\tif _, ok := s.muxers[pa.Name()]; !ok {\n\t\t\t\t\ts.createMuxer(pa.Name(), \"\", \"\")\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\nouter:\n\tfor {\n\t\tselect {\n\t\tcase pa := <-s.chPathReady:\n\t\t\tif s.AlwaysRemux && !pa.SafeConf().SourceOnDemand {\n\t\t\t\tif _, ok := s.muxers[pa.Name()]; !ok {\n\t\t\t\t\ts.createMuxer(pa.Name(), \"\", \"\")\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase pa := <-s.chPathNotReady:\n\t\t\tc, ok := s.muxers[pa.Name()]\n\t\t\tif ok && c.remoteAddr == \"\" { // created with \"always remux\"\n\t\t\t\tc.Close()\n\t\t\t\tdelete(s.muxers, pa.Name())\n\t\t\t}\n\n\t\tcase req := <-s.chGetMuxer:\n\t\t\tmux, ok := s.muxers[req.path]\n\t\t\tswitch {\n\t\t\tcase ok:\n\t\t\t\treq.res <- serverGetMuxerRes{muxer: mux}\n\t\t\tcase s.AlwaysRemux && !req.sourceOnDemand:\n\t\t\t\treq.res <- serverGetMuxerRes{err: fmt.Errorf(\"muxer is waiting to be created\")}\n\t\t\tdefault:\n\t\t\t\treq.res <- serverGetMuxerRes{muxer: s.createMuxer(req.path, req.remoteAddr, req.query)}\n\t\t\t}\n\n\t\tcase c := <-s.chCloseMuxer:\n\t\t\tif c2, ok := s.muxers[c.PathName()]; ok && c2 == c {\n\t\t\t\tdelete(s.muxers, c.PathName())\n\t\t\t}\n\n\t\tcase req := <-s.chAPIMuxerList:\n\t\t\tdata := &defs.APIHLSMuxerList{\n\t\t\t\tItems: []defs.APIHLSMuxer{},\n\t\t\t}\n\n\t\t\tfor _, muxer := range s.muxers {\n\t\t\t\tdata.Items = append(data.Items, *muxer.apiItem())\n\t\t\t}\n\n\t\t\tsort.Slice(data.Items, func(i, j int) bool {\n\t\t\t\treturn data.Items[i].Created.Before(data.Items[j].Created)\n\t\t\t})\n\n\t\t\treq.res <- serverAPIMuxersListRes{\n\t\t\t\tdata: data,\n\t\t\t}\n\n\t\tcase req := <-s.chAPIMuxerGet:\n\t\t\tmuxer, ok := s.muxers[req.name]\n\t\t\tif !ok {\n\t\t\t\treq.res <- serverAPIMuxersGetRes{err: ErrMuxerNotFound}\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treq.res <- serverAPIMuxersGetRes{data: muxer.apiItem()}\n\n\t\tcase <-s.ctx.Done():\n\t\t\tbreak outer\n\t\t}\n\t}\n\n\ts.ctxCancel()\n\n\ts.httpServer.close()\n}\n\nfunc (s *Server) createMuxer(pathName string, remoteAddr string, query string) *muxer {\n\tr := &muxer{\n\t\tparentCtx:       s.ctx,\n\t\tremoteAddr:      remoteAddr,\n\t\tvariant:         s.Variant,\n\t\tsegmentCount:    s.SegmentCount,\n\t\tsegmentDuration: s.SegmentDuration,\n\t\tpartDuration:    s.PartDuration,\n\t\tsegmentMaxSize:  s.SegmentMaxSize,\n\t\tdirectory:       s.Directory,\n\t\twg:              &s.wg,\n\t\tpathName:        pathName,\n\t\tpathManager:     s.PathManager,\n\t\tparent:          s,\n\t\tquery:           query,\n\t\tcloseAfter:      s.MuxerCloseAfter,\n\t}\n\tr.initialize()\n\ts.muxers[pathName] = r\n\treturn r\n}\n\n// closeMuxer is called by muxer.\nfunc (s *Server) closeMuxer(c *muxer) {\n\tselect {\n\tcase s.chCloseMuxer <- c:\n\tcase <-s.ctx.Done():\n\t}\n}\n\nfunc (s *Server) getMuxer(req serverGetMuxerReq) (*muxer, error) {\n\treq.res = make(chan serverGetMuxerRes)\n\n\tselect {\n\tcase s.chGetMuxer <- req:\n\t\tres := <-req.res\n\t\treturn res.muxer, res.err\n\n\tcase <-s.ctx.Done():\n\t\treturn nil, fmt.Errorf(\"terminated\")\n\t}\n}\n\n// PathReady is called by pathManager.\nfunc (s *Server) PathReady(pa defs.Path) {\n\tselect {\n\tcase s.chPathReady <- pa:\n\tcase <-s.ctx.Done():\n\t}\n}\n\n// PathNotReady is called by pathManager.\nfunc (s *Server) PathNotReady(pa defs.Path) {\n\tselect {\n\tcase s.chPathNotReady <- pa:\n\tcase <-s.ctx.Done():\n\t}\n}\n\n// APIMuxersList is called by api.\nfunc (s *Server) APIMuxersList() (*defs.APIHLSMuxerList, error) {\n\treq := serverAPIMuxersListReq{\n\t\tres: make(chan serverAPIMuxersListRes),\n\t}\n\n\tselect {\n\tcase s.chAPIMuxerList <- req:\n\t\tres := <-req.res\n\t\treturn res.data, res.err\n\n\tcase <-s.ctx.Done():\n\t\treturn nil, fmt.Errorf(\"terminated\")\n\t}\n}\n\n// APIMuxersGet is called by api.\nfunc (s *Server) APIMuxersGet(name string) (*defs.APIHLSMuxer, error) {\n\treq := serverAPIMuxersGetReq{\n\t\tname: name,\n\t\tres:  make(chan serverAPIMuxersGetRes),\n\t}\n\n\tselect {\n\tcase s.chAPIMuxerGet <- req:\n\t\tres := <-req.res\n\t\treturn res.data, res.err\n\n\tcase <-s.ctx.Done():\n\t\treturn nil, fmt.Errorf(\"terminated\")\n\t}\n}\n"
  },
  {
    "path": "internal/servers/hls/server_test.go",
    "content": "package hls\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gohlslib/v2\"\n\t\"github.com/bluenviron/gohlslib/v2/pkg/codecs\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio\"\n\t\"github.com/bluenviron/mediamtx/internal/auth\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/externalcmd\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype dummyPathManager struct {\n\tsetHLSServerImpl func() []defs.Path\n\tfindPathConfImpl func(req defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error)\n\taddReaderImpl    func(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error)\n}\n\nfunc (pm *dummyPathManager) SetHLSServer(*Server) []defs.Path {\n\tif pm.setHLSServerImpl != nil {\n\t\treturn pm.setHLSServerImpl()\n\t}\n\treturn nil\n}\n\nfunc (pm *dummyPathManager) FindPathConf(req defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error) {\n\treturn pm.findPathConfImpl(req)\n}\n\nfunc (pm *dummyPathManager) AddReader(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error) {\n\treturn pm.addReaderImpl(req)\n}\n\ntype dummyPath struct{}\n\nfunc (pa *dummyPath) Name() string {\n\treturn \"teststream\"\n}\n\nfunc (pa *dummyPath) SafeConf() *conf.Path {\n\treturn &conf.Path{}\n}\n\nfunc (pa *dummyPath) ExternalCmdEnv() externalcmd.Environment {\n\treturn nil\n}\n\nfunc (pa *dummyPath) RemovePublisher(_ defs.PathRemovePublisherReq) {\n}\n\nfunc (pa *dummyPath) RemoveReader(_ defs.PathRemoveReaderReq) {\n}\n\nfunc TestServerPreflightRequest(t *testing.T) {\n\ts := &Server{\n\t\tAddress:      \"127.0.0.1:8888\",\n\t\tAllowOrigins: []string{\"*\"},\n\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\tPathManager:  &dummyPathManager{},\n\t\tParent:       test.NilLogger,\n\t}\n\terr := s.Initialize()\n\trequire.NoError(t, err)\n\tdefer s.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\treq, err := http.NewRequest(http.MethodOptions, \"http://localhost:8888\", nil)\n\trequire.NoError(t, err)\n\n\treq.Header.Add(\"Access-Control-Request-Method\", \"GET\")\n\n\tres, err := hc.Do(req)\n\trequire.NoError(t, err)\n\tdefer res.Body.Close()\n\n\trequire.Equal(t, http.StatusNoContent, res.StatusCode)\n\n\tbyts, err := io.ReadAll(res.Body)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, \"*\", res.Header.Get(\"Access-Control-Allow-Origin\"))\n\trequire.Equal(t, \"true\", res.Header.Get(\"Access-Control-Allow-Credentials\"))\n\trequire.Equal(t, \"OPTIONS, GET\", res.Header.Get(\"Access-Control-Allow-Methods\"))\n\trequire.Equal(t, \"Authorization, Range\", res.Header.Get(\"Access-Control-Allow-Headers\"))\n\trequire.Equal(t, byts, []byte{})\n}\n\nfunc TestServerNotFound(t *testing.T) {\n\tfor _, ca := range []string{\n\t\t\"always remux off\",\n\t\t\"always remux on\",\n\t} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tpm := &dummyPathManager{\n\t\t\t\tfindPathConfImpl: func(req defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error) {\n\t\t\t\t\trequire.Equal(t, \"nonexisting\", req.AccessRequest.Name)\n\t\t\t\t\treturn &defs.PathFindPathConfRes{Conf: &conf.Path{}, User: req.AccessRequest.Credentials.User}, nil\n\t\t\t\t},\n\t\t\t\taddReaderImpl: func(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error) {\n\t\t\t\t\trequire.Equal(t, \"nonexisting\", req.AccessRequest.Name)\n\t\t\t\t\treturn nil, fmt.Errorf(\"not found\")\n\t\t\t\t},\n\t\t\t}\n\n\t\t\ts := &Server{\n\t\t\t\tAddress:         \"127.0.0.1:8888\",\n\t\t\t\tEncryption:      false,\n\t\t\t\tServerKey:       \"\",\n\t\t\t\tServerCert:      \"\",\n\t\t\t\tAlwaysRemux:     ca == \"always remux on\",\n\t\t\t\tVariant:         conf.HLSVariant(gohlslib.MuxerVariantMPEGTS),\n\t\t\t\tSegmentCount:    7,\n\t\t\t\tSegmentDuration: conf.Duration(1 * time.Second),\n\t\t\t\tPartDuration:    conf.Duration(200 * time.Millisecond),\n\t\t\t\tSegmentMaxSize:  50 * 1024 * 1024,\n\t\t\t\tTrustedProxies:  conf.IPNetworks{},\n\t\t\t\tDirectory:       \"\",\n\t\t\t\tReadTimeout:     conf.Duration(10 * time.Second),\n\t\t\t\tWriteTimeout:    conf.Duration(10 * time.Second),\n\t\t\t\tPathManager:     pm,\n\t\t\t\tParent:          test.NilLogger,\n\t\t\t}\n\t\t\terr := s.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer s.Close()\n\n\t\t\ttr := &http.Transport{}\n\t\t\tdefer tr.CloseIdleConnections()\n\t\t\thc := &http.Client{Transport: tr}\n\n\t\t\tfunc() {\n\t\t\t\tvar req *http.Request\n\t\t\t\treq, err = http.NewRequest(http.MethodGet, \"http://myuser:mypass@127.0.0.1:8888/nonexisting/\", nil)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tvar res *http.Response\n\t\t\t\tres, err = hc.Do(req)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer res.Body.Close()\n\t\t\t\trequire.Equal(t, http.StatusOK, res.StatusCode)\n\t\t\t}()\n\n\t\t\tfunc() {\n\t\t\t\tvar req *http.Request\n\t\t\t\treq, err = http.NewRequest(http.MethodGet, \"http://myuser:mypass@127.0.0.1:8888/nonexisting/index.m3u8\", nil)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tvar res *http.Response\n\t\t\t\tres, err = hc.Do(req)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer res.Body.Close()\n\t\t\t\trequire.Equal(t, http.StatusNotFound, res.StatusCode)\n\t\t\t}()\n\t\t})\n\t}\n}\n\nfunc TestServerRead(t *testing.T) {\n\tfor _, ca := range []string{\n\t\t\"always remux off\",\n\t\t\"always remux on\",\n\t} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tdesc := &description.Session{Medias: []*description.Media{\n\t\t\t\ttest.MediaH264,\n\t\t\t\ttest.MediaMPEG4Audio,\n\t\t\t}}\n\n\t\t\tstrm := &stream.Stream{\n\t\t\t\tDesc:              desc,\n\t\t\t\tWriteQueueSize:    512,\n\t\t\t\tRTPMaxPayloadSize: 1450,\n\t\t\t\tReplaceNTP:        false,\n\t\t\t\tParent:            test.NilLogger,\n\t\t\t}\n\t\t\terr := strm.Initialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsubStream := &stream.SubStream{\n\t\t\t\tStream:        strm,\n\t\t\t\tUseRTPPackets: false,\n\t\t\t}\n\t\t\terr = subStream.Initialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tpm := &dummyPathManager{\n\t\t\t\tfindPathConfImpl: func(req defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error) {\n\t\t\t\t\trequire.Equal(t, \"teststream\", req.AccessRequest.Name)\n\t\t\t\t\trequire.Equal(t, \"param=value\", req.AccessRequest.Query)\n\t\t\t\t\trequire.Equal(t, \"myuser\", req.AccessRequest.Credentials.User)\n\t\t\t\t\trequire.Equal(t, \"mypass\", req.AccessRequest.Credentials.Pass)\n\t\t\t\t\treturn &defs.PathFindPathConfRes{Conf: &conf.Path{}, User: req.AccessRequest.Credentials.User}, nil\n\t\t\t\t},\n\t\t\t\taddReaderImpl: func(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error) {\n\t\t\t\t\trequire.Equal(t, \"teststream\", req.AccessRequest.Name)\n\t\t\t\t\tif ca == \"always remux off\" {\n\t\t\t\t\t\trequire.Equal(t, \"param=value\", req.AccessRequest.Query)\n\t\t\t\t\t} else {\n\t\t\t\t\t\trequire.Equal(t, \"\", req.AccessRequest.Query)\n\t\t\t\t\t}\n\t\t\t\t\treturn &defs.PathAddReaderRes{Path: &dummyPath{}, Stream: strm}, nil\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tswitch ca {\n\t\t\tcase \"always remux off\":\n\t\t\t\ts := &Server{\n\t\t\t\t\tAddress:         \"127.0.0.1:8888\",\n\t\t\t\t\tAlwaysRemux:     false,\n\t\t\t\t\tVariant:         conf.HLSVariant(gohlslib.MuxerVariantMPEGTS),\n\t\t\t\t\tSegmentCount:    7,\n\t\t\t\t\tSegmentDuration: conf.Duration(1 * time.Second),\n\t\t\t\t\tPartDuration:    conf.Duration(200 * time.Millisecond),\n\t\t\t\t\tSegmentMaxSize:  50 * 1024 * 1024,\n\t\t\t\t\tTrustedProxies:  conf.IPNetworks{},\n\t\t\t\t\tReadTimeout:     conf.Duration(10 * time.Second),\n\t\t\t\t\tWriteTimeout:    conf.Duration(10 * time.Second),\n\t\t\t\t\tPathManager:     pm,\n\t\t\t\t\tParent:          test.NilLogger,\n\t\t\t\t}\n\t\t\t\terr = s.Initialize()\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer s.Close()\n\n\t\t\t\tc := &gohlslib.Client{\n\t\t\t\t\tURI:           \"http://myuser:mypass@127.0.0.1:8888/teststream/index.m3u8?param=value\",\n\t\t\t\t\tStartDistance: 1,\n\t\t\t\t}\n\n\t\t\t\trecv1 := make(chan struct{})\n\t\t\t\trecv2 := make(chan struct{})\n\n\t\t\t\tc.OnTracks = func(tracks []*gohlslib.Track) error { //nolint:dupl\n\t\t\t\t\trequire.Equal(t, []*gohlslib.Track{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCodec:     &codecs.H264{},\n\t\t\t\t\t\t\tClockRate: 90000,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCodec: &codecs.MPEG4Audio{\n\t\t\t\t\t\t\t\tConfig: mpeg4audio.AudioSpecificConfig{\n\t\t\t\t\t\t\t\t\tType:          2,\n\t\t\t\t\t\t\t\t\tChannelCount:  2,\n\t\t\t\t\t\t\t\t\tChannelConfig: 2,\n\t\t\t\t\t\t\t\t\tSampleRate:    44100,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tClockRate: 90000,\n\t\t\t\t\t\t},\n\t\t\t\t\t}, tracks)\n\n\t\t\t\t\tc.OnDataH26x(tracks[0], func(pts, dts int64, au [][]byte) {\n\t\t\t\t\t\trequire.Equal(t, int64(0), pts)\n\t\t\t\t\t\trequire.Equal(t, int64(0), dts)\n\t\t\t\t\t\trequire.Equal(t, [][]byte{\n\t\t\t\t\t\t\ttest.FormatH264.SPS,\n\t\t\t\t\t\t\ttest.FormatH264.PPS,\n\t\t\t\t\t\t\t{5, 1},\n\t\t\t\t\t\t}, au)\n\t\t\t\t\t\tclose(recv1)\n\t\t\t\t\t})\n\n\t\t\t\t\tc.OnDataMPEG4Audio(tracks[1], func(pts int64, aus [][]byte) {\n\t\t\t\t\t\trequire.Equal(t, int64(0), pts)\n\t\t\t\t\t\trequire.Equal(t, [][]byte{{1, 2}}, aus)\n\t\t\t\t\t\tclose(recv2)\n\t\t\t\t\t})\n\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\terr = c.Start()\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer c.Close()\n\n\t\t\t\tstrm.WaitForReaders()\n\n\t\t\t\tfor i := range 2 {\n\t\t\t\t\tsubStream.WriteUnit(test.MediaH264, test.FormatH264, &unit.Unit{\n\t\t\t\t\t\tNTP: time.Time{},\n\t\t\t\t\t\tPTS: int64(i) * 90000,\n\t\t\t\t\t\tPayload: unit.PayloadH264{\n\t\t\t\t\t\t\t{5, 1}, // IDR\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t\tsubStream.WriteUnit(test.MediaMPEG4Audio, test.FormatMPEG4Audio, &unit.Unit{\n\t\t\t\t\t\tNTP:     time.Time{},\n\t\t\t\t\t\tPTS:     int64(i) * 44100,\n\t\t\t\t\t\tPayload: unit.PayloadMPEG4Audio{{1, 2}},\n\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\t\t<-recv1\n\t\t\t\t<-recv2\n\n\t\t\tcase \"always remux on\":\n\t\t\t\ts := &Server{\n\t\t\t\t\tAddress:         \"127.0.0.1:8888\",\n\t\t\t\t\tAlwaysRemux:     true,\n\t\t\t\t\tVariant:         conf.HLSVariant(gohlslib.MuxerVariantMPEGTS),\n\t\t\t\t\tSegmentCount:    7,\n\t\t\t\t\tSegmentDuration: conf.Duration(1 * time.Second),\n\t\t\t\t\tPartDuration:    conf.Duration(200 * time.Millisecond),\n\t\t\t\t\tSegmentMaxSize:  50 * 1024 * 1024,\n\t\t\t\t\tTrustedProxies:  conf.IPNetworks{},\n\t\t\t\t\tReadTimeout:     conf.Duration(10 * time.Second),\n\t\t\t\t\tWriteTimeout:    conf.Duration(10 * time.Second),\n\t\t\t\t\tPathManager:     pm,\n\t\t\t\t\tParent:          test.NilLogger,\n\t\t\t\t}\n\t\t\t\terr = s.Initialize()\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer s.Close()\n\n\t\t\t\ts.PathReady(&dummyPath{})\n\n\t\t\t\tstrm.WaitForReaders()\n\n\t\t\t\tfor i := range 2 {\n\t\t\t\t\tsubStream.WriteUnit(test.MediaH264, test.FormatH264, &unit.Unit{\n\t\t\t\t\t\tNTP: time.Time{},\n\t\t\t\t\t\tPTS: int64(i) * 90000,\n\t\t\t\t\t\tPayload: unit.PayloadH264{\n\t\t\t\t\t\t\t{5, 1}, // IDR\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t\tsubStream.WriteUnit(test.MediaMPEG4Audio, test.FormatMPEG4Audio, &unit.Unit{\n\t\t\t\t\t\tNTP:     time.Time{},\n\t\t\t\t\t\tPTS:     int64(i) * 44100,\n\t\t\t\t\t\tPayload: unit.PayloadMPEG4Audio{{1, 2}},\n\t\t\t\t\t})\n\t\t\t\t}\n\n\t\t\t\tc := &gohlslib.Client{\n\t\t\t\t\tURI:           \"http://myuser:mypass@127.0.0.1:8888/teststream/index.m3u8?param=value\",\n\t\t\t\t\tStartDistance: 1,\n\t\t\t\t}\n\n\t\t\t\trecv1 := make(chan struct{})\n\t\t\t\trecv2 := make(chan struct{})\n\n\t\t\t\tc.OnTracks = func(tracks []*gohlslib.Track) error { //nolint:dupl\n\t\t\t\t\trequire.Equal(t, []*gohlslib.Track{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCodec:     &codecs.H264{},\n\t\t\t\t\t\t\tClockRate: 90000,\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tCodec: &codecs.MPEG4Audio{\n\t\t\t\t\t\t\t\tConfig: mpeg4audio.AudioSpecificConfig{\n\t\t\t\t\t\t\t\t\tType:          2,\n\t\t\t\t\t\t\t\t\tChannelCount:  2,\n\t\t\t\t\t\t\t\t\tChannelConfig: 2,\n\t\t\t\t\t\t\t\t\tSampleRate:    44100,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tClockRate: 90000,\n\t\t\t\t\t\t},\n\t\t\t\t\t}, tracks)\n\n\t\t\t\t\tc.OnDataH26x(tracks[0], func(pts, dts int64, au [][]byte) {\n\t\t\t\t\t\trequire.Equal(t, int64(0), pts)\n\t\t\t\t\t\trequire.Equal(t, int64(0), dts)\n\t\t\t\t\t\trequire.Equal(t, [][]byte{\n\t\t\t\t\t\t\ttest.FormatH264.SPS,\n\t\t\t\t\t\t\ttest.FormatH264.PPS,\n\t\t\t\t\t\t\t{5, 1},\n\t\t\t\t\t\t}, au)\n\t\t\t\t\t\tclose(recv1)\n\t\t\t\t\t})\n\n\t\t\t\t\tc.OnDataMPEG4Audio(tracks[1], func(pts int64, aus [][]byte) {\n\t\t\t\t\t\trequire.Equal(t, int64(0), pts)\n\t\t\t\t\t\trequire.Equal(t, [][]byte{{1, 2}}, aus)\n\t\t\t\t\t\tclose(recv2)\n\t\t\t\t\t})\n\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\n\t\t\t\terr = c.Start()\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer c.Close()\n\n\t\t\t\t<-recv1\n\t\t\t\t<-recv2\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestServerDirectory(t *testing.T) {\n\tdir, err := os.MkdirTemp(\"\", \"mediamtx-playback\")\n\trequire.NoError(t, err)\n\tdefer os.RemoveAll(dir)\n\n\tdesc := &description.Session{Medias: []*description.Media{test.MediaH264}}\n\n\tstrm := &stream.Stream{\n\t\tDesc:              desc,\n\t\tWriteQueueSize:    512,\n\t\tRTPMaxPayloadSize: 1450,\n\t\tParent:            test.NilLogger,\n\t}\n\terr = strm.Initialize()\n\trequire.NoError(t, err)\n\n\tsubStream := &stream.SubStream{\n\t\tStream:        strm,\n\t\tUseRTPPackets: false,\n\t}\n\terr = subStream.Initialize()\n\trequire.NoError(t, err)\n\n\tpm := &dummyPathManager{\n\t\taddReaderImpl: func(_ defs.PathAddReaderReq) (*defs.PathAddReaderRes, error) {\n\t\t\treturn &defs.PathAddReaderRes{Path: &dummyPath{}, Stream: strm}, nil\n\t\t},\n\t}\n\n\ts := &Server{\n\t\tAddress:         \"127.0.0.1:8888\",\n\t\tEncryption:      false,\n\t\tServerKey:       \"\",\n\t\tServerCert:      \"\",\n\t\tAlwaysRemux:     true,\n\t\tVariant:         conf.HLSVariant(gohlslib.MuxerVariantMPEGTS),\n\t\tSegmentCount:    7,\n\t\tSegmentDuration: conf.Duration(1 * time.Second),\n\t\tPartDuration:    conf.Duration(200 * time.Millisecond),\n\t\tSegmentMaxSize:  50 * 1024 * 1024,\n\t\tTrustedProxies:  conf.IPNetworks{},\n\t\tDirectory:       filepath.Join(dir, \"mydir\"),\n\t\tReadTimeout:     conf.Duration(10 * time.Second),\n\t\tWriteTimeout:    conf.Duration(10 * time.Second),\n\t\tPathManager:     pm,\n\t\tParent:          test.NilLogger,\n\t}\n\terr = s.Initialize()\n\trequire.NoError(t, err)\n\tdefer s.Close()\n\n\ts.PathReady(&dummyPath{})\n\n\ttime.Sleep(100 * time.Millisecond)\n\n\t_, err = os.Stat(filepath.Join(dir, \"mydir\", \"teststream\"))\n\trequire.NoError(t, err)\n}\n\nfunc TestServerDynamicAlwaysRemux(t *testing.T) {\n\tdesc := &description.Session{Medias: []*description.Media{test.MediaH264}}\n\n\tstrm := &stream.Stream{\n\t\tDesc:              desc,\n\t\tWriteQueueSize:    512,\n\t\tRTPMaxPayloadSize: 1450,\n\t\tParent:            test.NilLogger,\n\t}\n\terr := strm.Initialize()\n\trequire.NoError(t, err)\n\n\tsubStream := &stream.SubStream{\n\t\tStream:        strm,\n\t\tUseRTPPackets: false,\n\t}\n\terr = subStream.Initialize()\n\trequire.NoError(t, err)\n\n\tdone := make(chan struct{})\n\n\tpm := &dummyPathManager{\n\t\tsetHLSServerImpl: func() []defs.Path {\n\t\t\treturn []defs.Path{&dummyPath{}}\n\t\t},\n\t\taddReaderImpl: func(_ defs.PathAddReaderReq) (*defs.PathAddReaderRes, error) {\n\t\t\tclose(done)\n\t\t\treturn &defs.PathAddReaderRes{Path: &dummyPath{}, Stream: strm}, nil\n\t\t},\n\t}\n\n\ts := &Server{\n\t\tAddress:         \"127.0.0.1:8888\",\n\t\tEncryption:      false,\n\t\tServerKey:       \"\",\n\t\tServerCert:      \"\",\n\t\tAlwaysRemux:     true,\n\t\tVariant:         conf.HLSVariant(gohlslib.MuxerVariantMPEGTS),\n\t\tSegmentCount:    7,\n\t\tSegmentDuration: conf.Duration(1 * time.Second),\n\t\tPartDuration:    conf.Duration(200 * time.Millisecond),\n\t\tSegmentMaxSize:  50 * 1024 * 1024,\n\t\tReadTimeout:     conf.Duration(10 * time.Second),\n\t\tWriteTimeout:    conf.Duration(10 * time.Second),\n\t\tPathManager:     pm,\n\t\tParent:          test.NilLogger,\n\t}\n\terr = s.Initialize()\n\trequire.NoError(t, err)\n\tdefer s.Close()\n\n\t<-done\n}\n\nfunc TestAuthError(t *testing.T) {\n\tn := 0\n\n\ts := &Server{\n\t\tAddress:         \"127.0.0.1:8888\",\n\t\tEncryption:      false,\n\t\tServerKey:       \"\",\n\t\tServerCert:      \"\",\n\t\tAlwaysRemux:     true,\n\t\tVariant:         conf.HLSVariant(gohlslib.MuxerVariantMPEGTS),\n\t\tSegmentCount:    7,\n\t\tSegmentDuration: conf.Duration(1 * time.Second),\n\t\tPartDuration:    conf.Duration(200 * time.Millisecond),\n\t\tSegmentMaxSize:  50 * 1024 * 1024,\n\t\tReadTimeout:     conf.Duration(10 * time.Second),\n\t\tWriteTimeout:    conf.Duration(10 * time.Second),\n\t\tPathManager: &dummyPathManager{\n\t\t\tfindPathConfImpl: func(req defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error) {\n\t\t\t\tif req.AccessRequest.Credentials.User == \"\" && req.AccessRequest.Credentials.Pass == \"\" {\n\t\t\t\t\treturn nil, &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf(\"auth error\")}\n\t\t\t\t}\n\n\t\t\t\treturn nil, &auth.Error{Wrapped: fmt.Errorf(\"auth error\")}\n\t\t\t},\n\t\t},\n\t\tParent: test.Logger(func(l logger.Level, s string, i ...any) {\n\t\t\tif l == logger.Info {\n\t\t\t\tif n == 1 {\n\t\t\t\t\trequire.Regexp(t, \"failed to authenticate: auth error$\", fmt.Sprintf(s, i...))\n\t\t\t\t}\n\t\t\t\tn++\n\t\t\t}\n\t\t}),\n\t}\n\terr := s.Initialize()\n\trequire.NoError(t, err)\n\tdefer s.Close()\n\n\treq, err := http.NewRequest(http.MethodGet, \"http://127.0.0.1:8888/stream/index.m3u8\", nil)\n\trequire.NoError(t, err)\n\n\tres, err := http.DefaultClient.Do(req)\n\trequire.NoError(t, err)\n\tdefer res.Body.Close()\n\n\trequire.Equal(t, http.StatusUnauthorized, res.StatusCode)\n\trequire.Equal(t, `Basic realm=\"mediamtx\"`, res.Header.Get(\"WWW-Authenticate\"))\n\n\treq, err = http.NewRequest(http.MethodGet, \"http://myuser:mypass@127.0.0.1:8888/stream/index.m3u8\", nil)\n\trequire.NoError(t, err)\n\n\tstart := time.Now()\n\n\tres, err = http.DefaultClient.Do(req)\n\trequire.NoError(t, err)\n\tdefer res.Body.Close()\n\n\trequire.Greater(t, time.Since(start), 2*time.Second)\n\n\trequire.Equal(t, http.StatusUnauthorized, res.StatusCode)\n\n\trequire.Equal(t, 2, n)\n}\n"
  },
  {
    "path": "internal/servers/rtmp/conn.go",
    "content": "package rtmp\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortmplib\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/google/uuid\"\n\n\t\"github.com/bluenviron/mediamtx/internal/auth\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/externalcmd\"\n\t\"github.com/bluenviron/mediamtx/internal/hooks\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/rtmp\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n)\n\ntype conn struct {\n\tparentCtx           context.Context\n\tisTLS               bool\n\trtspAddress         string\n\treadTimeout         conf.Duration\n\twriteTimeout        conf.Duration\n\trunOnConnect        string\n\trunOnConnectRestart bool\n\trunOnDisconnect     string\n\twg                  *sync.WaitGroup\n\tnconn               net.Conn\n\texternalCmdPool     *externalcmd.Pool\n\tpathManager         serverPathManager\n\tparent              *Server\n\n\tctx       context.Context\n\tctxCancel func()\n\tuuid      uuid.UUID\n\tcreated   time.Time\n\tmutex     sync.RWMutex\n\trconn     *gortmplib.ServerConn\n\tstate     defs.APIRTMPConnState\n\tpathName  string\n\tquery     string\n\tuser      string\n\treader    *stream.Reader\n}\n\nfunc (c *conn) initialize() {\n\tc.ctx, c.ctxCancel = context.WithCancel(c.parentCtx)\n\n\tc.uuid = uuid.New()\n\tc.created = time.Now()\n\tc.state = defs.APIRTMPConnStateIdle\n\n\tc.Log(logger.Info, \"opened\")\n\n\tc.wg.Add(1)\n\tgo c.run()\n}\n\nfunc (c *conn) Close() {\n\tc.ctxCancel()\n}\n\nfunc (c *conn) remoteAddr() net.Addr {\n\treturn c.nconn.RemoteAddr()\n}\n\n// Log implements logger.Writer.\nfunc (c *conn) Log(level logger.Level, format string, args ...any) {\n\tc.parent.Log(level, \"[conn %v] \"+format, append([]any{c.nconn.RemoteAddr()}, args...)...)\n}\n\nfunc (c *conn) ip() net.IP {\n\treturn c.nconn.RemoteAddr().(*net.TCPAddr).IP\n}\n\nfunc (c *conn) run() { //nolint:dupl\n\tdefer c.wg.Done()\n\n\tonDisconnectHook := hooks.OnConnect(hooks.OnConnectParams{\n\t\tLogger:              c,\n\t\tExternalCmdPool:     c.externalCmdPool,\n\t\tRunOnConnect:        c.runOnConnect,\n\t\tRunOnConnectRestart: c.runOnConnectRestart,\n\t\tRunOnDisconnect:     c.runOnDisconnect,\n\t\tRTSPAddress:         c.rtspAddress,\n\t\tDesc:                *c.APIReaderDescribe(),\n\t})\n\tdefer onDisconnectHook()\n\n\terr := c.runInner()\n\n\tc.ctxCancel()\n\n\tc.parent.closeConn(c)\n\n\tc.Log(logger.Info, \"closed: %v\", err)\n}\n\nfunc (c *conn) runInner() error {\n\treaderErr := make(chan error)\n\tgo func() {\n\t\treaderErr <- c.runReader()\n\t}()\n\n\tselect {\n\tcase err := <-readerErr:\n\t\tc.nconn.Close()\n\t\treturn err\n\n\tcase <-c.ctx.Done():\n\t\tc.nconn.Close()\n\t\t<-readerErr\n\t\treturn errors.New(\"terminated\")\n\t}\n}\n\nfunc (c *conn) runReader() error {\n\tc.nconn.SetReadDeadline(time.Now().Add(time.Duration(c.readTimeout)))\n\tc.nconn.SetWriteDeadline(time.Now().Add(time.Duration(c.writeTimeout)))\n\n\tconn := &gortmplib.ServerConn{\n\t\tRW: c.nconn,\n\t}\n\terr := conn.Initialize()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = conn.Accept()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tc.mutex.Lock()\n\tc.rconn = conn\n\tc.mutex.Unlock()\n\n\tif !conn.Publish {\n\t\treturn c.runRead()\n\t}\n\treturn c.runPublish()\n}\n\nfunc (c *conn) runRead() error {\n\tpathName := strings.TrimLeft(c.rconn.URL.Path, \"/\")\n\tquery := c.rconn.URL.Query()\n\n\tres, err := c.pathManager.AddReader(defs.PathAddReaderReq{\n\t\tAuthor: c,\n\t\tAccessRequest: defs.PathAccessRequest{\n\t\t\tName:  pathName,\n\t\t\tQuery: c.rconn.URL.RawQuery,\n\t\t\tProto: auth.ProtocolRTMP,\n\t\t\tID:    &c.uuid,\n\t\t\tCredentials: &auth.Credentials{\n\t\t\t\tUser: query.Get(\"user\"),\n\t\t\t\tPass: query.Get(\"pass\"),\n\t\t\t},\n\t\t\tIP: c.ip(),\n\t\t},\n\t})\n\tif err != nil {\n\t\tvar terr *auth.Error\n\t\tif errors.As(err, &terr) {\n\t\t\t// wait some seconds to delay brute force attacks\n\t\t\t<-time.After(auth.PauseAfterError)\n\t\t\treturn terr\n\t\t}\n\t\treturn err\n\t}\n\n\tdefer res.Path.RemoveReader(defs.PathRemoveReaderReq{Author: c})\n\n\tc.mutex.Lock()\n\tc.state = defs.APIRTMPConnStateRead\n\tc.pathName = pathName\n\tc.query = c.rconn.URL.RawQuery\n\tc.user = res.User\n\tc.mutex.Unlock()\n\n\tr := &stream.Reader{Parent: c}\n\n\terr = rtmp.FromStream(res.Stream.Desc, r, c.rconn, c.nconn, time.Duration(c.writeTimeout))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tc.Log(logger.Info, \"is reading from path '%s', %s\",\n\t\tres.Path.Name(), defs.FormatsInfo(r.Formats()))\n\n\tonUnreadHook := hooks.OnRead(hooks.OnReadParams{\n\t\tLogger:          c,\n\t\tExternalCmdPool: c.externalCmdPool,\n\t\tConf:            res.Path.SafeConf(),\n\t\tExternalCmdEnv:  res.Path.ExternalCmdEnv(),\n\t\tReader:          *c.APIReaderDescribe(),\n\t\tQuery:           c.rconn.URL.RawQuery,\n\t})\n\tdefer onUnreadHook()\n\n\tc.nconn.SetReadDeadline(time.Time{})\n\n\tres.Stream.AddReader(r)\n\tdefer res.Stream.RemoveReader(r)\n\n\tc.mutex.Lock()\n\tc.reader = r\n\tc.mutex.Unlock()\n\n\tselect {\n\tcase <-c.ctx.Done():\n\t\treturn fmt.Errorf(\"terminated\")\n\n\tcase err = <-r.Error():\n\t\treturn err\n\t}\n}\n\nfunc (c *conn) runPublish() error {\n\tpathName := strings.TrimLeft(c.rconn.URL.Path, \"/\")\n\tquery := c.rconn.URL.Query()\n\n\tr := &gortmplib.Reader{\n\t\tConn: c.rconn,\n\t}\n\terr := r.Initialize()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar subStream *stream.SubStream\n\n\tmedias, err := rtmp.ToStream(r, &subStream)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tres, err := c.pathManager.AddPublisher(defs.PathAddPublisherReq{\n\t\tAuthor:        c,\n\t\tDesc:          &description.Session{Medias: medias},\n\t\tUseRTPPackets: false,\n\t\tReplaceNTP:    true,\n\t\tAccessRequest: defs.PathAccessRequest{\n\t\t\tName:    pathName,\n\t\t\tQuery:   c.rconn.URL.RawQuery,\n\t\t\tPublish: true,\n\t\t\tProto:   auth.ProtocolRTMP,\n\t\t\tID:      &c.uuid,\n\t\t\tCredentials: &auth.Credentials{\n\t\t\t\tUser: query.Get(\"user\"),\n\t\t\t\tPass: query.Get(\"pass\"),\n\t\t\t},\n\t\t\tIP: c.ip(),\n\t\t},\n\t})\n\tif err != nil {\n\t\tvar terr *auth.Error\n\t\tif errors.As(err, &terr) {\n\t\t\t// wait some seconds to delay brute force attacks\n\t\t\t<-time.After(auth.PauseAfterError)\n\t\t\treturn terr\n\t\t}\n\t\treturn err\n\t}\n\n\tdefer res.Path.RemovePublisher(defs.PathRemovePublisherReq{Author: c})\n\n\tsubStream = res.SubStream\n\n\tc.mutex.Lock()\n\tc.state = defs.APIRTMPConnStatePublish\n\tc.pathName = pathName\n\tc.query = c.rconn.URL.RawQuery\n\tc.user = res.User\n\tc.mutex.Unlock()\n\n\tc.nconn.SetWriteDeadline(time.Time{})\n\n\tfor {\n\t\tc.nconn.SetReadDeadline(time.Now().Add(time.Duration(c.readTimeout)))\n\t\terr = r.Read()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n}\n\n// APIReaderDescribe implements reader.\nfunc (c *conn) APIReaderDescribe() *defs.APIPathReader {\n\treturn &defs.APIPathReader{\n\t\tType: func() defs.APIPathReaderType {\n\t\t\tif c.isTLS {\n\t\t\t\treturn defs.APIPathReaderTypeRTMPSConn\n\t\t\t}\n\t\t\treturn defs.APIPathReaderTypeRTMPConn\n\t\t}(),\n\t\tID: c.uuid.String(),\n\t}\n}\n\n// APISourceDescribe implements source.\nfunc (c *conn) APISourceDescribe() *defs.APIPathSource {\n\treturn &defs.APIPathSource{\n\t\tType: func() defs.APIPathSourceType {\n\t\t\tif c.isTLS {\n\t\t\t\treturn defs.APIPathSourceTypeRTMPSConn\n\t\t\t}\n\t\t\treturn defs.APIPathSourceTypeRTMPConn\n\t\t}(),\n\t\tID: c.uuid.String(),\n\t}\n}\n\nfunc (c *conn) apiItem() *defs.APIRTMPConn {\n\tc.mutex.RLock()\n\tdefer c.mutex.RUnlock()\n\n\tbytesReceived := uint64(0)\n\tbytesSent := uint64(0)\n\toutboundFramesDiscarded := uint64(0)\n\n\tif c.rconn != nil {\n\t\tbytesReceived = c.rconn.BytesReceived()\n\t\tbytesSent = c.rconn.BytesSent()\n\t}\n\n\tif c.reader != nil {\n\t\toutboundFramesDiscarded = c.reader.OutboundFramesDiscarded()\n\t}\n\n\treturn &defs.APIRTMPConn{\n\t\tID:                      c.uuid,\n\t\tCreated:                 c.created,\n\t\tRemoteAddr:              c.remoteAddr().String(),\n\t\tState:                   c.state,\n\t\tPath:                    c.pathName,\n\t\tQuery:                   c.query,\n\t\tUser:                    c.user,\n\t\tInboundBytes:            bytesReceived,\n\t\tOutboundBytes:           bytesSent,\n\t\tBytesReceived:           bytesReceived,\n\t\tBytesSent:               bytesSent,\n\t\tOutboundFramesDiscarded: outboundFramesDiscarded,\n\t}\n}\n"
  },
  {
    "path": "internal/servers/rtmp/listener.go",
    "content": "package rtmp\n\nimport (\n\t\"net\"\n\t\"sync\"\n)\n\ntype listener struct {\n\tln     net.Listener\n\twg     *sync.WaitGroup\n\tparent *Server\n}\n\nfunc (l *listener) initialize() {\n\tl.wg.Add(1)\n\tgo l.run()\n}\n\nfunc (l *listener) run() {\n\tdefer l.wg.Done()\n\n\terr := l.runInner()\n\n\tl.parent.acceptError(err)\n}\n\nfunc (l *listener) runInner() error {\n\tfor {\n\t\tconn, err := l.ln.Accept()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tl.parent.newConn(conn)\n\t}\n}\n"
  },
  {
    "path": "internal/servers/rtmp/server.go",
    "content": "// Package rtmp contains a RTMP server.\npackage rtmp\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"reflect\"\n\t\"sort\"\n\t\"sync\"\n\n\t\"github.com/google/uuid\"\n\n\t\"github.com/bluenviron/mediamtx/internal/certloader\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/externalcmd\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/packetdumper\"\n\t\"github.com/bluenviron/mediamtx/internal/restrictnetwork\"\n)\n\n// ErrConnNotFound is returned when a connection is not found.\nvar ErrConnNotFound = errors.New(\"connection not found\")\n\nfunc interfaceIsEmpty(i any) bool {\n\treturn reflect.ValueOf(i).Kind() != reflect.Pointer || reflect.ValueOf(i).IsNil()\n}\n\ntype serverAPIConnsListRes struct {\n\tdata *defs.APIRTMPConnList\n\terr  error\n}\n\ntype serverAPIConnsListReq struct {\n\tres chan serverAPIConnsListRes\n}\n\ntype serverAPIConnsGetRes struct {\n\tdata *defs.APIRTMPConn\n\terr  error\n}\n\ntype serverAPIConnsGetReq struct {\n\tuuid uuid.UUID\n\tres  chan serverAPIConnsGetRes\n}\n\ntype serverAPIConnsKickRes struct {\n\terr error\n}\n\ntype serverAPIConnsKickReq struct {\n\tuuid uuid.UUID\n\tres  chan serverAPIConnsKickRes\n}\n\ntype serverMetrics interface {\n\tSetRTMPSServer(defs.APIRTMPServer)\n\tSetRTMPServer(defs.APIRTMPServer)\n}\n\ntype serverPathManager interface {\n\tAddPublisher(req defs.PathAddPublisherReq) (*defs.PathAddPublisherRes, error)\n\tAddReader(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error)\n}\n\ntype serverParent interface {\n\tlogger.Writer\n}\n\n// Server is a RTMP server.\ntype Server struct {\n\tAddress             string\n\tDumpPackets         bool\n\tReadTimeout         conf.Duration\n\tWriteTimeout        conf.Duration\n\tIsTLS               bool\n\tServerCert          string\n\tServerKey           string\n\tRTSPAddress         string\n\tRunOnConnect        string\n\tRunOnConnectRestart bool\n\tRunOnDisconnect     string\n\tExternalCmdPool     *externalcmd.Pool\n\tMetrics             serverMetrics\n\tPathManager         serverPathManager\n\tParent              serverParent\n\n\tctx       context.Context\n\tctxCancel func()\n\twg        sync.WaitGroup\n\tln        net.Listener\n\tconns     map[*conn]struct{}\n\tloader    *certloader.CertLoader\n\n\t// in\n\tchNewConn      chan net.Conn\n\tchAcceptErr    chan error\n\tchCloseConn    chan *conn\n\tchAPIConnsList chan serverAPIConnsListReq\n\tchAPIConnsGet  chan serverAPIConnsGetReq\n\tchAPIConnsKick chan serverAPIConnsKickReq\n}\n\n// Initialize initializes the server.\nfunc (s *Server) Initialize() error {\n\tvar err error\n\ts.ln, err = net.Listen(restrictnetwork.Restrict(\"tcp\", s.Address))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif s.DumpPackets {\n\t\ts.ln = &packetdumper.Listener{\n\t\t\tPrefix:   \"rtmp_server_conn\",\n\t\t\tListener: s.ln,\n\t\t}\n\t}\n\n\tif s.IsTLS {\n\t\ts.loader = &certloader.CertLoader{\n\t\t\tCertPath: s.ServerCert,\n\t\t\tKeyPath:  s.ServerKey,\n\t\t\tParent:   s.Parent,\n\t\t}\n\t\terr = s.loader.Initialize()\n\t\tif err != nil {\n\t\t\ts.ln.Close()\n\t\t\treturn err\n\t\t}\n\n\t\ts.ln = tls.NewListener(s.ln, &tls.Config{GetCertificate: s.loader.GetCertificate()})\n\t}\n\n\ts.ctx, s.ctxCancel = context.WithCancel(context.Background())\n\n\ts.conns = make(map[*conn]struct{})\n\ts.chNewConn = make(chan net.Conn)\n\ts.chAcceptErr = make(chan error)\n\ts.chCloseConn = make(chan *conn)\n\ts.chAPIConnsList = make(chan serverAPIConnsListReq)\n\ts.chAPIConnsGet = make(chan serverAPIConnsGetReq)\n\ts.chAPIConnsKick = make(chan serverAPIConnsKickReq)\n\n\ts.Log(logger.Info, \"listener opened on %s\", s.Address)\n\n\tl := &listener{\n\t\tln:     s.ln,\n\t\twg:     &s.wg,\n\t\tparent: s,\n\t}\n\tl.initialize()\n\n\ts.wg.Add(1)\n\tgo s.run()\n\n\tif !interfaceIsEmpty(s.Metrics) {\n\t\tif s.IsTLS {\n\t\t\ts.Metrics.SetRTMPSServer(s)\n\t\t} else {\n\t\t\ts.Metrics.SetRTMPServer(s)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Log implements logger.Writer.\nfunc (s *Server) Log(level logger.Level, format string, args ...any) {\n\tlabel := func() string {\n\t\tif s.IsTLS {\n\t\t\treturn \"RTMPS\"\n\t\t}\n\t\treturn \"RTMP\"\n\t}()\n\ts.Parent.Log(level, \"[%s] \"+format, append([]any{label}, args...)...)\n}\n\n// Close closes the server.\nfunc (s *Server) Close() {\n\ts.Log(logger.Info, \"listener is closing\")\n\n\tif !interfaceIsEmpty((s.Metrics)) {\n\t\tif s.IsTLS {\n\t\t\ts.Metrics.SetRTMPSServer(nil)\n\t\t} else {\n\t\t\ts.Metrics.SetRTMPServer(nil)\n\t\t}\n\t}\n\n\ts.ctxCancel()\n\ts.wg.Wait()\n\n\tif s.loader != nil {\n\t\ts.loader.Close()\n\t}\n}\n\nfunc (s *Server) run() {\n\tdefer s.wg.Done()\n\nouter:\n\tfor {\n\t\tselect {\n\t\tcase err := <-s.chAcceptErr:\n\t\t\ts.Log(logger.Error, \"%s\", err)\n\t\t\tbreak outer\n\n\t\tcase nconn := <-s.chNewConn:\n\t\t\tc := &conn{\n\t\t\t\tparentCtx:           s.ctx,\n\t\t\t\tisTLS:               s.IsTLS,\n\t\t\t\trtspAddress:         s.RTSPAddress,\n\t\t\t\treadTimeout:         s.ReadTimeout,\n\t\t\t\twriteTimeout:        s.WriteTimeout,\n\t\t\t\trunOnConnect:        s.RunOnConnect,\n\t\t\t\trunOnConnectRestart: s.RunOnConnectRestart,\n\t\t\t\trunOnDisconnect:     s.RunOnDisconnect,\n\t\t\t\twg:                  &s.wg,\n\t\t\t\tnconn:               nconn,\n\t\t\t\texternalCmdPool:     s.ExternalCmdPool,\n\t\t\t\tpathManager:         s.PathManager,\n\t\t\t\tparent:              s,\n\t\t\t}\n\t\t\tc.initialize()\n\t\t\ts.conns[c] = struct{}{}\n\n\t\tcase c := <-s.chCloseConn:\n\t\t\tdelete(s.conns, c)\n\n\t\tcase req := <-s.chAPIConnsList:\n\t\t\tdata := &defs.APIRTMPConnList{\n\t\t\t\tItems: []defs.APIRTMPConn{},\n\t\t\t}\n\n\t\t\tfor c := range s.conns {\n\t\t\t\tdata.Items = append(data.Items, *c.apiItem())\n\t\t\t}\n\n\t\t\tsort.Slice(data.Items, func(i, j int) bool {\n\t\t\t\treturn data.Items[i].Created.Before(data.Items[j].Created)\n\t\t\t})\n\n\t\t\treq.res <- serverAPIConnsListRes{data: data}\n\n\t\tcase req := <-s.chAPIConnsGet:\n\t\t\tc := s.findConnByUUID(req.uuid)\n\t\t\tif c == nil {\n\t\t\t\treq.res <- serverAPIConnsGetRes{err: ErrConnNotFound}\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treq.res <- serverAPIConnsGetRes{data: c.apiItem()}\n\n\t\tcase req := <-s.chAPIConnsKick:\n\t\t\tc := s.findConnByUUID(req.uuid)\n\t\t\tif c == nil {\n\t\t\t\treq.res <- serverAPIConnsKickRes{err: ErrConnNotFound}\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdelete(s.conns, c)\n\t\t\tc.Close()\n\t\t\treq.res <- serverAPIConnsKickRes{}\n\n\t\tcase <-s.ctx.Done():\n\t\t\tbreak outer\n\t\t}\n\t}\n\n\ts.ctxCancel()\n\n\ts.ln.Close()\n}\n\nfunc (s *Server) findConnByUUID(uuid uuid.UUID) *conn {\n\tfor c := range s.conns {\n\t\tif c.uuid == uuid {\n\t\t\treturn c\n\t\t}\n\t}\n\treturn nil\n}\n\n// newConn is called by rtmpListener.\nfunc (s *Server) newConn(conn net.Conn) {\n\tselect {\n\tcase s.chNewConn <- conn:\n\tcase <-s.ctx.Done():\n\t\tconn.Close()\n\t}\n}\n\n// acceptError is called by rtmpListener.\nfunc (s *Server) acceptError(err error) {\n\tselect {\n\tcase s.chAcceptErr <- err:\n\tcase <-s.ctx.Done():\n\t}\n}\n\n// closeConn is called by conn.\nfunc (s *Server) closeConn(c *conn) {\n\tselect {\n\tcase s.chCloseConn <- c:\n\tcase <-s.ctx.Done():\n\t}\n}\n\n// APIConnsList is called by api.\nfunc (s *Server) APIConnsList() (*defs.APIRTMPConnList, error) {\n\treq := serverAPIConnsListReq{\n\t\tres: make(chan serverAPIConnsListRes),\n\t}\n\n\tselect {\n\tcase s.chAPIConnsList <- req:\n\t\tres := <-req.res\n\t\treturn res.data, res.err\n\n\tcase <-s.ctx.Done():\n\t\treturn nil, fmt.Errorf(\"terminated\")\n\t}\n}\n\n// APIConnsGet is called by api.\nfunc (s *Server) APIConnsGet(uuid uuid.UUID) (*defs.APIRTMPConn, error) {\n\treq := serverAPIConnsGetReq{\n\t\tuuid: uuid,\n\t\tres:  make(chan serverAPIConnsGetRes),\n\t}\n\n\tselect {\n\tcase s.chAPIConnsGet <- req:\n\t\tres := <-req.res\n\t\treturn res.data, res.err\n\n\tcase <-s.ctx.Done():\n\t\treturn nil, fmt.Errorf(\"terminated\")\n\t}\n}\n\n// APIConnsKick is called by api.\nfunc (s *Server) APIConnsKick(uuid uuid.UUID) error {\n\treq := serverAPIConnsKickReq{\n\t\tuuid: uuid,\n\t\tres:  make(chan serverAPIConnsKickRes),\n\t}\n\n\tselect {\n\tcase s.chAPIConnsKick <- req:\n\t\tres := <-req.res\n\t\treturn res.err\n\n\tcase <-s.ctx.Done():\n\t\treturn fmt.Errorf(\"terminated\")\n\t}\n}\n"
  },
  {
    "path": "internal/servers/rtmp/server_test.go",
    "content": "package rtmp\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"net/url\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortmplib\"\n\t\"github.com/bluenviron/gortmplib/pkg/codecs\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/externalcmd\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype dummyPath struct{}\n\nfunc (p *dummyPath) Name() string {\n\treturn \"teststream\"\n}\n\nfunc (p *dummyPath) SafeConf() *conf.Path {\n\treturn &conf.Path{}\n}\n\nfunc (p *dummyPath) ExternalCmdEnv() externalcmd.Environment {\n\treturn externalcmd.Environment{}\n}\n\nfunc (p *dummyPath) RemovePublisher(_ defs.PathRemovePublisherReq) {\n}\n\nfunc (p *dummyPath) RemoveReader(_ defs.PathRemoveReaderReq) {\n}\n\nfunc TestServerPublish(t *testing.T) {\n\tfor _, encrypt := range []string{\n\t\t\"plain\",\n\t\t\"tls\",\n\t} {\n\t\tt.Run(encrypt, func(t *testing.T) {\n\t\t\tvar serverCertFpath string\n\t\t\tvar serverKeyFpath string\n\n\t\t\tif encrypt == \"tls\" {\n\t\t\t\tvar err error\n\t\t\t\tserverCertFpath, err = test.CreateTempFile(test.TLSCertPub)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer os.Remove(serverCertFpath)\n\n\t\t\t\tserverKeyFpath, err = test.CreateTempFile(test.TLSCertKey)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer os.Remove(serverKeyFpath)\n\t\t\t}\n\n\t\t\tvar strm *stream.Stream\n\t\t\tvar reader *stream.Reader\n\t\t\tdefer func() {\n\t\t\t\tstrm.RemoveReader(reader)\n\t\t\t}()\n\t\t\tdataReceived := make(chan struct{})\n\t\t\tn := 0\n\n\t\t\tpathManager := &test.PathManager{\n\t\t\t\tAddPublisherImpl: func(req defs.PathAddPublisherReq) (*defs.PathAddPublisherRes, error) {\n\t\t\t\t\trequire.Equal(t, \"teststream\", req.AccessRequest.Name)\n\t\t\t\t\trequire.Equal(t, \"user=myuser&pass=mypass&param=value\", req.AccessRequest.Query)\n\t\t\t\t\trequire.Equal(t, \"myuser\", req.AccessRequest.Credentials.User)\n\t\t\t\t\trequire.Equal(t, \"mypass\", req.AccessRequest.Credentials.Pass)\n\n\t\t\t\t\tstrm = &stream.Stream{\n\t\t\t\t\t\tDesc:              req.Desc,\n\t\t\t\t\t\tWriteQueueSize:    512,\n\t\t\t\t\t\tRTPMaxPayloadSize: 1450,\n\t\t\t\t\t\tParent:            test.NilLogger,\n\t\t\t\t\t}\n\t\t\t\t\terr := strm.Initialize()\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\tsubStream := &stream.SubStream{\n\t\t\t\t\t\tStream:        strm,\n\t\t\t\t\t\tUseRTPPackets: false,\n\t\t\t\t\t}\n\t\t\t\t\terr = subStream.Initialize()\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\treader = &stream.Reader{Parent: test.NilLogger}\n\n\t\t\t\t\treader.OnData(\n\t\t\t\t\t\tstrm.Desc.Medias[0],\n\t\t\t\t\t\tstrm.Desc.Medias[0].Formats[0],\n\t\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\t\tswitch n {\n\t\t\t\t\t\t\tcase 0:\n\t\t\t\t\t\t\t\trequire.Equal(t, unit.PayloadH264(nil), u.Payload)\n\n\t\t\t\t\t\t\tcase 1:\n\t\t\t\t\t\t\t\trequire.Equal(t, unit.PayloadH264{\n\t\t\t\t\t\t\t\t\ttest.FormatH264.SPS,\n\t\t\t\t\t\t\t\t\ttest.FormatH264.PPS,\n\t\t\t\t\t\t\t\t\t{5, 2, 3, 4},\n\t\t\t\t\t\t\t\t}, u.Payload)\n\t\t\t\t\t\t\t\tclose(dataReceived)\n\n\t\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\t\tt.Errorf(\"should not happen\")\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tn++\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t})\n\n\t\t\t\t\tstrm.AddReader(reader)\n\n\t\t\t\t\treturn &defs.PathAddPublisherRes{\n\t\t\t\t\t\tPath:      &dummyPath{},\n\t\t\t\t\t\tUser:      req.AccessRequest.Credentials.User,\n\t\t\t\t\t\tSubStream: subStream,\n\t\t\t\t\t}, nil\n\t\t\t\t},\n\t\t\t}\n\n\t\t\ts := &Server{\n\t\t\t\tAddress:             \"127.0.0.1:1939\",\n\t\t\t\tReadTimeout:         conf.Duration(10 * time.Second),\n\t\t\t\tWriteTimeout:        conf.Duration(10 * time.Second),\n\t\t\t\tIsTLS:               encrypt == \"tls\",\n\t\t\t\tServerCert:          serverCertFpath,\n\t\t\t\tServerKey:           serverKeyFpath,\n\t\t\t\tRTSPAddress:         \"\",\n\t\t\t\tRunOnConnect:        \"\",\n\t\t\t\tRunOnConnectRestart: false,\n\t\t\t\tRunOnDisconnect:     \"\",\n\t\t\t\tExternalCmdPool:     nil,\n\t\t\t\tPathManager:         pathManager,\n\t\t\t\tParent:              test.NilLogger,\n\t\t\t}\n\t\t\terr := s.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer s.Close()\n\n\t\t\tvar rawURL string\n\n\t\t\tif encrypt == \"tls\" {\n\t\t\t\trawURL += \"rtmps://\"\n\t\t\t} else {\n\t\t\t\trawURL += \"rtmp://\"\n\t\t\t}\n\n\t\t\trawURL += \"127.0.0.1:1939/teststream?user=myuser&pass=mypass&param=value\"\n\n\t\t\tu, err := url.Parse(rawURL)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tconn := &gortmplib.Client{\n\t\t\t\tURL:       u,\n\t\t\t\tTLSConfig: &tls.Config{InsecureSkipVerify: true},\n\t\t\t\tPublish:   true,\n\t\t\t}\n\t\t\terr = conn.Initialize(context.Background())\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer conn.Close()\n\n\t\t\tw := &gortmplib.Writer{\n\t\t\t\tConn: conn,\n\t\t\t\tTracks: []*gortmplib.Track{\n\t\t\t\t\t{Codec: &codecs.H264{\n\t\t\t\t\t\tSPS: test.FormatH264.SPS,\n\t\t\t\t\t\tPPS: test.FormatH264.PPS,\n\t\t\t\t\t}},\n\t\t\t\t\t{Codec: &codecs.MPEG4Audio{\n\t\t\t\t\t\tConfig: test.FormatMPEG4Audio.Config,\n\t\t\t\t\t}},\n\t\t\t\t},\n\t\t\t}\n\t\t\terr = w.Initialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = w.WriteH264(\n\t\t\t\tw.Tracks[0],\n\t\t\t\t2*time.Second, 2*time.Second, [][]byte{\n\t\t\t\t\t{5, 2, 3, 4},\n\t\t\t\t})\n\t\t\trequire.NoError(t, err)\n\n\t\t\t<-dataReceived\n\n\t\t\tlist, err := s.APIConnsList()\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, &defs.APIRTMPConnList{\n\t\t\t\tItems: []defs.APIRTMPConn{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:                      list.Items[0].ID,\n\t\t\t\t\t\tCreated:                 list.Items[0].Created,\n\t\t\t\t\t\tRemoteAddr:              list.Items[0].RemoteAddr,\n\t\t\t\t\t\tState:                   \"publish\",\n\t\t\t\t\t\tPath:                    \"teststream\",\n\t\t\t\t\t\tQuery:                   \"user=myuser&pass=mypass&param=value\",\n\t\t\t\t\t\tUser:                    \"myuser\",\n\t\t\t\t\t\tInboundBytes:            list.Items[0].InboundBytes,\n\t\t\t\t\t\tOutboundBytes:           list.Items[0].OutboundBytes,\n\t\t\t\t\t\tOutboundFramesDiscarded: list.Items[0].OutboundFramesDiscarded,\n\t\t\t\t\t\tBytesReceived:           list.Items[0].BytesReceived,\n\t\t\t\t\t\tBytesSent:               list.Items[0].BytesSent,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}, list)\n\t\t})\n\t}\n}\n\nfunc TestServerRead(t *testing.T) {\n\tfor _, encrypt := range []string{\n\t\t\"plain\",\n\t\t\"tls\",\n\t} {\n\t\tt.Run(encrypt, func(t *testing.T) {\n\t\t\tvar serverCertFpath string\n\t\t\tvar serverKeyFpath string\n\n\t\t\tif encrypt == \"tls\" {\n\t\t\t\tvar err error\n\t\t\t\tserverCertFpath, err = test.CreateTempFile(test.TLSCertPub)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer os.Remove(serverCertFpath)\n\n\t\t\t\tserverKeyFpath, err = test.CreateTempFile(test.TLSCertKey)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer os.Remove(serverKeyFpath)\n\t\t\t}\n\t\t\tdesc := &description.Session{Medias: []*description.Media{test.MediaH264}}\n\n\t\t\tstrm := &stream.Stream{\n\t\t\t\tDesc:              desc,\n\t\t\t\tWriteQueueSize:    512,\n\t\t\t\tRTPMaxPayloadSize: 1450,\n\t\t\t\tParent:            test.NilLogger,\n\t\t\t}\n\t\t\terr := strm.Initialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsubStream := &stream.SubStream{\n\t\t\t\tStream:        strm,\n\t\t\t\tUseRTPPackets: false,\n\t\t\t}\n\t\t\terr = subStream.Initialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tpathManager := &test.PathManager{\n\t\t\t\tAddReaderImpl: func(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error) {\n\t\t\t\t\trequire.Equal(t, \"teststream\", req.AccessRequest.Name)\n\t\t\t\t\trequire.Equal(t, \"user=myuser&pass=mypass&param=value\", req.AccessRequest.Query)\n\t\t\t\t\trequire.Equal(t, \"myuser\", req.AccessRequest.Credentials.User)\n\t\t\t\t\trequire.Equal(t, \"mypass\", req.AccessRequest.Credentials.Pass)\n\t\t\t\t\treturn &defs.PathAddReaderRes{Path: &dummyPath{}, User: req.AccessRequest.Credentials.User, Stream: strm}, nil\n\t\t\t\t},\n\t\t\t}\n\n\t\t\ts := &Server{\n\t\t\t\tAddress:             \"127.0.0.1:1939\",\n\t\t\t\tReadTimeout:         conf.Duration(10 * time.Second),\n\t\t\t\tWriteTimeout:        conf.Duration(10 * time.Second),\n\t\t\t\tIsTLS:               encrypt == \"tls\",\n\t\t\t\tServerCert:          serverCertFpath,\n\t\t\t\tServerKey:           serverKeyFpath,\n\t\t\t\tRTSPAddress:         \"\",\n\t\t\t\tRunOnConnect:        \"\",\n\t\t\t\tRunOnConnectRestart: false,\n\t\t\t\tRunOnDisconnect:     \"\",\n\t\t\t\tExternalCmdPool:     nil,\n\t\t\t\tPathManager:         pathManager,\n\t\t\t\tParent:              test.NilLogger,\n\t\t\t}\n\t\t\terr = s.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer s.Close()\n\n\t\t\tvar rawURL string\n\n\t\t\tif encrypt == \"tls\" {\n\t\t\t\trawURL += \"rtmps://\"\n\t\t\t} else {\n\t\t\t\trawURL += \"rtmp://\"\n\t\t\t}\n\n\t\t\trawURL += \"127.0.0.1:1939/teststream?user=myuser&pass=mypass&param=value\"\n\n\t\t\tu, err := url.Parse(rawURL)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tconn := &gortmplib.Client{\n\t\t\t\tURL:       u,\n\t\t\t\tTLSConfig: &tls.Config{InsecureSkipVerify: true},\n\t\t\t\tPublish:   false,\n\t\t\t}\n\t\t\terr = conn.Initialize(context.Background())\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer conn.Close()\n\n\t\t\tstrm.WaitForReaders()\n\n\t\t\tgo func() {\n\t\t\t\tsubStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{\n\t\t\t\t\tNTP: time.Time{},\n\t\t\t\t\tPayload: unit.PayloadH264{\n\t\t\t\t\t\t{5, 2, 3, 4}, // IDR\n\t\t\t\t\t},\n\t\t\t\t})\n\n\t\t\t\tsubStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{\n\t\t\t\t\tNTP: time.Time{},\n\t\t\t\t\tPTS: 2 * 90000,\n\t\t\t\t\tPayload: unit.PayloadH264{\n\t\t\t\t\t\t{5, 2, 3, 4}, // IDR\n\t\t\t\t\t},\n\t\t\t\t})\n\n\t\t\t\tsubStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{\n\t\t\t\t\tNTP: time.Time{},\n\t\t\t\t\tPTS: 3 * 90000,\n\t\t\t\t\tPayload: unit.PayloadH264{\n\t\t\t\t\t\t{5, 2, 3, 4}, // IDR\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t}()\n\n\t\t\tr := &gortmplib.Reader{\n\t\t\t\tConn: conn,\n\t\t\t}\n\t\t\terr = r.Initialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\ttracks := r.Tracks()\n\t\t\trequire.Len(t, tracks, 1)\n\t\t\t_, ok := tracks[0].Codec.(*codecs.H264)\n\t\t\trequire.True(t, ok)\n\n\t\t\tr.OnDataH264(tracks[0], func(_ time.Duration, _ time.Duration, au [][]byte) {\n\t\t\t\trequire.Equal(t, [][]byte{\n\t\t\t\t\ttest.FormatH264.SPS,\n\t\t\t\t\ttest.FormatH264.PPS,\n\t\t\t\t\t{5, 2, 3, 4},\n\t\t\t\t}, au)\n\t\t\t})\n\n\t\t\terr = r.Read()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tlist, err := s.APIConnsList()\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, &defs.APIRTMPConnList{\n\t\t\t\tItems: []defs.APIRTMPConn{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:                      list.Items[0].ID,\n\t\t\t\t\t\tCreated:                 list.Items[0].Created,\n\t\t\t\t\t\tRemoteAddr:              list.Items[0].RemoteAddr,\n\t\t\t\t\t\tState:                   \"read\",\n\t\t\t\t\t\tPath:                    \"teststream\",\n\t\t\t\t\t\tQuery:                   \"user=myuser&pass=mypass&param=value\",\n\t\t\t\t\t\tUser:                    \"myuser\",\n\t\t\t\t\t\tInboundBytes:            list.Items[0].InboundBytes,\n\t\t\t\t\t\tOutboundBytes:           list.Items[0].OutboundBytes,\n\t\t\t\t\t\tOutboundFramesDiscarded: list.Items[0].OutboundFramesDiscarded,\n\t\t\t\t\t\tBytesReceived:           list.Items[0].BytesReceived,\n\t\t\t\t\t\tBytesSent:               list.Items[0].BytesSent,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}, list)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/servers/rtsp/conn.go",
    "content": "package rtsp\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"slices\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5\"\n\trtspauth \"github.com/bluenviron/gortsplib/v5/pkg/auth\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/base\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/liberrors\"\n\t\"github.com/google/uuid\"\n\n\t\"github.com/bluenviron/mediamtx/internal/auth\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/externalcmd\"\n\t\"github.com/bluenviron/mediamtx/internal/hooks\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/rtsp\"\n)\n\nfunc absoluteURL(req *base.Request, v string) string {\n\tif strings.HasPrefix(v, \"/\") {\n\t\tur := base.URL{\n\t\t\tScheme: req.URL.Scheme,\n\t\t\tHost:   req.URL.Host,\n\t\t\tPath:   v,\n\t\t}\n\t\treturn ur.String()\n\t}\n\n\treturn v\n}\n\nfunc tunnelLabel(t gortsplib.Tunnel) string {\n\tswitch t {\n\tcase gortsplib.TunnelHTTP:\n\t\treturn \"http\"\n\tcase gortsplib.TunnelWebSocket:\n\t\treturn \"websocket\"\n\tcase gortsplib.TunnelNone:\n\t\treturn \"none\"\n\t}\n\treturn \"unknown\"\n}\n\ntype connParent interface {\n\tlogger.Writer\n\tgetSessionByRSessionUnsafe(rsession *gortsplib.ServerSession) *session\n}\n\ntype conn struct {\n\tisTLS               bool\n\trtspAddress         string\n\tauthMethods         []rtspauth.VerifyMethod\n\treadTimeout         conf.Duration\n\trunOnConnect        string\n\trunOnConnectRestart bool\n\trunOnDisconnect     string\n\texternalCmdPool     *externalcmd.Pool\n\tpathManager         serverPathManager\n\trconn               *gortsplib.ServerConn\n\trserver             *gortsplib.Server\n\tparent              connParent\n\n\tuuid             uuid.UUID\n\tcreated          time.Time\n\tonDisconnectHook func()\n}\n\nfunc (c *conn) initialize() {\n\tc.uuid = uuid.New()\n\tc.created = time.Now()\n\n\tc.Log(logger.Info, \"opened\")\n\n\tc.onDisconnectHook = hooks.OnConnect(hooks.OnConnectParams{\n\t\tLogger:              c,\n\t\tExternalCmdPool:     c.externalCmdPool,\n\t\tRunOnConnect:        c.runOnConnect,\n\t\tRunOnConnectRestart: c.runOnConnectRestart,\n\t\tRunOnDisconnect:     c.runOnDisconnect,\n\t\tRTSPAddress:         c.rtspAddress,\n\t\tDesc: defs.APIPathReader{\n\t\t\tType: func() defs.APIPathReaderType {\n\t\t\t\tif c.isTLS {\n\t\t\t\t\treturn defs.APIPathReaderTypeRTSPSConn\n\t\t\t\t}\n\t\t\t\treturn defs.APIPathReaderTypeRTSPConn\n\t\t\t}(),\n\t\t\tID: c.uuid.String(),\n\t\t},\n\t})\n}\n\n// Log implements logger.Writer.\nfunc (c *conn) Log(level logger.Level, format string, args ...any) {\n\tc.parent.Log(level, \"[conn %v] \"+format, append([]any{c.rconn.NetConn().RemoteAddr()}, args...)...)\n}\n\n// Conn returns the RTSP connection.\nfunc (c *conn) Conn() *gortsplib.ServerConn {\n\treturn c.rconn\n}\n\nfunc (c *conn) remoteAddr() net.Addr {\n\treturn c.rconn.NetConn().RemoteAddr()\n}\n\nfunc (c *conn) ip() net.IP {\n\treturn c.rconn.NetConn().RemoteAddr().(*net.TCPAddr).IP\n}\n\n// onClose is called by rtspServer.\nfunc (c *conn) onClose(err error) {\n\tc.Log(logger.Info, \"closed: %v\", err)\n\n\tc.onDisconnectHook()\n}\n\n// onRequest is called by rtspServer.\nfunc (c *conn) onRequest(req *base.Request) {\n\tc.Log(logger.Debug, \"[c->s] %v\", req)\n}\n\n// OnResponse is called by rtspServer.\nfunc (c *conn) OnResponse(res *base.Response) {\n\tc.Log(logger.Debug, \"[s->c] %v\", res)\n}\n\n// onDescribe is called by rtspServer.\nfunc (c *conn) onDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,\n) (*base.Response, *gortsplib.ServerStream, error) {\n\tif len(ctx.Path) == 0 || ctx.Path[0] != '/' {\n\t\treturn &base.Response{\n\t\t\tStatusCode: base.StatusBadRequest,\n\t\t}, nil, fmt.Errorf(\"invalid path\")\n\t}\n\tctx.Path = ctx.Path[1:]\n\n\t// CustomVerifyFunc prevents hashed credentials from working.\n\t// Use it only when strictly needed.\n\tvar customVerifyFunc func(expectedUser, expectedPass string) bool\n\tif slices.Contains(c.authMethods, rtspauth.VerifyMethodDigestMD5) {\n\t\tcustomVerifyFunc = func(expectedUser, expectedPass string) bool {\n\t\t\treturn c.rconn.VerifyCredentials(ctx.Request, expectedUser, expectedPass)\n\t\t}\n\t}\n\n\tres := c.pathManager.Describe(defs.PathDescribeReq{\n\t\tAccessRequest: defs.PathAccessRequest{\n\t\t\tName:             ctx.Path,\n\t\t\tQuery:            ctx.Query,\n\t\t\tProto:            auth.ProtocolRTSP,\n\t\t\tID:               &c.uuid,\n\t\t\tCredentials:      rtsp.Credentials(ctx.Request),\n\t\t\tIP:               c.ip(),\n\t\t\tCustomVerifyFunc: customVerifyFunc,\n\t\t},\n\t})\n\n\tif res.Err != nil {\n\t\tvar terr *auth.Error\n\t\tif errors.As(res.Err, &terr) {\n\t\t\tres, err2 := c.handleAuthError(terr)\n\t\t\treturn res, nil, err2\n\t\t}\n\n\t\tvar terr2 defs.PathNoStreamAvailableError\n\t\tif errors.As(res.Err, &terr2) {\n\t\t\treturn &base.Response{\n\t\t\t\tStatusCode: base.StatusNotFound,\n\t\t\t}, nil, res.Err\n\t\t}\n\n\t\treturn &base.Response{\n\t\t\tStatusCode: base.StatusBadRequest,\n\t\t}, nil, res.Err\n\t}\n\n\tif res.Redirect != \"\" {\n\t\treturn &base.Response{\n\t\t\tStatusCode: base.StatusMovedPermanently,\n\t\t\tHeader: base.Header{\n\t\t\t\t\"Location\": base.HeaderValue{absoluteURL(ctx.Request, res.Redirect)},\n\t\t\t},\n\t\t}, nil, nil\n\t}\n\n\tvar strm *gortsplib.ServerStream\n\tif !c.isTLS {\n\t\tstrm = res.Stream.RTSPStream(c.rserver)\n\t} else {\n\t\tstrm = res.Stream.RTSPSStream(c.rserver)\n\t}\n\n\treturn &base.Response{\n\t\tStatusCode: base.StatusOK,\n\t}, strm, nil\n}\n\nfunc (c *conn) handleAuthError(err *auth.Error) (*base.Response, error) {\n\tif err.AskCredentials {\n\t\treturn &base.Response{\n\t\t\tStatusCode: base.StatusUnauthorized,\n\t\t}, liberrors.ErrServerAuth{}\n\t}\n\n\t// wait some seconds to delay brute force attacks\n\t<-time.After(auth.PauseAfterError)\n\n\treturn &base.Response{\n\t\tStatusCode: base.StatusUnauthorized,\n\t}, err\n}\n\nfunc (c *conn) apiItem() *defs.APIRTSPConn {\n\tstats := c.rconn.Stats()\n\n\treturn &defs.APIRTSPConn{\n\t\tID:         c.uuid,\n\t\tCreated:    c.created,\n\t\tRemoteAddr: c.remoteAddr().String(),\n\t\tSession: func() *uuid.UUID {\n\t\t\tsx := c.parent.getSessionByRSessionUnsafe(c.rconn.Session())\n\t\t\tif sx != nil {\n\t\t\t\treturn &sx.uuid\n\t\t\t}\n\t\t\treturn nil\n\t\t}(),\n\t\tTunnel:        tunnelLabel(c.rconn.Transport().Tunnel),\n\t\tInboundBytes:  stats.InboundBytes,\n\t\tOutboundBytes: stats.OutboundBytes,\n\t\tBytesReceived: stats.InboundBytes,\n\t\tBytesSent:     stats.OutboundBytes,\n\t}\n}\n"
  },
  {
    "path": "internal/servers/rtsp/mpegts_demuxer.go",
    "content": "package rtsp\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/pion/rtp\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/errordumper\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/mpegts\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n)\n\n// mpegtsDemuxer demuxes an MPEG-TS stream received via RTP into component streams.\ntype mpegtsDemuxer struct {\n\tsession      *session\n\tpathManager  serverPathManager\n\tpathConf     *conf.Path\n\tmpegtsMedia  *description.Media\n\tmpegtsFormat *format.MPEGTS\n\tdecodeErrors *errordumper.Dumper\n\tpathName     string\n\tquery        string\n\n\tpipeWriter *io.PipeWriter\n}\n\nfunc (d *mpegtsDemuxer) initialize() error {\n\tdecoder, err := d.mpegtsFormat.CreateDecoder()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create MPEG-TS decoder: %w\", err)\n\t}\n\n\tpr, pw := io.Pipe()\n\td.pipeWriter = pw\n\n\td.session.rsession.OnPacketRTP(d.mpegtsMedia, d.mpegtsFormat, func(pkt *rtp.Packet) {\n\t\ttsData, decErr := decoder.Decode(pkt)\n\t\tif decErr != nil {\n\t\t\td.decodeErrors.Add(decErr)\n\t\t\treturn\n\t\t}\n\n\t\tfor _, data := range tsData {\n\t\t\t_, err = pw.Write(data)\n\t\t\tif err != nil {\n\t\t\t\td.session.Log(logger.Warn, \"demuxer pipe write error: %v\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t})\n\n\tgo d.run(pr)\n\n\treturn nil\n}\n\nfunc (d *mpegtsDemuxer) close() {\n\td.pipeWriter.CloseWithError(io.EOF)\n}\n\nfunc (d *mpegtsDemuxer) run(pr *io.PipeReader) {\n\terr := d.doRun(pr)\n\tif err != nil {\n\t\td.session.Log(logger.Error, \"MPEG-TS demuxer error: %v\", err)\n\t\td.session.Close()\n\t}\n}\n\nfunc (d *mpegtsDemuxer) doRun(pr *io.PipeReader) error {\n\tr := &mpegts.EnhancedReader{R: pr}\n\terr := r.Initialize()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to initialize MPEG-TS reader: %w\", err)\n\t}\n\n\tr.OnDecodeError(func(err error) {\n\t\td.decodeErrors.Add(err)\n\t})\n\n\tvar subStream *stream.SubStream\n\n\tmedias, err := mpegts.ToStream(r, &subStream, d.session)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to map MPEG-TS to stream: %w\", err)\n\t}\n\n\tres, err := d.pathManager.AddPublisher(defs.PathAddPublisherReq{\n\t\tAuthor:        d.session,\n\t\tDesc:          &description.Session{Medias: medias},\n\t\tUseRTPPackets: false,\n\t\tReplaceNTP:    true,\n\t\tConfToCompare: d.pathConf,\n\t\tAccessRequest: defs.PathAccessRequest{\n\t\t\tName:     d.pathName,\n\t\t\tQuery:    d.query,\n\t\t\tPublish:  true,\n\t\t\tSkipAuth: true,\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to add publisher: %w\", err)\n\t}\n\n\tdefer res.Path.RemovePublisher(defs.PathRemovePublisherReq{Author: d.session})\n\n\tsubStream = res.SubStream\n\n\tfor {\n\t\terr = r.Read()\n\t\tif err != nil {\n\t\t\tif !errors.Is(err, io.EOF) {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/servers/rtsp/server.go",
    "content": "// Package rtsp contains a RTSP server.\npackage rtsp\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"reflect\"\n\t\"sort\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/auth\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/base\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/liberrors\"\n\t\"github.com/google/uuid\"\n\n\t\"github.com/bluenviron/mediamtx/internal/certloader\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/externalcmd\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/packetdumper\"\n)\n\n// ErrConnNotFound is returned when a connection is not found.\nvar ErrConnNotFound = errors.New(\"connection not found\")\n\n// ErrSessionNotFound is returned when a session is not found.\nvar ErrSessionNotFound = errors.New(\"session not found\")\n\nfunc interfaceIsEmpty(i any) bool {\n\treturn reflect.ValueOf(i).Kind() != reflect.Pointer || reflect.ValueOf(i).IsNil()\n}\n\nfunc printAddresses(srv *gortsplib.Server) string {\n\tvar ret []string\n\n\ttmp := srv.RTSPAddress\n\tif srv.TLSConfig == nil {\n\t\ttmp += \" (TCP/RTSP)\"\n\t} else {\n\t\ttmp += \" (TCP/RTSPS)\"\n\t}\n\tret = append(ret, tmp)\n\n\tif srv.UDPRTPAddress != \"\" {\n\t\ttmp = srv.UDPRTPAddress\n\t\tif srv.TLSConfig == nil {\n\t\t\ttmp += \" (UDP/RTP)\"\n\t\t} else {\n\t\t\ttmp += \" (UDP/SRTP)\"\n\t\t}\n\t\tret = append(ret, tmp)\n\t}\n\n\tif srv.UDPRTCPAddress != \"\" {\n\t\ttmp = srv.UDPRTCPAddress\n\t\tif srv.TLSConfig == nil {\n\t\t\ttmp += \" (UDP/RTCP)\"\n\t\t} else {\n\t\t\ttmp += \" (UDP/SRTCP)\"\n\t\t}\n\t\tret = append(ret, tmp)\n\t}\n\n\treturn strings.Join(ret, \", \")\n}\n\ntype serverMetrics interface {\n\tSetRTSPSServer(defs.APIRTSPServer)\n\tSetRTSPServer(defs.APIRTSPServer)\n}\n\ntype serverPathManager interface {\n\tFindPathConf(req defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error)\n\tDescribe(req defs.PathDescribeReq) defs.PathDescribeRes\n\tAddPublisher(_ defs.PathAddPublisherReq) (*defs.PathAddPublisherRes, error)\n\tAddReader(_ defs.PathAddReaderReq) (*defs.PathAddReaderRes, error)\n}\n\ntype serverParent interface {\n\tlogger.Writer\n}\n\n// Server is a RTSP server.\ntype Server struct {\n\tAddress             string\n\tAuthMethods         []auth.VerifyMethod\n\tDumpPackets         bool\n\tUDPReadBufferSize   uint\n\tReadTimeout         conf.Duration\n\tWriteTimeout        conf.Duration\n\tWriteQueueSize      int\n\tRTSPTransports      conf.RTSPTransports\n\tRTPAddress          string\n\tRTCPAddress         string\n\tMulticastIPRange    string\n\tMulticastRTPPort    int\n\tMulticastRTCPPort   int\n\tIsTLS               bool\n\tServerCert          string\n\tServerKey           string\n\tRTSPAddress         string\n\tTransports          conf.RTSPTransports\n\tRunOnConnect        string\n\tRunOnConnectRestart bool\n\tRunOnDisconnect     string\n\tExternalCmdPool     *externalcmd.Pool\n\tMetrics             serverMetrics\n\tPathManager         serverPathManager\n\tParent              serverParent\n\n\tctx       context.Context\n\tctxCancel func()\n\twg        sync.WaitGroup\n\tsrv       *gortsplib.Server\n\tmutex     sync.RWMutex\n\tconns     map[*gortsplib.ServerConn]*conn\n\tsessions  map[*gortsplib.ServerSession]*session\n\tloader    *certloader.CertLoader\n}\n\n// Initialize initializes the server.\nfunc (s *Server) Initialize() error {\n\ts.ctx, s.ctxCancel = context.WithCancel(context.Background())\n\n\ts.conns = make(map[*gortsplib.ServerConn]*conn)\n\ts.sessions = make(map[*gortsplib.ServerSession]*session)\n\n\ts.srv = &gortsplib.Server{\n\t\tHandler:           s,\n\t\tReadTimeout:       time.Duration(s.ReadTimeout),\n\t\tWriteTimeout:      time.Duration(s.WriteTimeout),\n\t\tUDPReadBufferSize: int(s.UDPReadBufferSize),\n\t\tWriteQueueSize:    s.WriteQueueSize,\n\t\tRTSPAddress:       s.Address,\n\t\tAuthMethods:       s.AuthMethods,\n\t}\n\n\tif _, ok := s.RTSPTransports[gortsplib.ProtocolUDP]; ok {\n\t\ts.srv.UDPRTPAddress = s.RTPAddress\n\t\ts.srv.UDPRTCPAddress = s.RTCPAddress\n\t}\n\n\tif _, ok := s.RTSPTransports[gortsplib.ProtocolUDPMulticast]; ok {\n\t\ts.srv.MulticastIPRange = s.MulticastIPRange\n\t\ts.srv.MulticastRTPPort = s.MulticastRTPPort\n\t\ts.srv.MulticastRTCPPort = s.MulticastRTCPPort\n\t}\n\n\tif s.IsTLS {\n\t\ts.loader = &certloader.CertLoader{\n\t\t\tCertPath: s.ServerCert,\n\t\t\tKeyPath:  s.ServerKey,\n\t\t\tParent:   s.Parent,\n\t\t}\n\t\terr := s.loader.Initialize()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\ts.srv.TLSConfig = &tls.Config{GetCertificate: s.loader.GetCertificate()}\n\t}\n\n\tif s.DumpPackets {\n\t\ts.srv.Listen = (&packetdumper.Listen{\n\t\t\tPrefix: \"rtsp_server_conn\",\n\t\t\tListen: net.Listen,\n\t\t}).Do\n\n\t\ts.srv.ListenPacket = (&packetdumper.ListenPacket{\n\t\t\tPrefix:       \"rtsp_server_packetconn\",\n\t\t\tListenPacket: net.ListenPacket,\n\t\t}).Do\n\t}\n\n\terr := s.srv.Start()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ts.Log(logger.Info, \"listener opened on %s\", printAddresses(s.srv))\n\n\ts.wg.Add(1)\n\tgo s.run()\n\n\tif !interfaceIsEmpty(s.Metrics) {\n\t\tif s.IsTLS {\n\t\t\ts.Metrics.SetRTSPSServer(s)\n\t\t} else {\n\t\t\ts.Metrics.SetRTSPServer(s)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Log implements logger.Writer.\nfunc (s *Server) Log(level logger.Level, format string, args ...any) {\n\tlabel := func() string {\n\t\tif s.IsTLS {\n\t\t\treturn \"RTSPS\"\n\t\t}\n\t\treturn \"RTSP\"\n\t}()\n\ts.Parent.Log(level, \"[%s] \"+format, append([]any{label}, args...)...)\n}\n\n// Close closes the server.\nfunc (s *Server) Close() {\n\ts.Log(logger.Info, \"listener is closing\")\n\n\tif !interfaceIsEmpty(s.Metrics) {\n\t\tif s.IsTLS {\n\t\t\ts.Metrics.SetRTSPSServer(nil)\n\t\t} else {\n\t\t\ts.Metrics.SetRTSPServer(nil)\n\t\t}\n\t}\n\n\ts.ctxCancel()\n\ts.wg.Wait()\n\n\tif s.loader != nil {\n\t\ts.loader.Close()\n\t}\n}\n\nfunc (s *Server) run() {\n\tdefer s.wg.Done()\n\n\tserverErr := make(chan error)\n\tgo func() {\n\t\tserverErr <- s.srv.Wait()\n\t}()\n\nouter:\n\tselect {\n\tcase err := <-serverErr:\n\t\ts.Log(logger.Error, \"%s\", err)\n\t\tbreak outer\n\n\tcase <-s.ctx.Done():\n\t\ts.srv.Close()\n\t\t<-serverErr\n\t\tbreak outer\n\t}\n\n\ts.ctxCancel()\n}\n\n// OnConnOpen implements gortsplib.ServerHandlerOnConnOpen.\nfunc (s *Server) OnConnOpen(ctx *gortsplib.ServerHandlerOnConnOpenCtx) {\n\tc := &conn{\n\t\tisTLS:               s.IsTLS,\n\t\trtspAddress:         s.RTSPAddress,\n\t\tauthMethods:         s.AuthMethods,\n\t\treadTimeout:         s.ReadTimeout,\n\t\trunOnConnect:        s.RunOnConnect,\n\t\trunOnConnectRestart: s.RunOnConnectRestart,\n\t\trunOnDisconnect:     s.RunOnDisconnect,\n\t\texternalCmdPool:     s.ExternalCmdPool,\n\t\tpathManager:         s.PathManager,\n\t\trconn:               ctx.Conn,\n\t\trserver:             s.srv,\n\t\tparent:              s,\n\t}\n\tc.initialize()\n\ts.mutex.Lock()\n\ts.conns[ctx.Conn] = c\n\ts.mutex.Unlock()\n\n\tctx.Conn.SetUserData(c)\n}\n\n// OnConnClose implements gortsplib.ServerHandlerOnConnClose.\nfunc (s *Server) OnConnClose(ctx *gortsplib.ServerHandlerOnConnCloseCtx) {\n\ts.mutex.Lock()\n\tc := s.conns[ctx.Conn]\n\tdelete(s.conns, ctx.Conn)\n\ts.mutex.Unlock()\n\tc.onClose(ctx.Error)\n}\n\n// OnRequest implements gortsplib.ServerHandlerOnRequest.\nfunc (s *Server) OnRequest(sc *gortsplib.ServerConn, req *base.Request) {\n\tc := sc.UserData().(*conn)\n\tc.onRequest(req)\n}\n\n// OnResponse implements gortsplib.ServerHandlerOnResponse.\nfunc (s *Server) OnResponse(sc *gortsplib.ServerConn, res *base.Response) {\n\tc := sc.UserData().(*conn)\n\tc.OnResponse(res)\n}\n\n// OnSessionOpen implements gortsplib.ServerHandlerOnSessionOpen.\nfunc (s *Server) OnSessionOpen(ctx *gortsplib.ServerHandlerOnSessionOpenCtx) {\n\tse := &session{\n\t\tisTLS:           s.IsTLS,\n\t\ttransports:      s.Transports,\n\t\trsession:        ctx.Session,\n\t\trconn:           ctx.Conn,\n\t\trserver:         s.srv,\n\t\texternalCmdPool: s.ExternalCmdPool,\n\t\tpathManager:     s.PathManager,\n\t\tparent:          s,\n\t}\n\tse.initialize()\n\ts.mutex.Lock()\n\ts.sessions[ctx.Session] = se\n\ts.mutex.Unlock()\n\tctx.Session.SetUserData(se)\n}\n\n// OnSessionClose implements gortsplib.ServerHandlerOnSessionClose.\nfunc (s *Server) OnSessionClose(ctx *gortsplib.ServerHandlerOnSessionCloseCtx) {\n\ts.mutex.Lock()\n\tse := s.sessions[ctx.Session]\n\tdelete(s.sessions, ctx.Session)\n\ts.mutex.Unlock()\n\n\tif se != nil {\n\t\tse.onClose(ctx.Error)\n\t}\n}\n\n// OnDescribe implements gortsplib.ServerHandlerOnDescribe.\nfunc (s *Server) OnDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,\n) (*base.Response, *gortsplib.ServerStream, error) {\n\tc := ctx.Conn.UserData().(*conn)\n\treturn c.onDescribe(ctx)\n}\n\n// OnAnnounce implements gortsplib.ServerHandlerOnAnnounce.\nfunc (s *Server) OnAnnounce(ctx *gortsplib.ServerHandlerOnAnnounceCtx) (*base.Response, error) {\n\tc := ctx.Conn.UserData().(*conn)\n\tse := ctx.Session.UserData().(*session)\n\treturn se.onAnnounce(c, ctx)\n}\n\n// OnSetup implements gortsplib.ServerHandlerOnSetup.\nfunc (s *Server) OnSetup(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) {\n\tc := ctx.Conn.UserData().(*conn)\n\tse := ctx.Session.UserData().(*session)\n\treturn se.onSetup(c, ctx)\n}\n\n// OnPlay implements gortsplib.ServerHandlerOnPlay.\nfunc (s *Server) OnPlay(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) {\n\tse := ctx.Session.UserData().(*session)\n\treturn se.onPlay(ctx)\n}\n\n// OnRecord implements gortsplib.ServerHandlerOnRecord.\nfunc (s *Server) OnRecord(ctx *gortsplib.ServerHandlerOnRecordCtx) (*base.Response, error) {\n\tse := ctx.Session.UserData().(*session)\n\treturn se.onRecord(ctx)\n}\n\n// OnPause implements gortsplib.ServerHandlerOnPause.\nfunc (s *Server) OnPause(ctx *gortsplib.ServerHandlerOnPauseCtx) (*base.Response, error) {\n\tse := ctx.Session.UserData().(*session)\n\treturn se.onPause(ctx)\n}\n\n// OnPacketsLost implements gortsplib.ServerHandlerOnPacketsLost.\nfunc (s *Server) OnPacketsLost(ctx *gortsplib.ServerHandlerOnPacketsLostCtx) {\n\tse := ctx.Session.UserData().(*session)\n\tse.onPacketsLost(ctx)\n}\n\n// OnDecodeError implements gortsplib.ServerHandlerOnDecodeError.\nfunc (s *Server) OnDecodeError(ctx *gortsplib.ServerHandlerOnDecodeErrorCtx) {\n\tse := ctx.Session.UserData().(*session)\n\tse.onDecodeError(ctx)\n}\n\n// OnStreamWriteError implements gortsplib.ServerHandlerOnStreamWriteError.\nfunc (s *Server) OnStreamWriteError(ctx *gortsplib.ServerHandlerOnStreamWriteErrorCtx) {\n\tse := ctx.Session.UserData().(*session)\n\tse.onStreamWriteError(ctx)\n}\n\nfunc (s *Server) findConnByUUID(uuid uuid.UUID) *conn {\n\tfor _, c := range s.conns {\n\t\tif c.uuid == uuid {\n\t\t\treturn c\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (s *Server) findSessionByUUID(uuid uuid.UUID) (*gortsplib.ServerSession, *session) {\n\tfor key, sx := range s.sessions {\n\t\tif sx.uuid == uuid {\n\t\t\treturn key, sx\n\t\t}\n\t}\n\treturn nil, nil\n}\n\nfunc (s *Server) getConnByRConnUnsafe(rconn *gortsplib.ServerConn) *conn {\n\treturn s.conns[rconn]\n}\n\nfunc (s *Server) getSessionByRSessionUnsafe(rsession *gortsplib.ServerSession) *session {\n\treturn s.sessions[rsession]\n}\n\n// APIConnsList is called by api and metrics.\nfunc (s *Server) APIConnsList() (*defs.APIRTSPConnsList, error) {\n\tselect {\n\tcase <-s.ctx.Done():\n\t\treturn nil, fmt.Errorf(\"terminated\")\n\tdefault:\n\t}\n\n\ts.mutex.RLock()\n\tdefer s.mutex.RUnlock()\n\n\tdata := &defs.APIRTSPConnsList{\n\t\tItems: []defs.APIRTSPConn{},\n\t}\n\n\tfor _, c := range s.conns {\n\t\tdata.Items = append(data.Items, *c.apiItem())\n\t}\n\n\tsort.Slice(data.Items, func(i, j int) bool {\n\t\treturn data.Items[i].Created.Before(data.Items[j].Created)\n\t})\n\n\treturn data, nil\n}\n\n// APIConnsGet is called by api.\nfunc (s *Server) APIConnsGet(uuid uuid.UUID) (*defs.APIRTSPConn, error) {\n\tselect {\n\tcase <-s.ctx.Done():\n\t\treturn nil, fmt.Errorf(\"terminated\")\n\tdefault:\n\t}\n\n\ts.mutex.RLock()\n\tdefer s.mutex.RUnlock()\n\n\tconn := s.findConnByUUID(uuid)\n\tif conn == nil {\n\t\treturn nil, ErrConnNotFound\n\t}\n\n\treturn conn.apiItem(), nil\n}\n\n// APISessionsList is called by api and metrics.\nfunc (s *Server) APISessionsList() (*defs.APIRTSPSessionList, error) {\n\tselect {\n\tcase <-s.ctx.Done():\n\t\treturn nil, fmt.Errorf(\"terminated\")\n\tdefault:\n\t}\n\n\ts.mutex.RLock()\n\tdefer s.mutex.RUnlock()\n\n\tdata := &defs.APIRTSPSessionList{\n\t\tItems: []defs.APIRTSPSession{},\n\t}\n\n\tfor _, s := range s.sessions {\n\t\tdata.Items = append(data.Items, *s.apiItem())\n\t}\n\n\tsort.Slice(data.Items, func(i, j int) bool {\n\t\treturn data.Items[i].Created.Before(data.Items[j].Created)\n\t})\n\n\treturn data, nil\n}\n\n// APISessionsGet is called by api.\nfunc (s *Server) APISessionsGet(uuid uuid.UUID) (*defs.APIRTSPSession, error) {\n\tselect {\n\tcase <-s.ctx.Done():\n\t\treturn nil, fmt.Errorf(\"terminated\")\n\tdefault:\n\t}\n\n\ts.mutex.RLock()\n\tdefer s.mutex.RUnlock()\n\n\t_, sx := s.findSessionByUUID(uuid)\n\tif sx == nil {\n\t\treturn nil, ErrSessionNotFound\n\t}\n\n\treturn sx.apiItem(), nil\n}\n\n// APISessionsKick is called by api.\nfunc (s *Server) APISessionsKick(uuid uuid.UUID) error {\n\tselect {\n\tcase <-s.ctx.Done():\n\t\treturn fmt.Errorf(\"terminated\")\n\tdefault:\n\t}\n\n\ts.mutex.RLock()\n\tdefer s.mutex.RUnlock()\n\n\tkey, sx := s.findSessionByUUID(uuid)\n\tif sx == nil {\n\t\treturn ErrSessionNotFound\n\t}\n\n\tsx.Close()\n\tdelete(s.sessions, key)\n\tsx.onClose(liberrors.ErrServerTerminated{})\n\treturn nil\n}\n"
  },
  {
    "path": "internal/servers/rtsp/server_test.go",
    "content": "package rtsp\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"fmt\"\n\t\"sync/atomic\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5\"\n\trtspauth \"github.com/bluenviron/gortsplib/v5/pkg/auth\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/base\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\tmpegts \"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts\"\n\ttscodecs \"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts/codecs\"\n\t\"github.com/bluenviron/mediamtx/internal/auth\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/externalcmd\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n\t\"github.com/pion/rtp\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc ptrOf[T any](v T) *T {\n\tp := new(T)\n\t*p = v\n\treturn p\n}\n\ntype dummyPath struct{}\n\nfunc (p *dummyPath) Name() string {\n\treturn \"teststream\"\n}\n\nfunc (p *dummyPath) SafeConf() *conf.Path {\n\treturn &conf.Path{}\n}\n\nfunc (p *dummyPath) ExternalCmdEnv() externalcmd.Environment {\n\treturn externalcmd.Environment{}\n}\n\nfunc (p *dummyPath) RemovePublisher(_ defs.PathRemovePublisherReq) {\n}\n\nfunc (p *dummyPath) RemoveReader(_ defs.PathRemoveReaderReq) {\n}\n\nfunc TestServerPublish(t *testing.T) {\n\tfor _, ca := range []string{\"basic\", \"digest\", \"basic+digest\"} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tvar strm *stream.Stream\n\t\t\tvar reader *stream.Reader\n\t\t\tdefer func() {\n\t\t\t\tstrm.RemoveReader(reader)\n\t\t\t}()\n\t\t\tdataReceived := make(chan struct{})\n\n\t\t\tn := 0\n\n\t\t\tpathManager := &test.PathManager{\n\t\t\t\tFindPathConfImpl: func(req defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error) {\n\t\t\t\t\trequire.Equal(t, \"teststream\", req.AccessRequest.Name)\n\t\t\t\t\trequire.Equal(t, \"param=value\", req.AccessRequest.Query)\n\n\t\t\t\t\tif ca == \"basic\" {\n\t\t\t\t\t\trequire.Nil(t, req.AccessRequest.CustomVerifyFunc)\n\n\t\t\t\t\t\tif req.AccessRequest.Credentials.User == \"\" && req.AccessRequest.Credentials.Pass == \"\" {\n\t\t\t\t\t\t\treturn nil, &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf(\"auth error\")}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\trequire.Equal(t, \"myuser\", req.AccessRequest.Credentials.User)\n\t\t\t\t\t\trequire.Equal(t, \"mypass\", req.AccessRequest.Credentials.Pass)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tok := req.AccessRequest.CustomVerifyFunc(\"myuser\", \"mypass\")\n\t\t\t\t\t\tif n == 0 {\n\t\t\t\t\t\t\trequire.False(t, ok)\n\t\t\t\t\t\t\tn++\n\t\t\t\t\t\t\treturn nil, &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf(\"auth error\")}\n\t\t\t\t\t\t}\n\t\t\t\t\t\trequire.True(t, ok)\n\t\t\t\t\t}\n\n\t\t\t\t\treturn &defs.PathFindPathConfRes{Conf: &conf.Path{}, User: req.AccessRequest.Credentials.User}, nil\n\t\t\t\t},\n\t\t\t\tAddPublisherImpl: func(req defs.PathAddPublisherReq) (*defs.PathAddPublisherRes, error) {\n\t\t\t\t\trequire.Equal(t, \"teststream\", req.AccessRequest.Name)\n\t\t\t\t\trequire.Equal(t, \"param=value\", req.AccessRequest.Query)\n\t\t\t\t\trequire.True(t, req.AccessRequest.SkipAuth)\n\n\t\t\t\t\tstrm = &stream.Stream{\n\t\t\t\t\t\tDesc:              req.Desc,\n\t\t\t\t\t\tWriteQueueSize:    512,\n\t\t\t\t\t\tRTPMaxPayloadSize: 1450,\n\t\t\t\t\t\tParent:            test.NilLogger,\n\t\t\t\t\t}\n\t\t\t\t\terr := strm.Initialize()\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\tsubStream := &stream.SubStream{\n\t\t\t\t\t\tStream:        strm,\n\t\t\t\t\t\tUseRTPPackets: true,\n\t\t\t\t\t}\n\t\t\t\t\terr = subStream.Initialize()\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\treader = &stream.Reader{Parent: test.NilLogger}\n\n\t\t\t\t\treader.OnData(\n\t\t\t\t\t\tstrm.Desc.Medias[0],\n\t\t\t\t\t\tstrm.Desc.Medias[0].Formats[0],\n\t\t\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t\t\trequire.Equal(t, unit.PayloadH264{\n\t\t\t\t\t\t\t\ttest.FormatH264.SPS,\n\t\t\t\t\t\t\t\ttest.FormatH264.PPS,\n\t\t\t\t\t\t\t\t{5, 2, 3, 4},\n\t\t\t\t\t\t\t}, u.Payload)\n\t\t\t\t\t\t\tclose(dataReceived)\n\t\t\t\t\t\t\treturn nil\n\t\t\t\t\t\t})\n\n\t\t\t\t\tstrm.AddReader(reader)\n\n\t\t\t\t\treturn &defs.PathAddPublisherRes{Path: &dummyPath{}, SubStream: subStream}, nil\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tvar authMethods []rtspauth.VerifyMethod\n\t\t\tswitch ca {\n\t\t\tcase \"basic\":\n\t\t\t\tauthMethods = []rtspauth.VerifyMethod{rtspauth.VerifyMethodBasic}\n\t\t\tcase \"digest\":\n\t\t\t\tauthMethods = []rtspauth.VerifyMethod{rtspauth.VerifyMethodDigestMD5}\n\t\t\tdefault:\n\t\t\t\tauthMethods = []rtspauth.VerifyMethod{rtspauth.VerifyMethodBasic, rtspauth.VerifyMethodDigestMD5}\n\t\t\t}\n\n\t\t\ts := &Server{\n\t\t\t\tAddress:        \"127.0.0.1:8557\",\n\t\t\t\tAuthMethods:    authMethods,\n\t\t\t\tReadTimeout:    conf.Duration(10 * time.Second),\n\t\t\t\tWriteTimeout:   conf.Duration(10 * time.Second),\n\t\t\t\tWriteQueueSize: 512,\n\t\t\t\tTransports:     conf.RTSPTransports{gortsplib.ProtocolTCP: {}},\n\t\t\t\tPathManager:    pathManager,\n\t\t\t\tParent:         test.NilLogger,\n\t\t\t}\n\t\t\terr := s.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer s.Close()\n\n\t\t\tsource := gortsplib.Client{}\n\n\t\t\tmedia0 := test.UniqueMediaH264()\n\n\t\t\terr = source.StartRecording(\n\t\t\t\t\"rtsp://myuser:mypass@127.0.0.1:8557/teststream?param=value\",\n\t\t\t\t&description.Session{Medias: []*description.Media{media0}})\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer source.Close()\n\n\t\t\terr = source.WritePacketRTP(media0, &rtp.Packet{\n\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\tVersion:        2,\n\t\t\t\t\tMarker:         true,\n\t\t\t\t\tPayloadType:    96,\n\t\t\t\t\tSequenceNumber: 123,\n\t\t\t\t\tTimestamp:      45343,\n\t\t\t\t\tSSRC:           563423,\n\t\t\t\t},\n\t\t\t\tPayload: []byte{5, 2, 3, 4},\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\n\t\t\t<-dataReceived\n\n\t\t\tlist, err := s.APISessionsList()\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, &defs.APIRTSPSessionList{\n\t\t\t\tItems: []defs.APIRTSPSession{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:                 list.Items[0].ID,\n\t\t\t\t\t\tCreated:            list.Items[0].Created,\n\t\t\t\t\t\tRemoteAddr:         list.Items[0].RemoteAddr,\n\t\t\t\t\t\tState:              \"publish\",\n\t\t\t\t\t\tPath:               \"teststream\",\n\t\t\t\t\t\tQuery:              \"param=value\",\n\t\t\t\t\t\tUser:               \"myuser\",\n\t\t\t\t\t\tInboundBytes:       list.Items[0].InboundBytes,\n\t\t\t\t\t\tInboundRTPPackets:  list.Items[0].InboundRTPPackets,\n\t\t\t\t\t\tOutboundBytes:      list.Items[0].OutboundBytes,\n\t\t\t\t\t\tBytesReceived:      list.Items[0].BytesReceived,\n\t\t\t\t\t\tBytesSent:          list.Items[0].BytesSent,\n\t\t\t\t\t\tConns:              list.Items[0].Conns,\n\t\t\t\t\t\tRTPPacketsReceived: list.Items[0].RTPPacketsReceived,\n\t\t\t\t\t\tTransport:          ptrOf(\"TCP\"),\n\t\t\t\t\t\tProfile:            ptrOf(\"AVP\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}, list)\n\t\t})\n\t}\n}\n\nfunc TestServerPublishMPEGTS(t *testing.T) {\n\tvar strm *stream.Stream\n\tvar reader *stream.Reader\n\tdefer func() {\n\t\tif strm != nil && reader != nil {\n\t\t\tstrm.RemoveReader(reader)\n\t\t}\n\t}()\n\n\tdataReceived := make(chan struct{})\n\n\tpathConf := &conf.Path{RTSPDemuxMpegts: true}\n\n\tpathManager := &test.PathManager{\n\t\tFindPathConfImpl: func(req defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error) {\n\t\t\trequire.Equal(t, \"teststream\", req.AccessRequest.Name)\n\t\t\trequire.Equal(t, \"param=value\", req.AccessRequest.Query)\n\t\t\treturn &defs.PathFindPathConfRes{Conf: pathConf}, nil\n\t\t},\n\t\tAddPublisherImpl: func(req defs.PathAddPublisherReq) (*defs.PathAddPublisherRes, error) {\n\t\t\trequire.Equal(t, \"teststream\", req.AccessRequest.Name)\n\t\t\trequire.Equal(t, \"param=value\", req.AccessRequest.Query)\n\t\t\trequire.True(t, req.AccessRequest.SkipAuth)\n\t\t\trequire.False(t, req.UseRTPPackets)\n\t\t\trequire.True(t, req.ReplaceNTP)\n\t\t\trequire.Same(t, pathConf, req.ConfToCompare)\n\t\t\trequire.Equal(t, &description.Session{Medias: []*description.Media{{\n\t\t\t\tType: description.MediaTypeVideo,\n\t\t\t\tFormats: []format.Format{&format.H264{\n\t\t\t\t\tPayloadTyp:        96,\n\t\t\t\t\tPacketizationMode: 1,\n\t\t\t\t}},\n\t\t\t}}}, req.Desc)\n\n\t\t\tstrm = &stream.Stream{\n\t\t\t\tDesc:              req.Desc,\n\t\t\t\tWriteQueueSize:    512,\n\t\t\t\tRTPMaxPayloadSize: 1450,\n\t\t\t\tParent:            test.NilLogger,\n\t\t\t}\n\t\t\terr := strm.Initialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsubStream := &stream.SubStream{\n\t\t\t\tStream:        strm,\n\t\t\t\tUseRTPPackets: false,\n\t\t\t}\n\t\t\terr = subStream.Initialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\treader = &stream.Reader{Parent: test.NilLogger}\n\t\t\tn := 0\n\n\t\t\treader.OnData(\n\t\t\t\tstrm.Desc.Medias[0],\n\t\t\t\tstrm.Desc.Medias[0].Formats[0],\n\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\tif n == 0 {\n\t\t\t\t\t\trequire.Equal(t, unit.PayloadH264{\n\t\t\t\t\t\t\ttest.FormatH264.SPS,\n\t\t\t\t\t\t\ttest.FormatH264.PPS,\n\t\t\t\t\t\t\t{5, 1},\n\t\t\t\t\t\t}, u.Payload)\n\t\t\t\t\t\tclose(dataReceived)\n\t\t\t\t\t}\n\t\t\t\t\tn++\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\n\t\t\tstrm.AddReader(reader)\n\n\t\t\treturn &defs.PathAddPublisherRes{Path: &dummyPath{}, SubStream: subStream}, nil\n\t\t},\n\t}\n\n\ts := &Server{\n\t\tAddress:        \"127.0.0.1:8557\",\n\t\tReadTimeout:    conf.Duration(10 * time.Second),\n\t\tWriteTimeout:   conf.Duration(10 * time.Second),\n\t\tWriteQueueSize: 512,\n\t\tTransports:     conf.RTSPTransports{gortsplib.ProtocolTCP: {}},\n\t\tPathManager:    pathManager,\n\t\tParent:         test.NilLogger,\n\t}\n\terr := s.Initialize()\n\trequire.NoError(t, err)\n\tdefer s.Close()\n\n\tsource := gortsplib.Client{}\n\n\tmedia0 := &description.Media{\n\t\tType:    description.MediaTypeApplication,\n\t\tFormats: []format.Format{&format.MPEGTS{}},\n\t}\n\n\terr = source.StartRecording(\n\t\t\"rtsp://127.0.0.1:8557/teststream?param=value\",\n\t\t&description.Session{Medias: []*description.Media{media0}})\n\trequire.NoError(t, err)\n\tdefer source.Close()\n\n\ttrack := &mpegts.Track{Codec: &tscodecs.H264{}}\n\n\tvar buf bytes.Buffer\n\tbw := bufio.NewWriter(&buf)\n\tw := &mpegts.Writer{W: bw, Tracks: []*mpegts.Track{track}}\n\terr = w.Initialize()\n\trequire.NoError(t, err)\n\n\t// the MPEG-TS muxer needs two PES packets in order to write the first one\n\terr = w.WriteH264(track, 0, 0, [][]byte{\n\t\ttest.FormatH264.SPS,\n\t\ttest.FormatH264.PPS,\n\t\t{5, 1},\n\t})\n\trequire.NoError(t, err)\n\n\terr = w.WriteH264(track, 0, 0, [][]byte{{5, 2}})\n\trequire.NoError(t, err)\n\n\terr = bw.Flush()\n\trequire.NoError(t, err)\n\n\traw := buf.Bytes()\n\trequire.NotEmpty(t, raw)\n\trequire.Zero(t, len(raw)%188)\n\n\ttsPackets := make([][]byte, 0, len(raw)/188)\n\tfor len(raw) > 0 {\n\t\ttsPackets = append(tsPackets, raw[:188:188])\n\t\traw = raw[188:]\n\t}\n\n\tencoder, err := media0.Formats[0].(*format.MPEGTS).CreateEncoder()\n\trequire.NoError(t, err)\n\n\trtpPackets, err := encoder.Encode(tsPackets)\n\trequire.NoError(t, err)\n\n\tfor _, pkt := range rtpPackets {\n\t\terr = source.WritePacketRTP(media0, pkt)\n\t\trequire.NoError(t, err)\n\t}\n\n\t<-dataReceived\n}\n\nfunc TestServerRead(t *testing.T) {\n\tfor _, ca := range []string{\"basic\", \"digest\", \"basic+digest\"} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tdesc := &description.Session{Medias: []*description.Media{test.MediaH264}}\n\n\t\t\tstrm := &stream.Stream{\n\t\t\t\tDesc:              desc,\n\t\t\t\tWriteQueueSize:    512,\n\t\t\t\tRTPMaxPayloadSize: 1450,\n\t\t\t\tParent:            test.NilLogger,\n\t\t\t}\n\t\t\terr := strm.Initialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsubStream := &stream.SubStream{\n\t\t\t\tStream:        strm,\n\t\t\t\tUseRTPPackets: false,\n\t\t\t}\n\t\t\terr = subStream.Initialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tn := 0\n\n\t\t\tpathManager := &test.PathManager{\n\t\t\t\tDescribeImpl: func(req defs.PathDescribeReq) defs.PathDescribeRes {\n\t\t\t\t\trequire.Equal(t, \"teststream\", req.AccessRequest.Name)\n\t\t\t\t\trequire.Equal(t, \"param=value\", req.AccessRequest.Query)\n\n\t\t\t\t\tif ca == \"basic\" {\n\t\t\t\t\t\trequire.Nil(t, req.AccessRequest.CustomVerifyFunc)\n\n\t\t\t\t\t\tif req.AccessRequest.Credentials.User == \"\" && req.AccessRequest.Credentials.Pass == \"\" {\n\t\t\t\t\t\t\treturn defs.PathDescribeRes{Err: &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf(\"auth error\")}}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\trequire.Equal(t, \"myuser\", req.AccessRequest.Credentials.User)\n\t\t\t\t\t\trequire.Equal(t, \"mypass\", req.AccessRequest.Credentials.Pass)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tok := req.AccessRequest.CustomVerifyFunc(\"myuser\", \"mypass\")\n\t\t\t\t\t\tif n == 0 {\n\t\t\t\t\t\t\trequire.False(t, ok)\n\t\t\t\t\t\t\tn++\n\t\t\t\t\t\t\treturn defs.PathDescribeRes{Err: &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf(\"auth error\")}}\n\t\t\t\t\t\t}\n\t\t\t\t\t\trequire.True(t, ok)\n\t\t\t\t\t}\n\n\t\t\t\t\treturn defs.PathDescribeRes{\n\t\t\t\t\t\tPath:   &dummyPath{},\n\t\t\t\t\t\tStream: strm,\n\t\t\t\t\t\tErr:    nil,\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t\tAddReaderImpl: func(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error) {\n\t\t\t\t\trequire.Equal(t, \"teststream\", req.AccessRequest.Name)\n\t\t\t\t\trequire.Equal(t, \"param=value\", req.AccessRequest.Query)\n\n\t\t\t\t\tif ca == \"basic\" {\n\t\t\t\t\t\trequire.Nil(t, req.AccessRequest.CustomVerifyFunc)\n\t\t\t\t\t\trequire.Equal(t, \"myuser\", req.AccessRequest.Credentials.User)\n\t\t\t\t\t\trequire.Equal(t, \"mypass\", req.AccessRequest.Credentials.Pass)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tok := req.AccessRequest.CustomVerifyFunc(\"myuser\", \"mypass\")\n\t\t\t\t\t\trequire.True(t, ok)\n\t\t\t\t\t}\n\n\t\t\t\t\treturn &defs.PathAddReaderRes{Path: &dummyPath{}, User: req.AccessRequest.Credentials.User, Stream: strm}, nil\n\t\t\t\t},\n\t\t\t}\n\n\t\t\tvar authMethods []rtspauth.VerifyMethod\n\t\t\tswitch ca {\n\t\t\tcase \"basic\":\n\t\t\t\tauthMethods = []rtspauth.VerifyMethod{rtspauth.VerifyMethodBasic}\n\t\t\tcase \"digest\":\n\t\t\t\tauthMethods = []rtspauth.VerifyMethod{rtspauth.VerifyMethodDigestMD5}\n\t\t\tdefault:\n\t\t\t\tauthMethods = []rtspauth.VerifyMethod{rtspauth.VerifyMethodBasic, rtspauth.VerifyMethodDigestMD5}\n\t\t\t}\n\n\t\t\ts := &Server{\n\t\t\t\tAddress:        \"127.0.0.1:8557\",\n\t\t\t\tAuthMethods:    authMethods,\n\t\t\t\tReadTimeout:    conf.Duration(10 * time.Second),\n\t\t\t\tWriteTimeout:   conf.Duration(10 * time.Second),\n\t\t\t\tWriteQueueSize: 512,\n\t\t\t\tTransports:     conf.RTSPTransports{gortsplib.ProtocolTCP: {}},\n\t\t\t\tPathManager:    pathManager,\n\t\t\t\tParent:         test.NilLogger,\n\t\t\t}\n\t\t\terr = s.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer s.Close()\n\n\t\t\tu, err := base.ParseURL(\"rtsp://myuser:mypass@127.0.0.1:8557/teststream?param=value\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\treader := gortsplib.Client{\n\t\t\t\tScheme: u.Scheme,\n\t\t\t\tHost:   u.Host,\n\t\t\t}\n\n\t\t\terr = reader.Start()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer reader.Close()\n\n\t\t\tdesc2, _, err := reader.Describe(u)\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = reader.SetupAll(desc2.BaseURL, desc2.Medias)\n\t\t\trequire.NoError(t, err)\n\n\t\t\trecv := make(chan struct{})\n\n\t\t\treader.OnPacketRTPAny(func(_ *description.Media, _ format.Format, p *rtp.Packet) {\n\t\t\t\trequire.Equal(t, &rtp.Packet{\n\t\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\t\tVersion:        2,\n\t\t\t\t\t\tMarker:         true,\n\t\t\t\t\t\tPayloadType:    96,\n\t\t\t\t\t\tSequenceNumber: p.SequenceNumber,\n\t\t\t\t\t\tTimestamp:      p.Timestamp,\n\t\t\t\t\t\tSSRC:           p.SSRC,\n\t\t\t\t\t},\n\t\t\t\t\tPayload: []byte{\n\t\t\t\t\t\t0x18, 0x00, 0x19, 0x67, 0x42, 0xc0, 0x28, 0xd9,\n\t\t\t\t\t\t0x00, 0x78, 0x02, 0x27, 0xe5, 0x84, 0x00, 0x00,\n\t\t\t\t\t\t0x03, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0xf0,\n\t\t\t\t\t\t0x3c, 0x60, 0xc9, 0x20, 0x00, 0x04, 0x08, 0x06,\n\t\t\t\t\t\t0x07, 0x08, 0x00, 0x04, 0x05, 0x02, 0x03, 0x04,\n\t\t\t\t\t},\n\t\t\t\t}, p)\n\t\t\t\tclose(recv)\n\t\t\t})\n\n\t\t\t_, err = reader.Play(nil)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsubStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{\n\t\t\t\tNTP: time.Time{},\n\t\t\t\tPayload: unit.PayloadH264{\n\t\t\t\t\t{5, 2, 3, 4}, // IDR\n\t\t\t\t},\n\t\t\t})\n\n\t\t\t<-recv\n\n\t\t\tlist, err := s.APISessionsList()\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, &defs.APIRTSPSessionList{\n\t\t\t\tItems: []defs.APIRTSPSession{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:                 list.Items[0].ID,\n\t\t\t\t\t\tCreated:            list.Items[0].Created,\n\t\t\t\t\t\tRemoteAddr:         list.Items[0].RemoteAddr,\n\t\t\t\t\t\tState:              \"read\",\n\t\t\t\t\t\tPath:               \"teststream\",\n\t\t\t\t\t\tQuery:              \"param=value\",\n\t\t\t\t\t\tUser:               \"myuser\",\n\t\t\t\t\t\tInboundBytes:       list.Items[0].InboundBytes,\n\t\t\t\t\t\tInboundRTPPackets:  list.Items[0].InboundRTPPackets,\n\t\t\t\t\t\tOutboundBytes:      list.Items[0].OutboundBytes,\n\t\t\t\t\t\tOutboundRTPPackets: list.Items[0].OutboundRTPPackets,\n\t\t\t\t\t\tBytesReceived:      list.Items[0].BytesReceived,\n\t\t\t\t\t\tBytesSent:          list.Items[0].BytesSent,\n\t\t\t\t\t\tConns:              list.Items[0].Conns,\n\t\t\t\t\t\tRTPPacketsReceived: list.Items[0].RTPPacketsReceived,\n\t\t\t\t\t\tRTPPacketsSent:     list.Items[0].RTPPacketsSent,\n\t\t\t\t\t\tTransport:          ptrOf(\"TCP\"),\n\t\t\t\t\t\tProfile:            ptrOf(\"AVP\"),\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}, list)\n\t\t})\n\t}\n}\n\nfunc TestServerRedirect(t *testing.T) {\n\tfor _, ca := range []string{\"relative\", \"absolute\"} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tdesc := &description.Session{Medias: []*description.Media{test.MediaH264}}\n\n\t\t\tstrm := &stream.Stream{\n\t\t\t\tDesc:              desc,\n\t\t\t\tWriteQueueSize:    512,\n\t\t\t\tRTPMaxPayloadSize: 1450,\n\t\t\t\tParent:            test.NilLogger,\n\t\t\t}\n\t\t\terr := strm.Initialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsubStream := &stream.SubStream{\n\t\t\t\tStream:        strm,\n\t\t\t\tUseRTPPackets: true,\n\t\t\t}\n\t\t\terr = subStream.Initialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tpathManager := &test.PathManager{\n\t\t\t\tDescribeImpl: func(req defs.PathDescribeReq) defs.PathDescribeRes {\n\t\t\t\t\tif req.AccessRequest.Name == \"path1\" {\n\t\t\t\t\t\tif ca == \"relative\" {\n\t\t\t\t\t\t\treturn defs.PathDescribeRes{\n\t\t\t\t\t\t\t\tRedirect: \"/path2\",\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn defs.PathDescribeRes{\n\t\t\t\t\t\t\tRedirect: \"rtsp://localhost:8557/path2\",\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\tif req.AccessRequest.Credentials.User == \"\" && req.AccessRequest.Credentials.Pass == \"\" {\n\t\t\t\t\t\treturn defs.PathDescribeRes{Err: &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf(\"auth error\")}}\n\t\t\t\t\t}\n\n\t\t\t\t\trequire.Equal(t, \"path2\", req.AccessRequest.Name)\n\t\t\t\t\trequire.Equal(t, \"\", req.AccessRequest.Query)\n\t\t\t\t\trequire.Equal(t, \"myuser\", req.AccessRequest.Credentials.User)\n\t\t\t\t\trequire.Equal(t, \"mypass\", req.AccessRequest.Credentials.Pass)\n\n\t\t\t\t\treturn defs.PathDescribeRes{\n\t\t\t\t\t\tPath:   &dummyPath{},\n\t\t\t\t\t\tStream: strm,\n\t\t\t\t\t}\n\t\t\t\t},\n\t\t\t}\n\n\t\t\ts := &Server{\n\t\t\t\tAddress:        \"127.0.0.1:8557\",\n\t\t\t\tAuthMethods:    []rtspauth.VerifyMethod{rtspauth.VerifyMethodBasic},\n\t\t\t\tReadTimeout:    conf.Duration(10 * time.Second),\n\t\t\t\tWriteTimeout:   conf.Duration(10 * time.Second),\n\t\t\t\tWriteQueueSize: 512,\n\t\t\t\tTransports:     conf.RTSPTransports{gortsplib.ProtocolTCP: {}},\n\t\t\t\tPathManager:    pathManager,\n\t\t\t\tParent:         test.NilLogger,\n\t\t\t}\n\t\t\terr = s.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer s.Close()\n\n\t\t\tu, err := base.ParseURL(\"rtsp://myuser:mypass@127.0.0.1:8557/path1?param=value\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\treader := gortsplib.Client{\n\t\t\t\tScheme: u.Scheme,\n\t\t\t\tHost:   u.Host,\n\t\t\t}\n\n\t\t\terr = reader.Start()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer reader.Close()\n\n\t\t\tdesc2, _, err := reader.Describe(u)\n\t\t\trequire.NoError(t, err)\n\n\t\t\trequire.Equal(t, desc.Medias[0].Formats, desc2.Medias[0].Formats)\n\t\t})\n\t}\n}\n\nfunc TestAuthError(t *testing.T) {\n\tpathManager := &test.PathManager{\n\t\tDescribeImpl: func(req defs.PathDescribeReq) defs.PathDescribeRes {\n\t\t\tif req.AccessRequest.Credentials.User == \"\" && req.AccessRequest.Credentials.Pass == \"\" {\n\t\t\t\treturn defs.PathDescribeRes{Err: &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf(\"auth error\")}}\n\t\t\t}\n\n\t\t\treturn defs.PathDescribeRes{Err: &auth.Error{Wrapped: fmt.Errorf(\"auth error\")}}\n\t\t},\n\t}\n\n\tn := new(int64)\n\tdone := make(chan struct{})\n\n\ts := &Server{\n\t\tAddress:        \"127.0.0.1:8557\",\n\t\tReadTimeout:    conf.Duration(10 * time.Second),\n\t\tWriteTimeout:   conf.Duration(10 * time.Second),\n\t\tWriteQueueSize: 512,\n\t\tPathManager:    pathManager,\n\t\tParent: test.Logger(func(l logger.Level, s string, i ...any) {\n\t\t\tif l == logger.Info {\n\t\t\t\tif atomic.AddInt64(n, 1) == 3 {\n\t\t\t\t\trequire.Regexp(t, \"authentication failed: auth error$\", fmt.Sprintf(s, i...))\n\t\t\t\t\tclose(done)\n\t\t\t\t}\n\t\t\t}\n\t\t}),\n\t}\n\terr := s.Initialize()\n\trequire.NoError(t, err)\n\tdefer s.Close()\n\n\tu, err := base.ParseURL(\"rtsp://myuser:mypass@127.0.0.1:8557/teststream?param=value\")\n\trequire.NoError(t, err)\n\n\treader := gortsplib.Client{\n\t\tScheme: u.Scheme,\n\t\tHost:   u.Host,\n\t}\n\n\terr = reader.Start()\n\trequire.NoError(t, err)\n\tdefer reader.Close()\n\n\t_, _, err = reader.Describe(u)\n\trequire.EqualError(t, err, \"bad status code: 401 (Unauthorized)\")\n\n\t<-done\n}\n"
  },
  {
    "path": "internal/servers/rtsp/session.go",
    "content": "package rtsp\n\nimport (\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"slices\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5\"\n\trtspauth \"github.com/bluenviron/gortsplib/v5/pkg/auth\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/base\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/headers\"\n\t\"github.com/google/uuid\"\n\n\t\"github.com/bluenviron/mediamtx/internal/auth\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/counterdumper\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/errordumper\"\n\t\"github.com/bluenviron/mediamtx/internal/externalcmd\"\n\t\"github.com/bluenviron/mediamtx/internal/hooks\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/rtsp\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n)\n\nfunc profileLabel(p headers.TransportProfile) string {\n\tswitch p {\n\tcase headers.TransportProfileSAVP:\n\t\treturn \"SAVP\"\n\tcase headers.TransportProfileAVP:\n\t\treturn \"AVP\"\n\t}\n\treturn \"unknown\"\n}\n\nfunc findSingleMPEGTSFormat(desc *description.Session) (*description.Media, *format.MPEGTS) {\n\tif len(desc.Medias) != 1 || len(desc.Medias[0].Formats) != 1 {\n\t\treturn nil, nil\n\t}\n\n\tforma := desc.Medias[0].Formats[0]\n\tif forma, ok := forma.(*format.MPEGTS); ok {\n\t\treturn desc.Medias[0], forma\n\t}\n\n\treturn nil, nil\n}\n\ntype sessionParent interface {\n\tlogger.Writer\n\tgetConnByRConnUnsafe(rconn *gortsplib.ServerConn) *conn\n}\n\ntype session struct {\n\tisTLS           bool\n\ttransports      conf.RTSPTransports\n\trsession        *gortsplib.ServerSession\n\trconn           *gortsplib.ServerConn\n\trserver         *gortsplib.Server\n\texternalCmdPool *externalcmd.Pool\n\tpathManager     serverPathManager\n\tparent          sessionParent\n\n\tuuid                        uuid.UUID\n\tcreated                     time.Time\n\tpathConf                    *conf.Path // record only\n\tpath                        defs.Path\n\tstream                      *stream.Stream\n\tsubStream                   *stream.SubStream\n\tonUnreadHook                func()\n\tinboundRTPPacketsLost       *counterdumper.Dumper\n\tinboundRTPPacketsInError    *errordumper.Dumper\n\toutboundRTPPacketsDiscarded *counterdumper.Dumper\n\tmutex                       sync.RWMutex\n\tuser                        string\n\tmpegtsDemuxer               *mpegtsDemuxer\n}\n\nfunc (s *session) initialize() {\n\ts.uuid = uuid.New()\n\ts.created = time.Now()\n\n\ts.inboundRTPPacketsLost = &counterdumper.Dumper{\n\t\tOnReport: func(val uint64) {\n\t\t\ts.Log(logger.Warn, \"%d RTP %s lost\",\n\t\t\t\tval,\n\t\t\t\tfunc() string {\n\t\t\t\t\tif val == 1 {\n\t\t\t\t\t\treturn \"packet\"\n\t\t\t\t\t}\n\t\t\t\t\treturn \"packets\"\n\t\t\t\t}())\n\t\t},\n\t}\n\ts.inboundRTPPacketsLost.Start()\n\n\ts.inboundRTPPacketsInError = &errordumper.Dumper{\n\t\tOnReport: func(val uint64, last error) {\n\t\t\tif val == 1 {\n\t\t\t\ts.Log(logger.Warn, \"decode error: %v\", last)\n\t\t\t} else {\n\t\t\t\ts.Log(logger.Warn, \"%d decode errors, last was: %v\", val, last)\n\t\t\t}\n\t\t},\n\t}\n\ts.inboundRTPPacketsInError.Start()\n\n\ts.outboundRTPPacketsDiscarded = &counterdumper.Dumper{\n\t\tOnReport: func(val uint64) {\n\t\t\ts.Log(logger.Warn, \"reader is too slow, discarding %d %s\",\n\t\t\t\tval,\n\t\t\t\tfunc() string {\n\t\t\t\t\tif val == 1 {\n\t\t\t\t\t\treturn \"frame\"\n\t\t\t\t\t}\n\t\t\t\t\treturn \"frames\"\n\t\t\t\t}())\n\t\t},\n\t}\n\ts.outboundRTPPacketsDiscarded.Start()\n\n\ts.Log(logger.Info, \"created by %v\", s.rconn.NetConn().RemoteAddr())\n}\n\n// Close closes a Session.\n// this is not always called, so things that need to be released\n// must go in onClose().\nfunc (s *session) Close() {\n\ts.rsession.Close()\n}\n\nfunc (s *session) remoteAddr() net.Addr {\n\treturn s.rconn.NetConn().RemoteAddr()\n}\n\n// Log implements logger.Writer.\nfunc (s *session) Log(level logger.Level, format string, args ...any) {\n\tid := hex.EncodeToString(s.uuid[:4])\n\ts.parent.Log(level, \"[session %s] \"+format, append([]any{id}, args...)...)\n}\n\n// onClose is called by rtspServer.\nfunc (s *session) onClose(err error) {\n\tif s.rsession.State() == gortsplib.ServerSessionStatePlay {\n\t\ts.onUnreadHook()\n\t}\n\n\tif s.mpegtsDemuxer != nil {\n\t\ts.mpegtsDemuxer.close()\n\t}\n\n\tswitch s.rsession.State() {\n\tcase gortsplib.ServerSessionStatePrePlay, gortsplib.ServerSessionStatePlay:\n\t\ts.path.RemoveReader(defs.PathRemoveReaderReq{Author: s})\n\n\tcase gortsplib.ServerSessionStateRecord:\n\t\tif s.path != nil {\n\t\t\ts.path.RemovePublisher(defs.PathRemovePublisherReq{Author: s})\n\t\t}\n\t}\n\n\ts.path = nil\n\ts.stream = nil\n\ts.subStream = nil\n\n\ts.outboundRTPPacketsDiscarded.Stop()\n\ts.inboundRTPPacketsInError.Stop()\n\ts.inboundRTPPacketsLost.Stop()\n\n\ts.Log(logger.Info, \"destroyed: %v\", err)\n}\n\n// onAnnounce is called by rtspServer.\nfunc (s *session) onAnnounce(c *conn, ctx *gortsplib.ServerHandlerOnAnnounceCtx) (*base.Response, error) {\n\tif len(ctx.Path) == 0 || ctx.Path[0] != '/' {\n\t\treturn &base.Response{\n\t\t\tStatusCode: base.StatusBadRequest,\n\t\t}, fmt.Errorf(\"invalid path\")\n\t}\n\tctx.Path = ctx.Path[1:]\n\n\t// CustomVerifyFunc prevents hashed credentials from working.\n\t// Use it only when strictly needed.\n\tvar customVerifyFunc func(expectedUser, expectedPass string) bool\n\tif slices.Contains(c.authMethods, rtspauth.VerifyMethodDigestMD5) {\n\t\tcustomVerifyFunc = func(expectedUser, expectedPass string) bool {\n\t\t\treturn c.rconn.VerifyCredentials(ctx.Request, expectedUser, expectedPass)\n\t\t}\n\t}\n\n\tres, err := s.pathManager.FindPathConf(defs.PathFindPathConfReq{\n\t\tAccessRequest: defs.PathAccessRequest{\n\t\t\tName:             ctx.Path,\n\t\t\tQuery:            ctx.Query,\n\t\t\tPublish:          true,\n\t\t\tProto:            auth.ProtocolRTSP,\n\t\t\tID:               &c.uuid,\n\t\t\tCredentials:      rtsp.Credentials(ctx.Request),\n\t\t\tIP:               c.ip(),\n\t\t\tCustomVerifyFunc: customVerifyFunc,\n\t\t},\n\t})\n\tif err != nil {\n\t\tvar terr *auth.Error\n\t\tif errors.As(err, &terr) {\n\t\t\treturn c.handleAuthError(terr)\n\t\t}\n\n\t\treturn &base.Response{\n\t\t\tStatusCode: base.StatusBadRequest,\n\t\t}, err\n\t}\n\n\ts.pathConf = res.Conf\n\n\ts.mutex.Lock()\n\ts.user = res.User\n\ts.mutex.Unlock()\n\n\treturn &base.Response{\n\t\tStatusCode: base.StatusOK,\n\t}, nil\n}\n\nfunc (s *session) rtspStream() *gortsplib.ServerStream {\n\tif !s.isTLS {\n\t\treturn s.stream.RTSPStream(s.rserver)\n\t}\n\treturn s.stream.RTSPSStream(s.rserver)\n}\n\n// onSetup is called by rtspServer.\nfunc (s *session) onSetup(c *conn, ctx *gortsplib.ServerHandlerOnSetupCtx,\n) (*base.Response, *gortsplib.ServerStream, error) {\n\tif len(ctx.Path) == 0 || ctx.Path[0] != '/' {\n\t\treturn &base.Response{\n\t\t\tStatusCode: base.StatusBadRequest,\n\t\t}, nil, fmt.Errorf(\"invalid path\")\n\t}\n\tctx.Path = ctx.Path[1:]\n\n\t// in case the client is setupping a stream with UDP or UDP-multicast, and these\n\t// transport protocols are disabled, gortsplib already blocks the request.\n\t// we only have to handle the case in which the transport protocol is TCP\n\t// and it is disabled.\n\tif ctx.Transport.Protocol == gortsplib.ProtocolTCP {\n\t\tif _, ok := s.transports[gortsplib.ProtocolTCP]; !ok {\n\t\t\treturn &base.Response{\n\t\t\t\tStatusCode: base.StatusUnsupportedTransport,\n\t\t\t}, nil, nil\n\t\t}\n\t}\n\n\t// CustomVerifyFunc prevents hashed credentials from working.\n\t// Use it only when strictly needed.\n\tvar customVerifyFunc func(expectedUser, expectedPass string) bool\n\tif slices.Contains(c.authMethods, rtspauth.VerifyMethodDigestMD5) {\n\t\tcustomVerifyFunc = func(expectedUser, expectedPass string) bool {\n\t\t\treturn c.rconn.VerifyCredentials(ctx.Request, expectedUser, expectedPass)\n\t\t}\n\t}\n\n\tswitch s.rsession.State() {\n\tcase gortsplib.ServerSessionStateInitial: // play\n\t\tres, err := s.pathManager.AddReader(defs.PathAddReaderReq{\n\t\t\tAuthor: s,\n\t\t\tAccessRequest: defs.PathAccessRequest{\n\t\t\t\tName:             ctx.Path,\n\t\t\t\tQuery:            ctx.Query,\n\t\t\t\tProto:            auth.ProtocolRTSP,\n\t\t\t\tID:               &c.uuid,\n\t\t\t\tCredentials:      rtsp.Credentials(ctx.Request),\n\t\t\t\tIP:               c.ip(),\n\t\t\t\tCustomVerifyFunc: customVerifyFunc,\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\tvar terr *auth.Error\n\t\t\tif errors.As(err, &terr) {\n\t\t\t\tres, err2 := c.handleAuthError(terr)\n\t\t\t\treturn res, nil, err2\n\t\t\t}\n\n\t\t\tvar terr2 defs.PathNoStreamAvailableError\n\t\t\tif errors.As(err, &terr2) {\n\t\t\t\treturn &base.Response{\n\t\t\t\t\tStatusCode: base.StatusNotFound,\n\t\t\t\t}, nil, err\n\t\t\t}\n\n\t\t\treturn &base.Response{\n\t\t\t\tStatusCode: base.StatusBadRequest,\n\t\t\t}, nil, err\n\t\t}\n\n\t\ts.path = res.Path\n\t\ts.stream = res.Stream\n\n\t\ts.mutex.Lock()\n\t\ts.user = res.User\n\t\ts.mutex.Unlock()\n\n\t\treturn &base.Response{\n\t\t\tStatusCode: base.StatusOK,\n\t\t}, s.rtspStream(), nil\n\n\tcase gortsplib.ServerSessionStatePrePlay: // play, subsequent calls\n\t\treturn &base.Response{\n\t\t\tStatusCode: base.StatusOK,\n\t\t}, s.rtspStream(), nil\n\n\tdefault: // record\n\t\treturn &base.Response{\n\t\t\tStatusCode: base.StatusOK,\n\t\t}, nil, nil\n\t}\n}\n\n// onPlay is called by rtspServer.\nfunc (s *session) onPlay(_ *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) {\n\th := make(base.Header)\n\n\tif s.rsession.State() == gortsplib.ServerSessionStatePrePlay {\n\t\ts.Log(logger.Info, \"is reading from path '%s', with %s, %s\",\n\t\t\ts.path.Name(),\n\t\t\ts.rsession.Transport().Protocol,\n\t\t\tdefs.MediasInfo(s.rsession.Medias()))\n\n\t\ts.onUnreadHook = hooks.OnRead(hooks.OnReadParams{\n\t\t\tLogger:          s,\n\t\t\tExternalCmdPool: s.externalCmdPool,\n\t\t\tConf:            s.path.SafeConf(),\n\t\t\tExternalCmdEnv:  s.path.ExternalCmdEnv(),\n\t\t\tReader:          *s.APIReaderDescribe(),\n\t\t\tQuery:           s.rsession.Query(),\n\t\t})\n\t}\n\n\treturn &base.Response{\n\t\tStatusCode: base.StatusOK,\n\t\tHeader:     h,\n\t}, nil\n}\n\n// onRecord is called by rtspServer.\nfunc (s *session) onRecord(_ *gortsplib.ServerHandlerOnRecordCtx) (*base.Response, error) {\n\tif s.pathConf.RTSPDemuxMpegts {\n\t\tmpegtsMedia, mpegtsFormat := findSingleMPEGTSFormat(s.rsession.AnnouncedDescription())\n\t\tif mpegtsFormat != nil {\n\t\t\ts.Log(logger.Info, \"MPEG-TS demux mode enabled, starting demuxer...\")\n\n\t\t\ts.mpegtsDemuxer = &mpegtsDemuxer{\n\t\t\t\tsession:      s,\n\t\t\t\tpathManager:  s.pathManager,\n\t\t\t\tpathConf:     s.pathConf,\n\t\t\t\tmpegtsMedia:  mpegtsMedia,\n\t\t\t\tmpegtsFormat: mpegtsFormat,\n\t\t\t\tdecodeErrors: s.inboundRTPPacketsInError,\n\t\t\t\tpathName:     s.rsession.Path()[1:],\n\t\t\t\tquery:        s.rsession.Query(),\n\t\t\t}\n\t\t\terr := s.mpegtsDemuxer.initialize()\n\t\t\tif err != nil {\n\t\t\t\treturn &base.Response{\n\t\t\t\t\tStatusCode: base.StatusInternalServerError,\n\t\t\t\t}, err\n\t\t\t}\n\n\t\t\treturn &base.Response{\n\t\t\t\tStatusCode: base.StatusOK,\n\t\t\t}, nil\n\t\t}\n\t}\n\n\tres, err := s.pathManager.AddPublisher(defs.PathAddPublisherReq{\n\t\tAuthor:        s,\n\t\tDesc:          s.rsession.AnnouncedDescription(),\n\t\tUseRTPPackets: true,\n\t\tReplaceNTP:    !s.pathConf.UseAbsoluteTimestamp,\n\t\tConfToCompare: s.pathConf,\n\t\tAccessRequest: defs.PathAccessRequest{\n\t\t\tName:     s.rsession.Path()[1:],\n\t\t\tQuery:    s.rsession.Query(),\n\t\t\tPublish:  true,\n\t\t\tSkipAuth: true,\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn &base.Response{\n\t\t\tStatusCode: base.StatusBadRequest,\n\t\t}, err\n\t}\n\n\trtsp.ToStream(\n\t\ts.rsession,\n\t\ts.rsession.AnnouncedDescription().Medias,\n\t\tres.Path.SafeConf(),\n\t\t&s.subStream,\n\t\ts)\n\n\ts.path = res.Path\n\ts.subStream = res.SubStream\n\n\treturn &base.Response{\n\t\tStatusCode: base.StatusOK,\n\t}, nil\n}\n\n// onPause is called by rtspServer.\nfunc (s *session) onPause(_ *gortsplib.ServerHandlerOnPauseCtx) (*base.Response, error) {\n\t// we can't close mpegtsDemuxer during pause because OnPacketRTP() is paused after onPause(),\n\t// therefore a call to pipeWriter.CloseWithError() would cause a race condition.\n\tif s.mpegtsDemuxer != nil {\n\t\treturn &base.Response{\n\t\t\tStatusCode: base.StatusBadRequest,\n\t\t}, fmt.Errorf(\"cannot pause in MPEG-TS demux mode\")\n\t}\n\n\tswitch s.rsession.State() {\n\tcase gortsplib.ServerSessionStatePlay:\n\t\ts.onUnreadHook()\n\n\tcase gortsplib.ServerSessionStateRecord:\n\t\ts.path.RemovePublisher(defs.PathRemovePublisherReq{Author: s})\n\t}\n\n\treturn &base.Response{\n\t\tStatusCode: base.StatusOK,\n\t}, nil\n}\n\n// APIReaderDescribe implements reader.\nfunc (s *session) APIReaderDescribe() *defs.APIPathReader {\n\treturn &defs.APIPathReader{\n\t\tType: func() defs.APIPathReaderType {\n\t\t\tif s.isTLS {\n\t\t\t\treturn defs.APIPathReaderTypeRTSPSSession\n\t\t\t}\n\t\t\treturn defs.APIPathReaderTypeRTSPSession\n\t\t}(),\n\t\tID: s.uuid.String(),\n\t}\n}\n\n// APISourceDescribe implements source.\nfunc (s *session) APISourceDescribe() *defs.APIPathSource {\n\treturn &defs.APIPathSource{\n\t\tType: func() defs.APIPathSourceType {\n\t\t\tif s.isTLS {\n\t\t\t\treturn defs.APIPathSourceTypeRTSPSSession\n\t\t\t}\n\t\t\treturn defs.APIPathSourceTypeRTSPSession\n\t\t}(),\n\t\tID: s.uuid.String(),\n\t}\n}\n\n// onPacketLost is called by rtspServer.\nfunc (s *session) onPacketsLost(ctx *gortsplib.ServerHandlerOnPacketsLostCtx) {\n\ts.inboundRTPPacketsLost.Add(ctx.Lost)\n}\n\n// onDecodeError is called by rtspServer.\nfunc (s *session) onDecodeError(ctx *gortsplib.ServerHandlerOnDecodeErrorCtx) {\n\ts.inboundRTPPacketsInError.Add(ctx.Error)\n}\n\n// onStreamWriteError is called by rtspServer.\nfunc (s *session) onStreamWriteError(_ *gortsplib.ServerHandlerOnStreamWriteErrorCtx) {\n\t// currently the only error returned by OnStreamWriteError is ErrServerWriteQueueFull\n\ts.outboundRTPPacketsDiscarded.Increase()\n}\n\nfunc (s *session) apiItem() *defs.APIRTSPSession {\n\tstats := s.rsession.Stats()\n\n\ts.mutex.RLock()\n\tdefer s.mutex.RUnlock()\n\n\treturn &defs.APIRTSPSession{\n\t\tID:         s.uuid,\n\t\tCreated:    s.created,\n\t\tRemoteAddr: s.remoteAddr().String(),\n\t\tState: func() defs.APIRTSPSessionState {\n\t\t\tstate := s.rsession.State()\n\t\t\tswitch state {\n\t\t\tcase gortsplib.ServerSessionStatePlay:\n\t\t\t\treturn defs.APIRTSPSessionStateRead\n\n\t\t\tcase gortsplib.ServerSessionStateRecord:\n\t\t\t\treturn defs.APIRTSPSessionStatePublish\n\n\t\t\tdefault:\n\t\t\t\treturn defs.APIRTSPSessionStateIdle\n\t\t\t}\n\t\t}(),\n\t\tPath: func() string {\n\t\t\tpa := s.rsession.Path()\n\t\t\tif len(pa) >= 1 {\n\t\t\t\treturn pa[1:]\n\t\t\t}\n\t\t\treturn \"\"\n\t\t}(),\n\t\tQuery: s.rsession.Query(),\n\t\tUser:  s.user,\n\t\tTransport: func() *string {\n\t\t\ttransport := s.rsession.Transport()\n\t\t\tif transport == nil {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tv := transport.Protocol.String()\n\t\t\treturn &v\n\t\t}(),\n\t\tProfile: func() *string {\n\t\t\ttransport := s.rsession.Transport()\n\t\t\tif transport == nil {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t\tv := profileLabel(transport.Profile)\n\t\t\treturn &v\n\t\t}(),\n\t\tConns: func() []uuid.UUID {\n\t\t\tret := []uuid.UUID{}\n\n\t\t\tfor _, rconn := range s.rsession.Conns() {\n\t\t\t\tconn := s.parent.getConnByRConnUnsafe(rconn)\n\t\t\t\tif conn != nil {\n\t\t\t\t\tret = append(ret, conn.uuid)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\treturn ret\n\t\t}(),\n\t\tInboundBytes:                   stats.InboundBytes,\n\t\tInboundRTPPackets:              stats.InboundRTPPackets,\n\t\tInboundRTPPacketsLost:          stats.InboundRTPPacketsLost,\n\t\tInboundRTPPacketsInError:       stats.InboundRTPPacketsInError,\n\t\tInboundRTPPacketsJitter:        stats.InboundRTPPacketsJitter,\n\t\tInboundRTCPPackets:             stats.InboundRTCPPackets,\n\t\tInboundRTCPPacketsInError:      stats.InboundRTCPPacketsInError,\n\t\tOutboundBytes:                  stats.OutboundBytes,\n\t\tOutboundRTPPackets:             stats.OutboundRTPPackets,\n\t\tOutboundRTPPacketsReportedLost: stats.OutboundRTPPacketsReportedLost,\n\t\tOutboundRTPPacketsDiscarded:    s.outboundRTPPacketsDiscarded.Get(),\n\t\tOutboundRTCPPackets:            stats.OutboundRTCPPackets,\n\t\tBytesReceived:                  stats.InboundBytes,\n\t\tBytesSent:                      stats.OutboundBytes,\n\t\tRTPPacketsReceived:             stats.InboundRTPPackets,\n\t\tRTPPacketsSent:                 stats.OutboundRTPPackets,\n\t\tRTPPacketsLost:                 stats.InboundRTPPacketsLost,\n\t\tRTPPacketsInError:              stats.InboundRTPPacketsInError,\n\t\tRTPPacketsJitter:               stats.InboundRTPPacketsJitter,\n\t\tRTCPPacketsReceived:            stats.InboundRTCPPackets,\n\t\tRTCPPacketsSent:                stats.OutboundRTCPPackets,\n\t\tRTCPPacketsInError:             stats.InboundRTCPPacketsInError,\n\t}\n}\n"
  },
  {
    "path": "internal/servers/srt/conn.go",
    "content": "package srt\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\tsrt \"github.com/datarhei/gosrt\"\n\t\"github.com/google/uuid\"\n\n\t\"github.com/bluenviron/mediamtx/internal/auth\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/errordumper\"\n\t\"github.com/bluenviron/mediamtx/internal/externalcmd\"\n\t\"github.com/bluenviron/mediamtx/internal/hooks\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/mpegts\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n)\n\nfunc srtCheckPassphrase(connReq srt.ConnRequest, passphrase string) error {\n\tif passphrase == \"\" {\n\t\treturn nil\n\t}\n\n\tif !connReq.IsEncrypted() {\n\t\treturn fmt.Errorf(\"connection is encrypted, but not passphrase is defined in configuration\")\n\t}\n\n\terr := connReq.SetPassphrase(passphrase)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"invalid passphrase\")\n\t}\n\n\treturn nil\n}\n\ntype conn struct {\n\tparentCtx           context.Context\n\trtspAddress         string\n\treadTimeout         conf.Duration\n\twriteTimeout        conf.Duration\n\tudpMaxPayloadSize   int\n\tconnReq             srt.ConnRequest\n\trunOnConnect        string\n\trunOnConnectRestart bool\n\trunOnDisconnect     string\n\twg                  *sync.WaitGroup\n\texternalCmdPool     *externalcmd.Pool\n\tpathManager         serverPathManager\n\tparent              *Server\n\n\tctx       context.Context\n\tctxCancel func()\n\tcreated   time.Time\n\tuuid      uuid.UUID\n\tmutex     sync.RWMutex\n\tstate     defs.APISRTConnState\n\tpathName  string\n\tquery     string\n\tuser      string\n\tsconn     srt.Conn\n\treader    *stream.Reader\n}\n\nfunc (c *conn) initialize() {\n\tc.ctx, c.ctxCancel = context.WithCancel(c.parentCtx)\n\n\tc.created = time.Now()\n\tc.uuid = uuid.New()\n\tc.state = defs.APISRTConnStateIdle\n\n\tc.Log(logger.Info, \"opened\")\n\n\tc.wg.Add(1)\n\tgo c.run()\n}\n\nfunc (c *conn) Close() {\n\tc.ctxCancel()\n}\n\n// Log implements logger.Writer.\nfunc (c *conn) Log(level logger.Level, format string, args ...any) {\n\tc.parent.Log(level, \"[conn %v] \"+format, append([]any{c.connReq.RemoteAddr()}, args...)...)\n}\n\nfunc (c *conn) ip() net.IP {\n\treturn c.connReq.RemoteAddr().(*net.UDPAddr).IP\n}\n\nfunc (c *conn) run() { //nolint:dupl\n\tdefer c.wg.Done()\n\n\tonDisconnectHook := hooks.OnConnect(hooks.OnConnectParams{\n\t\tLogger:              c,\n\t\tExternalCmdPool:     c.externalCmdPool,\n\t\tRunOnConnect:        c.runOnConnect,\n\t\tRunOnConnectRestart: c.runOnConnectRestart,\n\t\tRunOnDisconnect:     c.runOnDisconnect,\n\t\tRTSPAddress:         c.rtspAddress,\n\t\tDesc:                *c.APIReaderDescribe(),\n\t})\n\tdefer onDisconnectHook()\n\n\terr := c.runInner()\n\n\tc.ctxCancel()\n\n\tc.parent.closeConn(c)\n\n\tc.Log(logger.Info, \"closed: %v\", err)\n}\n\nfunc (c *conn) runInner() error {\n\tvar streamID streamID\n\terr := streamID.unmarshal(c.connReq.StreamId())\n\tif err != nil {\n\t\tc.connReq.Reject(srt.REJ_PEER)\n\t\treturn fmt.Errorf(\"invalid stream ID '%s': %w\", c.connReq.StreamId(), err)\n\t}\n\n\tif streamID.mode == streamIDModePublish {\n\t\treturn c.runPublish(&streamID)\n\t}\n\treturn c.runRead(&streamID)\n}\n\nfunc (c *conn) runPublish(streamID *streamID) error {\n\tres, err := c.pathManager.FindPathConf(defs.PathFindPathConfReq{\n\t\tAccessRequest: defs.PathAccessRequest{\n\t\t\tName:    streamID.path,\n\t\t\tQuery:   streamID.query,\n\t\t\tPublish: true,\n\t\t\tProto:   auth.ProtocolSRT,\n\t\t\tID:      &c.uuid,\n\t\t\tCredentials: &auth.Credentials{\n\t\t\t\tUser: streamID.user,\n\t\t\t\tPass: streamID.pass,\n\t\t\t},\n\t\t\tIP: c.ip(),\n\t\t},\n\t})\n\tif err != nil {\n\t\tvar terr *auth.Error\n\t\tif errors.As(err, &terr) {\n\t\t\t// wait some seconds to delay brute force attacks\n\t\t\t<-time.After(auth.PauseAfterError)\n\t\t\tc.connReq.Reject(srt.REJ_PEER)\n\t\t\treturn terr\n\t\t}\n\t\tc.connReq.Reject(srt.REJ_PEER)\n\t\treturn err\n\t}\n\n\tc.mutex.Lock()\n\tc.user = res.User\n\tc.mutex.Unlock()\n\n\terr = srtCheckPassphrase(c.connReq, res.Conf.SRTPublishPassphrase)\n\tif err != nil {\n\t\tc.connReq.Reject(srt.REJ_PEER)\n\t\treturn err\n\t}\n\n\tsconn, err := c.connReq.Accept()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treaderErr := make(chan error)\n\tgo func() {\n\t\treaderErr <- c.runPublishReader(sconn, streamID, res.Conf)\n\t}()\n\n\tselect {\n\tcase err = <-readerErr:\n\t\tsconn.Close()\n\t\treturn err\n\n\tcase <-c.ctx.Done():\n\t\tsconn.Close()\n\t\t<-readerErr\n\t\treturn errors.New(\"terminated\")\n\t}\n}\n\nfunc (c *conn) runPublishReader(sconn srt.Conn, streamID *streamID, pathConf *conf.Path) error {\n\tsconn.SetReadDeadline(time.Now().Add(time.Duration(c.readTimeout)))\n\tr := &mpegts.EnhancedReader{R: sconn}\n\terr := r.Initialize()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdecodeErrors := &errordumper.Dumper{\n\t\tOnReport: func(val uint64, last error) {\n\t\t\tif val == 1 {\n\t\t\t\tc.Log(logger.Warn, \"decode error: %v\", last)\n\t\t\t} else {\n\t\t\t\tc.Log(logger.Warn, \"%d decode errors, last was: %v\", val, last)\n\t\t\t}\n\t\t},\n\t}\n\n\tdecodeErrors.Start()\n\tdefer decodeErrors.Stop()\n\n\tr.OnDecodeError(func(err error) {\n\t\tdecodeErrors.Add(err)\n\t})\n\n\tvar subStream *stream.SubStream\n\n\tmedias, err := mpegts.ToStream(r, &subStream, c)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tres, err := c.pathManager.AddPublisher(defs.PathAddPublisherReq{\n\t\tAuthor:        c,\n\t\tDesc:          &description.Session{Medias: medias},\n\t\tUseRTPPackets: false,\n\t\tReplaceNTP:    true,\n\t\tConfToCompare: pathConf,\n\t\tAccessRequest: defs.PathAccessRequest{\n\t\t\tName:     streamID.path,\n\t\t\tQuery:    streamID.query,\n\t\t\tPublish:  true,\n\t\t\tSkipAuth: true,\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer res.Path.RemovePublisher(defs.PathRemovePublisherReq{Author: c})\n\n\tsubStream = res.SubStream\n\n\tc.mutex.Lock()\n\tc.state = defs.APISRTConnStatePublish\n\tc.pathName = streamID.path\n\tc.query = streamID.query\n\tc.sconn = sconn\n\tc.mutex.Unlock()\n\n\tfor {\n\t\terr = r.Read()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n}\n\nfunc (c *conn) runRead(streamID *streamID) error {\n\tres, err := c.pathManager.AddReader(defs.PathAddReaderReq{\n\t\tAuthor: c,\n\t\tAccessRequest: defs.PathAccessRequest{\n\t\t\tName:  streamID.path,\n\t\t\tQuery: streamID.query,\n\t\t\tProto: auth.ProtocolSRT,\n\t\t\tID:    &c.uuid,\n\t\t\tCredentials: &auth.Credentials{\n\t\t\t\tUser: streamID.user,\n\t\t\t\tPass: streamID.pass,\n\t\t\t},\n\t\t\tIP: c.ip(),\n\t\t},\n\t})\n\tif err != nil {\n\t\tvar terr *auth.Error\n\t\tif errors.As(err, &terr) {\n\t\t\t// wait some seconds to delay brute force attacks\n\t\t\t<-time.After(auth.PauseAfterError)\n\t\t\tc.connReq.Reject(srt.REJ_PEER)\n\t\t\treturn terr\n\t\t}\n\t\tc.connReq.Reject(srt.REJ_PEER)\n\t\treturn err\n\t}\n\n\tdefer res.Path.RemoveReader(defs.PathRemoveReaderReq{Author: c})\n\n\terr = srtCheckPassphrase(c.connReq, res.Path.SafeConf().SRTReadPassphrase)\n\tif err != nil {\n\t\tc.connReq.Reject(srt.REJ_PEER)\n\t\treturn err\n\t}\n\n\tsconn, err := c.connReq.Accept()\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer sconn.Close()\n\n\tbw := bufio.NewWriterSize(sconn, srtMaxPayloadSize(c.udpMaxPayloadSize))\n\n\tr := &stream.Reader{Parent: c}\n\n\terr = mpegts.FromStream(res.Stream.Desc, r, bw, sconn, time.Duration(c.writeTimeout))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tc.mutex.Lock()\n\tc.state = defs.APISRTConnStateRead\n\tc.pathName = streamID.path\n\tc.query = streamID.query\n\tc.user = res.User\n\tc.sconn = sconn\n\tc.mutex.Unlock()\n\n\tc.Log(logger.Info, \"is reading from path '%s', %s\",\n\t\tres.Path.Name(), defs.FormatsInfo(r.Formats()))\n\n\tonUnreadHook := hooks.OnRead(hooks.OnReadParams{\n\t\tLogger:          c,\n\t\tExternalCmdPool: c.externalCmdPool,\n\t\tConf:            res.Path.SafeConf(),\n\t\tExternalCmdEnv:  res.Path.ExternalCmdEnv(),\n\t\tReader:          *c.APIReaderDescribe(),\n\t\tQuery:           streamID.query,\n\t})\n\tdefer onUnreadHook()\n\n\t// disable read deadline\n\tsconn.SetReadDeadline(time.Time{})\n\n\tres.Stream.AddReader(r)\n\tdefer res.Stream.RemoveReader(r)\n\n\tc.mutex.Lock()\n\tc.reader = r\n\tc.mutex.Unlock()\n\n\tselect {\n\tcase <-c.ctx.Done():\n\t\treturn fmt.Errorf(\"terminated\")\n\n\tcase err = <-r.Error():\n\t\treturn err\n\t}\n}\n\n// APIReaderDescribe implements reader.\nfunc (c *conn) APIReaderDescribe() *defs.APIPathReader {\n\treturn &defs.APIPathReader{\n\t\tType: defs.APIPathReaderTypeSRTConn,\n\t\tID:   c.uuid.String(),\n\t}\n}\n\n// APISourceDescribe implements source.\nfunc (c *conn) APISourceDescribe() *defs.APIPathSource {\n\treturn &defs.APIPathSource{\n\t\tType: defs.APIPathSourceTypeSRTConn,\n\t\tID:   c.uuid.String(),\n\t}\n}\n\nfunc (c *conn) apiItem() *defs.APISRTConn {\n\tc.mutex.RLock()\n\tdefer c.mutex.RUnlock()\n\n\titem := &defs.APISRTConn{\n\t\tID:         c.uuid,\n\t\tCreated:    c.created,\n\t\tRemoteAddr: c.connReq.RemoteAddr().String(),\n\t\tState:      c.state,\n\t\tPath:       c.pathName,\n\t\tQuery:      c.query,\n\t\tUser:       c.user,\n\t}\n\n\tif c.sconn != nil {\n\t\tvar s srt.Statistics\n\t\tc.sconn.Stats(&s)\n\n\t\titem.PacketsSent = s.Accumulated.PktSent\n\t\titem.PacketsReceived = s.Accumulated.PktRecv\n\t\titem.PacketsSentUnique = s.Accumulated.PktSentUnique\n\t\titem.PacketsReceivedUnique = s.Accumulated.PktRecvUnique\n\t\titem.PacketsSendLoss = s.Accumulated.PktSendLoss\n\t\titem.PacketsReceivedLoss = s.Accumulated.PktRecvLoss\n\t\titem.PacketsRetrans = s.Accumulated.PktRetrans\n\t\titem.PacketsReceivedRetrans = s.Accumulated.PktRecvRetrans\n\t\titem.PacketsSentACK = s.Accumulated.PktSentACK\n\t\titem.PacketsReceivedACK = s.Accumulated.PktRecvACK\n\t\titem.PacketsSentNAK = s.Accumulated.PktSentNAK\n\t\titem.PacketsReceivedNAK = s.Accumulated.PktRecvNAK\n\t\titem.PacketsSentKM = s.Accumulated.PktSentKM\n\t\titem.PacketsReceivedKM = s.Accumulated.PktRecvKM\n\t\titem.UsSndDuration = s.Accumulated.UsSndDuration\n\t\titem.PacketsReceivedBelated = s.Accumulated.PktRecvBelated\n\t\titem.PacketsSendDrop = s.Accumulated.PktSendDrop\n\t\titem.PacketsReceivedDrop = s.Accumulated.PktRecvDrop\n\t\titem.PacketsReceivedUndecrypt = s.Accumulated.PktRecvUndecrypt\n\t\titem.BytesSent = s.Accumulated.ByteSent\n\t\titem.BytesReceived = s.Accumulated.ByteRecv\n\t\titem.BytesSentUnique = s.Accumulated.ByteSentUnique\n\t\titem.BytesReceivedUnique = s.Accumulated.ByteRecvUnique\n\t\titem.BytesReceivedLoss = s.Accumulated.ByteRecvLoss\n\t\titem.BytesRetrans = s.Accumulated.ByteRetrans\n\t\titem.BytesReceivedRetrans = s.Accumulated.ByteRecvRetrans\n\t\titem.BytesReceivedBelated = s.Accumulated.ByteRecvBelated\n\t\titem.BytesSendDrop = s.Accumulated.ByteSendDrop\n\t\titem.BytesReceivedDrop = s.Accumulated.ByteRecvDrop\n\t\titem.BytesReceivedUndecrypt = s.Accumulated.ByteRecvUndecrypt\n\t\titem.UsPacketsSendPeriod = s.Instantaneous.UsPktSendPeriod\n\t\titem.PacketsFlowWindow = s.Instantaneous.PktFlowWindow\n\t\titem.PacketsFlightSize = s.Instantaneous.PktFlightSize\n\t\titem.MsRTT = s.Instantaneous.MsRTT\n\t\titem.MbpsSendRate = s.Instantaneous.MbpsSentRate\n\t\titem.MbpsReceiveRate = s.Instantaneous.MbpsRecvRate\n\t\titem.MbpsLinkCapacity = s.Instantaneous.MbpsLinkCapacity\n\t\titem.BytesAvailSendBuf = s.Instantaneous.ByteAvailSendBuf\n\t\titem.BytesAvailReceiveBuf = s.Instantaneous.ByteAvailRecvBuf\n\t\titem.MbpsMaxBW = s.Instantaneous.MbpsMaxBW\n\t\titem.ByteMSS = s.Instantaneous.ByteMSS\n\t\titem.PacketsSendBuf = s.Instantaneous.PktSendBuf\n\t\titem.BytesSendBuf = s.Instantaneous.ByteSendBuf\n\t\titem.MsSendBuf = s.Instantaneous.MsSendBuf\n\t\titem.MsSendTsbPdDelay = s.Instantaneous.MsSendTsbPdDelay\n\t\titem.PacketsReceiveBuf = s.Instantaneous.PktRecvBuf\n\t\titem.BytesReceiveBuf = s.Instantaneous.ByteRecvBuf\n\t\titem.MsReceiveBuf = s.Instantaneous.MsRecvBuf\n\t\titem.MsReceiveTsbPdDelay = s.Instantaneous.MsRecvTsbPdDelay\n\t\titem.PacketsReorderTolerance = s.Instantaneous.PktReorderTolerance\n\t\titem.PacketsReceivedAvgBelatedTime = s.Instantaneous.PktRecvAvgBelatedTime\n\t\titem.PacketsSendLossRate = s.Instantaneous.PktSendLossRate\n\t\titem.PacketsReceivedLossRate = s.Instantaneous.PktRecvLossRate\n\t}\n\n\tif c.reader != nil {\n\t\titem.OutboundFramesDiscarded = c.reader.OutboundFramesDiscarded()\n\t}\n\n\treturn item\n}\n"
  },
  {
    "path": "internal/servers/srt/listener.go",
    "content": "package srt\n\nimport (\n\t\"sync\"\n\n\tsrt \"github.com/datarhei/gosrt\"\n)\n\ntype listener struct {\n\tln     srt.Listener\n\twg     *sync.WaitGroup\n\tparent *Server\n}\n\nfunc (l *listener) initialize() {\n\tl.wg.Add(1)\n\tgo l.run()\n}\n\nfunc (l *listener) run() {\n\tdefer l.wg.Done()\n\n\terr := l.runInner()\n\n\tl.parent.acceptError(err)\n}\n\nfunc (l *listener) runInner() error {\n\tfor {\n\t\treq, err := l.ln.Accept2()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tl.parent.newConnRequest(req)\n\t}\n}\n"
  },
  {
    "path": "internal/servers/srt/server.go",
    "content": "// Package srt contains a SRT server.\npackage srt\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"reflect\"\n\t\"sort\"\n\t\"sync\"\n\t\"time\"\n\n\tsrt \"github.com/datarhei/gosrt\"\n\t\"github.com/google/uuid\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/externalcmd\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n)\n\n// ErrConnNotFound is returned when a connection is not found.\nvar ErrConnNotFound = errors.New(\"connection not found\")\n\nfunc interfaceIsEmpty(i any) bool {\n\treturn reflect.ValueOf(i).Kind() != reflect.Pointer || reflect.ValueOf(i).IsNil()\n}\n\nfunc srtMaxPayloadSize(u int) int {\n\treturn ((u - 16) / 188) * 188 // 16 = SRT header, 188 = MPEG-TS packet\n}\n\ntype serverAPIConnsListRes struct {\n\tdata *defs.APISRTConnList\n\terr  error\n}\n\ntype serverAPIConnsListReq struct {\n\tres chan serverAPIConnsListRes\n}\n\ntype serverAPIConnsGetRes struct {\n\tdata *defs.APISRTConn\n\terr  error\n}\n\ntype serverAPIConnsGetReq struct {\n\tuuid uuid.UUID\n\tres  chan serverAPIConnsGetRes\n}\n\ntype serverAPIConnsKickRes struct {\n\terr error\n}\n\ntype serverAPIConnsKickReq struct {\n\tuuid uuid.UUID\n\tres  chan serverAPIConnsKickRes\n}\n\ntype serverMetrics interface {\n\tSetSRTServer(defs.APISRTServer)\n}\n\ntype serverPathManager interface {\n\tFindPathConf(req defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error)\n\tAddPublisher(req defs.PathAddPublisherReq) (*defs.PathAddPublisherRes, error)\n\tAddReader(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error)\n}\n\ntype serverParent interface {\n\tlogger.Writer\n}\n\n// Server is a SRT server.\ntype Server struct {\n\tAddress             string\n\tRTSPAddress         string\n\tReadTimeout         conf.Duration\n\tWriteTimeout        conf.Duration\n\tUDPMaxPayloadSize   int\n\tRunOnConnect        string\n\tRunOnConnectRestart bool\n\tRunOnDisconnect     string\n\tExternalCmdPool     *externalcmd.Pool\n\tMetrics             serverMetrics\n\tPathManager         serverPathManager\n\tParent              serverParent\n\n\tctx       context.Context\n\tctxCancel func()\n\twg        sync.WaitGroup\n\tln        srt.Listener\n\tconns     map[*conn]struct{}\n\n\t// in\n\tchNewConnRequest chan srt.ConnRequest\n\tchAcceptErr      chan error\n\tchCloseConn      chan *conn\n\tchAPIConnsList   chan serverAPIConnsListReq\n\tchAPIConnsGet    chan serverAPIConnsGetReq\n\tchAPIConnsKick   chan serverAPIConnsKickReq\n}\n\n// Initialize initializes the server.\nfunc (s *Server) Initialize() error {\n\tconf := srt.DefaultConfig()\n\tconf.ConnectionTimeout = time.Duration(s.ReadTimeout)\n\tconf.PeerIdleTimeout = time.Duration(s.ReadTimeout)\n\tconf.PayloadSize = uint32(srtMaxPayloadSize(s.UDPMaxPayloadSize))\n\n\tvar err error\n\ts.ln, err = srt.Listen(\"srt\", s.Address, conf)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ts.ctx, s.ctxCancel = context.WithCancel(context.Background())\n\n\ts.conns = make(map[*conn]struct{})\n\ts.chNewConnRequest = make(chan srt.ConnRequest)\n\ts.chAcceptErr = make(chan error)\n\ts.chCloseConn = make(chan *conn)\n\ts.chAPIConnsList = make(chan serverAPIConnsListReq)\n\ts.chAPIConnsGet = make(chan serverAPIConnsGetReq)\n\ts.chAPIConnsKick = make(chan serverAPIConnsKickReq)\n\n\ts.Log(logger.Info, \"listener opened on \"+s.Address+\" (UDP)\")\n\n\tl := &listener{\n\t\tln:     s.ln,\n\t\twg:     &s.wg,\n\t\tparent: s,\n\t}\n\tl.initialize()\n\n\ts.wg.Add(1)\n\tgo s.run()\n\n\tif !interfaceIsEmpty(s.Metrics) {\n\t\ts.Metrics.SetSRTServer(s)\n\t}\n\n\treturn nil\n}\n\n// Log implements logger.Writer.\nfunc (s *Server) Log(level logger.Level, format string, args ...any) {\n\ts.Parent.Log(level, \"[SRT] \"+format, args...)\n}\n\n// Close closes the server.\nfunc (s *Server) Close() {\n\ts.Log(logger.Info, \"listener is closing\")\n\n\tif !interfaceIsEmpty(s.Metrics) {\n\t\ts.Metrics.SetSRTServer(nil)\n\t}\n\n\ts.ctxCancel()\n\ts.wg.Wait()\n}\n\nfunc (s *Server) run() {\n\tdefer s.wg.Done()\n\nouter:\n\tfor {\n\t\tselect {\n\t\tcase err := <-s.chAcceptErr:\n\t\t\ts.Log(logger.Error, \"%s\", err)\n\t\t\tbreak outer\n\n\t\tcase req := <-s.chNewConnRequest:\n\t\t\tc := &conn{\n\t\t\t\tparentCtx:           s.ctx,\n\t\t\t\trtspAddress:         s.RTSPAddress,\n\t\t\t\treadTimeout:         s.ReadTimeout,\n\t\t\t\twriteTimeout:        s.WriteTimeout,\n\t\t\t\tudpMaxPayloadSize:   s.UDPMaxPayloadSize,\n\t\t\t\tconnReq:             req,\n\t\t\t\trunOnConnect:        s.RunOnConnect,\n\t\t\t\trunOnConnectRestart: s.RunOnConnectRestart,\n\t\t\t\trunOnDisconnect:     s.RunOnDisconnect,\n\t\t\t\twg:                  &s.wg,\n\t\t\t\texternalCmdPool:     s.ExternalCmdPool,\n\t\t\t\tpathManager:         s.PathManager,\n\t\t\t\tparent:              s,\n\t\t\t}\n\t\t\tc.initialize()\n\t\t\ts.conns[c] = struct{}{}\n\n\t\tcase c := <-s.chCloseConn:\n\t\t\tdelete(s.conns, c)\n\n\t\tcase req := <-s.chAPIConnsList:\n\t\t\tdata := &defs.APISRTConnList{\n\t\t\t\tItems: []defs.APISRTConn{},\n\t\t\t}\n\n\t\t\tfor c := range s.conns {\n\t\t\t\tdata.Items = append(data.Items, *c.apiItem())\n\t\t\t}\n\n\t\t\tsort.Slice(data.Items, func(i, j int) bool {\n\t\t\t\treturn data.Items[i].Created.Before(data.Items[j].Created)\n\t\t\t})\n\n\t\t\treq.res <- serverAPIConnsListRes{data: data}\n\n\t\tcase req := <-s.chAPIConnsGet:\n\t\t\tc := s.findConnByUUID(req.uuid)\n\t\t\tif c == nil {\n\t\t\t\treq.res <- serverAPIConnsGetRes{err: ErrConnNotFound}\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treq.res <- serverAPIConnsGetRes{data: c.apiItem()}\n\n\t\tcase req := <-s.chAPIConnsKick:\n\t\t\tc := s.findConnByUUID(req.uuid)\n\t\t\tif c == nil {\n\t\t\t\treq.res <- serverAPIConnsKickRes{err: ErrConnNotFound}\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdelete(s.conns, c)\n\t\t\tc.Close()\n\t\t\treq.res <- serverAPIConnsKickRes{}\n\n\t\tcase <-s.ctx.Done():\n\t\t\tbreak outer\n\t\t}\n\t}\n\n\ts.ctxCancel()\n\n\ts.ln.Close()\n}\n\nfunc (s *Server) findConnByUUID(uuid uuid.UUID) *conn {\n\tfor sx := range s.conns {\n\t\tif sx.uuid == uuid {\n\t\t\treturn sx\n\t\t}\n\t}\n\treturn nil\n}\n\n// newConnRequest is called by srtListener.\nfunc (s *Server) newConnRequest(connReq srt.ConnRequest) {\n\tselect {\n\tcase s.chNewConnRequest <- connReq:\n\tcase <-s.ctx.Done():\n\t\tconnReq.Reject(srt.REJ_CLOSE)\n\t}\n}\n\n// acceptError is called by srtListener.\nfunc (s *Server) acceptError(err error) {\n\tselect {\n\tcase s.chAcceptErr <- err:\n\tcase <-s.ctx.Done():\n\t}\n}\n\n// closeConn is called by conn.\nfunc (s *Server) closeConn(c *conn) {\n\tselect {\n\tcase s.chCloseConn <- c:\n\tcase <-s.ctx.Done():\n\t}\n}\n\n// APIConnsList is called by api.\nfunc (s *Server) APIConnsList() (*defs.APISRTConnList, error) {\n\treq := serverAPIConnsListReq{\n\t\tres: make(chan serverAPIConnsListRes),\n\t}\n\n\tselect {\n\tcase s.chAPIConnsList <- req:\n\t\tres := <-req.res\n\t\treturn res.data, res.err\n\n\tcase <-s.ctx.Done():\n\t\treturn nil, fmt.Errorf(\"terminated\")\n\t}\n}\n\n// APIConnsGet is called by api.\nfunc (s *Server) APIConnsGet(uuid uuid.UUID) (*defs.APISRTConn, error) {\n\treq := serverAPIConnsGetReq{\n\t\tuuid: uuid,\n\t\tres:  make(chan serverAPIConnsGetRes),\n\t}\n\n\tselect {\n\tcase s.chAPIConnsGet <- req:\n\t\tres := <-req.res\n\t\treturn res.data, res.err\n\n\tcase <-s.ctx.Done():\n\t\treturn nil, fmt.Errorf(\"terminated\")\n\t}\n}\n\n// APIConnsKick is called by api.\nfunc (s *Server) APIConnsKick(uuid uuid.UUID) error {\n\treq := serverAPIConnsKickReq{\n\t\tuuid: uuid,\n\t\tres:  make(chan serverAPIConnsKickRes),\n\t}\n\n\tselect {\n\tcase s.chAPIConnsKick <- req:\n\t\tres := <-req.res\n\t\treturn res.err\n\n\tcase <-s.ctx.Done():\n\t\treturn fmt.Errorf(\"terminated\")\n\t}\n}\n"
  },
  {
    "path": "internal/servers/srt/server_test.go",
    "content": "package srt\n\nimport (\n\t\"bufio\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts\"\n\ttscodecs \"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts/codecs\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/externalcmd\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n\tsrt \"github.com/datarhei/gosrt\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype dummyPath struct{}\n\nfunc (p *dummyPath) Name() string {\n\treturn \"teststream\"\n}\n\nfunc (p *dummyPath) SafeConf() *conf.Path {\n\treturn &conf.Path{}\n}\n\nfunc (p *dummyPath) ExternalCmdEnv() externalcmd.Environment {\n\treturn externalcmd.Environment{}\n}\n\nfunc (p *dummyPath) RemovePublisher(_ defs.PathRemovePublisherReq) {\n}\n\nfunc (p *dummyPath) RemoveReader(_ defs.PathRemoveReaderReq) {\n}\n\nfunc TestServerPublish(t *testing.T) {\n\texternalCmdPool := &externalcmd.Pool{}\n\texternalCmdPool.Initialize()\n\tdefer externalCmdPool.Close()\n\n\tvar strm *stream.Stream\n\tvar reader *stream.Reader\n\tdefer func() {\n\t\tstrm.RemoveReader(reader)\n\t}()\n\tdataReceived := make(chan struct{})\n\tdataReceived2 := make(chan struct{})\n\tn := 0\n\n\tpathManager := &test.PathManager{\n\t\tFindPathConfImpl: func(req defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error) {\n\t\t\trequire.Equal(t, \"teststream\", req.AccessRequest.Name)\n\t\t\trequire.Equal(t, \"param=value\", req.AccessRequest.Query)\n\t\t\trequire.Equal(t, \"myuser\", req.AccessRequest.Credentials.User)\n\t\t\trequire.Equal(t, \"mypass\", req.AccessRequest.Credentials.Pass)\n\t\t\treturn &defs.PathFindPathConfRes{Conf: &conf.Path{}, User: req.AccessRequest.Credentials.User}, nil\n\t\t},\n\t\tAddPublisherImpl: func(req defs.PathAddPublisherReq) (*defs.PathAddPublisherRes, error) {\n\t\t\trequire.Equal(t, \"teststream\", req.AccessRequest.Name)\n\t\t\trequire.Equal(t, \"param=value\", req.AccessRequest.Query)\n\t\t\trequire.True(t, req.AccessRequest.SkipAuth)\n\n\t\t\tstrm = &stream.Stream{\n\t\t\t\tDesc:              req.Desc,\n\t\t\t\tWriteQueueSize:    512,\n\t\t\t\tRTPMaxPayloadSize: 1450,\n\t\t\t\tParent:            test.NilLogger,\n\t\t\t}\n\t\t\terr := strm.Initialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsubStream := &stream.SubStream{\n\t\t\t\tStream:        strm,\n\t\t\t\tUseRTPPackets: false,\n\t\t\t}\n\t\t\terr = subStream.Initialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\treader = &stream.Reader{Parent: test.NilLogger}\n\n\t\t\treader.OnData(\n\t\t\t\tstrm.Desc.Medias[0],\n\t\t\t\tstrm.Desc.Medias[0].Formats[0],\n\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\tswitch n {\n\t\t\t\t\tcase 0:\n\t\t\t\t\t\trequire.Equal(t, unit.PayloadH264{\n\t\t\t\t\t\t\ttest.FormatH264.SPS,\n\t\t\t\t\t\t\ttest.FormatH264.PPS,\n\t\t\t\t\t\t\t{5, 1},\n\t\t\t\t\t\t}, u.Payload)\n\t\t\t\t\t\tclose(dataReceived)\n\n\t\t\t\t\tcase 1:\n\t\t\t\t\t\trequire.Equal(t, unit.PayloadH264{\n\t\t\t\t\t\t\ttest.FormatH264.SPS,\n\t\t\t\t\t\t\ttest.FormatH264.PPS,\n\t\t\t\t\t\t\t{5, 2},\n\t\t\t\t\t\t}, u.Payload)\n\t\t\t\t\t\tclose(dataReceived2)\n\n\t\t\t\t\tdefault:\n\t\t\t\t\t\tt.Errorf(\"should not happen\")\n\t\t\t\t\t}\n\t\t\t\t\tn++\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\n\t\t\tstrm.AddReader(reader)\n\n\t\t\treturn &defs.PathAddPublisherRes{Path: &dummyPath{}, SubStream: subStream}, nil\n\t\t},\n\t}\n\n\ts := &Server{\n\t\tAddress:             \"127.0.0.1:8890\",\n\t\tRTSPAddress:         \"\",\n\t\tReadTimeout:         conf.Duration(10 * time.Second),\n\t\tWriteTimeout:        conf.Duration(10 * time.Second),\n\t\tUDPMaxPayloadSize:   1472,\n\t\tRunOnConnect:        \"\",\n\t\tRunOnConnectRestart: false,\n\t\tRunOnDisconnect:     \"string\",\n\t\tExternalCmdPool:     externalCmdPool,\n\t\tPathManager:         pathManager,\n\t\tParent:              test.NilLogger,\n\t}\n\terr := s.Initialize()\n\trequire.NoError(t, err)\n\tdefer s.Close()\n\n\tu := \"srt://127.0.0.1:8890?streamid=publish:teststream:myuser:mypass:param=value\"\n\n\tsrtConf := srt.DefaultConfig()\n\taddress, err := srtConf.UnmarshalURL(u)\n\trequire.NoError(t, err)\n\n\terr = srtConf.Validate()\n\trequire.NoError(t, err)\n\n\tpublisher, err := srt.Dial(\"srt\", address, srtConf)\n\trequire.NoError(t, err)\n\n\ttrack := &mpegts.Track{\n\t\tCodec: &tscodecs.H264{},\n\t}\n\n\tbw := bufio.NewWriter(publisher)\n\tw := &mpegts.Writer{W: bw, Tracks: []*mpegts.Track{track}}\n\terr = w.Initialize()\n\trequire.NoError(t, err)\n\n\t// the MPEG-TS muxer needs two PES packets in order to write the first one\n\n\terr = w.WriteH264(track, 0, 0, [][]byte{\n\t\ttest.FormatH264.SPS,\n\t\ttest.FormatH264.PPS,\n\t\t{5, 1}, // IDR\n\t})\n\trequire.NoError(t, err)\n\n\terr = w.WriteH264(track, 0, 0, [][]byte{\n\t\t{5, 2}, // IDR\n\t})\n\trequire.NoError(t, err)\n\n\terr = bw.Flush()\n\trequire.NoError(t, err)\n\n\t<-dataReceived\n\n\tlist, err := s.APIConnsList()\n\trequire.NoError(t, err)\n\trequire.Equal(t, &defs.APISRTConnList{ //nolint:dupl\n\t\tItems: []defs.APISRTConn{\n\t\t\t{\n\t\t\t\tID:                            list.Items[0].ID,\n\t\t\t\tCreated:                       list.Items[0].Created,\n\t\t\t\tRemoteAddr:                    list.Items[0].RemoteAddr,\n\t\t\t\tState:                         \"publish\",\n\t\t\t\tPath:                          \"teststream\",\n\t\t\t\tQuery:                         \"param=value\",\n\t\t\t\tUser:                          \"myuser\",\n\t\t\t\tPacketsSent:                   list.Items[0].PacketsSent,\n\t\t\t\tPacketsReceived:               list.Items[0].PacketsReceived,\n\t\t\t\tPacketsSentUnique:             list.Items[0].PacketsSentUnique,\n\t\t\t\tPacketsReceivedUnique:         list.Items[0].PacketsReceivedUnique,\n\t\t\t\tPacketsSendLoss:               list.Items[0].PacketsSendLoss,\n\t\t\t\tPacketsReceivedLoss:           list.Items[0].PacketsReceivedLoss,\n\t\t\t\tPacketsRetrans:                list.Items[0].PacketsRetrans,\n\t\t\t\tPacketsReceivedRetrans:        list.Items[0].PacketsReceivedRetrans,\n\t\t\t\tPacketsSentACK:                list.Items[0].PacketsSentACK,\n\t\t\t\tPacketsReceivedACK:            list.Items[0].PacketsReceivedACK,\n\t\t\t\tPacketsSentNAK:                list.Items[0].PacketsSentNAK,\n\t\t\t\tPacketsReceivedNAK:            list.Items[0].PacketsReceivedNAK,\n\t\t\t\tPacketsSentKM:                 list.Items[0].PacketsSentKM,\n\t\t\t\tPacketsReceivedKM:             list.Items[0].PacketsReceivedKM,\n\t\t\t\tUsSndDuration:                 list.Items[0].UsSndDuration,\n\t\t\t\tPacketsReceivedBelated:        list.Items[0].PacketsReceivedBelated,\n\t\t\t\tPacketsSendDrop:               list.Items[0].PacketsSendDrop,\n\t\t\t\tPacketsReceivedDrop:           list.Items[0].PacketsReceivedDrop,\n\t\t\t\tPacketsReceivedUndecrypt:      list.Items[0].PacketsReceivedUndecrypt,\n\t\t\t\tBytesReceived:                 list.Items[0].BytesReceived,\n\t\t\t\tBytesSent:                     list.Items[0].BytesSent,\n\t\t\t\tBytesSentUnique:               list.Items[0].BytesSentUnique,\n\t\t\t\tBytesReceivedUnique:           list.Items[0].BytesReceivedUnique,\n\t\t\t\tBytesReceivedLoss:             list.Items[0].BytesReceivedLoss,\n\t\t\t\tBytesRetrans:                  list.Items[0].BytesRetrans,\n\t\t\t\tBytesReceivedRetrans:          list.Items[0].BytesReceivedRetrans,\n\t\t\t\tBytesReceivedBelated:          list.Items[0].BytesReceivedBelated,\n\t\t\t\tBytesSendDrop:                 list.Items[0].BytesSendDrop,\n\t\t\t\tBytesReceivedDrop:             list.Items[0].BytesReceivedDrop,\n\t\t\t\tBytesReceivedUndecrypt:        list.Items[0].BytesReceivedUndecrypt,\n\t\t\t\tOutboundFramesDiscarded:       list.Items[0].OutboundFramesDiscarded,\n\t\t\t\tUsPacketsSendPeriod:           list.Items[0].UsPacketsSendPeriod,\n\t\t\t\tPacketsFlowWindow:             list.Items[0].PacketsFlowWindow,\n\t\t\t\tPacketsFlightSize:             list.Items[0].PacketsFlightSize,\n\t\t\t\tMsRTT:                         list.Items[0].MsRTT,\n\t\t\t\tMbpsSendRate:                  list.Items[0].MbpsSendRate,\n\t\t\t\tMbpsReceiveRate:               list.Items[0].MbpsReceiveRate,\n\t\t\t\tMbpsLinkCapacity:              list.Items[0].MbpsLinkCapacity,\n\t\t\t\tBytesAvailSendBuf:             list.Items[0].BytesAvailSendBuf,\n\t\t\t\tBytesAvailReceiveBuf:          list.Items[0].BytesAvailReceiveBuf,\n\t\t\t\tMbpsMaxBW:                     list.Items[0].MbpsMaxBW,\n\t\t\t\tByteMSS:                       list.Items[0].ByteMSS,\n\t\t\t\tPacketsSendBuf:                list.Items[0].PacketsSendBuf,\n\t\t\t\tBytesSendBuf:                  list.Items[0].BytesSendBuf,\n\t\t\t\tMsSendBuf:                     list.Items[0].MsSendBuf,\n\t\t\t\tMsSendTsbPdDelay:              list.Items[0].MsSendTsbPdDelay,\n\t\t\t\tPacketsReceiveBuf:             list.Items[0].PacketsReceiveBuf,\n\t\t\t\tBytesReceiveBuf:               list.Items[0].BytesReceiveBuf,\n\t\t\t\tMsReceiveBuf:                  list.Items[0].MsReceiveBuf,\n\t\t\t\tMsReceiveTsbPdDelay:           list.Items[0].MsReceiveTsbPdDelay,\n\t\t\t\tPacketsReorderTolerance:       list.Items[0].PacketsReorderTolerance,\n\t\t\t\tPacketsReceivedAvgBelatedTime: list.Items[0].PacketsReceivedAvgBelatedTime,\n\t\t\t\tPacketsSendLossRate:           list.Items[0].PacketsSendLossRate,\n\t\t\t\tPacketsReceivedLossRate:       list.Items[0].PacketsReceivedLossRate,\n\t\t\t},\n\t\t},\n\t}, list)\n\n\t// the second PES is written after writer is closed\n\tpublisher.Close()\n\t<-dataReceived2\n}\n\nfunc TestServerRead(t *testing.T) {\n\texternalCmdPool := &externalcmd.Pool{}\n\texternalCmdPool.Initialize()\n\tdefer externalCmdPool.Close()\n\n\tdesc := &description.Session{Medias: []*description.Media{test.MediaH264}}\n\n\tstrm := &stream.Stream{\n\t\tDesc:              desc,\n\t\tWriteQueueSize:    512,\n\t\tRTPMaxPayloadSize: 1450,\n\t\tParent:            test.NilLogger,\n\t}\n\terr := strm.Initialize()\n\trequire.NoError(t, err)\n\n\tsubStream := &stream.SubStream{\n\t\tStream:        strm,\n\t\tUseRTPPackets: false,\n\t}\n\terr = subStream.Initialize()\n\trequire.NoError(t, err)\n\n\tpathManager := &test.PathManager{\n\t\tAddReaderImpl: func(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error) {\n\t\t\trequire.Equal(t, \"teststream\", req.AccessRequest.Name)\n\t\t\trequire.Equal(t, \"param=value\", req.AccessRequest.Query)\n\t\t\trequire.Equal(t, \"myuser\", req.AccessRequest.Credentials.User)\n\t\t\trequire.Equal(t, \"mypass\", req.AccessRequest.Credentials.Pass)\n\t\t\treturn &defs.PathAddReaderRes{Path: &dummyPath{}, User: req.AccessRequest.Credentials.User, Stream: strm}, nil\n\t\t},\n\t}\n\n\ts := &Server{\n\t\tAddress:             \"127.0.0.1:8890\",\n\t\tRTSPAddress:         \"\",\n\t\tReadTimeout:         conf.Duration(10 * time.Second),\n\t\tWriteTimeout:        conf.Duration(10 * time.Second),\n\t\tUDPMaxPayloadSize:   1472,\n\t\tRunOnConnect:        \"\",\n\t\tRunOnConnectRestart: false,\n\t\tRunOnDisconnect:     \"string\",\n\t\tExternalCmdPool:     externalCmdPool,\n\t\tPathManager:         pathManager,\n\t\tParent:              test.NilLogger,\n\t}\n\terr = s.Initialize()\n\trequire.NoError(t, err)\n\tdefer s.Close()\n\n\tu := \"srt://127.0.0.1:8890?streamid=read:teststream:myuser:mypass:param=value\"\n\n\tsrtConf := srt.DefaultConfig()\n\taddress, err := srtConf.UnmarshalURL(u)\n\trequire.NoError(t, err)\n\n\terr = srtConf.Validate()\n\trequire.NoError(t, err)\n\n\treader, err := srt.Dial(\"srt\", address, srtConf)\n\trequire.NoError(t, err)\n\tdefer reader.Close()\n\n\tstrm.WaitForReaders()\n\n\tsubStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{\n\t\tNTP: time.Time{},\n\t\tPayload: unit.PayloadH264{\n\t\t\t{5, 1}, // IDR\n\t\t},\n\t})\n\n\tr := &mpegts.Reader{R: reader}\n\terr = r.Initialize()\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, []*mpegts.Track{{\n\t\tPID:   256,\n\t\tCodec: &tscodecs.H264{},\n\t}}, r.Tracks())\n\n\treceived := false\n\n\tr.OnDataH264(r.Tracks()[0], func(pts int64, dts int64, au [][]byte) error {\n\t\trequire.Equal(t, int64(0), pts)\n\t\trequire.Equal(t, int64(0), dts)\n\t\trequire.Equal(t, [][]byte{\n\t\t\ttest.FormatH264.SPS,\n\t\t\ttest.FormatH264.PPS,\n\t\t\t{0x05, 1},\n\t\t}, au)\n\t\treceived = true\n\t\treturn nil\n\t})\n\n\tsubStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{\n\t\tNTP: time.Time{},\n\t\tPayload: unit.PayloadH264{\n\t\t\t{5, 2},\n\t\t},\n\t})\n\n\tfor {\n\t\terr = r.Read()\n\t\trequire.NoError(t, err)\n\t\tif received {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tlist, err := s.APIConnsList()\n\trequire.NoError(t, err)\n\trequire.Equal(t, &defs.APISRTConnList{ //nolint:dupl\n\t\tItems: []defs.APISRTConn{\n\t\t\t{\n\t\t\t\tID:                            list.Items[0].ID,\n\t\t\t\tCreated:                       list.Items[0].Created,\n\t\t\t\tRemoteAddr:                    list.Items[0].RemoteAddr,\n\t\t\t\tState:                         \"read\",\n\t\t\t\tPath:                          \"teststream\",\n\t\t\t\tQuery:                         \"param=value\",\n\t\t\t\tUser:                          \"myuser\",\n\t\t\t\tPacketsSent:                   list.Items[0].PacketsSent,\n\t\t\t\tPacketsReceived:               list.Items[0].PacketsReceived,\n\t\t\t\tPacketsSentUnique:             list.Items[0].PacketsSentUnique,\n\t\t\t\tPacketsReceivedUnique:         list.Items[0].PacketsReceivedUnique,\n\t\t\t\tPacketsSendLoss:               list.Items[0].PacketsSendLoss,\n\t\t\t\tPacketsReceivedLoss:           list.Items[0].PacketsReceivedLoss,\n\t\t\t\tPacketsRetrans:                list.Items[0].PacketsRetrans,\n\t\t\t\tPacketsReceivedRetrans:        list.Items[0].PacketsReceivedRetrans,\n\t\t\t\tPacketsSentACK:                list.Items[0].PacketsSentACK,\n\t\t\t\tPacketsReceivedACK:            list.Items[0].PacketsReceivedACK,\n\t\t\t\tPacketsSentNAK:                list.Items[0].PacketsSentNAK,\n\t\t\t\tPacketsReceivedNAK:            list.Items[0].PacketsReceivedNAK,\n\t\t\t\tPacketsSentKM:                 list.Items[0].PacketsSentKM,\n\t\t\t\tPacketsReceivedKM:             list.Items[0].PacketsReceivedKM,\n\t\t\t\tUsSndDuration:                 list.Items[0].UsSndDuration,\n\t\t\t\tPacketsReceivedBelated:        list.Items[0].PacketsReceivedBelated,\n\t\t\t\tPacketsSendDrop:               list.Items[0].PacketsSendDrop,\n\t\t\t\tPacketsReceivedDrop:           list.Items[0].PacketsReceivedDrop,\n\t\t\t\tPacketsReceivedUndecrypt:      list.Items[0].PacketsReceivedUndecrypt,\n\t\t\t\tBytesReceived:                 list.Items[0].BytesReceived,\n\t\t\t\tBytesSent:                     list.Items[0].BytesSent,\n\t\t\t\tBytesSentUnique:               list.Items[0].BytesSentUnique,\n\t\t\t\tBytesReceivedUnique:           list.Items[0].BytesReceivedUnique,\n\t\t\t\tBytesReceivedLoss:             list.Items[0].BytesReceivedLoss,\n\t\t\t\tBytesRetrans:                  list.Items[0].BytesRetrans,\n\t\t\t\tBytesReceivedRetrans:          list.Items[0].BytesReceivedRetrans,\n\t\t\t\tBytesReceivedBelated:          list.Items[0].BytesReceivedBelated,\n\t\t\t\tBytesSendDrop:                 list.Items[0].BytesSendDrop,\n\t\t\t\tBytesReceivedDrop:             list.Items[0].BytesReceivedDrop,\n\t\t\t\tBytesReceivedUndecrypt:        list.Items[0].BytesReceivedUndecrypt,\n\t\t\t\tUsPacketsSendPeriod:           list.Items[0].UsPacketsSendPeriod,\n\t\t\t\tPacketsFlowWindow:             list.Items[0].PacketsFlowWindow,\n\t\t\t\tPacketsFlightSize:             list.Items[0].PacketsFlightSize,\n\t\t\t\tMsRTT:                         list.Items[0].MsRTT,\n\t\t\t\tMbpsSendRate:                  list.Items[0].MbpsSendRate,\n\t\t\t\tMbpsReceiveRate:               list.Items[0].MbpsReceiveRate,\n\t\t\t\tMbpsLinkCapacity:              list.Items[0].MbpsLinkCapacity,\n\t\t\t\tBytesAvailSendBuf:             list.Items[0].BytesAvailSendBuf,\n\t\t\t\tBytesAvailReceiveBuf:          list.Items[0].BytesAvailReceiveBuf,\n\t\t\t\tMbpsMaxBW:                     list.Items[0].MbpsMaxBW,\n\t\t\t\tByteMSS:                       list.Items[0].ByteMSS,\n\t\t\t\tPacketsSendBuf:                list.Items[0].PacketsSendBuf,\n\t\t\t\tBytesSendBuf:                  list.Items[0].BytesSendBuf,\n\t\t\t\tMsSendBuf:                     list.Items[0].MsSendBuf,\n\t\t\t\tMsSendTsbPdDelay:              list.Items[0].MsSendTsbPdDelay,\n\t\t\t\tPacketsReceiveBuf:             list.Items[0].PacketsReceiveBuf,\n\t\t\t\tBytesReceiveBuf:               list.Items[0].BytesReceiveBuf,\n\t\t\t\tMsReceiveBuf:                  list.Items[0].MsReceiveBuf,\n\t\t\t\tMsReceiveTsbPdDelay:           list.Items[0].MsReceiveTsbPdDelay,\n\t\t\t\tPacketsReorderTolerance:       list.Items[0].PacketsReorderTolerance,\n\t\t\t\tPacketsReceivedAvgBelatedTime: list.Items[0].PacketsReceivedAvgBelatedTime,\n\t\t\t\tPacketsSendLossRate:           list.Items[0].PacketsSendLossRate,\n\t\t\t\tPacketsReceivedLossRate:       list.Items[0].PacketsReceivedLossRate,\n\t\t\t},\n\t\t},\n\t}, list)\n}\n"
  },
  {
    "path": "internal/servers/srt/streamid.go",
    "content": "package srt\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n)\n\ntype streamIDMode int\n\nconst (\n\tstreamIDModeRead streamIDMode = iota\n\tstreamIDModePublish\n)\n\ntype streamID struct {\n\tmode  streamIDMode\n\tpath  string\n\tquery string\n\tuser  string\n\tpass  string\n}\n\nfunc (s *streamID) unmarshal(raw string) error {\n\t// standard syntax\n\t// https://github.com/Haivision/srt/blob/master/docs/features/access-control.md\n\tif strings.HasPrefix(raw, \"#!::\") {\n\t\tfor kv := range strings.SplitSeq(raw[len(\"#!::\"):], \",\") {\n\t\t\tkv2 := strings.SplitN(kv, \"=\", 2)\n\t\t\tif len(kv2) != 2 {\n\t\t\t\treturn fmt.Errorf(\"invalid value\")\n\t\t\t}\n\n\t\t\tkey, value := kv2[0], kv2[1]\n\n\t\t\tswitch key {\n\t\t\tcase \"u\":\n\t\t\t\ts.user = value\n\n\t\t\tcase \"r\":\n\t\t\t\ts.path = value\n\n\t\t\tcase \"h\":\n\n\t\t\tcase \"s\":\n\t\t\t\ts.pass = value\n\n\t\t\tcase \"t\":\n\n\t\t\tcase \"m\":\n\t\t\t\tswitch value {\n\t\t\t\tcase \"request\":\n\t\t\t\t\ts.mode = streamIDModeRead\n\n\t\t\t\tcase \"publish\":\n\t\t\t\t\ts.mode = streamIDModePublish\n\n\t\t\t\tdefault:\n\t\t\t\t\treturn fmt.Errorf(\"unsupported mode '%s'\", value)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\tparts := strings.Split(raw, \":\")\n\t\tif len(parts) < 2 || len(parts) > 5 {\n\t\t\treturn fmt.Errorf(\"stream ID must be 'action:pathname[:query]' or 'action:pathname:user:pass[:query]', \" +\n\t\t\t\t\"where action is either read or publish, pathname is the path name, user and pass are the credentials, \" +\n\t\t\t\t\"query is an optional token containing additional information\")\n\t\t}\n\n\t\tswitch parts[0] {\n\t\tcase \"read\":\n\t\t\ts.mode = streamIDModeRead\n\n\t\tcase \"publish\":\n\t\t\ts.mode = streamIDModePublish\n\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"stream ID must be 'action:pathname[:query]' or 'action:pathname:user:pass[:query]', \" +\n\t\t\t\t\"where action is either read or publish, pathname is the path name, user and pass are the credentials, \" +\n\t\t\t\t\"query is an optional token containing additional information\")\n\t\t}\n\n\t\ts.path = parts[1]\n\n\t\tif len(parts) == 4 || len(parts) == 5 {\n\t\t\ts.user, s.pass = parts[2], parts[3]\n\t\t}\n\n\t\tif len(parts) == 3 {\n\t\t\ts.query = parts[2]\n\t\t} else if len(parts) == 5 {\n\t\t\ts.query = parts[4]\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/servers/srt/streamid_test.go",
    "content": "package srt\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestStreamIDUnmarshal(t *testing.T) {\n\tfor _, ca := range []struct {\n\t\tname string\n\t\traw  string\n\t\tdec  streamID\n\t}{\n\t\t{\n\t\t\t\"mediamtx syntax 1\",\n\t\t\t\"read:mypath\",\n\t\t\tstreamID{\n\t\t\t\tmode: streamIDModeRead,\n\t\t\t\tpath: \"mypath\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"mediamtx syntax 2\",\n\t\t\t\"publish:mypath:myquery\",\n\t\t\tstreamID{\n\t\t\t\tmode:  streamIDModePublish,\n\t\t\t\tpath:  \"mypath\",\n\t\t\t\tquery: \"myquery\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"mediamtx syntax 3\",\n\t\t\t\"read:mypath:myuser:mypass:myquery\",\n\t\t\tstreamID{\n\t\t\t\tmode:  streamIDModeRead,\n\t\t\t\tpath:  \"mypath\",\n\t\t\t\tuser:  \"myuser\",\n\t\t\t\tpass:  \"mypass\",\n\t\t\t\tquery: \"myquery\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"standard syntax\",\n\t\t\t\"#!::u=johnny,t=file,m=publish,r=results.csv,s=mypass,h=myhost.com\",\n\t\t\tstreamID{\n\t\t\t\tmode: streamIDModePublish,\n\t\t\t\tpath: \"results.csv\",\n\t\t\t\tuser: \"johnny\",\n\t\t\t\tpass: \"mypass\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"issue 3701\",\n\t\t\t\"#!::bmd_uuid=0e1df79f-77e6-465c-b099-29a616e964f7,bmd_name=rdt-wp-003,r=test3,m=publish\",\n\t\t\tstreamID{\n\t\t\t\tmode: streamIDModePublish,\n\t\t\t\tpath: \"test3\",\n\t\t\t},\n\t\t},\n\t} {\n\t\tt.Run(ca.name, func(t *testing.T) {\n\t\t\tvar sid streamID\n\t\t\terr := sid.unmarshal(ca.raw)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, ca.dec, sid)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/servers/webrtc/http_server.go",
    "content": "package webrtc\n\nimport (\n\t_ \"embed\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/google/uuid\"\n\n\t\"github.com/bluenviron/mediamtx/internal/auth\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/httpp\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/whip\"\n)\n\n//go:embed publish_index.html\nvar publishIndex []byte\n\n//go:embed publisher.js\nvar publisherJS []byte\n\n//go:embed read_index.html\nvar readIndex []byte\n\n//go:embed reader.js\nvar readerJS []byte\n\nvar (\n\treWHIPWHEPNoID   = regexp.MustCompile(\"^/(.+?)/(whip|whep)$\")\n\treWHIPWHEPWithID = regexp.MustCompile(\"^/(.+?)/(whip|whep)/(.+?)$\")\n)\n\nfunc mergePathAndQuery(path string, rawQuery string) string {\n\tres := path\n\tif rawQuery != \"\" {\n\t\tres += \"?\" + rawQuery\n\t}\n\treturn res\n}\n\nfunc writeError(ctx *gin.Context, statusCode int, err error) {\n\tctx.JSON(statusCode, &defs.APIError{\n\t\tStatus: defs.APIErrorStatusError,\n\t\tError:  err.Error(),\n\t})\n}\n\nfunc sessionLocation(publish bool, path string, rawQuery string, secret uuid.UUID) string {\n\tret := \"/\" + path + \"/\"\n\n\tif publish {\n\t\tret += \"whip\"\n\t} else {\n\t\tret += \"whep\"\n\t}\n\n\tret += \"/\" + secret.String()\n\n\tif rawQuery != \"\" {\n\t\tret += \"?\" + rawQuery\n\t}\n\n\treturn ret\n}\n\ntype httpServer struct {\n\taddress        string\n\tdumpPackets    bool\n\tencryption     bool\n\tserverKey      string\n\tserverCert     string\n\tallowOrigins   []string\n\ttrustedProxies conf.IPNetworks\n\treadTimeout    conf.Duration\n\twriteTimeout   conf.Duration\n\tpathManager    serverPathManager\n\tparent         *Server\n\n\tinner *httpp.Server\n}\n\nfunc (s *httpServer) initialize() error {\n\trouter := gin.New()\n\trouter.SetTrustedProxies(s.trustedProxies.ToTrustedProxies()) //nolint:errcheck\n\n\trouter.Use(s.middlewarePreflightRequests)\n\n\trouter.Use(s.onRequest)\n\n\ts.inner = &httpp.Server{\n\t\tAddress:           s.address,\n\t\tAllowOrigins:      s.allowOrigins,\n\t\tDumpPackets:       s.dumpPackets,\n\t\tDumpPacketsPrefix: \"webrtc_server_conn\",\n\t\tReadTimeout:       time.Duration(s.readTimeout),\n\t\tWriteTimeout:      time.Duration(s.writeTimeout),\n\t\tEncryption:        s.encryption,\n\t\tServerCert:        s.serverCert,\n\t\tServerKey:         s.serverKey,\n\t\tHandler:           router,\n\t\tParent:            s,\n\t}\n\terr := s.inner.Initialize()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\n// Log implements logger.Writer.\nfunc (s *httpServer) Log(level logger.Level, format string, args ...any) {\n\ts.parent.Log(level, format, args...)\n}\n\nfunc (s *httpServer) close() {\n\ts.inner.Close()\n}\n\nfunc (s *httpServer) checkAuthOutsideSession(ctx *gin.Context, pathName string, publish bool) bool {\n\t_, err := s.pathManager.FindPathConf(defs.PathFindPathConfReq{\n\t\tAccessRequest: defs.PathAccessRequest{\n\t\t\tName:        pathName,\n\t\t\tQuery:       ctx.Request.URL.RawQuery,\n\t\t\tPublish:     publish,\n\t\t\tProto:       auth.ProtocolWebRTC,\n\t\t\tCredentials: httpp.Credentials(ctx.Request),\n\t\t\tIP:          net.ParseIP(ctx.ClientIP()),\n\t\t},\n\t})\n\tif err != nil {\n\t\tvar terr *auth.Error\n\t\tif errors.As(err, &terr) {\n\t\t\tif terr.AskCredentials {\n\t\t\t\tctx.Header(\"WWW-Authenticate\", `Basic realm=\"mediamtx\"`)\n\t\t\t\tctx.AbortWithStatusJSON(http.StatusUnauthorized, &defs.APIError{\n\t\t\t\t\tStatus: defs.APIErrorStatusError,\n\t\t\t\t\tError:  \"authentication error\",\n\t\t\t\t})\n\t\t\t\treturn false\n\t\t\t}\n\n\t\t\ts.Log(logger.Info, \"connection %v failed to authenticate: %v\", httpp.RemoteAddr(ctx), terr.Wrapped)\n\n\t\t\t// wait some seconds to delay brute force attacks\n\t\t\t<-time.After(auth.PauseAfterError)\n\n\t\t\twriteError(ctx, http.StatusUnauthorized, terr)\n\t\t\treturn false\n\t\t}\n\n\t\twriteError(ctx, http.StatusInternalServerError, err)\n\t\treturn false\n\t}\n\n\treturn true\n}\n\nfunc (s *httpServer) onWHIPOptions(ctx *gin.Context, pathName string, publish bool) {\n\tif !s.checkAuthOutsideSession(ctx, pathName, publish) {\n\t\treturn\n\t}\n\n\tservers, err := s.parent.generateICEServers(true)\n\tif err != nil {\n\t\twriteError(ctx, http.StatusInternalServerError, err)\n\t\treturn\n\t}\n\n\tctx.Header(\"Access-Control-Allow-Methods\", \"OPTIONS, GET, POST, PATCH, DELETE\")\n\tctx.Header(\"Access-Control-Allow-Headers\", \"Authorization, Content-Type, If-Match\")\n\tctx.Header(\"Access-Control-Expose-Headers\", \"Link\")\n\tctx.Writer.Header()[\"Link\"] = whip.LinkHeaderMarshal(servers)\n\tctx.Writer.WriteHeader(http.StatusNoContent)\n}\n\nfunc (s *httpServer) onWHIPPost(ctx *gin.Context, pathName string, publish bool) {\n\tcontentType := httpp.ParseContentType(ctx.Request.Header.Get(\"Content-Type\"))\n\tif contentType != \"application/sdp\" {\n\t\twriteError(ctx, http.StatusBadRequest, fmt.Errorf(\"invalid Content-Type\"))\n\t\treturn\n\t}\n\n\toffer, err := io.ReadAll(ctx.Request.Body)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tres := s.parent.newSession(webRTCNewSessionReq{\n\t\tpathName:    pathName,\n\t\tremoteAddr:  httpp.RemoteAddr(ctx),\n\t\toffer:       offer,\n\t\tpublish:     publish,\n\t\thttpRequest: ctx.Request,\n\t})\n\tif res.err != nil {\n\t\tvar terr *auth.Error\n\t\tif errors.As(res.err, &terr) {\n\t\t\tif terr.AskCredentials {\n\t\t\t\tctx.Header(\"WWW-Authenticate\", `Basic realm=\"mediamtx\"`)\n\t\t\t\tctx.AbortWithStatusJSON(http.StatusUnauthorized, &defs.APIError{\n\t\t\t\t\tStatus: defs.APIErrorStatusError,\n\t\t\t\t\tError:  \"authentication error\",\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\ts.Log(logger.Info, \"connection %v failed to authenticate: %v\", httpp.RemoteAddr(ctx), terr.Wrapped)\n\n\t\t\t// wait some seconds to delay brute force attacks\n\t\t\t<-time.After(auth.PauseAfterError)\n\n\t\t\tctx.AbortWithStatusJSON(http.StatusUnauthorized, &defs.APIError{\n\t\t\t\tStatus: defs.APIErrorStatusError,\n\t\t\t\tError:  \"authentication error\",\n\t\t\t})\n\t\t\treturn\n\t\t}\n\n\t\twriteError(ctx, res.errStatusCode, res.err)\n\t\treturn\n\t}\n\n\tservers, err := s.parent.generateICEServers(true)\n\tif err != nil {\n\t\twriteError(ctx, http.StatusInternalServerError, err)\n\t\treturn\n\t}\n\n\tctx.Header(\"Content-Type\", \"application/sdp\")\n\tctx.Header(\"Access-Control-Expose-Headers\", \"ETag, ID, Accept-Patch, Link, Location\")\n\tctx.Header(\"ETag\", \"*\")\n\tctx.Header(\"ID\", res.sx.uuid.String())\n\tctx.Header(\"Accept-Patch\", \"application/trickle-ice-sdpfrag\")\n\tctx.Writer.Header()[\"Link\"] = whip.LinkHeaderMarshal(servers)\n\tctx.Header(\"Location\", sessionLocation(publish, pathName, ctx.Request.URL.RawQuery, res.sx.secret))\n\tctx.Writer.WriteHeader(http.StatusCreated)\n\tctx.Writer.Write(res.answer)\n\n\tres.sx.Log(logger.Debug, \"SDP answer:\\n\"+string(res.answer))\n}\n\nfunc (s *httpServer) onWHIPPatch(ctx *gin.Context, pathName string, rawSecret string) {\n\tsecret, err := uuid.Parse(rawSecret)\n\tif err != nil {\n\t\twriteError(ctx, http.StatusBadRequest, fmt.Errorf(\"invalid secret\"))\n\t\treturn\n\t}\n\n\tcontentType := httpp.ParseContentType(ctx.Request.Header.Get(\"Content-Type\"))\n\tif contentType != \"application/trickle-ice-sdpfrag\" {\n\t\twriteError(ctx, http.StatusBadRequest, fmt.Errorf(\"invalid Content-Type\"))\n\t\treturn\n\t}\n\n\tbyts, err := io.ReadAll(ctx.Request.Body)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tcandidates, err := whip.ICEFragmentUnmarshal(byts)\n\tif err != nil {\n\t\twriteError(ctx, http.StatusBadRequest, err)\n\t\treturn\n\t}\n\n\tres := s.parent.addSessionCandidates(webRTCAddSessionCandidatesReq{\n\t\tpathName:   pathName,\n\t\tsecret:     secret,\n\t\tcandidates: candidates,\n\t})\n\tif res.err != nil {\n\t\tif errors.Is(res.err, ErrSessionNotFound) {\n\t\t\twriteError(ctx, http.StatusNotFound, res.err)\n\t\t} else {\n\t\t\twriteError(ctx, http.StatusInternalServerError, res.err)\n\t\t}\n\t\treturn\n\t}\n\n\tctx.AbortWithStatusJSON(http.StatusNoContent, &defs.APIOK{\n\t\tStatus: defs.APIOKStatusOK,\n\t})\n}\n\nfunc (s *httpServer) onWHIPDelete(ctx *gin.Context, pathName string, rawSecret string) {\n\tsecret, err := uuid.Parse(rawSecret)\n\tif err != nil {\n\t\twriteError(ctx, http.StatusBadRequest, fmt.Errorf(\"invalid secret\"))\n\t\treturn\n\t}\n\n\terr = s.parent.deleteSession(webRTCDeleteSessionReq{\n\t\tpathName: pathName,\n\t\tsecret:   secret,\n\t})\n\tif err != nil {\n\t\tif errors.Is(err, ErrSessionNotFound) {\n\t\t\twriteError(ctx, http.StatusNotFound, err)\n\t\t} else {\n\t\t\twriteError(ctx, http.StatusInternalServerError, err)\n\t\t}\n\t\treturn\n\t}\n\n\tctx.AbortWithStatusJSON(http.StatusOK, &defs.APIOK{\n\t\tStatus: defs.APIOKStatusOK,\n\t})\n}\n\nfunc (s *httpServer) onPage(ctx *gin.Context, pathName string, publish bool) {\n\tif !s.checkAuthOutsideSession(ctx, pathName, publish) {\n\t\treturn\n\t}\n\n\t// Do not cache the HTML page.\n\t// This prevents a bug in Firefox in which, when the page\n\t// is loaded in an iframe and the iframe is deleted and recreated,\n\t// WebRTC is unable to re-establish the connection.\n\tctx.Header(\"Cache-Control\", \"no-cache\")\n\tctx.Header(\"Content-Type\", \"text/html\")\n\tctx.Writer.WriteHeader(http.StatusOK)\n\n\tif publish {\n\t\tctx.Writer.Write(publishIndex)\n\t} else {\n\t\tctx.Writer.Write(readIndex)\n\t}\n}\n\nfunc (s *httpServer) middlewarePreflightRequests(ctx *gin.Context) {\n\tif ctx.Request.Method == http.MethodOptions &&\n\t\tctx.Request.Header.Get(\"Access-Control-Request-Method\") != \"\" {\n\t\tctx.Header(\"Access-Control-Allow-Methods\", \"OPTIONS, GET, POST, PATCH, DELETE\")\n\t\tctx.Header(\"Access-Control-Allow-Headers\", \"Authorization, Content-Type, If-Match\")\n\t\tctx.AbortWithStatus(http.StatusNoContent)\n\t\treturn\n\t}\n}\n\nfunc (s *httpServer) onRequest(ctx *gin.Context) {\n\tif strings.HasSuffix(ctx.Request.URL.Path, \"/publisher.js\") {\n\t\tctx.Header(\"Cache-Control\", \"max-age=3600\")\n\t\tctx.Header(\"Content-Type\", \"application/javascript\")\n\t\tctx.Writer.WriteHeader(http.StatusOK)\n\t\tctx.Writer.Write(publisherJS)\n\t\treturn\n\t}\n\n\tif strings.HasSuffix(ctx.Request.URL.Path, \"/reader.js\") {\n\t\tctx.Header(\"Cache-Control\", \"max-age=3600\")\n\t\tctx.Header(\"Content-Type\", \"application/javascript\")\n\t\tctx.Writer.WriteHeader(http.StatusOK)\n\t\tctx.Writer.Write(readerJS)\n\t\treturn\n\t}\n\n\t// WHIP/WHEP, outside session\n\tif m := reWHIPWHEPNoID.FindStringSubmatch(ctx.Request.URL.Path); m != nil {\n\t\tswitch ctx.Request.Method {\n\t\tcase http.MethodOptions:\n\t\t\ts.onWHIPOptions(ctx, m[1], m[2] == \"whip\")\n\n\t\tcase http.MethodPost:\n\t\t\ts.onWHIPPost(ctx, m[1], m[2] == \"whip\")\n\n\t\tcase http.MethodGet, http.MethodHead, http.MethodPut:\n\t\t\t// RFC draft-ietf-whip-09\n\t\t\t// The WHIP endpoints MUST return an \"405 Method Not Allowed\" response\n\t\t\t// for any HTTP GET, HEAD or PUT requests\n\t\t\twriteError(ctx, http.StatusMethodNotAllowed, fmt.Errorf(\"method not allowed\"))\n\t\t}\n\t\treturn\n\t}\n\n\t// WHIP/WHEP, inside session\n\tif m := reWHIPWHEPWithID.FindStringSubmatch(ctx.Request.URL.Path); m != nil {\n\t\tswitch ctx.Request.Method {\n\t\tcase http.MethodPatch:\n\t\t\ts.onWHIPPatch(ctx, m[1], m[3])\n\n\t\tcase http.MethodDelete:\n\t\t\ts.onWHIPDelete(ctx, m[1], m[3])\n\t\t}\n\t\treturn\n\t}\n\n\t// static resources\n\tif ctx.Request.Method == http.MethodGet {\n\t\tswitch {\n\t\tcase ctx.Request.URL.Path == \"/favicon.ico\":\n\n\t\tcase len(ctx.Request.URL.Path) >= 2:\n\t\t\tswitch {\n\t\t\tcase len(ctx.Request.URL.Path) > len(\"/publish\") && strings.HasSuffix(ctx.Request.URL.Path, \"/publish\"):\n\t\t\t\ts.onPage(ctx, ctx.Request.URL.Path[1:len(ctx.Request.URL.Path)-len(\"/publish\")], true)\n\n\t\t\tcase ctx.Request.URL.Path[len(ctx.Request.URL.Path)-1] != '/':\n\t\t\t\tctx.Header(\"Location\", mergePathAndQuery(ctx.Request.URL.Path+\"/\", ctx.Request.URL.RawQuery))\n\t\t\t\tctx.Writer.WriteHeader(http.StatusMovedPermanently)\n\n\t\t\tdefault:\n\t\t\t\ts.onPage(ctx, ctx.Request.URL.Path[1:len(ctx.Request.URL.Path)-1], false)\n\t\t\t}\n\t\t}\n\t\treturn\n\t}\n}\n"
  },
  {
    "path": "internal/servers/webrtc/publish_index.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width\">\n<style>\nhtml, body {\n\tmargin: 0;\n\tpadding: 0;\n\theight: 100%;\n\tfont-family: 'Arial', sans-serif;\n}\n#video {\n\tposition: absolute;\n\ttop: 0;\n\tleft: 0;\n\twidth: 100%;\n\theight: 100%;\n\tbackground: rgb(30, 30, 30);\n}\n#controls {\n\tdisplay: none;\n\tflex-shrink: 0;\n\talign-items: center;\n\tjustify-content: center;\n\tpadding: 10px;\n\tflex-direction: column;\n\tmin-height: 100%;\n\twidth: 100%;\n\tbox-sizing: border-box;\n\tbackground: rgb(30, 30, 30);\n\tcolor: white;\n}\n.item {\n\tdisplay: grid;\n\tgrid-auto-flow: column;\n\tgrid-template-columns: auto 220px;\n\talign-items: center;\n\tgap: 20px;\n\tmax-width: 500px;\n\tmargin: 10px 0;\n}\nselect, input[type=\"text\"] {\n\tappearance: none;\n\tbackground: inherit;\n\tcolor: inherit;\n\tborder: 1px solid rgb(200, 200, 200);\n\tborder-radius: 3px;\n\theight: 40px;\n\tpadding: 0 10px;\n}\nselect option {\n\tcolor: black;\n}\n#message {\n\tposition: absolute;\n\tleft: 0;\n\ttop: 0;\n\twidth: 100%;\n\theight: 100%;\n\tdisplay: flex;\n\talign-items: center;\n\ttext-align: center;\n\tjustify-content: center;\n\tfont-size: 16px;\n\tfont-weight: bold;\n\tcolor: white;\n\tpointer-events: none;\n\tpadding: 20px;\n\tbox-sizing: border-box;\n\ttext-shadow: 0 0 5px black;\n}\n#publish-button {\n\tmargin-top: 10px;\n\tappearance: none;\n\tbackground: rgb(200, 200, 200);\n\tcolor: black;\n\tborder-radius: 3px;\n\theight: 50px;\n\tpadding: 0 20px;\n\tborder: none;\n}\n</style>\n<script defer src=\"./publisher.js\"></script>\n</head>\n<body>\n\n<video id=\"video\" muted autoplay playsinline></video>\n\n<div id=\"controls\">\n\t<div id=\"items\">\n\t\t<div class=\"item\">\n\t\t\t<label for=\"video-device\">video device</label>\n\t\t\t<select id=\"video-device\">\n\t\t\t\t<option value=\"none\">none</option>\n\t\t\t</select>\n\t\t</div>\n\n\t\t<div class=\"item\">\n\t\t\t<label for=\"video-codec\">video codec</label>\n\t\t\t<select id=\"video-codec\">\n\t\t\t</select>\n\t\t</div>\n\n\t\t<div class=\"item\">\n\t\t\t<label for=\"video-bitrate\">video bitrate (kbps)</label>\n\t\t\t<input id=\"video-bitrate\" type=\"text\" value=\"10000\" />\n\t\t</div>\n\n\t\t<div class=\"item\">\n\t\t\t<label for=\"video-framerate\">video framerate (ideal)</label>\n\t\t\t<input id=\"video-framerate\" type=\"text\" value=\"30\" />\n\t\t</div>\n\n\t\t<div class=\"item\">\n\t\t\t<label for=\"video-width\">video width (ideal)</label>\n\t\t\t<input id=\"video-width\" type=\"text\" value=\"1920\" />\n\t\t</div>\n\n\t\t<div class=\"item\">\n\t\t\t<label for=\"video-height\">video height (ideal)</label>\n\t\t\t<input id=\"video-height\" type=\"text\" value=\"1080\" />\n\t\t</div>\n\n\t\t<div class=\"item\">\n\t\t\t<label for=\"audio-device\">audio device</label>\n\t\t\t<select id=\"audio-device\">\n\t\t\t\t<option value=\"none\">none</option>\n\t\t\t</select>\n\t\t</div>\n\n\t\t<div class=\"item\">\n\t\t\t<label for=\"audio-codec\">audio codec</label>\n\t\t\t<select id=\"audio-codec\">\n\t\t\t</select>\n\t\t</div>\n\n\t\t<div class=\"item\">\n\t\t\t<label for=\"audio-bitrate\">audio bitrate (kbps)</label>\n\t\t\t<input id=\"audio-bitrate\" type=\"text\" value=\"32\" />\n\t\t</div>\n\n\t\t<div class=\"item\">\n\t\t\t<label for=\"audio-voice\">optimize for voice</label>\n\t\t\t<div>\n\t\t\t\t<input id=\"audio-voice\" type=\"checkbox\" checked>\n\t\t\t</div>\n\t\t</div>\n\t</div>\n\n\t<div id=\"submit-line\">\n\t\t<button id=\"publish-button\">publish</button>\n\t</div>\n</div>\n\n<div id=\"message\"></div>\n\n<script>\n\nconst video = document.getElementById('video');\nconst controls = document.getElementById('controls');\nconst message = document.getElementById('message');\nconst publishButton = document.getElementById('publish-button');\nlet publisher = null;\n\nconst videoForm = {\n  device: document.getElementById('video-device'),\n  codec: document.getElementById('video-codec'),\n  bitrate: document.getElementById('video-bitrate'),\n  framerate: document.getElementById('video-framerate'),\n  width: document.getElementById('video-width'),\n  height: document.getElementById('video-height')\n};\n\nconst audioForm = {\n  device: document.getElementById('audio-device'),\n  codec: document.getElementById('audio-codec'),\n  bitrate: document.getElementById('audio-bitrate'),\n  voice: document.getElementById('audio-voice'),\n};\n\nconst setMessage = (str) => {\n  message.innerText = str;\n};\n\nconst onStream = (stream) => {\n  video.srcObject = stream;\n\n  publisher = new MediaMTXWebRTCPublisher({\n    url: new URL('whip', window.location.href) + window.location.search,\n    stream,\n    videoCodec: videoForm.codec.value,\n    videoBitrate: videoForm.bitrate.value,\n    audioCodec: audioForm.codec.value,\n    audioBitrate: audioForm.bitrate.value,\n    audioVoice: audioForm.voice.checked,\n    onError: (err) => {\n      setMessage(err);\n    },\n    onConnected: (evt) => {\n      setMessage('');\n    },\n  });\n};\n\nconst onPublish = () => {\n  controls.style.display = 'none';\n  video.style.display = 'block';\n  setMessage('connecting');\n\n  const videoId = videoForm.device.value;\n  const audioId = audioForm.device.value;\n\n  if (videoId !== 'screen') {\n    let videoOpts = false;\n\n    if (videoId !== 'none') {\n      videoOpts = {\n        deviceId: videoId,\n        width: { ideal: videoForm.width.value },\n        height: { ideal: videoForm.height.value },\n        frameRate: { ideal: videoForm.framerate.value },\n      };\n    }\n\n    let audioOpts = false;\n\n    if (audioId !== 'none') {\n      audioOpts = {\n        deviceId: audioId,\n      };\n\n      const voice = audioForm.voice.checked;\n      if (!voice) {\n        audioOpts.autoGainControl = false;\n        audioOpts.echoCancellation = false;\n        audioOpts.noiseSuppression = false;\n      }\n    }\n\n    navigator.mediaDevices.getUserMedia({\n      video: videoOpts,\n      audio: audioOpts,\n    })\n      .then((stream) => onStream(stream))\n      .catch((err) => {\n        setMessage(err.toString());\n      });\n  } else {\n    navigator.mediaDevices.getDisplayMedia({\n      video: {\n        width: { ideal: videoForm.width.value },\n        height: { ideal: videoForm.height.value },\n        frameRate: { ideal: videoForm.framerate.value },\n        cursor: 'always',\n      },\n      audio: true,\n    })\n      .then((stream) => onStream(stream))\n      .catch((err) => {\n        setMessage(err.toString());\n      });\n  }\n};\n\nconst selectHasOption = (select, option) => {\n  for (const opt of select.querySelectorAll('option')) {\n    if (opt.value === option) {\n      return true;\n    }\n  }\n  return false;\n};\n\nconst populateDevices = () => {\n  return navigator.mediaDevices.enumerateDevices()\n    .then((devices) => {\n      for (const device of devices) {\n        if (device.kind === 'videoinput' || device.kind === 'audioinput') {\n          const select = (device.kind === 'videoinput') ? videoForm.device : audioForm.device;\n\n          if (!selectHasOption(select, device.deviceId)) {\n            const opt = document.createElement('option');\n            opt.value = device.deviceId;\n            opt.text = device.label;\n            select.appendChild(opt);\n          }\n        }\n      }\n\n      if (navigator.mediaDevices.getDisplayMedia !== undefined) {\n        const opt = document.createElement('option');\n        opt.value = 'screen';\n        opt.text = 'screen';\n        videoForm.device.appendChild(opt);\n      }\n\n      // set first available device as default device\n      if (videoForm.device.children.length !== 0) {\n        videoForm.device.value = videoForm.device.children[1].value;\n      }\n\n      // set first available device as default device\n      if (audioForm.device.children.length !== 0) {\n        audioForm.device.value = audioForm.device.children[1].value;\n      }\n    });\n};\n\nconst populateCodecs = () => {\n  const tempPC = new RTCPeerConnection({});\n  tempPC.addTransceiver('video', { direction: 'sendonly' });\n  tempPC.addTransceiver('audio', { direction: 'sendonly' });\n\n  return tempPC.createOffer()\n    .then((desc) => {\n      const sdp = desc.sdp.toLowerCase();\n\n      for (const codec of ['av1/90000', 'vp9/90000', 'vp8/90000', 'h264/90000', 'h265/90000']) {\n        if (sdp.includes(codec)) {\n          const opt = document.createElement('option');\n          opt.value = codec;\n          opt.text = codec.split('/')[0].toUpperCase();\n          videoForm.codec.appendChild(opt);\n        }\n      }\n\n      for (const codec of ['opus/48000', 'g722/8000', 'pcmu/8000', 'pcma/8000']) {\n        if (sdp.includes(codec)) {\n          const opt = document.createElement('option');\n          opt.value = codec;\n          opt.text = codec.split('/')[0].toUpperCase();\n          audioForm.codec.appendChild(opt);\n        }\n      }\n\n      tempPC.close();\n    });\n};\n\nconst populateOptions = () => {\n  setMessage('loading devices');\n\n  navigator.mediaDevices.getUserMedia({ video: true, audio: true })\n    .then((tempStream) => {\n      return Promise.all([\n        populateDevices(),\n        populateCodecs(),\n      ])\n        .then(() => {\n          // free the webcam to prevent 'NotReadableError' on Android\n          tempStream.getTracks()\n            .forEach((track) => track.stop());\n\n          setMessage('');\n\n          loadValuesFromQuery();\n          setupEventListeners();\n\n          video.style.display = 'none';\n          controls.style.display = 'flex';\n        });\n    })\n    .catch((err) => {\n      setMessage(err.toString());\n    });\n};\n\nconst setupEventListeners = () => {\n  const url = new URL(window.location.href);\n  const inputs = [...Object.values(videoForm), ...Object.values(audioForm)]\n\n  for (const input of inputs) {\n    if (input instanceof HTMLInputElement && input.type === 'text') {\n      input.addEventListener('input', () => {\n        url.searchParams.set(input.id, input.value);\n        window.history.replaceState(null, null, url);\n      })\n    }\n\n    if (input instanceof HTMLInputElement && input.type === 'checkbox') {\n      input.addEventListener('input', () => {\n        url.searchParams.set(input.id, input.checked);\n        window.history.replaceState(null, null, url);\n      })\n    }\n\n    if (input instanceof HTMLSelectElement) {\n      input.addEventListener('input', () => {\n        url.searchParams.set(input.id, input.value);\n        window.history.replaceState(null, null, url);\n      })\n    }\n  }\n};\n\nconst loadValuesFromQuery = () => {\n  const params = new URLSearchParams(window.location.search);\n  const inputs = [...Object.values(videoForm), ...Object.values(audioForm)]\n\n  for (const input of inputs) {\n    const value = params.get(input.id);\n    if (value) {\n      if (input instanceof HTMLInputElement && input.type === 'text') {\n        input.value = value;\n      } else if (input instanceof HTMLInputElement && input.type === 'checkbox') {\n        input.checked = value === 'true';\n      } else if (input instanceof HTMLSelectElement) {\n        input.value = value;\n      }\n    }\n  }\n};\n\nwindow.addEventListener('load', () => {\n  if (navigator.mediaDevices === undefined) {\n    setMessage(`can't access webcams or microphones. Make sure that WebRTC encryption is enabled.`);\n    return;\n  }\n\n  publishButton.addEventListener('click', onPublish);\n  populateOptions();\n});\n\nwindow.addEventListener('beforeunload', () => {\n  if (publisher !== null) {\n    publisher.close();\n  }\n});\n\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "internal/servers/webrtc/publisher.js",
    "content": "'use strict';\n\n/**\n * @callback OnError\n * @param {string} err - error.\n */\n\n/**\n * @callback OnConnected\n */\n\n/**\n * @typedef Conf\n * @type {object}\n * @property {string} url - absolute URL of the WHIP endpoint.\n * @property {string} user - username.\n * @property {string} pass - password.\n * @property {string} token - token.\n * @property {MediaStream} stream - stream that contains outgoing tracks.\n * @property {string} videoCodec - outgoing video codec.\n * @property {number} videoBitrate - outgoing video bitrate.\n * @property {string} audioCodec - outgoing audio bitrate.\n * @property {number} audioBitrate - outgoing audio bitrate.\n * @property {boolean} audioVoice - whether audio is voice.\n * @property {OnError} onError - called when there's an error.\n * @property {OnConnected} onConnected - called when connected.\n */\n\n/** WebRTC/WHIP publisher. */\nclass MediaMTXWebRTCPublisher {\n  /**\n   * Create a MediaMTXWebRTCPublisher.\n   * @param {Conf} conf - configuration.\n   */\n  constructor(conf) {\n    this.retryPause = 2000;\n    this.conf = conf;\n    this.state = 'running';\n    this.restartTimeout = null;\n    this.pc = null;\n    this.offerData = null;\n    this.sessionUrl = null;\n    this.queuedCandidates = [];\n    this.#start();\n  }\n\n  /**\n   * Close the publisher and all its resources.\n   */\n  close = () => {\n    this.state = 'closed';\n\n    if (this.pc !== null) {\n      this.pc.close();\n    }\n\n    if (this.restartTimeout !== null) {\n      clearTimeout(this.restartTimeout);\n    }\n  };\n\n  static #unquoteCredential(v) {\n    return JSON.parse(`\"${v}\"`);\n  }\n\n  static #linkToIceServers(links) {\n    return (links !== null) ? links.split(', ').map((link) => {\n      const m = link.match(/^<(.+?)>; rel=\"ice-server\"(; username=\"(.*?)\"; credential=\"(.*?)\"; credential-type=\"password\")?/i);\n      const ret = {\n        urls: [m[1]],\n      };\n\n      if (m[3] !== undefined) {\n        ret.username = this.#unquoteCredential(m[3]);\n        ret.credential = this.#unquoteCredential(m[4]);\n        ret.credentialType = 'password';\n      }\n\n      return ret;\n    }) : [];\n  }\n\n  static #parseOffer(offer) {\n    const ret = {\n      iceUfrag: '',\n      icePwd: '',\n      medias: [],\n    };\n\n    for (const line of offer.split('\\r\\n')) {\n      if (line.startsWith('m=')) {\n        ret.medias.push(line.slice('m='.length));\n      } else if (ret.iceUfrag === '' && line.startsWith('a=ice-ufrag:')) {\n        ret.iceUfrag = line.slice('a=ice-ufrag:'.length);\n      } else if (ret.icePwd === '' && line.startsWith('a=ice-pwd:')) {\n        ret.icePwd = line.slice('a=ice-pwd:'.length);\n      }\n    }\n\n    return ret;\n  }\n\n  static #generateSdpFragment(od, candidates) {\n    const candidatesByMedia = {};\n    for (const candidate of candidates) {\n      const mid = candidate.sdpMLineIndex;\n      if (candidatesByMedia[mid] === undefined) {\n        candidatesByMedia[mid] = [];\n      }\n      candidatesByMedia[mid].push(candidate);\n    }\n\n    let frag = 'a=ice-ufrag:' + od.iceUfrag + '\\r\\n'\n      + 'a=ice-pwd:' + od.icePwd + '\\r\\n';\n\n    let mid = 0;\n\n    for (const media of od.medias) {\n      if (candidatesByMedia[mid] !== undefined) {\n        frag += 'm=' + media + '\\r\\n'\n          + 'a=mid:' + mid + '\\r\\n';\n\n        for (const candidate of candidatesByMedia[mid]) {\n          frag += 'a=' + candidate.candidate + '\\r\\n';\n        }\n      }\n      mid++;\n    }\n\n    return frag;\n  }\n\n  static #setCodec(section, codec) {\n    const lines = section.split('\\r\\n');\n    const lines2 = [];\n    const payloadFormats = [];\n\n    for (const line of lines) {\n      if (!line.startsWith('a=rtpmap:')) {\n        lines2.push(line);\n      } else {\n        if (line.toLowerCase().includes(codec)) {\n          payloadFormats.push(line.slice('a=rtpmap:'.length).split(' ')[0]);\n          lines2.push(line);\n        }\n      }\n    }\n\n    const lines3 = [];\n    let firstLine = true;\n\n    for (const line of lines2) {\n      if (firstLine) {\n        firstLine = false;\n        lines3.push(line.split(' ').slice(0, 3).concat(payloadFormats).join(' '));\n      } else if (line.startsWith('a=fmtp:')) {\n        if (payloadFormats.includes(line.slice('a=fmtp:'.length).split(' ')[0])) {\n          lines3.push(line);\n        }\n      } else if (line.startsWith('a=rtcp-fb:')) {\n        if (payloadFormats.includes(line.slice('a=rtcp-fb:'.length).split(' ')[0])) {\n          lines3.push(line);\n        }\n      } else {\n        lines3.push(line);\n      }\n    }\n\n    return lines3.join('\\r\\n');\n  }\n\n  static #setVideoBitrate(section, bitrate) {\n    let lines = section.split('\\r\\n');\n\n    for (let i = 0; i < lines.length; i++) {\n      if (lines[i].startsWith('c=')) {\n        lines = [...lines.slice(0, i+1), 'b=TIAS:' + (parseInt(bitrate) * 1024).toString(), ...lines.slice(i+1)];\n        break\n      }\n    }\n\n    return lines.join('\\r\\n');\n  }\n\n  static #setAudioBitrate(section, bitrate, voice) {\n    let opusPayloadFormat = '';\n    let lines = section.split('\\r\\n');\n\n    for (let i = 0; i < lines.length; i++) {\n      if (lines[i].startsWith('a=rtpmap:') && lines[i].toLowerCase().includes('opus/')) {\n        opusPayloadFormat = lines[i].slice('a=rtpmap:'.length).split(' ')[0];\n        break;\n      }\n    }\n\n    if (opusPayloadFormat === '') {\n      return section;\n    }\n\n    for (let i = 0; i < lines.length; i++) {\n      if (lines[i].startsWith('a=fmtp:' + opusPayloadFormat + ' ')) {\n        if (voice) {\n          lines[i] = 'a=fmtp:' + opusPayloadFormat + ' minptime=10;useinbandfec=1;maxaveragebitrate='\n            + (parseInt(bitrate) * 1024).toString();\n        } else {\n          lines[i] = 'a=fmtp:' + opusPayloadFormat + ' maxplaybackrate=48000;stereo=1;sprop-stereo=1;maxaveragebitrate='\n            + (parseInt(bitrate) * 1024).toString();\n        }\n      }\n    }\n\n    return lines.join('\\r\\n');\n  }\n\n  static #editOffer(sdp, videoCodec, audioCodec, audioBitrate, audioVoice) {\n    const sections = sdp.split('m=');\n\n    for (let i = 0; i < sections.length; i++) {\n      if (sections[i].startsWith('video')) {\n        sections[i] = this.#setCodec(sections[i], videoCodec);\n      } else if (sections[i].startsWith('audio')) {\n        sections[i] = this.#setAudioBitrate(this.#setCodec(sections[i], audioCodec), audioBitrate, audioVoice);\n      }\n    }\n\n    return sections.join('m=');\n  }\n\n  static #editAnswer(sdp, videoBitrate) {\n    const sections = sdp.split('m=');\n\n    for (let i = 0; i < sections.length; i++) {\n      if (sections[i].startsWith('video')) {\n        sections[i] = this.#setVideoBitrate(sections[i], videoBitrate);\n      }\n    }\n\n    return sections.join('m=');\n  }\n\n  #start() {\n    this.#requestICEServers()\n      .then((iceServers) => this.#setupPeerConnection(iceServers))\n      .then((offer) => this.#sendOffer(offer))\n      .then((answer) => this.#setAnswer(answer))\n      .catch((err) => {\n        this.#handleError(err.toString());\n      });\n  }\n\n  #handleError(err) {\n    if (this.state === 'running') {\n      if (this.pc !== null) {\n        this.pc.close();\n        this.pc = null;\n      }\n\n      this.offerData = null;\n\n      if (this.sessionUrl !== null) {\n        fetch(this.sessionUrl, {\n          method: 'DELETE',\n        });\n        this.sessionUrl = null;\n      }\n\n      this.queuedCandidates = [];\n      this.state = 'restarting';\n\n      this.restartTimeout = window.setTimeout(() => {\n        this.restartTimeout = null;\n        this.state = 'running';\n        this.#start();\n      }, this.retryPause);\n\n      if (this.conf.onError !== undefined) {\n        this.conf.onError(`${err}, retrying in some seconds`);\n      }\n    }\n  }\n\n  #authHeader() {\n    if (this.conf.user !== undefined && this.conf.user !== '') {\n      const credentials = btoa(`${this.conf.user}:${this.conf.pass}`);\n      return {'Authorization': `Basic ${credentials}`};\n    }\n    if (this.conf.token !== undefined && this.conf.token !== '') {\n      return {'Authorization': `Bearer ${this.conf.token}`};\n    }\n    return {};\n  }\n\n  #requestICEServers() {\n    return fetch(this.conf.url, {\n      method: 'OPTIONS',\n      headers: {\n        ...this.#authHeader(),\n      },\n    })\n      .then((res) => MediaMTXWebRTCPublisher.#linkToIceServers(res.headers.get('Link')));\n  }\n\n  #setupPeerConnection(iceServers) {\n    if (this.state !== 'running') {\n      throw new Error('closed');\n    }\n\n    this.pc = new RTCPeerConnection({\n      iceServers,\n      // https://webrtc.org/getting-started/unified-plan-transition-guide\n      sdpSemantics: 'unified-plan',\n    });\n\n    this.pc.onicecandidate = (evt) => this.#onLocalCandidate(evt);\n    this.pc.onconnectionstatechange = () => this.#onConnectionState();\n\n    this.conf.stream.getTracks().forEach((track) => {\n      this.pc.addTrack(track, this.conf.stream);\n    });\n\n    return this.pc.createOffer()\n      .then((offer) => {\n        this.offerData = MediaMTXWebRTCPublisher.#parseOffer(offer.sdp);\n\n        return this.pc.setLocalDescription(offer)\n          .then(() => offer.sdp);\n      });\n  }\n\n  #sendOffer(offer) {\n    if (this.state !== 'running') {\n      throw new Error('closed');\n    }\n\n    offer = MediaMTXWebRTCPublisher.#editOffer(\n      offer,\n      this.conf.videoCodec,\n      this.conf.audioCodec,\n      this.conf.audioBitrate,\n      this.conf.audioVoice);\n\n    return fetch(this.conf.url, {\n      method: 'POST',\n      headers: {\n        ...this.#authHeader(),\n        'Content-Type': 'application/sdp',\n      },\n      body: offer,\n    })\n      .then((res) => {\n        switch (res.status) {\n          case 201:\n            break;\n          case 400:\n            return res.json().then((e) => { throw new Error(e.error); });\n          default:\n            throw new Error(`bad status code ${res.status}`);\n        }\n\n        this.sessionUrl = new URL(res.headers.get('location'), this.conf.url).toString();\n\n        return res.text();\n      });\n  }\n\n  #setAnswer(answer) {\n    if (this.state !== 'running') {\n      throw new Error('closed');\n    }\n\n    answer = MediaMTXWebRTCPublisher.#editAnswer(answer, this.conf.videoBitrate);\n\n    return this.pc.setRemoteDescription(new RTCSessionDescription({\n      type: 'answer',\n      sdp: answer,\n    }))\n      .then(() => {\n        if (this.state !== 'running') {\n          return;\n        }\n\n        if (this.queuedCandidates.length !== 0) {\n          this.#sendLocalCandidates(this.queuedCandidates);\n          this.queuedCandidates = [];\n        }\n      });\n  }\n\n  #onLocalCandidate(evt) {\n    if (this.state !== 'running') {\n      return;\n    }\n\n    if (evt.candidate !== null) {\n      if (this.sessionUrl === null) {\n        this.queuedCandidates.push(evt.candidate);\n      } else {\n        this.#sendLocalCandidates([evt.candidate]);\n      }\n    }\n  }\n\n  #sendLocalCandidates(candidates) {\n    fetch(this.sessionUrl, {\n      method: 'PATCH',\n      headers: {\n        'Content-Type': 'application/trickle-ice-sdpfrag',\n        'If-Match': '*',\n      },\n      body: MediaMTXWebRTCPublisher.#generateSdpFragment(this.offerData, candidates),\n    })\n      .then((res) => {\n        switch (res.status) {\n          case 204:\n            break;\n          case 404:\n            throw new Error('stream not found');\n          default:\n            throw new Error(`bad status code ${res.status}`);\n        }\n      })\n      .catch((err) => {\n        this.#handleError(err.toString());\n      });\n  }\n\n  #onConnectionState() {\n    if (this.state !== 'running') {\n      return;\n    }\n\n    // \"closed\" can arrive before \"failed\" and without\n    // the close() method being called at all.\n    // It happens when the other peer sends a termination\n    // message like a DTLS CloseNotify.\n    if (this.pc.connectionState === 'failed'\n      || this.pc.connectionState === 'closed'\n    ) {\n      this.#handleError('peer connection closed');\n    } else if (this.pc.connectionState === 'connected') {\n      if (this.conf.onConnected !== undefined) {\n        this.conf.onConnected();\n      }\n    }\n  }\n\n}\n\nwindow.MediaMTXWebRTCPublisher = MediaMTXWebRTCPublisher;\n"
  },
  {
    "path": "internal/servers/webrtc/read_index.html",
    "content": "<!DOCTYPE html>\n<html>\n<head>\n<meta charset=\"utf-8\">\n<meta name=\"viewport\" content=\"width=device-width\">\n<style>\nhtml, body {\n\tmargin: 0;\n\tpadding: 0;\n\theight: 100%;\n\tfont-family: 'Arial', sans-serif;\n}\n#video {\n\tposition: absolute;\n\ttop: 0;\n\tleft: 0;\n\twidth: 100%;\n\theight: 100%;\n\tbackground: rgb(30, 30, 30);\n}\n#message {\n\tposition: absolute;\n\tleft: 0;\n\ttop: 0;\n\twidth: 100%;\n\theight: 100%;\n\tdisplay: flex;\n\talign-items: center;\n\ttext-align: center;\n\tjustify-content: center;\n\tfont-size: 16px;\n\tfont-weight: bold;\n\tcolor: white;\n\tpointer-events: none;\n\tpadding: 20px;\n\tbox-sizing: border-box;\n\ttext-shadow: 0 0 5px black;\n}\n</style>\n<script defer src=\"./reader.js\"></script>\n</head>\n<body>\n\n<video id=\"video\"></video>\n<div id=\"message\"></div>\n\n<script>\n\nconst video = document.getElementById('video');\nconst message = document.getElementById('message');\nlet defaultControls = false;\nlet reader = null;\n\nconst setMessage = (str) => {\n  if (str !== '') {\n    video.controls = false;\n  } else {\n    video.controls = defaultControls;\n  }\n  message.innerText = str;\n};\n\nconst parseBoolString = (str, defaultVal) => {\n  str = (str || '');\n\n  if (['1', 'yes', 'true'].includes(str.toLowerCase())) {\n    return true;\n  }\n  if (['0', 'no', 'false'].includes(str.toLowerCase())) {\n    return false;\n  }\n  return defaultVal;\n};\n\nconst loadAttributesFromQuery = () => {\n  const params = new URLSearchParams(window.location.search);\n  video.controls = parseBoolString(params.get('controls'), true);\n  video.muted = parseBoolString(params.get('muted'), true);\n  video.autoplay = parseBoolString(params.get('autoplay'), true);\n  video.playsInline = parseBoolString(params.get('playsinline'), true);\n  video.disablepictureinpicture = parseBoolString(params.get('disablepictureinpicture'), false);\n  defaultControls = video.controls;\n};\n\nwindow.addEventListener('load', () => {\n  loadAttributesFromQuery();\n\n  reader = new MediaMTXWebRTCReader({\n    url: new URL('whep', window.location.href) + window.location.search,\n    onError: (err) => {\n      setMessage(err);\n    },\n    onTrack: (evt) => {\n      setMessage('');\n      video.srcObject = evt.streams[0];\n    },\n    onDataChannel: (evt) => {\n      evt.channel.binaryType = 'arraybuffer';\n      evt.channel.onmessage = (evt) => {\n        console.log('data channel message', evt.data);\n      };\n    },\n  });\n});\n\nwindow.addEventListener('beforeunload', () => {\n  if (reader !== null) {\n    reader.close();\n  }\n});\n\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "internal/servers/webrtc/reader.js",
    "content": "'use strict';\n\n/**\n * @callback OnError\n * @param {string} err - error.\n */\n\n/**\n * @callback OnTrack\n * @param {RTCTrackEvent} evt - track event.\n */\n\n/**\n * @callback OnDataChannel\n * @param {RTCDataChannelEvent} evt - data channel event.\n */\n\n/**\n * @typedef Conf\n * @type {object}\n * @property {string} url - absolute URL of the WHEP endpoint.\n * @property {string} user - username.\n * @property {string} pass - password.\n * @property {string} token - token.\n * @property {OnError} onError - called when there's an error.\n * @property {OnTrack} onTrack - called when there's a track available.\n * @property {OnDataChannel} onDataChannel - called when there's a data channel available.\n */\n\n/** WebRTC/WHEP reader. */\nclass MediaMTXWebRTCReader {\n  /**\n   * Create a MediaMTXWebRTCReader.\n   * @param {Conf} conf - configuration.\n   */\n  constructor(conf) {\n    this.retryPause = 2000;\n    this.conf = conf;\n    this.state = 'getting_codecs';\n    this.restartTimeout = null;\n    this.pc = null;\n    this.offerData = null;\n    this.sessionUrl = null;\n    this.queuedCandidates = [];\n    this.#getNonAdvertisedCodecs();\n  }\n\n  /**\n   * Close the reader and all its resources.\n   */\n  close() {\n    this.state = 'closed';\n\n    if (this.pc !== null) {\n      this.pc.close();\n    }\n\n    if (this.restartTimeout !== null) {\n      clearTimeout(this.restartTimeout);\n    }\n  }\n\n  static #supportsNonAdvertisedCodec(codec, fmtp) {\n    return new Promise((resolve) => {\n      const pc = new RTCPeerConnection({ iceServers: [] });\n      const mediaType = 'audio';\n      let payloadType = '';\n\n      pc.addTransceiver(mediaType, { direction: 'recvonly' });\n      pc.createOffer()\n        .then((offer) => {\n          if (offer.sdp === undefined) {\n            throw new Error('SDP not present');\n          }\n          if (offer.sdp.includes(` ${codec}`)) { // codec is advertised, there's no need to add it manually\n            throw new Error('already present');\n          }\n\n          const sections = offer.sdp.split(`m=${mediaType}`);\n\n          const payloadTypes = sections.slice(1)\n            .map((s) => s.split('\\r\\n')[0].split(' ').slice(3))\n            .reduce((prev, cur) => [...prev, ...cur], []);\n          payloadType = this.#reservePayloadType(payloadTypes);\n\n          const lines = sections[1].split('\\r\\n');\n          lines[0] += ` ${payloadType}`;\n          lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} ${codec}`);\n          if (fmtp !== undefined) {\n            lines.splice(lines.length - 1, 0, `a=fmtp:${payloadType} ${fmtp}`);\n          }\n          sections[1] = lines.join('\\r\\n');\n          offer.sdp = sections.join(`m=${mediaType}`);\n          return pc.setLocalDescription(offer);\n        })\n        .then(() => (\n          pc.setRemoteDescription(new RTCSessionDescription({\n            type: 'answer',\n            sdp: 'v=0\\r\\n'\n            + 'o=- 6539324223450680508 0 IN IP4 0.0.0.0\\r\\n'\n            + 's=-\\r\\n'\n            + 't=0 0\\r\\n'\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'\n            + `m=${mediaType} 9 UDP/TLS/RTP/SAVPF ${payloadType}\\r\\n`\n            + 'c=IN IP4 0.0.0.0\\r\\n'\n            + 'a=ice-pwd:7c3bf4770007e7432ee4ea4d697db675\\r\\n'\n            + 'a=ice-ufrag:29e036dc\\r\\n'\n            + 'a=sendonly\\r\\n'\n            + 'a=rtcp-mux\\r\\n'\n            + `a=rtpmap:${payloadType} ${codec}\\r\\n`\n            + ((fmtp !== undefined) ? `a=fmtp:${payloadType} ${fmtp}\\r\\n` : ''),\n          }))\n        ))\n        .then(() => {\n          resolve(true);\n        })\n        .catch(() => {\n          resolve(false);\n        })\n        .finally(() => {\n          pc.close();\n        });\n    });\n  }\n\n  static #unquoteCredential(v) {\n    return JSON.parse(`\"${v}\"`);\n  }\n\n  static #linkToIceServers(links) {\n    return (links !== null) ? links.split(', ').map((link) => {\n      const m = link.match(/^<(.+?)>; rel=\"ice-server\"(; username=\"(.*?)\"; credential=\"(.*?)\"; credential-type=\"password\")?/i);\n      const ret = {\n        urls: [m[1]],\n      };\n\n      if (m[3] !== undefined) {\n        ret.username = this.#unquoteCredential(m[3]);\n        ret.credential = this.#unquoteCredential(m[4]);\n        ret.credentialType = 'password';\n      }\n\n      return ret;\n    }) : [];\n  }\n\n  static #parseOffer(sdp) {\n    const ret = {\n      iceUfrag: '',\n      icePwd: '',\n      medias: [],\n    };\n\n    for (const line of sdp.split('\\r\\n')) {\n      if (line.startsWith('m=')) {\n        ret.medias.push(line.slice('m='.length));\n      } else if (ret.iceUfrag === '' && line.startsWith('a=ice-ufrag:')) {\n        ret.iceUfrag = line.slice('a=ice-ufrag:'.length);\n      } else if (ret.icePwd === '' && line.startsWith('a=ice-pwd:')) {\n        ret.icePwd = line.slice('a=ice-pwd:'.length);\n      }\n    }\n\n    return ret;\n  }\n\n  static #reservePayloadType(payloadTypes) {\n    // everything is valid between 30 and 127, except for interval between 64 and 95\n    // https://chromium.googlesource.com/external/webrtc/+/refs/heads/master/call/payload_type.h#29\n    for (let i = 30; i <= 127; i++) {\n      if ((i <= 63 || i >= 96) && !payloadTypes.includes(i.toString())) {\n        const pl = i.toString();\n        payloadTypes.push(pl);\n        return pl;\n      }\n    }\n    throw Error('unable to find a free payload type');\n  }\n\n  static #enableStereoPcmau(payloadTypes, section) {\n    const lines = section.split('\\r\\n');\n\n    let payloadType = this.#reservePayloadType(payloadTypes);\n    lines[0] += ` ${payloadType}`;\n    lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} PCMU/8000/2`);\n    lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);\n\n    payloadType = this.#reservePayloadType(payloadTypes);\n    lines[0] += ` ${payloadType}`;\n    lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} PCMA/8000/2`);\n    lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);\n\n    return lines.join('\\r\\n');\n  }\n\n  static #enableMultichannelOpus(payloadTypes, section) {\n    const lines = section.split('\\r\\n');\n\n    let payloadType = this.#reservePayloadType(payloadTypes);\n    lines[0] += ` ${payloadType}`;\n    lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} multiopus/48000/3`);\n    lines.splice(lines.length - 1, 0, `a=fmtp:${payloadType} channel_mapping=0,2,1;num_streams=2;coupled_streams=1`);\n    lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);\n\n    payloadType = this.#reservePayloadType(payloadTypes);\n    lines[0] += ` ${payloadType}`;\n    lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} multiopus/48000/4`);\n    lines.splice(lines.length - 1, 0, `a=fmtp:${payloadType} channel_mapping=0,1,2,3;num_streams=2;coupled_streams=2`);\n    lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);\n\n    payloadType = this.#reservePayloadType(payloadTypes);\n    lines[0] += ` ${payloadType}`;\n    lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} multiopus/48000/5`);\n    lines.splice(lines.length - 1, 0, `a=fmtp:${payloadType} channel_mapping=0,4,1,2,3;num_streams=3;coupled_streams=2`);\n    lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);\n\n    payloadType = this.#reservePayloadType(payloadTypes);\n    lines[0] += ` ${payloadType}`;\n    lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} multiopus/48000/6`);\n    lines.splice(lines.length - 1, 0, `a=fmtp:${payloadType} channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2`);\n    lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);\n\n    payloadType = this.#reservePayloadType(payloadTypes);\n    lines[0] += ` ${payloadType}`;\n    lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} multiopus/48000/7`);\n    lines.splice(lines.length - 1, 0, `a=fmtp:${payloadType} channel_mapping=0,4,1,2,3,5,6;num_streams=4;coupled_streams=4`);\n    lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);\n\n    payloadType = this.#reservePayloadType(payloadTypes);\n    lines[0] += ` ${payloadType}`;\n    lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} multiopus/48000/8`);\n    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`);\n    lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);\n\n    return lines.join('\\r\\n');\n  }\n\n  static #enableL16(payloadTypes, section) {\n    const lines = section.split('\\r\\n');\n\n    let payloadType = this.#reservePayloadType(payloadTypes);\n    lines[0] += ` ${payloadType}`;\n    lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} L16/8000/2`);\n    lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);\n\n    payloadType = this.#reservePayloadType(payloadTypes);\n    lines[0] += ` ${payloadType}`;\n    lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} L16/16000/2`);\n    lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);\n\n    payloadType = this.#reservePayloadType(payloadTypes);\n    lines[0] += ` ${payloadType}`;\n    lines.splice(lines.length - 1, 0, `a=rtpmap:${payloadType} L16/48000/2`);\n    lines.splice(lines.length - 1, 0, `a=rtcp-fb:${payloadType} transport-cc`);\n\n    return lines.join('\\r\\n');\n  }\n\n  static #enableStereoOpus(section) {\n    let opusPayloadFormat = '';\n    const lines = section.split('\\r\\n');\n\n    for (let i = 0; i < lines.length; i++) {\n      if (lines[i].startsWith('a=rtpmap:') && lines[i].toLowerCase().includes('opus/')) {\n        opusPayloadFormat = lines[i].slice('a=rtpmap:'.length).split(' ')[0];\n        break;\n      }\n    }\n\n    if (opusPayloadFormat === '') {\n      return section;\n    }\n\n    for (let i = 0; i < lines.length; i++) {\n      if (lines[i].startsWith(`a=fmtp:${opusPayloadFormat} `)) {\n        if (!lines[i].includes('stereo')) {\n          lines[i] += ';stereo=1';\n        }\n        if (!lines[i].includes('sprop-stereo')) {\n          lines[i] += ';sprop-stereo=1';\n        }\n      }\n    }\n\n    return lines.join('\\r\\n');\n  }\n\n  static #editOffer(sdp, nonAdvertisedCodecs) {\n    const sections = sdp.split('m=');\n\n    const payloadTypes = sections.slice(1)\n      .map((s) => s.split('\\r\\n')[0].split(' ').slice(3))\n      .reduce((prev, cur) => [...prev, ...cur], []);\n\n    for (let i = 1; i < sections.length; i++) {\n      if (sections[i].startsWith('audio')) {\n        sections[i] = this.#enableStereoOpus(sections[i]);\n\n        if (nonAdvertisedCodecs.includes('pcma/8000/2')) {\n          sections[i] = this.#enableStereoPcmau(payloadTypes, sections[i]);\n        }\n        if (nonAdvertisedCodecs.includes('multiopus/48000/6')) {\n          sections[i] = this.#enableMultichannelOpus(payloadTypes, sections[i]);\n        }\n        if (nonAdvertisedCodecs.includes('L16/48000/2')) {\n          sections[i] = this.#enableL16(payloadTypes, sections[i]);\n        }\n\n        break;\n      }\n    }\n\n    return sections.join('m=');\n  }\n\n  static #generateSdpFragment(od, candidates) {\n    const candidatesByMedia = {};\n    for (const candidate of candidates) {\n      const mid = candidate.sdpMLineIndex;\n      if (candidatesByMedia[mid] === undefined) {\n        candidatesByMedia[mid] = [];\n      }\n      candidatesByMedia[mid].push(candidate);\n    }\n\n    let frag = `a=ice-ufrag:${od.iceUfrag}\\r\\n`\n      + `a=ice-pwd:${od.icePwd}\\r\\n`;\n\n    let mid = 0;\n\n    for (const media of od.medias) {\n      if (candidatesByMedia[mid] !== undefined) {\n        frag += `m=${media}\\r\\n`\n          + `a=mid:${mid}\\r\\n`;\n\n        for (const candidate of candidatesByMedia[mid]) {\n          frag += `a=${candidate.candidate}\\r\\n`;\n        }\n      }\n      mid++;\n    }\n\n    return frag;\n  }\n\n  #handleError(err) {\n    if (this.state === 'running') {\n      if (this.pc !== null) {\n        this.pc.close();\n        this.pc = null;\n      }\n\n      this.offerData = null;\n\n      if (this.sessionUrl !== null) {\n        fetch(this.sessionUrl, {\n          method: 'DELETE',\n        });\n        this.sessionUrl = null;\n      }\n\n      this.queuedCandidates = [];\n      this.state = 'restarting';\n\n      this.restartTimeout = window.setTimeout(() => {\n        this.restartTimeout = null;\n        this.state = 'running';\n        this.#start();\n      }, this.retryPause);\n\n      if (this.conf.onError !== undefined) {\n        this.conf.onError(`${err}, retrying in some seconds`);\n      }\n    } else if (this.state === 'getting_codecs') {\n      this.state = 'failed';\n\n      if (this.conf.onError !== undefined) {\n        this.conf.onError(err);\n      }\n    }\n  }\n\n  #getNonAdvertisedCodecs() {\n    Promise.all([\n      ['pcma/8000/2'],\n      ['multiopus/48000/6', 'channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2'],\n      ['L16/48000/2'],\n    ]\n      .map((c) => MediaMTXWebRTCReader.#supportsNonAdvertisedCodec(c[0], c[1]).then((r) => ((r) ? c[0] : false))))\n      .then((c) => c.filter((e) => e !== false))\n      .then((codecs) => {\n        if (this.state !== 'getting_codecs') {\n          throw new Error('closed');\n        }\n\n        this.nonAdvertisedCodecs = codecs;\n        this.state = 'running';\n        this.#start();\n      })\n      .catch((err) => {\n        this.#handleError(err);\n      });\n  }\n\n  #start() {\n    this.#requestICEServers()\n      .then((iceServers) => this.#setupPeerConnection(iceServers))\n      .then((offer) => this.#sendOffer(offer))\n      .then((answer) => this.#setAnswer(answer))\n      .catch((err) => {\n        this.#handleError(err.toString());\n      });\n  }\n\n  #authHeader() {\n    if (this.conf.user !== undefined && this.conf.user !== '') {\n      const credentials = btoa(`${this.conf.user}:${this.conf.pass}`);\n      return {'Authorization': `Basic ${credentials}`};\n    }\n    if (this.conf.token !== undefined && this.conf.token !== '') {\n      return {'Authorization': `Bearer ${this.conf.token}`};\n    }\n    return {};\n  }\n\n  #requestICEServers() {\n    return fetch(this.conf.url, {\n      method: 'OPTIONS',\n      headers: {\n        ...this.#authHeader(),\n      },\n    })\n      .then((res) => MediaMTXWebRTCReader.#linkToIceServers(res.headers.get('Link')));\n  }\n\n  #setupPeerConnection(iceServers) {\n    if (this.state !== 'running') {\n      throw new Error('closed');\n    }\n\n    this.pc = new RTCPeerConnection({\n      iceServers,\n      // https://webrtc.org/getting-started/unified-plan-transition-guide\n      sdpSemantics: 'unified-plan',\n    });\n\n    const direction = 'recvonly';\n    this.pc.addTransceiver('video', { direction });\n    this.pc.addTransceiver('audio', { direction });\n\n    // using data channels requires creating a data channel locally\n    this.pc.createDataChannel('');\n\n    this.pc.onicecandidate = (evt) => this.#onLocalCandidate(evt);\n    this.pc.onconnectionstatechange = () => this.#onConnectionState();\n    this.pc.ontrack = (evt) => this.#onTrack(evt);\n    this.pc.ondatachannel = (evt) => this.#onDataChannel(evt);\n\n    return this.pc.createOffer()\n      .then((offer) => {\n        offer.sdp = MediaMTXWebRTCReader.#editOffer(offer.sdp, this.nonAdvertisedCodecs);\n        this.offerData = MediaMTXWebRTCReader.#parseOffer(offer.sdp);\n\n        return this.pc.setLocalDescription(offer)\n          .then(() => offer.sdp);\n      });\n  }\n\n  #sendOffer(offer) {\n    if (this.state !== 'running') {\n      throw new Error('closed');\n    }\n\n    return fetch(this.conf.url, {\n      method: 'POST',\n      headers: {\n        ...this.#authHeader(),\n        'Content-Type': 'application/sdp',\n      },\n      body: offer,\n    })\n      .then((res) => {\n        switch (res.status) {\n          case 201:\n            break;\n          case 404:\n            throw new Error('stream not found');\n          case 400:\n            return res.json().then((e) => { throw new Error(e.error); });\n          default:\n            throw new Error(`bad status code ${res.status}`);\n        }\n\n        this.sessionUrl = new URL(res.headers.get('location'), this.conf.url).toString();\n\n        return res.text();\n      });\n  }\n\n  #setAnswer(answer) {\n    if (this.state !== 'running') {\n      throw new Error('closed');\n    }\n\n    return this.pc.setRemoteDescription(new RTCSessionDescription({\n      type: 'answer',\n      sdp: answer,\n    }))\n      .then(() => {\n        if (this.state !== 'running') {\n          return;\n        }\n\n        if (this.queuedCandidates.length !== 0) {\n          this.#sendLocalCandidates(this.queuedCandidates);\n          this.queuedCandidates = [];\n        }\n      });\n  }\n\n  #onLocalCandidate(evt) {\n    if (this.state !== 'running') {\n      return;\n    }\n\n    if (evt.candidate !== null) {\n      if (this.sessionUrl === null) {\n        this.queuedCandidates.push(evt.candidate);\n      } else {\n        this.#sendLocalCandidates([evt.candidate]);\n      }\n    }\n  }\n\n  #sendLocalCandidates(candidates) {\n    fetch(this.sessionUrl, {\n      method: 'PATCH',\n      headers: {\n        'Content-Type': 'application/trickle-ice-sdpfrag',\n        'If-Match': '*',\n      },\n      body: MediaMTXWebRTCReader.#generateSdpFragment(this.offerData, candidates),\n    })\n      .then((res) => {\n        switch (res.status) {\n          case 204:\n            break;\n          case 404:\n            throw new Error('stream not found');\n          default:\n            throw new Error(`bad status code ${res.status}`);\n        }\n      })\n      .catch((err) => {\n        this.#handleError(err.toString());\n      });\n  }\n\n  #onConnectionState() {\n    if (this.state !== 'running') {\n      return;\n    }\n\n    // \"closed\" can arrive before \"failed\" and without\n    // the close() method being called at all.\n    // It happens when the other peer sends a termination\n    // message like a DTLS CloseNotify.\n    if (this.pc.connectionState === 'failed'\n      || this.pc.connectionState === 'closed'\n    ) {\n      this.#handleError('peer connection closed');\n    }\n  }\n\n  #onTrack(evt) {\n    if (this.conf.onTrack !== undefined) {\n      this.conf.onTrack(evt);\n    }\n  }\n\n  #onDataChannel(evt) {\n    if (this.conf.onDataChannel !== undefined) {\n      this.conf.onDataChannel(evt);\n    }\n  }\n}\n\nwindow.MediaMTXWebRTCReader = MediaMTXWebRTCReader;\n"
  },
  {
    "path": "internal/servers/webrtc/server.go",
    "content": "// Package webrtc contains a WebRTC server.\npackage webrtc\n\nimport (\n\t\"context\"\n\t\"crypto/hmac\"\n\t\"crypto/rand\"\n\t\"crypto/sha1\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"reflect\"\n\t\"sort\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/pion/ice/v4\"\n\t\"github.com/pion/logging\"\n\tpwebrtc \"github.com/pion/webrtc/v4\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/readbuffer\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/externalcmd\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/webrtc\"\n\t\"github.com/bluenviron/mediamtx/internal/restrictnetwork\"\n)\n\nconst (\n\twebrtcTurnSecretExpiration = 24 * time.Hour\n)\n\n// ErrSessionNotFound is returned when a session is not found.\nvar ErrSessionNotFound = errors.New(\"session not found\")\n\nfunc interfaceIsEmpty(i any) bool {\n\treturn reflect.ValueOf(i).Kind() != reflect.Pointer || reflect.ValueOf(i).IsNil()\n}\n\ntype nilWriter struct{}\n\nfunc (nilWriter) Write(p []byte) (int, error) {\n\treturn len(p), nil\n}\n\nvar webrtcNilLogger = logging.NewDefaultLeveledLoggerForScope(\"\", 0, &nilWriter{})\n\nfunc randInt63() (int64, error) {\n\tvar b [8]byte\n\t_, err := rand.Read(b[:])\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn int64(uint64(b[0]&0b01111111)<<56 | uint64(b[1])<<48 | uint64(b[2])<<40 | uint64(b[3])<<32 |\n\t\tuint64(b[4])<<24 | uint64(b[5])<<16 | uint64(b[6])<<8 | uint64(b[7])), nil\n}\n\n// https://cs.opensource.google/go/go/+/refs/tags/go1.20.4:src/math/rand/rand.go;l=119\nfunc randInt63n(n int64) (int64, error) {\n\tif n&(n-1) == 0 { // n is power of two, can mask\n\t\tr, err := randInt63()\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\treturn r & (n - 1), nil\n\t}\n\n\tmaxVal := int64((1 << 63) - 1 - (1<<63)%uint64(n))\n\n\tv, err := randInt63()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tfor v > maxVal {\n\t\tv, err = randInt63()\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t}\n\n\treturn v % n, nil\n}\n\nfunc randomTurnUser() (string, error) {\n\tconst charset = \"abcdefghijklmnopqrstuvwxyz1234567890\"\n\tb := make([]byte, 20)\n\tfor i := range b {\n\t\tj, err := randInt63n(int64(len(charset)))\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tb[i] = charset[int(j)]\n\t}\n\n\treturn string(b), nil\n}\n\ntype serverAPISessionsListRes struct {\n\tdata *defs.APIWebRTCSessionList\n\terr  error\n}\n\ntype serverAPISessionsListReq struct {\n\tres chan serverAPISessionsListRes\n}\n\ntype serverAPISessionsGetRes struct {\n\tdata *defs.APIWebRTCSession\n\terr  error\n}\n\ntype serverAPISessionsGetReq struct {\n\tuuid uuid.UUID\n\tres  chan serverAPISessionsGetRes\n}\n\ntype serverAPISessionsKickRes struct {\n\terr error\n}\n\ntype serverAPISessionsKickReq struct {\n\tuuid uuid.UUID\n\tres  chan serverAPISessionsKickRes\n}\n\ntype webRTCNewSessionRes struct {\n\tsx            *session\n\tanswer        []byte\n\terrStatusCode int\n\terr           error\n}\n\ntype webRTCNewSessionReq struct {\n\tpathName    string\n\tremoteAddr  string\n\toffer       []byte\n\tpublish     bool\n\thttpRequest *http.Request\n\tres         chan webRTCNewSessionRes\n}\n\ntype webRTCAddSessionCandidatesRes struct {\n\tsx  *session\n\terr error\n}\n\ntype webRTCAddSessionCandidatesReq struct {\n\tpathName   string\n\tsecret     uuid.UUID\n\tcandidates []*pwebrtc.ICECandidateInit\n\tres        chan webRTCAddSessionCandidatesRes\n}\n\ntype webRTCDeleteSessionRes struct {\n\terr error\n}\n\ntype webRTCDeleteSessionReq struct {\n\tpathName string\n\tsecret   uuid.UUID\n\tres      chan webRTCDeleteSessionRes\n}\n\ntype serverMetrics interface {\n\tSetWebRTCServer(defs.APIWebRTCServer)\n}\n\ntype serverPathManager interface {\n\tFindPathConf(req defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error)\n\tAddPublisher(req defs.PathAddPublisherReq) (*defs.PathAddPublisherRes, error)\n\tAddReader(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error)\n}\n\ntype serverParent interface {\n\tlogger.Writer\n}\n\n// Server is a WebRTC server.\ntype Server struct {\n\tAddress               string\n\tDumpPackets           bool\n\tEncryption            bool\n\tServerKey             string\n\tServerCert            string\n\tAllowOrigins          []string\n\tTrustedProxies        conf.IPNetworks\n\tReadTimeout           conf.Duration\n\tWriteTimeout          conf.Duration\n\tUDPReadBufferSize     uint\n\tLocalUDPAddress       string\n\tLocalTCPAddress       string\n\tIPsFromInterfaces     bool\n\tIPsFromInterfacesList []string\n\tAdditionalHosts       []string\n\tICEServers            []conf.WebRTCICEServer\n\tSTUNGatherTimeout     conf.Duration\n\tHandshakeTimeout      conf.Duration\n\tTrackGatherTimeout    conf.Duration\n\tExternalCmdPool       *externalcmd.Pool\n\tMetrics               serverMetrics\n\tPathManager           serverPathManager\n\tParent                serverParent\n\n\tctx              context.Context\n\tctxCancel        func()\n\thttpServer       *httpServer\n\tudpMuxLn         net.PacketConn\n\ttcpMuxLn         net.Listener\n\ticeUDPMux        ice.UDPMux\n\ticeTCPMux        *webrtc.TCPMuxWrapper\n\tsessions         map[*session]struct{}\n\tsessionsBySecret map[uuid.UUID]*session\n\n\t// in\n\tchNewSession           chan webRTCNewSessionReq\n\tchCloseSession         chan *session\n\tchAddSessionCandidates chan webRTCAddSessionCandidatesReq\n\tchDeleteSession        chan webRTCDeleteSessionReq\n\tchAPISessionsList      chan serverAPISessionsListReq\n\tchAPISessionsGet       chan serverAPISessionsGetReq\n\tchAPIConnsKick         chan serverAPISessionsKickReq\n\n\t// out\n\tdone chan struct{}\n}\n\n// Initialize initializes the server.\nfunc (s *Server) Initialize() error {\n\tctx, ctxCancel := context.WithCancel(context.Background())\n\n\ts.ctx = ctx\n\ts.ctxCancel = ctxCancel\n\ts.sessions = make(map[*session]struct{})\n\ts.sessionsBySecret = make(map[uuid.UUID]*session)\n\ts.chNewSession = make(chan webRTCNewSessionReq)\n\ts.chCloseSession = make(chan *session)\n\ts.chAddSessionCandidates = make(chan webRTCAddSessionCandidatesReq)\n\ts.chDeleteSession = make(chan webRTCDeleteSessionReq)\n\ts.chAPISessionsList = make(chan serverAPISessionsListReq)\n\ts.chAPISessionsGet = make(chan serverAPISessionsGetReq)\n\ts.chAPIConnsKick = make(chan serverAPISessionsKickReq)\n\ts.done = make(chan struct{})\n\n\ts.httpServer = &httpServer{\n\t\taddress:        s.Address,\n\t\tdumpPackets:    s.DumpPackets,\n\t\tencryption:     s.Encryption,\n\t\tserverKey:      s.ServerKey,\n\t\tserverCert:     s.ServerCert,\n\t\tallowOrigins:   s.AllowOrigins,\n\t\ttrustedProxies: s.TrustedProxies,\n\t\treadTimeout:    s.ReadTimeout,\n\t\twriteTimeout:   s.WriteTimeout,\n\t\tpathManager:    s.PathManager,\n\t\tparent:         s,\n\t}\n\terr := s.httpServer.initialize()\n\tif err != nil {\n\t\tctxCancel()\n\t\treturn err\n\t}\n\n\tif s.LocalUDPAddress != \"\" {\n\t\ts.udpMuxLn, err = net.ListenPacket(restrictnetwork.Restrict(\"udp\", s.LocalUDPAddress))\n\t\tif err != nil {\n\t\t\ts.httpServer.close()\n\t\t\tctxCancel()\n\t\t\treturn err\n\t\t}\n\n\t\tif s.UDPReadBufferSize != 0 {\n\t\t\terr = readbuffer.SetReadBuffer(s.udpMuxLn.(*net.UDPConn), int(s.UDPReadBufferSize))\n\t\t\tif err != nil {\n\t\t\t\ts.udpMuxLn.Close()\n\t\t\t\ts.httpServer.close()\n\t\t\t\tctxCancel()\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\ts.iceUDPMux = pwebrtc.NewICEUDPMux(webrtcNilLogger, s.udpMuxLn)\n\t}\n\n\tif s.LocalTCPAddress != \"\" {\n\t\ts.tcpMuxLn, err = net.Listen(restrictnetwork.Restrict(\"tcp\", s.LocalTCPAddress))\n\t\tif err != nil {\n\t\t\tif s.udpMuxLn != nil {\n\t\t\t\ts.udpMuxLn.Close()\n\t\t\t}\n\t\t\ts.httpServer.close()\n\t\t\tctxCancel()\n\t\t\treturn err\n\t\t}\n\n\t\ts.iceTCPMux = &webrtc.TCPMuxWrapper{\n\t\t\tMux: pwebrtc.NewICETCPMux(webrtcNilLogger, s.tcpMuxLn, 8),\n\t\t\tLn:  s.tcpMuxLn,\n\t\t}\n\t}\n\n\tstr := \"listener opened on \" + s.Address + \" (HTTP)\"\n\tif s.udpMuxLn != nil {\n\t\tstr += \", \" + s.LocalUDPAddress + \" (ICE/UDP)\"\n\t}\n\tif s.tcpMuxLn != nil {\n\t\tstr += \", \" + s.LocalTCPAddress + \" (ICE/TCP)\"\n\t}\n\ts.Log(logger.Info, str)\n\n\tgo s.run()\n\n\tif !interfaceIsEmpty(s.Metrics) {\n\t\ts.Metrics.SetWebRTCServer(s)\n\t}\n\n\treturn nil\n}\n\n// Log implements logger.Writer.\nfunc (s *Server) Log(level logger.Level, format string, args ...any) {\n\ts.Parent.Log(level, \"[WebRTC] \"+format, args...)\n}\n\n// Close closes the server.\nfunc (s *Server) Close() {\n\ts.Log(logger.Info, \"listener is closing\")\n\n\tif !interfaceIsEmpty(s.Metrics) {\n\t\ts.Metrics.SetWebRTCServer(nil)\n\t}\n\n\ts.ctxCancel()\n\t<-s.done\n}\n\nfunc (s *Server) run() {\n\tdefer close(s.done)\n\n\tvar wg sync.WaitGroup\n\nouter:\n\tfor {\n\t\tselect {\n\t\tcase req := <-s.chNewSession:\n\t\t\tsx := &session{\n\t\t\t\tudpReadBufferSize:     s.UDPReadBufferSize,\n\t\t\t\tparentCtx:             s.ctx,\n\t\t\t\tipsFromInterfaces:     s.IPsFromInterfaces,\n\t\t\t\tipsFromInterfacesList: s.IPsFromInterfacesList,\n\t\t\t\tadditionalHosts:       s.AdditionalHosts,\n\t\t\t\ticeUDPMux:             s.iceUDPMux,\n\t\t\t\ticeTCPMux:             s.iceTCPMux,\n\t\t\t\tstunGatherTimeout:     s.STUNGatherTimeout,\n\t\t\t\thandshakeTimeout:      s.HandshakeTimeout,\n\t\t\t\ttrackGatherTimeout:    s.TrackGatherTimeout,\n\t\t\t\treq:                   req,\n\t\t\t\twg:                    &wg,\n\t\t\t\texternalCmdPool:       s.ExternalCmdPool,\n\t\t\t\tpathManager:           s.PathManager,\n\t\t\t\tparent:                s,\n\t\t\t}\n\t\t\tsx.initialize()\n\t\t\ts.sessions[sx] = struct{}{}\n\t\t\ts.sessionsBySecret[sx.secret] = sx\n\t\t\treq.res <- webRTCNewSessionRes{sx: sx}\n\n\t\tcase sx := <-s.chCloseSession:\n\t\t\tdelete(s.sessions, sx)\n\t\t\tdelete(s.sessionsBySecret, sx.secret)\n\n\t\tcase req := <-s.chAddSessionCandidates:\n\t\t\tsx, ok := s.sessionsBySecret[req.secret]\n\t\t\tif !ok || sx.req.pathName != req.pathName {\n\t\t\t\treq.res <- webRTCAddSessionCandidatesRes{err: ErrSessionNotFound}\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treq.res <- webRTCAddSessionCandidatesRes{sx: sx}\n\n\t\tcase req := <-s.chDeleteSession:\n\t\t\tsx, ok := s.sessionsBySecret[req.secret]\n\t\t\tif !ok || sx.req.pathName != req.pathName {\n\t\t\t\treq.res <- webRTCDeleteSessionRes{err: ErrSessionNotFound}\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdelete(s.sessions, sx)\n\t\t\tdelete(s.sessionsBySecret, sx.secret)\n\t\t\tsx.Close()\n\n\t\t\treq.res <- webRTCDeleteSessionRes{}\n\n\t\tcase req := <-s.chAPISessionsList:\n\t\t\tdata := &defs.APIWebRTCSessionList{\n\t\t\t\tItems: []defs.APIWebRTCSession{},\n\t\t\t}\n\n\t\t\tfor sx := range s.sessions {\n\t\t\t\tdata.Items = append(data.Items, *sx.apiItem())\n\t\t\t}\n\n\t\t\tsort.Slice(data.Items, func(i, j int) bool {\n\t\t\t\treturn data.Items[i].Created.Before(data.Items[j].Created)\n\t\t\t})\n\n\t\t\treq.res <- serverAPISessionsListRes{data: data}\n\n\t\tcase req := <-s.chAPISessionsGet:\n\t\t\tsx := s.findSessionByUUID(req.uuid)\n\t\t\tif sx == nil {\n\t\t\t\treq.res <- serverAPISessionsGetRes{err: ErrSessionNotFound}\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treq.res <- serverAPISessionsGetRes{data: sx.apiItem()}\n\n\t\tcase req := <-s.chAPIConnsKick:\n\t\t\tsx := s.findSessionByUUID(req.uuid)\n\t\t\tif sx == nil {\n\t\t\t\treq.res <- serverAPISessionsKickRes{err: ErrSessionNotFound}\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tdelete(s.sessions, sx)\n\t\t\tdelete(s.sessionsBySecret, sx.secret)\n\t\t\tsx.Close()\n\n\t\t\treq.res <- serverAPISessionsKickRes{}\n\n\t\tcase <-s.ctx.Done():\n\t\t\tbreak outer\n\t\t}\n\t}\n\n\ts.ctxCancel()\n\n\twg.Wait()\n\n\ts.httpServer.close()\n\n\tif s.udpMuxLn != nil {\n\t\ts.udpMuxLn.Close()\n\t}\n\n\tif s.tcpMuxLn != nil {\n\t\ts.tcpMuxLn.Close()\n\t}\n}\n\nfunc (s *Server) findSessionByUUID(uuid uuid.UUID) *session {\n\tfor sx := range s.sessions {\n\t\tif sx.uuid == uuid {\n\t\t\treturn sx\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (s *Server) generateICEServers(clientConfig bool) ([]pwebrtc.ICEServer, error) {\n\tret := make([]pwebrtc.ICEServer, 0, len(s.ICEServers))\n\n\tfor _, server := range s.ICEServers {\n\t\tif !server.ClientOnly || clientConfig {\n\t\t\tif server.Username == \"AUTH_SECRET\" {\n\t\t\t\texpireDate := time.Now().Add(webrtcTurnSecretExpiration).Unix()\n\n\t\t\t\tuser, err := randomTurnUser()\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tserver.Username = strconv.FormatInt(expireDate, 10) + \":\" + user\n\n\t\t\t\th := hmac.New(sha1.New, []byte(server.Password))\n\t\t\t\th.Write([]byte(server.Username))\n\n\t\t\t\tserver.Password = base64.StdEncoding.EncodeToString(h.Sum(nil))\n\t\t\t}\n\n\t\t\tret = append(ret, pwebrtc.ICEServer{\n\t\t\t\tURLs:       []string{server.URL},\n\t\t\t\tUsername:   server.Username,\n\t\t\t\tCredential: server.Password,\n\t\t\t})\n\t\t}\n\t}\n\n\treturn ret, nil\n}\n\n// newSession is called by webRTCHTTPServer.\nfunc (s *Server) newSession(req webRTCNewSessionReq) webRTCNewSessionRes {\n\treq.res = make(chan webRTCNewSessionRes)\n\n\tselect {\n\tcase s.chNewSession <- req:\n\t\tres := <-req.res\n\n\t\treturn res.sx.new(req)\n\n\tcase <-s.ctx.Done():\n\t\treturn webRTCNewSessionRes{\n\t\t\terrStatusCode: http.StatusInternalServerError,\n\t\t\terr:           fmt.Errorf(\"terminated\"),\n\t\t}\n\t}\n}\n\n// closeSession is called by session.\nfunc (s *Server) closeSession(sx *session) {\n\tselect {\n\tcase s.chCloseSession <- sx:\n\tcase <-s.ctx.Done():\n\t}\n}\n\n// addSessionCandidates is called by webRTCHTTPServer.\nfunc (s *Server) addSessionCandidates(\n\treq webRTCAddSessionCandidatesReq,\n) webRTCAddSessionCandidatesRes {\n\treq.res = make(chan webRTCAddSessionCandidatesRes)\n\tselect {\n\tcase s.chAddSessionCandidates <- req:\n\t\tres1 := <-req.res\n\t\tif res1.err != nil {\n\t\t\treturn res1\n\t\t}\n\n\t\treturn res1.sx.addCandidates(req)\n\n\tcase <-s.ctx.Done():\n\t\treturn webRTCAddSessionCandidatesRes{err: fmt.Errorf(\"terminated\")}\n\t}\n}\n\n// deleteSession is called by webRTCHTTPServer.\nfunc (s *Server) deleteSession(req webRTCDeleteSessionReq) error {\n\treq.res = make(chan webRTCDeleteSessionRes)\n\tselect {\n\tcase s.chDeleteSession <- req:\n\t\tres := <-req.res\n\t\treturn res.err\n\n\tcase <-s.ctx.Done():\n\t\treturn fmt.Errorf(\"terminated\")\n\t}\n}\n\n// APISessionsList is called by api.\nfunc (s *Server) APISessionsList() (*defs.APIWebRTCSessionList, error) {\n\treq := serverAPISessionsListReq{\n\t\tres: make(chan serverAPISessionsListRes),\n\t}\n\n\tselect {\n\tcase s.chAPISessionsList <- req:\n\t\tres := <-req.res\n\t\treturn res.data, res.err\n\n\tcase <-s.ctx.Done():\n\t\treturn nil, fmt.Errorf(\"terminated\")\n\t}\n}\n\n// APISessionsGet is called by api.\nfunc (s *Server) APISessionsGet(uuid uuid.UUID) (*defs.APIWebRTCSession, error) {\n\treq := serverAPISessionsGetReq{\n\t\tuuid: uuid,\n\t\tres:  make(chan serverAPISessionsGetRes),\n\t}\n\n\tselect {\n\tcase s.chAPISessionsGet <- req:\n\t\tres := <-req.res\n\t\treturn res.data, res.err\n\n\tcase <-s.ctx.Done():\n\t\treturn nil, fmt.Errorf(\"terminated\")\n\t}\n}\n\n// APISessionsKick is called by api.\nfunc (s *Server) APISessionsKick(uuid uuid.UUID) error {\n\treq := serverAPISessionsKickReq{\n\t\tuuid: uuid,\n\t\tres:  make(chan serverAPISessionsKickRes),\n\t}\n\n\tselect {\n\tcase s.chAPIConnsKick <- req:\n\t\tres := <-req.res\n\t\treturn res.err\n\n\tcase <-s.ctx.Done():\n\t\treturn fmt.Errorf(\"terminated\")\n\t}\n}\n"
  },
  {
    "path": "internal/servers/webrtc/server_test.go",
    "content": "package webrtc\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"reflect\"\n\t\"regexp\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/mediamtx/internal/auth\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/externalcmd\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/webrtc\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/whip\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n\t\"github.com/google/uuid\"\n\t\"github.com/pion/rtp\"\n\tpwebrtc \"github.com/pion/webrtc/v4\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc ptrOf[T any](v T) *T {\n\tp := new(T)\n\t*p = v\n\treturn p\n}\n\nfunc checkClose(t *testing.T, closeFunc func() error) {\n\trequire.NoError(t, closeFunc())\n}\n\ntype dummyPath struct{}\n\nfunc (p *dummyPath) Name() string {\n\treturn \"teststream\"\n}\n\nfunc (p *dummyPath) SafeConf() *conf.Path {\n\treturn &conf.Path{}\n}\n\nfunc (p *dummyPath) ExternalCmdEnv() externalcmd.Environment {\n\treturn externalcmd.Environment{}\n}\n\nfunc (p *dummyPath) RemovePublisher(_ defs.PathRemovePublisherReq) {\n}\n\nfunc (p *dummyPath) RemoveReader(_ defs.PathRemoveReaderReq) {\n}\n\nfunc initializeTestServer(t *testing.T) *Server {\n\tpm := &test.PathManager{\n\t\tFindPathConfImpl: func(req defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error) {\n\t\t\treturn &defs.PathFindPathConfRes{Conf: &conf.Path{}, User: req.AccessRequest.Credentials.User}, nil\n\t\t},\n\t}\n\n\ts := &Server{\n\t\tAddress:               \"127.0.0.1:8886\",\n\t\tAllowOrigins:          []string{\"*\"},\n\t\tTrustedProxies:        conf.IPNetworks{},\n\t\tReadTimeout:           conf.Duration(10 * time.Second),\n\t\tWriteTimeout:          conf.Duration(10 * time.Second),\n\t\tLocalUDPAddress:       \"127.0.0.1:8887\",\n\t\tLocalTCPAddress:       \"127.0.0.1:8887\",\n\t\tIPsFromInterfaces:     true,\n\t\tIPsFromInterfacesList: []string{},\n\t\tAdditionalHosts:       []string{},\n\t\tICEServers:            []conf.WebRTCICEServer{},\n\t\tHandshakeTimeout:      conf.Duration(10 * time.Second),\n\t\tTrackGatherTimeout:    conf.Duration(2 * time.Second),\n\t\tSTUNGatherTimeout:     conf.Duration(5 * time.Second),\n\t\tPathManager:           pm,\n\t\tParent:                test.NilLogger,\n\t}\n\terr := s.Initialize()\n\trequire.NoError(t, err)\n\n\treturn s\n}\n\nfunc TestServerStaticPages(t *testing.T) {\n\ts := initializeTestServer(t)\n\tdefer s.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\tfor _, path := range []string{\"/stream\", \"/stream/publish\", \"/publish\"} {\n\t\tfunc() {\n\t\t\treq, err := http.NewRequest(http.MethodGet, \"http://myuser:mypass@localhost:8886\"+path, nil)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tres, err := hc.Do(req)\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer res.Body.Close()\n\n\t\t\trequire.Equal(t, http.StatusOK, res.StatusCode)\n\t\t}()\n\t}\n}\n\nfunc TestPreflightRequest(t *testing.T) {\n\ts := initializeTestServer(t)\n\tdefer s.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\treq, err := http.NewRequest(http.MethodOptions, \"http://localhost:8886\", nil)\n\trequire.NoError(t, err)\n\n\treq.Header.Add(\"Access-Control-Request-Method\", \"GET\")\n\n\tres, err := hc.Do(req)\n\trequire.NoError(t, err)\n\tdefer res.Body.Close()\n\n\trequire.Equal(t, http.StatusNoContent, res.StatusCode)\n\n\tbyts, err := io.ReadAll(res.Body)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, \"*\", res.Header.Get(\"Access-Control-Allow-Origin\"))\n\trequire.Equal(t, \"true\", res.Header.Get(\"Access-Control-Allow-Credentials\"))\n\trequire.Equal(t, \"OPTIONS, GET, POST, PATCH, DELETE\", res.Header.Get(\"Access-Control-Allow-Methods\"))\n\trequire.Equal(t, \"Authorization, Content-Type, If-Match\", res.Header.Get(\"Access-Control-Allow-Headers\"))\n\trequire.Equal(t, byts, []byte{})\n}\n\nfunc TestServerOptionsICEServer(t *testing.T) {\n\tpathManager := &test.PathManager{\n\t\tFindPathConfImpl: func(req defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error) {\n\t\t\treturn &defs.PathFindPathConfRes{Conf: &conf.Path{}, User: req.AccessRequest.Credentials.User}, nil\n\t\t},\n\t}\n\n\ts := &Server{\n\t\tAddress:               \"127.0.0.1:8886\",\n\t\tTrustedProxies:        conf.IPNetworks{},\n\t\tReadTimeout:           conf.Duration(10 * time.Second),\n\t\tWriteTimeout:          conf.Duration(10 * time.Second),\n\t\tLocalUDPAddress:       \"127.0.0.1:8887\",\n\t\tLocalTCPAddress:       \"127.0.0.1:8887\",\n\t\tIPsFromInterfaces:     true,\n\t\tIPsFromInterfacesList: []string{},\n\t\tAdditionalHosts:       []string{},\n\t\tICEServers: []conf.WebRTCICEServer{{\n\t\t\tURL:      \"example.com\",\n\t\t\tUsername: \"myuser\",\n\t\t\tPassword: \"mypass\",\n\t\t}},\n\t\tSTUNGatherTimeout:  conf.Duration(5 * time.Second),\n\t\tHandshakeTimeout:   conf.Duration(10 * time.Second),\n\t\tTrackGatherTimeout: conf.Duration(2 * time.Second),\n\t\tPathManager:        pathManager,\n\t\tParent:             test.NilLogger,\n\t}\n\terr := s.Initialize()\n\trequire.NoError(t, err)\n\tdefer s.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\treq, err := http.NewRequest(http.MethodOptions,\n\t\t\"http://myuser:mypass@localhost:8886/nonexisting/whep\", nil)\n\trequire.NoError(t, err)\n\n\tres, err := hc.Do(req)\n\trequire.NoError(t, err)\n\tdefer res.Body.Close()\n\n\trequire.Equal(t, http.StatusNoContent, res.StatusCode)\n\n\ticeServers, err := whip.LinkHeaderUnmarshal(res.Header[\"Link\"])\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, []pwebrtc.ICEServer{{\n\t\tURLs:       []string{\"example.com\"},\n\t\tUsername:   \"myuser\",\n\t\tCredential: \"mypass\",\n\t}}, iceServers)\n}\n\nfunc TestServerPublish(t *testing.T) {\n\tvar strm *stream.Stream\n\tvar reader *stream.Reader\n\tdefer func() {\n\t\tstrm.RemoveReader(reader)\n\t}()\n\tdataReceived := make(chan struct{})\n\n\tpathManager := &test.PathManager{\n\t\tFindPathConfImpl: func(req defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error) {\n\t\t\trequire.Equal(t, \"teststream\", req.AccessRequest.Name)\n\t\t\trequire.Equal(t, \"param=value\", req.AccessRequest.Query)\n\t\t\trequire.Equal(t, \"myuser\", req.AccessRequest.Credentials.User)\n\t\t\trequire.Equal(t, \"mypass\", req.AccessRequest.Credentials.Pass)\n\t\t\treturn &defs.PathFindPathConfRes{Conf: &conf.Path{}, User: req.AccessRequest.Credentials.User}, nil\n\t\t},\n\t\tAddPublisherImpl: func(req defs.PathAddPublisherReq) (*defs.PathAddPublisherRes, error) {\n\t\t\trequire.Equal(t, \"teststream\", req.AccessRequest.Name)\n\t\t\trequire.Equal(t, \"param=value\", req.AccessRequest.Query)\n\t\t\trequire.True(t, req.AccessRequest.SkipAuth)\n\n\t\t\tstrm = &stream.Stream{\n\t\t\t\tDesc:              req.Desc,\n\t\t\t\tWriteQueueSize:    512,\n\t\t\t\tRTPMaxPayloadSize: 1450,\n\t\t\t\tParent:            test.NilLogger,\n\t\t\t}\n\t\t\terr := strm.Initialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsubStream := &stream.SubStream{\n\t\t\t\tStream:        strm,\n\t\t\t\tUseRTPPackets: true,\n\t\t\t}\n\t\t\terr = subStream.Initialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\treader = &stream.Reader{Parent: test.NilLogger}\n\n\t\t\treader.OnData(\n\t\t\t\tstrm.Desc.Medias[0],\n\t\t\t\tstrm.Desc.Medias[0].Formats[0],\n\t\t\t\tfunc(u *unit.Unit) error {\n\t\t\t\t\t/* select {\n\t\t\t\t\tcase <-recv:\n\t\t\t\t\t\treturn nil\n\t\t\t\t\tdefault:\n\t\t\t\t\t} */\n\t\t\t\t\trequire.Equal(t, unit.PayloadH264{\n\t\t\t\t\t\t{1},\n\t\t\t\t\t}, u.Payload)\n\t\t\t\t\tclose(dataReceived)\n\t\t\t\t\treturn nil\n\t\t\t\t})\n\n\t\t\tstrm.AddReader(reader)\n\n\t\t\treturn &defs.PathAddPublisherRes{Path: &dummyPath{}, SubStream: subStream}, nil\n\t\t},\n\t}\n\n\ts := &Server{\n\t\tAddress:               \"127.0.0.1:8886\",\n\t\tTrustedProxies:        conf.IPNetworks{},\n\t\tReadTimeout:           conf.Duration(10 * time.Second),\n\t\tWriteTimeout:          conf.Duration(10 * time.Second),\n\t\tLocalUDPAddress:       \"127.0.0.1:8887\",\n\t\tLocalTCPAddress:       \"127.0.0.1:8887\",\n\t\tIPsFromInterfaces:     true,\n\t\tIPsFromInterfacesList: []string{},\n\t\tAdditionalHosts:       []string{},\n\t\tICEServers:            []conf.WebRTCICEServer{},\n\t\tSTUNGatherTimeout:     conf.Duration(5 * time.Second),\n\t\tHandshakeTimeout:      conf.Duration(10 * time.Second),\n\t\tTrackGatherTimeout:    conf.Duration(2 * time.Second),\n\t\tPathManager:           pathManager,\n\t\tParent:                test.NilLogger,\n\t}\n\terr := s.Initialize()\n\trequire.NoError(t, err)\n\tdefer s.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\tsu, err := url.Parse(\"http://myuser:mypass@localhost:8886/teststream/whip?param=value\")\n\trequire.NoError(t, err)\n\n\ttrack := &webrtc.OutgoingTrack{\n\t\tCaps: pwebrtc.RTPCodecCapability{\n\t\t\tMimeType:    pwebrtc.MimeTypeH264,\n\t\t\tClockRate:   90000,\n\t\t\tSDPFmtpLine: \"level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\",\n\t\t},\n\t}\n\n\twc := &whip.Client{\n\t\tHTTPClient:     hc,\n\t\tURL:            su,\n\t\tPublish:        true,\n\t\tOutgoingTracks: []*webrtc.OutgoingTrack{track},\n\t\tLog:            test.NilLogger,\n\t}\n\n\terr = wc.Initialize(context.Background())\n\trequire.NoError(t, err)\n\tdefer checkClose(t, wc.Close)\n\n\terr = track.WriteRTP(&rtp.Packet{\n\t\tHeader: rtp.Header{\n\t\t\tVersion:        2,\n\t\t\tMarker:         true,\n\t\t\tPayloadType:    96,\n\t\t\tSequenceNumber: 123,\n\t\t\tTimestamp:      45343,\n\t\t\tSSRC:           563423,\n\t\t},\n\t\tPayload: []byte{1},\n\t})\n\trequire.NoError(t, err)\n\n\t<-dataReceived\n\n\tlist, err := s.APISessionsList()\n\trequire.NoError(t, err)\n\trequire.Equal(t, &defs.APIWebRTCSessionList{ //nolint:dupl\n\t\tItems: []defs.APIWebRTCSession{\n\t\t\t{\n\t\t\t\tID:                        list.Items[0].ID,\n\t\t\t\tCreated:                   list.Items[0].Created,\n\t\t\t\tRemoteAddr:                list.Items[0].RemoteAddr,\n\t\t\t\tState:                     \"publish\",\n\t\t\t\tPath:                      \"teststream\",\n\t\t\t\tQuery:                     \"param=value\",\n\t\t\t\tUser:                      \"myuser\",\n\t\t\t\tInboundBytes:              list.Items[0].InboundBytes,\n\t\t\t\tInboundRTPPackets:         list.Items[0].InboundRTPPackets,\n\t\t\t\tInboundRTPPacketsLost:     list.Items[0].InboundRTPPacketsLost,\n\t\t\t\tInboundRTPPacketsJitter:   list.Items[0].InboundRTPPacketsJitter,\n\t\t\t\tInboundRTCPPackets:        list.Items[0].InboundRTCPPackets,\n\t\t\t\tOutboundBytes:             list.Items[0].OutboundBytes,\n\t\t\t\tOutboundRTPPackets:        list.Items[0].OutboundRTPPackets,\n\t\t\t\tOutboundRTCPPackets:       list.Items[0].OutboundRTCPPackets,\n\t\t\t\tOutboundFramesDiscarded:   list.Items[0].OutboundFramesDiscarded,\n\t\t\t\tBytesReceived:             list.Items[0].BytesReceived,\n\t\t\t\tBytesSent:                 list.Items[0].BytesSent,\n\t\t\t\tRTPPacketsReceived:        list.Items[0].RTPPacketsReceived,\n\t\t\t\tRTPPacketsSent:            list.Items[0].RTPPacketsSent,\n\t\t\t\tRTCPPacketsReceived:       list.Items[0].RTCPPacketsReceived,\n\t\t\t\tRTCPPacketsSent:           list.Items[0].RTCPPacketsSent,\n\t\t\t\tPeerConnectionEstablished: true,\n\t\t\t\tLocalCandidate:            list.Items[0].LocalCandidate,\n\t\t\t\tRemoteCandidate:           list.Items[0].RemoteCandidate,\n\t\t\t},\n\t\t},\n\t}, list)\n}\n\nfunc TestServerRead(t *testing.T) {\n\tfor _, ca := range []struct {\n\t\tname          string\n\t\tmedias        []*description.Media\n\t\tunit          *unit.Unit\n\t\toutRTPPayload []byte\n\t}{\n\t\t{\n\t\t\t\"av1\",\n\t\t\t[]*description.Media{{\n\t\t\t\tType: description.MediaTypeVideo,\n\t\t\t\tFormats: []format.Format{&format.AV1{\n\t\t\t\t\tPayloadTyp: 96,\n\t\t\t\t}},\n\t\t\t}},\n\t\t\t&unit.Unit{\n\t\t\t\tPayload: unit.PayloadAV1{{1, 2}},\n\t\t\t},\n\t\t\t[]byte{0x10, 0x01, 0x02},\n\t\t},\n\t\t{\n\t\t\t\"vp9\",\n\t\t\t[]*description.Media{{\n\t\t\t\tType: description.MediaTypeVideo,\n\t\t\t\tFormats: []format.Format{&format.VP9{\n\t\t\t\t\tPayloadTyp: 96,\n\t\t\t\t}},\n\t\t\t}},\n\t\t\t&unit.Unit{\n\t\t\t\tPayload: unit.PayloadVP9{0x82, 0x49, 0x83, 0x42, 0x0, 0x77, 0xf0, 0x32, 0x34},\n\t\t\t},\n\t\t\t[]byte{\n\t\t\t\t0x8f, 0xa0, 0xfd, 0x18, 0x07, 0x80, 0x03, 0x24,\n\t\t\t\t0x01, 0x14, 0x01, 0x82, 0x49, 0x83, 0x42, 0x00,\n\t\t\t\t0x77, 0xf0, 0x32, 0x34,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"vp8\",\n\t\t\t[]*description.Media{{\n\t\t\t\tType: description.MediaTypeVideo,\n\t\t\t\tFormats: []format.Format{&format.VP8{\n\t\t\t\t\tPayloadTyp: 96,\n\t\t\t\t}},\n\t\t\t}},\n\t\t\t&unit.Unit{\n\t\t\t\tPayload: unit.PayloadVP8{1, 2},\n\t\t\t},\n\t\t\t[]byte{0x10, 1, 2},\n\t\t},\n\t\t{\n\t\t\t\"h264\",\n\t\t\t[]*description.Media{test.MediaH264},\n\t\t\t&unit.Unit{\n\t\t\t\tPayload: unit.PayloadH264{\n\t\t\t\t\t{5, 1},\n\t\t\t\t},\n\t\t\t},\n\t\t\t[]byte{\n\t\t\t\t0x18, 0x00, 0x19, 0x67, 0x42, 0xc0, 0x28, 0xd9,\n\t\t\t\t0x00, 0x78, 0x02, 0x27, 0xe5, 0x84, 0x00, 0x00,\n\t\t\t\t0x03, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0xf0,\n\t\t\t\t0x3c, 0x60, 0xc9, 0x20, 0x00, 0x04, 0x08, 0x06,\n\t\t\t\t0x07, 0x08, 0x00, 0x02, 0x05, 0x01,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\t\"opus\",\n\t\t\t[]*description.Media{{\n\t\t\t\tType: description.MediaTypeAudio,\n\t\t\t\tFormats: []format.Format{&format.Opus{\n\t\t\t\t\tPayloadTyp:   96,\n\t\t\t\t\tChannelCount: 2,\n\t\t\t\t}},\n\t\t\t}},\n\t\t\t&unit.Unit{\n\t\t\t\tPayload: unit.PayloadOpus{{1, 2}},\n\t\t\t},\n\t\t\t[]byte{1, 2},\n\t\t},\n\t\t{\n\t\t\t\"g722\",\n\t\t\t[]*description.Media{{\n\t\t\t\tType:    description.MediaTypeAudio,\n\t\t\t\tFormats: []format.Format{&format.G722{}},\n\t\t\t}},\n\t\t\t&unit.Unit{\n\t\t\t\tRTPPackets: []*rtp.Packet{{\n\t\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\t\tVersion:        2,\n\t\t\t\t\t\tMarker:         true,\n\t\t\t\t\t\tPayloadType:    9,\n\t\t\t\t\t\tSequenceNumber: 1123,\n\t\t\t\t\t\tTimestamp:      45343,\n\t\t\t\t\t\tSSRC:           563423,\n\t\t\t\t\t},\n\t\t\t\t\tPayload: []byte{1, 2},\n\t\t\t\t}},\n\t\t\t},\n\t\t\t[]byte{1, 2},\n\t\t},\n\t\t{\n\t\t\t\"g711 8khz mono\",\n\t\t\t[]*description.Media{{\n\t\t\t\tType: description.MediaTypeAudio,\n\t\t\t\tFormats: []format.Format{&format.G711{\n\t\t\t\t\tMULaw:        true,\n\t\t\t\t\tSampleRate:   8000,\n\t\t\t\t\tChannelCount: 1,\n\t\t\t\t}},\n\t\t\t}},\n\t\t\t&unit.Unit{\n\t\t\t\tPayload: unit.PayloadG711{1, 2, 3},\n\t\t\t},\n\t\t\t[]byte{1, 2, 3},\n\t\t},\n\t\t{\n\t\t\t\"g711 16khz stereo\",\n\t\t\t[]*description.Media{{\n\t\t\t\tType: description.MediaTypeAudio,\n\t\t\t\tFormats: []format.Format{&format.G711{\n\t\t\t\t\tMULaw:        true,\n\t\t\t\t\tSampleRate:   16000,\n\t\t\t\t\tChannelCount: 2,\n\t\t\t\t}},\n\t\t\t}},\n\t\t\t&unit.Unit{\n\t\t\t\tPayload: unit.PayloadG711{1, 2, 3, 4},\n\t\t\t},\n\t\t\t[]byte{0x86, 0x84, 0x8a, 0x84, 0x8e, 0x84, 0x92, 0x84},\n\t\t},\n\t\t{\n\t\t\t\"lpcm\",\n\t\t\t[]*description.Media{{\n\t\t\t\tType: description.MediaTypeAudio,\n\t\t\t\tFormats: []format.Format{&format.LPCM{\n\t\t\t\t\tPayloadTyp:   96,\n\t\t\t\t\tBitDepth:     16,\n\t\t\t\t\tSampleRate:   48000,\n\t\t\t\t\tChannelCount: 2,\n\t\t\t\t}},\n\t\t\t}},\n\t\t\t&unit.Unit{\n\t\t\t\tPayload: unit.PayloadLPCM{1, 2, 3, 4},\n\t\t\t},\n\t\t\t[]byte{1, 2, 3, 4},\n\t\t},\n\t} {\n\t\tt.Run(ca.name, func(t *testing.T) {\n\t\t\tdesc := &description.Session{Medias: ca.medias}\n\n\t\t\tstrm := &stream.Stream{\n\t\t\t\tDesc:              desc,\n\t\t\t\tWriteQueueSize:    512,\n\t\t\t\tRTPMaxPayloadSize: 1450,\n\t\t\t\tParent:            test.NilLogger,\n\t\t\t}\n\t\t\terr := strm.Initialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tsubStream := &stream.SubStream{\n\t\t\t\tStream:        strm,\n\t\t\t\tUseRTPPackets: (ca.unit.Payload == nil),\n\t\t\t}\n\t\t\terr = subStream.Initialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tpathManager := &test.PathManager{\n\t\t\t\tFindPathConfImpl: func(req defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error) {\n\t\t\t\t\trequire.Equal(t, \"teststream\", req.AccessRequest.Name)\n\t\t\t\t\trequire.Equal(t, \"param=value\", req.AccessRequest.Query)\n\t\t\t\t\trequire.Equal(t, \"myuser\", req.AccessRequest.Credentials.User)\n\t\t\t\t\trequire.Equal(t, \"mypass\", req.AccessRequest.Credentials.Pass)\n\t\t\t\t\treturn &defs.PathFindPathConfRes{Conf: &conf.Path{}, User: req.AccessRequest.Credentials.User}, nil\n\t\t\t\t},\n\t\t\t\tAddReaderImpl: func(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error) {\n\t\t\t\t\trequire.Equal(t, \"teststream\", req.AccessRequest.Name)\n\t\t\t\t\trequire.Equal(t, \"param=value\", req.AccessRequest.Query)\n\t\t\t\t\trequire.Equal(t, \"myuser\", req.AccessRequest.Credentials.User)\n\t\t\t\t\trequire.Equal(t, \"mypass\", req.AccessRequest.Credentials.Pass)\n\t\t\t\t\treturn &defs.PathAddReaderRes{Path: &dummyPath{}, User: req.AccessRequest.Credentials.User, Stream: strm}, nil\n\t\t\t\t},\n\t\t\t}\n\n\t\t\ts := &Server{\n\t\t\t\tAddress:               \"127.0.0.1:8886\",\n\t\t\t\tReadTimeout:           conf.Duration(10 * time.Second),\n\t\t\t\tWriteTimeout:          conf.Duration(10 * time.Second),\n\t\t\t\tLocalUDPAddress:       \"127.0.0.1:8887\",\n\t\t\t\tLocalTCPAddress:       \"127.0.0.1:8887\",\n\t\t\t\tIPsFromInterfaces:     true,\n\t\t\t\tIPsFromInterfacesList: []string{},\n\t\t\t\tAdditionalHosts:       []string{},\n\t\t\t\tICEServers:            []conf.WebRTCICEServer{},\n\t\t\t\tSTUNGatherTimeout:     conf.Duration(5 * time.Second),\n\t\t\t\tHandshakeTimeout:      conf.Duration(10 * time.Second),\n\t\t\t\tTrackGatherTimeout:    conf.Duration(2 * time.Second),\n\t\t\t\tPathManager:           pathManager,\n\t\t\t\tParent:                test.NilLogger,\n\t\t\t}\n\t\t\terr = s.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer s.Close()\n\n\t\t\tu, err := url.Parse(\"http://myuser:mypass@localhost:8886/teststream/whep?param=value\")\n\t\t\trequire.NoError(t, err)\n\n\t\t\ttr := &http.Transport{}\n\t\t\tdefer tr.CloseIdleConnections()\n\t\t\thc := &http.Client{Transport: tr}\n\n\t\t\twc := &whip.Client{\n\t\t\t\tHTTPClient: hc,\n\t\t\t\tURL:        u,\n\t\t\t\tLog:        test.NilLogger,\n\t\t\t}\n\n\t\t\twriterDone := make(chan struct{})\n\n\t\t\tgo func() {\n\t\t\t\tdefer close(writerDone)\n\n\t\t\t\ttime.Sleep(500 * time.Millisecond)\n\n\t\t\t\tr := reflect.New(reflect.TypeOf(ca.unit).Elem())\n\t\t\t\tr.Elem().Set(reflect.ValueOf(ca.unit).Elem())\n\n\t\t\t\tif ca.unit.Payload == nil {\n\t\t\t\t\tclone := *ca.unit.RTPPackets[0]\n\t\t\t\t\tsubStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{\n\t\t\t\t\t\tPTS:        0,\n\t\t\t\t\t\tNTP:        time.Time{},\n\t\t\t\t\t\tRTPPackets: []*rtp.Packet{&clone},\n\t\t\t\t\t})\n\t\t\t\t} else {\n\t\t\t\t\tsubStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], r.Interface().(*unit.Unit))\n\t\t\t\t}\n\t\t\t}()\n\n\t\t\terr = wc.Initialize(context.Background())\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer checkClose(t, wc.Close)\n\n\t\t\tdone := make(chan struct{})\n\n\t\t\twc.IncomingTracks()[0].OnPacketRTP = func(pkt *rtp.Packet) {\n\t\t\t\tselect {\n\t\t\t\tcase <-done:\n\t\t\t\tdefault:\n\t\t\t\t\trequire.Equal(t, ca.outRTPPayload, pkt.Payload)\n\t\t\t\t\tclose(done)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\twc.StartReading()\n\n\t\t\t<-writerDone\n\t\t\t<-done\n\n\t\t\tlist, err := s.APISessionsList()\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, &defs.APIWebRTCSessionList{ //nolint:dupl\n\t\t\t\tItems: []defs.APIWebRTCSession{\n\t\t\t\t\t{\n\t\t\t\t\t\tID:                        list.Items[0].ID,\n\t\t\t\t\t\tCreated:                   list.Items[0].Created,\n\t\t\t\t\t\tRemoteAddr:                list.Items[0].RemoteAddr,\n\t\t\t\t\t\tState:                     \"read\",\n\t\t\t\t\t\tPath:                      \"teststream\",\n\t\t\t\t\t\tQuery:                     \"param=value\",\n\t\t\t\t\t\tUser:                      \"myuser\",\n\t\t\t\t\t\tInboundBytes:              list.Items[0].InboundBytes,\n\t\t\t\t\t\tInboundRTPPackets:         list.Items[0].InboundRTPPackets,\n\t\t\t\t\t\tInboundRTPPacketsLost:     list.Items[0].InboundRTPPacketsLost,\n\t\t\t\t\t\tInboundRTPPacketsJitter:   list.Items[0].InboundRTPPacketsJitter,\n\t\t\t\t\t\tInboundRTCPPackets:        list.Items[0].InboundRTCPPackets,\n\t\t\t\t\t\tOutboundBytes:             list.Items[0].OutboundBytes,\n\t\t\t\t\t\tOutboundRTPPackets:        list.Items[0].OutboundRTPPackets,\n\t\t\t\t\t\tOutboundRTCPPackets:       list.Items[0].OutboundRTCPPackets,\n\t\t\t\t\t\tOutboundFramesDiscarded:   list.Items[0].OutboundFramesDiscarded,\n\t\t\t\t\t\tBytesReceived:             list.Items[0].BytesReceived,\n\t\t\t\t\t\tBytesSent:                 list.Items[0].BytesSent,\n\t\t\t\t\t\tRTPPacketsReceived:        list.Items[0].RTPPacketsReceived,\n\t\t\t\t\t\tRTPPacketsSent:            list.Items[0].RTPPacketsSent,\n\t\t\t\t\t\tRTCPPacketsReceived:       list.Items[0].RTCPPacketsReceived,\n\t\t\t\t\t\tRTCPPacketsSent:           list.Items[0].RTCPPacketsSent,\n\t\t\t\t\t\tPeerConnectionEstablished: true,\n\t\t\t\t\t\tLocalCandidate:            list.Items[0].LocalCandidate,\n\t\t\t\t\t\tRemoteCandidate:           list.Items[0].RemoteCandidate,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}, list)\n\t\t})\n\t}\n}\n\nfunc TestServerReadNotFound(t *testing.T) {\n\tpm := &test.PathManager{\n\t\tFindPathConfImpl: func(req defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error) {\n\t\t\treturn &defs.PathFindPathConfRes{Conf: &conf.Path{}, User: req.AccessRequest.Credentials.User}, nil\n\t\t},\n\t\tAddReaderImpl: func(_ defs.PathAddReaderReq) (*defs.PathAddReaderRes, error) {\n\t\t\treturn nil, defs.PathNoStreamAvailableError{}\n\t\t},\n\t}\n\n\ts := &Server{\n\t\tAddress:               \"127.0.0.1:8886\",\n\t\tTrustedProxies:        conf.IPNetworks{},\n\t\tReadTimeout:           conf.Duration(10 * time.Second),\n\t\tWriteTimeout:          conf.Duration(10 * time.Second),\n\t\tLocalUDPAddress:       \"127.0.0.1:8887\",\n\t\tLocalTCPAddress:       \"127.0.0.1:8887\",\n\t\tIPsFromInterfaces:     true,\n\t\tIPsFromInterfacesList: []string{},\n\t\tAdditionalHosts:       []string{},\n\t\tICEServers:            []conf.WebRTCICEServer{},\n\t\tSTUNGatherTimeout:     conf.Duration(5 * time.Second),\n\t\tHandshakeTimeout:      conf.Duration(10 * time.Second),\n\t\tTrackGatherTimeout:    conf.Duration(2 * time.Second),\n\t\tPathManager:           pm,\n\t\tParent:                test.NilLogger,\n\t}\n\terr := s.Initialize()\n\trequire.NoError(t, err)\n\tdefer s.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\tpc, err := pwebrtc.NewPeerConnection(pwebrtc.Configuration{})\n\trequire.NoError(t, err)\n\tdefer pc.GracefulClose() //nolint:errcheck\n\n\t_, err = pc.AddTransceiverFromKind(pwebrtc.RTPCodecTypeVideo)\n\trequire.NoError(t, err)\n\n\toffer, err := pc.CreateOffer(nil)\n\trequire.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodPost,\n\t\t\"http://myuser:mypass@localhost:8886/nonexisting/whep\", bytes.NewReader([]byte(offer.SDP)))\n\trequire.NoError(t, err)\n\n\treq.Header.Set(\"Content-Type\", \"application/sdp\")\n\n\tres, err := hc.Do(req)\n\trequire.NoError(t, err)\n\tdefer res.Body.Close()\n\n\trequire.Equal(t, http.StatusNotFound, res.StatusCode)\n}\n\nfunc TestServerPatchNotFound(t *testing.T) {\n\ts := initializeTestServer(t)\n\tdefer s.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\tpc, err := pwebrtc.NewPeerConnection(pwebrtc.Configuration{})\n\trequire.NoError(t, err)\n\tdefer pc.GracefulClose() //nolint:errcheck\n\n\t_, err = pc.AddTransceiverFromKind(pwebrtc.RTPCodecTypeVideo)\n\trequire.NoError(t, err)\n\n\toffer, err := pc.CreateOffer(nil)\n\trequire.NoError(t, err)\n\n\tfrag, err := whip.ICEFragmentMarshal(offer.SDP, []*pwebrtc.ICECandidateInit{{\n\t\tCandidate:     \"mycandidate\",\n\t\tSDPMLineIndex: ptrOf(uint16(0)),\n\t}})\n\trequire.NoError(t, err)\n\n\treq, err := http.NewRequest(http.MethodPatch,\n\t\t\"http://localhost:8886/nonexisting/whep/\"+uuid.UUID{}.String(), bytes.NewReader(frag))\n\trequire.NoError(t, err)\n\n\treq.Header.Set(\"Content-Type\", \"application/trickle-ice-sdpfrag\")\n\n\tres, err := hc.Do(req)\n\trequire.NoError(t, err)\n\tdefer res.Body.Close()\n\n\trequire.Equal(t, http.StatusNotFound, res.StatusCode)\n}\n\nfunc TestServerDeleteNotFound(t *testing.T) {\n\ts := initializeTestServer(t)\n\tdefer s.Close()\n\n\ttr := &http.Transport{}\n\tdefer tr.CloseIdleConnections()\n\thc := &http.Client{Transport: tr}\n\n\treq, err := http.NewRequest(http.MethodDelete,\n\t\t\"http://localhost:8886/nonexisting/whep/\"+uuid.UUID{}.String(), nil)\n\trequire.NoError(t, err)\n\n\tres, err := hc.Do(req)\n\trequire.NoError(t, err)\n\tdefer res.Body.Close()\n\n\trequire.Equal(t, http.StatusNotFound, res.StatusCode)\n}\n\nfunc TestICEServerNoClientOnly(t *testing.T) {\n\ts := &Server{\n\t\tICEServers: []conf.WebRTCICEServer{\n\t\t\t{\n\t\t\t\tURL:      \"turn:turn.example.com:1234\",\n\t\t\t\tUsername: \"user\",\n\t\t\t\tPassword: \"passwrd\",\n\t\t\t},\n\t\t},\n\t}\n\tclientICEServers, err := s.generateICEServers(true)\n\trequire.NoError(t, err)\n\trequire.Equal(t, len(s.ICEServers), len(clientICEServers))\n\tserverICEServers, err := s.generateICEServers(false)\n\trequire.NoError(t, err)\n\trequire.Equal(t, len(s.ICEServers), len(serverICEServers))\n}\n\nfunc TestICEServerClientOnly(t *testing.T) {\n\ts := &Server{\n\t\tICEServers: []conf.WebRTCICEServer{\n\t\t\t{\n\t\t\t\tURL:        \"turn:turn.example.com:1234\",\n\t\t\t\tUsername:   \"user\",\n\t\t\t\tPassword:   \"passwrd\",\n\t\t\t\tClientOnly: true,\n\t\t\t},\n\t\t},\n\t}\n\tclientICEServers, err := s.generateICEServers(true)\n\trequire.NoError(t, err)\n\trequire.Equal(t, len(s.ICEServers), len(clientICEServers))\n\tserverICEServers, err := s.generateICEServers(false)\n\trequire.NoError(t, err)\n\trequire.Empty(t, serverICEServers)\n}\n\nfunc TestAuthError(t *testing.T) {\n\tfor _, ca := range []string{\n\t\t\"publish page\",\n\t\t\"whip options\",\n\t\t\"whip post\",\n\t} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tauthFailed := false\n\n\t\t\ts := &Server{\n\t\t\t\tAddress:      \"127.0.0.1:8886\",\n\t\t\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\t\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\t\t\tPathManager: &test.PathManager{\n\t\t\t\t\tFindPathConfImpl: func(req defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error) {\n\t\t\t\t\t\tif req.AccessRequest.Credentials.User == \"\" && req.AccessRequest.Credentials.Pass == \"\" {\n\t\t\t\t\t\t\treturn nil, &auth.Error{AskCredentials: true, Wrapped: fmt.Errorf(\"auth error\")}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn nil, &auth.Error{Wrapped: fmt.Errorf(\"auth error\")}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tParent: test.Logger(func(l logger.Level, s string, i ...any) {\n\t\t\t\t\tif l == logger.Info {\n\t\t\t\t\t\tif regexp.MustCompile(\"failed to authenticate: auth error$\").MatchString(fmt.Sprintf(s, i...)) {\n\t\t\t\t\t\t\tauthFailed = true\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}),\n\t\t\t}\n\t\t\terr := s.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer s.Close()\n\n\t\t\tvar req *http.Request\n\n\t\t\tswitch ca {\n\t\t\tcase \"publish page\":\n\t\t\t\treq, err = http.NewRequest(http.MethodGet, \"http://127.0.0.1:8886/stream/publish\", nil)\n\n\t\t\tcase \"whip options\":\n\t\t\t\treq, err = http.NewRequest(http.MethodOptions, \"http://127.0.0.1:8886/teststream/whip\", nil)\n\n\t\t\tcase \"whip post\":\n\t\t\t\tvar pc *pwebrtc.PeerConnection\n\t\t\t\tpc, err = pwebrtc.NewPeerConnection(pwebrtc.Configuration{})\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer pc.GracefulClose() //nolint:errcheck\n\n\t\t\t\t_, err = pc.AddTransceiverFromKind(pwebrtc.RTPCodecTypeVideo)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tvar offer pwebrtc.SessionDescription\n\t\t\t\toffer, err = pc.CreateOffer(nil)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\treq, err = http.NewRequest(http.MethodPost, \"http://127.0.0.1:8886/teststream/whip\",\n\t\t\t\t\tbytes.NewReader([]byte(offer.SDP)))\n\t\t\t\treq.Header.Set(\"Content-Type\", \"application/sdp\")\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\n\t\t\tres, err := http.DefaultClient.Do(req)\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer res.Body.Close()\n\n\t\t\trequire.Equal(t, http.StatusUnauthorized, res.StatusCode)\n\t\t\trequire.Equal(t, `Basic realm=\"mediamtx\"`, res.Header.Get(\"WWW-Authenticate\"))\n\n\t\t\tswitch ca {\n\t\t\tcase \"publish page\":\n\t\t\t\treq, err = http.NewRequest(http.MethodGet, \"http://myuser:mypass@127.0.0.1:8886/stream/publish\", nil)\n\n\t\t\tcase \"whip options\":\n\t\t\t\treq, err = http.NewRequest(http.MethodOptions, \"http://myuser:mypass@127.0.0.1:8886/teststream/whip\", nil)\n\n\t\t\tcase \"whip post\":\n\t\t\t\tvar pc *pwebrtc.PeerConnection\n\t\t\t\tpc, err = pwebrtc.NewPeerConnection(pwebrtc.Configuration{})\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer pc.GracefulClose() //nolint:errcheck\n\n\t\t\t\t_, err = pc.AddTransceiverFromKind(pwebrtc.RTPCodecTypeVideo)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tvar offer pwebrtc.SessionDescription\n\t\t\t\toffer, err = pc.CreateOffer(nil)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\treq, err = http.NewRequest(http.MethodPost, \"http://myuser:mypass@127.0.0.1:8886/teststream/whip\",\n\t\t\t\t\tbytes.NewReader([]byte(offer.SDP)))\n\t\t\t\treq.Header.Set(\"Content-Type\", \"application/sdp\")\n\t\t\t}\n\n\t\t\trequire.NoError(t, err)\n\n\t\t\tstart := time.Now()\n\n\t\t\tres, err = http.DefaultClient.Do(req)\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer res.Body.Close()\n\n\t\t\trequire.Greater(t, time.Since(start), 2*time.Second)\n\n\t\t\trequire.Equal(t, http.StatusUnauthorized, res.StatusCode)\n\n\t\t\trequire.True(t, authFailed)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/servers/webrtc/session.go",
    "content": "package webrtc\n\nimport (\n\t\"context\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/google/uuid\"\n\t\"github.com/pion/ice/v4\"\n\t\"github.com/pion/sdp/v3\"\n\tpwebrtc \"github.com/pion/webrtc/v4\"\n\n\t\"github.com/bluenviron/mediamtx/internal/auth\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/externalcmd\"\n\t\"github.com/bluenviron/mediamtx/internal/hooks\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/httpp\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/webrtc\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n)\n\nfunc whipOffer(body []byte) *pwebrtc.SessionDescription {\n\treturn &pwebrtc.SessionDescription{\n\t\tType: pwebrtc.SDPTypeOffer,\n\t\tSDP:  string(body),\n\t}\n}\n\ntype sessionParent interface {\n\tcloseSession(sx *session)\n\tgenerateICEServers(clientConfig bool) ([]pwebrtc.ICEServer, error)\n\tlogger.Writer\n}\n\ntype session struct {\n\tudpReadBufferSize     uint\n\tparentCtx             context.Context\n\tipsFromInterfaces     bool\n\tipsFromInterfacesList []string\n\tadditionalHosts       []string\n\ticeUDPMux             ice.UDPMux\n\ticeTCPMux             *webrtc.TCPMuxWrapper\n\tstunGatherTimeout     conf.Duration\n\thandshakeTimeout      conf.Duration\n\ttrackGatherTimeout    conf.Duration\n\treq                   webRTCNewSessionReq\n\twg                    *sync.WaitGroup\n\texternalCmdPool       *externalcmd.Pool\n\tpathManager           serverPathManager\n\tparent                sessionParent\n\n\tctx       context.Context\n\tctxCancel func()\n\tcreated   time.Time\n\tuuid      uuid.UUID\n\tsecret    uuid.UUID\n\tmutex     sync.RWMutex\n\treader    *stream.Reader\n\tpc        *webrtc.PeerConnection\n\tuser      string\n\n\tchNew           chan webRTCNewSessionReq\n\tchAddCandidates chan webRTCAddSessionCandidatesReq\n}\n\nfunc (s *session) initialize() {\n\tctx, ctxCancel := context.WithCancel(s.parentCtx)\n\n\ts.ctx = ctx\n\ts.ctxCancel = ctxCancel\n\ts.created = time.Now()\n\ts.uuid = uuid.New()\n\ts.secret = uuid.New()\n\ts.chNew = make(chan webRTCNewSessionReq)\n\ts.chAddCandidates = make(chan webRTCAddSessionCandidatesReq)\n\n\ts.Log(logger.Info, \"created by %s\", s.req.remoteAddr)\n\n\ts.wg.Add(1)\n\n\tgo s.run()\n}\n\n// Log implements logger.Writer.\nfunc (s *session) Log(level logger.Level, format string, args ...any) {\n\tid := hex.EncodeToString(s.uuid[:4])\n\ts.parent.Log(level, \"[session %v] \"+format, append([]any{id}, args...)...)\n}\n\nfunc (s *session) Close() {\n\ts.ctxCancel()\n}\n\nfunc (s *session) run() {\n\tdefer s.wg.Done()\n\n\terr := s.runInner()\n\n\ts.ctxCancel()\n\n\ts.parent.closeSession(s)\n\n\ts.Log(logger.Info, \"closed: %v\", err)\n}\n\nfunc (s *session) runInner() error {\n\tselect {\n\tcase <-s.chNew:\n\tcase <-s.ctx.Done():\n\t\treturn fmt.Errorf(\"terminated\")\n\t}\n\n\terrStatusCode, err := s.runInner2()\n\n\tif errStatusCode != 0 {\n\t\ts.req.res <- webRTCNewSessionRes{\n\t\t\terrStatusCode: errStatusCode,\n\t\t\terr:           err,\n\t\t}\n\t}\n\n\treturn err\n}\n\nfunc (s *session) runInner2() (int, error) {\n\tif s.req.publish {\n\t\treturn s.runPublish()\n\t}\n\treturn s.runRead()\n}\n\nfunc (s *session) runPublish() (int, error) {\n\tip, _, _ := net.SplitHostPort(s.req.remoteAddr)\n\n\tres1, err := s.pathManager.FindPathConf(defs.PathFindPathConfReq{\n\t\tAccessRequest: defs.PathAccessRequest{\n\t\t\tName:        s.req.pathName,\n\t\t\tQuery:       s.req.httpRequest.URL.RawQuery,\n\t\t\tPublish:     true,\n\t\t\tProto:       auth.ProtocolWebRTC,\n\t\t\tID:          &s.uuid,\n\t\t\tCredentials: httpp.Credentials(s.req.httpRequest),\n\t\t\tIP:          net.ParseIP(ip),\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn http.StatusBadRequest, err\n\t}\n\n\ts.mutex.Lock()\n\ts.user = res1.User\n\ts.mutex.Unlock()\n\n\ticeServers, err := s.parent.generateICEServers(false)\n\tif err != nil {\n\t\treturn http.StatusInternalServerError, err\n\t}\n\n\tpc := &webrtc.PeerConnection{\n\t\tUDPReadBufferSize:     s.udpReadBufferSize,\n\t\tICEUDPMux:             s.iceUDPMux,\n\t\tICETCPMux:             s.iceTCPMux,\n\t\tICEServers:            iceServers,\n\t\tIPsFromInterfaces:     s.ipsFromInterfaces,\n\t\tIPsFromInterfacesList: s.ipsFromInterfacesList,\n\t\tAdditionalHosts:       s.additionalHosts,\n\t\tSTUNGatherTimeout:     time.Duration(s.stunGatherTimeout),\n\t\tPublish:               false,\n\t\tLog:                   s,\n\t}\n\terr = pc.Start()\n\tif err != nil {\n\t\treturn http.StatusBadRequest, err\n\t}\n\n\tterminatorDone := make(chan struct{})\n\tdefer func() { <-terminatorDone }()\n\n\tterminatorRun := make(chan struct{})\n\tdefer close(terminatorRun)\n\n\tgo func() {\n\t\tdefer close(terminatorDone)\n\t\tselect {\n\t\tcase <-s.ctx.Done():\n\t\tcase <-terminatorRun:\n\t\t}\n\t\tpc.Close()\n\t}()\n\n\toffer := whipOffer(s.req.offer)\n\n\tvar sdp sdp.SessionDescription\n\terr = sdp.Unmarshal([]byte(offer.SDP))\n\tif err != nil {\n\t\treturn http.StatusBadRequest, err\n\t}\n\n\terr = webrtc.TracksAreValid(sdp.MediaDescriptions)\n\tif err != nil {\n\t\t// RFC draft-ietf-wish-whip\n\t\t// if the number of audio and or video\n\t\t// tracks or number streams is not supported by the WHIP Endpoint, it\n\t\t// MUST reject the HTTP POST request with a \"406 Not Acceptable\" error\n\t\t// response.\n\t\treturn http.StatusNotAcceptable, err\n\t}\n\n\tanswer, err := pc.CreateFullAnswer(offer)\n\tif err != nil {\n\t\treturn http.StatusBadRequest, err\n\t}\n\n\ts.writeAnswer(answer)\n\n\tgo s.readRemoteCandidates(pc)\n\n\terr = pc.WaitUntilConnected(time.Duration(s.handshakeTimeout))\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\ts.mutex.Lock()\n\ts.pc = pc\n\ts.mutex.Unlock()\n\n\terr = pc.GatherIncomingTracks(time.Duration(s.trackGatherTimeout))\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tvar subStream *stream.SubStream\n\n\tmedias, err := webrtc.ToStream(pc, res1.Conf, &subStream, s)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tres2, err := s.pathManager.AddPublisher(defs.PathAddPublisherReq{\n\t\tAuthor:        s,\n\t\tDesc:          &description.Session{Medias: medias},\n\t\tUseRTPPackets: true,\n\t\tReplaceNTP:    !res1.Conf.UseAbsoluteTimestamp,\n\t\tConfToCompare: res1.Conf,\n\t\tAccessRequest: defs.PathAccessRequest{\n\t\t\tName:     s.req.pathName,\n\t\t\tQuery:    s.req.httpRequest.URL.RawQuery,\n\t\t\tPublish:  true,\n\t\t\tSkipAuth: true,\n\t\t},\n\t})\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tdefer res2.Path.RemovePublisher(defs.PathRemovePublisherReq{Author: s})\n\n\tsubStream = res2.SubStream\n\n\tpc.StartReading()\n\n\tselect {\n\tcase <-pc.Failed():\n\t\treturn 0, fmt.Errorf(\"peer connection closed\")\n\n\tcase <-s.ctx.Done():\n\t\treturn 0, fmt.Errorf(\"terminated\")\n\t}\n}\n\nfunc (s *session) runRead() (int, error) {\n\tip, _, _ := net.SplitHostPort(s.req.remoteAddr)\n\n\treq := defs.PathAccessRequest{\n\t\tName:        s.req.pathName,\n\t\tQuery:       s.req.httpRequest.URL.RawQuery,\n\t\tProto:       auth.ProtocolWebRTC,\n\t\tID:          &s.uuid,\n\t\tCredentials: httpp.Credentials(s.req.httpRequest),\n\t\tIP:          net.ParseIP(ip),\n\t}\n\n\tres, err := s.pathManager.AddReader(defs.PathAddReaderReq{\n\t\tAuthor:        s,\n\t\tAccessRequest: req,\n\t})\n\tif err != nil {\n\t\tvar terr2 defs.PathNoStreamAvailableError\n\t\tif errors.As(err, &terr2) {\n\t\t\treturn http.StatusNotFound, err\n\t\t}\n\n\t\treturn http.StatusBadRequest, err\n\t}\n\n\tdefer res.Path.RemoveReader(defs.PathRemoveReaderReq{Author: s})\n\n\ts.mutex.Lock()\n\ts.user = res.User\n\ts.mutex.Unlock()\n\n\ticeServers, err := s.parent.generateICEServers(false)\n\tif err != nil {\n\t\treturn http.StatusInternalServerError, err\n\t}\n\n\tpc := &webrtc.PeerConnection{\n\t\tUDPReadBufferSize:     s.udpReadBufferSize,\n\t\tICEUDPMux:             s.iceUDPMux,\n\t\tICETCPMux:             s.iceTCPMux,\n\t\tICEServers:            iceServers,\n\t\tIPsFromInterfaces:     s.ipsFromInterfaces,\n\t\tIPsFromInterfacesList: s.ipsFromInterfacesList,\n\t\tAdditionalHosts:       s.additionalHosts,\n\t\tSTUNGatherTimeout:     time.Duration(s.stunGatherTimeout),\n\t\tPublish:               true,\n\t\tLog:                   s,\n\t}\n\n\tr := &stream.Reader{Parent: s}\n\n\terr = webrtc.FromStream(res.Stream.Desc, r, pc)\n\tif err != nil {\n\t\treturn http.StatusBadRequest, err\n\t}\n\n\terr = pc.Start()\n\tif err != nil {\n\t\treturn http.StatusBadRequest, err\n\t}\n\n\tterminatorDone := make(chan struct{})\n\tdefer func() { <-terminatorDone }()\n\n\tterminatorRun := make(chan struct{})\n\tdefer close(terminatorRun)\n\n\tgo func() {\n\t\tdefer close(terminatorDone)\n\t\tselect {\n\t\tcase <-s.ctx.Done():\n\t\tcase <-terminatorRun:\n\t\t}\n\t\tpc.Close()\n\t}()\n\n\toffer := whipOffer(s.req.offer)\n\n\tanswer, err := pc.CreateFullAnswer(offer)\n\tif err != nil {\n\t\treturn http.StatusBadRequest, err\n\t}\n\n\ts.writeAnswer(answer)\n\n\tgo s.readRemoteCandidates(pc)\n\n\terr = pc.WaitUntilConnected(time.Duration(s.handshakeTimeout))\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\ts.mutex.Lock()\n\ts.pc = pc\n\ts.mutex.Unlock()\n\n\ts.Log(logger.Info, \"is reading from path '%s', %s\",\n\t\tres.Path.Name(), defs.FormatsInfo(r.Formats()))\n\n\tonUnreadHook := hooks.OnRead(hooks.OnReadParams{\n\t\tLogger:          s,\n\t\tExternalCmdPool: s.externalCmdPool,\n\t\tConf:            res.Path.SafeConf(),\n\t\tExternalCmdEnv:  res.Path.ExternalCmdEnv(),\n\t\tReader:          *s.APIReaderDescribe(),\n\t\tQuery:           s.req.httpRequest.URL.RawQuery,\n\t})\n\tdefer onUnreadHook()\n\n\tres.Stream.AddReader(r)\n\tdefer res.Stream.RemoveReader(r)\n\n\ts.mutex.Lock()\n\ts.reader = r\n\ts.mutex.Unlock()\n\n\tselect {\n\tcase <-pc.Failed():\n\t\treturn 0, fmt.Errorf(\"peer connection closed\")\n\n\tcase err = <-r.Error():\n\t\treturn 0, err\n\n\tcase <-s.ctx.Done():\n\t\treturn 0, fmt.Errorf(\"terminated\")\n\t}\n}\n\nfunc (s *session) writeAnswer(answer *pwebrtc.SessionDescription) {\n\ts.req.res <- webRTCNewSessionRes{\n\t\tsx:     s,\n\t\tanswer: []byte(answer.SDP),\n\t}\n}\n\nfunc (s *session) readRemoteCandidates(pc *webrtc.PeerConnection) {\n\tfor {\n\t\tselect {\n\t\tcase req := <-s.chAddCandidates:\n\t\t\tfor _, candidate := range req.candidates {\n\t\t\t\terr := pc.AddRemoteCandidate(candidate)\n\t\t\t\tif err != nil {\n\t\t\t\t\treq.res <- webRTCAddSessionCandidatesRes{err: err}\n\t\t\t\t}\n\t\t\t}\n\t\t\treq.res <- webRTCAddSessionCandidatesRes{}\n\n\t\tcase <-s.ctx.Done():\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// new is called by webRTCHTTPServer through Server.\nfunc (s *session) new(req webRTCNewSessionReq) webRTCNewSessionRes {\n\tselect {\n\tcase s.chNew <- req:\n\t\treturn <-req.res\n\n\tcase <-s.ctx.Done():\n\t\treturn webRTCNewSessionRes{err: fmt.Errorf(\"terminated\"), errStatusCode: http.StatusInternalServerError}\n\t}\n}\n\n// addCandidates is called by webRTCHTTPServer through Server.\nfunc (s *session) addCandidates(\n\treq webRTCAddSessionCandidatesReq,\n) webRTCAddSessionCandidatesRes {\n\tselect {\n\tcase s.chAddCandidates <- req:\n\t\treturn <-req.res\n\n\tcase <-s.ctx.Done():\n\t\treturn webRTCAddSessionCandidatesRes{err: fmt.Errorf(\"terminated\")}\n\t}\n}\n\n// APIReaderDescribe implements reader.\nfunc (s *session) APIReaderDescribe() *defs.APIPathReader {\n\treturn &defs.APIPathReader{\n\t\tType: defs.APIPathReaderTypeWebRTCSession,\n\t\tID:   s.uuid.String(),\n\t}\n}\n\n// APISourceDescribe implements source.\nfunc (s *session) APISourceDescribe() *defs.APIPathSource {\n\treturn &defs.APIPathSource{\n\t\tType: defs.APIPathSourceTypeWebRTCSession,\n\t\tID:   s.uuid.String(),\n\t}\n}\n\nfunc (s *session) apiItem() *defs.APIWebRTCSession {\n\ts.mutex.RLock()\n\tdefer s.mutex.RUnlock()\n\n\tpeerConnectionEstablished := false\n\tlocalCandidate := \"\"\n\tremoteCandidate := \"\"\n\tbytesReceived := uint64(0)\n\tbytesSent := uint64(0)\n\trtpPacketsReceived := uint64(0)\n\trtpPacketsSent := uint64(0)\n\trtpPacketsLost := uint64(0)\n\trtpPacketsJitter := float64(0)\n\trtcpPacketsReceived := uint64(0)\n\trtcpPacketsSent := uint64(0)\n\toutboundFramesDiscarded := uint64(0)\n\n\tif s.pc != nil {\n\t\tpeerConnectionEstablished = true\n\t\tlocalCandidate = s.pc.LocalCandidate()\n\t\tremoteCandidate = s.pc.RemoteCandidate()\n\t\tstats := s.pc.Stats()\n\t\tbytesReceived = stats.BytesReceived\n\t\tbytesSent = stats.BytesSent\n\t\trtpPacketsReceived = stats.RTPPacketsReceived\n\t\trtpPacketsSent = stats.RTPPacketsSent\n\t\trtpPacketsLost = stats.RTPPacketsLost\n\t\trtpPacketsJitter = stats.RTPPacketsJitter\n\t\trtcpPacketsReceived = stats.RTCPPacketsReceived\n\t\trtcpPacketsSent = stats.RTCPPacketsSent\n\t}\n\n\tif s.reader != nil {\n\t\toutboundFramesDiscarded = s.reader.OutboundFramesDiscarded()\n\t}\n\n\treturn &defs.APIWebRTCSession{\n\t\tID:                        s.uuid,\n\t\tCreated:                   s.created,\n\t\tRemoteAddr:                s.req.remoteAddr,\n\t\tPeerConnectionEstablished: peerConnectionEstablished,\n\t\tLocalCandidate:            localCandidate,\n\t\tRemoteCandidate:           remoteCandidate,\n\t\tState: func() defs.APIWebRTCSessionState {\n\t\t\tif s.req.publish {\n\t\t\t\treturn defs.APIWebRTCSessionStatePublish\n\t\t\t}\n\t\t\treturn defs.APIWebRTCSessionStateRead\n\t\t}(),\n\t\tPath:                    s.req.pathName,\n\t\tQuery:                   s.req.httpRequest.URL.RawQuery,\n\t\tUser:                    s.user,\n\t\tInboundBytes:            bytesReceived,\n\t\tInboundRTPPackets:       rtpPacketsReceived,\n\t\tInboundRTPPacketsLost:   rtpPacketsLost,\n\t\tInboundRTPPacketsJitter: rtpPacketsJitter,\n\t\tInboundRTCPPackets:      rtcpPacketsReceived,\n\t\tOutboundBytes:           bytesSent,\n\t\tOutboundRTPPackets:      rtpPacketsSent,\n\t\tOutboundRTCPPackets:     rtcpPacketsSent,\n\t\tOutboundFramesDiscarded: outboundFramesDiscarded,\n\t\tBytesReceived:           bytesReceived,\n\t\tBytesSent:               bytesSent,\n\t\tRTPPacketsReceived:      rtpPacketsReceived,\n\t\tRTPPacketsSent:          rtpPacketsSent,\n\t\tRTPPacketsLost:          rtpPacketsLost,\n\t\tRTPPacketsJitter:        rtpPacketsJitter,\n\t\tRTCPPacketsReceived:     rtcpPacketsReceived,\n\t\tRTCPPacketsSent:         rtcpPacketsSent,\n\t}\n}\n"
  },
  {
    "path": "internal/staticsources/handler.go",
    "content": "// Package staticsources contains static source implementations.\npackage staticsources\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\tsshls \"github.com/bluenviron/mediamtx/internal/staticsources/hls\"\n\tssmpegts \"github.com/bluenviron/mediamtx/internal/staticsources/mpegts\"\n\tssrpicamera \"github.com/bluenviron/mediamtx/internal/staticsources/rpicamera\"\n\tssrtmp \"github.com/bluenviron/mediamtx/internal/staticsources/rtmp\"\n\tssrtp \"github.com/bluenviron/mediamtx/internal/staticsources/rtp\"\n\tssrtsp \"github.com/bluenviron/mediamtx/internal/staticsources/rtsp\"\n\tsssrt \"github.com/bluenviron/mediamtx/internal/staticsources/srt\"\n\tsswebrtc \"github.com/bluenviron/mediamtx/internal/staticsources/webrtc\"\n)\n\nconst (\n\tretryPause = 5 * time.Second\n)\n\nfunc emptyTimer() *time.Timer {\n\tt := time.NewTimer(0)\n\t<-t.C\n\treturn t\n}\n\nfunc resolveSource(s string, matches []string, query string) string {\n\tif len(matches) > 1 {\n\t\tfor i, ma := range matches[1:] {\n\t\t\ts = strings.ReplaceAll(s, \"$G\"+strconv.FormatInt(int64(i+1), 10), ma)\n\t\t}\n\t}\n\n\ts = strings.ReplaceAll(s, \"$MTX_QUERY\", query)\n\n\treturn s\n}\n\ntype staticSource interface {\n\tlogger.Writer\n\tRun(defs.StaticSourceRunParams) error\n\tAPISourceDescribe() *defs.APIPathSource\n}\n\ntype handlerPathManager interface {\n\tAddReader(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error)\n}\n\ntype handlerParent interface {\n\tlogger.Writer\n\tStaticSourceHandlerSetReady(context.Context, defs.PathSourceStaticSetReadyReq)\n\tStaticSourceHandlerSetNotReady(context.Context, defs.PathSourceStaticSetNotReadyReq)\n}\n\n// Handler is a static source handler.\ntype Handler struct {\n\tConf              *conf.Path\n\tLogLevel          conf.LogLevel\n\tDumpPackets       bool\n\tReadTimeout       conf.Duration\n\tWriteTimeout      conf.Duration\n\tWriteQueueSize    int\n\tUDPReadBufferSize uint\n\tRTPMaxPayloadSize int\n\tMatches           []string\n\tPathManager       handlerPathManager\n\tParent            handlerParent\n\n\tctx       context.Context\n\tctxCancel func()\n\tinstance  staticSource\n\trunning   bool\n\tquery     string\n\n\t// in\n\tchReloadConf          chan *conf.Path\n\tchInstanceSetReady    chan defs.PathSourceStaticSetReadyReq\n\tchInstanceSetNotReady chan defs.PathSourceStaticSetNotReadyReq\n\n\t// out\n\tdone chan struct{}\n}\n\n// Initialize initializes Handler.\nfunc (s *Handler) Initialize() {\n\ts.chReloadConf = make(chan *conf.Path)\n\ts.chInstanceSetReady = make(chan defs.PathSourceStaticSetReadyReq)\n\ts.chInstanceSetNotReady = make(chan defs.PathSourceStaticSetNotReadyReq)\n\n\tswitch {\n\tcase strings.HasPrefix(s.Conf.Source, \"rtsp://\") ||\n\t\tstrings.HasPrefix(s.Conf.Source, \"rtsps://\") ||\n\t\tstrings.HasPrefix(s.Conf.Source, \"rtsp+http://\") ||\n\t\tstrings.HasPrefix(s.Conf.Source, \"rtsps+http://\") ||\n\t\tstrings.HasPrefix(s.Conf.Source, \"rtsp+ws://\") ||\n\t\tstrings.HasPrefix(s.Conf.Source, \"rtsps+ws://\"):\n\t\ts.instance = &ssrtsp.Source{\n\t\t\tDumpPackets:       s.DumpPackets,\n\t\t\tReadTimeout:       s.ReadTimeout,\n\t\t\tWriteTimeout:      s.WriteTimeout,\n\t\t\tWriteQueueSize:    s.WriteQueueSize,\n\t\t\tUDPReadBufferSize: s.UDPReadBufferSize,\n\t\t\tParent:            s,\n\t\t}\n\n\tcase strings.HasPrefix(s.Conf.Source, \"rtmp://\") ||\n\t\tstrings.HasPrefix(s.Conf.Source, \"rtmps://\"):\n\t\ts.instance = &ssrtmp.Source{\n\t\t\tDumpPackets:  s.DumpPackets,\n\t\t\tReadTimeout:  s.ReadTimeout,\n\t\t\tWriteTimeout: s.WriteTimeout,\n\t\t\tParent:       s,\n\t\t}\n\n\tcase strings.HasPrefix(s.Conf.Source, \"http://\") ||\n\t\tstrings.HasPrefix(s.Conf.Source, \"https://\"):\n\t\ts.instance = &sshls.Source{\n\t\t\tDumpPackets: s.DumpPackets,\n\t\t\tReadTimeout: s.ReadTimeout,\n\t\t\tParent:      s,\n\t\t}\n\n\tcase strings.HasPrefix(s.Conf.Source, \"udp://\") ||\n\t\tstrings.HasPrefix(s.Conf.Source, \"udp+mpegts://\") ||\n\t\tstrings.HasPrefix(s.Conf.Source, \"unix+mpegts://\"):\n\t\ts.instance = &ssmpegts.Source{\n\t\t\tDumpPackets:       s.DumpPackets,\n\t\t\tReadTimeout:       s.ReadTimeout,\n\t\t\tUDPReadBufferSize: s.UDPReadBufferSize,\n\t\t\tParent:            s,\n\t\t}\n\n\tcase strings.HasPrefix(s.Conf.Source, \"srt://\"):\n\t\ts.instance = &sssrt.Source{\n\t\t\tReadTimeout: s.ReadTimeout,\n\t\t\tParent:      s,\n\t\t}\n\n\tcase strings.HasPrefix(s.Conf.Source, \"whep://\") ||\n\t\tstrings.HasPrefix(s.Conf.Source, \"wheps://\"):\n\t\ts.instance = &sswebrtc.Source{\n\t\t\tDumpPackets:       s.DumpPackets,\n\t\t\tReadTimeout:       s.ReadTimeout,\n\t\t\tUDPReadBufferSize: s.UDPReadBufferSize,\n\t\t\tParent:            s,\n\t\t}\n\n\tcase strings.HasPrefix(s.Conf.Source, \"udp+rtp://\") ||\n\t\tstrings.HasPrefix(s.Conf.Source, \"unix+rtp://\"):\n\t\ts.instance = &ssrtp.Source{\n\t\t\tDumpPackets:       s.DumpPackets,\n\t\t\tReadTimeout:       s.ReadTimeout,\n\t\t\tUDPReadBufferSize: s.UDPReadBufferSize,\n\t\t\tParent:            s,\n\t\t}\n\n\tcase s.Conf.Source == \"rpiCamera\":\n\t\ts.instance = &ssrpicamera.Source{\n\t\t\tRTPMaxPayloadSize: s.RTPMaxPayloadSize,\n\t\t\tLogLevel:          s.LogLevel,\n\t\t\tParent:            s,\n\t\t}\n\n\tdefault:\n\t\tpanic(\"should not happen\")\n\t}\n}\n\n// Close closes Handler.\nfunc (s *Handler) Close(reason string) {\n\ts.Stop(reason)\n}\n\n// Start starts Handler.\nfunc (s *Handler) Start(onDemand bool, query string) {\n\tif s.running {\n\t\tpanic(\"should not happen\")\n\t}\n\n\ts.running = true\n\ts.query = query\n\ts.ctx, s.ctxCancel = context.WithCancel(context.Background())\n\ts.done = make(chan struct{})\n\n\ts.instance.Log(logger.Info, \"started%s\",\n\t\tfunc() string {\n\t\t\tif onDemand {\n\t\t\t\treturn \" on demand\"\n\t\t\t}\n\t\t\treturn \"\"\n\t\t}())\n\n\tgo s.run()\n}\n\n// Stop stops Handler.\nfunc (s *Handler) Stop(reason string) {\n\tif !s.running {\n\t\tpanic(\"should not happen\")\n\t}\n\n\ts.running = false\n\n\ts.instance.Log(logger.Info, \"stopped: %s\", reason)\n\n\ts.ctxCancel()\n\n\t// we must wait since s.ctx is not thread safe\n\t<-s.done\n}\n\n// Log implements logger.Writer.\nfunc (s *Handler) Log(level logger.Level, format string, args ...any) {\n\ts.Parent.Log(level, format, args...)\n}\n\nfunc (s *Handler) run() {\n\tdefer close(s.done)\n\n\tvar runCtx context.Context\n\tvar runCtxCancel func()\n\trunErr := make(chan error)\n\trunReloadConf := make(chan *conf.Path)\n\n\trecreate := func() {\n\t\tresolvedSource := resolveSource(s.Conf.Source, s.Matches, s.query)\n\n\t\trunCtx, runCtxCancel = context.WithCancel(context.Background())\n\t\tgo func() {\n\t\t\trunErr <- s.instance.Run(defs.StaticSourceRunParams{\n\t\t\t\tContext:        runCtx,\n\t\t\t\tResolvedSource: resolvedSource,\n\t\t\t\tConf:           s.Conf,\n\t\t\t\tReloadConf:     runReloadConf,\n\t\t\t})\n\t\t}()\n\t}\n\n\trecreate()\n\n\trecreating := false\n\trecreateTimer := emptyTimer()\n\n\tfor {\n\t\tselect {\n\t\tcase err := <-runErr:\n\t\t\trunCtxCancel()\n\t\t\ts.instance.Log(logger.Error, err.Error())\n\t\t\trecreating = true\n\t\t\trecreateTimer = time.NewTimer(retryPause)\n\n\t\tcase req := <-s.chInstanceSetReady:\n\t\t\ts.Parent.StaticSourceHandlerSetReady(s.ctx, req)\n\n\t\tcase req := <-s.chInstanceSetNotReady:\n\t\t\ts.Parent.StaticSourceHandlerSetNotReady(s.ctx, req)\n\n\t\tcase newConf := <-s.chReloadConf:\n\t\t\ts.Conf = newConf\n\t\t\tif !recreating {\n\t\t\t\tcReloadConf := runReloadConf\n\t\t\t\tcInnerCtx := runCtx\n\t\t\t\tgo func() {\n\t\t\t\t\tselect {\n\t\t\t\t\tcase cReloadConf <- newConf:\n\t\t\t\t\tcase <-cInnerCtx.Done():\n\t\t\t\t\t}\n\t\t\t\t}()\n\t\t\t}\n\n\t\tcase <-recreateTimer.C:\n\t\t\trecreate()\n\t\t\trecreating = false\n\n\t\tcase <-s.ctx.Done():\n\t\t\tif !recreating {\n\t\t\t\trunCtxCancel()\n\t\t\t\t<-runErr\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t}\n}\n\n// ReloadConf is called by path.\nfunc (s *Handler) ReloadConf(newConf *conf.Path) {\n\tctx := s.ctx\n\n\tif !s.running {\n\t\treturn\n\t}\n\n\tgo func() {\n\t\tselect {\n\t\tcase s.chReloadConf <- newConf:\n\t\tcase <-ctx.Done():\n\t\t}\n\t}()\n}\n\n// APISourceDescribe instanceements source.\nfunc (s *Handler) APISourceDescribe() *defs.APIPathSource {\n\treturn s.instance.APISourceDescribe()\n}\n\n// SetReady is called by a staticSource.\nfunc (s *Handler) SetReady(req defs.PathSourceStaticSetReadyReq) defs.PathSourceStaticSetReadyRes {\n\treq.Res = make(chan defs.PathSourceStaticSetReadyRes)\n\tselect {\n\tcase s.chInstanceSetReady <- req:\n\t\treturn <-req.Res\n\n\tcase <-s.ctx.Done():\n\t\treturn defs.PathSourceStaticSetReadyRes{Err: fmt.Errorf(\"terminated\")}\n\t}\n}\n\n// SetNotReady is called by a staticSource.\nfunc (s *Handler) SetNotReady(req defs.PathSourceStaticSetNotReadyReq) {\n\treq.Res = make(chan struct{})\n\tselect {\n\tcase s.chInstanceSetNotReady <- req:\n\t\t<-req.Res\n\tcase <-s.ctx.Done():\n\t}\n}\n\n// AddReader is called by a staticSource.\nfunc (s *Handler) AddReader(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error) {\n\treturn s.PathManager.AddReader(req)\n}\n"
  },
  {
    "path": "internal/staticsources/hls/source.go",
    "content": "// Package hls contains the HLS static source.\npackage hls\n\nimport (\n\t\"net\"\n\t\"net/http\"\n\t\"net/http/cookiejar\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gohlslib/v2\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/errordumper\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/packetdumper\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/hls\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/tls\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n)\n\ntype parent interface {\n\tlogger.Writer\n\tSetReady(req defs.PathSourceStaticSetReadyReq) defs.PathSourceStaticSetReadyRes\n\tSetNotReady(req defs.PathSourceStaticSetNotReadyReq)\n}\n\n// Source is a HLS static source.\ntype Source struct {\n\tDumpPackets bool\n\tReadTimeout conf.Duration\n\tParent      parent\n}\n\n// Log implements logger.Writer.\nfunc (s *Source) Log(level logger.Level, format string, args ...any) {\n\ts.Parent.Log(level, \"[HLS source] \"+format, args...)\n}\n\n// Run implements StaticSource.\nfunc (s *Source) Run(params defs.StaticSourceRunParams) error {\n\tvar subStream *stream.SubStream\n\n\tdefer func() {\n\t\tif subStream != nil {\n\t\t\ts.Parent.SetNotReady(defs.PathSourceStaticSetNotReadyReq{})\n\t\t}\n\t}()\n\n\tdecodeErrors := &errordumper.Dumper{\n\t\tOnReport: func(val uint64, last error) {\n\t\t\tif val == 1 {\n\t\t\t\ts.Log(logger.Warn, \"decode error: %v\", last)\n\t\t\t} else {\n\t\t\t\ts.Log(logger.Warn, \"%d decode errors, last was: %v\", val, last)\n\t\t\t}\n\t\t},\n\t}\n\n\tdecodeErrors.Start()\n\tdefer decodeErrors.Stop()\n\n\tu, err := url.Parse(params.ResolvedSource)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdialContext := (&net.Dialer{}).DialContext\n\n\tif s.DumpPackets {\n\t\tdialContext = (&packetdumper.DialContext{\n\t\t\tPrefix:      \"hls_source_conn\",\n\t\t\tDialContext: dialContext,\n\t\t}).Do\n\t}\n\n\ttr := &http.Transport{\n\t\tDialContext:     dialContext,\n\t\tTLSClientConfig: tls.MakeConfig(u.Hostname(), params.Conf.SourceFingerprint),\n\t}\n\tdefer tr.CloseIdleConnections()\n\n\tjar, _ := cookiejar.New(nil)\n\n\tvar c *gohlslib.Client\n\tc = &gohlslib.Client{\n\t\tURI: params.ResolvedSource,\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout:   time.Duration(s.ReadTimeout),\n\t\t\tTransport: tr,\n\t\t\tJar:       jar,\n\t\t},\n\t\tOnDownloadPrimaryPlaylist: func(u string) {\n\t\t\ts.Log(logger.Debug, \"downloading primary playlist %v\", u)\n\t\t},\n\t\tOnDownloadStreamPlaylist: func(u string) {\n\t\t\ts.Log(logger.Debug, \"downloading stream playlist %v\", u)\n\t\t},\n\t\tOnDownloadSegment: func(u string) {\n\t\t\ts.Log(logger.Debug, \"downloading segment %v\", u)\n\t\t},\n\t\tOnDownloadPart: func(u string) {\n\t\t\ts.Log(logger.Debug, \"downloading part %v\", u)\n\t\t},\n\t\tOnDecodeError: func(err error) {\n\t\t\tdecodeErrors.Add(err)\n\t\t},\n\t\tOnTracks: func(tracks []*gohlslib.Track) error {\n\t\t\tmedias, err2 := hls.ToStream(c, tracks, params.Conf, &subStream)\n\t\t\tif err2 != nil {\n\t\t\t\treturn err2\n\t\t\t}\n\n\t\t\tres := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{\n\t\t\t\tDesc:          &description.Session{Medias: medias},\n\t\t\t\tUseRTPPackets: false,\n\t\t\t\tReplaceNTP:    false,\n\t\t\t})\n\t\t\tif res.Err != nil {\n\t\t\t\treturn res.Err\n\t\t\t}\n\n\t\t\tsubStream = res.SubStream\n\n\t\t\treturn nil\n\t\t},\n\t}\n\n\terr = c.Start()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\twaitErr := make(chan error)\n\tgo func() {\n\t\twaitErr <- c.Wait2()\n\t}()\n\n\tfor {\n\t\tselect {\n\t\tcase err = <-waitErr:\n\t\t\tc.Close()\n\t\t\treturn err\n\n\t\tcase <-params.ReloadConf:\n\n\t\tcase <-params.Context.Done():\n\t\t\tc.Close()\n\t\t\t<-waitErr\n\t\t\treturn nil\n\t\t}\n\t}\n}\n\n// APISourceDescribe implements StaticSource.\nfunc (*Source) APISourceDescribe() *defs.APIPathSource {\n\treturn &defs.APIPathSource{\n\t\tType: defs.APIPathSourceTypeHLSSource,\n\t\tID:   \"\",\n\t}\n}\n"
  },
  {
    "path": "internal/staticsources/hls/source_test.go",
    "content": "package hls\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"net/http\"\n\t\"testing\"\n\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts\"\n\ttscodecs \"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts/codecs\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n)\n\nfunc TestSource(t *testing.T) {\n\ttrack1 := &mpegts.Track{\n\t\tCodec: &tscodecs.H264{},\n\t}\n\n\ttrack2 := &mpegts.Track{\n\t\tCodec: &tscodecs.MPEG4Audio{\n\t\t\tConfig: mpeg4audio.AudioSpecificConfig{\n\t\t\t\tType:         2,\n\t\t\t\tSampleRate:   44100,\n\t\t\t\tChannelCount: 2,\n\t\t\t},\n\t\t},\n\t}\n\n\ttracks := []*mpegts.Track{\n\t\ttrack1,\n\t\ttrack2,\n\t}\n\n\ts := &http.Server{\n\t\tHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tswitch {\n\t\t\tcase r.Method == http.MethodGet && r.URL.Path == \"/stream.m3u8\":\n\t\t\t\tw.Header().Set(\"Content-Type\", `application/vnd.apple.mpegurl`)\n\t\t\t\tw.Write([]byte(\"#EXTM3U\\n\" +\n\t\t\t\t\t\"#EXT-X-VERSION:3\\n\" +\n\t\t\t\t\t\"#EXT-X-ALLOW-CACHE:NO\\n\" +\n\t\t\t\t\t\"#EXT-X-TARGETDURATION:2\\n\" +\n\t\t\t\t\t\"#EXT-X-MEDIA-SEQUENCE:0\\n\" +\n\t\t\t\t\t\"#EXTINF:2,\\n\" +\n\t\t\t\t\t\"segment1.ts\\n\" +\n\t\t\t\t\t\"#EXTINF:2,\\n\" +\n\t\t\t\t\t\"segment2.ts\\n\" +\n\t\t\t\t\t\"#EXTINF:2,\\n\" +\n\t\t\t\t\t\"segment2.ts\\n\" +\n\t\t\t\t\t\"#EXT-X-ENDLIST\\n\"))\n\n\t\t\tcase r.Method == http.MethodGet && r.URL.Path == \"/segment1.ts\":\n\t\t\t\tw.Header().Set(\"Content-Type\", `video/MP2T`)\n\n\t\t\t\tw := &mpegts.Writer{W: w, Tracks: tracks}\n\t\t\t\terr := w.Initialize()\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\terr = w.WriteMPEG4Audio(track2, 1*90000, [][]byte{{1, 2, 3, 4}})\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\terr = w.WriteH264(track1, 2*90000, 2*90000, [][]byte{\n\t\t\t\t\t{7, 1, 2, 3}, // SPS\n\t\t\t\t\t{8},          // PPS\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\tcase r.Method == http.MethodGet && r.URL.Path == \"/segment2.ts\":\n\t\t\t\tw.Header().Set(\"Content-Type\", `video/MP2T`)\n\n\t\t\t\tw := &mpegts.Writer{W: w, Tracks: tracks}\n\t\t\t\terr := w.Initialize()\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\terr = w.WriteMPEG4Audio(track2, 3*90000, [][]byte{{1, 2, 3, 4}})\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t}),\n\t}\n\n\tln, err := net.Listen(\"tcp\", \"localhost:5780\")\n\trequire.NoError(t, err)\n\n\tgo s.Serve(ln)\n\tdefer s.Shutdown(context.Background())\n\n\tp := &test.StaticSourceParent{}\n\tp.Initialize()\n\tdefer p.Close()\n\n\tso := &Source{\n\t\tParent: p,\n\t}\n\n\tdone := make(chan struct{})\n\tdefer func() { <-done }()\n\n\tctx, ctxCancel := context.WithCancel(context.Background())\n\tdefer ctxCancel()\n\n\treloadConf := make(chan *conf.Path)\n\n\tgo func() {\n\t\tso.Run(defs.StaticSourceRunParams{ //nolint:errcheck\n\t\t\tContext:        ctx,\n\t\t\tResolvedSource: \"http://localhost:5780/stream.m3u8\",\n\t\t\tConf:           &conf.Path{},\n\t\t\tReloadConf:     reloadConf,\n\t\t})\n\t\tclose(done)\n\t}()\n\n\t<-p.Unit\n\n\t// the source must be listening on ReloadConf\n\treloadConf <- nil\n}\n\nfunc TestSourceCookie(t *testing.T) {\n\ttrack1 := &mpegts.Track{\n\t\tCodec: &tscodecs.H264{},\n\t}\n\n\ttracks := []*mpegts.Track{track1}\n\n\ts := &http.Server{\n\t\tHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tswitch {\n\t\t\tcase r.Method == http.MethodGet && r.URL.Path == \"/stream.m3u8\":\n\t\t\t\tw.Header().Set(\"Set-Cookie\", \"testcookie=123456; Path=/; Max-Age=3600\")\n\t\t\t\tw.Header().Set(\"Content-Type\", `application/vnd.apple.mpegurl`)\n\t\t\t\tw.Write([]byte(\"#EXTM3U\\n\" +\n\t\t\t\t\t\"#EXT-X-VERSION:3\\n\" +\n\t\t\t\t\t\"#EXT-X-ALLOW-CACHE:NO\\n\" +\n\t\t\t\t\t\"#EXT-X-TARGETDURATION:2\\n\" +\n\t\t\t\t\t\"#EXT-X-MEDIA-SEQUENCE:0\\n\" +\n\t\t\t\t\t\"#EXTINF:2,\\n\" +\n\t\t\t\t\t\"segment1.ts\\n\" +\n\t\t\t\t\t\"#EXTINF:2,\\n\" +\n\t\t\t\t\t\"segment2.ts\\n\" +\n\t\t\t\t\t\"#EXTINF:2,\\n\" +\n\t\t\t\t\t\"segment2.ts\\n\" +\n\t\t\t\t\t\"#EXT-X-ENDLIST\\n\"))\n\n\t\t\tcase r.Method == http.MethodGet && r.URL.Path == \"/segment1.ts\":\n\t\t\t\trequire.Equal(t, \"testcookie=123456\", r.Header.Get(\"Cookie\"))\n\t\t\t\tw.Header().Set(\"Content-Type\", `video/MP2T`)\n\n\t\t\t\tw := &mpegts.Writer{W: w, Tracks: tracks}\n\t\t\t\terr := w.Initialize()\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\terr = w.WriteH264(track1, 2*90000, 2*90000, [][]byte{\n\t\t\t\t\t{7, 1, 2, 3}, // SPS\n\t\t\t\t\t{8},          // PPS\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\tcase r.Method == http.MethodGet && r.URL.Path == \"/segment2.ts\":\n\t\t\t\tw.Header().Set(\"Content-Type\", `video/MP2T`)\n\n\t\t\t\tw := &mpegts.Writer{W: w, Tracks: tracks}\n\t\t\t\terr := w.Initialize()\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\t\t}),\n\t}\n\n\tln, err := net.Listen(\"tcp\", \"localhost:5780\")\n\trequire.NoError(t, err)\n\n\tgo s.Serve(ln)\n\tdefer s.Shutdown(context.Background())\n\n\tp := &test.StaticSourceParent{}\n\tp.Initialize()\n\tdefer p.Close()\n\n\tso := &Source{\n\t\tParent: p,\n\t}\n\n\tdone := make(chan struct{})\n\tdefer func() { <-done }()\n\n\tctx, ctxCancel := context.WithCancel(context.Background())\n\tdefer ctxCancel()\n\n\tgo func() {\n\t\tso.Run(defs.StaticSourceRunParams{ //nolint:errcheck\n\t\t\tContext:        ctx,\n\t\t\tResolvedSource: \"http://localhost:5780/stream.m3u8\",\n\t\t\tConf:           &conf.Path{},\n\t\t})\n\t\tclose(done)\n\t}()\n\n\t<-p.Unit\n}\n"
  },
  {
    "path": "internal/staticsources/mpegts/source.go",
    "content": "// Package mpegts contains the MPEG-TS static source.\npackage mpegts\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/errordumper\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/packetdumper\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/mpegts\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/udp\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/unix\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n)\n\ntype parent interface {\n\tlogger.Writer\n\tSetReady(req defs.PathSourceStaticSetReadyReq) defs.PathSourceStaticSetReadyRes\n\tSetNotReady(req defs.PathSourceStaticSetNotReadyReq)\n}\n\n// Source is a MPEG-TS static source.\ntype Source struct {\n\tDumpPackets       bool\n\tReadTimeout       conf.Duration\n\tUDPReadBufferSize uint\n\tParent            parent\n}\n\n// Log implements logger.Writer.\nfunc (s *Source) Log(level logger.Level, format string, args ...any) {\n\ts.Parent.Log(level, \"[MPEG-TS source] \"+format, args...)\n}\n\n// Run implements StaticSource.\nfunc (s *Source) Run(params defs.StaticSourceRunParams) error {\n\ts.Log(logger.Debug, \"connecting\")\n\n\tu, err := url.Parse(params.ResolvedSource)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar nc net.Conn\n\n\tswitch u.Scheme {\n\tcase \"unix+mpegts\":\n\t\tparams := unix.URLToParams(u)\n\t\tl := &unix.Listener{\n\t\t\tPath: params.Path,\n\t\t}\n\t\terr = l.Initialize()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tnc = l\n\n\tdefault:\n\t\tudpReadBufferSize := s.UDPReadBufferSize\n\t\tif params.Conf.MPEGTSUDPReadBufferSize != nil {\n\t\t\tudpReadBufferSize = *params.Conf.MPEGTSUDPReadBufferSize\n\t\t}\n\n\t\tlistenPacket := net.ListenPacket\n\n\t\tif s.DumpPackets {\n\t\t\tlistenPacket = func(network, address string) (net.PacketConn, error) {\n\t\t\t\tpc, err2 := net.ListenPacket(network, address)\n\t\t\t\tif err2 != nil {\n\t\t\t\t\treturn nil, err2\n\t\t\t\t}\n\n\t\t\t\td := &packetdumper.PacketConn{\n\t\t\t\t\tPrefix:     \"mpegts_source_packetconn\",\n\t\t\t\t\tPacketConn: pc,\n\t\t\t\t}\n\t\t\t\terr2 = d.Initialize()\n\t\t\t\tif err2 != nil {\n\t\t\t\t\treturn nil, err2\n\t\t\t\t}\n\n\t\t\t\treturn d, nil\n\t\t\t}\n\t\t}\n\n\t\tparams := udp.URLToParams(u)\n\t\tl := &udp.Listener{\n\t\t\tAddress:           params.Address,\n\t\t\tSource:            params.Source,\n\t\t\tIntfName:          params.IntfName,\n\t\t\tUDPReadBufferSize: int(udpReadBufferSize),\n\t\t\tListenPacket:      listenPacket,\n\t\t}\n\t\terr = l.Initialize()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tnc = l\n\t}\n\n\treaderErr := make(chan error)\n\tgo func() {\n\t\treaderErr <- s.runReader(nc)\n\t}()\n\n\tfor {\n\t\tselect {\n\t\tcase err = <-readerErr:\n\t\t\tnc.Close()\n\t\t\treturn err\n\n\t\tcase <-params.ReloadConf:\n\n\t\tcase <-params.Context.Done():\n\t\t\tnc.Close()\n\t\t\t<-readerErr\n\t\t\treturn fmt.Errorf(\"terminated\")\n\t\t}\n\t}\n}\n\nfunc (s *Source) runReader(nc net.Conn) error {\n\tnc.SetReadDeadline(time.Now().Add(time.Duration(s.ReadTimeout)))\n\tmr := &mpegts.EnhancedReader{R: nc}\n\terr := mr.Initialize()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdecodeErrors := &errordumper.Dumper{\n\t\tOnReport: func(val uint64, last error) {\n\t\t\tif val == 1 {\n\t\t\t\ts.Log(logger.Warn, \"decode error: %v\", last)\n\t\t\t} else {\n\t\t\t\ts.Log(logger.Warn, \"%d decode errors, last was: %v\", val, last)\n\t\t\t}\n\t\t},\n\t}\n\n\tdecodeErrors.Start()\n\tdefer decodeErrors.Stop()\n\n\tmr.OnDecodeError(func(err error) {\n\t\tdecodeErrors.Add(err)\n\t})\n\n\tvar subStream *stream.SubStream\n\n\tmedias, err := mpegts.ToStream(mr, &subStream, s)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tres := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{\n\t\tDesc:          &description.Session{Medias: medias},\n\t\tUseRTPPackets: false,\n\t\tReplaceNTP:    true,\n\t})\n\tif res.Err != nil {\n\t\treturn res.Err\n\t}\n\n\tdefer s.Parent.SetNotReady(defs.PathSourceStaticSetNotReadyReq{})\n\n\tsubStream = res.SubStream\n\n\tfor {\n\t\tnc.SetReadDeadline(time.Now().Add(time.Duration(s.ReadTimeout)))\n\t\terr = mr.Read()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n}\n\n// APISourceDescribe implements StaticSource.\nfunc (*Source) APISourceDescribe() *defs.APIPathSource {\n\treturn &defs.APIPathSource{\n\t\tType: defs.APIPathSourceTypeMPEGTSSource,\n\t\tID:   \"\",\n\t}\n}\n"
  },
  {
    "path": "internal/staticsources/mpegts/source_test.go",
    "content": "package mpegts\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts\"\n\ttscodecs \"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts/codecs\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n)\n\nfunc multicastCapableInterface(t *testing.T) string {\n\tintfs, err := net.Interfaces()\n\trequire.NoError(t, err)\n\n\tfor _, intf := range intfs {\n\t\tif (intf.Flags & net.FlagMulticast) != 0 {\n\t\t\treturn intf.Name\n\t\t}\n\t}\n\n\tt.Errorf(\"unable to find a multicast IP\")\n\treturn \"\"\n}\n\nfunc TestSourceUDP(t *testing.T) {\n\tfor _, ca := range []string{\n\t\t\"unicast\",\n\t\t\"multicast\",\n\t\t\"multicast with interface\",\n\t\t\"unicast with source\",\n\t} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tvar src string\n\n\t\t\tswitch ca {\n\t\t\tcase \"unicast\":\n\t\t\t\tsrc = \"udp+mpegts://127.0.0.1:9001\"\n\n\t\t\tcase \"multicast\":\n\t\t\t\tsrc = \"udp+mpegts://238.0.0.1:9001\"\n\n\t\t\tcase \"multicast with interface\":\n\t\t\t\tsrc = \"udp+mpegts://238.0.0.1:9001?interface=\" + multicastCapableInterface(t)\n\n\t\t\tcase \"unicast with source\":\n\t\t\t\tsrc = \"udp+mpegts://127.0.0.1:9001?source=127.0.1.1\"\n\t\t\t}\n\n\t\t\tp := &test.StaticSourceParent{}\n\t\t\tp.Initialize()\n\t\t\tdefer p.Close()\n\n\t\t\tso := &Source{\n\t\t\t\tReadTimeout: conf.Duration(10 * time.Second),\n\t\t\t\tParent:      p,\n\t\t\t}\n\n\t\t\tdone := make(chan struct{})\n\t\t\tdefer func() { <-done }()\n\n\t\t\tctx, ctxCancel := context.WithCancel(context.Background())\n\t\t\tdefer ctxCancel()\n\n\t\t\treloadConf := make(chan *conf.Path)\n\n\t\t\tgo func() {\n\t\t\t\tso.Run(defs.StaticSourceRunParams{ //nolint:errcheck\n\t\t\t\t\tContext:        ctx,\n\t\t\t\t\tResolvedSource: src,\n\t\t\t\t\tConf:           &conf.Path{},\n\t\t\t\t\tReloadConf:     reloadConf,\n\t\t\t\t})\n\t\t\t\tclose(done)\n\t\t\t}()\n\n\t\t\ttime.Sleep(50 * time.Millisecond)\n\n\t\t\tvar dest string\n\n\t\t\tswitch ca {\n\t\t\tcase \"unicast\":\n\t\t\t\tdest = \"127.0.0.1:9001\"\n\n\t\t\tcase \"multicast\":\n\t\t\t\tdest = \"238.0.0.1:9001\"\n\n\t\t\tcase \"multicast with interface\":\n\t\t\t\tdest = \"238.0.0.1:9001\"\n\n\t\t\tcase \"unicast with source\":\n\t\t\t\tdest = \"127.0.0.1:9001\"\n\t\t\t}\n\n\t\t\tudest, err := net.ResolveUDPAddr(\"udp\", dest)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tvar usrc *net.UDPAddr\n\t\t\tif ca == \"unicast with source\" {\n\t\t\t\tusrc, err = net.ResolveUDPAddr(\"udp\", \"127.0.1.1:9022\")\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\tconn, err := net.DialUDP(\"udp\", usrc, udest)\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer conn.Close() //nolint:errcheck\n\n\t\t\ttrack := &mpegts.Track{\n\t\t\t\tCodec: &tscodecs.H264{},\n\t\t\t}\n\n\t\t\tbw := bufio.NewWriter(conn)\n\t\t\tw := &mpegts.Writer{W: bw, Tracks: []*mpegts.Track{track}}\n\t\t\terr = w.Initialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = w.WriteH264(track, 0, 0, [][]byte{{ // IDR\n\t\t\t\t5, 1,\n\t\t\t}})\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = bw.Flush()\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = w.WriteH264(track, 0, 0, [][]byte{{ // non-IDR\n\t\t\t\t5, 2,\n\t\t\t}})\n\t\t\trequire.NoError(t, err)\n\n\t\t\terr = bw.Flush()\n\t\t\trequire.NoError(t, err)\n\n\t\t\t<-p.Unit\n\n\t\t\t// the source must be listening on ReloadConf\n\t\t\treloadConf <- nil\n\t\t})\n\t}\n}\n\nfunc TestSourceUnixSocket(t *testing.T) {\n\tfor _, ca := range []string{\n\t\t\"relative\",\n\t\t\"absolute\",\n\t} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tvar pa string\n\t\t\tif ca == \"relative\" {\n\t\t\t\tpa = \"test_mpegts.sock\"\n\t\t\t} else {\n\t\t\t\tpa = filepath.Join(os.TempDir(), \"test_mpegts.sock\")\n\t\t\t}\n\n\t\t\tfunc() {\n\t\t\t\tp := &test.StaticSourceParent{}\n\t\t\t\tp.Initialize()\n\t\t\t\tdefer p.Close()\n\n\t\t\t\tso := &Source{\n\t\t\t\t\tReadTimeout: conf.Duration(10 * time.Second),\n\t\t\t\t\tParent:      p,\n\t\t\t\t}\n\n\t\t\t\tdone := make(chan struct{})\n\t\t\t\tdefer func() { <-done }()\n\n\t\t\t\tctx, ctxCancel := context.WithCancel(context.Background())\n\t\t\t\tdefer ctxCancel()\n\n\t\t\t\tgo func() {\n\t\t\t\t\tso.Run(defs.StaticSourceRunParams{ //nolint:errcheck\n\t\t\t\t\t\tContext:        ctx,\n\t\t\t\t\t\tResolvedSource: \"unix+mpegts://\" + pa,\n\t\t\t\t\t\tConf:           &conf.Path{},\n\t\t\t\t\t})\n\t\t\t\t\tclose(done)\n\t\t\t\t}()\n\n\t\t\t\ttime.Sleep(50 * time.Millisecond)\n\n\t\t\t\t_, err := os.Stat(pa)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tconn, err := net.Dial(\"unix\", pa)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\ttrack := &mpegts.Track{\n\t\t\t\t\tCodec: &tscodecs.H264{},\n\t\t\t\t}\n\n\t\t\t\tbw := bufio.NewWriter(conn)\n\t\t\t\tw := &mpegts.Writer{W: bw, Tracks: []*mpegts.Track{track}}\n\t\t\t\terr = w.Initialize()\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\terr = w.WriteH264(track, 0, 0, [][]byte{{ // IDR\n\t\t\t\t\t5, 1,\n\t\t\t\t}})\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\terr = bw.Flush()\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tconn.Close() // trigger a flush\n\n\t\t\t\t<-p.Unit\n\t\t\t}()\n\n\t\t\t_, err := os.Stat(pa)\n\t\t\trequire.Error(t, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/staticsources/rpicamera/camera_arm32_.go",
    "content": "//go:build linux && arm\n\npackage rpicamera\n\nimport (\n\t\"embed\"\n)\n\n//go:embed mtxrpicam_32/*\nvar mtxrpicam embed.FS\n"
  },
  {
    "path": "internal/staticsources/rpicamera/camera_arm64_.go",
    "content": "//go:build linux && arm64\n\npackage rpicamera\n\nimport (\n\t\"embed\"\n)\n\n//go:embed mtxrpicam_64/*\nvar mtxrpicam embed.FS\n"
  },
  {
    "path": "internal/staticsources/rpicamera/camera_arm_.go",
    "content": "//go:build (linux && arm) || (linux && arm64)\n\npackage rpicamera\n\nimport (\n\t\"debug/elf\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"syscall\"\n\t\"time\"\n\t\"unsafe\"\n\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/h264\"\n)\n\nconst (\n\tlibraryToCheckArchitecture = \"libc.so.6\"\n\tdumpPrefix                 = \"/dev/shm/mediamtx-rpicamera-\"\n\texecutableName             = \"mtxrpicam\"\n)\n\nvar (\n\tdumpMutex sync.Mutex\n\tdumpCount = 0\n\tdumpPath  = \"\"\n)\n\nfunc ntpTime() syscall.Timespec {\n\tvar t syscall.Timespec\n\tsyscall.Syscall(syscall.SYS_CLOCK_GETTIME, 0, uintptr(unsafe.Pointer(&t)), 0)\n\treturn t\n}\n\nfunc monotonicTime() syscall.Timespec {\n\tvar t syscall.Timespec\n\tsyscall.Syscall(syscall.SYS_CLOCK_GETTIME, 1, uintptr(unsafe.Pointer(&t)), 0)\n\treturn t\n}\n\nfunc multiplyAndDivide(v, m, d int64) int64 {\n\tsecs := v / d\n\tdec := v % d\n\treturn (secs*m + dec*m/d)\n}\n\nfunc getArchitecture(libPath string) (bool, error) {\n\tf, err := os.Open(libPath)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tdefer f.Close()\n\n\tef, err := elf.NewFile(f)\n\tif err != nil {\n\t\treturn false, err\n\t}\n\tdefer ef.Close()\n\n\treturn (ef.FileHeader.Class == elf.ELFCLASS64), nil\n}\n\nfunc checkArchitecture() error {\n\tbyts, err := exec.Command(\"/sbin/ldconfig\", \"-p\").Output()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"ldconfig failed: %w\", err)\n\t}\n\n\tfor _, line := range strings.Split(string(byts), \"\\n\") {\n\t\tf := strings.Split(line, \" => \")\n\t\tif len(f) == 2 && strings.Contains(f[1], libraryToCheckArchitecture) {\n\t\t\tis64, err := getArchitecture(f[1])\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif runtime.GOARCH == \"arm\" {\n\t\t\t\tif !is64 {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tif is64 {\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif runtime.GOARCH == \"arm\" {\n\t\treturn fmt.Errorf(\"the operating system is 64-bit, you need the 64-bit server version\")\n\t}\n\n\treturn fmt.Errorf(\"the operating system is 32-bit, you need the 32-bit server version\")\n}\n\nfunc dumpEmbedFSRecursive(src string, dest string) error {\n\tfiles, err := mtxrpicam.ReadDir(src)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, f := range files {\n\t\tif f.IsDir() {\n\t\t\terr = os.Mkdir(filepath.Join(dest, f.Name()), 0o755)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\terr = dumpEmbedFSRecursive(filepath.Join(src, f.Name()), filepath.Join(dest, f.Name()))\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\tbuf, err := mtxrpicam.ReadFile(filepath.Join(src, f.Name()))\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\terr = os.WriteFile(filepath.Join(dest, f.Name()), buf, 0o644)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc dumpComponent() error {\n\tdumpMutex.Lock()\n\tdefer dumpMutex.Unlock()\n\n\tif dumpCount > 0 {\n\t\tdumpCount++\n\t\treturn nil\n\t}\n\n\terr := checkArchitecture()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdumpPath = dumpPrefix + strconv.FormatInt(time.Now().UnixNano(), 10)\n\n\terr = os.Mkdir(dumpPath, 0o755)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfiles, err := mtxrpicam.ReadDir(\".\")\n\tif err != nil {\n\t\tos.RemoveAll(dumpPath)\n\t\treturn err\n\t}\n\n\terr = dumpEmbedFSRecursive(files[0].Name(), dumpPath)\n\tif err != nil {\n\t\tos.RemoveAll(dumpPath)\n\t\treturn err\n\t}\n\n\terr = os.Chmod(filepath.Join(dumpPath, executableName), 0o755)\n\tif err != nil {\n\t\tos.RemoveAll(dumpPath)\n\t\treturn err\n\t}\n\n\tdumpCount++\n\n\treturn nil\n}\n\nfunc freeComponent() {\n\tdumpMutex.Lock()\n\tdefer dumpMutex.Unlock()\n\n\tdumpCount--\n\n\tif dumpCount == 0 {\n\t\tos.RemoveAll(dumpPath)\n\t}\n}\n\ntype camera struct {\n\tparams          params\n\tonData          func(int64, time.Time, [][]byte)\n\tonDataSecondary func(int64, time.Time, []byte)\n\n\tcmd      *exec.Cmd\n\tpipeOut  *pipe\n\tpipeIn   *pipe\n\tfinalErr error\n\n\tterminate chan struct{}\n\tdone      chan struct{}\n}\n\nfunc (c *camera) initialize() error {\n\terr := dumpComponent()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tc.pipeOut, err = newPipe()\n\tif err != nil {\n\t\tfreeComponent()\n\t\treturn err\n\t}\n\n\tc.pipeIn, err = newPipe()\n\tif err != nil {\n\t\tc.pipeOut.close()\n\t\tfreeComponent()\n\t\treturn err\n\t}\n\n\tenv := []string{\n\t\t\"PIPE_CONF_FD=\" + strconv.FormatInt(int64(c.pipeOut.readFD), 10),\n\t\t\"PIPE_VIDEO_FD=\" + strconv.FormatInt(int64(c.pipeIn.writeFD), 10),\n\t\t\"LD_LIBRARY_PATH=\" + dumpPath,\n\t}\n\n\tc.cmd = exec.Command(filepath.Join(dumpPath, executableName))\n\tc.cmd.Stdout = os.Stdout\n\tc.cmd.Stderr = os.Stderr\n\tc.cmd.Env = env\n\tc.cmd.Dir = dumpPath\n\n\t// prevent the subprocess from receiving signals (in particular SIGINT and SIGTERM)\n\t// from the parent process.\n\tc.cmd.SysProcAttr = &syscall.SysProcAttr{\n\t\tSetpgid: true,\n\t}\n\n\terr = c.cmd.Start()\n\tif err != nil {\n\t\tc.pipeOut.close()\n\t\tc.pipeIn.close()\n\t\tfreeComponent()\n\t\treturn err\n\t}\n\n\tc.terminate = make(chan struct{})\n\tc.done = make(chan struct{})\n\n\tgo c.run()\n\n\tc.pipeOut.write(append([]byte{'c'}, c.params.serialize()...))\n\n\treturn nil\n}\n\nfunc (c *camera) close() {\n\tclose(c.terminate)\n\t<-c.done\n\tfreeComponent()\n}\n\nfunc (c *camera) run() {\n\tdefer close(c.done)\n\tc.finalErr = c.runInner()\n}\n\nfunc (c *camera) runInner() error {\n\tcmdDone := make(chan error)\n\tgo func() {\n\t\tcmdDone <- c.cmd.Wait()\n\t}()\n\n\treadDone := make(chan error)\n\tgo func() {\n\t\treadDone <- c.runReader()\n\t}()\n\n\tfor {\n\t\tselect {\n\t\tcase err := <-cmdDone:\n\t\t\tc.pipeIn.close()\n\t\t\tc.pipeOut.close()\n\n\t\t\t<-readDone\n\n\t\t\treturn err\n\n\t\tcase err := <-readDone:\n\t\t\tc.pipeOut.write([]byte{'e'})\n\n\t\t\t<-cmdDone\n\n\t\t\tc.pipeIn.close()\n\t\t\tc.pipeOut.close()\n\n\t\t\treturn err\n\n\t\tcase <-c.terminate:\n\t\t\tc.pipeOut.write([]byte{'e'})\n\n\t\t\t<-cmdDone\n\n\t\t\tc.pipeOut.close()\n\t\t\tc.pipeIn.close()\n\n\t\t\t<-readDone\n\n\t\t\treturn fmt.Errorf(\"terminated\")\n\t\t}\n\t}\n}\n\nfunc (c *camera) runReader() error {\nouter:\n\tfor {\n\t\tbuf, err := c.pipeIn.read()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tswitch buf[0] {\n\t\tcase 'e':\n\t\t\treturn fmt.Errorf(string(buf[1:]))\n\n\t\tcase 'r':\n\t\t\tbreak outer\n\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"unexpected data from pipe: '0x%.2x'\", buf[0])\n\t\t}\n\t}\n\n\tfor {\n\t\tbuf, err := c.pipeIn.read()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tswitch buf[0] {\n\t\tcase 'e':\n\t\t\treturn fmt.Errorf(string(buf[1:]))\n\n\t\tcase 'd':\n\t\t\tdts := int64(buf[8])<<56 | int64(buf[7])<<48 | int64(buf[6])<<40 | int64(buf[5])<<32 |\n\t\t\t\tint64(buf[4])<<24 | int64(buf[3])<<16 | int64(buf[2])<<8 | int64(buf[1])\n\n\t\t\tvar nalus h264.AnnexB\n\t\t\terr = nalus.Unmarshal(buf[9:])\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tunixNTP := ntpTime()\n\t\t\tunixMono := monotonicTime()\n\n\t\t\t// subtract from NTP the delay from now to the moment the frame was taken\n\t\t\tntp := time.Unix(int64(unixNTP.Sec), int64(unixNTP.Nsec))\n\t\t\tdeltaT := time.Duration(unixMono.Nano()-dts*1e3) * time.Nanosecond\n\t\t\tntp = ntp.Add(-deltaT)\n\n\t\t\tc.onData(\n\t\t\t\tmultiplyAndDivide(dts, 90000, 1e6),\n\t\t\t\tntp,\n\t\t\t\tnalus)\n\n\t\tcase 's':\n\t\t\tdts := int64(buf[8])<<56 | int64(buf[7])<<48 | int64(buf[6])<<40 | int64(buf[5])<<32 |\n\t\t\t\tint64(buf[4])<<24 | int64(buf[3])<<16 | int64(buf[2])<<8 | int64(buf[1])\n\n\t\t\tunixNTP := ntpTime()\n\t\t\tunixMono := monotonicTime()\n\n\t\t\t// subtract from NTP the delay from now to the moment the frame was taken\n\t\t\tntp := time.Unix(int64(unixNTP.Sec), int64(unixNTP.Nsec))\n\t\t\tdeltaT := time.Duration(unixMono.Nano()-dts*1e3) * time.Nanosecond\n\t\t\tntp = ntp.Add(-deltaT)\n\n\t\t\tc.onDataSecondary(\n\t\t\t\tmultiplyAndDivide(dts, 90000, 1e6),\n\t\t\t\tntp,\n\t\t\t\tbuf[9:])\n\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"unexpected data from pipe: '0x%.2x'\", buf[0])\n\t\t}\n\t}\n}\n\nfunc (c *camera) reloadParams(params params) {\n\tc.pipeOut.write(append([]byte{'c'}, params.serialize()...))\n}\n\nfunc (c *camera) wait() error {\n\t<-c.done\n\treturn c.finalErr\n}\n"
  },
  {
    "path": "internal/staticsources/rpicamera/camera_other.go",
    "content": "//go:build !linux || (!arm && !arm64)\n\npackage rpicamera\n\nimport (\n\t\"fmt\"\n\t\"time\"\n)\n\ntype camera struct {\n\tparams          params\n\tonData          func(int64, time.Time, [][]byte)\n\tonDataSecondary func(int64, time.Time, []byte)\n}\n\nfunc (c *camera) initialize() error {\n\treturn fmt.Errorf(\"server was compiled without support for the Raspberry Pi Camera\")\n}\n\nfunc (c *camera) close() {\n}\n\nfunc (c *camera) reloadParams(_ params) {\n}\n\nfunc (c *camera) wait() error {\n\treturn nil\n}\n"
  },
  {
    "path": "internal/staticsources/rpicamera/downloader.go",
    "content": "package rpicamera\n\n//go:generate go run ./mtxrpicamdownloader\n"
  },
  {
    "path": "internal/staticsources/rpicamera/mtxrpicamdownloader/HASH_MTXRPICAM_32_TAR_GZ",
    "content": "f16657475469a8330f7fd5a801c1931d58be2c7e8f93cad318d298f2fbaff429\n"
  },
  {
    "path": "internal/staticsources/rpicamera/mtxrpicamdownloader/HASH_MTXRPICAM_64_TAR_GZ",
    "content": "c8ed15cc06b6b8604d6757ae01031809f75749f67a1e7b27d14dd161b75d71c2\n"
  },
  {
    "path": "internal/staticsources/rpicamera/mtxrpicamdownloader/VERSION",
    "content": "v2.5.4\n"
  },
  {
    "path": "internal/staticsources/rpicamera/mtxrpicamdownloader/main.go",
    "content": "// Package main contains an utility to download hls.js\npackage main\n\nimport (\n\t\"archive/tar\"\n\t\"bytes\"\n\t\"compress/gzip\"\n\t\"crypto/sha256\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n)\n\nfunc dumpTar(src io.Reader) error {\n\tuncompressed, err := gzip.NewReader(src)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttr := tar.NewReader(uncompressed)\n\n\tfor {\n\t\tvar header *tar.Header\n\t\theader, err = tr.Next()\n\t\tif err != nil {\n\t\t\tif errors.Is(err, io.EOF) {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\n\t\tswitch header.Typeflag {\n\t\tcase tar.TypeDir:\n\t\t\terr = os.Mkdir(header.Name, header.FileInfo().Mode())\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\tcase tar.TypeReg:\n\t\t\tvar f *os.File\n\t\t\tf, err = os.OpenFile(header.Name, os.O_WRONLY|os.O_CREATE, header.FileInfo().Mode())\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tdefer f.Close()\n\n\t\t\t_, err = io.Copy(f, tr)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc doSingle(version string, f string) error {\n\terr := os.RemoveAll(strings.TrimSuffix(f, \".tar.gz\"))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tres, err := http.Get(\"https://github.com/bluenviron/mediamtx-rpicamera/releases/download/\" + version + \"/\" + f)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer res.Body.Close()\n\n\tif res.StatusCode != http.StatusOK {\n\t\treturn fmt.Errorf(\"bad status code: %v\", res.StatusCode)\n\t}\n\n\tbuf, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\thashBuf, err := os.ReadFile(\"./mtxrpicamdownloader/HASH_\" + strings.ToUpper(strings.ReplaceAll(f, \".\", \"_\")))\n\tif err != nil {\n\t\treturn err\n\t}\n\tstr := strings.TrimSpace(string(hashBuf))\n\n\thash, err := hex.DecodeString(str)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif sum := sha256.Sum256(buf); !bytes.Equal(sum[:], hash) {\n\t\treturn fmt.Errorf(\"hash mismatch\")\n\t}\n\n\treturn dumpTar(bytes.NewReader(buf))\n}\n\nfunc do() error {\n\tbuf, err := os.ReadFile(\"./mtxrpicamdownloader/VERSION\")\n\tif err != nil {\n\t\treturn err\n\t}\n\tversion := strings.TrimSpace(string(buf))\n\n\tlog.Printf(\"downloading mediamtx-rpicamera %s...\", version)\n\n\tfor _, f := range []string{\"mtxrpicam_32.tar.gz\", \"mtxrpicam_64.tar.gz\"} {\n\t\terr = doSingle(version, f)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tlog.Println(\"ok\")\n\treturn nil\n}\n\nfunc main() {\n\terr := do()\n\tif err != nil {\n\t\tlog.Printf(\"ERR: %v\", err)\n\t\tos.Exit(1)\n\t}\n}\n"
  },
  {
    "path": "internal/staticsources/rpicamera/params.go",
    "content": "package rpicamera\n\ntype params struct {\n\tLogLevel              string\n\tCameraID              uint32\n\tWidth                 uint32\n\tHeight                uint32\n\tHFlip                 bool\n\tVFlip                 bool\n\tBrightness            float32\n\tContrast              float32\n\tSaturation            float32\n\tSharpness             float32\n\tExposure              string\n\tAWB                   string\n\tAWBGainRed            float32\n\tAWBGainBlue           float32\n\tDenoise               string\n\tShutter               uint32\n\tMetering              string\n\tGain                  float32\n\tEV                    float32\n\tROI                   string\n\tHDR                   bool\n\tTuningFile            string\n\tMode                  string\n\tFPS                   float32\n\tAfMode                string\n\tAfRange               string\n\tAfSpeed               string\n\tLensPosition          float32\n\tAfWindow              string\n\tFlickerPeriod         uint32\n\tTextOverlayEnable     bool\n\tTextOverlay           string\n\tCodec                 string\n\tIDRPeriod             uint32\n\tBitrate               uint32\n\tHardwareH264Profile   string\n\tHardwareH264Level     string\n\tSoftwareH264Profile   string\n\tSoftwareH264Level     string\n\tSecondaryWidth        uint32\n\tSecondaryHeight       uint32\n\tSecondaryFPS          float32\n\tSecondaryMJPEGQuality uint32\n}\n"
  },
  {
    "path": "internal/staticsources/rpicamera/params_serialize.go",
    "content": "//go:build (linux && arm) || (linux && arm64)\n\npackage rpicamera\n\nimport (\n\t\"encoding/base64\"\n\t\"reflect\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nfunc (p params) serialize() []byte {\n\trv := reflect.ValueOf(p)\n\trt := rv.Type()\n\tnf := rv.NumField()\n\tret := make([]string, nf)\n\n\tfor i := range nf {\n\t\tentry := rt.Field(i).Name + \":\"\n\t\tf := rv.Field(i)\n\t\tv := f.Interface()\n\n\t\tswitch v := v.(type) {\n\t\tcase uint32:\n\t\t\tentry += strconv.FormatUint(uint64(v), 10)\n\n\t\tcase float32:\n\t\t\tentry += strconv.FormatFloat(float64(v), 'f', -1, 64)\n\n\t\tcase string:\n\t\t\tentry += base64.StdEncoding.EncodeToString([]byte(v))\n\n\t\tcase bool:\n\t\t\tif f.Bool() {\n\t\t\t\tentry += \"1\"\n\t\t\t} else {\n\t\t\t\tentry += \"0\"\n\t\t\t}\n\n\t\tdefault:\n\t\t\tpanic(\"unhandled type\")\n\t\t}\n\n\t\tret[i] = entry\n\t}\n\n\treturn []byte(strings.Join(ret, \" \"))\n}\n"
  },
  {
    "path": "internal/staticsources/rpicamera/pipe.go",
    "content": "//go:build (linux && arm) || (linux && arm64)\n\npackage rpicamera\n\nimport (\n\t\"syscall\"\n)\n\nfunc syscallReadAll(fd int, buf []byte) error {\n\tsize := len(buf)\n\tread := 0\n\n\tfor {\n\t\tn, err := syscall.Read(fd, buf[read:size])\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tread += n\n\t\tif read >= size {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn nil\n}\n\ntype pipe struct {\n\treadFD  int\n\twriteFD int\n}\n\nfunc newPipe() (*pipe, error) {\n\tfds := make([]int, 2)\n\terr := syscall.Pipe(fds)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &pipe{\n\t\treadFD:  fds[0],\n\t\twriteFD: fds[1],\n\t}, nil\n}\n\nfunc (p *pipe) close() {\n\tsyscall.Close(p.readFD)\n\tsyscall.Close(p.writeFD)\n}\n\nfunc (p *pipe) read() ([]byte, error) {\n\tbuf := make([]byte, 4)\n\terr := syscallReadAll(p.readFD, buf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tle := int(buf[3])<<24 | int(buf[2])<<16 | int(buf[1])<<8 | int(buf[0])\n\tbuf = make([]byte, le)\n\n\terr = syscallReadAll(p.readFD, buf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn buf, nil\n}\n\nfunc (p *pipe) write(byts []byte) error {\n\tle := len(byts)\n\t_, err := syscall.Write(p.writeFD, []byte{byte(le), byte(le >> 8), byte(le >> 16), byte(le >> 24)})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = syscall.Write(p.writeFD, byts)\n\treturn err\n}\n"
  },
  {
    "path": "internal/staticsources/rpicamera/source.go",
    "content": "// Package rpicamera contains the Raspberry Pi Camera static source.\npackage rpicamera\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtph264\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtpmjpeg\"\n\t\"github.com/pion/rtp\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n)\n\nconst (\n\tpauseBetweenErrors = 1 * time.Second\n)\n\nfunc paramsFromConf(logLevel conf.LogLevel, cnf *conf.Path) params {\n\treturn params{\n\t\tLogLevel: func() string {\n\t\t\tswitch logLevel {\n\t\t\tcase conf.LogLevel(logger.Debug):\n\t\t\t\treturn \"debug\"\n\t\t\tcase conf.LogLevel(logger.Info):\n\t\t\t\treturn \"info\"\n\t\t\tcase conf.LogLevel(logger.Warn):\n\t\t\t\treturn \"warn\"\n\t\t\t}\n\t\t\treturn \"error\"\n\t\t}(),\n\t\tCameraID:              uint32(cnf.RPICameraCamID),\n\t\tWidth:                 uint32(cnf.RPICameraWidth),\n\t\tHeight:                uint32(cnf.RPICameraHeight),\n\t\tHFlip:                 cnf.RPICameraHFlip,\n\t\tVFlip:                 cnf.RPICameraVFlip,\n\t\tBrightness:            float32(cnf.RPICameraBrightness),\n\t\tContrast:              float32(cnf.RPICameraContrast),\n\t\tSaturation:            float32(cnf.RPICameraSaturation),\n\t\tSharpness:             float32(cnf.RPICameraSharpness),\n\t\tExposure:              cnf.RPICameraExposure,\n\t\tAWB:                   cnf.RPICameraAWB,\n\t\tAWBGainRed:            float32(cnf.RPICameraAWBGains[0]),\n\t\tAWBGainBlue:           float32(cnf.RPICameraAWBGains[1]),\n\t\tDenoise:               cnf.RPICameraDenoise,\n\t\tShutter:               uint32(cnf.RPICameraShutter),\n\t\tMetering:              cnf.RPICameraMetering,\n\t\tGain:                  float32(cnf.RPICameraGain),\n\t\tEV:                    float32(cnf.RPICameraEV),\n\t\tROI:                   cnf.RPICameraROI,\n\t\tHDR:                   cnf.RPICameraHDR,\n\t\tTuningFile:            cnf.RPICameraTuningFile,\n\t\tMode:                  cnf.RPICameraMode,\n\t\tFPS:                   float32(cnf.RPICameraFPS),\n\t\tAfMode:                cnf.RPICameraAfMode,\n\t\tAfRange:               cnf.RPICameraAfRange,\n\t\tAfSpeed:               cnf.RPICameraAfSpeed,\n\t\tLensPosition:          float32(cnf.RPICameraLensPosition),\n\t\tAfWindow:              cnf.RPICameraAfWindow,\n\t\tFlickerPeriod:         uint32(cnf.RPICameraFlickerPeriod),\n\t\tTextOverlayEnable:     cnf.RPICameraTextOverlayEnable,\n\t\tTextOverlay:           cnf.RPICameraTextOverlay,\n\t\tCodec:                 cnf.RPICameraCodec,\n\t\tIDRPeriod:             uint32(cnf.RPICameraIDRPeriod),\n\t\tBitrate:               uint32(cnf.RPICameraBitrate),\n\t\tHardwareH264Profile:   cnf.RPICameraHardwareH264Profile,\n\t\tHardwareH264Level:     cnf.RPICameraHardwareH264Level,\n\t\tSoftwareH264Profile:   cnf.RPICameraSoftwareH264Profile,\n\t\tSoftwareH264Level:     cnf.RPICameraSoftwareH264Level,\n\t\tSecondaryWidth:        uint32(cnf.RPICameraSecondaryWidth),\n\t\tSecondaryHeight:       uint32(cnf.RPICameraSecondaryHeight),\n\t\tSecondaryFPS:          float32(cnf.RPICameraSecondaryFPS),\n\t\tSecondaryMJPEGQuality: uint32(cnf.RPICameraSecondaryMJPEGQuality),\n\t}\n}\n\ntype secondaryReader struct {\n\tctx       context.Context\n\tctxCancel func()\n}\n\n// Close implements reader.\nfunc (r *secondaryReader) Close() {\n\tr.ctxCancel()\n}\n\n// APIReaderDescribe implements reader.\nfunc (*secondaryReader) APIReaderDescribe() *defs.APIPathReader {\n\treturn &defs.APIPathReader{\n\t\tType: defs.APIPathReaderTypeRPICameraSecondary,\n\t\tID:   \"\",\n\t}\n}\n\ntype parent interface {\n\tlogger.Writer\n\tSetReady(req defs.PathSourceStaticSetReadyReq) defs.PathSourceStaticSetReadyRes\n\tSetNotReady(req defs.PathSourceStaticSetNotReadyReq)\n\tAddReader(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error)\n}\n\n// Source is a Raspberry Pi Camera static source.\ntype Source struct {\n\tRTPMaxPayloadSize int\n\tLogLevel          conf.LogLevel\n\tParent            parent\n}\n\n// Log implements logger.Writer.\nfunc (s *Source) Log(level logger.Level, format string, args ...any) {\n\ts.Parent.Log(level, \"[RPI Camera source] \"+format, args...)\n}\n\n// Run implements StaticSource.\nfunc (s *Source) Run(params defs.StaticSourceRunParams) error {\n\tif !params.Conf.RPICameraSecondary {\n\t\treturn s.runPrimary(params)\n\t}\n\treturn s.runSecondary(params)\n}\n\nfunc (s *Source) runPrimary(params defs.StaticSourceRunParams) error {\n\tvar medias []*description.Media\n\n\tmedi := &description.Media{\n\t\tType: description.MediaTypeVideo,\n\t\tFormats: []format.Format{&format.H264{\n\t\t\tPayloadTyp:        96,\n\t\t\tPacketizationMode: 1,\n\t\t}},\n\t}\n\tmedias = append(medias, medi)\n\n\tvar mediaSecondary *description.Media\n\n\tif params.Conf.RPICameraSecondaryWidth != 0 {\n\t\tmediaSecondary = &description.Media{\n\t\t\tType: description.MediaTypeApplication,\n\t\t\tFormats: []format.Format{&format.Generic{\n\t\t\t\tPayloadTyp: 96,\n\t\t\t\tRTPMa:      \"rpicamera_secondary/90000\",\n\t\t\t\tClockRat:   90000,\n\t\t\t}},\n\t\t}\n\t\tmedias = append(medias, mediaSecondary)\n\t}\n\n\tvar subStream *stream.SubStream\n\n\tinitializeStream := func() {\n\t\tif subStream == nil {\n\t\t\tres := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{\n\t\t\t\tDesc:          &description.Session{Medias: medias},\n\t\t\t\tUseRTPPackets: true,\n\t\t\t\tReplaceNTP:    false,\n\t\t\t})\n\t\t\tif res.Err != nil {\n\t\t\t\tpanic(\"should not happen\")\n\t\t\t}\n\n\t\t\tsubStream = res.SubStream\n\t\t}\n\t}\n\n\tencH264 := &rtph264.Encoder{\n\t\tPayloadType:    96,\n\t\tPayloadMaxSize: s.RTPMaxPayloadSize,\n\t}\n\terr := encH264.Init()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tonData := func(pts int64, ntp time.Time, au [][]byte) {\n\t\tinitializeStream()\n\n\t\tpkts, err2 := encH264.Encode(au)\n\t\tif err2 != nil {\n\t\t\ts.Log(logger.Error, err2.Error())\n\t\t\treturn\n\t\t}\n\n\t\tfor _, pkt := range pkts {\n\t\t\tpkt.Timestamp = uint32(pts)\n\t\t\tsubStream.WriteUnit(medi, medi.Formats[0], &unit.Unit{\n\t\t\t\tPTS:        pts,\n\t\t\t\tNTP:        ntp,\n\t\t\t\tRTPPackets: []*rtp.Packet{pkt},\n\t\t\t})\n\t\t}\n\t}\n\n\tvar onDataSecondary func(pts int64, ntp time.Time, au []byte)\n\n\tif params.Conf.RPICameraSecondaryWidth != 0 {\n\t\tencJpeg := &rtpmjpeg.Encoder{\n\t\t\tPayloadMaxSize: s.RTPMaxPayloadSize,\n\t\t}\n\t\terr = encJpeg.Init()\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\n\t\tonDataSecondary = func(pts int64, ntp time.Time, au []byte) {\n\t\t\tinitializeStream()\n\n\t\t\tpkts, err2 := encJpeg.Encode(au)\n\t\t\tif err2 != nil {\n\t\t\t\ts.Log(logger.Error, err2.Error())\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tfor _, pkt := range pkts {\n\t\t\t\tpkt.Timestamp = uint32(pts)\n\t\t\t\tpkt.PayloadType = 96\n\t\t\t\tsubStream.WriteUnit(mediaSecondary, mediaSecondary.Formats[0], &unit.Unit{\n\t\t\t\t\tPTS:        pts,\n\t\t\t\t\tNTP:        ntp,\n\t\t\t\t\tRTPPackets: []*rtp.Packet{pkt},\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n\n\tdefer func() {\n\t\tif subStream != nil {\n\t\t\ts.Parent.SetNotReady(defs.PathSourceStaticSetNotReadyReq{})\n\t\t}\n\t}()\n\n\tcam := &camera{\n\t\tparams:          paramsFromConf(s.LogLevel, params.Conf),\n\t\tonData:          onData,\n\t\tonDataSecondary: onDataSecondary,\n\t}\n\terr = cam.initialize() //nolint:staticcheck\n\tif err != nil {        //nolint:staticcheck\n\t\treturn err\n\t}\n\tdefer cam.close()\n\n\tcameraErr := make(chan error)\n\tgo func() {\n\t\tcameraErr <- cam.wait()\n\t}()\n\n\tfor {\n\t\tselect {\n\t\tcase err = <-cameraErr:\n\t\t\treturn err\n\n\t\tcase cnf := <-params.ReloadConf:\n\t\t\tcam.reloadParams(paramsFromConf(s.LogLevel, cnf))\n\n\t\tcase <-params.Context.Done():\n\t\t\treturn nil\n\t\t}\n\t}\n}\n\nfunc (s *Source) runSecondary(params defs.StaticSourceRunParams) error {\n\tr := &secondaryReader{}\n\tr.ctx, r.ctxCancel = context.WithCancel(context.Background())\n\tdefer r.ctxCancel()\n\n\tpath, primaryStream, err := s.waitForPrimary(r, params)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdefer path.RemoveReader(defs.PathRemoveReaderReq{Author: r})\n\n\tmedia := &description.Media{\n\t\tType:    description.MediaTypeVideo,\n\t\tFormats: []format.Format{&format.MJPEG{}},\n\t}\n\n\tres := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{\n\t\tDesc:          &description.Session{Medias: []*description.Media{media}},\n\t\tUseRTPPackets: true,\n\t})\n\tif res.Err != nil {\n\t\treturn res.Err\n\t}\n\n\trdr := &stream.Reader{Parent: s}\n\n\trdr.OnData(\n\t\tprimaryStream.Desc.Medias[1],\n\t\tprimaryStream.Desc.Medias[1].Formats[0],\n\t\tfunc(u *unit.Unit) error {\n\t\t\tpkt := u.RTPPackets[0]\n\n\t\t\tnewPkt := &rtp.Packet{\n\t\t\t\tHeader:  pkt.Header,\n\t\t\t\tPayload: pkt.Payload,\n\t\t\t}\n\t\t\tnewPkt.PayloadType = 26\n\n\t\t\tres.SubStream.WriteUnit(media, media.Formats[0], &unit.Unit{\n\t\t\t\tPTS:        u.PTS,\n\t\t\t\tNTP:        u.NTP,\n\t\t\t\tRTPPackets: []*rtp.Packet{newPkt},\n\t\t\t})\n\t\t\treturn nil\n\t\t})\n\n\tprimaryStream.AddReader(rdr)\n\tdefer primaryStream.RemoveReader(rdr)\n\n\tselect {\n\tcase err = <-rdr.Error():\n\t\treturn err\n\n\tcase <-r.ctx.Done():\n\t\treturn fmt.Errorf(\"primary stream closed\")\n\n\tcase <-params.Context.Done():\n\t\treturn fmt.Errorf(\"terminated\")\n\t}\n}\n\nfunc (s *Source) waitForPrimary(\n\tr *secondaryReader,\n\tparams defs.StaticSourceRunParams,\n) (defs.Path, *stream.Stream, error) {\n\tfor {\n\t\tres, err := s.Parent.AddReader(defs.PathAddReaderReq{\n\t\t\tAuthor: r,\n\t\t\tAccessRequest: defs.PathAccessRequest{\n\t\t\t\tName:     params.Conf.RPICameraPrimaryName,\n\t\t\t\tSkipAuth: true,\n\t\t\t},\n\t\t})\n\t\tif err != nil {\n\t\t\tvar err2 defs.PathNoStreamAvailableError\n\t\t\tif errors.As(err, &err2) {\n\t\t\t\tselect {\n\t\t\t\tcase <-time.After(pauseBetweenErrors):\n\t\t\t\tcase <-params.Context.Done():\n\t\t\t\t\treturn nil, nil, fmt.Errorf(\"terminated\")\n\t\t\t\t}\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treturn nil, nil, err\n\t\t}\n\n\t\treturn res.Path, res.Stream, nil\n\t}\n}\n\n// APISourceDescribe implements StaticSource.\nfunc (*Source) APISourceDescribe() *defs.APIPathSource {\n\treturn &defs.APIPathSource{\n\t\tType: defs.APIPathSourceTypeRPICameraSource,\n\t\tID:   \"\",\n\t}\n}\n"
  },
  {
    "path": "internal/staticsources/rtmp/source.go",
    "content": "// Package rtmp contains the RTMP static source.\npackage rtmp\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortmplib\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/packetdumper\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/rtmp\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/tls\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n)\n\ntype parent interface {\n\tlogger.Writer\n\tSetReady(req defs.PathSourceStaticSetReadyReq) defs.PathSourceStaticSetReadyRes\n\tSetNotReady(req defs.PathSourceStaticSetNotReadyReq)\n}\n\n// Source is a RTMP static source.\ntype Source struct {\n\tDumpPackets  bool\n\tReadTimeout  conf.Duration\n\tWriteTimeout conf.Duration\n\tParent       parent\n}\n\n// Log implements logger.Writer.\nfunc (s *Source) Log(level logger.Level, format string, args ...any) {\n\ts.Parent.Log(level, \"[RTMP source] \"+format, args...)\n}\n\n// Run implements StaticSource.\nfunc (s *Source) Run(params defs.StaticSourceRunParams) error {\n\ts.Log(logger.Debug, \"connecting\")\n\n\tu, err := url.Parse(params.ResolvedSource)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// add default port\n\t_, _, err = net.SplitHostPort(u.Host)\n\tif err != nil {\n\t\tif u.Scheme == \"rtmp\" {\n\t\t\tu.Host = net.JoinHostPort(u.Host, \"1935\")\n\t\t} else {\n\t\t\tu.Host = net.JoinHostPort(u.Host, \"1936\")\n\t\t}\n\t}\n\n\tdialContext := (&net.Dialer{}).DialContext\n\n\tif s.DumpPackets {\n\t\tdialContext = (&packetdumper.DialContext{\n\t\t\tPrefix:      \"rtmp_source_conn\",\n\t\t\tDialContext: dialContext,\n\t\t}).Do\n\t}\n\n\tconnectCtx, connectCtxCancel := context.WithTimeout(params.Context, time.Duration(s.ReadTimeout))\n\n\tconn := &gortmplib.Client{\n\t\tURL:         u,\n\t\tTLSConfig:   tls.MakeConfig(u.Hostname(), params.Conf.SourceFingerprint),\n\t\tPublish:     false,\n\t\tDialContext: dialContext,\n\t}\n\terr = conn.Initialize(connectCtx)\n\tconnectCtxCancel()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treadDone := make(chan error)\n\tgo func() {\n\t\treadDone <- s.runReader(conn)\n\t}()\n\n\tfor {\n\t\tselect {\n\t\tcase err = <-readDone:\n\t\t\tconn.Close()\n\t\t\treturn err\n\n\t\tcase <-params.ReloadConf:\n\n\t\tcase <-params.Context.Done():\n\t\t\tconn.Close()\n\t\t\t<-readDone\n\t\t\treturn nil\n\t\t}\n\t}\n}\n\nfunc (s *Source) runReader(conn *gortmplib.Client) error {\n\tconn.NetConn().SetReadDeadline(time.Now().Add(time.Duration(s.ReadTimeout)))\n\tconn.NetConn().SetWriteDeadline(time.Now().Add(time.Duration(s.WriteTimeout)))\n\n\tr := &gortmplib.Reader{\n\t\tConn: conn,\n\t}\n\terr := r.Initialize()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar subStream *stream.SubStream\n\n\tmedias, err := rtmp.ToStream(r, &subStream)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif len(medias) == 0 {\n\t\treturn fmt.Errorf(\"no supported tracks found\")\n\t}\n\n\tres := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{\n\t\tDesc:          &description.Session{Medias: medias},\n\t\tUseRTPPackets: false,\n\t\tReplaceNTP:    true,\n\t})\n\tif res.Err != nil {\n\t\treturn res.Err\n\t}\n\n\tdefer s.Parent.SetNotReady(defs.PathSourceStaticSetNotReadyReq{})\n\n\tsubStream = res.SubStream\n\n\tconn.NetConn().SetWriteDeadline(time.Time{})\n\n\tfor {\n\t\tconn.NetConn().SetReadDeadline(time.Now().Add(time.Duration(s.ReadTimeout)))\n\t\terr = r.Read()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n}\n\n// APISourceDescribe implements StaticSource.\nfunc (*Source) APISourceDescribe() *defs.APIPathSource {\n\treturn &defs.APIPathSource{\n\t\tType: defs.APIPathSourceTypeRTMPSource,\n\t\tID:   \"\",\n\t}\n}\n"
  },
  {
    "path": "internal/staticsources/rtmp/source_test.go",
    "content": "package rtmp\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"net\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/bluenviron/gortmplib\"\n\t\"github.com/bluenviron/gortmplib/pkg/codecs\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n)\n\nfunc TestSource(t *testing.T) {\n\tfor _, encryption := range []string{\n\t\t\"plain\",\n\t\t\"tls\",\n\t} {\n\t\tfor _, auth := range []string{\n\t\t\t\"no auth\",\n\t\t\t\"auth\",\n\t\t} {\n\t\t\tt.Run(encryption+\"_\"+auth, func(t *testing.T) {\n\t\t\t\tvar ln net.Listener\n\n\t\t\t\tif encryption == \"plain\" {\n\t\t\t\t\tvar err error\n\t\t\t\t\tln, err = net.Listen(\"tcp\", \"127.0.0.1:1935\")\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t} else {\n\t\t\t\t\tserverCertFpath, err := test.CreateTempFile(test.TLSCertPub)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\tdefer os.Remove(serverCertFpath)\n\n\t\t\t\t\tserverKeyFpath, err := test.CreateTempFile(test.TLSCertKey)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\tdefer os.Remove(serverKeyFpath)\n\n\t\t\t\t\tvar cert tls.Certificate\n\t\t\t\t\tcert, err = tls.LoadX509KeyPair(serverCertFpath, serverKeyFpath)\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\tln, err = tls.Listen(\"tcp\", \"127.0.0.1:1936\", &tls.Config{Certificates: []tls.Certificate{cert}})\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t}\n\n\t\t\t\tdefer ln.Close()\n\n\t\t\t\tvar source string\n\n\t\t\t\tif encryption == \"plain\" {\n\t\t\t\t\tsource = \"rtmp://\"\n\t\t\t\t} else {\n\t\t\t\t\tsource = \"rtmps://\"\n\t\t\t\t}\n\n\t\t\t\tif auth == \"auth\" {\n\t\t\t\t\tsource += \"myuser:mypass@\"\n\t\t\t\t}\n\n\t\t\t\tsource += \"localhost/teststream\"\n\n\t\t\t\tp := &test.StaticSourceParent{}\n\t\t\t\tp.Initialize()\n\t\t\t\tdefer p.Close()\n\n\t\t\t\tso := &Source{\n\t\t\t\t\tReadTimeout:  conf.Duration(10 * time.Second),\n\t\t\t\t\tWriteTimeout: conf.Duration(10 * time.Second),\n\t\t\t\t\tParent:       p,\n\t\t\t\t}\n\n\t\t\t\tdone := make(chan struct{})\n\t\t\t\tdefer func() { <-done }()\n\n\t\t\t\tctx, ctxCancel := context.WithCancel(context.Background())\n\t\t\t\tdefer ctxCancel()\n\n\t\t\t\treloadConf := make(chan *conf.Path)\n\n\t\t\t\tgo func() {\n\t\t\t\t\tso.Run(defs.StaticSourceRunParams{ //nolint:errcheck\n\t\t\t\t\t\tContext:        ctx,\n\t\t\t\t\t\tResolvedSource: source,\n\t\t\t\t\t\tConf: &conf.Path{\n\t\t\t\t\t\t\tSourceFingerprint: \"33949E05FFFB5FF3E8AA16F8213A6251B4D9363804BA53233C4DA9A46D6F2739\",\n\t\t\t\t\t\t},\n\t\t\t\t\t\tReloadConf: reloadConf,\n\t\t\t\t\t})\n\t\t\t\t\tclose(done)\n\t\t\t\t}()\n\n\t\t\t\tfor {\n\t\t\t\t\tnconn, err := ln.Accept()\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\tdefer nconn.Close()\n\n\t\t\t\t\tconn := &gortmplib.ServerConn{\n\t\t\t\t\t\tRW: nconn,\n\t\t\t\t\t}\n\t\t\t\t\terr = conn.Initialize()\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\tif auth == \"auth\" {\n\t\t\t\t\t\terr = conn.CheckCredentials(\"myuser\", \"mypass\")\n\t\t\t\t\t\tif err != nil {\n\t\t\t\t\t\t\tcontinue\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\n\t\t\t\t\terr = conn.Accept()\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\tw := &gortmplib.Writer{\n\t\t\t\t\t\tConn: conn,\n\t\t\t\t\t\tTracks: []*gortmplib.Track{\n\t\t\t\t\t\t\t{Codec: &codecs.H264{\n\t\t\t\t\t\t\t\tSPS: test.FormatH264.SPS,\n\t\t\t\t\t\t\t\tPPS: test.FormatH264.PPS,\n\t\t\t\t\t\t\t}},\n\t\t\t\t\t\t\t{Codec: &codecs.MPEG4Audio{\n\t\t\t\t\t\t\t\tConfig: test.FormatMPEG4Audio.Config,\n\t\t\t\t\t\t\t}},\n\t\t\t\t\t\t},\n\t\t\t\t\t}\n\t\t\t\t\terr = w.Initialize()\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\terr = w.WriteMPEG4Audio(w.Tracks[1], 2*time.Second, []byte{5, 2, 3, 4})\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\t<-p.Unit\n\n\t\t\t\t// the source must be listening on ReloadConf\n\t\t\t\treloadConf <- nil\n\t\t\t})\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/staticsources/rtp/format.go",
    "content": "package rtp\n\nimport (\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/rtpreceiver\"\n\t\"github.com/pion/rtcp\"\n)\n\ntype rtpFormat struct {\n\tdesc format.Format\n\n\trtpReceiver *rtpreceiver.Receiver\n}\n\nfunc (f *rtpFormat) initialize() {\n\tf.rtpReceiver = &rtpreceiver.Receiver{\n\t\tClockRate:            f.desc.ClockRate(),\n\t\tUnrealiableTransport: true,\n\t\tPeriod:               10 * time.Second,\n\t\tWritePacketRTCP: func(_ rtcp.Packet) {\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "internal/staticsources/rtp/media.go",
    "content": "package rtp\n\nimport \"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\ntype rtpMedia struct {\n\tdesc *description.Media\n}\n"
  },
  {
    "path": "internal/staticsources/rtp/source.go",
    "content": "// Package rtp contains the RTP static source.\npackage rtp\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/rtptime\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/sdp\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/counterdumper\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/errordumper\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/packetdumper\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/udp\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/unix\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n\t\"github.com/pion/rtp\"\n)\n\ntype parent interface {\n\tlogger.Writer\n\tSetReady(req defs.PathSourceStaticSetReadyReq) defs.PathSourceStaticSetReadyRes\n\tSetNotReady(req defs.PathSourceStaticSetNotReadyReq)\n}\n\n// Source is a RTP static source.\ntype Source struct {\n\tDumpPackets       bool\n\tReadTimeout       conf.Duration\n\tUDPReadBufferSize uint\n\tParent            parent\n}\n\n// Log implements logger.Writer.\nfunc (s *Source) Log(level logger.Level, format string, args ...any) {\n\ts.Parent.Log(level, \"[RTP source] \"+format, args...)\n}\n\n// Run implements StaticSource.\nfunc (s *Source) Run(params defs.StaticSourceRunParams) error {\n\tvar sd sdp.SessionDescription\n\terr := sd.Unmarshal([]byte(params.Conf.RTPSDP))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar desc description.Session\n\terr = desc.Unmarshal(&sd)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ts.Log(logger.Debug, \"connecting\")\n\n\tu, err := url.Parse(params.ResolvedSource)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar nc net.Conn\n\n\tswitch u.Scheme {\n\tcase \"unix+rtp\":\n\t\tparams := unix.URLToParams(u)\n\t\tl := &unix.Listener{\n\t\t\tPath: params.Path,\n\t\t}\n\t\terr = l.Initialize()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tnc = l\n\n\tdefault:\n\t\tudpReadBufferSize := s.UDPReadBufferSize\n\t\tif params.Conf.RTPUDPReadBufferSize != nil {\n\t\t\tudpReadBufferSize = *params.Conf.RTPUDPReadBufferSize\n\t\t}\n\n\t\tlistenPacket := net.ListenPacket\n\n\t\tif s.DumpPackets {\n\t\t\tlistenPacket = func(network, address string) (net.PacketConn, error) {\n\t\t\t\tpc, err2 := net.ListenPacket(network, address)\n\t\t\t\tif err2 != nil {\n\t\t\t\t\treturn nil, err2\n\t\t\t\t}\n\n\t\t\t\td := &packetdumper.PacketConn{\n\t\t\t\t\tPrefix:     \"rtp_source_packetconn\",\n\t\t\t\t\tPacketConn: pc,\n\t\t\t\t}\n\t\t\t\terr2 = d.Initialize()\n\t\t\t\tif err2 != nil {\n\t\t\t\t\treturn nil, err2\n\t\t\t\t}\n\n\t\t\t\treturn d, nil\n\t\t\t}\n\t\t}\n\n\t\tparams := udp.URLToParams(u)\n\t\tl := &udp.Listener{\n\t\t\tAddress:           params.Address,\n\t\t\tSource:            params.Source,\n\t\t\tIntfName:          params.IntfName,\n\t\t\tUDPReadBufferSize: int(udpReadBufferSize),\n\t\t\tListenPacket:      listenPacket,\n\t\t}\n\t\terr = l.Initialize()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tnc = l\n\t}\n\n\treaderErr := make(chan error)\n\tgo func() {\n\t\treaderErr <- s.runReader(&desc, nc)\n\t}()\n\n\tfor {\n\t\tselect {\n\t\tcase err = <-readerErr:\n\t\t\tnc.Close()\n\t\t\treturn err\n\n\t\tcase <-params.ReloadConf:\n\n\t\tcase <-params.Context.Done():\n\t\t\tnc.Close()\n\t\t\t<-readerErr\n\t\t\treturn fmt.Errorf(\"terminated\")\n\t\t}\n\t}\n}\n\nfunc (s *Source) runReader(desc *description.Session, nc net.Conn) error {\n\tpacketsLost := &counterdumper.Dumper{\n\t\tOnReport: func(val uint64) {\n\t\t\ts.Log(logger.Warn, \"%d RTP %s lost\",\n\t\t\t\tval,\n\t\t\t\tfunc() string {\n\t\t\t\t\tif val == 1 {\n\t\t\t\t\t\treturn \"packet\"\n\t\t\t\t\t}\n\t\t\t\t\treturn \"packets\"\n\t\t\t\t}())\n\t\t},\n\t}\n\n\tpacketsLost.Start()\n\tdefer packetsLost.Stop()\n\n\tdecodeErrors := &errordumper.Dumper{\n\t\tOnReport: func(val uint64, last error) {\n\t\t\tif val == 1 {\n\t\t\t\ts.Log(logger.Warn, \"decode error: %v\", last)\n\t\t\t} else {\n\t\t\t\ts.Log(logger.Warn, \"%d decode errors, last was: %v\", val, last)\n\t\t\t}\n\t\t},\n\t}\n\tdecodeErrors.Start()\n\tdefer decodeErrors.Stop()\n\n\tvar subStream *stream.SubStream\n\n\ttimeDecoder := &rtptime.GlobalDecoder{}\n\ttimeDecoder.Initialize()\n\n\tmediasByPayloadType := make(map[uint8]*rtpMedia)\n\tformatsByPayloadType := make(map[uint8]*rtpFormat)\n\n\tfor _, descMedia := range desc.Medias {\n\t\trtpMedia := &rtpMedia{\n\t\t\tdesc: descMedia,\n\t\t}\n\n\t\tfor _, descFormat := range descMedia.Formats {\n\t\t\trtpFormat := &rtpFormat{\n\t\t\t\tdesc: descFormat,\n\t\t\t}\n\t\t\trtpFormat.initialize()\n\n\t\t\tmediasByPayloadType[descFormat.PayloadType()] = rtpMedia\n\t\t\tformatsByPayloadType[descFormat.PayloadType()] = rtpFormat\n\t\t}\n\t}\n\n\tfor {\n\t\tbuf := make([]byte, 1500)\n\t\tnc.SetReadDeadline(time.Now().Add(time.Duration(s.ReadTimeout)))\n\t\tn, err := nc.Read(buf)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tvar pkt rtp.Packet\n\t\terr = pkt.Unmarshal(buf[:n])\n\t\tif err != nil {\n\t\t\tif subStream != nil {\n\t\t\t\tdecodeErrors.Add(err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\n\t\tif subStream == nil {\n\t\t\tres := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{\n\t\t\t\tDesc:          desc,\n\t\t\t\tUseRTPPackets: true,\n\t\t\t\tReplaceNTP:    true,\n\t\t\t})\n\t\t\tif res.Err != nil {\n\t\t\t\treturn res.Err\n\t\t\t}\n\n\t\t\tdefer s.Parent.SetNotReady(defs.PathSourceStaticSetNotReadyReq{})\n\n\t\t\tsubStream = res.SubStream\n\t\t}\n\n\t\tmedia, ok := mediasByPayloadType[pkt.PayloadType]\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tforma := formatsByPayloadType[pkt.PayloadType]\n\n\t\tpkts, lost := forma.rtpReceiver.ProcessPacket2(&pkt, time.Now(), forma.desc.PTSEqualsDTS(&pkt))\n\n\t\tif lost != 0 {\n\t\t\tpacketsLost.Add(lost)\n\t\t}\n\n\t\tfor _, pkt := range pkts {\n\t\t\tpts, ok2 := timeDecoder.Decode(forma.desc, pkt)\n\t\t\tif !ok2 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tsubStream.WriteUnit(media.desc, forma.desc, &unit.Unit{\n\t\t\t\tPTS:        pts,\n\t\t\t\tRTPPackets: []*rtp.Packet{pkt},\n\t\t\t})\n\t\t}\n\t}\n}\n\n// APISourceDescribe implements StaticSource.\nfunc (*Source) APISourceDescribe() *defs.APIPathSource {\n\treturn &defs.APIPathSource{\n\t\tType: defs.APIPathSourceTypeRTPSource,\n\t\tID:   \"\",\n\t}\n}\n"
  },
  {
    "path": "internal/staticsources/rtp/source_test.go",
    "content": "package rtp\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtph264\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n)\n\nfunc multicastCapableInterface(t *testing.T) string {\n\tintfs, err := net.Interfaces()\n\trequire.NoError(t, err)\n\n\tfor _, intf := range intfs {\n\t\tif (intf.Flags & net.FlagMulticast) != 0 {\n\t\t\treturn intf.Name\n\t\t}\n\t}\n\n\tt.Errorf(\"unable to find a multicast IP\")\n\treturn \"\"\n}\n\nfunc TestSourceUDP(t *testing.T) {\n\tfor _, ca := range []string{\n\t\t\"unicast\",\n\t\t\"multicast\",\n\t\t\"multicast with interface\",\n\t\t\"unicast with source\",\n\t} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tvar src string\n\n\t\t\tswitch ca {\n\t\t\tcase \"unicast\":\n\t\t\t\tsrc = \"udp+rtp://127.0.0.1:9004\"\n\n\t\t\tcase \"multicast\":\n\t\t\t\tsrc = \"udp+rtp://238.0.0.1:9004\"\n\n\t\t\tcase \"multicast with interface\":\n\t\t\t\tsrc = \"udp+rtp://238.0.0.1:9004?interface=\" + multicastCapableInterface(t)\n\n\t\t\tcase \"unicast with source\":\n\t\t\t\tsrc = \"udp+rtp://127.0.0.1:9004?source=127.0.1.1\"\n\t\t\t}\n\n\t\t\tp := &test.StaticSourceParent{}\n\t\t\tp.Initialize()\n\t\t\tdefer p.Close()\n\n\t\t\tso := &Source{\n\t\t\t\tReadTimeout: conf.Duration(10 * time.Second),\n\t\t\t\tParent:      p,\n\t\t\t}\n\n\t\t\tdone := make(chan struct{})\n\t\t\tdefer func() { <-done }()\n\n\t\t\tctx, ctxCancel := context.WithCancel(context.Background())\n\t\t\tdefer ctxCancel()\n\n\t\t\treloadConf := make(chan *conf.Path)\n\n\t\t\tgo func() {\n\t\t\t\tso.Run(defs.StaticSourceRunParams{ //nolint:errcheck\n\t\t\t\t\tContext:        ctx,\n\t\t\t\t\tResolvedSource: src,\n\t\t\t\t\tConf: &conf.Path{\n\t\t\t\t\t\tRTPSDP: \"v=0\\n\" +\n\t\t\t\t\t\t\t\"o=- 123456789 123456789 IN IP4 192.168.1.100\\n\" +\n\t\t\t\t\t\t\t\"s=H264 Video Stream\\n\" +\n\t\t\t\t\t\t\t\"c=IN IP4 192.168.1.100\\n\" +\n\t\t\t\t\t\t\t\"t=0 0\\n\" +\n\t\t\t\t\t\t\t\"m=video 5004 RTP/AVP 96\\n\" +\n\t\t\t\t\t\t\t\"a=rtpmap:96 H264/90000\\n\" +\n\t\t\t\t\t\t\t\"a=fmtp:96 profile-level-id=42e01e;packetization-mode=1\\n\",\n\t\t\t\t\t},\n\t\t\t\t\tReloadConf: reloadConf,\n\t\t\t\t})\n\t\t\t\tclose(done)\n\t\t\t}()\n\n\t\t\ttime.Sleep(50 * time.Millisecond)\n\n\t\t\tvar dest string\n\n\t\t\tswitch ca {\n\t\t\tcase \"unicast\":\n\t\t\t\tdest = \"127.0.0.1:9004\"\n\n\t\t\tcase \"multicast\":\n\t\t\t\tdest = \"238.0.0.1:9004\"\n\n\t\t\tcase \"multicast with interface\":\n\t\t\t\tdest = \"238.0.0.1:9004\"\n\n\t\t\tcase \"unicast with source\":\n\t\t\t\tdest = \"127.0.0.1:9004\"\n\t\t\t}\n\n\t\t\tudest, err := net.ResolveUDPAddr(\"udp\", dest)\n\t\t\trequire.NoError(t, err)\n\n\t\t\tvar usrc *net.UDPAddr\n\t\t\tif ca == \"unicast with source\" {\n\t\t\t\tusrc, err = net.ResolveUDPAddr(\"udp\", \"127.0.1.1:9020\")\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\tconn, err := net.DialUDP(\"udp\", usrc, udest)\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer conn.Close() //nolint:errcheck\n\n\t\t\tenc := &rtph264.Encoder{\n\t\t\t\tPayloadType: 96,\n\t\t\t}\n\t\t\terr = enc.Init()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tpkts, err := enc.Encode([][]byte{\n\t\t\t\t{5, 1},\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\n\t\t\tfor _, pkt := range pkts {\n\t\t\t\tvar buf []byte\n\t\t\t\tbuf, err = pkt.Marshal()\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t_, err = conn.Write(buf)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t}\n\n\t\t\t<-p.Unit\n\n\t\t\t// the source must be listening on ReloadConf\n\t\t\treloadConf <- nil\n\t\t})\n\t}\n}\n\nfunc TestSourceUnixSocket(t *testing.T) {\n\tfor _, ca := range []string{\n\t\t\"relative\",\n\t\t\"absolute\",\n\t} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tvar pa string\n\t\t\tif ca == \"relative\" {\n\t\t\t\tpa = \"test_rtp.sock\"\n\t\t\t} else {\n\t\t\t\tpa = filepath.Join(os.TempDir(), \"test_rtp.sock\")\n\t\t\t}\n\n\t\t\tfunc() {\n\t\t\t\tp := &test.StaticSourceParent{}\n\t\t\t\tp.Initialize()\n\t\t\t\tdefer p.Close()\n\n\t\t\t\tso := &Source{\n\t\t\t\t\tReadTimeout: conf.Duration(10 * time.Second),\n\t\t\t\t\tParent:      p,\n\t\t\t\t}\n\n\t\t\t\tdone := make(chan struct{})\n\t\t\t\tdefer func() { <-done }()\n\n\t\t\t\tctx, ctxCancel := context.WithCancel(context.Background())\n\t\t\t\tdefer ctxCancel()\n\n\t\t\t\tgo func() {\n\t\t\t\t\tso.Run(defs.StaticSourceRunParams{ //nolint:errcheck\n\t\t\t\t\t\tContext:        ctx,\n\t\t\t\t\t\tResolvedSource: \"unix+rtp://\" + pa,\n\t\t\t\t\t\tConf: &conf.Path{\n\t\t\t\t\t\t\tRTPSDP: \"v=0\\n\" +\n\t\t\t\t\t\t\t\t\"o=- 123456789 123456789 IN IP4 192.168.1.100\\n\" +\n\t\t\t\t\t\t\t\t\"s=H264 Video Stream\\n\" +\n\t\t\t\t\t\t\t\t\"c=IN IP4 192.168.1.100\\n\" +\n\t\t\t\t\t\t\t\t\"t=0 0\\n\" +\n\t\t\t\t\t\t\t\t\"m=video 5004 RTP/AVP 96\\n\" +\n\t\t\t\t\t\t\t\t\"a=rtpmap:96 H264/90000\\n\" +\n\t\t\t\t\t\t\t\t\"a=fmtp:96 profile-level-id=42e01e;packetization-mode=1\\n\",\n\t\t\t\t\t\t},\n\t\t\t\t\t})\n\t\t\t\t\tclose(done)\n\t\t\t\t}()\n\n\t\t\t\ttime.Sleep(50 * time.Millisecond)\n\n\t\t\t\t_, err := os.Stat(pa)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tconn, err := net.Dial(\"unix\", pa)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer conn.Close()\n\n\t\t\t\tenc := &rtph264.Encoder{\n\t\t\t\t\tPayloadType: 96,\n\t\t\t\t}\n\t\t\t\terr = enc.Init()\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tpkts, err := enc.Encode([][]byte{\n\t\t\t\t\t{5, 1},\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\tfor _, pkt := range pkts {\n\t\t\t\t\tvar buf []byte\n\t\t\t\t\tbuf, err = pkt.Marshal()\n\t\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\t\t_, err = conn.Write(buf)\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t}\n\n\t\t\t\t<-p.Unit\n\t\t\t}()\n\n\t\t\t_, err := os.Stat(pa)\n\t\t\trequire.Error(t, err)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/staticsources/rtsp/source.go",
    "content": "// Package rtsp contains the RTSP static source.\npackage rtsp\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/base\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/headers\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/counterdumper\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/errordumper\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/packetdumper\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/rtsp\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/tls\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n)\n\nfunc createRangeHeader(cnf *conf.Path) (*headers.Range, error) {\n\tswitch cnf.RTSPRangeType {\n\tcase conf.RTSPRangeTypeClock:\n\t\tstart, err := time.Parse(\"20060102T150405Z\", cnf.RTSPRangeStart)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn &headers.Range{\n\t\t\tValue: &headers.RangeUTC{\n\t\t\t\tStart: start,\n\t\t\t},\n\t\t}, nil\n\n\tcase conf.RTSPRangeTypeNPT:\n\t\tstart, err := time.ParseDuration(cnf.RTSPRangeStart)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn &headers.Range{\n\t\t\tValue: &headers.RangeNPT{\n\t\t\t\tStart: start,\n\t\t\t},\n\t\t}, nil\n\n\tcase conf.RTSPRangeTypeSMPTE:\n\t\tstart, err := time.ParseDuration(cnf.RTSPRangeStart)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn &headers.Range{\n\t\t\tValue: &headers.RangeSMPTE{\n\t\t\t\tStart: headers.RangeSMPTETime{\n\t\t\t\t\tTime: start,\n\t\t\t\t},\n\t\t\t},\n\t\t}, nil\n\n\tdefault:\n\t\treturn nil, nil\n\t}\n}\n\ntype parent interface {\n\tlogger.Writer\n\tSetReady(req defs.PathSourceStaticSetReadyReq) defs.PathSourceStaticSetReadyRes\n\tSetNotReady(req defs.PathSourceStaticSetNotReadyReq)\n}\n\n// Source is a RTSP static source.\ntype Source struct {\n\tDumpPackets       bool\n\tReadTimeout       conf.Duration\n\tWriteTimeout      conf.Duration\n\tWriteQueueSize    int\n\tUDPReadBufferSize uint\n\tParent            parent\n}\n\n// Log implements logger.Writer.\nfunc (s *Source) Log(level logger.Level, format string, args ...any) {\n\ts.Parent.Log(level, \"[RTSP source] \"+format, args...)\n}\n\n// Run implements StaticSource.\nfunc (s *Source) Run(params defs.StaticSourceRunParams) error {\n\ts.Log(logger.Debug, \"connecting\")\n\n\tpacketsLost := &counterdumper.Dumper{\n\t\tOnReport: func(val uint64) {\n\t\t\ts.Log(logger.Warn, \"%d RTP %s lost\",\n\t\t\t\tval,\n\t\t\t\tfunc() string {\n\t\t\t\t\tif val == 1 {\n\t\t\t\t\t\treturn \"packet\"\n\t\t\t\t\t}\n\t\t\t\t\treturn \"packets\"\n\t\t\t\t}())\n\t\t},\n\t}\n\n\tpacketsLost.Start()\n\tdefer packetsLost.Stop()\n\n\tdecodeErrors := &errordumper.Dumper{\n\t\tOnReport: func(val uint64, last error) {\n\t\t\tif val == 1 {\n\t\t\t\ts.Log(logger.Warn, \"decode error: %v\", last)\n\t\t\t} else {\n\t\t\t\ts.Log(logger.Warn, \"%d decode errors, last was: %v\", val, last)\n\t\t\t}\n\t\t},\n\t}\n\n\tdecodeErrors.Start()\n\tdefer decodeErrors.Stop()\n\n\tc := &gortsplib.Client{\n\t\tProtocol:          params.Conf.RTSPTransport.Protocol,\n\t\tReadTimeout:       time.Duration(s.ReadTimeout),\n\t\tWriteTimeout:      time.Duration(s.WriteTimeout),\n\t\tUDPReadBufferSize: int(s.UDPReadBufferSize),\n\t\tWriteQueueSize:    s.WriteQueueSize,\n\t\tAnyPortEnable:     params.Conf.RTSPAnyPort,\n\t\tUDPSourcePortRange: [2]uint16{\n\t\t\tuint16(params.Conf.RTSPUDPSourcePortRange[0]),\n\t\t\tuint16(params.Conf.RTSPUDPSourcePortRange[1]),\n\t\t},\n\t\tOnRequest: func(req *base.Request) {\n\t\t\ts.Log(logger.Debug, \"[c->s] %v\", req)\n\t\t},\n\t\tOnResponse: func(res *base.Response) {\n\t\t\ts.Log(logger.Debug, \"[s->c] %v\", res)\n\t\t},\n\t\tOnTransportSwitch: func(err error) {\n\t\t\ts.Log(logger.Warn, err.Error())\n\t\t},\n\t\tOnPacketsLost: func(lost uint64) {\n\t\t\tpacketsLost.Add(lost)\n\t\t},\n\t\tOnDecodeError: func(err error) {\n\t\t\tdecodeErrors.Add(err)\n\t\t},\n\t}\n\n\tu0, err := url.Parse(params.ResolvedSource)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tswitch u0.Scheme {\n\tcase \"rtsp+http\", \"rtsps+http\":\n\t\tc.Tunnel = gortsplib.TunnelHTTP\n\tcase \"rtsp+ws\", \"rtsps+ws\":\n\t\tc.Tunnel = gortsplib.TunnelWebSocket\n\t}\n\n\tswitch u0.Scheme {\n\tcase \"rtsp\", \"rtsp+http\", \"rtsp+ws\":\n\t\tu0.Scheme = \"rtsp\"\n\tdefault:\n\t\tu0.Scheme = \"rtsps\"\n\t\tc.TLSConfig = tls.MakeConfig(u0.Hostname(), params.Conf.SourceFingerprint)\n\t}\n\n\tu, err := base.ParseURL(u0.String())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tc.Scheme = u.Scheme\n\tc.Host = u.Host\n\n\tif params.Conf.RTSPUDPReadBufferSize != nil {\n\t\ts.UDPReadBufferSize = *params.Conf.RTSPUDPReadBufferSize\n\t}\n\n\tif s.DumpPackets {\n\t\tc.DialContext = (&packetdumper.DialContext{\n\t\t\tPrefix:      \"rtsp_source_conn\",\n\t\t\tDialContext: (&net.Dialer{}).DialContext,\n\t\t}).Do\n\n\t\tc.ListenPacket = (&packetdumper.ListenPacket{\n\t\t\tPrefix:       \"rtsp_source_packetconn\",\n\t\t\tListenPacket: net.ListenPacket,\n\t\t}).Do\n\t}\n\n\terr = c.Start()\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer c.Close()\n\n\treadErr := make(chan error)\n\tgo func() {\n\t\treadErr <- s.runInner(c, u, params.Conf)\n\t}()\n\n\tfor {\n\t\tselect {\n\t\tcase err = <-readErr:\n\t\t\treturn err\n\n\t\tcase <-params.ReloadConf:\n\n\t\tcase <-params.Context.Done():\n\t\t\tc.Close()\n\t\t\t<-readErr\n\t\t\treturn nil\n\t\t}\n\t}\n}\n\nfunc (s *Source) runInner(c *gortsplib.Client, u *base.URL, pathConf *conf.Path) error {\n\tdesc, _, err := c.Describe(u)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar medias []*description.Media\n\n\tfor _, m := range desc.Medias {\n\t\tif !m.IsBackChannel {\n\t\t\t_, err = c.Setup(desc.BaseURL, m, 0, 0)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tmedias = append(medias, m)\n\t\t}\n\t}\n\n\tif medias == nil {\n\t\treturn fmt.Errorf(\"no medias have been setupped\")\n\t}\n\n\tdesc2 := &description.Session{\n\t\tTitle:  desc.Title,\n\t\tMedias: medias,\n\t}\n\n\tvar subStream *stream.SubStream\n\n\trtsp.ToStream(\n\t\tc,\n\t\tdesc2.Medias,\n\t\tpathConf,\n\t\t&subStream,\n\t\ts)\n\n\tres := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{\n\t\tDesc:          desc2,\n\t\tUseRTPPackets: true,\n\t\tReplaceNTP:    !pathConf.UseAbsoluteTimestamp,\n\t})\n\tif res.Err != nil {\n\t\treturn res.Err\n\t}\n\n\tdefer s.Parent.SetNotReady(defs.PathSourceStaticSetNotReadyReq{})\n\n\tsubStream = res.SubStream\n\n\trangeHeader, err := createRangeHeader(pathConf)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = c.Play(rangeHeader)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn c.Wait()\n}\n\n// APISourceDescribe implements StaticSource.\nfunc (*Source) APISourceDescribe() *defs.APIPathSource {\n\treturn &defs.APIPathSource{\n\t\tType: defs.APIPathSourceTypeRTSPSource,\n\t\tID:   \"\",\n\t}\n}\n"
  },
  {
    "path": "internal/staticsources/rtsp/source_test.go",
    "content": "package rtsp\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/auth\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/base\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/pion/rtp\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n)\n\nfunc ptrOf[T any](v T) *T {\n\tp := new(T)\n\t*p = v\n\treturn p\n}\n\ntype testServer struct {\n\tonDescribe func(*gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error)\n\tonSetup    func(*gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error)\n\tonPlay     func(*gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error)\n}\n\nfunc (sh *testServer) OnDescribe(ctx *gortsplib.ServerHandlerOnDescribeCtx,\n) (*base.Response, *gortsplib.ServerStream, error) {\n\treturn sh.onDescribe(ctx)\n}\n\nfunc (sh *testServer) OnSetup(ctx *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) {\n\treturn sh.onSetup(ctx)\n}\n\nfunc (sh *testServer) OnPlay(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) {\n\treturn sh.onPlay(ctx)\n}\n\nfunc TestSource(t *testing.T) {\n\tfor _, ca := range []string{\n\t\t\"udp\",\n\t\t\"tcp\",\n\t\t\"rtsps\",\n\t\t\"rtsp+http\",\n\t\t\"rtsps+http\",\n\t\t\"rtsp+ws\",\n\t\t\"rtsps+ws\",\n\t} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tvar strm *gortsplib.ServerStream\n\n\t\t\tnonce, err := auth.GenerateNonce()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tmedia0 := test.UniqueMediaH264()\n\n\t\t\ts := gortsplib.Server{\n\t\t\t\tHandler: &testServer{\n\t\t\t\t\tonDescribe: func(ctx *gortsplib.ServerHandlerOnDescribeCtx,\n\t\t\t\t\t) (*base.Response, *gortsplib.ServerStream, error) {\n\t\t\t\t\t\tswitch ca {\n\t\t\t\t\t\tcase \"rtsp+http\", \"rtsps+http\":\n\t\t\t\t\t\t\trequire.Equal(t, gortsplib.TunnelHTTP, ctx.Conn.Transport().Tunnel)\n\t\t\t\t\t\tcase \"rtsp+ws\", \"rtsps+ws\":\n\t\t\t\t\t\t\trequire.Equal(t, gortsplib.TunnelWebSocket, ctx.Conn.Transport().Tunnel)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tswitch ca {\n\t\t\t\t\t\tcase \"rtsps\", \"rtsps+http\", \"rtsps+ws\":\n\t\t\t\t\t\t\trequire.Equal(t, \"rtsps\", ctx.Request.URL.Scheme)\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\trequire.Equal(t, \"rtsp\", ctx.Request.URL.Scheme)\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\terr2 := auth.Verify(ctx.Request, \"testuser\", \"testpass\", nil, \"IPCAM\", nonce)\n\t\t\t\t\t\tif err2 != nil {\n\t\t\t\t\t\t\treturn &base.Response{ //nolint:nilerr\n\t\t\t\t\t\t\t\tStatusCode: base.StatusUnauthorized,\n\t\t\t\t\t\t\t\tHeader: base.Header{\n\t\t\t\t\t\t\t\t\t\"WWW-Authenticate\": auth.GenerateWWWAuthenticate(nil, \"IPCAM\", nonce),\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t}, nil, nil\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\treturn &base.Response{\n\t\t\t\t\t\t\tStatusCode: base.StatusOK,\n\t\t\t\t\t\t}, strm, nil\n\t\t\t\t\t},\n\t\t\t\t\tonSetup: func(_ *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) {\n\t\t\t\t\t\treturn &base.Response{\n\t\t\t\t\t\t\tStatusCode: base.StatusOK,\n\t\t\t\t\t\t}, strm, nil\n\t\t\t\t\t},\n\t\t\t\t\tonPlay: func(_ *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) {\n\t\t\t\t\t\tgo func() {\n\t\t\t\t\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t\t\t\t\t\terr2 := strm.WritePacketRTP(media0, &rtp.Packet{\n\t\t\t\t\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\t\t\t\t\tVersion:        0x02,\n\t\t\t\t\t\t\t\t\tPayloadType:    96,\n\t\t\t\t\t\t\t\t\tSequenceNumber: 57899,\n\t\t\t\t\t\t\t\t\tTimestamp:      345234345,\n\t\t\t\t\t\t\t\t\tSSRC:           978651231,\n\t\t\t\t\t\t\t\t\tMarker:         true,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tPayload: []byte{5, 1, 2, 3, 4},\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\trequire.NoError(t, err2)\n\t\t\t\t\t\t}()\n\n\t\t\t\t\t\treturn &base.Response{\n\t\t\t\t\t\t\tStatusCode: base.StatusOK,\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tRTSPAddress: \"127.0.0.1:8555\",\n\t\t\t}\n\n\t\t\tswitch ca {\n\t\t\tcase \"udp\":\n\t\t\t\ts.UDPRTPAddress = \"127.0.0.1:8002\"\n\t\t\t\ts.UDPRTCPAddress = \"127.0.0.1:8003\"\n\n\t\t\tcase \"rtsps\", \"rtsps+http\", \"rtsps+ws\":\n\t\t\t\tvar serverCertFpath string\n\t\t\t\tserverCertFpath, err = test.CreateTempFile(test.TLSCertPub)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer os.Remove(serverCertFpath)\n\n\t\t\t\tvar serverKeyFpath string\n\t\t\t\tserverKeyFpath, err = test.CreateTempFile(test.TLSCertKey)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer os.Remove(serverKeyFpath)\n\n\t\t\t\tvar cert tls.Certificate\n\t\t\t\tcert, err = tls.LoadX509KeyPair(serverCertFpath, serverKeyFpath)\n\t\t\t\trequire.NoError(t, err)\n\n\t\t\t\ts.TLSConfig = &tls.Config{Certificates: []tls.Certificate{cert}}\n\t\t\t}\n\n\t\t\terr = s.Start()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer s.Close()\n\n\t\t\tstrm = &gortsplib.ServerStream{\n\t\t\t\tServer: &s,\n\t\t\t\tDesc:   &description.Session{Medias: []*description.Media{media0}},\n\t\t\t}\n\t\t\terr = strm.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer strm.Close()\n\n\t\t\tvar ur string\n\t\t\tcnf := &conf.Path{\n\t\t\t\tRTSPUDPSourcePortRange: []uint{10000, 65535},\n\t\t\t}\n\n\t\t\tswitch ca {\n\t\t\tcase \"udp\", \"tcp\":\n\t\t\t\tur = \"rtsp://testuser:testpass@localhost:8555/teststream\"\n\t\t\t\tvar sp conf.RTSPTransport\n\t\t\t\tsp.UnmarshalJSON([]byte(`\"` + ca + `\"`)) //nolint:errcheck\n\t\t\t\tcnf.RTSPTransport = sp\n\n\t\t\tcase \"rtsps\", \"rtsps+http\", \"rtsps+ws\":\n\t\t\t\tur = ca + \"://testuser:testpass@localhost:8555/teststream\"\n\t\t\t\tcnf.SourceFingerprint = \"33949E05FFFB5FF3E8AA16F8213A6251B4D9363804BA53233C4DA9A46D6F2739\"\n\n\t\t\tcase \"rtsp+http\", \"rtsp+ws\":\n\t\t\t\tur = ca + \"://testuser:testpass@localhost:8555/teststream\"\n\t\t\t}\n\n\t\t\tp := &test.StaticSourceParent{}\n\t\t\tp.Initialize()\n\t\t\tdefer p.Close()\n\n\t\t\tso := &Source{\n\t\t\t\tReadTimeout:    conf.Duration(10 * time.Second),\n\t\t\t\tWriteTimeout:   conf.Duration(10 * time.Second),\n\t\t\t\tWriteQueueSize: 2048,\n\t\t\t\tParent:         p,\n\t\t\t}\n\n\t\t\tdone := make(chan struct{})\n\t\t\tdefer func() { <-done }()\n\n\t\t\tctx, ctxCancel := context.WithCancel(context.Background())\n\t\t\tdefer ctxCancel()\n\n\t\t\treloadConf := make(chan *conf.Path)\n\n\t\t\tgo func() {\n\t\t\t\tso.Run(defs.StaticSourceRunParams{ //nolint:errcheck\n\t\t\t\t\tContext:        ctx,\n\t\t\t\t\tResolvedSource: ur,\n\t\t\t\t\tConf:           cnf,\n\t\t\t\t\tReloadConf:     reloadConf,\n\t\t\t\t})\n\t\t\t\tclose(done)\n\t\t\t}()\n\n\t\t\t<-p.Unit\n\n\t\t\t// the source must be listening on ReloadConf\n\t\t\treloadConf <- nil\n\t\t})\n\t}\n}\n\nfunc TestNoPassword(t *testing.T) {\n\tvar strm *gortsplib.ServerStream\n\n\tnonce, err := auth.GenerateNonce()\n\trequire.NoError(t, err)\n\n\tmedia0 := test.UniqueMediaH264()\n\n\ts := gortsplib.Server{\n\t\tHandler: &testServer{\n\t\t\tonDescribe: func(ctx *gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error) {\n\t\t\t\terr2 := auth.Verify(ctx.Request, \"testuser\", \"\", nil, \"IPCAM\", nonce)\n\t\t\t\tif err2 != nil {\n\t\t\t\t\treturn &base.Response{ //nolint:nilerr\n\t\t\t\t\t\tStatusCode: base.StatusUnauthorized,\n\t\t\t\t\t\tHeader: base.Header{\n\t\t\t\t\t\t\t\"WWW-Authenticate\": auth.GenerateWWWAuthenticate(nil, \"IPCAM\", nonce),\n\t\t\t\t\t\t},\n\t\t\t\t\t}, nil, nil\n\t\t\t\t}\n\n\t\t\t\treturn &base.Response{\n\t\t\t\t\tStatusCode: base.StatusOK,\n\t\t\t\t}, strm, nil\n\t\t\t},\n\t\t\tonSetup: func(_ *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) {\n\t\t\t\tgo func() {\n\t\t\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t\t\t\terr2 := strm.WritePacketRTP(media0, &rtp.Packet{\n\t\t\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\t\t\tVersion:        0x02,\n\t\t\t\t\t\t\tPayloadType:    96,\n\t\t\t\t\t\t\tSequenceNumber: 57899,\n\t\t\t\t\t\t\tTimestamp:      345234345,\n\t\t\t\t\t\t\tSSRC:           978651231,\n\t\t\t\t\t\t\tMarker:         true,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPayload: []byte{5, 1, 2, 3, 4},\n\t\t\t\t\t})\n\t\t\t\t\trequire.NoError(t, err2)\n\t\t\t\t}()\n\n\t\t\t\treturn &base.Response{\n\t\t\t\t\tStatusCode: base.StatusOK,\n\t\t\t\t}, strm, nil\n\t\t\t},\n\t\t\tonPlay: func(_ *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) {\n\t\t\t\treturn &base.Response{\n\t\t\t\t\tStatusCode: base.StatusOK,\n\t\t\t\t}, nil\n\t\t\t},\n\t\t},\n\t\tRTSPAddress: \"127.0.0.1:8555\",\n\t}\n\n\terr = s.Start()\n\trequire.NoError(t, err)\n\tdefer s.Close()\n\n\tstrm = &gortsplib.ServerStream{\n\t\tServer: &s,\n\t\tDesc:   &description.Session{Medias: []*description.Media{media0}},\n\t}\n\terr = strm.Initialize()\n\trequire.NoError(t, err)\n\tdefer strm.Close()\n\n\tvar sp conf.RTSPTransport\n\tsp.UnmarshalJSON([]byte(`\"tcp\"`)) //nolint:errcheck\n\n\tp := &test.StaticSourceParent{}\n\tp.Initialize()\n\tdefer p.Close()\n\n\tso := &Source{\n\t\tReadTimeout:    conf.Duration(10 * time.Second),\n\t\tWriteTimeout:   conf.Duration(10 * time.Second),\n\t\tWriteQueueSize: 2048,\n\t\tParent:         p,\n\t}\n\n\tdone := make(chan struct{})\n\tdefer func() { <-done }()\n\n\tctx, ctxCancel := context.WithCancel(context.Background())\n\tdefer ctxCancel()\n\n\tgo func() {\n\t\tso.Run(defs.StaticSourceRunParams{ //nolint:errcheck\n\t\t\tContext:        ctx,\n\t\t\tResolvedSource: \"rtsp://testuser:@127.0.0.1:8555/teststream\",\n\t\t\tConf: &conf.Path{\n\t\t\t\tRTSPTransport:          sp,\n\t\t\t\tRTSPUDPSourcePortRange: []uint{10000, 65535},\n\t\t\t},\n\t\t})\n\t\tclose(done)\n\t}()\n\n\t<-p.Unit\n}\n\nfunc TestRange(t *testing.T) {\n\tfor _, ca := range []string{\"clock\", \"npt\", \"smpte\"} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tvar strm *gortsplib.ServerStream\n\n\t\t\tmedia0 := test.UniqueMediaH264()\n\n\t\t\ts := gortsplib.Server{\n\t\t\t\tHandler: &testServer{\n\t\t\t\t\tonDescribe: func(_ *gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error) {\n\t\t\t\t\t\treturn &base.Response{\n\t\t\t\t\t\t\tStatusCode: base.StatusOK,\n\t\t\t\t\t\t}, strm, nil\n\t\t\t\t\t},\n\t\t\t\t\tonSetup: func(_ *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) {\n\t\t\t\t\t\treturn &base.Response{\n\t\t\t\t\t\t\tStatusCode: base.StatusOK,\n\t\t\t\t\t\t}, strm, nil\n\t\t\t\t\t},\n\t\t\t\t\tonPlay: func(ctx *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) {\n\t\t\t\t\t\tswitch ca {\n\t\t\t\t\t\tcase \"clock\":\n\t\t\t\t\t\t\trequire.Equal(t, base.HeaderValue{\"clock=20230812T120000Z-\"}, ctx.Request.Header[\"Range\"])\n\n\t\t\t\t\t\tcase \"npt\":\n\t\t\t\t\t\t\trequire.Equal(t, base.HeaderValue{\"npt=0.35-\"}, ctx.Request.Header[\"Range\"])\n\n\t\t\t\t\t\tcase \"smpte\":\n\t\t\t\t\t\t\trequire.Equal(t, base.HeaderValue{\"smpte=0:02:10-\"}, ctx.Request.Header[\"Range\"])\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tgo func() {\n\t\t\t\t\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t\t\t\t\t\terr := strm.WritePacketRTP(media0, &rtp.Packet{\n\t\t\t\t\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\t\t\t\t\tVersion:        0x02,\n\t\t\t\t\t\t\t\t\tPayloadType:    96,\n\t\t\t\t\t\t\t\t\tSequenceNumber: 57899,\n\t\t\t\t\t\t\t\t\tTimestamp:      345234345,\n\t\t\t\t\t\t\t\t\tSSRC:           978651231,\n\t\t\t\t\t\t\t\t\tMarker:         true,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tPayload: []byte{5, 1, 2, 3, 4},\n\t\t\t\t\t\t\t})\n\t\t\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t\t\t}()\n\n\t\t\t\t\t\treturn &base.Response{\n\t\t\t\t\t\t\tStatusCode: base.StatusOK,\n\t\t\t\t\t\t}, nil\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tRTSPAddress: \"127.0.0.1:8555\",\n\t\t\t}\n\n\t\t\terr := s.Start()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer s.Close()\n\n\t\t\tstrm = &gortsplib.ServerStream{\n\t\t\t\tServer: &s,\n\t\t\t\tDesc:   &description.Session{Medias: []*description.Media{media0}},\n\t\t\t}\n\t\t\terr = strm.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer strm.Close()\n\n\t\t\tcnf := &conf.Path{\n\t\t\t\tRTSPUDPSourcePortRange: []uint{10000, 65535},\n\t\t\t}\n\n\t\t\tswitch ca {\n\t\t\tcase \"clock\":\n\t\t\t\tcnf.RTSPRangeType = conf.RTSPRangeTypeClock\n\t\t\t\tcnf.RTSPRangeStart = \"20230812T120000Z\"\n\n\t\t\tcase \"npt\":\n\t\t\t\tcnf.RTSPRangeType = conf.RTSPRangeTypeNPT\n\t\t\t\tcnf.RTSPRangeStart = \"350ms\"\n\n\t\t\tcase \"smpte\":\n\t\t\t\tcnf.RTSPRangeType = conf.RTSPRangeTypeSMPTE\n\t\t\t\tcnf.RTSPRangeStart = \"130s\"\n\t\t\t}\n\n\t\t\tp := &test.StaticSourceParent{}\n\t\t\tp.Initialize()\n\t\t\tdefer p.Close()\n\n\t\t\tso := &Source{\n\t\t\t\tReadTimeout:    conf.Duration(10 * time.Second),\n\t\t\t\tWriteTimeout:   conf.Duration(10 * time.Second),\n\t\t\t\tWriteQueueSize: 2048,\n\t\t\t\tParent:         p,\n\t\t\t}\n\n\t\t\tdone := make(chan struct{})\n\t\t\tdefer func() { <-done }()\n\n\t\t\tctx, ctxCancel := context.WithCancel(context.Background())\n\t\t\tdefer ctxCancel()\n\n\t\t\tgo func() {\n\t\t\t\tso.Run(defs.StaticSourceRunParams{ //nolint:errcheck\n\t\t\t\t\tContext:        ctx,\n\t\t\t\t\tResolvedSource: \"rtsp://127.0.0.1:8555/teststream\",\n\t\t\t\t\tConf:           cnf,\n\t\t\t\t})\n\t\t\t\tclose(done)\n\t\t\t}()\n\n\t\t\t<-p.Unit\n\t\t})\n\t}\n}\n\nfunc TestSkipBackChannel(t *testing.T) {\n\tmedia0 := test.UniqueMediaH264()\n\tmedia1 := test.UniqueMediaMPEG4Audio()\n\tbackChannelMedia := &description.Media{\n\t\tType:          description.MediaTypeAudio,\n\t\tFormats:       []format.Format{&format.Opus{PayloadTyp: 96, ChannelCount: 2}},\n\t\tIsBackChannel: true,\n\t}\n\n\tvar strm *gortsplib.ServerStream\n\tsetupCount := 0\n\n\ts := gortsplib.Server{\n\t\tHandler: &testServer{\n\t\t\tonDescribe: func(_ *gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error) {\n\t\t\t\treturn &base.Response{\n\t\t\t\t\tStatusCode: base.StatusOK,\n\t\t\t\t}, strm, nil\n\t\t\t},\n\t\t\tonSetup: func(_ *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) {\n\t\t\t\tsetupCount++\n\t\t\t\treturn &base.Response{\n\t\t\t\t\tStatusCode: base.StatusOK,\n\t\t\t\t}, strm, nil\n\t\t\t},\n\t\t\tonPlay: func(_ *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) {\n\t\t\t\tgo func() {\n\t\t\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t\t\t\terr := strm.WritePacketRTP(media0, &rtp.Packet{\n\t\t\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\t\t\tVersion:        0x02,\n\t\t\t\t\t\t\tPayloadType:    96,\n\t\t\t\t\t\t\tSequenceNumber: 57899,\n\t\t\t\t\t\t\tTimestamp:      345234345,\n\t\t\t\t\t\t\tSSRC:           978651231,\n\t\t\t\t\t\t\tMarker:         true,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPayload: []byte{5, 1, 2, 3, 4},\n\t\t\t\t\t})\n\t\t\t\t\trequire.NoError(t, err)\n\t\t\t\t}()\n\n\t\t\t\treturn &base.Response{\n\t\t\t\t\tStatusCode: base.StatusOK,\n\t\t\t\t}, nil\n\t\t\t},\n\t\t},\n\t\tRTSPAddress: \"127.0.0.1:8555\",\n\t}\n\n\terr := s.Start()\n\trequire.NoError(t, err)\n\tdefer s.Close()\n\n\tstrm = &gortsplib.ServerStream{\n\t\tServer: &s,\n\t\tDesc:   &description.Session{Medias: []*description.Media{media0, media1, backChannelMedia}},\n\t}\n\terr = strm.Initialize()\n\trequire.NoError(t, err)\n\tdefer strm.Close()\n\n\tvar sp conf.RTSPTransport\n\tsp.UnmarshalJSON([]byte(`\"tcp\"`)) //nolint:errcheck\n\n\tp := &test.StaticSourceParent{}\n\tp.Initialize()\n\tdefer p.Close()\n\n\tso := &Source{\n\t\tReadTimeout:    conf.Duration(10 * time.Second),\n\t\tWriteTimeout:   conf.Duration(10 * time.Second),\n\t\tWriteQueueSize: 2048,\n\t\tParent:         p,\n\t}\n\n\tdone := make(chan struct{})\n\tdefer func() { <-done }()\n\n\tctx, ctxCancel := context.WithCancel(context.Background())\n\tdefer ctxCancel()\n\n\tgo func() {\n\t\tso.Run(defs.StaticSourceRunParams{ //nolint:errcheck\n\t\t\tContext:        ctx,\n\t\t\tResolvedSource: \"rtsp://127.0.0.1:8555/teststream\",\n\t\t\tConf: &conf.Path{\n\t\t\t\tRTSPTransport:          conf.RTSPTransport{Protocol: ptrOf(gortsplib.ProtocolTCP)},\n\t\t\t\tRTSPUDPSourcePortRange: []uint{10000, 65535},\n\t\t\t},\n\t\t})\n\t\tclose(done)\n\t}()\n\n\t<-p.Unit\n\n\trequire.Equal(t, 2, setupCount)\n}\n\nfunc TestOnlyBackChannelsError(t *testing.T) {\n\tbackChannelMedia1 := &description.Media{\n\t\tType:          description.MediaTypeAudio,\n\t\tFormats:       []format.Format{&format.Opus{PayloadTyp: 96, ChannelCount: 2}},\n\t\tIsBackChannel: true,\n\t}\n\tbackChannelMedia2 := &description.Media{\n\t\tType:          description.MediaTypeAudio,\n\t\tFormats:       []format.Format{&format.G711{PayloadTyp: 8, SampleRate: 8000, ChannelCount: 1}},\n\t\tIsBackChannel: true,\n\t}\n\n\tvar strm *gortsplib.ServerStream\n\n\ts := gortsplib.Server{\n\t\tHandler: &testServer{\n\t\t\tonDescribe: func(_ *gortsplib.ServerHandlerOnDescribeCtx) (*base.Response, *gortsplib.ServerStream, error) {\n\t\t\t\treturn &base.Response{\n\t\t\t\t\tStatusCode: base.StatusOK,\n\t\t\t\t}, strm, nil\n\t\t\t},\n\t\t\tonSetup: func(_ *gortsplib.ServerHandlerOnSetupCtx) (*base.Response, *gortsplib.ServerStream, error) {\n\t\t\t\treturn &base.Response{\n\t\t\t\t\tStatusCode: base.StatusOK,\n\t\t\t\t}, strm, nil\n\t\t\t},\n\t\t\tonPlay: func(_ *gortsplib.ServerHandlerOnPlayCtx) (*base.Response, error) {\n\t\t\t\treturn &base.Response{\n\t\t\t\t\tStatusCode: base.StatusOK,\n\t\t\t\t}, nil\n\t\t\t},\n\t\t},\n\t\tRTSPAddress: \"127.0.0.1:8555\",\n\t}\n\n\terr := s.Start()\n\trequire.NoError(t, err)\n\tdefer s.Close()\n\n\tstrm = &gortsplib.ServerStream{\n\t\tServer: &s,\n\t\tDesc:   &description.Session{Medias: []*description.Media{backChannelMedia1, backChannelMedia2}},\n\t}\n\terr = strm.Initialize()\n\trequire.NoError(t, err)\n\tdefer strm.Close()\n\n\tp := &test.StaticSourceParent{}\n\tp.Initialize()\n\n\tso := &Source{\n\t\tReadTimeout:    conf.Duration(10 * time.Second),\n\t\tWriteTimeout:   conf.Duration(10 * time.Second),\n\t\tWriteQueueSize: 2048,\n\t\tParent:         p,\n\t}\n\n\tctx, ctxCancel := context.WithCancel(context.Background())\n\tdefer ctxCancel()\n\n\terr = so.Run(defs.StaticSourceRunParams{\n\t\tContext:        ctx,\n\t\tResolvedSource: \"rtsp://127.0.0.1:8555/teststream\",\n\t\tConf: &conf.Path{\n\t\t\tRTSPTransport:          conf.RTSPTransport{Protocol: ptrOf(gortsplib.ProtocolTCP)},\n\t\t\tRTSPUDPSourcePortRange: []uint{10000, 65535},\n\t\t},\n\t})\n\n\trequire.Error(t, err)\n\trequire.Contains(t, err.Error(), \"no media\")\n}\n"
  },
  {
    "path": "internal/staticsources/srt/source.go",
    "content": "// Package srt contains the SRT static source.\npackage srt\n\nimport (\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\tsrt \"github.com/datarhei/gosrt\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/errordumper\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/mpegts\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n)\n\ntype parent interface {\n\tlogger.Writer\n\tSetReady(req defs.PathSourceStaticSetReadyReq) defs.PathSourceStaticSetReadyRes\n\tSetNotReady(req defs.PathSourceStaticSetNotReadyReq)\n}\n\n// Source is a SRT static source.\ntype Source struct {\n\tReadTimeout conf.Duration\n\tParent      parent\n}\n\n// Log implements logger.Writer.\nfunc (s *Source) Log(level logger.Level, format string, args ...any) {\n\ts.Parent.Log(level, \"[SRT source] \"+format, args...)\n}\n\n// Run implements StaticSource.\nfunc (s *Source) Run(params defs.StaticSourceRunParams) error {\n\ts.Log(logger.Debug, \"connecting\")\n\n\tconf := srt.DefaultConfig()\n\taddress, err := conf.UnmarshalURL(params.ResolvedSource)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\terr = conf.Validate()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tsconn, err := srt.Dial(\"srt\", address, conf)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treadDone := make(chan error)\n\tgo func() {\n\t\treadDone <- s.runReader(sconn)\n\t}()\n\n\tfor {\n\t\tselect {\n\t\tcase err = <-readDone:\n\t\t\tsconn.Close()\n\t\t\treturn err\n\n\t\tcase <-params.ReloadConf:\n\n\t\tcase <-params.Context.Done():\n\t\t\tsconn.Close()\n\t\t\t<-readDone\n\t\t\treturn nil\n\t\t}\n\t}\n}\n\nfunc (s *Source) runReader(sconn srt.Conn) error {\n\tsconn.SetReadDeadline(time.Now().Add(time.Duration(s.ReadTimeout)))\n\tr := &mpegts.EnhancedReader{R: sconn}\n\terr := r.Initialize()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tdecodeErrors := &errordumper.Dumper{\n\t\tOnReport: func(val uint64, last error) {\n\t\t\tif val == 1 {\n\t\t\t\ts.Log(logger.Warn, \"decode error: %v\", last)\n\t\t\t} else {\n\t\t\t\ts.Log(logger.Warn, \"%d decode errors, last was: %v\", val, last)\n\t\t\t}\n\t\t},\n\t}\n\n\tdecodeErrors.Start()\n\tdefer decodeErrors.Stop()\n\n\tr.OnDecodeError(func(err error) {\n\t\tdecodeErrors.Add(err)\n\t})\n\n\tvar subStream *stream.SubStream\n\n\tmedias, err := mpegts.ToStream(r, &subStream, s)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tres := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{\n\t\tDesc:          &description.Session{Medias: medias},\n\t\tUseRTPPackets: false,\n\t\tReplaceNTP:    true,\n\t})\n\tif res.Err != nil {\n\t\treturn res.Err\n\t}\n\n\tdefer s.Parent.SetNotReady(defs.PathSourceStaticSetNotReadyReq{})\n\n\tsubStream = res.SubStream\n\n\tfor {\n\t\tsconn.SetReadDeadline(time.Now().Add(time.Duration(s.ReadTimeout)))\n\t\terr = r.Read()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n}\n\n// APISourceDescribe implements StaticSource.\nfunc (*Source) APISourceDescribe() *defs.APIPathSource {\n\treturn &defs.APIPathSource{\n\t\tType: defs.APIPathSourceTypeSRTSource,\n\t\tID:   \"\",\n\t}\n}\n"
  },
  {
    "path": "internal/staticsources/srt/source_test.go",
    "content": "package srt\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts\"\n\ttscodecs \"github.com/bluenviron/mediacommon/v2/pkg/formats/mpegts/codecs\"\n\tsrt \"github.com/datarhei/gosrt\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n)\n\nfunc TestSource(t *testing.T) {\n\tln, err := srt.Listen(\"srt\", \"127.0.0.1:9002\", srt.DefaultConfig())\n\trequire.NoError(t, err)\n\tdefer ln.Close()\n\n\tp := &test.StaticSourceParent{}\n\tp.Initialize()\n\n\tso := &Source{\n\t\tReadTimeout: conf.Duration(10 * time.Second),\n\t\tParent:      p,\n\t}\n\n\tdone := make(chan struct{})\n\tdefer func() { <-done }()\n\n\tctx, ctxCancel := context.WithCancel(context.Background())\n\tdefer ctxCancel()\n\n\treloadConf := make(chan *conf.Path)\n\n\tgo func() {\n\t\tso.Run(defs.StaticSourceRunParams{ //nolint:errcheck\n\t\t\tContext:        ctx,\n\t\t\tResolvedSource: \"srt://127.0.0.1:9002?streamid=sidname&passphrase=ttest1234567\",\n\t\t\tConf:           &conf.Path{},\n\t\t\tReloadConf:     reloadConf,\n\t\t})\n\t\tclose(done)\n\t}()\n\n\treq, err2 := ln.Accept2()\n\trequire.NoError(t, err2)\n\n\trequire.Equal(t, \"sidname\", req.StreamId())\n\terr2 = req.SetPassphrase(\"ttest1234567\")\n\trequire.NoError(t, err2)\n\n\tconn, err2 := req.Accept()\n\trequire.NoError(t, err2)\n\tdefer conn.Close()\n\n\ttrack := &mpegts.Track{Codec: &tscodecs.H264{}}\n\n\tbw := bufio.NewWriter(conn)\n\tw := &mpegts.Writer{W: bw, Tracks: []*mpegts.Track{track}}\n\terr2 = w.Initialize()\n\trequire.NoError(t, err2)\n\n\terr2 = w.WriteH264(track, 0, 0, [][]byte{{ // IDR\n\t\t5, 1,\n\t}})\n\trequire.NoError(t, err2)\n\n\terr = bw.Flush()\n\trequire.NoError(t, err)\n\n\terr = w.WriteH264(track, 0, 0, [][]byte{{ // non-IDR\n\t\t5, 2,\n\t}})\n\trequire.NoError(t, err)\n\n\terr = bw.Flush()\n\trequire.NoError(t, err)\n\n\t<-p.Unit\n\n\t// the source must be listening on ReloadConf\n\treloadConf <- nil\n\n\t// stop test reader before 2nd H264 packet is received to avoid a crash\n\tp.Close()\n}\n"
  },
  {
    "path": "internal/staticsources/webrtc/source.go",
    "content": "// Package webrtc contains the WebRTC static source.\npackage webrtc\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/packetdumper\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/tls\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/webrtc\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/whip\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n)\n\ntype parent interface {\n\tlogger.Writer\n\tSetReady(req defs.PathSourceStaticSetReadyReq) defs.PathSourceStaticSetReadyRes\n\tSetNotReady(req defs.PathSourceStaticSetNotReadyReq)\n}\n\n// Source is a WebRTC static source.\ntype Source struct {\n\tDumpPackets       bool\n\tReadTimeout       conf.Duration\n\tUDPReadBufferSize uint\n\tParent            parent\n}\n\n// Log implements logger.Writer.\nfunc (s *Source) Log(level logger.Level, format string, args ...any) {\n\ts.Parent.Log(level, \"[WebRTC source] \"+format, args...)\n}\n\n// Run implements StaticSource.\nfunc (s *Source) Run(params defs.StaticSourceRunParams) error {\n\ts.Log(logger.Debug, \"connecting\")\n\n\tu, err := url.Parse(params.ResolvedSource)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tu.Scheme = strings.ReplaceAll(u.Scheme, \"whep\", \"http\")\n\n\tdialContext := (&net.Dialer{}).DialContext\n\n\tif s.DumpPackets {\n\t\tdialContext = (&packetdumper.DialContext{\n\t\t\tPrefix:      \"webrtc_source_conn\",\n\t\t\tDialContext: dialContext,\n\t\t}).Do\n\t}\n\n\ttr := &http.Transport{\n\t\tDialContext:     dialContext,\n\t\tTLSClientConfig: tls.MakeConfig(u.Hostname(), params.Conf.SourceFingerprint),\n\t}\n\tdefer tr.CloseIdleConnections()\n\n\tclient := whip.Client{\n\t\tURL: u,\n\t\tHTTPClient: &http.Client{\n\t\t\tTimeout:   time.Duration(s.ReadTimeout),\n\t\t\tTransport: tr,\n\t\t},\n\t\tBearerToken:        params.Conf.WHEPBearerToken,\n\t\tUDPReadBufferSize:  s.UDPReadBufferSize,\n\t\tSTUNGatherTimeout:  time.Duration(params.Conf.WHEPSTUNGatherTimeout),\n\t\tHandshakeTimeout:   time.Duration(params.Conf.WHEPHandshakeTimeout),\n\t\tTrackGatherTimeout: time.Duration(params.Conf.WHEPTrackGatherTimeout),\n\t\tLog:                s,\n\t}\n\terr = client.Initialize(params.Context)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar subStream *stream.SubStream\n\n\tmedias, err := webrtc.ToStream(client.PeerConnection(), params.Conf, &subStream, s)\n\tif err != nil {\n\t\tclient.Close() //nolint:errcheck\n\t\treturn err\n\t}\n\n\trres := s.Parent.SetReady(defs.PathSourceStaticSetReadyReq{\n\t\tDesc:          &description.Session{Medias: medias},\n\t\tUseRTPPackets: true,\n\t\tReplaceNTP:    !params.Conf.UseAbsoluteTimestamp,\n\t})\n\tif rres.Err != nil {\n\t\tclient.Close() //nolint:errcheck\n\t\treturn rres.Err\n\t}\n\n\tdefer s.Parent.SetNotReady(defs.PathSourceStaticSetNotReadyReq{})\n\n\tsubStream = rres.SubStream\n\n\tclient.StartReading()\n\n\treadErr := make(chan error)\n\n\tgo func() {\n\t\treadErr <- client.Wait()\n\t}()\n\n\tfor {\n\t\tselect {\n\t\tcase err = <-readErr:\n\t\t\tclient.Close() //nolint:errcheck\n\t\t\treturn err\n\n\t\tcase <-params.ReloadConf:\n\n\t\tcase <-params.Context.Done():\n\t\t\tclient.Close() //nolint:errcheck\n\t\t\t<-readErr\n\t\t\treturn fmt.Errorf(\"terminated\")\n\t\t}\n\t}\n}\n\n// APISourceDescribe implements StaticSource.\nfunc (*Source) APISourceDescribe() *defs.APIPathSource {\n\treturn &defs.APIPathSource{\n\t\tType: defs.APIPathSourceTypeWebRTCSource,\n\t\tID:   \"\",\n\t}\n}\n"
  },
  {
    "path": "internal/staticsources/webrtc/source_test.go",
    "content": "package webrtc\n\nimport (\n\t\"context\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/pion/rtp\"\n\tpwebrtc \"github.com/pion/webrtc/v4\"\n\t\"github.com/stretchr/testify/require\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/protocols/webrtc\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n)\n\nfunc whipOffer(body []byte) *pwebrtc.SessionDescription {\n\treturn &pwebrtc.SessionDescription{\n\t\tType: pwebrtc.SDPTypeOffer,\n\t\tSDP:  string(body),\n\t}\n}\n\nfunc TestSource(t *testing.T) {\n\toutgoingTracks := []*webrtc.OutgoingTrack{{\n\t\tCaps: pwebrtc.RTPCodecCapability{\n\t\t\tMimeType:    \"audio/opus\",\n\t\t\tClockRate:   48000,\n\t\t\tChannels:    2,\n\t\t\tSDPFmtpLine: \"minptime=10;useinbandfec=1;stereo=1;sprop-stereo=1\",\n\t\t},\n\t}}\n\n\tpc := &webrtc.PeerConnection{\n\t\tLocalRandomUDP:    true,\n\t\tIPsFromInterfaces: true,\n\t\tPublish:           true,\n\t\tOutgoingTracks:    outgoingTracks,\n\t\tLog:               test.NilLogger,\n\t}\n\terr := pc.Start()\n\trequire.NoError(t, err)\n\tdefer pc.Close()\n\n\tstate := 0\n\n\thttpServ := &http.Server{\n\t\tHandler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\t\tswitch state {\n\t\t\tcase 0:\n\t\t\t\trequire.Equal(t, http.MethodOptions, r.Method)\n\t\t\t\trequire.Equal(t, \"/my/resource\", r.URL.Path)\n\n\t\t\t\tw.Header().Set(\"Access-Control-Allow-Methods\", \"OPTIONS, GET, POST, PATCH\")\n\t\t\t\tw.Header().Set(\"Access-Control-Allow-Headers\", \"Authorization, Content-Type, If-Match\")\n\t\t\t\tw.WriteHeader(http.StatusNoContent)\n\n\t\t\tcase 1:\n\t\t\t\trequire.Equal(t, http.MethodPost, r.Method)\n\t\t\t\trequire.Equal(t, \"/my/resource\", r.URL.Path)\n\t\t\t\trequire.Equal(t, \"application/sdp\", r.Header.Get(\"Content-Type\"))\n\n\t\t\t\tbody, err2 := io.ReadAll(r.Body)\n\t\t\t\trequire.NoError(t, err2)\n\t\t\t\toffer := whipOffer(body)\n\n\t\t\t\tanswer, err2 := pc.CreateFullAnswer(offer)\n\t\t\t\trequire.NoError(t, err2)\n\n\t\t\t\tw.Header().Set(\"Content-Type\", \"application/sdp\")\n\t\t\t\tw.Header().Set(\"Accept-Patch\", \"application/trickle-ice-sdpfrag\")\n\t\t\t\tw.Header().Set(\"ETag\", \"test_etag\")\n\t\t\t\tw.Header().Set(\"Location\", \"/my/resource/sessionid\")\n\t\t\t\tw.WriteHeader(http.StatusCreated)\n\t\t\t\tw.Write([]byte(answer.SDP))\n\n\t\t\t\tgo func() {\n\t\t\t\t\terr3 := pc.WaitUntilConnected(10 * time.Second)\n\t\t\t\t\trequire.NoError(t, err3)\n\n\t\t\t\t\terr3 = outgoingTracks[0].WriteRTP(&rtp.Packet{\n\t\t\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\t\t\tVersion:        2,\n\t\t\t\t\t\t\tMarker:         true,\n\t\t\t\t\t\t\tPayloadType:    111,\n\t\t\t\t\t\t\tSequenceNumber: 1123,\n\t\t\t\t\t\t\tTimestamp:      45343,\n\t\t\t\t\t\t\tSSRC:           563424,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tPayload: []byte{5, 2},\n\t\t\t\t\t})\n\t\t\t\t\trequire.NoError(t, err3)\n\t\t\t\t}()\n\n\t\t\tdefault:\n\t\t\t\trequire.Equal(t, \"/my/resource/sessionid\", r.URL.Path)\n\n\t\t\t\tswitch r.Method {\n\t\t\t\tcase http.MethodPatch:\n\t\t\t\t\tw.WriteHeader(http.StatusNoContent)\n\n\t\t\t\tcase http.MethodDelete:\n\t\t\t\t\tw.WriteHeader(http.StatusOK)\n\n\t\t\t\tdefault:\n\t\t\t\t\tt.Errorf(\"should not happen\")\n\t\t\t\t}\n\t\t\t}\n\t\t\tstate++\n\t\t}),\n\t}\n\n\tln, err := net.Listen(\"tcp\", \"localhost:9003\")\n\trequire.NoError(t, err)\n\n\tgo httpServ.Serve(ln)\n\tdefer httpServ.Shutdown(context.Background())\n\n\tp := &test.StaticSourceParent{}\n\tp.Initialize()\n\tdefer p.Close()\n\n\tso := &Source{\n\t\tReadTimeout: conf.Duration(10 * time.Second),\n\t\tParent:      p,\n\t}\n\n\tdone := make(chan struct{})\n\tdefer func() { <-done }()\n\n\tctx, ctxCancel := context.WithCancel(context.Background())\n\tdefer ctxCancel()\n\n\treloadConf := make(chan *conf.Path)\n\n\tgo func() {\n\t\tso.Run(defs.StaticSourceRunParams{ //nolint:errcheck\n\t\t\tContext:        ctx,\n\t\t\tResolvedSource: \"whep://localhost:9003/my/resource\",\n\t\t\tConf:           &conf.Path{},\n\t\t\tReloadConf:     reloadConf,\n\t\t})\n\t\tclose(done)\n\t}()\n\n\t<-p.Unit\n\n\t// the source must be listening on ReloadConf\n\treloadConf <- nil\n}\n"
  },
  {
    "path": "internal/stream/format_updater.go",
    "content": "package stream\n\nimport (\n\t\"bytes\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\tmch264 \"github.com/bluenviron/mediacommon/v2/pkg/codecs/h264\"\n\tmch265 \"github.com/bluenviron/mediacommon/v2/pkg/codecs/h265\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4video\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n)\n\ntype formatUpdater func(format.Format, unit.Payload)\n\nfunc formatUpdaterH265(forma format.Format, payload unit.Payload) {\n\tformatH265 := forma.(*format.H265)\n\tau := payload.(unit.PayloadH265)\n\n\tvps, sps, pps := formatH265.VPS, formatH265.SPS, formatH265.PPS\n\tupdate := false\n\n\tfor _, nalu := range au {\n\t\ttyp := mch265.NALUType((nalu[0] >> 1) & 0b111111)\n\n\t\tswitch typ {\n\t\tcase mch265.NALUType_VPS_NUT:\n\t\t\tif !bytes.Equal(nalu, formatH265.VPS) {\n\t\t\t\tvps = nalu\n\t\t\t\tupdate = true\n\t\t\t}\n\n\t\tcase mch265.NALUType_SPS_NUT:\n\t\t\tif !bytes.Equal(nalu, formatH265.SPS) {\n\t\t\t\tsps = nalu\n\t\t\t\tupdate = true\n\t\t\t}\n\n\t\tcase mch265.NALUType_PPS_NUT:\n\t\t\tif !bytes.Equal(nalu, formatH265.PPS) {\n\t\t\t\tpps = nalu\n\t\t\t\tupdate = true\n\t\t\t}\n\t\t}\n\t}\n\n\tif update {\n\t\tformatH265.SafeSetParams(vps, sps, pps)\n\t}\n}\n\nfunc formatUpdaterH264(forma format.Format, payload unit.Payload) {\n\tformatH264 := forma.(*format.H264)\n\tau := payload.(unit.PayloadH264)\n\n\tsps, pps := formatH264.SPS, formatH264.PPS\n\tupdate := false\n\n\tfor _, nalu := range au {\n\t\ttyp := mch264.NALUType(nalu[0] & 0x1F)\n\n\t\tswitch typ {\n\t\tcase mch264.NALUTypeSPS:\n\t\t\tif !bytes.Equal(nalu, sps) {\n\t\t\t\tsps = nalu\n\t\t\t\tupdate = true\n\t\t\t}\n\n\t\tcase mch264.NALUTypePPS:\n\t\t\tif !bytes.Equal(nalu, pps) {\n\t\t\t\tpps = nalu\n\t\t\t\tupdate = true\n\t\t\t}\n\t\t}\n\t}\n\n\tif update {\n\t\tformatH264.SafeSetParams(sps, pps)\n\t}\n}\n\nfunc formatUpdaterMPEG4Video(forma format.Format, payload unit.Payload) {\n\tformatMPEG4Video := forma.(*format.MPEG4Video)\n\tframe := payload.(unit.PayloadMPEG4Video)\n\n\tif bytes.HasPrefix(frame, []byte{0, 0, 1, byte(mpeg4video.VisualObjectSequenceStartCode)}) {\n\t\tend := bytes.Index(frame[4:], []byte{0, 0, 1, byte(mpeg4video.GroupOfVOPStartCode)})\n\t\tif end < 0 {\n\t\t\treturn\n\t\t}\n\t\tconf := frame[:end+4]\n\n\t\tif !bytes.Equal(conf, formatMPEG4Video.Config) {\n\t\t\tformatMPEG4Video.SafeSetParams(conf)\n\t\t}\n\t}\n}\n\nfunc newFormatUpdater(forma format.Format) formatUpdater {\n\tswitch forma.(type) {\n\tcase *format.H265:\n\t\treturn formatUpdaterH265\n\n\tcase *format.H264:\n\t\treturn formatUpdaterH264\n\n\tcase *format.MPEG4Video:\n\t\treturn formatUpdaterMPEG4Video\n\n\tdefault:\n\t\treturn formatUpdater(func(_ format.Format, _ unit.Payload) {\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/stream/offline_sub_stream.go",
    "content": "package stream\n\nimport (\n\t\"context\"\n\t\"sync\"\n\t\"time\"\n)\n\nfunc multiplyAndDivide2(v, m, d time.Duration) time.Duration {\n\tsecs := v / d\n\tdec := v % d\n\treturn (secs*m + dec*m/d)\n}\n\ntype offlineSubStream struct {\n\tstream *Stream\n\n\tsubStream *SubStream\n\tctx       context.Context\n\tctxCancel func()\n\twg        sync.WaitGroup\n\ttracks    []*offlineSubStreamTrack\n}\n\nfunc (o *offlineSubStream) initialize() error {\n\to.subStream = &SubStream{\n\t\tStream:        o.stream,\n\t\tCurDesc:       o.stream.offlineDesc,\n\t\tUseRTPPackets: false,\n\t}\n\terr := o.subStream.Initialize()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\to.ctx, o.ctxCancel = context.WithCancel(context.Background())\n\n\tpos := 0\n\to.tracks = make([]*offlineSubStreamTrack, len(o.subStream.CurDesc.Medias))\n\n\tfor _, media := range o.subStream.CurDesc.Medias {\n\t\tfor _, forma := range media.Formats {\n\t\t\tt := &offlineSubStreamTrack{\n\t\t\t\twg:        &o.wg,\n\t\t\t\tfile:      o.stream.AlwaysAvailableFile,\n\t\t\t\tpos:       pos,\n\t\t\t\tctx:       o.ctx,\n\t\t\t\tsubStream: o.subStream,\n\t\t\t\tmedia:     media,\n\t\t\t\tformat:    forma,\n\t\t\t}\n\t\t\tt.initialize()\n\t\t\to.tracks[pos] = t\n\t\t\tpos++\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (o *offlineSubStream) close(waitLastSample bool) {\n\tfor _, track := range o.tracks {\n\t\ttrack.waitLastSample = waitLastSample\n\t}\n\n\to.ctxCancel()\n\to.wg.Wait()\n}\n"
  },
  {
    "path": "internal/stream/offline_sub_stream_track.go",
    "content": "package stream\n\nimport (\n\t\"bytes\"\n\t\"context\"\n\t_ \"embed\"\n\t\"io\"\n\t\"os\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/av1\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/h264\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio\"\n\tmcodecs \"github.com/bluenviron/mediacommon/v2/pkg/formats/mp4/codecs\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/pmp4\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n)\n\n//go:embed offline_av1.mp4\nvar offlineAV1 []byte\n\n//go:embed offline_vp9.mp4\nvar offlineVP9 []byte\n\n//go:embed offline_h265.mp4\nvar offlineH265 []byte\n\nvar offlineH265VPS = []byte{\n\t0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x60,\n\t0x00, 0x00, 0x03, 0x00, 0x90, 0x00, 0x00, 0x03,\n\t0x00, 0x00, 0x03, 0x00, 0x78, 0xba, 0x02, 0x40,\n}\n\nvar offlineH265SPS = []byte{\n\t0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x03,\n\t0x00, 0x90, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03,\n\t0x00, 0x78, 0xa0, 0x03, 0xc0, 0x80, 0x11, 0x07,\n\t0xcb, 0x96, 0xe9, 0x29, 0x30, 0xbc, 0x05, 0xa0,\n\t0x20, 0x00, 0x00, 0x03, 0x00, 0x20, 0x00, 0x00,\n\t0x03, 0x03, 0xc1,\n}\n\nvar offlineH265PPS = []byte{\n\t0x44, 0x01, 0xc0, 0x73, 0xc1, 0x89,\n}\n\n//go:embed offline_h264.mp4\nvar offlineH264 []byte\n\nvar offlineH264SPS = []byte{\n\t0x67, 0x42, 0xc0, 0x28, 0xda, 0x01, 0xe0, 0x08,\n\t0x9f, 0x97, 0x01, 0x10, 0x00, 0x00, 0x03, 0x00,\n\t0x10, 0x00, 0x00, 0x03, 0x03, 0xc0, 0xf1, 0x83,\n\t0x2a,\n}\n\nvar offlineH264PPS = []byte{\n\t0x68, 0xce, 0x3c, 0x80,\n}\n\ntype offlineSubStreamTrack struct {\n\twg             *sync.WaitGroup\n\tfile           string\n\tpos            int\n\tctx            context.Context\n\tsubStream      *SubStream\n\tmedia          *description.Media\n\tformat         format.Format\n\twaitLastSample bool\n}\n\nfunc (t *offlineSubStreamTrack) initialize() {\n\tt.wg.Add(1)\n\tgo t.run()\n}\n\nfunc (t *offlineSubStreamTrack) run() {\n\tdefer t.wg.Done()\n\n\tif t.file != \"\" {\n\t\tf, err := os.Open(t.file)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\tdefer f.Close()\n\n\t\terr = t.runFile(f, t.pos)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t\treturn\n\t}\n\n\tconst audioWritesPerSecond = 10\n\tvar pts int64\n\tstartSystemTime := time.Now()\n\n\tswitch forma := t.format.(type) {\n\tcase *format.Opus:\n\t\tunitsPerWrite := (forma.ClockRate() / 960) / audioWritesPerSecond\n\t\twriteDuration := 960 * int64(unitsPerWrite)\n\n\t\tfor {\n\t\t\tpayload := make(unit.PayloadOpus, unitsPerWrite)\n\t\t\tfor i := range payload {\n\t\t\t\tpayload[i] = []byte{0xF8, 0xFF, 0xFE} // DTX frame\n\t\t\t}\n\n\t\t\tt.subStream.WriteUnit(t.media, t.format, &unit.Unit{\n\t\t\t\tPTS:     pts,\n\t\t\t\tNTP:     time.Time{},\n\t\t\t\tPayload: payload,\n\t\t\t})\n\n\t\t\tpts += writeDuration\n\n\t\t\tptsGo := multiplyAndDivide2(time.Duration(pts), time.Second, 48000)\n\t\t\tsystemTime := startSystemTime.Add(ptsGo)\n\n\t\t\tif !t.sleep(systemTime) {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\tcase *format.MPEG4Audio:\n\t\tunitsPerWrite := (forma.ClockRate() / mpeg4audio.SamplesPerAccessUnit) / audioWritesPerSecond\n\t\twriteDuration := mpeg4audio.SamplesPerAccessUnit * int64(unitsPerWrite)\n\n\t\tfor {\n\t\t\tvar frame []byte\n\t\t\tswitch forma.Config.ChannelConfig {\n\t\t\tcase 1:\n\t\t\t\tframe = []byte{0x01, 0x18, 0x20, 0x07}\n\n\t\t\tdefault:\n\t\t\t\tframe = []byte{0x21, 0x10, 0x04, 0x60, 0x8c, 0x1c}\n\t\t\t}\n\n\t\t\tpayload := make(unit.PayloadMPEG4Audio, unitsPerWrite)\n\t\t\tfor i := range payload {\n\t\t\t\tpayload[i] = frame\n\t\t\t}\n\n\t\t\tt.subStream.WriteUnit(t.media, t.format, &unit.Unit{\n\t\t\t\tPTS:     pts,\n\t\t\t\tNTP:     time.Time{},\n\t\t\t\tPayload: payload,\n\t\t\t})\n\n\t\t\tpts += writeDuration\n\n\t\t\tptsGo := multiplyAndDivide2(time.Duration(pts), time.Second, time.Duration(forma.ClockRate()))\n\t\t\tsystemTime := startSystemTime.Add(ptsGo)\n\n\t\t\tif !t.sleep(systemTime) {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\tcase *format.G711:\n\t\tsamplesPerWrite := forma.ClockRate() / audioWritesPerSecond\n\t\twriteDuration := samplesPerWrite\n\n\t\tfor {\n\t\t\tvar sample byte\n\t\t\tif forma.MULaw {\n\t\t\t\tsample = 0xFF\n\t\t\t} else {\n\t\t\t\tsample = 0xD5\n\t\t\t}\n\n\t\t\tpayload := make(unit.PayloadG711, samplesPerWrite*forma.ChannelCount)\n\t\t\tfor i := range payload {\n\t\t\t\tpayload[i] = sample\n\t\t\t}\n\n\t\t\tt.subStream.WriteUnit(t.media, t.format, &unit.Unit{\n\t\t\t\tPTS:     pts,\n\t\t\t\tNTP:     time.Time{},\n\t\t\t\tPayload: payload,\n\t\t\t})\n\n\t\t\tpts += int64(writeDuration)\n\n\t\t\tptsGo := multiplyAndDivide2(time.Duration(pts), time.Second, time.Duration(forma.ClockRate()))\n\t\t\tsystemTime := startSystemTime.Add(ptsGo)\n\n\t\t\tif !t.sleep(systemTime) {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\tcase *format.LPCM:\n\t\tsamplesPerWrite := forma.ClockRate() / audioWritesPerSecond\n\t\twriteDuration := samplesPerWrite\n\n\t\tfor {\n\t\t\tpayload := make(unit.PayloadLPCM, samplesPerWrite*forma.ChannelCount*(forma.BitDepth/8))\n\n\t\t\tt.subStream.WriteUnit(t.media, t.format, &unit.Unit{\n\t\t\t\tPTS:     pts,\n\t\t\t\tNTP:     time.Time{},\n\t\t\t\tPayload: payload,\n\t\t\t})\n\n\t\t\tpts += int64(writeDuration)\n\n\t\t\tptsGo := multiplyAndDivide2(time.Duration(pts), time.Second, time.Duration(forma.ClockRate()))\n\t\t\tsystemTime := startSystemTime.Add(ptsGo)\n\n\t\t\tif !t.sleep(systemTime) {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\tvar buf []byte\n\n\t\tswitch t.format.(type) {\n\t\tcase *format.AV1:\n\t\t\tbuf = offlineAV1\n\n\t\tcase *format.VP9:\n\t\t\tbuf = offlineVP9\n\n\t\tcase *format.H265:\n\t\t\tbuf = offlineH265\n\n\t\tcase *format.H264:\n\t\t\tbuf = offlineH264\n\n\t\tdefault:\n\t\t\tpanic(\"should not happen\")\n\t\t}\n\n\t\tr := bytes.NewReader(buf)\n\n\t\terr := t.runFile(r, 0)\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n}\n\nfunc (t *offlineSubStreamTrack) runFile(r io.ReadSeeker, pos int) error {\n\tvar presentation pmp4.Presentation\n\terr := presentation.Unmarshal(r)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttrack := presentation.Tracks[pos]\n\tvar pts int64\n\tstartSystemTime := time.Now()\n\n\tfor {\n\t\tfor _, sample := range track.Samples {\n\t\t\tvar payload []byte\n\t\t\tpayload, err = sample.GetPayload()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tswitch track.Codec.(type) {\n\t\t\tcase *mcodecs.AV1:\n\t\t\t\tvar bs av1.Bitstream\n\t\t\t\terr = bs.Unmarshal(payload)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tt.subStream.WriteUnit(t.media, t.format, &unit.Unit{\n\t\t\t\t\tPTS:     pts,\n\t\t\t\t\tNTP:     time.Time{},\n\t\t\t\t\tPayload: unit.PayloadAV1(bs),\n\t\t\t\t})\n\n\t\t\tcase *mcodecs.VP9:\n\t\t\t\tt.subStream.WriteUnit(t.media, t.format, &unit.Unit{\n\t\t\t\t\tPTS:     pts,\n\t\t\t\t\tNTP:     time.Time{},\n\t\t\t\t\tPayload: unit.PayloadVP9(payload),\n\t\t\t\t})\n\n\t\t\tcase *mcodecs.H265:\n\t\t\t\tvar avcc h264.AVCC\n\t\t\t\terr = avcc.Unmarshal(payload)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tt.subStream.WriteUnit(t.media, t.format, &unit.Unit{\n\t\t\t\t\tPTS:     pts,\n\t\t\t\t\tNTP:     time.Time{},\n\t\t\t\t\tPayload: unit.PayloadH265(avcc),\n\t\t\t\t})\n\n\t\t\tcase *mcodecs.H264:\n\t\t\t\tvar avcc h264.AVCC\n\t\t\t\terr = avcc.Unmarshal(payload)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tt.subStream.WriteUnit(t.media, t.format, &unit.Unit{\n\t\t\t\t\tPTS:     pts,\n\t\t\t\t\tNTP:     time.Time{},\n\t\t\t\t\tPayload: unit.PayloadH264(avcc),\n\t\t\t\t})\n\n\t\t\tcase *mcodecs.Opus:\n\t\t\t\tt.subStream.WriteUnit(t.media, t.format, &unit.Unit{\n\t\t\t\t\tPTS:     pts,\n\t\t\t\t\tNTP:     time.Time{},\n\t\t\t\t\tPayload: unit.PayloadOpus([][]byte{payload}),\n\t\t\t\t})\n\n\t\t\tcase *mcodecs.MPEG4Audio:\n\t\t\t\tt.subStream.WriteUnit(t.media, t.format, &unit.Unit{\n\t\t\t\t\tPTS:     pts,\n\t\t\t\t\tNTP:     time.Time{},\n\t\t\t\t\tPayload: unit.PayloadMPEG4Audio([][]byte{payload}),\n\t\t\t\t})\n\n\t\t\tcase *mcodecs.LPCM:\n\t\t\t\tt.subStream.WriteUnit(t.media, t.format, &unit.Unit{\n\t\t\t\t\tPTS:     pts,\n\t\t\t\t\tNTP:     time.Time{},\n\t\t\t\t\tPayload: unit.PayloadLPCM(payload),\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tpts += multiplyAndDivide(int64(sample.Duration)+int64(sample.PTSOffset),\n\t\t\t\tint64(t.format.ClockRate()), int64(track.TimeScale))\n\n\t\t\tptsGo := multiplyAndDivide2(time.Duration(pts), time.Second, time.Duration(t.format.ClockRate()))\n\t\t\tsystemTime := startSystemTime.Add(ptsGo)\n\n\t\t\tif !t.sleep(systemTime) {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (t *offlineSubStreamTrack) sleep(systemTime time.Time) bool {\n\tselect {\n\tcase <-time.After(time.Until(systemTime)):\n\tcase <-t.ctx.Done():\n\t\tif t.waitLastSample {\n\t\t\ttime.Sleep(time.Until(systemTime))\n\t\t}\n\t\treturn false\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "internal/stream/reader.go",
    "content": "package stream\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/ringbuffer\"\n\t\"github.com/bluenviron/mediamtx/internal/counterdumper\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n)\n\n// OnDataFunc is the callback passed to OnData().\ntype OnDataFunc func(*unit.Unit) error\n\n// Reader is a stream reader.\ntype Reader struct {\n\tSkipBytesSent bool\n\tParent        logger.Writer\n\n\tonDatas                 map[*description.Media]map[format.Format]OnDataFunc\n\tqueueSize               int\n\tbuffer                  *ringbuffer.RingBuffer\n\toutboundFramesDiscarded *counterdumper.Dumper\n\n\t// out\n\terr chan error\n}\n\n// OnData registers a callback that is called when data from given format is available.\nfunc (r *Reader) OnData(medi *description.Media, forma format.Format, cb OnDataFunc) {\n\tif r.onDatas == nil {\n\t\tr.onDatas = make(map[*description.Media]map[format.Format]OnDataFunc)\n\t}\n\tif r.onDatas[medi] == nil {\n\t\tr.onDatas[medi] = make(map[format.Format]OnDataFunc)\n\t}\n\tr.onDatas[medi][forma] = cb\n}\n\n// Formats returns all formats for which the reader has registered a OnData callback.\nfunc (r *Reader) Formats() []format.Format {\n\tn := 0\n\tfor _, formats := range r.onDatas {\n\t\tfor range formats {\n\t\t\tn++\n\t\t}\n\t}\n\n\tif n == 0 {\n\t\treturn nil\n\t}\n\n\tout := make([]format.Format, n)\n\tn = 0\n\n\tfor _, formats := range r.onDatas {\n\t\tfor forma := range formats {\n\t\t\tout[n] = forma\n\t\t\tn++\n\t\t}\n\t}\n\n\treturn out\n}\n\n// OutboundFramesDiscarded returns the number of frames discarded because the reader is too slow.\nfunc (r *Reader) OutboundFramesDiscarded() uint64 {\n\treturn r.outboundFramesDiscarded.Get()\n}\n\n// error returns whenever there's an error.\n// It can be called only after stream.AddReader().\nfunc (r *Reader) Error() chan error {\n\treturn r.err\n}\n\nfunc (r *Reader) start() {\n\tbuffer, _ := ringbuffer.New(uint64(r.queueSize))\n\tr.buffer = buffer\n\tr.err = make(chan error)\n\n\tr.outboundFramesDiscarded = &counterdumper.Dumper{\n\t\tOnReport: func(val uint64) {\n\t\t\tr.Parent.Log(logger.Warn, \"reader is too slow, discarding %d %s\",\n\t\t\t\tval,\n\t\t\t\tfunc() string {\n\t\t\t\t\tif val == 1 {\n\t\t\t\t\t\treturn \"frame\"\n\t\t\t\t\t}\n\t\t\t\t\treturn \"frames\"\n\t\t\t\t}())\n\t\t},\n\t}\n\tr.outboundFramesDiscarded.Start()\n\n\tgo r.run()\n}\n\nfunc (r *Reader) stop() {\n\tr.buffer.Close()\n\tr.outboundFramesDiscarded.Stop()\n\t<-r.err\n}\n\nfunc (r *Reader) run() {\n\tr.err <- r.runInner()\n\tclose(r.err)\n}\n\nfunc (r *Reader) runInner() error {\n\tfor {\n\t\tcb, ok := r.buffer.Pull()\n\t\tif !ok {\n\t\t\treturn fmt.Errorf(\"terminated\")\n\t\t}\n\n\t\terr := cb.(func() error)()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n}\n\nfunc (r *Reader) push(cb func() error) {\n\tok := r.buffer.Push(cb)\n\tif !ok {\n\t\tr.outboundFramesDiscarded.Increase()\n\t}\n}\n"
  },
  {
    "path": "internal/stream/rtp_decoder.go",
    "content": "package stream\n\nimport (\n\t\"errors\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtpac3\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtpav1\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtpfragmented\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtph264\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtph265\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtpklv\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtplpcm\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtpmjpeg\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtpmpeg1audio\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtpmpeg1video\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtpmpeg4audio\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtpsimpleaudio\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtpvp8\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtpvp9\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n\t\"github.com/pion/rtp\"\n)\n\ntype rtpDecoder interface {\n\tdecode(*rtp.Packet) (unit.Payload, error)\n}\n\ntype rtpDecoderAV1 rtpav1.Decoder\n\nfunc (d *rtpDecoderAV1) decode(pkt *rtp.Packet) (unit.Payload, error) {\n\ttu, err := (*rtpav1.Decoder)(d).Decode(pkt)\n\tif err != nil {\n\t\tif errors.Is(err, rtpav1.ErrNonStartingPacketAndNoPrevious) ||\n\t\t\terrors.Is(err, rtpav1.ErrMorePacketsNeeded) {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\n\treturn unit.PayloadAV1(tu), nil\n}\n\ntype rtpDecoderVP9 rtpvp9.Decoder\n\nfunc (d *rtpDecoderVP9) decode(pkt *rtp.Packet) (unit.Payload, error) {\n\tframe, err := (*rtpvp9.Decoder)(d).Decode(pkt)\n\tif err != nil {\n\t\tif errors.Is(err, rtpvp9.ErrNonStartingPacketAndNoPrevious) ||\n\t\t\terrors.Is(err, rtpvp9.ErrMorePacketsNeeded) {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\n\treturn unit.PayloadVP9(frame), nil\n}\n\ntype rtpDecoderVP8 rtpvp8.Decoder\n\nfunc (d *rtpDecoderVP8) decode(pkt *rtp.Packet) (unit.Payload, error) {\n\tframe, err := (*rtpvp8.Decoder)(d).Decode(pkt)\n\tif err != nil {\n\t\tif errors.Is(err, rtpvp8.ErrNonStartingPacketAndNoPrevious) ||\n\t\t\terrors.Is(err, rtpvp8.ErrMorePacketsNeeded) {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\n\treturn unit.PayloadVP8(frame), nil\n}\n\ntype rtpDecoderH265 rtph265.Decoder\n\nfunc (d *rtpDecoderH265) decode(pkt *rtp.Packet) (unit.Payload, error) {\n\tau, err := (*rtph265.Decoder)(d).Decode(pkt)\n\tif err != nil {\n\t\tif errors.Is(err, rtph265.ErrNonStartingPacketAndNoPrevious) ||\n\t\t\terrors.Is(err, rtph265.ErrMorePacketsNeeded) {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\n\treturn unit.PayloadH265(au), nil\n}\n\ntype rtpDecoderH264 rtph264.Decoder\n\nfunc (d *rtpDecoderH264) decode(pkt *rtp.Packet) (unit.Payload, error) {\n\tau, err := (*rtph264.Decoder)(d).Decode(pkt)\n\tif err != nil {\n\t\tif errors.Is(err, rtph264.ErrNonStartingPacketAndNoPrevious) ||\n\t\t\terrors.Is(err, rtph264.ErrMorePacketsNeeded) {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\n\treturn unit.PayloadH264(au), nil\n}\n\ntype rtpDecoderMPEG4Video rtpfragmented.Decoder\n\nfunc (d *rtpDecoderMPEG4Video) decode(pkt *rtp.Packet) (unit.Payload, error) {\n\tframe, err := (*rtpfragmented.Decoder)(d).Decode(pkt)\n\tif err != nil {\n\t\tif errors.Is(err, rtpfragmented.ErrMorePacketsNeeded) {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\n\treturn unit.PayloadMPEG4Video(frame), nil\n}\n\ntype rtpDecoderMPEG1Video rtpmpeg1video.Decoder\n\nfunc (d *rtpDecoderMPEG1Video) decode(pkt *rtp.Packet) (unit.Payload, error) {\n\tframe, err := (*rtpmpeg1video.Decoder)(d).Decode(pkt)\n\tif err != nil {\n\t\tif errors.Is(err, rtpmpeg1video.ErrNonStartingPacketAndNoPrevious) ||\n\t\t\terrors.Is(err, rtpmpeg1video.ErrMorePacketsNeeded) {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\n\treturn unit.PayloadMPEG1Video(frame), nil\n}\n\ntype rtpDecoderMJPEG rtpmjpeg.Decoder\n\nfunc (d *rtpDecoderMJPEG) decode(pkt *rtp.Packet) (unit.Payload, error) {\n\tframe, err := (*rtpmjpeg.Decoder)(d).Decode(pkt)\n\tif err != nil {\n\t\tif errors.Is(err, rtpmjpeg.ErrNonStartingPacketAndNoPrevious) ||\n\t\t\terrors.Is(err, rtpmjpeg.ErrMorePacketsNeeded) {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\n\treturn unit.PayloadMJPEG(frame), nil\n}\n\ntype rtpDecoderOpus rtpsimpleaudio.Decoder\n\nfunc (d *rtpDecoderOpus) decode(pkt *rtp.Packet) (unit.Payload, error) {\n\tpacket, err := (*rtpsimpleaudio.Decoder)(d).Decode(pkt)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn unit.PayloadOpus{packet}, nil\n}\n\ntype rtpDecoderMPEG4Audio rtpmpeg4audio.Decoder\n\nfunc (d *rtpDecoderMPEG4Audio) decode(pkt *rtp.Packet) (unit.Payload, error) {\n\taus, err := (*rtpmpeg4audio.Decoder)(d).Decode(pkt)\n\tif err != nil {\n\t\tif errors.Is(err, rtpmpeg4audio.ErrMorePacketsNeeded) {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\n\treturn unit.PayloadMPEG4Audio(aus), nil\n}\n\ntype rtpDecoderMPEG4AudioLATM rtpfragmented.Decoder\n\nfunc (d *rtpDecoderMPEG4AudioLATM) decode(pkt *rtp.Packet) (unit.Payload, error) {\n\tpayload, err := (*rtpfragmented.Decoder)(d).Decode(pkt)\n\tif err != nil {\n\t\tif errors.Is(err, rtpfragmented.ErrMorePacketsNeeded) {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\n\treturn unit.PayloadMPEG4AudioLATM(payload), nil\n}\n\ntype rtpDecoderMPEG1Audio rtpmpeg1audio.Decoder\n\nfunc (d *rtpDecoderMPEG1Audio) decode(pkt *rtp.Packet) (unit.Payload, error) {\n\tframes, err := (*rtpmpeg1audio.Decoder)(d).Decode(pkt)\n\tif err != nil {\n\t\tif errors.Is(err, rtpmpeg1audio.ErrNonStartingPacketAndNoPrevious) ||\n\t\t\terrors.Is(err, rtpmpeg1audio.ErrMorePacketsNeeded) {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\n\treturn unit.PayloadMPEG1Audio(frames), nil\n}\n\ntype rtpDecoderAC3 rtpac3.Decoder\n\nfunc (d *rtpDecoderAC3) decode(pkt *rtp.Packet) (unit.Payload, error) {\n\tframes, err := (*rtpac3.Decoder)(d).Decode(pkt)\n\tif err != nil {\n\t\tif errors.Is(err, rtpac3.ErrMorePacketsNeeded) {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, err\n\t}\n\n\treturn unit.PayloadAC3(frames), nil\n}\n\ntype rtpDecoderG711 rtplpcm.Decoder\n\nfunc (d *rtpDecoderG711) decode(pkt *rtp.Packet) (unit.Payload, error) {\n\tsamples, err := (*rtplpcm.Decoder)(d).Decode(pkt)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn unit.PayloadG711(samples), nil\n}\n\ntype rtpDecoderLPCM rtplpcm.Decoder\n\nfunc (d *rtpDecoderLPCM) decode(pkt *rtp.Packet) (unit.Payload, error) {\n\tsamples, err := (*rtplpcm.Decoder)(d).Decode(pkt)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn unit.PayloadLPCM(samples), nil\n}\n\ntype rtpDecoderKLV rtpklv.Decoder\n\nfunc (d *rtpDecoderKLV) decode(pkt *rtp.Packet) (unit.Payload, error) {\n\tpayload, err := (*rtpklv.Decoder)(d).Decode(pkt)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn unit.PayloadKLV(payload), nil\n}\n\nfunc newRTPDecoder(forma format.Format) (rtpDecoder, error) {\n\tswitch forma := forma.(type) {\n\tcase *format.AV1:\n\t\twrapped, err := forma.CreateDecoder()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn (*rtpDecoderAV1)(wrapped), nil\n\n\tcase *format.VP9:\n\t\twrapped, err := forma.CreateDecoder()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn (*rtpDecoderVP9)(wrapped), nil\n\n\tcase *format.VP8:\n\t\twrapped, err := forma.CreateDecoder()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn (*rtpDecoderVP8)(wrapped), nil\n\n\tcase *format.H265:\n\t\twrapped, err := forma.CreateDecoder()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn (*rtpDecoderH265)(wrapped), nil\n\n\tcase *format.H264:\n\t\twrapped, err := forma.CreateDecoder()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn (*rtpDecoderH264)(wrapped), nil\n\n\tcase *format.MPEG4Video:\n\t\twrapped, err := forma.CreateDecoder()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn (*rtpDecoderMPEG4Video)(wrapped), nil\n\n\tcase *format.MPEG1Video:\n\t\twrapped, err := forma.CreateDecoder()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn (*rtpDecoderMPEG1Video)(wrapped), nil\n\n\tcase *format.MJPEG:\n\t\twrapped, err := forma.CreateDecoder()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn (*rtpDecoderMJPEG)(wrapped), nil\n\n\tcase *format.Opus:\n\t\twrapped, err := forma.CreateDecoder()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn (*rtpDecoderOpus)(wrapped), nil\n\n\tcase *format.MPEG4Audio:\n\t\twrapped, err := forma.CreateDecoder()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn (*rtpDecoderMPEG4Audio)(wrapped), nil\n\n\tcase *format.MPEG4AudioLATM:\n\t\twrapped, err := forma.CreateDecoder()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn (*rtpDecoderMPEG4AudioLATM)(wrapped), nil\n\n\tcase *format.MPEG1Audio:\n\t\twrapped, err := forma.CreateDecoder()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn (*rtpDecoderMPEG1Audio)(wrapped), nil\n\n\tcase *format.AC3:\n\t\twrapped, err := forma.CreateDecoder()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn (*rtpDecoderAC3)(wrapped), nil\n\n\tcase *format.G711:\n\t\twrapped, err := forma.CreateDecoder()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn (*rtpDecoderG711)(wrapped), nil\n\n\tcase *format.LPCM:\n\t\twrapped, err := forma.CreateDecoder()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn (*rtpDecoderLPCM)(wrapped), nil\n\n\tcase *format.KLV:\n\t\twrapped, err := forma.CreateDecoder()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn (*rtpDecoderKLV)(wrapped), nil\n\n\tdefault:\n\t\treturn nil, nil\n\t}\n}\n"
  },
  {
    "path": "internal/stream/rtp_encoder.go",
    "content": "package stream\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtpac3\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtpav1\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtpfragmented\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtph264\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtph265\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtpklv\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtplpcm\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtpmjpeg\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtpmpeg1audio\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtpmpeg1video\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtpmpeg4audio\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtpsimpleaudio\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtpvp8\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format/rtpvp9\"\n\tmcopus \"github.com/bluenviron/mediacommon/v2/pkg/codecs/opus\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n\t\"github.com/pion/rtp\"\n)\n\nfunc ptrOf[T any](v T) *T {\n\tp := new(T)\n\t*p = v\n\treturn p\n}\n\ntype rtpEncoderNotAvailableError struct {\n\tformat format.Format\n}\n\nfunc (e rtpEncoderNotAvailableError) Error() string {\n\treturn fmt.Sprintf(\"RTP encoder not available for format %T\", e.format)\n}\n\ntype rtpEncoder interface {\n\tencode(unit.Payload) ([]*rtp.Packet, error)\n}\n\ntype rtpEncoderH265 rtph265.Encoder\n\nfunc (e *rtpEncoderH265) encode(payload unit.Payload) ([]*rtp.Packet, error) {\n\treturn (*rtph265.Encoder)(e).Encode(payload.(unit.PayloadH265))\n}\n\ntype rtpEncoderH264 rtph264.Encoder\n\nfunc (e *rtpEncoderH264) encode(payload unit.Payload) ([]*rtp.Packet, error) {\n\treturn (*rtph264.Encoder)(e).Encode(payload.(unit.PayloadH264))\n}\n\ntype rtpEncoderAV1 rtpav1.Encoder\n\nfunc (e *rtpEncoderAV1) encode(payload unit.Payload) ([]*rtp.Packet, error) {\n\treturn (*rtpav1.Encoder)(e).Encode(payload.(unit.PayloadAV1))\n}\n\ntype rtpEncoderVP9 rtpvp9.Encoder\n\nfunc (e *rtpEncoderVP9) encode(payload unit.Payload) ([]*rtp.Packet, error) {\n\treturn (*rtpvp9.Encoder)(e).Encode(payload.(unit.PayloadVP9))\n}\n\ntype rtpEncoderVP8 rtpvp8.Encoder\n\nfunc (e *rtpEncoderVP8) encode(payload unit.Payload) ([]*rtp.Packet, error) {\n\treturn (*rtpvp8.Encoder)(e).Encode(payload.(unit.PayloadVP8))\n}\n\ntype rtpEncoderMPEG4Video rtpfragmented.Encoder\n\nfunc (e *rtpEncoderMPEG4Video) encode(payload unit.Payload) ([]*rtp.Packet, error) {\n\treturn (*rtpfragmented.Encoder)(e).Encode(payload.(unit.PayloadMPEG4Video))\n}\n\ntype rtpEncoderMPEG1Video rtpmpeg1video.Encoder\n\nfunc (e *rtpEncoderMPEG1Video) encode(payload unit.Payload) ([]*rtp.Packet, error) {\n\treturn (*rtpmpeg1video.Encoder)(e).Encode(payload.(unit.PayloadMPEG1Video))\n}\n\ntype rtpEncoderMJPEG rtpmjpeg.Encoder\n\nfunc (e *rtpEncoderMJPEG) encode(payload unit.Payload) ([]*rtp.Packet, error) {\n\treturn (*rtpmjpeg.Encoder)(e).Encode(payload.(unit.PayloadMJPEG))\n}\n\ntype rtpEncoderOpus rtpsimpleaudio.Encoder\n\nfunc (e *rtpEncoderOpus) encode(payload unit.Payload) ([]*rtp.Packet, error) {\n\tpts := int64(0)\n\tpackets := make([]*rtp.Packet, len(payload.(unit.PayloadOpus)))\n\n\tfor i, packet := range payload.(unit.PayloadOpus) {\n\t\tpkt, err := (*rtpsimpleaudio.Encoder)(e).Encode(packet)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tpkt.Timestamp += uint32(pts)\n\t\tpts += mcopus.PacketDuration2(packet)\n\t\tpackets[i] = pkt\n\t}\n\n\treturn packets, nil\n}\n\ntype rtpEncoderMPEG4Audio rtpmpeg4audio.Encoder\n\nfunc (e *rtpEncoderMPEG4Audio) encode(payload unit.Payload) ([]*rtp.Packet, error) {\n\treturn (*rtpmpeg4audio.Encoder)(e).Encode(payload.(unit.PayloadMPEG4Audio))\n}\n\ntype rtpEncoderMPEG4AudioLATM rtpfragmented.Encoder\n\nfunc (e *rtpEncoderMPEG4AudioLATM) encode(payload unit.Payload) ([]*rtp.Packet, error) {\n\treturn (*rtpfragmented.Encoder)(e).Encode(payload.(unit.PayloadMPEG4AudioLATM))\n}\n\ntype rtpEncoderMPEG1Audio rtpmpeg1audio.Encoder\n\nfunc (e *rtpEncoderMPEG1Audio) encode(payload unit.Payload) ([]*rtp.Packet, error) {\n\treturn (*rtpmpeg1audio.Encoder)(e).Encode(payload.(unit.PayloadMPEG1Audio))\n}\n\ntype rtpEncoderAC3 rtpac3.Encoder\n\nfunc (e *rtpEncoderAC3) encode(payload unit.Payload) ([]*rtp.Packet, error) {\n\treturn (*rtpac3.Encoder)(e).Encode(payload.(unit.PayloadAC3))\n}\n\ntype rtpEncoderG711 rtplpcm.Encoder\n\nfunc (e *rtpEncoderG711) encode(payload unit.Payload) ([]*rtp.Packet, error) {\n\treturn (*rtplpcm.Encoder)(e).Encode(payload.(unit.PayloadG711))\n}\n\ntype rtpEncoderLPCM rtplpcm.Encoder\n\nfunc (e *rtpEncoderLPCM) encode(payload unit.Payload) ([]*rtp.Packet, error) {\n\treturn (*rtplpcm.Encoder)(e).Encode(payload.(unit.PayloadLPCM))\n}\n\ntype rtpEncoderKLV rtpklv.Encoder\n\nfunc (e *rtpEncoderKLV) encode(payload unit.Payload) ([]*rtp.Packet, error) {\n\treturn (*rtpklv.Encoder)(e).Encode(payload.(unit.PayloadKLV))\n}\n\nfunc newRTPEncoder(\n\tforma format.Format,\n\trtpMaxPayloadSize int,\n\tssrc *uint32,\n\tinitialSequenceNumber *uint16,\n) (rtpEncoder, error) {\n\tswitch forma := forma.(type) {\n\tcase *format.H265:\n\t\twrapped := &rtph265.Encoder{\n\t\t\tPayloadMaxSize:        rtpMaxPayloadSize,\n\t\t\tPayloadType:           forma.PayloadTyp,\n\t\t\tSSRC:                  ssrc,\n\t\t\tInitialSequenceNumber: initialSequenceNumber,\n\t\t\tMaxDONDiff:            forma.MaxDONDiff,\n\t\t}\n\t\terr := wrapped.Init()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn (*rtpEncoderH265)(wrapped), nil\n\n\tcase *format.H264:\n\t\twrapped := &rtph264.Encoder{\n\t\t\tPayloadMaxSize:        rtpMaxPayloadSize,\n\t\t\tPayloadType:           forma.PayloadTyp,\n\t\t\tSSRC:                  ssrc,\n\t\t\tInitialSequenceNumber: initialSequenceNumber,\n\t\t\tPacketizationMode:     forma.PacketizationMode,\n\t\t}\n\t\terr := wrapped.Init()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn (*rtpEncoderH264)(wrapped), nil\n\n\tcase *format.AV1:\n\t\twrapped := &rtpav1.Encoder{\n\t\t\tPayloadMaxSize:        rtpMaxPayloadSize,\n\t\t\tPayloadType:           forma.PayloadTyp,\n\t\t\tSSRC:                  ssrc,\n\t\t\tInitialSequenceNumber: initialSequenceNumber,\n\t\t}\n\t\terr := wrapped.Init()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn (*rtpEncoderAV1)(wrapped), nil\n\n\tcase *format.VP9:\n\t\twrapped := &rtpvp9.Encoder{\n\t\t\tPayloadMaxSize:        rtpMaxPayloadSize,\n\t\t\tPayloadType:           forma.PayloadTyp,\n\t\t\tSSRC:                  ssrc,\n\t\t\tInitialSequenceNumber: initialSequenceNumber,\n\t\t\tInitialPictureID:      ptrOf(uint16(0x35af)),\n\t\t}\n\t\terr := wrapped.Init()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn (*rtpEncoderVP9)(wrapped), nil\n\n\tcase *format.VP8:\n\t\twrapped := &rtpvp8.Encoder{\n\t\t\tPayloadMaxSize:        rtpMaxPayloadSize,\n\t\t\tPayloadType:           forma.PayloadTyp,\n\t\t\tSSRC:                  ssrc,\n\t\t\tInitialSequenceNumber: initialSequenceNumber,\n\t\t}\n\t\terr := wrapped.Init()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn (*rtpEncoderVP8)(wrapped), nil\n\n\tcase *format.MPEG4Video:\n\t\twrapped := &rtpfragmented.Encoder{\n\t\t\tPayloadMaxSize:        rtpMaxPayloadSize,\n\t\t\tPayloadType:           forma.PayloadTyp,\n\t\t\tSSRC:                  ssrc,\n\t\t\tInitialSequenceNumber: initialSequenceNumber,\n\t\t}\n\t\terr := wrapped.Init()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn (*rtpEncoderMPEG4Video)(wrapped), nil\n\n\tcase *format.MPEG1Video:\n\t\twrapped := &rtpmpeg1video.Encoder{\n\t\t\tPayloadMaxSize:        rtpMaxPayloadSize,\n\t\t\tSSRC:                  ssrc,\n\t\t\tInitialSequenceNumber: initialSequenceNumber,\n\t\t}\n\t\terr := wrapped.Init()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn (*rtpEncoderMPEG1Video)(wrapped), nil\n\n\tcase *format.MJPEG:\n\t\twrapped := &rtpmjpeg.Encoder{\n\t\t\tPayloadMaxSize:        rtpMaxPayloadSize,\n\t\t\tSSRC:                  ssrc,\n\t\t\tInitialSequenceNumber: initialSequenceNumber,\n\t\t}\n\t\terr := wrapped.Init()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn (*rtpEncoderMJPEG)(wrapped), nil\n\n\tcase *format.Opus:\n\t\twrapped := &rtpsimpleaudio.Encoder{\n\t\t\tPayloadMaxSize:        rtpMaxPayloadSize,\n\t\t\tPayloadType:           forma.PayloadTyp,\n\t\t\tSSRC:                  ssrc,\n\t\t\tInitialSequenceNumber: initialSequenceNumber,\n\t\t}\n\t\terr := wrapped.Init()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn (*rtpEncoderOpus)(wrapped), nil\n\n\tcase *format.MPEG4Audio:\n\t\twrapped := &rtpmpeg4audio.Encoder{\n\t\t\tPayloadMaxSize:        rtpMaxPayloadSize,\n\t\t\tPayloadType:           forma.PayloadTyp,\n\t\t\tSSRC:                  ssrc,\n\t\t\tInitialSequenceNumber: initialSequenceNumber,\n\t\t\tSizeLength:            forma.SizeLength,\n\t\t\tIndexLength:           forma.IndexLength,\n\t\t\tIndexDeltaLength:      forma.IndexDeltaLength,\n\t\t}\n\t\terr := wrapped.Init()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn (*rtpEncoderMPEG4Audio)(wrapped), nil\n\n\tcase *format.MPEG4AudioLATM:\n\t\twrapped := &rtpfragmented.Encoder{\n\t\t\tPayloadMaxSize:        rtpMaxPayloadSize,\n\t\t\tPayloadType:           forma.PayloadTyp,\n\t\t\tSSRC:                  ssrc,\n\t\t\tInitialSequenceNumber: initialSequenceNumber,\n\t\t}\n\t\terr := wrapped.Init()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn (*rtpEncoderMPEG4AudioLATM)(wrapped), nil\n\n\tcase *format.MPEG1Audio:\n\t\twrapped := &rtpmpeg1audio.Encoder{\n\t\t\tPayloadMaxSize:        rtpMaxPayloadSize,\n\t\t\tSSRC:                  ssrc,\n\t\t\tInitialSequenceNumber: initialSequenceNumber,\n\t\t}\n\t\terr := wrapped.Init()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn (*rtpEncoderMPEG1Audio)(wrapped), nil\n\n\tcase *format.AC3:\n\t\twrapped := &rtpac3.Encoder{\n\t\t\tPayloadType:           forma.PayloadTyp,\n\t\t\tSSRC:                  ssrc,\n\t\t\tInitialSequenceNumber: initialSequenceNumber,\n\t\t}\n\t\terr := wrapped.Init()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn (*rtpEncoderAC3)(wrapped), nil\n\n\tcase *format.G711:\n\t\twrapped := &rtplpcm.Encoder{\n\t\t\tPayloadMaxSize:        rtpMaxPayloadSize,\n\t\t\tPayloadType:           forma.PayloadType(),\n\t\t\tSSRC:                  ssrc,\n\t\t\tInitialSequenceNumber: initialSequenceNumber,\n\t\t\tBitDepth:              8,\n\t\t\tChannelCount:          forma.ChannelCount,\n\t\t}\n\t\terr := wrapped.Init()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn (*rtpEncoderG711)(wrapped), nil\n\n\tcase *format.LPCM:\n\t\twrapped := &rtplpcm.Encoder{\n\t\t\tPayloadMaxSize:        rtpMaxPayloadSize,\n\t\t\tPayloadType:           forma.PayloadTyp,\n\t\t\tSSRC:                  ssrc,\n\t\t\tInitialSequenceNumber: initialSequenceNumber,\n\t\t\tBitDepth:              forma.BitDepth,\n\t\t\tChannelCount:          forma.ChannelCount,\n\t\t}\n\t\terr := wrapped.Init()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn (*rtpEncoderLPCM)(wrapped), nil\n\n\tcase *format.KLV:\n\t\twrapped := &rtpklv.Encoder{\n\t\t\tPayloadMaxSize:        rtpMaxPayloadSize,\n\t\t\tPayloadType:           forma.PayloadTyp,\n\t\t\tSSRC:                  ssrc,\n\t\t\tInitialSequenceNumber: initialSequenceNumber,\n\t\t}\n\t\terr := wrapped.Init()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn (*rtpEncoderKLV)(wrapped), nil\n\n\tdefault:\n\t\treturn nil, rtpEncoderNotAvailableError{forma}\n\t}\n}\n"
  },
  {
    "path": "internal/stream/stream.go",
    "content": "// Package stream contains the Stream object.\npackage stream\n\nimport (\n\t\"os\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/mp4/codecs\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/pmp4\"\n\t\"github.com/pion/rtp\"\n\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/errordumper\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n)\n\nfunc mediasFromAlwaysAvailableFile(alwaysAvailableFile string) ([]*description.Media, error) {\n\tf, err := os.Open(alwaysAvailableFile)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer f.Close()\n\n\tvar presentation pmp4.Presentation\n\terr = presentation.Unmarshal(f)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar medias []*description.Media\n\n\tfor _, track := range presentation.Tracks {\n\t\tswitch codec := track.Codec.(type) {\n\t\tcase *codecs.AV1:\n\t\t\tmedias = append(medias, &description.Media{\n\t\t\t\tType: description.MediaTypeVideo,\n\t\t\t\tFormats: []format.Format{&format.AV1{\n\t\t\t\t\tPayloadTyp: 96,\n\t\t\t\t}},\n\t\t\t})\n\n\t\tcase *codecs.VP9:\n\t\t\tmedias = append(medias, &description.Media{\n\t\t\t\tType: description.MediaTypeVideo,\n\t\t\t\tFormats: []format.Format{&format.VP9{\n\t\t\t\t\tPayloadTyp: 96,\n\t\t\t\t}},\n\t\t\t})\n\n\t\tcase *codecs.H265:\n\t\t\tmedias = append(medias, &description.Media{\n\t\t\t\tType: description.MediaTypeVideo,\n\t\t\t\tFormats: []format.Format{&format.H265{\n\t\t\t\t\tPayloadTyp: 96,\n\t\t\t\t\tVPS:        codec.VPS,\n\t\t\t\t\tSPS:        codec.SPS,\n\t\t\t\t\tPPS:        codec.PPS,\n\t\t\t\t}},\n\t\t\t})\n\n\t\tcase *codecs.H264:\n\t\t\tmedias = append(medias, &description.Media{\n\t\t\t\tType: description.MediaTypeVideo,\n\t\t\t\tFormats: []format.Format{&format.H264{\n\t\t\t\t\tPayloadTyp:        96,\n\t\t\t\t\tPacketizationMode: 1,\n\t\t\t\t\tSPS:               codec.SPS,\n\t\t\t\t\tPPS:               codec.PPS,\n\t\t\t\t}},\n\t\t\t})\n\n\t\tcase *codecs.Opus:\n\t\t\tmedias = append(medias, &description.Media{\n\t\t\t\tType: description.MediaTypeAudio,\n\t\t\t\tFormats: []format.Format{&format.Opus{\n\t\t\t\t\tPayloadTyp:   96,\n\t\t\t\t\tChannelCount: codec.ChannelCount,\n\t\t\t\t}},\n\t\t\t})\n\n\t\tcase *codecs.MPEG4Audio:\n\t\t\tmedias = append(medias, &description.Media{\n\t\t\t\tType: description.MediaTypeAudio,\n\t\t\t\tFormats: []format.Format{&format.MPEG4Audio{\n\t\t\t\t\tPayloadTyp:       96,\n\t\t\t\t\tSizeLength:       13,\n\t\t\t\t\tIndexLength:      3,\n\t\t\t\t\tIndexDeltaLength: 3,\n\t\t\t\t\tConfig: &mpeg4audio.AudioSpecificConfig{\n\t\t\t\t\t\tType:          codec.Config.Type,\n\t\t\t\t\t\tSampleRate:    codec.Config.SampleRate,\n\t\t\t\t\t\tChannelConfig: codec.Config.ChannelConfig,\n\t\t\t\t\t\tChannelCount:  codec.Config.ChannelCount, //nolint:staticcheck\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t})\n\n\t\tcase *codecs.LPCM:\n\t\t\tmedias = append(medias, &description.Media{\n\t\t\t\tType: description.MediaTypeAudio,\n\t\t\t\tFormats: []format.Format{&format.LPCM{\n\t\t\t\t\tPayloadTyp:   96,\n\t\t\t\t\tBitDepth:     codec.BitDepth,\n\t\t\t\t\tSampleRate:   codec.SampleRate,\n\t\t\t\t\tChannelCount: codec.ChannelCount,\n\t\t\t\t}},\n\t\t\t})\n\t\t}\n\t}\n\n\treturn medias, nil\n}\n\nfunc mediasFromAlwaysAvailableTracks(alwaysAvailableTracks []conf.AlwaysAvailableTrack) []*description.Media {\n\tvar medias []*description.Media\n\n\tfor _, track := range alwaysAvailableTracks {\n\t\tswitch track.Codec {\n\t\tcase conf.CodecAV1:\n\t\t\tmedias = append(medias, &description.Media{\n\t\t\t\tType: description.MediaTypeVideo,\n\t\t\t\tFormats: []format.Format{&format.AV1{\n\t\t\t\t\tPayloadTyp: 96,\n\t\t\t\t}},\n\t\t\t})\n\n\t\tcase conf.CodecVP9:\n\t\t\tmedias = append(medias, &description.Media{\n\t\t\t\tType: description.MediaTypeVideo,\n\t\t\t\tFormats: []format.Format{&format.VP9{\n\t\t\t\t\tPayloadTyp: 96,\n\t\t\t\t}},\n\t\t\t})\n\n\t\tcase conf.CodecH265:\n\t\t\tmedias = append(medias, &description.Media{\n\t\t\t\tType: description.MediaTypeVideo,\n\t\t\t\tFormats: []format.Format{&format.H265{\n\t\t\t\t\tPayloadTyp: 96,\n\t\t\t\t\tVPS:        offlineH265VPS,\n\t\t\t\t\tSPS:        offlineH265SPS,\n\t\t\t\t\tPPS:        offlineH265PPS,\n\t\t\t\t}},\n\t\t\t})\n\n\t\tcase conf.CodecH264:\n\t\t\tmedias = append(medias, &description.Media{\n\t\t\t\tType: description.MediaTypeVideo,\n\t\t\t\tFormats: []format.Format{&format.H264{\n\t\t\t\t\tPayloadTyp:        96,\n\t\t\t\t\tPacketizationMode: 1,\n\t\t\t\t\tSPS:               offlineH264SPS,\n\t\t\t\t\tPPS:               offlineH264PPS,\n\t\t\t\t}},\n\t\t\t})\n\n\t\tcase conf.CodecOpus:\n\t\t\tmedias = append(medias, &description.Media{\n\t\t\t\tType: description.MediaTypeAudio,\n\t\t\t\tFormats: []format.Format{&format.Opus{\n\t\t\t\t\tPayloadTyp:   96,\n\t\t\t\t\tChannelCount: 2,\n\t\t\t\t}},\n\t\t\t})\n\n\t\tcase conf.CodecMPEG4Audio:\n\t\t\tmedias = append(medias, &description.Media{\n\t\t\t\tType: description.MediaTypeAudio,\n\t\t\t\tFormats: []format.Format{&format.MPEG4Audio{\n\t\t\t\t\tPayloadTyp:       96,\n\t\t\t\t\tSizeLength:       13,\n\t\t\t\t\tIndexLength:      3,\n\t\t\t\t\tIndexDeltaLength: 3,\n\t\t\t\t\tConfig: &mpeg4audio.AudioSpecificConfig{\n\t\t\t\t\t\tType:          mpeg4audio.ObjectTypeAACLC,\n\t\t\t\t\t\tSampleRate:    track.SampleRate,\n\t\t\t\t\t\tChannelConfig: uint8(track.ChannelCount),\n\t\t\t\t\t\tChannelCount:  track.ChannelCount,\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t})\n\n\t\tcase conf.CodecG711:\n\t\t\tmedias = append(medias, &description.Media{\n\t\t\t\tType: description.MediaTypeAudio,\n\t\t\t\tFormats: []format.Format{&format.G711{\n\t\t\t\t\tPayloadTyp: func() uint8 {\n\t\t\t\t\t\tswitch {\n\t\t\t\t\t\tcase track.ChannelCount == 1 && track.MULaw:\n\t\t\t\t\t\t\treturn 0\n\t\t\t\t\t\tcase track.ChannelCount == 1 && !track.MULaw:\n\t\t\t\t\t\t\treturn 8\n\t\t\t\t\t\tdefault:\n\t\t\t\t\t\t\treturn 96\n\t\t\t\t\t\t}\n\t\t\t\t\t}(),\n\t\t\t\t\tMULaw:        track.MULaw,\n\t\t\t\t\tSampleRate:   track.SampleRate,\n\t\t\t\t\tChannelCount: track.ChannelCount,\n\t\t\t\t}},\n\t\t\t})\n\n\t\tcase conf.CodecLPCM:\n\t\t\tmedias = append(medias, &description.Media{\n\t\t\t\tType: description.MediaTypeAudio,\n\t\t\t\tFormats: []format.Format{&format.LPCM{\n\t\t\t\t\tPayloadTyp:   96,\n\t\t\t\t\tBitDepth:     16,\n\t\t\t\t\tSampleRate:   track.SampleRate,\n\t\t\t\t\tChannelCount: track.ChannelCount,\n\t\t\t\t}},\n\t\t\t})\n\t\t}\n\t}\n\n\treturn medias\n}\n\n// only fields filled by mediasFromAlwaysAvailableFile and mediasFromAlwaysAvailableTracks are cloned\nfunc cloneFormat(forma format.Format) format.Format {\n\tswitch forma := forma.(type) {\n\tcase *format.AV1:\n\t\treturn &format.AV1{\n\t\t\tPayloadTyp: forma.PayloadTyp,\n\t\t}\n\n\tcase *format.VP9:\n\t\treturn &format.VP9{\n\t\t\tPayloadTyp: forma.PayloadTyp,\n\t\t}\n\n\tcase *format.H265:\n\t\treturn &format.H265{\n\t\t\tPayloadTyp: forma.PayloadTyp,\n\t\t\tVPS:        forma.VPS,\n\t\t\tSPS:        forma.SPS,\n\t\t\tPPS:        forma.PPS,\n\t\t}\n\n\tcase *format.H264:\n\t\treturn &format.H264{\n\t\t\tPayloadTyp:        forma.PayloadTyp,\n\t\t\tPacketizationMode: forma.PacketizationMode,\n\t\t\tSPS:               forma.SPS,\n\t\t\tPPS:               forma.PPS,\n\t\t}\n\n\tcase *format.Opus:\n\t\treturn &format.Opus{\n\t\t\tPayloadTyp:   forma.PayloadTyp,\n\t\t\tChannelCount: forma.ChannelCount,\n\t\t}\n\n\tcase *format.MPEG4Audio:\n\t\treturn &format.MPEG4Audio{\n\t\t\tPayloadTyp:       forma.PayloadTyp,\n\t\t\tSizeLength:       forma.SizeLength,\n\t\t\tIndexLength:      forma.IndexLength,\n\t\t\tIndexDeltaLength: forma.IndexDeltaLength,\n\t\t\tConfig:           forma.Config,\n\t\t}\n\n\tcase *format.G711:\n\t\treturn &format.G711{\n\t\t\tPayloadTyp:   forma.PayloadTyp,\n\t\t\tMULaw:        forma.MULaw,\n\t\t\tSampleRate:   forma.SampleRate,\n\t\t\tChannelCount: forma.ChannelCount,\n\t\t}\n\n\tcase *format.LPCM:\n\t\treturn &format.LPCM{\n\t\t\tPayloadTyp:   forma.PayloadTyp,\n\t\t\tBitDepth:     forma.BitDepth,\n\t\t\tSampleRate:   forma.SampleRate,\n\t\t\tChannelCount: forma.ChannelCount,\n\t\t}\n\n\tdefault:\n\t\tpanic(\"unsupported format\")\n\t}\n}\n\n// only fields filled by mediasFromAlwaysAvailableFile and mediasFromAlwaysAvailableTracks are cloned\nfunc cloneDesc(desc *description.Session) *description.Session {\n\tmedias := make([]*description.Media, len(desc.Medias))\n\n\tfor i, media := range desc.Medias {\n\t\tformats := make([]format.Format, len(media.Formats))\n\n\t\tfor j, forma := range media.Formats {\n\t\t\tformats[j] = cloneFormat(forma)\n\t\t}\n\n\t\tmedias[i] = &description.Media{\n\t\t\tType:    media.Type,\n\t\t\tFormats: formats,\n\t\t}\n\t}\n\n\treturn &description.Session{\n\t\tMedias: medias,\n\t}\n}\n\n// Stream is a media stream.\n// It stores tracks, readers and allows to write data to readers, remuxing it when needed.\ntype Stream struct {\n\tDesc                  *description.Session\n\tAlwaysAvailable       bool\n\tAlwaysAvailableTracks []conf.AlwaysAvailableTrack\n\tAlwaysAvailableFile   string\n\tWriteQueueSize        int\n\tRTPMaxPayloadSize     int\n\tReplaceNTP            bool\n\tParent                logger.Writer\n\n\tofflineDesc          *description.Session\n\tmutex                sync.RWMutex\n\tsubStream            *SubStream\n\tofflineSubStream     *offlineSubStream\n\tinboundBytes         *uint64\n\toutboundBytes        *uint64\n\tmedias               map[*description.Media]*streamMedia\n\trtspStream           *gortsplib.ServerStream\n\trtspsStream          *gortsplib.ServerStream\n\treaders              map[*Reader]struct{}\n\tinboundFramesInError *errordumper.Dumper\n\n\ttimeMutex         sync.Mutex\n\tfirstTimeReceived bool\n\tlastPTS           time.Duration\n\tlastSystemTime    time.Time\n\n\thasReaders chan struct{}\n}\n\n// Initialize initializes a Stream.\nfunc (s *Stream) Initialize() error {\n\tif s.AlwaysAvailable {\n\t\tif s.Desc != nil {\n\t\t\tpanic(\"should not happen\")\n\t\t}\n\t\tif !s.ReplaceNTP {\n\t\t\tpanic(\"should not happen\")\n\t\t}\n\n\t\tvar medias []*description.Media\n\n\t\tif s.AlwaysAvailableFile != \"\" {\n\t\t\tvar err error\n\t\t\tmedias, err = mediasFromAlwaysAvailableFile(s.AlwaysAvailableFile)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t} else {\n\t\t\tmedias = mediasFromAlwaysAvailableTracks(s.AlwaysAvailableTracks)\n\t\t}\n\n\t\ts.offlineDesc = &description.Session{\n\t\t\tMedias: medias,\n\t\t}\n\n\t\t// clone the description since its parameters can be modified\n\t\ts.Desc = cloneDesc(s.offlineDesc)\n\t}\n\n\ts.inboundBytes = new(uint64)\n\ts.outboundBytes = new(uint64)\n\ts.medias = make(map[*description.Media]*streamMedia)\n\ts.readers = make(map[*Reader]struct{})\n\ts.hasReaders = make(chan struct{})\n\n\ts.inboundFramesInError = &errordumper.Dumper{\n\t\tOnReport: func(val uint64, last error) {\n\t\t\tif val == 1 {\n\t\t\t\ts.Parent.Log(logger.Warn, \"processing error: %v\", last)\n\t\t\t} else {\n\t\t\t\ts.Parent.Log(logger.Warn, \"%d processing errors, last was: %v\", val, last)\n\t\t\t}\n\t\t},\n\t}\n\ts.inboundFramesInError.Start()\n\n\ts.lastSystemTime = time.Now()\n\n\tfor _, media := range s.Desc.Medias {\n\t\tsm := &streamMedia{\n\t\t\tmedia:                media,\n\t\t\talwaysAvailable:      s.AlwaysAvailable,\n\t\t\trtpMaxPayloadSize:    s.RTPMaxPayloadSize,\n\t\t\treplaceNTP:           s.ReplaceNTP,\n\t\t\taddInboundBytes:      s.addInboundBytes,\n\t\t\taddOutboundBytes:     s.addOutboundBytes,\n\t\t\tupdateLastTime:       s.updateLastTime,\n\t\t\twriteRTSP:            s.writeRTSP,\n\t\t\tinboundFramesInError: s.inboundFramesInError,\n\t\t\tparent:               s.Parent,\n\t\t}\n\t\terr := sm.initialize()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\ts.medias[media] = sm\n\t}\n\n\tif s.AlwaysAvailable {\n\t\terr := s.StartOfflineSubStream()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// Close closes all resources of the stream.\nfunc (s *Stream) Close() {\n\tif s.offlineSubStream != nil {\n\t\ts.offlineSubStream.close(false)\n\t}\n\n\ts.inboundFramesInError.Stop()\n\n\tif s.rtspStream != nil {\n\t\ts.rtspStream.Close()\n\t}\n\tif s.rtspsStream != nil {\n\t\ts.rtspsStream.Close()\n\t}\n}\n\n// StartOfflineSubStream starts the offline substream.\nfunc (s *Stream) StartOfflineSubStream() error {\n\tif !s.AlwaysAvailable {\n\t\tpanic(\"should not happen\")\n\t}\n\n\toss := &offlineSubStream{\n\t\tstream: s,\n\t}\n\terr := oss.initialize()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif s.offlineSubStream != nil {\n\t\ts.Parent.Log(logger.Info, \"stream is offline\")\n\t}\n\n\ts.offlineSubStream = oss\n\n\treturn nil\n}\n\n// InboundBytes returns received bytes.\nfunc (s *Stream) InboundBytes() uint64 {\n\treturn atomic.LoadUint64(s.inboundBytes)\n}\n\n// OutboundBytes returns sent bytes.\nfunc (s *Stream) OutboundBytes() uint64 {\n\toutboundBytes := atomic.LoadUint64(s.outboundBytes)\n\n\ts.mutex.RLock()\n\tdefer s.mutex.RUnlock()\n\n\tif s.rtspStream != nil {\n\t\tstats := s.rtspStream.Stats()\n\t\toutboundBytes += stats.OutboundBytes\n\t}\n\tif s.rtspsStream != nil {\n\t\tstats := s.rtspsStream.Stats()\n\t\toutboundBytes += stats.OutboundBytes\n\t}\n\n\treturn outboundBytes\n}\n\n// InboundFramesInError returns the number of frames received with processing errors.\nfunc (s *Stream) InboundFramesInError() uint64 {\n\treturn s.inboundFramesInError.Get()\n}\n\n// RTSPStream returns the RTSP stream.\nfunc (s *Stream) RTSPStream(server *gortsplib.Server) *gortsplib.ServerStream {\n\ts.mutex.Lock()\n\tdefer s.mutex.Unlock()\n\n\tif s.rtspStream == nil {\n\t\ts.rtspStream = &gortsplib.ServerStream{\n\t\t\tServer: server,\n\t\t\tDesc:   s.Desc,\n\t\t}\n\t\terr := s.rtspStream.Initialize()\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\treturn s.rtspStream\n}\n\n// RTSPSStream returns the RTSPS stream.\nfunc (s *Stream) RTSPSStream(server *gortsplib.Server) *gortsplib.ServerStream {\n\ts.mutex.Lock()\n\tdefer s.mutex.Unlock()\n\n\tif s.rtspsStream == nil {\n\t\ts.rtspsStream = &gortsplib.ServerStream{\n\t\t\tServer: server,\n\t\t\tDesc:   s.Desc,\n\t\t}\n\t\terr := s.rtspsStream.Initialize()\n\t\tif err != nil {\n\t\t\tpanic(err)\n\t\t}\n\t}\n\treturn s.rtspsStream\n}\n\n// AddReader adds a reader.\n// Used by all protocols except RTSP.\nfunc (s *Stream) AddReader(r *Reader) {\n\ts.mutex.Lock()\n\tdefer s.mutex.Unlock()\n\n\ts.readers[r] = struct{}{}\n\n\tfor medi, formats := range r.onDatas {\n\t\tsm := s.medias[medi]\n\n\t\tfor forma, onData := range formats {\n\t\t\tsf := sm.formats[forma]\n\t\t\tsf.onDatas[r] = onData\n\t\t}\n\t}\n\n\tr.queueSize = s.WriteQueueSize\n\tr.start()\n\n\tselect {\n\tcase <-s.hasReaders:\n\tdefault:\n\t\tclose(s.hasReaders)\n\t}\n}\n\n// RemoveReader removes a reader.\n// Used by all protocols except RTSP.\nfunc (s *Stream) RemoveReader(r *Reader) {\n\ts.mutex.Lock()\n\tdefer s.mutex.Unlock()\n\n\tr.stop()\n\n\tfor medi, formats := range r.onDatas {\n\t\tsm := s.medias[medi]\n\n\t\tfor forma := range formats {\n\t\t\tsf := sm.formats[forma]\n\t\t\tdelete(sf.onDatas, r)\n\t\t}\n\t}\n\n\tdelete(s.readers, r)\n}\n\n// WaitForReaders waits for the stream to have at least one reader.\nfunc (s *Stream) WaitForReaders() {\n\t<-s.hasReaders\n}\n\nfunc (s *Stream) addInboundBytes(v uint64) {\n\tatomic.AddUint64(s.inboundBytes, v)\n}\n\nfunc (s *Stream) addOutboundBytes(v uint64) {\n\tatomic.AddUint64(s.outboundBytes, v)\n}\n\nfunc (s *Stream) updateLastTime(pts time.Duration) {\n\ts.timeMutex.Lock()\n\tdefer s.timeMutex.Unlock()\n\n\ts.firstTimeReceived = true\n\n\tif pts > s.lastPTS {\n\t\ts.lastPTS = pts\n\t}\n\n\ts.lastSystemTime = time.Now()\n}\n\nfunc (s *Stream) writeRTSP(medi *description.Media, pkts []*rtp.Packet, ntp time.Time) {\n\tif s.rtspStream != nil {\n\t\tfor _, pkt := range pkts {\n\t\t\ts.rtspStream.WritePacketRTPWithNTP(medi, pkt, ntp) //nolint:errcheck\n\t\t}\n\t}\n\n\tif s.rtspsStream != nil {\n\t\tfor _, pkt := range pkts {\n\t\t\ts.rtspsStream.WritePacketRTPWithNTP(medi, pkt, ntp) //nolint:errcheck\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/stream/stream_alwaysavailable_test.go",
    "content": "package stream\n\nimport (\n\t\"os\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"testing\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/mp4/codecs\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/formats/pmp4\"\n\t\"github.com/bluenviron/mediamtx/internal/conf\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestStreamAlwaysAvailableErrors(t *testing.T) {\n\tfor _, ca := range []struct {\n\t\tname   string\n\t\ttracks []conf.AlwaysAvailableTrack\n\t\tdesc   *description.Session\n\t\terr    string\n\t}{\n\t\t{\n\t\t\t\"wrong tracks\",\n\t\t\t[]conf.AlwaysAvailableTrack{\n\t\t\t\t{Codec: \"H264\"},\n\t\t\t\t{Codec: \"H265\"},\n\t\t\t},\n\t\t\t&description.Session{\n\t\t\t\tMedias: []*description.Media{\n\t\t\t\t\t{\n\t\t\t\t\t\tType:    description.MediaTypeVideo,\n\t\t\t\t\t\tFormats: []format.Format{&format.H264{}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"wants to publish [H264], but stream expects [H264 H265]\",\n\t\t},\n\t\t{\n\t\t\t\"wrong mpeg-4 audio config\",\n\t\t\t[]conf.AlwaysAvailableTrack{\n\t\t\t\t{Codec: \"MPEG4Audio\", SampleRate: 44100, ChannelCount: 2},\n\t\t\t},\n\t\t\t&description.Session{\n\t\t\t\tMedias: []*description.Media{\n\t\t\t\t\t{\n\t\t\t\t\t\tType: description.MediaTypeAudio,\n\t\t\t\t\t\tFormats: []format.Format{&format.MPEG4Audio{\n\t\t\t\t\t\t\tConfig: &mpeg4audio.AudioSpecificConfig{\n\t\t\t\t\t\t\t\tType:          2,\n\t\t\t\t\t\t\t\tSampleRate:    48000,\n\t\t\t\t\t\t\t\tChannelConfig: 1,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"MPEG-4 audio configuration does not match, is type=2, sampleRate=48000, \" +\n\t\t\t\t\"channelCount=1, but stream expects type=2, sampleRate=44100, channelCount=2\",\n\t\t},\n\t\t{\n\t\t\t\"wrong g711 config\",\n\t\t\t[]conf.AlwaysAvailableTrack{\n\t\t\t\t{Codec: \"G711\", MULaw: true, SampleRate: 8000, ChannelCount: 2},\n\t\t\t},\n\t\t\t&description.Session{\n\t\t\t\tMedias: []*description.Media{\n\t\t\t\t\t{\n\t\t\t\t\t\tType: description.MediaTypeAudio,\n\t\t\t\t\t\tFormats: []format.Format{&format.G711{\n\t\t\t\t\t\t\tMULaw:        false,\n\t\t\t\t\t\t\tSampleRate:   8000,\n\t\t\t\t\t\t\tChannelCount: 2,\n\t\t\t\t\t\t}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"G711 configuration does not match, is MULaw=false, sampleRate=8000, \" +\n\t\t\t\t\"channelCount=2, but stream expects MULaw=true, sampleRate=8000, channelCount=2\",\n\t\t},\n\t\t{\n\t\t\t\"wrong lpcm config\",\n\t\t\t[]conf.AlwaysAvailableTrack{\n\t\t\t\t{Codec: \"LPCM\", SampleRate: 44100, ChannelCount: 2},\n\t\t\t},\n\t\t\t&description.Session{\n\t\t\t\tMedias: []*description.Media{\n\t\t\t\t\t{\n\t\t\t\t\t\tType: description.MediaTypeAudio,\n\t\t\t\t\t\tFormats: []format.Format{&format.LPCM{\n\t\t\t\t\t\t\tBitDepth:     16,\n\t\t\t\t\t\t\tSampleRate:   48000,\n\t\t\t\t\t\t\tChannelCount: 2,\n\t\t\t\t\t\t}},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\t\"LPCM configuration does not match, is bitDepth=16, sampleRate=48000, \" +\n\t\t\t\t\"channelCount=2, but stream expects bitDepth=16, sampleRate=44100, channelCount=2\",\n\t\t},\n\t} {\n\t\tt.Run(ca.name, func(t *testing.T) {\n\t\t\tstrm := &Stream{\n\t\t\t\tAlwaysAvailable:       true,\n\t\t\t\tAlwaysAvailableTracks: ca.tracks,\n\t\t\t\tWriteQueueSize:        512,\n\t\t\t\tRTPMaxPayloadSize:     1450,\n\t\t\t\tReplaceNTP:            true,\n\t\t\t\tParent:                &nilLogger{},\n\t\t\t}\n\t\t\terr := strm.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer strm.Close()\n\n\t\t\tsubStream := &SubStream{\n\t\t\t\tStream:        strm,\n\t\t\t\tCurDesc:       ca.desc,\n\t\t\t\tUseRTPPackets: false,\n\t\t\t}\n\t\t\terr = subStream.Initialize()\n\t\t\trequire.EqualError(t, err, ca.err)\n\t\t})\n\t}\n}\n\nfunc TestStreamAlwaysAvailable(t *testing.T) {\n\tfor _, ca := range []string{\"default\", \"file\"} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tstrm := &Stream{\n\t\t\t\tAlwaysAvailable:   true,\n\t\t\t\tWriteQueueSize:    512,\n\t\t\t\tRTPMaxPayloadSize: 1450,\n\t\t\t\tReplaceNTP:        true,\n\t\t\t\tParent:            &nilLogger{},\n\t\t\t}\n\n\t\t\tif ca == \"default\" {\n\t\t\t\tstrm.AlwaysAvailableTracks = []conf.AlwaysAvailableTrack{\n\t\t\t\t\t{Codec: conf.CodecAV1},\n\t\t\t\t\t{Codec: conf.CodecVP9},\n\t\t\t\t\t{Codec: conf.CodecH265},\n\t\t\t\t\t{Codec: conf.CodecH264},\n\t\t\t\t\t{Codec: conf.CodecOpus},\n\t\t\t\t\t{Codec: conf.CodecMPEG4Audio, SampleRate: 44100, ChannelCount: 2},\n\t\t\t\t\t{Codec: conf.CodecLPCM, SampleRate: 48000, ChannelCount: 2},\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\ttmpf, err := os.CreateTemp(os.TempDir(), \"rtsp-\")\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer os.Remove(tmpf.Name())\n\n\t\t\t\tpmp4 := &pmp4.Presentation{\n\t\t\t\t\tTracks: []*pmp4.Track{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        1,\n\t\t\t\t\t\t\tTimeScale: 90000,\n\t\t\t\t\t\t\tCodec: &codecs.AV1{\n\t\t\t\t\t\t\t\tSequenceHeader: []byte{8, 0, 0, 0, 66, 167, 191, 228, 96, 13, 0, 64},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSamples: []*pmp4.Sample{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tDuration:    90000,\n\t\t\t\t\t\t\t\t\tPayloadSize: 13,\n\t\t\t\t\t\t\t\t\tGetPayload: func() ([]byte, error) {\n\t\t\t\t\t\t\t\t\t\treturn []byte{0xa, 0xb, 0x0, 0x0, 0x0, 0x42, 0xa7, 0xbf, 0xe4, 0x60, 0xd, 0x0, 0x40}, nil\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        2,\n\t\t\t\t\t\t\tTimeScale: 90000,\n\t\t\t\t\t\t\tCodec: &codecs.VP9{\n\t\t\t\t\t\t\t\tWidth:             1280,\n\t\t\t\t\t\t\t\tHeight:            720,\n\t\t\t\t\t\t\t\tProfile:           1,\n\t\t\t\t\t\t\t\tBitDepth:          8,\n\t\t\t\t\t\t\t\tChromaSubsampling: 1,\n\t\t\t\t\t\t\t\tColorRange:        false,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSamples: []*pmp4.Sample{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tDuration:    90000,\n\t\t\t\t\t\t\t\t\tPayloadSize: 4,\n\t\t\t\t\t\t\t\t\tGetPayload: func() ([]byte, error) {\n\t\t\t\t\t\t\t\t\t\treturn []byte{1, 2, 3, 4}, nil\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        3,\n\t\t\t\t\t\t\tTimeScale: 90000,\n\t\t\t\t\t\t\tCodec: &codecs.H265{\n\t\t\t\t\t\t\t\tVPS: []byte{\n\t\t\t\t\t\t\t\t\t0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x02, 0x20,\n\t\t\t\t\t\t\t\t\t0x00, 0x00, 0x03, 0x00, 0xb0, 0x00, 0x00, 0x03,\n\t\t\t\t\t\t\t\t\t0x00, 0x00, 0x03, 0x00, 0x7b, 0x18, 0xb0, 0x24,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tSPS: []byte{\n\t\t\t\t\t\t\t\t\t0x42, 0x01, 0x01, 0x02, 0x20, 0x00, 0x00, 0x03,\n\t\t\t\t\t\t\t\t\t0x00, 0xb0, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03,\n\t\t\t\t\t\t\t\t\t0x00, 0x7b, 0xa0, 0x07, 0x82, 0x00, 0x88, 0x7d,\n\t\t\t\t\t\t\t\t\t0xb6, 0x71, 0x8b, 0x92, 0x44, 0x80, 0x53, 0x88,\n\t\t\t\t\t\t\t\t\t0x88, 0x92, 0xcf, 0x24, 0xa6, 0x92, 0x72, 0xc9,\n\t\t\t\t\t\t\t\t\t0x12, 0x49, 0x22, 0xdc, 0x91, 0xaa, 0x48, 0xfc,\n\t\t\t\t\t\t\t\t\t0xa2, 0x23, 0xff, 0x00, 0x01, 0x00, 0x01, 0x6a,\n\t\t\t\t\t\t\t\t\t0x02, 0x02, 0x02, 0x01,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tPPS: []byte{\n\t\t\t\t\t\t\t\t\t0x44, 0x01, 0xc0, 0x25, 0x2f, 0x05, 0x32, 0x40,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSamples: []*pmp4.Sample{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tDuration:    90000,\n\t\t\t\t\t\t\t\t\tPayloadSize: 8,\n\t\t\t\t\t\t\t\t\tGetPayload: func() ([]byte, error) {\n\t\t\t\t\t\t\t\t\t\treturn []byte{0x0, 0x0, 0x0, 0x4, 0x1, 0x2, 0x3, 0x4}, nil\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        4,\n\t\t\t\t\t\t\tTimeScale: 90000,\n\t\t\t\t\t\t\tCodec: &codecs.H264{\n\t\t\t\t\t\t\t\tSPS: []byte{ // 1920x1080 baseline\n\t\t\t\t\t\t\t\t\t0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02,\n\t\t\t\t\t\t\t\t\t0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04,\n\t\t\t\t\t\t\t\t\t0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, 0x20,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\tPPS: []byte{0x08, 0x06, 0x07, 0x08},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSamples: []*pmp4.Sample{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tDuration:    90000,\n\t\t\t\t\t\t\t\t\tPayloadSize: 8,\n\t\t\t\t\t\t\t\t\tGetPayload: func() ([]byte, error) {\n\t\t\t\t\t\t\t\t\t\treturn []byte{0x0, 0x0, 0x0, 0x4, 0x1, 0x2, 0x3, 0x4}, nil\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        5,\n\t\t\t\t\t\t\tTimeScale: 48000,\n\t\t\t\t\t\t\tCodec: &codecs.Opus{\n\t\t\t\t\t\t\t\tChannelCount: 2,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSamples: []*pmp4.Sample{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tDuration:    48000,\n\t\t\t\t\t\t\t\t\tPayloadSize: 2,\n\t\t\t\t\t\t\t\t\tGetPayload: func() ([]byte, error) {\n\t\t\t\t\t\t\t\t\t\treturn []byte{0x1, 0x2}, nil\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        6,\n\t\t\t\t\t\t\tTimeScale: 44100,\n\t\t\t\t\t\t\tCodec: &codecs.MPEG4Audio{\n\t\t\t\t\t\t\t\tConfig: mpeg4audio.AudioSpecificConfig{\n\t\t\t\t\t\t\t\t\tType:          2,\n\t\t\t\t\t\t\t\t\tSampleRate:    44100,\n\t\t\t\t\t\t\t\t\tChannelConfig: 2,\n\t\t\t\t\t\t\t\t\tChannelCount:  2,\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSamples: []*pmp4.Sample{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tDuration:    44100,\n\t\t\t\t\t\t\t\t\tPayloadSize: 4,\n\t\t\t\t\t\t\t\t\tGetPayload: func() ([]byte, error) {\n\t\t\t\t\t\t\t\t\t\treturn []byte{0x12, 0x10, 0x0, 0x0}, nil\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tID:        7,\n\t\t\t\t\t\t\tTimeScale: 48000,\n\t\t\t\t\t\t\tCodec: &codecs.LPCM{\n\t\t\t\t\t\t\t\tBitDepth:     16,\n\t\t\t\t\t\t\t\tSampleRate:   48000,\n\t\t\t\t\t\t\t\tChannelCount: 2,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\tSamples: []*pmp4.Sample{\n\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\tDuration:    48000,\n\t\t\t\t\t\t\t\t\tPayloadSize: 4,\n\t\t\t\t\t\t\t\t\tGetPayload: func() ([]byte, error) {\n\t\t\t\t\t\t\t\t\t\treturn []byte{0x12, 0x10, 0x0, 0x0}, nil\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t},\n\t\t\t\t}\n\n\t\t\t\terr = pmp4.Marshal(tmpf)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\ttmpf.Close()\n\n\t\t\t\tstrm.AlwaysAvailableFile = tmpf.Name()\n\t\t\t}\n\n\t\t\terr := strm.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer strm.Close()\n\n\t\t\tr := &Reader{\n\t\t\t\tParent: &nilLogger{},\n\t\t\t}\n\n\t\t\tvar wg sync.WaitGroup\n\t\t\tn := 0\n\t\t\tvar phase2 atomic.Bool\n\n\t\t\twg.Add(1)\n\t\t\tvar lastPTSAV1 int64\n\t\t\tvar soAV1a sync.Once\n\t\t\tvar soAV1b sync.Once\n\t\t\tr.OnData(strm.Desc.Medias[n], strm.Desc.Medias[n].Formats[0], func(u *unit.Unit) error {\n\t\t\t\trequire.GreaterOrEqual(t, u.PTS, lastPTSAV1)\n\t\t\t\tlastPTSAV1 = u.PTS\n\n\t\t\t\tif !phase2.Load() {\n\t\t\t\t\tsoAV1a.Do(func() {\n\t\t\t\t\t\trequire.NotEmpty(t, u.Payload)\n\t\t\t\t\t\twg.Done()\n\t\t\t\t\t})\n\t\t\t\t} else {\n\t\t\t\t\tsoAV1b.Do(func() {\n\t\t\t\t\t\trequire.Equal(t, unit.PayloadAV1{{1, 2, 3, 4}}, u.Payload)\n\t\t\t\t\t\twg.Done()\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tn++\n\n\t\t\twg.Add(1)\n\t\t\tvar lastPTSVP9 int64\n\t\t\tvar soVP9a sync.Once\n\t\t\tvar soVP9b sync.Once\n\t\t\tr.OnData(strm.Desc.Medias[n], strm.Desc.Medias[n].Formats[0], func(u *unit.Unit) error {\n\t\t\t\trequire.GreaterOrEqual(t, u.PTS, lastPTSVP9)\n\t\t\t\tlastPTSVP9 = u.PTS\n\n\t\t\t\tif !phase2.Load() {\n\t\t\t\t\tsoVP9a.Do(func() {\n\t\t\t\t\t\trequire.NotEmpty(t, u.Payload)\n\t\t\t\t\t\twg.Done()\n\t\t\t\t\t})\n\t\t\t\t} else {\n\t\t\t\t\tsoVP9b.Do(func() {\n\t\t\t\t\t\trequire.Equal(t, unit.PayloadVP9{1, 2, 3, 4}, u.Payload)\n\t\t\t\t\t\twg.Done()\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tn++\n\n\t\t\twg.Add(1)\n\t\t\tvar lastPTSH265 int64\n\t\t\tvar soH265a sync.Once\n\t\t\tvar soH265b sync.Once\n\t\t\tr.OnData(strm.Desc.Medias[n], strm.Desc.Medias[n].Formats[0], func(u *unit.Unit) error {\n\t\t\t\trequire.GreaterOrEqual(t, u.PTS, lastPTSH265)\n\t\t\t\tlastPTSH265 = u.PTS\n\n\t\t\t\tif !phase2.Load() {\n\t\t\t\t\tsoH265a.Do(func() {\n\t\t\t\t\t\trequire.NotEmpty(t, u.Payload)\n\t\t\t\t\t\twg.Done()\n\t\t\t\t\t})\n\t\t\t\t} else {\n\t\t\t\t\tsoH265b.Do(func() {\n\t\t\t\t\t\trequire.Equal(t, unit.PayloadH265{{1, 2, 3, 4}}, u.Payload)\n\t\t\t\t\t\twg.Done()\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tn++\n\n\t\t\twg.Add(1)\n\t\t\tvar lastPTSH264 int64\n\t\t\tvar soH264a sync.Once\n\t\t\tvar soH264b sync.Once\n\t\t\tr.OnData(strm.Desc.Medias[n], strm.Desc.Medias[n].Formats[0], func(u *unit.Unit) error {\n\t\t\t\trequire.GreaterOrEqual(t, u.PTS, lastPTSH264)\n\t\t\t\tlastPTSH264 = u.PTS\n\n\t\t\t\tif !phase2.Load() {\n\t\t\t\t\tsoH264a.Do(func() {\n\t\t\t\t\t\trequire.NotEmpty(t, u.Payload)\n\t\t\t\t\t\twg.Done()\n\t\t\t\t\t})\n\t\t\t\t} else {\n\t\t\t\t\tsoH264b.Do(func() {\n\t\t\t\t\t\trequire.Equal(t, unit.PayloadH264{{1, 2, 3, 4}}, u.Payload)\n\t\t\t\t\t\twg.Done()\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tn++\n\n\t\t\twg.Add(1)\n\t\t\tvar lastPTSOpus int64\n\t\t\tvar soOpusa sync.Once\n\t\t\tvar soOpusb sync.Once\n\t\t\tr.OnData(strm.Desc.Medias[n], strm.Desc.Medias[n].Formats[0], func(u *unit.Unit) error {\n\t\t\t\trequire.GreaterOrEqual(t, u.PTS, lastPTSOpus)\n\t\t\t\tlastPTSOpus = u.PTS\n\n\t\t\t\tif !phase2.Load() {\n\t\t\t\t\tsoOpusa.Do(func() {\n\t\t\t\t\t\trequire.NotEmpty(t, u.Payload)\n\t\t\t\t\t\twg.Done()\n\t\t\t\t\t})\n\t\t\t\t} else {\n\t\t\t\t\tsoOpusb.Do(func() {\n\t\t\t\t\t\trequire.Equal(t, unit.PayloadOpus{{1, 2}}, u.Payload)\n\t\t\t\t\t\twg.Done()\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tn++\n\n\t\t\twg.Add(1)\n\t\t\tvar lastPTSMPEG4Audio int64\n\t\t\tvar soMPEG4Audioa sync.Once\n\t\t\tvar soMPEG4Audiob sync.Once\n\t\t\tr.OnData(strm.Desc.Medias[n], strm.Desc.Medias[n].Formats[0], func(u *unit.Unit) error {\n\t\t\t\trequire.GreaterOrEqual(t, u.PTS, lastPTSMPEG4Audio)\n\t\t\t\tlastPTSMPEG4Audio = u.PTS\n\n\t\t\t\tif !phase2.Load() {\n\t\t\t\t\tsoMPEG4Audioa.Do(func() {\n\t\t\t\t\t\trequire.NotEmpty(t, u.Payload)\n\t\t\t\t\t\twg.Done()\n\t\t\t\t\t})\n\t\t\t\t} else {\n\t\t\t\t\tsoMPEG4Audiob.Do(func() {\n\t\t\t\t\t\trequire.Equal(t, unit.PayloadMPEG4Audio{{1, 2, 3, 4}}, u.Payload)\n\t\t\t\t\t\twg.Done()\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t})\n\t\t\tn++\n\n\t\t\twg.Add(1)\n\t\t\tvar lastPTSLPCM int64\n\t\t\tvar soLPCMa sync.Once\n\t\t\tvar soLPCMb sync.Once\n\t\t\tr.OnData(strm.Desc.Medias[n], strm.Desc.Medias[n].Formats[0], func(u *unit.Unit) error {\n\t\t\t\trequire.GreaterOrEqual(t, u.PTS, lastPTSLPCM)\n\t\t\t\tlastPTSLPCM = u.PTS\n\n\t\t\t\tif !phase2.Load() {\n\t\t\t\t\tsoLPCMa.Do(func() {\n\t\t\t\t\t\trequire.NotEmpty(t, u.Payload)\n\t\t\t\t\t\twg.Done()\n\t\t\t\t\t})\n\t\t\t\t} else {\n\t\t\t\t\tsoLPCMb.Do(func() {\n\t\t\t\t\t\trequire.Equal(t, unit.PayloadLPCM{1, 2, 3, 4}, u.Payload)\n\t\t\t\t\t\twg.Done()\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\t\tstrm.AddReader(r)\n\t\t\tdefer strm.RemoveReader(r)\n\n\t\t\twg.Wait()\n\n\t\t\tsubStream := &SubStream{\n\t\t\t\tStream: strm,\n\t\t\t\tCurDesc: &description.Session{Medias: []*description.Media{\n\t\t\t\t\t{\n\t\t\t\t\t\tType:    description.MediaTypeVideo,\n\t\t\t\t\t\tFormats: []format.Format{&format.AV1{}},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tType:    description.MediaTypeVideo,\n\t\t\t\t\t\tFormats: []format.Format{&format.VP9{}},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tType:    description.MediaTypeVideo,\n\t\t\t\t\t\tFormats: []format.Format{&format.H265{}},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tType:    description.MediaTypeVideo,\n\t\t\t\t\t\tFormats: []format.Format{&format.H264{}},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tType:    description.MediaTypeAudio,\n\t\t\t\t\t\tFormats: []format.Format{&format.Opus{}},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tType: description.MediaTypeAudio,\n\t\t\t\t\t\tFormats: []format.Format{&format.MPEG4Audio{\n\t\t\t\t\t\t\tConfig: &mpeg4audio.AudioSpecificConfig{\n\t\t\t\t\t\t\t\tType:          2,\n\t\t\t\t\t\t\t\tSampleRate:    44100,\n\t\t\t\t\t\t\t\tChannelConfig: 2,\n\t\t\t\t\t\t\t\tChannelCount:  2,\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t}},\n\t\t\t\t\t},\n\t\t\t\t\t{\n\t\t\t\t\t\tType: description.MediaTypeAudio,\n\t\t\t\t\t\tFormats: []format.Format{&format.LPCM{\n\t\t\t\t\t\t\tBitDepth:     16,\n\t\t\t\t\t\t\tSampleRate:   48000,\n\t\t\t\t\t\t\tChannelCount: 2,\n\t\t\t\t\t\t}},\n\t\t\t\t\t},\n\t\t\t\t}},\n\t\t\t\tUseRTPPackets: false,\n\t\t\t}\n\t\t\terr = subStream.Initialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\twg.Add(7)\n\t\t\tphase2.Store(true)\n\n\t\t\tsubStream.WriteUnit(subStream.CurDesc.Medias[0], subStream.CurDesc.Medias[0].Formats[0], &unit.Unit{\n\t\t\t\tPTS:     0,\n\t\t\t\tPayload: unit.PayloadAV1{{1, 2, 3, 4}},\n\t\t\t})\n\n\t\t\tsubStream.WriteUnit(subStream.CurDesc.Medias[1], subStream.CurDesc.Medias[1].Formats[0], &unit.Unit{\n\t\t\t\tPTS:     0,\n\t\t\t\tPayload: unit.PayloadVP9{1, 2, 3, 4},\n\t\t\t})\n\n\t\t\tsubStream.WriteUnit(subStream.CurDesc.Medias[2], subStream.CurDesc.Medias[2].Formats[0], &unit.Unit{\n\t\t\t\tPTS:     0,\n\t\t\t\tPayload: unit.PayloadH265{{1, 2, 3, 4}},\n\t\t\t})\n\n\t\t\tsubStream.WriteUnit(subStream.CurDesc.Medias[3], subStream.CurDesc.Medias[3].Formats[0], &unit.Unit{\n\t\t\t\tPTS:     0,\n\t\t\t\tPayload: unit.PayloadH264{{1, 2, 3, 4}},\n\t\t\t})\n\n\t\t\tsubStream.WriteUnit(subStream.CurDesc.Medias[4], subStream.CurDesc.Medias[4].Formats[0], &unit.Unit{\n\t\t\t\tPTS:     0,\n\t\t\t\tPayload: unit.PayloadOpus{{1, 2}},\n\t\t\t})\n\n\t\t\tsubStream.WriteUnit(subStream.CurDesc.Medias[5], subStream.CurDesc.Medias[5].Formats[0], &unit.Unit{\n\t\t\t\tPTS:     0,\n\t\t\t\tPayload: unit.PayloadMPEG4Audio{{1, 2, 3, 4}},\n\t\t\t})\n\n\t\t\tsubStream.WriteUnit(subStream.CurDesc.Medias[6], subStream.CurDesc.Medias[6].Formats[0], &unit.Unit{\n\t\t\t\tPTS:     0,\n\t\t\t\tPayload: unit.PayloadLPCM{1, 2, 3, 4},\n\t\t\t})\n\n\t\t\twg.Wait()\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/stream/stream_format.go",
    "content": "package stream\n\nimport (\n\t\"crypto/rand\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/pion/rtp\"\n\n\t\"github.com/bluenviron/mediamtx/internal/errordumper\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/ntpestimator\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n)\n\nfunc multiplyAndDivide(v, m, d int64) int64 {\n\tsecs := v / d\n\tdec := v % d\n\treturn (secs*m + dec*m/d)\n}\n\nfunc unitSize(u *unit.Unit) uint64 {\n\tn := uint64(0)\n\tfor _, pkt := range u.RTPPackets {\n\t\tn += uint64(pkt.MarshalSize())\n\t}\n\treturn n\n}\n\nfunc randUint32() (uint32, error) {\n\tvar b [4]byte\n\t_, err := rand.Read(b[:])\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn uint32(b[0])<<24 | uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3]), nil\n}\n\ntype streamFormat struct {\n\tformat               format.Format\n\tmedia                *description.Media\n\talwaysAvailable      bool\n\trtpMaxPayloadSize    int\n\treplaceNTP           bool\n\tinboundFramesInError *errordumper.Dumper\n\taddInboundBytes      func(uint64)\n\taddOutboundBytes     func(uint64)\n\tupdateLastTime       func(time.Duration)\n\twriteRTSP            func(*description.Media, []*rtp.Packet, time.Time)\n\tparent               logger.Writer\n\n\tptsOffset     int64\n\tformatUpdater formatUpdater\n\tunitRemuxer   unitRemuxer\n\trtpEncoder    rtpEncoder\n\trtpTimeOffset uint32\n\tntpEstimator  *ntpestimator.Estimator\n\tonDatas       map[*Reader]OnDataFunc\n}\n\nfunc (sf *streamFormat) initialize() error {\n\tsf.formatUpdater = newFormatUpdater(sf.format)\n\tsf.unitRemuxer = newUnitRemuxer(sf.format)\n\n\tif sf.replaceNTP {\n\t\tsf.ntpEstimator = &ntpestimator.Estimator{\n\t\t\tClockRate: sf.format.ClockRate(),\n\t\t}\n\t}\n\n\tsf.onDatas = make(map[*Reader]OnDataFunc)\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/stream/stream_media.go",
    "content": "package stream\n\nimport (\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/mediamtx/internal/errordumper\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/pion/rtp\"\n)\n\ntype streamMedia struct {\n\tmedia                *description.Media\n\talwaysAvailable      bool\n\trtpMaxPayloadSize    int\n\treplaceNTP           bool\n\taddInboundBytes      func(uint64)\n\taddOutboundBytes     func(uint64)\n\tupdateLastTime       func(time.Duration)\n\twriteRTSP            func(*description.Media, []*rtp.Packet, time.Time)\n\tinboundFramesInError *errordumper.Dumper\n\tparent               logger.Writer\n\n\tformats map[format.Format]*streamFormat\n}\n\nfunc (sm *streamMedia) initialize() error {\n\tsm.formats = make(map[format.Format]*streamFormat)\n\n\tfor _, forma := range sm.media.Formats {\n\t\tsf := &streamFormat{\n\t\t\tformat:               forma,\n\t\t\tmedia:                sm.media,\n\t\t\talwaysAvailable:      sm.alwaysAvailable,\n\t\t\trtpMaxPayloadSize:    sm.rtpMaxPayloadSize,\n\t\t\treplaceNTP:           sm.replaceNTP,\n\t\t\tinboundFramesInError: sm.inboundFramesInError,\n\t\t\taddInboundBytes:      sm.addInboundBytes,\n\t\t\taddOutboundBytes:     sm.addOutboundBytes,\n\t\t\tupdateLastTime:       sm.updateLastTime,\n\t\t\twriteRTSP:            sm.writeRTSP,\n\t\t\tparent:               sm.parent,\n\t\t}\n\t\terr := sf.initialize()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tsm.formats[forma] = sf\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/stream/stream_standard_test.go",
    "content": "package stream\n\nimport (\n\t\"testing\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n\t\"github.com/pion/rtp\"\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype nilLogger struct{}\n\nfunc (nilLogger) Log(logger.Level, string, ...any) {\n}\n\nfunc TestStream(t *testing.T) {\n\tdesc := &description.Session{Medias: []*description.Media{\n\t\t{\n\t\t\tType:    description.MediaTypeVideo,\n\t\t\tFormats: []format.Format{&format.H264{}},\n\t\t},\n\t\t{\n\t\t\tType:    description.MediaTypeVideo,\n\t\t\tFormats: []format.Format{&format.VP8{}},\n\t\t},\n\t}}\n\n\tstrm := &Stream{\n\t\tDesc:              desc,\n\t\tWriteQueueSize:    512,\n\t\tRTPMaxPayloadSize: 1450,\n\t}\n\terr := strm.Initialize()\n\trequire.NoError(t, err)\n\tdefer strm.Close()\n\n\tsubStream := &SubStream{\n\t\tStream:        strm,\n\t\tUseRTPPackets: false,\n\t}\n\terr = subStream.Initialize()\n\trequire.NoError(t, err)\n\n\tr := &Reader{}\n\n\trecv := make(chan struct{})\n\n\tr.OnData(desc.Medias[0], desc.Medias[0].Formats[0], func(_ *unit.Unit) error {\n\t\tclose(recv)\n\t\treturn nil\n\t})\n\n\tstrm.AddReader(r)\n\tdefer strm.RemoveReader(r)\n\n\tsubStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{\n\t\tPTS: 30000 * 2,\n\t\tPayload: unit.PayloadH264{\n\t\t\t{5, 2}, // IDR\n\t\t},\n\t})\n\n\t<-recv\n\n\trequire.Equal(t, uint64(14), strm.InboundBytes())\n\trequire.Equal(t, uint64(14), strm.OutboundBytes())\n}\n\nfunc TestStreamSkipBytesSent(t *testing.T) {\n\tdesc := &description.Session{Medias: []*description.Media{\n\t\t{\n\t\t\tType:    description.MediaTypeVideo,\n\t\t\tFormats: []format.Format{&format.H264{}},\n\t\t},\n\t\t{\n\t\t\tType:    description.MediaTypeVideo,\n\t\t\tFormats: []format.Format{&format.VP8{}},\n\t\t},\n\t}}\n\n\tstrm := &Stream{\n\t\tDesc:              desc,\n\t\tWriteQueueSize:    512,\n\t\tRTPMaxPayloadSize: 1450,\n\t}\n\terr := strm.Initialize()\n\trequire.NoError(t, err)\n\tdefer strm.Close()\n\n\tsubStream := &SubStream{\n\t\tStream:        strm,\n\t\tUseRTPPackets: false,\n\t}\n\terr = subStream.Initialize()\n\trequire.NoError(t, err)\n\n\tr := &Reader{\n\t\tSkipBytesSent: true,\n\t}\n\n\trecv := make(chan struct{})\n\n\tr.OnData(desc.Medias[0], desc.Medias[0].Formats[0], func(_ *unit.Unit) error {\n\t\tclose(recv)\n\t\treturn nil\n\t})\n\n\tstrm.AddReader(r)\n\tdefer strm.RemoveReader(r)\n\n\tsubStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{\n\t\tPTS: 30000 * 2,\n\t\tPayload: unit.PayloadH264{\n\t\t\t{5, 2}, // IDR\n\t\t},\n\t})\n\n\t<-recv\n\n\trequire.Equal(t, uint64(14), strm.InboundBytes())\n\trequire.Equal(t, uint64(0), strm.OutboundBytes())\n}\n\nfunc TestStreamResizeOversizedRTPPackets(t *testing.T) {\n\tdesc := &description.Session{Medias: []*description.Media{\n\t\t{\n\t\t\tType: description.MediaTypeVideo,\n\t\t\tFormats: []format.Format{&format.H264{\n\t\t\t\tSPS: []byte{ // 1920x1080 baseline\n\t\t\t\t\t0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02,\n\t\t\t\t\t0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04,\n\t\t\t\t\t0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, 0x20,\n\t\t\t\t},\n\t\t\t\tPPS: []byte{0x08, 0x06, 0x07, 0x08},\n\t\t\t}},\n\t\t},\n\t}}\n\n\tstrm := &Stream{\n\t\tDesc:              desc,\n\t\tWriteQueueSize:    512,\n\t\tRTPMaxPayloadSize: 400,\n\t\tParent:            &nilLogger{},\n\t}\n\terr := strm.Initialize()\n\trequire.NoError(t, err)\n\tdefer strm.Close()\n\n\tsubStream := &SubStream{\n\t\tStream:        strm,\n\t\tUseRTPPackets: true,\n\t}\n\terr = subStream.Initialize()\n\trequire.NoError(t, err)\n\n\tr := &Reader{}\n\n\trecv := make(chan *unit.Unit)\n\tn := 0\n\n\tr.OnData(desc.Medias[0], desc.Medias[0].Formats[0], func(u *unit.Unit) error {\n\t\tswitch n {\n\t\tcase 0:\n\t\tcase 1:\n\t\t\trecv <- u\n\t\tdefault:\n\t\t\tt.Error(\"should not happen\")\n\t\t}\n\t\tn++\n\t\treturn nil\n\t})\n\n\tstrm.AddReader(r)\n\tdefer strm.RemoveReader(r)\n\n\tsubStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{\n\t\tPTS: 90000,\n\t\tRTPPackets: []*rtp.Packet{\n\t\t\t{\n\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\tVersion:        2,\n\t\t\t\t\tMarker:         true,\n\t\t\t\t\tPayloadType:    96,\n\t\t\t\t\tSequenceNumber: 122,\n\t\t\t\t\tTimestamp:      45343,\n\t\t\t\t\tSSRC:           563423,\n\t\t\t\t},\n\t\t\t\tPayload: []byte{1, 2, 3, 4},\n\t\t\t},\n\t\t},\n\t})\n\n\toversizedPayload := make([]byte, 1000)\n\tfor i := range oversizedPayload {\n\t\toversizedPayload[i] = byte(i % 256)\n\t}\n\n\tsubStream.WriteUnit(desc.Medias[0], desc.Medias[0].Formats[0], &unit.Unit{\n\t\tPTS: 90000,\n\t\tRTPPackets: []*rtp.Packet{\n\t\t\t{\n\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\tVersion:        2,\n\t\t\t\t\tMarker:         true,\n\t\t\t\t\tPayloadType:    96,\n\t\t\t\t\tSequenceNumber: 123,\n\t\t\t\t\tTimestamp:      45343,\n\t\t\t\t\tSSRC:           563423,\n\t\t\t\t},\n\t\t\t\tPayload: oversizedPayload,\n\t\t\t},\n\t\t},\n\t})\n\n\treceived := <-recv\n\n\trequire.Equal(t, 3, len(received.RTPPackets))\n\n\tfor i, pkt := range received.RTPPackets {\n\t\trequire.Equal(t, 123+uint16(i), pkt.SequenceNumber)\n\t\trequire.Equal(t, uint32(45343), pkt.Timestamp)\n\t}\n\n\ttotalPayloadSize := 0\n\tfor _, pkt := range received.RTPPackets {\n\t\trequire.LessOrEqual(t, len(pkt.Payload), 400)\n\t\ttotalPayloadSize += len(pkt.Payload)\n\t}\n\n\trequire.Equal(t, 1005, totalPayloadSize)\n}\n\nfunc TestStreamUpdateFormatParams(t *testing.T) {\n\tfor _, ca := range []string{\"h264\", \"h265\", \"mpeg4video\"} {\n\t\tt.Run(ca, func(t *testing.T) {\n\t\t\tvar desc *description.Session\n\t\t\tvar media *description.Media\n\t\t\tvar forma format.Format\n\t\t\tvar u *unit.Unit\n\n\t\t\tswitch ca {\n\t\t\tcase \"h264\":\n\t\t\t\tsps := []byte{\n\t\t\t\t\t0x67, 0x64, 0x00, 0x20, 0xac, 0xd9, 0x40, 0x78,\n\t\t\t\t\t0x02, 0x27, 0xe5, 0x9a, 0x80, 0x80, 0x80, 0xa0,\n\t\t\t\t}\n\t\t\t\tpps := []byte{0x08, 0x07, 0x08, 0x09}\n\n\t\t\t\tformatH264 := &format.H264{\n\t\t\t\t\tSPS: []byte{0x67, 0x42, 0xc0, 0x28},\n\t\t\t\t\tPPS: []byte{0x08, 0x06},\n\t\t\t\t}\n\n\t\t\t\tdesc = &description.Session{Medias: []*description.Media{{\n\t\t\t\t\tType:    description.MediaTypeVideo,\n\t\t\t\t\tFormats: []format.Format{formatH264},\n\t\t\t\t}}}\n\t\t\t\tmedia = desc.Medias[0]\n\t\t\t\tforma = formatH264\n\n\t\t\t\tu = &unit.Unit{\n\t\t\t\t\tPTS: 90000,\n\t\t\t\t\tPayload: unit.PayloadH264{\n\t\t\t\t\t\tsps,    // New SPS\n\t\t\t\t\t\tpps,    // New PPS\n\t\t\t\t\t\t{5, 1}, // IDR\n\t\t\t\t\t},\n\t\t\t\t}\n\n\t\t\tcase \"h265\":\n\t\t\t\tvps := []byte{0x40, 0x01, 0x0c, 0x01, 0xff, 0xfe}\n\t\t\t\tsps := []byte{0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x04}\n\t\t\t\tpps := []byte{0x44, 0x01, 0xc1, 0x73, 0xd1, 0x8a}\n\n\t\t\t\tformatH265 := &format.H265{\n\t\t\t\t\tVPS: []byte{0x40, 0x01, 0x0c},\n\t\t\t\t\tSPS: []byte{0x42, 0x01, 0x01},\n\t\t\t\t\tPPS: []byte{0x44, 0x01, 0xc1},\n\t\t\t\t}\n\n\t\t\t\tdesc = &description.Session{Medias: []*description.Media{{\n\t\t\t\t\tType:    description.MediaTypeVideo,\n\t\t\t\t\tFormats: []format.Format{formatH265},\n\t\t\t\t}}}\n\t\t\t\tmedia = desc.Medias[0]\n\t\t\t\tforma = formatH265\n\n\t\t\t\tu = &unit.Unit{\n\t\t\t\t\tPTS: 90000,\n\t\t\t\t\tPayload: unit.PayloadH265{\n\t\t\t\t\t\tvps,                      // New VPS\n\t\t\t\t\t\tsps,                      // New SPS\n\t\t\t\t\t\tpps,                      // New PPS\n\t\t\t\t\t\t{0x26, 0x01, 0x01, 0x02}, // IDR\n\t\t\t\t\t},\n\t\t\t\t}\n\n\t\t\tcase \"mpeg4video\":\n\t\t\t\tconfig := []byte{\n\t\t\t\t\t0x00, 0x00, 0x01, 0xb0, // Visual Object Sequence Start\n\t\t\t\t\t0x02, 0x00, 0x00, 0x01, 0xb5, 0x8a,\n\t\t\t\t\t0x14, 0x00, 0x00, 0x01, 0x00,\n\t\t\t\t}\n\n\t\t\t\tformatMPEG4Video := &format.MPEG4Video{\n\t\t\t\t\tConfig: []byte{0x00, 0x00, 0x01, 0xb0, 0x01},\n\t\t\t\t}\n\n\t\t\t\tdesc = &description.Session{Medias: []*description.Media{{\n\t\t\t\t\tType:    description.MediaTypeVideo,\n\t\t\t\t\tFormats: []format.Format{formatMPEG4Video},\n\t\t\t\t}}}\n\t\t\t\tmedia = desc.Medias[0]\n\t\t\t\tforma = formatMPEG4Video\n\n\t\t\t\tframe := make([]byte, 0, len(config)+20)\n\t\t\t\tframe = append(frame, config...)\n\t\t\t\tframe = append(frame, []byte{0x00, 0x00, 0x01, 0xb3}...) // Group of VOP\n\t\t\t\tframe = append(frame, []byte{0x01, 0x02, 0x03, 0x04}...)\n\n\t\t\t\tu = &unit.Unit{\n\t\t\t\t\tPTS:     90000,\n\t\t\t\t\tPayload: unit.PayloadMPEG4Video(frame),\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tstrm := &Stream{\n\t\t\t\tDesc:              desc,\n\t\t\t\tWriteQueueSize:    512,\n\t\t\t\tRTPMaxPayloadSize: 1450,\n\t\t\t}\n\t\t\terr := strm.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer strm.Close()\n\n\t\t\tsubStream := &SubStream{\n\t\t\t\tStream:        strm,\n\t\t\t\tUseRTPPackets: false,\n\t\t\t}\n\t\t\terr = subStream.Initialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tr := &Reader{}\n\t\t\trecv := make(chan struct{})\n\n\t\t\tr.OnData(media, forma, func(_ *unit.Unit) error {\n\t\t\t\tclose(recv)\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\t\tstrm.AddReader(r)\n\t\t\tdefer strm.RemoveReader(r)\n\n\t\t\tsubStream.WriteUnit(media, forma, u)\n\n\t\t\t<-recv\n\n\t\t\t// Verify that format parameters were updated\n\t\t\tswitch ca {\n\t\t\tcase \"h264\":\n\t\t\t\tformatH264 := forma.(*format.H264)\n\t\t\t\trequire.Equal(t, []byte{\n\t\t\t\t\t0x67, 0x64, 0x00, 0x20, 0xac, 0xd9, 0x40, 0x78,\n\t\t\t\t\t0x02, 0x27, 0xe5, 0x9a, 0x80, 0x80, 0x80, 0xa0,\n\t\t\t\t}, formatH264.SPS)\n\t\t\t\trequire.Equal(t, []byte{0x08, 0x07, 0x08, 0x09}, formatH264.PPS)\n\n\t\t\tcase \"h265\":\n\t\t\t\tformatH265 := forma.(*format.H265)\n\t\t\t\trequire.Equal(t, []byte{0x40, 0x01, 0x0c, 0x01, 0xff, 0xfe}, formatH265.VPS)\n\t\t\t\trequire.Equal(t, []byte{0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x04}, formatH265.SPS)\n\t\t\t\trequire.Equal(t, []byte{0x44, 0x01, 0xc1, 0x73, 0xd1, 0x8a}, formatH265.PPS)\n\n\t\t\tcase \"mpeg4video\":\n\t\t\t\tformatMPEG4Video := forma.(*format.MPEG4Video)\n\t\t\t\trequire.Equal(t, []byte{\n\t\t\t\t\t0x00, 0x00, 0x01, 0xb0,\n\t\t\t\t\t0x02, 0x00, 0x00, 0x01, 0xb5, 0x8a,\n\t\t\t\t\t0x14, 0x00, 0x00, 0x01, 0x00,\n\t\t\t\t}, formatMPEG4Video.Config)\n\t\t\t}\n\t\t})\n\t}\n}\n\nvar casesDecodeEncode = []struct {\n\tname    string\n\tformat  format.Format\n\tencoded []*rtp.Packet\n\tdecoded unit.Payload\n}{\n\t{\n\t\tname: \"av1\",\n\t\tformat: &format.AV1{\n\t\t\tPayloadTyp: 96,\n\t\t},\n\t\tencoded: []*rtp.Packet{\n\t\t\t{\n\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\tVersion:        2,\n\t\t\t\t\tMarker:         true,\n\t\t\t\t\tPayloadType:    96,\n\t\t\t\t\tSequenceNumber: 123,\n\t\t\t\t\tTimestamp:      45343,\n\t\t\t\t\tSSRC:           563423,\n\t\t\t\t},\n\t\t\t\tPayload: []byte{\n\t\t\t\t\t0x10,\n\t\t\t\t\t0x02,       // Size = 2\n\t\t\t\t\t0x01, 0x02, // OBU data\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tdecoded: unit.PayloadAV1{\n\t\t\t{0x02, 0x01, 0x02}, // Size byte included with OBU data\n\t\t},\n\t},\n\t{\n\t\tname: \"vp9\",\n\t\tformat: &format.VP9{\n\t\t\tPayloadTyp: 96,\n\t\t},\n\t\tencoded: []*rtp.Packet{\n\t\t\t{\n\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\tVersion:        2,\n\t\t\t\t\tMarker:         true,\n\t\t\t\t\tPayloadType:    96,\n\t\t\t\t\tSequenceNumber: 123,\n\t\t\t\t\tTimestamp:      45343,\n\t\t\t\t\tSSRC:           563423,\n\t\t\t\t},\n\t\t\t\tPayload: []byte{\n\t\t\t\t\t0x8f, 0xb5, 0xaf, 0x18, 0x07, 0x80, 0x03, 0x24,\n\t\t\t\t\t0x01, 0x14, 0x01, 0x82, 0x49, 0x83, 0x42, 0x00,\n\t\t\t\t\t0x77, 0xf0, 0x32, 0x34,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tdecoded: unit.PayloadVP9{0x82, 0x49, 0x83, 0x42, 0x0, 0x77, 0xf0, 0x32, 0x34},\n\t},\n\t{\n\t\tname: \"vp8\",\n\t\tformat: &format.VP8{\n\t\t\tPayloadTyp: 96,\n\t\t},\n\t\tencoded: []*rtp.Packet{\n\t\t\t{\n\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\tVersion:        2,\n\t\t\t\t\tMarker:         true,\n\t\t\t\t\tPayloadType:    96,\n\t\t\t\t\tSequenceNumber: 123,\n\t\t\t\t\tTimestamp:      45343,\n\t\t\t\t\tSSRC:           563423,\n\t\t\t\t},\n\t\t\t\tPayload: []byte{\n\t\t\t\t\t0x10, // X=0, R=0, N=0, S=1, PartID=0\n\t\t\t\t\t0x01, 0x02, 0x03, 0x04,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tdecoded: unit.PayloadVP8{0x01, 0x02, 0x03, 0x04},\n\t},\n\t{\n\t\tname: \"h265\",\n\t\tformat: &format.H265{\n\t\t\tPayloadTyp: 96,\n\t\t\tVPS:        []byte{0x40, 0x01, 0x0c, 0x01, 0xff, 0xfe},\n\t\t\tSPS:        []byte{0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x04},\n\t\t\tPPS:        []byte{0x44, 0x01, 0xc1, 0x73, 0xd1, 0x8a},\n\t\t},\n\t\tencoded: []*rtp.Packet{\n\t\t\t{\n\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\tVersion:        2,\n\t\t\t\t\tMarker:         true,\n\t\t\t\t\tPayloadType:    96,\n\t\t\t\t\tSequenceNumber: 123,\n\t\t\t\t\tTimestamp:      45343,\n\t\t\t\t\tSSRC:           563423,\n\t\t\t\t},\n\t\t\t\tPayload: []byte{\n\t\t\t\t\t0x60, 0x01, 0x00, 0x06, 0x40, 0x01, 0x0c, 0x01,\n\t\t\t\t\t0xff, 0xfe, 0x00, 0x08, 0x42, 0x01, 0x01, 0x01,\n\t\t\t\t\t0x60, 0x00, 0x00, 0x04, 0x00, 0x06, 0x44, 0x01,\n\t\t\t\t\t0xc1, 0x73, 0xd1, 0x8a, 0x00, 0x05, 0x26, 0x01,\n\t\t\t\t\t0x01, 0x02, 0x03,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tdecoded: unit.PayloadH265{\n\t\t\t{0x40, 0x01, 0x0c, 0x01, 0xff, 0xfe},             // VPS\n\t\t\t{0x42, 0x01, 0x01, 0x01, 0x60, 0x00, 0x00, 0x04}, // SPS\n\t\t\t{0x44, 0x01, 0xc1, 0x73, 0xd1, 0x8a},             // PPS\n\t\t\t{0x26, 0x01, 0x01, 0x02, 0x03},                   // IDR\n\t\t},\n\t},\n\t{\n\t\tname: \"h264\",\n\t\tformat: &format.H264{\n\t\t\tPayloadTyp: 96,\n\t\t\tSPS: []byte{\n\t\t\t\t0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02,\n\t\t\t\t0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04,\n\t\t\t\t0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, 0x20,\n\t\t\t},\n\t\t\tPPS: []byte{0x08, 0x06, 0x07, 0x08},\n\t\t},\n\t\tencoded: []*rtp.Packet{\n\t\t\t{\n\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\tVersion:        2,\n\t\t\t\t\tMarker:         true,\n\t\t\t\t\tPayloadType:    96,\n\t\t\t\t\tSequenceNumber: 123,\n\t\t\t\t\tTimestamp:      45343,\n\t\t\t\t\tSSRC:           563423,\n\t\t\t\t},\n\t\t\t\tPayload: []byte{\n\t\t\t\t\t0x18, 0x00, 0x19, 0x67, 0x42, 0xc0, 0x28, 0xd9,\n\t\t\t\t\t0x00, 0x78, 0x02, 0x27, 0xe5, 0x84, 0x00, 0x00,\n\t\t\t\t\t0x03, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0xf0,\n\t\t\t\t\t0x3c, 0x60, 0xc9, 0x20, 0x00, 0x04, 0x08, 0x06,\n\t\t\t\t\t0x07, 0x08, 0x00, 0x05, 0x05, 0x01, 0x02, 0x03,\n\t\t\t\t\t0x04,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tdecoded: unit.PayloadH264{\n\t\t\t{\n\t\t\t\t0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02, 0x27, 0xe5, 0x84,\n\t\t\t\t0x00, 0x00, 0x03, 0x00, 0x04, 0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, 0x20,\n\t\t\t}, // SPS\n\t\t\t{0x08, 0x06, 0x07, 0x08},       // PPS\n\t\t\t{0x05, 0x01, 0x02, 0x03, 0x04}, // IDR\n\t\t},\n\t},\n\t{\n\t\tname: \"mpeg4video\",\n\t\tformat: &format.MPEG4Video{\n\t\t\tPayloadTyp: 96,\n\t\t\tConfig:     []byte{0x00, 0x00, 0x01, 0xb0, 0x01},\n\t\t},\n\t\tencoded: []*rtp.Packet{\n\t\t\t{\n\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\tVersion:        2,\n\t\t\t\t\tMarker:         true,\n\t\t\t\t\tPayloadType:    96,\n\t\t\t\t\tSequenceNumber: 123,\n\t\t\t\t\tTimestamp:      45343,\n\t\t\t\t\tSSRC:           563423,\n\t\t\t\t},\n\t\t\t\tPayload: []byte{0x00, 0x01, 0x02, 0x03, 0x04},\n\t\t\t},\n\t\t},\n\t\tdecoded: unit.PayloadMPEG4Video{0x00, 0x01, 0x02, 0x03, 0x04},\n\t},\n\t{\n\t\tname:   \"mpeg1video\",\n\t\tformat: &format.MPEG1Video{},\n\t\tencoded: []*rtp.Packet{\n\t\t\t{\n\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\tVersion:        2,\n\t\t\t\t\tMarker:         true, // Marker indicates complete frame\n\t\t\t\t\tPayloadType:    32,\n\t\t\t\t\tSequenceNumber: 123,\n\t\t\t\t\tTimestamp:      45343,\n\t\t\t\t\tSSRC:           563423,\n\t\t\t\t},\n\t\t\t\tPayload: []byte{\n\t\t\t\t\t// MPEG-1 Video RTP header (4 bytes)\n\t\t\t\t\t0x00, // MBZ=0, T=0 (MPEG-1)\n\t\t\t\t\t0x00, // TR (temporal reference) - low 8 bits\n\t\t\t\t\t0x18, // AN=0, N=0, S=0 (no sequence header), B=1, E=1 (complete slice), FBV=0, BFC=0, FFV=0, FFC=0\n\t\t\t\t\t0x00, // FFC (continued)\n\t\t\t\t\t// MPEG-1 Video data (slice or frame data)\n\t\t\t\t\t0x00, 0x00, 0x01, 0x01, // Slice start code\n\t\t\t\t\t0x01, 0x02, 0x03, 0x04, // Slice data\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tdecoded: unit.PayloadMPEG1Video{\n\t\t\t// Only the video data after the 4-byte RTP header\n\t\t\t0x00, 0x00, 0x01, 0x01,\n\t\t\t0x01, 0x02, 0x03, 0x04,\n\t\t},\n\t},\n\t{\n\t\tname:   \"mjpeg\",\n\t\tformat: &format.MJPEG{},\n\t\tencoded: []*rtp.Packet{\n\t\t\t{\n\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\tVersion:     2,\n\t\t\t\t\tPayloadType: 26,\n\t\t\t\t},\n\t\t\t\tPayload: []byte{\n\t\t\t\t\t0x00, 0x00, 0x00, 0x00, 0x01, 0xff, 0xf0, 0x87,\n\t\t\t\t\t0x00, 0x00, 0x00, 0x80, 0x0d, 0x09, 0x0a, 0x0b,\n\t\t\t\t\t0x0a, 0x08, 0x0d, 0x0b, 0x0a, 0x0b, 0x0e, 0x0e,\n\t\t\t\t\t0x0d, 0x0f, 0x13, 0x20, 0x15, 0x13, 0x12, 0x12,\n\t\t\t\t\t0x13, 0x27, 0x1c, 0x1e, 0x17, 0x20, 0x2e, 0x29,\n\t\t\t\t\t0x31, 0x30, 0x2e, 0x29, 0x2d, 0x2c, 0x33, 0x3a,\n\t\t\t\t\t0x4a, 0x3e, 0x33, 0x36, 0x46, 0x37, 0x2c, 0x2d,\n\t\t\t\t\t0x40, 0x57, 0x41, 0x46, 0x4c, 0x4e, 0x52, 0x53,\n\t\t\t\t\t0x52, 0x32, 0x3e, 0x5a, 0x61, 0x5a, 0x50, 0x60,\n\t\t\t\t\t0x4a, 0x51, 0x52, 0x4f, 0x0e, 0x0e, 0x0e, 0x13,\n\t\t\t\t\t0x11, 0x13, 0x26, 0x15, 0x15, 0x26, 0x4f, 0x35,\n\t\t\t\t\t0x2d, 0x35, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f,\n\t\t\t\t\t0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f,\n\t\t\t\t\t0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f,\n\t\t\t\t\t0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f,\n\t\t\t\t\t0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f,\n\t\t\t\t\t0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f,\n\t\t\t\t\t0x4f, 0x4f, 0x4f, 0x4f, 0x92, 0x8a, 0x28, 0xaf,\n\t\t\t\t\t0x54, 0xf2, 0x42, 0x8a, 0x28, 0xa0, 0x02, 0x96,\n\t\t\t\t\t0x92, 0x96, 0x80, 0x0a, 0x4a, 0x75, 0x25, 0x02,\n\t\t\t\t\t0x12, 0x8a, 0x5a, 0x28, 0x18, 0x94, 0x52, 0xd1,\n\t\t\t\t\t0x40, 0x09, 0x45, 0x2d, 0x14, 0x08, 0x29, 0x69,\n\t\t\t\t\t0x29, 0x68, 0x00, 0xa5, 0xa4, 0xa5, 0xa0, 0x02,\n\t\t\t\t\t0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x04,\n\t\t\t\t\t0xa5, 0xa2, 0x8a, 0x00, 0x5a, 0x28, 0xa2, 0x80,\n\t\t\t\t\t0x0a, 0x28, 0xa2, 0x80, 0x0a, 0x28, 0xa2, 0x80,\n\t\t\t\t\t0x12, 0x8a, 0x5a, 0x28, 0x24, 0x29, 0x69, 0x29,\n\t\t\t\t\t0x68, 0x00, 0xa2, 0x8a, 0x28, 0x00, 0xa2, 0x8a,\n\t\t\t\t\t0x28, 0x00, 0xa2, 0x8a, 0x28, 0x00, 0xa2, 0x8a,\n\t\t\t\t\t0x28, 0x00, 0xa2, 0x8a, 0x28, 0x00, 0xa2, 0x8a,\n\t\t\t\t\t0x28, 0x00, 0xa2, 0x8a, 0x28, 0x00, 0xa2, 0x8a,\n\t\t\t\t\t0x28, 0x00, 0xa2, 0x8a, 0x28, 0x00, 0xa2, 0x8a,\n\t\t\t\t\t0x28, 0x00, 0xa2, 0x8a, 0x28, 0x00, 0xa4, 0xa5,\n\t\t\t\t\t0xa4, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a,\n\t\t\t\t\t0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a,\n\t\t\t\t\t0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a,\n\t\t\t\t\t0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a,\n\t\t\t\t\t0x28, 0xa0, 0x02, 0x96, 0x92, 0x96, 0x80, 0x0a,\n\t\t\t\t\t0x28, 0xa2, 0x80, 0x0a, 0x28, 0xa2, 0x80, 0x0a,\n\t\t\t\t\t0x28, 0xa2, 0x80, 0x0a, 0x28, 0xa2, 0x80, 0x0a,\n\t\t\t\t\t0x28, 0xa2, 0x80, 0x0a, 0x28, 0xa2, 0x80, 0x0a,\n\t\t\t\t\t0x28, 0xa2, 0x80, 0x0a, 0x28, 0xa2, 0x80, 0x0a,\n\t\t\t\t\t0x28, 0xa2, 0x81, 0x85, 0x14, 0x51, 0x40, 0x05,\n\t\t\t\t\t0x14, 0x51, 0x40, 0x05, 0x14, 0x51, 0x40, 0x05,\n\t\t\t\t\t0x14, 0x52, 0xd0, 0x01, 0x45, 0x14, 0x50, 0x01,\n\t\t\t\t\t0x45, 0x14, 0x50, 0x01, 0x45, 0x14, 0x50, 0x01,\n\t\t\t\t\t0x45, 0x2d, 0x14, 0x00, 0x94, 0xb4, 0x51, 0x40,\n\t\t\t\t\t0x05, 0x14, 0x52, 0xd0, 0x02, 0x51, 0x4b, 0x45,\n\t\t\t\t\t0x00, 0x25, 0x2d, 0x14, 0x50, 0x01, 0x45, 0x14,\n\t\t\t\t\t0x50, 0x01, 0x45, 0x14, 0x50, 0x20, 0xa5, 0xa4,\n\t\t\t\t\t0xa5, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02, 0x8a,\n\t\t\t\t\t0x28, 0xa0, 0x02, 0x8a, 0x5a, 0x28, 0x18, 0x94,\n\t\t\t\t\t0xb4, 0x51, 0x40, 0xc2, 0x8a, 0x28, 0xa0, 0x05,\n\t\t\t\t\t0xa2, 0x92, 0x9d, 0x40, 0x05, 0x14, 0x51, 0x48,\n\t\t\t\t\t0x02, 0x8a, 0x28, 0xa4, 0x01, 0x4b, 0x49, 0x4b,\n\t\t\t\t\t0x40, 0x05, 0x14, 0x51, 0x40, 0x05, 0x14, 0xb4,\n\t\t\t\t\t0x50, 0x02, 0x51, 0x4b, 0x45, 0x00, 0x25, 0x2d,\n\t\t\t\t\t0x14, 0x50, 0x03, 0xa8, 0xa2, 0x8a, 0x00, 0x28,\n\t\t\t\t\t0xa2, 0x8a, 0x00, 0x5a, 0x29, 0x29, 0x68, 0x00,\n\t\t\t\t\t0xa2, 0x8a, 0x28, 0x00, 0xa2, 0x96, 0x8a, 0x06,\n\t\t\t\t\t0x25, 0x14, 0xb4, 0x50, 0x01, 0x45, 0x14, 0x50,\n\t\t\t\t\t0x02, 0xd2, 0xd2, 0x52, 0xd0, 0x20, 0xa2, 0x8a,\n\t\t\t\t\t0x28, 0x01, 0x68, 0xa2, 0x8a, 0x40, 0x14, 0x51,\n\t\t\t\t\t0x45, 0x30, 0x0a, 0x5a, 0x4a, 0x5a, 0x06, 0x14,\n\t\t\t\t\t0x51, 0x45, 0x02, 0x0a, 0x28, 0xa5, 0xa0, 0x62,\n\t\t\t\t\t0x51, 0x4b, 0x45, 0x00, 0x2d, 0x14, 0x51, 0x48,\n\t\t\t\t\t0x02, 0x8a, 0x28, 0xa0, 0x61, 0x45, 0x14, 0x50,\n\t\t\t\t\t0x03, 0xa8, 0xa2, 0x8a, 0x06, 0x2d, 0x14, 0x51,\n\t\t\t\t\t0x48, 0x02, 0x8a, 0x28, 0xa4, 0x30, 0xa2, 0x8a,\n\t\t\t\t\t0x2a, 0x80, 0x28, 0xa2, 0x8a, 0x00, 0x28, 0xa2,\n\t\t\t\t\t0x8a, 0x92, 0x45, 0xa5, 0xa2, 0x96, 0x82, 0x82,\n\t\t\t\t\t0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x05,\n\t\t\t\t\t0xa2, 0x8a, 0x29, 0x80, 0x52, 0xd2, 0x52, 0xd0,\n\t\t\t\t\t0x01, 0x45, 0x14, 0x50, 0x01, 0x4e, 0xa2, 0x8a,\n\t\t\t\t\t0x43, 0x0a, 0x28, 0xa2, 0x80, 0x0a, 0x28, 0xa4,\n\t\t\t\t\t0xa4, 0x31, 0x68, 0xa4, 0xf3, 0x62, 0xff, 0x00,\n\t\t\t\t\t0x9e, 0xd1, 0x7e, 0xea, 0x9f, 0xfe, 0xb6, 0xa4,\n\t\t\t\t\t0x62, 0x52, 0x53, 0xa9, 0x28, 0x01, 0x28, 0xa2,\n\t\t\t\t\t0x6f, 0xdd, 0x7f, 0xaf, 0xa5, 0xa0, 0x62, 0x51,\n\t\t\t\t\t0x4b, 0x45, 0x00, 0x25, 0x14, 0xbf, 0xba, 0xff,\n\t\t\t\t\t0x00, 0xae, 0xb4, 0xea, 0x60, 0x36, 0x9d, 0x49,\n\t\t\t\t\t0x49, 0x34, 0xb1, 0x45, 0xfe, 0xbe, 0x6f, 0x2a,\n\t\t\t\t\t0x98, 0x0f, 0xa2, 0xb9, 0xbd, 0x0f, 0x59, 0x97,\n\t\t\t\t\t0x54, 0xf1, 0x2d, 0xd7, 0xfc, 0xf9, 0xd7, 0x49,\n\t\t\t\t\t0x52, 0x30, 0xac, 0x7d, 0x5b, 0x54, 0xd5, 0x74,\n\t\t\t\t\t0xbf, 0xdf, 0x41, 0x67, 0x15, 0xd4, 0x35, 0x63,\n\t\t\t\t\t0x56, 0xd5, 0x22, 0xd2, 0xe0, 0xf3, 0xbc, 0x99,\n\t\t\t\t\t0x65, 0xae, 0x4b, 0x51, 0xf1, 0x44, 0x52, 0xff,\n\t\t\t\t\t0x00, 0xc7, 0x97, 0x9b, 0xe4, 0xd0, 0x69, 0x4c,\n\t\t\t\t\t0x76, 0xa3, 0xe2, 0xd9, 0xa5, 0x82, 0x29, 0xb4,\n\t\t\t\t\t0xbf, 0xdd, 0x79, 0xbf, 0xeb, 0x22, 0xaa, 0x7e,\n\t\t\t\t\t0x1e, 0xd5, 0x3e, 0xc1, 0x3f, 0xfa, 0x9f, 0xfa,\n\t\t\t\t\t0xe9, 0x25, 0x61, 0xd6, 0x9e, 0x87, 0xf6, 0xbf,\n\t\t\t\t\t0xed, 0x68, 0xbc, 0x8a, 0x82, 0xcf, 0x4e, 0x86,\n\t\t\t\t\t0x5f, 0x36, 0x0a, 0x92, 0x9b, 0xff, 0x00, 0x5d,\n\t\t\t\t\t0xeb, 0x3a, 0x6d, 0x53, 0xfd, 0x3f, 0xec, 0x76,\n\t\t\t\t\t0x3f, 0xbd, 0xff, 0x00, 0xa6, 0xbf, 0xc1, 0x54,\n\t\t\t\t\t0x66, 0x69, 0x51, 0x50, 0x4b, 0x2c, 0x51, 0x7e,\n\t\t\t\t\t0xe7, 0xce, 0xf3, 0x66, 0xff, 0x00, 0xa6, 0x75,\n\t\t\t\t\t0x35, 0x00, 0x2d, 0x65, 0x6a, 0xde, 0x23, 0xd3,\n\t\t\t\t\t0xec, 0x3f, 0xe5, 0xb7, 0x9b, 0x34, 0x5f, 0xf2,\n\t\t\t\t\t0xce, 0xb4, 0x7e, 0xd5, 0x69, 0x2f, 0xfc, 0xbe,\n\t\t\t\t\t0x45, 0x5e, 0x7b, 0xe2, 0x7d, 0x07, 0xec, 0x1e,\n\t\t\t\t\t0x6e, 0xa5, 0xf6, 0xc8, 0xa5, 0xf3, 0x6a, 0xc0,\n\t\t\t\t\t0xdf, 0xa2, 0x9d, 0x45, 0x6c, 0x72, 0x0d, 0xa5,\n\t\t\t\t\t0xa5, 0xa2, 0x80, 0x12, 0x96, 0x8a, 0x28, 0x10,\n\t\t\t\t\t0x51, 0x4b, 0x45, 0x00, 0x25, 0x14, 0xb4, 0x50,\n\t\t\t\t\t0x02, 0x51, 0x4b, 0x45, 0x03, 0x12, 0x8a, 0x5a,\n\t\t\t\t\t0x28, 0x10, 0x94, 0xb4, 0x51, 0x40, 0x05, 0x14,\n\t\t\t\t\t0x51, 0x40, 0x05, 0x14, 0xb4, 0x50, 0x02, 0x51,\n\t\t\t\t\t0x4b, 0x45, 0x00, 0x14, 0x51, 0x4b, 0x40, 0x84,\n\t\t\t\t\t0xa2, 0x8a, 0x28, 0x00, 0xa2, 0x96, 0x8a, 0x00,\n\t\t\t\t\t0x4a, 0x29, 0x68, 0xa0, 0x04, 0xa2, 0x96, 0x8a,\n\t\t\t\t\t0x00, 0x4a, 0x29, 0x68, 0xa0, 0x41, 0x45, 0x14,\n\t\t\t\t\t0x50, 0x01, 0x45, 0x14, 0x50, 0x01, 0x45, 0x14,\n\t\t\t\t\t0x50, 0x01, 0x45, 0x14, 0x50, 0x01, 0x45, 0x14,\n\t\t\t\t\t0x50, 0x01, 0x45, 0x14, 0x50, 0x01, 0x45, 0x14,\n\t\t\t\t\t0x50, 0x01, 0x45, 0x14, 0x50, 0x01, 0x45, 0x14,\n\t\t\t\t\t0x50, 0x01, 0x45, 0x14, 0x50, 0x01, 0x45, 0x14,\n\t\t\t\t\t0x50, 0x01, 0x49, 0x4b, 0x45, 0x00, 0x25, 0x14,\n\t\t\t\t\t0xb4, 0x50, 0x01, 0x45, 0x14, 0x50, 0x30, 0xa4,\n\t\t\t\t\t0xa5, 0xa2, 0x81, 0x09, 0x45, 0x2d, 0x14, 0x00,\n\t\t\t\t\t0x94, 0x52, 0xd1, 0x40, 0x09, 0x45, 0x2d, 0x14,\n\t\t\t\t\t0x00, 0x94, 0x52, 0xd1, 0x40, 0x05, 0x14, 0xb4,\n\t\t\t\t\t0x94, 0x0c, 0x28, 0xa2, 0x8a, 0x04, 0x14, 0x51,\n\t\t\t\t\t0x45, 0x00, 0x14, 0x51, 0x45, 0x00, 0x14, 0x51,\n\t\t\t\t\t0x45, 0x00, 0x14, 0x51, 0x45, 0x00, 0x14, 0x52,\n\t\t\t\t\t0xd1, 0x40, 0xc4, 0xa2, 0x96, 0x8a, 0x04, 0x25,\n\t\t\t\t\t0x14, 0xb4, 0x50, 0x31, 0x28, 0xa5, 0xa2, 0x80,\n\t\t\t\t\t0x12, 0x8a, 0x5a, 0x4a, 0x00, 0x5a, 0x28, 0xa2,\n\t\t\t\t\t0x80, 0x0a, 0x29, 0x68, 0xa0, 0x04, 0xa2, 0x96,\n\t\t\t\t\t0x8a, 0x00, 0x4a, 0x29, 0x68, 0xa0, 0x04, 0xa2,\n\t\t\t\t\t0x9d, 0x45, 0x03, 0x1b, 0x45, 0x3a, 0x8a, 0x00,\n\t\t\t\t\t0x6d, 0x14, 0xea, 0x28, 0x10, 0x94, 0x52, 0xd1,\n\t\t\t\t\t0x40, 0x09, 0x4b, 0x45, 0x14, 0x08, 0x28, 0xa2,\n\t\t\t\t\t0x8a, 0x06, 0x14, 0x52, 0xd1, 0x40, 0x09, 0x45,\n\t\t\t\t\t0x2d, 0x2d, 0x00, 0x25, 0x14, 0xb4, 0x50, 0x21,\n\t\t\t\t\t0x28, 0xa5, 0xa2, 0x81, 0x89, 0x4b, 0x45, 0x14,\n\t\t\t\t\t0x00, 0x51, 0x45, 0x14, 0x00, 0x51, 0x4b, 0x45,\n\t\t\t\t\t0x00, 0x25, 0x14, 0xb4, 0x50, 0x01, 0x45, 0x2d,\n\t\t\t\t\t0x14, 0x0c, 0x28, 0xa2, 0x8a, 0x00, 0x4a, 0x5a,\n\t\t\t\t\t0x5a, 0x28, 0x01, 0x28, 0xa5, 0xa2, 0x90, 0x05,\n\t\t\t\t\t0x14, 0x51, 0x40, 0x05, 0x14, 0xb4, 0x52, 0x01,\n\t\t\t\t\t0x28, 0xa5, 0xa2, 0x80, 0x0a, 0x28, 0xa7, 0x50,\n\t\t\t\t\t0x03, 0x68, 0xa7, 0x51, 0x40, 0x0d, 0xa7, 0x51,\n\t\t\t\t\t0x45, 0x00, 0x14, 0x51, 0x4b, 0x40, 0x09, 0x45,\n\t\t\t\t\t0x2d, 0x2d, 0x00, 0x25, 0x14, 0xb4, 0x50, 0x02,\n\t\t\t\t\t0x51, 0x4b, 0x45, 0x00, 0x14, 0x51, 0x45, 0x00,\n\t\t\t\t\t0x14, 0x51, 0x45, 0x00, 0x14, 0xb4, 0x53, 0xa8,\n\t\t\t\t\t0x18, 0xda, 0x5a, 0x5a, 0x28, 0x10, 0x94, 0xb4,\n\t\t\t\t\t0x51, 0x40, 0xc2, 0x8a, 0x5a, 0x29, 0x00, 0x94,\n\t\t\t\t\t0x52, 0xd1, 0x40, 0x05, 0x14, 0x51, 0x4c, 0x02,\n\t\t\t\t\t0x96, 0x8a, 0x29, 0x00, 0x52, 0xd2, 0x52, 0xd0,\n\t\t\t\t\t0x01, 0x45, 0x14, 0x50, 0x02, 0xd1, 0x45, 0x14,\n\t\t\t\t\t0x0c, 0x28, 0xa2, 0x96, 0x80, 0x0a, 0x28, 0xa2,\n\t\t\t\t\t0x81, 0x85, 0x2d, 0x14, 0x50, 0x01, 0x45, 0x14,\n\t\t\t\t\t0xb4, 0x80, 0x4a, 0x29, 0x68, 0xa0, 0x62, 0xd1,\n\t\t\t\t\t0x45, 0x14, 0xc0, 0x28, 0xa5, 0xa2, 0x90, 0x0d,\n\t\t\t\t\t0xa5, 0xa5, 0xa2, 0x80, 0x0a, 0x5a, 0x28, 0xa4,\n\t\t\t\t\t0x01, 0x45, 0x14, 0xb4, 0x00, 0x94, 0x52, 0xd1,\n\t\t\t\t\t0x40, 0x05, 0x14, 0x51, 0x4c, 0x02, 0x8a, 0x29,\n\t\t\t\t\t0x68, 0x01, 0x28, 0xa5, 0xa2, 0x80, 0x16, 0x8a,\n\t\t\t\t\t0x28, 0xa4, 0x30, 0xa7, 0x55, 0x7b, 0xbb, 0x5f,\n\t\t\t\t\t0xb7, 0xc1, 0xe4, 0xff, 0x00, 0xaa, 0xac, 0x49,\n\t\t\t\t\t0xb4, 0x6d, 0x57, 0x4b, 0xf3, 0x66, 0xd2, 0xef,\n\t\t\t\t\t0x3c, 0xd8, 0x69, 0x17, 0x4c, 0xe9, 0x2b, 0x07,\n\t\t\t\t\t0xc4, 0x32, 0xcb, 0x75, 0xa4, 0xff, 0x00, 0xc4,\n\t\t\t\t\t0xae, 0x68, 0xbf, 0x75, 0xfe, 0xb2, 0xb9, 0x09,\n\t\t\t\t\t0xb5, 0x9d, 0x42, 0x5f, 0x36, 0xcf, 0xce, 0xff,\n\t\t\t\t\t0x00, 0x5b, 0x56, 0x3c, 0x3d, 0x6b, 0xfd, 0xa9,\n\t\t\t\t\t0xab, 0x4b, 0x67, 0x3f, 0xfc, 0xf3, 0xff, 0x00,\n\t\t\t\t\t0x96, 0x74, 0x8d, 0xbd, 0x99, 0x9b, 0x0d, 0xfc,\n\t\t\t\t\t0xb2, 0xf9, 0xbf, 0xeb, 0x7f, 0x7b, 0x5a, 0xf0,\n\t\t\t\t\t0xf8, 0x8e, 0xee, 0x2d, 0x27, 0xec, 0x70, 0x7f,\n\t\t\t\t\t0xdf, 0xca, 0x66, 0xa3, 0xe1, 0x7f, 0xb0, 0x79,\n\t\t\t\t\t0xbe, 0x46, 0xa5, 0x17, 0xfd, 0x73, 0x92, 0xb0,\n\t\t\t\t\t0xe1, 0xa8, 0x0f, 0x66, 0x76, 0x90, 0xf8, 0xa2,\n\t\t\t\t\t0x29, 0x6c, 0x3c, 0x9f, 0xde, 0xf9, 0xde, 0x5f,\n\t\t\t\t\t0xef, 0x24, 0xaa, 0x76, 0x9e, 0x2d, 0xbb, 0x8b,\n\t\t\t\t\t0x49, 0xf2, 0x7f, 0xe5, 0xb5, 0x62, 0x5a, 0x5f,\n\t\t\t\t\t0xcb, 0x6b, 0xfe, 0xa3, 0xfe, 0x5a, 0xd1, 0xe5,\n\t\t\t\t\t0x45, 0xff,\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\tVersion:     2,\n\t\t\t\t\tPayloadType: 26,\n\t\t\t\t\tMarker:      true,\n\t\t\t\t},\n\t\t\t\tPayload: []byte{\n\t\t\t\t\t0x00, 0x00, 0x05, 0x1e, 0x01, 0xff, 0xf0, 0x87,\n\t\t\t\t\t0x00, 0x2c, 0x29, 0x0f, 0xd9, 0x97, 0xff, 0x00,\n\t\t\t\t\t0xb6, 0x65, 0xba, 0x82, 0x5f, 0xb7, 0x4d, 0xe6,\n\t\t\t\t\t0xcd, 0xff, 0x00, 0x2c, 0xe2, 0xae, 0xa7, 0x49,\n\t\t\t\t\t0xd5, 0x3f, 0xe2, 0x9a, 0xf3, 0xa7, 0xff, 0x00,\n\t\t\t\t\t0x5d, 0x15, 0x70, 0xb4, 0x43, 0xfb, 0xdf, 0xf5,\n\t\t\t\t\t0xf3, 0x4b, 0xff, 0x00, 0x5c, 0xe8, 0xf6, 0x83,\n\t\t\t\t\t0xf6, 0x67, 0xa1, 0x68, 0x7a, 0xcc, 0x5a, 0xcf,\n\t\t\t\t\t0xfd, 0x32, 0x9b, 0xfe, 0x79, 0xd5, 0xeb, 0xbb,\n\t\t\t\t\t0xab, 0x4b, 0x08, 0x3f, 0xd3, 0xab, 0xcd, 0x66,\n\t\t\t\t\t0xba, 0xfb, 0x57, 0xfd, 0x32, 0x9a, 0x2a, 0x96,\n\t\t\t\t\t0xd2, 0xff, 0x00, 0xcd, 0xbf, 0xf3, 0xb5, 0x49,\n\t\t\t\t\t0xbc, 0xdf, 0x2b, 0xfe, 0x59, 0xd3, 0x27, 0xd9,\n\t\t\t\t\t0x9a, 0x30, 0xdd, 0x7d, 0xab, 0x5e, 0xfb, 0x1d,\n\t\t\t\t\t0x8c, 0xde, 0x55, 0x9c, 0xb2, 0x79, 0x95, 0xd3,\n\t\t\t\t\t0x6a, 0x3a, 0xa7, 0xd9, 0x75, 0xdb, 0x5b, 0x39,\n\t\t\t\t\t0xff, 0x00, 0xd4, 0xcb, 0xff, 0x00, 0x2d, 0x2b,\n\t\t\t\t\t0xcf, 0xbe, 0xdf, 0xe5, 0x6a, 0xdf, 0x6c, 0x83,\n\t\t\t\t\t0xfe, 0x59, 0x54, 0xda, 0xb6, 0xb3, 0x2e, 0xb3,\n\t\t\t\t\t0x7f, 0xe7, 0x7f, 0xaa, 0xff, 0xff, 0xd9,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tdecoded: unit.PayloadMJPEG{\n\t\t\t0xff, 0xd8, 0xff, 0xdb, 0x00, 0x84, 0x00, 0x0d,\n\t\t\t0x09, 0x0a, 0x0b, 0x0a, 0x08, 0x0d, 0x0b, 0x0a,\n\t\t\t0x0b, 0x0e, 0x0e, 0x0d, 0x0f, 0x13, 0x20, 0x15,\n\t\t\t0x13, 0x12, 0x12, 0x13, 0x27, 0x1c, 0x1e, 0x17,\n\t\t\t0x20, 0x2e, 0x29, 0x31, 0x30, 0x2e, 0x29, 0x2d,\n\t\t\t0x2c, 0x33, 0x3a, 0x4a, 0x3e, 0x33, 0x36, 0x46,\n\t\t\t0x37, 0x2c, 0x2d, 0x40, 0x57, 0x41, 0x46, 0x4c,\n\t\t\t0x4e, 0x52, 0x53, 0x52, 0x32, 0x3e, 0x5a, 0x61,\n\t\t\t0x5a, 0x50, 0x60, 0x4a, 0x51, 0x52, 0x4f, 0x01,\n\t\t\t0x0e, 0x0e, 0x0e, 0x13, 0x11, 0x13, 0x26, 0x15,\n\t\t\t0x15, 0x26, 0x4f, 0x35, 0x2d, 0x35, 0x4f, 0x4f,\n\t\t\t0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f,\n\t\t\t0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f,\n\t\t\t0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f,\n\t\t\t0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f,\n\t\t\t0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f,\n\t\t\t0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f, 0x4f,\n\t\t\t0xff, 0xc0, 0x00, 0x11, 0x08, 0x04, 0x38, 0x07,\n\t\t\t0x80, 0x03, 0x00, 0x22, 0x00, 0x01, 0x11, 0x01,\n\t\t\t0x02, 0x11, 0x01, 0xff, 0xc4, 0x00, 0x1f, 0x00,\n\t\t\t0x00, 0x01, 0x05, 0x01, 0x01, 0x01, 0x01, 0x01,\n\t\t\t0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,\n\t\t\t0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,\n\t\t\t0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5,\n\t\t\t0x10, 0x00, 0x02, 0x01, 0x03, 0x03, 0x02, 0x04,\n\t\t\t0x03, 0x05, 0x05, 0x04, 0x04, 0x00, 0x00, 0x01,\n\t\t\t0x7d, 0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05,\n\t\t\t0x12, 0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61,\n\t\t\t0x07, 0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1,\n\t\t\t0x08, 0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1,\n\t\t\t0xf0, 0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a,\n\t\t\t0x16, 0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27,\n\t\t\t0x28, 0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38,\n\t\t\t0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48,\n\t\t\t0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58,\n\t\t\t0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68,\n\t\t\t0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78,\n\t\t\t0x79, 0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88,\n\t\t\t0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97,\n\t\t\t0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6,\n\t\t\t0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5,\n\t\t\t0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4,\n\t\t\t0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3,\n\t\t\t0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1,\n\t\t\t0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9,\n\t\t\t0xea, 0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7,\n\t\t\t0xf8, 0xf9, 0xfa, 0xff, 0xc4, 0x00, 0x1f, 0x01,\n\t\t\t0x00, 0x03, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01,\n\t\t\t0x01, 0x01, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00,\n\t\t\t0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,\n\t\t\t0x08, 0x09, 0x0a, 0x0b, 0xff, 0xc4, 0x00, 0xb5,\n\t\t\t0x11, 0x00, 0x02, 0x01, 0x02, 0x04, 0x04, 0x03,\n\t\t\t0x04, 0x07, 0x05, 0x04, 0x04, 0x00, 0x01, 0x02,\n\t\t\t0x77, 0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05,\n\t\t\t0x21, 0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61,\n\t\t\t0x71, 0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42,\n\t\t\t0x91, 0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52,\n\t\t\t0xf0, 0x15, 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24,\n\t\t\t0x34, 0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a,\n\t\t\t0x26, 0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37,\n\t\t\t0x38, 0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47,\n\t\t\t0x48, 0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57,\n\t\t\t0x58, 0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67,\n\t\t\t0x68, 0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77,\n\t\t\t0x78, 0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86,\n\t\t\t0x87, 0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95,\n\t\t\t0x96, 0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4,\n\t\t\t0xa5, 0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3,\n\t\t\t0xb4, 0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2,\n\t\t\t0xc3, 0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca,\n\t\t\t0xd2, 0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9,\n\t\t\t0xda, 0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8,\n\t\t\t0xe9, 0xea, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7,\n\t\t\t0xf8, 0xf9, 0xfa, 0xff, 0xda, 0x00, 0x0c, 0x03,\n\t\t\t0x00, 0x00, 0x01, 0x11, 0x02, 0x11, 0x00, 0x3f,\n\t\t\t0x00, 0x92, 0x8a, 0x28, 0xaf, 0x54, 0xf2, 0x42,\n\t\t\t0x8a, 0x28, 0xa0, 0x02, 0x96, 0x92, 0x96, 0x80,\n\t\t\t0x0a, 0x4a, 0x75, 0x25, 0x02, 0x12, 0x8a, 0x5a,\n\t\t\t0x28, 0x18, 0x94, 0x52, 0xd1, 0x40, 0x09, 0x45,\n\t\t\t0x2d, 0x14, 0x08, 0x29, 0x69, 0x29, 0x68, 0x00,\n\t\t\t0xa5, 0xa4, 0xa5, 0xa0, 0x02, 0x8a, 0x28, 0xa0,\n\t\t\t0x02, 0x8a, 0x28, 0xa0, 0x04, 0xa5, 0xa2, 0x8a,\n\t\t\t0x00, 0x5a, 0x28, 0xa2, 0x80, 0x0a, 0x28, 0xa2,\n\t\t\t0x80, 0x0a, 0x28, 0xa2, 0x80, 0x12, 0x8a, 0x5a,\n\t\t\t0x28, 0x24, 0x29, 0x69, 0x29, 0x68, 0x00, 0xa2,\n\t\t\t0x8a, 0x28, 0x00, 0xa2, 0x8a, 0x28, 0x00, 0xa2,\n\t\t\t0x8a, 0x28, 0x00, 0xa2, 0x8a, 0x28, 0x00, 0xa2,\n\t\t\t0x8a, 0x28, 0x00, 0xa2, 0x8a, 0x28, 0x00, 0xa2,\n\t\t\t0x8a, 0x28, 0x00, 0xa2, 0x8a, 0x28, 0x00, 0xa2,\n\t\t\t0x8a, 0x28, 0x00, 0xa2, 0x8a, 0x28, 0x00, 0xa2,\n\t\t\t0x8a, 0x28, 0x00, 0xa4, 0xa5, 0xa4, 0xa0, 0x02,\n\t\t\t0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02,\n\t\t\t0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02,\n\t\t\t0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02,\n\t\t\t0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02,\n\t\t\t0x96, 0x92, 0x96, 0x80, 0x0a, 0x28, 0xa2, 0x80,\n\t\t\t0x0a, 0x28, 0xa2, 0x80, 0x0a, 0x28, 0xa2, 0x80,\n\t\t\t0x0a, 0x28, 0xa2, 0x80, 0x0a, 0x28, 0xa2, 0x80,\n\t\t\t0x0a, 0x28, 0xa2, 0x80, 0x0a, 0x28, 0xa2, 0x80,\n\t\t\t0x0a, 0x28, 0xa2, 0x80, 0x0a, 0x28, 0xa2, 0x81,\n\t\t\t0x85, 0x14, 0x51, 0x40, 0x05, 0x14, 0x51, 0x40,\n\t\t\t0x05, 0x14, 0x51, 0x40, 0x05, 0x14, 0x52, 0xd0,\n\t\t\t0x01, 0x45, 0x14, 0x50, 0x01, 0x45, 0x14, 0x50,\n\t\t\t0x01, 0x45, 0x14, 0x50, 0x01, 0x45, 0x2d, 0x14,\n\t\t\t0x00, 0x94, 0xb4, 0x51, 0x40, 0x05, 0x14, 0x52,\n\t\t\t0xd0, 0x02, 0x51, 0x4b, 0x45, 0x00, 0x25, 0x2d,\n\t\t\t0x14, 0x50, 0x01, 0x45, 0x14, 0x50, 0x01, 0x45,\n\t\t\t0x14, 0x50, 0x20, 0xa5, 0xa4, 0xa5, 0xa0, 0x02,\n\t\t\t0x8a, 0x28, 0xa0, 0x02, 0x8a, 0x28, 0xa0, 0x02,\n\t\t\t0x8a, 0x5a, 0x28, 0x18, 0x94, 0xb4, 0x51, 0x40,\n\t\t\t0xc2, 0x8a, 0x28, 0xa0, 0x05, 0xa2, 0x92, 0x9d,\n\t\t\t0x40, 0x05, 0x14, 0x51, 0x48, 0x02, 0x8a, 0x28,\n\t\t\t0xa4, 0x01, 0x4b, 0x49, 0x4b, 0x40, 0x05, 0x14,\n\t\t\t0x51, 0x40, 0x05, 0x14, 0xb4, 0x50, 0x02, 0x51,\n\t\t\t0x4b, 0x45, 0x00, 0x25, 0x2d, 0x14, 0x50, 0x03,\n\t\t\t0xa8, 0xa2, 0x8a, 0x00, 0x28, 0xa2, 0x8a, 0x00,\n\t\t\t0x5a, 0x29, 0x29, 0x68, 0x00, 0xa2, 0x8a, 0x28,\n\t\t\t0x00, 0xa2, 0x96, 0x8a, 0x06, 0x25, 0x14, 0xb4,\n\t\t\t0x50, 0x01, 0x45, 0x14, 0x50, 0x02, 0xd2, 0xd2,\n\t\t\t0x52, 0xd0, 0x20, 0xa2, 0x8a, 0x28, 0x01, 0x68,\n\t\t\t0xa2, 0x8a, 0x40, 0x14, 0x51, 0x45, 0x30, 0x0a,\n\t\t\t0x5a, 0x4a, 0x5a, 0x06, 0x14, 0x51, 0x45, 0x02,\n\t\t\t0x0a, 0x28, 0xa5, 0xa0, 0x62, 0x51, 0x4b, 0x45,\n\t\t\t0x00, 0x2d, 0x14, 0x51, 0x48, 0x02, 0x8a, 0x28,\n\t\t\t0xa0, 0x61, 0x45, 0x14, 0x50, 0x03, 0xa8, 0xa2,\n\t\t\t0x8a, 0x06, 0x2d, 0x14, 0x51, 0x48, 0x02, 0x8a,\n\t\t\t0x28, 0xa4, 0x30, 0xa2, 0x8a, 0x2a, 0x80, 0x28,\n\t\t\t0xa2, 0x8a, 0x00, 0x28, 0xa2, 0x8a, 0x92, 0x45,\n\t\t\t0xa5, 0xa2, 0x96, 0x82, 0x82, 0x8a, 0x28, 0xa0,\n\t\t\t0x02, 0x8a, 0x28, 0xa0, 0x05, 0xa2, 0x8a, 0x29,\n\t\t\t0x80, 0x52, 0xd2, 0x52, 0xd0, 0x01, 0x45, 0x14,\n\t\t\t0x50, 0x01, 0x4e, 0xa2, 0x8a, 0x43, 0x0a, 0x28,\n\t\t\t0xa2, 0x80, 0x0a, 0x28, 0xa4, 0xa4, 0x31, 0x68,\n\t\t\t0xa4, 0xf3, 0x62, 0xff, 0x00, 0x9e, 0xd1, 0x7e,\n\t\t\t0xea, 0x9f, 0xfe, 0xb6, 0xa4, 0x62, 0x52, 0x53,\n\t\t\t0xa9, 0x28, 0x01, 0x28, 0xa2, 0x6f, 0xdd, 0x7f,\n\t\t\t0xaf, 0xa5, 0xa0, 0x62, 0x51, 0x4b, 0x45, 0x00,\n\t\t\t0x25, 0x14, 0xbf, 0xba, 0xff, 0x00, 0xae, 0xb4,\n\t\t\t0xea, 0x60, 0x36, 0x9d, 0x49, 0x49, 0x34, 0xb1,\n\t\t\t0x45, 0xfe, 0xbe, 0x6f, 0x2a, 0x98, 0x0f, 0xa2,\n\t\t\t0xb9, 0xbd, 0x0f, 0x59, 0x97, 0x54, 0xf1, 0x2d,\n\t\t\t0xd7, 0xfc, 0xf9, 0xd7, 0x49, 0x52, 0x30, 0xac,\n\t\t\t0x7d, 0x5b, 0x54, 0xd5, 0x74, 0xbf, 0xdf, 0x41,\n\t\t\t0x67, 0x15, 0xd4, 0x35, 0x63, 0x56, 0xd5, 0x22,\n\t\t\t0xd2, 0xe0, 0xf3, 0xbc, 0x99, 0x65, 0xae, 0x4b,\n\t\t\t0x51, 0xf1, 0x44, 0x52, 0xff, 0x00, 0xc7, 0x97,\n\t\t\t0x9b, 0xe4, 0xd0, 0x69, 0x4c, 0x76, 0xa3, 0xe2,\n\t\t\t0xd9, 0xa5, 0x82, 0x29, 0xb4, 0xbf, 0xdd, 0x79,\n\t\t\t0xbf, 0xeb, 0x22, 0xaa, 0x7e, 0x1e, 0xd5, 0x3e,\n\t\t\t0xc1, 0x3f, 0xfa, 0x9f, 0xfa, 0xe9, 0x25, 0x61,\n\t\t\t0xd6, 0x9e, 0x87, 0xf6, 0xbf, 0xed, 0x68, 0xbc,\n\t\t\t0x8a, 0x82, 0xcf, 0x4e, 0x86, 0x5f, 0x36, 0x0a,\n\t\t\t0x92, 0x9b, 0xff, 0x00, 0x5d, 0xeb, 0x3a, 0x6d,\n\t\t\t0x53, 0xfd, 0x3f, 0xec, 0x76, 0x3f, 0xbd, 0xff,\n\t\t\t0x00, 0xa6, 0xbf, 0xc1, 0x54, 0x66, 0x69, 0x51,\n\t\t\t0x50, 0x4b, 0x2c, 0x51, 0x7e, 0xe7, 0xce, 0xf3,\n\t\t\t0x66, 0xff, 0x00, 0xa6, 0x75, 0x35, 0x00, 0x2d,\n\t\t\t0x65, 0x6a, 0xde, 0x23, 0xd3, 0xec, 0x3f, 0xe5,\n\t\t\t0xb7, 0x9b, 0x34, 0x5f, 0xf2, 0xce, 0xb4, 0x7e,\n\t\t\t0xd5, 0x69, 0x2f, 0xfc, 0xbe, 0x45, 0x5e, 0x7b,\n\t\t\t0xe2, 0x7d, 0x07, 0xec, 0x1e, 0x6e, 0xa5, 0xf6,\n\t\t\t0xc8, 0xa5, 0xf3, 0x6a, 0xc0, 0xdf, 0xa2, 0x9d,\n\t\t\t0x45, 0x6c, 0x72, 0x0d, 0xa5, 0xa5, 0xa2, 0x80,\n\t\t\t0x12, 0x96, 0x8a, 0x28, 0x10, 0x51, 0x4b, 0x45,\n\t\t\t0x00, 0x25, 0x14, 0xb4, 0x50, 0x02, 0x51, 0x4b,\n\t\t\t0x45, 0x03, 0x12, 0x8a, 0x5a, 0x28, 0x10, 0x94,\n\t\t\t0xb4, 0x51, 0x40, 0x05, 0x14, 0x51, 0x40, 0x05,\n\t\t\t0x14, 0xb4, 0x50, 0x02, 0x51, 0x4b, 0x45, 0x00,\n\t\t\t0x14, 0x51, 0x4b, 0x40, 0x84, 0xa2, 0x8a, 0x28,\n\t\t\t0x00, 0xa2, 0x96, 0x8a, 0x00, 0x4a, 0x29, 0x68,\n\t\t\t0xa0, 0x04, 0xa2, 0x96, 0x8a, 0x00, 0x4a, 0x29,\n\t\t\t0x68, 0xa0, 0x41, 0x45, 0x14, 0x50, 0x01, 0x45,\n\t\t\t0x14, 0x50, 0x01, 0x45, 0x14, 0x50, 0x01, 0x45,\n\t\t\t0x14, 0x50, 0x01, 0x45, 0x14, 0x50, 0x01, 0x45,\n\t\t\t0x14, 0x50, 0x01, 0x45, 0x14, 0x50, 0x01, 0x45,\n\t\t\t0x14, 0x50, 0x01, 0x45, 0x14, 0x50, 0x01, 0x45,\n\t\t\t0x14, 0x50, 0x01, 0x45, 0x14, 0x50, 0x01, 0x49,\n\t\t\t0x4b, 0x45, 0x00, 0x25, 0x14, 0xb4, 0x50, 0x01,\n\t\t\t0x45, 0x14, 0x50, 0x30, 0xa4, 0xa5, 0xa2, 0x81,\n\t\t\t0x09, 0x45, 0x2d, 0x14, 0x00, 0x94, 0x52, 0xd1,\n\t\t\t0x40, 0x09, 0x45, 0x2d, 0x14, 0x00, 0x94, 0x52,\n\t\t\t0xd1, 0x40, 0x05, 0x14, 0xb4, 0x94, 0x0c, 0x28,\n\t\t\t0xa2, 0x8a, 0x04, 0x14, 0x51, 0x45, 0x00, 0x14,\n\t\t\t0x51, 0x45, 0x00, 0x14, 0x51, 0x45, 0x00, 0x14,\n\t\t\t0x51, 0x45, 0x00, 0x14, 0x52, 0xd1, 0x40, 0xc4,\n\t\t\t0xa2, 0x96, 0x8a, 0x04, 0x25, 0x14, 0xb4, 0x50,\n\t\t\t0x31, 0x28, 0xa5, 0xa2, 0x80, 0x12, 0x8a, 0x5a,\n\t\t\t0x4a, 0x00, 0x5a, 0x28, 0xa2, 0x80, 0x0a, 0x29,\n\t\t\t0x68, 0xa0, 0x04, 0xa2, 0x96, 0x8a, 0x00, 0x4a,\n\t\t\t0x29, 0x68, 0xa0, 0x04, 0xa2, 0x9d, 0x45, 0x03,\n\t\t\t0x1b, 0x45, 0x3a, 0x8a, 0x00, 0x6d, 0x14, 0xea,\n\t\t\t0x28, 0x10, 0x94, 0x52, 0xd1, 0x40, 0x09, 0x4b,\n\t\t\t0x45, 0x14, 0x08, 0x28, 0xa2, 0x8a, 0x06, 0x14,\n\t\t\t0x52, 0xd1, 0x40, 0x09, 0x45, 0x2d, 0x2d, 0x00,\n\t\t\t0x25, 0x14, 0xb4, 0x50, 0x21, 0x28, 0xa5, 0xa2,\n\t\t\t0x81, 0x89, 0x4b, 0x45, 0x14, 0x00, 0x51, 0x45,\n\t\t\t0x14, 0x00, 0x51, 0x4b, 0x45, 0x00, 0x25, 0x14,\n\t\t\t0xb4, 0x50, 0x01, 0x45, 0x2d, 0x14, 0x0c, 0x28,\n\t\t\t0xa2, 0x8a, 0x00, 0x4a, 0x5a, 0x5a, 0x28, 0x01,\n\t\t\t0x28, 0xa5, 0xa2, 0x90, 0x05, 0x14, 0x51, 0x40,\n\t\t\t0x05, 0x14, 0xb4, 0x52, 0x01, 0x28, 0xa5, 0xa2,\n\t\t\t0x80, 0x0a, 0x28, 0xa7, 0x50, 0x03, 0x68, 0xa7,\n\t\t\t0x51, 0x40, 0x0d, 0xa7, 0x51, 0x45, 0x00, 0x14,\n\t\t\t0x51, 0x4b, 0x40, 0x09, 0x45, 0x2d, 0x2d, 0x00,\n\t\t\t0x25, 0x14, 0xb4, 0x50, 0x02, 0x51, 0x4b, 0x45,\n\t\t\t0x00, 0x14, 0x51, 0x45, 0x00, 0x14, 0x51, 0x45,\n\t\t\t0x00, 0x14, 0xb4, 0x53, 0xa8, 0x18, 0xda, 0x5a,\n\t\t\t0x5a, 0x28, 0x10, 0x94, 0xb4, 0x51, 0x40, 0xc2,\n\t\t\t0x8a, 0x5a, 0x29, 0x00, 0x94, 0x52, 0xd1, 0x40,\n\t\t\t0x05, 0x14, 0x51, 0x4c, 0x02, 0x96, 0x8a, 0x29,\n\t\t\t0x00, 0x52, 0xd2, 0x52, 0xd0, 0x01, 0x45, 0x14,\n\t\t\t0x50, 0x02, 0xd1, 0x45, 0x14, 0x0c, 0x28, 0xa2,\n\t\t\t0x96, 0x80, 0x0a, 0x28, 0xa2, 0x81, 0x85, 0x2d,\n\t\t\t0x14, 0x50, 0x01, 0x45, 0x14, 0xb4, 0x80, 0x4a,\n\t\t\t0x29, 0x68, 0xa0, 0x62, 0xd1, 0x45, 0x14, 0xc0,\n\t\t\t0x28, 0xa5, 0xa2, 0x90, 0x0d, 0xa5, 0xa5, 0xa2,\n\t\t\t0x80, 0x0a, 0x5a, 0x28, 0xa4, 0x01, 0x45, 0x14,\n\t\t\t0xb4, 0x00, 0x94, 0x52, 0xd1, 0x40, 0x05, 0x14,\n\t\t\t0x51, 0x4c, 0x02, 0x8a, 0x29, 0x68, 0x01, 0x28,\n\t\t\t0xa5, 0xa2, 0x80, 0x16, 0x8a, 0x28, 0xa4, 0x30,\n\t\t\t0xa7, 0x55, 0x7b, 0xbb, 0x5f, 0xb7, 0xc1, 0xe4,\n\t\t\t0xff, 0x00, 0xaa, 0xac, 0x49, 0xb4, 0x6d, 0x57,\n\t\t\t0x4b, 0xf3, 0x66, 0xd2, 0xef, 0x3c, 0xd8, 0x69,\n\t\t\t0x17, 0x4c, 0xe9, 0x2b, 0x07, 0xc4, 0x32, 0xcb,\n\t\t\t0x75, 0xa4, 0xff, 0x00, 0xc4, 0xae, 0x68, 0xbf,\n\t\t\t0x75, 0xfe, 0xb2, 0xb9, 0x09, 0xb5, 0x9d, 0x42,\n\t\t\t0x5f, 0x36, 0xcf, 0xce, 0xff, 0x00, 0x5b, 0x56,\n\t\t\t0x3c, 0x3d, 0x6b, 0xfd, 0xa9, 0xab, 0x4b, 0x67,\n\t\t\t0x3f, 0xfc, 0xf3, 0xff, 0x00, 0x96, 0x74, 0x8d,\n\t\t\t0xbd, 0x99, 0x9b, 0x0d, 0xfc, 0xb2, 0xf9, 0xbf,\n\t\t\t0xeb, 0x7f, 0x7b, 0x5a, 0xf0, 0xf8, 0x8e, 0xee,\n\t\t\t0x2d, 0x27, 0xec, 0x70, 0x7f, 0xdf, 0xca, 0x66,\n\t\t\t0xa3, 0xe1, 0x7f, 0xb0, 0x79, 0xbe, 0x46, 0xa5,\n\t\t\t0x17, 0xfd, 0x73, 0x92, 0xb0, 0xe1, 0xa8, 0x0f,\n\t\t\t0x66, 0x76, 0x90, 0xf8, 0xa2, 0x29, 0x6c, 0x3c,\n\t\t\t0x9f, 0xde, 0xf9, 0xde, 0x5f, 0xef, 0x24, 0xaa,\n\t\t\t0x76, 0x9e, 0x2d, 0xbb, 0x8b, 0x49, 0xf2, 0x7f,\n\t\t\t0xe5, 0xb5, 0x62, 0x5a, 0x5f, 0xcb, 0x6b, 0xfe,\n\t\t\t0xa3, 0xfe, 0x5a, 0xd1, 0xe5, 0x45, 0xff, 0x00,\n\t\t\t0x2c, 0x29, 0x0f, 0xd9, 0x97, 0xff, 0x00, 0xb6,\n\t\t\t0x65, 0xba, 0x82, 0x5f, 0xb7, 0x4d, 0xe6, 0xcd,\n\t\t\t0xff, 0x00, 0x2c, 0xe2, 0xae, 0xa7, 0x49, 0xd5,\n\t\t\t0x3f, 0xe2, 0x9a, 0xf3, 0xa7, 0xff, 0x00, 0x5d,\n\t\t\t0x15, 0x70, 0xb4, 0x43, 0xfb, 0xdf, 0xf5, 0xf3,\n\t\t\t0x4b, 0xff, 0x00, 0x5c, 0xe8, 0xf6, 0x83, 0xf6,\n\t\t\t0x67, 0xa1, 0x68, 0x7a, 0xcc, 0x5a, 0xcf, 0xfd,\n\t\t\t0x32, 0x9b, 0xfe, 0x79, 0xd5, 0xeb, 0xbb, 0xab,\n\t\t\t0x4b, 0x08, 0x3f, 0xd3, 0xab, 0xcd, 0x66, 0xba,\n\t\t\t0xfb, 0x57, 0xfd, 0x32, 0x9a, 0x2a, 0x96, 0xd2,\n\t\t\t0xff, 0x00, 0xcd, 0xbf, 0xf3, 0xb5, 0x49, 0xbc,\n\t\t\t0xdf, 0x2b, 0xfe, 0x59, 0xd3, 0x27, 0xd9, 0x9a,\n\t\t\t0x30, 0xdd, 0x7d, 0xab, 0x5e, 0xfb, 0x1d, 0x8c,\n\t\t\t0xde, 0x55, 0x9c, 0xb2, 0x79, 0x95, 0xd3, 0x6a,\n\t\t\t0x3a, 0xa7, 0xd9, 0x75, 0xdb, 0x5b, 0x39, 0xff,\n\t\t\t0x00, 0xd4, 0xcb, 0xff, 0x00, 0x2d, 0x2b, 0xcf,\n\t\t\t0xbe, 0xdf, 0xe5, 0x6a, 0xdf, 0x6c, 0x83, 0xfe,\n\t\t\t0x59, 0x54, 0xda, 0xb6, 0xb3, 0x2e, 0xb3, 0x7f,\n\t\t\t0xe7, 0x7f, 0xaa, 0xff, 0xff, 0xd9,\n\t\t},\n\t},\n\t{\n\t\tname: \"mpeg4audio\",\n\t\tformat: &format.MPEG4Audio{\n\t\t\tPayloadTyp:       96,\n\t\t\tSizeLength:       13,\n\t\t\tIndexLength:      3,\n\t\t\tIndexDeltaLength: 3,\n\t\t},\n\t\tencoded: []*rtp.Packet{\n\t\t\t{\n\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\tVersion:        2,\n\t\t\t\t\tMarker:         true,\n\t\t\t\t\tPayloadType:    96,\n\t\t\t\t\tSequenceNumber: 123,\n\t\t\t\t\tTimestamp:      45343,\n\t\t\t\t\tSSRC:           563423,\n\t\t\t\t},\n\t\t\t\tPayload: []byte{\n\t\t\t\t\t// AU-headers-length: 16 bits (2 bytes) = 16 bits of headers\n\t\t\t\t\t0x00, 0x10,\n\t\t\t\t\t// AU-header: 13 bits size + 3 bits index\n\t\t\t\t\t// size=4 (13 bits): 0000000000100\n\t\t\t\t\t// index=0 (3 bits): 000\n\t\t\t\t\t// Combined: 0000000000100000 = 0x0020\n\t\t\t\t\t0x00, 0x20,\n\t\t\t\t\t// AU data\n\t\t\t\t\t0x01, 0x02, 0x03, 0x04,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tdecoded: unit.PayloadMPEG4Audio{\n\t\t\t{0x01, 0x02, 0x03, 0x04},\n\t\t},\n\t},\n\t{\n\t\tname: \"opus\",\n\t\tformat: &format.Opus{\n\t\t\tPayloadTyp:   96,\n\t\t\tChannelCount: 2,\n\t\t},\n\t\tencoded: []*rtp.Packet{\n\t\t\t{\n\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\tVersion:        2,\n\t\t\t\t\tMarker:         false,\n\t\t\t\t\tPayloadType:    96,\n\t\t\t\t\tSequenceNumber: 123,\n\t\t\t\t\tTimestamp:      45343,\n\t\t\t\t\tSSRC:           563423,\n\t\t\t\t},\n\t\t\t\tPayload: []byte{0x01, 0x02, 0x03, 0x04},\n\t\t\t},\n\t\t},\n\t\tdecoded: unit.PayloadOpus{\n\t\t\t{0x01, 0x02, 0x03, 0x04},\n\t\t},\n\t},\n\t{\n\t\tname: \"g711\",\n\t\tformat: &format.G711{\n\t\t\tMULaw:        true,\n\t\t\tSampleRate:   8000,\n\t\t\tChannelCount: 1,\n\t\t},\n\t\tencoded: []*rtp.Packet{\n\t\t\t{\n\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\tVersion:        2,\n\t\t\t\t\tMarker:         false,\n\t\t\t\t\tPayloadType:    0,\n\t\t\t\t\tSequenceNumber: 123,\n\t\t\t\t\tTimestamp:      45343,\n\t\t\t\t\tSSRC:           563423,\n\t\t\t\t},\n\t\t\t\tPayload: []byte{0x01, 0x02, 0x03, 0x04},\n\t\t\t},\n\t\t},\n\t\tdecoded: unit.PayloadG711{0x01, 0x02, 0x03, 0x04},\n\t},\n\t{\n\t\tname: \"lpcm\",\n\t\tformat: &format.LPCM{\n\t\t\tPayloadTyp:   96,\n\t\t\tBitDepth:     16,\n\t\t\tSampleRate:   48000,\n\t\t\tChannelCount: 2,\n\t\t},\n\t\tencoded: []*rtp.Packet{\n\t\t\t{\n\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\tVersion:        2,\n\t\t\t\t\tMarker:         false,\n\t\t\t\t\tPayloadType:    96,\n\t\t\t\t\tSequenceNumber: 123,\n\t\t\t\t\tTimestamp:      45343,\n\t\t\t\t\tSSRC:           563423,\n\t\t\t\t},\n\t\t\t\tPayload: []byte{0x01, 0x02, 0x03, 0x04},\n\t\t\t},\n\t\t},\n\t\tdecoded: unit.PayloadLPCM{0x01, 0x02, 0x03, 0x04},\n\t},\n\t{\n\t\tname: \"klv\",\n\t\tformat: &format.KLV{\n\t\t\tPayloadTyp: 96,\n\t\t},\n\t\tencoded: []*rtp.Packet{\n\t\t\t{\n\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\tVersion:        2,\n\t\t\t\t\tMarker:         true, // Marker bit indicates complete KLV unit\n\t\t\t\t\tPayloadType:    96,\n\t\t\t\t\tSequenceNumber: 123,\n\t\t\t\t\tTimestamp:      45343,\n\t\t\t\t\tSSRC:           563423,\n\t\t\t\t},\n\t\t\t\tPayload: []byte{\n\t\t\t\t\t// KLV Universal Label Key (16 bytes) - starts with 0x060e2b34\n\t\t\t\t\t0x06, 0x0e, 0x2b, 0x34, 0x01, 0x01, 0x01, 0x01,\n\t\t\t\t\t0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,\n\t\t\t\t\t// Length (1 byte, short form: 4 bytes of data)\n\t\t\t\t\t0x04,\n\t\t\t\t\t// Value (4 bytes)\n\t\t\t\t\t0x01, 0x02, 0x03, 0x04,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tdecoded: unit.PayloadKLV{\n\t\t\t// Complete KLV unit: Universal Label Key + Length + Value\n\t\t\t0x06, 0x0e, 0x2b, 0x34, 0x01, 0x01, 0x01, 0x01,\n\t\t\t0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08,\n\t\t\t0x04,\n\t\t\t0x01, 0x02, 0x03, 0x04,\n\t\t},\n\t},\n}\n\nfunc TestStreamDecode(t *testing.T) {\n\tfor _, ca := range casesDecodeEncode {\n\t\tt.Run(ca.name, func(t *testing.T) {\n\t\t\tdesc := &description.Session{Medias: []*description.Media{{\n\t\t\t\tFormats: []format.Format{ca.format},\n\t\t\t}}}\n\n\t\t\tstrm := &Stream{\n\t\t\t\tDesc:              desc,\n\t\t\t\tWriteQueueSize:    512,\n\t\t\t\tRTPMaxPayloadSize: 1450,\n\t\t\t\tParent:            &nilLogger{},\n\t\t\t}\n\t\t\terr := strm.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer strm.Close()\n\n\t\t\tsubStream := &SubStream{\n\t\t\t\tStream:        strm,\n\t\t\t\tUseRTPPackets: true,\n\t\t\t}\n\t\t\terr = subStream.Initialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tr := &Reader{}\n\t\t\trecv := make(chan *unit.Unit)\n\n\t\t\tr.OnData(desc.Medias[0], ca.format, func(u *unit.Unit) error {\n\t\t\t\tif !u.NilPayload() {\n\t\t\t\t\trecv <- u\n\t\t\t\t\tclose(recv)\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\t\tstrm.AddReader(r)\n\t\t\tdefer strm.RemoveReader(r)\n\n\t\t\tfor _, pkt := range ca.encoded {\n\t\t\t\tsubStream.WriteUnit(desc.Medias[0], ca.format, &unit.Unit{\n\t\t\t\t\tRTPPackets: []*rtp.Packet{pkt},\n\t\t\t\t})\n\t\t\t}\n\n\t\t\treceived := <-recv\n\t\t\trequire.Equal(t, ca.decoded, received.Payload)\n\t\t})\n\t}\n}\n\nfunc TestStreamEncode(t *testing.T) {\n\tfor _, ca := range casesDecodeEncode {\n\t\tt.Run(ca.name, func(t *testing.T) {\n\t\t\tdesc := &description.Session{Medias: []*description.Media{{\n\t\t\t\tFormats: []format.Format{ca.format},\n\t\t\t}}}\n\n\t\t\tstrm := &Stream{\n\t\t\t\tDesc:              desc,\n\t\t\t\tWriteQueueSize:    512,\n\t\t\t\tRTPMaxPayloadSize: 1450,\n\t\t\t\tParent:            &nilLogger{},\n\t\t\t}\n\t\t\terr := strm.Initialize()\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer strm.Close()\n\n\t\t\tsubStream := &SubStream{\n\t\t\t\tStream:        strm,\n\t\t\t\tUseRTPPackets: false,\n\t\t\t}\n\t\t\terr = subStream.Initialize()\n\t\t\trequire.NoError(t, err)\n\n\t\t\tr := &Reader{}\n\t\t\trecv := make(chan struct{})\n\n\t\t\tr.OnData(desc.Medias[0], ca.format, func(u *unit.Unit) error {\n\t\t\t\tfor i := range min(len(u.RTPPackets), len(ca.encoded)) {\n\t\t\t\t\tu.RTPPackets[i].Timestamp = ca.encoded[i].Timestamp\n\t\t\t\t\tu.RTPPackets[i].SequenceNumber = ca.encoded[i].SequenceNumber\n\t\t\t\t\tu.RTPPackets[i].SSRC = ca.encoded[i].SSRC\n\t\t\t\t}\n\t\t\t\trequire.Equal(t, ca.encoded, u.RTPPackets)\n\t\t\t\tclose(recv)\n\t\t\t\treturn nil\n\t\t\t})\n\n\t\t\tstrm.AddReader(r)\n\t\t\tdefer strm.RemoveReader(r)\n\n\t\t\tsubStream.WriteUnit(desc.Medias[0], ca.format, &unit.Unit{\n\t\t\t\tPayload: ca.decoded,\n\t\t\t})\n\n\t\t\t<-recv\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/stream/sub_stream.go",
    "content": "package stream\n\nimport (\n\t\"fmt\"\n\t\"reflect\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n)\n\n// FormatsToCodecs returns the name of codecs of given formats.\nfunc FormatsToCodecs(formats []format.Format) []string {\n\tret := make([]string, len(formats))\n\tfor i, forma := range formats {\n\t\tret[i] = forma.Codec()\n\t}\n\treturn ret\n}\n\nfunc gatherFormats(medias []*description.Media) []format.Format {\n\tn := 0\n\tfor _, media := range medias {\n\t\tn += len(media.Formats)\n\t}\n\n\tif n == 0 {\n\t\treturn nil\n\t}\n\n\tformats := make([]format.Format, n)\n\tn = 0\n\n\tfor _, media := range medias {\n\t\tn += copy(formats[n:], media.Formats)\n\t}\n\n\treturn formats\n}\n\nfunc mediasToCodecs(medias []*description.Media) []string {\n\treturn FormatsToCodecs(gatherFormats(medias))\n}\n\nfunc formatMPEG4AudioConfig(asc *mpeg4audio.AudioSpecificConfig) string {\n\treturn fmt.Sprintf(\"type=%d, sampleRate=%d, channelCount=%d\",\n\t\tasc.Type, asc.SampleRate, asc.ChannelConfig)\n}\n\nfunc formatG711Config(f *format.G711) string {\n\treturn fmt.Sprintf(\"MULaw=%v, sampleRate=%d, channelCount=%d\",\n\t\tf.MULaw, f.SampleRate, f.ChannelCount)\n}\n\nfunc formatLPCMConfig(f *format.LPCM) string {\n\treturn fmt.Sprintf(\"bitDepth=%d, sampleRate=%d, channelCount=%d\",\n\t\tf.BitDepth, f.SampleRate, f.ChannelCount)\n}\n\nfunc mediasAreCompatible(medias1 []*description.Media, medias2 []*description.Media) error {\n\tif len(medias1) != len(medias2) {\n\t\treturn fmt.Errorf(\"wants to publish %v, but stream expects %v\",\n\t\t\tmediasToCodecs(medias2), mediasToCodecs(medias1))\n\t}\n\n\tfor i := range medias1 {\n\t\tif len(medias1[i].Formats) != len(medias2[i].Formats) {\n\t\t\treturn fmt.Errorf(\"wants to publish %v, but stream expects %v\",\n\t\t\t\tmediasToCodecs(medias2), mediasToCodecs(medias1))\n\t\t}\n\n\t\tfor j := range medias1[i].Formats {\n\t\t\tif reflect.TypeOf(medias1[i].Formats[j]) != reflect.TypeOf(medias2[i].Formats[j]) {\n\t\t\t\treturn fmt.Errorf(\"wants to publish %v, but stream expects %v\",\n\t\t\t\t\tmediasToCodecs(medias2), mediasToCodecs(medias1))\n\t\t\t}\n\t\t}\n\t}\n\n\tfor i := range medias1 {\n\t\tfor j := range medias1[i].Formats {\n\t\t\tswitch format1 := medias1[i].Formats[j].(type) {\n\t\t\tcase *format.MPEG4Audio:\n\t\t\t\tformat2 := medias2[i].Formats[j].(*format.MPEG4Audio)\n\n\t\t\t\tif !reflect.DeepEqual(format1.Config, format2.Config) {\n\t\t\t\t\treturn fmt.Errorf(\"MPEG-4 audio configuration does not match, is %s, but stream expects %s\",\n\t\t\t\t\t\tformatMPEG4AudioConfig(format2.Config), formatMPEG4AudioConfig(format1.Config))\n\t\t\t\t}\n\n\t\t\tcase *format.G711:\n\t\t\t\tformat2 := medias2[i].Formats[j].(*format.G711)\n\n\t\t\t\tif format1.MULaw != format2.MULaw ||\n\t\t\t\t\tformat1.SampleRate != format2.SampleRate ||\n\t\t\t\t\tformat1.ChannelCount != format2.ChannelCount {\n\t\t\t\t\treturn fmt.Errorf(\"G711 configuration does not match, is %s, but stream expects %s\",\n\t\t\t\t\t\tformatG711Config(format2), formatG711Config(format1))\n\t\t\t\t}\n\n\t\t\tcase *format.LPCM:\n\t\t\t\tformat2 := medias2[i].Formats[j].(*format.LPCM)\n\n\t\t\t\tif format1.BitDepth != format2.BitDepth ||\n\t\t\t\t\tformat1.SampleRate != format2.SampleRate ||\n\t\t\t\t\tformat1.ChannelCount != format2.ChannelCount {\n\t\t\t\t\treturn fmt.Errorf(\"LPCM configuration does not match, is %s, but stream expects %s\",\n\t\t\t\t\t\tformatLPCMConfig(format2), formatLPCMConfig(format1))\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// SubStream is a Stream without interruptions.\ntype SubStream struct {\n\tStream        *Stream\n\tCurDesc       *description.Session\n\tUseRTPPackets bool\n\n\tmedias map[*description.Media]*subStreamMedia\n}\n\n// Initialize initializes the SubStream.\nfunc (ss *SubStream) Initialize() error {\n\tif !ss.Stream.AlwaysAvailable {\n\t\tif ss.Stream.subStream != nil {\n\t\t\tpanic(\"should not happen\")\n\t\t}\n\n\t\tif ss.CurDesc != nil {\n\t\t\tpanic(\"should not happen\")\n\t\t}\n\t} else {\n\t\tif ss.CurDesc == nil {\n\t\t\tpanic(\"should not happen\")\n\t\t}\n\n\t\terr := mediasAreCompatible(ss.Stream.Desc.Medias, ss.CurDesc.Medias)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif !ss.Stream.AlwaysAvailable {\n\t\tss.CurDesc = ss.Stream.Desc\n\t}\n\n\tss.medias = make(map[*description.Media]*subStreamMedia)\n\n\tfor i, curMedia := range ss.CurDesc.Medias {\n\t\tmedia := ss.Stream.Desc.Medias[i]\n\n\t\tssm := &subStreamMedia{\n\t\t\tcurMedia:      curMedia,\n\t\t\tstreamMedia:   ss.Stream.medias[media],\n\t\t\tuseRTPPackets: ss.UseRTPPackets,\n\t\t}\n\t\terr := ssm.initialize()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tss.medias[curMedia] = ssm\n\t}\n\n\tif ss.Stream.AlwaysAvailable {\n\t\tif ss.Stream.offlineSubStream != nil {\n\t\t\tss.Stream.Parent.Log(logger.Info, \"stream is online\")\n\n\t\t\t// wait for the entire duration of the last sample of the offline sub stream\n\t\t\t// to minimize errors in clients.\n\t\t\t// TODO: it would be better in the future to wait for the last sample\n\t\t\t// of normal sub streams as well (this is currently impossible because\n\t\t\t// we don't know the duration of their samples).\n\t\t\tss.Stream.offlineSubStream.close(true)\n\t\t\tss.Stream.offlineSubStream = nil\n\t\t}\n\t}\n\n\tss.Stream.mutex.Lock()\n\tss.Stream.subStream = ss\n\tss.Stream.mutex.Unlock()\n\n\tfor _, ssm := range ss.medias {\n\t\tfor _, ssf := range ssm.formats {\n\t\t\tssf.initialize2(ss.Stream.firstTimeReceived, ss.Stream.lastPTS, ss.Stream.lastSystemTime)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// WriteUnit writes a Unit.\nfunc (ss *SubStream) WriteUnit(medi *description.Media, forma format.Format, u *unit.Unit) {\n\tss.Stream.mutex.RLock()\n\tdefer ss.Stream.mutex.RUnlock()\n\n\tif ss.Stream.subStream != ss {\n\t\treturn\n\t}\n\n\tssm := ss.medias[medi]\n\tssf := ssm.formats[forma]\n\n\tssf.writeUnit(u)\n}\n"
  },
  {
    "path": "internal/stream/sub_stream_format.go",
    "content": "package stream\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n)\n\ntype subStreamFormat struct {\n\tcurFormat     format.Format\n\tstreamFormat  *streamFormat\n\tuseRTPPackets bool\n\n\trtpDecoder        rtpDecoder\n\ttempRTPEncoder    rtpEncoder\n\ttempRTPTimeOffset uint32\n}\n\nfunc (ssf *subStreamFormat) initialize() error {\n\tif ssf.useRTPPackets {\n\t\tvar err error\n\t\tssf.rtpDecoder, err = newRTPDecoder(ssf.curFormat)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tif ssf.streamFormat.rtpEncoder == nil && (!ssf.useRTPPackets || ssf.streamFormat.alwaysAvailable) {\n\t\tvar err error\n\t\tssf.tempRTPEncoder, err = newRTPEncoder(ssf.curFormat, ssf.streamFormat.rtpMaxPayloadSize, nil, nil)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tssf.tempRTPTimeOffset, err = randUint32()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (ssf *subStreamFormat) initialize2(firstTimeReceived bool, lastPTS time.Duration, lastSystemTime time.Time) {\n\tif ssf.tempRTPEncoder != nil {\n\t\tif ssf.streamFormat.rtpEncoder == nil {\n\t\t\tssf.streamFormat.rtpEncoder = ssf.tempRTPEncoder\n\t\t\tssf.streamFormat.rtpTimeOffset = ssf.tempRTPTimeOffset\n\t\t}\n\n\t\tssf.tempRTPEncoder = nil\n\t\tssf.tempRTPTimeOffset = 0\n\t}\n\n\tif ssf.streamFormat.alwaysAvailable {\n\t\tif firstTimeReceived {\n\t\t\tptsOffsetGo := lastPTS + time.Since(lastSystemTime)\n\t\t\tssf.streamFormat.ptsOffset = multiplyAndDivide(int64(ptsOffsetGo),\n\t\t\t\tint64(ssf.streamFormat.format.ClockRate()), int64(time.Second))\n\t\t}\n\n\t\tswitch curFormat := ssf.curFormat.(type) {\n\t\tcase *format.H265:\n\t\t\tvps, sps, pps := curFormat.SafeParams()\n\n\t\t\tif vps != nil && sps != nil && pps != nil {\n\t\t\t\tssf.writeUnit(&unit.Unit{\n\t\t\t\t\tPTS:        0,\n\t\t\t\t\tNTP:        time.Time{},\n\t\t\t\t\tRTPPackets: nil,\n\t\t\t\t\tPayload:    unit.PayloadH265([][]byte{vps, sps, pps}),\n\t\t\t\t})\n\t\t\t}\n\n\t\tcase *format.H264:\n\t\t\tsps, pps := curFormat.SafeParams()\n\n\t\t\tif sps != nil && pps != nil {\n\t\t\t\tssf.writeUnit(&unit.Unit{\n\t\t\t\t\tPTS:        0,\n\t\t\t\t\tNTP:        time.Time{},\n\t\t\t\t\tRTPPackets: nil,\n\t\t\t\t\tPayload:    unit.PayloadH264([][]byte{sps, pps}),\n\t\t\t\t})\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (ssf *subStreamFormat) writeUnit(u *unit.Unit) {\n\terr := ssf.writeUnitInner(u)\n\tif err != nil {\n\t\tssf.streamFormat.inboundFramesInError.Add(err)\n\t\treturn\n\t}\n}\n\nfunc (ssf *subStreamFormat) writeUnitInner(u *unit.Unit) error {\n\tif ssf.streamFormat.alwaysAvailable {\n\t\tu.PTS += ssf.streamFormat.ptsOffset\n\n\t\tssf.streamFormat.updateLastTime(\n\t\t\tmultiplyAndDivide2(time.Duration(u.PTS),\n\t\t\t\ttime.Second, time.Duration(ssf.streamFormat.format.ClockRate())))\n\t}\n\n\tif ssf.streamFormat.replaceNTP {\n\t\tu.NTP = ssf.streamFormat.ntpEstimator.Estimate(u.PTS)\n\t}\n\n\tif len(u.RTPPackets) != 0 {\n\t\tif ssf.rtpDecoder != nil {\n\t\t\tvar err error\n\t\t\tu.Payload, err = ssf.rtpDecoder.decode(u.RTPPackets[0])\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tif ssf.streamFormat.rtpEncoder == nil {\n\t\t\tfor _, pkt := range u.RTPPackets {\n\t\t\t\tif len(pkt.Payload) > ssf.streamFormat.rtpMaxPayloadSize {\n\t\t\t\t\tvar err error\n\t\t\t\t\tssf.streamFormat.rtpEncoder, err = newRTPEncoder(ssf.streamFormat.format, ssf.streamFormat.rtpMaxPayloadSize,\n\t\t\t\t\t\tptrOf(pkt.SSRC), ptrOf(pkt.SequenceNumber))\n\t\t\t\t\tif err != nil {\n\t\t\t\t\t\tvar err2 rtpEncoderNotAvailableError\n\t\t\t\t\t\tif errors.As(err, &err2) {\n\t\t\t\t\t\t\treturn fmt.Errorf(\"RTP payload size (%d) is greater than maximum allowed (%d)\",\n\t\t\t\t\t\t\t\tlen(pkt.Payload), ssf.streamFormat.rtpMaxPayloadSize)\n\t\t\t\t\t\t}\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\n\t\t\t\t\tssf.streamFormat.rtpTimeOffset = pkt.Timestamp - uint32(u.PTS)\n\n\t\t\t\t\tssf.streamFormat.parent.Log(logger.Info,\n\t\t\t\t\t\t\"RTP packets are too big (%d > %d), remuxing them into smaller ones\",\n\t\t\t\t\t\tlen(pkt.Payload), ssf.streamFormat.rtpMaxPayloadSize)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif ssf.streamFormat.rtpEncoder != nil {\n\t\t\tu.RTPPackets = nil\n\t\t}\n\t}\n\n\tif !u.NilPayload() {\n\t\tssf.streamFormat.formatUpdater(ssf.streamFormat.format, u.Payload)\n\n\t\tu.Payload = ssf.streamFormat.unitRemuxer(ssf.streamFormat.format, u.Payload)\n\n\t\tif ssf.streamFormat.rtpEncoder != nil && !u.NilPayload() {\n\t\t\tvar err error\n\t\t\tu.RTPPackets, err = ssf.streamFormat.rtpEncoder.encode(u.Payload)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tfor _, pkt := range u.RTPPackets {\n\t\t\t\tpkt.Timestamp += ssf.streamFormat.rtpTimeOffset + uint32(u.PTS)\n\t\t\t}\n\t\t}\n\t}\n\n\tsize := unitSize(u)\n\tssf.streamFormat.addInboundBytes(size)\n\n\tssf.streamFormat.writeRTSP(ssf.streamFormat.media, u.RTPPackets, u.NTP)\n\n\tfor sr, onData := range ssf.streamFormat.onDatas {\n\t\tcsr := sr\n\t\tcOnData := onData\n\t\tsr.push(func() error {\n\t\t\tif !csr.SkipBytesSent {\n\t\t\t\tssf.streamFormat.addOutboundBytes(size)\n\t\t\t}\n\t\t\treturn cOnData(u)\n\t\t})\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/stream/sub_stream_media.go",
    "content": "package stream\n\nimport (\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n)\n\ntype subStreamMedia struct {\n\tcurMedia      *description.Media\n\tstreamMedia   *streamMedia\n\tuseRTPPackets bool\n\n\tformats map[format.Format]*subStreamFormat\n}\n\nfunc (ssm *subStreamMedia) initialize() error {\n\tssm.formats = make(map[format.Format]*subStreamFormat)\n\n\tfor i, curFormat := range ssm.curMedia.Formats {\n\t\tforma := ssm.streamMedia.media.Formats[i]\n\n\t\tssf := &subStreamFormat{\n\t\t\tcurFormat:     curFormat,\n\t\t\tstreamFormat:  ssm.streamMedia.formats[forma],\n\t\t\tuseRTPPackets: ssm.useRTPPackets,\n\t\t}\n\t\terr := ssf.initialize()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tssm.formats[curFormat] = ssf\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/stream/unit_remuxer.go",
    "content": "package stream\n\nimport (\n\t\"bytes\"\n\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\tmch264 \"github.com/bluenviron/mediacommon/v2/pkg/codecs/h264\"\n\tmch265 \"github.com/bluenviron/mediacommon/v2/pkg/codecs/h265\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4video\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n)\n\ntype unitRemuxer func(format.Format, unit.Payload) unit.Payload\n\nfunc unitRemuxerH265(forma format.Format, payload unit.Payload) unit.Payload {\n\tformatH265 := forma.(*format.H265)\n\tau := payload.(unit.PayloadH265)\n\n\tisKeyFrame := false\n\tn := 0\n\n\tfor _, nalu := range au {\n\t\ttyp := mch265.NALUType((nalu[0] >> 1) & 0b111111)\n\n\t\tswitch typ {\n\t\tcase mch265.NALUType_VPS_NUT, mch265.NALUType_SPS_NUT, mch265.NALUType_PPS_NUT:\n\t\t\tcontinue\n\n\t\tcase mch265.NALUType_AUD_NUT:\n\t\t\tcontinue\n\n\t\tcase mch265.NALUType_IDR_W_RADL, mch265.NALUType_IDR_N_LP, mch265.NALUType_CRA_NUT:\n\t\t\tif !isKeyFrame {\n\t\t\t\tisKeyFrame = true\n\n\t\t\t\t// prepend parameters\n\t\t\t\tif formatH265.VPS != nil && formatH265.SPS != nil && formatH265.PPS != nil {\n\t\t\t\t\tn += 3\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tn++\n\t}\n\n\tif n == 0 {\n\t\treturn unit.PayloadH265(nil)\n\t}\n\n\tfilteredAU := make([][]byte, n)\n\ti := 0\n\n\tif isKeyFrame && formatH265.VPS != nil && formatH265.SPS != nil && formatH265.PPS != nil {\n\t\tfilteredAU[0] = formatH265.VPS\n\t\tfilteredAU[1] = formatH265.SPS\n\t\tfilteredAU[2] = formatH265.PPS\n\t\ti = 3\n\t}\n\n\tfor _, nalu := range au {\n\t\ttyp := mch265.NALUType((nalu[0] >> 1) & 0b111111)\n\n\t\tswitch typ {\n\t\tcase mch265.NALUType_VPS_NUT, mch265.NALUType_SPS_NUT, mch265.NALUType_PPS_NUT:\n\t\t\tcontinue\n\n\t\tcase mch265.NALUType_AUD_NUT:\n\t\t\tcontinue\n\t\t}\n\n\t\tfilteredAU[i] = nalu\n\t\ti++\n\t}\n\n\treturn unit.PayloadH265(filteredAU)\n}\n\nfunc unitRemuxerH264(forma format.Format, payload unit.Payload) unit.Payload {\n\tformatH264 := forma.(*format.H264)\n\tau := payload.(unit.PayloadH264)\n\n\tisKeyFrame := false\n\tn := 0\n\n\tfor _, nalu := range au {\n\t\ttyp := mch264.NALUType(nalu[0] & 0x1F)\n\n\t\tswitch typ {\n\t\tcase mch264.NALUTypeSPS, mch264.NALUTypePPS:\n\t\t\tcontinue\n\n\t\tcase mch264.NALUTypeAccessUnitDelimiter:\n\t\t\tcontinue\n\n\t\tcase mch264.NALUTypeIDR:\n\t\t\tif !isKeyFrame {\n\t\t\t\tisKeyFrame = true\n\n\t\t\t\t// prepend parameters\n\t\t\t\tif formatH264.SPS != nil && formatH264.PPS != nil {\n\t\t\t\t\tn += 2\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\tn++\n\t}\n\n\tif n == 0 {\n\t\treturn unit.PayloadH264(nil)\n\t}\n\n\tfilteredAU := make([][]byte, n)\n\ti := 0\n\n\tif isKeyFrame && formatH264.SPS != nil && formatH264.PPS != nil {\n\t\tfilteredAU[0] = formatH264.SPS\n\t\tfilteredAU[1] = formatH264.PPS\n\t\ti = 2\n\t}\n\n\tfor _, nalu := range au {\n\t\ttyp := mch264.NALUType(nalu[0] & 0x1F)\n\n\t\tswitch typ {\n\t\tcase mch264.NALUTypeSPS, mch264.NALUTypePPS:\n\t\t\tcontinue\n\n\t\tcase mch264.NALUTypeAccessUnitDelimiter:\n\t\t\tcontinue\n\t\t}\n\n\t\tfilteredAU[i] = nalu\n\t\ti++\n\t}\n\n\treturn unit.PayloadH264(filteredAU)\n}\n\nfunc unitRemuxerMPEG4Video(forma format.Format, payload unit.Payload) unit.Payload {\n\tformatMPEG4Video := forma.(*format.MPEG4Video)\n\tframe := payload.(unit.PayloadMPEG4Video)\n\n\t// remove config\n\tif bytes.HasPrefix(frame, []byte{0, 0, 1, byte(mpeg4video.VisualObjectSequenceStartCode)}) {\n\t\tend := bytes.Index(frame[4:], []byte{0, 0, 1, byte(mpeg4video.GroupOfVOPStartCode)})\n\t\tif end >= 0 {\n\t\t\tframe = frame[end+4:]\n\t\t}\n\t}\n\n\t// add config\n\tif bytes.Contains(frame, []byte{0, 0, 1, byte(mpeg4video.GroupOfVOPStartCode)}) {\n\t\tf := make([]byte, len(formatMPEG4Video.Config)+len(frame))\n\t\tn := copy(f, formatMPEG4Video.Config)\n\t\tcopy(f[n:], frame)\n\t\tframe = f\n\t}\n\n\tif len(frame) == 0 {\n\t\treturn unit.PayloadMPEG4Video(nil)\n\t}\n\n\treturn frame\n}\n\nfunc newUnitRemuxer(forma format.Format) unitRemuxer {\n\tswitch forma.(type) {\n\tcase *format.H265:\n\t\treturn unitRemuxerH265\n\n\tcase *format.H264:\n\t\treturn unitRemuxerH264\n\n\tcase *format.MPEG4Video:\n\t\treturn unitRemuxerMPEG4Video\n\n\tdefault:\n\t\treturn unitRemuxer(func(_ format.Format, payload unit.Payload) unit.Payload {\n\t\t\treturn payload\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/test/auth_manager.go",
    "content": "// Package test contains test utilities.\npackage test\n\nimport \"github.com/bluenviron/mediamtx/internal/auth\"\n\n// AuthManager is a dummy auth manager.\ntype AuthManager struct {\n\tAuthenticateImpl   func(req *auth.Request) (string, *auth.Error)\n\tRefreshJWTJWKSImpl func()\n}\n\n// Authenticate replicates auth.Manager.Authenticate.\nfunc (m *AuthManager) Authenticate(req *auth.Request) (string, *auth.Error) {\n\treturn m.AuthenticateImpl(req)\n}\n\n// RefreshJWTJWKS is a function that simulates a JWKS refresh.\nfunc (m *AuthManager) RefreshJWTJWKS() {\n\tm.RefreshJWTJWKSImpl()\n}\n\n// NilAuthManager is an auth manager that accepts everything.\nvar NilAuthManager = &AuthManager{\n\tAuthenticateImpl: func(_ *auth.Request) (string, *auth.Error) {\n\t\treturn \"\", nil\n\t},\n}\n"
  },
  {
    "path": "internal/test/formats.go",
    "content": "package test\n\nimport (\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n\t\"github.com/bluenviron/mediacommon/v2/pkg/codecs/mpeg4audio\"\n)\n\n// FormatH264 is a dummy H264 format.\nvar FormatH264 = &format.H264{\n\tPayloadTyp: 96,\n\tSPS: []byte{ // 1920x1080 baseline\n\t\t0x67, 0x42, 0xc0, 0x28, 0xd9, 0x00, 0x78, 0x02,\n\t\t0x27, 0xe5, 0x84, 0x00, 0x00, 0x03, 0x00, 0x04,\n\t\t0x00, 0x00, 0x03, 0x00, 0xf0, 0x3c, 0x60, 0xc9, 0x20,\n\t},\n\tPPS:               []byte{0x08, 0x06, 0x07, 0x08},\n\tPacketizationMode: 1,\n}\n\n// FormatH265 is a dummy H265 format.\nvar FormatH265 = &format.H265{\n\tPayloadTyp: 96,\n\tVPS: []byte{\n\t\t0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x02, 0x20,\n\t\t0x00, 0x00, 0x03, 0x00, 0xb0, 0x00, 0x00, 0x03,\n\t\t0x00, 0x00, 0x03, 0x00, 0x7b, 0x18, 0xb0, 0x24,\n\t},\n\tSPS: []byte{\n\t\t0x42, 0x01, 0x01, 0x02, 0x20, 0x00, 0x00, 0x03,\n\t\t0x00, 0xb0, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03,\n\t\t0x00, 0x7b, 0xa0, 0x07, 0x82, 0x00, 0x88, 0x7d,\n\t\t0xb6, 0x71, 0x8b, 0x92, 0x44, 0x80, 0x53, 0x88,\n\t\t0x88, 0x92, 0xcf, 0x24, 0xa6, 0x92, 0x72, 0xc9,\n\t\t0x12, 0x49, 0x22, 0xdc, 0x91, 0xaa, 0x48, 0xfc,\n\t\t0xa2, 0x23, 0xff, 0x00, 0x01, 0x00, 0x01, 0x6a,\n\t\t0x02, 0x02, 0x02, 0x01,\n\t},\n\tPPS: []byte{\n\t\t0x44, 0x01, 0xc0, 0x25, 0x2f, 0x05, 0x32, 0x40,\n\t},\n}\n\n// FormatMPEG4Audio is a dummy MPEG-4 audio format.\nvar FormatMPEG4Audio = &format.MPEG4Audio{\n\tPayloadTyp: 96,\n\tConfig: &mpeg4audio.AudioSpecificConfig{\n\t\tType:          2,\n\t\tSampleRate:    44100,\n\t\tChannelCount:  2,\n\t\tChannelConfig: 2,\n\t},\n\tSizeLength:       13,\n\tIndexLength:      3,\n\tIndexDeltaLength: 3,\n}\n"
  },
  {
    "path": "internal/test/logger.go",
    "content": "package test\n\nimport \"github.com/bluenviron/mediamtx/internal/logger\"\n\ntype nilLogger struct{}\n\nfunc (nilLogger) Log(_ logger.Level, _ string, _ ...any) {\n}\n\n// NilLogger is a logger to /dev/null\nvar NilLogger logger.Writer = &nilLogger{}\n\ntype testLogger struct {\n\tcb func(level logger.Level, format string, args ...any)\n}\n\nfunc (l *testLogger) Log(level logger.Level, format string, args ...any) {\n\tl.cb(level, format, args...)\n}\n\n// Logger returns a dummy logger.\nfunc Logger(cb func(logger.Level, string, ...any)) logger.Writer {\n\treturn &testLogger{cb: cb}\n}\n"
  },
  {
    "path": "internal/test/medias.go",
    "content": "package test\n\nimport (\n\t\"github.com/bluenviron/gortsplib/v5/pkg/description\"\n\t\"github.com/bluenviron/gortsplib/v5/pkg/format\"\n)\n\n// MediaH264 is a dummy H264 media.\nvar MediaH264 = UniqueMediaH264()\n\n// MediaMPEG4Audio is a dummy MPEG-4 audio media.\nvar MediaMPEG4Audio = UniqueMediaMPEG4Audio()\n\n// UniqueMediaH264 is a dummy H264 media.\nfunc UniqueMediaH264() *description.Media {\n\treturn &description.Media{\n\t\tType:    description.MediaTypeVideo,\n\t\tFormats: []format.Format{FormatH264},\n\t}\n}\n\n// UniqueMediaMPEG4Audio is a dummy MPEG-4 audio media.\nfunc UniqueMediaMPEG4Audio() *description.Media {\n\treturn &description.Media{\n\t\tType:    description.MediaTypeAudio,\n\t\tFormats: []format.Format{FormatMPEG4Audio},\n\t}\n}\n"
  },
  {
    "path": "internal/test/path_manager.go",
    "content": "package test\n\nimport (\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n)\n\n// PathManager is a dummy path manager.\ntype PathManager struct {\n\tFindPathConfImpl func(req defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error)\n\tDescribeImpl     func(req defs.PathDescribeReq) defs.PathDescribeRes\n\tAddPublisherImpl func(req defs.PathAddPublisherReq) (*defs.PathAddPublisherRes, error)\n\tAddReaderImpl    func(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error)\n}\n\n// FindPathConf implements PathManager.\nfunc (pm *PathManager) FindPathConf(req defs.PathFindPathConfReq) (*defs.PathFindPathConfRes, error) {\n\treturn pm.FindPathConfImpl(req)\n}\n\n// Describe implements PathManager.\nfunc (pm *PathManager) Describe(req defs.PathDescribeReq) defs.PathDescribeRes {\n\treturn pm.DescribeImpl(req)\n}\n\n// AddPublisher implements PathManager.\nfunc (pm *PathManager) AddPublisher(req defs.PathAddPublisherReq) (*defs.PathAddPublisherRes, error) {\n\treturn pm.AddPublisherImpl(req)\n}\n\n// AddReader implements PathManager.\nfunc (pm *PathManager) AddReader(req defs.PathAddReaderReq) (*defs.PathAddReaderRes, error) {\n\treturn pm.AddReaderImpl(req)\n}\n"
  },
  {
    "path": "internal/test/static_source_parent.go",
    "content": "package test\n\nimport (\n\t\"github.com/bluenviron/mediamtx/internal/defs\"\n\t\"github.com/bluenviron/mediamtx/internal/logger\"\n\t\"github.com/bluenviron/mediamtx/internal/stream\"\n\t\"github.com/bluenviron/mediamtx/internal/unit\"\n)\n\n// StaticSourceParent is a dummy static source parent.\ntype StaticSourceParent struct {\n\tstream *stream.Stream\n\treader *stream.Reader\n\tUnit   chan *unit.Unit\n}\n\n// Log implements logger.Writer.\nfunc (*StaticSourceParent) Log(logger.Level, string, ...any) {}\n\n// Initialize initializes StaticSourceParent.\nfunc (p *StaticSourceParent) Initialize() {\n\tp.Unit = make(chan *unit.Unit)\n}\n\n// Close closes StaticSourceParent.\nfunc (p *StaticSourceParent) Close() {\n\tp.stream.RemoveReader(p.reader)\n}\n\n// SetReady implements parent.\nfunc (p *StaticSourceParent) SetReady(req defs.PathSourceStaticSetReadyReq) defs.PathSourceStaticSetReadyRes {\n\tp.stream = &stream.Stream{\n\t\tDesc:              req.Desc,\n\t\tWriteQueueSize:    512,\n\t\tRTPMaxPayloadSize: 1450,\n\t\tReplaceNTP:        req.ReplaceNTP,\n\t\tParent:            p,\n\t}\n\terr := p.stream.Initialize()\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tsubStream := &stream.SubStream{\n\t\tStream:        p.stream,\n\t\tUseRTPPackets: req.UseRTPPackets,\n\t}\n\terr = subStream.Initialize()\n\tif err != nil {\n\t\tpanic(\"should not happen\")\n\t}\n\n\tp.reader = &stream.Reader{Parent: NilLogger}\n\n\tp.reader.OnData(\n\t\treq.Desc.Medias[0],\n\t\treq.Desc.Medias[0].Formats[0],\n\t\tfunc(u *unit.Unit) error {\n\t\t\tp.Unit <- u\n\t\t\tclose(p.Unit)\n\t\t\treturn nil\n\t\t})\n\n\tp.stream.AddReader(p.reader)\n\n\treturn defs.PathSourceStaticSetReadyRes{SubStream: subStream}\n}\n\n// SetNotReady implements parent.\nfunc (StaticSourceParent) SetNotReady(_ defs.PathSourceStaticSetNotReadyReq) {}\n"
  },
  {
    "path": "internal/test/temp_file.go",
    "content": "package test\n\nimport \"os\"\n\n// CreateTempFile creates a temporary file with given content.\nfunc CreateTempFile(byts []byte) (string, error) {\n\ttmpf, err := os.CreateTemp(os.TempDir(), \"rtsp-\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer tmpf.Close()\n\n\t_, err = tmpf.Write(byts)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn tmpf.Name(), nil\n}\n"
  },
  {
    "path": "internal/test/tls_cert.go",
    "content": "package test\n\n// TLSCertPub is the public key of a dummy certificate.\nvar TLSCertPub = []byte(`-----BEGIN CERTIFICATE-----\nMIIDazCCAlOgAwIBAgIUXw1hEC3LFpTsllv7D3ARJyEq7sIwDQYJKoZIhvcNAQEL\nBQAwRTELMAkGA1UEBhMCQVUxEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoM\nGEludGVybmV0IFdpZGdpdHMgUHR5IEx0ZDAeFw0yMDEyMTMxNzQ0NThaFw0zMDEy\nMTExNzQ0NThaMEUxCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEw\nHwYDVQQKDBhJbnRlcm5ldCBXaWRnaXRzIFB0eSBMdGQwggEiMA0GCSqGSIb3DQEB\nAQUAA4IBDwAwggEKAoIBAQDG8DyyS51810GsGwgWr5rjJK7OE1kTTLSNEEKax8Bj\nzOyiaz8rA2JGl2VUEpi2UjDr9Cm7nd+YIEVs91IIBOb7LGqObBh1kGF3u5aZxLkv\nNJE+HrLVvUhaDobK2NU+Wibqc/EI3DfUkt1rSINvv9flwTFu1qHeuLWhoySzDKEp\nOzYxpFhwjVSokZIjT4Red3OtFz7gl2E6OAWe2qoh5CwLYVdMWtKR0Xuw3BkDPk9I\nqkQKx3fqv97LPEzhyZYjDT5WvGrgZ1WDAN3booxXF3oA1H3GHQc4m/vcLatOtb8e\nnI59gMQLEbnp08cl873bAuNuM95EZieXTHNbwUnq5iybAgMBAAGjUzBRMB0GA1Ud\nDgQWBBQBKhJh8eWu0a4au9X/2fKhkFX2vjAfBgNVHSMEGDAWgBQBKhJh8eWu0a4a\nu9X/2fKhkFX2vjAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3DQEBCwUAA4IBAQBj\n3aCW0YPKukYgVK9cwN0IbVy/D0C1UPT4nupJcy/E0iC7MXPZ9D/SZxYQoAkdptdO\nxfI+RXkpQZLdODNx9uvV+cHyZHZyjtE5ENu/i5Rer2cWI/mSLZm5lUQyx+0KZ2Yu\ntEI1bsebDK30msa8QSTn0WidW9XhFnl3gRi4wRdimcQapOWYVs7ih+nAlSvng7NI\nXpAyRs8PIEbpDDBMWnldrX4TP6EWYUi49gCp8OUDRREKX3l6Ls1vZ02F34yHIt/7\n7IV/XSKG096bhW+icKBWV0IpcEsgTzPK1J1hMxgjhzIMxGboAeUU+kidthOob6Sd\nXQxaORfgM//NzX9LhUPk\n-----END CERTIFICATE-----\n`)\n\n// TLSCertKey is the private key of a dummy certificate.\nvar TLSCertKey = []byte(`-----BEGIN RSA PRIVATE KEY-----\nMIIEogIBAAKCAQEAxvA8skudfNdBrBsIFq+a4ySuzhNZE0y0jRBCmsfAY8zsoms/\nKwNiRpdlVBKYtlIw6/Qpu53fmCBFbPdSCATm+yxqjmwYdZBhd7uWmcS5LzSRPh6y\n1b1IWg6GytjVPlom6nPxCNw31JLda0iDb7/X5cExbtah3ri1oaMkswyhKTs2MaRY\ncI1UqJGSI0+EXndzrRc+4JdhOjgFntqqIeQsC2FXTFrSkdF7sNwZAz5PSKpECsd3\n6r/eyzxM4cmWIw0+Vrxq4GdVgwDd26KMVxd6ANR9xh0HOJv73C2rTrW/HpyOfYDE\nCxG56dPHJfO92wLjbjPeRGYnl0xzW8FJ6uYsmwIDAQABAoIBACi0BKcyQ3HElSJC\nkaAao+Uvnzh4yvPg8Nwf5JDIp/uDdTMyIEWLtrLczRWrjGVZYbsVROinP5VfnPTT\nkYwkfKINj2u+gC6lsNuPnRuvHXikF8eO/mYvCTur1zZvsQnF5kp4GGwIqr+qoPUP\nbB0UMndG1PdpoMryHe+JcrvTrLHDmCeH10TqOwMsQMLHYLkowvxwJWsmTY7/Qr5S\nWm3PPpOcW2i0uyPVuyuv4yD1368fqnqJ8QFsQp1K6QtYsNnJ71Hut1/IoxK/e6hj\n5Z+byKtHVtmcLnABuoOT7BhleJNFBksX9sh83jid4tMBgci+zXNeGmgqo2EmaWAb\nagQslkECgYEA8B1rzjOHVQx/vwSzDa4XOrpoHQRfyElrGNz9JVBvnoC7AorezBXQ\nM9WTHQIFTGMjzD8pb+YJGi3gj93VN51r0SmJRxBaBRh1ZZI9kFiFzngYev8POgD3\nygmlS3kTHCNxCK/CJkB+/jMBgtPj5ygDpCWVcTSuWlQFphePkW7jaaECgYEA1Blz\nulqgAyJHZaqgcbcCsI2q6m527hVr9pjzNjIVmkwu38yS9RTCgdlbEVVDnS0hoifl\n+jVMEGXjF3xjyMvL50BKbQUH+KAa+V4n1WGlnZOxX9TMny8MBjEuSX2+362vQ3BX\n4vOlX00gvoc+sY+lrzvfx/OdPCHQGVYzoKCxhLsCgYA07HcviuIAV/HsO2/vyvhp\nxF5gTu+BqNUHNOZDDDid+ge+Jre2yfQLCL8VPLXIQW3Jff53IH/PGl+NtjphuLvj\n7UDJvgvpZZuymIojP6+2c3gJ3CASC9aR3JBnUzdoE1O9s2eaoMqc4scpe+SWtZYf\n3vzSZ+cqF6zrD/Rf/M35IQKBgHTU4E6ShPm09CcoaeC5sp2WK8OevZw/6IyZi78a\nr5Oiy18zzO97U/k6xVMy6F+38ILl/2Rn31JZDVJujniY6eSkIVsUHmPxrWoXV1HO\ny++U32uuSFiXDcSLarfIsE992MEJLSAynbF1Rsgsr3gXbGiuToJRyxbIeVy7gwzD\n94TpAoGAY4/PejWQj9psZfAhyk5dRGra++gYRQ/gK1IIc1g+Dd2/BxbT/RHr05GK\n6vwrfjsoRyMWteC1SsNs/CurjfQ/jqCfHNP5XPvxgd5Ec8sRJIiV7V5RTuWJsPu1\n+3K6cnKEyg+0ekYmLertRFIY6SwWmY1fyKgTvxudMcsBY7dC4xs=\n-----END RSA PRIVATE KEY-----\n`)\n\n// TLSCertPubAlt is the public key of an alternative dummy certificate.\nvar TLSCertPubAlt = []byte(`-----BEGIN CERTIFICATE-----\nMIIDSTCCAjECFEut6ZxIOnbxi3bhrPLfPQZCLReNMA0GCSqGSIb3DQEBCwUAMGEx\nCzAJBgNVBAYTAkFVMRMwEQYDVQQIDApTb21lLVN0YXRlMSEwHwYDVQQKDBhJbnRl\ncm5ldCBXaWRnaXRzIFB0eSBMdGQxGjAYBgNVBAMMEW1lZGlhbXR4LnRlc3QuY29t\nMB4XDTI0MDgwMTIzNDY0MloXDTM0MDczMDIzNDY0MlowYTELMAkGA1UEBhMCQVUx\nEzARBgNVBAgMClNvbWUtU3RhdGUxITAfBgNVBAoMGEludGVybmV0IFdpZGdpdHMg\nUHR5IEx0ZDEaMBgGA1UEAwwRbWVkaWFtdHgudGVzdC5jb20wggEiMA0GCSqGSIb3\nDQEBAQUAA4IBDwAwggEKAoIBAQCzfvG9eLXKSTDBoM+cgV/ThiNRI2JY6dpQV8rK\nQFQ5bkkDUDP+2Ae/IWylgLLXmozsMwjz1Pu42awmGymBuo5HDbI4bxPJNQR9qRrR\n2+MvfDgmZxyhw5NfZDlVl+enxhb3FRgbHsLBy4oSoHbRUdLApVdM0Kg6r3bXzkih\nEEs63boFJOkPhs5H0NX7AzXyBp2WnvB71j+7avnMwAsjJHOiTs8wkp5wvRcIZpJl\nMCandUkcZShMirug7QOcR9fAr5CVKxsO/DjqEjwkslJHFfizOl3yRx6nsxvW8JUd\ndforpSRj84dkHTi7k37YTiji90GsOvh0qc0MfAmeE181HIb/AgMBAAEwDQYJKoZI\nhvcNAQELBQADggEBAEWkLL/7nvt3iD7BVJNHLvAS6GwuTH99vCil6TFYwVl4goht\nDur7YfzN43vUq+lAwS3Ry4ka7tH72pAMkpNFRvHOikWGmWUSDo2DcLd8iu3ruLF7\nyUg2ASQuekK0sUv4YKpAqV8gS2R4Jh4vLU+8L5iJ1XWGELbQ+H5wm4l7l+r2X6cD\n/opmdV8Slfi0FlNQtflLsGoSlfZF5jHxqi3zyt8QdEf9WZt8e6JPxcx2Fq7Op51u\nQx9nosr5fLwhkx46+B/cotsbI/xPDjLF6RQ1OUpcHwg1HI6czoW4hHn33S0zstCf\nBWt5Q1Mb2tGInbmbUgw3wUu/4nWoY+Mq4DKPlKs=\n-----END CERTIFICATE-----`)\n\n// TLSCertKeyAlt is the private key of an alternative dummy certificate.\nvar TLSCertKeyAlt = []byte(`-----BEGIN RSA PRIVATE KEY-----\nMIIEoQIBAAKCAQEAs37xvXi1ykkwwaDPnIFf04YjUSNiWOnaUFfKykBUOW5JA1Az\n/tgHvyFspYCy15qM7DMI89T7uNmsJhspgbqORw2yOG8TyTUEfaka0dvjL3w4Jmcc\nocOTX2Q5VZfnp8YW9xUYGx7CwcuKEqB20VHSwKVXTNCoOq92185IoRBLOt26BSTp\nD4bOR9DV+wM18gadlp7we9Y/u2r5zMALIyRzok7PMJKecL0XCGaSZTAmp3VJHGUo\nTIq7oO0DnEfXwK+QlSsbDvw46hI8JLJSRxX4szpd8kcep7Mb1vCVHXX6K6UkY/OH\nZB04u5N+2E4o4vdBrDr4dKnNDHwJnhNfNRyG/wIDAQABAoH/WmCqV6Lv5dEnofCj\nZUO/Fdv0hf/LBS0g2SAoFRSCIM8aJ3dUUH0PaXoeINDGCMlIxT7tKXJg5jJNYhWx\ng7oegw6vLe5ZiA+p5miL/uue+Jas4kLVp9DrfQLgQevt0gw4g/00pgy9adbFlTUD\na2HhPB7RIvXs8gYA6nVAT9jK1ST2pbeUgQNO4Ji4EjpPUkR2O7ISOlu5EV8Cj0eV\n1Vs5B92Z7ORh7P2fFV2YBu+igd04+uYvei6slQl+F9cETvJv2Z9r37Yashvnn1in\nuy/u1U4B1t4oOz81nHz6kxTixPpBOdJ6x8jLDgNGSsauJQfXT9xmB/rAr/NFq+7I\ntbTNAoGBAMOgm3XXHWokmJnX9pfNj6ixNlrMuuez/yXMVwuxa2WFwAFN16tjJhBi\nXOjestcvu/SRhOAMmYac5QdopJpLjO/FxO165r73eZhW/SJefyOHtfD29kHagA1u\nJjcznU6tiA0O1owy6nuuaTfyVbDQj32PhVBx9ZwSI4778GFbjWl7AoGBAOrj4WCC\ngTMaExpwNo+L+3VkM79YD1Obl13FcgtVoxjcoWjQeMx9D0k7adTV3xlchHFAjiD5\nGs/MZl8+seq+GDX3mODsmJkdRQbYId4g6IesiOnQ3Ug/Y282WZRnpB5h/BMnrcCZ\nVoohnATA7f96c7XtPUgZyROmh24T7UIVwVdNAoGAbeeGT276TI6g2RWWqXRIOFrP\nEbYhb1kViFPDt4MGtjOtSk5EUzpRwTSxw/aRfQmJS/6RKxqJCjKNDVuB1lmJpY9z\ncoPwrOr1+lssvalfPkPZOLZWZWrvNBxlBfBOeUxOuh9S89MLH08+N7tC3yJc6wq9\nuBM+DF+4cHUkeF3qFY8CgYBzS+IwBj82/0CLRLNzaKnIqKPB846qYoA9NhLRv3ps\nVLgiA9qXvXdIYhKDt2toPoKAOMjLJJtljpZdgB/C8wZdTyjKlzgcSEK+pk6RgyPA\nnQ8jfjNwKDU9vLbh4rGrfDtIh7yBAoN5ECBOMQlh0xCDJ21iO834iFCH1t4qBxW9\nLQKBgQC36adC2Gu+FJRvx4Mkm73fLmVdFbP6Do7qNwyVVyaG80PDVrFQrlWm4Dt7\nAO9IwzaS1Lx+qmU1Fj1WfCtXuQa5nc9AzZ36TmM6+pAn8AC7PdNqc0qSdefVrIjj\nzRGhUPaJV3A+sfO+xedBsAFnqNuX9oODYVGbTjuc2OWC30MGaw==\n-----END RSA PRIVATE KEY-----\n`)\n"
  },
  {
    "path": "internal/teste2e/build_images_test.go",
    "content": "//go:build enable_e2e_tests\n\npackage teste2e\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc buildImage(image string) error {\n\tecmd := exec.Command(\"docker\", \"build\", filepath.Join(\"images\", image),\n\t\t\"-t\", \"mediamtx-test-\"+image)\n\tecmd.Stdout = nil\n\tecmd.Stderr = os.Stderr\n\treturn ecmd.Run()\n}\n\nfunc TestBuildImages(t *testing.T) {\n\tfiles, err := os.ReadDir(\"images\")\n\trequire.NoError(t, err)\n\n\tfor _, file := range files {\n\t\terr := buildImage(file.Name())\n\t\trequire.NoError(t, err)\n\t}\n}\n"
  },
  {
    "path": "internal/teste2e/hls_manager_test.go",
    "content": "//go:build enable_e2e_tests\n\npackage teste2e\n\nimport (\n\t\"net/http\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestHLSServerRead(t *testing.T) {\n\tp, ok := newInstance(\"paths:\\n\" +\n\t\t\"  all_others:\\n\")\n\trequire.Equal(t, true, ok)\n\tdefer p.Close()\n\n\tcnt1, err := newContainer(\"ffmpeg\", \"source\", []string{\n\t\t\"-re\",\n\t\t\"-stream_loop\", \"-1\",\n\t\t\"-i\", \"emptyvideo.mkv\",\n\t\t\"-c\", \"copy\",\n\t\t\"-f\", \"rtsp\",\n\t\t\"rtsp://127.0.0.1:8554/test/stream\",\n\t})\n\trequire.NoError(t, err)\n\tdefer cnt1.close()\n\n\ttime.Sleep(1 * time.Second)\n\n\tcnt2, err := newContainer(\"ffmpeg\", \"dest\", []string{\n\t\t\"-i\", \"http://127.0.0.1:8888/test/stream/index.m3u8\",\n\t\t\"-vframes\", \"1\",\n\t\t\"-f\", \"image2\",\n\t\t\"-y\", \"/dev/null\",\n\t})\n\trequire.NoError(t, err)\n\tdefer cnt2.close()\n\trequire.Equal(t, 0, cnt2.wait())\n}\n\nfunc TestHLSServerAuth(t *testing.T) {\n\tfor _, result := range []string{\n\t\t\"success\",\n\t\t\"fail\",\n\t} {\n\t\tt.Run(result, func(t *testing.T) {\n\t\t\tcnf := \"paths:\\n\" +\n\t\t\t\t\"  all_others:\\n\" +\n\t\t\t\t\"    readUser: testreader\\n\" +\n\t\t\t\t\"    readPass: testpass\\n\" +\n\t\t\t\t\"    readIPs: [127.0.0.0/16]\\n\"\n\n\t\t\tp, ok := newInstance(cnf)\n\t\t\trequire.Equal(t, true, ok)\n\t\t\tdefer p.Close()\n\n\t\t\tcnt1, err := newContainer(\"ffmpeg\", \"source\", []string{\n\t\t\t\t\"-re\",\n\t\t\t\t\"-stream_loop\", \"-1\",\n\t\t\t\t\"-i\", \"emptyvideo.mkv\",\n\t\t\t\t\"-c\", \"copy\",\n\t\t\t\t\"-f\", \"rtsp\",\n\t\t\t\t\"rtsp://testpublisher:testpass@127.0.0.1:8554/teststream?param=value\",\n\t\t\t})\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer cnt1.close()\n\n\t\t\ttime.Sleep(1 * time.Second)\n\n\t\t\tvar usr string\n\t\t\tif result == \"success\" {\n\t\t\t\tusr = \"testreader\"\n\t\t\t} else {\n\t\t\t\tusr = \"testreader2\"\n\t\t\t}\n\n\t\t\ttr := &http.Transport{}\n\t\t\tdefer tr.CloseIdleConnections()\n\t\t\thc := &http.Client{Transport: tr}\n\n\t\t\tres, err := hc.Get(\"http://\" + usr + \":testpass@127.0.0.1:8888/teststream/index.m3u8?param=value\")\n\t\t\trequire.NoError(t, err)\n\t\t\tdefer res.Body.Close()\n\n\t\t\tif result == \"success\" {\n\t\t\t\trequire.Equal(t, http.StatusOK, res.StatusCode)\n\t\t\t} else {\n\t\t\t\trequire.Equal(t, http.StatusUnauthorized, res.StatusCode)\n\t\t\t}\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/teste2e/images/ffmpeg/Dockerfile",
    "content": "FROM amd64/alpine:3.14\n\nRUN apk add --no-cache \\\n    ffmpeg\n\nCOPY *.mkv /\n\nCOPY start.sh /\nRUN chmod +x /start.sh\n\nENTRYPOINT [ \"/start.sh\" ]\n"
  },
  {
    "path": "internal/teste2e/images/ffmpeg/start.sh",
    "content": "#!/bin/sh -e\n\nexec ffmpeg -hide_banner -loglevel error $@ 2>&1\n"
  },
  {
    "path": "internal/teste2e/images/gstreamer/Dockerfile",
    "content": "######################################\nFROM ubuntu:20.04 AS build\n\nRUN apt update && apt install -y --no-install-recommends \\\n    pkg-config \\\n    gcc \\\n    libgstreamer-plugins-base1.0-dev \\\n    && rm -rf /var/lib/apt/lists/*\n\nCOPY exitafterframe.c /s/\nRUN cd /s \\\n    && gcc \\\n    exitafterframe.c \\\n    -o libexitafterframe.so \\\n    -Ofast \\\n    -s \\\n    -Werror \\\n    -Wall \\\n    -Wextra \\\n    -Wno-unused-parameter \\\n    -fPIC \\\n    -shared \\\n    -Wl,--no-undefined \\\n    $(pkg-config --cflags --libs gstreamer-1.0) \\\n    && mv libexitafterframe.so /usr/lib/x86_64-linux-gnu/gstreamer-1.0/ \\\n    && rm -rf /s\n\n######################################\nFROM ubuntu:20.04\n\nRUN apt update && apt install -y --no-install-recommends \\\n    gstreamer1.0-tools \\\n    gstreamer1.0-plugins-good \\\n    gstreamer1.0-plugins-bad \\\n    gstreamer1.0-rtsp \\\n    gstreamer1.0-libav \\\n    && rm -rf /var/lib/apt/lists/*\n\nCOPY --from=build /usr/lib/x86_64-linux-gnu/gstreamer-1.0/libexitafterframe.so /usr/lib/x86_64-linux-gnu/gstreamer-1.0/\n\nCOPY emptyvideo.mkv /\n\nCOPY start.sh /\nRUN chmod +x /start.sh\n\nENTRYPOINT [ \"/start.sh\" ]\n"
  },
  {
    "path": "internal/teste2e/images/gstreamer/exitafterframe.c",
    "content": "\n#include <gst/gst.h>\n\nGType gst_exitafterframe_get_type ();\n\n#define GST_TYPE_EXITAFTERFRAME (gst_exitafterframe_get_type())\n#define GST_EXITAFTERFRAME(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_EXITAFTERFRAME,GstExitAfterFrame))\n\ntypedef struct\n{\n  GstElement element;\n  GstPad *srcpad;\n  GstPad *sinkpad;\n\n} GstExitAfterFrame;\n\ntypedef struct\n{\n  GstElementClass parent_class;\n\n} GstExitAfterFrameClass;\n\n#define gst_exitafterframe_parent_class parent_class\nG_DEFINE_TYPE (GstExitAfterFrame, gst_exitafterframe, GST_TYPE_ELEMENT);\n\nstatic GstStaticPadTemplate sink_factory = GST_STATIC_PAD_TEMPLATE(\n    \"sink\",\n    GST_PAD_SINK,\n    GST_PAD_ALWAYS,\n    GST_STATIC_CAPS(\"video/x-raw\")\n);\n\nstatic GstStaticPadTemplate src_factory = GST_STATIC_PAD_TEMPLATE(\n    \"src\",\n    GST_PAD_SRC,\n    GST_PAD_ALWAYS,\n    GST_STATIC_CAPS(\"video/x-raw\")\n);\n\nstatic GstFlowReturn\ngst_exitafterframe_chain (GstPad * pad, GstObject * parent, GstBuffer * buf)\n{\n  GstExitAfterFrame *filter = GST_EXITAFTERFRAME (parent);\n  exit(0);\n  return gst_pad_push (filter->srcpad, buf);\n}\n\nstatic void\ngst_exitafterframe_class_init(GstExitAfterFrameClass* klass) {\n    GstElementClass* element_class = (GstElementClass*)klass;\n\n    gst_element_class_set_details_simple(\n        element_class,\n        \"Plugin\",\n        \"FIXME:Generic\",\n        \"FIXME:Generic Template Element\",\n        \"AUTHOR_NAME AUTHOR_EMAIL\"\n    );\n\n    gst_element_class_add_pad_template(element_class,\n        gst_static_pad_template_get(&src_factory));\n    gst_element_class_add_pad_template(element_class,\n        gst_static_pad_template_get(&sink_factory));\n}\n\nstatic void\ngst_exitafterframe_init (GstExitAfterFrame* filter)\n{\n  GstElement* element = GST_ELEMENT(filter);\n\n  filter->sinkpad = gst_pad_new_from_static_template(&sink_factory, \"sink\");\n  gst_pad_set_chain_function(filter->sinkpad, gst_exitafterframe_chain);\n  GST_PAD_SET_PROXY_CAPS(filter->sinkpad);\n  gst_element_add_pad(element, filter->sinkpad);\n\n  filter->srcpad = gst_pad_new_from_static_template(&src_factory, \"src\");\n  GST_PAD_SET_PROXY_CAPS(filter->srcpad);\n  gst_element_add_pad(element, filter->srcpad);\n}\n\nstatic gboolean\nplugin_init (GstPlugin * plugin)\n{\n  return gst_element_register (plugin, \"exitafterframe\", GST_RANK_NONE,\n          GST_TYPE_EXITAFTERFRAME);\n}\n\n#define PACKAGE \"exitafterframe\"\n\nGST_PLUGIN_DEFINE (GST_VERSION_MAJOR, GST_VERSION_MINOR, exitafterframe,\n    \"exitafterframe\", plugin_init, \"1.0\", \"LGPL\", \"exitafterframe\",\n    \"http://example.com\")\n"
  },
  {
    "path": "internal/teste2e/images/gstreamer/start.sh",
    "content": "#!/bin/sh -e\n\nexec gst-launch-1.0 $@ 2>&1\n"
  },
  {
    "path": "internal/teste2e/images/vlc/Dockerfile",
    "content": "FROM amd64/alpine:3.14\n\nRUN apk add --no-cache \\\n    vlc\n\nRUN adduser -D -H -s /bin/sh -u 9337 user\n\nCOPY start.sh /\nRUN chmod +x /start.sh\n\nRUN mkdir /out \\\n    && chown user:user /out\n\nUSER user\nENTRYPOINT [ \"/start.sh\" ]\n"
  },
  {
    "path": "internal/teste2e/images/vlc/start.sh",
    "content": "#!/bin/sh -e\n\ncvlc --play-and-exit --no-audio --no-video --sout file/ts:/out/stream.ts -vvv $@ 2>&1 &\n\nCOUNTER=0\nwhile true; do\n    sleep 1\n\n    if [ $(stat -c \"%s\" /out/stream.ts) -gt 0 ]; then\n        exit 0\n    fi\n\n    COUNTER=$(($COUNTER + 1))\n\n    if [ $COUNTER -ge 15 ]; then\n        exit 1\n    fi\ndone\n"
  },
  {
    "path": "internal/teste2e/rtsp_server_test.go",
    "content": "//go:build enable_e2e_tests\n\npackage teste2e\n\nimport (\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRTSPServerPublishRead(t *testing.T) {\n\tfor _, ca := range []struct {\n\t\tpublisherSoft  string\n\t\tpublisherProto string\n\t\treaderSoft     string\n\t\treaderProto    string\n\t}{\n\t\t{\"ffmpeg\", \"udp\", \"ffmpeg\", \"udp\"},\n\t\t{\"ffmpeg\", \"udp\", \"ffmpeg\", \"multicast\"},\n\t\t{\"ffmpeg\", \"udp\", \"ffmpeg\", \"tcp\"},\n\t\t{\"ffmpeg\", \"udp\", \"gstreamer\", \"udp\"},\n\t\t{\"ffmpeg\", \"udp\", \"gstreamer\", \"multicast\"},\n\t\t{\"ffmpeg\", \"udp\", \"gstreamer\", \"tcp\"},\n\t\t{\"ffmpeg\", \"udp\", \"vlc\", \"udp\"},\n\t\t{\"ffmpeg\", \"udp\", \"vlc\", \"multicast\"},\n\t\t{\"ffmpeg\", \"udp\", \"vlc\", \"tcp\"},\n\t\t{\"ffmpeg\", \"tcp\", \"ffmpeg\", \"udp\"},\n\t\t{\"gstreamer\", \"udp\", \"ffmpeg\", \"udp\"},\n\t\t{\"gstreamer\", \"tcp\", \"ffmpeg\", \"udp\"},\n\t\t{\"ffmpeg\", \"tls\", \"ffmpeg\", \"tls\"},\n\t\t{\"ffmpeg\", \"tls\", \"gstreamer\", \"tls\"},\n\t\t{\"gstreamer\", \"tls\", \"ffmpeg\", \"tls\"},\n\t} {\n\t\tt.Run(ca.publisherSoft+\"_\"+ca.publisherProto+\"_\"+\n\t\t\tca.readerSoft+\"_\"+ca.readerProto, func(t *testing.T) {\n\t\t\tvar proto string\n\t\t\tvar port string\n\t\t\tif ca.publisherProto != \"tls\" {\n\t\t\t\tproto = \"rtsp\"\n\t\t\t\tport = \"8554\"\n\n\t\t\t\tp, ok := newInstance(\"rtmp: no\\n\" +\n\t\t\t\t\t\"hls: no\\n\" +\n\t\t\t\t\t\"webrtc: no\\n\" +\n\t\t\t\t\t\"readTimeout: 20s\\n\" +\n\t\t\t\t\t\"paths:\\n\" +\n\t\t\t\t\t\"  all_others:\\n\")\n\t\t\t\trequire.Equal(t, true, ok)\n\t\t\t\tdefer p.Close()\n\t\t\t} else {\n\t\t\t\tproto = \"rtsps\"\n\t\t\t\tport = \"8322\"\n\n\t\t\t\tserverCertFpath, err := test.CreateTempFile(test.TLSCertPub)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer os.Remove(serverCertFpath)\n\n\t\t\t\tserverKeyFpath, err := test.CreateTempFile(test.TLSCertKey)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer os.Remove(serverKeyFpath)\n\n\t\t\t\tp, ok := newInstance(\"rtmp: no\\n\" +\n\t\t\t\t\t\"hls: no\\n\" +\n\t\t\t\t\t\"webrtc: no\\n\" +\n\t\t\t\t\t\"readTimeout: 20s\\n\" +\n\t\t\t\t\t\"rtspTransports: [tcp]\\n\" +\n\t\t\t\t\t\"rtspEncryption: \\\"yes\\\"\\n\" +\n\t\t\t\t\t\"rtspServerCert: \" + serverCertFpath + \"\\n\" +\n\t\t\t\t\t\"rtspServerKey: \" + serverKeyFpath + \"\\n\" +\n\t\t\t\t\t\"paths:\\n\" +\n\t\t\t\t\t\"  all_others:\\n\")\n\t\t\t\trequire.Equal(t, true, ok)\n\t\t\t\tdefer p.Close()\n\t\t\t}\n\n\t\t\tswitch ca.publisherSoft {\n\t\t\tcase \"ffmpeg\":\n\t\t\t\tps := func() string {\n\t\t\t\t\tswitch ca.publisherProto {\n\t\t\t\t\tcase \"udp\", \"tcp\":\n\t\t\t\t\t\treturn ca.publisherProto\n\n\t\t\t\t\tdefault: // tls\n\t\t\t\t\t\treturn \"tcp\"\n\t\t\t\t\t}\n\t\t\t\t}()\n\n\t\t\t\tcnt1, err := newContainer(\"ffmpeg\", \"source\", []string{\n\t\t\t\t\t\"-re\",\n\t\t\t\t\t\"-stream_loop\", \"-1\",\n\t\t\t\t\t\"-i\", \"emptyvideo.mkv\",\n\t\t\t\t\t\"-c\", \"copy\",\n\t\t\t\t\t\"-f\", \"rtsp\",\n\t\t\t\t\t\"-rtsp_transport\",\n\t\t\t\t\tps,\n\t\t\t\t\tproto + \"://localhost:\" + port + \"/teststream\",\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer cnt1.close()\n\n\t\t\t\ttime.Sleep(1 * time.Second)\n\n\t\t\tcase \"gstreamer\":\n\t\t\t\tps := func() string {\n\t\t\t\t\tswitch ca.publisherProto {\n\t\t\t\t\tcase \"udp\", \"tcp\":\n\t\t\t\t\t\treturn ca.publisherProto\n\n\t\t\t\t\tdefault: // tls\n\t\t\t\t\t\treturn \"tcp\"\n\t\t\t\t\t}\n\t\t\t\t}()\n\n\t\t\t\tcnt1, err := newContainer(\"gstreamer\", \"source\", []string{\n\t\t\t\t\t\"filesrc location=emptyvideo.mkv ! matroskademux ! video/x-h264 ! rtspclientsink \" +\n\t\t\t\t\t\t\"location=\" + proto + \"://localhost:\" + port + \"/teststream \" +\n\t\t\t\t\t\t\"protocols=\" + ps + \" tls-validation-flags=0 latency=0 timeout=0 rtx-time=0\",\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer cnt1.close()\n\n\t\t\t\ttime.Sleep(1 * time.Second)\n\t\t\t}\n\n\t\t\ttime.Sleep(1 * time.Second)\n\n\t\t\tswitch ca.readerSoft {\n\t\t\tcase \"ffmpeg\":\n\t\t\t\tps := func() string {\n\t\t\t\t\tswitch ca.readerProto {\n\t\t\t\t\tcase \"udp\", \"tcp\":\n\t\t\t\t\t\treturn ca.publisherProto\n\n\t\t\t\t\tcase \"multicast\":\n\t\t\t\t\t\treturn \"udp_multicast\"\n\n\t\t\t\t\tdefault: // tls\n\t\t\t\t\t\treturn \"tcp\"\n\t\t\t\t\t}\n\t\t\t\t}()\n\n\t\t\t\tcnt2, err := newContainer(\"ffmpeg\", \"dest\", []string{\n\t\t\t\t\t\"-rtsp_transport\", ps,\n\t\t\t\t\t\"-i\", proto + \"://localhost:\" + port + \"/teststream\",\n\t\t\t\t\t\"-vframes\", \"1\",\n\t\t\t\t\t\"-f\", \"image2\",\n\t\t\t\t\t\"-y\", \"/dev/null\",\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer cnt2.close()\n\t\t\t\trequire.Equal(t, 0, cnt2.wait())\n\n\t\t\tcase \"gstreamer\":\n\t\t\t\tps := func() string {\n\t\t\t\t\tswitch ca.readerProto {\n\t\t\t\t\tcase \"udp\", \"tcp\":\n\t\t\t\t\t\treturn ca.publisherProto\n\n\t\t\t\t\tcase \"multicast\":\n\t\t\t\t\t\treturn \"udp-mcast\"\n\n\t\t\t\t\tdefault: // tls\n\t\t\t\t\t\treturn \"tcp\"\n\t\t\t\t\t}\n\t\t\t\t}()\n\n\t\t\t\tcnt2, err := newContainer(\"gstreamer\", \"read\", []string{\n\t\t\t\t\t\"rtspsrc location=\" + proto + \"://127.0.0.1:\" + port + \"/teststream \" +\n\t\t\t\t\t\t\"protocols=\" + ps + \" \" +\n\t\t\t\t\t\t\"tls-validation-flags=0 latency=0 \" +\n\t\t\t\t\t\t\"! application/x-rtp,media=video ! decodebin ! exitafterframe ! fakesink\",\n\t\t\t\t})\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer cnt2.close()\n\t\t\t\trequire.Equal(t, 0, cnt2.wait())\n\n\t\t\tcase \"vlc\":\n\t\t\t\targs := []string{}\n\t\t\t\tif ca.readerProto == \"tcp\" {\n\t\t\t\t\targs = append(args, \"--rtsp-tcp\")\n\t\t\t\t}\n\n\t\t\t\tur := proto + \"://localhost:\" + port + \"/teststream\"\n\t\t\t\tif ca.readerProto == \"multicast\" {\n\t\t\t\t\tur += \"?vlcmulticast\"\n\t\t\t\t}\n\n\t\t\t\targs = append(args, ur)\n\t\t\t\tcnt2, err := newContainer(\"vlc\", \"dest\", args)\n\t\t\t\trequire.NoError(t, err)\n\t\t\t\tdefer cnt2.close()\n\t\t\t\trequire.Equal(t, 0, cnt2.wait())\n\t\t\t}\n\t\t})\n\t}\n}\n\nfunc TestRTSPServerRedirect(t *testing.T) {\n\tp1, ok := newInstance(\"rtmp: no\\n\" +\n\t\t\"hls: no\\n\" +\n\t\t\"webrtc: no\\n\" +\n\t\t\"paths:\\n\" +\n\t\t\"  path1:\\n\" +\n\t\t\"    source: redirect\\n\" +\n\t\t\"    sourceRedirect: rtsp://localhost:8554/path2\\n\" +\n\t\t\"  path2:\\n\")\n\trequire.Equal(t, true, ok)\n\tdefer p1.Close()\n\n\tcnt1, err := newContainer(\"ffmpeg\", \"source\", []string{\n\t\t\"-re\",\n\t\t\"-stream_loop\", \"-1\",\n\t\t\"-i\", \"emptyvideo.mkv\",\n\t\t\"-c\", \"copy\",\n\t\t\"-f\", \"rtsp\",\n\t\t\"-rtsp_transport\", \"udp\",\n\t\t\"rtsp://localhost:8554/path2\",\n\t})\n\trequire.NoError(t, err)\n\tdefer cnt1.close()\n\n\ttime.Sleep(1 * time.Second)\n\n\tcnt2, err := newContainer(\"ffmpeg\", \"dest\", []string{\n\t\t\"-rtsp_transport\", \"udp\",\n\t\t\"-i\", \"rtsp://localhost:8554/path1\",\n\t\t\"-vframes\", \"1\",\n\t\t\"-f\", \"image2\",\n\t\t\"-y\", \"/dev/null\",\n\t})\n\trequire.NoError(t, err)\n\tdefer cnt2.close()\n\trequire.Equal(t, 0, cnt2.wait())\n}\n"
  },
  {
    "path": "internal/teste2e/tests_test.go",
    "content": "//go:build enable_e2e_tests\n\npackage teste2e\n\nimport (\n\t\"os\"\n\t\"os/exec\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/bluenviron/mediamtx/internal/core\"\n\t\"github.com/bluenviron/mediamtx/internal/test\"\n)\n\nfunc newInstance(conf string) (*core.Core, bool) {\n\tif conf == \"\" {\n\t\treturn core.New([]string{})\n\t}\n\n\ttmpf, err := test.CreateTempFile([]byte(conf))\n\tif err != nil {\n\t\treturn nil, false\n\t}\n\tdefer os.Remove(tmpf)\n\n\treturn core.New([]string{tmpf})\n}\n\ntype container struct {\n\tname string\n}\n\nfunc newContainer(image string, name string, args []string) (*container, error) {\n\tc := &container{\n\t\tname: name,\n\t}\n\n\texec.Command(\"docker\", \"kill\", \"mediamtx-test-\"+name).Run()\n\texec.Command(\"docker\", \"wait\", \"mediamtx-test-\"+name).Run()\n\n\t// --network=host is needed to test multicast\n\tcmd := []string{\n\t\t\"docker\", \"run\",\n\t\t\"--network=host\",\n\t\t\"--name=mediamtx-test-\" + name,\n\t\t\"mediamtx-test-\" + image,\n\t}\n\tcmd = append(cmd, args...)\n\tecmd := exec.Command(cmd[0], cmd[1:]...)\n\tecmd.Stdout = nil\n\tecmd.Stderr = os.Stderr\n\n\terr := ecmd.Start()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttime.Sleep(1 * time.Second)\n\n\treturn c, nil\n}\n\nfunc (c *container) close() {\n\texec.Command(\"docker\", \"kill\", \"mediamtx-test-\"+c.name).Run()\n\texec.Command(\"docker\", \"wait\", \"mediamtx-test-\"+c.name).Run()\n\texec.Command(\"docker\", \"rm\", \"mediamtx-test-\"+c.name).Run()\n}\n\nfunc (c *container) wait() int {\n\texec.Command(\"docker\", \"wait\", \"mediamtx-test-\"+c.name).Run()\n\tout, _ := exec.Command(\"docker\", \"inspect\", \"mediamtx-test-\"+c.name,\n\t\t\"-f\", \"{{.State.ExitCode}}\").Output()\n\tcode, _ := strconv.ParseInt(string(out[:len(out)-1]), 10, 32)\n\treturn int(code)\n}\n"
  },
  {
    "path": "internal/unit/payload.go",
    "content": "package unit\n\n// Payload is a codec-dependent payload.\ntype Payload interface {\n\tisPayload()\n}\n"
  },
  {
    "path": "internal/unit/payload_ac3.go",
    "content": "package unit\n\n// PayloadAC3 is the payload of an AC3 track.\ntype PayloadAC3 [][]byte\n\nfunc (PayloadAC3) isPayload() {}\n"
  },
  {
    "path": "internal/unit/payload_av1.go",
    "content": "package unit\n\n// PayloadAV1 is the payload of an AV1 track.\ntype PayloadAV1 [][]byte\n\nfunc (PayloadAV1) isPayload() {}\n"
  },
  {
    "path": "internal/unit/payload_g711.go",
    "content": "package unit\n\n// PayloadG711 is the payload of a G711 track.\ntype PayloadG711 []byte\n\nfunc (PayloadG711) isPayload() {}\n"
  },
  {
    "path": "internal/unit/payload_h264.go",
    "content": "package unit\n\n// PayloadH264 is the payload of a H264 track.\ntype PayloadH264 [][]byte\n\nfunc (PayloadH264) isPayload() {}\n"
  },
  {
    "path": "internal/unit/payload_h265.go",
    "content": "package unit\n\n// PayloadH265 is the payload of a H265 track.\ntype PayloadH265 [][]byte\n\nfunc (PayloadH265) isPayload() {}\n"
  },
  {
    "path": "internal/unit/payload_klv.go",
    "content": "package unit\n\n// PayloadKLV is the payload of a KLV track.\ntype PayloadKLV []byte\n\nfunc (PayloadKLV) isPayload() {}\n"
  },
  {
    "path": "internal/unit/payload_lpcm.go",
    "content": "package unit\n\n// PayloadLPCM is the payload of a LPCM track.\ntype PayloadLPCM []byte\n\nfunc (PayloadLPCM) isPayload() {}\n"
  },
  {
    "path": "internal/unit/payload_mjpeg.go",
    "content": "package unit\n\n// PayloadMJPEG is the payload of a MJPEG track.\ntype PayloadMJPEG []byte\n\nfunc (PayloadMJPEG) isPayload() {}\n"
  },
  {
    "path": "internal/unit/payload_mpeg1_audio.go",
    "content": "package unit\n\n// PayloadMPEG1Audio is the payload of a MPEG-1 Audio track.\ntype PayloadMPEG1Audio [][]byte\n\nfunc (PayloadMPEG1Audio) isPayload() {}\n"
  },
  {
    "path": "internal/unit/payload_mpeg1_video.go",
    "content": "package unit\n\n// PayloadMPEG1Video is the payload of a MPEG-1 Video track.\ntype PayloadMPEG1Video []byte\n\nfunc (PayloadMPEG1Video) isPayload() {}\n"
  },
  {
    "path": "internal/unit/payload_mpeg4_audio.go",
    "content": "package unit\n\n// PayloadMPEG4Audio is the payload of a MPEG-4 Audio track.\ntype PayloadMPEG4Audio [][]byte\n\nfunc (PayloadMPEG4Audio) isPayload() {}\n"
  },
  {
    "path": "internal/unit/payload_mpeg4_audio_latm.go",
    "content": "package unit\n\n// PayloadMPEG4AudioLATM is the payload of a MPEG-4 Audio LATM track.\ntype PayloadMPEG4AudioLATM []byte\n\nfunc (PayloadMPEG4AudioLATM) isPayload() {}\n"
  },
  {
    "path": "internal/unit/payload_mpeg4_video.go",
    "content": "package unit\n\n// PayloadMPEG4Video is the payload of a MPEG-4 Video track.\ntype PayloadMPEG4Video []byte\n\nfunc (PayloadMPEG4Video) isPayload() {}\n"
  },
  {
    "path": "internal/unit/payload_opus.go",
    "content": "package unit\n\n// PayloadOpus is the payload of a Opus track.\ntype PayloadOpus [][]byte\n\nfunc (PayloadOpus) isPayload() {}\n"
  },
  {
    "path": "internal/unit/payload_vp8.go",
    "content": "package unit\n\n// PayloadVP8 is the payload of a VP8 track.\ntype PayloadVP8 []byte\n\nfunc (PayloadVP8) isPayload() {}\n"
  },
  {
    "path": "internal/unit/payload_vp9.go",
    "content": "package unit\n\n// PayloadVP9 is the payload of a VP9 track.\ntype PayloadVP9 []byte\n\nfunc (PayloadVP9) isPayload() {}\n"
  },
  {
    "path": "internal/unit/unit.go",
    "content": "// Package unit contains the unit definition.\npackage unit\n\nimport (\n\t\"reflect\"\n\t\"time\"\n\n\t\"github.com/pion/rtp\"\n)\n\n// Unit is an atomic unit of a stream.\ntype Unit struct {\n\t// relative time\n\tPTS int64\n\n\t// absolute time\n\tNTP time.Time\n\n\t// RTP packets\n\tRTPPackets []*rtp.Packet\n\n\t// codec-dependent payload\n\tPayload Payload\n}\n\n// NilPayload checks whether the payload is nil.\nfunc (u Unit) NilPayload() bool {\n\treturn u.Payload == nil || reflect.ValueOf(u.Payload).IsNil()\n}\n"
  },
  {
    "path": "main.go",
    "content": "// main executable.\npackage main\n\nimport (\n\t\"os\"\n\n\t\"github.com/bluenviron/mediamtx/internal/core\"\n)\n\nfunc main() {\n\ts, ok := core.New(os.Args[1:])\n\tif !ok {\n\t\tos.Exit(1)\n\t}\n\ts.Wait()\n}\n"
  },
  {
    "path": "mediamtx.yml",
    "content": "###############################################\n# Global settings\n\n# Settings in this section are applied anywhere.\n\n###############################################\n# Global settings -> General\n\n# Verbosity of the program; available values are \"error\", \"warn\", \"info\", \"debug\".\nlogLevel: info\n# Destinations of log messages; available values are \"stdout\", \"file\" and \"syslog\".\nlogDestinations: [stdout]\n# When destination is \"stdout\" or \"file\", emit logs in structured format (JSONL).\nlogStructured: false\n# When \"file\" is in logDestinations, this is the file which will receive logs.\nlogFile: mediamtx.log\n# When \"syslog\" is in logDestinations, use prefix for logs.\nsysLogPrefix: mediamtx\n# Dump packets to disk. This is useful for debugging.\ndumpPackets: false\n\n# Timeout of read operations.\nreadTimeout: 10s\n# Timeout of write operations.\nwriteTimeout: 10s\n# Size of the queue of outgoing packets.\n# A higher value allows to increase throughput, a lower value allows to save RAM.\nwriteQueueSize: 512\n# Maximum size of outgoing UDP payloads.\n# It defaults to the maximum packet size on ethernet (1500) minus IPv6 and UDP headers (48).\n# This can be decreased to avoid fragmentation on networks with a low MTU.\nudpMaxPayloadSize: 1452\n# Size of the read buffer of every UDP socket.\n# This can be increased to decrease packet losses.\n# It defaults to the default value of the operating system.\nudpReadBufferSize: 0\n\n# Command to run when a client connects to the server.\n# This is terminated with SIGINT when a client disconnects from the server.\n# The following environment variables are available:\n# * MTX_CONN_TYPE: connection type\n# * MTX_CONN_ID: connection ID\n# * RTSP_PORT: RTSP server port\nrunOnConnect:\n# Restart the command if it exits.\nrunOnConnectRestart: false\n# Command to run when a client disconnects from the server.\n# Environment variables are the same as runOnConnect.\nrunOnDisconnect:\n\n###############################################\n# Global settings -> Authentication\n\n# Authentication method. Available values are:\n# * internal: credentials are stored in the configuration file\n# * http: an external HTTP URL is contacted to perform authentication\n# * jwt: an external identity server provides authentication through JWTs\nauthMethod: internal\n\n# Internal authentication.\n# Enabled users.\nauthInternalUsers:\n  # Default unprivileged user.\n  # Username. 'any' means any user, including anonymous ones.\n- user: any\n  # Password. Not used in case of 'any' user.\n  pass:\n  # IPs or networks allowed to use this user. An empty list means any IP.\n  ips: []\n  # Permissions.\n  permissions:\n    # Available actions are: publish, read, playback, api, metrics, pprof.\n  - action: publish\n    # Paths can be set to further restrict access to a specific path.\n    # An empty path means any path.\n    # Regular expressions can be used by using a tilde as prefix.\n    path:\n  - action: read\n    path:\n  - action: playback\n    path:\n\n  # Default administrator.\n  # This allows to use API, metrics and PPROF without authentication,\n  # if the IP is localhost.\n- user: any\n  pass:\n  ips: ['127.0.0.1', '::1']\n  permissions:\n  - action: api\n  - action: metrics\n  - action: pprof\n\n# HTTP-based authentication.\n# URL called to perform authentication. Every time a user wants\n# to authenticate, the server calls this URL with the POST method\n# and a body containing:\n# {\n#   \"user\": \"user\",\n#   \"password\": \"password\",\n#   \"token\": \"token\",\n#   \"ip\": \"ip\",\n#   \"action\": \"publish|read|playback|api|metrics|pprof\",\n#   \"path\": \"path\",\n#   \"protocol\": \"rtsp|rtmp|hls|webrtc|srt\",\n#   \"id\": \"id\",\n#   \"query\": \"query\"\n# }\n# If the response code is 20x, authentication is accepted, otherwise\n# it is discarded.\nauthHTTPAddress:\n# If the HTTP authentication URL has a self-signed or invalid certificate,\n# you can provide the fingerprint of the certificate in order to\n# validate it anyway. It can be obtained by running:\n# openssl s_client -connect auth_http_domain:443 </dev/null 2>/dev/null | sed -n '/BEGIN/,/END/p' > server.crt\n# openssl x509 -in server.crt -noout -fingerprint -sha256 | cut -d \"=\" -f2 | tr -d ':'\nauthHTTPFingerprint:\n# Actions to exclude from HTTP-based authentication.\n# Format is the same as the one of user permissions.\nauthHTTPExclude:\n- action: api\n- action: metrics\n- action: pprof\n\n# JWT-based authentication.\n# Users have to log in through an external identity server and obtain a JWT.\n# This JWT must contain the claim \"mediamtx_permissions\" with permissions,\n# for instance:\n# {\n#  \"mediamtx_permissions\": [\n#     {\n#       \"action\": \"publish\",\n#       \"path\": \"somepath\"\n#     }\n#   ]\n# }\n# Users are expected to pass the JWT in the Authorization header or as password.\n# This is the JWKS URL that will be used to pull (once) the public key that allows\n# to validate JWTs.\nauthJWTJWKS:\n# If the JWKS URL has a self-signed or invalid certificate,\n# you can provide the fingerprint of the certificate in order to\n# validate it anyway. It can be obtained by running:\n# openssl s_client -connect jwt_jwks_domain:443 </dev/null 2>/dev/null | sed -n '/BEGIN/,/END/p' > server.crt\n# openssl x509 -in server.crt -noout -fingerprint -sha256 | cut -d \"=\" -f2 | tr -d ':'\nauthJWTJWKSFingerprint:\n# name of the claim that contains permissions.\nauthJWTClaimKey: mediamtx_permissions\n# Actions to exclude from JWT-based authentication.\n# Format is the same as the one of user permissions.\nauthJWTExclude: []\n# Allow passing the JWT through query parameters of HTTP requests (i.e. ?jwt=JWT).\n# This is a security risk and will be disabled in the future.\n# RTSP and RTMP always allow JWT in query even if disabled, since there is no alternative.\nauthJWTInHTTPQuery: true\n# Expected issuer (iss) claim in the JWT. Leave empty to skip validation.\nauthJWTIssuer:\n# Expected audience (aud) claim in the JWT. Leave empty to skip validation.\nauthJWTAudience:\n\n###############################################\n# Global settings -> Control API\n\n# Enable controlling the server through the Control API.\napi: false\n# Address of the Control API listener.\napiAddress: :9997\n# Enable HTTPS on the Control API server.\napiEncryption: false\n# Path to the server key. This is needed only when encryption is yes.\n# This can be generated with:\n# openssl genrsa -out server.key 2048\n# openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650\napiServerKey: server.key\n# Path to the server certificate.\napiServerCert: server.crt\n# Allowed CORS origins.\n# Supports wildcards: ['http://*.example.com']\napiAllowOrigins: ['*']\n# IPs or CIDRs of proxies placed before the HTTP server.\n# These proxies can use the X-Forwarded-For header to set the real IP of clients,\n# and the X-Forwarded-Proto header to set the original protocol.\napiTrustedProxies: []\n\n###############################################\n# Global settings -> Metrics\n\n# Enable Prometheus-compatible metrics.\nmetrics: false\n# Address of the metrics HTTP listener.\nmetricsAddress: :9998\n# Enable HTTPS on the Metrics server.\nmetricsEncryption: false\n# Path to the server key. This is needed only when encryption is yes.\n# This can be generated with:\n# openssl genrsa -out server.key 2048\n# openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650\nmetricsServerKey: server.key\n# Path to the server certificate.\nmetricsServerCert: server.crt\n# Allowed CORS origins.\n# Supports wildcards: ['http://*.example.com']\nmetricsAllowOrigins: ['*']\n# IPs or CIDRs of proxies placed before the HTTP server.\n# These proxies can use the X-Forwarded-For header to set the real IP of clients,\n# and the X-Forwarded-Proto header to set the original protocol.\nmetricsTrustedProxies: []\n\n###############################################\n# Global settings -> PPROF\n\n# Enable pprof-compatible endpoint to monitor performances.\npprof: false\n# Address of the pprof listener.\npprofAddress: :9999\n# Enable HTTPS on the pprof server.\npprofEncryption: false\n# Path to the server key. This is needed only when encryption is yes.\n# This can be generated with:\n# openssl genrsa -out server.key 2048\n# openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650\npprofServerKey: server.key\n# Path to the server certificate.\npprofServerCert: server.crt\n# Allowed CORS origins.\n# Supports wildcards: ['http://*.example.com']\npprofAllowOrigins: ['*']\n# IPs or CIDRs of proxies placed before the HTTP server.\n# These proxies can use the X-Forwarded-For header to set the real IP of clients,\n# and the X-Forwarded-Proto header to set the original protocol.\npprofTrustedProxies: []\n\n###############################################\n# Global settings -> Playback server\n\n# Enable downloading recordings from the playback server.\nplayback: false\n# Address of the playback server listener.\nplaybackAddress: :9996\n# Enable HTTPS on the playback server.\nplaybackEncryption: false\n# Path to the server key. This is needed only when encryption is yes.\n# This can be generated with:\n# openssl genrsa -out server.key 2048\n# openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650\nplaybackServerKey: server.key\n# Path to the server certificate.\nplaybackServerCert: server.crt\n# Allowed CORS origins.\n# Supports wildcards: ['http://*.example.com']\nplaybackAllowOrigins: ['*']\n# IPs or CIDRs of proxies placed before the HTTP server.\n# These proxies can use the X-Forwarded-For header to set the real IP of clients,\n# and the X-Forwarded-Proto header to set the original protocol.\nplaybackTrustedProxies: []\n\n###############################################\n# Global settings -> RTSP server\n\n# Enable publishing and reading streams with the RTSP protocol.\nrtsp: true\n# Enabled RTSP transport protocols. The handshake is always performed with TCP.\nrtspTransports: [udp, multicast, tcp]\n# Use secure protocol variants (RTSPS, SRTP, SRTCP).\n# Available values are \"no\", \"strict\", \"optional\".\nrtspEncryption: \"no\"\n# Address of the TCP/RTSP listener. This is needed only when encryption is \"no\" or \"optional\".\nrtspAddress: :8554\n# Address of the TCP/RTSPS listener. This is needed only when encryption is \"strict\" or \"optional\".\nrtspsAddress: :8322\n# Address of the UDP/RTP listener. This is needed only when \"udp\" is in rtspTransports and encryption is \"no\" or \"optional\".\nrtpAddress: :8000\n# Address of the UDP/RTCP listener. This is needed only when \"udp\" is in rtspTransports and encryption is \"no\" or \"optional\".\nrtcpAddress: :8001\n# IP range of all UDP-multicast listeners. This is needed only when \"multicast\" is in rtspTransports and encryption is \"no\" or \"optional\".\nmulticastIPRange: 224.1.0.0/16\n# Port of all UDP-multicast/RTP listeners. This is needed only when \"multicast\" is in rtspTransports and encryption is \"no\" or \"optional\".\nmulticastRTPPort: 8002\n# Port of all UDP-multicast/RTCP listeners. This is needed only when \"multicast\" is in rtspTransports and encryption is \"no\" or \"optional\".\nmulticastRTCPPort: 8003\n# Address of the UDP/SRTP listener. This is needed only when \"udp\" is in rtspTransports and encryption is \"strict\" or \"optional\".\nsrtpAddress: :8004\n# Address of the UDP/SRTCP listener. This is needed only when \"udp\" is in rtspTransports and encryption is \"strict\" or \"optional\".\nsrtcpAddress: :8005\n# Port of all UDP-multicast/SRTP listeners. This is needed only when \"multicast\" is in rtspTransports and encryption is \"strict\" or \"optional\".\nmulticastSRTPPort: 8006\n# Port of all UDP-multicast/SRTCP listeners. This is needed only when \"multicast\" is in rtspTransports and encryption is \"strict\" or \"optional\".\nmulticastSRTCPPort: 8007\n# Path to the server key. This is needed only when encryption is \"strict\" or \"optional\".\n# This can be generated with:\n# openssl genrsa -out server.key 2048\n# openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650\nrtspServerKey: server.key\n# Path to the server certificate. This is needed only when encryption is \"strict\" or \"optional\".\nrtspServerCert: server.crt\n# Authentication methods. Available are \"basic\" and \"digest\".\n# \"digest\" doesn't provide any additional security and is available for compatibility only.\nrtspAuthMethods: [basic]\n\n###############################################\n# Global settings -> RTMP server\n\n# Enable publishing and reading streams with the RTMP protocol.\nrtmp: true\n# Use the secure protocol variant (RTMPS).\n# Available values are \"no\", \"strict\", \"optional\".\nrtmpEncryption: \"no\"\n# Address of the RTMP listener. This is needed only when encryption is \"no\" or \"optional\".\nrtmpAddress: :1935\n# Address of the RTMPS listener. This is needed only when encryption is \"strict\" or \"optional\".\nrtmpsAddress: :1936\n# Path to the server key. This is needed only when encryption is \"strict\" or \"optional\".\n# This can be generated with:\n# openssl genrsa -out server.key 2048\n# openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650\nrtmpServerKey: server.key\n# Path to the server certificate. This is needed only when encryption is \"strict\" or \"optional\".\nrtmpServerCert: server.crt\n\n###############################################\n# Global settings -> HLS server\n\n# Enable reading streams with the HLS protocol.\nhls: true\n# Address of the HLS listener.\nhlsAddress: :8888\n# Enable HTTPS on the HLS server.\n# This is required for Low-Latency HLS to function correctly on Apple devices.\nhlsEncryption: false\n# Path to the server key. This is needed only when encryption is yes.\n# This can be generated with:\n# openssl genrsa -out server.key 2048\n# openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650\nhlsServerKey: server.key\n# Path to the server certificate.\nhlsServerCert: server.crt\n# Allowed CORS origins.\n# Supports wildcards: ['http://*.example.com']\nhlsAllowOrigins: ['*']\n# IPs or CIDRs of proxies placed before the HLS server.\n# If the server receives a request from one of these entries, IP in logs\n# will be taken from the X-Forwarded-For header.\nhlsTrustedProxies: []\n# By default, HLS is generated only when requested by a user.\n# This option allows to generate it always, avoiding the delay between request and generation.\nhlsAlwaysRemux: false\n# Variant of the HLS protocol to use. Available options are:\n# * mpegts - uses MPEG-TS segments, for maximum compatibility.\n# * fmp4 - uses fragmented MP4 segments, more efficient.\n# * lowLatency - uses Low-Latency HLS.\nhlsVariant: lowLatency\n# Number of HLS segments to keep on the server.\n# Segments allow to seek through the stream.\n# Their number doesn't influence latency.\nhlsSegmentCount: 7\n# Minimum duration of each segment.\n# A player usually puts 3 segments in a buffer before reproducing the stream.\n# The final segment duration is also influenced by the interval between IDR frames,\n# since the server changes the duration in order to include at least one IDR frame\n# in each segment.\nhlsSegmentDuration: 1s\n# Minimum duration of each part.\n# A player usually puts 3 parts in a buffer before reproducing the stream.\n# Parts are used in Low-Latency HLS in place of segments.\n# Part duration is influenced by the distance between video/audio samples\n# and is adjusted in order to produce segments with a similar duration.\nhlsPartDuration: 200ms\n# Maximum size of each segment.\n# This prevents RAM exhaustion.\nhlsSegmentMaxSize: 50M\n# Directory in which to save segments, instead of keeping them in the RAM.\n# This decreases performance, since reading from disk is less performant than\n# reading from RAM, but allows to save RAM.\nhlsDirectory: ''\n# The muxer will be closed when there are no\n# reader requests and this amount of time has passed.\nhlsMuxerCloseAfter: 60s\n\n###############################################\n# Global settings -> WebRTC server\n\n# Enable publishing and reading streams with the WebRTC protocol.\nwebrtc: true\n# Address of the WebRTC HTTP listener.\nwebrtcAddress: :8889\n# Enable HTTPS on the WebRTC server.\n# This covers only the WebRTC handshake and does not influence the encryption of WebRTC streams\n# which are always encrypted, with a key that is exchanged during the WebRTC handshake.\nwebrtcEncryption: false\n# Path to the server key.\n# This can be generated with:\n# openssl genrsa -out server.key 2048\n# openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650\nwebrtcServerKey: server.key\n# Path to the server certificate.\nwebrtcServerCert: server.crt\n# Allowed CORS origins.\n# Supports wildcards: ['http://*.example.com']\nwebrtcAllowOrigins: ['*']\n# IPs or CIDRs of proxies placed before the WebRTC server.\n# If the server receives a request from one of these entries, IP in logs\n# will be taken from the X-Forwarded-For header.\nwebrtcTrustedProxies: []\n# Address of a local UDP listener that will receive connections.\n# Use a blank string to disable.\nwebrtcLocalUDPAddress: :8189\n# Address of a local TCP listener that will receive connections.\n# This is disabled by default since TCP is less efficient than UDP and\n# introduces a progressive delay when network is congested.\nwebrtcLocalTCPAddress: ''\n# WebRTC clients need to know the IP of the server.\n# Gather IPs from interfaces and send them to clients.\nwebrtcIPsFromInterfaces: true\n# Interfaces whose IPs will be sent to clients.\n# An empty value means to use all available interfaces.\nwebrtcIPsFromInterfacesList: []\n# Additional hosts or IPs to send to clients.\nwebrtcAdditionalHosts: []\n# ICE servers. Needed only when local listeners can't be reached by clients.\n# STUN servers allow to obtain and share the public IP of the server.\n# TURN/TURNS servers force all traffic through them.\nwebrtcICEServers2: []\n  # - url: stun:stun.l.google.com:19302\n  # if user is \"AUTH_SECRET\", then authentication is secret based.\n  # the secret must be inserted into the password field.\n  # username: ''\n  # password: ''\n  # clientOnly: false\n# Maximum time to gather STUN candidates.\nwebrtcSTUNGatherTimeout: 5s\n# Time to wait for the WebRTC handshake to complete.\nwebrtcHandshakeTimeout: 10s\n# Maximum time to gather tracks.\nwebrtcTrackGatherTimeout: 2s\n\n###############################################\n# Global settings -> SRT server\n\n# Enable publishing and reading streams with the SRT protocol.\nsrt: true\n# Address of the SRT listener.\nsrtAddress: :8890\n\n###############################################\n# Default path settings\n\n# Settings in \"pathDefaults\" are applied anywhere,\n# unless they are overridden in \"paths\".\npathDefaults:\n\n  ###############################################\n  # Default path settings -> General\n\n  # Source of the stream. This can be:\n  # * publisher -> the stream is provided by a RTSP, RTMP, WebRTC or SRT client\n  # * rtsp://existing-url -> the stream is pulled from another RTSP server / camera\n  # * rtsps://existing-url -> the stream is pulled from another RTSP server / camera with RTSPS\n  # * rtsp+http://existing-url -> the stream is pulled from another RTSP server / camera, with HTTP tunneling\n  # * rtsps+http://existing-url -> the stream is pulled from another RTSP server / camera, with HTTPS tunneling\n  # * rtsp+ws://existing-url -> the stream is pulled from another RTSP server / camera, with WebSocket tunneling\n  # * rtsps+ws://existing-url -> the stream is pulled from another RTSP server / camera, with secure WebSocket tunneling\n  # * rtmp://existing-url -> the stream is pulled from another RTMP server / camera\n  # * rtmps://existing-url -> the stream is pulled from another RTMP server / camera with RTMPS\n  # * http://existing-url/stream.m3u8 -> the stream is pulled from another HLS server / camera\n  # * https://existing-url/stream.m3u8 -> the stream is pulled from another HLS server / camera with HTTPS\n  # * udp+mpegts://ip:port -> the stream is pulled from MPEG-TS over UDP, by listening on the specified address\n  # * unix+mpegts://socket -> the stream is pulled from MPEG-TS over Unix socket, by using the socket\n  # * udp+rtp://ip:port -> the stream is pulled from RTP over UDP, by listening on the specified address\n  # * srt://existing-url -> the stream is pulled from another SRT server / camera\n  # * whep://existing-url -> the stream is pulled from another WebRTC server / camera with HTTP+WHEP\n  # * wheps://existing-url -> the stream is pulled from another WebRTC server / camera with HTTPS+WHEP\n  # * redirect -> the stream is provided by another path or server\n  # * rpiCamera -> the stream is provided by a Raspberry Pi Camera\n  # The following variables can be used in the source string:\n  # * $MTX_QUERY: query parameters (passed by first reader)\n  # * $G1, $G2, ...: regular expression groups, if path name is\n  #   a regular expression.\n  source: publisher\n  # If the source is a URL, and the source TLS certificate is self-signed\n  # or invalid, you can provide the fingerprint of the certificate in order to\n  # validate it anyway. It can be obtained by running:\n  # openssl s_client -connect source_ip:source_port </dev/null 2>/dev/null | sed -n '/BEGIN/,/END/p' > server.crt\n  # openssl x509 -in server.crt -noout -fingerprint -sha256 | cut -d \"=\" -f2 | tr -d ':'\n  sourceFingerprint:\n  # If the source is a URL, it will be pulled only when at least\n  # one reader is connected, saving bandwidth.\n  sourceOnDemand: false\n  # If sourceOnDemand is \"yes\", readers will be put on hold until the source is\n  # ready or until this amount of time has passed.\n  sourceOnDemandStartTimeout: 10s\n  # If sourceOnDemand is \"yes\", the source will be closed when there are no\n  # readers connected and this amount of time has passed.\n  sourceOnDemandCloseAfter: 10s\n  # Maximum number of readers. Zero means no limit.\n  maxReaders: 0\n  # SRT encryption passphrase required to read from this path.\n  srtReadPassphrase:\n  # Use absolute timestamp of frames, instead of replacing them with the current time.\n  useAbsoluteTimestamp: false\n\n  ###############################################\n  # Default path settings -> Always available\n\n  # Enable always-available mode, in which an offline segment is played on repeat when the stream is not available.\n  alwaysAvailable: false\n  # Tracks of the default offline segment.\n  alwaysAvailableTracks: []\n    # Available values are: AV1, VP9, H265, H264, Opus, MPEG4Audio, G711, LPCM\n    # - codec: H264\n    #   # in case of MPEG4Audio, G711, LPCM, sampleRate and ChannelCount must be provided too.\n    #   sampleRate: 48000\n    #   channelCount: 2\n    #   # in case of G711, muLaw must be provided too.\n    #   muLaw: false\n  # An MP4 file can be used instead of the default offline segment.\n  alwaysAvailableFile: ''\n\n  ###############################################\n  # Default path settings -> Record\n\n  # Record streams to disk.\n  record: false\n  # Path of recording segments.\n  # Extension is added automatically.\n  # Available variables are %path (path name), %Y %m %d (year, month, day),\n  # %H %M %S (hours, minutes, seconds), %f (microseconds), %z (time zone), %s (unix epoch).\n  recordPath: ./recordings/%path/%Y-%m-%d_%H-%M-%S-%f\n  # Format of recorded segments.\n  # Available formats are \"fmp4\" (fragmented MP4) and \"mpegts\" (MPEG-TS).\n  recordFormat: fmp4\n  # fMP4 segments are concatenation of small MP4 files (parts), each with this duration.\n  # MPEG-TS segments are concatenation of 188-bytes packets, flushed to disk with this period.\n  # When a system failure occurs, the last part gets lost.\n  # Therefore, the part duration is equal to the RPO (recovery point objective).\n  recordPartDuration: 1s\n  # This prevents RAM exhaustion.\n  recordMaxPartSize: 50M\n  # Minimum duration of each segment.\n  recordSegmentDuration: 1h\n  # Delete segments after this timespan.\n  # Set to 0s to disable automatic deletion.\n  recordDeleteAfter: 1d\n\n  ###############################################\n  # Default path settings -> Publisher source (when source is \"publisher\")\n\n  # Allow another client to disconnect the current publisher and publish in its place.\n  overridePublisher: true\n  # SRT encryption passphrase required to publish to this path.\n  srtPublishPassphrase:\n  # Demux MPEG-TS over RTSP into elementary streams.\n  # When enabled, RTSP publishers sending MP2T/90000 will be demultiplexed\n  # and their elementary streams (H.264, H.265, AAC, etc.) exposed as native tracks.\n  # This allows HLS, WebRTC, and other outputs to work transparently with MPEG-TS sources.\n  rtspDemuxMpegts: false\n\n  ###############################################\n  # Default path settings -> RTSP source (when source is a RTSP or a RTSPS URL)\n\n  # Transport protocol used to pull the stream. available values are \"automatic\", \"udp\", \"multicast\", \"tcp\".\n  rtspTransport: automatic\n  # Support sources that don't provide server ports or use random server ports. This is a security issue\n  # and must be used only when interacting with sources that require it.\n  rtspAnyPort: false\n  # Range header to send to the source, in order to start streaming from the specified offset.\n  # available values:\n  # * clock: Absolute time\n  # * npt: Normal Play Time\n  # * smpte: SMPTE timestamps relative to the start of the recording\n  rtspRangeType:\n  # Available values:\n  # * clock: UTC ISO 8601 combined date and time string, e.g. 20230812T120000Z\n  # * npt: duration such as \"300ms\", \"1.5m\" or \"2h45m\", valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\"\n  # * smpte: duration such as \"300ms\", \"1.5m\" or \"2h45m\", valid time units are \"ns\", \"us\" (or \"µs\"), \"ms\", \"s\", \"m\", \"h\"\n  rtspRangeStart:\n  # Range of ports used as source port in outgoing UDP packets.\n  rtspUDPSourcePortRange: [10000, 65535]\n\n  ###############################################\n  # Default path settings -> RTP source (when source is RTP)\n\n  # session description protocol (SDP) of the RTP stream.\n  rtpSDP:\n\n  ###############################################\n  # Default path settings -> WebRTC / WHEP source (when source is WHEP)\n\n  # Token to insert in the Authorization: Bearer header.\n  whepBearerToken: ''\n  # Maximum time to gather STUN candidates.\n  whepSTUNGatherTimeout: 5s\n  # Time to wait for the WebRTC handshake to complete.\n  whepHandshakeTimeout: 10s\n  # Maximum time to gather tracks.\n  whepTrackGatherTimeout: 2s\n\n  ###############################################\n  # Default path settings -> Redirect source (when source is \"redirect\")\n\n  # Path to which clients will be redirected.\n  # It can be a relative path (i.e. /otherstream) or an absolute RTSP URL.\n  sourceRedirect:\n\n  ###############################################\n  # Default path settings -> Raspberry Pi Camera source (when source is \"rpiCamera\")\n\n  # ID of the camera.\n  rpiCameraCamID: 0\n  # Whether this is a secondary stream.\n  rpiCameraSecondary: false\n  # Width of frames.\n  rpiCameraWidth: 1920\n  # Height of frames.\n  rpiCameraHeight: 1080\n  # Flip horizontally.\n  rpiCameraHFlip: false\n  # Flip vertically.\n  rpiCameraVFlip: false\n  # Brightness [-1, 1].\n  rpiCameraBrightness: 0\n  # Contrast [0, 16].\n  rpiCameraContrast: 1\n  # Saturation [0, 16].\n  rpiCameraSaturation: 1\n  # Sharpness [0, 16].\n  rpiCameraSharpness: 1\n  # Exposure mode.\n  # values: normal, short, long, custom.\n  rpiCameraExposure: normal\n  # Auto-white-balance mode.\n  # (auto, incandescent, tungsten, fluorescent, indoor, daylight, cloudy or custom).\n  rpiCameraAWB: auto\n  # Auto-white-balance fixed gains. This can be used in place of rpiCameraAWB.\n  # format: [red,blue].\n  rpiCameraAWBGains: [0, 0]\n  # Denoise operating mode (off, cdn_off, cdn_fast, cdn_hq).\n  rpiCameraDenoise: \"off\"\n  # Fixed shutter speed, in microseconds.\n  rpiCameraShutter: 0\n  # Metering mode of the AEC/AGC algorithm (centre, spot, matrix or custom).\n  rpiCameraMetering: centre\n  # Fixed gain.\n  rpiCameraGain: 0\n  # EV compensation of the image in range [-10, 10].\n  rpiCameraEV: 0\n  # Region of interest, in format x,y,width,height (all normalized between 0 and 1).\n  rpiCameraROI:\n  # Whether to enable HDR on Raspberry Camera 3.\n  rpiCameraHDR: false\n  # Tuning file.\n  rpiCameraTuningFile:\n  # Sensor mode, in format [width]:[height]:[bit-depth]:[packing]\n  # bit-depth and packing are optional.\n  rpiCameraMode:\n  # frames per second.\n  rpiCameraFPS: 30\n  # Autofocus mode (auto, manual or continuous).\n  rpiCameraAfMode: continuous\n  # Autofocus range (normal, macro or full).\n  rpiCameraAfRange: normal\n  # Autofocus speed (normal or fast).\n  rpiCameraAfSpeed: normal\n  # Lens position (for manual autofocus only), will be set to focus to a specific distance\n  # calculated by the following formula: d = 1 / value\n  # Examples: 0 moves the lens to infinity.\n  #           0.5 moves the lens to focus on objects 2m away.\n  #           2 moves the lens to focus on objects 50cm away.\n  rpiCameraLensPosition: 0.0\n  # Autofocus window, in the form x,y,width,height where the coordinates\n  # are given as a proportion of the entire image.\n  rpiCameraAfWindow:\n  # Manual flicker correction period, in microseconds.\n  rpiCameraFlickerPeriod: 0\n  # Enables printing text on each frame.\n  rpiCameraTextOverlayEnable: false\n  # Text that is printed on each frame.\n  # format is the one of the strftime() function.\n  rpiCameraTextOverlay: '%Y-%m-%d %H:%M:%S - MediaMTX'\n  # Codec (auto, hardwareH264, softwareH264 or mjpeg).\n  # When is \"auto\" and stream is primary, it defaults to hardwareH264 (if available) or softwareH264.\n  # When is \"auto\" and stream is secondary, it defaults to mjpeg.\n  rpiCameraCodec: auto\n  # Period between IDR frames (when codec is hardwareH264 or softwareH264).\n  rpiCameraIDRPeriod: 60\n  # Bitrate (when codec is hardwareH264 or softwareH264).\n  rpiCameraBitrate: 5000000\n  # Hardware H264 profile (baseline, main or high) (when codec is hardwareH264).\n  rpiCameraHardwareH264Profile: main\n  # Hardware H264 level (4.0, 4.1 or 4.2) (when codec is hardwareH264).\n  rpiCameraHardwareH264Level: '4.1'\n  # Software H264 profile (baseline, main or high) (when codec is softwareH264).\n  rpiCameraSoftwareH264Profile: baseline\n  # Software H264 level (4.0, 4.1 or 4.2) (when codec is softwareH264).\n  rpiCameraSoftwareH264Level: '4.1'\n  # M-JPEG JPEG quality (when codec is mjpeg).\n  rpiCameraMJPEGQuality: 60\n\n  ###############################################\n  # Default path settings -> Hooks\n\n  # Command to run when this path is initialized.\n  # This can be used to publish a stream when the server is launched.\n  # This is terminated with SIGINT when the program closes.\n  # The following environment variables are available:\n  # * MTX_PATH: path name\n  # * RTSP_PORT: RTSP server port\n  # * G1, G2, ...: regular expression groups, if path name is\n  #   a regular expression.\n  runOnInit:\n  # Restart the command if it exits.\n  runOnInitRestart: false\n\n  # Command to run when this path is requested by a reader\n  # and no one is publishing to this path yet.\n  # This can be used to publish a stream on demand.\n  # This is terminated with SIGINT when there are no readers anymore.\n  # The following environment variables are available:\n  # * MTX_PATH: path name\n  # * MTX_QUERY: query parameters (passed by first reader)\n  # * RTSP_PORT: RTSP server port\n  # * G1, G2, ...: regular expression groups, if path name is\n  #   a regular expression.\n  runOnDemand:\n  # Restart the command if it exits.\n  runOnDemandRestart: false\n  # Readers will be put on hold until the runOnDemand command starts publishing\n  # or until this amount of time has passed.\n  runOnDemandStartTimeout: 10s\n  # The command will be closed when there are no\n  # readers connected and this amount of time has passed.\n  runOnDemandCloseAfter: 10s\n  # Command to run when there are no readers anymore.\n  # Environment variables are the same as runOnDemand.\n  runOnUnDemand:\n\n  # Command to run when the stream is ready to be read, whenever it is\n  # published by a client or pulled from a server / camera.\n  # This is terminated with SIGINT when the stream is not ready anymore.\n  # The following environment variables are available:\n  # * MTX_PATH: path name\n  # * MTX_QUERY: query parameters (passed by publisher)\n  # * MTX_SOURCE_TYPE: source type\n  # * MTX_SOURCE_ID: source ID\n  # * RTSP_PORT: RTSP server port\n  # * G1, G2, ...: regular expression groups, if path name is\n  #   a regular expression.\n  runOnReady:\n  # Restart the command if it exits.\n  runOnReadyRestart: false\n  # Command to run when the stream is not available anymore.\n  # Environment variables are the same as runOnReady.\n  runOnNotReady:\n\n  # Command to run when a client starts reading.\n  # This is terminated with SIGINT when a client stops reading.\n  # The following environment variables are available:\n  # * MTX_PATH: path name\n  # * MTX_QUERY: query parameters (passed by reader)\n  # * MTX_READER_TYPE: reader type\n  # * MTX_READER_ID: reader ID\n  # * RTSP_PORT: RTSP server port\n  # * G1, G2, ...: regular expression groups, if path name is\n  #   a regular expression.\n  runOnRead:\n  # Restart the command if it exits.\n  runOnReadRestart: false\n  # Command to run when a client stops reading.\n  # Environment variables are the same as runOnRead.\n  runOnUnread:\n\n  # Command to run when a recording segment is created.\n  # The following environment variables are available:\n  # * MTX_PATH: path name\n  # * MTX_SEGMENT_PATH: segment file path\n  # * RTSP_PORT: RTSP server port\n  # * G1, G2, ...: regular expression groups, if path name is\n  #   a regular expression.\n  runOnRecordSegmentCreate:\n\n  # Command to run when a recording segment is complete.\n  # The following environment variables are available:\n  # * MTX_PATH: path name\n  # * MTX_SEGMENT_PATH: segment file path\n  # * MTX_SEGMENT_DURATION: segment duration\n  # * RTSP_PORT: RTSP server port\n  # * G1, G2, ...: regular expression groups, if path name is\n  #   a regular expression.\n  runOnRecordSegmentComplete:\n\n###############################################\n# Path settings\n\n# Settings in \"paths\" are applied to specific paths, and the map key\n# is the name of the path.\n# Any setting in \"pathDefaults\" can be overridden here.\n# It's possible to use regular expressions by using a tilde as prefix,\n# for example \"~^(test1|test2)$\" will match both \"test1\" and \"test2\",\n# for example \"~^prefix\" will match all paths that start with \"prefix\".\npaths:\n  # example:\n  # my_camera:\n  #   source: rtsp://my_camera\n\n  # Settings under path \"all_others\" are applied to all paths that\n  # do not match another entry.\n  all_others:\n"
  },
  {
    "path": "scripts/binaries.mk",
    "content": "BINARY_NAME = mediamtx\n\ndefine DOCKERFILE_BINARIES\nFROM $(BASE_IMAGE) AS build-base\nRUN apk add --no-cache zip make git tar\nWORKDIR /s\nCOPY go.mod go.sum ./\nRUN go mod download\nCOPY . ./\nENV CGO_ENABLED=0\nRUN rm -rf tmp binaries\nRUN mkdir tmp binaries\nRUN cp mediamtx.yml LICENSE tmp/\nRUN go generate ./...\n\nFROM build-base AS build-windows-amd64\nRUN GOOS=windows GOARCH=amd64 go build -tags enable_upgrade -o \"tmp/$(BINARY_NAME).exe\"\nRUN go install github.com/tc-hib/go-winres@v0.3.3\nRUN go-winres patch --in scripts/winres.json --product-version \"$$(git describe --tags --abbrev=0 | sed 's/^v//')\" --file-version \"$$(git describe --tags --abbrev=0 | sed 's/^v//')\" tmp/mediamtx.exe\nRUN cd tmp && zip -q \"../binaries/$(BINARY_NAME)_$$(cat ../internal/core/VERSION)_windows_amd64.zip\" \"$(BINARY_NAME).exe\" mediamtx.yml LICENSE\n\nFROM build-base AS build-linux-amd64\nRUN GOOS=linux GOARCH=amd64 go build -tags enable_upgrade -o \"tmp/$(BINARY_NAME)\"\nRUN tar -C tmp -czf \"binaries/$(BINARY_NAME)_$$(cat internal/core/VERSION)_linux_amd64.tar.gz\" --owner=0 --group=0 \"$(BINARY_NAME)\" mediamtx.yml LICENSE\n\nFROM build-base AS build-darwin-amd64\nRUN GOOS=darwin GOARCH=amd64 go build -tags enable_upgrade -o \"tmp/$(BINARY_NAME)\"\nRUN tar -C tmp -czf \"binaries/$(BINARY_NAME)_$$(cat internal/core/VERSION)_darwin_amd64.tar.gz\" --owner=0 --group=0 \"$(BINARY_NAME)\" mediamtx.yml LICENSE\n\nFROM build-base AS build-darwin-arm64\nRUN GOOS=darwin GOARCH=arm64 go build -tags enable_upgrade -o \"tmp/$(BINARY_NAME)\"\nRUN tar -C tmp -czf \"binaries/$(BINARY_NAME)_$$(cat internal/core/VERSION)_darwin_arm64.tar.gz\" --owner=0 --group=0 \"$(BINARY_NAME)\" mediamtx.yml LICENSE\n\nFROM build-base AS build-linux-armv6\nRUN GOOS=linux GOARCH=arm GOARM=6 go build -tags enable_upgrade -o \"tmp/$(BINARY_NAME)\"\nRUN tar -C tmp -czf \"binaries/$(BINARY_NAME)_$$(cat internal/core/VERSION)_linux_armv6.tar.gz\" --owner=0 --group=0 \"$(BINARY_NAME)\" mediamtx.yml LICENSE\n\nFROM build-base AS build-linux-armv7\nRUN GOOS=linux GOARCH=arm GOARM=7 go build -tags enable_upgrade -o \"tmp/$(BINARY_NAME)\"\nRUN tar -C tmp -czf \"binaries/$(BINARY_NAME)_$$(cat internal/core/VERSION)_linux_armv7.tar.gz\" --owner=0 --group=0 \"$(BINARY_NAME)\" mediamtx.yml LICENSE\n\nFROM build-base AS build-linux-arm64\nRUN GOOS=linux GOARCH=arm64 go build -tags enable_upgrade -o \"tmp/$(BINARY_NAME)\"\nRUN tar -C tmp -czf \"binaries/$(BINARY_NAME)_$$(cat internal/core/VERSION)_linux_arm64.tar.gz\" --owner=0 --group=0 \"$(BINARY_NAME)\" mediamtx.yml LICENSE\n\nFROM $(BASE_IMAGE)\nCOPY --from=build-windows-amd64 /s/binaries /s/binaries\nCOPY --from=build-linux-amd64 /s/binaries /s/binaries\nCOPY --from=build-darwin-amd64 /s/binaries /s/binaries\nCOPY --from=build-darwin-arm64 /s/binaries /s/binaries\nCOPY --from=build-linux-armv6 /s/binaries /s/binaries\nCOPY --from=build-linux-armv7 /s/binaries /s/binaries\nCOPY --from=build-linux-arm64 /s/binaries /s/binaries\nendef\nexport DOCKERFILE_BINARIES\n\nbinaries:\n\techo \"$$DOCKERFILE_BINARIES\" | docker build . -f - \\\n\t-t temp\n\tdocker run --rm -v \"$(shell pwd):/out\" \\\n\ttemp sh -c \"rm -rf /out/binaries && cp -r /s/binaries /out/\"\n\tsudo chown -R $(shell id -u):$(shell id -g) binaries\n"
  },
  {
    "path": "scripts/dockerhub.mk",
    "content": "DOCKER_REPOSITORY = bluenviron/mediamtx\n\ndockerhub:\n\t$(eval VERSION := $(shell git describe --tags | tr -d v))\n\n\tdocker login -u $(DOCKER_USER) -p $(DOCKER_PASSWORD)\n\n\tdocker buildx rm builder 2>/dev/null || true\n\tdocker buildx create --name=builder\n\n\tdocker build --builder=builder \\\n\t-f docker/ffmpeg-rpi.Dockerfile . \\\n\t--platform=linux/arm/v6,linux/arm/v7,linux/arm64 \\\n\t-t $(DOCKER_REPOSITORY):$(VERSION)-ffmpeg-rpi \\\n\t-t $(DOCKER_REPOSITORY):1-ffmpeg-rpi \\\n\t-t $(DOCKER_REPOSITORY):latest-ffmpeg-rpi \\\n\t--push\n\n\tdocker build --builder=builder \\\n\t-f docker/rpi.Dockerfile . \\\n\t--platform=linux/arm/v6,linux/arm/v7,linux/arm64 \\\n\t-t $(DOCKER_REPOSITORY):$(VERSION)-rpi \\\n\t-t $(DOCKER_REPOSITORY):1-rpi \\\n\t-t $(DOCKER_REPOSITORY):latest-rpi \\\n\t--push\n\n\tdocker build --builder=builder \\\n\t-f docker/ffmpeg.Dockerfile . \\\n\t--platform=linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 \\\n\t-t $(DOCKER_REPOSITORY):$(VERSION)-ffmpeg \\\n\t-t $(DOCKER_REPOSITORY):1-ffmpeg \\\n\t-t $(DOCKER_REPOSITORY):latest-ffmpeg \\\n\t--push\n\n\tdocker build --builder=builder \\\n\t-f docker/standard.Dockerfile . \\\n\t--platform=linux/amd64,linux/arm/v6,linux/arm/v7,linux/arm64 \\\n\t-t $(DOCKER_REPOSITORY):$(VERSION) \\\n\t-t $(DOCKER_REPOSITORY):1 \\\n\t-t $(DOCKER_REPOSITORY):latest \\\n\t--push\n\n\tdocker buildx rm builder\n"
  },
  {
    "path": "scripts/format.mk",
    "content": "define DOCKERFILE_FORMAT\nFROM $(BASE_IMAGE)\nRUN go install mvdan.cc/gofumpt@v0.5.0\nendef\nexport DOCKERFILE_FORMAT\n\nformat:\n\techo \"$$DOCKERFILE_FORMAT\" | docker build -q . -f - -t temp\n\tdocker run --rm -it -v \"$(shell pwd):/s\" -w /s temp \\\n\tsh -c \"gofumpt -l -w .\"\n"
  },
  {
    "path": "scripts/lint.mk",
    "content": "define DOCKERFILE_DOCS_LINT\nFROM $(NODE_IMAGE)\nRUN yarn global add prettier@3.6.2\nendef\nexport DOCKERFILE_DOCS_LINT\n\ndefine DOCKERFILE_API_DOCS_LINT\nFROM $(NODE_IMAGE)\nRUN yarn global add @redocly/cli@1.0.0-beta.123\nendef\nexport DOCKERFILE_API_DOCS_LINT\n\nlint-go:\n\tdocker run --rm -v \"$(shell pwd):/app\" -w /app \\\n\t$(GOLANGCI_LINT_IMAGE) \\\n\tgolangci-lint run -v\n\nlint-go-mod:\n\tgo mod tidy\n\tgit diff --exit-code\n\nlint-conf:\n\tgo test -v -tags enable_linters ./internal/linters/conf\n\nlint-go2api:\n\tgo test -v -tags enable_linters ./internal/linters/go2api\n\nlint-docs:\n\techo \"$$DOCKERFILE_DOCS_LINT\" | docker build . -f - -t temp\n\tdocker run --rm -v \"$(shell pwd)/docs:/s\" -w /s temp \\\n\tsh -c \"prettier --write .\"\n\tgit diff --exit-code\n\nlint-api-docs:\n\techo \"$$DOCKERFILE_API_DOCS_LINT\" | docker build . -f - -t temp\n\tdocker run --rm -v \"$(shell pwd)/api:/s\" -w /s temp \\\n\tsh -c \"openapi lint openapi.yaml\"\n\nlint: lint-go lint-go-mod lint-conf lint-go2api lint-docs lint-api-docs\n"
  },
  {
    "path": "scripts/test-e2e.mk",
    "content": "test-e2e-nodocker:\n\tgo generate ./...\n\tgo test -v -race -tags enable_e2e_tests ./internal/teste2e\n\ndefine DOCKERFILE_E2E_TEST\nFROM $(BASE_IMAGE)\nRUN apk add --no-cache make docker-cli gcc musl-dev\nWORKDIR /s\nCOPY go.mod go.sum ./\nRUN go mod download\nCOPY . ./\nendef\nexport DOCKERFILE_E2E_TEST\n\ntest-e2e:\n\techo \"$$DOCKERFILE_E2E_TEST\" | docker build -q . -f - -t temp\n\tdocker run --rm -it \\\n\t-v /var/run/docker.sock:/var/run/docker.sock:ro \\\n\t--network=host \\\n\ttemp \\\n\tmake test-e2e-nodocker\n"
  },
  {
    "path": "scripts/test.mk",
    "content": "ifeq ($(shell getconf LONG_BIT),64)\n  RACE=-race\nendif\n\ntest-internal:\n\tgo generate ./...\n\tgo test -v $(RACE) -coverprofile=coverage-internal.txt \\\n\t$$(go list ./internal/... | grep -v /core)\n\ntest-core:\n\tgo test -v $(RACE) -coverprofile=coverage-core.txt ./internal/core\n\ntest-nodocker: test-internal test-core\n\ndefine DOCKERFILE_TEST\nARG ARCH\nFROM $$ARCH/$(BASE_IMAGE)\nRUN apk add --no-cache make gcc musl-dev\nWORKDIR /s\nCOPY go.mod go.sum ./\nRUN go mod download\nendef\nexport DOCKERFILE_TEST\n\ntest:\n\techo \"$$DOCKERFILE_TEST\" | docker build -q . -f - -t temp --build-arg ARCH=amd64\n\tdocker run --rm \\\n\t-v \"$(shell pwd):/s\" \\\n\ttemp \\\n\tmake test-nodocker\n\ntest-32:\n\techo \"$$DOCKERFILE_TEST\" | docker build -q . -f - -t temp --build-arg ARCH=i386\n\tdocker run --rm \\\n\t-v \"$(shell pwd):/s\" \\\n\ttemp \\\n\tmake test-nodocker\n"
  },
  {
    "path": "scripts/winres.json",
    "content": "{\n  \"RT_VERSION\": {\n    \"#1\": {\n      \"0000\": {\n        \"fixed\": {\n          \"file_version\": \"0.0.0.0\",\n          \"product_version\": \"0.0.0.0\"\n        },\n        \"info\": {\n          \"0409\": {\n            \"Comments\": \"\",\n            \"CompanyName\": \"\",\n            \"FileDescription\": \"MediaMTX\",\n            \"FileVersion\": \"\",\n            \"InternalName\": \"\",\n            \"LegalCopyright\": \"\",\n            \"LegalTrademarks\": \"\",\n            \"OriginalFilename\": \"\",\n            \"PrivateBuild\": \"\",\n            \"ProductName\": \"MediaMTX\",\n            \"ProductVersion\": \"\",\n            \"SpecialBuild\": \"\"\n          }\n        }\n      }\n    }\n  }\n}\n"
  }
]