[
  {
    "path": ".github/workflows/build.yml",
    "content": "name: Build and Push\n\non:\n  workflow_dispatch:\n  push:\n    branches:\n      - 'master'\n    tags:\n      - 'v*'\n\njobs:\n  build-binaries:\n    name: Build binaries\n    runs-on: ubuntu-latest\n    env: { CGO_ENABLED: 0 }\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Setup Go\n        uses: actions/setup-go@v5\n        with: { go-version: '1.25' }\n\n      - name: Build go2rtc_win64\n        env: { GOOS: windows, GOARCH: amd64 }\n        run: go build -ldflags \"-s -w\" -trimpath\n      - name: Upload go2rtc_win64\n        uses: actions/upload-artifact@v4\n        with: { name: go2rtc_win64, path: go2rtc.exe }\n\n      - name: Build go2rtc_win32\n        env: { GOOS: windows, GOARCH: 386 }\n        run: go build -ldflags \"-s -w\" -trimpath\n      - name: Upload go2rtc_win32\n        uses: actions/upload-artifact@v4\n        with: { name: go2rtc_win32, path: go2rtc.exe }\n\n      - name: Build go2rtc_win_arm64\n        env: { GOOS: windows, GOARCH: arm64 }\n        run: go build -ldflags \"-s -w\" -trimpath\n      - name: Upload go2rtc_win_arm64\n        uses: actions/upload-artifact@v4\n        with: { name: go2rtc_win_arm64, path: go2rtc.exe }\n\n      - name: Build go2rtc_linux_amd64\n        env: { GOOS: linux, GOARCH: amd64 }\n        run: go build -ldflags \"-s -w\" -trimpath\n      - name: Upload go2rtc_linux_amd64\n        uses: actions/upload-artifact@v4\n        with: { name: go2rtc_linux_amd64, path: go2rtc }\n\n      - name: Build go2rtc_linux_i386\n        env: { GOOS: linux, GOARCH: 386 }\n        run: go build -ldflags \"-s -w\" -trimpath\n      - name: Upload go2rtc_linux_i386\n        uses: actions/upload-artifact@v4\n        with: { name: go2rtc_linux_i386, path: go2rtc }\n\n      - name: Build go2rtc_linux_arm64\n        env: { GOOS: linux, GOARCH: arm64 }\n        run: go build -ldflags \"-s -w\" -trimpath\n      - name: Upload go2rtc_linux_arm64\n        uses: actions/upload-artifact@v4\n        with: { name: go2rtc_linux_arm64, path: go2rtc }\n\n      - name: Build go2rtc_linux_arm\n        env: { GOOS: linux, GOARCH: arm, GOARM: 7 }\n        run: go build -ldflags \"-s -w\" -trimpath\n      - name: Upload go2rtc_linux_arm\n        uses: actions/upload-artifact@v4\n        with: { name: go2rtc_linux_arm, path: go2rtc }\n\n      - name: Build go2rtc_linux_armv6\n        env: { GOOS: linux, GOARCH: arm, GOARM: 6 }\n        run: go build -ldflags \"-s -w\" -trimpath\n      - name: Upload go2rtc_linux_armv6\n        uses: actions/upload-artifact@v4\n        with: { name: go2rtc_linux_armv6, path: go2rtc }\n\n      - name: Build go2rtc_linux_mipsel\n        env: { GOOS: linux, GOARCH: mipsle }\n        run: go build -ldflags \"-s -w\" -trimpath\n      - name: Upload go2rtc_linux_mipsel\n        uses: actions/upload-artifact@v4\n        with: { name: go2rtc_linux_mipsel, path: go2rtc }\n\n      - name: Build go2rtc_mac_amd64\n        env: { GOOS: darwin, GOARCH: amd64 }\n        run: go build -ldflags \"-s -w\" -trimpath\n      - name: Upload go2rtc_mac_amd64\n        uses: actions/upload-artifact@v4\n        with: { name: go2rtc_mac_amd64, path: go2rtc }\n\n      - name: Build go2rtc_mac_arm64\n        env: { GOOS: darwin, GOARCH: arm64 }\n        run: go build -ldflags \"-s -w\" -trimpath\n      - name: Upload go2rtc_mac_arm64\n        uses: actions/upload-artifact@v4\n        with: { name: go2rtc_mac_arm64, path: go2rtc }\n\n      - name: Build go2rtc_freebsd_amd64\n        env: { GOOS: freebsd, GOARCH: amd64 }\n        run: go build -ldflags \"-s -w\" -trimpath\n      - name: Upload go2rtc_freebsd_amd64\n        uses: actions/upload-artifact@v4\n        with: { name: go2rtc_freebsd_amd64, path: go2rtc }\n\n      - name: Build go2rtc_freebsd_arm64\n        env: { GOOS: freebsd, GOARCH: arm64 }\n        run: go build -ldflags \"-s -w\" -trimpath\n      - name: Upload go2rtc_freebsd_arm64\n        uses: actions/upload-artifact@v4\n        with: { name: go2rtc_freebsd_arm64, path: go2rtc }\n\n  docker-master:\n    name: Build docker master\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Docker meta\n        id: meta\n        uses: docker/metadata-action@v5\n        with:\n          images: |\n            name=${{ github.repository }},enable=${{ github.event.repository.fork == false }}\n            ghcr.io/${{ github.repository }}\n          tags: |\n            type=ref,event=branch\n            type=semver,pattern={{version}},enable=false\n            type=match,pattern=v(.*),group=1\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Login to DockerHub\n        if: github.event_name == 'push' && github.event.repository.fork == false\n        uses: docker/login-action@v3\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: Login to GitHub Container Registry\n        if: github.event_name == 'push'\n        uses: docker/login-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Build and push\n        uses: docker/build-push-action@v5\n        with:\n          context: .\n          file: docker/Dockerfile\n          platforms: |\n            linux/amd64\n            linux/386\n            linux/arm/v6\n            linux/arm/v7\n            linux/arm64/v8\n          push: ${{ github.event_name != 'pull_request' }}\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n\n  docker-hardware:\n    name: Build docker hardware\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Docker meta\n        id: meta-hw\n        uses: docker/metadata-action@v5\n        with:\n          images: |\n            name=${{ github.repository }},enable=${{ github.event.repository.fork == false }}\n            ghcr.io/${{ github.repository }}\n          flavor: |\n            suffix=-hardware,onlatest=true\n            latest=auto\n          tags: |\n            type=ref,event=branch\n            type=semver,pattern={{version}},enable=false\n            type=match,pattern=v(.*),group=1\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Login to DockerHub\n        if: github.event_name == 'push' && github.event.repository.fork == false\n        uses: docker/login-action@v3\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: Login to GitHub Container Registry\n        if: github.event_name == 'push'\n        uses: docker/login-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Build and push\n        uses: docker/build-push-action@v5\n        with:\n          context: .\n          file: docker/hardware.Dockerfile\n          platforms: linux/amd64\n          push: ${{ github.event_name != 'pull_request' }}\n          tags: ${{ steps.meta-hw.outputs.tags }}\n          labels: ${{ steps.meta-hw.outputs.labels }}\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n\n  docker-rockchip:\n    name: Build docker rockchip\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Docker meta\n        id: meta-rk\n        uses: docker/metadata-action@v5\n        with:\n          images: |\n            name=${{ github.repository }},enable=${{ github.event.repository.fork == false }}\n            ghcr.io/${{ github.repository }}\n          flavor: |\n            suffix=-rockchip,onlatest=true\n            latest=auto\n          tags: |\n            type=ref,event=branch\n            type=semver,pattern={{version}},enable=false\n            type=match,pattern=v(.*),group=1\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n\n      - name: Login to DockerHub\n        if: github.event_name == 'push' && github.event.repository.fork == false\n        uses: docker/login-action@v3\n        with:\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n\n      - name: Login to GitHub Container Registry\n        if: github.event_name == 'push'\n        uses: docker/login-action@v3\n        with:\n          registry: ghcr.io\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Build and push\n        uses: docker/build-push-action@v5\n        with:\n          context: .\n          file: docker/rockchip.Dockerfile\n          platforms: linux/arm64\n          push: ${{ github.event_name != 'pull_request' }}\n          tags: ${{ steps.meta-rk.outputs.tags }}\n          labels: ${{ steps.meta-rk.outputs.labels }}\n          cache-from: type=gha\n          cache-to: type=gha,mode=max\n"
  },
  {
    "path": ".github/workflows/gh-pages.yml",
    "content": "# Simple workflow for deploying static content to GitHub Pages\nname: Deploy static content to Pages\n\non:\n  # Allows you to run this workflow manually from the Actions tab\n  workflow_dispatch:\n\n# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages\npermissions:\n  contents: read\n  pages: write\n  id-token: write\n\n# Allow one concurrent deployment\nconcurrency:\n  group: \"pages\"\n  cancel-in-progress: true\n\njobs:\n  build:\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v5\n      - name: Setup Node\n        uses: actions/setup-node@v6\n        with:\n          node-version: 24\n          package-manager-cache: false\n      - name: Install dependencies\n        run: npm install --no-package-lock\n      - name: Build docs\n        run: npm run docs:build\n      - name: Copy docs into website\n        run: rsync -a --exclude '.vitepress/' --exclude 'README.md' website/ website/.vitepress/dist/\n      - name: Setup Pages\n        uses: actions/configure-pages@v5\n      - name: Upload artifact\n        uses: actions/upload-pages-artifact@v3\n        with:\n          path: website/.vitepress/dist\n\n  # Single deploy job since we're just deploying\n  deploy:\n    environment:\n      name: github-pages\n      url: ${{ steps.deployment.outputs.page_url }}\n    runs-on: ubuntu-latest\n    needs: build\n    steps:\n      - name: Deploy to GitHub Pages\n        id: deployment\n        uses: actions/deploy-pages@v4\n"
  },
  {
    "path": ".github/workflows/test.yml",
    "content": "name: Test Build and Run\n\non:\n#  push:\n#    branches:\n#      - '*'\n#  pull_request:\n#  merge_group:\n  workflow_dispatch:\n\njobs:\n  build-test:\n    strategy:\n      matrix:\n        os: [windows-latest, ubuntu-latest, macos-latest]\n        arch: [amd64, arm64]\n\n    runs-on: ${{ matrix.os }}\n    continue-on-error: true\n    env:\n      GOARCH: ${{ matrix.arch }}\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n\n      - name: Setup Go\n        uses: actions/setup-go@v5\n        with:\n          go-version: '1.24'\n\n      - name: Build Go binary\n        run: go build -ldflags \"-s -w\" -trimpath -o ./go2rtc\n\n      - name: Test Go binary on linux\n        if: matrix.os == 'ubuntu-latest'\n        run: |\n            if [ \"${{ matrix.arch }}\" = \"amd64\" ]; then\n              ./go2rtc -version\n            else\n              sudo apt-get update && sudo apt-get install -y qemu-user-static\n              sudo cp /usr/bin/qemu-aarch64-static .\n              sudo chown $USER:$USER ./qemu-aarch64-static\n              qemu-aarch64-static ./go2rtc -version\n            fi\n      - name: Test Go binary on macos\n        if: matrix.os == 'macos-latest'\n        run: |\n            if [ \"${{ matrix.arch }}\" = \"amd64\" ]; then\n              ./go2rtc -version\n            else\n              echo \"ARM64 architecture is not yet supported on macOS\"\n            fi\n      - name: Test Go binary on windows\n        if: matrix.os == 'windows-latest'\n        run: |\n            if (\"${{ matrix.arch }}\" -eq \"amd64\") {\n                .\\go2rtc* -version\n            } else {\n                Write-Host \"ARM64 architecture is not yet supported on Windows\"\n            }\n  docker-test:\n    strategy:\n      matrix:\n        platform:\n          - amd64\n          - \"386\"\n          - arm/v7\n          - arm64/v8\n    continue-on-error: true\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v4\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v3\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v3\n      - name: Build and push\n        uses: docker/build-push-action@v5\n        with:\n          context: .\n          file: docker/Dockerfile\n          platforms: linux/${{ matrix.platform }}\n          push: false\n          load: true\n          tags: go2rtc-${{ matrix.platform }}\n      - name: test run\n        run: |\n          docker run --platform=linux/${{ matrix.platform }} --rm go2rtc-${{ matrix.platform }} go2rtc -version\n\n      - name: Build and push Hardware\n        if: matrix.platform == 'amd64'\n        uses: docker/build-push-action@v5\n        with:\n          context: .\n          file: docker/hardware.Dockerfile\n          platforms: linux/amd64\n          push: false\n          load: true\n          tags: go2rtc-${{ matrix.platform }}-hardware\n      - name: test run\n        if: matrix.platform == 'amd64'\n        run: |\n          docker run --platform=linux/${{ matrix.platform }} --rm go2rtc-${{ matrix.platform }}-hardware go2rtc -version"
  },
  {
    "path": ".gitignore",
    "content": ".idea/\n.tmp/\n\ngo2rtc.yaml\ngo2rtc.json\n\ngo2rtc_freebsd*\ngo2rtc_linux*\ngo2rtc_mac*\ngo2rtc_win*\n\n/go2rtc\n/go2rtc.exe\n\n0_test.go\n\n.DS_Store\n\nwebsite/.vitepress/cache\nwebsite/.vitepress/dist\n\nnode_modules\npackage-lock.json\nCLAUDE.md"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2022 Alexey Khit\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": "README.md",
    "content": "<h1 align=\"center\">\n  <a href=\"https://github.com/AlexxIT/go2rtc\">\n    <img src=\"./website/images/logo.gif\" alt=\"go2rtc - GitHub\">\n  </a>\n</h1>\n<p align=\"center\">\n  <a href=\"https://github.com/AlexxIT/go2rtc/stargazers\" target=\"_blank\">\n    <img style=\"display: inline\" src=\"https://img.shields.io/github/stars/AlexxIT/go2rtc?style=flat-square&logo=github\" alt=\"go2rtc - GitHub Stars\">\n  </a>\n  <a href=\"https://hub.docker.com/r/alexxit/go2rtc\" target=\"_blank\">\n    <img style=\"display: inline\" src=\"https://img.shields.io/docker/pulls/alexxit/go2rtc?style=flat-square&logo=docker&logoColor=white&label=pulls\" alt=\"go2rtc - Docker Pulls\">\n  </a>\n  <a href=\"https://github.com/AlexxIT/go2rtc/releases\" target=\"_blank\">\n    <img style=\"display: inline\" src=\"https://img.shields.io/github/downloads/AlexxIT/go2rtc/total?color=blue&style=flat-square&logo=github\" alt=\"go2rtc - GitHub Downloads\">\n  </a>\n</p>\n<p align=\"center\">\n  <a href=\"https://trendshift.io/repositories/4628\" target=\"_blank\">\n    <img src=\"https://trendshift.io/api/badge/repositories/4628\" alt=\"go2rtc - Trendshift\"/>\n  </a>\n</p>\n\nUltimate camera streaming application with support for dozens formats and protocols.\n\n- zero-dependency [small app](#go2rtc-binary) for all OS (Windows, macOS, Linux, FreeBSD)\n- zero-delay for many [supported protocols](#codecs-madness) (lowest possible streaming latency)\n- [streaming input](#streaming-input) from dozens formats and protocols\n- [streaming output](#streaming-output) in all popular formats\n- [streaming ingest](#streaming-ingest) in a number of popular formats\n- [publish](#publish-stream) any source to popular streaming services (YouTube, Telegram)\n- on-the-fly transcoding only if necessary via [FFmpeg](internal/ffmpeg/README.md)\n- [two-way audio](#two-way-audio) support for many formats\n- [streaming audio](#stream-to-camera) to all cameras with [two-way audio](#two-way-audio) support\n- mixing tracks from different sources to single stream\n- [auto-match](www/README.md#javascript-api) client-supported streaming formats and codecs\n- [streaming stats](#streaming-stats) for all active connections\n- can be [integrated to any project](#projects-using-go2rtc) or be used as [standalone app](#go2rtc-binary)\n\n#### Inspired by\n\n- series of streaming projects from [@deepch](https://github.com/deepch)\n- [webrtc](https://github.com/pion/webrtc) go library and whole [@pion](https://github.com/pion) team\n- [rtsp-simple-server](https://github.com/aler9/rtsp-simple-server) idea from [@aler9](https://github.com/aler9)\n- [GStreamer](https://gstreamer.freedesktop.org/) framework pipeline idea\n- [MediaSoup](https://mediasoup.org/) framework routing idea\n- HomeKit Accessory Protocol from [@brutella](https://github.com/brutella/hap)\n- creator of the project's logo [@v_novoseltsev](https://www.instagram.com/v_novoseltsev)\n\n<br>\n<details>\n<summary><b>Table of Contents</b></summary>\n\n- [Installation](#installation)\n  - [go2rtc: Binary](#go2rtc-binary)\n  - [go2rtc: Docker](#go2rtc-docker)\n  - [go2rtc: Home Assistant add-on](#go2rtc-home-assistant-add-on)\n  - [go2rtc: Home Assistant Integration](#go2rtc-home-assistant-integration)\n  - [go2rtc: Master version](#go2rtc-master-version)\n- [Configuration](#configuration)\n- [Features](#features)\n  - [Streaming input](#streaming-input)\n  - [Streaming output](#streaming-output)\n  - [Streaming ingest](#streaming-ingest)\n  - [Two-way audio](#two-way-audio)\n  - [Stream to camera](#stream-to-camera)\n  - [Publish stream](#publish-stream)\n  - [Preload stream](#preload-stream)\n  - [Streaming stats](#streaming-stats)\n- [Codecs](#codecs)\n  - [Codecs filters](#codecs-filters)\n  - [Codecs madness](#codecs-madness)\n  - [Built-in transcoding](#built-in-transcoding)\n  - [Codecs negotiation](#codecs-negotiation)\n- [Security](#security)\n- [Projects using go2rtc](#projects-using-go2rtc)\n- [Camera experience](#camera-experience)\n- [Tips](#tips)\n\n</details>\n\n## Installation\n\n1. Download [binary](#go2rtc-binary) or use [Docker](#go2rtc-docker) or Home Assistant [add-on](#go2rtc-home-assistant-add-on) or [integration](#go2rtc-home-assistant-integration)\n2. Open web interface: `http://localhost:1984/`\n3. Add [streams](#streaming-input) to [config](#configuration)\n\n**Developers:** integrate [HTTP API](internal/api/README.md) into your smart home platform.\n\n### go2rtc: Binary\n\nDownload binary for your OS from [latest release](https://github.com/AlexxIT/go2rtc/releases/):\n\n| name                                                                                                            | description                                                                                                                               |\n|-----------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------|\n| [go2rtc_win64.zip](https://github.com/AlexxIT/go2rtc/releases/latest/download/go2rtc_win64.zip)                 | Windows 10+ 64-bit                                                                                                                        |\n| [go2rtc_win32.zip](https://github.com/AlexxIT/go2rtc/releases/latest/download/go2rtc_win32.zip)                 | Windows 10+ 32-bit                                                                                                                        |\n| [go2rtc_win_arm64.zip](https://github.com/AlexxIT/go2rtc/releases/latest/download/go2rtc_win_arm64.zip)         | Windows ARM 64-bit                                                                                                                        |\n| [go2rtc_linux_amd64](https://github.com/AlexxIT/go2rtc/releases/latest/download/go2rtc_linux_amd64)             | Linux 64-bit                                                                                                                              |\n| [go2rtc_linux_i386](https://github.com/AlexxIT/go2rtc/releases/latest/download/go2rtc_linux_i386)               | Linux 32-bit                                                                                                                              |\n| [go2rtc_linux_arm64](https://github.com/AlexxIT/go2rtc/releases/latest/download/go2rtc_linux_arm64)             | Linux ARM 64-bit (ex. Raspberry 64-bit OS)                                                                                                |\n| [go2rtc_linux_arm](https://github.com/AlexxIT/go2rtc/releases/latest/download/go2rtc_linux_arm)                 | Linux ARM 32-bit (ex. Raspberry 32-bit OS)                                                                                                |\n| [go2rtc_linux_armv6](https://github.com/AlexxIT/go2rtc/releases/latest/download/go2rtc_linux_armv6)             | Linux ARMv6 (for old Raspberry 1 and Zero)                                                                                                |\n| [go2rtc_linux_mipsel](https://github.com/AlexxIT/go2rtc/releases/latest/download/go2rtc_linux_mipsel)           | Linux MIPS (ex. [Xiaomi Gateway 3](https://github.com/AlexxIT/XiaomiGateway3), [Wyze cameras](https://github.com/gtxaspec/wz_mini_hacks)) |\n| [go2rtc_mac_amd64.zip](https://github.com/AlexxIT/go2rtc/releases/latest/download/go2rtc_mac_amd64.zip)         | macOS 11+ Intel 64-bit                                                                                                                    |\n| [go2rtc_mac_arm64.zip](https://github.com/AlexxIT/go2rtc/releases/latest/download/go2rtc_mac_arm64.zip)         | macOS ARM 64-bit                                                                                                                          |\n| [go2rtc_freebsd_amd64.zip](https://github.com/AlexxIT/go2rtc/releases/latest/download/go2rtc_freebsd_amd64.zip) | FreeBSD 64-bit                                                                                                                            |\n| [go2rtc_freebsd_arm64.zip](https://github.com/AlexxIT/go2rtc/releases/latest/download/go2rtc_freebsd_arm64.zip) | FreeBSD ARM 64-bit                                                                                                                        |\n\nDon't forget to fix the rights `chmod +x go2rtc_xxx_xxx` on Linux and Mac.\n\nPS. The application is compiled with the latest versions of the Go language for maximum speed and security. Therefore, the [minimum OS versions](https://go.dev/wiki/MinimumRequirements) depend on the Go language.\n\n### go2rtc: Docker\n\nThe Docker containers [`alexxit/go2rtc`](https://hub.docker.com/r/alexxit/go2rtc) and [`ghcr.io/alexxit/go2rtc`](https://github.com/AlexxIT/go2rtc/pkgs/container/go2rtc) support multiple architectures including `386`, `amd64`, `arm/v6`, `arm/v7` and `arm64`.\nThese containers offer the same functionality as the Home Assistant [add-on](#go2rtc-home-assistant-add-on) but are designed to operate independently of Home Assistant.\nIt comes preinstalled with [FFmpeg](internal/ffmpeg/README.md) and [Python](internal/echo/README.md).\n\n### go2rtc: Home Assistant add-on\n\n[![Open your Home Assistant instance and show the add add-on repository dialog with a specific repository URL pre-filled.](https://my.home-assistant.io/badges/supervisor_add_addon_repository.svg)](https://my.home-assistant.io/redirect/supervisor_add_addon_repository/?repository_url=https%3A%2F%2Fgithub.com%2FAlexxIT%2Fhassio-addons)\n\n1. Settings > Add-ons > Plus > Repositories > Add\n   ```\n   https://github.com/AlexxIT/hassio-addons\n   ```\n2. go2rtc > Install > Start\n\n### go2rtc: Home Assistant Integration\n\n[WebRTC Camera](https://github.com/AlexxIT/WebRTC) custom component can be used on any Home Assistant [installation](https://www.home-assistant.io/installation/), including [HassWP](https://github.com/AlexxIT/HassWP) on Windows. It can automatically download and use the latest version of go2rtc. Or it can connect to an existing version of go2rtc. Addon installation in this case is optional.\n\n### go2rtc: Master version\n\nLatest, but maybe unstable version:\n\n- Binary: [latest master build](https://nightly.link/AlexxIT/go2rtc/workflows/build/master)\n- Docker: `alexxit/go2rtc:master` or `alexxit/go2rtc:master-hardware` versions\n- Home Assistant add-on: `go2rtc master` or `go2rtc master hardware` versions\n\n## Configuration\n\nThis is the `go2rtc.yaml` file in [YAML-format](https://en.wikipedia.org/wiki/YAML).\nThe configuration can be changed in the [WebUI](www/README.md) at `http://localhost:1984`.\nThe editor provides syntax highlighting and checking.\n\n![go2rtc webui config](website/images/webui-config.png)\n\nThe simplest config looks like this:\n\n```yaml\nstreams:\n  hall-camera: rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0\n```\n\n- by default go2rtc will search `go2rtc.yaml` in the current work directory\n- `api` server will start on default **1984 port** (TCP)\n- `rtsp` server will start on default **8554 port** (TCP)\n- `webrtc` will use port **8555** (TCP/UDP) for connections\n\nMore information can be [found here](internal/app/README.md).\n\n## Features\n\nA summary table of all modules and features can be found [here](internal/README.md).\n\n**Core modules**\n\n- [`app`](internal/app/README.md) - Reading [configs](internal/app/README.md) and setting up [logs](internal/app/README.md#log).\n- [`api`](internal/api/README.md) - Handle [HTTP](internal/api/README.md) and [WebSocket](internal/api/ws/README.md) API.\n- [`streams`](internal/streams/README.md) - Handle a list of streams.\n\n### Streaming input\n\n#### public protocols\n\n- [`mpjpeg`](internal/mjpeg/README.md#mjpeg-client) - The legacy but still used [MJPEG](https://en.wikipedia.org/wiki/Motion_JPEG) protocol for real-time media transmission.\n- [`onvif`](internal/onvif/README.md#onvif-client) - A popular [ONVIF](https://en.wikipedia.org/wiki/ONVIF) protocol for receiving media in RTSP format.\n- [`rtmp`](internal/rtmp/README.md#rtmp-client) - The legacy but still used [RTMP](https://en.wikipedia.org/wiki/Real-Time_Messaging_Protocol) protocol for real-time media transmission.\n- [`rtsp`](internal/rtsp/README.md#rtsp-client) - The most common [RTSP](https://en.wikipedia.org/wiki/Real-Time_Streaming_Protocol) protocol for real-time media transmission.\n- [`webrtc`](internal/webrtc/README.md#webrtc-client) - [WebRTC](https://en.wikipedia.org/wiki/WebRTC) web-compatible protocol for real-time media transmission.\n- [`yuv4mpegpipe`](internal/http/README.md#tcp) - Raw [YUV](https://en.wikipedia.org/wiki/Y%E2%80%B2UV) frame stream with [YUV4MPEG](https://manned.org/yuv4mpeg) header.\n\n#### private protocols\n\n- [`bubble`](internal/bubble/README.md) - Some NVR from [dvr163.com](http://help.dvr163.com/) and [eseecloud.com](http://www.eseecloud.com/).\n- [`doorbird`](internal/doorbird/README.md) - [Doorbird](https://www.doorbird.com/) devices with two-way audio.\n- [`dvrip`](internal/dvrip/README.md) - DVR-IP NVR, NetSurveillance, Sofia protocol (XMeye SDK).\n- [`eseecloud`](internal/eseecloud/README.md) - Some NVR from [dvr163.com](http://help.dvr163.com/) and [eseecloud.com](http://www.eseecloud.com/).\n- [`gopro`](internal/gopro/README.md) - [GoPro](https://gopro.com/) cameras, connected via USB or Wi-Fi.\n- [`hass`](internal/hass/README.md) - Import cameras from [Home Assistant](https://www.home-assistant.io/) config files.\n- [`homekit`](internal/homekit/README.md) - Cameras with [Apple HomeKit](https://www.apple.com/home-app/accessories/) protocol.\n- [`isapi`](internal/isapi/README.md) - Two-way audio for [Hikvision ISAPI](https://tpp.hikvision.com/download/ISAPI_OTAP) protocol.\n- [`kasa`](internal/kasa/README.md) - [TP-Link Kasa](https://www.kasasmart.com/) cameras.\n- [`multitrans`](internal/multitrans/README.md) - Two-way audio for Chinese version of [TP-Link](https://www.tp-link.com.cn/) cameras.\n- [`nest`](internal/nest/README.md) - [Google Nest](https://developers.google.com/nest/device-access/supported-devices) cameras through user-unfriendly and paid APIs.\n- [`ring`](internal/ring/README.md) - Ring cameras with two-way audio support.\n- [`roborock`](internal/roborock/README.md) - [Roborock](https://roborock.com/) vacuums with cameras with two-way audio support. \n- [`tapo`](internal/tapo/README.md) - [TP-Link Tapo](https://www.tapo.com/) cameras with two-way audio support.\n- [`vigi`](internal/tapo/README.md#tp-link-vigi) - TP-Link Vigi cameras.\n- [`tuya`](internal/tuya/README.md) - [Tuya](https://www.tuya.com/) ecosystem cameras with two-way audio support.\n- [`webtorrent`](internal/webtorrent/README.md) - Stream from another go2rtc via [WebTorrent](https://en.wikipedia.org/wiki/WebTorrent) protocol.\n- [`wyze`](internal/wyze/README.md) - [Wyze](https://wyze.com/) cameras using native P2P protocol\n- [`xiaomi`](internal/xiaomi/README.md) - [Xiaomi Mi Home](https://home.mi.com/) ecosystem cameras with two-way audio support.\n\n#### devices\n\n- [`alsa`](internal/alsa/README.md) - A [framework](https://en.wikipedia.org/wiki/Advanced_Linux_Sound_Architecture) for receiving audio from devices on Linux OS.\n- [`v4l2`](internal/v4l2/README.md) - A [framework](https://en.wikipedia.org/wiki/Video4Linux) for receiving video from devices on Linux OS.\n\n#### files\n\n- [`adts`](internal/http/README.md#tcp) - Audio stream in [AAC](https://en.wikipedia.org/wiki/Advanced_Audio_Coding) codec with Audio Data Transport Stream headers.\n- [`flv`](internal/http/README.md#tcp) - The legacy but still used [Flash Video](https://en.wikipedia.org/wiki/Flash_Video) format.\n- [`h264`](internal/http/README.md#tcp) - AVC/H.264 bitstream.\n- [`hevc`](internal/http/README.md#tcp) - HEVC/H.265 bitstream.\n- [`hls`](internal/http/README.md) - A popular [HTTP Live Streaming](https://en.wikipedia.org/wiki/HTTP_Live_Streaming) format.\n- [`mjpeg`](internal/http/README.md#tcp) - A continuous sequence of JPEG frames (without HTTP headers).\n- [`mpegts`](internal/http/README.md#tcp) - The legacy [MPEG transport stream](https://en.wikipedia.org/wiki/MPEG_transport_stream) format.\n- [`wav`](internal/http/README.md#tcp) - Audio stream in [Waveform Audio File](https://en.wikipedia.org/wiki/WAV) format.\n\n#### scripts\n\n- [`echo`](internal/echo/README.md) - If the source has a dynamic link, you can use a bash or python script to get it.\n- [`exec`](internal/exec/README.md) - You can run an external application (`ffmpeg`, `gstreamer`, `rpicam`, etc.) and receive a media stream from it.\n- [`expr`](internal/expr/README.md) - If the source has a dynamic link, you can use [Expr](https://github.com/expr-lang/expr) language to get it.\n- [`ffmpeg`](internal/ffmpeg/README.md) - Use [FFmpeg](https://ffmpeg.org/) as a stream source. Hardware-accelerated transcoding and streaming from USB devices are supported.\n\n#### webrtc\n\n- [`creality`](internal/webrtc/README.md#creality) - [Creality](https://www.creality.com/) 3D printer cameras.\n- [`kinesis`](internal/webrtc/README.md#kinesis) - [Amazon Kinesis](https://aws.amazon.com/kinesis/video-streams/) video streams.\n- [`openipc`](internal/webrtc/README.md#openipc) - Cameras on open-source [OpenIPC](https://openipc.org/) firmware.\n- [`switchbot`](internal/webrtc/README.md#switchbot) - [SwitchBot](https://us.switch-bot.com/) cameras.\n- [`whep`](internal/webrtc/README.md#whep) - [WebRTC/WHEP](https://datatracker.ietf.org/doc/draft-murillo-whep/) is replaced by [WebRTC/WISH](https://datatracker.ietf.org/doc/charter-ietf-wish/02/) standard for WebRTC video/audio viewers.\n- [`wyze`](internal/webrtc/README.md#wyze) - Legacy method to connect to [Wyze](https://www.wyze.com/) cameras via [docker-wyze-bridge](https://github.com/mrlt8/docker-wyze-bridge).\n\n### Streaming output\n\n- [`adts`](internal/mpeg/README.md) - Output stream in ADTS format with [AAC](https://en.wikipedia.org/wiki/Advanced_Audio_Coding) audio.\n- [`ascii`](internal/mjpeg/README.md#ascii) - Just for fun stream as [ASCII to Terminal](https://www.youtube.com/watch?v=sHj_3h_sX7M).\n- [`flv`](internal/rtmp/README.md) - Output stream in [Flash Video](https://en.wikipedia.org/wiki/Flash_Video) format.\n- [`hls`](internal/hls/README.md) - Output stream in [HTTP Live Streaming](https://en.wikipedia.org/wiki/HTTP_Live_Streaming) format.\n- [`homekit`](internal/homekit/README.md#homekit-server) - Output stream to [Apple Home](https://www.apple.com/home-app/) using [HomeKit](https://en.wikipedia.org/wiki/Apple_Home) protocol.\n- [`jpeg`](internal/mjpeg/README.md#jpeg) - Output snapshots in [JPEG](https://en.wikipedia.org/wiki/JPEG) format.\n- [`mpjpeg`](internal/mjpeg/README.md#mpjpeg) - Output a stream in [MJPEG](https://en.wikipedia.org/wiki/Motion_JPEG) format.\n- [`mp4`](internal/mp4/README.md) - Output as [MP4 stream](https://en.wikipedia.org/wiki/Progressive_download) or [Media Source Extensions](https://developer.mozilla.org/en-US/docs/Web/API/Media_Source_Extensions_API) (MSE) compatible format.\n- [`mpegts`](internal/mpeg/README.md) - Output stream in [MPEG transport stream](https://en.wikipedia.org/wiki/MPEG_transport_stream) format.\n- [`onvif`](internal/onvif/README.md#onvif-server) - Output stream using [ONVIF](https://en.wikipedia.org/wiki/ONVIF) protocol.\n- [`rtmp`](internal/rtmp/README.md#rtmp-server) - Output stream using [Real-Time Messaging](https://en.wikipedia.org/wiki/Real-Time_Messaging_Protocol) protocol.\n- [`rtsp`](internal/rtsp/README.md#rtsp-server) - Output stream using [Real-Time Streaming](https://en.wikipedia.org/wiki/Real-Time_Streaming_Protocol) protocol.\n- [`webrtc`](internal/webrtc/README.md#webrtc-server) - Output stream using [Web Real-Time Communication](https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API) API.\n- [`webtorrent`](internal/webtorrent/README.md#webtorrent-server) - Output stream using [WebTorrent](https://en.wikipedia.org/wiki/WebTorrent) protocol.\n- [`yuv4mpegpipe`](internal/mjpeg/README.md#yuv4mpegpipe) - Output in raw [YUV](https://en.wikipedia.org/wiki/Y%E2%80%B2UV) frame stream with [YUV4MPEG](https://manned.org/yuv4mpeg) header.\n\n### Streaming ingest\n\nSupported for: \n[`flv`](internal/rtmp/README.md#flv-server), \n[`mjpeg`](internal/mjpeg/README.md#streaming-ingest), \n[`mpegts`](internal/mpeg/README.md#streaming-ingest), \n[`rtmp`](internal/rtmp/README.md#rtmp-server), \n[`rtsp`](internal/rtsp/README.md#streaming-ingest), \n[`webrtc`](internal/webrtc/README.md#streaming-ingest).\n\nThis is a feature when go2rtc expects to receive an incoming stream from an external application. The stream transmission is started and stopped by an external application.\n\n- You can push data only to an existing stream (create a stream with empty source in config).\n- You can push multiple incoming sources to the same stream.\n- You can push data to a non-empty stream, so it will have additional codecs inside.\n\n### Two-way audio\n\nSupported for:\n[`doorbird`](internal/doorbird/README.md), \n[`dvrip`](internal/dvrip/README.md), \n[`exec`](internal/exec/README.md), \n[`isapi`](internal/isapi/README.md), \n[`multitrans`](internal/multitrans/README.md), \n[`ring`](internal/ring/README.md), \n[`roborock`](internal/roborock/README.md), \n[`rtsp`](internal/rtsp/README.md#two-way-audio), \n[`tapo`](internal/tapo/README.md), \n[`tuya`](internal/tuya/README.md), \n[`webrtc`](internal/webrtc/README.md), \n[`wyze`](internal/wyze/README.md), \n[`xiaomi`](internal/xiaomi/README.md).\n\nTwo-way audio can be used in browser with [WebRTC](internal/webrtc/README.md) technology. The browser will give access to the microphone only for HTTPS sites ([read more](https://stackoverflow.com/questions/52759992/how-to-access-camera-and-microphone-in-chrome-without-https)).\n\n### Stream to camera\n\nYou can play audio files or live streams on any camera with [two-way audio](#two-way-audio) support.\n\n[read more](internal/streams/README.md#stream-to-camera)\n\n### Publish stream\n\nYou can publish any stream to streaming services (YouTube, Telegram, etc.) via RTMP/RTMPS.\n\n[read more](internal/streams/README.md#publish-stream)\n\n### Preload stream\n\nYou can preload any stream on go2rtc start. This is useful for cameras that take a long time to start up.\n\n[read more](internal/streams/README.md#preload-stream)\n\n### Streaming stats\n\n[WebUI](www/README.md) provides detailed information about all active connections, including IP-addresses, formats, protocols, number of packets and bytes transferred. \nVia the [HTTP API](internal/api/README.md) in [`json`](https://en.wikipedia.org/wiki/JSON) or [`dot`](https://en.wikipedia.org/wiki/DOT_(graph_description_language)) format on an interactive connection map.\n\n![go2rtc webui net](website/images/webui-net.png)\n\n## Codecs\n\nIf you have questions about why video or audio is not displayed, you need to read the following sections.\n\n| Name                         | FFmpeg   | RTSP          | Aliases     |\n|------------------------------|----------|---------------|-------------|\n| Advanced Audio Coding        | `aac`    | MPEG4-GENERIC |             |\n| Advanced Video Coding        | `h264`   | H264          | AVC, H.264  |\n| G.711 PCM (A-law)            | `alaw`   | PCMA          | G711A       |\n| G.711 PCM (µ-law)            | `mulaw`  | PCMU          | G711u       |\n| High Efficiency Video Coding | `hevc`   | H265          | HEVC, H.265 |\n| Motion JPEG                  | `mpjpeg` | JPEG          |             |\n| MPEG-1 Audio Layer III       | `mp3`    | MPA           |             |\n| Opus Codec                   | `opus`   | OPUS          |             |\n| PCM signed 16-bit big-endian | `s16be`  | L16           |             |\n\n### Codecs filters\n\ngo2rtc can automatically detect which codecs your device supports for [WebRTC](internal/webrtc/README.md) and [MSE](internal/mp4/README.md) technologies.\n\nBut it cannot be done for [RTSP](internal/rtsp/README.md), [HTTP progressive streaming](internal/mp4/README.md), [HLS](internal/hls/README.md) technologies. \nYou can manually add a codec filter when you create a link to a stream. \nThe filters work the same for all three technologies. \nFilters do not create a new codec, they only select the suitable codec from existing sources. \nYou can add new codecs to the stream using the [FFmpeg transcoding](internal/ffmpeg/README.md).\n\nWithout filters:\n\n- RTSP will provide only the first video and only the first audio (any codec)\n- MP4 will include only compatible codecs (H264, H265, AAC)\n- HLS will output in the legacy TS format (H264 without audio)\n\nSome examples:\n\n- `rtsp://192.168.1.123:8554/camera1?mp4` - useful for recording as MP4 files (e.g. Home Assistant or Frigate)\n- `rtsp://192.168.1.123:8554/camera1?video=h264,h265&audio=aac` - full version of the filter above\n- `rtsp://192.168.1.123:8554/camera1?video=h264&audio=aac&audio=opus` - H264 video codec and two separate audio tracks\n- `rtsp://192.168.1.123:8554/camera1?video&audio=all` - any video codec and all audio codecs as separate tracks\n- `http://192.168.1.123:1984/api/stream.m3u8?src=camera1&mp4` - HLS stream with MP4 compatible codecs (HLS/fMP4)\n- `http://192.168.1.123:1984/api/stream.m3u8?src=camera1&mp4=flac` - HLS stream with PCMA/PCMU/PCM audio support (HLS/fMP4), won't work on old devices\n- `http://192.168.1.123:1984/api/stream.mp4?src=camera1&mp4=flac` - MP4 file with PCMA/PCMU/PCM audio support, won't work on old devices (ex. iOS 12)\n- `http://192.168.1.123:1984/api/stream.mp4?src=camera1&mp4=all` - MP4 file with non-standard audio codecs, won't work on some players\n\n### Codecs madness\n\n`AVC/H.264` video can be played almost anywhere. But `HEVC/H.265` has many limitations in supporting different devices and browsers.\n\n| Device                                                             | WebRTC                                  | MSE                                     | HTTP*                                        | HLS                         |\n|--------------------------------------------------------------------|-----------------------------------------|-----------------------------------------|----------------------------------------------|-----------------------------|\n| *latency*                                                          | best                                    | medium                                  | bad                                          | bad                         |\n| Desktop Chrome 136+ <br/> Desktop Edge <br/> Android Chrome 136+   | H264, H265* <br/> PCMU, PCMA <br/> OPUS | H264, H265* <br/> AAC, FLAC* <br/> OPUS | H264, H265* <br/> AAC, FLAC* <br/> OPUS, MP3 | no                          |\n| Desktop Firefox                                                    | H264 <br/> PCMU, PCMA <br/> OPUS        | H264 <br/> AAC, FLAC* <br/> OPUS        | H264 <br/> AAC, FLAC* <br/> OPUS             | no                          |\n| Desktop Safari 14+ <br/> iPad Safari 14+ <br/> iPhone Safari 17.1+ | H264, H265* <br/> PCMU, PCMA <br/> OPUS | H264, H265 <br/> AAC, FLAC*             | **no!**                                      | H264, H265 <br/> AAC, FLAC* |\n| iPhone Safari 14+                                                  | H264, H265* <br/> PCMU, PCMA <br/> OPUS | **no!**                                 | **no!**                                      | H264, H265 <br/> AAC, FLAC* |\n| macOS [Hass App][1]                                                | no                                      | no                                      | no                                           | H264, H265 <br/> AAC, FLAC* |\n\n[1]: https://apps.apple.com/app/home-assistant/id1099568401\n\n- `HTTP*` - HTTP Progressive Streaming, not related to [progressive download](https://en.wikipedia.org/wiki/Progressive_download), because the file has no size and no end\n- `WebRTC H265` - supported in [Chrome 136+](https://developer.chrome.com/release-notes/136), supported in [Safari 18+](https://developer.apple.com/documentation/safari-release-notes/safari-18-release-notes)\n- `MSE iPhone` - supported in [iOS 17.1+](https://webkit.org/blog/14735/webkit-features-in-safari-17-1/)\n\n**Audio**\n\n- go2rtc supports [automatic repackaging](#built-in-transcoding) of `PCMA/PCMU/PCM` codecs into `FLAC` for MSE/MP4/HLS so they'll work almost anywhere\n- **WebRTC** audio codecs: `PCMU/8000`, `PCMA/8000`, `OPUS/48000/2`\n- `OPUS` and `MP3` inside **MP4** are part of the standard, but some players do not support them anyway (especially Apple)\n\n**Apple devices**\n\n- all Apple devices don't support HTTP progressive streaming\n- old iPhone firmwares don't support MSE technology because it competes with the HTTP Live Streaming (HLS) technology, invented by Apple\n- HLS is the worst technology for **live** streaming, it still exists only because of iPhones\n\n### Built-in transcoding\n\nThere are no plans to embed complex transcoding algorithms inside go2rtc. \n[FFmpeg source](internal/ffmpeg/README.md) does a great job with this. \nIncluding [hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration) support.\n\nBut go2rtc has some simple algorithms. They are turned on automatically; you do not need to set them up additionally.\n\n**PCM for MSE/MP4/HLS**\n\nGo2rtc can pack `PCMA`, `PCMU` and `PCM` codecs into an MP4 container so that they work in all browsers and all built-in players on modern devices. Including Apple QuickTime:\n\n```text\nPCMA/PCMU => PCM => FLAC => MSE/MP4/HLS\n```\n\n**Resample PCMA/PCMU for WebRTC**\n\nBy default WebRTC supports only `PCMA/8000` and `PCMU/8000`. But go2rtc can automatically resample PCMA and PCMU codecs with a different sample rate. Also, go2rtc can transcode `PCM` codec to `PCMA/8000`, so WebRTC can play it:\n\n```text\nPCM/xxx => PCMA/8000 => WebRTC\nPCMA/xxx => PCMA/8000 => WebRTC\nPCMU/xxx => PCMU/8000 => WebRTC\n```\n\n**Important**\n\n- FLAC codec not supported in an RTSP stream. If you are using Frigate or Home Assistant for recording MP4 files with PCMA/PCMU/PCM audio, you should set up transcoding to the AAC codec.\n- PCMA and PCMU are VERY low-quality codecs. They support only 256! different sounds. Use them only when you have no other options.\n\n### Codecs negotiation\n\nFor example, you want to watch an RTSP stream from a [Dahua IPC-K42](https://www.dahuasecurity.com/fr/products/All-Products/Network-Cameras/Wireless-Series/Wi-Fi-Series/4MP/IPC-K42) camera in your Chrome browser.\n\n- this camera supports two-way audio standard **ONVIF Profile T**\n- this camera supports codecs **H264, H265** for sending video, and you select `H264` in camera settings\n- this camera supports codecs **AAC, PCMU, PCMA** for sending audio (from mic), and you select `AAC/16000` in camera settings\n- this camera supports codecs **AAC, PCMU, PCMA** for receiving audio (to speaker), you don't need to select them\n- your browser supports codecs **H264, VP8, VP9, AV1** for receiving video, you don't need to select them\n- your browser supports codecs **OPUS, PCMU, PCMA** for sending and receiving audio, you don't need to select them\n- you can't get the camera audio directly because its audio codecs don't match your browser's codecs\n    - so you decide to use transcoding via FFmpeg and add this setting to the config YAML file\n    - you have chosen `OPUS/48000/2` codec, because it is higher quality than the `PCMU/8000` or `PCMA/8000`\n\nNow you have a stream with two sources - **RTSP and FFmpeg**:\n\n```yaml\nstreams:\n  dahua:\n    - rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif\n    - ffmpeg:rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0#audio=opus\n```\n\n**go2rtc** automatically matches codecs for your browser across all of your stream sources. This is called **multi-source two-way codec negotiation**, and it's one of the main features of this app.\n\n**PS.** You can select `PCMU` or `PCMA` codec in camera settings and not use transcoding at all. Or you can select `AAC` codec for main stream and `PCMU` codec for second stream and add both RTSP to YAML config, this also will work fine.\n\n## Security\n\n> [!IMPORTANT]\n> If an attacker gains access to the API, you are in danger. Through the API, an attacker can use insecure sources such as echo and exec. And get full access to your server.\n\nFor maximum (paranoid) security, go2rtc has special settings:\n\n```yaml\napp:\n  # use only allowed modules\n  modules: [api, rtsp, webrtc, exec, ffmpeg, mjpeg]\n\napi:\n  # use only allowed API paths\n  allow_paths: [/api, /api/streams, /api/webrtc, /api/frame.jpeg]\n  # enable auth for localhost (used together with username and password)\n  local_auth: true\n\nexec:\n  # use only allowed exec paths\n  allow_paths: [ffmpeg]\n```\n\nBy default, `go2rtc` starts the Web interface on port `1984` and RTSP on port `8554`, as well as uses port `8555` for WebRTC connections. The three ports are accessible from your local network. So anyone on your local network can watch video from your cameras without authorization. The same rule applies to the Home Assistant add-on.\n\nThis is not a problem if you trust your local network as much as I do. But you can change this behaviour with a `go2rtc.yaml` config:\n\n```yaml\napi:\n  listen: \"127.0.0.1:1984\" # localhost\n\nrtsp:\n  listen: \"127.0.0.1:8554\" # localhost\n\nwebrtc:\n  listen: \":8555\" # external TCP/UDP port\n```\n\n- local access to RTSP is not a problem for [FFmpeg](internal/ffmpeg/README.md) integration, because it runs locally on your server\n- local access to API is not a problem for the [Home Assistant add-on](#go2rtc-home-assistant-add-on), because Home Assistant runs locally on the same server, and the add-on web UI is protected with Home Assistant authorization ([Ingress feature](https://www.home-assistant.io/blog/2019/04/15/hassio-ingress/))\n- external access to WebRTC TCP port is not a problem, because it is used only for transmitting encrypted media data\n    - anyway you need to open this port to your local network and to the Internet for WebRTC to work\n\nIf you need web interface protection without the Home Assistant add-on, you need to use a reverse proxy, like [Nginx](https://nginx.org/), [Caddy](https://caddyserver.com/), etc.\n\nPS. Additionally, WebRTC will try to use the 8555 UDP port to transmit encrypted media. It works without problems on the local network, and sometimes also works for external access, even if you haven't opened this port on your router ([read more](https://en.wikipedia.org/wiki/UDP_hole_punching)). But for stable external WebRTC access, you need to open the 8555 port on your router for both TCP and UDP.\n\n## Projects using go2rtc\n\n- [Home Assistant](https://www.home-assistant.io/) [2024.11+](https://www.home-assistant.io/integrations/go2rtc/) - top open-source smart home project\n- [Frigate](https://frigate.video/) [0.12+](https://docs.frigate.video/guides/configuring_go2rtc/) - open-source NVR built around real-time AI object detection\n- [Advanced Camera Card](https://github.com/dermotduffy/advanced-camera-card) - custom card for Home Assistant\n- [OpenIPC](https://github.com/OpenIPC/firmware/tree/master/general/package/go2rtc) - alternative IP camera firmware from an open community\n- [wz_mini_hacks](https://github.com/gtxaspec/wz_mini_hacks) - custom firmware for Wyze cameras\n- [EufyP2PStream](https://github.com/oischinger/eufyp2pstream) - a small project that provides a video/audio stream from Eufy cameras that don't directly support RTSP\n- [ioBroker.euSec](https://github.com/bropat/ioBroker.eusec) - [ioBroker](https://www.iobroker.net/) adapter for controlling Eufy security devices\n- [MMM-go2rtc](https://github.com/Anonym-tsk/MMM-go2rtc) - MagicMirror² module\n- [ring-mqtt](https://github.com/tsightler/ring-mqtt) - Ring-to-MQTT bridge\n- [lightNVR](https://github.com/opensensor/lightNVR)\n\n**Distributions**\n\n- [Alpine Linux](https://pkgs.alpinelinux.org/packages?name=go2rtc)\n- [Arch User Repository](https://linux-packages.com/aur/package/go2rtc)\n- [Gentoo](https://github.com/inode64/inode64-overlay/tree/main/media-video/go2rtc)\n- [NixOS](https://search.nixos.org/packages?query=go2rtc)\n- [Proxmox Helper Scripts](https://github.com/community-scripts/ProxmoxVE/)\n- [QNAP](https://www.myqnap.org/product/go2rtc/)\n- [Synology NAS](https://synocommunity.com/package/go2rtc)\n- [Unraid](https://unraid.net/community/apps?q=go2rtc)\n\n## Camera experience\n\n- [Dahua](https://www.dahuasecurity.com/) - reference implementation streaming protocols, a lot of settings, high stream quality, multiple streaming clients\n- [EZVIZ](https://www.ezviz.com/) - awful RTSP protocol implementation, many bugs in SDP\n- [Hikvision](https://www.hikvision.com/) - a lot of proprietary streaming technologies\n- [Reolink](https://reolink.com/) - some models have an awful, unusable RTSP implementation and not the best RTMP alternative (I recommend that you contact Reolink support for new firmware), few settings\n- [Sonoff](https://sonoff.tech/) - very low stream quality, no settings, not the best protocol implementation\n- [TP-Link](https://www.tp-link.com/) - few streaming clients, packet loss?\n- Cheap noname cameras, Wyze Cams, Xiaomi cameras with hacks (usually have `/live/ch00_1` in RTSP URL) - awful but usable RTSP protocol implementation, low stream quality, few settings, packet loss?\n\n## Tips\n\n**Using apps for low RTSP delay**\n\n- `ffplay -fflags nobuffer -flags low_delay \"rtsp://192.168.1.123:8554/camera1\"`\n- VLC > Preferences > Input / Codecs > Default Caching Level: Lowest Latency\n\n**Snapshots to Telegram**\n\n[read more](https://github.com/AlexxIT/go2rtc/wiki/Snapshot-to-Telegram)\n"
  },
  {
    "path": "docker/Dockerfile",
    "content": "# syntax=docker/dockerfile:labs\n\n# 0. Prepare images\nARG PYTHON_VERSION=\"3.13\"\nARG GO_VERSION=\"1.25\"\n\n\n# 1. Build go2rtc binary\nFROM --platform=$BUILDPLATFORM golang:${GO_VERSION}-alpine AS build\nARG TARGETPLATFORM\nARG TARGETOS\nARG TARGETARCH\n\nENV GOOS=${TARGETOS}\nENV GOARCH=${TARGETARCH}\n\nWORKDIR /build\n\nRUN apk add git\n\n# Cache dependencies\nCOPY go.mod go.sum ./\nRUN --mount=type=cache,target=/root/.cache/go-build go mod download\n\nCOPY . .\nRUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags \"-s -w\" -trimpath\n\n\n# 2. Final image\nFROM python:${PYTHON_VERSION}-alpine AS base\n\n# Install ffmpeg, tini (for signal handling),\n# and other common tools for the echo source.\n# alsa-plugins-pulse for ALSA support (+0MB)\n# font-droid for FFmpeg drawtext filter (+2MB)\nRUN apk add --no-cache tini ffmpeg ffplay bash curl jq alsa-plugins-pulse font-droid\n\n# Hardware Acceleration for Intel CPU (+50MB)\nARG TARGETARCH\n\nRUN if [ \"${TARGETARCH}\" = \"amd64\" ]; then apk add --no-cache libva-intel-driver intel-media-driver; fi\n\n# Hardware: AMD and NVidia VAAPI (not sure about this)\n# RUN libva-glx mesa-va-gallium\n# Hardware: AMD and NVidia VDPAU (not sure about this)\n# RUN libva-vdpau-driver mesa-vdpau-gallium (+150MB total)\n\nCOPY --from=build /build/go2rtc /usr/local/bin/\n\nEXPOSE 1984 8554 8555 8555/udp\nENTRYPOINT [\"/sbin/tini\", \"--\"]\nVOLUME /config\nWORKDIR /config\n\nCMD [\"go2rtc\", \"-config\", \"/config/go2rtc.yaml\"]\n"
  },
  {
    "path": "docker/README.md",
    "content": "# Docker\n\nImages are built automatically via [GitHub actions](https://github.com/AlexxIT/go2rtc/actions) and published on [Docker Hub](https://hub.docker.com/r/alexxit/go2rtc) and [GitHub](https://github.com/AlexxIT/go2rtc/pkgs/container/go2rtc).\n\n## Versions\n\n- `alexxit/go2rtc:latest` - latest release based on `alpine` (`amd64`, `386`, `arm/v6`, `arm/v7`, `arm64`) with support for hardware transcoding for Intel iGPU and Raspberry\n- `alexxit/go2rtc:latest-hardware` - latest release based on `debian 13` (`amd64`) with support for hardware transcoding for Intel iGPU, AMD GPU and NVidia GPU\n- `alexxit/go2rtc:latest-rockchip` - latest release based on `debian 12` (`arm64`) with support for hardware transcoding for Rockchip RK35xx\n- `alexxit/go2rtc:master` - latest unstable version based on `alpine`\n- `alexxit/go2rtc:master-hardware` - latest unstable version based on `debian 13` (`amd64`)\n- `alexxit/go2rtc:master-rockchip` - latest unstable version based on `debian 12` (`arm64`)\n\n## Docker compose\n\n```yaml\nservices:\n  go2rtc:\n    image: alexxit/go2rtc\n    network_mode: host       # important for WebRTC, HomeKit, UDP cameras\n    privileged: true         # only for FFmpeg hardware transcoding\n    restart: unless-stopped  # autorestart on fail or config change from WebUI\n    environment:\n      - TZ=Atlantic/Bermuda  # timezone in logs\n    volumes:\n      - \"~/go2rtc:/config\"   # folder for go2rtc.yaml file (edit from WebUI)\n```\n\n## Basic Deployment\n\n```bash\ndocker run -d \\\n  --name go2rtc \\\n  --network host \\\n  --privileged \\\n  --restart unless-stopped \\\n  -e TZ=Atlantic/Bermuda \\\n  -v ~/go2rtc:/config \\\n  alexxit/go2rtc\n```\n\n## Deployment with GPU Acceleration\n\n```bash\ndocker run -d \\\n  --name go2rtc \\\n  --network host \\\n  --privileged \\\n  --restart unless-stopped \\\n  -e TZ=Atlantic/Bermuda \\\n  --gpus all \\\n  -v ~/go2rtc:/config \\\n  alexxit/go2rtc:latest-hardware\n```\n"
  },
  {
    "path": "docker/hardware.Dockerfile",
    "content": "# syntax=docker/dockerfile:labs\n\n# 0. Prepare images\n# only debian 13 (trixie) has latest ffmpeg\n# https://packages.debian.org/trixie/ffmpeg\nARG DEBIAN_VERSION=\"trixie-slim\"\nARG GO_VERSION=\"1.25-bookworm\"\n\n\n# 1. Build go2rtc binary\nFROM --platform=$BUILDPLATFORM golang:${GO_VERSION} AS build\nARG TARGETPLATFORM\nARG TARGETOS\nARG TARGETARCH\n\nENV GOOS=${TARGETOS}\nENV GOARCH=${TARGETARCH}\n\nWORKDIR /build\n\n# Cache dependencies\nCOPY go.mod go.sum ./\nRUN --mount=type=cache,target=/root/.cache/go-build go mod download\n\nCOPY . .\nRUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags \"-s -w\" -trimpath\n\n\n# 2. Final image\nFROM debian:${DEBIAN_VERSION}\n\n# Prepare apt for buildkit cache\nRUN rm -f /etc/apt/apt.conf.d/docker-clean \\\n  && echo 'Binary::apt::APT::Keep-Downloaded-Packages \"true\";' >/etc/apt/apt.conf.d/keep-cache\n\n# Install ffmpeg, tini (for signal handling),\n# and other common tools for the echo source.\n# non-free for Intel QSV support (not used by go2rtc, just for tests)\n# mesa-va-drivers for AMD APU\n# libasound2-plugins for ALSA support\nRUN --mount=type=cache,target=/var/cache/apt,sharing=locked --mount=type=cache,target=/var/lib/apt,sharing=locked \\\n    echo 'deb http://deb.debian.org/debian trixie non-free' > /etc/apt/sources.list.d/debian-non-free.list && \\\n    apt-get -y update && apt-get -y install ffmpeg tini \\\n        python3 curl jq \\\n        intel-media-va-driver-non-free \\\n        mesa-va-drivers \\\n        libasound2-plugins && \\\n    apt-get clean && rm -rf /var/lib/apt/lists/*\n\nCOPY --from=build /build/go2rtc /usr/local/bin/\n\nEXPOSE 1984 8554 8555 8555/udp\nENTRYPOINT [\"/usr/bin/tini\", \"--\"]\nVOLUME /config\nWORKDIR /config\n# https://github.com/NVIDIA/nvidia-docker/wiki/Installation-(Native-GPU-Support)\nENV NVIDIA_VISIBLE_DEVICES all\nENV NVIDIA_DRIVER_CAPABILITIES compute,video,utility\n\nCMD [\"go2rtc\", \"-config\", \"/config/go2rtc.yaml\"]\n"
  },
  {
    "path": "docker/rockchip.Dockerfile",
    "content": "# syntax=docker/dockerfile:labs\n\n# 0. Prepare images\nARG PYTHON_VERSION=\"3.13-slim-bookworm\"\nARG GO_VERSION=\"1.25-bookworm\"\n\n\n# 1. Build go2rtc binary\nFROM --platform=$BUILDPLATFORM golang:${GO_VERSION} AS build\nARG TARGETPLATFORM\nARG TARGETOS\nARG TARGETARCH\n\nENV GOOS=${TARGETOS}\nENV GOARCH=${TARGETARCH}\n\nWORKDIR /build\n\n# Cache dependencies\nCOPY go.mod go.sum ./\nRUN --mount=type=cache,target=/root/.cache/go-build go mod download\n\nCOPY . .\nRUN --mount=type=cache,target=/root/.cache/go-build CGO_ENABLED=0 go build -ldflags \"-s -w\" -trimpath\n\n\n# 2. Final image\nFROM python:${PYTHON_VERSION}\n\n# Prepare apt for buildkit cache\nRUN rm -f /etc/apt/apt.conf.d/docker-clean \\\n  && echo 'Binary::apt::APT::Keep-Downloaded-Packages \"true\";' >/etc/apt/apt.conf.d/keep-cache\n\n# Install ffmpeg, tini (for signal handling),\n# and other common tools for the echo source.\n# libasound2-plugins for ALSA support\nRUN --mount=type=cache,target=/var/cache/apt,sharing=locked --mount=type=cache,target=/var/lib/apt,sharing=locked \\\n    apt-get -y update && apt-get -y install tini \\\n        curl jq \\\n        libasound2-plugins && \\\n    apt-get clean && rm -rf /var/lib/apt/lists/*\n\nCOPY --from=build /build/go2rtc /usr/local/bin/\nADD --chmod=755 https://github.com/MarcA711/Rockchip-FFmpeg-Builds/releases/download/6.1-8-no_extra_dump/ffmpeg /usr/local/bin\n\nEXPOSE 1984 8554 8555 8555/udp\nENTRYPOINT [\"/usr/bin/tini\", \"--\"]\nVOLUME /config\nWORKDIR /config\n\nCMD [\"go2rtc\", \"-config\", \"/config/go2rtc.yaml\"]\n"
  },
  {
    "path": "examples/go2rtc_hass/main.go",
    "content": "package main\n\nimport (\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/internal/app\"\n\t\"github.com/AlexxIT/go2rtc/internal/hass\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/shell\"\n)\n\nfunc main() {\n\tapp.Init()\n\tstreams.Init()\n\n\tapi.Init()\n\n\thass.Init()\n\n\tshell.RunUntilSignal()\n}\n"
  },
  {
    "path": "examples/go2rtc_mjpeg/main.go",
    "content": "package main\n\nimport (\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/internal/api/ws\"\n\t\"github.com/AlexxIT/go2rtc/internal/app\"\n\t\"github.com/AlexxIT/go2rtc/internal/ffmpeg\"\n\t\"github.com/AlexxIT/go2rtc/internal/mjpeg\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/internal/v4l2\"\n\t\"github.com/AlexxIT/go2rtc/pkg/shell\"\n)\n\nfunc main() {\n\tapp.Init()\n\tstreams.Init()\n\n\tapi.Init()\n\tws.Init()\n\n\tffmpeg.Init()\n\tmjpeg.Init()\n\tv4l2.Init()\n\n\tshell.RunUntilSignal()\n}\n"
  },
  {
    "path": "examples/go2rtc_rtsp/main.go",
    "content": "package main\n\nimport (\n\t\"github.com/AlexxIT/go2rtc/internal/app\"\n\t\"github.com/AlexxIT/go2rtc/internal/rtsp\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/shell\"\n)\n\nfunc main() {\n\tapp.Init()\n\tstreams.Init()\n\n\trtsp.Init()\n\n\tshell.RunUntilSignal()\n}\n"
  },
  {
    "path": "examples/homekit_info/main.go",
    "content": "package main\n\nimport (\n\t\"encoding/json\"\n\t\"os\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/hap\"\n)\n\nvar servs = map[string]string{\n\t\"3E\":  \"Accessory Information\",\n\t\"7E\":  \"Security System\",\n\t\"85\":  \"Motion Sensor\",\n\t\"96\":  \"Battery\",\n\t\"A2\":  \"Protocol Information\",\n\t\"110\": \"Camera RTP Stream Management\",\n\t\"112\": \"Microphone\",\n\t\"113\": \"Speaker\",\n\t\"121\": \"Doorbell\",\n\t\"129\": \"Data Stream Transport Management\",\n\t\"204\": \"Camera Recording Management\",\n\t\"21A\": \"Camera Operating Mode\",\n\t\"22A\": \"Wi-Fi Transport\",\n\t\"239\": \"Accessory Runtime Information\",\n}\n\nvar chars = map[string]string{\n\t\"14\":  \"Identify\",\n\t\"20\":  \"Manufacturer\",\n\t\"21\":  \"Model\",\n\t\"23\":  \"Name\",\n\t\"30\":  \"Serial Number\",\n\t\"52\":  \"Firmware Revision\",\n\t\"53\":  \"Hardware Revision\",\n\t\"220\": \"Product Data\",\n\t\"A6\":  \"Accessory Flags\",\n\n\t\"22\": \"Motion Detected\",\n\t\"75\": \"Status Active\",\n\n\t\"11A\": \"Mute\",\n\t\"119\": \"Volume\",\n\n\t\"B0\":  \"Active\",\n\t\"209\": \"Selected Camera Recording Configuration\",\n\t\"207\": \"Supported Audio Recording Configuration\",\n\t\"205\": \"Supported Camera Recording Configuration\",\n\t\"206\": \"Supported Video Recording Configuration\",\n\t\"226\": \"Recording Audio Active\",\n\n\t\"223\": \"Event Snapshots Active\",\n\t\"225\": \"Periodic Snapshots Active\",\n\t\"21B\": \"HomeKit Camera Active\",\n\t\"21C\": \"Third Party Camera Active\",\n\t\"21D\": \"Camera Operating Mode Indicator\",\n\t\"11B\": \"Night Vision\",\n\t//\"129\": \"Supported Data Stream Transport Configuration\",\n\t\"37\":  \"Version\",\n\t\"131\": \"Setup Data Stream Transport\",\n\t\"130\": \"Supported Data Stream Transport Configuration\",\n\n\t\"120\": \"Streaming Status\",\n\t\"115\": \"Supported Audio Stream Configuration\",\n\t\"116\": \"Supported RTP Configuration\",\n\t\"114\": \"Supported Video Stream Configuration\",\n\t\"117\": \"Selected RTP Stream Configuration\",\n\t\"118\": \"Setup Endpoints\",\n\n\t\"22B\": \"Current Transport\",\n\t\"22C\": \"Wi-Fi Capabilities\",\n\t\"22D\": \"Wi-Fi Configuration Control\",\n\n\t\"23C\": \"Ping\",\n\n\t\"68\": \"Battery Level\",\n\t\"79\": \"Status Low Battery\",\n\t\"8F\": \"Charging State\",\n\n\t\"73\":  \"Programmable Switch Event\",\n\t\"232\": \"Operating State Response\",\n\n\t\"66\": \"Security System Current State\",\n\t\"67\": \"Security System Target State\",\n}\n\nfunc main() {\n\tsrc := os.Args[1]\n\tdst := os.Args[2]\n\n\tf, err := os.Open(src)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tvar v hap.JSONAccessories\n\tif err = json.NewDecoder(f).Decode(&v); err != nil {\n\t\tpanic(err)\n\t}\n\n\tfor _, acc := range v.Value {\n\t\tfor _, srv := range acc.Services {\n\t\t\tif srv.Desc == \"\" {\n\t\t\t\tsrv.Desc = servs[srv.Type]\n\t\t\t}\n\t\t\tfor _, chr := range srv.Characters {\n\t\t\t\tif chr.Desc == \"\" {\n\t\t\t\t\tchr.Desc = chars[chr.Type]\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tf, err = os.Create(dst)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tenc := json.NewEncoder(f)\n\tenc.SetIndent(\"\", \"  \")\n\tif err = enc.Encode(v); err != nil {\n\t\tpanic(err)\n\t}\n}\n"
  },
  {
    "path": "examples/mdns/main.go",
    "content": "package main\n\nimport (\n\t\"log\"\n\t\"os\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/mdns\"\n)\n\nfunc main() {\n\tvar service = mdns.ServiceHAP\n\n\tif len(os.Args) >= 2 {\n\t\tservice = os.Args[1]\n\t}\n\n\tonentry := func(entry *mdns.ServiceEntry) bool {\n\t\tlog.Printf(\"name=%s, addr=%s, info=%s\\n\", entry.Name, entry.Addr(), entry.Info)\n\t\treturn false\n\t}\n\n\tvar err error\n\n\tif len(os.Args) >= 3 {\n\t\thost := os.Args[2]\n\n\t\tlog.Printf(\"run discovery service=%s host=%s\\n\", service, host)\n\n\t\terr = mdns.QueryOrDiscovery(host, service, onentry)\n\t} else {\n\t\tlog.Printf(\"run discovery service=%s\\n\", service)\n\n\t\terr = mdns.Discovery(service, onentry)\n\t}\n\n\tif err != nil {\n\t\tlog.Println(err)\n\t}\n}\n"
  },
  {
    "path": "examples/mod_pinggy/go.mod",
    "content": "module pinggy\n\ngo 1.25\n\nrequire (\n\tgithub.com/Pinggy-io/pinggy-go/pinggy v0.6.9 // indirect\n\tgolang.org/x/crypto v0.8.0 // indirect\n\tgolang.org/x/sys v0.7.0 // indirect\n)\n"
  },
  {
    "path": "examples/mod_pinggy/go.sum",
    "content": "github.com/Pinggy-io/pinggy-go/pinggy v0.6.9 h1:lzZ00JK6BUGQXnpkJZ+cVj8kIkXsmiVBUci9uEkSwEY=\ngithub.com/Pinggy-io/pinggy-go/pinggy v0.6.9/go.mod h1:V1Sxb+4zyr36o9atZiqtT4XhsKtW1RSb2GvsbTbTJYw=\ngithub.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=\ngolang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=\ngolang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=\ngolang.org/x/crypto v0.8.0 h1:pd9TJtTueMTVQXzk8E2XESSMQDj/U7OUu0PqJqPXQjQ=\ngolang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=\ngolang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=\ngolang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=\ngolang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=\ngolang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=\ngolang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=\ngolang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=\ngolang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=\ngolang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=\ngolang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=\ngolang.org/x/sys v0.0.0-20201119102817-f84b799fce68/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-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.7.0 h1:3jlCCIQZPdOYu1h8BkNvLz8Kgwtae2cagcG/VamtZRU=\ngolang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\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.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=\ngolang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=\ngolang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=\ngolang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=\ngolang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=\ngolang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=\ngolang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=\ngolang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=\ngolang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=\ngolang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=\ngolang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=\ngolang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=\n"
  },
  {
    "path": "examples/mod_pinggy/main.go",
    "content": "package main\n\nimport (\n\t\"log\"\n\t\"os\"\n\n\t\"github.com/Pinggy-io/pinggy-go/pinggy\"\n)\n\nfunc main() {\n\ttunType := os.Args[1]\n\taddress := os.Args[2]\n\n\tlog.SetFlags(log.Llongfile | log.LstdFlags)\n\n\tconfig := pinggy.Config{\n\t\tType:              pinggy.TunnelType(tunType),\n\t\tTcpForwardingAddr: address,\n\n\t\t//SshOverSsl: true,\n\t\t//Stdout:     os.Stderr,\n\t\t//Stderr:     os.Stderr,\n\t}\n\n\tif tunType == \"http\" {\n\t\thman := pinggy.CreateHeaderManipulationAndAuthConfig()\n\t\t//hman.SetReverseProxy(address)\n\t\t//hman.SetPassPreflight(true)\n\t\t//hman.SetNoReverseProxy()\n\t\tconfig.HeaderManipulationAndAuth = hman\n\t}\n\n\tpl, err := pinggy.ConnectWithConfig(config)\n\tif err != nil {\n\t\tlog.Panicln(err)\n\t}\n\tlog.Println(\"Addrs: \", pl.RemoteUrls())\n\t//err = pl.InitiateWebDebug(\"localhost:3424\")\n\t//log.Println(err)\n\tpl.StartForwarding()\n}\n"
  },
  {
    "path": "examples/onvif_client/README.md",
    "content": "## ONVIF Client\n\n```shell\ngo run examples/onvif_client/main.go http://admin:password@192.168.10.90 GetAudioEncoderConfigurations\n```"
  },
  {
    "path": "examples/onvif_client/main.go",
    "content": "package main\n\nimport (\n\t\"log\"\n\t\"net/url\"\n\t\"os\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/onvif\"\n)\n\nfunc main() {\n\tvar rawURL = os.Args[1]\n\tvar operation = os.Args[2]\n\tvar token string\n\tif len(os.Args) > 3 {\n\t\ttoken = os.Args[3]\n\t}\n\n\tclient, err := onvif.NewClient(rawURL)\n\tif err != nil {\n\t\tlog.Panic(err)\n\t}\n\n\tvar b []byte\n\n\tswitch operation {\n\tcase onvif.ServiceGetServiceCapabilities:\n\t\tb, err = client.MediaRequest(operation)\n\tcase onvif.DeviceGetCapabilities,\n\t\tonvif.DeviceGetDeviceInformation,\n\t\tonvif.DeviceGetDiscoveryMode,\n\t\tonvif.DeviceGetDNS,\n\t\tonvif.DeviceGetHostname,\n\t\tonvif.DeviceGetNetworkDefaultGateway,\n\t\tonvif.DeviceGetNetworkInterfaces,\n\t\tonvif.DeviceGetNetworkProtocols,\n\t\tonvif.DeviceGetNTP,\n\t\tonvif.DeviceGetScopes,\n\t\tonvif.DeviceGetServices,\n\t\tonvif.DeviceGetSystemDateAndTime,\n\t\tonvif.DeviceSystemReboot:\n\t\tb, err = client.DeviceRequest(operation)\n\tcase onvif.MediaGetProfiles,\n\t\tonvif.MediaGetVideoEncoderConfigurations,\n\t\tonvif.MediaGetVideoSources,\n\t\tonvif.MediaGetVideoSourceConfigurations,\n\t\tonvif.MediaGetAudioEncoderConfigurations,\n\t\tonvif.MediaGetAudioSources,\n\t\tonvif.MediaGetAudioSourceConfigurations:\n\t\tb, err = client.MediaRequest(operation)\n\tcase onvif.MediaGetProfile:\n\t\tb, err = client.GetProfile(token)\n\tcase onvif.MediaGetVideoSourceConfiguration:\n\t\tb, err = client.GetVideoSourceConfiguration(token)\n\tcase onvif.MediaGetStreamUri:\n\t\tb, err = client.GetStreamUri(token)\n\tcase onvif.MediaGetSnapshotUri:\n\t\tb, err = client.GetSnapshotUri(token)\n\tdefault:\n\t\tlog.Printf(\"unknown action\\n\")\n\t}\n\n\tif err != nil {\n\t\tlog.Printf(\"%s\\n\", err)\n\t}\n\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tif err = os.WriteFile(u.Hostname()+\"_\"+operation+\".xml\", b, 0644); err != nil {\n\t\tlog.Printf(\"%s\\n\", err)\n\t}\n}\n"
  },
  {
    "path": "examples/rtsp_client/main.go",
    "content": "package main\n\nimport (\n\t\"log\"\n\t\"os\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/rtsp\"\n\t\"github.com/AlexxIT/go2rtc/pkg/shell\"\n)\n\nfunc main() {\n\tclient := rtsp.NewClient(os.Args[1])\n\tif err := client.Dial(); err != nil {\n\t\tlog.Panic(err)\n\t}\n\n\tclient.Medias = []*core.Media{\n\t\t{\n\t\t\tKind:      core.KindAudio,\n\t\t\tDirection: core.DirectionRecvonly,\n\t\t\tCodecs: []*core.Codec{\n\t\t\t\t{Name: core.CodecPCMU, ClockRate: 8000},\n\t\t\t},\n\t\t\tID: \"streamid=0\",\n\t\t},\n\t}\n\tif err := client.Announce(); err != nil {\n\t\tlog.Panic(err)\n\t}\n\tif _, err := client.SetupMedia(client.Medias[0]); err != nil {\n\t\tlog.Panic(err)\n\t}\n\tif err := client.Record(); err != nil {\n\t\tlog.Panic(err)\n\t}\n\n\tshell.RunUntilSignal()\n}\n"
  },
  {
    "path": "examples/tutk_decoder/README.md",
    "content": "# tutk_decoder\n\n1. Wireshark > Select any packet > Follow > UDP Stream\n2. Wireshark > File > Export Packet Dissections > As JSON > Displayed, Values\n3. `tutk_decoder wireshark.json decoded.txt`\n"
  },
  {
    "path": "examples/tutk_decoder/main.go",
    "content": "package main\n\nimport (\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/tutk\"\n)\n\nfunc main() {\n\tif len(os.Args) != 3 {\n\t\tfmt.Println(\"Usage: tutk_decoder wireshark.json decoded.txt\")\n\t\treturn\n\t}\n\n\tsrc, err := os.Open(os.Args[1])\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tdefer src.Close()\n\n\tdst, err := os.Create(os.Args[2])\n\tif err != nil {\n\t\tlog.Fatal(err)\n\t}\n\tdefer dst.Close()\n\n\tvar items []item\n\tif err = json.NewDecoder(src).Decode(&items); err != nil {\n\t\tlog.Fatal(err)\n\t}\n\n\tvar b []byte\n\n\tfor _, v := range items {\n\t\tif v.Source.Layers.Data.DataData == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\ts := strings.ReplaceAll(v.Source.Layers.Data.DataData, \":\", \"\")\n\t\tb, err = hex.DecodeString(s)\n\t\tif err != nil {\n\t\t\tlog.Fatal(err)\n\t\t}\n\n\t\ttutk.ReverseTransCodePartial(b, b)\n\n\t\tts := v.Source.Layers.Frame.FrameTimeRelative\n\n\t\t_, _ = fmt.Fprintf(dst, \"%8s: %s -> %s [%4d] %x\\n\",\n\t\t\tts[:len(ts)-6],\n\t\t\tv.Source.Layers.Ip.IpSrc, v.Source.Layers.Ip.IpDst,\n\t\t\tlen(b), b)\n\t}\n}\n\ntype item struct {\n\tSource struct {\n\t\tLayers struct {\n\t\t\tFrame struct {\n\t\t\t\tFrameTimeRelative string `json:\"frame.time_relative\"`\n\t\t\t\tFrameNumber       string `json:\"frame.number\"`\n\t\t\t} `json:\"frame\"`\n\t\t\tIp struct {\n\t\t\t\tIpSrc string `json:\"ip.src\"`\n\t\t\t\tIpDst string `json:\"ip.dst\"`\n\t\t\t} `json:\"ip\"`\n\t\t\tUdp struct {\n\t\t\t\tUdpSrcport string `json:\"udp.srcport\"`\n\t\t\t\tUdpDstport string `json:\"udp.dstport\"`\n\t\t\t} `json:\"udp\"`\n\t\t\tData struct {\n\t\t\t\tDataData string `json:\"data.data\"`\n\t\t\t\tDataLen  string `json:\"data.len\"`\n\t\t\t} `json:\"data\"`\n\t\t} `json:\"layers\"`\n\t} `json:\"_source\"`\n}\n"
  },
  {
    "path": "go.mod",
    "content": "module github.com/AlexxIT/go2rtc\n\ngo 1.24.0\n\nrequire (\n\tgithub.com/asticode/go-astits v1.14.0\n\tgithub.com/eclipse/paho.mqtt.golang v1.5.1\n\tgithub.com/expr-lang/expr v1.17.7\n\tgithub.com/google/uuid v1.6.0\n\tgithub.com/gorilla/websocket v1.5.3\n\tgithub.com/mattn/go-isatty v0.0.20\n\tgithub.com/miekg/dns v1.1.70\n\tgithub.com/pion/dtls/v3 v3.0.10\n\tgithub.com/pion/ice/v4 v4.2.0\n\tgithub.com/pion/interceptor v0.1.43\n\tgithub.com/pion/rtcp v1.2.16\n\tgithub.com/pion/rtp v1.10.0\n\tgithub.com/pion/sdp/v3 v3.0.17\n\tgithub.com/pion/srtp/v3 v3.0.10\n\tgithub.com/pion/stun/v3 v3.1.1\n\tgithub.com/pion/webrtc/v4 v4.2.3\n\tgithub.com/rs/zerolog v1.34.0\n\tgithub.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1\n\tgithub.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f\n\tgithub.com/stretchr/testify v1.11.1\n\tgithub.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9\n\tgolang.org/x/crypto v0.47.0\n\tgolang.org/x/net v0.49.0\n\tgopkg.in/yaml.v3 v3.0.1\n)\n\nrequire (\n\tgithub.com/asticode/go-astikit v0.57.1 // indirect\n\tgithub.com/davecgh/go-spew v1.1.1 // indirect\n\tgithub.com/kr/pretty v0.3.1 // indirect\n\tgithub.com/mattn/go-colorable v0.1.14 // indirect\n\tgithub.com/pion/datachannel v1.6.0 // indirect\n\tgithub.com/pion/logging v0.2.4 // 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/transport/v3 v3.1.1 // indirect\n\tgithub.com/pion/transport/v4 v4.0.1 // indirect\n\tgithub.com/pion/turn/v4 v4.1.4 // indirect\n\tgithub.com/pmezard/go-difflib v1.0.0 // indirect\n\tgithub.com/wlynxg/anet v0.0.5 // indirect\n\tgolang.org/x/mod v0.32.0 // indirect\n\tgolang.org/x/sync v0.19.0 // indirect\n\tgolang.org/x/sys v0.40.0 // indirect\n\tgolang.org/x/time v0.14.0 // indirect\n\tgolang.org/x/tools v0.41.0 // indirect\n)\n"
  },
  {
    "path": "go.sum",
    "content": "github.com/asticode/go-astikit v0.30.0/go.mod h1:h4ly7idim1tNhaVkdVBeXQZEE3L0xblP7fCWbgwipF0=\ngithub.com/asticode/go-astikit v0.57.1 h1:fEykwH98Nny08kcRbk4uer+S8h0rKveCIpG9F6NVLuA=\ngithub.com/asticode/go-astikit v0.57.1/go.mod h1:fV43j20UZYfXzP9oBn33udkvCvDvCDhzjVqoLFuuYZE=\ngithub.com/asticode/go-astits v1.14.0 h1:zkgnZzipx2XX5mWycqsSBeEyDH58+i4HtyF4j2ROb00=\ngithub.com/asticode/go-astits v1.14.0/go.mod h1:QSHmknZ51pf6KJdHKZHJTLlMegIrhega3LPWz3ND/iI=\ngithub.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=\ngithub.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=\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/eclipse/paho.mqtt.golang v1.5.1 h1:/VSOv3oDLlpqR2Epjn1Q7b2bSTplJIeV2ISgCl2W7nE=\ngithub.com/eclipse/paho.mqtt.golang v1.5.1/go.mod h1:1/yJCneuyOoCOzKSsOTUc0AJfpsItBGWvYpBLimhArU=\ngithub.com/expr-lang/expr v1.17.6 h1:1h6i8ONk9cexhDmowO/A64VPxHScu7qfSl2k8OlINec=\ngithub.com/expr-lang/expr v1.17.6/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=\ngithub.com/expr-lang/expr v1.17.7 h1:Q0xY/e/2aCIp8g9s/LGvMDCC5PxYlvHgDZRQ4y16JX8=\ngithub.com/expr-lang/expr v1.17.7/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=\ngithub.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=\ngithub.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=\ngithub.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=\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/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=\ngithub.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=\ngithub.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=\ngithub.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=\ngithub.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=\ngithub.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=\ngithub.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=\ngithub.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=\ngithub.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=\ngithub.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=\ngithub.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=\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/miekg/dns v1.1.69 h1:Kb7Y/1Jo+SG+a2GtfoFUfDkG//csdRPwRLkCsxDG9Sc=\ngithub.com/miekg/dns v1.1.69/go.mod h1:7OyjD9nEba5OkqQ/hB4fy3PIoxafSZJtducccIelz3g=\ngithub.com/miekg/dns v1.1.70 h1:DZ4u2AV35VJxdD9Fo9fIWm119BsQL5cZU1cQ9s0LkqA=\ngithub.com/miekg/dns v1.1.70/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=\ngithub.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o=\ngithub.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M=\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.0.9 h1:4AijfFRm8mAjd1gfdlB1wzJF3fjjR/VPIpJgkEtvYmM=\ngithub.com/pion/dtls/v3 v3.0.9/go.mod h1:abApPjgadS/ra1wvUzHLc3o2HvoxppAh+NZkyApL4Os=\ngithub.com/pion/dtls/v3 v3.0.10 h1:k9ekkq1kaZoxnNEbyLKI8DI37j/Nbk1HWmMuywpQJgg=\ngithub.com/pion/dtls/v3 v3.0.10/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8=\ngithub.com/pion/ice/v4 v4.1.0 h1:YlxIii2bTPWyC08/4hdmtYq4srbrY0T9xcTsTjldGqU=\ngithub.com/pion/ice/v4 v4.1.0/go.mod h1:5gPbzYxqenvn05k7zKPIZFuSAufolygiy6P1U9HzvZ4=\ngithub.com/pion/ice/v4 v4.2.0 h1:jJC8S+CvXCCvIQUgx+oNZnoUpt6zwc34FhjWwCU4nlw=\ngithub.com/pion/ice/v4 v4.2.0/go.mod h1:EgjBGxDgmd8xB0OkYEVFlzQuEI7kWSCFu+mULqaisy4=\ngithub.com/pion/interceptor v0.1.42 h1:0/4tvNtruXflBxLfApMVoMubUMik57VZ+94U0J7cmkQ=\ngithub.com/pion/interceptor v0.1.42/go.mod h1:g6XYTChs9XyolIQFhRHOOUS+bGVGLRfgTCUzH29EfVU=\ngithub.com/pion/interceptor v0.1.43 h1:6hmRfnmjogSs300xfkR0JxYFZ9k5blTEvCD7wxEDuNQ=\ngithub.com/pion/interceptor v0.1.43/go.mod h1:BSiC1qKIJt1XVr3l3xQ2GEmCFStk9tx8fwtCZxxgR7M=\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.8.26 h1:VB+ESQFQhBXFytD+Gk8cxB6dXeVf2WQzg4aORvAvAAc=\ngithub.com/pion/rtp v1.8.26/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=\ngithub.com/pion/rtp v1.10.0 h1:XN/xca4ho6ZEcijpdF2VGFbwuHUfiIMf3ew8eAAE43w=\ngithub.com/pion/rtp v1.10.0/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=\ngithub.com/pion/sctp v1.8.41 h1:20R4OHAno4Vky3/iE4xccInAScAa83X6nWUfyc65MIs=\ngithub.com/pion/sctp v1.8.41/go.mod h1:2wO6HBycUH7iCssuGyc2e9+0giXVW0pyCv3ZuL8LiyY=\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.16 h1:0dKzYO6gTAvuLaAKQkC02eCPjMIi4NuAr/ibAwrGDCo=\ngithub.com/pion/sdp/v3 v3.0.16/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=\ngithub.com/pion/sdp/v3 v3.0.17 h1:9SfLAW/fF1XC8yRqQ3iWGzxkySxup4k4V7yN8Fs8nuo=\ngithub.com/pion/sdp/v3 v3.0.17/go.mod h1:9tyKzznud3qiweZcD86kS0ff1pGYB3VX+Bcsmkx6IXo=\ngithub.com/pion/srtp/v3 v3.0.9 h1:lRGF4G61xxj+m/YluB3ZnBpiALSri2lTzba0kGZMrQY=\ngithub.com/pion/srtp/v3 v3.0.9/go.mod h1:E+AuWd7Ug2Fp5u38MKnhduvpVkveXJX6J4Lq4rxUYt8=\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.0.2 h1:BJuGEN2oLrJisiNEJtUTJC4BGbzbfp37LizfqswblFU=\ngithub.com/pion/stun/v3 v3.0.2/go.mod h1:JFJKfIWvt178MCF5H/YIgZ4VX3LYE77vca4b9HP60SA=\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.3 h1:jVNW0iR05AS94ysEtvzsrk3gKs9Zqxf6HmnsLfRvlzA=\ngithub.com/pion/turn/v4 v4.1.3/go.mod h1:TD/eiBUf5f5LwXbCJa35T7dPtTpCHRJ9oJWmyPLVT3A=\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.1.8 h1:ynkjfiURDQ1+8EcJsoa60yumHAmyeYjz08AaOuor+sk=\ngithub.com/pion/webrtc/v4 v4.1.8/go.mod h1:KVaARG2RN0lZx0jc7AWTe38JpPv+1/KicOZ9jN52J/s=\ngithub.com/pion/webrtc/v4 v4.2.3 h1:RtdWDnkenNQGxUrZqWa5gSkTm5ncsLg5d+zu0M4cXt4=\ngithub.com/pion/webrtc/v4 v4.2.3/go.mod h1:7vsyFzRzaKP5IELUnj8zLcglPyIT6wWwqTppBZ1k6Kc=\ngithub.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=\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/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=\ngithub.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=\ngithub.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=\ngithub.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=\ngithub.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=\ngithub.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1 h1:NVK+OqnavpyFmUiKfUMHrpvbCi2VFoWTrcpI7aDaJ2I=\ngithub.com/sigurn/crc16 v0.0.0-20240131213347-83fcde1e29d1/go.mod h1:9/etS5gpQq9BJsJMWg1wpLbfuSnkm8dPF6FdW2JXVhA=\ngithub.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f h1:1R9KdKjCNSd7F8iGTxIpoID9prlYH8nuNYKt0XvweHA=\ngithub.com/sigurn/crc8 v0.0.0-20220107193325-2243fe600f9f/go.mod h1:vQhwQ4meQEDfahT5kd61wLAF5AAeh5ZPLVI4JJ/tYo8=\ngithub.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=\ngithub.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=\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/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9 h1:aeN+ghOV0b2VCmKKO3gqnDQ8mLbpABZgRR2FVYx4ouI=\ngithub.com/tadglines/go-pkgs v0.0.0-20210623144937-b983b20f54f9/go.mod h1:roo6cZ/uqpwKMuvPG0YmzI5+AmUiMWfjCBZpGXqbTxE=\ngithub.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=\ngithub.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=\ngolang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=\ngolang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=\ngolang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=\ngolang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A=\ngolang.org/x/mod v0.31.0 h1:HaW9xtz0+kOcWKwli0ZXy79Ix+UW/vOfmWI5QVd2tgI=\ngolang.org/x/mod v0.31.0/go.mod h1:43JraMp9cGx1Rx3AqioxrbrhNsLl2l/iNAvuBkrezpg=\ngolang.org/x/mod v0.32.0 h1:9F4d3PHLljb6x//jOyokMv3eX+YDeepZSEo3mFJy93c=\ngolang.org/x/mod v0.32.0/go.mod h1:SgipZ/3h2Ci89DlEtEXWUk/HteuRin+HHhN+WbNhguU=\ngolang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=\ngolang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=\ngolang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o=\ngolang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8=\ngolang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=\ngolang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=\ngolang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=\ngolang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=\ngolang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ=\ngolang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=\ngolang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q=\ngolang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg=\ngolang.org/x/term v0.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY=\ngolang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=\ngolang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=\ngolang.org/x/tools v0.40.0 h1:yLkxfA+Qnul4cs9QA3KnlFu0lVmd8JJfoq+E41uSutA=\ngolang.org/x/tools v0.40.0/go.mod h1:Ik/tzLRlbscWpqqMRjyWYDisX8bG13FrdXp3o4Sr9lc=\ngolang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=\ngolang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=\ngopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=\ngopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=\ngopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=\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/README.md",
    "content": "# Modules\n\ngo2rtc tries to name formats, protocols and codecs the same way they are named in FFmpeg.\nSome formats and protocols go2rtc supports exclusively. They have no equivalent in FFmpeg.\n\n- The [`echo`], [`expr`], [`hass`] and [`onvif`] modules receive a link to a stream. They don't know the protocol in advance.\n- The [`exec`] and [`ffmpeg`] modules support many formats. They are identical to the [`http`] module.\n- The [`api`], [`app`], [`debug`], [`ngrok`], [`pinggy`], [`srtp`], [`streams`] are supporting modules.\n\n**Modules** implement communication APIs: authorization, encryption, command set, structure of media packets.\n\n**Formats** describe the structure of the data being transmitted.\n\n**Protocols** implement transport for data transmission.\n\n| module         | formats         | protocols        | input | output | ingest | two-way |\n|----------------|-----------------|------------------|-------|--------|--------|---------|\n| [`alsa`]       | `pcm`           | `ioctl`          | yes   |        |        |         |\n| [`bubble`]     | -               | `http`           | yes   |        |        |         |\n| [`doorbird`]   | `mulaw`         | `http`           | yes   |        |        | yes     |\n| [`dvrip`]      | -               | `tcp`            | yes   |        |        | yes     |\n| [`echo`]       | *               | *                | yes   |        |        |         |\n| [`eseecloud`]  | `rtp`           | `http`           | yes   |        |        |         |\n| [`exec`]       | *               | `pipe`, `rtsp`   | yes   |        |        | yes     |\n| [`expr`]       | *               | *                | yes   |        |        |         |\n| [`ffmpeg`]     | *               | `pipe`, `rtsp`   | yes   |        |        |         |\n| [`flussonic`]  | `mp4`           | `ws`             | yes   |        |        |         |\n| [`gopro`]      | `mpegts`        | `udp`            | yes   |        |        |         |\n| [`hass`]       | *               | *                | yes   |        |        |         |\n| [`hls`]        | `mpegts`, `mp4` | `http`           |       | yes    |        |         |\n| [`homekit`]    | `srtp`          | `hap`            | yes   | yes    |        | no      |\n| [`http`]       | `adts`          | `http`, `tcp`    | yes   |        |        |         |\n| [`http`]       | `flv`           | `http`, `tcp`    | yes   |        |        |         |\n| [`http`]       | `h264`          | `http`, `tcp`    | yes   |        |        |         |\n| [`http`]       | `hevc`          | `http`, `tcp`    | yes   |        |        |         |\n| [`http`]       | `hls`           | `http`, `tcp`    | yes   |        |        |         |\n| [`http`]       | `mjpeg`         | `http`, `tcp`    | yes   |        |        |         |\n| [`http`]       | `mpjpeg`        | `http`           | yes   |        |        |         |\n| [`http`]       | `mpegts`        | `http`, `tcp`    | yes   |        |        |         |\n| [`http`]       | `wav`           | `http`, `tcp`    | yes   |        |        |         |\n| [`http`]       | `yuv4mpegpipe`  | `http`, `tcp`    | yes   |        |        |         |\n| [`isapi`]      | `alaw`, `mulaw` | `http`           |       |        |        | yes     |\n| [`ivideon`]    | `mp4`           | `ws`             | yes   |        |        |         |\n| [`kasa`]       | `h264`, `mulaw` | `http`           | yes   |        |        |         |\n| [`mjpeg`]      | `ascii`         | `http`           |       | yes    |        |         |\n| [`mjpeg`]      | `jpeg`          | `http`           |       | yes    |        |         |\n| [`mjpeg`]      | `mpjpeg`        | `http`           |       | yes    | yes    |         |\n| [`mjpeg`]      | `yuv4mpegpipe`  | `http`           |       | yes    |        |         |\n| [`mp4`]        | `mp4`           | `http`, `ws`     |       | yes    |        |         |\n| [`mpeg`]       | `adts`          | `http`           |       | yes    |        |         |\n| [`mpeg`]       | `mpegts`        | `http`           |       | yes    | yes    |         |\n| [`multitrans`] | `rtp`           | `tcp`            |       |        |        | yes     |\n| [`nest`]       | `srtp`          | `rtsp`, `webrtc` | yes   |        |        | no      |\n| [`onvif`]      | `rtp`           | *                | yes   | yes    |        |         |\n| [`ring`]       | `srtp`          | `webrtc`         | yes   |        |        | yes     |\n| [`roborock`]   | `srtp`          | `webrtc`         | yes   |        |        | yes     |\n| [`rtmp`]       | `flv`           | `rtmp`           | yes   | yes    | yes    |         |\n| [`rtmp`]       | `flv`           | `http`           |       | yes    | yes    |         |\n| [`rtsp`]       | `rtsp`          | `rtsp`           | yes   | yes    | yes    | yes     |\n| [`tapo`]       | `mpegts`        | `http`           | yes   |        |        | yes     |\n| [`tuya`]       | `srtp`          | `webrtc`         | yes   |        |        | yes     |\n| [`v4l2`]       | `rawvideo`      | `ioctl`          | yes   |        |        |         |\n| [`webrtc`]     | `srtp`          | `webrtc`         | yes   | yes    | yes    | yes     |\n| [`webtorrent`] | `srtp`          | `webrtc`         | yes   | yes    |        |         |\n| [`wyoming`]    | `pcm`           | `tcp`            |       | yes    |        |         |\n| [`wyze`]       | -               | `tutk`           | yes   |        |        | yes     |\n| [`xiaomi`]     | -               | `cs2`, `tutk`    | yes   |        |        | yes     |\n| [`yandex`]     | `srtp`          | `webrtc`         | yes   |        |        |         |\n\n[`alsa`]: alsa/README.md\n[`api`]: api/README.md\n[`app`]: app/README.md\n[`bubble`]: bubble/README.md\n[`debug`]: debug/README.md\n[`doorbird`]: doorbird/README.md\n[`dvrip`]: dvrip/README.md\n[`echo`]: echo/README.md\n[`eseecloud`]: eseecloud/README.md\n[`exec`]: exec/README.md\n[`expr`]: expr/README.md\n[`ffmpeg`]: ffmpeg/README.md\n[`flussonic`]: flussonic/README.md\n[`gopro`]: gopro/README.md\n[`hass`]: hass/README.md\n[`hls`]: hls/README.md\n[`homekit`]: homekit/README.md\n[`http`]: http/README.md\n[`isapi`]: isapi/README.md\n[`ivideon`]: ivideon/README.md\n[`kasa`]: kasa/README.md\n[`mjpeg`]: mjpeg/README.md\n[`mp4`]: mp4/README.md\n[`mpeg`]: mpeg/README.md\n[`multitrans`]: multitrans/README.md\n[`nest`]: nest/README.md\n[`ngrok`]: ngrok/README.md\n[`onvif`]: onvif/README.md\n[`pinggy`]: pinggy/README.md\n[`ring`]: ring/README.md\n[`roborock`]: roborock/README.md\n[`rtmp`]: rtmp/README.md\n[`rtsp`]: rtsp/README.md\n[`srtp`]: srtp/README.md\n[`streams`]: streams/README.md\n[`tapo`]: tapo/README.md\n[`tuya`]: tuya/README.md\n[`v4l2`]: v4l2/README.md\n[`webrtc`]: webrtc/README.md\n[`webtorrent`]: webtorrent/README.md\n[`wyoming`]: wyze/README.md\n[`wyze`]: wyze/README.md\n[`xiaomi`]: xiaomi/README.md\n[`yandex`]: yandex/README.md\n"
  },
  {
    "path": "internal/alsa/README.md",
    "content": "# ALSA\n\n[`new in v1.9.10`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.9.10)\n\n> [!WARNING]\n> This source is under development and does not always work well.\n\n[Advanced Linux Sound Architecture](https://en.wikipedia.org/wiki/Advanced_Linux_Sound_Architecture) - a framework for receiving audio from devices on Linux OS.\n\nEasy to add via **WebUI > add > ALSA**.\n\nAlternatively, you can use FFmpeg source.\n"
  },
  {
    "path": "internal/alsa/alsa.go",
    "content": "//go:build !(linux && (386 || amd64 || arm || arm64 || mipsle))\n\npackage alsa\n\nfunc Init() {\n\t// not supported\n}\n"
  },
  {
    "path": "internal/alsa/alsa_linux.go",
    "content": "//go:build linux && (386 || amd64 || arm || arm64 || mipsle)\n\npackage alsa\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/alsa\"\n\t\"github.com/AlexxIT/go2rtc/pkg/alsa/device\"\n)\n\nfunc Init() {\n\tstreams.HandleFunc(\"alsa\", alsa.Open)\n\n\tapi.HandleFunc(\"api/alsa\", apiAlsa)\n}\n\nfunc apiAlsa(w http.ResponseWriter, r *http.Request) {\n\tfiles, err := os.ReadDir(\"/dev/snd/\")\n\tif err != nil {\n\t\treturn\n\t}\n\n\tvar sources []*api.Source\n\n\tfor _, file := range files {\n\t\tif !strings.HasPrefix(file.Name(), \"pcm\") {\n\t\t\tcontinue\n\t\t}\n\n\t\tpath := \"/dev/snd/\" + file.Name()\n\n\t\tdev, err := device.Open(path)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tinfo, err := dev.Info()\n\t\tif err == nil {\n\t\t\tformats := formatsToString(dev.ListFormats())\n\t\t\tr1, r2 := dev.RangeRates()\n\t\t\tc1, c2 := dev.RangeChannels()\n\t\t\tsource := &api.Source{\n\t\t\t\tName: info.ID,\n\t\t\t\tInfo: fmt.Sprintf(\"Formats: %s, Rates: %d-%d, Channels: %d-%d\", formats, r1, r2, c1, c2),\n\t\t\t\tURL:  \"alsa:device?audio=\" + path,\n\t\t\t}\n\t\t\tif !strings.Contains(source.Name, info.Name) {\n\t\t\t\tsource.Name += \", \" + info.Name\n\t\t\t}\n\t\t\tsources = append(sources, source)\n\t\t}\n\n\t\t_ = dev.Close()\n\t}\n\n\tapi.ResponseSources(w, sources)\n}\n\nfunc formatsToString(formats []byte) string {\n\tvar s string\n\tfor i, format := range formats {\n\t\tif i > 0 {\n\t\t\ts += \" \"\n\t\t}\n\t\tswitch format {\n\t\tcase 2:\n\t\t\ts += \"s16le\"\n\t\tcase 10:\n\t\t\ts += \"s32le\"\n\t\tdefault:\n\t\t\ts += strconv.Itoa(int(format))\n\t\t}\n\n\t}\n\treturn s\n}\n"
  },
  {
    "path": "internal/api/README.md",
    "content": "# HTTP API\n\nThe HTTP API is the main part for interacting with the application. Default address: `http://localhost:1984/`.\n\nThe HTTP API is described in [OpenAPI](../../website/api/openapi.yaml) format. It can be explored in [interactive viewer](https://go2rtc.org/api/). WebSocket API described [here](ws/README.md).\n\nThe project's static HTML and JS files are located in the [www](../../www/README.md) folder. An external developer can use them as a basis for integrating go2rtc into their project or for developing a custom web interface for go2rtc.\n\nThe contents of `www` folder are built into go2rtc when building, but you can use configuration to specify an external folder as the source of static files.\n\n## Configuration\n\n**Important!** go2rtc passes requests from localhost and Unix sockets without HTTP authorization, even if you have it configured. It is your responsibility to set up secure external access to the API. If not properly configured, an attacker can gain access to your cameras and even your server.\n\n- you can disable HTTP API with `listen: \"\"` and use, for example, only RTSP client/server protocol\n- you can enable HTTP API only on localhost with `listen: \"127.0.0.1:1984\"` setting\n- you can change the API `base_path` and host go2rtc on your main app webserver suburl\n- all files from `static_dir` hosted on root path: `/`\n- you can use raw TLS cert/key content or path to files\n\n```yaml\napi:\n  listen: \":1984\"    # default \":1984\", HTTP API port (\"\" - disabled)\n  username: \"admin\"  # default \"\", Basic auth for WebUI\n  password: \"pass\"   # default \"\", Basic auth for WebUI\n  local_auth: true   # default false, Enable auth check for localhost requests\n  base_path: \"/rtc\"  # default \"\", API prefix for serving on suburl (/api => /rtc/api)\n  static_dir: \"www\"  # default \"\", folder for static files (custom web interface)\n  origin: \"*\"        # default \"\", allow CORS requests (only * supported)\n  tls_listen: \":443\" # default \"\", enable HTTPS server\n  tls_cert: |        # default \"\", PEM-encoded fullchain certificate for HTTPS\n    -----BEGIN CERTIFICATE-----\n    ...\n    -----END CERTIFICATE-----\n  tls_key: |         # default \"\", PEM-encoded private key for HTTPS\n    -----BEGIN PRIVATE KEY-----\n    ...\n    -----END PRIVATE KEY-----\n  unix_listen: \"/tmp/go2rtc.sock\"  # default \"\", unix socket listener for API\n```\n\n**PS:**\n\n- MJPEG over WebSocket plays better than native MJPEG because Chrome [bug](https://bugs.chromium.org/p/chromium/issues/detail?id=527446)\n- MP4 over WebSocket was created only for Apple iOS because it doesn't support file streaming\n"
  },
  {
    "path": "internal/api/api.go",
    "content": "package api\n\nimport (\n\t\"crypto/tls\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"os\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/app\"\n\t\"github.com/rs/zerolog\"\n)\n\nfunc Init() {\n\tvar cfg struct {\n\t\tMod struct {\n\t\t\tListen     string `yaml:\"listen\"`\n\t\t\tUsername   string `yaml:\"username\"`\n\t\t\tPassword   string `yaml:\"password\"`\n\t\t\tLocalAuth  bool   `yaml:\"local_auth\"`\n\t\t\tBasePath   string `yaml:\"base_path\"`\n\t\t\tStaticDir  string `yaml:\"static_dir\"`\n\t\t\tOrigin     string `yaml:\"origin\"`\n\t\t\tTLSListen  string `yaml:\"tls_listen\"`\n\t\t\tTLSCert    string `yaml:\"tls_cert\"`\n\t\t\tTLSKey     string `yaml:\"tls_key\"`\n\t\t\tUnixListen string `yaml:\"unix_listen\"`\n\n\t\t\tAllowPaths []string `yaml:\"allow_paths\"`\n\t\t} `yaml:\"api\"`\n\t}\n\n\t// default config\n\tcfg.Mod.Listen = \":1984\"\n\n\t// load config from YAML\n\tapp.LoadConfig(&cfg)\n\n\tif cfg.Mod.Listen == \"\" && cfg.Mod.UnixListen == \"\" && cfg.Mod.TLSListen == \"\" {\n\t\treturn\n\t}\n\n\tallowPaths = cfg.Mod.AllowPaths\n\tbasePath = cfg.Mod.BasePath\n\tlog = app.GetLogger(\"api\")\n\n\tinitStatic(cfg.Mod.StaticDir)\n\n\tHandleFunc(\"api\", apiHandler)\n\tHandleFunc(\"api/config\", configHandler)\n\tHandleFunc(\"api/exit\", exitHandler)\n\tHandleFunc(\"api/restart\", restartHandler)\n\tHandleFunc(\"api/log\", logHandler)\n\n\tHandler = http.DefaultServeMux // 4th\n\n\tif cfg.Mod.Origin == \"*\" {\n\t\tHandler = middlewareCORS(Handler) // 3rd\n\t}\n\n\tif cfg.Mod.Username != \"\" {\n\t\tHandler = middlewareAuth(cfg.Mod.Username, cfg.Mod.Password, cfg.Mod.LocalAuth, Handler) // 2nd\n\t}\n\n\tif log.Trace().Enabled() {\n\t\tHandler = middlewareLog(Handler) // 1st\n\t}\n\n\tif cfg.Mod.Listen != \"\" {\n\t\t_, port, _ := net.SplitHostPort(cfg.Mod.Listen)\n\t\tPort, _ = strconv.Atoi(port)\n\t\tgo listen(\"tcp\", cfg.Mod.Listen)\n\t}\n\n\tif cfg.Mod.UnixListen != \"\" {\n\t\t_ = syscall.Unlink(cfg.Mod.UnixListen)\n\t\tgo listen(\"unix\", cfg.Mod.UnixListen)\n\t}\n\n\t// Initialize the HTTPS server\n\tif cfg.Mod.TLSListen != \"\" && cfg.Mod.TLSCert != \"\" && cfg.Mod.TLSKey != \"\" {\n\t\tgo tlsListen(\"tcp\", cfg.Mod.TLSListen, cfg.Mod.TLSCert, cfg.Mod.TLSKey)\n\t}\n}\n\nfunc listen(network, address string) {\n\tln, err := net.Listen(network, address)\n\tif err != nil {\n\t\tlog.Error().Err(err).Msg(\"[api] listen\")\n\t\treturn\n\t}\n\n\tlog.Info().Str(\"addr\", address).Msg(\"[api] listen\")\n\n\tserver := http.Server{\n\t\tHandler:           Handler,\n\t\tReadHeaderTimeout: 5 * time.Second, // Example: Set to 5 seconds\n\t}\n\tif err = server.Serve(ln); err != nil {\n\t\tlog.Fatal().Err(err).Msg(\"[api] serve\")\n\t}\n}\n\nfunc tlsListen(network, address, certFile, keyFile string) {\n\tvar cert tls.Certificate\n\tvar err error\n\tif strings.IndexByte(certFile, '\\n') < 0 && strings.IndexByte(keyFile, '\\n') < 0 {\n\t\t// check if file path\n\t\tcert, err = tls.LoadX509KeyPair(certFile, keyFile)\n\t} else {\n\t\t// if text file content\n\t\tcert, err = tls.X509KeyPair([]byte(certFile), []byte(keyFile))\n\t}\n\tif err != nil {\n\t\tlog.Error().Err(err).Caller().Send()\n\t\treturn\n\t}\n\n\tln, err := net.Listen(network, address)\n\tif err != nil {\n\t\tlog.Error().Err(err).Msg(\"[api] tls listen\")\n\t\treturn\n\t}\n\n\tlog.Info().Str(\"addr\", address).Msg(\"[api] tls listen\")\n\n\tserver := &http.Server{\n\t\tHandler:           Handler,\n\t\tTLSConfig:         &tls.Config{Certificates: []tls.Certificate{cert}},\n\t\tReadHeaderTimeout: 5 * time.Second,\n\t}\n\tif err = server.ServeTLS(ln, \"\", \"\"); err != nil {\n\t\tlog.Fatal().Err(err).Msg(\"[api] tls serve\")\n\t}\n}\n\nvar Port int\n\nconst (\n\tMimeJSON = \"application/json\"\n\tMimeText = \"text/plain\"\n)\n\nvar Handler http.Handler\n\n// HandleFunc handle pattern with relative path:\n// - \"api/streams\" => \"{basepath}/api/streams\"\n// - \"/streams\"    => \"/streams\"\nfunc HandleFunc(pattern string, handler http.HandlerFunc) {\n\tif len(pattern) == 0 || pattern[0] != '/' {\n\t\tpattern = basePath + \"/\" + pattern\n\t}\n\tif allowPaths != nil && !slices.Contains(allowPaths, pattern) {\n\t\tlog.Trace().Str(\"path\", pattern).Msg(\"[api] ignore path not in allow_paths\")\n\t\treturn\n\t}\n\tlog.Trace().Str(\"path\", pattern).Msg(\"[api] register path\")\n\thttp.HandleFunc(pattern, handler)\n}\n\n// ResponseJSON important always add Content-Type\n// so go won't need to call http.DetectContentType\nfunc ResponseJSON(w http.ResponseWriter, v any) {\n\tw.Header().Set(\"Content-Type\", MimeJSON)\n\t_ = json.NewEncoder(w).Encode(v)\n}\n\nfunc ResponsePrettyJSON(w http.ResponseWriter, v any) {\n\tw.Header().Set(\"Content-Type\", MimeJSON)\n\tenc := json.NewEncoder(w)\n\tenc.SetIndent(\"\", \"  \")\n\t_ = enc.Encode(v)\n}\n\nfunc Response(w http.ResponseWriter, body any, contentType string) {\n\tw.Header().Set(\"Content-Type\", contentType)\n\n\tswitch v := body.(type) {\n\tcase []byte:\n\t\t_, _ = w.Write(v)\n\tcase string:\n\t\t_, _ = w.Write([]byte(v))\n\tdefault:\n\t\t_, _ = fmt.Fprint(w, body)\n\t}\n}\n\nconst StreamNotFound = \"stream not found\"\n\nvar allowPaths []string\nvar basePath string\nvar log zerolog.Logger\n\nfunc middlewareLog(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tlog.Trace().Msgf(\"[api] %s %s %s\", r.Method, r.URL, r.RemoteAddr)\n\t\tnext.ServeHTTP(w, r)\n\t})\n}\n\nfunc isLoopback(remoteAddr string) bool {\n\treturn strings.HasPrefix(remoteAddr, \"127.\") || strings.HasPrefix(remoteAddr, \"[::1]\") || remoteAddr == \"@\"\n}\n\nfunc middlewareAuth(username, password string, localAuth bool, next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tif localAuth || !isLoopback(r.RemoteAddr) {\n\t\t\tuser, pass, ok := r.BasicAuth()\n\t\t\tif !ok || user != username || pass != password {\n\t\t\t\tw.Header().Set(\"Www-Authenticate\", `Basic realm=\"go2rtc\"`)\n\t\t\t\thttp.Error(w, \"Unauthorized\", http.StatusUnauthorized)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tnext.ServeHTTP(w, r)\n\t})\n}\n\nfunc middlewareCORS(next http.Handler) http.Handler {\n\treturn http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {\n\t\tw.Header().Set(\"Access-Control-Allow-Origin\", \"*\")\n\t\tw.Header().Set(\"Access-Control-Allow-Methods\", \"GET, POST, PUT, DELETE, OPTIONS\")\n\t\tw.Header().Set(\"Access-Control-Allow-Headers\", \"Authorization, Content-Type\")\n\t\tnext.ServeHTTP(w, r)\n\t})\n}\n\nvar mu sync.Mutex\n\nfunc apiHandler(w http.ResponseWriter, r *http.Request) {\n\tmu.Lock()\n\tapp.Info[\"host\"] = r.Host\n\tmu.Unlock()\n\n\tResponseJSON(w, app.Info)\n}\n\nfunc exitHandler(w http.ResponseWriter, r *http.Request) {\n\tif r.Method != \"POST\" {\n\t\thttp.Error(w, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\ts := r.URL.Query().Get(\"code\")\n\tcode, err := strconv.Atoi(s)\n\n\t// https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_08_02\n\tif err != nil || code < 0 || code > 125 {\n\t\thttp.Error(w, \"Code must be in the range [0, 125]\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tos.Exit(code)\n}\n\nfunc restartHandler(w http.ResponseWriter, r *http.Request) {\n\tif r.Method != \"POST\" {\n\t\thttp.Error(w, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tpath, err := os.Executable()\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Debug().Msgf(\"[api] restart %s\", path)\n\n\tgo syscall.Exec(path, os.Args, os.Environ())\n}\n\nfunc logHandler(w http.ResponseWriter, r *http.Request) {\n\tswitch r.Method {\n\tcase \"GET\":\n\t\t// Send current state of the log file immediately\n\t\tw.Header().Set(\"Content-Type\", \"application/jsonlines\")\n\t\t_, _ = app.MemoryLog.WriteTo(w)\n\tcase \"DELETE\":\n\t\tapp.MemoryLog.Reset()\n\t\tResponse(w, \"OK\", \"text/plain\")\n\tdefault:\n\t\thttp.Error(w, \"Method not allowed\", http.StatusBadRequest)\n\t}\n}\n\ntype Source struct {\n\tID       string `json:\"id,omitempty\"`\n\tName     string `json:\"name,omitempty\"`\n\tInfo     string `json:\"info,omitempty\"`\n\tURL      string `json:\"url,omitempty\"`\n\tLocation string `json:\"location,omitempty\"`\n}\n\nfunc ResponseSources(w http.ResponseWriter, sources []*Source) {\n\tif len(sources) == 0 {\n\t\thttp.Error(w, \"no sources\", http.StatusNotFound)\n\t\treturn\n\t}\n\n\tvar response = struct {\n\t\tSources []*Source `json:\"sources\"`\n\t}{\n\t\tSources: sources,\n\t}\n\tResponseJSON(w, response)\n}\n\nfunc Error(w http.ResponseWriter, err error) {\n\tlog.Error().Err(err).Caller(1).Send()\n\n\thttp.Error(w, err.Error(), http.StatusInsufficientStorage)\n}\n"
  },
  {
    "path": "internal/api/config.go",
    "content": "package api\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/app\"\n\t\"gopkg.in/yaml.v3\"\n)\n\nfunc configHandler(w http.ResponseWriter, r *http.Request) {\n\tif app.ConfigPath == \"\" {\n\t\thttp.Error(w, \"\", http.StatusGone)\n\t\treturn\n\t}\n\n\tswitch r.Method {\n\tcase \"GET\":\n\t\tdata, err := os.ReadFile(app.ConfigPath)\n\t\tif err != nil {\n\t\t\thttp.Error(w, \"\", http.StatusNotFound)\n\t\t\treturn\n\t\t}\n\t\t// https://www.ietf.org/archive/id/draft-ietf-httpapi-yaml-mediatypes-00.html\n\t\tResponse(w, data, \"application/yaml\")\n\n\tcase \"POST\", \"PATCH\":\n\t\tdata, err := io.ReadAll(r.Body)\n\t\tif err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\tif r.Method == \"PATCH\" {\n\t\t\t// no need to validate after merge\n\t\t\tdata, err = mergeYAML(app.ConfigPath, data)\n\t\t\tif err != nil {\n\t\t\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\t\t\treturn\n\t\t\t}\n\t\t} else {\n\t\t\t// validate config\n\t\t\tif err = yaml.Unmarshal(data, map[string]any{}); err != nil {\n\t\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tif err = os.WriteFile(app.ConfigPath, data, 0644); err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc mergeYAML(file1 string, yaml2 []byte) ([]byte, error) {\n\t// Read the contents of the first YAML file\n\tdata1, err := os.ReadFile(file1)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Unmarshal the first YAML file into a map\n\tvar config1 map[string]any\n\tif err = yaml.Unmarshal(data1, &config1); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Unmarshal the second YAML document into a map\n\tvar config2 map[string]any\n\tif err = yaml.Unmarshal(yaml2, &config2); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Merge the two maps\n\tconfig1 = merge(config1, config2)\n\n\t// Marshal the merged map into YAML\n\treturn yaml.Marshal(&config1)\n}\n\nfunc merge(dst, src map[string]any) map[string]any {\n\tfor k, v := range src {\n\t\tif vv, ok := dst[k]; ok {\n\t\t\tswitch vv := vv.(type) {\n\t\t\tcase map[string]any:\n\t\t\t\tv := v.(map[string]any)\n\t\t\t\tdst[k] = merge(vv, v)\n\t\t\tcase []any:\n\t\t\t\tv := v.([]any)\n\t\t\t\tdst[k] = v\n\t\t\tdefault:\n\t\t\t\tdst[k] = v\n\t\t\t}\n\t\t} else {\n\t\t\tdst[k] = v\n\t\t}\n\t}\n\treturn dst\n}\n"
  },
  {
    "path": "internal/api/static.go",
    "content": "package api\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/AlexxIT/go2rtc/www\"\n)\n\nfunc initStatic(staticDir string) {\n\tvar root http.FileSystem\n\tif staticDir != \"\" {\n\t\tlog.Info().Str(\"dir\", staticDir).Msg(\"[api] serve static\")\n\t\troot = http.Dir(staticDir)\n\t} else {\n\t\troot = http.FS(www.Static)\n\t}\n\n\tbase := len(basePath)\n\tfileServer := http.FileServer(root)\n\n\tHandleFunc(\"\", func(w http.ResponseWriter, r *http.Request) {\n\t\tif base > 0 {\n\t\t\tr.URL.Path = r.URL.Path[base:]\n\t\t}\n\t\tfileServer.ServeHTTP(w, r)\n\t})\n}\n"
  },
  {
    "path": "internal/api/ws/README.md",
    "content": "# WebSocket\n\nEndpoint: `/api/ws`\n\nQuery parameters:\n\n- `src` (required) - Stream name\n\n### WebRTC\n\nRequest SDP:\n\n```json\n{\"type\":\"webrtc/offer\",\"value\":\"v=0\\r\\n...\"}\n```\n\nResponse SDP:\n\n```json\n{\"type\":\"webrtc/answer\",\"value\":\"v=0\\r\\n...\"}\n```\n\nRequest/response candidate:\n\n- empty value also allowed and optional\n\n```json\n{\"type\":\"webrtc/candidate\",\"value\":\"candidate:3277516026 1 udp 2130706431 192.168.1.123 54321 typ host\"}\n```\n\n### MSE\n\nRequest:\n\n- codecs list optional\n\n```json\n{\"type\":\"mse\",\"value\":\"avc1.640029,avc1.64002A,avc1.640033,hvc1.1.6.L153.B0,mp4a.40.2,mp4a.40.5,flac,opus\"}\n```\n\nResponse:\n\n```json\n{\"type\":\"mse\",\"value\":\"video/mp4; codecs=\\\"avc1.64001F,mp4a.40.2\\\"\"}\n```\n\n### HLS\n\nRequest:\n\n```json\n{\"type\":\"hls\",\"value\":\"avc1.640029,avc1.64002A,avc1.640033,hvc1.1.6.L153.B0,mp4a.40.2,mp4a.40.5,flac\"}\n```\n\nResponse:\n\n- you MUST rewrite full HTTP path to `http://192.168.1.123:1984/api/hls/playlist.m3u8`\n\n```json\n{\"type\":\"hls\",\"value\":\"#EXTM3U\\n#EXT-X-STREAM-INF:BANDWIDTH=1000000,CODECS=\\\"avc1.64001F,mp4a.40.2\\\"\\nhls/playlist.m3u8?id=DvmHdd9w\"}\n```\n\n### MJPEG\n\nRequest/response:\n\n```json\n{\"type\":\"mjpeg\"}\n```\n"
  },
  {
    "path": "internal/api/ws/ws.go",
    "content": "package ws\n\nimport (\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/internal/app\"\n\t\"github.com/AlexxIT/go2rtc/pkg/creds\"\n\t\"github.com/gorilla/websocket\"\n\t\"github.com/rs/zerolog\"\n)\n\nfunc Init() {\n\tvar cfg struct {\n\t\tMod struct {\n\t\t\tOrigin string `yaml:\"origin\"`\n\t\t} `yaml:\"api\"`\n\t}\n\n\tapp.LoadConfig(&cfg)\n\n\tlog = app.GetLogger(\"api\")\n\n\tinitWS(cfg.Mod.Origin)\n\n\tapi.HandleFunc(\"api/ws\", apiWS)\n}\n\nvar log zerolog.Logger\n\n// Message - struct for data exchange in Web API\ntype Message struct {\n\tType  string `json:\"type\"`\n\tValue any    `json:\"value,omitempty\"`\n}\n\nfunc (m *Message) String() (value string) {\n\tif s, ok := m.Value.(string); ok {\n\t\treturn s\n\t}\n\treturn\n}\n\nfunc (m *Message) Unmarshal(v any) error {\n\tb, err := json.Marshal(m.Value)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn json.Unmarshal(b, v)\n}\n\ntype WSHandler func(tr *Transport, msg *Message) error\n\nfunc HandleFunc(msgType string, handler WSHandler) {\n\twsHandlers[msgType] = handler\n}\n\nvar wsHandlers = make(map[string]WSHandler)\n\nfunc initWS(origin string) {\n\twsUp = &websocket.Upgrader{\n\t\tReadBufferSize:  4096,       // for SDP\n\t\tWriteBufferSize: 512 * 1024, // 512K\n\t}\n\n\tswitch origin {\n\tcase \"\":\n\t\t// same origin + ignore port\n\t\twsUp.CheckOrigin = func(r *http.Request) bool {\n\t\t\torigin := r.Header[\"Origin\"]\n\t\t\tif len(origin) == 0 {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\to, err := url.Parse(origin[0])\n\t\t\tif err != nil {\n\t\t\t\treturn false\n\t\t\t}\n\t\t\tif o.Host == r.Host {\n\t\t\t\treturn true\n\t\t\t}\n\t\t\tlog.Trace().Msgf(\"[api] ws origin=%s, host=%s\", o.Host, r.Host)\n\t\t\t// https://github.com/AlexxIT/go2rtc/issues/118\n\t\t\tif i := strings.IndexByte(o.Host, ':'); i > 0 {\n\t\t\t\treturn o.Host[:i] == r.Host\n\t\t\t}\n\t\t\treturn false\n\t\t}\n\tcase \"*\":\n\t\t// any origin\n\t\twsUp.CheckOrigin = func(r *http.Request) bool {\n\t\t\treturn true\n\t\t}\n\t}\n}\n\nfunc apiWS(w http.ResponseWriter, r *http.Request) {\n\tws, err := wsUp.Upgrade(w, r, nil)\n\tif err != nil {\n\t\torigin := r.Header.Get(\"Origin\")\n\t\tlog.Error().Err(err).Caller().Msgf(\"host=%s origin=%s\", r.Host, origin)\n\t\treturn\n\t}\n\n\ttr := &Transport{Request: r}\n\ttr.OnWrite(func(msg any) error {\n\t\t_ = ws.SetWriteDeadline(time.Now().Add(time.Second * 5))\n\n\t\tif data, ok := msg.([]byte); ok {\n\t\t\treturn ws.WriteMessage(websocket.BinaryMessage, data)\n\t\t} else {\n\t\t\treturn ws.WriteJSON(msg)\n\t\t}\n\t})\n\n\tfor {\n\t\tmsg := new(Message)\n\t\tif err = ws.ReadJSON(msg); err != nil {\n\t\t\tif !websocket.IsCloseError(err, websocket.CloseNoStatusReceived) {\n\t\t\t\tlog.Trace().Err(err).Caller().Send()\n\t\t\t}\n\t\t\t_ = ws.Close()\n\t\t\tbreak\n\t\t}\n\n\t\tlog.Trace().Str(\"type\", msg.Type).Msg(\"[api] ws msg\")\n\n\t\tif handler := wsHandlers[msg.Type]; handler != nil {\n\t\t\tgo func() {\n\t\t\t\tif err = handler(tr, msg); err != nil {\n\t\t\t\t\terrMsg := creds.SecretString(err.Error())\n\t\t\t\t\ttr.Write(&Message{Type: \"error\", Value: msg.Type + \": \" + errMsg})\n\t\t\t\t}\n\t\t\t}()\n\t\t}\n\t}\n\n\ttr.Close()\n}\n\nvar wsUp *websocket.Upgrader\n\ntype Transport struct {\n\tRequest *http.Request\n\n\tctx map[any]any\n\n\tclosed bool\n\tmx     sync.Mutex\n\twrmx   sync.Mutex\n\n\tonChange func()\n\tonWrite  func(msg any) error\n\tonClose  []func()\n}\n\nfunc (t *Transport) OnWrite(f func(msg any) error) {\n\tt.mx.Lock()\n\tif t.onChange != nil {\n\t\tt.onChange()\n\t}\n\tt.onWrite = f\n\tt.mx.Unlock()\n}\n\nfunc (t *Transport) Write(msg any) {\n\tt.wrmx.Lock()\n\t_ = t.onWrite(msg)\n\tt.wrmx.Unlock()\n}\n\nfunc (t *Transport) Close() {\n\tt.mx.Lock()\n\tfor _, f := range t.onClose {\n\t\tf()\n\t}\n\tt.closed = true\n\tt.mx.Unlock()\n}\n\nfunc (t *Transport) OnChange(f func()) {\n\tt.mx.Lock()\n\tt.onChange = f\n\tt.mx.Unlock()\n}\n\nfunc (t *Transport) OnClose(f func()) {\n\tt.mx.Lock()\n\tif t.closed {\n\t\tf()\n\t} else {\n\t\tt.onClose = append(t.onClose, f)\n\t}\n\tt.mx.Unlock()\n}\n\n// WithContext - run function with Context variable\nfunc (t *Transport) WithContext(f func(ctx map[any]any)) {\n\tt.mx.Lock()\n\tif t.ctx == nil {\n\t\tt.ctx = map[any]any{}\n\t}\n\tf(t.ctx)\n\tt.mx.Unlock()\n}\n\nfunc (t *Transport) Writer() io.Writer {\n\treturn &writer{t: t}\n}\n\ntype writer struct {\n\tt *Transport\n}\n\nfunc (w *writer) Write(p []byte) (n int, err error) {\n\tw.t.wrmx.Lock()\n\tif err = w.t.onWrite(p); err == nil {\n\t\tn = len(p)\n\t}\n\tw.t.wrmx.Unlock()\n\treturn\n}\n"
  },
  {
    "path": "internal/app/README.md",
    "content": "# App\n\nThe application module is responsible for reading configuration files, running other modules and setting up [logs](#log).\n\nThe configuration can be edited through the application's WebUI with code highlighting, syntax and specification checking.\n\n- By default, go2rtc will search for the `go2rtc.yaml` config file in the current working directory\n- go2rtc supports multiple config files:\n  - `go2rtc -c config1.yaml -c config2.yaml -c config3.yaml`\n- go2rtc supports inline config in multiple formats from the command line:\n  - **YAML**: `go2rtc -c '{log: {format: text}}'`\n  - **JSON**: `go2rtc -c '{\"log\":{\"format\":\"text\"}}'`\n  - **key=value**: `go2rtc -c log.format=text`\n- Each subsequent config will overwrite the previous one (but only for defined params)\n\n```\ngo2rtc -config \"{log: {format: text}}\" -config /config/go2rtc.yaml -config \"{rtsp: {listen: ''}}\" -config /usr/local/go2rtc/go2rtc.yaml\n```\n\nor a simpler version\n\n```\ngo2rtc -c log.format=text -c /config/go2rtc.yaml -c rtsp.listen='' -c /usr/local/go2rtc/go2rtc.yaml\n```\n\n## Environment variables\n\nThere is support for loading external variables into the config. First, they will be loaded from [credential files](https://systemd.io/CREDENTIALS). If `CREDENTIALS_DIRECTORY` is not set, then the key will be loaded from an environment variable. If no environment variable is set, then the string will be left as-is.\n\n```yaml\nstreams:\n  camera1: rtsp://rtsp:${CAMERA_PASSWORD}@192.168.1.123/av_stream/ch0\n\nrtsp:\n  username: ${RTSP_USER:admin}   # \"admin\" if \"RTSP_USER\" not set\n  password: ${RTSP_PASS:secret}  # \"secret\" if \"RTSP_PASS\" not set\n```\n\n## JSON Schema\n\nEditors like [GoLand](https://www.jetbrains.com/go/) and [VS Code](https://code.visualstudio.com/) support autocomplete and syntax validation.\n\n```yaml\n# yaml-language-server: $schema=https://raw.githubusercontent.com/AlexxIT/go2rtc/master/www/schema.json\n```\n\nor from a running go2rtc:\n\n```yaml\n# yaml-language-server: $schema=http://localhost:1984/schema.json\n```\n\n## Defaults\n\n- Default values may change in updates\n- FFmpeg module has many presets, they are not listed here because they may also change in updates\n\n```yaml\napi:\n  listen: \":1984\"  # default public port for WebUI and HTTP API\n\nffmpeg:\n  bin: \"ffmpeg\"    # default binary path for FFmpeg\n\nlog:\n  level: \"info\"    # default log level\n  output: \"stdout\"\n  time: \"UNIXMS\"\n\nrtsp:\n  listen: \":8554\"  # default public port for RTSP server\n  default_query: \"video&audio\"\n\nsrtp:\n  listen: \":8443\"  # default public port for SRTP server (used for HomeKit)\n\nwebrtc:\n  listen: \":8555\"  # default public port for WebRTC server (TCP and UDP)\n  ice_servers:\n    - urls: [ \"stun:stun.cloudflare.com:3478\", \"stun:stun.l.google.com:19302\" ]\n```\n\n## Log\n\nYou can set different log levels for different modules.\n\n```yaml\nlog:\n  format: \"\"        # empty (default, autodetect color support), color, json, text \n  level: \"info\"     # disabled, trace, debug, info (default), warn, error\n  output: \"stdout\"  # empty (only to memory), stderr, stdout (default)\n  time: \"UNIXMS\"    # empty (disable timestamp), UNIXMS (default), UNIXMICRO, UNIXNANO\n  \n  api: trace   # module name: log level\n```\n\nModules: `api`, `streams`, `rtsp`, `webrtc`, `mp4`, `hls`, `mjpeg`, `hass`, `homekit`, `onvif`, `rtmp`, `webtorrent`, `wyoming`, `echo`, `exec`, `expr`, `ffmpeg`, `wyze`, `xiaomi`.\n"
  },
  {
    "path": "internal/app/app.go",
    "content": "package app\n\nimport (\n\t\"flag\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"runtime\"\n\t\"runtime/debug\"\n)\n\nvar (\n\tVersion    string\n\tModules    []string\n\tUserAgent  string\n\tConfigPath string\n\tInfo       = make(map[string]any)\n)\n\nconst usage = `Usage of go2rtc:\n\n  -c, --config   Path to config file or config string as YAML or JSON, support multiple\n  -d, --daemon   Run in background\n  -v, --version  Print version and exit\n`\n\nfunc Init() {\n\tvar config flagConfig\n\tvar daemon bool\n\tvar version bool\n\n\tflag.Var(&config, \"config\", \"\")\n\tflag.Var(&config, \"c\", \"\")\n\tflag.BoolVar(&daemon, \"daemon\", false, \"\")\n\tflag.BoolVar(&daemon, \"d\", false, \"\")\n\tflag.BoolVar(&version, \"version\", false, \"\")\n\tflag.BoolVar(&version, \"v\", false, \"\")\n\n\tflag.Usage = func() { fmt.Print(usage) }\n\tflag.Parse()\n\n\trevision, vcsTime := readRevisionTime()\n\n\tif version {\n\t\tfmt.Printf(\"go2rtc version %s (%s) %s/%s\\n\", Version, revision, runtime.GOOS, runtime.GOARCH)\n\t\tos.Exit(0)\n\t}\n\n\tif daemon && os.Getppid() != 1 {\n\t\tif runtime.GOOS == \"windows\" {\n\t\t\tfmt.Println(\"Daemon mode is not supported on Windows\")\n\t\t\tos.Exit(1)\n\t\t}\n\n\t\t// Re-run the program in background and exit\n\t\tcmd := exec.Command(os.Args[0], os.Args[1:]...)\n\t\tif err := cmd.Start(); err != nil {\n\t\t\tfmt.Println(\"Failed to start daemon:\", err)\n\t\t\tos.Exit(1)\n\t\t}\n\t\tfmt.Println(\"Running in daemon mode with PID:\", cmd.Process.Pid)\n\t\tos.Exit(0)\n\t}\n\n\tUserAgent = \"go2rtc/\" + Version\n\n\tInfo[\"version\"] = Version\n\tInfo[\"revision\"] = revision\n\n\tinitConfig(config)\n\tinitLogger()\n\n\tplatform := fmt.Sprintf(\"%s/%s\", runtime.GOOS, runtime.GOARCH)\n\tLogger.Info().Str(\"version\", Version).Str(\"platform\", platform).Str(\"revision\", revision).Msg(\"go2rtc\")\n\tLogger.Debug().Str(\"version\", runtime.Version()).Str(\"vcs.time\", vcsTime).Msg(\"build\")\n\n\tif ConfigPath != \"\" {\n\t\tLogger.Info().Str(\"path\", ConfigPath).Msg(\"config\")\n\t}\n\n\tvar cfg struct {\n\t\tMod struct {\n\t\t\tModules []string `yaml:\"modules\"`\n\t\t} `yaml:\"app\"`\n\t}\n\n\tLoadConfig(&cfg)\n\n\tModules = cfg.Mod.Modules\n}\n\nfunc readRevisionTime() (revision, vcsTime string) {\n\tif info, ok := debug.ReadBuildInfo(); ok {\n\t\tfor _, setting := range info.Settings {\n\t\t\tswitch setting.Key {\n\t\t\tcase \"vcs.revision\":\n\t\t\t\tif len(setting.Value) > 7 {\n\t\t\t\t\trevision = setting.Value[:7]\n\t\t\t\t} else {\n\t\t\t\t\trevision = setting.Value\n\t\t\t\t}\n\t\t\tcase \"vcs.time\":\n\t\t\t\tvcsTime = setting.Value\n\t\t\tcase \"vcs.modified\":\n\t\t\t\tif setting.Value == \"true\" {\n\t\t\t\t\trevision += \".dirty\"\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Check version from -buildvcs info\n\t\t// Format for tagged version : v1.9.13\n\t\t// Format for modified code:   v1.9.14-0.20251215184105-753d6617ab58+dirty\n\t\tif info.Main.Version != \"v\"+Version {\n\t\t\t// Format: 1.9.13+dev.753d661[.dirty]\n\t\t\t// Compatible with \"awesomeversion\" and \"packaging.version\" from python.\n\t\t\t// Version will be larger than the previous release, but smaller than the next release.\n\t\t\tVersion += \"+dev.\" + revision\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "internal/app/config.go",
    "content": "package app\n\nimport (\n\t\"errors\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/creds\"\n\t\"github.com/AlexxIT/go2rtc/pkg/yaml\"\n)\n\nfunc LoadConfig(v any) {\n\tfor _, data := range configs {\n\t\tif err := yaml.Unmarshal(data, v); err != nil {\n\t\t\tLogger.Warn().Err(err).Send()\n\t\t}\n\t}\n}\n\nvar configMu sync.Mutex\n\nfunc PatchConfig(path []string, value any) error {\n\tif ConfigPath == \"\" {\n\t\treturn errors.New(\"config file disabled\")\n\t}\n\n\tconfigMu.Lock()\n\tdefer configMu.Unlock()\n\n\t// empty config is OK\n\tb, _ := os.ReadFile(ConfigPath)\n\n\tb, err := yaml.Patch(b, path, value)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn os.WriteFile(ConfigPath, b, 0644)\n}\n\ntype flagConfig []string\n\nfunc (c *flagConfig) String() string {\n\treturn strings.Join(*c, \" \")\n}\n\nfunc (c *flagConfig) Set(value string) error {\n\t*c = append(*c, value)\n\treturn nil\n}\n\nvar configs [][]byte\n\nfunc initConfig(confs flagConfig) {\n\tif confs == nil {\n\t\tconfs = []string{\"go2rtc.yaml\"}\n\t}\n\n\tfor _, conf := range confs {\n\t\tif len(conf) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tif conf[0] == '{' {\n\t\t\t// config as raw YAML or JSON\n\t\t\tconfigs = append(configs, []byte(conf))\n\t\t} else if data := parseConfString(conf); data != nil {\n\t\t\tconfigs = append(configs, data)\n\t\t} else {\n\t\t\t// config as file\n\t\t\tif ConfigPath == \"\" {\n\t\t\t\tConfigPath = conf\n\t\t\t\tinitStorage()\n\t\t\t}\n\n\t\t\tif data, _ = os.ReadFile(conf); data == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tloadEnv(data)\n\t\t\tdata = creds.ReplaceVars(data)\n\t\t\tconfigs = append(configs, data)\n\t\t}\n\t}\n\n\tif ConfigPath != \"\" {\n\t\tif !filepath.IsAbs(ConfigPath) {\n\t\t\tif cwd, err := os.Getwd(); err == nil {\n\t\t\t\tConfigPath = filepath.Join(cwd, ConfigPath)\n\t\t\t}\n\t\t}\n\t\tInfo[\"config_path\"] = ConfigPath\n\t}\n}\n\nfunc parseConfString(s string) []byte {\n\ti := strings.IndexByte(s, '=')\n\tif i < 0 {\n\t\treturn nil\n\t}\n\n\titems := strings.Split(s[:i], \".\")\n\tif len(items) < 2 {\n\t\treturn nil\n\t}\n\n\t// `log.level=trace` => `{log: {level: trace}}`\n\tvar pre string\n\tvar suf = s[i+1:]\n\tfor _, item := range items {\n\t\tpre += \"{\" + item + \": \"\n\t\tsuf += \"}\"\n\t}\n\n\treturn []byte(pre + suf)\n}\n"
  },
  {
    "path": "internal/app/log.go",
    "content": "package app\n\nimport (\n\t\"io\"\n\t\"os\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/creds\"\n\t\"github.com/mattn/go-isatty\"\n\t\"github.com/rs/zerolog\"\n)\n\nvar MemoryLog = newBuffer()\n\nfunc GetLogger(module string) zerolog.Logger {\n\tLogger.Trace().Str(\"module\", module).Msgf(\"[log] init\")\n\n\tif s, ok := modules[module]; ok {\n\t\tlvl, err := zerolog.ParseLevel(s)\n\t\tif err == nil {\n\t\t\treturn Logger.Level(lvl)\n\t\t}\n\t\tLogger.Warn().Err(err).Caller().Send()\n\t}\n\n\treturn Logger\n}\n\n// initLogger support:\n// - output: empty (only to memory), stderr, stdout\n// - format: empty (autodetect color support), color, json, text\n// - time:   empty (disable timestamp), UNIXMS, UNIXMICRO, UNIXNANO\n// - level:  disabled, trace, debug, info, warn, error...\nfunc initLogger() {\n\tvar cfg struct {\n\t\tMod map[string]string `yaml:\"log\"`\n\t}\n\n\tcfg.Mod = modules // defaults\n\n\tLoadConfig(&cfg)\n\n\tvar writer io.Writer\n\n\tswitch output, path, _ := strings.Cut(modules[\"output\"], \":\"); output {\n\tcase \"stderr\":\n\t\twriter = os.Stderr\n\tcase \"stdout\":\n\t\twriter = os.Stdout\n\tcase \"file\":\n\t\tif path == \"\" {\n\t\t\tpath = \"go2rtc.log\"\n\t\t}\n\t\t// if fail - only MemoryLog will be available\n\t\twriter, _ = os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)\n\t}\n\n\ttimeFormat := modules[\"time\"]\n\n\tif writer != nil {\n\t\tif format := modules[\"format\"]; format != \"json\" {\n\t\t\tconsole := &zerolog.ConsoleWriter{Out: writer}\n\n\t\t\tswitch format {\n\t\t\tcase \"text\":\n\t\t\t\tconsole.NoColor = true\n\t\t\tcase \"color\":\n\t\t\t\tconsole.NoColor = false // useless, but anyway\n\t\t\tdefault:\n\t\t\t\t// autodetection if output support color\n\t\t\t\t// go-isatty - dependency for go-colorable - dependency for ConsoleWriter\n\t\t\t\tconsole.NoColor = !isatty.IsTerminal(writer.(*os.File).Fd())\n\t\t\t}\n\n\t\t\tif timeFormat != \"\" {\n\t\t\t\tconsole.TimeFormat = \"15:04:05.000\"\n\t\t\t} else {\n\t\t\t\tconsole.PartsOrder = []string{\n\t\t\t\t\tzerolog.LevelFieldName,\n\t\t\t\t\tzerolog.CallerFieldName,\n\t\t\t\t\tzerolog.MessageFieldName,\n\t\t\t\t}\n\t\t\t}\n\n\t\t\twriter = console\n\t\t}\n\n\t\twriter = zerolog.MultiLevelWriter(writer, MemoryLog)\n\t} else {\n\t\twriter = MemoryLog\n\t}\n\n\twriter = creds.SecretWriter(writer)\n\n\tlvl, _ := zerolog.ParseLevel(modules[\"level\"])\n\tLogger = zerolog.New(writer).Level(lvl)\n\n\tif timeFormat != \"\" {\n\t\tzerolog.TimeFieldFormat = timeFormat\n\t\tLogger = Logger.With().Timestamp().Logger()\n\t}\n}\n\nvar Logger zerolog.Logger\n\n// modules log levels\nvar modules = map[string]string{\n\t\"format\": \"\", // useless, but anyway\n\t\"level\":  \"info\",\n\t\"output\": \"stdout\", // TODO: change to stderr someday\n\t\"time\":   zerolog.TimeFormatUnixMs,\n}\n\nconst (\n\tchunkCount = 16\n\tchunkSize  = 1 << 16\n)\n\ntype circularBuffer struct {\n\tchunks [][]byte\n\tr, w   int\n\tmu     sync.Mutex\n}\n\nfunc newBuffer() *circularBuffer {\n\tb := &circularBuffer{chunks: make([][]byte, 0, chunkCount)}\n\t// create first chunk\n\tb.chunks = append(b.chunks, make([]byte, 0, chunkSize))\n\treturn b\n}\n\nfunc (b *circularBuffer) Write(p []byte) (n int, err error) {\n\tn = len(p)\n\n\tb.mu.Lock()\n\t// check if chunk has size\n\tif len(b.chunks[b.w])+n > chunkSize {\n\t\t// increase write chunk index\n\t\tif b.w++; b.w == chunkCount {\n\t\t\tb.w = 0\n\t\t}\n\t\t// check overflow\n\t\tif b.r == b.w {\n\t\t\t// increase read chunk index\n\t\t\tif b.r++; b.r == chunkCount {\n\t\t\t\tb.r = 0\n\t\t\t}\n\t\t}\n\t\t// check if current chunk exists\n\t\tif b.w == len(b.chunks) {\n\t\t\t// allocate new chunk\n\t\t\tb.chunks = append(b.chunks, make([]byte, 0, chunkSize))\n\t\t} else {\n\t\t\t// reset len of current chunk\n\t\t\tb.chunks[b.w] = b.chunks[b.w][:0]\n\t\t}\n\t}\n\n\tb.chunks[b.w] = append(b.chunks[b.w], p...)\n\tb.mu.Unlock()\n\treturn\n}\n\nfunc (b *circularBuffer) WriteTo(w io.Writer) (n int64, err error) {\n\tbuf := make([]byte, 0, chunkCount*chunkSize)\n\n\t// use temp buffer inside mutex because w.Write can take some time\n\tb.mu.Lock()\n\tfor i := b.r; ; {\n\t\tbuf = append(buf, b.chunks[i]...)\n\t\tif i == b.w {\n\t\t\tbreak\n\t\t}\n\t\tif i++; i == chunkCount {\n\t\t\ti = 0\n\t\t}\n\t}\n\tb.mu.Unlock()\n\n\tnn, err := w.Write(buf)\n\treturn int64(nn), err\n}\n\nfunc (b *circularBuffer) Reset() {\n\tb.mu.Lock()\n\tb.chunks[0] = b.chunks[0][:0]\n\tb.r = 0\n\tb.w = 0\n\tb.mu.Unlock()\n}\n"
  },
  {
    "path": "internal/app/storage.go",
    "content": "package app\n\nimport (\n\t\"sync\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/creds\"\n\t\"github.com/AlexxIT/go2rtc/pkg/yaml\"\n)\n\nfunc initStorage() {\n\tstorage = &envStorage{data: make(map[string]string)}\n\tcreds.SetStorage(storage)\n}\n\nfunc loadEnv(data []byte) {\n\tvar cfg struct {\n\t\tEnv map[string]string `yaml:\"env\"`\n\t}\n\n\tif err := yaml.Unmarshal(data, &cfg); err != nil {\n\t\treturn\n\t}\n\n\tstorage.mu.Lock()\n\tfor name, value := range cfg.Env {\n\t\tstorage.data[name] = value\n\t\tcreds.AddSecret(value)\n\t}\n\tstorage.mu.Unlock()\n}\n\nvar storage *envStorage\n\ntype envStorage struct {\n\tdata map[string]string\n\tmu   sync.Mutex\n}\n\nfunc (s *envStorage) SetValue(name, value string) error {\n\tif err := PatchConfig([]string{\"env\", name}, value); err != nil {\n\t\treturn err\n\t}\n\n\ts.mu.Lock()\n\ts.data[name] = value\n\ts.mu.Unlock()\n\n\treturn nil\n}\n\nfunc (s *envStorage) GetValue(name string) (value string, ok bool) {\n\ts.mu.Lock()\n\tvalue, ok = s.data[name]\n\ts.mu.Unlock()\n\treturn\n}\n"
  },
  {
    "path": "internal/bubble/README.md",
    "content": "# Bubble\n\n[`new in v1.6.1`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.1)\n\nPrivate format in some cameras from [dvr163.com](http://help.dvr163.com/) and [eseecloud.com](http://www.eseecloud.com/).\n\n## Configuration\n\n- you can skip `username`, `password`, `port`, `ch` and `stream` if they are default\n- set up separate streams for different channels and streams\n\n```yaml\nstreams:\n  camera1: bubble://username:password@192.168.1.123:34567/bubble/live?ch=0&stream=0\n```\n"
  },
  {
    "path": "internal/bubble/bubble.go",
    "content": "package bubble\n\nimport (\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/bubble\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\nfunc Init() {\n\tstreams.HandleFunc(\"bubble\", func(source string) (core.Producer, error) {\n\t\treturn bubble.Dial(source)\n\t})\n}\n"
  },
  {
    "path": "internal/debug/README.md",
    "content": "# Debug\n\nThis module provides `GET /api/stack`, with which you can find hanging goroutines\n"
  },
  {
    "path": "internal/debug/debug.go",
    "content": "package debug\n\nimport (\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n)\n\nfunc Init() {\n\tapi.HandleFunc(\"api/stack\", stackHandler)\n}\n"
  },
  {
    "path": "internal/debug/stack.go",
    "content": "package debug\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"runtime\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n)\n\nvar stackSkip = [][]byte{\n\t// main.go\n\t[]byte(\"main.main()\"),\n\t[]byte(\"created by os/signal.Notify\"),\n\n\t// api/stack.go\n\t[]byte(\"github.com/AlexxIT/go2rtc/internal/api.stackHandler\"),\n\n\t// api/api.go\n\t[]byte(\"created by github.com/AlexxIT/go2rtc/internal/api.Init\"),\n\t[]byte(\"created by net/http.(*connReader).startBackgroundRead\"),\n\t[]byte(\"created by net/http.(*Server).Serve\"), // TODO: why two?\n\n\t[]byte(\"created by github.com/AlexxIT/go2rtc/internal/rtsp.Init\"),\n\t[]byte(\"created by github.com/AlexxIT/go2rtc/internal/srtp.Init\"),\n\n\t// homekit\n\t[]byte(\"created by github.com/AlexxIT/go2rtc/internal/homekit.Init\"),\n\n\t// webrtc/api.go\n\t[]byte(\"created by github.com/pion/ice/v4.NewTCPMuxDefault\"),\n\t[]byte(\"created by github.com/pion/ice/v4.NewUDPMuxDefault\"),\n}\n\nfunc stackHandler(w http.ResponseWriter, r *http.Request) {\n\tsep := []byte(\"\\n\\n\")\n\tbuf := make([]byte, 65535)\n\ti := 0\n\tn := runtime.Stack(buf, true)\n\tskipped := 0\n\tfor _, item := range bytes.Split(buf[:n], sep) {\n\t\tfor _, skip := range stackSkip {\n\t\t\tif bytes.Contains(item, skip) {\n\t\t\t\titem = nil\n\t\t\t\tskipped++\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif item != nil {\n\t\t\ti += copy(buf[i:], item)\n\t\t\ti += copy(buf[i:], sep)\n\t\t}\n\t}\n\ti += copy(buf[i:], fmt.Sprintf(\n\t\t\"Total: %d, Skipped: %d\", runtime.NumGoroutine(), skipped),\n\t)\n\n\tapi.Response(w, buf[:i], api.MimeText)\n}\n"
  },
  {
    "path": "internal/doorbird/README.md",
    "content": "# Doorbird\n\n[`new in v1.9.8`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.9.8)\n\nThis source type supports [Doorbird](https://www.doorbird.com/) devices including MJPEG stream, audio stream as well as two-way audio.\n\nIt is recommended to create a separate user within your doorbird setup for go2rtc. Minimum permissions for the user are:\n\n- Watch always\n- API operator\n\n## Configuration\n\n```yaml\nstreams:\n  doorbird1:\n    - rtsp://admin:password@192.168.1.123:8557/mpeg/720p/media.amp  # RTSP stream\n    - doorbird://admin:password@192.168.1.123?media=video           # MJPEG stream\n    - doorbird://admin:password@192.168.1.123?media=audio           # audio stream\n    - doorbird://admin:password@192.168.1.123                       # two-way audio\n```\n"
  },
  {
    "path": "internal/doorbird/doorbird.go",
    "content": "package doorbird\n\nimport (\n\t\"net/url\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/doorbird\"\n)\n\nfunc Init() {\n\tstreams.RedirectFunc(\"doorbird\", func(rawURL string) (string, error) {\n\t\tu, err := url.Parse(rawURL)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// https://www.doorbird.com/downloads/api_lan.pdf\n\t\tswitch u.Query().Get(\"media\") {\n\t\tcase \"video\":\n\t\t\tu.Path = \"/bha-api/video.cgi\"\n\t\tcase \"audio\":\n\t\t\tu.Path = \"/bha-api/audio-receive.cgi\"\n\t\tdefault:\n\t\t\treturn \"\", nil\n\t\t}\n\n\t\tu.Scheme = \"http\"\n\n\t\treturn u.String(), nil\n\t})\n\n\tstreams.HandleFunc(\"doorbird\", func(source string) (core.Producer, error) {\n\t\treturn doorbird.Dial(source)\n\t})\n}\n"
  },
  {
    "path": "internal/dvrip/README.md",
    "content": "# DVR-IP\n\n[`new in v1.2.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.2.0)\n\nPrivate format from DVR-IP NVR, NetSurveillance, Sofia protocol (NETsurveillance ActiveX plugin XMeye SDK).\n\n## Configuration\n\n- you can skip `username`, `password`, `port`, `channel` and `subtype` if they are default\n- set up separate streams for different channels\n- use `subtype=0` for Main stream, and `subtype=1` for Extra1 stream\n- only the TCP protocol is supported\n\n```yaml\nstreams:\n  only_stream: dvrip://username:password@192.168.1.123:34567?channel=0&subtype=0\n  only_tts: dvrip://username:password@192.168.1.123:34567?backchannel=1\n  two_way_audio:\n    - dvrip://username:password@192.168.1.123:34567?channel=0&subtype=0\n    - dvrip://username:password@192.168.1.123:34567?backchannel=1\n```\n"
  },
  {
    "path": "internal/dvrip/dvrip.go",
    "content": "package dvrip\n\nimport (\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/dvrip\"\n)\n\nfunc Init() {\n\tstreams.HandleFunc(\"dvrip\", dvrip.Dial)\n\n\t// DVRIP client autodiscovery\n\tapi.HandleFunc(\"api/dvrip\", apiDvrip)\n}\n\nconst Port = 34569 // UDP port number for dvrip discovery\n\nfunc apiDvrip(w http.ResponseWriter, r *http.Request) {\n\titems, err := discover()\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tapi.ResponseSources(w, items)\n}\n\nfunc discover() ([]*api.Source, error) {\n\taddr := &net.UDPAddr{\n\t\tPort: Port,\n\t\tIP:   net.IP{239, 255, 255, 250},\n\t}\n\n\tconn, err := net.ListenUDP(\"udp4\", addr)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdefer conn.Close()\n\n\tgo sendBroadcasts(conn)\n\n\tvar items []*api.Source\n\n\tfor _, info := range getResponses(conn) {\n\t\tif info.HostIP == \"\" || info.HostName == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\thost, err := hexToDecimalBytes(info.HostIP)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\titems = append(items, &api.Source{\n\t\t\tName: info.HostName,\n\t\t\tURL:  \"dvrip://user:pass@\" + host + \"?channel=0&subtype=0\",\n\t\t})\n\t}\n\n\treturn items, nil\n}\n\nfunc sendBroadcasts(conn *net.UDPConn) {\n\t// broadcasting the same multiple times because the devies some times don't answer\n\tdata, err := hex.DecodeString(\"ff00000000000000000000000000fa0500000000\")\n\tif err != nil {\n\t\treturn\n\t}\n\n\taddr := &net.UDPAddr{\n\t\tPort: Port,\n\t\tIP:   net.IP{255, 255, 255, 255},\n\t}\n\n\tfor i := 0; i < 3; i++ {\n\t\ttime.Sleep(100 * time.Millisecond)\n\t\t_, _ = conn.WriteToUDP(data, addr)\n\t}\n}\n\ntype Message struct {\n\tNetCommon NetCommon `json:\"NetWork.NetCommon\"`\n\tRet       int       `json:\"Ret\"`\n\tSessionID string    `json:\"SessionID\"`\n}\n\ntype NetCommon struct {\n\tBuildDate       string `json:\"BuildDate\"`\n\tChannelNum      int    `json:\"ChannelNum\"`\n\tDeviceType      int    `json:\"DeviceType\"`\n\tGateWay         string `json:\"GateWay\"`\n\tHostIP          string `json:\"HostIP\"`\n\tHostName        string `json:\"HostName\"`\n\tHttpPort        int    `json:\"HttpPort\"`\n\tMAC             string `json:\"MAC\"`\n\tMonMode         string `json:\"MonMode\"`\n\tNetConnectState int    `json:\"NetConnectState\"`\n\tOtherFunction   string `json:\"OtherFunction\"`\n\tSN              string `json:\"SN\"`\n\tSSLPort         int    `json:\"SSLPort\"`\n\tSubmask         string `json:\"Submask\"`\n\tTCPMaxConn      int    `json:\"TCPMaxConn\"`\n\tTCPPort         int    `json:\"TCPPort\"`\n\tUDPPort         int    `json:\"UDPPort\"`\n\tUseHSDownLoad   bool   `json:\"UseHSDownLoad\"`\n\tVersion         string `json:\"Version\"`\n}\n\nfunc getResponses(conn *net.UDPConn) (infos []*NetCommon) {\n\tif err := conn.SetReadDeadline(time.Now().Add(time.Second * 2)); err != nil {\n\t\treturn\n\t}\n\n\tvar ips []net.IP // processed IPs\n\n\tb := make([]byte, 4096)\nloop:\n\tfor {\n\t\tn, addr, err := conn.ReadFromUDP(b)\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\n\t\tfor _, ip := range ips {\n\t\t\tif ip.Equal(addr.IP) {\n\t\t\t\tcontinue loop\n\t\t\t}\n\t\t}\n\n\t\tif n <= 20+1 {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar msg Message\n\n\t\tif err = json.Unmarshal(b[20:n-1], &msg); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tinfos = append(infos, &msg.NetCommon)\n\t\tips = append(ips, addr.IP)\n\t}\n\n\treturn\n}\n\nfunc hexToDecimalBytes(hexIP string) (string, error) {\n\tb, err := hex.DecodeString(hexIP[2:]) // remove the '0x' prefix\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn fmt.Sprintf(\"%d.%d.%d.%d\", b[3], b[2], b[1], b[0]), nil\n}\n"
  },
  {
    "path": "internal/echo/README.md",
    "content": "# Echo\n\nSome sources may have a dynamic link. And you will need to get it using a Bash or Python script. Your script should echo a link to the source. RTSP, FFmpeg or any of the supported sources.\n\n**Docker** and **Home Assistant add-on** users have preinstalled `python3`, `curl`, `jq`.\n\n## Configuration\n\n```yaml\nstreams:\n  apple_hls: echo:python3 hls.py https://developer.apple.com/streaming/examples/basic-stream-osx-ios5.html\n```\n\n## Install python libraries\n\n**Docker** and **Hass Add-on** users have preinstalled `python3` without any additional libraries, like [requests](https://requests.readthedocs.io/) or others. If you need some additional libraries - you need to install them to folder with your script:\n\n1. Install [SSH & Web Terminal](https://github.com/hassio-addons/addon-ssh)\n2. Goto Add-on Web UI\n3. Install library: `pip install requests -t /config/echo`\n4. Add your script to `/config/echo/myscript.py`\n5. Use your script as source `echo:python3 /config/echo/myscript.py`\n\n## Example: Apple HLS\n\n```yaml\nstreams:\n  apple_hls: echo:python3 hls.py https://developer.apple.com/streaming/examples/basic-stream-osx-ios5.html\n```\n\n**hls.py**\n\n```python\nimport re\nimport sys\nfrom urllib.parse import urljoin\nfrom urllib.request import urlopen\n\nhtml = urlopen(sys.argv[1]).read().decode(\"utf-8\")\nurl = re.search(r\"https.+?m3u8\", html)[0]\n\nhtml = urlopen(url).read().decode(\"utf-8\")\nm = re.search(r\"^[a-z0-1/_]+\\.m3u8$\", html, flags=re.MULTILINE)\nurl = urljoin(url, m[0])\n\n# ffmpeg:https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/gear1/prog_index.m3u8#video=copy\nprint(\"ffmpeg:\" + url + \"#video=copy\")\n```\n"
  },
  {
    "path": "internal/echo/echo.go",
    "content": "package echo\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"os/exec\"\n\t\"slices\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/app\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/shell\"\n)\n\nfunc Init() {\n\tvar cfg struct {\n\t\tMod struct {\n\t\t\tAllowPaths []string `yaml:\"allow_paths\"`\n\t\t} `yaml:\"echo\"`\n\t}\n\n\tapp.LoadConfig(&cfg)\n\n\tallowPaths := cfg.Mod.AllowPaths\n\n\tlog := app.GetLogger(\"echo\")\n\n\tstreams.RedirectFunc(\"echo\", func(url string) (string, error) {\n\t\targs := shell.QuoteSplit(url[5:])\n\n\t\tif allowPaths != nil && !slices.Contains(allowPaths, args[0]) {\n\t\t\treturn \"\", errors.New(\"echo: bin not in allow_paths: \" + args[0])\n\t\t}\n\n\t\tb, err := exec.Command(args[0], args[1:]...).Output()\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tb = bytes.TrimSpace(b)\n\n\t\tlog.Debug().Str(\"url\", url).Msgf(\"[echo] %s\", b)\n\n\t\treturn string(b), nil\n\t})\n\tstreams.MarkInsecure(\"echo\")\n}\n"
  },
  {
    "path": "internal/eseecloud/README.md",
    "content": "# EseeCloud\n\n[`new in v1.9.10`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.9.10)\n\nThis source is for cameras with a link like this `http://admin:@192.168.1.123:80/livestream/12`. Related [issue](https://github.com/AlexxIT/go2rtc/issues/1690).\n\n## Configuration\n\n```yaml\nstreams:\n  camera1: eseecloud://user:pass@192.168.1.123:80/livestream/12\n```\n"
  },
  {
    "path": "internal/eseecloud/eseecloud.go",
    "content": "package eseecloud\n\nimport (\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/eseecloud\"\n)\n\nfunc Init() {\n\tstreams.HandleFunc(\"eseecloud\", eseecloud.Dial)\n}\n"
  },
  {
    "path": "internal/exec/README.md",
    "content": "# Exec\n\nExec source can run any external application and expect data from it. Two transports are supported - **pipe** ([`new in v1.5.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.5.0)) and **RTSP**.\n\nIf you want to use **RTSP** transport, the command must contain the `{output}` argument in any place. On launch, it will be replaced by the local address of the RTSP server.\n\n**pipe** reads data from app stdout in different formats: **MJPEG**, **H.264/H.265 bitstream**, **MPEG-TS**. Also pipe can write data to app stdin in two formats: **PCMA** and **PCM/48000**.\n\nThe source can be used with:\n\n- [FFmpeg](https://ffmpeg.org/) - go2rtc ffmpeg source is just a shortcut to exec source\n- [FFplay](https://ffmpeg.org/ffplay.html) - play audio on your server\n- [GStreamer](https://gstreamer.freedesktop.org/)\n- [Raspberry Pi Cameras](https://www.raspberrypi.com/documentation/computers/camera_software.html)\n- any of your own software\n\n## Configuration\n\nPipe commands support parameters (format: `exec:{command}#{param1}#{param2}`):\n\n- `killsignal` - signal which will be sent to stop the process (numeric form)\n- `killtimeout` - time in seconds for forced termination with sigkill\n- `backchannel` - enable backchannel for two-way audio\n- `starttimeout` - time in seconds for waiting first byte from RTSP\n\n```yaml\nstreams:\n  stream: exec:ffmpeg -re -i /media/BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp {output}\n  picam_h264: exec:libcamera-vid -t 0 --inline -o -\n  picam_mjpeg: exec:libcamera-vid -t 0 --codec mjpeg -o -\n  pi5cam_h264: exec:libcamera-vid -t 0 --libav-format h264 -o -\n  canon: exec:gphoto2 --capture-movie --stdout#killsignal=2#killtimeout=5\n  play_pcma: exec:ffplay -fflags nobuffer -f alaw -ar 8000 -i -#backchannel=1\n  play_pcm48k: exec:ffplay -fflags nobuffer -f s16be -ar 48000 -i -#backchannel=1\n```\n\n## Backchannel\n\n- You can check audio card names in the **Go2rtc > WebUI > Add**\n- You can specify multiple backchannel lines with different codecs\n\n```yaml\nsources:\n  two_way_audio_win:\n    - exec:ffmpeg -hide_banner -f dshow -i \"audio=Microphone (High Definition Audio Device)\" -c pcm_s16le -ar 16000 -ac 1 -f wav -\n    - exec:ffplay -nodisp -probesize 32 -f s16le -ar 16000 -#backchannel=1#audio=s16le/16000\n    - exec:ffplay -nodisp -probesize 32 -f alaw -ar 8000 -#backchannel=1#audio=alaw/8000\n```\n"
  },
  {
    "path": "internal/exec/exec.go",
    "content": "package exec\n\nimport (\n\t\"bufio\"\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/url\"\n\t\"os\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/app\"\n\t\"github.com/AlexxIT/go2rtc/internal/rtsp\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/magic\"\n\t\"github.com/AlexxIT/go2rtc/pkg/pcm\"\n\tpkg \"github.com/AlexxIT/go2rtc/pkg/rtsp\"\n\t\"github.com/AlexxIT/go2rtc/pkg/shell\"\n\t\"github.com/rs/zerolog\"\n)\n\nfunc Init() {\n\tvar cfg struct {\n\t\tMod struct {\n\t\t\tAllowPaths []string `yaml:\"allow_paths\"`\n\t\t} `yaml:\"exec\"`\n\t}\n\n\tapp.LoadConfig(&cfg)\n\n\tallowPaths = cfg.Mod.AllowPaths\n\n\trtsp.HandleFunc(func(conn *pkg.Conn) bool {\n\t\twaitersMu.Lock()\n\t\twaiter := waiters[conn.URL.Path]\n\t\twaitersMu.Unlock()\n\n\t\tif waiter == nil {\n\t\t\treturn false\n\t\t}\n\n\t\t// unblocking write to channel\n\t\tselect {\n\t\tcase waiter <- conn:\n\t\t\treturn true\n\t\tdefault:\n\t\t\treturn false\n\t\t}\n\t})\n\n\tstreams.HandleFunc(\"exec\", execHandle)\n\tstreams.MarkInsecure(\"exec\")\n\n\tlog = app.GetLogger(\"exec\")\n}\n\nvar allowPaths []string\n\nfunc execHandle(rawURL string) (prod core.Producer, err error) {\n\trawURL, rawQuery, _ := strings.Cut(rawURL, \"#\")\n\tquery := streams.ParseQuery(rawQuery)\n\n\tvar path string\n\n\t// RTSP flow should have `{output}` inside URL\n\t// pipe flow may have `#{params}` inside URL\n\tif i := strings.Index(rawURL, \"{output}\"); i > 0 {\n\t\tif rtsp.Port == \"\" {\n\t\t\treturn nil, errors.New(\"exec: rtsp module disabled\")\n\t\t}\n\n\t\tsum := md5.Sum([]byte(rawURL))\n\t\tpath = \"/\" + hex.EncodeToString(sum[:])\n\t\trawURL = rawURL[:i] + \"rtsp://127.0.0.1:\" + rtsp.Port + path + rawURL[i+8:]\n\t}\n\n\tcmd := shell.NewCommand(rawURL[5:]) // remove `exec:`\n\tcmd.Stderr = &logWriter{\n\t\tbuf:   make([]byte, 512),\n\t\tdebug: log.Debug().Enabled(),\n\t}\n\n\tif allowPaths != nil && !slices.Contains(allowPaths, cmd.Args[0]) {\n\t\t_ = cmd.Close()\n\t\treturn nil, errors.New(\"exec: bin not in allow_paths: \" + cmd.Args[0])\n\t}\n\n\tif s := query.Get(\"killsignal\"); s != \"\" {\n\t\tsig := syscall.Signal(core.Atoi(s))\n\t\tcmd.Cancel = func() error {\n\t\t\tlog.Debug().Msgf(\"[exec] kill with signal=%d\", sig)\n\t\t\treturn cmd.Process.Signal(sig)\n\t\t}\n\t}\n\n\tif s := query.Get(\"killtimeout\"); s != \"\" {\n\t\tcmd.WaitDelay = time.Duration(core.Atoi(s)) * time.Second\n\t}\n\n\tif query.Get(\"backchannel\") == \"1\" {\n\t\treturn pcm.NewBackchannel(cmd, query.Get(\"audio\"))\n\t}\n\n\tvar timeout time.Duration\n\tif s := query.Get(\"starttimeout\"); s != \"\" {\n\t\ttimeout = time.Duration(core.Atoi(s)) * time.Second\n\t} else {\n\t\ttimeout = 30 * time.Second\n\t}\n\n\tif path == \"\" {\n\t\tprod, err = handlePipe(rawURL, cmd)\n\t} else {\n\t\tprod, err = handleRTSP(rawURL, cmd, path, timeout)\n\t}\n\n\tif err != nil {\n\t\t_ = cmd.Close()\n\t}\n\n\treturn\n}\n\nfunc handlePipe(source string, cmd *shell.Command) (core.Producer, error) {\n\tstdout, err := cmd.StdoutPipe()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trd := struct {\n\t\tio.Reader\n\t\tio.Closer\n\t}{\n\t\t// add buffer for pipe reader to reduce syscall\n\t\tbufio.NewReaderSize(stdout, core.BufferSize),\n\t\t// stop cmd on close pipe call\n\t\tcmd,\n\t}\n\n\tlog.Debug().Strs(\"args\", cmd.Args).Msg(\"[exec] run pipe\")\n\n\tts := time.Now()\n\n\tif err = cmd.Start(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tprod, err := magic.Open(rd)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"exec/pipe: %w\\n%s\", err, cmd.Stderr)\n\t}\n\n\tif info, ok := prod.(core.Info); ok {\n\t\tinfo.SetProtocol(\"pipe\")\n\t\tsetRemoteInfo(info, source, cmd.Args)\n\t}\n\n\tlog.Debug().Stringer(\"launch\", time.Since(ts)).Msg(\"[exec] run pipe\")\n\n\treturn prod, nil\n}\n\nfunc handleRTSP(source string, cmd *shell.Command, path string, timeout time.Duration) (core.Producer, error) {\n\tif log.Trace().Enabled() {\n\t\tcmd.Stdout = os.Stdout\n\t}\n\n\twaiter := make(chan *pkg.Conn, 1)\n\n\twaitersMu.Lock()\n\twaiters[path] = waiter\n\twaitersMu.Unlock()\n\n\tdefer func() {\n\t\twaitersMu.Lock()\n\t\tdelete(waiters, path)\n\t\twaitersMu.Unlock()\n\t}()\n\n\tlog.Debug().Strs(\"args\", cmd.Args).Msg(\"[exec] run rtsp\")\n\n\tts := time.Now()\n\n\tif err := cmd.Start(); err != nil {\n\t\tlog.Error().Err(err).Str(\"source\", source).Msg(\"[exec]\")\n\t\treturn nil, err\n\t}\n\n\ttimer := time.NewTimer(timeout)\n\tdefer timer.Stop()\n\n\tselect {\n\tcase <-timer.C:\n\t\t// haven't received data from app in timeout\n\t\tlog.Error().Str(\"source\", source).Msg(\"[exec] timeout\")\n\t\treturn nil, errors.New(\"exec: timeout\")\n\tcase <-cmd.Done():\n\t\t// app fail before we receive any data\n\t\treturn nil, fmt.Errorf(\"exec/rtsp\\n%s\", cmd.Stderr)\n\tcase prod := <-waiter:\n\t\t// app started successfully\n\t\tlog.Debug().Stringer(\"launch\", time.Since(ts)).Msg(\"[exec] run rtsp\")\n\t\tsetRemoteInfo(prod, source, cmd.Args)\n\t\tprod.OnClose = cmd.Close\n\t\treturn prod, nil\n\t}\n}\n\n// internal\n\nvar (\n\tlog       zerolog.Logger\n\twaiters   = make(map[string]chan *pkg.Conn)\n\twaitersMu sync.Mutex\n)\n\ntype logWriter struct {\n\tbuf   []byte\n\tdebug bool\n\tn     int\n}\n\nfunc (l *logWriter) String() string {\n\tif l.n == len(l.buf) {\n\t\treturn string(l.buf) + \"...\"\n\t}\n\treturn string(l.buf[:l.n])\n}\n\nfunc (l *logWriter) Write(p []byte) (n int, err error) {\n\tif l.n < cap(l.buf) {\n\t\tl.n += copy(l.buf[l.n:], p)\n\t}\n\tn = len(p)\n\tif l.debug {\n\t\tif p = trimSpace(p); p != nil {\n\t\t\tlog.Debug().Msgf(\"[exec] %s\", p)\n\t\t}\n\t}\n\treturn\n}\n\nfunc trimSpace(b []byte) []byte {\n\tstart := 0\n\tstop := len(b)\n\tfor ; start < stop; start++ {\n\t\tif b[start] >= ' ' {\n\t\t\tbreak // trim all ASCII before 0x20\n\t\t}\n\t}\n\tfor ; ; stop-- {\n\t\tif stop == start {\n\t\t\treturn nil // skip empty output\n\t\t}\n\t\tif b[stop-1] > ' ' {\n\t\t\tbreak // trim all ASCII before 0x21\n\t\t}\n\t}\n\treturn b[start:stop]\n}\n\nfunc setRemoteInfo(info core.Info, source string, args []string) {\n\tinfo.SetSource(source)\n\n\tif i := core.Index(args, \"-i\"); i > 0 && i < len(args)-1 {\n\t\trawURL := args[i+1]\n\t\tif u, err := url.Parse(rawURL); err == nil && u.Host != \"\" {\n\t\t\tinfo.SetRemoteAddr(u.Host)\n\t\t\tinfo.SetURL(rawURL)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/expr/README.md",
    "content": "# Expr\n\n[`new in v1.8.2`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.8.2)\n\n[Expr](https://github.com/antonmedv/expr) - expression language and expression evaluation for Go.\n\n- [language definition](https://expr.medv.io/docs/Language-Definition) - takes best from JS, Python, Jinja2 syntax\n- your expression should return a link of any supported source\n- expression supports multiple operation, but:\n  - all operations must be separated by a semicolon\n  - all operations, except the last one, must declare a new variable (`let s = \"abc\";`)\n  - the last operation should return a string\n- go2rtc supports additional functions:\n  - `fetch` - JS-like HTTP requests\n  - `match` - JS-like RegExp queries\n\n## Fetch examples\n\nMultiple fetch requests are executed within a single session. They share the same cookie.\n\n**HTTP GET**\n\n```js\nvar r = fetch('https://example.org/products.json');\n```\n\n**HTTP POST JSON**\n\n```js\nvar r = fetch('https://example.org/post', {\n    method: 'POST',\n    // Content-Type: application/json will be set automatically\n    json: {username: 'example'}\n});\n```\n\n**HTTP POST Form**\n\n```js\nvar r = fetch('https://example.org/post', {\n    method: 'POST',\n    // Content-Type: application/x-www-form-urlencoded will be set automatically\n    data: {username: 'example', password: 'password'}\n});\n```\n\n## Script examples\n\n**Two way audio for Dahua VTO**\n\n```yaml\nstreams:\n  dahua_vto: |\n    expr:\n    let host = 'admin:password@192.168.1.123';\n\n    var r = fetch('http://' + host + '/cgi-bin/configManager.cgi?action=setConfig&Encode[0].MainFormat[0].Audio.Compression=G.711A&Encode[0].MainFormat[0].Audio.Frequency=8000');\n\n    'rtsp://' + host + '/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif'\n```\n\n**dom.ru**\n\nYou can get credentials from https://github.com/ad/domru\n\n```yaml\nstreams:\n  dom_ru: |\n    expr:\n    let camera   = '***';\n    let token    = '***';\n    let operator = '***';\n\n    fetch('https://myhome.proptech.ru/rest/v1/forpost/cameras/' + camera + '/video', {\n      headers: {\n        'Authorization': 'Bearer ' + token,\n        'User-Agent': 'Google sdkgphone64x8664 | Android 14 | erth | 8.26.0 (82600010) | 0 | 0 | 0',\n        'Operator': operator\n      }\n    }).json().data.URL\n```\n\n**dom.ufanet.ru**\n\n```yaml\nstreams:\n  ufanet_ru: |\n    expr:\n    let username = '***';\n    let password = '***';\n    let cameraid = '***';\n\n    let r1 = fetch('https://ucams.ufanet.ru/api/internal/login/', {\n      method: 'POST',\n      data: {username: username, password: password}\n    });\n    let r2 = fetch('https://ucams.ufanet.ru/api/v0/cameras/this/?lang=ru', {\n      method: 'POST',\n      json: {'fields': ['token_l', 'server'], 'token_l_ttl': 3600, 'numbers': [cameraid]},\n    }).json().results[0];\n\n    'rtsp://' + r2.server.domain + '/' + r2.number + '?token=' + r2.token_l\n```\n\n**Parse HLS files from Apple**\n\nSame example in two languages - python and expr.\n\n```yaml\nstreams:\n  example_python: |\n    echo:python -c 'from urllib.request import urlopen; import re\n\n    # url1 = \"https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8\"\n    html1 = urlopen(\"https://developer.apple.com/streaming/examples/basic-stream-osx-ios5.html\").read().decode(\"utf-8\")\n    url1 = re.search(r\"https.+?m3u8\", html1)[0]\n\n    # url2 = \"gear1/prog_index.m3u8\"\n    html2 = urlopen(url1).read().decode(\"utf-8\")\n    url2 = re.search(r\"^[a-z0-1/_]+\\.m3u8$\", html2, flags=re.MULTILINE)[0]\n\n    # url3 = \"https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/gear1/prog_index.m3u8\"\n    url3 = url1[:url1.rindex(\"/\")+1] + url2\n\n    print(\"ffmpeg:\" + url3 + \"#video=copy\")'\n\n  example_expr: |\n    expr:\n\n    let html1 = fetch(\"https://developer.apple.com/streaming/examples/basic-stream-osx-ios5.html\").text;\n    let url1 = match(html1, \"https.+?m3u8\")[0];\n\n    let html2 = fetch(url1).text;\n    let url2 = match(html2, \"^[a-z0-1/_]+\\\\.m3u8$\", \"m\")[0];\n\n    let url3 = url1[:lastIndexOf(url1, \"/\")+1] + url2;\n\n    \"ffmpeg:\" + url3 + \"#video=copy\"\n```\n\n## Comparison\n\n| expr                         | python                     | js                             |\n|------------------------------|----------------------------|--------------------------------|\n| let x = 1;                   | x = 1                      | let x = 1                      |\n| {a: 1, b: 2}                 | {\"a\": 1, \"b\": 2}           | {a: 1, b: 2}                   |\n| let r = fetch(url, {method}) | r = request(method, url)   | r = await fetch(url, {method}) |\n| r.ok                         | r.ok                       | r.ok                           |\n| r.status                     | r.status_code              | r.status                       |\n| r.text                       | r.text                     | await r.text()                 |\n| r.json()                     | r.json()                   | await r.json()                 |\n| r.headers                    | r.headers                  | r.headers                      |\n| let m = match(text, \"abc\")   | m = re.search(\"abc\", text) | let m = text.match(/abc/)      |\n"
  },
  {
    "path": "internal/expr/expr.go",
    "content": "package expr\n\nimport (\n\t\"errors\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/app\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/expr\"\n)\n\nfunc Init() {\n\tlog := app.GetLogger(\"expr\")\n\n\tstreams.RedirectFunc(\"expr\", func(url string) (string, error) {\n\t\tv, err := expr.Eval(url[5:], nil)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tlog.Debug().Msgf(\"[expr] url=%s\", url)\n\n\t\tif url = v.(string); url == \"\" {\n\t\t\treturn \"\", errors.New(\"expr: result is empty\")\n\t\t}\n\n\t\treturn url, nil\n\t})\n\tstreams.MarkInsecure(\"expr\")\n}\n"
  },
  {
    "path": "internal/ffmpeg/README.md",
    "content": "# FFmpeg\n\nYou can get any stream, file or device via FFmpeg and push it to go2rtc. The app will automatically start FFmpeg with the proper arguments when someone starts watching the stream.\n\n- FFmpeg preinstalled for **Docker** and **Home Assistant add-on** users\n- **Home Assistant add-on** users can target files from [/media](https://www.home-assistant.io/more-info/local-media/setup-media/) folder\n\n## Configuration\n\nFormat: `ffmpeg:{input}#{param1}#{param2}#{param3}`. Examples:\n\n```yaml\nstreams:\n  # [FILE] all tracks will be copied without transcoding codecs\n  file1: ffmpeg:/media/BigBuckBunny.mp4\n\n  # [FILE] video will be transcoded to H264, audio will be skipped\n  file2: ffmpeg:/media/BigBuckBunny.mp4#video=h264\n\n  # [FILE] video will be copied, audio will be transcoded to PCMU\n  file3: ffmpeg:/media/BigBuckBunny.mp4#video=copy#audio=pcmu\n\n  # [HLS] video will be copied, audio will be skipped\n  hls: ffmpeg:https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/gear5/prog_index.m3u8#video=copy\n\n  # [MJPEG] video will be transcoded to H264\n  mjpeg: ffmpeg:http://185.97.122.128/cgi-bin/faststream.jpg#video=h264\n\n  # [RTSP] video with rotation, should be transcoded, so select H264\n  rotate: ffmpeg:rtsp://12345678@192.168.1.123/av_stream/ch0#video=h264#rotate=90\n```\n\nAll transcoding formats have [built-in templates](ffmpeg.go): `h264`, `h265`, `opus`, `pcmu`, `pcmu/16000`, `pcmu/48000`, `pcma`, `pcma/16000`, `pcma/48000`, `aac`, `aac/16000`.\n\nBut you can override them via YAML config. You can also add your own formats to the config and use them with source params.\n\n```yaml\nffmpeg:\n  bin: ffmpeg  # path to ffmpeg binary\n  global: \"-hide_banner\"\n  timeout: 5  # default timeout in seconds for rtsp inputs\n  h264: \"-codec:v libx264 -g:v 30 -preset:v superfast -tune:v zerolatency -profile:v main -level:v 4.1\"\n  mycodec: \"-any args that supported by ffmpeg...\"\n  myinput: \"-fflags nobuffer -flags low_delay -timeout {timeout} -i {input}\"\n  myraw: \"-ss 00:00:20\"\n```\n\n- You can use go2rtc stream name as ffmpeg input (ex. `ffmpeg:camera1#video=h264`)\n- You can use `video` and `audio` params multiple times (ex. `#video=copy#audio=copy#audio=pcmu`)\n- You can use `rotate` param with `90`, `180`, `270` or `-90` values, important with transcoding (ex. `#video=h264#rotate=90`)\n- You can use `width` and/or `height` params, important with transcoding (ex. `#video=h264#width=1280`)\n- You can use `drawtext` to add a timestamp (ex. `drawtext=x=2:y=2:fontsize=12:fontcolor=white:box=1:boxcolor=black`)\n    - This will greatly increase the CPU of the server, even with hardware acceleration\n- You can use `timeout` param to set RTSP input timeout in seconds (ex. `#timeout=10`)\n- You can use `raw` param for any additional FFmpeg arguments (ex. `#raw=-vf transpose=1`)\n- You can use `input` param to override default input template (ex. `#input=rtsp/udp` will change RTSP transport from TCP to UDP+TCP)\n    - You can use raw input value (ex. `#input=-timeout {timeout} -i {input}`)\n    - You can add your own input templates\n\nRead more about [hardware acceleration](hardware/README.md).\n\n**PS.** It is recommended to check the available hardware in the WebUI add page.\n"
  },
  {
    "path": "internal/ffmpeg/api.go",
    "content": "package ffmpeg\n\nimport (\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n)\n\nfunc apiFFmpeg(w http.ResponseWriter, r *http.Request) {\n\tif r.Method != \"POST\" {\n\t\thttp.Error(w, \"\", http.StatusMethodNotAllowed)\n\t\treturn\n\t}\n\n\tquery := r.URL.Query()\n\tdst := query.Get(\"dst\")\n\tstream := streams.Get(dst)\n\tif stream == nil {\n\t\thttp.Error(w, \"\", http.StatusNotFound)\n\t\treturn\n\t}\n\n\tvar src string\n\tif s := query.Get(\"file\"); s != \"\" {\n\t\tif streams.Validate(s) == nil {\n\t\t\tsrc = \"ffmpeg:\" + s + \"#audio=auto#input=file\"\n\t\t}\n\t} else if s = query.Get(\"live\"); s != \"\" {\n\t\tif streams.Validate(s) == nil {\n\t\t\tsrc = \"ffmpeg:\" + s + \"#audio=auto\"\n\t\t}\n\t} else if s = query.Get(\"text\"); s != \"\" {\n\t\tif strings.IndexAny(s, `'\"&%$`) < 0 {\n\t\t\tsrc = \"ffmpeg:tts?text=\" + s\n\t\t\tif s = query.Get(\"voice\"); s != \"\" {\n\t\t\t\tsrc += \"&voice=\" + s\n\t\t\t}\n\t\t\tsrc += \"#audio=auto\"\n\t\t}\n\t}\n\n\tif src == \"\" {\n\t\thttp.Error(w, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tif err := stream.Play(src); err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t}\n}\n"
  },
  {
    "path": "internal/ffmpeg/device/README.md",
    "content": "# FFmpeg Device\n\nYou can get video from any USB camera or Webcam as RTSP or WebRTC stream. This is part of FFmpeg integration.\n\n- check available devices in web interface\n- `video_size` and `framerate` must be supported by your camera!\n- for Linux supported only video for now\n- for macOS you can stream FaceTime camera or whole desktop!\n- for macOS important to set right framerate\n\n## Configuration\n\nFormat: `ffmpeg:device?{input-params}#{param1}#{param2}#{param3}`\n\n```yaml\nstreams:\n  linux_usbcam:   ffmpeg:device?video=0&video_size=1280x720#video=h264\n  windows_webcam: ffmpeg:device?video=0#video=h264\n  macos_facetime: ffmpeg:device?video=0&audio=1&video_size=1280x720&framerate=30#video=h264#audio=pcma\n```\n\n**PS.** It is recommended to check the available devices in the WebUI add page.\n"
  },
  {
    "path": "internal/ffmpeg/device/device_bsd.go",
    "content": "//go:build freebsd || netbsd || openbsd || dragonfly\n\npackage device\n\nimport (\n\t\"net/url\"\n\t\"os\"\n\t\"os/exec\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\nfunc queryToInput(query url.Values) string {\n\tif video := query.Get(\"video\"); video != \"\" {\n\t\t// https://ffmpeg.org/ffmpeg-devices.html#video4linux2_002c-v4l2\n\t\tinput := \"-f v4l2\"\n\n\t\tfor key, value := range query {\n\t\t\tswitch key {\n\t\t\tcase \"resolution\":\n\t\t\t\tinput += \" -video_size \" + value[0]\n\t\t\tcase \"video_size\", \"pixel_format\", \"input_format\", \"framerate\", \"use_libv4l2\":\n\t\t\t\tinput += \" -\" + key + \" \" + value[0]\n\t\t\t}\n\t\t}\n\n\t\treturn input + \" -i \" + indexToItem(videos, video)\n\t}\n\n\tif audio := query.Get(\"audio\"); audio != \"\" {\n\t\tinput := \"-f oss\"\n\n\t\tfor key, value := range query {\n\t\t\tswitch key {\n\t\t\tcase \"channels\", \"sample_rate\":\n\t\t\t\tinput += \" -\" + key + \" \" + value[0]\n\t\t\t}\n\t\t}\n\n\t\treturn input + \" -i \" + indexToItem(audios, audio)\n\t}\n\n\treturn \"\"\n}\n\nfunc initDevices() {\n\tfiles, err := os.ReadDir(\"/dev\")\n\tif err != nil {\n\t\treturn\n\t}\n\n\tfor _, file := range files {\n\t\tif !strings.HasPrefix(file.Name(), core.KindVideo) {\n\t\t\tcontinue\n\t\t}\n\n\t\tname := \"/dev/\" + file.Name()\n\n\t\tcmd := exec.Command(\n\t\t\tBin, \"-hide_banner\", \"-f\", \"v4l2\", \"-list_formats\", \"all\", \"-i\", name,\n\t\t)\n\t\tb, _ := cmd.CombinedOutput()\n\n\t\t// [video4linux2,v4l2 @ 0x860b92280] Raw       :     yuyv422 :           YUYV 4:2:2 : 640x480 160x120 176x144 320x176 320x240 352x288 432x240 544x288 640x360 752x416 800x448 800x600 864x480 960x544 960x720 1024x576 1184x656 1280x720 1280x960\n\t\t// [video4linux2,v4l2 @ 0x860b92280] Compressed:       mjpeg :          Motion-JPEG : 640x480 160x120 176x144 320x176 320x240 352x288 432x240 544x288 640x360 752x416 800x448 800x600 864x480 960x544 960x720 1024x576 1184x656 1280x720 1280x960\n\t\tre := regexp.MustCompile(\"(Raw *|Compressed): +(.+?) : +(.+?) : (.+)\")\n\t\tm := re.FindAllStringSubmatch(string(b), -1)\n\t\tfor _, i := range m {\n\t\t\tsize, _, _ := strings.Cut(i[4], \" \")\n\t\t\tstream := &api.Source{\n\t\t\t\tName: i[3],\n\t\t\t\tInfo: i[4],\n\t\t\t\tURL:  \"ffmpeg:device?video=\" + name + \"&input_format=\" + i[2] + \"&video_size=\" + size,\n\t\t\t}\n\n\t\t\tif i[1] != \"Compressed\" {\n\t\t\t\tstream.URL += \"#video=h264#hardware\"\n\t\t\t}\n\n\t\t\tvideos = append(videos, name)\n\t\t\tstreams = append(streams, stream)\n\t\t}\n\t}\n\n\terr = exec.Command(Bin, \"-f\", \"oss\", \"-i\", \"/dev/dsp\", \"-t\", \"1\", \"-f\", \"null\", \"-\").Run()\n\tif err == nil {\n\t\tstream := &api.Source{\n\t\t\tName: \"OSS default\",\n\t\t\tInfo: \" \",\n\t\t\tURL:  \"ffmpeg:device?audio=default&channels=1&sample_rate=16000&#audio=opus\",\n\t\t}\n\n\t\taudios = append(audios, \"default\")\n\t\tstreams = append(streams, stream)\n\t}\n}\n"
  },
  {
    "path": "internal/ffmpeg/device/device_darwin.go",
    "content": "//go:build darwin || ios\n\npackage device\n\nimport (\n\t\"net/url\"\n\t\"os/exec\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\nfunc queryToInput(query url.Values) string {\n\tvideo := query.Get(\"video\")\n\taudio := query.Get(\"audio\")\n\n\tif video == \"\" && audio == \"\" {\n\t\treturn \"\"\n\t}\n\n\t// https://ffmpeg.org/ffmpeg-devices.html#avfoundation\n\tinput := \"-f avfoundation\"\n\n\tif video != \"\" {\n\t\tvideo = indexToItem(videos, video)\n\n\t\tfor key, value := range query {\n\t\t\tswitch key {\n\t\t\tcase \"resolution\":\n\t\t\t\tinput += \" -video_size \" + value[0]\n\t\t\tcase \"pixel_format\", \"framerate\", \"video_size\", \"capture_cursor\", \"capture_mouse_clicks\", \"capture_raw_data\":\n\t\t\t\tinput += \" -\" + key + \" \" + value[0]\n\t\t\t}\n\t\t}\n\t}\n\n\tif audio != \"\" {\n\t\taudio = indexToItem(audios, audio)\n\t}\n\n\treturn input + ` -i \"` + video + `:` + audio + `\"`\n}\n\nfunc initDevices() {\n\t// [AVFoundation indev @ 0x147f04510] AVFoundation video devices:\n\t// [AVFoundation indev @ 0x147f04510] [0] FaceTime HD Camera\n\t// [AVFoundation indev @ 0x147f04510] [1] Capture screen 0\n\t// [AVFoundation indev @ 0x147f04510] AVFoundation audio devices:\n\t// [AVFoundation indev @ 0x147f04510] [0] MacBook Pro Microphone\n\tcmd := exec.Command(\n\t\tBin, \"-hide_banner\", \"-list_devices\", \"true\", \"-f\", \"avfoundation\", \"-i\", \"\",\n\t)\n\tb, _ := cmd.CombinedOutput()\n\n\tre := regexp.MustCompile(`\\[\\d+] (.+)`)\n\n\tvar kind string\n\tfor _, line := range strings.Split(string(b), \"\\n\") {\n\t\tswitch {\n\t\tcase strings.HasSuffix(line, \"video devices:\"):\n\t\t\tkind = core.KindVideo\n\t\t\tcontinue\n\t\tcase strings.HasSuffix(line, \"audio devices:\"):\n\t\t\tkind = core.KindAudio\n\t\t\tcontinue\n\t\t}\n\n\t\tm := re.FindStringSubmatch(line)\n\t\tif m == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tname := m[1]\n\n\t\tswitch kind {\n\t\tcase core.KindVideo:\n\t\t\tvideos = append(videos, name)\n\t\tcase core.KindAudio:\n\t\t\taudios = append(audios, name)\n\t\t}\n\n\t\tstreams = append(streams, &api.Source{\n\t\t\tName: name, URL: \"ffmpeg:device?\" + kind + \"=\" + name,\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/ffmpeg/device/device_unix.go",
    "content": "//go:build unix && !darwin && !freebsd && !netbsd && !openbsd && !dragonfly\n\npackage device\n\nimport (\n\t\"net/url\"\n\t\"os\"\n\t\"os/exec\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\nfunc queryToInput(query url.Values) string {\n\tif video := query.Get(\"video\"); video != \"\" {\n\t\t// https://ffmpeg.org/ffmpeg-devices.html#video4linux2_002c-v4l2\n\t\tinput := \"-f v4l2\"\n\n\t\tfor key, value := range query {\n\t\t\tswitch key {\n\t\t\tcase \"resolution\":\n\t\t\t\tinput += \" -video_size \" + value[0]\n\t\t\tcase \"video_size\", \"pixel_format\", \"input_format\", \"framerate\", \"use_libv4l2\":\n\t\t\t\tinput += \" -\" + key + \" \" + value[0]\n\t\t\t}\n\t\t}\n\n\t\treturn input + \" -i \" + indexToItem(videos, video)\n\t}\n\n\tif audio := query.Get(\"audio\"); audio != \"\" {\n\t\t// https://trac.ffmpeg.org/wiki/Capture/ALSA\n\t\tinput := \"-f alsa\"\n\n\t\tfor key, value := range query {\n\t\t\tswitch key {\n\t\t\tcase \"channels\", \"sample_rate\":\n\t\t\t\tinput += \" -\" + key + \" \" + value[0]\n\t\t\t}\n\t\t}\n\n\t\treturn input + \" -i \" + indexToItem(audios, audio)\n\t}\n\n\treturn \"\"\n}\n\nfunc initDevices() {\n\tfiles, err := os.ReadDir(\"/dev\")\n\tif err != nil {\n\t\treturn\n\t}\n\n\tfor _, file := range files {\n\t\tif !strings.HasPrefix(file.Name(), core.KindVideo) {\n\t\t\tcontinue\n\t\t}\n\n\t\tname := \"/dev/\" + file.Name()\n\n\t\tcmd := exec.Command(\n\t\t\tBin, \"-hide_banner\", \"-f\", \"v4l2\", \"-list_formats\", \"all\", \"-i\", name,\n\t\t)\n\t\tb, _ := cmd.CombinedOutput()\n\n\t\t// [video4linux2,v4l2 @ 0x204e1c0] Compressed:       mjpeg :          Motion-JPEG : 640x360 1280x720 1920x1080\n\t\t// [video4linux2,v4l2 @ 0x204e1c0] Raw       :     yuyv422 :           YUYV 4:2:2 : 640x360 1280x720 1920x1080\n\t\t// [video4linux2,v4l2 @ 0x204e1c0] Compressed:        h264 :                H.264 : 640x360 1280x720 1920x1080\n\t\tre := regexp.MustCompile(\"(Raw *|Compressed): +(.+?) : +(.+?) : (.+)\")\n\t\tm := re.FindAllStringSubmatch(string(b), -1)\n\t\tfor _, i := range m {\n\t\t\tsize, _, _ := strings.Cut(i[4], \" \")\n\t\t\tstream := &api.Source{\n\t\t\t\tName: i[3],\n\t\t\t\tInfo: i[4],\n\t\t\t\tURL:  \"ffmpeg:device?video=\" + name + \"&input_format=\" + i[2] + \"&video_size=\" + size,\n\t\t\t}\n\n\t\t\tif i[1] != \"Compressed\" {\n\t\t\t\tstream.URL += \"#video=h264#hardware\"\n\t\t\t}\n\n\t\t\tvideos = append(videos, name)\n\t\t\tstreams = append(streams, stream)\n\t\t}\n\t}\n\n\terr = exec.Command(Bin, \"-f\", \"alsa\", \"-i\", \"default\", \"-t\", \"1\", \"-f\", \"null\", \"-\").Run()\n\tif err == nil {\n\t\tstream := &api.Source{\n\t\t\tName: \"ALSA default\",\n\t\t\tInfo: \" \",\n\t\t\tURL:  \"ffmpeg:device?audio=default&channels=1&sample_rate=16000&#audio=opus\",\n\t\t}\n\n\t\taudios = append(audios, \"default\")\n\t\tstreams = append(streams, stream)\n\t}\n}\n"
  },
  {
    "path": "internal/ffmpeg/device/device_windows.go",
    "content": "//go:build windows\n\npackage device\n\nimport (\n\t\"net/url\"\n\t\"os/exec\"\n\t\"regexp\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\nfunc queryToInput(query url.Values) string {\n\tvideo := query.Get(\"video\")\n\taudio := query.Get(\"audio\")\n\n\tif video == \"\" && audio == \"\" {\n\t\treturn \"\"\n\t}\n\n\t// https://ffmpeg.org/ffmpeg-devices.html#dshow\n\tinput := \"-f dshow\"\n\n\tif video != \"\" {\n\t\tvideo = indexToItem(videos, video)\n\n\t\tfor key, value := range query {\n\t\t\tswitch key {\n\t\t\tcase \"resolution\":\n\t\t\t\tinput += \" -video_size \" + value[0]\n\t\t\tcase \"video_size\", \"framerate\", \"pixel_format\":\n\t\t\t\tinput += \" -\" + key + \" \" + value[0]\n\t\t\t}\n\t\t}\n\t}\n\n\tif audio != \"\" {\n\t\taudio = indexToItem(audios, audio)\n\n\t\tfor key, value := range query {\n\t\t\tswitch key {\n\t\t\tcase \"sample_rate\", \"sample_size\", \"channels\", \"audio_buffer_size\":\n\t\t\t\tinput += \" -\" + key + \" \" + value[0]\n\t\t\t}\n\t\t}\n\t}\n\n\tif video != \"\" {\n\t\tinput += ` -i \"video=` + video\n\n\t\tif audio != \"\" {\n\t\t\tinput += `:audio=` + audio\n\t\t}\n\n\t\tinput += `\"`\n\t} else {\n\t\tinput += ` -i \"audio=` + audio + `\"`\n\t}\n\n\treturn input\n}\n\nfunc initDevices() {\n\tcmd := exec.Command(\n\t\tBin, \"-hide_banner\", \"-list_devices\", \"true\", \"-f\", \"dshow\", \"-i\", \"\",\n\t)\n\tb, _ := cmd.CombinedOutput()\n\n\tre := regexp.MustCompile(`\"([^\"]+)\" \\((video|audio)\\)`)\n\tfor _, m := range re.FindAllStringSubmatch(string(b), -1) {\n\t\tname := m[1]\n\t\tkind := m[2]\n\n\t\tstream := &api.Source{\n\t\t\tName: name, URL: \"ffmpeg:device?\" + kind + \"=\" + name,\n\t\t}\n\n\t\tswitch kind {\n\t\tcase core.KindVideo:\n\t\t\tvideos = append(videos, name)\n\t\t\tstream.URL += \"#video=h264#hardware\"\n\t\tcase core.KindAudio:\n\t\t\taudios = append(audios, name)\n\t\t\tstream.URL += \"&channels=1&sample_rate=16000&audio_buffer_size=10\"\n\t\t}\n\n\t\tstreams = append(streams, stream)\n\t}\n}\n"
  },
  {
    "path": "internal/ffmpeg/device/devices.go",
    "content": "package device\n\nimport (\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"sync\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n)\n\nfunc Init(bin string) {\n\tBin = bin\n\n\tapi.HandleFunc(\"api/ffmpeg/devices\", apiDevices)\n}\n\nfunc GetInput(src string) string {\n\tquery, err := url.ParseQuery(src)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\trunonce.Do(initDevices)\n\n\treturn queryToInput(query)\n}\n\nvar Bin string\n\nvar videos, audios []string\nvar streams []*api.Source\nvar runonce sync.Once\n\nfunc apiDevices(w http.ResponseWriter, r *http.Request) {\n\trunonce.Do(initDevices)\n\n\tapi.ResponseSources(w, streams)\n}\n\nfunc indexToItem(items []string, index string) string {\n\tif i, err := strconv.Atoi(index); err == nil && i < len(items) {\n\t\treturn items[i]\n\t}\n\treturn index\n}\n"
  },
  {
    "path": "internal/ffmpeg/ffmpeg.go",
    "content": "package ffmpeg\n\nimport (\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/internal/app\"\n\t\"github.com/AlexxIT/go2rtc/internal/ffmpeg/device\"\n\t\"github.com/AlexxIT/go2rtc/internal/ffmpeg/hardware\"\n\t\"github.com/AlexxIT/go2rtc/internal/ffmpeg/virtual\"\n\t\"github.com/AlexxIT/go2rtc/internal/rtsp\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/ffmpeg\"\n\t\"github.com/rs/zerolog\"\n)\n\nfunc Init() {\n\tvar cfg struct {\n\t\tMod map[string]string `yaml:\"ffmpeg\"`\n\t\tLog struct {\n\t\t\tLevel string `yaml:\"ffmpeg\"`\n\t\t} `yaml:\"log\"`\n\t}\n\n\tcfg.Mod = defaults // will be overriden from yaml\n\tcfg.Log.Level = \"error\"\n\n\tapp.LoadConfig(&cfg)\n\n\tlog = app.GetLogger(\"ffmpeg\")\n\n\t// zerolog levels: trace debug         info warn    error fatal panic disabled\n\t// FFmpeg  levels: trace debug verbose info warning error fatal panic quiet\n\tif cfg.Log.Level == \"warn\" {\n\t\tcfg.Log.Level = \"warning\"\n\t}\n\tdefaults[\"global\"] += \" -v \" + cfg.Log.Level\n\n\tstreams.RedirectFunc(\"ffmpeg\", func(url string) (string, error) {\n\t\tif _, err := Version(); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\targs := parseArgs(url[7:])\n\t\tif core.Contains(args.Codecs, \"auto\") {\n\t\t\treturn \"\", nil // force call streams.HandleFunc(\"ffmpeg\")\n\t\t}\n\t\treturn \"exec:\" + args.String(), nil\n\t})\n\n\tstreams.HandleFunc(\"ffmpeg\", NewProducer)\n\n\tapi.HandleFunc(\"api/ffmpeg\", apiFFmpeg)\n\n\tdevice.Init(defaults[\"bin\"])\n\thardware.Init(defaults[\"bin\"])\n}\n\nvar defaults = map[string]string{\n\t\"bin\":     \"ffmpeg\",\n\t\"global\":  \"-hide_banner\",\n\t\"timeout\": \"5\",\n\n\t// inputs\n\t\"file\":     \"-re -i {input}\",\n\t\"http\":     \"-fflags nobuffer -flags low_delay -i {input}\",\n\t\"rtsp\":     \"-fflags nobuffer -flags low_delay -timeout {timeout} -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i {input}\",\n\t\"rtsp/udp\": \"-fflags nobuffer -flags low_delay -timeout {timeout} -user_agent go2rtc/ffmpeg -i {input}\",\n\n\t// output\n\t\"output\":       \"-user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}\",\n\t\"output/mjpeg\": \"-f mjpeg -\",\n\t\"output/raw\":   \"-f yuv4mpegpipe -\",\n\t\"output/aac\":   \"-f adts -\",\n\t\"output/wav\":   \"-f wav -\",\n\n\t// `-preset superfast` - we can't use ultrafast because it doesn't support `-profile main -level 4.1`\n\t// `-tune zerolatency` - for minimal latency\n\t// `-profile high -level 4.1` - most used streaming profile\n\t// `-pix_fmt:v yuv420p` - important for Telegram\n\t\"h264\":  \"-c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p\",\n\t\"h265\":  \"-c:v libx265 -g 50 -profile:v main -x265-params level=5.1:high-tier=0 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p\",\n\t\"mjpeg\": \"-c:v mjpeg\",\n\t//\"mjpeg\": \"-c:v mjpeg -force_duplicated_matrix:v 1 -huffman:v 0 -pix_fmt:v yuvj420p\",\n\n\t\"raw\":         \"-c:v rawvideo\",\n\t\"raw/gray8\":   \"-c:v rawvideo -pix_fmt:v gray8\",\n\t\"raw/yuv420p\": \"-c:v rawvideo -pix_fmt:v yuv420p\",\n\t\"raw/yuv422p\": \"-c:v rawvideo -pix_fmt:v yuv422p\",\n\t\"raw/yuv444p\": \"-c:v rawvideo -pix_fmt:v yuv444p\",\n\n\t// https://ffmpeg.org/ffmpeg-codecs.html#libopus-1\n\t// https://github.com/pion/webrtc/issues/1514\n\t// https://ffmpeg.org/ffmpeg-resampler.html\n\t// `-async 1` or `-min_comp 0` - force resampling for static timestamp inc, important for WebRTC audio quality\n\t\"opus\":       \"-c:a libopus -application:a lowdelay -min_comp 0\",\n\t\"opus/16000\": \"-c:a libopus -application:a lowdelay -min_comp 0 -ar:a 16000 -ac:a 1\",\n\t\"pcmu\":       \"-c:a pcm_mulaw -ar:a 8000 -ac:a 1\",\n\t\"pcmu/8000\":  \"-c:a pcm_mulaw -ar:a 8000 -ac:a 1\",\n\t\"pcmu/16000\": \"-c:a pcm_mulaw -ar:a 16000 -ac:a 1\",\n\t\"pcmu/48000\": \"-c:a pcm_mulaw -ar:a 48000 -ac:a 1\",\n\t\"pcma\":       \"-c:a pcm_alaw -ar:a 8000 -ac:a 1\",\n\t\"pcma/8000\":  \"-c:a pcm_alaw -ar:a 8000 -ac:a 1\",\n\t\"pcma/16000\": \"-c:a pcm_alaw -ar:a 16000 -ac:a 1\",\n\t\"pcma/48000\": \"-c:a pcm_alaw -ar:a 48000 -ac:a 1\",\n\t\"aac\":        \"-c:a aac\", // keep sample rate and channels\n\t\"aac/16000\":  \"-c:a aac -ar:a 16000 -ac:a 1\",\n\t\"mp3\":        \"-c:a libmp3lame -q:a 8\",\n\t\"pcm\":        \"-c:a pcm_s16be -ar:a 8000 -ac:a 1\",\n\t\"pcm/8000\":   \"-c:a pcm_s16be -ar:a 8000 -ac:a 1\",\n\t\"pcm/16000\":  \"-c:a pcm_s16be -ar:a 16000 -ac:a 1\",\n\t\"pcm/48000\":  \"-c:a pcm_s16be -ar:a 48000 -ac:a 1\",\n\t\"pcml\":       \"-c:a pcm_s16le -ar:a 8000 -ac:a 1\",\n\t\"pcml/8000\":  \"-c:a pcm_s16le -ar:a 8000 -ac:a 1\",\n\t\"pcml/16000\": \"-c:a pcm_s16le -ar:a 16000 -ac:a 1\",\n\t\"pcml/44100\": \"-c:a pcm_s16le -ar:a 44100 -ac:a 1\",\n\n\t// hardware Intel and AMD on Linux\n\t// better not to set `-async_depth:v 1` like for QSV, because framedrops\n\t// `-bf 0` - disable B-frames is very important\n\t\"h264/vaapi\":  \"-c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0\",\n\t\"h265/vaapi\":  \"-c:v hevc_vaapi -g 50 -bf 0 -profile:v main -level:v 5.1 -sei:v 0\",\n\t\"mjpeg/vaapi\": \"-c:v mjpeg_vaapi\",\n\n\t// hardware Raspberry\n\t\"h264/v4l2m2m\": \"-c:v h264_v4l2m2m -g 50 -bf 0\",\n\t\"h265/v4l2m2m\": \"-c:v hevc_v4l2m2m -g 50 -bf 0\",\n\n\t// hardware Rockchip\n\t// important to use custom ffmpeg https://github.com/AlexxIT/go2rtc/issues/768\n\t// hevc - doesn't have a profile setting\n\t\"h264/rkmpp\":  \"-c:v h264_rkmpp -g 50 -bf 0 -profile:v high -level:v 4.1\",\n\t\"h265/rkmpp\":  \"-c:v hevc_rkmpp -g 50 -bf 0 -profile:v main -level:v 5.1\",\n\t\"mjpeg/rkmpp\": \"-c:v mjpeg_rkmpp\",\n\n\t// hardware NVidia on Linux and Windows\n\t// preset=p2 - faster, tune=ll - low latency\n\t\"h264/cuda\": \"-c:v h264_nvenc -g 50 -bf 0 -profile:v high -level:v auto -preset:v p2 -tune:v ll\",\n\t\"h265/cuda\": \"-c:v hevc_nvenc -g 50 -bf 0 -profile:v main -level:v auto\",\n\n\t// hardware Intel on Windows\n\t\"h264/dxva2\":  \"-c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1\",\n\t\"h265/dxva2\":  \"-c:v hevc_qsv -g 50 -bf 0 -profile:v main -level:v 5.1 -async_depth:v 1\",\n\t\"mjpeg/dxva2\": \"-c:v mjpeg_qsv\",\n\n\t// hardware macOS\n\t\"h264/videotoolbox\": \"-c:v h264_videotoolbox -g 50 -bf 0 -profile:v high -level:v 4.1\",\n\t\"h265/videotoolbox\": \"-c:v hevc_videotoolbox -g 50 -bf 0 -profile:v main -level:v 5.1\",\n}\n\nvar log zerolog.Logger\n\n// configTemplate - return template from config (defaults) if exist or return raw template\nfunc configTemplate(template string) string {\n\tif s := defaults[template]; s != \"\" {\n\t\treturn s\n\t}\n\treturn template\n}\n\n// inputTemplate - select input template from YAML config by template name\n// if query has input param - select another template by this name\n// if there is no another template - use input param as template\nfunc inputTemplate(name, s string, query url.Values) string {\n\tvar template string\n\tif input := query.Get(\"input\"); input != \"\" {\n\t\ttemplate = configTemplate(input)\n\t} else {\n\t\ttemplate = defaults[name]\n\t}\n\tif strings.Contains(template, \"{timeout}\") {\n\t\ttimeout := query.Get(\"timeout\")\n\t\tif timeout == \"\" {\n\t\t\ttimeout = defaults[\"timeout\"]\n\t\t}\n\t\ttemplate = strings.Replace(template, \"{timeout}\", timeout+\"000000\", 1)\n\t}\n\treturn strings.Replace(template, \"{input}\", s, 1)\n}\n\nfunc parseArgs(s string) *ffmpeg.Args {\n\t// init FFmpeg arguments\n\targs := &ffmpeg.Args{\n\t\tBin:     defaults[\"bin\"],\n\t\tGlobal:  defaults[\"global\"],\n\t\tOutput:  defaults[\"output\"],\n\t\tVersion: verAV,\n\t}\n\n\tvar source = s\n\tvar query url.Values\n\tif i := strings.IndexByte(s, '#'); i >= 0 {\n\t\tquery = streams.ParseQuery(s[i+1:])\n\t\targs.Video = len(query[\"video\"])\n\t\targs.Audio = len(query[\"audio\"])\n\t\ts = s[:i]\n\t}\n\n\t// Parse input:\n\t//   1. Input as xxxx:// link (http or rtsp or any other)\n\t//   2. Input as stream name\n\t//   3. Input as FFmpeg device (local USB camera)\n\tif i := strings.Index(s, \"://\"); i > 0 {\n\t\tswitch s[:i] {\n\t\tcase \"http\", \"https\", \"rtmp\":\n\t\t\targs.Input = inputTemplate(\"http\", s, query)\n\t\tcase \"rtsp\", \"rtsps\":\n\t\t\t// https://ffmpeg.org/ffmpeg-protocols.html#rtsp\n\t\t\t// skip unnecessary input tracks\n\t\t\tswitch {\n\t\t\tcase (args.Video > 0 && args.Audio > 0) || (args.Video == 0 && args.Audio == 0):\n\t\t\t\targs.Input = \"-allowed_media_types video+audio \"\n\t\t\tcase args.Video > 0:\n\t\t\t\targs.Input = \"-allowed_media_types video \"\n\t\t\tcase args.Audio > 0:\n\t\t\t\targs.Input = \"-allowed_media_types audio \"\n\t\t\t}\n\n\t\t\targs.Input += inputTemplate(\"rtsp\", s, query)\n\t\tdefault:\n\t\t\targs.Input = \"-i \" + s\n\t\t}\n\t} else if streams.Get(s) != nil {\n\t\ts = \"rtsp://127.0.0.1:\" + rtsp.Port + \"/\" + s\n\t\tswitch {\n\t\tcase args.Video > 0 && args.Audio == 0:\n\t\t\ts += \"?video\"\n\t\tcase args.Audio > 0 && args.Video == 0:\n\t\t\ts += \"?audio\"\n\t\tdefault:\n\t\t\ts += \"?video&audio\"\n\t\t}\n\t\ts += \"&source=ffmpeg:\" + url.QueryEscape(source)\n\t\tfor _, v := range query[\"query\"] {\n\t\t\ts += \"&\" + v\n\t\t}\n\t\targs.Input = inputTemplate(\"rtsp\", s, query)\n\t} else if i = strings.Index(s, \"?\"); i > 0 {\n\t\tswitch s[:i] {\n\t\tcase \"device\":\n\t\t\targs.Input = device.GetInput(s[i+1:])\n\t\tcase \"virtual\":\n\t\t\targs.Input = virtual.GetInput(s[i+1:])\n\t\tcase \"tts\":\n\t\t\targs.Input = virtual.GetInputTTS(s[i+1:])\n\t\t}\n\t} else {\n\t\targs.Input = inputTemplate(\"file\", s, query)\n\t}\n\n\tif query[\"async\"] != nil {\n\t\targs.Input = \"-use_wallclock_as_timestamps 1 -async 1 \" + args.Input\n\t}\n\n\t// Parse query params:\n\t//   1. `width`/`height` params\n\t//   2. `rotate` param\n\t//   3. `video` params (support multiple)\n\t//   4. `audio` params (support multiple)\n\t//   5. `hardware` param\n\tif query != nil {\n\t\t// 1. Process raw params for FFmpeg\n\t\tfor _, raw := range query[\"raw\"] {\n\t\t\t// support templates https://github.com/AlexxIT/go2rtc/issues/487\n\t\t\traw = configTemplate(raw)\n\t\t\targs.AddCodec(raw)\n\t\t}\n\n\t\t// 2. Process video filters (resize and rotation)\n\t\tif query[\"width\"] != nil || query[\"height\"] != nil {\n\t\t\tfilter := \"scale=\"\n\t\t\tif query[\"width\"] != nil {\n\t\t\t\tfilter += query[\"width\"][0]\n\t\t\t} else {\n\t\t\t\tfilter += \"-1\"\n\t\t\t}\n\t\t\tfilter += \":\"\n\t\t\tif query[\"height\"] != nil {\n\t\t\t\tfilter += query[\"height\"][0]\n\t\t\t} else {\n\t\t\t\tfilter += \"-1\"\n\t\t\t}\n\t\t\targs.AddFilter(filter)\n\t\t}\n\n\t\tif query[\"rotate\"] != nil {\n\t\t\tvar filter string\n\t\t\tswitch query[\"rotate\"][0] {\n\t\t\tcase \"90\":\n\t\t\t\tfilter = \"transpose=1\" // 90 degrees clockwise\n\t\t\tcase \"180\":\n\t\t\t\tfilter = \"transpose=1,transpose=1\"\n\t\t\tcase \"-90\", \"270\":\n\t\t\t\tfilter = \"transpose=2\" // 90 degrees counterclockwise\n\t\t\t}\n\t\t\tif filter != \"\" {\n\t\t\t\targs.AddFilter(filter)\n\t\t\t}\n\t\t}\n\n\t\tfor _, drawtext := range query[\"drawtext\"] {\n\t\t\t// support templates https://github.com/AlexxIT/go2rtc/issues/487\n\t\t\tdrawtext = configTemplate(drawtext)\n\n\t\t\t// support default timestamp format\n\t\t\tif !strings.Contains(drawtext, \"text=\") {\n\t\t\t\tdrawtext += `:text='%{localtime\\:%Y-%m-%d %X}'`\n\t\t\t}\n\n\t\t\targs.AddFilter(\"drawtext=\" + drawtext)\n\t\t}\n\n\t\t// 3. Process video codecs\n\t\tif args.Video > 0 {\n\t\t\tfor _, video := range query[\"video\"] {\n\t\t\t\tif video != \"copy\" {\n\t\t\t\t\tif codec := defaults[video]; codec != \"\" {\n\t\t\t\t\t\targs.AddCodec(codec)\n\t\t\t\t\t} else {\n\t\t\t\t\t\targs.AddCodec(video)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\targs.AddCodec(\"-c:v copy\")\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif query[\"bitrate\"] != nil {\n\t\t\t// https://trac.ffmpeg.org/wiki/Limiting%20the%20output%20bitrate\n\t\t\tb := query[\"bitrate\"][0]\n\t\t\targs.AddCodec(\"-b:v \" + b + \" -maxrate \" + b + \" -bufsize \" + b)\n\t\t}\n\n\t\t// 4. Process audio codecs\n\t\tif args.Audio > 0 {\n\t\t\tfor _, audio := range query[\"audio\"] {\n\t\t\t\tif audio != \"copy\" {\n\t\t\t\t\tif codec := defaults[audio]; codec != \"\" {\n\t\t\t\t\t\targs.AddCodec(codec)\n\t\t\t\t\t} else {\n\t\t\t\t\t\targs.AddCodec(audio)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\targs.AddCodec(\"-c:a copy\")\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif query[\"hardware\"] != nil {\n\t\t\thardware.MakeHardware(args, query[\"hardware\"][0], defaults)\n\t\t}\n\t}\n\n\tswitch {\n\tcase args.Video == 0 && args.Audio == 0:\n\t\targs.AddCodec(\"-c copy\")\n\tcase args.Video == 0:\n\t\targs.AddCodec(\"-vn\")\n\tcase args.Audio == 0:\n\t\targs.AddCodec(\"-an\")\n\t}\n\n\t// change otput from RTSP to some other pipe format\n\tswitch {\n\tcase args.Video == 0 && args.Audio == 0:\n\t\t// no transcoding from mjpeg input (ffmpeg device with support output as raw MJPEG)\n\t\tif strings.Contains(args.Input, \" mjpeg \") {\n\t\t\targs.Output = defaults[\"output/mjpeg\"]\n\t\t}\n\tcase args.Video == 1 && args.Audio == 0:\n\t\tswitch core.Before(query.Get(\"video\"), \"/\") {\n\t\tcase \"mjpeg\":\n\t\t\targs.Output = defaults[\"output/mjpeg\"]\n\t\tcase \"raw\":\n\t\t\targs.Output = defaults[\"output/raw\"]\n\t\t}\n\tcase args.Video == 0 && args.Audio == 1:\n\t\tswitch core.Before(query.Get(\"audio\"), \"/\") {\n\t\tcase \"aac\":\n\t\t\targs.Output = defaults[\"output/aac\"]\n\t\tcase \"pcma\", \"pcmu\", \"pcml\":\n\t\t\targs.Output = defaults[\"output/wav\"]\n\t\t}\n\t}\n\n\treturn args\n}\n"
  },
  {
    "path": "internal/ffmpeg/ffmpeg_test.go",
    "content": "package ffmpeg\n\nimport (\n\t\"testing\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/ffmpeg\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestParseArgsFile(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\tsource string\n\t\texpect string\n\t}{\n\t\t{\n\t\t\tname:   \"[FILE] all tracks will be copied without transcoding codecs\",\n\t\t\tsource: \"/media/bbb.mp4\",\n\t\t\texpect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,\n\t\t},\n\t\t{\n\t\t\tname:   \"[FILE] video will be transcoded to H264, audio will be skipped\",\n\t\t\tsource: \"/media/bbb.mp4#video=h264\",\n\t\t\texpect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,\n\t\t},\n\t\t{\n\t\t\tname:   \"[FILE] video will be copied, audio will be transcoded to pcmu\",\n\t\t\tsource: \"/media/bbb.mp4#video=copy#audio=pcmu\",\n\t\t\texpect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v copy -c:a pcm_mulaw -ar:a 8000 -ac:a 1 -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,\n\t\t},\n\t\t{\n\t\t\tname:   \"[FILE] video will be transcoded to H265 and rotate 270º, audio will be skipped\",\n\t\t\tsource: \"/media/bbb.mp4#video=h265#rotate=-90\",\n\t\t\texpect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf \"transpose=2\" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,\n\t\t},\n\t\t{\n\t\t\tname:   \"[FILE] video will be output for MJPEG to pipe, audio will be skipped\",\n\t\t\tsource: \"/media/bbb.mp4#video=mjpeg\",\n\t\t\texpect: `ffmpeg -hide_banner -re -i /media/bbb.mp4 -c:v mjpeg -an -f mjpeg -`,\n\t\t},\n\t\t{\n\t\t\tname:   \"https://github.com/AlexxIT/go2rtc/issues/509\",\n\t\t\tsource: \"ffmpeg:test.mp4#raw=-ss 00:00:20\",\n\t\t\texpect: `ffmpeg -hide_banner -re -i ffmpeg:test.mp4 -ss 00:00:20 -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\targs := parseArgs(test.source)\n\t\t\trequire.Equal(t, test.expect, args.String())\n\t\t})\n\t}\n}\n\nfunc TestParseArgsDevice(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\tsource string\n\t\texpect string\n\t}{\n\t\t{\n\t\t\tname:   \"[DEVICE] video will be output for MJPEG to pipe, with size 1920x1080\",\n\t\t\tsource: \"device?video=0&video_size=1920x1080\",\n\t\t\texpect: `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i \"video=0\" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,\n\t\t},\n\t\t{\n\t\t\tname:   \"[DEVICE] video will be transcoded to H265 with framerate 20, audio will be skipped\",\n\t\t\tsource: \"device?video=0&framerate=20#video=h265\",\n\t\t\texpect: `ffmpeg -hide_banner -f dshow -framerate 20 -i \"video=0\" -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,\n\t\t},\n\t\t{\n\t\t\tname:   \"[DEVICE] video/audio\",\n\t\t\tsource: \"device?video=FaceTime HD Camera&audio=Microphone (High Definition Audio Device)\",\n\t\t\texpect: `ffmpeg -hide_banner -f dshow -i \"video=FaceTime HD Camera:audio=Microphone (High Definition Audio Device)\" -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\targs := parseArgs(test.source)\n\t\t\trequire.Equal(t, test.expect, args.String())\n\t\t})\n\t}\n}\n\nfunc TestParseArgsIpCam(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\tsource string\n\t\texpect string\n\t}{\n\t\t{\n\t\t\tname:   \"[HTTP] video will be copied\",\n\t\t\tsource: \"http://example.com\",\n\t\t\texpect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,\n\t\t},\n\t\t{\n\t\t\tname:   \"[HTTP-MJPEG] video will be transcoded to H264\",\n\t\t\tsource: \"http://example.com#video=h264\",\n\t\t\texpect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http://example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,\n\t\t},\n\t\t{\n\t\t\tname:   \"[HLS] video will be copied, audio will be skipped\",\n\t\t\tsource: \"https://example.com#video=copy\",\n\t\t\texpect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i https://example.com -c:v copy -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,\n\t\t},\n\t\t{\n\t\t\tname:   \"[RTSP] video will be copied without transcoding codecs\",\n\t\t\tsource: \"rtsp://example.com\",\n\t\t\texpect: `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,\n\t\t},\n\t\t{\n\t\t\tname:   \"[RTSP] video with resize to 1280x720, should be transcoded, so select H265\",\n\t\t\tsource: \"rtsp://example.com#video=h265#width=1280#height=720\",\n\t\t\texpect: `ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v libx265 -g 50 -profile:v main -level:v 5.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf \"scale=1280:720\" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,\n\t\t},\n\t\t{\n\t\t\tname:   \"[RTSP] video will be copied, changing RTSP transport from TCP to UDP+TCP\",\n\t\t\tsource: \"rtsp://example.com#input=rtsp/udp\",\n\t\t\texpect: `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,\n\t\t},\n\t\t{\n\t\t\tname:   \"[RTMP] video will be copied, changing RTSP transport from TCP to UDP+TCP\",\n\t\t\tsource: \"rtmp://example.com#input=rtsp/udp\",\n\t\t\texpect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i rtmp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,\n\t\t},\n\t\t{\n\t\t\tname:   \"[RTSP] custom timeout\",\n\t\t\tsource: \"rtsp://example.com#timeout=10\",\n\t\t\texpect: `ffmpeg -hide_banner -allowed_media_types video+audio -fflags nobuffer -flags low_delay -timeout 10000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\targs := parseArgs(test.source)\n\t\t\trequire.Equal(t, test.expect, args.String())\n\t\t})\n\t}\n}\n\nfunc TestParseArgsAudio(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\tsource string\n\t\texpect string\n\t}{\n\t\t{\n\t\t\tname:   \"[AUDIO] audio will be transcoded to AAC, video will be skipped\",\n\t\t\tsource: \"rtsp://example.com#audio=aac\",\n\t\t\texpect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a aac -vn -f adts -`,\n\t\t},\n\t\t{\n\t\t\tname:   \"[AUDIO] audio will be transcoded to AAC/16000, video will be skipped\",\n\t\t\tsource: \"rtsp://example.com#audio=aac/16000\",\n\t\t\texpect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a aac -ar:a 16000 -ac:a 1 -vn -f adts -`,\n\t\t},\n\t\t{\n\t\t\tname:   \"[AUDIO] audio will be transcoded to OPUS, video will be skipped\",\n\t\t\tsource: \"rtsp://example.com#audio=opus\",\n\t\t\texpect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a libopus -application:a lowdelay -min_comp 0 -vn -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,\n\t\t},\n\t\t{\n\t\t\tname:   \"[AUDIO] audio will be transcoded to PCMU, video will be skipped\",\n\t\t\tsource: \"rtsp://example.com#audio=pcmu\",\n\t\t\texpect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_mulaw -ar:a 8000 -ac:a 1 -vn -f wav -`,\n\t\t},\n\t\t{\n\t\t\tname:   \"[AUDIO] audio will be transcoded to PCMU/16000, video will be skipped\",\n\t\t\tsource: \"rtsp://example.com#audio=pcmu/16000\",\n\t\t\texpect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_mulaw -ar:a 16000 -ac:a 1 -vn -f wav -`,\n\t\t},\n\t\t{\n\t\t\tname:   \"[AUDIO] audio will be transcoded to PCMU/48000, video will be skipped\",\n\t\t\tsource: \"rtsp://example.com#audio=pcmu/48000\",\n\t\t\texpect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_mulaw -ar:a 48000 -ac:a 1 -vn -f wav -`,\n\t\t},\n\t\t{\n\t\t\tname:   \"[AUDIO] audio will be transcoded to PCMA, video will be skipped\",\n\t\t\tsource: \"rtsp://example.com#audio=pcma\",\n\t\t\texpect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_alaw -ar:a 8000 -ac:a 1 -vn -f wav -`,\n\t\t},\n\t\t{\n\t\t\tname:   \"[AUDIO] audio will be transcoded to PCMA/16000, video will be skipped\",\n\t\t\tsource: \"rtsp://example.com#audio=pcma/16000\",\n\t\t\texpect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_alaw -ar:a 16000 -ac:a 1 -vn -f wav -`,\n\t\t},\n\t\t{\n\t\t\tname:   \"[AUDIO] audio will be transcoded to PCMA/48000, video will be skipped\",\n\t\t\tsource: \"rtsp://example.com#audio=pcma/48000\",\n\t\t\texpect: `ffmpeg -hide_banner -allowed_media_types audio -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:a pcm_alaw -ar:a 48000 -ac:a 1 -vn -f wav -`,\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\targs := parseArgs(test.source)\n\t\t\trequire.Equal(t, test.expect, args.String())\n\t\t})\n\t}\n}\n\nfunc TestParseArgsHwVaapi(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\tsource string\n\t\texpect string\n\t}{\n\t\t{\n\t\t\tname:   \"[HTTP-MJPEG] video will be transcoded to H264\",\n\t\t\tsource: \"http:///example.com#video=h264#hardware=vaapi\",\n\t\t\texpect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf \"format=vaapi|nv12,hwupload,scale_vaapi=out_color_matrix=bt709:out_range=tv:format=nv12\" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,\n\t\t},\n\t\t{\n\t\t\tname:   \"[RTSP] video with rotation, should be transcoded, so select H264\",\n\t\t\tsource: \"rtsp://example.com#video=h264#rotate=180#hardware=vaapi\",\n\t\t\texpect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf \"format=vaapi|nv12,hwupload,transpose_vaapi=4,scale_vaapi=out_color_matrix=bt709:out_range=tv:format=nv12\" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,\n\t\t},\n\t\t{\n\t\t\tname:   \"[RTSP] video with resize to 1280x720, should be transcoded, so select H265\",\n\t\t\tsource: \"rtsp://example.com#video=h265#width=1280#height=720#hardware=vaapi\",\n\t\t\texpect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_vaapi -g 50 -bf 0 -profile:v main -level:v 5.1 -sei:v 0 -an -vf \"format=vaapi|nv12,hwupload,scale_vaapi=1280:720\" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,\n\t\t},\n\t\t{\n\t\t\tname:   \"[FILE] video will be output for MJPEG to pipe, audio will be skipped\",\n\t\t\tsource: \"/media/bbb.mp4#video=mjpeg#hardware=vaapi\",\n\t\t\texpect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -re -i /media/bbb.mp4 -c:v mjpeg_vaapi -an -vf \"format=vaapi|nv12,hwupload\" -f mjpeg -`,\n\t\t},\n\t\t{\n\t\t\tname:   \"[DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265\",\n\t\t\tsource: \"device?video=0&video_size=1920x1080#video=h265#hardware=vaapi\",\n\t\t\texpect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -f dshow -video_size 1920x1080 -i \"video=0\" -c:v hevc_vaapi -g 50 -bf 0 -profile:v main -level:v 5.1 -sei:v 0 -an -vf \"format=vaapi|nv12,hwupload\" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\targs := parseArgs(test.source)\n\t\t\trequire.Equal(t, test.expect, args.String())\n\t\t})\n\t}\n}\n\nfunc _TestParseArgsHwV4l2m2m(t *testing.T) {\n\t// [HTTP-MJPEG] video will be transcoded to H264\n\targs := parseArgs(\"http:///example.com#video=h264#hardware=v4l2m2m\")\n\trequire.Equal(t, `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_v4l2m2m -g 50 -bf 0 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())\n\n\t// [RTSP] video with rotation, should be transcoded, so select H264\n\targs = parseArgs(\"rtsp://example.com#video=h264#rotate=180#hardware=v4l2m2m\")\n\trequire.Equal(t, `ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_v4l2m2m -g 50 -bf 0 -an -vf \"transpose=1,transpose=1\" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())\n\n\t// [RTSP] video with resize to 1280x720, should be transcoded, so select H265\n\targs = parseArgs(\"rtsp://example.com#video=h265#width=1280#height=720#hardware=v4l2m2m\")\n\trequire.Equal(t, `ffmpeg -hide_banner -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_v4l2m2m -g 50 -bf 0 -an -vf \"scale=1280:720\" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())\n\n\t// [DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265\n\targs = parseArgs(\"device?video=0&video_size=1920x1080#video=h265#hardware=v4l2m2m\")\n\trequire.Equal(t, `ffmpeg -hide_banner -f dshow -video_size 1920x1080 -i video=\"0\" -c:v hevc_v4l2m2m -g 50 -bf 0 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())\n}\n\nfunc TestParseArgsHwRKMPP(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\tsource string\n\t\texpect string\n\t}{\n\t\t{\n\t\t\tname:   \"[FILE] transcoding to H264\",\n\t\t\tsource: \"bbb.mp4#video=h264#hardware=rkmpp\",\n\t\t\texpect: `ffmpeg -hide_banner -hwaccel rkmpp -hwaccel_output_format drm_prime -afbc rga -re -i bbb.mp4 -c:v h264_rkmpp -g 50 -bf 0 -profile:v high -level:v 4.1 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,\n\t\t},\n\t\t{\n\t\t\tname:   \"[FILE] transcoding with rotation\",\n\t\t\tsource: \"bbb.mp4#video=h264#rotate=180#hardware=rkmpp\",\n\t\t\texpect: `ffmpeg -hide_banner -hwaccel rkmpp -hwaccel_output_format drm_prime -afbc rga -re -i bbb.mp4 -c:v h264_rkmpp -g 50 -bf 0 -profile:v high -level:v 4.1 -an -vf \"format=drm_prime|nv12,hwupload,vpp_rkrga=transpose=4\" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,\n\t\t},\n\t\t{\n\t\t\tname:   \"[FILE] transcoding with scaling\",\n\t\t\tsource: \"bbb.mp4#video=h264#height=320#hardware=rkmpp\",\n\t\t\texpect: `ffmpeg -hide_banner -hwaccel rkmpp -hwaccel_output_format drm_prime -afbc rga -re -i bbb.mp4 -c:v h264_rkmpp -g 50 -bf 0 -profile:v high -level:v 4.1 -an -vf \"format=drm_prime|nv12,hwupload,scale_rkrga=-1:320:force_original_aspect_ratio=0\" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\targs := parseArgs(test.source)\n\t\t\trequire.Equal(t, test.expect, args.String())\n\t\t})\n\t}\n}\n\nfunc _TestParseArgsHwCuda(t *testing.T) {\n\t// [HTTP-MJPEG] video will be transcoded to H264\n\targs := parseArgs(\"http:///example.com#video=h264#hardware=cuda\")\n\trequire.Equal(t, `ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format cuda -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_nvenc -g 50 -bf 0 -profile:v high -level:v auto -preset:v p2 -tune:v ll -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())\n\n\t// [RTSP] video with rotation, should be transcoded, so select H264\n\targs = parseArgs(\"rtsp://example.com#video=h264#rotate=180#hardware=cuda\")\n\trequire.Equal(t, `ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format nv12 -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_nvenc -g 50 -bf 0 -profile:v high -level:v auto -preset:v p2 -tune:v ll -an -vf \"transpose=1,transpose=1,hwupload\" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())\n\n\t// [RTSP] video with resize to 1280x720, should be transcoded, so select H265\n\targs = parseArgs(\"rtsp://example.com#video=h265#width=1280#height=720#hardware=cuda\")\n\trequire.Equal(t, `ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format cuda -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_nvenc -g 50 -bf 0 -profile:v high -level:v auto -an -vf \"scale_cuda=1280:720\" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())\n\n\t// [DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265\n\targs = parseArgs(\"device?video=0&video_size=1920x1080#video=h265#hardware=cuda\")\n\trequire.Equal(t, `ffmpeg -hide_banner -hwaccel cuda -hwaccel_output_format cuda -f dshow -video_size 1920x1080 -i video=\"0\" -c:v hevc_nvenc -g 50 -bf 0 -profile:v high -level:v auto -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())\n}\n\nfunc _TestParseArgsHwDxva2(t *testing.T) {\n\t// [HTTP-MJPEG] video will be transcoded to H264\n\targs := parseArgs(\"http:///example.com#video=h264#hardware=dxva2\")\n\trequire.Equal(t, `ffmpeg -hide_banner -hwaccel dxva2 -hwaccel_output_format dxva2_vld -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 -an -vf \"hwmap=derive_device=qsv,format=qsv\" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())\n\n\t// [RTSP] video with rotation, should be transcoded, so select H264\n\targs = parseArgs(\"rtsp://example.com#video=h264#rotate=180#hardware=dxva2\")\n\trequire.Equal(t, `ffmpeg -hide_banner -hwaccel dxva2 -hwaccel_output_format dxva2_vld -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_qsv -g 50 -bf 0 -profile:v high -level:v 4.1 -async_depth:v 1 -an -vf \"hwmap=derive_device=qsv,format=qsv,transpose=1,transpose=1\" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())\n\n\t// [RTSP] video with resize to 1280x720, should be transcoded, so select H265\n\targs = parseArgs(\"rtsp://example.com#video=h265#width=1280#height=720#hardware=dxva2\")\n\trequire.Equal(t, `ffmpeg -hide_banner -hwaccel dxva2 -hwaccel_output_format dxva2_vld -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_qsv -g 50 -bf 0 -profile:v high -level:v 5.1 -async_depth:v 1 -an -vf \"hwmap=derive_device=qsv,format=qsv,scale_qsv=1280:720\" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())\n\n\t// [FILE] video will be output for MJPEG to pipe, audio will be skipped\n\targs = parseArgs(\"/media/bbb.mp4#video=mjpeg#hardware=dxva2\")\n\trequire.Equal(t, `ffmpeg -hide_banner -hwaccel dxva2 -hwaccel_output_format dxva2_vld -re -i /media/bbb.mp4 -c:v mjpeg_qsv -profile:v high -level:v 5.1 -an -vf \"hwmap=derive_device=qsv,format=qsv\" -f mjpeg -`, args.String())\n\n\t// [DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265\n\targs = parseArgs(\"device?video=0&video_size=1920x1080#video=h265#hardware=dxva2\")\n\trequire.Equal(t, `ffmpeg -hide_banner -hwaccel dxva2 -hwaccel_output_format dxva2_vld -f dshow -video_size 1920x1080 -i video=\"0\" -c:v hevc_qsv -g 50 -bf 0 -profile:v high -level:v 5.1 -async_depth:v 1 -an -vf \"hwmap=derive_device=qsv,format=qsv\" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())\n}\n\nfunc _TestParseArgsHwVideotoolbox(t *testing.T) {\n\t// [HTTP-MJPEG] video will be transcoded to H264\n\targs := parseArgs(\"http:///example.com#video=h264#hardware=videotoolbox\")\n\trequire.Equal(t, `ffmpeg -hide_banner -hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_videotoolbox -g 50 -bf 0 -profile:v high -level:v 4.1 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())\n\n\t// [RTSP] video with rotation, should be transcoded, so select H264\n\targs = parseArgs(\"rtsp://example.com#video=h264#rotate=180#hardware=videotoolbox\")\n\trequire.Equal(t, `ffmpeg -hide_banner -hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v h264_videotoolbox -g 50 -bf 0 -profile:v high -level:v 4.1 -an -vf \"transpose=1,transpose=1\" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())\n\n\t// [RTSP] video with resize to 1280x720, should be transcoded, so select H265\n\targs = parseArgs(\"rtsp://example.com#video=h265#width=1280#height=720#hardware=videotoolbox\")\n\trequire.Equal(t, `ffmpeg -hide_banner -hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld -allowed_media_types video -fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i rtsp://example.com -c:v hevc_videotoolbox -g 50 -bf 0 -profile:v high -level:v 5.1 -an -vf \"scale=1280:720\" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())\n\n\t// [DEVICE] MJPEG video with size 1920x1080 will be transcoded to H265\n\targs = parseArgs(\"device?video=0&video_size=1920x1080#video=h265#hardware=videotoolbox\")\n\trequire.Equal(t, `ffmpeg -hide_banner -hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld -f dshow -video_size 1920x1080 -i video=\"0\" -c:v hevc_videotoolbox -g 50 -bf 0 -profile:v high -level:v 5.1 -an -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())\n}\n\nfunc TestDeckLink(t *testing.T) {\n\targs := parseArgs(`DeckLink SDI (2)#video=h264#hardware=vaapi#input=-format_code Hp29 -f decklink -i \"{input}\"`)\n\trequire.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -format_code Hp29 -f decklink -i \"DeckLink SDI (2)\" -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf \"format=vaapi|nv12,hwupload,scale_vaapi=out_color_matrix=bt709:out_range=tv:format=nv12\" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`, args.String())\n}\n\nfunc TestDrawText(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\tsource string\n\t\texpect string\n\t}{\n\t\t{\n\t\t\tsource: \"http:///example.com#video=h264#drawtext=fontsize=12\",\n\t\t\texpect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http:///example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf \"drawtext=fontsize=12:text='%{localtime\\:%Y-%m-%d %X}'\" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,\n\t\t},\n\t\t{\n\t\t\tsource: \"http:///example.com#video=h264#width=640#drawtext=fontsize=12\",\n\t\t\texpect: `ffmpeg -hide_banner -fflags nobuffer -flags low_delay -i http:///example.com -c:v libx264 -g 50 -profile:v high -level:v 4.1 -preset:v superfast -tune:v zerolatency -pix_fmt:v yuv420p -an -vf \"scale=640:-1,drawtext=fontsize=12:text='%{localtime\\:%Y-%m-%d %X}'\" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,\n\t\t},\n\t\t{\n\t\t\tsource: \"http:///example.com#video=h264#width=640#drawtext=fontsize=12#hardware=vaapi\",\n\t\t\texpect: `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format nv12 -hwaccel_flags allow_profile_mismatch -fflags nobuffer -flags low_delay -i http:///example.com -c:v h264_vaapi -g 50 -bf 0 -profile:v high -level:v 4.1 -sei:v 0 -an -vf \"scale=640:-1,drawtext=fontsize=12:text='%{localtime\\:%Y-%m-%d %X}',hwupload\" -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\targs := parseArgs(test.source)\n\t\t\trequire.Equal(t, test.expect, args.String())\n\t\t})\n\t}\n}\n\nfunc TestVersion(t *testing.T) {\n\tverAV = ffmpeg.Version61\n\ttests := []struct {\n\t\tname   string\n\t\tsource string\n\t\texpect string\n\t}{\n\t\t{\n\t\t\tsource: \"/media/bbb.mp4\",\n\t\t\texpect: `ffmpeg -hide_banner -readrate_initial_burst 0.001 -re -i /media/bbb.mp4 -c copy -user_agent ffmpeg/go2rtc -rtsp_transport tcp -f rtsp {output}`,\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\targs := parseArgs(test.source)\n\t\t\trequire.Equal(t, test.expect, args.String())\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "internal/ffmpeg/hardware/README.md",
    "content": "# Hardware\n\nYou **DON'T** need hardware acceleration if:\n\n- you're not using the [FFmpeg source](../README.md)\n- you're using only `#video=copy` for the FFmpeg source\n- you're using only `#audio=...` (any audio) transcoding for the FFmpeg source\n\nYou **NEED** hardware acceleration if you're using `#video=h264`, `#video=h265`, `#video=mjpeg` (video) transcoding.\n\n## Important\n\n- Acceleration is disabled by default because it can be unstable (this may change in the future)\n- go2rtc can automatically detect supported hardware acceleration if enabled\n- go2rtc will enable hardware decoding only if hardware encoding is supported\n- go2rtc will use the same GPU for decoder and encoder\n- Intel and AMD will switch to a software decoder if the input codec isn't supported by the hardware decoder\n- NVIDIA will fail if the input codec isn't supported by the hardware decoder\n- Raspberry Pi always uses a software decoder\n\n```yaml\nstreams:\n  # auto select hardware encoder\n  camera1_hw: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=h264#hardware\n\n  # manual select hardware encoder (vaapi, cuda, v4l2m2m, dxva2, videotoolbox)\n  camera1_vaapi: ffmpeg:rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0#video=h264#hardware=vaapi\n```\n\n## Docker and Hass Addon\n\nThere are two versions of the Docker container and Hass Add-on:\n\n- Latest (Alpine) supports hardware acceleration for Intel iGPU (CPU with graphics) and Raspberry Pi.\n- Hardware (Debian 12) supports Intel iGPU, AMD GPU, NVIDIA GPU.\n\n## Intel iGPU\n\n**Supported on:** Windows binary, Linux binary, Docker, Hass Addon.\n\nIf you have an Intel Sandy Bridge (2011) CPU with graphics, you already have hardware decoding/encoding support for `AVC/H.264`.\n\nIf you have an Intel Skylake (2015) CPU with graphics, you already have hardware decoding/encoding support for `AVC/H.264`, `HEVC/H.265` and `MJPEG`.\n\nRead more [here](https://en.wikipedia.org/wiki/Intel_Quick_Sync_Video#Hardware_decoding_and_encoding) and [here](https://en.wikipedia.org/wiki/Intel_Graphics_Technology#Capabilities_(GPU_video_acceleration)).\n\nLinux and Docker:\n\n- It may be important to have a recent OS and Linux kernel. For example, on my **Debian 10 (kernel 4.19)** it did not work, but after updating to **Debian 11 (kernel 5.10)** everything was fine.\n- If you run into trouble, check that you have the `/dev/dri/` folder on your host.\n\nDocker users should add the `--privileged` option to the container for access to the hardware.\n\n**PS.** Supported via [VAAPI](https://trac.ffmpeg.org/wiki/Hardware/VAAPI) engine on Linux and [DXVA2+QSV](https://trac.ffmpeg.org/wiki/Hardware/QuickSync) engine on Windows.\n\n## AMD GPU\n\n*I don't have the hardware to test this!!!*\n\n**Supported on:** Linux binary, Docker, Hass Addon.\n\nDocker users should install: `alexxit/go2rtc:master-hardware`. Docker users should add the `--privileged` option to the container for access to the hardware.\n\nHass Addon users should install **go2rtc master hardware** version.\n\n**PS.** Supported via [VAAPI](https://trac.ffmpeg.org/wiki/Hardware/VAAPI) engine.\n\n## NVIDIA GPU\n\n**Supported on:** Windows binary, Linux binary, Docker.\n\nDocker users should install: `alexxit/go2rtc:master-hardware`.\n\nRead more [here](https://docs.frigate.video/configuration/hardware_acceleration) and [here](https://jellyfin.org/docs/general/administration/hardware-acceleration/#nvidia-hardware-acceleration-on-docker-linux).\n\n**PS.** Supported via [CUDA](https://trac.ffmpeg.org/wiki/HWAccelIntro#CUDANVENCNVDEC) engine.\n\n## Raspberry Pi 3\n\n**Supported on:** Linux binary, Docker, Hass Addon.\n\nI don't recommend using transcoding on the Raspberry Pi 3. It's extremely slow, even with hardware acceleration. Also, it may fail when transcoding a 2K+ stream.\n\n## Raspberry Pi 4\n\n*I don't have the hardware to test this!!!*\n\n**Supported on:** Linux binary, Docker, Hass Addon.\n\n**PS.** Supported via [v4l2m2m](https://lalitm.com/hw-encoding-raspi/) engine.\n\n## macOS\n\nIn my tests, transcoding is faster on the M1 CPU than on the M1 GPU. Transcoding time on the M1 CPU is better than any Intel iGPU and comparable to an NVIDIA RTX 2070.\n\n**PS.** Supported via [videotoolbox](https://trac.ffmpeg.org/wiki/HWAccelIntro#VideoToolbox) engine.\n\n## Rockchip\n\n- It's important to use a custom FFmpeg build with Rockchip support from [@nyanmisaka](https://github.com/nyanmisaka/ffmpeg-rockchip)\n  - Static binaries from [@MarcA711](https://github.com/MarcA711/Rockchip-FFmpeg-Builds/releases/)\n- It's important to have Linux kernel 5.10 or 6.1\n\n**Tested**\n\n- [Orange Pi 3B](https://www.armbian.com/orangepi3b/) with Armbian 6.1, supports transcoding H.264, H.265, MJPEG\n"
  },
  {
    "path": "internal/ffmpeg/hardware/hardware.go",
    "content": "package hardware\n\nimport (\n\t\"net/http\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/pkg/ffmpeg\"\n)\n\nconst (\n\tEngineSoftware     = \"software\"\n\tEngineVAAPI        = \"vaapi\"        // Intel iGPU and AMD GPU\n\tEngineV4L2M2M      = \"v4l2m2m\"      // Raspberry Pi 3 and 4\n\tEngineCUDA         = \"cuda\"         // NVidia on Windows and Linux\n\tEngineDXVA2        = \"dxva2\"        // Intel on Windows\n\tEngineVideoToolbox = \"videotoolbox\" // macOS\n\tEngineRKMPP        = \"rkmpp\"        // Rockchip\n)\n\nfunc Init(bin string) {\n\tapi.HandleFunc(\"api/ffmpeg/hardware\", func(w http.ResponseWriter, r *http.Request) {\n\t\tapi.ResponseSources(w, ProbeAll(bin))\n\t})\n}\n\n// MakeHardware converts software FFmpeg args to hardware args\n// empty engine for autoselect\nfunc MakeHardware(args *ffmpeg.Args, engine string, defaults map[string]string) {\n\tfor i, codec := range args.Codecs {\n\t\tif len(codec) < 10 {\n\t\t\tcontinue // skip short line (-c:v mjpeg...)\n\t\t}\n\n\t\t// get current codec name\n\t\tname := cut(codec, ' ', 1)\n\t\tswitch name {\n\t\tcase \"libx264\":\n\t\t\tname = \"h264\"\n\t\tcase \"libx265\":\n\t\t\tname = \"h265\"\n\t\tcase \"mjpeg\":\n\t\tdefault:\n\t\t\tcontinue // skip unsupported codec\n\t\t}\n\n\t\t// temporary disable probe for H265\n\t\tif engine == \"\" && name != \"h265\" {\n\t\t\tif engine = cache[name]; engine == \"\" {\n\t\t\t\tengine = ProbeHardware(args.Bin, name)\n\t\t\t\tcache[name] = engine\n\t\t\t}\n\t\t}\n\n\t\tswitch engine {\n\t\tcase EngineVAAPI:\n\t\t\targs.Codecs[i] = defaults[name+\"/\"+engine]\n\n\t\t\tif !args.HasFilters(\"drawtext=\") {\n\t\t\t\targs.Input = \"-hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch \" + args.Input\n\n\t\t\t\tif name == \"h264\" {\n\t\t\t\t\tfixPixelFormat(args)\n\t\t\t\t}\n\n\t\t\t\tfor i, filter := range args.Filters {\n\t\t\t\t\tif strings.HasPrefix(filter, \"scale=\") {\n\t\t\t\t\t\targs.Filters[i] = \"scale_vaapi=\" + filter[6:]\n\t\t\t\t\t}\n\t\t\t\t\tif strings.HasPrefix(filter, \"transpose=\") {\n\t\t\t\t\t\tif filter == \"transpose=1,transpose=1\" { // 180 degrees half-turn\n\t\t\t\t\t\t\targs.Filters[i] = \"transpose_vaapi=4\" // reversal\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\targs.Filters[i] = \"transpose_vaapi=\" + filter[10:]\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\t// fix if input doesn't support hwaccel, do nothing when support\n\t\t\t\t// insert as first filter before hardware scale and transpose\n\t\t\t\targs.InsertFilter(\"format=vaapi|nv12,hwupload\")\n\t\t\t} else {\n\t\t\t\t// enable software pixel for drawtext, scale and transpose\n\t\t\t\targs.Input = \"-hwaccel vaapi -hwaccel_output_format nv12 -hwaccel_flags allow_profile_mismatch \" + args.Input\n\n\t\t\t\targs.AddFilter(\"hwupload\")\n\t\t\t}\n\n\t\tcase EngineCUDA:\n\t\t\targs.Codecs[i] = defaults[name+\"/\"+engine]\n\n\t\t\t// CUDA doesn't support hardware transpose\n\t\t\t// https://github.com/AlexxIT/go2rtc/issues/389\n\t\t\tif !args.HasFilters(\"drawtext=\", \"transpose=\") {\n\t\t\t\targs.Input = \"-hwaccel cuda -hwaccel_output_format cuda \" + args.Input\n\n\t\t\t\tfor i, filter := range args.Filters {\n\t\t\t\t\tif strings.HasPrefix(filter, \"scale=\") {\n\t\t\t\t\t\targs.Filters[i] = \"scale_cuda=\" + filter[6:]\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\targs.Input = \"-hwaccel cuda -hwaccel_output_format nv12 \" + args.Input\n\n\t\t\t\targs.AddFilter(\"hwupload\")\n\t\t\t}\n\n\t\tcase EngineDXVA2:\n\t\t\targs.Input = \"-hwaccel dxva2 -hwaccel_output_format dxva2_vld \" + args.Input\n\t\t\targs.Codecs[i] = defaults[name+\"/\"+engine]\n\n\t\t\tfor i, filter := range args.Filters {\n\t\t\t\tif strings.HasPrefix(filter, \"scale=\") {\n\t\t\t\t\targs.Filters[i] = \"scale_qsv=\" + filter[6:]\n\t\t\t\t}\n\t\t\t}\n\n\t\t\targs.InsertFilter(\"hwmap=derive_device=qsv,format=qsv\")\n\n\t\tcase EngineVideoToolbox:\n\t\t\targs.Input = \"-hwaccel videotoolbox -hwaccel_output_format videotoolbox_vld \" + args.Input\n\t\t\targs.Codecs[i] = defaults[name+\"/\"+engine]\n\n\t\tcase EngineV4L2M2M:\n\t\t\targs.Codecs[i] = defaults[name+\"/\"+engine]\n\n\t\tcase EngineRKMPP:\n\t\t\targs.Codecs[i] = defaults[name+\"/\"+engine]\n\n\t\t\tif !args.HasFilters(\"drawtext=\") {\n\t\t\t\targs.Input = \"-hwaccel rkmpp -hwaccel_output_format drm_prime -afbc rga \" + args.Input\n\n\t\t\t\tfor i, filter := range args.Filters {\n\t\t\t\t\tif strings.HasPrefix(filter, \"scale=\") {\n\t\t\t\t\t\targs.Filters[i] = \"scale_rkrga=\" + filter[6:] + \":force_original_aspect_ratio=0\"\n\t\t\t\t\t}\n\t\t\t\t\tif strings.HasPrefix(filter, \"transpose=\") {\n\t\t\t\t\t\tif filter == \"transpose=1,transpose=1\" { // 180 degrees half-turn\n\t\t\t\t\t\t\targs.Filters[i] = \"vpp_rkrga=transpose=4\" // reversal\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\targs.Filters[i] = \"vpp_rkrga=transpose=\" + filter[10:]\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tif len(args.Filters) > 0 {\n\t\t\t\t\t// fix if input doesn't support hwaccel, do nothing when support\n\t\t\t\t\t// insert as first filter before hardware scale and transpose\n\t\t\t\t\targs.InsertFilter(\"format=drm_prime|nv12,hwupload\")\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// enable software pixel for drawtext, scale and transpose\n\t\t\t\targs.Input = \"-hwaccel rkmpp -hwaccel_output_format nv12 -afbc rga \" + args.Input\n\n\t\t\t\targs.AddFilter(\"hwupload\")\n\t\t\t}\n\t\t}\n\t}\n}\n\nvar cache = map[string]string{}\n\nfunc run(bin string, args string) bool {\n\terr := exec.Command(bin, strings.Split(args, \" \")...).Run()\n\treturn err == nil\n}\n\nfunc runToString(bin string, args string) string {\n\tif run(bin, args) {\n\t\treturn \"OK\"\n\t} else {\n\t\treturn \"ERROR\"\n\t}\n}\n\nfunc cut(s string, sep byte, pos int) string {\n\tfor n := 0; n < pos; n++ {\n\t\tif i := strings.IndexByte(s, sep); i > 0 {\n\t\t\ts = s[i+1:]\n\t\t} else {\n\t\t\treturn \"\"\n\t\t}\n\t}\n\tif i := strings.IndexByte(s, sep); i > 0 {\n\t\treturn s[:i]\n\t}\n\treturn s\n}\n\n// fixPixelFormat:\n//   - good h264 pixel: yuv420p(tv, bt709) == yuv420p (mpeg/limited/tv)\n//   - bad h264 pixel: yuvj420p(pc, bt709) == yuvj420p (jpeg/full/pc)\n//   - bad jpeg pixel: yuvj422p(pc, bt470bg)\nfunc fixPixelFormat(args *ffmpeg.Args) {\n\t// in my tests this filters has same CPU/GPU load:\n\t//   - \"hwupload\"\n\t//   - \"hwupload,scale_vaapi=out_color_matrix=bt709:out_range=tv\"\n\t//   - \"hwupload,scale_vaapi=out_color_matrix=bt709:out_range=tv:format=nv12\"\n\tconst fixPixFmt = \"out_color_matrix=bt709:out_range=tv:format=nv12\"\n\n\tfor i, filter := range args.Filters {\n\t\tif strings.HasPrefix(filter, \"scale=\") {\n\t\t\targs.Filters[i] = filter + \":\" + fixPixFmt\n\t\t\treturn\n\t\t}\n\t}\n\n\targs.Filters = append(args.Filters, \"scale=\"+fixPixFmt)\n}\n"
  },
  {
    "path": "internal/ffmpeg/hardware/hardware_bsd.go",
    "content": "//go:build freebsd || netbsd || openbsd || dragonfly\n\npackage hardware\n\nimport (\n\t\"runtime\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n)\n\nconst (\n\tProbeV4L2M2MH264 = \"-f lavfi -i testsrc2 -t 1 -c h264_v4l2m2m -f null -\"\n\tProbeV4L2M2MH265 = \"-f lavfi -i testsrc2 -t 1 -c hevc_v4l2m2m -f null -\"\n\tProbeRKMPPH264   = \"-f lavfi -i testsrc2 -t 1 -c h264_rkmpp_encoder -f null -\"\n\tProbeRKMPPH265   = \"-f lavfi -i testsrc2 -t 1 -c hevc_rkmpp_encoder -f null -\"\n)\n\nfunc ProbeAll(bin string) []*api.Source {\n\treturn []*api.Source{\n\t\t{\n\t\t\tName: runToString(bin, ProbeV4L2M2MH264),\n\t\t\tURL:  \"ffmpeg:...#video=h264#hardware=\" + EngineV4L2M2M,\n\t\t},\n\t\t{\n\t\t\tName: runToString(bin, ProbeV4L2M2MH265),\n\t\t\tURL:  \"ffmpeg:...#video=h265#hardware=\" + EngineV4L2M2M,\n\t\t},\n\t\t{\n\t\t\tName: runToString(bin, ProbeRKMPPH264),\n\t\t\tURL:  \"ffmpeg:...#video=h264#hardware=\" + EngineRKMPP,\n\t\t},\n\t\t{\n\t\t\tName: runToString(bin, ProbeRKMPPH265),\n\t\t\tURL:  \"ffmpeg:...#video=h265#hardware=\" + EngineRKMPP,\n\t\t},\n\t}\n}\n\nfunc ProbeHardware(bin, name string) string {\n\tif runtime.GOARCH == \"arm64\" || runtime.GOARCH == \"arm\" {\n\t\tswitch name {\n\t\tcase \"h264\":\n\t\t\tif run(bin, ProbeV4L2M2MH264) {\n\t\t\t\treturn EngineV4L2M2M\n\t\t\t}\n\t\t\tif run(bin, ProbeRKMPPH264) {\n\t\t\t\treturn EngineRKMPP\n\t\t\t}\n\t\tcase \"h265\":\n\t\t\tif run(bin, ProbeV4L2M2MH265) {\n\t\t\t\treturn EngineV4L2M2M\n\t\t\t}\n\t\t\tif run(bin, ProbeRKMPPH265) {\n\t\t\t\treturn EngineRKMPP\n\t\t\t}\n\t\t}\n\n\t\treturn EngineSoftware\n\t}\n\n\treturn EngineSoftware\n}\n"
  },
  {
    "path": "internal/ffmpeg/hardware/hardware_darwin.go",
    "content": "//go:build darwin || ios\n\npackage hardware\n\nimport (\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n)\n\nconst ProbeVideoToolboxH264 = \"-f lavfi -i testsrc2=size=svga -t 1 -c h264_videotoolbox -f null -\"\nconst ProbeVideoToolboxH265 = \"-f lavfi -i testsrc2=size=svga -t 1 -c hevc_videotoolbox -f null -\"\n\nfunc ProbeAll(bin string) []*api.Source {\n\treturn []*api.Source{\n\t\t{\n\t\t\tName: runToString(bin, ProbeVideoToolboxH264),\n\t\t\tURL:  \"ffmpeg:...#video=h264#hardware=\" + EngineVideoToolbox,\n\t\t},\n\t\t{\n\t\t\tName: runToString(bin, ProbeVideoToolboxH265),\n\t\t\tURL:  \"ffmpeg:...#video=h265#hardware=\" + EngineVideoToolbox,\n\t\t},\n\t}\n}\n\nfunc ProbeHardware(bin, name string) string {\n\tswitch name {\n\tcase \"h264\":\n\t\tif run(bin, ProbeVideoToolboxH264) {\n\t\t\treturn EngineVideoToolbox\n\t\t}\n\n\tcase \"h265\":\n\t\tif run(bin, ProbeVideoToolboxH265) {\n\t\t\treturn EngineVideoToolbox\n\t\t}\n\t}\n\n\treturn EngineSoftware\n}\n"
  },
  {
    "path": "internal/ffmpeg/hardware/hardware_unix.go",
    "content": "//go:build unix && !darwin && !freebsd && !netbsd && !openbsd && !dragonfly\n\npackage hardware\n\nimport (\n\t\"runtime\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n)\n\nconst (\n\tProbeV4L2M2MH264 = \"-f lavfi -i testsrc2 -t 1 -c h264_v4l2m2m -f null -\"\n\tProbeV4L2M2MH265 = \"-f lavfi -i testsrc2 -t 1 -c hevc_v4l2m2m -f null -\"\n\tProbeRKMPPH264   = \"-f lavfi -i testsrc2 -t 1 -c h264_rkmpp -f null -\"\n\tProbeRKMPPH265   = \"-f lavfi -i testsrc2 -t 1 -c hevc_rkmpp -f null -\"\n\tProbeRKMPPJPEG   = \"-f lavfi -i testsrc2 -t 1 -c mjpeg_rkmpp -f null -\"\n\tProbeVAAPIH264   = \"-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c h264_vaapi -f null -\"\n\tProbeVAAPIH265   = \"-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c hevc_vaapi -f null -\"\n\tProbeVAAPIJPEG   = \"-init_hw_device vaapi -f lavfi -i testsrc2 -t 1 -vf format=nv12,hwupload -c mjpeg_vaapi -f null -\"\n\tProbeCUDAH264    = \"-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c h264_nvenc -f null -\"\n\tProbeCUDAH265    = \"-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c hevc_nvenc -f null -\"\n)\n\nfunc ProbeAll(bin string) []*api.Source {\n\tif runtime.GOARCH == \"arm64\" || runtime.GOARCH == \"arm\" {\n\t\treturn []*api.Source{\n\t\t\t{\n\t\t\t\tName: runToString(bin, ProbeV4L2M2MH264),\n\t\t\t\tURL:  \"ffmpeg:...#video=h264#hardware=\" + EngineV4L2M2M,\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: runToString(bin, ProbeV4L2M2MH265),\n\t\t\t\tURL:  \"ffmpeg:...#video=h265#hardware=\" + EngineV4L2M2M,\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: runToString(bin, ProbeRKMPPH264),\n\t\t\t\tURL:  \"ffmpeg:...#video=h264#hardware=\" + EngineRKMPP,\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: runToString(bin, ProbeRKMPPH265),\n\t\t\t\tURL:  \"ffmpeg:...#video=h265#hardware=\" + EngineRKMPP,\n\t\t\t},\n\t\t\t{\n\t\t\t\tName: runToString(bin, ProbeRKMPPJPEG),\n\t\t\t\tURL:  \"ffmpeg:...#video=mjpeg#hardware=\" + EngineRKMPP,\n\t\t\t},\n\t\t}\n\t}\n\n\treturn []*api.Source{\n\t\t{\n\t\t\tName: runToString(bin, ProbeVAAPIH264),\n\t\t\tURL:  \"ffmpeg:...#video=h264#hardware=\" + EngineVAAPI,\n\t\t},\n\t\t{\n\t\t\tName: runToString(bin, ProbeVAAPIH265),\n\t\t\tURL:  \"ffmpeg:...#video=h265#hardware=\" + EngineVAAPI,\n\t\t},\n\t\t{\n\t\t\tName: runToString(bin, ProbeVAAPIJPEG),\n\t\t\tURL:  \"ffmpeg:...#video=mjpeg#hardware=\" + EngineVAAPI,\n\t\t},\n\t\t{\n\t\t\tName: runToString(bin, ProbeCUDAH264),\n\t\t\tURL:  \"ffmpeg:...#video=h264#hardware=\" + EngineCUDA,\n\t\t},\n\t\t{\n\t\t\tName: runToString(bin, ProbeCUDAH265),\n\t\t\tURL:  \"ffmpeg:...#video=h265#hardware=\" + EngineCUDA,\n\t\t},\n\t}\n}\n\nfunc ProbeHardware(bin, name string) string {\n\tif runtime.GOARCH == \"arm64\" || runtime.GOARCH == \"arm\" {\n\t\tswitch name {\n\t\tcase \"h264\":\n\t\t\tif run(bin, ProbeV4L2M2MH264) {\n\t\t\t\treturn EngineV4L2M2M\n\t\t\t}\n\t\t\tif run(bin, ProbeRKMPPH264) {\n\t\t\t\treturn EngineRKMPP\n\t\t\t}\n\t\tcase \"h265\":\n\t\t\tif run(bin, ProbeV4L2M2MH265) {\n\t\t\t\treturn EngineV4L2M2M\n\t\t\t}\n\t\t\tif run(bin, ProbeRKMPPH265) {\n\t\t\t\treturn EngineRKMPP\n\t\t\t}\n\t\tcase \"mjpeg\":\n\t\t\tif run(bin, ProbeRKMPPJPEG) {\n\t\t\t\treturn EngineRKMPP\n\t\t\t}\n\t\t}\n\n\t\treturn EngineSoftware\n\t}\n\n\tswitch name {\n\tcase \"h264\":\n\t\tif run(bin, ProbeCUDAH264) {\n\t\t\treturn EngineCUDA\n\t\t}\n\t\tif run(bin, ProbeVAAPIH264) {\n\t\t\treturn EngineVAAPI\n\t\t}\n\n\tcase \"h265\":\n\t\tif run(bin, ProbeCUDAH265) {\n\t\t\treturn EngineCUDA\n\t\t}\n\t\tif run(bin, ProbeVAAPIH265) {\n\t\t\treturn EngineVAAPI\n\t\t}\n\n\tcase \"mjpeg\":\n\t\tif run(bin, ProbeVAAPIJPEG) {\n\t\t\treturn EngineVAAPI\n\t\t}\n\t}\n\n\treturn EngineSoftware\n}\n"
  },
  {
    "path": "internal/ffmpeg/hardware/hardware_windows.go",
    "content": "//go:build windows\n\npackage hardware\n\nimport \"github.com/AlexxIT/go2rtc/internal/api\"\n\nconst ProbeDXVA2H264 = \"-init_hw_device dxva2 -f lavfi -i testsrc2 -t 1 -c h264_qsv -f null -\"\nconst ProbeDXVA2H265 = \"-init_hw_device dxva2 -f lavfi -i testsrc2 -t 1 -c hevc_qsv -f null -\"\nconst ProbeDXVA2JPEG = \"-init_hw_device dxva2 -f lavfi -i testsrc2 -t 1 -c mjpeg_qsv -f null -\"\nconst ProbeCUDAH264 = \"-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c h264_nvenc -f null -\"\nconst ProbeCUDAH265 = \"-init_hw_device cuda -f lavfi -i testsrc2 -t 1 -c hevc_nvenc -f null -\"\n\nfunc ProbeAll(bin string) []*api.Source {\n\treturn []*api.Source{\n\t\t{\n\t\t\tName: runToString(bin, ProbeDXVA2H264),\n\t\t\tURL:  \"ffmpeg:...#video=h264#hardware=\" + EngineDXVA2,\n\t\t},\n\t\t{\n\t\t\tName: runToString(bin, ProbeDXVA2H265),\n\t\t\tURL:  \"ffmpeg:...#video=h265#hardware=\" + EngineDXVA2,\n\t\t},\n\t\t{\n\t\t\tName: runToString(bin, ProbeDXVA2JPEG),\n\t\t\tURL:  \"ffmpeg:...#video=mjpeg#hardware=\" + EngineDXVA2,\n\t\t},\n\t\t{\n\t\t\tName: runToString(bin, ProbeCUDAH264),\n\t\t\tURL:  \"ffmpeg:...#video=h264#hardware=\" + EngineCUDA,\n\t\t},\n\t\t{\n\t\t\tName: runToString(bin, ProbeCUDAH265),\n\t\t\tURL:  \"ffmpeg:...#video=h265#hardware=\" + EngineCUDA,\n\t\t},\n\t}\n}\n\nfunc ProbeHardware(bin, name string) string {\n\tswitch name {\n\tcase \"h264\":\n\t\tif run(bin, ProbeCUDAH264) {\n\t\t\treturn EngineCUDA\n\t\t}\n\t\tif run(bin, ProbeDXVA2H264) {\n\t\t\treturn EngineDXVA2\n\t\t}\n\n\tcase \"h265\":\n\t\tif run(bin, ProbeCUDAH265) {\n\t\t\treturn EngineCUDA\n\t\t}\n\t\tif run(bin, ProbeDXVA2H265) {\n\t\t\treturn EngineDXVA2\n\t\t}\n\n\tcase \"mjpeg\":\n\t\tif run(bin, ProbeDXVA2JPEG) {\n\t\t\treturn EngineDXVA2\n\t\t}\n\t}\n\n\treturn EngineSoftware\n}\n"
  },
  {
    "path": "internal/ffmpeg/jpeg.go",
    "content": "package ffmpeg\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"os/exec\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/ffmpeg/hardware\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/ffmpeg\"\n\t\"github.com/AlexxIT/go2rtc/pkg/shell\"\n)\n\nfunc JPEGWithQuery(b []byte, query url.Values) ([]byte, error) {\n\targs := parseQuery(query)\n\treturn transcode(b, args.String())\n}\n\nfunc JPEGWithScale(b []byte, width, height int) ([]byte, error) {\n\targs := defaultArgs()\n\targs.AddFilter(fmt.Sprintf(\"scale=%d:%d\", width, height))\n\treturn transcode(b, args.String())\n}\n\nfunc transcode(b []byte, args string) ([]byte, error) {\n\tcmdArgs := shell.QuoteSplit(args)\n\tcmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)\n\tcmd.Stdin = bytes.NewBuffer(b)\n\treturn cmd.Output()\n}\n\nfunc defaultArgs() *ffmpeg.Args {\n\treturn &ffmpeg.Args{\n\t\tBin:    defaults[\"bin\"],\n\t\tGlobal: defaults[\"global\"],\n\t\tInput:  \"-i -\",\n\t\tCodecs: []string{defaults[\"mjpeg\"]},\n\t\tOutput: defaults[\"output/mjpeg\"],\n\t}\n}\n\nfunc parseQuery(query url.Values) *ffmpeg.Args {\n\targs := defaultArgs()\n\n\tvar width = -1\n\tvar height = -1\n\tvar r, hw string\n\n\tfor k, v := range query {\n\t\tswitch k {\n\t\tcase \"width\", \"w\":\n\t\t\twidth = core.Atoi(v[0])\n\t\tcase \"height\", \"h\":\n\t\t\theight = core.Atoi(v[0])\n\t\tcase \"rotate\":\n\t\t\tr = v[0]\n\t\tcase \"hardware\", \"hw\":\n\t\t\thw = v[0]\n\t\t}\n\t}\n\n\tif width > 0 || height > 0 {\n\t\targs.AddFilter(fmt.Sprintf(\"scale=%d:%d\", width, height))\n\t}\n\n\tif r != \"\" {\n\t\tswitch r {\n\t\tcase \"90\":\n\t\t\targs.AddFilter(\"transpose=1\") // 90 degrees clockwise\n\t\tcase \"180\":\n\t\t\targs.AddFilter(\"transpose=1,transpose=1\")\n\t\tcase \"-90\", \"270\":\n\t\t\targs.AddFilter(\"transpose=2\") // 90 degrees counterclockwise\n\t\t}\n\t}\n\n\tif hw != \"\" {\n\t\thardware.MakeHardware(args, hw, defaults)\n\t}\n\n\treturn args\n}\n"
  },
  {
    "path": "internal/ffmpeg/jpeg_test.go",
    "content": "package ffmpeg\n\nimport (\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestParseQuery(t *testing.T) {\n\targs := parseQuery(nil)\n\trequire.Equal(t, `ffmpeg -hide_banner -i - -c:v mjpeg -f mjpeg -`, args.String())\n\n\tquery, err := url.ParseQuery(\"h=480\")\n\trequire.Nil(t, err)\n\targs = parseQuery(query)\n\trequire.Equal(t, `ffmpeg -hide_banner -i - -c:v mjpeg -vf \"scale=-1:480\" -f mjpeg -`, args.String())\n\n\tquery, err = url.ParseQuery(\"hw=vaapi\")\n\trequire.Nil(t, err)\n\targs = parseQuery(query)\n\trequire.Equal(t, `ffmpeg -hide_banner -hwaccel vaapi -hwaccel_output_format vaapi -hwaccel_flags allow_profile_mismatch -i - -c:v mjpeg_vaapi -vf \"format=vaapi|nv12,hwupload\" -f mjpeg -`, args.String())\n}\n"
  },
  {
    "path": "internal/ffmpeg/producer.go",
    "content": "package ffmpeg\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/aac\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\ntype Producer struct {\n\tcore.Connection\n\turl    string\n\tquery  url.Values\n\tffmpeg core.Producer\n}\n\n// NewProducer - FFmpeg producer with auto selection video/audio codec based on client capabilities\nfunc NewProducer(url string) (core.Producer, error) {\n\tp := &Producer{}\n\n\ti := strings.IndexByte(url, '#')\n\tp.url, p.query = url[:i], streams.ParseQuery(url[i+1:])\n\n\t// ffmpeg.NewProducer support only one audio\n\tif len(p.query[\"video\"]) != 0 || len(p.query[\"audio\"]) != 1 {\n\t\treturn nil, errors.New(\"ffmpeg: unsupported params: \" + url[i:])\n\t}\n\n\tp.ID = core.NewID()\n\tp.FormatName = \"ffmpeg\"\n\tp.Medias = []*core.Media{\n\t\t{\n\t\t\t// we can support only audio, because don't know FmtpLine for H264 and PayloadType for MJPEG\n\t\t\tKind:      core.KindAudio,\n\t\t\tDirection: core.DirectionRecvonly,\n\t\t\t// codecs in order from best to worst\n\t\t\tCodecs: []*core.Codec{\n\t\t\t\t// OPUS will always marked as OPUS/48000/2\n\t\t\t\t{Name: core.CodecOpus, ClockRate: 48000, Channels: 2},\n\t\t\t\t{Name: core.CodecPCML, ClockRate: 16000},\n\t\t\t\t{Name: core.CodecPCM, ClockRate: 16000},\n\t\t\t\t{Name: core.CodecPCMA, ClockRate: 16000},\n\t\t\t\t{Name: core.CodecPCMU, ClockRate: 16000},\n\t\t\t\t{Name: core.CodecPCML, ClockRate: 8000},\n\t\t\t\t{Name: core.CodecPCM, ClockRate: 8000},\n\t\t\t\t{Name: core.CodecPCMA, ClockRate: 8000},\n\t\t\t\t{Name: core.CodecPCMU, ClockRate: 8000},\n\t\t\t\t// AAC has unknown problems on Dahua two way\n\t\t\t\t{Name: core.CodecAAC, ClockRate: 16000, FmtpLine: aac.FMTP + \"1408\"},\n\t\t\t},\n\t\t},\n\t}\n\treturn p, nil\n}\n\nfunc (p *Producer) Start() error {\n\tvar err error\n\tif p.ffmpeg, err = streams.GetProducer(p.newURL()); err != nil {\n\t\treturn err\n\t}\n\n\tfor i, media := range p.ffmpeg.GetMedias() {\n\t\ttrack, err := p.ffmpeg.GetTrack(media, media.Codecs[0])\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tp.Receivers[i].Replace(track)\n\t}\n\n\treturn p.ffmpeg.Start()\n}\n\nfunc (p *Producer) Stop() error {\n\tif p.ffmpeg == nil {\n\t\treturn nil\n\t}\n\treturn p.ffmpeg.Stop()\n}\n\nfunc (p *Producer) MarshalJSON() ([]byte, error) {\n\tif p.ffmpeg == nil {\n\t\treturn json.Marshal(p.Connection)\n\t}\n\treturn json.Marshal(p.ffmpeg)\n}\n\nfunc (p *Producer) newURL() string {\n\ts := p.url\n\t// rewrite codecs in url from auto to known presets from defaults\n\tfor _, receiver := range p.Receivers {\n\t\tcodec := receiver.Codec\n\t\tswitch codec.Name {\n\t\tcase core.CodecOpus:\n\t\t\ts += \"#audio=opus/16000\"\n\t\tcase core.CodecAAC:\n\t\t\ts += \"#audio=aac/16000\"\n\t\tcase core.CodecPCML:\n\t\t\ts += \"#audio=pcml/\" + strconv.Itoa(int(codec.ClockRate))\n\t\tcase core.CodecPCM:\n\t\t\ts += \"#audio=pcm/\" + strconv.Itoa(int(codec.ClockRate))\n\t\tcase core.CodecPCMA:\n\t\t\ts += \"#audio=pcma/\" + strconv.Itoa(int(codec.ClockRate))\n\t\tcase core.CodecPCMU:\n\t\t\ts += \"#audio=pcmu/\" + strconv.Itoa(int(codec.ClockRate))\n\t\t}\n\t}\n\t// add other params\n\tfor key, values := range p.query {\n\t\tif key != \"audio\" {\n\t\t\tfor _, value := range values {\n\t\t\t\ts += \"#\" + key + \"=\" + value\n\t\t\t}\n\t\t}\n\t}\n\n\treturn s\n}\n"
  },
  {
    "path": "internal/ffmpeg/version.go",
    "content": "package ffmpeg\n\nimport (\n\t\"errors\"\n\t\"os/exec\"\n\t\"sync\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/ffmpeg\"\n)\n\nvar verMu sync.Mutex\nvar verErr error\nvar verFF string\nvar verAV string\n\nfunc Version() (string, error) {\n\tverMu.Lock()\n\tdefer verMu.Unlock()\n\n\tif verFF != \"\" {\n\t\treturn verFF, verErr\n\t}\n\n\tcmd := exec.Command(defaults[\"bin\"], \"-version\")\n\tb, err := cmd.Output()\n\tif err != nil {\n\t\tverFF = \"-\"\n\t\tverErr = err\n\t\treturn verFF, verErr\n\t}\n\n\tverFF, verAV = ffmpeg.ParseVersion(b)\n\n\tif verFF == \"\" {\n\t\tverFF = \"?\"\n\t}\n\n\t// better to compare libavformat, because nightly/master builds\n\tif verAV != \"\" && verAV < ffmpeg.Version50 {\n\t\tverErr = errors.New(\"ffmpeg: unsupported version: \" + verFF)\n\t}\n\n\tlog.Debug().Str(\"version\", verFF).Str(\"libavformat\", verAV).Msgf(\"[ffmpeg] bin\")\n\n\treturn verFF, verErr\n}\n"
  },
  {
    "path": "internal/ffmpeg/virtual/virtual.go",
    "content": "package virtual\n\nimport (\n\t\"net/url\"\n)\n\nfunc GetInput(src string) string {\n\tquery, err := url.ParseQuery(src)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tinput := \"-re\"\n\n\tfor _, video := range query[\"video\"] {\n\t\t// https://ffmpeg.org/ffmpeg-filters.html\n\t\tsep := \"=\" // first separator\n\n\t\tif video == \"\" {\n\t\t\tvideo = \"testsrc=decimals=2\" // default video\n\t\t\tsep = \":\"\n\t\t}\n\n\t\tinput += \" -f lavfi -i \" + video\n\n\t\t// set defaults (using Add instead of Set)\n\t\tquery.Add(\"size\", \"1920x1080\")\n\n\t\tfor key, values := range query {\n\t\t\tvalue := values[0]\n\n\t\t\t// https://ffmpeg.org/ffmpeg-utils.html#video-size-syntax\n\t\t\tswitch key {\n\t\t\tcase \"color\", \"rate\", \"duration\", \"sar\", \"decimals\":\n\t\t\tcase \"size\":\n\t\t\t\tswitch value {\n\t\t\t\tcase \"720\":\n\t\t\t\t\tvalue = \"1280x720\" // crf=1 -> 12 Mbps\n\t\t\t\tcase \"1080\":\n\t\t\t\t\tvalue = \"1920x1080\" // crf=1 -> 25 Mbps\n\t\t\t\tcase \"2K\":\n\t\t\t\t\tvalue = \"2560x1440\" // crf=1 -> 43 Mbps\n\t\t\t\tcase \"4K\":\n\t\t\t\t\tvalue = \"3840x2160\" // crf=1 -> 103 Mbps\n\t\t\t\tcase \"8K\":\n\t\t\t\t\tvalue = \"7680x4230\" // https://reolink.com/blog/8k-resolution/\n\t\t\t\t}\n\t\t\tdefault:\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tinput += sep + key + \"=\" + value\n\t\t\tsep = \":\" // next separator\n\t\t}\n\n\t\tif s := query.Get(\"format\"); s != \"\" {\n\t\t\tinput += \",format=\" + s\n\t\t}\n\t}\n\n\treturn input\n}\n\nfunc GetInputTTS(src string) string {\n\tquery, err := url.ParseQuery(src)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\n\tinput := `-re -f lavfi -i \"flite=text='` + query.Get(\"text\") + `'`\n\n\t// ffmpeg -f lavfi -i flite=list_voices=1\n\t// awb, kal, kal16, rms, slt\n\tif voice := query.Get(\"voice\"); voice != \"\" {\n\t\tinput += \":voice\" + voice\n\t}\n\n\treturn input + `\"`\n}\n"
  },
  {
    "path": "internal/ffmpeg/virtual/virtual_test.go",
    "content": "package virtual\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGetInput(t *testing.T) {\n\ts := GetInput(\"video\")\n\trequire.Equal(t, \"-re -f lavfi -i testsrc=decimals=2:size=1920x1080\", s)\n\n\ts = GetInput(\"video=testsrc2&size=4K\")\n\trequire.Equal(t, \"-re -f lavfi -i testsrc2=size=3840x2160\", s)\n}\n\nfunc TestGetInputTTS(t *testing.T) {\n\ts := GetInputTTS(\"text=hello world&voice=slt\")\n\trequire.Equal(t, `-re -f lavfi -i \"flite=text='hello world':voiceslt\"`, s)\n}\n"
  },
  {
    "path": "internal/flussonic/README.md",
    "content": "# Flussonic\n\n[`new in v1.9.10`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.9.10)\n\nSupport streams from [Flussonic](https://flussonic.com/) server. Related [issue](https://github.com/AlexxIT/go2rtc/issues/1678).\n"
  },
  {
    "path": "internal/flussonic/flussonic.go",
    "content": "package flussonic\n\nimport (\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/flussonic\"\n)\n\nfunc Init() {\n\tstreams.HandleFunc(\"flussonic\", flussonic.Dial)\n}\n"
  },
  {
    "path": "internal/gopro/README.md",
    "content": "# GoPro\n\n[`new in v1.8.3`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.8.3)\n\nSupport streaming from [GoPro](https://gopro.com/) cameras, connected via USB or Wi-Fi to Linux, Mac, Windows.\n\nSupported models: HERO9, HERO10, HERO11, HERO12.  \nSupported OS: Linux, Mac, Windows, [HassOS](https://www.home-assistant.io/installation/)\n\nOther camera models have different APIs. I will try to add them in future versions.\n\n## Configuration\n\n- USB-connected cameras create a new network interface in the system\n- Linux users do not need to install anything\n- Windows users should install the [network driver](https://community.gopro.com/s/article/GoPro-Webcam)\n- if the camera is detected but the stream does not start, you need to disable the firewall\n\n1. Discover camera address: WebUI > Add > GoPro\n2. Add camera to config\n\n```yaml\nstreams:\n  hero12: gopro://172.20.100.51\n```\n\n## Useful links\n\n- https://gopro.github.io/OpenGoPro/\n"
  },
  {
    "path": "internal/gopro/gopro.go",
    "content": "package gopro\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/gopro\"\n)\n\nfunc Init() {\n\tstreams.HandleFunc(\"gopro\", func(source string) (core.Producer, error) {\n\t\treturn gopro.Dial(source)\n\t})\n\n\tapi.HandleFunc(\"api/gopro\", apiGoPro)\n}\n\nfunc apiGoPro(w http.ResponseWriter, r *http.Request) {\n\tvar items []*api.Source\n\n\tfor _, host := range gopro.Discovery() {\n\t\titems = append(items, &api.Source{Name: host, URL: \"gopro://\" + host})\n\t}\n\n\tapi.ResponseSources(w, items)\n}\n"
  },
  {
    "path": "internal/hass/README.md",
    "content": "# Hass\n\nSupport import camera links from [Home Assistant](https://www.home-assistant.io/) config files:\n\n- [Generic Camera](https://www.home-assistant.io/integrations/generic/), setup via GUI\n- [HomeKit Camera](https://www.home-assistant.io/integrations/homekit_controller/)\n- [ONVIF](https://www.home-assistant.io/integrations/onvif/)\n- [Roborock](https://github.com/humbertogontijo/homeassistant-roborock) vacuums with camera\n\n## Configuration\n\n```yaml\nhass:\n  config: \"/config\"  # skip this setting if you are a Home Assistant add-on user\n\nstreams:\n  generic_camera: hass:Camera1  # Settings > Integrations > Integration Name\n  aqara_g3: hass:Camera-Hub-G3-AB12\n```\n\n### WebRTC Cameras\n\n[`new in v1.6.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.0)\n\nAny cameras in WebRTC format are supported. But at the moment Home Assistant only supports some [Nest](https://www.home-assistant.io/integrations/nest/) cameras in this format.\n\n**Important.** The Nest API only allows you to get a link to a stream for 5 minutes.\nDo not use this with Frigate! If the stream expires, Frigate will consume all available RAM on your machine within seconds.\nIt's recommended to use [Nest source](../nest/README.md) - it supports extending the stream.\n\n```yaml\nstreams:\n  # link to Home Assistant Supervised\n  hass-webrtc1: hass://supervisor?entity_id=camera.nest_doorbell\n  # link to external Home Assistant with Long-Lived Access Tokens\n  hass-webrtc2: hass://192.168.1.123:8123?entity_id=camera.nest_doorbell&token=eyXYZ...\n```\n\n### RTSP Cameras\n\nBy default, the Home Assistant API does not allow you to get a dynamic RTSP link to a camera stream. [This method](https://github.com/felipecrs/hass-expose-camera-stream-source#importing-cameras-from-home-assistant-to-go2rtc-or-frigate) can work around it.\n"
  },
  {
    "path": "internal/hass/api.go",
    "content": "package hass\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"net\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/internal/webrtc\"\n)\n\nfunc apiOK(w http.ResponseWriter, r *http.Request) {\n\tapi.Response(w, `{\"status\":1,\"payload\":{}}`, api.MimeJSON)\n}\n\nfunc apiStream(w http.ResponseWriter, r *http.Request) {\n\tswitch {\n\t// /stream/{id}/add\n\tcase strings.HasSuffix(r.RequestURI, \"/add\"):\n\t\tvar v addJSON\n\t\tif err := json.NewDecoder(r.Body).Decode(&v); err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\t// we can get three types of links:\n\t\t// 1. link to go2rtc stream: rtsp://...:8554/{stream_name}\n\t\t// 2. static link to Hass camera\n\t\t// 3. dynamic link to Hass camera\n\t\tif _, err := streams.Patch(v.Name, v.Channels.First.Url); err == nil {\n\t\t\tapiOK(w, r)\n\t\t} else {\n\t\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\t}\n\n\t// /stream/{id}/channel/0/webrtc\n\tdefault:\n\t\ti := strings.IndexByte(r.RequestURI[8:], '/')\n\t\tif i <= 0 {\n\t\t\thttp.Error(w, \"\", http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\tname := r.RequestURI[8 : 8+i]\n\t\tstream := streams.Get(name)\n\t\tif stream == nil {\n\t\t\thttp.Error(w, api.StreamNotFound, http.StatusNotFound)\n\t\t\treturn\n\t\t}\n\n\t\tif err := r.ParseForm(); err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\ts := r.FormValue(\"data\")\n\t\toffer, err := base64.StdEncoding.DecodeString(s)\n\t\tif err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\ts, err = webrtc.ExchangeSDP(stream, string(offer), \"hass/webrtc\", r.UserAgent())\n\t\tif err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\ts = base64.StdEncoding.EncodeToString([]byte(s))\n\t\t_, _ = w.Write([]byte(s))\n\t}\n}\n\nfunc HassioAddr() string {\n\tints, _ := net.Interfaces()\n\n\tfor _, i := range ints {\n\t\tif i.Name != \"hassio\" {\n\t\t\tcontinue\n\t\t}\n\n\t\taddrs, _ := i.Addrs()\n\t\tfor _, addr := range addrs {\n\t\t\tif addr, ok := addr.(*net.IPNet); ok {\n\t\t\t\treturn addr.IP.String()\n\t\t\t}\n\t\t}\n\t}\n\n\treturn \"\"\n}\n\ntype addJSON struct {\n\tName     string `json:\"name\"`\n\tChannels struct {\n\t\tFirst struct {\n\t\t\t//Name string `json:\"name\"`\n\t\t\tUrl string `json:\"url\"`\n\t\t} `json:\"0\"`\n\t} `json:\"channels\"`\n}\n"
  },
  {
    "path": "internal/hass/hass.go",
    "content": "package hass\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"path\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/internal/app\"\n\t\"github.com/AlexxIT/go2rtc/internal/roborock\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hass\"\n\t\"github.com/rs/zerolog\"\n)\n\nfunc Init() {\n\tvar conf struct {\n\t\tAPI struct {\n\t\t\tListen string `yaml:\"listen\"`\n\t\t} `yaml:\"api\"`\n\t\tMod struct {\n\t\t\tConfig string `yaml:\"config\"`\n\t\t} `yaml:\"hass\"`\n\t}\n\n\tapp.LoadConfig(&conf)\n\n\tlog = app.GetLogger(\"hass\")\n\n\t// support API for https://www.home-assistant.io/integrations/rtsp_to_webrtc/\n\tapi.HandleFunc(\"/static\", apiOK)\n\tapi.HandleFunc(\"/streams\", apiOK)\n\tapi.HandleFunc(\"/stream/\", apiStream)\n\n\tstreams.RedirectFunc(\"hass\", func(rawURL string) (string, error) {\n\t\trawURL, rawQuery, _ := strings.Cut(rawURL, \"#\")\n\n\t\tif location := entities[rawURL[5:]]; location != \"\" {\n\t\t\tif rawQuery != \"\" {\n\t\t\t\treturn location + \"#\" + rawQuery, nil\n\t\t\t}\n\t\t\treturn location, nil\n\t\t}\n\n\t\treturn \"\", nil\n\t})\n\n\tstreams.HandleFunc(\"hass\", func(source string) (core.Producer, error) {\n\t\t// support hass://supervisor?entity_id=camera.driveway_doorbell\n\t\treturn hass.NewClient(source)\n\t})\n\n\t// load static entries from Hass config\n\tif err := importConfig(conf.Mod.Config); err != nil {\n\t\tlog.Trace().Msgf(\"[hass] can't import config: %s\", err)\n\n\t\tapi.HandleFunc(\"api/hass\", func(w http.ResponseWriter, _ *http.Request) {\n\t\t\thttp.Error(w, \"no hass config\", http.StatusNotFound)\n\t\t})\n\t\treturn\n\t}\n\n\tapi.HandleFunc(\"api/hass\", func(w http.ResponseWriter, _ *http.Request) {\n\t\tonce.Do(func() {\n\t\t\t// load WebRTC entities from Hass API, works only for add-on version\n\t\t\tif token := hass.SupervisorToken(); token != \"\" {\n\t\t\t\tif err := importWebRTC(token); err != nil {\n\t\t\t\t\tlog.Warn().Err(err).Caller().Send()\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\n\t\tvar items []*api.Source\n\t\tfor name, url := range entities {\n\t\t\titems = append(items, &api.Source{\n\t\t\t\tName: name, URL: \"hass:\" + name, Location: url,\n\t\t\t})\n\t\t}\n\t\tapi.ResponseSources(w, items)\n\t})\n\n\t// for Addon listen on hassio interface, so WebUI feature will work\n\tif conf.API.Listen == \"127.0.0.1:1984\" {\n\t\tif addr := HassioAddr(); addr != \"\" {\n\t\t\taddr += \":1984\"\n\t\t\tgo func() {\n\t\t\t\tlog.Info().Str(\"addr\", addr).Msg(\"[hass] listen\")\n\t\t\t\tif err := http.ListenAndServe(addr, api.Handler); err != nil {\n\t\t\t\t\tlog.Error().Err(err).Caller().Send()\n\t\t\t\t}\n\t\t\t}()\n\t\t}\n\t}\n}\n\nfunc importConfig(config string) error {\n\t// support load cameras from Hass config file\n\tfilename := path.Join(config, \".storage/core.config_entries\")\n\tb, err := os.ReadFile(filename)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar storage struct {\n\t\tData struct {\n\t\t\tEntries []struct {\n\t\t\t\tTitle   string          `json:\"title\"`\n\t\t\t\tDomain  string          `json:\"domain\"`\n\t\t\t\tData    json.RawMessage `json:\"data\"`\n\t\t\t\tOptions json.RawMessage `json:\"options\"`\n\t\t\t} `json:\"entries\"`\n\t\t} `json:\"data\"`\n\t}\n\n\tif err = json.Unmarshal(b, &storage); err != nil {\n\t\treturn err\n\t}\n\n\tfor _, entrie := range storage.Data.Entries {\n\t\tswitch entrie.Domain {\n\t\tcase \"generic\":\n\t\t\tvar options struct {\n\t\t\t\tStreamSource string `json:\"stream_source\"`\n\t\t\t}\n\t\t\tif err = json.Unmarshal(entrie.Options, &options); err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tentities[entrie.Title] = options.StreamSource\n\n\t\tcase \"homekit_controller\":\n\t\t\tif !bytes.Contains(entrie.Data, []byte(\"iOSPairingId\")) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tvar data struct {\n\t\t\t\tClientID      string `json:\"iOSPairingId\"`\n\t\t\t\tClientPrivate string `json:\"iOSDeviceLTSK\"`\n\t\t\t\tClientPublic  string `json:\"iOSDeviceLTPK\"`\n\t\t\t\tDeviceID      string `json:\"AccessoryPairingID\"`\n\t\t\t\tDevicePublic  string `json:\"AccessoryLTPK\"`\n\t\t\t\tDeviceHost    string `json:\"AccessoryIP\"`\n\t\t\t\tDevicePort    uint16 `json:\"AccessoryPort\"`\n\t\t\t}\n\t\t\tif err = json.Unmarshal(entrie.Data, &data); err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tentities[entrie.Title] = fmt.Sprintf(\n\t\t\t\t\"homekit://%s:%d?client_id=%s&client_private=%s%s&device_id=%s&device_public=%s\",\n\t\t\t\tdata.DeviceHost, data.DevicePort,\n\t\t\t\tdata.ClientID, data.ClientPrivate, data.ClientPublic,\n\t\t\t\tdata.DeviceID, data.DevicePublic,\n\t\t\t)\n\n\t\tcase \"roborock\":\n\t\t\t_ = json.Unmarshal(entrie.Data, &roborock.Auth)\n\n\t\tcase \"onvif\":\n\t\t\tvar data struct {\n\t\t\t\tHost     string `json:\"host\" json:\"host\"`\n\t\t\t\tPort     uint16 `json:\"port\" json:\"port\"`\n\t\t\t\tUsername string `json:\"username\" json:\"username\"`\n\t\t\t\tPassword string `json:\"password\" json:\"password\"`\n\t\t\t}\n\t\t\tif err = json.Unmarshal(entrie.Data, &data); err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif data.Username != \"\" && data.Password != \"\" {\n\t\t\t\tentities[entrie.Title] = fmt.Sprintf(\n\t\t\t\t\t\"onvif://%s:%s@%s:%d\", data.Username, data.Password, data.Host, data.Port,\n\t\t\t\t)\n\t\t\t} else {\n\t\t\t\tentities[entrie.Title] = fmt.Sprintf(\"onvif://%s:%d\", data.Host, data.Port)\n\t\t\t}\n\n\t\tdefault:\n\t\t\tcontinue\n\t\t}\n\n\t\tlog.Debug().Str(\"url\", \"hass:\"+entrie.Title).Msg(\"[hass] load config\")\n\t\t//streams.Get(\"hass:\" + entrie.Title)\n\t}\n\n\treturn nil\n}\n\nfunc importWebRTC(token string) error {\n\thassAPI, err := hass.NewAPI(\"ws://supervisor/core/websocket\", token)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\twebrtcEntities, err := hassAPI.GetWebRTCEntities()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif len(webrtcEntities) == 0 {\n\t\tlog.Debug().Msg(\"[hass] webrtc cameras not found\")\n\t}\n\n\tfor name, entityID := range webrtcEntities {\n\t\tentities[name] = \"hass://supervisor?entity_id=\" + entityID\n\n\t\tlog.Debug().Msgf(\"[hass] load webrtc name=%s entity_id=%s\", name, entityID)\n\t}\n\n\treturn nil\n}\n\nvar entities = map[string]string{}\nvar log zerolog.Logger\nvar once sync.Once\n"
  },
  {
    "path": "internal/hls/README.md",
    "content": "# HLS\n\n[`new in v1.1.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.1.0)\n\n[HLS](https://en.wikipedia.org/wiki/HTTP_Live_Streaming) is the worst technology for real-time streaming. \nIt can only be useful on devices that do not support more modern technology, like [WebRTC](../webrtc/README.md), [MP4](../mp4/README.md).\n\nThe go2rtc implementation differs from the standards and may not work with all players.\n\nAPI examples:\n\n- HLS/TS stream: `http://192.168.1.123:1984/api/stream.m3u8?src=camera1` (H264)\n- HLS/fMP4 stream: `http://192.168.1.123:1984/api/stream.m3u8?src=camera1&mp4` (H264, H265, AAC)\n\nRead more about [codecs filters](../../README.md#codecs-filters).\n\n## Useful links\n\n- https://walterebert.com/playground/video/hls/\n"
  },
  {
    "path": "internal/hls/hls.go",
    "content": "package hls\n\nimport (\n\t\"net/http\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/internal/api/ws\"\n\t\"github.com/AlexxIT/go2rtc/internal/app\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/mp4\"\n\t\"github.com/AlexxIT/go2rtc/pkg/mpegts\"\n\t\"github.com/rs/zerolog\"\n)\n\nfunc Init() {\n\tlog = app.GetLogger(\"hls\")\n\n\tapi.HandleFunc(\"api/stream.m3u8\", handlerStream)\n\tapi.HandleFunc(\"api/hls/playlist.m3u8\", handlerPlaylist)\n\n\t// HLS (TS)\n\tapi.HandleFunc(\"api/hls/segment.ts\", handlerSegmentTS)\n\n\t// HLS (fMP4)\n\tapi.HandleFunc(\"api/hls/init.mp4\", handlerInit)\n\tapi.HandleFunc(\"api/hls/segment.m4s\", handlerSegmentMP4)\n\n\tws.HandleFunc(\"hls\", handlerWSHLS)\n}\n\nvar log zerolog.Logger\n\nconst keepalive = 5 * time.Second\n\n// once I saw 404 on MP4 segment, so better to use mutex\nvar sessions = map[string]*Session{}\nvar sessionsMu sync.RWMutex\n\nfunc handlerStream(w http.ResponseWriter, r *http.Request) {\n\t// CORS important for Chromecast\n\tw.Header().Set(\"Access-Control-Allow-Origin\", \"*\")\n\tw.Header().Set(\"Content-Type\", \"application/vnd.apple.mpegurl\")\n\n\tif r.Method == \"OPTIONS\" {\n\t\tw.Header().Set(\"Access-Control-Allow-Methods\", \"GET\")\n\t\treturn\n\t}\n\n\tsrc := r.URL.Query().Get(\"src\")\n\tstream := streams.Get(src)\n\tif stream == nil {\n\t\thttp.Error(w, api.StreamNotFound, http.StatusNotFound)\n\t\treturn\n\t}\n\n\tvar cons core.Consumer\n\n\t// use fMP4 with codecs filter and TS without\n\tmedias := mp4.ParseQuery(r.URL.Query())\n\tif medias != nil {\n\t\tc := mp4.NewConsumer(medias)\n\t\tc.FormatName = \"hls/fmp4\"\n\t\tc.WithRequest(r)\n\t\tcons = c\n\t} else {\n\t\tc := mpegts.NewConsumer()\n\t\tc.FormatName = \"hls/mpegts\"\n\t\tc.WithRequest(r)\n\t\tcons = c\n\t}\n\n\tif err := stream.AddConsumer(cons); err != nil {\n\t\tlog.Error().Err(err).Caller().Send()\n\t\treturn\n\t}\n\n\tsession := NewSession(cons)\n\tsession.alive = time.AfterFunc(keepalive, func() {\n\t\tsessionsMu.Lock()\n\t\tdelete(sessions, session.id)\n\t\tsessionsMu.Unlock()\n\n\t\tstream.RemoveConsumer(cons)\n\t})\n\n\tsessionsMu.Lock()\n\tsessions[session.id] = session\n\tsessionsMu.Unlock()\n\n\tgo session.Run()\n\n\tif _, err := w.Write(session.Main()); err != nil {\n\t\tlog.Error().Err(err).Caller().Send()\n\t}\n}\n\nfunc handlerPlaylist(w http.ResponseWriter, r *http.Request) {\n\tw.Header().Set(\"Access-Control-Allow-Origin\", \"*\")\n\tw.Header().Set(\"Content-Type\", \"application/vnd.apple.mpegurl\")\n\n\tif r.Method == \"OPTIONS\" {\n\t\tw.Header().Set(\"Access-Control-Allow-Methods\", \"GET\")\n\t\treturn\n\t}\n\n\tsid := r.URL.Query().Get(\"id\")\n\tsessionsMu.RLock()\n\tsession := sessions[sid]\n\tsessionsMu.RUnlock()\n\tif session == nil {\n\t\thttp.NotFound(w, r)\n\t\treturn\n\t}\n\n\tif _, err := w.Write(session.Playlist()); err != nil {\n\t\tlog.Error().Err(err).Caller().Send()\n\t}\n}\n\nfunc handlerSegmentTS(w http.ResponseWriter, r *http.Request) {\n\tw.Header().Set(\"Access-Control-Allow-Origin\", \"*\")\n\tw.Header().Set(\"Content-Type\", \"video/mp2t\")\n\n\tif r.Method == \"OPTIONS\" {\n\t\tw.Header().Set(\"Access-Control-Allow-Methods\", \"GET\")\n\t\treturn\n\t}\n\n\tsid := r.URL.Query().Get(\"id\")\n\tsessionsMu.RLock()\n\tsession := sessions[sid]\n\tsessionsMu.RUnlock()\n\tif session == nil {\n\t\thttp.NotFound(w, r)\n\t\treturn\n\t}\n\n\tsession.alive.Reset(keepalive)\n\n\tdata := session.Segment()\n\tif data == nil {\n\t\tlog.Warn().Msgf(\"[hls] can't get segment %s\", r.URL.RawQuery)\n\t\thttp.NotFound(w, r)\n\t\treturn\n\t}\n\n\tif _, err := w.Write(data); err != nil {\n\t\tlog.Error().Err(err).Caller().Send()\n\t}\n}\n\nfunc handlerInit(w http.ResponseWriter, r *http.Request) {\n\tw.Header().Set(\"Access-Control-Allow-Origin\", \"*\")\n\tw.Header().Add(\"Content-Type\", \"video/mp4\")\n\n\tif r.Method == \"OPTIONS\" {\n\t\tw.Header().Set(\"Access-Control-Allow-Methods\", \"GET\")\n\t\treturn\n\t}\n\n\tsid := r.URL.Query().Get(\"id\")\n\tsessionsMu.RLock()\n\tsession := sessions[sid]\n\tsessionsMu.RUnlock()\n\tif session == nil {\n\t\thttp.NotFound(w, r)\n\t\treturn\n\t}\n\n\tdata := session.Init()\n\tif data == nil {\n\t\tlog.Warn().Msgf(\"[hls] can't get init %s\", r.URL.RawQuery)\n\t\thttp.NotFound(w, r)\n\t\treturn\n\t}\n\n\tif _, err := w.Write(data); err != nil {\n\t\tlog.Error().Err(err).Caller().Send()\n\t}\n}\n\nfunc handlerSegmentMP4(w http.ResponseWriter, r *http.Request) {\n\tw.Header().Set(\"Access-Control-Allow-Origin\", \"*\")\n\tw.Header().Add(\"Content-Type\", \"video/iso.segment\")\n\n\tif r.Method == \"OPTIONS\" {\n\t\tw.Header().Set(\"Access-Control-Allow-Methods\", \"GET\")\n\t\treturn\n\t}\n\n\tquery := r.URL.Query()\n\n\tsid := query.Get(\"id\")\n\tsessionsMu.RLock()\n\tsession := sessions[sid]\n\tsessionsMu.RUnlock()\n\tif session == nil {\n\t\thttp.NotFound(w, r)\n\t\treturn\n\t}\n\n\tsession.alive.Reset(keepalive)\n\n\tdata := session.Segment()\n\tif data == nil {\n\t\tlog.Warn().Msgf(\"[hls] can't get segment %s\", r.URL.RawQuery)\n\t\thttp.NotFound(w, r)\n\t\treturn\n\t}\n\n\tif _, err := w.Write(data); err != nil {\n\t\tlog.Error().Err(err).Caller().Send()\n\t}\n}\n"
  },
  {
    "path": "internal/hls/session.go",
    "content": "package hls\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/mp4\"\n)\n\ntype Session struct {\n\tcons     core.Consumer\n\tid       string\n\ttemplate string\n\tinit     []byte\n\tbuffer   []byte\n\tseq      int\n\talive    *time.Timer\n\tmu       sync.Mutex\n}\n\nfunc NewSession(cons core.Consumer) *Session {\n\ts := &Session{\n\t\tid:   core.RandString(8, 62),\n\t\tcons: cons,\n\t}\n\n\t// two segments important for Chromecast\n\tif _, ok := cons.(*mp4.Consumer); ok {\n\t\ts.template = `#EXTM3U\n#EXT-X-VERSION:6\n#EXT-X-TARGETDURATION:1\n#EXT-X-MEDIA-SEQUENCE:%d\n#EXT-X-MAP:URI=\"init.mp4?id=` + s.id + `\"\n#EXTINF:0.500,\nsegment.m4s?id=` + s.id + `&n=%d\n#EXTINF:0.500,\nsegment.m4s?id=` + s.id + `&n=%d`\n\t} else {\n\t\ts.template = `#EXTM3U\n#EXT-X-VERSION:3\n#EXT-X-TARGETDURATION:1\n#EXT-X-MEDIA-SEQUENCE:%d\n#EXTINF:0.500,\nsegment.ts?id=` + s.id + `&n=%d\n#EXTINF:0.500,\nsegment.ts?id=` + s.id + `&n=%d`\n\t}\n\n\treturn s\n}\n\nfunc (s *Session) Write(p []byte) (n int, err error) {\n\ts.mu.Lock()\n\tif s.init == nil {\n\t\ts.init = p\n\t} else {\n\t\ts.buffer = append(s.buffer, p...)\n\t}\n\ts.mu.Unlock()\n\treturn len(p), nil\n}\n\nfunc (s *Session) Run() {\n\t_, _ = s.cons.(io.WriterTo).WriteTo(s)\n}\n\nfunc (s *Session) Main() []byte {\n\ttype withCodecs interface {\n\t\tCodecs() []*core.Codec\n\t}\n\n\tcodecs := mp4.MimeCodecs(s.cons.(withCodecs).Codecs())\n\tcodecs = strings.Replace(codecs, mp4.MimeFlac, \"fLaC\", 1)\n\n\t// bandwidth important for Safari, codecs useful for smooth playback\n\treturn []byte(`#EXTM3U\n#EXT-X-STREAM-INF:BANDWIDTH=192000,CODECS=\"` + codecs + `\"\nhls/playlist.m3u8?id=` + s.id)\n}\n\nfunc (s *Session) Playlist() []byte {\n\treturn []byte(fmt.Sprintf(s.template, s.seq, s.seq, s.seq+1))\n}\n\nfunc (s *Session) Init() (init []byte) {\n\tfor i := 0; i < 60 && init == nil; i++ {\n\t\tif i > 0 {\n\t\t\ttime.Sleep(50 * time.Millisecond)\n\t\t}\n\n\t\ts.mu.Lock()\n\t\t// return init only when have some buffer\n\t\tif len(s.buffer) > 0 {\n\t\t\tinit = s.init\n\t\t}\n\t\ts.mu.Unlock()\n\t}\n\n\treturn\n}\n\nfunc (s *Session) Segment() (segment []byte) {\n\tfor i := 0; i < 60 && segment == nil; i++ {\n\t\tif i > 0 {\n\t\t\ttime.Sleep(50 * time.Millisecond)\n\t\t}\n\n\t\ts.mu.Lock()\n\t\tif len(s.buffer) > 0 {\n\t\t\tsegment = s.buffer\n\t\t\tif _, ok := s.cons.(*mp4.Consumer); ok {\n\t\t\t\ts.buffer = nil\n\t\t\t} else {\n\t\t\t\t// for TS important to start new segment with init\n\t\t\t\ts.buffer = s.init\n\t\t\t}\n\t\t\ts.seq++\n\t\t}\n\t\ts.mu.Unlock()\n\t}\n\n\treturn\n}\n"
  },
  {
    "path": "internal/hls/ws.go",
    "content": "package hls\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/internal/api/ws\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/mp4\"\n)\n\nfunc handlerWSHLS(tr *ws.Transport, msg *ws.Message) error {\n\tstream, _ := streams.GetOrPatch(tr.Request.URL.Query())\n\tif stream == nil {\n\t\treturn errors.New(api.StreamNotFound)\n\t}\n\n\tcodecs := msg.String()\n\tmedias := mp4.ParseCodecs(codecs, true)\n\tcons := mp4.NewConsumer(medias)\n\tcons.FormatName = \"hls/fmp4\"\n\tcons.WithRequest(tr.Request)\n\n\tlog.Trace().Msgf(\"[hls] new ws consumer codecs=%s\", codecs)\n\n\tif err := stream.AddConsumer(cons); err != nil {\n\t\tlog.Error().Err(err).Caller().Send()\n\t\treturn err\n\t}\n\n\tsession := NewSession(cons)\n\n\tsession.alive = time.AfterFunc(keepalive, func() {\n\t\tsessionsMu.Lock()\n\t\tdelete(sessions, session.id)\n\t\tsessionsMu.Unlock()\n\n\t\tstream.RemoveConsumer(cons)\n\t})\n\n\tsessionsMu.Lock()\n\tsessions[session.id] = session\n\tsessionsMu.Unlock()\n\n\tgo session.Run()\n\n\tmain := session.Main()\n\ttr.Write(&ws.Message{Type: \"hls\", Value: string(main)})\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/homekit/README.md",
    "content": "# Apple HomeKit\n\nThis module supports both client and server for the [Apple HomeKit](https://www.apple.com/home-app/accessories/) protocol.\n\n## HomeKit Client\n\n**Important:**\n\n- You can use HomeKit Cameras **without Apple devices** (iPhone, iPad, etc.), it's just a yet another protocol\n- HomeKit device can be paired with only one ecosystem. So, if you have paired it to an iPhone (Apple Home), you can't pair it with Home Assistant or go2rtc. Or if you have paired it to go2rtc, you can't pair it with an iPhone\n- HomeKit device should be on the same network with working [mDNS](https://en.wikipedia.org/wiki/Multicast_DNS) between the device and go2rtc\n\ngo2rtc supports importing paired HomeKit devices from [Home Assistant](../hass/README.md). \nSo you can use HomeKit camera with Home Assistant and go2rtc simultaneously. \nIf you are using Home Assistant, I recommend pairing devices with it; it will give you more options.\n\nYou can pair device with go2rtc on the HomeKit page. If you can't see your devices, reload the page. \nAlso, try rebooting your HomeKit device (power off). If you still can't see it, you have a problem with mDNS.\n\nIf you see a device but it does not have a pairing button, it is paired to some ecosystem (Apple Home, Home Assistant, HomeBridge, etc.). You need to delete the device from that ecosystem, and it will be available for pairing. If you cannot unpair the device, you will have to reset it.\n\n**Important:**\n\n- HomeKit audio uses very non-standard **AAC-ELD** codec with very non-standard params and specification violations\n- Audio can't be played in `VLC` and probably any other player\n- Audio should be transcoded for use with MSE, WebRTC, etc.\n\n### Client Configuration\n\nRecommended settings for using HomeKit Camera with WebRTC, MSE, MP4, RTSP:\n\n```yaml\nstreams:\n  aqara_g3:\n    - hass:Camera-Hub-G3-AB12\n    - ffmpeg:aqara_g3#audio=aac#audio=opus\n```\n\nRTSP link with \"normal\" audio for any player: `rtsp://192.168.1.123:8554/aqara_g3?video&audio=aac`\n\n**This source is in active development!** Tested only with [Aqara Camera Hub G3](https://www.aqara.com/eu/product/camera-hub-g3) (both EU and CN versions).\n\n## HomeKit Server\n\n[`new in v1.7.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.7.0)\n\nHomeKit module can work in two modes:\n\n- export any H264 camera to Apple HomeKit\n- transparent proxy any Apple HomeKit camera (Aqara, Eve, Eufy, etc.) back to Apple HomeKit, so you will have all camera features in Apple Home and also will have RTSP/WebRTC/MP4/etc. from your HomeKit camera\n\n**Important**\n\n- HomeKit cameras support only H264 video and OPUS audio\n\n### Server Configuration\n\n**Minimal config**\n\n```yaml\nstreams:\n  dahua1: rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0\nhomekit:\n  dahua1:  # same stream ID from streams list, default PIN - 19550224\n```\n\n**Full config**\n\n```yaml\nstreams:\n  dahua1:\n    - rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0\n    - ffmpeg:dahua1#video=h264#hardware  # if your camera doesn't support H264, important for HomeKit\n    - ffmpeg:dahua1#audio=opus           # only OPUS audio supported by HomeKit\n\nhomekit:\n  dahua1:                   # same stream ID from streams list\n    pin: 12345678           # custom PIN, default: 19550224\n    name: Dahua camera      # custom camera name, default: generated from stream ID\n    device_id: dahua1       # custom ID, default: generated from stream ID\n    device_private: dahua1  # custom key, default: generated from stream ID\n```\n\n**Proxy HomeKit camera**\n\n- Video stream from HomeKit camera to Apple device (iPhone, Apple TV) will be transmitted directly\n- Video stream from HomeKit camera to RTSP/WebRTC/MP4/etc. will be transmitted via go2rtc\n\n```yaml\nstreams:\n  aqara1:\n    - homekit://...\n    - ffmpeg:aqara1#audio=aac#audio=opus  # optional audio transcoding\n\nhomekit:\n  aqara1:  # same stream ID from streams list\n```\n"
  },
  {
    "path": "internal/homekit/api.go",
    "content": "package homekit\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/internal/app\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hap\"\n\t\"github.com/AlexxIT/go2rtc/pkg/mdns\"\n)\n\nfunc apiDiscovery(w http.ResponseWriter, r *http.Request) {\n\tsources, err := discovery()\n\tif err != nil {\n\t\tapi.Error(w, err)\n\t\treturn\n\t}\n\n\turls := findHomeKitURLs()\n\tfor id, u := range urls {\n\t\tdeviceID := u.Query().Get(\"device_id\")\n\t\tfor _, source := range sources {\n\t\t\tif strings.Contains(source.URL, deviceID) {\n\t\t\t\tsource.Location = id\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tfor _, source := range sources {\n\t\tif source.Location == \"\" {\n\t\t\tsource.Location = \" \"\n\t\t}\n\t}\n\n\tapi.ResponseSources(w, sources)\n}\n\nfunc apiHomekit(w http.ResponseWriter, r *http.Request) {\n\tif err := r.ParseForm(); err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tswitch r.Method {\n\tcase \"GET\":\n\t\tif id := r.Form.Get(\"id\"); id != \"\" {\n\t\t\tif srv := servers[id]; srv != nil {\n\t\t\t\tapi.ResponsePrettyJSON(w, srv)\n\t\t\t} else {\n\t\t\t\thttp.Error(w, \"server not found\", http.StatusNotFound)\n\t\t\t}\n\t\t} else {\n\t\t\tapi.ResponsePrettyJSON(w, servers)\n\t\t}\n\n\tcase \"POST\":\n\t\tid := r.Form.Get(\"id\")\n\t\trawURL := r.Form.Get(\"src\") + \"&pin=\" + r.Form.Get(\"pin\")\n\t\tif err := apiPair(id, rawURL); err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t}\n\n\tcase \"DELETE\":\n\t\tid := r.Form.Get(\"id\")\n\t\tif err := apiUnpair(id); err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t}\n\t}\n}\n\nfunc apiHomekitAccessories(w http.ResponseWriter, r *http.Request) {\n\tid := r.URL.Query().Get(\"id\")\n\tstream := streams.Get(id)\n\tif stream == nil {\n\t\thttp.Error(w, \"\", http.StatusNotFound)\n\t\treturn\n\t}\n\n\trawURL := findHomeKitURL(stream.Sources())\n\tif rawURL == \"\" {\n\t\thttp.Error(w, \"\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tclient, err := hap.Dial(rawURL)\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\tdefer client.Close()\n\n\tres, err := client.Get(hap.PathAccessories)\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tw.Header().Set(\"Content-Type\", api.MimeJSON)\n\t_, _ = io.Copy(w, res.Body)\n}\n\nfunc discovery() ([]*api.Source, error) {\n\tvar sources []*api.Source\n\n\t// 1. Get streams from Discovery\n\terr := mdns.Discovery(mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool {\n\t\tlog.Trace().Msgf(\"[homekit] mdns=%s\", entry)\n\n\t\tcategory := entry.Info[hap.TXTCategory]\n\t\tif entry.Complete() && (category == hap.CategoryCamera || category == hap.CategoryDoorbell) {\n\t\t\tsource := &api.Source{\n\t\t\t\tName: entry.Name,\n\t\t\t\tInfo: entry.Info[hap.TXTModel],\n\t\t\t\tURL: fmt.Sprintf(\n\t\t\t\t\t\"homekit://%s:%d?device_id=%s&feature=%s&status=%s\",\n\t\t\t\t\tentry.IP, entry.Port, entry.Info[hap.TXTDeviceID],\n\t\t\t\t\tentry.Info[hap.TXTFeatureFlags], entry.Info[hap.TXTStatusFlags],\n\t\t\t\t),\n\t\t\t}\n\n\t\t\tsources = append(sources, source)\n\t\t}\n\t\treturn false\n\t})\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn sources, nil\n}\n\nfunc apiPair(id, url string) error {\n\tconn, err := hap.Pair(url)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tstreams.New(id, conn.URL())\n\n\treturn app.PatchConfig([]string{\"streams\", id}, conn.URL())\n}\n\nfunc apiUnpair(id string) error {\n\tstream := streams.Get(id)\n\tif stream == nil {\n\t\treturn errors.New(api.StreamNotFound)\n\t}\n\n\trawURL := findHomeKitURL(stream.Sources())\n\tif rawURL == \"\" {\n\t\treturn errors.New(\"not homekit source\")\n\t}\n\n\tif err := hap.Unpair(rawURL); err != nil {\n\t\treturn err\n\t}\n\n\tstreams.Delete(id)\n\n\treturn app.PatchConfig([]string{\"streams\", id}, nil)\n}\n\nfunc findHomeKitURLs() map[string]*url.URL {\n\turls := map[string]*url.URL{}\n\tfor name, sources := range streams.GetAllSources() {\n\t\tif rawURL := findHomeKitURL(sources); rawURL != \"\" {\n\t\t\tif u, err := url.Parse(rawURL); err == nil {\n\t\t\t\turls[name] = u\n\t\t\t}\n\t\t}\n\t}\n\treturn urls\n}\n"
  },
  {
    "path": "internal/homekit/homekit.go",
    "content": "package homekit\n\nimport (\n\t\"errors\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/internal/app\"\n\t\"github.com/AlexxIT/go2rtc/internal/srtp\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hap\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hap/camera\"\n\t\"github.com/AlexxIT/go2rtc/pkg/homekit\"\n\t\"github.com/AlexxIT/go2rtc/pkg/mdns\"\n\t\"github.com/rs/zerolog\"\n)\n\nfunc Init() {\n\tvar cfg struct {\n\t\tMod map[string]struct {\n\t\t\tPin           string   `yaml:\"pin\"`\n\t\t\tName          string   `yaml:\"name\"`\n\t\t\tDeviceID      string   `yaml:\"device_id\"`\n\t\t\tDevicePrivate string   `yaml:\"device_private\"`\n\t\t\tCategoryID    string   `yaml:\"category_id\"`\n\t\t\tPairings      []string `yaml:\"pairings\"`\n\t\t} `yaml:\"homekit\"`\n\t}\n\tapp.LoadConfig(&cfg)\n\n\tlog = app.GetLogger(\"homekit\")\n\n\tstreams.HandleFunc(\"homekit\", streamHandler)\n\n\tapi.HandleFunc(\"api/homekit\", apiHomekit)\n\tapi.HandleFunc(\"api/homekit/accessories\", apiHomekitAccessories)\n\tapi.HandleFunc(\"api/discovery/homekit\", apiDiscovery)\n\n\tif cfg.Mod == nil {\n\t\treturn\n\t}\n\n\thosts = map[string]*server{}\n\tservers = map[string]*server{}\n\tvar entries []*mdns.ServiceEntry\n\n\tfor id, conf := range cfg.Mod {\n\t\tstream := streams.Get(id)\n\t\tif stream == nil {\n\t\t\tlog.Warn().Msgf(\"[homekit] missing stream: %s\", id)\n\t\t\tcontinue\n\t\t}\n\n\t\tif conf.Pin == \"\" {\n\t\t\tconf.Pin = \"19550224\" // default PIN\n\t\t}\n\n\t\tpin, err := hap.SanitizePin(conf.Pin)\n\t\tif err != nil {\n\t\t\tlog.Error().Err(err).Caller().Send()\n\t\t\tcontinue\n\t\t}\n\n\t\tdeviceID := calcDeviceID(conf.DeviceID, id) // random MAC-address\n\t\tname := calcName(conf.Name, deviceID)\n\t\tsetupID := calcSetupID(id)\n\n\t\tsrv := &server{\n\t\t\tstream:   id,\n\t\t\tpairings: conf.Pairings,\n\t\t\tsetupID:  setupID,\n\t\t}\n\n\t\tsrv.hap = &hap.Server{\n\t\t\tPin:             pin,\n\t\t\tDeviceID:        deviceID,\n\t\t\tDevicePrivate:   calcDevicePrivate(conf.DevicePrivate, id),\n\t\t\tGetClientPublic: srv.GetPair,\n\t\t}\n\n\t\tsrv.mdns = &mdns.ServiceEntry{\n\t\t\tName: name,\n\t\t\tPort: uint16(api.Port),\n\t\t\tInfo: map[string]string{\n\t\t\t\thap.TXTConfigNumber: \"1\",\n\t\t\t\thap.TXTFeatureFlags: \"0\",\n\t\t\t\thap.TXTDeviceID:     deviceID,\n\t\t\t\thap.TXTModel:        app.UserAgent,\n\t\t\t\thap.TXTProtoVersion: \"1.1\",\n\t\t\t\thap.TXTStateNumber:  \"1\",\n\t\t\t\thap.TXTStatusFlags:  hap.StatusNotPaired,\n\t\t\t\thap.TXTCategory:     calcCategoryID(conf.CategoryID),\n\t\t\t\thap.TXTSetupHash:    hap.SetupHash(setupID, deviceID),\n\t\t\t},\n\t\t}\n\t\tentries = append(entries, srv.mdns)\n\n\t\tsrv.UpdateStatus()\n\n\t\tif url := findHomeKitURL(stream.Sources()); url != \"\" {\n\t\t\t// 1. Act as transparent proxy for HomeKit camera\n\t\t\tsrv.proxyURL = url\n\t\t} else {\n\t\t\t// 2. Act as basic HomeKit camera\n\t\t\tsrv.accessory = camera.NewAccessory(\"AlexxIT\", \"go2rtc\", name, \"-\", app.Version)\n\t\t}\n\n\t\thost := srv.mdns.Host(mdns.ServiceHAP)\n\t\thosts[host] = srv\n\t\tservers[id] = srv\n\n\t\tlog.Trace().Msgf(\"[homekit] new server: %s\", srv.mdns)\n\t}\n\n\tapi.HandleFunc(hap.PathPairSetup, hapHandler)\n\tapi.HandleFunc(hap.PathPairVerify, hapHandler)\n\n\tgo func() {\n\t\tif err := mdns.Serve(mdns.ServiceHAP, entries); err != nil {\n\t\t\tlog.Error().Err(err).Caller().Send()\n\t\t}\n\t}()\n}\n\nvar log zerolog.Logger\nvar hosts map[string]*server\nvar servers map[string]*server\n\nfunc streamHandler(rawURL string) (core.Producer, error) {\n\tif srtp.Server == nil {\n\t\treturn nil, errors.New(\"homekit: can't work without SRTP server\")\n\t}\n\n\trawURL, rawQuery, _ := strings.Cut(rawURL, \"#\")\n\tclient, err := homekit.Dial(rawURL, srtp.Server)\n\tif client != nil && rawQuery != \"\" {\n\t\tquery := streams.ParseQuery(rawQuery)\n\t\tclient.MaxWidth = core.Atoi(query.Get(\"maxwidth\"))\n\t\tclient.MaxHeight = core.Atoi(query.Get(\"maxheight\"))\n\t\tclient.Bitrate = parseBitrate(query.Get(\"bitrate\"))\n\t}\n\n\treturn client, err\n}\n\nfunc resolve(host string) *server {\n\tif len(hosts) == 1 {\n\t\tfor _, srv := range hosts {\n\t\t\treturn srv\n\t\t}\n\t}\n\tif srv, ok := hosts[host]; ok {\n\t\treturn srv\n\t}\n\treturn nil\n}\n\nfunc hapHandler(w http.ResponseWriter, r *http.Request) {\n\t// Can support multiple HomeKit cameras on single port ONLY for Apple devices.\n\t// Doesn't support Home Assistant and any other open source projects\n\t// because they don't send the host header in requests.\n\tsrv := resolve(r.Host)\n\tif srv == nil {\n\t\tlog.Error().Msg(\"[homekit] unknown host: \" + r.Host)\n\t\treturn\n\t}\n\tsrv.Handle(w, r)\n}\n\nfunc findHomeKitURL(sources []string) string {\n\tif len(sources) == 0 {\n\t\treturn \"\"\n\t}\n\n\turl := sources[0]\n\tif strings.HasPrefix(url, \"homekit\") {\n\t\treturn url\n\t}\n\n\tif strings.HasPrefix(url, \"hass\") {\n\t\tlocation, _ := streams.Location(url)\n\t\tif strings.HasPrefix(location, \"homekit\") {\n\t\t\treturn location\n\t\t}\n\t}\n\n\treturn \"\"\n}\n\nfunc parseBitrate(s string) int {\n\tn := len(s)\n\tif n == 0 {\n\t\treturn 0\n\t}\n\n\tvar k int\n\tswitch n--; s[n] {\n\tcase 'K':\n\t\tk = 1024\n\t\ts = s[:n]\n\tcase 'M':\n\t\tk = 1024 * 1024\n\t\ts = s[:n]\n\tdefault:\n\t\tk = 1\n\t}\n\n\treturn k * core.Atoi(s)\n}\n"
  },
  {
    "path": "internal/homekit/server.go",
    "content": "package homekit\n\nimport (\n\t\"crypto/ed25519\"\n\t\"crypto/sha512\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/app\"\n\t\"github.com/AlexxIT/go2rtc/internal/ffmpeg\"\n\tsrtp2 \"github.com/AlexxIT/go2rtc/internal/srtp\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hap\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hap/camera\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hap/hds\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hap/tlv8\"\n\t\"github.com/AlexxIT/go2rtc/pkg/homekit\"\n\t\"github.com/AlexxIT/go2rtc/pkg/magic\"\n\t\"github.com/AlexxIT/go2rtc/pkg/mdns\"\n)\n\ntype server struct {\n\thap  *hap.Server // server for HAP connection and encryption\n\tmdns *mdns.ServiceEntry\n\n\tpairings []string // pairings list\n\tconns    []any\n\tmu       sync.Mutex\n\n\taccessory *hap.Accessory // HAP accessory\n\tconsumer  *homekit.Consumer\n\tproxyURL  string\n\tsetupID   string\n\tstream    string // stream name from YAML\n}\n\nfunc (s *server) MarshalJSON() ([]byte, error) {\n\tv := struct {\n\t\tName       string `json:\"name\"`\n\t\tDeviceID   string `json:\"device_id\"`\n\t\tPaired     int    `json:\"paired,omitempty\"`\n\t\tCategoryID string `json:\"category_id,omitempty\"`\n\t\tSetupCode  string `json:\"setup_code,omitempty\"`\n\t\tSetupID    string `json:\"setup_id,omitempty\"`\n\t\tConns      []any  `json:\"connections,omitempty\"`\n\t}{\n\t\tName:       s.mdns.Name,\n\t\tDeviceID:   s.mdns.Info[hap.TXTDeviceID],\n\t\tCategoryID: s.mdns.Info[hap.TXTCategory],\n\t\tPaired:     len(s.pairings),\n\t\tConns:      s.conns,\n\t}\n\tif v.Paired == 0 {\n\t\tv.SetupCode = s.hap.Pin\n\t\tv.SetupID = s.setupID\n\t}\n\treturn json.Marshal(v)\n}\n\nfunc (s *server) Handle(w http.ResponseWriter, r *http.Request) {\n\tconn, rw, err := w.(http.Hijacker).Hijack()\n\tif err != nil {\n\t\treturn\n\t}\n\n\tdefer conn.Close()\n\n\t// Fix reading from Body after Hijack.\n\tr.Body = io.NopCloser(rw)\n\n\tswitch r.RequestURI {\n\tcase hap.PathPairSetup:\n\t\tid, key, err := s.hap.PairSetup(r, rw)\n\t\tif err != nil {\n\t\t\tlog.Error().Err(err).Caller().Send()\n\t\t\treturn\n\t\t}\n\n\t\ts.AddPair(id, key, hap.PermissionAdmin)\n\n\tcase hap.PathPairVerify:\n\t\tid, key, err := s.hap.PairVerify(r, rw)\n\t\tif err != nil {\n\t\t\tlog.Debug().Err(err).Caller().Send()\n\t\t\treturn\n\t\t}\n\n\t\tlog.Debug().Str(\"stream\", s.stream).Str(\"client_id\", id).Msgf(\"[homekit] %s: new conn\", conn.RemoteAddr())\n\n\t\tcontroller, err := hap.NewConn(conn, rw, key, false)\n\t\tif err != nil {\n\t\t\tlog.Error().Err(err).Caller().Send()\n\t\t\treturn\n\t\t}\n\n\t\ts.AddConn(controller)\n\t\tdefer s.DelConn(controller)\n\n\t\tvar handler homekit.HandlerFunc\n\n\t\tswitch {\n\t\tcase s.accessory != nil:\n\t\t\thandler = homekit.ServerHandler(s)\n\t\tcase s.proxyURL != \"\":\n\t\t\tclient, err := hap.Dial(s.proxyURL)\n\t\t\tif err != nil {\n\t\t\t\tlog.Error().Err(err).Caller().Send()\n\t\t\t\treturn\n\t\t\t}\n\t\t\thandler = homekit.ProxyHandler(s, client.Conn)\n\t\t}\n\n\t\t// If your iPhone goes to sleep, it will be an EOF error.\n\t\tif err = handler(controller); err != nil && !errors.Is(err, io.EOF) {\n\t\t\tlog.Error().Err(err).Caller().Send()\n\t\t\treturn\n\t\t}\n\t}\n}\n\ntype logger struct {\n\tv any\n}\n\nfunc (l logger) String() string {\n\tswitch v := l.v.(type) {\n\tcase *hap.Conn:\n\t\treturn \"hap \" + v.RemoteAddr().String()\n\tcase *hds.Conn:\n\t\treturn \"hds \" + v.RemoteAddr().String()\n\tcase *homekit.Consumer:\n\t\treturn \"rtp \" + v.RemoteAddr\n\t}\n\treturn \"unknown\"\n}\n\nfunc (s *server) AddConn(v any) {\n\tlog.Trace().Str(\"stream\", s.stream).Msgf(\"[homekit] add conn %s\", logger{v})\n\ts.mu.Lock()\n\ts.conns = append(s.conns, v)\n\ts.mu.Unlock()\n}\n\nfunc (s *server) DelConn(v any) {\n\tlog.Trace().Str(\"stream\", s.stream).Msgf(\"[homekit] del conn %s\", logger{v})\n\ts.mu.Lock()\n\tif i := slices.Index(s.conns, v); i >= 0 {\n\t\ts.conns = slices.Delete(s.conns, i, i+1)\n\t}\n\ts.mu.Unlock()\n}\n\nfunc (s *server) UpdateStatus() {\n\t// true status is important, or device may be offline in Apple Home\n\tif len(s.pairings) == 0 {\n\t\ts.mdns.Info[hap.TXTStatusFlags] = hap.StatusNotPaired\n\t} else {\n\t\ts.mdns.Info[hap.TXTStatusFlags] = hap.StatusPaired\n\t}\n}\n\nfunc (s *server) pairIndex(id string) int {\n\tid = \"client_id=\" + id\n\tfor i, pairing := range s.pairings {\n\t\tif strings.HasPrefix(pairing, id) {\n\t\t\treturn i\n\t\t}\n\t}\n\treturn -1\n}\n\nfunc (s *server) GetPair(id string) []byte {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\tif i := s.pairIndex(id); i >= 0 {\n\t\tquery, _ := url.ParseQuery(s.pairings[i])\n\t\tb, _ := hex.DecodeString(query.Get(\"client_public\"))\n\t\treturn b\n\t}\n\treturn nil\n}\n\nfunc (s *server) AddPair(id string, public []byte, permissions byte) {\n\tlog.Debug().Str(\"stream\", s.stream).Msgf(\"[homekit] add pair id=%s public=%x perm=%d\", id, public, permissions)\n\n\ts.mu.Lock()\n\tif s.pairIndex(id) < 0 {\n\t\ts.pairings = append(s.pairings, fmt.Sprintf(\n\t\t\t\"client_id=%s&client_public=%x&permissions=%d\", id, public, permissions,\n\t\t))\n\t\ts.UpdateStatus()\n\t\ts.PatchConfig()\n\t}\n\ts.mu.Unlock()\n}\n\nfunc (s *server) DelPair(id string) {\n\tlog.Debug().Str(\"stream\", s.stream).Msgf(\"[homekit] del pair id=%s\", id)\n\n\ts.mu.Lock()\n\tif i := s.pairIndex(id); i >= 0 {\n\t\ts.pairings = append(s.pairings[:i], s.pairings[i+1:]...)\n\t\ts.UpdateStatus()\n\t\ts.PatchConfig()\n\t}\n\ts.mu.Unlock()\n}\n\nfunc (s *server) PatchConfig() {\n\tif err := app.PatchConfig([]string{\"homekit\", s.stream, \"pairings\"}, s.pairings); err != nil {\n\t\tlog.Error().Err(err).Msgf(\n\t\t\t\"[homekit] can't save %s pairings=%v\", s.stream, s.pairings,\n\t\t)\n\t}\n}\n\nfunc (s *server) GetAccessories(_ net.Conn) []*hap.Accessory {\n\treturn []*hap.Accessory{s.accessory}\n}\n\nfunc (s *server) GetCharacteristic(conn net.Conn, aid uint8, iid uint64) any {\n\tlog.Trace().Str(\"stream\", s.stream).Msgf(\"[homekit] get char aid=%d iid=0x%x\", aid, iid)\n\n\tchar := s.accessory.GetCharacterByID(iid)\n\tif char == nil {\n\t\tlog.Warn().Msgf(\"[homekit] get unknown characteristic: %d\", iid)\n\t\treturn nil\n\t}\n\n\tswitch char.Type {\n\tcase camera.TypeSetupEndpoints:\n\t\tconsumer := s.consumer\n\t\tif consumer == nil {\n\t\t\treturn nil\n\t\t}\n\n\t\tanswer := consumer.GetAnswer()\n\t\tv, err := tlv8.MarshalBase64(answer)\n\t\tif err != nil {\n\t\t\treturn nil\n\t\t}\n\n\t\treturn v\n\t}\n\n\treturn char.Value\n}\n\nfunc (s *server) SetCharacteristic(conn net.Conn, aid uint8, iid uint64, value any) {\n\tlog.Trace().Str(\"stream\", s.stream).Msgf(\"[homekit] set char aid=%d iid=0x%x value=%v\", aid, iid, value)\n\n\tchar := s.accessory.GetCharacterByID(iid)\n\tif char == nil {\n\t\tlog.Warn().Msgf(\"[homekit] set unknown characteristic: %d\", iid)\n\t\treturn\n\t}\n\n\tswitch char.Type {\n\tcase camera.TypeSetupEndpoints:\n\t\tvar offer camera.SetupEndpointsRequest\n\t\tif err := tlv8.UnmarshalBase64(value, &offer); err != nil {\n\t\t\treturn\n\t\t}\n\n\t\tconsumer := homekit.NewConsumer(conn, srtp2.Server)\n\t\tconsumer.SetOffer(&offer)\n\t\ts.consumer = consumer\n\n\tcase camera.TypeSelectedStreamConfiguration:\n\t\tvar conf camera.SelectedStreamConfiguration\n\t\tif err := tlv8.UnmarshalBase64(value, &conf); err != nil {\n\t\t\treturn\n\t\t}\n\n\t\tlog.Trace().Str(\"stream\", s.stream).Msgf(\"[homekit] stream id=%x cmd=%d\", conf.Control.SessionID, conf.Control.Command)\n\n\t\tswitch conf.Control.Command {\n\t\tcase camera.SessionCommandEnd:\n\t\t\tfor _, consumer := range s.conns {\n\t\t\t\tif consumer, ok := consumer.(*homekit.Consumer); ok {\n\t\t\t\t\tif consumer.SessionID() == conf.Control.SessionID {\n\t\t\t\t\t\t_ = consumer.Stop()\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\tcase camera.SessionCommandStart:\n\t\t\tconsumer := s.consumer\n\t\t\tif consumer == nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif !consumer.SetConfig(&conf) {\n\t\t\t\tlog.Warn().Msgf(\"[homekit] wrong config\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\ts.AddConn(consumer)\n\n\t\t\tstream := streams.Get(s.stream)\n\t\t\tif err := stream.AddConsumer(consumer); err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tgo func() {\n\t\t\t\t_, _ = consumer.WriteTo(nil)\n\t\t\t\tstream.RemoveConsumer(consumer)\n\n\t\t\t\ts.DelConn(consumer)\n\t\t\t}()\n\t\t}\n\t}\n}\n\nfunc (s *server) GetImage(conn net.Conn, width, height int) []byte {\n\tlog.Trace().Str(\"stream\", s.stream).Msgf(\"[homekit] get image width=%d height=%d\", width, height)\n\n\tstream := streams.Get(s.stream)\n\tcons := magic.NewKeyframe()\n\n\tif err := stream.AddConsumer(cons); err != nil {\n\t\treturn nil\n\t}\n\n\tonce := &core.OnceBuffer{} // init and first frame\n\t_, _ = cons.WriteTo(once)\n\tb := once.Buffer()\n\n\tstream.RemoveConsumer(cons)\n\n\tswitch cons.CodecName() {\n\tcase core.CodecH264, core.CodecH265:\n\t\tvar err error\n\t\tif b, err = ffmpeg.JPEGWithScale(b, width, height); err != nil {\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn b\n}\n\nfunc calcName(name, seed string) string {\n\tif name != \"\" {\n\t\treturn name\n\t}\n\tb := sha512.Sum512([]byte(seed))\n\treturn fmt.Sprintf(\"go2rtc-%02X%02X\", b[0], b[2])\n}\n\nfunc calcDeviceID(deviceID, seed string) string {\n\tif deviceID != \"\" {\n\t\tif len(deviceID) >= 17 {\n\t\t\t// 1. Returd device_id as is (ex. AA:BB:CC:DD:EE:FF)\n\t\t\treturn deviceID\n\t\t}\n\t\t// 2. Use device_id as seed if not zero\n\t\tseed = deviceID\n\t}\n\tb := sha512.Sum512([]byte(seed))\n\treturn fmt.Sprintf(\"%02X:%02X:%02X:%02X:%02X:%02X\", b[32], b[34], b[36], b[38], b[40], b[42])\n}\n\nfunc calcDevicePrivate(private, seed string) []byte {\n\tif private != \"\" {\n\t\t// 1. Decode private from HEX string\n\t\tif b, _ := hex.DecodeString(private); len(b) == ed25519.PrivateKeySize {\n\t\t\t// 2. Return if OK\n\t\t\treturn b\n\t\t}\n\t\t// 3. Use private as seed if not zero\n\t\tseed = private\n\t}\n\tb := sha512.Sum512([]byte(seed))\n\treturn ed25519.NewKeyFromSeed(b[:ed25519.SeedSize])\n}\n\nfunc calcSetupID(seed string) string {\n\tb := sha512.Sum512([]byte(seed))\n\treturn fmt.Sprintf(\"%02X%02X\", b[44], b[46])\n}\n\nfunc calcCategoryID(categoryID string) string {\n\tswitch categoryID {\n\tcase \"bridge\":\n\t\treturn hap.CategoryBridge\n\tcase \"doorbell\":\n\t\treturn hap.CategoryDoorbell\n\t}\n\tif core.Atoi(categoryID) > 0 {\n\t\treturn categoryID\n\t}\n\treturn hap.CategoryCamera\n}\n"
  },
  {
    "path": "internal/http/README.md",
    "content": "# HTTP\n\nThis source supports receiving a stream via an HTTP link.\n\nIt can determine the source format from the`Content-Type` HTTP header:\n\n- **HTTP-JPEG** (`image/jpeg`) - camera snapshot link, can be converted by go2rtc to MJPEG stream\n- **HTTP-MJPEG** (`multipart/x-mixed-replace`) - A continuous sequence of JPEG frames (with HTTP headers).\n- **HLS** (`application/vnd.apple.mpegurl`) - A popular [HTTP Live Streaming](https://en.wikipedia.org/wiki/HTTP_Live_Streaming) (HLS) format, which is not designed for real-time media transmission.\n\n> [!WARNING]\n> The HLS format is not designed for real time and is supported quite poorly. It is recommended to use it via ffmpeg source with buffering enabled (disabled by default).\n\n## TCP\n\nSource also supports HTTP and TCP streams with autodetection for different formats:\n\n- `adts` - Audio stream in [AAC](https://en.wikipedia.org/wiki/Advanced_Audio_Coding) codec with Audio Data Transport Stream (ADTS) headers.\n- `flv` - The legacy but still used [Flash Video](https://en.wikipedia.org/wiki/Flash_Video) format.\n- `h264` - AVC/H.264 bitstream.\n- `hevc` - HEVC/H.265 bitstream.\n- `mjpeg` - A continuous sequence of JPEG frames (without HTTP headers).\n- `mpegts` - The legacy [MPEG transport stream](https://en.wikipedia.org/wiki/MPEG_transport_stream) format.\n- `wav` - Audio stream in [WAV](https://en.wikipedia.org/wiki/WAV) format.\n- `yuv4mpegpipe` - Raw YUV frame stream with YUV4MPEG header.\n\n## Configuration\n\n```yaml\nstreams:\n  # [HTTP-FLV] stream in video/x-flv format\n  http_flv: http://192.168.1.123:20880/api/camera/stream/780900131155/657617\n  \n  # [JPEG] snapshots from Dahua camera, will be converted to MJPEG stream\n  dahua_snap: http://admin:password@192.168.1.123/cgi-bin/snapshot.cgi?channel=1\n\n  # [MJPEG] stream will be proxied without modification\n  http_mjpeg: https://mjpeg.sanford.io/count.mjpeg\n\n  # [MJPEG or H.264/H.265 bitstream or MPEG-TS]\n  tcp_magic: tcp://192.168.1.123:12345\n\n  # Add custom header\n  custom_header: \"https://mjpeg.sanford.io/count.mjpeg#header=Authorization: Bearer XXX\"\n```\n\n**PS.** Dahua camera has a bug: if you select MJPEG codec for RTSP second stream, snapshot won't work.\n"
  },
  {
    "path": "internal/http/http.go",
    "content": "package http\n\nimport (\n\t\"errors\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hls\"\n\t\"github.com/AlexxIT/go2rtc/pkg/image\"\n\t\"github.com/AlexxIT/go2rtc/pkg/magic\"\n\t\"github.com/AlexxIT/go2rtc/pkg/mpjpeg\"\n\t\"github.com/AlexxIT/go2rtc/pkg/pcm\"\n\t\"github.com/AlexxIT/go2rtc/pkg/tcp\"\n)\n\nfunc Init() {\n\tstreams.HandleFunc(\"http\", handleHTTP)\n\tstreams.HandleFunc(\"https\", handleHTTP)\n\tstreams.HandleFunc(\"httpx\", handleHTTP)\n\n\tstreams.HandleFunc(\"tcp\", handleTCP)\n\n\tapi.HandleFunc(\"api/stream\", apiStream)\n}\n\nfunc handleHTTP(rawURL string) (core.Producer, error) {\n\trawURL, rawQuery, _ := strings.Cut(rawURL, \"#\")\n\n\t// first we get the Content-Type to define supported producer\n\treq, err := http.NewRequest(\"GET\", rawURL, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif rawQuery != \"\" {\n\t\tquery := streams.ParseQuery(rawQuery)\n\n\t\tfor _, header := range query[\"header\"] {\n\t\t\tkey, value, _ := strings.Cut(header, \":\")\n\t\t\treq.Header.Add(key, strings.TrimSpace(value))\n\t\t}\n\t}\n\n\tprod, err := do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif info, ok := prod.(core.Info); ok {\n\t\tinfo.SetProtocol(\"http\")\n\t\tinfo.SetRemoteAddr(req.URL.Host) // TODO: rewrite to net.Conn\n\t\tinfo.SetURL(rawURL)\n\t}\n\n\treturn prod, nil\n}\n\nfunc do(req *http.Request) (core.Producer, error) {\n\tres, err := tcp.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif res.StatusCode != http.StatusOK {\n\t\treturn nil, errors.New(res.Status)\n\t}\n\n\t// 1. Guess format from content type\n\tct := res.Header.Get(\"Content-Type\")\n\tif i := strings.IndexByte(ct, ';'); i > 0 {\n\t\tct = ct[:i]\n\t}\n\n\tvar ext string\n\tif i := strings.LastIndexByte(req.URL.Path, '.'); i > 0 {\n\t\text = req.URL.Path[i+1:]\n\t}\n\n\tswitch {\n\tcase ct == \"application/vnd.apple.mpegurl\" || ext == \"m3u8\":\n\t\treturn hls.OpenURL(req.URL, res.Body)\n\tcase ct == \"image/jpeg\":\n\t\treturn image.Open(res)\n\tcase ct == \"multipart/x-mixed-replace\":\n\t\treturn mpjpeg.Open(res.Body)\n\t//https://www.iana.org/assignments/media-types/audio/basic\n\tcase ct == \"audio/basic\":\n\t\treturn pcm.Open(res.Body)\n\t}\n\n\treturn magic.Open(res.Body)\n}\n\nfunc handleTCP(rawURL string) (core.Producer, error) {\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tconn, err := net.DialTimeout(\"tcp\", u.Host, core.ConnDialTimeout)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn magic.Open(conn)\n}\n\nfunc apiStream(w http.ResponseWriter, r *http.Request) {\n\tdst := r.URL.Query().Get(\"dst\")\n\tstream := streams.Get(dst)\n\tif stream == nil {\n\t\thttp.Error(w, api.StreamNotFound, http.StatusNotFound)\n\t\treturn\n\t}\n\n\tclient, err := magic.Open(r.Body)\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tstream.AddProducer(client)\n\tdefer stream.RemoveProducer(client)\n\n\tif err = client.Start(); err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n}\n"
  },
  {
    "path": "internal/isapi/README.md",
    "content": "# Hikvision ISAPI\n\n[`new in v1.3.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)\n\nThis source type supports only backchannel audio for the [Hikvision ISAPI](https://tpp.hikvision.com/download/ISAPI_OTAP) protocol. So it should be used as a second source in addition to the RTSP protocol.\n\n## Configuration\n\n```yaml\nstreams:\n  hikvision1:\n    - rtsp://admin:password@192.168.1.123:554/Streaming/Channels/101\n    - isapi://admin:password@192.168.1.123:80/\n```\n"
  },
  {
    "path": "internal/isapi/init.go",
    "content": "package isapi\n\nimport (\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/isapi\"\n)\n\nfunc Init() {\n\tstreams.HandleFunc(\"isapi\", func(source string) (core.Producer, error) {\n\t\treturn isapi.Dial(source)\n\t})\n}\n"
  },
  {
    "path": "internal/ivideon/README.md",
    "content": "# Ivideon\n\nSupport public cameras from the service [Ivideon](https://tv.ivideon.com/).\n\n## Configuration\n\n```yaml\nstreams:\n  quailcam: ivideon:100-tu5dkUPct39cTp9oNEN2B6/0\n```\n"
  },
  {
    "path": "internal/ivideon/ivideon.go",
    "content": "package ivideon\n\nimport (\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/ivideon\"\n)\n\nfunc Init() {\n\tstreams.HandleFunc(\"ivideon\", ivideon.Dial)\n}\n"
  },
  {
    "path": "internal/kasa/README.md",
    "content": "# TP-Link Kasa\n\n[`new in v1.7.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.7.0)\n\n[TP-Link Kasa](https://www.kasasmart.com/) non-standard protocol [more info](https://medium.com/@hu3vjeen/reverse-engineering-tp-link-kc100-bac4641bf1cd).\n\n- `username` - urlsafe email, `alex@gmail.com` -> `alex%40gmail.com`\n- `password` - base64password, `secret1` -> `c2VjcmV0MQ==`\n\n```yaml\nstreams:\n  kc401: kasa://username:password@192.168.1.123:19443/https/stream/mixed\n```\n\nTested: KD110, KC200, KC401, KC420WS, EC71.\n"
  },
  {
    "path": "internal/kasa/kasa.go",
    "content": "package kasa\n\nimport (\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/kasa\"\n)\n\nfunc Init() {\n\tstreams.HandleFunc(\"kasa\", func(source string) (core.Producer, error) {\n\t\treturn kasa.Dial(source)\n\t})\n}\n"
  },
  {
    "path": "internal/mjpeg/README.md",
    "content": "# Motion JPEG\n\n- This module can provide and receive streams in MJPEG format.\n- This module is also responsible for receiving snapshots in JPEG format.\n- This module also supports streaming to the server console (terminal) in the **animated ASCII art** format.\n\n## MJPEG Client\n\n**Important.** For a stream in MJPEG format, your source MUST contain the MJPEG codec. If your stream has the MJPEG codec, you can receive an **MJPEG stream** or **JPEG snapshots** via the API.\n\nYou can receive an MJPEG stream in several ways:\n\n- some cameras support MJPEG codec inside [RTSP stream](../rtsp/README.md) (ex. second stream for Dahua cameras)\n- some cameras have an HTTP link with [MJPEG stream](../http/README.md)\n- some cameras have an HTTP link with snapshots - go2rtc can convert them to [MJPEG stream](../http/README.md)\n- you can convert an H264/H265 stream from your camera via [FFmpeg integration](../ffmpeg/README.md)\n\nWith this example, your stream will have both H264 and MJPEG codecs:\n\n```yaml\nstreams:\n  camera1:\n    - rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0\n    - ffmpeg:camera1#video=mjpeg\n```\n\n## MJPEG Server\n\n### mpjpeg\n\nOutput a stream in [MJPEG](https://en.wikipedia.org/wiki/Motion_JPEG) format. In [FFmpeg](https://ffmpeg.org/), this format is called `mpjpeg` because it contains HTTP headers.\n\n```\nffplay http://192.168.1.123:1984/api/stream.mjpeg?src=camera1\n```\n\n### jpeg\n\nReceiving a JPEG snapshot.\n\n```\ncurl http://192.168.1.123:1984/api/frame.jpeg?src=camera1\n```\n\n- You can use `width`/`w` and/or `height`/`h` parameters.\n- You can use `rotate` param with `90`, `180`, `270` or `-90` values.\n- You can use `hardware`/`hw` param [read more](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration).\n- You can use `cache` param (`1m`, `10s`, etc.) to get a cached snapshot.\n  - The snapshot is cached only when requested with the `cache` parameter.\n  - A cached snapshot will be used if its time is not older than the time specified in the `cache` parameter.\n  - The `cache` parameter does not check the image dimensions from the cache and those specified in the query.\n\n### ascii\n\nStream as ASCII to Terminal. This format is just for fun. You can boast to your friends that you can stream cameras even to the server console without a GUI.\n\n[![](https://img.youtube.com/vi/sHj_3h_sX7M/mqdefault.jpg)](https://www.youtube.com/watch?v=sHj_3h_sX7M)\n\n> The demo video features a combination of several settings for this format with added audio. Of course, the format doesn't support audio out of the box.\n\n**Tips**\n\n- this feature works only with MJPEG codec (use transcoding)\n- choose a low frame rate (FPS)\n- choose the width and height to fit in your terminal\n- different terminals support different numbers of colors (8, 256, rgb)\n- URL-encode the `text` parameter\n- you can stream any camera or file from disk\n\n**go2rtc.yaml** - transcoding to MJPEG, terminal size - 210x59 (16/9), fps - 10\n\n```yaml\nstreams:\n  gamazda: ffmpeg:gamazda.mp4#video=mjpeg#hardware#width=210#height=59#raw=-r 10\n```\n\n**API params**\n\n- `color` - foreground color, values: empty, `8`, `256`, `rgb`, [SGR](https://en.wikipedia.org/wiki/ANSI_escape_code)\n  - example: `30` (black), `37` (white), `38;5;226` (yellow)\n- `back` - background color, values: empty, `8`, `256`, `rgb`, [SGR](https://en.wikipedia.org/wiki/ANSI_escape_code)\n  - example: `40` (black), `47` (white), `48;5;226` (yellow)\n- `text` - character set, values: empty, one character, `block`, list of chars (in order of brightness)\n  - example: `%20` (space), `block` (keyword for block elements), `ox` (two chars)\n\n**Examples**\n\n```bash\n% curl \"http://192.168.1.123:1984/api/stream.ascii?src=gamazda\"\n% curl \"http://192.168.1.123:1984/api/stream.ascii?src=gamazda&color=256\"\n% curl \"http://192.168.1.123:1984/api/stream.ascii?src=gamazda&back=256&text=%20\"\n% curl \"http://192.168.1.123:1984/api/stream.ascii?src=gamazda&back=8&text=%20%20\"\n% curl \"http://192.168.1.123:1984/api/stream.ascii?src=gamazda&text=helloworld\"\n```\n\n### yuv4mpegpipe\n\nRaw [YUV](https://en.wikipedia.org/wiki/Y%E2%80%B2UV) frame stream with [YUV4MPEG](https://manned.org/yuv4mpeg) header.\n\n```\nffplay http://192.168.1.123:1984/api/stream.y4m?src=camera1\n```\n\n## Streaming ingest\n\n```shell\nffmpeg -re -i BigBuckBunny.mp4 -c mjpeg -f mpjpeg http://localhost:1984/api/stream.mjpeg?dst=camera1\n```\n"
  },
  {
    "path": "internal/mjpeg/mjpeg.go",
    "content": "package mjpeg\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/internal/api/ws\"\n\t\"github.com/AlexxIT/go2rtc/internal/app\"\n\t\"github.com/AlexxIT/go2rtc/internal/ffmpeg\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/ascii\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/magic\"\n\t\"github.com/AlexxIT/go2rtc/pkg/mjpeg\"\n\t\"github.com/AlexxIT/go2rtc/pkg/mpjpeg\"\n\t\"github.com/AlexxIT/go2rtc/pkg/y4m\"\n\t\"github.com/rs/zerolog\"\n)\n\nfunc Init() {\n\tapi.HandleFunc(\"api/frame.jpeg\", handlerKeyframe)\n\tapi.HandleFunc(\"api/stream.mjpeg\", handlerStream)\n\tapi.HandleFunc(\"api/stream.ascii\", handlerStream)\n\tapi.HandleFunc(\"api/stream.y4m\", apiStreamY4M)\n\n\tws.HandleFunc(\"mjpeg\", handlerWS)\n\n\tlog = app.GetLogger(\"mjpeg\")\n}\n\nvar log zerolog.Logger\n\nfunc handlerKeyframe(w http.ResponseWriter, r *http.Request) {\n\tquery := r.URL.Query()\n\tstream, _ := streams.GetOrPatch(query)\n\tif stream == nil {\n\t\thttp.Error(w, api.StreamNotFound, http.StatusNotFound)\n\t\treturn\n\t}\n\n\tvar b []byte\n\n\tif s := query.Get(\"cache\"); s != \"\" {\n\t\tif timeout, err := time.ParseDuration(s); err == nil {\n\t\t\tsrc := query.Get(\"src\")\n\n\t\t\tcacheMu.Lock()\n\t\t\tentry, found := cache[src]\n\t\t\tcacheMu.Unlock()\n\n\t\t\tif found && time.Since(entry.timestamp) < timeout {\n\t\t\t\twriteJPEGResponse(w, entry.payload)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tdefer func() {\n\t\t\t\tif b == nil {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tentry = cacheEntry{payload: b, timestamp: time.Now()}\n\t\t\t\tcacheMu.Lock()\n\t\t\t\tif cache == nil {\n\t\t\t\t\tcache = map[string]cacheEntry{src: entry}\n\t\t\t\t} else {\n\t\t\t\t\tcache[src] = entry\n\t\t\t\t}\n\t\t\t\tcacheMu.Unlock()\n\t\t\t}()\n\t\t}\n\t}\n\n\tcons := magic.NewKeyframe()\n\tcons.WithRequest(r)\n\n\tif err := stream.AddConsumer(cons); err != nil {\n\t\tlog.Error().Err(err).Caller().Send()\n\t\treturn\n\t}\n\n\tonce := &core.OnceBuffer{} // init and first frame\n\t_, _ = cons.WriteTo(once)\n\tb = once.Buffer()\n\n\tstream.RemoveConsumer(cons)\n\n\tswitch cons.CodecName() {\n\tcase core.CodecH264, core.CodecH265:\n\t\tts := time.Now()\n\t\tvar err error\n\t\tif b, err = ffmpeg.JPEGWithQuery(b, query); err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\tlog.Debug().Msgf(\"[mjpeg] transcoding time=%s\", time.Since(ts))\n\tcase core.CodecJPEG:\n\t\tb = mjpeg.FixJPEG(b)\n\t}\n\n\twriteJPEGResponse(w, b)\n}\n\nvar cache map[string]cacheEntry\nvar cacheMu sync.Mutex\n\n// cacheEntry represents a cached keyframe with its timestamp\ntype cacheEntry struct {\n\tpayload   []byte\n\ttimestamp time.Time\n}\n\nfunc writeJPEGResponse(w http.ResponseWriter, b []byte) {\n\th := w.Header()\n\th.Set(\"Content-Type\", \"image/jpeg\")\n\th.Set(\"Content-Length\", strconv.Itoa(len(b)))\n\th.Set(\"Cache-Control\", \"no-cache\")\n\th.Set(\"Connection\", \"close\")\n\th.Set(\"Pragma\", \"no-cache\")\n\n\tif _, err := w.Write(b); err != nil {\n\t\tlog.Error().Err(err).Caller().Send()\n\t}\n}\n\nfunc handlerStream(w http.ResponseWriter, r *http.Request) {\n\tif r.Method != \"POST\" {\n\t\toutputMjpeg(w, r)\n\t} else {\n\t\tinputMjpeg(w, r)\n\t}\n}\n\nfunc outputMjpeg(w http.ResponseWriter, r *http.Request) {\n\tsrc := r.URL.Query().Get(\"src\")\n\tstream := streams.Get(src)\n\tif stream == nil {\n\t\thttp.Error(w, api.StreamNotFound, http.StatusNotFound)\n\t\treturn\n\t}\n\n\tcons := mjpeg.NewConsumer()\n\tcons.WithRequest(r)\n\n\tif err := stream.AddConsumer(cons); err != nil {\n\t\tlog.Error().Err(err).Msg(\"[api.mjpeg] add consumer\")\n\t\treturn\n\t}\n\n\th := w.Header()\n\th.Set(\"Cache-Control\", \"no-cache\")\n\th.Set(\"Connection\", \"close\")\n\th.Set(\"Pragma\", \"no-cache\")\n\n\tif strings.HasSuffix(r.URL.Path, \"mjpeg\") {\n\t\twr := mjpeg.NewWriter(w)\n\t\t_, _ = cons.WriteTo(wr)\n\t} else {\n\t\tcons.FormatName = \"ascii\"\n\n\t\tquery := r.URL.Query()\n\t\twr := ascii.NewWriter(w, query.Get(\"color\"), query.Get(\"back\"), query.Get(\"text\"))\n\t\t_, _ = cons.WriteTo(wr)\n\t}\n\n\tstream.RemoveConsumer(cons)\n}\n\nfunc inputMjpeg(w http.ResponseWriter, r *http.Request) {\n\tdst := r.URL.Query().Get(\"dst\")\n\tstream := streams.Get(dst)\n\tif stream == nil {\n\t\thttp.Error(w, api.StreamNotFound, http.StatusNotFound)\n\t\treturn\n\t}\n\n\tprod, _ := mpjpeg.Open(r.Body)\n\tprod.WithRequest(r)\n\n\tstream.AddProducer(prod)\n\n\tif err := prod.Start(); err != nil && err != io.EOF {\n\t\tlog.Warn().Err(err).Caller().Send()\n\t}\n\n\tstream.RemoveProducer(prod)\n}\n\nfunc handlerWS(tr *ws.Transport, _ *ws.Message) error {\n\tstream, _ := streams.GetOrPatch(tr.Request.URL.Query())\n\tif stream == nil {\n\t\treturn errors.New(api.StreamNotFound)\n\t}\n\n\tcons := mjpeg.NewConsumer()\n\tcons.WithRequest(tr.Request)\n\n\tif err := stream.AddConsumer(cons); err != nil {\n\t\tlog.Debug().Err(err).Msg(\"[mjpeg] add consumer\")\n\t\treturn err\n\t}\n\n\ttr.Write(&ws.Message{Type: \"mjpeg\"})\n\n\tgo cons.WriteTo(tr.Writer())\n\n\ttr.OnClose(func() {\n\t\tstream.RemoveConsumer(cons)\n\t})\n\n\treturn nil\n}\n\nfunc apiStreamY4M(w http.ResponseWriter, r *http.Request) {\n\tsrc := r.URL.Query().Get(\"src\")\n\tstream := streams.Get(src)\n\tif stream == nil {\n\t\thttp.Error(w, api.StreamNotFound, http.StatusNotFound)\n\t\treturn\n\t}\n\n\tcons := y4m.NewConsumer()\n\tcons.WithRequest(r)\n\n\tif err := stream.AddConsumer(cons); err != nil {\n\t\tlog.Error().Err(err).Caller().Send()\n\t\treturn\n\t}\n\n\t_, _ = cons.WriteTo(w)\n\n\tstream.RemoveConsumer(cons)\n}\n"
  },
  {
    "path": "internal/mp4/README.md",
    "content": "# MP4\n\nThis module provides several features:\n\n1. MSE stream (fMP4 over WebSocket)\n2. Camera snapshots in MP4 format (single frame), can be sent to [Telegram](#snapshot-to-telegram)\n3. HTTP progressive streaming (MP4 file stream) - bad format for streaming because of high start delay. This format doesn't work in all Safari browsers, but go2rtc will automatically redirect it to HLS/fMP4 in this case.\n\n## API examples\n\n- MP4 snapshot: `http://192.168.1.123:1984/api/frame.mp4?src=camera1` (H264, H265)\n- MP4 stream: `http://192.168.1.123:1984/api/stream.mp4?src=camera1` (H264, H265, AAC)\n- MP4 file: `http://192.168.1.123:1984/api/stream.mp4?src=camera1` (H264, H265*, AAC, OPUS, MP3, PCMA, PCMU, PCM)\n    - You can use `mp4`, `mp4=flac` and `mp4=all` param for codec filters\n    - You can use `duration` param in seconds (ex. `duration=15`)\n    - You can use `filename` param (ex. `filename=record.mp4`)\n    - You can use `rotate` param with `90`, `180` or `270` values\n    - You can use `scale` param with positive integer values (ex. `scale=4:3`)\n\nRead more about [codecs filters](../../README.md#codecs-filters).\n\n**PS.** Rotate and scale params don't use transcoding and change video using metadata.\n\n## Snapshot to Telegram\n\nThis examples for Home Assistant [Telegram Bot](https://www.home-assistant.io/integrations/telegram_bot/) integration.\n\n- change `url` to your go2rtc web API (`http://localhost:1984/` for most users)\n- change `target` to your Telegram chat ID (support list)\n- change `src=camera1` to your stream name from go2rtc config\n\n**Important.** Snapshot will be near instant for most cameras and many sources, except `ffmpeg` source. Because it takes a long time for ffmpeg to start streaming with video, even when you use `#video=copy`. Also the delay can be with cameras that do not start the stream with a keyframe.\n\n### Snapshot from H264 or H265 camera\n\n```yaml\nservice: telegram_bot.send_video\ndata:\n  url: http://localhost:1984/api/frame.mp4?src=camera1\n  target: 123456789\n```\n\n### Record from H264 or H265 camera\n\nRecord from service call to the future. Doesn't support loopback.\n\n- `mp4=flac` - adds support PCM audio family\n- `filename=record.mp4` - set name for downloaded file\n\n```yaml\nservice: telegram_bot.send_video\ndata:\n  url: http://localhost:1984/api/stream.mp4?src=camera1&mp4=flac&duration=5&filename=record.mp4  # duration in seconds\n  target: 123456789\n```\n\n### Snapshot from JPEG or MJPEG camera\n\nThis example works via the [mjpeg](../mjpeg/README.md) module.\n\n```yaml\nservice: telegram_bot.send_photo\ndata:\n  url: http://localhost:1984/api/frame.jpeg?src=camera1\n  target: 123456789\n```\n"
  },
  {
    "path": "internal/mp4/mp4.go",
    "content": "package mp4\n\nimport (\n\t\"context\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/internal/api/ws\"\n\t\"github.com/AlexxIT/go2rtc/internal/app\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/mp4\"\n\t\"github.com/rs/zerolog\"\n)\n\nfunc Init() {\n\tlog = app.GetLogger(\"mp4\")\n\n\tws.HandleFunc(\"mse\", handlerWSMSE)\n\tws.HandleFunc(\"mp4\", handlerWSMP4)\n\n\tapi.HandleFunc(\"api/frame.mp4\", handlerKeyframe)\n\tapi.HandleFunc(\"api/stream.mp4\", handlerMP4)\n}\n\nvar log zerolog.Logger\n\nfunc handlerKeyframe(w http.ResponseWriter, r *http.Request) {\n\t// Chrome 105 does two requests: without Range and with `Range: bytes=0-`\n\tua := r.UserAgent()\n\tif strings.Contains(ua, \" Chrome/\") {\n\t\tif r.Header.Values(\"Range\") == nil {\n\t\t\tw.Header().Set(\"Content-Type\", \"video/mp4\")\n\t\t\tw.WriteHeader(http.StatusOK)\n\t\t\treturn\n\t\t}\n\t}\n\n\tquery := r.URL.Query()\n\tsrc := query.Get(\"src\")\n\tstream := streams.Get(src)\n\tif stream == nil {\n\t\thttp.Error(w, api.StreamNotFound, http.StatusNotFound)\n\t\treturn\n\t}\n\n\tcons := mp4.NewKeyframe(nil)\n\n\tif err := stream.AddConsumer(cons); err != nil {\n\t\tlog.Error().Err(err).Caller().Send()\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tonce := &core.OnceBuffer{} // init and first frame\n\t_, _ = cons.WriteTo(once)\n\n\tstream.RemoveConsumer(cons)\n\n\t// Apple Safari won't show frame without length\n\theader := w.Header()\n\theader.Set(\"Content-Length\", strconv.Itoa(once.Len()))\n\theader.Set(\"Content-Type\", mp4.ContentType(cons.Codecs()))\n\n\tif filename := query.Get(\"filename\"); filename != \"\" {\n\t\theader.Set(\"Content-Disposition\", `attachment; filename=\"`+filename+`\"`)\n\t}\n\n\tif _, err := once.WriteTo(w); err != nil {\n\t\tlog.Error().Err(err).Caller().Send()\n\t}\n}\n\nfunc handlerMP4(w http.ResponseWriter, r *http.Request) {\n\tlog.Trace().Msgf(\"[mp4] %s %+v\", r.Method, r.Header)\n\n\tquery := r.URL.Query()\n\n\tua := r.UserAgent()\n\tif strings.Contains(ua, \" Safari/\") && !strings.Contains(ua, \" Chrome/\") && !query.Has(\"duration\") {\n\t\t// auto redirect to HLS/fMP4 format, because Safari not support MP4 stream\n\t\turl := \"stream.m3u8?\" + r.URL.RawQuery\n\t\tif !query.Has(\"mp4\") {\n\t\t\turl += \"&mp4\"\n\t\t}\n\n\t\thttp.Redirect(w, r, url, http.StatusMovedPermanently)\n\t\treturn\n\t}\n\n\tstream, _ := streams.GetOrPatch(query)\n\tif stream == nil {\n\t\thttp.Error(w, api.StreamNotFound, http.StatusNotFound)\n\t\treturn\n\t}\n\n\tmedias := mp4.ParseQuery(r.URL.Query())\n\tcons := mp4.NewConsumer(medias)\n\tcons.FormatName = \"mp4\"\n\tcons.Protocol = \"http\"\n\tcons.WithRequest(r)\n\n\tif err := stream.AddConsumer(cons); err != nil {\n\t\tlog.Error().Err(err).Caller().Send()\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif rotate := query.Get(\"rotate\"); rotate != \"\" {\n\t\tcons.Rotate = core.Atoi(rotate)\n\t}\n\n\tif scale := query.Get(\"scale\"); scale != \"\" {\n\t\tif sx, sy, ok := strings.Cut(scale, \":\"); ok {\n\t\t\tcons.ScaleX = core.Atoi(sx)\n\t\t\tcons.ScaleY = core.Atoi(sy)\n\t\t}\n\t}\n\n\theader := w.Header()\n\theader.Set(\"Content-Type\", mp4.ContentType(cons.Codecs()))\n\n\tif filename := query.Get(\"filename\"); filename != \"\" {\n\t\theader.Set(\"Content-Disposition\", `attachment; filename=\"`+filename+`\"`)\n\t}\n\n\tctx := r.Context() // handle when the client drops the connection\n\n\tif i := core.Atoi(query.Get(\"duration\")); i > 0 {\n\t\ttimeout := time.Second * time.Duration(i)\n\t\tvar cancel context.CancelFunc\n\t\tctx, cancel = context.WithTimeout(ctx, timeout)\n\t\tdefer cancel()\n\t}\n\n\tgo func() {\n\t\t<-ctx.Done()\n\t\t_ = cons.Stop()\n\t\tstream.RemoveConsumer(cons)\n\t}()\n\n\t_, _ = cons.WriteTo(w)\n}\n"
  },
  {
    "path": "internal/mp4/ws.go",
    "content": "package mp4\n\nimport (\n\t\"errors\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/internal/api/ws\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/mp4\"\n)\n\nfunc handlerWSMSE(tr *ws.Transport, msg *ws.Message) error {\n\tstream, _ := streams.GetOrPatch(tr.Request.URL.Query())\n\tif stream == nil {\n\t\treturn errors.New(api.StreamNotFound)\n\t}\n\n\tvar medias []*core.Media\n\tif codecs := msg.String(); codecs != \"\" {\n\t\tlog.Trace().Str(\"codecs\", codecs).Msgf(\"[mp4] new WS/MSE consumer\")\n\t\tmedias = mp4.ParseCodecs(codecs, true)\n\t}\n\n\tcons := mp4.NewConsumer(medias)\n\tcons.FormatName = \"mse/fmp4\"\n\tcons.WithRequest(tr.Request)\n\n\tif err := stream.AddConsumer(cons); err != nil {\n\t\tlog.Debug().Err(err).Msg(\"[mp4] add consumer\")\n\t\treturn err\n\t}\n\n\ttr.Write(&ws.Message{Type: \"mse\", Value: mp4.ContentType(cons.Codecs())})\n\n\tgo cons.WriteTo(tr.Writer())\n\n\ttr.OnClose(func() {\n\t\tstream.RemoveConsumer(cons)\n\t})\n\n\treturn nil\n}\n\nfunc handlerWSMP4(tr *ws.Transport, msg *ws.Message) error {\n\tstream, _ := streams.GetOrPatch(tr.Request.URL.Query())\n\tif stream == nil {\n\t\treturn errors.New(api.StreamNotFound)\n\t}\n\n\tvar medias []*core.Media\n\tif codecs := msg.String(); codecs != \"\" {\n\t\tlog.Trace().Str(\"codecs\", codecs).Msgf(\"[mp4] new WS/MP4 consumer\")\n\t\tmedias = mp4.ParseCodecs(codecs, false)\n\t}\n\n\tcons := mp4.NewKeyframe(medias)\n\tcons.WithRequest(tr.Request)\n\n\tif err := stream.AddConsumer(cons); err != nil {\n\t\tlog.Error().Err(err).Caller().Send()\n\t\treturn err\n\t}\n\n\ttr.Write(&ws.Message{Type: \"mse\", Value: mp4.ContentType(cons.Codecs())})\n\n\tgo cons.WriteTo(tr.Writer())\n\n\ttr.OnClose(func() {\n\t\tstream.RemoveConsumer(cons)\n\t})\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/mpeg/README.md",
    "content": "# MPEG\n\nThis module provides an [HTTP API](../api/README.md) for:\n \n- Streaming output in `mpegts` format.\n- Streaming output in `adts` format.\n- Streaming ingest in `mpegts` format.\n\n## MPEG-TS Server\n\n```shell\nffplay http://localhost:1984/api/stream.ts?src=camera1\n```\n\n## ADTS Server\n\n```shell\nffplay http://localhost:1984/api/stream.aac?src=camera1\n```\n\n## Streaming ingest\n\n```shell\nffmpeg -re -i BigBuckBunny.mp4 -c copy -f mpegts http://localhost:1984/api/stream.ts?dst=camera1\n```\n"
  },
  {
    "path": "internal/mpeg/mpeg.go",
    "content": "package mpeg\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/aac\"\n\t\"github.com/AlexxIT/go2rtc/pkg/mpegts\"\n)\n\nfunc Init() {\n\tapi.HandleFunc(\"api/stream.ts\", apiHandle)\n\tapi.HandleFunc(\"api/stream.aac\", apiStreamAAC)\n}\n\nfunc apiHandle(w http.ResponseWriter, r *http.Request) {\n\tif r.Method != \"POST\" {\n\t\toutputMpegTS(w, r)\n\t} else {\n\t\tinputMpegTS(w, r)\n\t}\n}\n\nfunc outputMpegTS(w http.ResponseWriter, r *http.Request) {\n\tsrc := r.URL.Query().Get(\"src\")\n\tstream := streams.Get(src)\n\tif stream == nil {\n\t\thttp.Error(w, api.StreamNotFound, http.StatusNotFound)\n\t\treturn\n\t}\n\n\tcons := mpegts.NewConsumer()\n\tcons.WithRequest(r)\n\n\tif err := stream.AddConsumer(cons); err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tw.Header().Add(\"Content-Type\", \"video/mp2t\")\n\n\t_, _ = cons.WriteTo(w)\n\n\tstream.RemoveConsumer(cons)\n}\n\nfunc inputMpegTS(w http.ResponseWriter, r *http.Request) {\n\tdst := r.URL.Query().Get(\"dst\")\n\tstream := streams.Get(dst)\n\tif stream == nil {\n\t\thttp.Error(w, api.StreamNotFound, http.StatusNotFound)\n\t\treturn\n\t}\n\n\tclient, err := mpegts.Open(r.Body)\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tstream.AddProducer(client)\n\tdefer stream.RemoveProducer(client)\n\n\tif err = client.Start(); err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n}\n\nfunc apiStreamAAC(w http.ResponseWriter, r *http.Request) {\n\tsrc := r.URL.Query().Get(\"src\")\n\tstream := streams.Get(src)\n\tif stream == nil {\n\t\thttp.Error(w, api.StreamNotFound, http.StatusNotFound)\n\t\treturn\n\t}\n\n\tcons := aac.NewConsumer()\n\tcons.WithRequest(r)\n\n\tif err := stream.AddConsumer(cons); err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tw.Header().Add(\"Content-Type\", \"audio/aac\")\n\n\t_, _ = cons.WriteTo(w)\n\n\tstream.RemoveConsumer(cons)\n}\n"
  },
  {
    "path": "internal/multitrans/README.md",
    "content": "# TP-Link MULTITRANS\n\n[`new in v1.9.14`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.9.14) by [@forrestsocool](https://github.com/forrestsocool)\n\nTwo-way audio support for Chinese version of [TP-Link](https://www.tp-link.com.cn/) cameras.\n\n## Configuration\n\n```yaml\nstreams:\n  tplink_cam:\n    # video uses standard RTSP\n    - rtsp://admin:admin@192.168.1.202:554/stream1\n    # two-way audio uses MULTITRANS schema\n    - multitrans://admin:admin@192.168.1.202:554\n```\n\n## Useful links\n\n- https://www.tp-link.com.cn/list_2549.html\n- https://github.com/AlexxIT/go2rtc/issues/1724\n- https://github.com/bingooo/hass-tplink-ipc/\n"
  },
  {
    "path": "internal/multitrans/multitrans.go",
    "content": "package multitrans\n\nimport (\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/multitrans\"\n)\n\nfunc Init() {\n\tstreams.HandleFunc(\"multitrans\", multitrans.Dial)\n}\n"
  },
  {
    "path": "internal/nest/README.md",
    "content": "# Google Nest\n\n[`new in v1.6.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.0)\n\nFor simplicity, it is recommended to connect the Nest/WebRTC camera to the [Home Assistant](../hass/README.md). \nBut if you can somehow get the below parameters, Nest/WebRTC source will work without Home Assistant.\n\n```yaml\nstreams:\n  nest-doorbell: nest:?client_id=***&client_secret=***&refresh_token=***&project_id=***&device_id=***\n```\n"
  },
  {
    "path": "internal/nest/init.go",
    "content": "package nest\n\nimport (\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/nest\"\n)\n\nfunc Init() {\n\tstreams.HandleFunc(\"nest\", func(source string) (core.Producer, error) {\n\t\treturn nest.Dial(source)\n\t})\n\n\tapi.HandleFunc(\"api/nest\", apiNest)\n}\n\nfunc apiNest(w http.ResponseWriter, r *http.Request) {\n\tquery := r.URL.Query()\n\tcliendID := query.Get(\"client_id\")\n\tcliendSecret := query.Get(\"client_secret\")\n\trefreshToken := query.Get(\"refresh_token\")\n\tprojectID := query.Get(\"project_id\")\n\n\tnestAPI, err := nest.NewAPI(cliendID, cliendSecret, refreshToken)\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tdevices, err := nestAPI.GetDevices(projectID)\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tvar items []*api.Source\n\n\tfor _, device := range devices {\n\t\tquery.Set(\"device_id\", device.DeviceID)\n\t\tquery.Set(\"protocols\", strings.Join(device.Protocols, \",\"))\n\n\t\titems = append(items, &api.Source{\n\t\t\tName: device.Name, URL: \"nest:?\" + query.Encode(),\n\t\t})\n\t}\n\n\tapi.ResponseSources(w, items)\n}\n"
  },
  {
    "path": "internal/ngrok/README.md",
    "content": "# ngrok\n\nWith the ngrok integration, you can get external access to your streams when your Internet connection is behind a private IP address.\n\n- you may need external access for two different things:\n    - WebRTC streams (tunnel the WebRTC TCP port, e.g. 8555)\n    - go2rtc web interface (tunnel the API HTTP port, e.g. 1984)\n- ngrok supports authorization for your web interface\n- ngrok automatically adds HTTPS to your web interface\n\nThe ngrok free subscription has the following limitations:\n\n- You can reserve a free domain for serving the web interface, but the TCP address you get will always be random and will change with each restart of the ngrok agent (not a problem for WebRTC streams)\n- You can forward multiple ports from a single agent, but you can only run one ngrok agent on the free plan\n\ngo2rtc will automatically get your external TCP address (if you enable it in the ngrok config) and use it for WebRTC connections (if you enable it in the WebRTC config).\n\nYou need to manually download the [ngrok agent](https://ngrok.com/download) for your OS and register with the [ngrok service](https://ngrok.com/signup).\n\n**Tunnel for only WebRTC Stream**\n\nYou need to add your [ngrok authtoken](https://dashboard.ngrok.com/get-started/your-authtoken) and WebRTC TCP port to YAML:\n\n```yaml\nngrok:\n  command: ngrok tcp 8555 --authtoken eW91IHNoYWxsIG5vdCBwYXNzCnlvdSBzaGFsbCBub3QgcGFzcw\n```\n\n**Tunnel for WebRTC and Web interface**\n\nYou need to create `ngrok.yaml` config file and add it to the go2rtc config:\n\n```yaml\nngrok:\n  command: ngrok start --all --config ngrok.yaml\n```\n\nngrok config example:\n\n```yaml\nversion: \"2\"\nauthtoken: eW91IHNoYWxsIG5vdCBwYXNzCnlvdSBzaGFsbCBub3QgcGFzcw\ntunnels:\n  api:\n    addr: 1984  # use the same port as in the go2rtc config\n    proto: http\n    basic_auth:\n      - admin:password  # you can set login/pass for your web interface\n  webrtc:\n    addr: 8555  # use the same port as in the go2rtc config\n    proto: tcp\n```\n\nSee the [ngrok agent documentation](https://ngrok.com/docs/agent/config/) for more details on the ngrok configuration file.\n"
  },
  {
    "path": "internal/ngrok/ngrok.go",
    "content": "package ngrok\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/app\"\n\t\"github.com/AlexxIT/go2rtc/internal/webrtc\"\n\t\"github.com/AlexxIT/go2rtc/pkg/ngrok\"\n\t\"github.com/rs/zerolog\"\n)\n\nfunc Init() {\n\tvar cfg struct {\n\t\tMod struct {\n\t\t\tCmd string `yaml:\"command\"`\n\t\t} `yaml:\"ngrok\"`\n\t}\n\n\tapp.LoadConfig(&cfg)\n\n\tif cfg.Mod.Cmd == \"\" {\n\t\treturn\n\t}\n\n\tlog = app.GetLogger(\"ngrok\")\n\n\tngr, err := ngrok.NewNgrok(cfg.Mod.Cmd)\n\tif err != nil {\n\t\tlog.Error().Err(err).Msg(\"[ngrok] start\")\n\t}\n\n\tngr.Listen(func(msg any) {\n\t\tif msg := msg.(*ngrok.Message); msg != nil {\n\t\t\tif strings.HasPrefix(msg.Line, \"ERROR:\") {\n\t\t\t\tlog.Warn().Msg(\"[ngrok] \" + msg.Line)\n\t\t\t} else {\n\t\t\t\tlog.Debug().Msg(\"[ngrok] \" + msg.Line)\n\t\t\t}\n\n\t\t\t// Addr: \"//localhost:8555\", URL: \"tcp://1.tcp.eu.ngrok.io:12345\"\n\t\t\tif strings.HasPrefix(msg.Addr, \"//localhost:\") && strings.HasPrefix(msg.URL, \"tcp://\") {\n\t\t\t\t// don't know if really necessary use IP\n\t\t\t\taddress, err := ConvertHostToIP(msg.URL[6:])\n\t\t\t\tif err != nil {\n\t\t\t\t\tlog.Warn().Err(err).Msg(\"[ngrok] add candidate\")\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tlog.Info().Str(\"addr\", address).Msg(\"[ngrok] add external candidate for WebRTC\")\n\n\t\t\t\twebrtc.AddCandidate(\"tcp\", address)\n\t\t\t}\n\t\t}\n\t})\n\n\tgo func() {\n\t\tif err = ngr.Serve(); err != nil {\n\t\t\tlog.Error().Err(err).Msg(\"[ngrok] run\")\n\t\t}\n\t}()\n\n}\n\nvar log zerolog.Logger\n\nfunc ConvertHostToIP(address string) (string, error) {\n\thost, port, err := net.SplitHostPort(address)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tip, err := net.LookupIP(host)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif len(ip) == 0 {\n\t\treturn \"\", fmt.Errorf(\"can't resolve: %s\", host)\n\t}\n\n\treturn ip[0].String() + \":\" + port, nil\n}\n"
  },
  {
    "path": "internal/onvif/README.md",
    "content": "# ONVIF\n\n## ONVIF Client\n\n[`new in v1.5.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.5.0)\n\nThe source is not very useful if you already know RTSP and snapshot links for your camera. But it can be useful if you don't.\n\n**WebUI > Add** webpage supports ONVIF autodiscovery. Your server must be on the same subnet as the camera. If you use Docker, you must use \"network host\".\n\n```yaml\nstreams:\n  dahua1: onvif://admin:password@192.168.1.123\n  reolink1: onvif://admin:password@192.168.1.123:8000\n  tapo1: onvif://admin:password@192.168.1.123:2020\n```\n\n## ONVIF Server\n\nA regular camera has a single video source (`GetVideoSources`) and two profiles (`GetProfiles`).\n\nGo2rtc has one video source and one profile per stream.\n\n## Tested clients\n\nGo2rtc works as ONVIF server:\n\n- Happytime onvif client (windows)\n- Home Assistant ONVIF integration (linux)\n- Onvier (android)\n- ONVIF Device Manager (windows)\n\nPS. Supports only TCP transport for RTSP protocol. UDP and HTTP transports - unsupported yet.\n\n## Tested cameras\n\nGo2rtc works as ONVIF client:\n\n- Dahua IPC-K42\n- OpenIPC\n- Reolink RLC-520A\n- TP-Link Tapo TC60\n"
  },
  {
    "path": "internal/onvif/onvif.go",
    "content": "package onvif\n\nimport (\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/internal/app\"\n\t\"github.com/AlexxIT/go2rtc/internal/rtsp\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/onvif\"\n\t\"github.com/rs/zerolog\"\n)\n\nfunc Init() {\n\tlog = app.GetLogger(\"onvif\")\n\n\tstreams.HandleFunc(\"onvif\", streamOnvif)\n\n\t// ONVIF server on all suburls\n\tapi.HandleFunc(\"/onvif/\", onvifDeviceService)\n\n\t// ONVIF client autodiscovery\n\tapi.HandleFunc(\"api/onvif\", apiOnvif)\n}\n\nvar log zerolog.Logger\n\nfunc streamOnvif(rawURL string) (core.Producer, error) {\n\tclient, err := onvif.NewClient(rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\turi, err := client.GetURI()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Append hash-based arguments to the retrieved URI\n\tif i := strings.IndexByte(rawURL, '#'); i > 0 {\n\t\turi += rawURL[i:]\n\t}\n\n\tlog.Debug().Msgf(\"[onvif] new uri=%s\", uri)\n\n\tif err = streams.Validate(uri); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn streams.GetProducer(uri)\n}\n\nfunc onvifDeviceService(w http.ResponseWriter, r *http.Request) {\n\tb, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\toperation := onvif.GetRequestAction(b)\n\tif operation == \"\" {\n\t\thttp.Error(w, \"malformed request body\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tlog.Trace().Msgf(\"[onvif] server request %s %s:\\n%s\", r.Method, r.RequestURI, b)\n\n\tswitch operation {\n\tcase onvif.ServiceGetServiceCapabilities, // important for Hass\n\t\tonvif.DeviceGetNetworkInterfaces, // important for Hass\n\t\tonvif.DeviceGetSystemDateAndTime, // important for Hass\n\t\tonvif.DeviceSetSystemDateAndTime, // return just OK\n\t\tonvif.DeviceGetDiscoveryMode,\n\t\tonvif.DeviceGetDNS,\n\t\tonvif.DeviceGetHostname,\n\t\tonvif.DeviceGetNetworkDefaultGateway,\n\t\tonvif.DeviceGetNetworkProtocols,\n\t\tonvif.DeviceGetNTP,\n\t\tonvif.DeviceGetScopes,\n\t\tonvif.MediaGetVideoEncoderConfiguration,\n\t\tonvif.MediaGetVideoEncoderConfigurations,\n\t\tonvif.MediaGetAudioEncoderConfigurations,\n\t\tonvif.MediaGetVideoEncoderConfigurationOptions,\n\t\tonvif.MediaGetAudioSources,\n\t\tonvif.MediaGetAudioSourceConfigurations:\n\t\tb = onvif.StaticResponse(operation)\n\n\tcase onvif.DeviceGetCapabilities:\n\t\t// important for Hass: Media section\n\t\tb = onvif.GetCapabilitiesResponse(r.Host)\n\n\tcase onvif.DeviceGetServices:\n\t\tb = onvif.GetServicesResponse(r.Host)\n\n\tcase onvif.DeviceGetDeviceInformation:\n\t\t// important for Hass: SerialNumber (unique server ID)\n\t\tb = onvif.GetDeviceInformationResponse(\"\", \"go2rtc\", app.Version, r.Host)\n\n\tcase onvif.DeviceSystemReboot:\n\t\tb = onvif.StaticResponse(operation)\n\n\t\ttime.AfterFunc(time.Second, func() {\n\t\t\tos.Exit(0)\n\t\t})\n\n\tcase onvif.MediaGetVideoSources:\n\t\tb = onvif.GetVideoSourcesResponse(streams.GetAllNames())\n\n\tcase onvif.MediaGetProfiles:\n\t\t// important for Hass: H264 codec, width, height\n\t\tb = onvif.GetProfilesResponse(streams.GetAllNames())\n\n\tcase onvif.MediaGetProfile:\n\t\ttoken := onvif.FindTagValue(b, \"ProfileToken\")\n\t\tb = onvif.GetProfileResponse(token)\n\n\tcase onvif.MediaGetVideoSourceConfigurations:\n\t\t// important for Happytime Onvif Client\n\t\tb = onvif.GetVideoSourceConfigurationsResponse(streams.GetAllNames())\n\n\tcase onvif.MediaGetVideoSourceConfiguration:\n\t\ttoken := onvif.FindTagValue(b, \"ConfigurationToken\")\n\t\tb = onvif.GetVideoSourceConfigurationResponse(token)\n\n\tcase onvif.MediaGetStreamUri:\n\t\thost, _, err := net.SplitHostPort(r.Host)\n\t\tif err != nil {\n\t\t\thost = r.Host // in case of Host without port\n\t\t}\n\n\t\turi := \"rtsp://\" + host + \":\" + rtsp.Port + \"/\" + onvif.FindTagValue(b, \"ProfileToken\")\n\t\tb = onvif.GetStreamUriResponse(uri)\n\n\tcase onvif.MediaGetSnapshotUri:\n\t\turi := \"http://\" + r.Host + \"/api/frame.jpeg?src=\" + onvif.FindTagValue(b, \"ProfileToken\")\n\t\tb = onvif.GetSnapshotUriResponse(uri)\n\n\tdefault:\n\t\thttp.Error(w, \"unsupported operation\", http.StatusBadRequest)\n\t\tlog.Warn().Msgf(\"[onvif] unsupported operation: %s\", operation)\n\t\tlog.Debug().Msgf(\"[onvif] unsupported request:\\n%s\", b)\n\t\treturn\n\t}\n\n\tlog.Trace().Msgf(\"[onvif] server response:\\n%s\", b)\n\n\tw.Header().Set(\"Content-Type\", \"application/soap+xml; charset=utf-8\")\n\tif _, err = w.Write(b); err != nil {\n\t\tlog.Error().Err(err).Caller().Send()\n\t}\n}\n\nfunc apiOnvif(w http.ResponseWriter, r *http.Request) {\n\tsrc := r.URL.Query().Get(\"src\")\n\n\tvar items []*api.Source\n\n\tif src == \"\" {\n\t\tdevices, err := onvif.DiscoveryStreamingDevices()\n\t\tif err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tfor _, device := range devices {\n\t\t\tu, err := url.Parse(device.URL)\n\t\t\tif err != nil {\n\t\t\t\tlog.Warn().Str(\"url\", device.URL).Msg(\"[onvif] broken\")\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif u.Scheme != \"http\" {\n\t\t\t\tlog.Warn().Str(\"url\", device.URL).Msg(\"[onvif] unsupported\")\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tu.Scheme = \"onvif\"\n\t\t\tu.User = url.UserPassword(\"user\", \"pass\")\n\n\t\t\tif u.Path == onvif.PathDevice {\n\t\t\t\tu.Path = \"\"\n\t\t\t}\n\n\t\t\titems = append(items, &api.Source{\n\t\t\t\tName: u.Host,\n\t\t\t\tURL:  u.String(),\n\t\t\t\tInfo: device.Name + \" \" + device.Hardware,\n\t\t\t})\n\t\t}\n\t} else {\n\t\tclient, err := onvif.NewClient(src)\n\t\tif err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tif l := log.Trace(); l.Enabled() {\n\t\t\tb, _ := client.MediaRequest(onvif.MediaGetProfiles)\n\t\t\tl.Msgf(\"[onvif] src=%s profiles:\\n%s\", src, b)\n\t\t}\n\n\t\tname, err := client.GetName()\n\t\tif err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\ttokens, err := client.GetProfilesTokens()\n\t\tif err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tfor i, token := range tokens {\n\t\t\titems = append(items, &api.Source{\n\t\t\t\tName: name + \" stream\" + strconv.Itoa(i),\n\t\t\t\tURL:  src + \"?subtype=\" + token,\n\t\t\t})\n\t\t}\n\n\t\tif len(tokens) > 0 && client.HasSnapshots() {\n\t\t\titems = append(items, &api.Source{\n\t\t\t\tName: name + \" snapshot\",\n\t\t\t\tURL:  src + \"?subtype=\" + tokens[0] + \"&snapshot\",\n\t\t\t})\n\t\t}\n\t}\n\n\tapi.ResponseSources(w, items)\n}\n"
  },
  {
    "path": "internal/pinggy/README.md",
    "content": "# Pinggy\n\n[Pinggy](https://pinggy.io/) - nice service for public tunnels to your local services.\n\n**Features:**\n\n- A free account does not require registration.\n- It does not require downloading third-party binaries and works over the SSH protocol.\n- Works with HTTP, TCP and UDP protocols.\n- Creates HTTPS for your HTTP services.\n\n> [!IMPORTANT]\n> A free account creates a tunnel with a random address that only works for an hour. It is suitable for testing purposes ONLY.\n\n> [!CAUTION]\n> Public access to go2rtc without authorization puts your entire home network at risk. Use with caution.\n\n**Why:**\n\n- It's easy to set up HTTPS for testing two-way audio.\n- It's easy to check whether external access via WebRTC technology will work.\n- It's easy to share direct access to your RTSP or HTTP camera with the go2rtc developer. If such access is necessary to debug your problem.\n\n## Configuration\n\nYou will find public links in the go2rtc log after startup.\n\n**Tunnel to go2rtc WebUI.**\n\n```yaml\npinggy:\n  tunnel: http://localhost:1984\n```\n\n**Tunnel to RTSP camera.**\n\nFor example, you have camera: `rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0`\n\n```yaml\npinggy:\n  tunnel: tcp://192.168.10.91:554\n```\n\nIn go2rtc logs you will get similar output:\n\n```\n16:17:43.167 INF [pinggy] proxy url=tcp://abcde-123-123-123-123.a.free.pinggy.link:12345\n```\n\nNow you have a working stream:\n\n```\nrtsp://admin:password@abcde-123-123-123-123.a.free.pinggy.link:12345/cam/realmonitor?channel=1&subtype=0\n```\n"
  },
  {
    "path": "internal/pinggy/pinggy.go",
    "content": "package pinggy\n\nimport (\n\t\"net/url\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/app\"\n\t\"github.com/AlexxIT/go2rtc/pkg/pinggy\"\n\t\"github.com/rs/zerolog\"\n)\n\nfunc Init() {\n\tvar cfg struct {\n\t\tMod struct {\n\t\t\tTunnel string `yaml:\"tunnel\"`\n\t\t} `yaml:\"pinggy\"`\n\t}\n\n\tapp.LoadConfig(&cfg)\n\n\tif cfg.Mod.Tunnel == \"\" {\n\t\treturn\n\t}\n\n\tlog = app.GetLogger(\"pinggy\")\n\n\tu, err := url.Parse(cfg.Mod.Tunnel)\n\tif err != nil {\n\t\tlog.Error().Err(err).Send()\n\t\treturn\n\t}\n\n\tgo proxy(u.Scheme, u.Host)\n}\n\nvar log zerolog.Logger\n\nfunc proxy(proto, address string) {\n\tclient, err := pinggy.NewClient(proto)\n\tif err != nil {\n\t\tlog.Error().Err(err).Send()\n\t\treturn\n\t}\n\tdefer client.Close()\n\n\turls, err := client.GetURLs()\n\tif err != nil {\n\t\tlog.Error().Err(err).Send()\n\t\treturn\n\t}\n\n\tfor _, s := range urls {\n\t\tlog.Info().Str(\"url\", s).Msgf(\"[pinggy] proxy\")\n\t}\n\n\terr = client.Proxy(address)\n\tif err != nil {\n\t\tlog.Error().Err(err).Send()\n\t\treturn\n\t}\n}\n"
  },
  {
    "path": "internal/ring/README.md",
    "content": "# Ring\n\n[`new in v1.9.13`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.9.13) by [@seydx](https://github.com/seydx)\n\nThis source type supports Ring cameras with [two-way audio](../../README.md#two-way-audio) support.\n\n## Configuration\n\nIf you have a `refresh_token` and `device_id`, you can use them in the `go2rtc.yaml` config file.\n\nOtherwise, you can use the go2rtc web interface and add your Ring account (WebUI > Add > Ring). Once added, it will list all your Ring cameras.\n\n```yaml\nstreams:\n  ring: ring:?device_id=XXX&refresh_token=XXX\n  ring_snapshot: ring:?device_id=XXX&refresh_token=XXX&snapshot\n```\n"
  },
  {
    "path": "internal/ring/ring.go",
    "content": "package ring\n\nimport (\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"fmt\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/ring\"\n)\n\nfunc Init() {\n\tstreams.HandleFunc(\"ring\", func(source string) (core.Producer, error) {\n\t\treturn ring.Dial(source)\n\t})\n\n\tapi.HandleFunc(\"api/ring\", apiRing)\n}\n\nfunc apiRing(w http.ResponseWriter, r *http.Request) {\n\tquery := r.URL.Query()\n\tvar ringAPI *ring.RingApi\n\n\t// Check auth method\n\tif email := query.Get(\"email\"); email != \"\" {\n\t\t// Email/Password Flow\n\t\tpassword := query.Get(\"password\")\n\t\tcode := query.Get(\"code\")\n\n\t\tvar err error\n\t\tringAPI, err = ring.NewRestClient(ring.EmailAuth{\n\t\t\tEmail:    email,\n\t\t\tPassword: password,\n\t\t}, nil)\n\n\t\tif err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\t// Try authentication (this will trigger 2FA if needed)\n\t\tif _, err = ringAPI.GetAuth(code); err != nil {\n\t\t\tif ringAPI.Using2FA {\n\t\t\t\t// Return 2FA prompt\n\t\t\t\tapi.ResponseJSON(w, map[string]interface{}{\n\t\t\t\t\t\"needs_2fa\": true,\n\t\t\t\t\t\"prompt\":    ringAPI.PromptFor2FA,\n\t\t\t\t})\n\t\t\t\treturn\n\t\t\t}\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t} else if refreshToken := query.Get(\"refresh_token\"); refreshToken != \"\" {\n\t\t// Refresh Token Flow\n\t\tif refreshToken == \"\" {\n\t\t\thttp.Error(w, \"either email/password or refresh_token is required\", http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\tvar err error\n\t\tringAPI, err = ring.NewRestClient(ring.RefreshTokenAuth{\n\t\t\tRefreshToken: refreshToken,\n\t\t}, nil)\n\n\t\tif err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t} else {\n\t\thttp.Error(w, \"either email/password or refresh token is required\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tdevices, err := ringAPI.FetchRingDevices()\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tcleanQuery := url.Values{}\n\tcleanQuery.Set(\"refresh_token\", ringAPI.RefreshToken)\n\n\tvar items []*api.Source\n\tfor _, camera := range devices.AllCameras {\n\t\tcleanQuery.Set(\"camera_id\", fmt.Sprint(camera.ID))\n\t\tcleanQuery.Set(\"device_id\", camera.DeviceID)\n\n\t\t// Stream source\n\t\titems = append(items, &api.Source{\n\t\t\tName: camera.Description,\n\t\t\tURL:  \"ring:?\" + cleanQuery.Encode(),\n\t\t})\n\n\t\t// Snapshot source\n\t\titems = append(items, &api.Source{\n\t\t\tName: camera.Description + \" Snapshot\",\n\t\t\tURL:  \"ring:?\" + cleanQuery.Encode() + \"&snapshot\",\n\t\t})\n\t}\n\n\tapi.ResponseSources(w, items)\n}\n"
  },
  {
    "path": "internal/roborock/README.md",
    "content": "# Roborock\n\n[`new in v1.3.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)\n\nThis source type supports Roborock vacuums with cameras. Known working models:\n\n- **Roborock S6 MaxV** - only video (the vacuum has no microphone)\n- **Roborock S7 MaxV** - video and two-way audio\n- **Roborock Qrevo MaxV** - video and two-way audio\n\n## Configuration\n\nThis source supports loading Roborock credentials from the Home Assistant [custom integration](https://github.com/humbertogontijo/homeassistant-roborock) or the [core integration](https://www.home-assistant.io/integrations/roborock). Otherwise, you need to log in to your Roborock account (MiHome account is not supported). Go to go2rtc WebUI > Add webpage. Copy the `roborock://...` source for your vacuum and paste it into your `go2rtc.yaml` config.\n\nIf you have a pattern PIN for your vacuum, add it as a numeric PIN (lines: 123, 456, 789) to the end of the `roborock` link.\n"
  },
  {
    "path": "internal/roborock/roborock.go",
    "content": "package roborock\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/roborock\"\n)\n\nfunc Init() {\n\tstreams.HandleFunc(\"roborock\", func(source string) (core.Producer, error) {\n\t\treturn roborock.Dial(source)\n\t})\n\n\tapi.HandleFunc(\"api/roborock\", apiHandle)\n}\n\nvar Auth struct {\n\tUserData *roborock.UserInfo `json:\"user_data\"`\n\tBaseURL  string             `json:\"base_url\"`\n}\n\nfunc apiHandle(w http.ResponseWriter, r *http.Request) {\n\tswitch r.Method {\n\tcase \"GET\":\n\t\tif Auth.UserData == nil {\n\t\t\thttp.Error(w, \"no auth\", http.StatusNotFound)\n\t\t\treturn\n\t\t}\n\n\tcase \"POST\":\n\t\tif err := r.ParseMultipartForm(1024); err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tusername := r.Form.Get(\"username\")\n\t\tpassword := r.Form.Get(\"password\")\n\t\tif username == \"\" || password == \"\" {\n\t\t\thttp.Error(w, \"empty username or password\", http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\tbase, err := roborock.GetBaseURL(username)\n\t\tif err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tui, err := roborock.Login(base, username, password)\n\t\tif err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tAuth.BaseURL = base\n\t\tAuth.UserData = ui\n\n\tdefault:\n\t\thttp.Error(w, \"\", http.StatusMethodNotAllowed)\n\t\treturn\n\t}\n\n\thomeID, err := roborock.GetHomeID(Auth.BaseURL, Auth.UserData.Token)\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tdevices, err := roborock.GetDevices(Auth.UserData, homeID)\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tvar items []*api.Source\n\n\tfor _, device := range devices {\n\t\tsource := fmt.Sprintf(\n\t\t\t\"roborock://%s?u=%s&s=%s&k=%s&did=%s&key=%s&pin=\",\n\t\t\tAuth.UserData.IoT.URL.MQTT[6:],\n\t\t\tAuth.UserData.IoT.User, Auth.UserData.IoT.Pass, Auth.UserData.IoT.Domain,\n\t\t\tdevice.DID, device.Key,\n\t\t)\n\t\titems = append(items, &api.Source{Name: device.Name, URL: source})\n\t}\n\n\tapi.ResponseSources(w, items)\n}\n"
  },
  {
    "path": "internal/rtmp/README.md",
    "content": "# Real-Time Messaging Protocol\n\nThis module provides the following features for the RTMP protocol:\n\n- Streaming input - [RTMP client](#rtmp-client)\n- Streaming output and ingest in `rtmp` format - [RTMP server](#rtmp-server)\n- Streaming output and ingest in `flv` format - [FLV server](#flv-server)\n\n## RTMP Client\n\nYou can get a stream from an RTMP server, for example [Nginx with nginx-rtmp-module](https://github.com/arut/nginx-rtmp-module).\n\n### Client Configuration\n\n```yaml\nstreams:\n  rtmp_stream: rtmp://192.168.1.123/live/camera1\n```\n\n## RTMP Server\n\n[`new in v1.8.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.8.0)\n\nStreaming output stream in `rtmp` format:\n\n```shell\nffplay rtmp://localhost:1935/camera1\n```\n\nStreaming ingest stream in `rtmp` format:\n\n```shell\nffmpeg -re -i BigBuckBunny.mp4 -c copy -f flv rtmp://localhost:1935/camera1\n```\n\n### Server Configuration\n\nBy default, the RTMP server is disabled.\n\n```yaml\nrtmp:\n  listen: \":1935\"  # by default - disabled!\n```\n\n## FLV Server\n\nStreaming output in `flv` format.\n\n```shell\nffplay http://localhost:1984/stream.flv?src=camera1\n```\n\nStreaming ingest in `flv` format.\n\n```shell\nffmpeg -re -i BigBuckBunny.mp4 -c copy -f flv http://localhost:1984/api/stream.flv?dst=camera1\n```\n\n## Tested clients\n\n| From   | To                              | Comment |\n|--------|---------------------------------|---------|\n| go2rtc | Reolink RLC-520A fw. v3.1.0.801 | OK      |\n\n**go2rtc.yaml**\n\n```yaml\nstreams:\n  rtmp-reolink1: rtmp://192.168.10.92/bcs/channel0_main.bcs?channel=0&stream=0&user=admin&password=password\n  rtmp-reolink2: rtmp://192.168.10.92/bcs/channel0_sub.bcs?channel=0&stream=1&user=admin&password=password\n  rtmp-reolink3: rtmp://192.168.10.92/bcs/channel0_ext.bcs?channel=0&stream=1&user=admin&password=password\n```\n\n## Tested server\n\n| From                   | To     | Comment             |\n|------------------------|--------|---------------------|\n| OBS 31.0.2             | go2rtc | OK                  |\n| OpenIPC 2.5.03.02-lite | go2rtc | OK                  |\n| FFmpeg 6.1             | go2rtc | OK                  |\n| GoPro Black 12         | go2rtc | OK, 1080p, 5000kbps |\n\n**go2rtc.yaml**\n\n```yaml\nrtmp:\n  listen: :1935\nstreams:\n  tmp:\n```\n\n**OBS**\n \nSettings > Stream:\n\n- Service: Custom\n- Server: rtmp://192.168.10.101/tmp\n- Stream Key: `<empty>`\n- Use auth: `<disabled>`\n\n**OpenIPC**\n\nWebUI > Majestic > Settings > Outgoing\n\n- Enable\n- Address: rtmp://192.168.10.101/tmp\n- Save\n- Restart\n\n**FFmpeg**\n\n```shell\nffmpeg -re -i bbb.mp4 -c copy -f flv rtmp://192.168.10.101/tmp\n```\n\n**GoPro**\n\nGoPro Quik > Camera > Translation > Other\n"
  },
  {
    "path": "internal/rtmp/rtmp.go",
    "content": "package rtmp\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/internal/app\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/flv\"\n\t\"github.com/AlexxIT/go2rtc/pkg/rtmp\"\n\t\"github.com/rs/zerolog\"\n)\n\nfunc Init() {\n\tvar conf struct {\n\t\tMod struct {\n\t\t\tListen string `yaml:\"listen\" json:\"listen\"`\n\t\t} `yaml:\"rtmp\"`\n\t}\n\n\tapp.LoadConfig(&conf)\n\n\tlog = app.GetLogger(\"rtmp\")\n\n\tstreams.HandleFunc(\"rtmp\", streamsHandle)\n\tstreams.HandleFunc(\"rtmps\", streamsHandle)\n\tstreams.HandleFunc(\"rtmpx\", streamsHandle)\n\n\tapi.HandleFunc(\"api/stream.flv\", apiHandle)\n\n\tstreams.HandleConsumerFunc(\"rtmp\", streamsConsumerHandle)\n\tstreams.HandleConsumerFunc(\"rtmps\", streamsConsumerHandle)\n\tstreams.HandleConsumerFunc(\"rtmpx\", streamsConsumerHandle)\n\n\taddress := conf.Mod.Listen\n\tif address == \"\" {\n\t\treturn\n\t}\n\n\tln, err := net.Listen(\"tcp\", address)\n\tif err != nil {\n\t\tlog.Error().Err(err).Caller().Send()\n\t\treturn\n\t}\n\n\tlog.Info().Str(\"addr\", address).Msg(\"[rtmp] listen\")\n\n\tgo func() {\n\t\tfor {\n\t\t\tconn, err := ln.Accept()\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tgo func() {\n\t\t\t\tif err = tcpHandle(conn); err != nil {\n\t\t\t\t\tlog.Error().Err(err).Caller().Send()\n\t\t\t\t}\n\t\t\t}()\n\t\t}\n\t}()\n}\n\nfunc tcpHandle(netConn net.Conn) error {\n\trtmpConn, err := rtmp.NewServer(netConn)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err = rtmpConn.ReadCommands(); err != nil {\n\t\treturn err\n\t}\n\n\tswitch rtmpConn.Intent {\n\tcase rtmp.CommandPlay:\n\t\tstream := streams.Get(rtmpConn.App)\n\t\tif stream == nil {\n\t\t\treturn errors.New(\"stream not found: \" + rtmpConn.App)\n\t\t}\n\n\t\tcons := flv.NewConsumer()\n\t\tif err = stream.AddConsumer(cons); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tdefer stream.RemoveConsumer(cons)\n\n\t\tif err = rtmpConn.WriteStart(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t_, _ = cons.WriteTo(rtmpConn)\n\n\t\treturn nil\n\n\tcase rtmp.CommandPublish:\n\t\tstream := streams.Get(rtmpConn.App)\n\t\tif stream == nil {\n\t\t\treturn errors.New(\"stream not found: \" + rtmpConn.App)\n\t\t}\n\n\t\tif err = rtmpConn.WriteStart(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tprod, err := rtmpConn.Producer()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tstream.AddProducer(prod)\n\n\t\tdefer stream.RemoveProducer(prod)\n\n\t\t_ = prod.Start()\n\n\t\treturn nil\n\t}\n\n\treturn errors.New(\"rtmp: unknown command: \" + rtmpConn.Intent)\n}\n\nvar log zerolog.Logger\n\nfunc streamsHandle(url string) (core.Producer, error) {\n\treturn rtmp.DialPlay(url)\n}\n\nfunc streamsConsumerHandle(url string) (core.Consumer, func(), error) {\n\tcons := flv.NewConsumer()\n\trun := func() {\n\t\twr, err := rtmp.DialPublish(url, cons)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\t_, err = cons.WriteTo(wr)\n\t}\n\n\treturn cons, run, nil\n}\n\nfunc apiHandle(w http.ResponseWriter, r *http.Request) {\n\tif r.Method != \"POST\" {\n\t\toutputFLV(w, r)\n\t} else {\n\t\tinputFLV(w, r)\n\t}\n}\n\nfunc outputFLV(w http.ResponseWriter, r *http.Request) {\n\tsrc := r.URL.Query().Get(\"src\")\n\tstream := streams.Get(src)\n\tif stream == nil {\n\t\thttp.Error(w, api.StreamNotFound, http.StatusNotFound)\n\t\treturn\n\t}\n\n\tcons := flv.NewConsumer()\n\tcons.WithRequest(r)\n\n\tif err := stream.AddConsumer(cons); err != nil {\n\t\tlog.Error().Err(err).Caller().Send()\n\t\treturn\n\t}\n\n\th := w.Header()\n\th.Set(\"Content-Type\", \"video/x-flv\")\n\n\t_, _ = cons.WriteTo(w)\n\n\tstream.RemoveConsumer(cons)\n}\n\nfunc inputFLV(w http.ResponseWriter, r *http.Request) {\n\tdst := r.URL.Query().Get(\"dst\")\n\tstream := streams.Get(dst)\n\tif stream == nil {\n\t\thttp.Error(w, api.StreamNotFound, http.StatusNotFound)\n\t\treturn\n\t}\n\n\tclient, err := flv.Open(r.Body)\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tstream.AddProducer(client)\n\n\tif err = client.Start(); err != nil && err != io.EOF {\n\t\tlog.Warn().Err(err).Caller().Send()\n\t}\n\n\tstream.RemoveProducer(client)\n}\n"
  },
  {
    "path": "internal/rtsp/README.md",
    "content": "# Real Time Streaming Protocol\n\nThis module provides the following features for the RTSP protocol:\n\n - Streaming input - [RTSP client](#rtsp-client)\n - Streaming output - [RTSP server](#rtsp-server)\n - [Streaming ingest](#streaming-ingest)\n - [Two-way audio](#two-way-audio)\n\n## RTSP Client\n\n### Configuration\n\n```yaml\nstreams:\n  sonoff_camera: rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0\n  dahua_camera:\n    - rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif\n    - rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=1#backchannel=0\n  amcrest_doorbell:\n    - rtsp://username:password@192.168.1.123:554/cam/realmonitor?channel=1&subtype=0#backchannel=0\n  unifi_camera: rtspx://192.168.1.123:7441/fD6ouM72bWoFijxK\n  glichy_camera: ffmpeg:rtsp://username:password@192.168.1.123/live/ch00_1 \n```\n\n### Recommendations\n\n- **Amcrest Doorbell** users may want to disable two-way audio, because with an active stream, you won't have a working call button. You need to add `#backchannel=0` to the end of your RTSP link in YAML config file\n- **Dahua Doorbell** users may want to change [audio codec](https://github.com/AlexxIT/go2rtc/issues/49#issuecomment-2127107379) for proper two-way audio. Make sure not to request backchannel multiple times by adding `#backchannel=0` to other stream sources of the same doorbell. The `unicast=true&proto=Onvif` is preferred for two-way audio as this makes the doorbell accept multiple codecs for the incoming audio\n- **Reolink** users may want NOT to use RTSP protocol at all, some camera models have a very awful, unusable stream implementation\n- **Ubiquiti UniFi** users may want to disable HTTPS verification. Use `rtspx://` prefix instead of `rtsps://`. And don't use `?enableSrtp` [suffix](https://github.com/AlexxIT/go2rtc/issues/81)\n- **TP-Link Tapo** users may skip login and password, because go2rtc supports login [without them](https://drmnsamoliu.github.io/video.html)\n- If your camera has two RTSP links, you can add both as sources. This is useful when streams have different codecs, for example AAC audio with main stream and PCMU/PCMA audio with second stream\n- If the stream from your camera is glitchy, try using [ffmpeg source](../ffmpeg/README.md). It will not add CPU load if you don't use transcoding\n- If the stream from your camera is very glitchy, try to use transcoding with [ffmpeg source](../ffmpeg/README.md)\n\n### Other options\n\nFormat: `rtsp...#{param1}#{param2}#{param3}`\n\n- Add custom timeout `#timeout=30` (in seconds)\n- Ignore audio - `#media=video` or ignore video - `#media=audio`\n- Ignore two-way audio API `#backchannel=0` - important for some glitchy cameras\n- Use WebSocket transport `#transport=ws...`\n\n### RTSP over WebSocket\n\n```yaml\nstreams:\n  # WebSocket with authorization, RTSP - without\n  axis-rtsp-ws:  rtsp://192.168.1.123:4567/axis-media/media.amp?overview=0&camera=1&resolution=1280x720&videoframeskipmode=empty&Axis-Orig-Sw=true#transport=ws://user:pass@192.168.1.123:4567/rtsp-over-websocket\n  # WebSocket without authorization, RTSP - with\n  dahua-rtsp-ws: rtsp://user:pass@192.168.1.123/cam/realmonitor?channel=1&subtype=1&proto=Private3#transport=ws://192.168.1.123/rtspoverwebsocket\n```\n\n## RTSP Server\n\nYou can get any stream as RTSP-stream: `rtsp://192.168.1.123:8554/{stream_name}`\n\nYou can enable external password protection for your RTSP streams. Password protection is always disabled for localhost calls (ex. FFmpeg or Home Assistant on the same server).\n\n### Configuration\n\n```yaml\nrtsp:\n  listen: \":8554\"    # RTSP Server TCP port, default - 8554\n  username: \"admin\"  # optional, default - disabled\n  password: \"pass\"   # optional, default - disabled\n  default_query: \"video&audio\"  # optional, default codecs filters \n```\n\nBy default go2rtc provide RTSP-stream with only one first video and only one first audio. You can change it with the `default_query` setting:\n\n- `default_query: \"mp4\"` - MP4 compatible codecs (H264, H265, AAC)\n- `default_query: \"video=all&audio=all\"` - all tracks from all source (not all players can handle this)\n- `default_query: \"video=h264,h265\"` - only one video track (H264 or H265)\n- `default_query: \"video&audio=all\"` - only one first any video and all audio as separate tracks\n\nRead more about [codecs filters](../../README.md#codecs-filters).\n\n## Streaming ingest\n\n```shell\nffmpeg -re -i BigBuckBunny.mp4 -c copy -rtsp_transport tcp -f rtsp rtsp://localhost:8554/camera1\n```\n\n## Two-way audio\n\nBefore purchasing, it is difficult to understand whether the camera supports two-way audio via the RTSP protocol or not. This isn't usually mentioned in a camera's description. You can only find out by reading reviews from real buyers.\n\nA camera is considered to support two-way audio if it supports the ONVIF Profile T protocol. But in reality, this isn't always the case. And the ONVIF protocol has no connection with the camera's RTSP implementation.\n\nIn go2rtc you can find out if the camera supports two-way audio via WebUI > stream probe.\n"
  },
  {
    "path": "internal/rtsp/rtsp.go",
    "content": "package rtsp\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"net\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/app\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/rtsp\"\n\t\"github.com/AlexxIT/go2rtc/pkg/tcp\"\n\t\"github.com/rs/zerolog\"\n)\n\nfunc Init() {\n\tvar conf struct {\n\t\tMod struct {\n\t\t\tListen       string `yaml:\"listen\" json:\"listen\"`\n\t\t\tUsername     string `yaml:\"username\" json:\"-\"`\n\t\t\tPassword     string `yaml:\"password\" json:\"-\"`\n\t\t\tDefaultQuery string `yaml:\"default_query\" json:\"default_query\"`\n\t\t\tPacketSize   uint16 `yaml:\"pkt_size\" json:\"pkt_size,omitempty\"`\n\t\t} `yaml:\"rtsp\"`\n\t}\n\n\t// default config\n\tconf.Mod.Listen = \":8554\"\n\tconf.Mod.DefaultQuery = \"video&audio\"\n\n\tapp.LoadConfig(&conf)\n\tapp.Info[\"rtsp\"] = conf.Mod\n\n\tlog = app.GetLogger(\"rtsp\")\n\n\t// RTSP client support\n\tstreams.HandleFunc(\"rtsp\", rtspHandler)\n\tstreams.HandleFunc(\"rtsps\", rtspHandler)\n\tstreams.HandleFunc(\"rtspx\", rtspHandler)\n\n\t// RTSP server support\n\taddress := conf.Mod.Listen\n\tif address == \"\" {\n\t\treturn\n\t}\n\n\tln, err := net.Listen(\"tcp\", address)\n\tif err != nil {\n\t\tlog.Error().Err(err).Msg(\"[rtsp] listen\")\n\t\treturn\n\t}\n\n\t_, Port, _ = net.SplitHostPort(address)\n\n\tlog.Info().Str(\"addr\", address).Msg(\"[rtsp] listen\")\n\n\tif query, err := url.ParseQuery(conf.Mod.DefaultQuery); err == nil {\n\t\tdefaultMedias = ParseQuery(query)\n\t}\n\n\tgo func() {\n\t\tfor {\n\t\t\tconn, err := ln.Accept()\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tc := rtsp.NewServer(conn)\n\t\t\tc.PacketSize = conf.Mod.PacketSize\n\t\t\t// skip check auth for localhost\n\t\t\tif conf.Mod.Username != \"\" && !conn.RemoteAddr().(*net.TCPAddr).IP.IsLoopback() {\n\t\t\t\tc.Auth(conf.Mod.Username, conf.Mod.Password)\n\t\t\t}\n\t\t\tgo tcpHandler(c)\n\t\t}\n\t}()\n}\n\ntype Handler func(conn *rtsp.Conn) bool\n\nfunc HandleFunc(handler Handler) {\n\thandlers = append(handlers, handler)\n}\n\nvar Port string\n\n// internal\n\nvar log zerolog.Logger\nvar handlers []Handler\nvar defaultMedias []*core.Media\n\nfunc rtspHandler(rawURL string) (core.Producer, error) {\n\trawURL, rawQuery, _ := strings.Cut(rawURL, \"#\")\n\n\tconn := rtsp.NewClient(rawURL)\n\tconn.Backchannel = true\n\tconn.UserAgent = app.UserAgent\n\n\tif rawQuery != \"\" {\n\t\tquery := streams.ParseQuery(rawQuery)\n\t\tconn.Backchannel = query.Get(\"backchannel\") == \"1\"\n\t\tconn.Media = query.Get(\"media\")\n\t\tconn.Timeout = core.Atoi(query.Get(\"timeout\"))\n\t\tconn.Transport = query.Get(\"transport\")\n\t}\n\n\tif log.Trace().Enabled() {\n\t\tconn.Listen(func(msg any) {\n\t\t\tswitch msg := msg.(type) {\n\t\t\tcase *tcp.Request:\n\t\t\t\tlog.Trace().Msgf(\"[rtsp] client request:\\n%s\", msg)\n\t\t\tcase *tcp.Response:\n\t\t\t\tlog.Trace().Msgf(\"[rtsp] client response:\\n%s\", msg)\n\t\t\tcase string:\n\t\t\t\tlog.Trace().Msgf(\"[rtsp] client msg: %s\", msg)\n\t\t\t}\n\t\t})\n\t}\n\n\tif err := conn.Dial(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := conn.Describe(); err != nil {\n\t\tif !conn.Backchannel {\n\t\t\treturn nil, err\n\t\t}\n\t\tlog.Trace().Msgf(\"[rtsp] describe (backchannel=%t) err: %v\", conn.Backchannel, err)\n\n\t\t// second try without backchannel, we need to reconnect\n\t\tconn.Backchannel = false\n\t\tif err = conn.Dial(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif err = conn.Describe(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn conn, nil\n}\n\nfunc tcpHandler(conn *rtsp.Conn) {\n\tvar name string\n\tvar closer func()\n\n\ttrace := log.Trace().Enabled()\n\tlevel := zerolog.WarnLevel\n\n\tconn.Listen(func(msg any) {\n\t\tif trace {\n\t\t\tswitch msg := msg.(type) {\n\t\t\tcase *tcp.Request:\n\t\t\t\tlog.Trace().Msgf(\"[rtsp] server request:\\n%s\", msg)\n\t\t\tcase *tcp.Response:\n\t\t\t\tlog.Trace().Msgf(\"[rtsp] server response:\\n%s\", msg)\n\t\t\t}\n\t\t}\n\n\t\tswitch msg {\n\t\tcase rtsp.MethodDescribe:\n\t\t\tif len(conn.URL.Path) == 0 {\n\t\t\t\tlog.Warn().Msg(\"[rtsp] server empty URL on DESCRIBE\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tname = conn.URL.Path[1:]\n\n\t\t\tstream := streams.Get(name)\n\t\t\tif stream == nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tlog.Debug().Str(\"stream\", name).Msg(\"[rtsp] new consumer\")\n\n\t\t\tconn.SessionName = app.UserAgent\n\n\t\t\tquery := conn.URL.Query()\n\t\t\tconn.Medias = ParseQuery(query)\n\t\t\tif conn.Medias == nil {\n\t\t\t\tfor _, media := range defaultMedias {\n\t\t\t\t\tconn.Medias = append(conn.Medias, media.Clone())\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif query.Get(\"backchannel\") == \"1\" {\n\t\t\t\tconn.Medias = append(conn.Medias, &core.Media{\n\t\t\t\t\tKind:      core.KindAudio,\n\t\t\t\t\tDirection: core.DirectionRecvonly,\n\t\t\t\t\tCodecs: []*core.Codec{\n\t\t\t\t\t\t{Name: core.CodecOpus, ClockRate: 48000, Channels: 2},\n\t\t\t\t\t\t{Name: core.CodecPCM, ClockRate: 16000},\n\t\t\t\t\t\t{Name: core.CodecPCMA, ClockRate: 16000},\n\t\t\t\t\t\t{Name: core.CodecPCMU, ClockRate: 16000},\n\t\t\t\t\t\t{Name: core.CodecPCM, ClockRate: 8000},\n\t\t\t\t\t\t{Name: core.CodecPCMA, ClockRate: 8000},\n\t\t\t\t\t\t{Name: core.CodecPCMU, ClockRate: 8000},\n\t\t\t\t\t\t{Name: core.CodecAAC, ClockRate: 8000},\n\t\t\t\t\t\t{Name: core.CodecAAC, ClockRate: 16000},\n\t\t\t\t\t},\n\t\t\t\t})\n\t\t\t}\n\n\t\t\tif s := query.Get(\"pkt_size\"); s != \"\" {\n\t\t\t\tconn.PacketSize = uint16(core.Atoi(s))\n\t\t\t}\n\n\t\t\t// param name like ffmpeg style https://ffmpeg.org/ffmpeg-protocols.html\n\t\t\tif s := query.Get(\"log_level\"); s != \"\" {\n\t\t\t\tif lvl, err := zerolog.ParseLevel(s); err == nil {\n\t\t\t\t\tlevel = lvl\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// will help to protect looping requests to same source\n\t\t\tconn.Connection.Source = query.Get(\"source\")\n\n\t\t\tif err := stream.AddConsumer(conn); err != nil {\n\t\t\t\tlog.WithLevel(level).Err(err).Str(\"stream\", name).Msg(\"[rtsp]\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tcloser = func() {\n\t\t\t\tstream.RemoveConsumer(conn)\n\t\t\t}\n\n\t\tcase rtsp.MethodAnnounce:\n\t\t\tif len(conn.URL.Path) == 0 {\n\t\t\t\tlog.Warn().Msg(\"[rtsp] server empty URL on ANNOUNCE\")\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tname = conn.URL.Path[1:]\n\n\t\t\tstream := streams.Get(name)\n\t\t\tif stream == nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tquery := conn.URL.Query()\n\t\t\tif s := query.Get(\"timeout\"); s != \"\" {\n\t\t\t\tconn.Timeout = core.Atoi(s)\n\t\t\t}\n\n\t\t\tlog.Debug().Str(\"stream\", name).Msg(\"[rtsp] new producer\")\n\n\t\t\tstream.AddProducer(conn)\n\n\t\t\tcloser = func() {\n\t\t\t\tstream.RemoveProducer(conn)\n\t\t\t}\n\t\t}\n\t})\n\n\tif err := conn.Accept(); err != nil {\n\t\tif errors.Is(err, rtsp.FailedAuth) {\n\t\t\tlog.Warn().Str(\"remote_addr\", conn.Connection.RemoteAddr).Msg(\"[rtsp] failed authentication\")\n\t\t} else if err != io.EOF {\n\t\t\tlog.WithLevel(level).Err(err).Caller().Send()\n\t\t}\n\t\tif closer != nil {\n\t\t\tcloser()\n\t\t}\n\t\t_ = conn.Close()\n\t\treturn\n\t}\n\n\tfor _, handler := range handlers {\n\t\tif handler(conn) {\n\t\t\treturn\n\t\t}\n\t}\n\n\tif closer != nil {\n\t\tif err := conn.Handle(); err != nil {\n\t\t\tlog.Debug().Err(err).Msg(\"[rtsp] handle\")\n\t\t}\n\n\t\tcloser()\n\n\t\tlog.Debug().Str(\"stream\", name).Msg(\"[rtsp] disconnect\")\n\t}\n\n\t_ = conn.Close()\n}\n\nfunc ParseQuery(query map[string][]string) []*core.Media {\n\tif v := query[\"mp4\"]; v != nil {\n\t\treturn []*core.Media{\n\t\t\t{\n\t\t\t\tKind:      core.KindVideo,\n\t\t\t\tDirection: core.DirectionSendonly,\n\t\t\t\tCodecs: []*core.Codec{\n\t\t\t\t\t{Name: core.CodecH264},\n\t\t\t\t\t{Name: core.CodecH265},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tKind:      core.KindAudio,\n\t\t\t\tDirection: core.DirectionSendonly,\n\t\t\t\tCodecs: []*core.Codec{\n\t\t\t\t\t{Name: core.CodecAAC},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t}\n\n\treturn core.ParseQuery(query)\n}\n"
  },
  {
    "path": "internal/srtp/README.md",
    "content": "# SRTP\n\nThis is a support module for the [HomeKit](../homekit/README.md) module.\n\n> [!NOTE]\n> This module can be removed and its functionality transferred to the homekit module.\n\n## Configuration\n\n```yaml\nsrtp:\n  listen: :8443  # enabled by default\n```\n"
  },
  {
    "path": "internal/srtp/srtp.go",
    "content": "package srtp\n\nimport (\n\t\"github.com/AlexxIT/go2rtc/internal/app\"\n\t\"github.com/AlexxIT/go2rtc/pkg/srtp\"\n)\n\nfunc Init() {\n\tvar cfg struct {\n\t\tMod struct {\n\t\t\tListen string `yaml:\"listen\"`\n\t\t} `yaml:\"srtp\"`\n\t}\n\n\t// default config\n\tcfg.Mod.Listen = \":8443\"\n\n\t// load config from YAML\n\tapp.LoadConfig(&cfg)\n\n\tif cfg.Mod.Listen == \"\" {\n\t\treturn\n\t}\n\n\t// create SRTP server (endpoint) for receiving video from HomeKit cameras\n\tServer = srtp.NewServer(cfg.Mod.Listen)\n}\n\nvar Server *srtp.Server\n"
  },
  {
    "path": "internal/streams/README.md",
    "content": "# Streams\n\nThis core module is responsible for managing the stream list.\n\n## Stream to camera\n\n[`new in v1.3.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)\n\ngo2rtc supports playing audio files (ex. music or [TTS](https://www.home-assistant.io/integrations/#text-to-speech)) and live streams (ex. radio) on cameras with [two-way audio](../../README.md#two-way-audio) support.\n\nAPI example:\n\n```text\nPOST http://localhost:1984/api/streams?dst=camera1&src=ffmpeg:http://example.com/song.mp3#audio=pcma#input=file\n```\n\n- you can stream: local files, web files, live streams or any format, supported by FFmpeg\n- you should use [ffmpeg source](../ffmpeg/README.md) for transcoding audio to codec, that your camera supports\n- you can check camera codecs on the go2rtc WebUI info page when the stream is active\n- some cameras support only low quality `PCMA/8000` codec (ex. [Tapo](../tapo/README.md))\n- it is recommended to choose higher quality formats if your camera supports them (ex. `PCMA/48000` for some Dahua cameras)\n- if you play files over `http` link, you need to add `#input=file` params for transcoding, so the file will be transcoded and played in real time\n- if you play live streams, you should skip `#input` param, because it is already in real time\n- you can stop active playback by calling the API with the empty `src` parameter\n- you will see one active producer and one active consumer in go2rtc WebUI info page during streaming\n\n## Publish stream\n\n[`new in v1.8.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.8.0)\n\nYou can publish any stream to streaming services (YouTube, Telegram, etc.) via RTMP/RTMPS. Important:\n\n- Supported codecs: H264 for video and AAC for audio\n- AAC audio is required for YouTube; videos without audio will not work\n- You don't need to enable [RTMP module](../rtmp/README.md) listening for this task\n\nYou can use the API:\n\n```text\nPOST http://localhost:1984/api/streams?src=camera1&dst=rtmps://...\n```\n\nOr config file:\n\n```yaml\npublish:\n  # publish stream \"video_audio_transcode\" to Telegram\n  video_audio_transcode:\n    - rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx\n  # publish stream \"audio_transcode\" to Telegram and YouTube\n  audio_transcode:\n    - rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx\n    - rtmp://xxx.rtmp.youtube.com/live2/xxxx-xxxx-xxxx-xxxx-xxxx\n\nstreams:\n  video_audio_transcode:\n    - ffmpeg:rtsp://user:pass@192.168.1.123/stream1#video=h264#hardware#audio=aac\n  audio_transcode:\n    - ffmpeg:rtsp://user:pass@192.168.1.123/stream1#video=copy#audio=aac\n```\n\n- **Telegram Desktop App** > Any public or private channel or group (where you admin) > Live stream > Start with... > Start streaming.\n- **YouTube** > Create > Go live > Stream latency: Ultra low-latency > Copy: Stream URL + Stream key.\n\n## Preload stream\n\n[`new in v1.9.11`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.9.11)\n\nYou can preload any stream on go2rtc start. This is useful for cameras that take a long time to start up.\n\n```yaml\npreload:\n  camera1:                                     # default: video&audio = ANY\n  camera2: \"video\"                             # preload only video track\n  camera3: \"video=h264&audio=opus\"             # preload H264 video and OPUS audio\n\nstreams:\n  camera1: \n    - rtsp://192.168.1.100/stream\n  camera2: \n    - rtsp://192.168.1.101/stream  \n  camera3: \n    - rtsp://192.168.1.102/h265stream\n    - ffmpeg:camera3#video=h264#audio=opus#hardware\n```\n\n## Examples\n\n```yaml\nstreams:\n  # known RTSP sources\n  rtsp-dahua1:   rtsp://admin:password@192.168.10.90/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif\n  rtsp-dahua2:   rtsp://admin:password@192.168.10.90/cam/realmonitor?channel=1&subtype=1\n  rtsp-tplink1:  rtsp://admin:password@192.168.10.91/stream1\n  rtsp-tplink2:  rtsp://admin:password@192.168.10.91/stream2\n  rtsp-reolink1: rtsp://admin:password@192.168.10.92/h264Preview_01_main\n  rtsp-reolink2: rtsp://admin:password@192.168.10.92/h264Preview_01_sub\n  rtsp-sonoff1:  rtsp://admin:password@192.168.10.93/av_stream/ch0\n  rtsp-sonoff2:  rtsp://admin:password@192.168.10.93/av_stream/ch1\n\n  # known RTMP sources\n  rtmp-reolink1: rtmp://192.168.10.92/bcs/channel0_main.bcs?channel=0&stream=0&user=admin&password=password\n  rtmp-reolink2: rtmp://192.168.10.92/bcs/channel0_sub.bcs?channel=0&stream=1&user=admin&password=password\n  rtmp-reolink3: rtmp://192.168.10.92/bcs/channel0_ext.bcs?channel=0&stream=1&user=admin&password=password\n\n  # known HTTP sources\n  http-reolink1: http://192.168.10.92/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=admin&password=password\n  http-reolink2: http://192.168.10.92/flv?port=1935&app=bcs&stream=channel0_sub.bcs&user=admin&password=password\n  http-reolink3: http://192.168.10.92/flv?port=1935&app=bcs&stream=channel0_ext.bcs&user=admin&password=password\n\n  # known ONVIF sources\n  onvif-dahua1:   onvif://admin:password@192.168.10.90?subtype=MediaProfile00000\n  onvif-dahua2:   onvif://admin:password@192.168.10.90?subtype=MediaProfile00001\n  onvif-dahua3:   onvif://admin:password@192.168.10.90?subtype=MediaProfile00000&snapshot\n  onvif-tplink1:  onvif://admin:password@192.168.10.91:2020?subtype=profile_1\n  onvif-tplink2:  onvif://admin:password@192.168.10.91:2020?subtype=profile_2\n  onvif-reolink1: onvif://admin:password@192.168.10.92:8000?subtype=000\n  onvif-reolink2: onvif://admin:password@192.168.10.92:8000?subtype=001\n  onvif-reolink3: onvif://admin:password@192.168.10.92:8000?subtype=000&snapshot\n  onvif-openipc1: onvif://admin:password@192.168.10.95:80?subtype=PROFILE_000\n  onvif-openipc2: onvif://admin:password@192.168.10.95:80?subtype=PROFILE_001\n\n  # some EXEC examples\n  exec-h264-pipe:   exec:ffmpeg -re -i bbb.mp4 -c copy -f h264 -\n  exec-flv-pipe:    exec:ffmpeg -re -i bbb.mp4 -c copy -f flv -\n  exec-mpegts-pipe: exec:ffmpeg -re -i bbb.mp4 -c copy -f mpegts -\n  exec-adts-pipe:   exec:ffmpeg -re -i bbb.mp4 -c copy -f adts -\n  exec-mjpeg-pipe:  exec:ffmpeg -re -i bbb.mp4 -c mjpeg -f mjpeg -\n  exec-hevc-pipe:   exec:ffmpeg -re -i bbb.mp4 -c libx265 -preset superfast -tune zerolatency -f hevc -\n  exec-wav-pipe:    exec:ffmpeg -re -i bbb.mp4 -c pcm_alaw -ar 8000 -ac 1 -f wav -\n  exec-y4m-pipe:    exec:ffmpeg -re -i bbb.mp4 -c rawvideo -f yuv4mpegpipe -\n  exec-pcma-pipe:   exec:ffmpeg -re -i numb.mp3 -c:a pcm_alaw -ar:a 8000 -ac:a 1 -f wav -\n  exec-pcmu-pipe:   exec:ffmpeg -re -i numb.mp3 -c:a pcm_mulaw -ar:a 8000 -ac:a 1 -f wav -\n  exec-s16le-pipe:  exec:ffmpeg -re -i numb.mp3 -c:a pcm_s16le -ar:a 16000 -ac:a 1 -f wav -\n\n  # some FFmpeg examples\n  ffmpeg-video-h264: ffmpeg:virtual?video#video=h264\n  ffmpeg-video-4K:   ffmpeg:virtual?video&size=4K#video=h264\n  ffmpeg-video-10s:  ffmpeg:virtual?video&duration=10#video=h264\n  ffmpeg-video-src2: ffmpeg:virtual?video=testsrc2&size=2K#video=h264\n```\n"
  },
  {
    "path": "internal/streams/add_consumer.go",
    "content": "package streams\n\nimport (\n\t\"errors\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\nfunc (s *Stream) AddConsumer(cons core.Consumer) (err error) {\n\t// support for multiple simultaneous pending from different consumers\n\tconsN := s.pending.Add(1) - 1\n\n\tvar prodErrors = make([]error, len(s.producers))\n\tvar prodMedias []*core.Media\n\tvar prodStarts []*Producer\n\n\t// Step 1. Get consumer medias\n\tconsMedias := cons.GetMedias()\n\tfor _, consMedia := range consMedias {\n\t\tlog.Trace().Msgf(\"[streams] check cons=%d media=%s\", consN, consMedia)\n\n\tproducers:\n\t\tfor prodN, prod := range s.producers {\n\t\t\t// check for loop request, ex. `camera1: ffmpeg:camera1`\n\t\t\tif info, ok := cons.(core.Info); ok && prod.url == info.GetSource() {\n\t\t\t\tlog.Trace().Msgf(\"[streams] skip cons=%d prod=%d\", consN, prodN)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif prodErrors[prodN] != nil {\n\t\t\t\tlog.Trace().Msgf(\"[streams] skip cons=%d prod=%d\", consN, prodN)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif err = prod.Dial(); err != nil {\n\t\t\t\tlog.Trace().Err(err).Msgf(\"[streams] dial cons=%d prod=%d\", consN, prodN)\n\t\t\t\tprodErrors[prodN] = err\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// Step 2. Get producer medias (not tracks yet)\n\t\t\tfor _, prodMedia := range prod.GetMedias() {\n\t\t\t\tlog.Trace().Msgf(\"[streams] check cons=%d prod=%d media=%s\", consN, prodN, prodMedia)\n\t\t\t\tprodMedias = append(prodMedias, prodMedia)\n\n\t\t\t\t// Step 3. Match consumer/producer codecs list\n\t\t\t\tprodCodec, consCodec := prodMedia.MatchMedia(consMedia)\n\t\t\t\tif prodCodec == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tvar track *core.Receiver\n\n\t\t\t\tswitch prodMedia.Direction {\n\t\t\t\tcase core.DirectionRecvonly:\n\t\t\t\t\tlog.Trace().Msgf(\"[streams] match cons=%d <= prod=%d\", consN, prodN)\n\n\t\t\t\t\t// Step 4. Get recvonly track from producer\n\t\t\t\t\tif track, err = prod.GetTrack(prodMedia, prodCodec); err != nil {\n\t\t\t\t\t\tlog.Info().Err(err).Msg(\"[streams] can't get track\")\n\t\t\t\t\t\tprodErrors[prodN] = err\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\t// Step 5. Add track to consumer\n\t\t\t\t\tif err = cons.AddTrack(consMedia, consCodec, track); err != nil {\n\t\t\t\t\t\tlog.Info().Err(err).Msg(\"[streams] can't add track\")\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\n\t\t\t\tcase core.DirectionSendonly:\n\t\t\t\t\tlog.Trace().Msgf(\"[streams] match cons=%d => prod=%d\", consN, prodN)\n\n\t\t\t\t\t// Step 4. Get recvonly track from consumer (backchannel)\n\t\t\t\t\tif track, err = cons.(core.Producer).GetTrack(consMedia, consCodec); err != nil {\n\t\t\t\t\t\tlog.Info().Err(err).Msg(\"[streams] can't get track\")\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t\t// Step 5. Add track to producer\n\t\t\t\t\tif err = prod.AddTrack(prodMedia, prodCodec, track); err != nil {\n\t\t\t\t\t\tlog.Info().Err(err).Msg(\"[streams] can't add track\")\n\t\t\t\t\t\tprodErrors[prodN] = err\n\t\t\t\t\t\tcontinue\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tprodStarts = append(prodStarts, prod)\n\n\t\t\t\tif !consMedia.MatchAll() {\n\t\t\t\t\tbreak producers\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// stop producers if they don't have readers\n\tif s.pending.Add(-1) == 0 {\n\t\ts.stopProducers()\n\t}\n\n\tif len(prodStarts) == 0 {\n\t\treturn formatError(consMedias, prodMedias, prodErrors)\n\t}\n\n\ts.mu.Lock()\n\ts.consumers = append(s.consumers, cons)\n\ts.mu.Unlock()\n\n\t// there may be duplicates, but that's not a problem\n\tfor _, prod := range prodStarts {\n\t\tprod.start()\n\t}\n\n\treturn nil\n}\n\nfunc formatError(consMedias, prodMedias []*core.Media, prodErrors []error) error {\n\t// 1. Return errors if any not nil\n\tvar text string\n\n\tfor _, err := range prodErrors {\n\t\tif err != nil {\n\t\t\ttext = appendString(text, err.Error())\n\t\t}\n\t}\n\n\tif len(text) != 0 {\n\t\treturn errors.New(\"streams: \" + text)\n\t}\n\n\t// 2. Return \"codecs not matched\"\n\tif prodMedias != nil {\n\t\tvar prod, cons string\n\n\t\tfor _, media := range prodMedias {\n\t\t\tif media.Direction == core.DirectionRecvonly {\n\t\t\t\tfor _, codec := range media.Codecs {\n\t\t\t\t\tprod = appendString(prod, media.Kind+\":\"+codec.PrintName())\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tfor _, media := range consMedias {\n\t\t\tif media.Direction == core.DirectionSendonly {\n\t\t\t\tfor _, codec := range media.Codecs {\n\t\t\t\t\tcons = appendString(cons, media.Kind+\":\"+codec.PrintName())\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\treturn errors.New(\"streams: codecs not matched: \" + prod + \" => \" + cons)\n\t}\n\n\t// 3. Return unknown error\n\treturn errors.New(\"streams: unknown error\")\n}\n\nfunc appendString(s, elem string) string {\n\tif strings.Contains(s, elem) {\n\t\treturn s\n\t}\n\tif len(s) == 0 {\n\t\treturn elem\n\t}\n\treturn s + \", \" + elem\n}\n"
  },
  {
    "path": "internal/streams/api.go",
    "content": "package streams\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/internal/app\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/creds\"\n\t\"github.com/AlexxIT/go2rtc/pkg/probe\"\n)\n\nfunc apiStreams(w http.ResponseWriter, r *http.Request) {\n\tw = creds.SecretResponse(w)\n\n\tquery := r.URL.Query()\n\tsrc := query.Get(\"src\")\n\n\t// without source - return all streams list\n\tif src == \"\" && r.Method != \"POST\" {\n\t\tapi.ResponseJSON(w, streams)\n\t\treturn\n\t}\n\n\t// Not sure about all this API. Should be rewrited...\n\tswitch r.Method {\n\tcase \"GET\":\n\t\tstream := Get(src)\n\t\tif stream == nil {\n\t\t\thttp.Error(w, \"\", http.StatusNotFound)\n\t\t\treturn\n\t\t}\n\n\t\tcons := probe.Create(\"probe\", query)\n\t\tif len(cons.Medias) != 0 {\n\t\t\tcons.WithRequest(r)\n\t\t\tif err := stream.AddConsumer(cons); err != nil {\n\t\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tapi.ResponsePrettyJSON(w, stream)\n\n\t\t\tstream.RemoveConsumer(cons)\n\t\t} else {\n\t\t\tapi.ResponsePrettyJSON(w, streams[src])\n\t\t}\n\n\tcase \"PUT\":\n\t\tname := query.Get(\"name\")\n\t\tif name == \"\" {\n\t\t\tname = src\n\t\t}\n\n\t\tif _, err := New(name, query[\"src\"]...); err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\tif err := app.PatchConfig([]string{\"streams\", name}, query[\"src\"]); err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\t}\n\n\tcase \"PATCH\":\n\t\tname := query.Get(\"name\")\n\t\tif name == \"\" {\n\t\t\thttp.Error(w, \"\", http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\t// support {input} templates: https://github.com/AlexxIT/go2rtc#module-hass\n\t\tif _, err := Patch(name, src); err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\t}\n\n\tcase \"POST\":\n\t\t// with dst - redirect source to dst\n\t\tif dst := query.Get(\"dst\"); dst != \"\" {\n\t\t\tif stream := Get(dst); stream != nil {\n\t\t\t\tif err := Validate(src); err != nil {\n\t\t\t\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\t\t\t} else if err = stream.Play(src); err != nil {\n\t\t\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\t\t} else {\n\t\t\t\t\tapi.ResponseJSON(w, stream)\n\t\t\t\t}\n\t\t\t} else if stream = Get(src); stream != nil {\n\t\t\t\tif err := Validate(dst); err != nil {\n\t\t\t\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\t\t\t} else if err = stream.Publish(dst); err != nil {\n\t\t\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\thttp.Error(w, \"\", http.StatusNotFound)\n\t\t\t}\n\t\t} else {\n\t\t\thttp.Error(w, \"\", http.StatusBadRequest)\n\t\t}\n\n\tcase \"DELETE\":\n\t\tdelete(streams, src)\n\n\t\tif err := app.PatchConfig([]string{\"streams\", src}, nil); err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\t}\n\t}\n}\n\nfunc apiStreamsDOT(w http.ResponseWriter, r *http.Request) {\n\tquery := r.URL.Query()\n\n\tdot := make([]byte, 0, 1024)\n\tdot = append(dot, \"digraph {\\n\"...)\n\tif query.Has(\"src\") {\n\t\tfor _, name := range query[\"src\"] {\n\t\t\tif stream := streams[name]; stream != nil {\n\t\t\t\tdot = AppendDOT(dot, stream)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfor _, stream := range streams {\n\t\t\tdot = AppendDOT(dot, stream)\n\t\t}\n\t}\n\tdot = append(dot, '}')\n\n\tdot = []byte(creds.SecretString(string(dot)))\n\n\tapi.Response(w, dot, \"text/vnd.graphviz\")\n}\n\nfunc apiPreload(w http.ResponseWriter, r *http.Request) {\n\t// GET - return all preloads\n\tif r.Method == \"GET\" {\n\t\tapi.ResponseJSON(w, GetPreloads())\n\t\treturn\n\t}\n\n\tquery := r.URL.Query()\n\tsrc := query.Get(\"src\")\n\n\tswitch r.Method {\n\tcase \"PUT\":\n\t\t// it's safe to delete from map while iterating\n\t\tfor k := range query {\n\t\t\tswitch k {\n\t\t\tcase core.KindVideo, core.KindAudio, \"microphone\":\n\t\t\tdefault:\n\t\t\t\tdelete(query, k)\n\t\t\t}\n\t\t}\n\n\t\trawQuery := query.Encode()\n\n\t\tif err := AddPreload(src, rawQuery); err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tif err := app.PatchConfig([]string{\"preload\", src}, rawQuery); err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t}\n\n\tcase \"DELETE\":\n\t\tif err := DelPreload(src); err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\n\t\tif err := app.PatchConfig([]string{\"preload\", src}, nil); err != nil {\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t}\n\n\tdefault:\n\t\thttp.Error(w, \"\", http.StatusMethodNotAllowed)\n\t}\n}\n\nfunc apiSchemes(w http.ResponseWriter, r *http.Request) {\n\tapi.ResponseJSON(w, SupportedSchemes())\n}\n"
  },
  {
    "path": "internal/streams/api_test.go",
    "content": "package streams\n\nimport (\n\t\"encoding/json\"\n\t\"net/http\"\n\t\"net/http/httptest\"\n\t\"testing\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestApiSchemes(t *testing.T) {\n\t// Setup: Register some test handlers and redirects\n\tHandleFunc(\"rtsp\", func(url string) (core.Producer, error) { return nil, nil })\n\tHandleFunc(\"rtmp\", func(url string) (core.Producer, error) { return nil, nil })\n\tRedirectFunc(\"http\", func(url string) (string, error) { return \"\", nil })\n\n\tt.Run(\"GET request returns schemes\", func(t *testing.T) {\n\t\treq := httptest.NewRequest(\"GET\", \"/api/schemes\", nil)\n\t\tw := httptest.NewRecorder()\n\n\t\tapiSchemes(w, req)\n\n\t\trequire.Equal(t, http.StatusOK, w.Code)\n\t\trequire.Equal(t, \"application/json\", w.Header().Get(\"Content-Type\"))\n\n\t\tvar schemes []string\n\t\terr := json.Unmarshal(w.Body.Bytes(), &schemes)\n\t\trequire.NoError(t, err)\n\t\trequire.NotEmpty(t, schemes)\n\n\t\t// Check that our test schemes are in the response\n\t\trequire.Contains(t, schemes, \"rtsp\")\n\t\trequire.Contains(t, schemes, \"rtmp\")\n\t\trequire.Contains(t, schemes, \"http\")\n\t})\n}\n\nfunc TestApiSchemesNoDuplicates(t *testing.T) {\n\t// Setup: Register a scheme in both handlers and redirects\n\tHandleFunc(\"duplicate\", func(url string) (core.Producer, error) { return nil, nil })\n\tRedirectFunc(\"duplicate\", func(url string) (string, error) { return \"\", nil })\n\n\treq := httptest.NewRequest(\"GET\", \"/api/schemes\", nil)\n\tw := httptest.NewRecorder()\n\n\tapiSchemes(w, req)\n\n\trequire.Equal(t, http.StatusOK, w.Code)\n\n\tvar schemes []string\n\terr := json.Unmarshal(w.Body.Bytes(), &schemes)\n\trequire.NoError(t, err)\n\n\t// Count occurrences of \"duplicate\"\n\tcount := 0\n\tfor _, scheme := range schemes {\n\t\tif scheme == \"duplicate\" {\n\t\t\tcount++\n\t\t}\n\t}\n\n\t// Should only appear once\n\trequire.Equal(t, 1, count, \"scheme 'duplicate' should appear exactly once\")\n}\n"
  },
  {
    "path": "internal/streams/dot.go",
    "content": "package streams\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n)\n\nfunc AppendDOT(dot []byte, stream *Stream) []byte {\n\tfor _, prod := range stream.producers {\n\t\tif prod.conn == nil {\n\t\t\tcontinue\n\t\t}\n\t\tc, err := marshalConn(prod.conn)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tdot = c.appendDOT(dot, \"producer\")\n\t}\n\tfor _, cons := range stream.consumers {\n\t\tc, err := marshalConn(cons)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tdot = c.appendDOT(dot, \"consumer\")\n\t}\n\treturn dot\n}\n\nfunc marshalConn(v any) (*conn, error) {\n\tb, err := json.Marshal(v)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tvar c conn\n\tif err = json.Unmarshal(b, &c); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &c, nil\n}\n\nconst bytesK = \"KMGTP\"\n\nfunc humanBytes(i int) string {\n\tif i < 1000 {\n\t\treturn fmt.Sprintf(\"%d B\", i)\n\t}\n\n\tf := float64(i) / 1000\n\tvar n uint8\n\tfor f >= 1000 && n < 5 {\n\t\tf /= 1000\n\t\tn++\n\t}\n\treturn fmt.Sprintf(\"%.2f %cB\", f, bytesK[n])\n}\n\ntype node struct {\n\tID     uint32         `json:\"id\"`\n\tCodec  map[string]any `json:\"codec\"`\n\tParent uint32         `json:\"parent\"`\n\tChilds []uint32       `json:\"childs\"`\n\tBytes  int            `json:\"bytes\"`\n\t//Packets uint32         `json:\"packets\"`\n\t//Drops   uint32         `json:\"drops\"`\n}\n\nvar codecKeys = []string{\"codec_name\", \"sample_rate\", \"channels\", \"profile\", \"level\"}\n\nfunc (n *node) name() string {\n\tif name, ok := n.Codec[\"codec_name\"].(string); ok {\n\t\treturn name\n\t}\n\treturn \"unknown\"\n}\n\nfunc (n *node) codec() []byte {\n\tb := make([]byte, 0, 128)\n\tfor _, k := range codecKeys {\n\t\tif v := n.Codec[k]; v != nil {\n\t\t\tb = fmt.Appendf(b, \"%s=%v\\n\", k, v)\n\t\t}\n\t}\n\tif l := len(b); l > 0 {\n\t\treturn b[:l-1]\n\t}\n\treturn b\n}\n\nfunc (n *node) appendDOT(dot []byte, group string) []byte {\n\tdot = fmt.Appendf(dot, \"%d [group=%s, label=%q, title=%q];\\n\", n.ID, group, n.name(), n.codec())\n\t//for _, sink := range n.Childs {\n\t//\tdot = fmt.Appendf(dot, \"%d -> %d;\\n\", n.ID, sink)\n\t//}\n\treturn dot\n}\n\ntype conn struct {\n\tID         uint32 `json:\"id\"`\n\tFormatName string `json:\"format_name\"`\n\tProtocol   string `json:\"protocol\"`\n\tRemoteAddr string `json:\"remote_addr\"`\n\tSource     string `json:\"source\"`\n\tURL        string `json:\"url\"`\n\tUserAgent  string `json:\"user_agent\"`\n\tReceivers  []node `json:\"receivers\"`\n\tSenders    []node `json:\"senders\"`\n\tBytesRecv  int    `json:\"bytes_recv\"`\n\tBytesSend  int    `json:\"bytes_send\"`\n}\n\nfunc (c *conn) appendDOT(dot []byte, group string) []byte {\n\thost := c.host()\n\tdot = fmt.Appendf(dot, \"%s [group=host];\\n\", host)\n\tdot = fmt.Appendf(dot, \"%d [group=%s, label=%q, title=%q];\\n\", c.ID, group, c.FormatName, c.label())\n\tif group == \"producer\" {\n\t\tdot = fmt.Appendf(dot, \"%s -> %d [label=%q];\\n\", host, c.ID, humanBytes(c.BytesRecv))\n\t} else {\n\t\tdot = fmt.Appendf(dot, \"%d -> %s [label=%q];\\n\", c.ID, host, humanBytes(c.BytesSend))\n\t}\n\n\tfor _, recv := range c.Receivers {\n\t\tdot = fmt.Appendf(dot, \"%d -> %d [label=%q];\\n\", c.ID, recv.ID, humanBytes(recv.Bytes))\n\t\tdot = recv.appendDOT(dot, \"node\")\n\t}\n\tfor _, send := range c.Senders {\n\t\tdot = fmt.Appendf(dot, \"%d -> %d [label=%q];\\n\", send.Parent, c.ID, humanBytes(send.Bytes))\n\t\t//dot = fmt.Appendf(dot, \"%d -> %d [label=%q];\\n\", send.ID, c.ID, humanBytes(send.Bytes))\n\t\t//dot = send.appendDOT(dot, \"node\")\n\t}\n\treturn dot\n}\n\nfunc (c *conn) host() (s string) {\n\tif c.Protocol == \"pipe\" {\n\t\treturn \"127.0.0.1\"\n\t}\n\n\tif s = c.RemoteAddr; s == \"\" {\n\t\treturn \"unknown\"\n\t}\n\n\tif i := strings.Index(s, \"forwarded\"); i > 0 {\n\t\ts = s[i+10:]\n\t}\n\n\tif s[0] == '[' {\n\t\tif i := strings.Index(s, \"]\"); i > 0 {\n\t\t\treturn s[1:i]\n\t\t}\n\t}\n\n\tif i := strings.IndexAny(s, \" ,:\"); i > 0 {\n\t\treturn s[:i]\n\t}\n\treturn\n}\n\nfunc (c *conn) label() string {\n\tvar sb strings.Builder\n\tsb.WriteString(\"format_name=\" + c.FormatName)\n\tif c.Protocol != \"\" {\n\t\tsb.WriteString(\"\\nprotocol=\" + c.Protocol)\n\t}\n\tif c.Source != \"\" {\n\t\tsb.WriteString(\"\\nsource=\" + c.Source)\n\t}\n\tif c.URL != \"\" {\n\t\tsb.WriteString(\"\\nurl=\" + c.URL)\n\t}\n\tif c.UserAgent != \"\" {\n\t\tsb.WriteString(\"\\nuser_agent=\" + c.UserAgent)\n\t}\n\t// escape quotes https://github.com/AlexxIT/go2rtc/issues/1603\n\treturn strings.ReplaceAll(sb.String(), `\"`, `'`)\n}\n"
  },
  {
    "path": "internal/streams/handlers.go",
    "content": "package streams\n\nimport (\n\t\"errors\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\ntype Handler func(source string) (core.Producer, error)\n\nvar handlers = map[string]Handler{}\n\nfunc HandleFunc(scheme string, handler Handler) {\n\thandlers[scheme] = handler\n}\n\nfunc SupportedSchemes() []string {\n\tuniqueKeys := make(map[string]struct{}, len(handlers)+len(redirects))\n\tfor scheme := range handlers {\n\t\tuniqueKeys[scheme] = struct{}{}\n\t}\n\tfor scheme := range redirects {\n\t\tuniqueKeys[scheme] = struct{}{}\n\t}\n\tresultKeys := make([]string, 0, len(uniqueKeys))\n\tfor key := range uniqueKeys {\n\t\tresultKeys = append(resultKeys, key)\n\t}\n\treturn resultKeys\n}\n\nfunc HasProducer(url string) bool {\n\tif i := strings.IndexByte(url, ':'); i > 0 {\n\t\tscheme := url[:i]\n\n\t\tif _, ok := handlers[scheme]; ok {\n\t\t\treturn true\n\t\t}\n\n\t\tif _, ok := redirects[scheme]; ok {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc GetProducer(url string) (core.Producer, error) {\n\tif i := strings.IndexByte(url, ':'); i > 0 {\n\t\tscheme := url[:i]\n\n\t\tif redirect, ok := redirects[scheme]; ok {\n\t\t\tlocation, err := redirect(url)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tif location != \"\" {\n\t\t\t\treturn GetProducer(location)\n\t\t\t}\n\t\t}\n\n\t\tif handler, ok := handlers[scheme]; ok {\n\t\t\treturn handler(url)\n\t\t}\n\t}\n\n\treturn nil, errors.New(\"streams: unsupported scheme: \" + url)\n}\n\n// Redirect can return: location URL or error or empty URL and error\ntype Redirect func(url string) (string, error)\n\nvar redirects = map[string]Redirect{}\n\nfunc RedirectFunc(scheme string, redirect Redirect) {\n\tredirects[scheme] = redirect\n}\n\nfunc Location(url string) (string, error) {\n\tif i := strings.IndexByte(url, ':'); i > 0 {\n\t\tscheme := url[:i]\n\n\t\tif redirect, ok := redirects[scheme]; ok {\n\t\t\treturn redirect(url)\n\t\t}\n\t}\n\n\treturn \"\", nil\n}\n\n// TODO: rework\n\ntype ConsumerHandler func(url string) (core.Consumer, func(), error)\n\nvar consumerHandlers = map[string]ConsumerHandler{}\n\nfunc HandleConsumerFunc(scheme string, handler ConsumerHandler) {\n\tconsumerHandlers[scheme] = handler\n}\n\nfunc GetConsumer(url string) (core.Consumer, func(), error) {\n\tif i := strings.IndexByte(url, ':'); i > 0 {\n\t\tscheme := url[:i]\n\n\t\tif handler, ok := consumerHandlers[scheme]; ok {\n\t\t\treturn handler(url)\n\t\t}\n\t}\n\n\treturn nil, nil, errors.New(\"streams: unsupported scheme: \" + url)\n}\n\nvar insecure = map[string]bool{}\n\nfunc MarkInsecure(scheme string) {\n\tinsecure[scheme] = true\n}\n\nvar sanitize = regexp.MustCompile(`\\s`)\n\nfunc Validate(source string) error {\n\t// TODO: Review the entire logic of insecure sources\n\tif i := strings.IndexByte(source, ':'); i > 0 {\n\t\tif insecure[source[:i]] {\n\t\t\treturn errors.New(\"streams: source from insecure producer\")\n\t\t}\n\t}\n\tif sanitize.MatchString(source) {\n\t\treturn errors.New(\"streams: source with spaces may be insecure\")\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "internal/streams/helpers.go",
    "content": "package streams\n\nimport (\n\t\"net/url\"\n\t\"strings\"\n)\n\nfunc ParseQuery(s string) url.Values {\n\tif len(s) == 0 {\n\t\treturn nil\n\t}\n\tparams := url.Values{}\n\tfor _, key := range strings.Split(s, \"#\") {\n\t\tvar value string\n\t\ti := strings.IndexByte(key, '=')\n\t\tif i > 0 {\n\t\t\tkey, value = key[:i], key[i+1:]\n\t\t}\n\t\tparams[key] = append(params[key], value)\n\t}\n\treturn params\n}\n"
  },
  {
    "path": "internal/streams/play.go",
    "content": "package streams\n\nimport (\n\t\"errors\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\nfunc (s *Stream) Play(urlOrProd any) error {\n\ts.mu.Lock()\n\tfor _, producer := range s.producers {\n\t\tif producer.state == stateInternal && producer.conn != nil {\n\t\t\t_ = producer.conn.Stop()\n\t\t}\n\t}\n\ts.mu.Unlock()\n\n\tvar source string\n\tvar src core.Producer\n\n\tswitch urlOrProd.(type) {\n\tcase string:\n\t\tif source = urlOrProd.(string); source == \"\" {\n\t\t\treturn nil\n\t\t}\n\tcase core.Producer:\n\t\tsrc = urlOrProd.(core.Producer)\n\t}\n\n\tfor _, producer := range s.producers {\n\t\tif producer.conn == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tcons, ok := producer.conn.(core.Consumer)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tif src == nil {\n\t\t\tvar err error\n\t\t\tif src, err = GetProducer(source); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tif !matchMedia(src, cons) {\n\t\t\tcontinue\n\t\t}\n\n\t\ts.AddInternalProducer(src)\n\n\t\tgo func() {\n\t\t\t_ = src.Start()\n\t\t\ts.RemoveProducer(src)\n\t\t}()\n\n\t\treturn nil\n\t}\n\n\tfor _, producer := range s.producers {\n\t\t// start new client\n\t\tdst, err := GetProducer(producer.url)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// check if client support consumer interface\n\t\tcons, ok := dst.(core.Consumer)\n\t\tif !ok {\n\t\t\t_ = dst.Stop()\n\t\t\tcontinue\n\t\t}\n\n\t\t// start new producer\n\t\tif src == nil {\n\t\t\tif src, err = GetProducer(source); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\n\t\tif !matchMedia(src, cons) {\n\t\t\t_ = dst.Stop()\n\t\t\tcontinue\n\t\t}\n\n\t\ts.AddInternalProducer(src)\n\t\ts.AddInternalConsumer(cons)\n\n\t\tgo func() {\n\t\t\t_ = dst.Start()\n\t\t\t_ = src.Stop()\n\t\t\ts.RemoveInternalConsumer(cons)\n\t\t}()\n\n\t\tgo func() {\n\t\t\t_ = src.Start()\n\t\t\t// little timeout before stop dst, so the buffer can be transferred\n\t\t\ttime.Sleep(time.Second)\n\t\t\t_ = dst.Stop()\n\t\t\ts.RemoveProducer(src)\n\t\t}()\n\n\t\treturn nil\n\t}\n\n\treturn errors.New(\"can't find consumer\")\n}\n\nfunc (s *Stream) AddInternalProducer(conn core.Producer) {\n\tproducer := &Producer{conn: conn, state: stateInternal, url: \"internal\"}\n\ts.mu.Lock()\n\ts.producers = append(s.producers, producer)\n\ts.mu.Unlock()\n}\n\nfunc (s *Stream) AddInternalConsumer(conn core.Consumer) {\n\ts.mu.Lock()\n\ts.consumers = append(s.consumers, conn)\n\ts.mu.Unlock()\n}\n\nfunc (s *Stream) RemoveInternalConsumer(conn core.Consumer) {\n\ts.mu.Lock()\n\tfor i, consumer := range s.consumers {\n\t\tif consumer == conn {\n\t\t\ts.consumers = append(s.consumers[:i], s.consumers[i+1:]...)\n\t\t\tbreak\n\t\t}\n\t}\n\ts.mu.Unlock()\n}\n\nfunc matchMedia(prod core.Producer, cons core.Consumer) bool {\n\tfor _, consMedia := range cons.GetMedias() {\n\t\tfor _, prodMedia := range prod.GetMedias() {\n\t\t\tif prodMedia.Direction != core.DirectionRecvonly {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tprodCodec, consCodec := prodMedia.MatchMedia(consMedia)\n\t\t\tif prodCodec == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ttrack, err := prod.GetTrack(prodMedia, prodCodec)\n\t\t\tif err != nil {\n\t\t\t\tlog.Warn().Err(err).Msg(\"[streams] can't get track\")\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif err = cons.AddTrack(consMedia, consCodec, track); err != nil {\n\t\t\t\tlog.Warn().Err(err).Msg(\"[streams] can't add track\")\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n"
  },
  {
    "path": "internal/streams/preload.go",
    "content": "package streams\n\nimport (\n\t\"fmt\"\n\t\"maps\"\n\t\"net/url\"\n\t\"sync\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/probe\"\n)\n\ntype Preload struct {\n\tstream *Stream      // Don't include the stream in JSON to avoid leaking secrets.\n\tCons   *probe.Probe `json:\"consumer\"`\n\tQuery  string       `json:\"query\"`\n}\n\nvar preloads = map[string]*Preload{}\nvar preloadsMu sync.Mutex\n\nfunc AddPreload(name, rawQuery string) error {\n\tif rawQuery == \"\" {\n\t\trawQuery = \"video&audio\"\n\t}\n\n\tquery, err := url.ParseQuery(rawQuery)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpreloadsMu.Lock()\n\tdefer preloadsMu.Unlock()\n\n\tif p := preloads[name]; p != nil {\n\t\tp.stream.RemoveConsumer(p.Cons)\n\t}\n\n\tstream := Get(name)\n\tif stream == nil {\n\t\treturn fmt.Errorf(\"streams: stream not found: %s\", name)\n\t}\n\tcons := probe.Create(\"preload\", query)\n\n\tif err = stream.AddConsumer(cons); err != nil {\n\t\treturn err\n\t}\n\n\tpreloads[name] = &Preload{stream: stream, Cons: cons, Query: rawQuery}\n\treturn nil\n}\n\nfunc DelPreload(name string) error {\n\tpreloadsMu.Lock()\n\tdefer preloadsMu.Unlock()\n\n\tif p := preloads[name]; p != nil {\n\t\tp.stream.RemoveConsumer(p.Cons)\n\t\tdelete(preloads, name)\n\t\treturn nil\n\t}\n\n\treturn fmt.Errorf(\"streams: preload not found: %s\", name)\n}\n\nfunc GetPreloads() map[string]*Preload {\n\tpreloadsMu.Lock()\n\tdefer preloadsMu.Unlock()\n\treturn maps.Clone(preloads)\n}\n"
  },
  {
    "path": "internal/streams/producer.go",
    "content": "package streams\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\ntype state byte\n\nconst (\n\tstateNone state = iota\n\tstateMedias\n\tstateTracks\n\tstateStart\n\tstateExternal\n\tstateInternal\n)\n\ntype Producer struct {\n\tcore.Listener\n\n\turl      string\n\ttemplate string\n\n\tconn      core.Producer\n\treceivers []*core.Receiver\n\tsenders   []*core.Receiver\n\n\tstate    state\n\tmu       sync.Mutex\n\tworkerID int\n}\n\nconst SourceTemplate = \"{input}\"\n\nfunc NewProducer(source string) *Producer {\n\tif strings.Contains(source, SourceTemplate) {\n\t\treturn &Producer{template: source}\n\t}\n\n\treturn &Producer{url: source}\n}\n\nfunc (p *Producer) SetSource(s string) {\n\tif p.template == \"\" {\n\t\tp.url = s\n\t} else {\n\t\tp.url = strings.Replace(p.template, SourceTemplate, s, 1)\n\t}\n}\n\nfunc (p *Producer) Dial() error {\n\tp.mu.Lock()\n\tdefer p.mu.Unlock()\n\n\tif p.state == stateNone {\n\t\tconn, err := GetProducer(p.url)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tp.conn = conn\n\t\tp.state = stateMedias\n\t}\n\n\treturn nil\n}\n\nfunc (p *Producer) GetMedias() []*core.Media {\n\tp.mu.Lock()\n\tdefer p.mu.Unlock()\n\n\tif p.conn == nil {\n\t\treturn nil\n\t}\n\n\treturn p.conn.GetMedias()\n}\n\nfunc (p *Producer) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {\n\tp.mu.Lock()\n\tdefer p.mu.Unlock()\n\n\tif p.state == stateNone {\n\t\treturn nil, errors.New(\"get track from none state\")\n\t}\n\n\tfor _, track := range p.receivers {\n\t\tif track.Codec == codec {\n\t\t\treturn track, nil\n\t\t}\n\t}\n\n\ttrack, err := p.conn.GetTrack(media, codec)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tp.receivers = append(p.receivers, track)\n\n\tif p.state == stateMedias {\n\t\tp.state = stateTracks\n\t}\n\n\treturn track, nil\n}\n\nfunc (p *Producer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {\n\tp.mu.Lock()\n\tdefer p.mu.Unlock()\n\n\tif p.state == stateNone {\n\t\treturn errors.New(\"add track from none state\")\n\t}\n\n\tif err := p.conn.(core.Consumer).AddTrack(media, codec, track); err != nil {\n\t\treturn err\n\t}\n\n\tp.senders = append(p.senders, track)\n\n\tif p.state == stateMedias {\n\t\tp.state = stateTracks\n\t}\n\n\treturn nil\n}\n\nfunc (p *Producer) MarshalJSON() ([]byte, error) {\n\tif conn := p.conn; conn != nil {\n\t\treturn json.Marshal(conn)\n\t}\n\tinfo := map[string]string{\"url\": p.url}\n\treturn json.Marshal(info)\n}\n\n// internals\n\nfunc (p *Producer) start() {\n\tp.mu.Lock()\n\tdefer p.mu.Unlock()\n\n\tif p.state != stateTracks {\n\t\treturn\n\t}\n\n\tlog.Debug().Msgf(\"[streams] start producer url=%s\", p.url)\n\n\tp.state = stateStart\n\tp.workerID++\n\n\tgo p.worker(p.conn, p.workerID)\n}\n\nfunc (p *Producer) worker(conn core.Producer, workerID int) {\n\tif err := conn.Start(); err != nil {\n\t\tp.mu.Lock()\n\t\tclosed := p.workerID != workerID\n\t\tp.mu.Unlock()\n\n\t\tif closed {\n\t\t\treturn\n\t\t}\n\n\t\tlog.Warn().Err(err).Str(\"url\", p.url).Caller().Send()\n\t}\n\n\tp.reconnect(workerID, 0)\n}\n\nfunc (p *Producer) reconnect(workerID, retry int) {\n\tp.mu.Lock()\n\tdefer p.mu.Unlock()\n\n\tif p.workerID != workerID {\n\t\tlog.Trace().Msgf(\"[streams] stop reconnect url=%s\", p.url)\n\t\treturn\n\t}\n\n\tlog.Debug().Msgf(\"[streams] retry=%d to url=%s\", retry, p.url)\n\n\tconn, err := GetProducer(p.url)\n\tif err != nil {\n\t\tlog.Debug().Msgf(\"[streams] producer=%s\", err)\n\n\t\ttimeout := time.Minute\n\t\tif retry < 5 {\n\t\t\ttimeout = time.Second\n\t\t} else if retry < 10 {\n\t\t\ttimeout = time.Second * 5\n\t\t} else if retry < 20 {\n\t\t\ttimeout = time.Second * 10\n\t\t}\n\n\t\ttime.AfterFunc(timeout, func() {\n\t\t\tp.reconnect(workerID, retry+1)\n\t\t})\n\t\treturn\n\t}\n\n\tfor _, media := range conn.GetMedias() {\n\t\tswitch media.Direction {\n\t\tcase core.DirectionRecvonly:\n\t\t\tfor i, receiver := range p.receivers {\n\t\t\t\tcodec := media.MatchCodec(receiver.Codec)\n\t\t\t\tif codec == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\ttrack, err := conn.GetTrack(media, codec)\n\t\t\t\tif err != nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\treceiver.Replace(track)\n\t\t\t\tp.receivers[i] = track\n\t\t\t\tbreak\n\t\t\t}\n\n\t\tcase core.DirectionSendonly:\n\t\t\tfor _, sender := range p.senders {\n\t\t\t\tcodec := media.MatchCodec(sender.Codec)\n\t\t\t\tif codec == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\t_ = conn.(core.Consumer).AddTrack(media, codec, sender)\n\t\t\t}\n\t\t}\n\t}\n\n\t// stop previous connection after moving tracks (fix ghost exec/ffmpeg)\n\t_ = p.conn.Stop()\n\t// swap connections\n\tp.conn = conn\n\n\tgo p.worker(conn, workerID)\n}\n\nfunc (p *Producer) stop() {\n\tp.mu.Lock()\n\tdefer p.mu.Unlock()\n\n\tswitch p.state {\n\tcase stateExternal:\n\t\tlog.Trace().Msgf(\"[streams] skip stop external producer\")\n\t\treturn\n\tcase stateNone:\n\t\tlog.Trace().Msgf(\"[streams] skip stop none producer\")\n\t\treturn\n\tcase stateStart:\n\t\tp.workerID++\n\t}\n\n\tlog.Debug().Msgf(\"[streams] stop producer url=%s\", p.url)\n\n\tif p.conn != nil {\n\t\t_ = p.conn.Stop()\n\t\tp.conn = nil\n\t}\n\n\tp.state = stateNone\n\tp.receivers = nil\n\tp.senders = nil\n}\n"
  },
  {
    "path": "internal/streams/publish.go",
    "content": "package streams\n\nimport \"time\"\n\nfunc (s *Stream) Publish(url string) error {\n\tcons, run, err := GetConsumer(url)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err = s.AddConsumer(cons); err != nil {\n\t\treturn err\n\t}\n\n\tgo func() {\n\t\trun()\n\t\ts.RemoveConsumer(cons)\n\n\t\t// TODO: more smart retry\n\t\ttime.Sleep(5 * time.Second)\n\t\t_ = s.Publish(url)\n\t}()\n\n\treturn nil\n}\n\nfunc Publish(stream *Stream, destination any) {\n\tswitch v := destination.(type) {\n\tcase string:\n\t\tif err := stream.Publish(v); err != nil {\n\t\t\tlog.Error().Err(err).Caller().Send()\n\t\t}\n\tcase []any:\n\t\tfor _, v := range v {\n\t\t\tPublish(stream, v)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/streams/stream.go",
    "content": "package streams\n\nimport (\n\t\"encoding/json\"\n\t\"sync\"\n\t\"sync/atomic\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\ntype Stream struct {\n\tproducers []*Producer\n\tconsumers []core.Consumer\n\tmu        sync.Mutex\n\tpending   atomic.Int32\n}\n\nfunc NewStream(source any) *Stream {\n\tswitch source := source.(type) {\n\tcase string:\n\t\treturn &Stream{\n\t\t\tproducers: []*Producer{NewProducer(source)},\n\t\t}\n\tcase []string:\n\t\ts := new(Stream)\n\t\tfor _, str := range source {\n\t\t\ts.producers = append(s.producers, NewProducer(str))\n\t\t}\n\t\treturn s\n\tcase []any:\n\t\ts := new(Stream)\n\t\tfor _, src := range source {\n\t\t\tstr, ok := src.(string)\n\t\t\tif !ok {\n\t\t\t\tlog.Error().Msgf(\"[stream] NewStream: Expected string, got %v\", src)\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\ts.producers = append(s.producers, NewProducer(str))\n\t\t}\n\t\treturn s\n\tcase map[string]any:\n\t\treturn NewStream(source[\"url\"])\n\tcase nil:\n\t\treturn new(Stream)\n\tdefault:\n\t\tpanic(core.Caller())\n\t}\n}\n\nfunc (s *Stream) Sources() []string {\n\tsources := make([]string, 0, len(s.producers))\n\tfor _, prod := range s.producers {\n\t\tsources = append(sources, prod.url)\n\t}\n\treturn sources\n}\n\nfunc (s *Stream) SetSource(source string) {\n\tfor _, prod := range s.producers {\n\t\tprod.SetSource(source)\n\t}\n}\n\nfunc (s *Stream) RemoveConsumer(cons core.Consumer) {\n\t_ = cons.Stop()\n\n\ts.mu.Lock()\n\tfor i, consumer := range s.consumers {\n\t\tif consumer == cons {\n\t\t\ts.consumers = append(s.consumers[:i], s.consumers[i+1:]...)\n\t\t\tbreak\n\t\t}\n\t}\n\ts.mu.Unlock()\n\n\ts.stopProducers()\n}\n\nfunc (s *Stream) AddProducer(prod core.Producer) {\n\tproducer := &Producer{conn: prod, state: stateExternal, url: \"external\"}\n\ts.mu.Lock()\n\ts.producers = append(s.producers, producer)\n\ts.mu.Unlock()\n}\n\nfunc (s *Stream) RemoveProducer(prod core.Producer) {\n\ts.mu.Lock()\n\tfor i, producer := range s.producers {\n\t\tif producer.conn == prod {\n\t\t\ts.producers = append(s.producers[:i], s.producers[i+1:]...)\n\t\t\tbreak\n\t\t}\n\t}\n\ts.mu.Unlock()\n}\n\nfunc (s *Stream) stopProducers() {\n\tif s.pending.Load() > 0 {\n\t\tlog.Trace().Msg(\"[streams] skip stop pending producer\")\n\t\treturn\n\t}\n\n\ts.mu.Lock()\nproducers:\n\tfor _, producer := range s.producers {\n\t\tfor _, track := range producer.receivers {\n\t\t\tif len(track.Senders()) > 0 {\n\t\t\t\tcontinue producers\n\t\t\t}\n\t\t}\n\t\tfor _, track := range producer.senders {\n\t\t\tif len(track.Senders()) > 0 {\n\t\t\t\tcontinue producers\n\t\t\t}\n\t\t}\n\t\tproducer.stop()\n\t}\n\ts.mu.Unlock()\n}\n\nfunc (s *Stream) MarshalJSON() ([]byte, error) {\n\tvar info = struct {\n\t\tProducers []*Producer     `json:\"producers\"`\n\t\tConsumers []core.Consumer `json:\"consumers\"`\n\t}{\n\t\tProducers: s.producers,\n\t\tConsumers: s.consumers,\n\t}\n\treturn json.Marshal(info)\n}\n"
  },
  {
    "path": "internal/streams/stream_test.go",
    "content": "package streams\n\nimport (\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRecursion(t *testing.T) {\n\t// create stream with some source\n\tstream1, err := New(\"from_yaml\", \"does_not_matter\")\n\trequire.NoError(t, err)\n\trequire.Len(t, streams, 1)\n\n\t// ask another unnamed stream that links go2rtc\n\tquery, err := url.ParseQuery(\"src=rtsp://localhost:8554/from_yaml?video\")\n\trequire.NoError(t, err)\n\tstream2, err := GetOrPatch(query)\n\trequire.NoError(t, err)\n\n\t// check stream is same\n\trequire.Equal(t, stream1, stream2)\n\t// check stream urls is same\n\trequire.Equal(t, stream1.producers[0].url, stream2.producers[0].url)\n\trequire.Len(t, streams, 2)\n}\n\nfunc TestTempate(t *testing.T) {\n\tHandleFunc(\"rtsp\", func(url string) (core.Producer, error) { return nil, nil }) // bypass HasProducer\n\n\t// config from yaml\n\tstream1, err := New(\"camera.from_hass\", \"ffmpeg:{input}#video=copy\")\n\trequire.NoError(t, err)\n\t// request from hass\n\tstream2, err := Patch(\"camera.from_hass\", \"rtsp://example.com\")\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, stream1, stream2)\n\trequire.Equal(t, \"ffmpeg:rtsp://example.com#video=copy\", stream1.producers[0].url)\n}\n"
  },
  {
    "path": "internal/streams/streams.go",
    "content": "package streams\n\nimport (\n\t\"errors\"\n\t\"net/url\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/internal/app\"\n\t\"github.com/rs/zerolog\"\n)\n\nfunc Init() {\n\tvar cfg struct {\n\t\tStreams map[string]any    `yaml:\"streams\"`\n\t\tPublish map[string]any    `yaml:\"publish\"`\n\t\tPreload map[string]string `yaml:\"preload\"`\n\t}\n\n\tapp.LoadConfig(&cfg)\n\n\tlog = app.GetLogger(\"streams\")\n\n\tfor name, item := range cfg.Streams {\n\t\tstreams[name] = NewStream(item)\n\t}\n\n\tapi.HandleFunc(\"api/streams\", apiStreams)\n\tapi.HandleFunc(\"api/streams.dot\", apiStreamsDOT)\n\tapi.HandleFunc(\"api/preload\", apiPreload)\n\tapi.HandleFunc(\"api/schemes\", apiSchemes)\n\n\tif cfg.Publish == nil && cfg.Preload == nil {\n\t\treturn\n\t}\n\n\ttime.AfterFunc(time.Second, func() {\n\t\t// range for nil map is OK\n\t\tfor name, dst := range cfg.Publish {\n\t\t\tif stream := Get(name); stream != nil {\n\t\t\t\tPublish(stream, dst)\n\t\t\t}\n\t\t}\n\t\tfor name, rawQuery := range cfg.Preload {\n\t\t\tif err := AddPreload(name, rawQuery); err != nil {\n\t\t\t\tlog.Error().Err(err).Caller().Send()\n\t\t\t}\n\t\t}\n\t})\n}\n\nfunc New(name string, sources ...string) (*Stream, error) {\n\tfor _, source := range sources {\n\t\tif !HasProducer(source) {\n\t\t\treturn nil, errors.New(\"streams: source not supported\")\n\t\t}\n\n\t\tif err := Validate(source); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tstream := NewStream(sources)\n\n\tstreamsMu.Lock()\n\tstreams[name] = stream\n\tstreamsMu.Unlock()\n\n\treturn stream, nil\n}\n\nfunc Patch(name string, source string) (*Stream, error) {\n\tstreamsMu.Lock()\n\tdefer streamsMu.Unlock()\n\n\t// check if source links to some stream name from go2rtc\n\tif u, err := url.Parse(source); err == nil && u.Scheme == \"rtsp\" && len(u.Path) > 1 {\n\t\trtspName := u.Path[1:]\n\t\tif stream, ok := streams[rtspName]; ok {\n\t\t\tif streams[name] != stream {\n\t\t\t\t// link (alias) streams[name] to streams[rtspName]\n\t\t\t\tstreams[name] = stream\n\t\t\t}\n\t\t\treturn stream, nil\n\t\t}\n\t}\n\n\tif stream, ok := streams[source]; ok {\n\t\tif name != source {\n\t\t\t// link (alias) streams[name] to streams[source]\n\t\t\tstreams[name] = stream\n\t\t}\n\t\treturn stream, nil\n\t}\n\n\t// check if src has supported scheme\n\tif !HasProducer(source) {\n\t\treturn nil, errors.New(\"streams: source not supported\")\n\t}\n\n\tif err := Validate(source); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// check an existing stream with this name\n\tif stream, ok := streams[name]; ok {\n\t\tstream.SetSource(source)\n\t\treturn stream, nil\n\t}\n\n\t// create new stream with this name\n\tstream := NewStream(source)\n\tstreams[name] = stream\n\treturn stream, nil\n}\n\nfunc GetOrPatch(query url.Values) (*Stream, error) {\n\t// check if src param exists\n\tsource := query.Get(\"src\")\n\tif source == \"\" {\n\t\treturn nil, errors.New(\"streams: source empty\")\n\t}\n\n\t// check if src is stream name\n\tif stream := Get(source); stream != nil {\n\t\treturn stream, nil\n\t}\n\n\t// check if name param provided\n\tif name := query.Get(\"name\"); name != \"\" {\n\t\treturn Patch(name, source)\n\t}\n\n\t// return new stream with src as name\n\treturn Patch(source, source)\n}\n\nvar log zerolog.Logger\n\n// streams map\n\nvar streams = map[string]*Stream{}\nvar streamsMu sync.Mutex\n\nfunc Get(name string) *Stream {\n\tstreamsMu.Lock()\n\tdefer streamsMu.Unlock()\n\treturn streams[name]\n}\n\nfunc Delete(name string) {\n\tstreamsMu.Lock()\n\tdefer streamsMu.Unlock()\n\tdelete(streams, name)\n}\n\nfunc GetAllNames() []string {\n\tstreamsMu.Lock()\n\tnames := make([]string, 0, len(streams))\n\tfor name := range streams {\n\t\tnames = append(names, name)\n\t}\n\tstreamsMu.Unlock()\n\treturn names\n}\n\nfunc GetAllSources() map[string][]string {\n\tstreamsMu.Lock()\n\tsources := make(map[string][]string, len(streams))\n\tfor name, stream := range streams {\n\t\tsources[name] = stream.Sources()\n\t}\n\tstreamsMu.Unlock()\n\treturn sources\n}\n"
  },
  {
    "path": "internal/tapo/README.md",
    "content": "# TP-Link Tapo\n\n[`new in v1.2.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.2.0)\n\n[TP-Link Tapo](https://www.tapo.com/) proprietary camera protocol with **two-way audio** support.\n\n- stream quality is the same as [RTSP protocol](https://www.tapo.com/en/faq/34/)\n- use the **cloud password**, this is not the RTSP password! you do not need to add a login!\n- you can also use **UPPERCASE** MD5 hash from your cloud password with `admin` username\n- some new camera firmwares require SHA256 instead of MD5\n\n## Configuration\n\n```yaml\nstreams:\n  # cloud password without username\n  camera1: tapo://cloud-password@192.168.1.123\n  # admin username and UPPERCASE MD5 cloud-password hash\n  camera2: tapo://admin:UPPERCASE-MD5@192.168.1.123\n  # admin username and UPPERCASE SHA256 cloud-password hash\n  camera3: tapo://admin:UPPERCASE-SHA256@192.168.1.123\n  # VGA stream (the so called substream, the lower resolution one)\n  camera4: tapo://cloud-password@192.168.1.123?subtype=1 \n  # HD stream (default)\n  camera5: tapo://cloud-password@192.168.1.123?subtype=0 \n```\n\n```bash\necho -n \"cloud password\" | md5 | awk '{print toupper($0)}'\necho -n \"cloud password\" | shasum -a 256 | awk '{print toupper($0)}'\n```\n\n## TP-Link Kasa\n\n[`new in v1.7.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.7.0)\n\n> [!NOTE]\n> This source should be moved to separate module. Because it's source code not related to Tapo.\n\n[TP-Link Kasa](https://www.kasasmart.com/) non-standard protocol [more info](https://medium.com/@hu3vjeen/reverse-engineering-tp-link-kc100-bac4641bf1cd).\n\n- `username` - urlsafe email, `alex@gmail.com` -> `alex%40gmail.com`\n- `password` - base64password, `secret1` -> `c2VjcmV0MQ==`\n\n```yaml\nstreams:\n  kc401: kasa://username:password@192.168.1.123:19443/https/stream/mixed\n```\n\nTested: KD110, KC200, KC401, KC420WS, EC71.\n\n## TP-Link Vigi\n\n[`new in v1.9.8`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.9.8)\n\n[TP-Link VIGI](https://www.vigi.com/) cameras. These are cameras from a different sub-brand, but the format is very similar to Tapo. Only the authorization is different. Read more [here](https://github.com/AlexxIT/go2rtc/issues/1470).\n\n```yaml\nstreams:\n  camera1: vigi://admin:{password}@192.168.1.123\n```\n"
  },
  {
    "path": "internal/tapo/tapo.go",
    "content": "package tapo\n\nimport (\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/kasa\"\n\t\"github.com/AlexxIT/go2rtc/pkg/tapo\"\n)\n\nfunc Init() {\n\tstreams.HandleFunc(\"kasa\", func(source string) (core.Producer, error) {\n\t\treturn kasa.Dial(source)\n\t})\n\n\tstreams.HandleFunc(\"tapo\", func(source string) (core.Producer, error) {\n\t\treturn tapo.Dial(source)\n\t})\n\n\tstreams.HandleFunc(\"vigi\", func(source string) (core.Producer, error) {\n\t\treturn tapo.Dial(source)\n\t})\n}\n"
  },
  {
    "path": "internal/tuya/README.md",
    "content": "# Tuya\n\n[`new in v1.9.13`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.9.13) by [@seydx](https://github.com/seydx)\n\n[Tuya](https://www.tuya.com/) is a proprietary camera protocol with **two-way audio** support. go2rtc supports `Tuya Smart API` and `Tuya Cloud API`.\n\n**Tuya Smart API (recommended)**:\n- **Smart Life accounts are NOT supported**, you need to create a Tuya Smart account. If the cameras are already added to the Smart Life app, you need to remove them and add them again to the [Tuya Smart](https://play.google.com/store/apps/details?id=com.tuya.smart) app.\n- Cameras can be discovered through the go2rtc web interface via Tuya Smart account (Add > Tuya > Select region and fill in email and password > Login).\n\n**Tuya Cloud API**:\n- Requires setting up a cloud project in the Tuya Developer Platform.\n- Obtain `device_id`, `client_id`, `client_secret`, and `uid` from [Tuya IoT Platform](https://iot.tuya.com/). [Here's a guide](https://xzetsubou.github.io/hass-localtuya/cloud_api/).\n- Please ensure that you have subscribed to the `IoT Video Live Stream` service (Free Trial) in the Tuya Developer Platform, otherwise the stream will not work (Tuya Developer Platform > Service API > Authorize > IoT Video Live Stream).\n\n## Configuration\n\nUse the `resolution` parameter to select the stream (not all cameras support an `hd` stream through WebRTC even if the camera supports it):\n- `hd` - HD stream (default)\n- `sd` - SD stream\n\n```yaml\nstreams:\n  # Tuya Smart API: WebRTC main stream (use Add > Tuya to discover the URL)\n  tuya_main:\n    - tuya://protect-us.ismartlife.me?device_id=XXX&email=XXX&password=XXX\n\n  # Tuya Smart API: WebRTC sub stream (use Add > Tuya to discover the URL)\n  tuya_sub:\n    - tuya://protect-us.ismartlife.me?device_id=XXX&email=XXX&password=XXX&resolution=sd\n\n  # Tuya Cloud API: WebRTC main stream\n  tuya_webrtc:\n   - tuya://openapi.tuyaus.com?device_id=XXX&uid=XXX&client_id=XXX&client_secret=XXX\n  \n  # Tuya Cloud API: WebRTC sub stream\n  tuya_webrtc_sd:\n   - tuya://openapi.tuyaus.com?device_id=XXX&uid=XXX&client_id=XXX&client_secret=XXX&resolution=sd\n```\n"
  },
  {
    "path": "internal/tuya/tuya.go",
    "content": "package tuya\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/tuya\"\n)\n\nfunc Init() {\n\tstreams.HandleFunc(\"tuya\", func(source string) (core.Producer, error) {\n\t\treturn tuya.Dial(source)\n\t})\n\n\tapi.HandleFunc(\"api/tuya\", apiTuya)\n}\n\nfunc apiTuya(w http.ResponseWriter, r *http.Request) {\n\tquery := r.URL.Query()\n\tregion := query.Get(\"region\")\n\temail := query.Get(\"email\")\n\tpassword := query.Get(\"password\")\n\n\tif email == \"\" || password == \"\" || region == \"\" {\n\t\thttp.Error(w, \"email, password and region are required\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tvar tuyaRegion *tuya.Region\n\tfor _, r := range tuya.AvailableRegions {\n\t\tif r.Host == region {\n\t\t\ttuyaRegion = &r\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif tuyaRegion == nil {\n\t\thttp.Error(w, fmt.Sprintf(\"invalid region: %s\", region), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\thttpClient := tuya.CreateHTTPClientWithSession()\n\n\t_, err := login(httpClient, tuyaRegion.Host, email, password, tuyaRegion.Continent)\n\tif err != nil {\n\t\thttp.Error(w, fmt.Sprintf(\"login failed: %v\", err), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\ttuyaAPI, err := tuya.NewTuyaSmartApiClient(\n\t\thttpClient,\n\t\ttuyaRegion.Host,\n\t\temail,\n\t\tpassword,\n\t\t\"\",\n\t)\n\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tvar devices []tuya.Device\n\n\thomes, _ := tuyaAPI.GetHomeList()\n\tif homes != nil && len(homes.Result) > 0 {\n\t\tfor _, home := range homes.Result {\n\t\t\troomList, err := tuyaAPI.GetRoomList(strconv.Itoa(home.Gid))\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfor _, room := range roomList.Result {\n\t\t\t\tfor _, device := range room.DeviceList {\n\t\t\t\t\tif (device.Category == \"sp\" || device.Category == \"dghsxj\") && !containsDevice(devices, device.DeviceId) {\n\t\t\t\t\t\tdevices = append(devices, device)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tsharedHomes, _ := tuyaAPI.GetSharedHomeList()\n\tif sharedHomes != nil && len(sharedHomes.Result.SecurityWebCShareInfoList) > 0 {\n\t\tfor _, sharedHome := range sharedHomes.Result.SecurityWebCShareInfoList {\n\t\t\tfor _, device := range sharedHome.DeviceInfoList {\n\t\t\t\tif (device.Category == \"sp\" || device.Category == \"dghsxj\") && !containsDevice(devices, device.DeviceId) {\n\t\t\t\t\tdevices = append(devices, device)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tif len(devices) == 0 {\n\t\thttp.Error(w, \"no cameras found\", http.StatusNotFound)\n\t\treturn\n\t}\n\n\tvar items []*api.Source\n\tfor _, device := range devices {\n\t\tcleanQuery := url.Values{}\n\t\tcleanQuery.Set(\"device_id\", device.DeviceId)\n\t\tcleanQuery.Set(\"email\", email)\n\t\tcleanQuery.Set(\"password\", password)\n\t\turl := fmt.Sprintf(\"tuya://%s?%s\", tuyaRegion.Host, cleanQuery.Encode())\n\n\t\titems = append(items, &api.Source{\n\t\t\tName: device.DeviceName,\n\t\t\tURL:  url,\n\t\t})\n\t}\n\n\tapi.ResponseSources(w, items)\n}\n\nfunc login(client *http.Client, serverHost, email, password, countryCode string) (*tuya.LoginResult, error) {\n\ttokenResp, err := getLoginToken(client, serverHost, email, countryCode)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tencryptedPassword, err := tuya.EncryptPassword(password, tokenResp.Result.PbKey)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to encrypt password: %v\", err)\n\t}\n\n\tvar loginResp *tuya.PasswordLoginResponse\n\tvar url string\n\n\tloginReq := tuya.PasswordLoginRequest{\n\t\tCountryCode: countryCode,\n\t\tPasswd:      encryptedPassword,\n\t\tToken:       tokenResp.Result.Token,\n\t\tIfEncrypt:   1,\n\t\tOptions:     `{\"group\":1}`,\n\t}\n\n\tif tuya.IsEmailAddress(email) {\n\t\turl = fmt.Sprintf(\"https://%s/api/private/email/login\", serverHost)\n\t\tloginReq.Email = email\n\t} else {\n\t\turl = fmt.Sprintf(\"https://%s/api/private/phone/login\", serverHost)\n\t\tloginReq.Mobile = email\n\t}\n\n\tloginResp, err = performLogin(client, url, loginReq, serverHost)\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif !loginResp.Success {\n\t\treturn nil, errors.New(loginResp.ErrorMsg)\n\t}\n\n\treturn &loginResp.Result, nil\n}\n\nfunc getLoginToken(client *http.Client, serverHost, username, countryCode string) (*tuya.LoginTokenResponse, error) {\n\turl := fmt.Sprintf(\"https://%s/api/login/token\", serverHost)\n\n\ttokenReq := tuya.LoginTokenRequest{\n\t\tCountryCode: countryCode,\n\t\tUsername:    username,\n\t\tIsUid:       false,\n\t}\n\n\tjsonData, err := json.Marshal(tokenReq)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq, err := http.NewRequest(\"POST\", url, bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json; charset=utf-8\")\n\treq.Header.Set(\"Accept\", \"*/*\")\n\treq.Header.Set(\"Origin\", fmt.Sprintf(\"https://%s\", serverHost))\n\treq.Header.Set(\"Referer\", fmt.Sprintf(\"https://%s/login\", serverHost))\n\treq.Header.Set(\"X-Requested-With\", \"XMLHttpRequest\")\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\tvar tokenResp tuya.LoginTokenResponse\n\tif err = json.NewDecoder(resp.Body).Decode(&tokenResp); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif !tokenResp.Success {\n\t\treturn nil, errors.New(\"tuya: \" + tokenResp.Msg)\n\t}\n\n\treturn &tokenResp, nil\n}\n\nfunc performLogin(client *http.Client, url string, loginReq tuya.PasswordLoginRequest, serverHost string) (*tuya.PasswordLoginResponse, error) {\n\tjsonData, err := json.Marshal(loginReq)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq, err := http.NewRequest(\"POST\", url, bytes.NewBuffer(jsonData))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json; charset=utf-8\")\n\treq.Header.Set(\"Accept\", \"*/*\")\n\treq.Header.Set(\"Origin\", fmt.Sprintf(\"https://%s\", serverHost))\n\treq.Header.Set(\"Referer\", fmt.Sprintf(\"https://%s/login\", serverHost))\n\treq.Header.Set(\"X-Requested-With\", \"XMLHttpRequest\")\n\n\tresp, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\tvar loginResp tuya.PasswordLoginResponse\n\tif err := json.NewDecoder(resp.Body).Decode(&loginResp); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &loginResp, nil\n}\n\nfunc containsDevice(devices []tuya.Device, deviceID string) bool {\n\tfor _, device := range devices {\n\t\tif device.DeviceId == deviceID {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "internal/v4l2/README.md",
    "content": "# Video4Linux\n\n[`new in v1.9.9`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.9.9)\n\nWhat you should to know about [V4L2](https://en.wikipedia.org/wiki/Video4Linux):\n\n- V4L2 (Video for Linux API version 2) works only in Linux\n- supports USB cameras and other similar devices\n- one device can only be connected to one software simultaneously\n- cameras support a fixed list of formats, resolutions and frame rates\n- basic cameras supports only RAW (non-compressed) pixel formats\n- regular cameras supports MJPEG format (series of JPEG frames)\n- advances cameras support H264 format (MSE/MP4, WebRTC compatible)\n- using MJPEG and H264 formats (if the camera supports them) won't cost you the CPU usage\n- transcoding RAW format to MJPEG or H264 - will cost you a significant CPU usage\n- H265 (HEVC) format is also supported (if the camera supports it)\n\nTests show that the basic Keenetic router with MIPS processor can broadcast three MJPEG cameras in the following resolutions: 1600х1200 + 640х480 + 640х480. The USB bus bandwidth is no more enough for larger resolutions. CPU consumption is no more than 5%.\n\nSupported formats for your camera can be found here: **Go2rtc > WebUI > Add > V4L2**.\n\n## RAW format\n\nExample:\n\n```yaml\nstreams:\n  camera1: v4l2:device?video=/dev/video0&input_format=yuyv422&video_size=1280x720&framerate=10\n```\n\nGo2rtc supports built-in transcoding of RAW to MJPEG format. This does not need to be additionally configured.\n\n```\nffplay http://localhost:1984/api/stream.mjpeg?src=camera1\n```\n\n**Important.** You don't have to transcode the RAW format to transmit it over the network. You can stream it in `y4m` format, which is perfectly supported by ffmpeg. It won't cost you a CPU usage. But will require high network bandwidth.\n\n```\nffplay http://localhost:1984/api/stream.y4m?src=camera1\n```\n"
  },
  {
    "path": "internal/v4l2/v4l2.go",
    "content": "//go:build !(linux && (386 || arm || mipsle || amd64 || arm64))\n\npackage v4l2\n\nfunc Init() {\n\t// not supported\n}\n"
  },
  {
    "path": "internal/v4l2/v4l2_linux.go",
    "content": "//go:build linux && (386 || arm || mipsle || amd64 || arm64)\n\npackage v4l2\n\nimport (\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/v4l2\"\n\t\"github.com/AlexxIT/go2rtc/pkg/v4l2/device\"\n)\n\nfunc Init() {\n\tstreams.HandleFunc(\"v4l2\", func(source string) (core.Producer, error) {\n\t\treturn v4l2.Open(source)\n\t})\n\n\tapi.HandleFunc(\"api/v4l2\", apiV4L2)\n}\n\nfunc apiV4L2(w http.ResponseWriter, r *http.Request) {\n\tfiles, err := os.ReadDir(\"/dev\")\n\tif err != nil {\n\t\treturn\n\t}\n\n\tvar sources []*api.Source\n\n\tfor _, file := range files {\n\t\tif !strings.HasPrefix(file.Name(), core.KindVideo) {\n\t\t\tcontinue\n\t\t}\n\n\t\tpath := \"/dev/\" + file.Name()\n\n\t\tdev, err := device.Open(path)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tformats, _ := dev.ListFormats()\n\t\tfor _, fourCC := range formats {\n\t\t\tname, ffmpeg := findFormat(fourCC)\n\t\t\tsource := &api.Source{Name: name}\n\n\t\t\tsizes, _ := dev.ListSizes(fourCC)\n\t\t\tfor _, wh := range sizes {\n\t\t\t\tif source.Info != \"\" {\n\t\t\t\t\tsource.Info += \" \"\n\t\t\t\t}\n\n\t\t\t\tsource.Info += fmt.Sprintf(\"%dx%d\", wh[0], wh[1])\n\n\t\t\t\tframeRates, _ := dev.ListFrameRates(fourCC, wh[0], wh[1])\n\t\t\t\tfor _, fr := range frameRates {\n\t\t\t\t\tsource.Info += fmt.Sprintf(\"@%d\", fr)\n\n\t\t\t\t\tif source.URL == \"\" && ffmpeg != \"\" {\n\t\t\t\t\t\tsource.URL = fmt.Sprintf(\n\t\t\t\t\t\t\t\"v4l2:device?video=%s&input_format=%s&video_size=%dx%d&framerate=%d\",\n\t\t\t\t\t\t\tpath, ffmpeg, wh[0], wh[1], fr,\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\tif source.Info != \"\" {\n\t\t\t\tsources = append(sources, source)\n\t\t\t}\n\t\t}\n\n\t\t_ = dev.Close()\n\t}\n\n\tapi.ResponseSources(w, sources)\n}\n\nfunc findFormat(fourCC uint32) (name, ffmpeg string) {\n\tfor _, format := range device.Formats {\n\t\tif format.FourCC == fourCC {\n\t\t\treturn format.Name, format.FFmpeg\n\t\t}\n\t}\n\treturn string(binary.LittleEndian.AppendUint32(nil, fourCC)), \"\"\n}\n"
  },
  {
    "path": "internal/webrtc/README.md",
    "content": "# WebRTC\n\n## WebRTC Client\n\n[`new in v1.3.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)\n\nThis source type supports four connection formats.\n\n### Creality\n\n[`new in v1.9.10`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.9.10)\n\n[Creality](https://www.creality.com/) 3D printer camera. Read more [here](https://github.com/AlexxIT/go2rtc/issues/1600).\n\n```yaml\nstreams:\n  creality_k2p: webrtc:http://192.168.1.123:8000/call/webrtc_local#format=creality\n```\n\n### go2rtc\n\nThis format is only supported in go2rtc. Unlike WHEP, it supports asynchronous WebRTC connections and two-way audio.\n\n```yaml\nstreams:\n  webrtc-go2rtc:    webrtc:ws://192.168.1.123:1984/api/ws?src=camera1\n```\n\n### Kinesis\n\n[`new in v1.6.1`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.1)\n\nSupports [Amazon Kinesis Video Streams](https://aws.amazon.com/kinesis/video-streams/), using WebRTC protocol. You need to specify the signaling WebSocket URL with all credentials in query params, `client_id` and `ice_servers` list in [JSON format](https://developer.mozilla.org/en-US/docs/Web/API/RTCIceServer).\n\n```yaml\nstreams:\n  webrtc-kinesis:   webrtc:wss://...amazonaws.com/?...#format=kinesis#client_id=...#ice_servers=[{...},{...}]\n```\n\n**PS.** For `kinesis` sources, you can use [echo](../echo/README.md) to get connection params using `bash`, `python` or any other script language.\n\n### OpenIPC\n\n[`new in v1.7.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.7.0)\n\nCameras on open-source [OpenIPC](https://openipc.org/) firmware.\n\n```yaml\nstreams:\n  webrtc-openipc:   webrtc:ws://192.168.1.123/webrtc_ws#format=openipc#ice_servers=[{\"urls\":\"stun:stun.kinesisvideo.eu-north-1.amazonaws.com:443\"}]\n```\n\n### SwitchBot\n\nSupport connection to [SwitchBot](https://us.switch-bot.com/) cameras that are based on Kinesis Video Streams. Specifically, this includes [Pan/Tilt Cam Plus 2K](https://us.switch-bot.com/pages/switchbot-pan-tilt-cam-plus-2k) and [Pan/Tilt Cam Plus 3K](https://us.switch-bot.com/pages/switchbot-pan-tilt-cam-plus-3k) and [Smart Video Doorbell](https://www.switchbot.jp/products/switchbot-smart-video-doorbell). `Outdoor Spotlight Cam 1080P`, `Outdoor Spotlight Cam 2K`, `Pan/Tilt Cam`, `Pan/Tilt Cam 2K`, `Indoor Cam` are based on Tuya, so this feature is not available.\n\n```yaml\nstreams:\n  webrtc-switchbot: webrtc:wss://...amazonaws.com/?...#format=switchbot#resolution=hd#play_type=0#client_id=...#ice_servers=[{...},{...}]\n```\n\n### WHEP\n\n[WebRTC/WHEP](https://datatracker.ietf.org/doc/draft-murillo-whep/) is replaced by [WebRTC/WISH](https://datatracker.ietf.org/doc/charter-ietf-wish/02/) standard for WebRTC video/audio viewers. But it may already be supported in some third-party software. It is supported in go2rtc.\n\n```yaml\nstreams:\n  webrtc-whep:      webrtc:http://192.168.1.123:1984/api/webrtc?src=camera1\n```\n\n### Wyze\n\n[`new in v1.6.1`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.6.1)\n\nLegacy method to connect to [Wyze](https://www.wyze.com/) cameras using WebRTC protocol via [docker-wyze-bridge](https://github.com/mrlt8/docker-wyze-bridge). For native P2P support without docker-wyze-bridge, see [Source: Wyze](../wyze/README.md).\n\n```yaml\nstreams:\n  webrtc-wyze:      webrtc:http://192.168.1.123:5000/signaling/camera1?kvs#format=wyze\n```\n\n## WebRTC Server\n\nWhat you should know about WebRTC:\n\n- It's almost always a **direct [peer-to-peer](https://en.wikipedia.org/wiki/Peer-to-peer) connection** from your browser to the go2rtc app\n- When you use Home Assistant, Frigate, Nginx, Nabu Casa, Cloudflare, and other software, they are only **involved in establishing** the connection; they are **not involved in transferring** media data\n- WebRTC media cannot be transferred inside an HTTP connection\n- Usually, WebRTC uses random UDP ports on the client and server to establish a connection\n- Usually, WebRTC uses public [STUN](https://en.wikipedia.org/wiki/STUN) servers to establish a connection outside the LAN; these servers are only needed to establish a connection and are not involved in data transfer\n- Usually, WebRTC will automatically discover all of your local and public addresses and try to establish a connection\n\nIf an external connection via STUN is used:\n\n- Uses [UDP hole punching](https://en.wikipedia.org/wiki/UDP_hole_punching) technology to bypass NAT even if you haven't opened your server to the world\n- For about 20% of users, the technology will not work because of the [Symmetric NAT](https://tomchen.github.io/symmetric-nat-test/)\n- UDP is not suitable for transmitting 2K and 4K high bit rate video over open networks because of the high loss rate:\n  - https://habr.com/ru/companies/flashphoner/articles/480006/\n  - https://www.youtube.com/watch?v=FXVg2ckuKfs\n\n### Configuration suggestions\n\n- by default, WebRTC uses both TCP and UDP on port 8555 for connections\n- you can use this port for external access\n- you can change the port in YAML config:\n\n```yaml\nwebrtc:\n  listen: \":8555\"  # address of your local server and port (TCP/UDP)\n```\n\n#### Static public IP\n\n- forward the port 8555 on your router (you can use the same 8555 port or any other as external port)\n- add your external IP address and external port to the YAML config\n\n```yaml\nwebrtc:\n  candidates:\n    - 216.58.210.174:8555  # if you have a static public IP address\n```\n\n#### Dynamic public IP\n\n- forward the port 8555 on your router (you can use the same 8555 port or any other as the external port)\n- add `stun` word and external port to YAML config\n    - go2rtc automatically detects your external address with STUN server\n\n```yaml\nwebrtc:\n  candidates:\n    - stun:8555  # if you have a dynamic public IP address\n```\n\n#### Hard tech way 1. Own TCP-tunnel\n\nIf you have a personal [VPS](https://en.wikipedia.org/wiki/Virtual_private_server), you can create a TCP tunnel and setup in the same way as \"Static public IP\". But use your VPS IP address in the YAML config.\n\n#### Hard tech way 2. Using TURN-server\n\nIf you have personal [VPS](https://en.wikipedia.org/wiki/Virtual_private_server), you can install TURN server (e.g. [coturn](https://github.com/coturn/coturn), config [example](https://github.com/AlexxIT/WebRTC/wiki/Coturn-Example)).\n\n```yaml\nwebrtc:\n  ice_servers:\n    - urls: [stun:stun.l.google.com:19302]\n    - urls: [turn:123.123.123.123:3478]\n      username: your_user\n      credential: your_pass\n```\n\n### Full configuration\n\n**Important!** This example is not for copy/pasting!\n\n```yaml\nwebrtc:\n  # fix local TCP or UDP or both ports for WebRTC media\n  listen: \":8555\"            # address of your local server\n\n  # add additional host candidates manually\n  # order is important, the first will have a higher priority\n  candidates:\n    - 216.58.210.174:8555    # if you have static public IP-address\n    - stun:8555              # if you have dynamic public IP-address\n    - home.duckdns.org:8555  # if you have domain\n\n  # add custom STUN and TURN servers\n  # use `ice_servers: []` to remove defaults and leave it empty\n  ice_servers:\n    - urls: [ stun:stun1.l.google.com:19302 ]\n    - urls: [ turn:123.123.123.123:3478 ]\n      username: your_user\n      credential: your_pass\n\n  # optional filter list for auto-discovery logic\n  # some settings only make sense if you don't specify a fixed UDP port\n  filters:\n    # list of host candidates from auto-discovery to be sent\n    # includes candidates from the `listen` option\n    # use `candidates: []` to remove all auto-discovery candidates\n    candidates: [ 192.168.1.123 ]\n\n    # enable localhost candidates\n    loopback: true\n\n    # list of network types to be used for the connection\n    # includes candidates from the `listen` option\n    networks: [ udp4, udp6, tcp4, tcp6 ]\n\n    # list of interfaces to be used for the connection\n    # includes interfaces from unspecified `listen` option (empty host)\n    interfaces: [ eno1 ]\n\n    # list of host IP addresses to be used for the connection\n    # includes IPs from unspecified `listen` option (empty host)\n    ips: [ 192.168.1.123 ]\n\n    # range for random UDP ports [min, max] to be used for connection\n    # not related to the `listen` option\n    udp_ports: [ 50000, 50100 ]\n```\n\nBy default, go2rtc uses a **fixed TCP** port and **fixed UDP** ports for each **direct** WebRTC connection: `listen: \":8555\"`.\n\nYou can set a **fixed TCP** port and a **random UDP** port for all connections: `listen: \":8555/tcp\"`.\n\nYou can also disable the TCP port and leave only random UDP ports: `listen: \"\"`.\n\n### Configuration filters\n\n**Important!** By default, go2rtc excludes all Docker-like candidates (`172.16.0.0/12`). This cannot be disabled.\n\nFilters allow you to exclude unnecessary candidates. Extra candidates don't make your connection worse or better. But the wrong filter settings can break everything. Skip this setting if you don't understand it.\n\nFor example, go2rtc is installed on the host system. And there are unnecessary interfaces. You can keep only the relevant via `interfaces` or `ips` options. You can also exclude IPv6 candidates if your server supports them but your home network does not.\n\n```yaml\nwebrtc:\n  listen: \":8555/tcp\"         # use fixed TCP port and random UDP ports\n  filters:\n    ips: [ 192.168.1.2 ]      # IP-address of your server\n    networks: [ udp4, tcp4 ]  # skip IPv6, if it's not supported for you\n```\n\nFor example, go2rtc is inside a closed Docker container (e.g. [Frigate](https://frigate.video/)). You shouldn't filter Docker interfaces; otherwise, go2rtc won't be able to connect anywhere. But you can filter the Docker candidates because no one can connect to them.\n\n```yaml\nwebrtc:\n  listen: \":8555\"                   # use fixed TCP and UDP ports\n  candidates: [ 192.168.1.2:8555 ]  # add manual host candidate (use docker port forwarding)\n```\n\n## Streaming ingest\n\n### Ingest: Browser\n\n[`new in v1.3.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)\n\nYou can turn the browser of any PC or mobile into an IP camera with support for video and two-way audio. Or even broadcast your PC screen:\n\n1. Create empty stream in the `go2rtc.yaml`\n2. Go to go2rtc WebUI\n3. Open `links` page for your stream\n4. Select `camera+microphone` or `display+speaker` option\n5. Open `webrtc` local page (your go2rtc **should work over HTTPS!**) or `share link` via [WebTorrent](../webtorrent/README.md) technology (work over HTTPS by default)\n\n### Ingest: WHIP\n\n[`new in v1.3.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)\n\nYou can use **OBS Studio** or any other broadcast software with [WHIP](https://www.ietf.org/archive/id/draft-ietf-wish-whip-01.html) protocol support. This standard has not yet been approved. But you can download OBS Studio [dev version](https://github.com/obsproject/obs-studio/actions/runs/3969201209):\n\n- Settings > Stream > Service: WHIP > `http://192.168.1.123:1984/api/webrtc?dst=camera1`\n\n## Useful links\n\n- https://www.ietf.org/archive/id/draft-ietf-wish-whip-01.html\n- https://www.ietf.org/id/draft-murillo-whep-01.html\n- https://github.com/Glimesh/broadcast-box/\n- https://github.com/obsproject/obs-studio/pull/7926\n- https://misi.github.io/webrtc-c0d3l4b/\n- https://github.com/webtorrent/webtorrent/blob/master/docs/faq.md\n"
  },
  {
    "path": "internal/webrtc/candidates.go",
    "content": "package webrtc\n\nimport (\n\t\"net\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api/ws\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/webrtc\"\n\t\"github.com/AlexxIT/go2rtc/pkg/xnet\"\n\tpion \"github.com/pion/webrtc/v4\"\n)\n\ntype Address struct {\n\thost     string\n\tPort     string\n\tNetwork  string\n\tPriority uint32\n}\n\nvar stuns []string\n\nfunc (a *Address) Host() string {\n\tif a.host == \"stun\" {\n\t\tip, err := webrtc.GetCachedPublicIP(stuns...)\n\t\tif err != nil {\n\t\t\treturn \"\"\n\t\t}\n\t\treturn ip.String()\n\t}\n\treturn a.host\n}\n\nfunc (a *Address) Marshal() string {\n\tif host := a.Host(); host != \"\" {\n\t\treturn webrtc.CandidateICE(a.Network, host, a.Port, a.Priority)\n\t}\n\treturn \"\"\n}\n\nvar addresses []*Address\nvar filters webrtc.Filters\n\nfunc AddCandidate(network, address string) {\n\tif network == \"\" {\n\t\tAddCandidate(\"tcp\", address)\n\t\tAddCandidate(\"udp\", address)\n\t\treturn\n\t}\n\n\thost, port, err := net.SplitHostPort(address)\n\tif err != nil {\n\t\treturn\n\t}\n\n\t// start from 1, so manual candidates will be lower than built-in\n\t// and every next candidate will have a lower priority\n\tcandidateIndex := 1 + len(addresses)\n\n\tpriority := webrtc.CandidateHostPriority(network, candidateIndex)\n\taddresses = append(addresses, &Address{host, port, network, priority})\n}\n\nfunc GetCandidates() (candidates []string) {\n\tfor _, address := range addresses {\n\t\tif candidate := address.Marshal(); candidate != \"\" {\n\t\t\tcandidates = append(candidates, candidate)\n\t\t}\n\t}\n\treturn\n}\n\n// FilterCandidate return true if candidate passed the check\nfunc FilterCandidate(candidate *pion.ICECandidate) bool {\n\tif candidate == nil {\n\t\treturn false\n\t}\n\n\t// remove any Docker-like IP from candidates\n\tif ip := net.ParseIP(candidate.Address); ip != nil && xnet.Docker.Contains(ip) {\n\t\treturn false\n\t}\n\n\t// host candidate should be in the hosts list\n\tif candidate.Typ == pion.ICECandidateTypeHost && filters.Candidates != nil {\n\t\tif !core.Contains(filters.Candidates, candidate.Address) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\tif filters.Networks != nil {\n\t\tnetworkType := NetworkType(candidate.Protocol.String(), candidate.Address)\n\t\tif !core.Contains(filters.Networks, networkType) {\n\t\t\treturn false\n\t\t}\n\t}\n\n\treturn true\n}\n\n// NetworkType convert tcp/udp network to tcp4/tcp6/udp4/udp6\nfunc NetworkType(network, host string) string {\n\tif strings.IndexByte(host, ':') >= 0 {\n\t\treturn network + \"6\"\n\t} else {\n\t\treturn network + \"4\"\n\t}\n}\n\nfunc asyncCandidates(tr *ws.Transport, cons *webrtc.Conn) {\n\ttr.WithContext(func(ctx map[any]any) {\n\t\tif candidates, ok := ctx[\"candidate\"].([]string); ok {\n\t\t\t// process candidates that receive before this moment\n\t\t\tfor _, candidate := range candidates {\n\t\t\t\t_ = cons.AddCandidate(candidate)\n\t\t\t}\n\n\t\t\t// remove already processed candidates\n\t\t\tdelete(ctx, \"candidate\")\n\t\t}\n\n\t\t// set variable for process candidates after this moment\n\t\tctx[\"webrtc\"] = cons\n\t})\n\n\tfor _, candidate := range GetCandidates() {\n\t\tlog.Trace().Str(\"candidate\", candidate).Msg(\"[webrtc] config\")\n\t\ttr.Write(&ws.Message{Type: \"webrtc/candidate\", Value: candidate})\n\t}\n}\n\nfunc candidateHandler(tr *ws.Transport, msg *ws.Message) error {\n\t// process incoming candidate in sync function\n\ttr.WithContext(func(ctx map[any]any) {\n\t\tcandidate := msg.String()\n\t\tlog.Trace().Str(\"candidate\", candidate).Msg(\"[webrtc] remote\")\n\n\t\tif cons, ok := ctx[\"webrtc\"].(*webrtc.Conn); ok {\n\t\t\t// if webrtc.Server already initialized - process candidate\n\t\t\t_ = cons.AddCandidate(candidate)\n\t\t} else {\n\t\t\t// or collect candidate and process it later\n\t\t\tlist, _ := ctx[\"candidate\"].([]string)\n\t\t\tctx[\"candidate\"] = append(list, candidate)\n\t\t}\n\t})\n\n\treturn nil\n}\n"
  },
  {
    "path": "internal/webrtc/client.go",
    "content": "package webrtc\n\nimport (\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api/ws\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/webrtc\"\n\t\"github.com/gorilla/websocket\"\n\tpion \"github.com/pion/webrtc/v4\"\n)\n\n// streamsHandler supports:\n//  1. WHEP:    webrtc:http://192.168.1.123:1984/api/webrtc?src=camera1\n//  2. go2rtc:  webrtc:ws://192.168.1.123:1984/api/ws?src=camera1\n//  3. Wyze:    webrtc:http://192.168.1.123:5000/signaling/camera1?kvs#format=wyze\n//  4. Kinesis: webrtc:wss://...amazonaws.com/?...#format=kinesis#client_id=...#ice_servers=[{...},{...}]\nfunc streamsHandler(rawURL string) (core.Producer, error) {\n\tvar query url.Values\n\tif i := strings.IndexByte(rawURL, '#'); i > 0 {\n\t\tquery = streams.ParseQuery(rawURL[i+1:])\n\t\trawURL = rawURL[:i]\n\t}\n\n\trawURL = rawURL[7:] // remove webrtc:\n\tif i := strings.IndexByte(rawURL, ':'); i > 0 {\n\t\tscheme := rawURL[:i]\n\t\tformat := query.Get(\"format\")\n\n\t\tswitch scheme {\n\t\tcase \"ws\", \"wss\":\n\t\t\tif format == \"kinesis\" {\n\t\t\t\t// https://aws.amazon.com/kinesis/video-streams/\n\t\t\t\t// https://docs.aws.amazon.com/kinesisvideostreams-webrtc-dg/latest/devguide/what-is-kvswebrtc.html\n\t\t\t\t// https://github.com/orgs/awslabs/repositories?q=kinesis+webrtc\n\t\t\t\treturn kinesisClient(rawURL, query, \"webrtc/kinesis\", nil)\n\t\t\t} else if format == \"openipc\" {\n\t\t\t\treturn openIPCClient(rawURL, query)\n\t\t\t} else if format == \"switchbot\" {\n\t\t\t\treturn switchbotClient(rawURL, query)\n\t\t\t} else {\n\t\t\t\treturn go2rtcClient(rawURL)\n\t\t\t}\n\n\t\tcase \"http\", \"https\":\n\t\t\tif format == \"milestone\" {\n\t\t\t\treturn milestoneClient(rawURL, query)\n\t\t\t} else if format == \"wyze\" {\n\t\t\t\t// https://github.com/mrlt8/docker-wyze-bridge\n\t\t\t\treturn wyzeClient(rawURL)\n\t\t\t} else if format == \"creality\" {\n\t\t\t\treturn crealityClient(rawURL)\n\t\t\t} else {\n\t\t\t\treturn whepClient(rawURL)\n\t\t\t}\n\t\t}\n\t}\n\treturn nil, errors.New(\"unsupported url: \" + rawURL)\n}\n\n// go2rtcClient can connect only to go2rtc server\n// ex: ws://localhost:1984/api/ws?src=camera1\nfunc go2rtcClient(url string) (core.Producer, error) {\n\t// 1. Connect to signalign server\n\tconn, _, err := Dial(url)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// close websocket when we ready return Producer or connection error\n\tdefer conn.Close()\n\n\t// 2. Create PeerConnection\n\tpc, err := PeerConnection(true)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdefer func() {\n\t\tif err != nil {\n\t\t\t_ = pc.Close()\n\t\t}\n\t}()\n\n\t// waiter will wait PC error or WS error or nil (connection OK)\n\tvar connState core.Waiter\n\tvar connMu sync.Mutex\n\n\tprod := webrtc.NewConn(pc)\n\tprod.Mode = core.ModeActiveProducer\n\tprod.Protocol = \"ws\"\n\tprod.URL = url\n\tprod.Listen(func(msg any) {\n\t\tswitch msg := msg.(type) {\n\t\tcase *pion.ICECandidate:\n\t\t\ts := msg.ToJSON().Candidate\n\t\t\tlog.Trace().Str(\"candidate\", s).Msg(\"[webrtc] local \")\n\t\t\tconnMu.Lock()\n\t\t\t_ = conn.WriteJSON(&ws.Message{Type: \"webrtc/candidate\", Value: s})\n\t\t\tconnMu.Unlock()\n\n\t\tcase pion.PeerConnectionState:\n\t\t\tswitch msg {\n\t\t\tcase pion.PeerConnectionStateConnecting:\n\t\t\tcase pion.PeerConnectionStateConnected:\n\t\t\t\tconnState.Done(nil)\n\t\t\tdefault:\n\t\t\t\tconnState.Done(errors.New(\"webrtc: \" + msg.String()))\n\t\t\t}\n\t\t}\n\t})\n\n\tmedias := []*core.Media{\n\t\t{Kind: core.KindVideo, Direction: core.DirectionRecvonly},\n\t\t{Kind: core.KindAudio, Direction: core.DirectionRecvonly},\n\t\t{Kind: core.KindAudio, Direction: core.DirectionSendonly},\n\t}\n\n\t// 3. Create offer\n\toffer, err := prod.CreateOffer(medias)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 4. Send offer\n\tmsg := &ws.Message{Type: \"webrtc/offer\", Value: offer}\n\tconnMu.Lock()\n\t_ = conn.WriteJSON(msg)\n\tconnMu.Unlock()\n\n\t// 5. Get answer\n\tif err = conn.ReadJSON(msg); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif msg.Type != \"webrtc/answer\" {\n\t\terr = errors.New(\"wrong answer: \" + msg.String())\n\t\treturn nil, err\n\t}\n\n\tanswer := msg.String()\n\tif err = prod.SetAnswer(answer); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 6. Continue to receiving candidates\n\tgo func() {\n\t\tvar err error\n\n\t\tfor {\n\t\t\t// receive data from remote\n\t\t\tvar msg ws.Message\n\t\t\tif err = conn.ReadJSON(&msg); err != nil {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tswitch msg.Type {\n\t\t\tcase \"webrtc/candidate\":\n\t\t\t\tif msg.Value != nil {\n\t\t\t\t\t_ = prod.AddCandidate(msg.String())\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconnState.Done(err)\n\t}()\n\n\tif err = connState.Wait(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn prod, nil\n}\n\n// whepClient - support WebRTC-HTTP Egress Protocol (WHEP)\n// ex: http://localhost:1984/api/webrtc?src=camera1\nfunc whepClient(url string) (core.Producer, error) {\n\t// 2. Create PeerConnection\n\tpc, err := PeerConnection(true)\n\tif err != nil {\n\t\tlog.Error().Err(err).Caller().Send()\n\t\treturn nil, err\n\t}\n\n\tprod := webrtc.NewConn(pc)\n\tprod.Mode = core.ModeActiveProducer\n\tprod.Protocol = \"http\"\n\tprod.URL = url\n\n\tmedias := []*core.Media{\n\t\t{Kind: core.KindVideo, Direction: core.DirectionRecvonly},\n\t\t{Kind: core.KindAudio, Direction: core.DirectionRecvonly},\n\t}\n\n\t// 3. Create offer\n\toffer, err := prod.CreateCompleteOffer(medias)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq, err := http.NewRequest(\"POST\", url, strings.NewReader(offer))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header.Set(\"Content-Type\", MimeSDP)\n\n\tclient := http.Client{Timeout: time.Second * 5000}\n\tdefer client.CloseIdleConnections()\n\n\tres, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tanswer, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err = prod.SetAnswer(string(answer)); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn prod, nil\n}\n\n// Dial - websocket.Dial with Basic auth support\nfunc Dial(rawURL string) (*websocket.Conn, *http.Response, error) {\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tif u.User == nil {\n\t\treturn websocket.DefaultDialer.Dial(rawURL, nil)\n\t}\n\n\tuser := u.User.Username()\n\tpass, _ := u.User.Password()\n\tu.User = nil\n\n\theader := http.Header{\n\t\t\"Authorization\": []string{\n\t\t\t\"Basic \" + base64.StdEncoding.EncodeToString([]byte(user+\":\"+pass)),\n\t\t},\n\t}\n\n\treturn websocket.DefaultDialer.Dial(u.String(), header)\n}\n"
  },
  {
    "path": "internal/webrtc/client_creality.go",
    "content": "package webrtc\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/webrtc\"\n\t\"github.com/pion/sdp/v3\"\n)\n\n// https://github.com/AlexxIT/go2rtc/issues/1600\nfunc crealityClient(url string) (core.Producer, error) {\n\tpc, err := PeerConnection(true)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tprod := webrtc.NewConn(pc)\n\tprod.FormatName = \"webrtc/creality\"\n\tprod.Mode = core.ModeActiveProducer\n\tprod.Protocol = \"http\"\n\tprod.URL = url\n\n\tmedias := []*core.Media{\n\t\t{Kind: core.KindVideo, Direction: core.DirectionRecvonly},\n\t}\n\n\t// TODO: return webrtc.SessionDescription\n\toffer, err := prod.CreateCompleteOffer(medias)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tlog.Trace().Msgf(\"[webrtc] offer:\\n%s\", offer)\n\n\tbody, err := offerToB64(offer)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq, err := http.NewRequest(\"POST\", url, body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header.Set(\"Content-Type\", \"plain/text\")\n\n\t// TODO: change http.DefaultClient settings\n\tclient := http.Client{Timeout: time.Second * 5000}\n\tdefer client.CloseIdleConnections()\n\n\tres, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tanswer, err := answerFromB64(res.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tlog.Trace().Msgf(\"[webrtc] answer:\\n%s\", answer)\n\n\tif answer, err = fixCrealitySDP(answer); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err = prod.SetAnswer(answer); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn prod, nil\n}\n\nfunc offerToB64(sdp string) (io.Reader, error) {\n\t// JS object\n\tv := map[string]string{\n\t\t\"type\": \"offer\",\n\t\t\"sdp\":  sdp,\n\t}\n\n\t// bytes\n\tb, err := json.Marshal(v)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// base64, why? who knows...\n\ts := base64.StdEncoding.EncodeToString(b)\n\n\treturn strings.NewReader(s), nil\n}\n\nfunc answerFromB64(r io.Reader) (string, error) {\n\t// base64\n\tb, err := io.ReadAll(r)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// bytes\n\tif b, err = base64.StdEncoding.DecodeString(string(b)); err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// JS object\n\tvar v map[string]string\n\tif err = json.Unmarshal(b, &v); err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// string \"v=0...\"\n\treturn v[\"sdp\"], nil\n}\n\nfunc fixCrealitySDP(value string) (string, error) {\n\tvar sd sdp.SessionDescription\n\tif err := sd.UnmarshalString(value); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tmd := sd.MediaDescriptions[0]\n\n\t// important to skip first codec, because second codec will be used\n\tskip := md.MediaName.Formats[0]\n\tmd.MediaName.Formats = md.MediaName.Formats[1:]\n\n\tattrs := make([]sdp.Attribute, 0, len(md.Attributes))\n\tfor _, attr := range md.Attributes {\n\t\tswitch attr.Key {\n\t\tcase \"fmtp\", \"rtpmap\":\n\t\t\t// important to skip fmtp with x-google, because this is second fmtp for same codec\n\t\t\t// and pion library will fail parsing this SDP\n\t\t\tif strings.HasPrefix(attr.Value, skip) || strings.Contains(attr.Value, \"x-google\") {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\t\tattrs = append(attrs, attr)\n\t}\n\n\tmd.Attributes = attrs\n\n\tb, err := sd.Marshal()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn string(b), nil\n}\n"
  },
  {
    "path": "internal/webrtc/kinesis.go",
    "content": "package webrtc\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/webrtc\"\n\t\"github.com/gorilla/websocket\"\n\tpion \"github.com/pion/webrtc/v4\"\n)\n\ntype kinesisRequest struct {\n\tAction   string `json:\"action\"`\n\tClientID string `json:\"recipientClientId\"`\n\tPayload  []byte `json:\"messagePayload\"`\n}\n\nfunc (k kinesisRequest) String() string {\n\treturn fmt.Sprintf(\"action=%s, payload=%s\", k.Action, k.Payload)\n}\n\ntype kinesisResponse struct {\n\tPayload []byte `json:\"messagePayload\"`\n\tType    string `json:\"messageType\"`\n}\n\nfunc (k kinesisResponse) String() string {\n\treturn fmt.Sprintf(\"type=%s, payload=%s\", k.Type, k.Payload)\n}\n\nfunc kinesisClient(\n\trawURL string, query url.Values, format string,\n\tsdpOffer func(prod *webrtc.Conn, query url.Values) (any, error),\n) (core.Producer, error) {\n\t// 1. Connect to signalign server\n\tconn, _, err := websocket.DefaultDialer.Dial(rawURL, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 2. Load ICEServers from query param (base64 json)\n\tconf := pion.Configuration{}\n\n\tif s := query.Get(\"ice_servers\"); s != \"\" {\n\t\tconf.ICEServers, err = webrtc.UnmarshalICEServers([]byte(s))\n\t\tif err != nil {\n\t\t\tlog.Warn().Err(err).Caller().Send()\n\t\t}\n\t}\n\n\t// close websocket when we ready return Producer or connection error\n\tdefer conn.Close()\n\n\t// 3. Create Peer Connection\n\tapi, err := webrtc.NewAPI()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpc, err := api.NewPeerConnection(conf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// protect from sending ICE candidate before Offer\n\tvar sendOffer core.Waiter\n\n\t// protect from blocking on errors\n\tdefer sendOffer.Done(nil)\n\n\t// waiter will wait PC error or WS error or nil (connection OK)\n\tvar connState core.Waiter\n\n\treq := kinesisRequest{\n\t\tClientID: query.Get(\"client_id\"),\n\t}\n\n\tprod := webrtc.NewConn(pc)\n\tprod.FormatName = format\n\tprod.Mode = core.ModeActiveProducer\n\tprod.Protocol = \"ws\"\n\tprod.URL = rawURL\n\tprod.Listen(func(msg any) {\n\t\tswitch msg := msg.(type) {\n\t\tcase *pion.ICECandidate:\n\t\t\t_ = sendOffer.Wait()\n\n\t\t\treq.Action = \"ICE_CANDIDATE\"\n\t\t\treq.Payload, _ = json.Marshal(msg.ToJSON())\n\t\t\tif err = conn.WriteJSON(&req); err != nil {\n\t\t\t\tconnState.Done(err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tlog.Trace().Msgf(\"[webrtc] kinesis send: %s\", req)\n\n\t\tcase pion.PeerConnectionState:\n\t\t\tswitch msg {\n\t\t\tcase pion.PeerConnectionStateConnecting:\n\t\t\tcase pion.PeerConnectionStateConnected:\n\t\t\t\tconnState.Done(nil)\n\t\t\tdefault:\n\t\t\t\tconnState.Done(errors.New(\"webrtc: \" + msg.String()))\n\t\t\t}\n\t\t}\n\t})\n\n\tvar payload any\n\n\tif sdpOffer == nil {\n\t\tmedias := []*core.Media{\n\t\t\t{Kind: core.KindVideo, Direction: core.DirectionRecvonly},\n\t\t\t{Kind: core.KindAudio, Direction: core.DirectionRecvonly},\n\t\t}\n\n\t\t// 4. Create offer\n\t\tvar offer string\n\t\tif offer, err = prod.CreateOffer(medias); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// 5. Send offer\n\t\tpayload = pion.SessionDescription{\n\t\t\tType: pion.SDPTypeOffer,\n\t\t\tSDP:  offer,\n\t\t}\n\t} else {\n\t\tif payload, err = sdpOffer(prod, query); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treq.Action = \"SDP_OFFER\"\n\treq.Payload, _ = json.Marshal(payload)\n\tif err = conn.WriteJSON(req); err != nil {\n\t\treturn nil, err\n\t}\n\n\tlog.Trace().Msgf(\"[webrtc] kinesis send: %s\", req)\n\n\tsendOffer.Done(nil)\n\n\tgo func() {\n\t\tvar err error\n\n\t\t// will be closed when conn will be closed\n\t\tfor {\n\t\t\tvar res kinesisResponse\n\t\t\tif err = conn.ReadJSON(&res); err != nil {\n\t\t\t\t// some buggy messages from Amazon servers\n\t\t\t\tif errors.Is(err, io.ErrUnexpectedEOF) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tlog.Trace().Msgf(\"[webrtc] kinesis recv: %s\", res)\n\n\t\t\tswitch res.Type {\n\t\t\tcase \"SDP_ANSWER\":\n\t\t\t\t// 6. Get answer\n\t\t\t\tvar sd pion.SessionDescription\n\t\t\t\tif err = json.Unmarshal(res.Payload, &sd); err != nil {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tif err = prod.SetAnswer(sd.SDP); err != nil {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\tcase \"ICE_CANDIDATE\":\n\t\t\t\t// 7. Continue to receiving candidates\n\t\t\t\tvar ci pion.ICECandidateInit\n\t\t\t\tif err = json.Unmarshal(res.Payload, &ci); err != nil {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tif err = prod.AddCandidate(ci.Candidate); err != nil {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconnState.Done(err)\n\t}()\n\n\tif err = connState.Wait(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn prod, nil\n}\n\ntype wyzeKVS struct {\n\tClientId string          `json:\"ClientId\"`\n\tCam      string          `json:\"cam\"`\n\tResult   string          `json:\"result\"`\n\tServers  json.RawMessage `json:\"servers\"`\n\tURL      string          `json:\"signalingUrl\"`\n}\n\nfunc wyzeClient(rawURL string) (core.Producer, error) {\n\tclient := http.Client{Timeout: 5 * time.Second}\n\tres, err := client.Get(rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tb, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar kvs wyzeKVS\n\tif err = json.Unmarshal(b, &kvs); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif kvs.Result != \"ok\" {\n\t\treturn nil, errors.New(\"wyse: wrong result: \" + kvs.Result)\n\t}\n\n\tquery := url.Values{\n\t\t\"client_id\":   []string{kvs.ClientId},\n\t\t\"ice_servers\": []string{string(kvs.Servers)},\n\t}\n\n\treturn kinesisClient(kvs.URL, query, \"webrtc/wyze\", nil)\n}\n"
  },
  {
    "path": "internal/webrtc/milestone.go",
    "content": "package webrtc\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/tcp\"\n\t\"github.com/AlexxIT/go2rtc/pkg/webrtc\"\n\tpion \"github.com/pion/webrtc/v4\"\n)\n\n// This package handles the Milestone WebRTC session lifecycle, including authentication,\n// session creation, and session update with an SDP answer. It is designed to be used with\n// a specific URL format that encodes session parameters. For example:\n// webrtc:https://milestone-host/api#format=milestone#username=User#password=TestPassword#cameraId=a539f254-af05-4d67-a1bb-cd9b3c74d122\n//\n// https://github.com/milestonesys/mipsdk-samples-protocol/tree/main/WebRTC_JavaScript\n\ntype milestoneAPI struct {\n\turl       string\n\tquery     url.Values\n\ttoken     string\n\tsessionID string\n}\n\nfunc (m *milestoneAPI) GetToken() error {\n\tdata := url.Values{\n\t\t\"client_id\":  {\"GrantValidatorClient\"},\n\t\t\"grant_type\": {\"password\"},\n\t\t\"username\":   m.query[\"username\"],\n\t\t\"password\":   m.query[\"password\"],\n\t}\n\n\treq, err := http.NewRequest(\"POST\", m.url+\"/IDP/connect/token\", strings.NewReader(data.Encode()))\n\tif err != nil {\n\t\treturn err\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\n\t// support httpx protocol\n\tres, err := tcp.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 errors.New(\"milesone: authentication failed: \" + res.Status)\n\t}\n\n\tvar payload map[string]interface{}\n\tif err = json.NewDecoder(res.Body).Decode(&payload); err != nil {\n\t\treturn err\n\t}\n\n\ttoken, ok := payload[\"access_token\"].(string)\n\tif !ok {\n\t\treturn errors.New(\"milesone: token not found in the response\")\n\t}\n\n\tm.token = token\n\n\treturn nil\n}\n\nfunc parseFloat(s string) float64 {\n\tif s == \"\" {\n\t\treturn 0\n\t}\n\tf, _ := strconv.ParseFloat(s, 64)\n\treturn f\n}\n\nfunc (m *milestoneAPI) GetOffer() (string, error) {\n\trequest := struct {\n\t\tCameraId         string `json:\"cameraId\"`\n\t\tStreamId         string `json:\"streamId,omitempty\"`\n\t\tPlaybackTimeNode struct {\n\t\t\tPlaybackTime string  `json:\"playbackTime,omitempty\"`\n\t\t\tSkipGaps     bool    `json:\"skipGaps,omitempty\"`\n\t\t\tSpeed        float64 `json:\"speed,omitempty\"`\n\t\t} `json:\"playbackTimeNode,omitempty\"`\n\t\t//ICEServers []string `json:\"iceServers,omitempty\"`\n\t\t//Resolution string   `json:\"resolution,omitempty\"`\n\t}{\n\t\tCameraId: m.query.Get(\"cameraId\"),\n\t\tStreamId: m.query.Get(\"streamId\"),\n\t}\n\trequest.PlaybackTimeNode.PlaybackTime = m.query.Get(\"playbackTime\")\n\trequest.PlaybackTimeNode.SkipGaps = m.query.Has(\"skipGaps\")\n\trequest.PlaybackTimeNode.Speed = parseFloat(m.query.Get(\"speed\"))\n\n\tdata, err := json.Marshal(request)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treq, err := http.NewRequest(\"POST\", m.url+\"/REST/v1/WebRTC/Session\", bytes.NewBuffer(data))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treq.Header.Set(\"Authorization\", \"Bearer \"+m.token)\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tres, err := tcp.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 \"\", errors.New(\"milesone: create session: \" + res.Status)\n\t}\n\n\tvar response struct {\n\t\tSessionId string `json:\"sessionId\"`\n\t\tOfferSDP  string `json:\"offerSDP\"`\n\t}\n\tif err = json.NewDecoder(res.Body).Decode(&response); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar offer pion.SessionDescription\n\tif err = json.Unmarshal([]byte(response.OfferSDP), &offer); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tm.sessionID = response.SessionId\n\n\treturn offer.SDP, nil\n}\n\nfunc (m *milestoneAPI) SetAnswer(sdp string) error {\n\tanswer := pion.SessionDescription{\n\t\tType: pion.SDPTypeAnswer,\n\t\tSDP:  sdp,\n\t}\n\tdata, err := json.Marshal(answer)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\trequest := struct {\n\t\tAnswerSDP string `json:\"answerSDP\"`\n\t}{\n\t\tAnswerSDP: string(data),\n\t}\n\tif data, err = json.Marshal(request); err != nil {\n\t\treturn err\n\t}\n\n\treq, err := http.NewRequest(\"PATCH\", m.url+\"/REST/v1/WebRTC/Session/\"+m.sessionID, bytes.NewBuffer(data))\n\tif err != nil {\n\t\treturn err\n\t}\n\treq.Header.Set(\"Authorization\", \"Bearer \"+m.token)\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tres, err := tcp.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 errors.New(\"milesone: patch session: \" + res.Status)\n\t}\n\n\treturn nil\n}\n\nfunc milestoneClient(rawURL string, query url.Values) (core.Producer, error) {\n\tmc := &milestoneAPI{url: rawURL, query: query}\n\tif err := mc.GetToken(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tapi, err := webrtc.NewAPI()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tconf := pion.Configuration{}\n\tpc, err := api.NewPeerConnection(conf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tprod := webrtc.NewConn(pc)\n\tprod.FormatName = \"webrtc/milestone\"\n\tprod.Mode = core.ModeActiveProducer\n\tprod.Protocol = \"http\"\n\tprod.URL = rawURL\n\n\toffer, err := mc.GetOffer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err = prod.SetOffer(offer); err != nil {\n\t\treturn nil, err\n\t}\n\n\tanswer, err := prod.GetAnswer()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err = mc.SetAnswer(answer); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn prod, nil\n}\n"
  },
  {
    "path": "internal/webrtc/openipc.go",
    "content": "package webrtc\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net/url\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/webrtc\"\n\t\"github.com/gorilla/websocket\"\n\tpion \"github.com/pion/webrtc/v4\"\n)\n\nfunc openIPCClient(rawURL string, query url.Values) (core.Producer, error) {\n\t// 1. Connect to signalign server\n\tconn, _, err := websocket.DefaultDialer.Dial(rawURL, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 2. Load ICEServers from query param (base64 json)\n\tvar conf pion.Configuration\n\n\tif s := query.Get(\"ice_servers\"); s != \"\" {\n\t\tconf.ICEServers, err = webrtc.UnmarshalICEServers([]byte(s))\n\t\tif err != nil {\n\t\t\tlog.Warn().Err(err).Caller().Send()\n\t\t}\n\t}\n\n\t// close websocket when we ready return Producer or connection error\n\tdefer conn.Close()\n\n\t// 3. Create Peer Connection\n\tapi, err := webrtc.NewAPI()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpc, err := api.NewPeerConnection(conf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// protect from sending ICE candidate before Offer\n\tvar sendAnswer core.Waiter\n\n\t// protect from blocking on errors\n\tdefer sendAnswer.Done(nil)\n\n\t// waiter will wait PC error or WS error or nil (connection OK)\n\tvar connState core.Waiter\n\n\tprod := webrtc.NewConn(pc)\n\tprod.FormatName = \"webrtc/openipc\"\n\tprod.Mode = core.ModeActiveProducer\n\tprod.Protocol = \"ws\"\n\tprod.URL = rawURL\n\tprod.Listen(func(msg any) {\n\t\tswitch msg := msg.(type) {\n\t\tcase *pion.ICECandidate:\n\t\t\t_ = sendAnswer.Wait()\n\n\t\t\treq := openIPCReq{\n\t\t\t\tData: msg.ToJSON().Candidate,\n\t\t\t\tReq:  \"candidate\",\n\t\t\t}\n\t\t\tif err = conn.WriteJSON(&req); err != nil {\n\t\t\t\tconnState.Done(err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tlog.Trace().Msgf(\"[webrtc] openipc send: %s\", req)\n\n\t\tcase pion.PeerConnectionState:\n\t\t\tswitch msg {\n\t\t\tcase pion.PeerConnectionStateConnecting:\n\t\t\tcase pion.PeerConnectionStateConnected:\n\t\t\t\tconnState.Done(nil)\n\t\t\tdefault:\n\t\t\t\tconnState.Done(errors.New(\"webrtc: \" + msg.String()))\n\t\t\t}\n\t\t}\n\t})\n\n\tgo func() {\n\t\tvar err error\n\n\t\t// will be closed when conn will be closed\n\t\tfor err == nil {\n\t\t\tvar rep openIPCReply\n\t\t\tif err = conn.ReadJSON(&rep); err != nil {\n\t\t\t\t// some buggy messages from Amazon servers\n\t\t\t\tif errors.Is(err, io.ErrUnexpectedEOF) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tlog.Trace().Msgf(\"[webrtc] openipc recv: %s\", rep)\n\n\t\t\tswitch rep.Reply {\n\t\t\tcase \"webrtc_answer\":\n\t\t\t\t// 6. Get answer\n\t\t\t\tvar sd pion.SessionDescription\n\t\t\t\tif err = json.Unmarshal(rep.Data, &sd); err != nil {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tif err = prod.SetOffer(sd.SDP); err != nil {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tvar answer string\n\t\t\t\tif answer, err = prod.GetAnswer(); err != nil {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\treq := openIPCReq{Data: answer, Req: \"answer\"}\n\t\t\t\tif err = conn.WriteJSON(req); err != nil {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tlog.Trace().Msgf(\"[webrtc] kinesis send: %s\", req)\n\n\t\t\t\tsendAnswer.Done(nil)\n\n\t\t\tcase \"webrtc_candidate\":\n\t\t\t\t// 7. Continue to receiving candidates\n\t\t\t\tvar ci pion.ICECandidateInit\n\t\t\t\tif err = json.Unmarshal(rep.Data, &ci); err != nil {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\tif err = prod.AddCandidate(ci.Candidate); err != nil {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tconnState.Done(err)\n\t}()\n\n\tif err = connState.Wait(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn prod, nil\n}\n\ntype openIPCReply struct {\n\tData  json.RawMessage `json:\"data\"`\n\tReply string          `json:\"reply\"`\n}\n\nfunc (r openIPCReply) String() string {\n\tb, _ := json.Marshal(r)\n\treturn string(b)\n}\n\ntype openIPCReq struct {\n\tData string `json:\"data\"`\n\tReq  string `json:\"req\"`\n}\n\nfunc (r openIPCReq) String() string {\n\tb, _ := json.Marshal(r)\n\treturn string(b)\n}\n"
  },
  {
    "path": "internal/webrtc/server.go",
    "content": "package webrtc\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/webrtc\"\n\tpion \"github.com/pion/webrtc/v4\"\n)\n\nconst MimeSDP = \"application/sdp\"\n\nvar sessions = map[string]*webrtc.Conn{}\n\nfunc syncHandler(w http.ResponseWriter, r *http.Request) {\n\tswitch r.Method {\n\tcase \"POST\":\n\t\tquery := r.URL.Query()\n\t\tif query.Get(\"src\") != \"\" {\n\t\t\t// WHEP or JSON SDP or raw SDP exchange\n\t\t\toutputWebRTC(w, r)\n\t\t} else if query.Get(\"dst\") != \"\" {\n\t\t\t// WHIP SDP exchange\n\t\t\tinputWebRTC(w, r)\n\t\t} else {\n\t\t\thttp.Error(w, \"\", http.StatusBadRequest)\n\t\t}\n\n\tcase \"PATCH\":\n\t\t// TODO: WHEP/WHIP\n\t\thttp.Error(w, \"\", http.StatusMethodNotAllowed)\n\n\tcase \"DELETE\":\n\t\tif id := r.URL.Query().Get(\"id\"); id != \"\" {\n\t\t\tif conn, ok := sessions[id]; ok {\n\t\t\t\tdelete(sessions, id)\n\t\t\t\t_ = conn.Close()\n\t\t\t} else {\n\t\t\t\thttp.Error(w, \"\", http.StatusNotFound)\n\t\t\t}\n\t\t} else {\n\t\t\thttp.Error(w, \"\", http.StatusBadRequest)\n\t\t}\n\n\tcase \"OPTIONS\":\n\t\tw.WriteHeader(http.StatusNoContent)\n\n\tdefault:\n\t\thttp.Error(w, \"\", http.StatusMethodNotAllowed)\n\t}\n}\n\n// outputWebRTC support API depending on Content-Type:\n// 1. application/json - receive {\"type\":\"offer\",\"sdp\":\"v=0\\r\\n...\"} and response {\"type\":\"answer\",\"sdp\":\"v=0\\r\\n...\"}\n// 2. application/sdp - receive/response SDP via WebRTC-HTTP Egress Protocol (WHEP)\n// 3. other - receive/response raw SDP\nfunc outputWebRTC(w http.ResponseWriter, r *http.Request) {\n\tu := r.URL.Query().Get(\"src\")\n\tstream := streams.Get(u)\n\tif stream == nil {\n\t\thttp.Error(w, api.StreamNotFound, http.StatusNotFound)\n\t\treturn\n\t}\n\n\tmediaType := r.Header.Get(\"Content-Type\")\n\tif mediaType != \"\" {\n\t\tmediaType, _, _ = strings.Cut(mediaType, \";\")\n\t\tmediaType = strings.ToLower(strings.TrimSpace(mediaType))\n\t}\n\n\tvar offer string\n\n\tswitch mediaType {\n\tcase \"application/json\":\n\t\tvar desc pion.SessionDescription\n\t\tif err := json.NewDecoder(r.Body).Decode(&desc); err != nil {\n\t\t\tlog.Error().Err(err).Caller().Send()\n\t\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\toffer = desc.SDP\n\n\tcase \"application/x-www-form-urlencoded\":\n\t\tif err := r.ParseForm(); err != nil {\n\t\t\tlog.Error().Err(err).Caller().Send()\n\t\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\tofferB64 := r.Form.Get(\"data\")\n\t\tb, err := base64.StdEncoding.DecodeString(offerB64)\n\t\tif err != nil {\n\t\t\tlog.Error().Err(err).Caller().Send()\n\t\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\t\toffer = string(b)\n\n\tdefault:\n\t\tbody, err := io.ReadAll(r.Body)\n\t\tif err != nil {\n\t\t\tlog.Error().Err(err).Caller().Send()\n\t\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\t\treturn\n\t\t}\n\t\toffer = string(body)\n\t}\n\n\tvar desc string\n\n\tswitch mediaType {\n\tcase \"application/json\":\n\t\tdesc = \"webrtc/json\"\n\tcase MimeSDP:\n\t\tdesc = \"webrtc/whep\"\n\tdefault:\n\t\tdesc = \"webrtc/post\"\n\t}\n\n\tanswer, err := ExchangeSDP(stream, offer, desc, r.UserAgent())\n\tif err != nil {\n\t\tlog.Error().Err(err).Caller().Send()\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tswitch mediaType {\n\tcase \"application/json\":\n\t\tw.Header().Set(\"Content-Type\", mediaType)\n\n\t\tv := pion.SessionDescription{\n\t\t\tType: pion.SDPTypeAnswer, SDP: answer,\n\t\t}\n\t\terr = json.NewEncoder(w).Encode(v)\n\n\tcase \"application/x-www-form-urlencoded\":\n\t\tw.Header().Set(\"Content-Type\", mediaType)\n\t\tanswerB64 := base64.StdEncoding.EncodeToString([]byte(answer))\n\t\t_, err = w.Write([]byte(answerB64))\n\n\tcase MimeSDP:\n\t\tw.Header().Set(\"Content-Type\", mediaType)\n\t\tw.WriteHeader(http.StatusCreated)\n\n\t\t_, err = w.Write([]byte(answer))\n\n\tdefault:\n\t\tw.Header().Set(\"Content-Type\", mediaType)\n\n\t\t_, err = w.Write([]byte(answer))\n\t}\n\n\tif err != nil {\n\t\tlog.Error().Err(err).Caller().Send()\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t}\n}\n\nfunc inputWebRTC(w http.ResponseWriter, r *http.Request) {\n\tdst := r.URL.Query().Get(\"dst\")\n\tstream := streams.Get(dst)\n\tif stream == nil {\n\t\thttp.Error(w, api.StreamNotFound, http.StatusNotFound)\n\t\treturn\n\t}\n\n\t// 1. Get offer\n\toffer, err := io.ReadAll(r.Body)\n\tif err != nil {\n\t\tlog.Error().Err(err).Caller().Send()\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Trace().Msgf(\"[webrtc] WHIP offer\\n%s\", offer)\n\n\tpc, err := PeerConnection(false)\n\tif err != nil {\n\t\tlog.Error().Err(err).Caller().Send()\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\t// create new webrtc instance\n\tprod := webrtc.NewConn(pc)\n\tprod.Mode = core.ModePassiveProducer\n\tprod.Protocol = \"http\"\n\tprod.UserAgent = r.UserAgent()\n\n\tif err = prod.SetOffer(string(offer)); err != nil {\n\t\tlog.Warn().Err(err).Caller().Send()\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tanswer, err := prod.GetCompleteAnswer(GetCandidates(), FilterCandidate)\n\tif err != nil {\n\t\tlog.Warn().Err(err).Caller().Send()\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tlog.Trace().Msgf(\"[webrtc] WHIP answer\\n%s\", answer)\n\n\tid := strconv.FormatInt(time.Now().UnixNano(), 36)\n\tsessions[id] = prod\n\n\tprod.Listen(func(msg any) {\n\t\tswitch msg := msg.(type) {\n\t\tcase pion.PeerConnectionState:\n\t\t\tif msg == pion.PeerConnectionStateClosed {\n\t\t\t\tstream.RemoveProducer(prod)\n\t\t\t\tdelete(sessions, id)\n\t\t\t}\n\t\t}\n\t})\n\n\tstream.AddProducer(prod)\n\n\tw.Header().Set(\"Content-Type\", MimeSDP)\n\tw.Header().Set(\"Location\", \"webrtc?id=\"+id)\n\tw.WriteHeader(http.StatusCreated)\n\n\tif _, err = w.Write([]byte(answer)); err != nil {\n\t\tlog.Warn().Err(err).Caller().Send()\n\t\treturn\n\t}\n}\n"
  },
  {
    "path": "internal/webrtc/switchbot.go",
    "content": "package webrtc\n\nimport (\n\t\"net/url\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/webrtc\"\n)\n\nfunc switchbotClient(rawURL string, query url.Values) (core.Producer, error) {\n\treturn kinesisClient(rawURL, query, \"webrtc/switchbot\", func(prod *webrtc.Conn, query url.Values) (any, error) {\n\t\tmedias := []*core.Media{\n\t\t\t{Kind: core.KindVideo, Direction: core.DirectionRecvonly},\n\t\t}\n\n\t\toffer, err := prod.CreateOffer(medias)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tv := struct {\n\t\t\tType       string `json:\"type\"`\n\t\t\tSDP        string `json:\"sdp\"`\n\t\t\tResolution int    `json:\"resolution\"`\n\t\t\tPlayType   int    `json:\"play_type\"`\n\t\t}{\n\t\t\tType: \"offer\",\n\t\t\tSDP:  offer,\n\t\t}\n\n\t\tswitch query.Get(\"resolution\") {\n\t\tcase \"hd\":\n\t\t\tv.Resolution = 0\n\t\tcase \"sd\":\n\t\t\tv.Resolution = 1\n\t\tcase \"auto\":\n\t\t\tv.Resolution = 2\n\t\t}\n\n\t\tv.PlayType = core.Atoi(query.Get(\"play_type\")) // zero by default\n\n\t\treturn v, nil\n\t})\n}\n"
  },
  {
    "path": "internal/webrtc/webrtc.go",
    "content": "package webrtc\n\nimport (\n\t\"errors\"\n\t\"net\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/internal/api/ws\"\n\t\"github.com/AlexxIT/go2rtc/internal/app\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/webrtc\"\n\tpion \"github.com/pion/webrtc/v4\"\n\t\"github.com/rs/zerolog\"\n)\n\nfunc Init() {\n\tvar cfg struct {\n\t\tMod struct {\n\t\t\tListen     string           `yaml:\"listen\"`\n\t\t\tCandidates []string         `yaml:\"candidates\"`\n\t\t\tIceServers []pion.ICEServer `yaml:\"ice_servers\"`\n\t\t\tFilters    webrtc.Filters   `yaml:\"filters\"`\n\t\t} `yaml:\"webrtc\"`\n\t}\n\n\tcfg.Mod.Listen = \":8555\"\n\tcfg.Mod.IceServers = []pion.ICEServer{\n\t\t{URLs: []string{\"stun:stun.cloudflare.com:3478\", \"stun:stun.l.google.com:19302\"}},\n\t}\n\n\tapp.LoadConfig(&cfg)\n\n\tlog = app.GetLogger(\"webrtc\")\n\n\tif log.Debug().Enabled() {\n\t\titfs, _ := net.Interfaces()\n\t\tfor _, itf := range itfs {\n\t\t\taddrs, _ := itf.Addrs()\n\t\t\tlog.Debug().Msgf(\"[webrtc] interface %+v addrs %v\", itf, addrs)\n\t\t}\n\t}\n\n\taddress, network, _ := strings.Cut(cfg.Mod.Listen, \"/\")\n\tfor _, candidate := range cfg.Mod.Candidates {\n\t\tAddCandidate(network, candidate)\n\n\t\tif strings.HasPrefix(candidate, \"stun:\") && stuns == nil {\n\t\t\tfor _, ice := range cfg.Mod.IceServers {\n\t\t\t\tfor _, url := range ice.URLs {\n\t\t\t\t\tif strings.HasPrefix(url, \"stun:\") {\n\t\t\t\t\t\tstuns = append(stuns, url[5:])\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\twebrtc.OnNewListener = func(ln any) {\n\t\tswitch ln := ln.(type) {\n\t\tcase *net.TCPListener:\n\t\t\tlog.Info().Stringer(\"addr\", ln.Addr()).Msg(\"[webrtc] listen tcp\")\n\t\tcase *net.UDPConn:\n\t\t\tlog.Info().Stringer(\"addr\", ln.LocalAddr()).Msg(\"[webrtc] listen udp\")\n\t\t}\n\t}\n\n\tvar err error\n\n\t// create pionAPI with custom codecs list and custom network settings\n\tserverAPI, err = webrtc.NewServerAPI(network, address, &cfg.Mod.Filters)\n\tif err != nil {\n\t\tlog.Error().Err(err).Caller().Send()\n\t\treturn\n\t}\n\n\t// use same API for WebRTC server and client if no address\n\tclientAPI = serverAPI\n\n\tif address != \"\" {\n\t\tclientAPI, _ = webrtc.NewAPI()\n\t}\n\n\tpionConf := pion.Configuration{\n\t\tICEServers:   cfg.Mod.IceServers,\n\t\tSDPSemantics: pion.SDPSemanticsUnifiedPlanWithFallback,\n\t}\n\n\tPeerConnection = func(active bool) (*pion.PeerConnection, error) {\n\t\t// active - client, passive - server\n\t\tif active {\n\t\t\treturn clientAPI.NewPeerConnection(pionConf)\n\t\t} else {\n\t\t\treturn serverAPI.NewPeerConnection(pionConf)\n\t\t}\n\t}\n\n\t// async WebRTC server (two API versions)\n\tws.HandleFunc(\"webrtc\", asyncHandler)\n\tws.HandleFunc(\"webrtc/offer\", asyncHandler)\n\tws.HandleFunc(\"webrtc/candidate\", candidateHandler)\n\n\t// sync WebRTC server (two API versions)\n\tapi.HandleFunc(\"api/webrtc\", syncHandler)\n\n\t// WebRTC client\n\tstreams.HandleFunc(\"webrtc\", streamsHandler)\n}\n\nvar serverAPI, clientAPI *pion.API\n\nvar log zerolog.Logger\n\nvar PeerConnection func(active bool) (*pion.PeerConnection, error)\n\nfunc asyncHandler(tr *ws.Transport, msg *ws.Message) (err error) {\n\tvar stream *streams.Stream\n\tvar mode core.Mode\n\n\tquery := tr.Request.URL.Query()\n\tif name := query.Get(\"src\"); name != \"\" {\n\t\tstream, _ = streams.GetOrPatch(query)\n\t\tmode = core.ModePassiveConsumer\n\t\tlog.Debug().Str(\"src\", name).Msg(\"[webrtc] new consumer\")\n\t} else if name = query.Get(\"dst\"); name != \"\" {\n\t\tstream = streams.Get(name)\n\t\tmode = core.ModePassiveProducer\n\t\tlog.Debug().Str(\"src\", name).Msg(\"[webrtc] new producer\")\n\t}\n\n\tif stream == nil {\n\t\treturn errors.New(api.StreamNotFound)\n\t}\n\n\tvar offer struct {\n\t\tType       string           `json:\"type\"`\n\t\tSDP        string           `json:\"sdp\"`\n\t\tICEServers []pion.ICEServer `json:\"ice_servers\"`\n\t}\n\n\t// V2 - json/object exchange, V1 - raw SDP exchange\n\tapiV2 := msg.Type == \"webrtc\"\n\n\tif apiV2 {\n\t\tif err = msg.Unmarshal(&offer); err != nil {\n\t\t\treturn err\n\t\t}\n\t} else {\n\t\toffer.SDP = msg.String()\n\t}\n\n\t// create new PeerConnection instance\n\tvar pc *pion.PeerConnection\n\tif offer.ICEServers == nil {\n\t\tpc, err = PeerConnection(false)\n\t} else {\n\t\tpc, err = serverAPI.NewPeerConnection(pion.Configuration{ICEServers: offer.ICEServers})\n\t}\n\tif err != nil {\n\t\tlog.Error().Err(err).Caller().Send()\n\t\treturn err\n\t}\n\n\tvar sendAnswer core.Waiter\n\n\t// protect from blocking on errors\n\tdefer sendAnswer.Done(nil)\n\n\tconn := webrtc.NewConn(pc)\n\tconn.Mode = mode\n\tconn.Protocol = \"ws\"\n\tconn.UserAgent = tr.Request.UserAgent()\n\tconn.Listen(func(msg any) {\n\t\tswitch msg := msg.(type) {\n\t\tcase pion.PeerConnectionState:\n\t\t\tif msg != pion.PeerConnectionStateClosed {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tswitch mode {\n\t\t\tcase core.ModePassiveConsumer:\n\t\t\t\tstream.RemoveConsumer(conn)\n\t\t\tcase core.ModePassiveProducer:\n\t\t\t\tstream.RemoveProducer(conn)\n\t\t\t}\n\n\t\tcase *pion.ICECandidate:\n\t\t\tif !FilterCandidate(msg) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t_ = sendAnswer.Wait()\n\n\t\t\ts := msg.ToJSON().Candidate\n\t\t\tlog.Trace().Str(\"candidate\", s).Msg(\"[webrtc] local \")\n\t\t\ttr.Write(&ws.Message{Type: \"webrtc/candidate\", Value: s})\n\t\t}\n\t})\n\n\tlog.Trace().Msgf(\"[webrtc] offer:\\n%s\", offer.SDP)\n\n\t// 1. SetOffer, so we can get remote client codecs\n\tif err = conn.SetOffer(offer.SDP); err != nil {\n\t\tlog.Warn().Err(err).Caller().Send()\n\t\treturn err\n\t}\n\n\tswitch mode {\n\tcase core.ModePassiveConsumer:\n\t\t// 2. AddConsumer, so we get new tracks\n\t\tif err = stream.AddConsumer(conn); err != nil {\n\t\t\tlog.Debug().Err(err).Msg(\"[webrtc] add consumer\")\n\t\t\t_ = conn.Close()\n\t\t\treturn err\n\t\t}\n\tcase core.ModePassiveProducer:\n\t\tstream.AddProducer(conn)\n\t}\n\n\t// 3. Exchange SDP without waiting all candidates\n\tanswer, err := conn.GetAnswer()\n\tlog.Trace().Msgf(\"[webrtc] answer\\n%s\", answer)\n\n\tif err != nil {\n\t\tlog.Error().Err(err).Caller().Send()\n\t\treturn err\n\t}\n\n\tif apiV2 {\n\t\tdesc := pion.SessionDescription{Type: pion.SDPTypeAnswer, SDP: answer}\n\t\ttr.Write(&ws.Message{Type: \"webrtc\", Value: desc})\n\t} else {\n\t\ttr.Write(&ws.Message{Type: \"webrtc/answer\", Value: answer})\n\t}\n\n\tsendAnswer.Done(nil)\n\n\tasyncCandidates(tr, conn)\n\n\treturn nil\n}\n\nfunc ExchangeSDP(stream *streams.Stream, offer, desc, userAgent string) (answer string, err error) {\n\tpc, err := PeerConnection(false)\n\tif err != nil {\n\t\tlog.Error().Err(err).Caller().Send()\n\t\treturn\n\t}\n\n\t// create new webrtc instance\n\tconn := webrtc.NewConn(pc)\n\tconn.FormatName = desc\n\tconn.UserAgent = userAgent\n\tconn.Protocol = \"http\"\n\tconn.Listen(func(msg any) {\n\t\tswitch msg := msg.(type) {\n\t\tcase pion.PeerConnectionState:\n\t\t\tif msg != pion.PeerConnectionStateClosed {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif conn.Mode == core.ModePassiveConsumer {\n\t\t\t\tstream.RemoveConsumer(conn)\n\t\t\t} else {\n\t\t\t\tstream.RemoveProducer(conn)\n\t\t\t}\n\t\t}\n\t})\n\n\t// 1. SetOffer, so we can get remote client codecs\n\tlog.Trace().Msgf(\"[webrtc] offer:\\n%s\", offer)\n\n\tif err = conn.SetOffer(offer); err != nil {\n\t\tlog.Warn().Err(err).Caller().Send()\n\t\treturn\n\t}\n\n\tif IsConsumer(conn) {\n\t\tconn.Mode = core.ModePassiveConsumer\n\n\t\t// 2. AddConsumer, so we get new tracks\n\t\tif err = stream.AddConsumer(conn); err != nil {\n\t\t\tlog.Warn().Err(err).Caller().Send()\n\t\t\t_ = conn.Close()\n\t\t\treturn\n\t\t}\n\t} else {\n\t\tconn.Mode = core.ModePassiveProducer\n\n\t\tstream.AddProducer(conn)\n\t}\n\n\tanswer, err = conn.GetCompleteAnswer(GetCandidates(), FilterCandidate)\n\tlog.Trace().Msgf(\"[webrtc] answer\\n%s\", answer)\n\n\tif err != nil {\n\t\tlog.Error().Err(err).Caller().Send()\n\t}\n\n\treturn\n}\n\nfunc IsConsumer(conn *webrtc.Conn) bool {\n\t// if wants get video - consumer\n\tfor _, media := range conn.GetMedias() {\n\t\tif media.Kind == core.KindVideo && media.Direction == core.DirectionSendonly {\n\t\t\treturn true\n\t\t}\n\t}\n\t// if wants send video - producer\n\tfor _, media := range conn.GetMedias() {\n\t\tif media.Kind == core.KindVideo && media.Direction == core.DirectionRecvonly {\n\t\t\treturn false\n\t\t}\n\t}\n\t// if wants something - consumer\n\tfor _, media := range conn.GetMedias() {\n\t\tif media.Direction == core.DirectionSendonly {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "internal/webrtc/webrtc_test.go",
    "content": "package webrtc\n\nimport (\n\t\"encoding/json\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api/ws\"\n\tpion \"github.com/pion/webrtc/v4\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestWebRTCAPIv1(t *testing.T) {\n\traw := `{\"type\":\"webrtc/offer\",\"value\":\"v=0\\n...\"}`\n\tmsg := new(ws.Message)\n\terr := json.Unmarshal([]byte(raw), msg)\n\trequire.Nil(t, err)\n\n\trequire.Equal(t, \"v=0\\n...\", msg.String())\n}\n\nfunc TestWebRTCAPIv2(t *testing.T) {\n\traw := `{\"type\":\"webrtc\",\"value\":{\"type\":\"offer\",\"sdp\":\"v=0\\n...\",\"ice_servers\":[{\"urls\":[\"stun:stun.l.google.com:19302\"]}]}}`\n\tmsg := new(ws.Message)\n\terr := json.Unmarshal([]byte(raw), msg)\n\trequire.Nil(t, err)\n\n\tvar offer struct {\n\t\tType       string           `json:\"type\"`\n\t\tSDP        string           `json:\"sdp\"`\n\t\tICEServers []pion.ICEServer `json:\"ice_servers\"`\n\t}\n\terr = msg.Unmarshal(&offer)\n\trequire.Nil(t, err)\n\n\trequire.Equal(t, \"offer\", offer.Type)\n\trequire.Equal(t, \"v=0\\n...\", offer.SDP)\n\trequire.Equal(t, \"stun:stun.l.google.com:19302\", offer.ICEServers[0].URLs[0])\n}\n\nfunc TestCrealitySDP(t *testing.T) {\n\tsdp := `v=0\no=- 1495799811084970 1495799811084970 IN IP4 0.0.0.0\ns=-\nt=0 0\na=msid-semantic:WMS *\na=group:BUNDLE 0\nm=video 9 UDP/TLS/RTP/SAVPF 96 98\na=rtcp-fb:98 nack\na=rtcp-fb:98 nack pli\na=fmtp:96 profile-level-id=42e01f;level-asymmetry-allowed=1\na=fmtp:98 profile-level-id=42e01f;packetization-mode=1;level-asymmetry-allowed=1\na=fmtp:98 x-google-max-bitrate=6000;x-google-min-bitrate=2000;x-google-start-bitrate=4000\na=rtpmap:96 H264/90000\na=rtpmap:98 H264/90000\na=ssrc:1 cname:pear\nc=IN IP4 0.0.0.0\na=sendonly\na=mid:0\na=rtcp-mux\na=ice-ufrag:7AVa\na=ice-pwd:T+F/5y05Paw+mtG5Jrd8N3\na=ice-options:trickle\na=fingerprint:sha-256 A5:AB:C0:4E:29:5B:BD:3B:7D:88:24:6C:56:6B:2A:79:A3:76:99:35:57:75:AD:C8:5A:A6:34:20:88:1B:68:EF\na=setup:passive\na=candidate:1 1 UDP 2015363327 172.22.233.10 48929 typ host\na=candidate:2 1 TCP 1015021823 172.22.233.10 0 typ host tcptype active\na=candidate:3 1 TCP 1010827519 172.22.233.10 60677 typ host tcptype passive\n`\n\tsdp, err := fixCrealitySDP(sdp)\n\trequire.Nil(t, err)\n\trequire.False(t, strings.Contains(sdp, \"x-google-max-bitrate\"))\n}\n"
  },
  {
    "path": "internal/webtorrent/README.md",
    "content": "# WebTorrent\n\n> [!NOTE]\n> This section needs some improvement.\n\n## WebTorrent Client\n\n[`new in v1.3.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)\n\nThis source can get a stream from another go2rtc via [WebTorrent](https://en.wikipedia.org/wiki/WebTorrent) protocol.\n\n### Client Configuration\n\n```yaml\nstreams:\n  webtorrent1: webtorrent:?share=huofssuxaty00izc&pwd=k3l2j9djeg8v8r7e\n```\n\n## WebTorrent Server\n\n[`new in v1.3.0`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.3.0)\n\nThis module supports:\n\n- Share any local stream via [WebTorrent](https://webtorrent.io/) technology\n- Get any [incoming stream](../webrtc/README.md#ingest-browser) from PC or mobile via [WebTorrent](https://webtorrent.io/) technology\n- Get any remote go2rtc source via [WebTorrent](https://webtorrent.io/) technology\n\nSecurely and freely. You do not need to open a public access to the go2rtc server. But in some cases (Symmetric NAT), you may need to set up external access to [WebRTC module](../webrtc/README.md).\n\nTo generate a sharing link or incoming link, go to the go2rtc WebUI (stream links page). This link is **temporary** and will stop working after go2rtc is restarted!\n\n### Server Configuration\n\nYou can create permanent external links in the go2rtc config:\n\n```yaml\nwebtorrent:\n  shares:\n    super-secret-share:  # share name, should be unique among all go2rtc users!\n      pwd: super-secret-password\n      src: rtsp-dahua1   # stream name from streams section\n```\n\nLink example: `https://go2rtc.org/webtorrent/#share=02SNtgjKXY&pwd=wznEQqznxW&media=video+audio`\n"
  },
  {
    "path": "internal/webtorrent/init.go",
    "content": "package webtorrent\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/internal/app\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/internal/webrtc\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/webtorrent\"\n\t\"github.com/rs/zerolog\"\n)\n\nfunc Init() {\n\tvar cfg struct {\n\t\tMod struct {\n\t\t\tTrackers []string `yaml:\"trackers\"`\n\t\t\tShares   map[string]struct {\n\t\t\t\tPwd string `yaml:\"pwd\"`\n\t\t\t\tSrc string `yaml:\"src\"`\n\t\t\t} `yaml:\"shares\"`\n\t\t} `yaml:\"webtorrent\"`\n\t}\n\n\tcfg.Mod.Trackers = []string{\"wss://tracker.openwebtorrent.com\"}\n\n\tapp.LoadConfig(&cfg)\n\n\tif len(cfg.Mod.Trackers) == 0 {\n\t\treturn\n\t}\n\n\tlog = app.GetLogger(\"webtorrent\")\n\n\tstreams.HandleFunc(\"webtorrent\", streamHandle)\n\n\tapi.HandleFunc(\"api/webtorrent\", apiHandle)\n\n\tsrv = &webtorrent.Server{\n\t\tURL: cfg.Mod.Trackers[0],\n\t\tExchange: func(src, offer string) (answer string, err error) {\n\t\t\tstream := streams.Get(src)\n\t\t\tif stream == nil {\n\t\t\t\treturn \"\", errors.New(api.StreamNotFound)\n\t\t\t}\n\t\t\treturn webrtc.ExchangeSDP(stream, offer, \"webtorrent\", \"\")\n\t\t},\n\t}\n\n\tif log.Debug().Enabled() {\n\t\tsrv.Listen(func(msg any) {\n\t\t\tswitch msg.(type) {\n\t\t\tcase string, error:\n\t\t\t\tlog.Debug().Msgf(\"[webtorrent] %s\", msg)\n\t\t\tcase *webtorrent.Message:\n\t\t\t\tlog.Trace().Any(\"msg\", msg).Msgf(\"[webtorrent]\")\n\t\t\t}\n\t\t})\n\t}\n\n\tfor name, share := range cfg.Mod.Shares {\n\t\tif len(name) < 8 {\n\t\t\tlog.Warn().Str(\"name\", name).Msgf(\"min share name len - 8 symbols\")\n\t\t\tcontinue\n\t\t}\n\t\tif len(share.Pwd) < 4 {\n\t\t\tlog.Warn().Str(\"name\", name).Str(\"pwd\", share.Pwd).Msgf(\"min share pwd len - 4 symbols\")\n\t\t\tcontinue\n\t\t}\n\t\tif streams.Get(share.Src) == nil {\n\t\t\tlog.Warn().Str(\"stream\", share.Src).Msgf(\"stream not exists\")\n\t\t\tcontinue\n\t\t}\n\n\t\tsrv.AddShare(name, share.Pwd, share.Src)\n\n\t\t// adds to GET /api/webtorrent\n\t\tshares[name] = name\n\t}\n}\n\nvar log zerolog.Logger\n\nvar shares = map[string]string{}\nvar srv *webtorrent.Server\n\nfunc apiHandle(w http.ResponseWriter, r *http.Request) {\n\tsrc := r.URL.Query().Get(\"src\")\n\tshare, ok := shares[src]\n\n\tswitch r.Method {\n\tcase \"GET\":\n\t\t// support act as WebTorrent tracker (for testing purposes)\n\t\tif r.Header.Get(\"Connection\") == \"Upgrade\" {\n\t\t\ttracker(w, r)\n\t\t\treturn\n\t\t}\n\n\t\tif src != \"\" {\n\t\t\t// response one share\n\t\t\tif ok {\n\t\t\t\tpwd := srv.GetSharePwd(share)\n\t\t\t\tdata := fmt.Sprintf(`{\"share\":%q,\"pwd\":%q}`, share, pwd)\n\t\t\t\t_, _ = w.Write([]byte(data))\n\t\t\t} else {\n\t\t\t\thttp.Error(w, \"\", http.StatusNotFound)\n\t\t\t}\n\t\t} else {\n\t\t\t// response all shares\n\t\t\tvar items []*api.Source\n\t\t\tfor src, share := range shares {\n\t\t\t\tpwd := srv.GetSharePwd(share)\n\t\t\t\tsource := fmt.Sprintf(\"webtorrent:?share=%s&pwd=%s\", share, pwd)\n\t\t\t\titems = append(items, &api.Source{ID: src, URL: source})\n\t\t\t}\n\t\t\tapi.ResponseSources(w, items)\n\t\t}\n\n\tcase \"POST\":\n\t\t// check if share already exist\n\t\tif ok {\n\t\t\thttp.Error(w, \"\", http.StatusBadRequest)\n\t\t\treturn\n\t\t}\n\n\t\t// check if stream exists\n\t\tif stream := streams.Get(src); stream == nil {\n\t\t\thttp.Error(w, \"\", http.StatusNotFound)\n\t\t\treturn\n\t\t}\n\n\t\t// create new random share\n\t\tshare = core.RandString(10, 62)\n\t\tpwd := core.RandString(10, 62)\n\t\tsrv.AddShare(share, pwd, src)\n\n\t\tshares[src] = share\n\n\t\tw.WriteHeader(http.StatusCreated)\n\t\tdata := fmt.Sprintf(`{\"share\":%q,\"pwd\":%q}`, share, pwd)\n\t\tapi.Response(w, data, api.MimeJSON)\n\n\tcase \"DELETE\":\n\t\tif ok {\n\t\t\tsrv.RemoveShare(share)\n\t\t\tdelete(shares, src)\n\t\t} else {\n\t\t\thttp.Error(w, \"\", http.StatusNotFound)\n\t\t}\n\t}\n}\n\nfunc streamHandle(rawURL string) (core.Producer, error) {\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tquery := u.Query()\n\tshare := query.Get(\"share\")\n\tpwd := query.Get(\"pwd\")\n\tif len(share) < 8 || len(pwd) < 4 {\n\t\treturn nil, errors.New(\"wrong URL: \" + rawURL)\n\t}\n\n\tpc, err := webrtc.PeerConnection(true)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn webtorrent.NewClient(srv.URL, share, pwd, pc)\n}\n"
  },
  {
    "path": "internal/webtorrent/tracker.go",
    "content": "package webtorrent\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/webtorrent\"\n\t\"github.com/gorilla/websocket\"\n)\n\nvar upgrader *websocket.Upgrader\nvar hashes map[string]map[string]*websocket.Conn\n\nfunc tracker(w http.ResponseWriter, r *http.Request) {\n\tif upgrader == nil {\n\t\tupgrader = &websocket.Upgrader{\n\t\t\tReadBufferSize:  1024,\n\t\t\tWriteBufferSize: 2028,\n\t\t}\n\t\tupgrader.CheckOrigin = func(r *http.Request) bool {\n\t\t\treturn true\n\t\t}\n\t}\n\n\tws, err := upgrader.Upgrade(w, r, nil)\n\tif err != nil {\n\t\tlog.Warn().Err(err).Send()\n\t\treturn\n\t}\n\n\tdefer ws.Close()\n\n\tfor {\n\t\tvar msg webtorrent.Message\n\t\tif err = ws.ReadJSON(&msg); err != nil {\n\t\t\treturn\n\t\t}\n\n\t\t//log.Trace().Msgf(\"[webtorrent] message=%v\", msg)\n\n\t\tif msg.InfoHash == \"\" || msg.PeerId == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tif hashes == nil {\n\t\t\thashes = map[string]map[string]*websocket.Conn{}\n\t\t}\n\n\t\t// new or old client with offers\n\t\tclients := hashes[msg.InfoHash]\n\t\tif clients == nil {\n\t\t\tclients = map[string]*websocket.Conn{\n\t\t\t\tmsg.PeerId: ws,\n\t\t\t}\n\t\t\thashes[msg.InfoHash] = clients\n\t\t} else {\n\t\t\tclients[msg.PeerId] = ws\n\t\t}\n\n\t\tswitch {\n\t\tcase msg.Offers != nil:\n\t\t\t// ask for ping\n\t\t\traw := fmt.Sprintf(\n\t\t\t\t`{\"action\":\"announce\",\"interval\":120,\"info_hash\":\"%s\",\"complete\":0,\"incomplete\":1}`,\n\t\t\t\tmsg.InfoHash,\n\t\t\t)\n\t\t\tif err = ws.WriteMessage(websocket.TextMessage, []byte(raw)); err != nil {\n\t\t\t\tlog.Warn().Err(err).Send()\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// skip if no offers (server)\n\t\t\tif len(msg.Offers) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// get and check only first offer\n\t\t\toffer := msg.Offers[0]\n\t\t\tif offer.OfferId == \"\" || offer.Offer.Type != \"offer\" || offer.Offer.SDP == \"\" {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// send offer to all clients (one of them - server)\n\t\t\traw = fmt.Sprintf(\n\t\t\t\t`{\"action\":\"announce\",\"info_hash\":\"%s\",\"peer_id\":\"%s\",\"offer_id\":\"%s\",\"offer\":{\"type\":\"offer\",\"sdp\":\"%s\"}}`,\n\t\t\t\tmsg.InfoHash, msg.PeerId, offer.OfferId, offer.Offer.SDP,\n\t\t\t)\n\n\t\t\tfor _, server := range clients {\n\t\t\t\tif server != ws {\n\t\t\t\t\t_ = server.WriteMessage(websocket.TextMessage, []byte(raw))\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase msg.OfferId != \"\" && msg.ToPeerId != \"\" && msg.Answer != nil:\n\t\t\tws1, ok := clients[msg.ToPeerId]\n\t\t\tif !ok {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\traw := fmt.Sprintf(\n\t\t\t\t`{\"action\":\"announce\",\"info_hash\":\"%s\",\"peer_id\":\"%s\",\"offer_id\":\"%s\",\"answer\":{\"type\":\"answer\",\"sdp\":\"%s\"}}`,\n\t\t\t\tmsg.InfoHash, msg.PeerId, msg.OfferId, msg.Answer.SDP,\n\t\t\t)\n\t\t\t_ = ws1.WriteMessage(websocket.TextMessage, []byte(raw))\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "internal/wyoming/README.md",
    "content": "# Wyoming\n\n> [!NOTE]\n> The format is under development and does not yet work stably.\n\nThis module provide [Wyoming Protocol](https://www.home-assistant.io/integrations/wyoming/) support to create local voice assistants using [Home Assistant](https://www.home-assistant.io/).\n\n- go2rtc can act as [Wyoming Satellite](https://github.com/rhasspy/wyoming-satellite)\n- go2rtc can act as [Wyoming External Microphone](https://github.com/rhasspy/wyoming-mic-external)\n- go2rtc can act as [Wyoming External Sound](https://github.com/rhasspy/wyoming-snd-external)\n- any supported audio source with PCM codec can be used as audio input\n- any supported two-way audio source with PCM codec can be used as audio output\n- any desktop/server microphone/speaker can be used as two-way audio source\n  - supported any OS via FFmpeg or any similar software\n  - supported Linux via alsa source\n- you can change the behavior using the built-in scripting engine\n\n## Typical Voice Pipeline\n\n1. Audio stream (MIC)\n   - any audio source with PCM codec support (include PCMA/PCMU)\n2. Voice Activity Detector (VAD)\n3. Wake Word (WAKE)\n   - [OpenWakeWord](https://www.home-assistant.io/voice_control/create_wake_word/)\n4. Speech-to-Text (STT)\n   - [Whisper](https://github.com/home-assistant/addons/blob/master/whisper/) \n   - [Vosk](https://github.com/rhasspy/hassio-addons/blob/master/vosk/)\n5. Conversation agent (INTENT)\n   - [Home Assistant](https://www.home-assistant.io/integrations/conversation/)\n6. Text-to-speech (TTS)\n   - [Google Translate](https://www.home-assistant.io/integrations/google_translate/)\n   - [Piper](https://github.com/home-assistant/addons/blob/master/piper/)\n7. Audio stream (SND)\n   - any source with two-way audio (backchannel) and PCM codec support (include PCMA/PCMU)\n\nYou can use a large number of different projects for WAKE, STT, INTENT and TTS thanks to the Home Assistant.\n\nAnd you can use a large number of different technologies for MIC and SND thanks to Go2rtc.\n\n## Configuration\n\nYou can optionally specify WAKE service. So go2rtc will start transmitting audio to Home Assistant only after WAKE word. If the WAKE service cannot be connected to or not specified - go2rtc will pass all audio to Home Assistant. In this case WAKE service must be configured in your Voice Assistant pipeline.\n\nYou can optionally specify VAD threshold. So go2rtc will start transmitting audio to WAKE service only after some audio noise.\n\nYour stream must support audio transmission in PCM codec (include PCMA/PCMU).\n\n```yaml\nwyoming:\n  stream_name_from_streams_section:\n    listen: :10700 \n    name: \"My Satellite\"                # optional name\n    wake_uri: tcp://192.168.1.23:10400  # optional WAKE service\n    vad_threshold: 1                    # optional VAD threshold (from 0.1 to 3.5)\n```\n\nHome Assistant -> Settings -> Integrations -> Add -> Wyoming Protocol -> Host + Port from `go2rtc.yaml`\n\nSelect one or multiple wake words:\n```yaml\nwake_uri: tcp://192.168.1.23:10400?name=alexa_v0.1&name=hey_jarvis_v0.1&name=hey_mycroft_v0.1&name=hey_rhasspy_v0.1&name=ok_nabu_v0.1\n```\n\n## Events\n\nYou can add wyoming event handling using the [expr](../expr/README.md) language. For example, to pronounce TTS on some media player from HA.\n\nTurn on the logs to see what kind of events happens.\n\nThis is what the default scripts look like:\n\n```yaml\nwyoming:\n  script_example:\n    event:\n      run-satellite: Detect()\n      pause-satellite: Stop()\n      voice-stopped: Pause()\n      audio-stop: PlayAudio() && WriteEvent(\"played\") && Detect()\n      error: Detect()\n      internal-run: WriteEvent(\"run-pipeline\", '{\"start_stage\":\"wake\",\"end_stage\":\"tts\"}') && Stream()\n      internal-detection: WriteEvent(\"run-pipeline\", '{\"start_stage\":\"asr\",\"end_stage\":\"tts\"}') && Stream()\n```\n\nSupported functions and variables:\n\n- `Detect()` - start the VAD and WAKE word detection process\n- `Stream()` - start transmission of audio data to the client (Home Assistant)\n- `Stop()` - stop and disconnect stream without disconnecting client (Home Assistant)\n- `Pause()` - temporary pause of audio transfer, without disconnecting the stream\n- `PlayAudio()` - playing the last audio that was sent from client (Home Assistant)\n- `WriteEvent(type, data)` - send event to client (Home Assistant)\n- `Sleep(duration)` - temporary script pause (ex. `Sleep('1.5s')`)\n- `PlayFile(path)` - play audio from `wav` file\n- `Type` - type (name) of event\n- `Data` - event data in JSON format (ex. `{\"text\":\"how are you\"}`)\n- also available other functions from [expr](../expr/README.md) module (ex. `fetch`)\n\nIf you write a script for an event - the default action is no longer executed. You need to repeat the necessary steps yourself.\n\nIn addition to the standard events, there are two additional events:\n\n- `internal-run` - called after `Detect()` when VAD detected, but WAKE service unavailable\n- `internal-detection` - called after `Detect()` when WAKE word detected\n\n**Example 1.** You want to play a sound file when a wake word detected (only `wav` supported):\n\n- `PlayFile` and `PlayAudio` functions are executed synchronously, the following steps will be executed only after they are completed\n\n```yaml\nwyoming:\n  script_example:\n    event:\n      internal-detection: PlayFile('/media/beep.wav') && WriteEvent(\"run-pipeline\", '{\"start_stage\":\"asr\",\"end_stage\":\"tts\"}') && Stream()\n```\n\n**Example 2.** You want to play TTS on a Home Assistant media player:\n\nEach event has a `Type` and `Data` in JSON format. You can use their values in scripts.\n\n- in the `synthesize` step, we get the value of the `text` and call the HA REST API\n- in the `audio-stop` step we get the duration of the TTS in seconds, wait for this time and start the pipeline again\n\n```yaml\nwyoming:\n  script_example:\n    event:\n      synthesize: |\n        let text = fromJSON(Data).text;\n        let token = 'eyJhbGci...';\n        fetch('http://localhost:8123/api/services/tts/speak', {\n          method: 'POST',\n          headers: {'Authorization': 'Bearer '+token,'Content-Type': 'application/json'},\n          body: toJSON({\n            entity_id: 'tts.google_translate_com',\n            media_player_entity_id: 'media_player.google_nest',\n            message: text,\n            language: 'en',\n          }),\n        }).ok\n      audio-stop: |\n        let timestamp = fromJSON(Data).timestamp;\n        let delay = string(timestamp)+'s';\n        Sleep(delay) && WriteEvent(\"played\") && Detect()\n```\n\n## Config examples\n\nSatellite on Windows server using FFmpeg and FFplay.\n\n```yaml\nstreams:\n  satellite_win:\n    - exec:ffmpeg -hide_banner -f dshow -i \"audio=Microphone (High Definition Audio Device)\" -c pcm_s16le -ar 16000 -ac 1 -f wav -\n    - exec:ffplay -hide_banner -nodisp -probesize 32 -f s16le -ar 22050 -#backchannel=1#audio=s16le/22050\n\nwyoming:\n  satellite_win:\n    listen: :10700\n    name: \"Windows Satellite\"\n    wake_uri: tcp://192.168.1.23:10400\n    vad_threshold: 1\n```\n\nSatellite on Dahua camera with two-way audio support.\n\n```yaml\nstreams:\n  dahua_camera:\n    - rtsp://admin:password@192.168.1.123/cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif\n\nwyoming:\n  dahua_camera:\n    listen: :10700\n    name: \"Dahua Satellite\"\n    wake_uri: tcp://192.168.1.23:10400\n    vad_threshold: 1\n```\n\nSatellite on external wyoming Microphone and Sound.\n\n```yaml\nstreams:\n  wyoming_external:\n     - wyoming://192.168.1.23:10600                # wyoming-mic-external\n     - wyoming://192.168.1.23:10601?backchannel=1  # wyoming-snd-external\n\nwyoming:\n   wyoming_external:\n    listen: :10700\n    name: \"Wyoming Satellite\"\n    wake_uri: tcp://192.168.1.23:10400\n    vad_threshold: 1\n```\n\n## Wyoming External Microphone and Sound\n\nAdvanced users, who want to enjoy the [Wyoming Satellite](https://github.com/rhasspy/wyoming-satellite) project, can use go2rtc as a [Wyoming External Microphone](https://github.com/rhasspy/wyoming-mic-external) or [Wyoming External Sound](https://github.com/rhasspy/wyoming-snd-external).\n\n**go2rtc.yaml**\n\n```yaml\nstreams:\n  wyoming_mic_external:\n    - exec:ffmpeg -hide_banner -f dshow -i \"audio=Microphone (High Definition Audio Device)\" -c pcm_s16le -ar 16000 -ac 1 -f wav -\n  wyoming_snd_external:\n    - exec:ffplay -hide_banner -nodisp -probesize 32 -f s16le -ar 22050 -#backchannel=1#audio=s16le/22050\n\nwyoming:\n  wyoming_mic_external:\n    listen: :10600\n    mode: mic\n  wyoming_snd_external:\n    listen: :10601\n    mode: snd\n```\n\n**docker-compose.yml**\n\n```yaml\nversion: \"3.8\"\nservices:\n  satellite:\n    build: wyoming-satellite  # https://github.com/rhasspy/wyoming-satellite\n    ports:\n      - \"10700:10700\"\n    command:\n      - \"--name\"\n      - \"my satellite\"\n      - \"--mic-uri\"\n      - \"tcp://192.168.1.23:10600\"\n      - \"--snd-uri\"\n      - \"tcp://192.168.1.23:10601\"\n      - \"--debug\"\n```\n\n## Wyoming External Source\n\n**go2rtc.yaml**\n\n```yaml\nstreams:\n  wyoming_external:\n    - wyoming://192.168.1.23:10600\n    - wyoming://192.168.1.23:10601?backchannel=1\n```\n\n**docker-compose.yml**\n\n```yaml\nversion: \"3.8\"\nservices:\n   microphone:\n      build: wyoming-mic-external  # https://github.com/rhasspy/wyoming-mic-external\n      ports:\n         - \"10600:10600\"\n      devices:\n         - /dev/snd:/dev/snd\n      group_add:\n         - audio\n      command:\n         - \"--device\"\n         - \"sysdefault\"\n         - \"--debug\"\n   playback:\n      build: wyoming-snd-external  # https://github.com/rhasspy/wyoming-snd-external\n      ports:\n         - \"10601:10601\"\n      devices:\n         - /dev/snd:/dev/snd\n      group_add:\n         - audio\n      command:\n         - \"--device\"\n         - \"sysdefault\"\n         - \"--debug\"\n```\n\n## Debug\n\n```yaml\nlog:\n  wyoming: trace\n```\n"
  },
  {
    "path": "internal/wyoming/wyoming.go",
    "content": "package wyoming\n\nimport (\n\t\"net\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/app\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/wyoming\"\n\t\"github.com/rs/zerolog\"\n)\n\nfunc Init() {\n\tstreams.HandleFunc(\"wyoming\", wyoming.Dial)\n\n\t// server\n\tvar cfg struct {\n\t\tMod map[string]struct {\n\t\t\tListen       string            `yaml:\"listen\"`\n\t\t\tName         string            `yaml:\"name\"`\n\t\t\tMode         string            `yaml:\"mode\"`\n\t\t\tEvent        map[string]string `yaml:\"event\"`\n\t\t\tWakeURI      string            `yaml:\"wake_uri\"`\n\t\t\tVADThreshold float32           `yaml:\"vad_threshold\"`\n\t\t} `yaml:\"wyoming\"`\n\t}\n\tapp.LoadConfig(&cfg)\n\n\tlog = app.GetLogger(\"wyoming\")\n\n\tfor name, cfg := range cfg.Mod {\n\t\tstream := streams.Get(name)\n\t\tif stream == nil {\n\t\t\tlog.Warn().Msgf(\"[wyoming] missing stream: %s\", name)\n\t\t\tcontinue\n\t\t}\n\n\t\tif cfg.Name == \"\" {\n\t\t\tcfg.Name = name\n\t\t}\n\n\t\tsrv := &wyoming.Server{\n\t\t\tName:         cfg.Name,\n\t\t\tEvent:        cfg.Event,\n\t\t\tVADThreshold: int16(1000 * cfg.VADThreshold), // 1.0 => 1000\n\t\t\tWakeURI:      cfg.WakeURI,\n\t\t\tMicHandler: func(cons core.Consumer) error {\n\t\t\t\tif err := stream.AddConsumer(cons); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t\t// not best solution\n\t\t\t\tif i, ok := cons.(interface{ OnClose(func()) }); ok {\n\t\t\t\t\ti.OnClose(func() {\n\t\t\t\t\t\tstream.RemoveConsumer(cons)\n\t\t\t\t\t})\n\t\t\t\t}\n\t\t\t\treturn nil\n\t\t\t},\n\t\t\tSndHandler: func(prod core.Producer) error {\n\t\t\t\treturn stream.Play(prod)\n\t\t\t},\n\t\t\tTrace: func(format string, v ...any) {\n\t\t\t\tlog.Trace().Msgf(\"[wyoming] \"+format, v...)\n\t\t\t},\n\t\t\tError: func(format string, v ...any) {\n\t\t\t\tlog.Error().Msgf(\"[wyoming] \"+format, v...)\n\t\t\t},\n\t\t}\n\t\tgo serve(srv, cfg.Mode, cfg.Listen)\n\t}\n}\n\nvar log zerolog.Logger\n\nfunc serve(srv *wyoming.Server, mode, address string) {\n\tln, err := net.Listen(\"tcp\", address)\n\tif err != nil {\n\t\tlog.Warn().Err(err).Msgf(\"[wyoming] listen\")\n\t}\n\n\tfor {\n\t\tconn, err := ln.Accept()\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\n\t\tgo handle(srv, mode, conn)\n\t}\n}\n\nfunc handle(srv *wyoming.Server, mode string, conn net.Conn) {\n\taddr := conn.RemoteAddr()\n\n\tlog.Trace().Msgf(\"[wyoming] %s connected\", addr)\n\n\tswitch mode {\n\tcase \"mic\":\n\t\tsrv.HandleMic(conn)\n\tcase \"snd\":\n\t\tsrv.HandleSnd(conn)\n\tdefault:\n\t\tsrv.Handle(conn)\n\t}\n\n\tlog.Trace().Msgf(\"[wyoming] %s disconnected\", addr)\n}\n"
  },
  {
    "path": "internal/wyze/README.md",
    "content": "# Wyze\n\n[`new in v1.9.14`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.9.14) by [@seydx](https://github.com/seydx)\n\nThis source allows you to stream from [Wyze](https://wyze.com/) cameras using native P2P protocol without the Wyze app or SDK.\n\n**Important:**\n\n1. **Requires Wyze account**. You need to login once via the WebUI to load your cameras.\n2. **Requires firmware with DTLS**. Only cameras with DTLS-enabled firmware are supported.\n3. Internet access is only needed when loading cameras from your account. After that, all streaming is local P2P.\n4. Connection to the camera is local only (direct P2P to camera IP).\n\n**Features:**\n\n- H.264 and H.265 video codec support\n- AAC, G.711, PCM, and Opus audio codec support\n- Two-way audio (intercom) support\n- Resolution switching (HD/SD)\n\n## Setup\n\n1. Get your API Key from [Wyze Developer Portal](https://support.wyze.com/hc/en-us/articles/16129834216731)\n2. Go to go2rtc WebUI > Add > Wyze\n3. Enter your API ID, API Key, email, and password\n4. Select cameras to add - stream URLs are generated automatically\n\n**Example Config**\n\n```yaml\nwyze:\n  user@email.com:\n    api_id: \"your-api-id\"\n    api_key: \"your-api-key\"\n    password: \"yourpassword\"    # or MD5 triple-hash with \"md5:\" prefix\n\nstreams:\n  wyze_cam: wyze://192.168.1.123?uid=WYZEUID1234567890AB&enr=xxx&mac=AABBCCDDEEFF&model=HL_CAM4&dtls=true\n```\n\n## Stream URL Format\n\nThe stream URL is automatically generated when you add cameras via the WebUI:\n\n```\nwyze://[IP]?uid=[P2P_ID]&enr=[ENR]&mac=[MAC]&model=[MODEL]&subtype=[hd|sd]&dtls=true\n```\n\n| Parameter | Description                                     |\n|-----------|-------------------------------------------------|\n| `IP`      | Camera's local IP address                       |\n| `uid`     | P2P identifier (20 chars)                       |\n| `enr`     | Encryption key for DTLS                         |\n| `mac`     | Device MAC address                              |\n| `model`   | Camera model (e.g., HL_CAM4)                    |\n| `dtls`    | Enable DTLS encryption (default: true)          |\n| `subtype` | Camera resolution: `hd` or `sd` (default: `hd`) |\n\n## Configuration\n\n### Resolution\n\nYou can change the camera's resolution using the `subtype` parameter:\n\n```yaml\nstreams:\n  wyze_hd: wyze://...&subtype=hd\n  wyze_sd: wyze://...&subtype=sd\n```\n\n### Two-Way Audio\n\nTwo-way audio (intercom) is supported automatically. When a consumer sends audio to the stream, it will be transmitted to the camera's speaker.\n\n## Camera Compatibility\n\n| Name                        | Model          | Firmware     | Protocol | Encryption | Codecs     |\n|-----------------------------|----------------|--------------|----------|------------|------------|\n| Wyze Cam v4                 | HL_CAM4        | 4.52.9.4188  | TUTK     | TransCode  | h264, aac  |\n|                             |                | 4.52.9.5332  | TUTK     | HMAC-SHA1  | h264, aac  |\n| Wyze Cam v3 Pro             |                |              | TUTK     |            |            |\n| Wyze Cam v3                 | WYZE_CAKP2JFUS | 4.36.14.3497 | TUTK     | TransCode  | h264, pcm  |\n| Wyze Cam v2                 | WYZEC1-JZ      | 4.9.9.3006   | TUTK     | TransCode  | h264, pcmu |\n| Wyze Cam v1                 |                |              | TUTK     |            |            |\n| Wyze Cam Pan v4             |                |              | Gwell*   |            |            |\n| Wyze Cam Pan v3             |                |              | TUTK     |            |            |\n| Wyze Cam Pan v2             |                |              | TUTK     |            |            |\n| Wyze Cam Pan v1             |                |              | TUTK     |            |            |\n| Wyze Cam OG                 |                |              | Gwell*   |            |            |\n| Wyze Cam OG Telephoto       |                |              | Gwell*   |            |            |\n| Wyze Cam OG (2025)          |                |              | Gwell*   |            |            |\n| Wyze Cam Outdoor v2         |                |              | TUTK     |            |            |\n| Wyze Cam Outdoor v1         |                |              | TUTK     |            |            |\n| Wyze Cam Floodlight Pro     |                |              | ?        |            |            |\n| Wyze Cam Floodlight v2      |                |              | TUTK     |            |            |\n| Wyze Cam Floodlight         |                |              | TUTK     |            |            |\n| Wyze Video Doorbell v2      | HL_DB2         | 4.51.3.4992  | TUTK     | TransCode  | h264, pcm  |\n| Wyze Video Doorbell v1      |                |              | TUTK     |            |            |\n| Wyze Video Doorbell Pro     |                |              | ?        |            |            |\n| Wyze Battery Video Doorbell |                |              | ?        |            |            |\n| Wyze Duo Cam Doorbell       |                |              | ?        |            |            |\n| Wyze Battery Cam Pro        |                |              | ?        |            |            |\n| Wyze Solar Cam Pan          |                |              | ?        |            |            |\n| Wyze Duo Cam Pan            |                |              | ?        |            |            |\n| Wyze Window Cam             |                |              | ?        |            |            |\n| Wyze Bulb Cam               |                |              | ?        |            |            |\n\n_* Gwell based protocols are not yet supported._\n"
  },
  {
    "path": "internal/wyze/wyze.go",
    "content": "package wyze\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/internal/app\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/wyze\"\n)\n\ntype AccountConfig struct {\n\tAPIKey   string `yaml:\"api_key\"`\n\tAPIID    string `yaml:\"api_id\"`\n\tPassword string `yaml:\"password\"`\n}\n\nvar accounts map[string]AccountConfig\n\nfunc Init() {\n\tvar v struct {\n\t\tCfg map[string]AccountConfig `yaml:\"wyze\"`\n\t}\n\tapp.LoadConfig(&v)\n\n\taccounts = v.Cfg\n\n\tlog := app.GetLogger(\"wyze\")\n\n\tstreams.HandleFunc(\"wyze\", func(rawURL string) (core.Producer, error) {\n\t\tlog.Debug().Msgf(\"wyze: dial %s\", rawURL)\n\t\treturn wyze.NewProducer(rawURL)\n\t})\n\n\tapi.HandleFunc(\"api/wyze\", apiWyze)\n}\n\nfunc getCloud(email string) (*wyze.Cloud, error) {\n\tcfg, ok := accounts[email]\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"wyze: account not found: %s\", email)\n\t}\n\n\tif cfg.APIKey == \"\" || cfg.APIID == \"\" {\n\t\treturn nil, fmt.Errorf(\"wyze: api_key and api_id required for account: %s\", email)\n\t}\n\n\tcloud := wyze.NewCloud(cfg.APIKey, cfg.APIID)\n\n\tif err := cloud.Login(email, cfg.Password); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn cloud, nil\n}\n\nfunc apiWyze(w http.ResponseWriter, r *http.Request) {\n\tswitch r.Method {\n\tcase \"GET\":\n\t\tapiDeviceList(w, r)\n\tcase \"POST\":\n\t\tapiAuth(w, r)\n\t}\n}\n\nfunc apiDeviceList(w http.ResponseWriter, r *http.Request) {\n\tquery := r.URL.Query()\n\n\temail := query.Get(\"id\")\n\tif email == \"\" {\n\t\taccountList := make([]string, 0, len(accounts))\n\t\tfor id := range accounts {\n\t\t\taccountList = append(accountList, id)\n\t\t}\n\t\tapi.ResponseJSON(w, accountList)\n\t\treturn\n\t}\n\n\terr := func() error {\n\t\tcloud, err := getCloud(email)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tcameras, err := cloud.GetCameraList()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tvar items []*api.Source\n\t\tfor _, cam := range cameras {\n\t\t\titems = append(items, &api.Source{\n\t\t\t\tName: cam.Nickname,\n\t\t\t\tInfo: fmt.Sprintf(\"%s | %s | %s\", cam.ProductModel, cam.MAC, cam.IP),\n\t\t\t\tURL:  buildStreamURL(cam),\n\t\t\t})\n\t\t}\n\n\t\tapi.ResponseSources(w, items)\n\t\treturn nil\n\t}()\n\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t}\n}\n\nfunc apiAuth(w http.ResponseWriter, r *http.Request) {\n\tif err := r.ParseForm(); err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\temail := r.Form.Get(\"email\")\n\tpassword := r.Form.Get(\"password\")\n\tapiKey := r.Form.Get(\"api_key\")\n\tapiID := r.Form.Get(\"api_id\")\n\n\tif email == \"\" || password == \"\" || apiKey == \"\" || apiID == \"\" {\n\t\thttp.Error(w, \"email, password, api_key and api_id required\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\t// Try to login\n\tcloud := wyze.NewCloud(apiKey, apiID)\n\n\tif err := cloud.Login(email, password); err != nil {\n\t\t// Check for MFA error\n\t\tvar authErr *wyze.AuthError\n\t\tif ok := isAuthError(err, &authErr); ok {\n\t\t\tw.Header().Set(\"Content-Type\", api.MimeJSON)\n\t\t\tw.WriteHeader(http.StatusUnauthorized)\n\t\t\t_ = json.NewEncoder(w).Encode(authErr)\n\t\t\treturn\n\t\t}\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tcfg := map[string]string{\n\t\t\"password\": password,\n\t\t\"api_key\":  apiKey,\n\t\t\"api_id\":   apiID,\n\t}\n\n\tif err := app.PatchConfig([]string{\"wyze\", email}, cfg); err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tif accounts == nil {\n\t\taccounts = make(map[string]AccountConfig)\n\t}\n\taccounts[email] = AccountConfig{\n\t\tAPIKey:   apiKey,\n\t\tAPIID:    apiID,\n\t\tPassword: password,\n\t}\n\n\tcameras, err := cloud.GetCameraList()\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t\treturn\n\t}\n\n\tvar items []*api.Source\n\tfor _, cam := range cameras {\n\t\titems = append(items, &api.Source{\n\t\t\tName: cam.Nickname,\n\t\t\tInfo: fmt.Sprintf(\"%s | %s | %s\", cam.ProductModel, cam.MAC, cam.IP),\n\t\t\tURL:  buildStreamURL(cam),\n\t\t})\n\t}\n\n\tapi.ResponseSources(w, items)\n}\n\nfunc buildStreamURL(cam *wyze.Camera) string {\n\tquery := url.Values{}\n\tquery.Set(\"uid\", cam.P2PID)\n\tquery.Set(\"enr\", cam.ENR)\n\tquery.Set(\"mac\", cam.MAC)\n\tquery.Set(\"model\", cam.ProductModel)\n\n\tif cam.DTLS == 1 {\n\t\tquery.Set(\"dtls\", \"true\")\n\t}\n\n\treturn fmt.Sprintf(\"wyze://%s?%s\", cam.IP, query.Encode())\n}\n\nfunc isAuthError(err error, target **wyze.AuthError) bool {\n\tif e, ok := err.(*wyze.AuthError); ok {\n\t\t*target = e\n\t\treturn true\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "internal/xiaomi/README.md",
    "content": "# Xiaomi Mi Home\n\n[`new in v1.9.13`](https://github.com/AlexxIT/go2rtc/releases/tag/v1.9.13)\n\nThis source allows you to view cameras from the [Xiaomi Mi Home](https://home.mi.com/) ecosystem.\n\nSince 2020, Xiaomi has introduced a unified protocol for cameras called `miss`. I think it means **Mi Secure Streaming**. Until this point, the camera protocols were in chaos. Almost every model had different authorization, encryption, command lists, and media packet formats.\n\ngo2rtc supports two formats: `xiaomi/mess` and `xiaomi/legacy`.\nAnd multiple P2P protocols: `cs2+udp`, `cs2+tcp`, several versions of `tutk+udp`.\n\nAlmost all cameras in the `xiaomi/mess` format and the `cs2` protocol work well.\nOlder `xiaomi/legacy` format cameras may have support issues.\nThe `tutk` protocol is the worst thing that's ever happened to the P2P world. It works terribly.\n\n**Important:**\n\n1. **Not all cameras are supported**. The list of supported cameras is collected in [this issue](https://github.com/AlexxIT/go2rtc/issues/1982).\n2. Each time you connect to the camera, you need Internet access to obtain encryption keys.\n3. Connection to the camera is local only.\n\n**Features:**\n\n- Multiple Xiaomi accounts supported\n- Cameras from multiple regions are supported for a single account\n- Two-way audio is supported\n- Cameras with multiple lenses are supported\n\n## Setup\n\n1. Go to go2rtc WebUI > Add > Xiaomi > Login with username and password\n2. Receive verification code by email or phone if required.\n3. Complete the captcha if required.\n4. If everything is OK, your account will be added, and you can load cameras from it.\n\n**Example**\n\n```yaml\nxiaomi:\n  1234567890: V1:***\n\nstreams:\n  xiaomi1: xiaomi://1234567890:cn@192.168.1.123?did=9876543210&model=isa.camera.hlc7\n```\n\n## Configuration\n\nQuality in the `miss` protocol is specified by a number from 0 to 5. Usually 0 means auto, 1 - sd, 2 - hd.\nGo2rtc by default sets quality to 2. But some new cameras have HD quality at number 3.\nOld cameras may have broken codec settings at number 3, so this number should not be set for all cameras.\n\nYou can change camera quality: `subtype=hd/sd/auto/0-5`.\n\n```yaml\nstreams:\n  xiaomi1: xiaomi://***&subtype=sd\n```\n\nYou can use a second channel for dual cameras: `channel=2`.\n\n```yaml\nstreams:\n  xiaomi1: xiaomi://***&channel=2\n```\n"
  },
  {
    "path": "internal/xiaomi/xiaomi.go",
    "content": "package xiaomi\n\nimport (\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/internal/app\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/xiaomi\"\n\t\"github.com/AlexxIT/go2rtc/pkg/xiaomi/crypto\"\n\t\"github.com/rs/zerolog\"\n)\n\nfunc Init() {\n\tvar v struct {\n\t\tCfg map[string]string `yaml:\"xiaomi\"`\n\t}\n\tapp.LoadConfig(&v)\n\n\ttokens = v.Cfg\n\n\tlog = app.GetLogger(\"xiaomi\")\n\n\tstreams.HandleFunc(\"xiaomi\", func(rawURL string) (core.Producer, error) {\n\t\tu, err := url.Parse(rawURL)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif u.User != nil {\n\t\t\trawURL, err = getCameraURL(u)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\n\t\tlog.Debug().Msgf(\"xiaomi: dial %s\", rawURL)\n\n\t\treturn xiaomi.Dial(rawURL)\n\t})\n\n\tapi.HandleFunc(\"api/xiaomi\", apiXiaomi)\n}\n\nvar log zerolog.Logger\n\nvar tokens map[string]string\nvar clouds map[string]*xiaomi.Cloud\nvar cloudsMu sync.Mutex\n\nfunc getCloud(userID string) (*xiaomi.Cloud, error) {\n\tcloudsMu.Lock()\n\tdefer cloudsMu.Unlock()\n\n\tif cloud := clouds[userID]; cloud != nil {\n\t\treturn cloud, nil\n\t}\n\n\tcloud := xiaomi.NewCloud(AppXiaomiHome)\n\tif err := cloud.LoginWithToken(userID, tokens[userID]); err != nil {\n\t\treturn nil, err\n\t}\n\tif clouds == nil {\n\t\tclouds = map[string]*xiaomi.Cloud{userID: cloud}\n\t} else {\n\t\tclouds[userID] = cloud\n\t}\n\treturn cloud, nil\n}\n\nfunc cloudRequest(userID, region, apiURL, params string) ([]byte, error) {\n\tcloud, err := getCloud(userID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn cloud.Request(GetBaseURL(region), apiURL, params, nil)\n}\n\nfunc cloudUserRequest(user *url.Userinfo, apiURL, params string) ([]byte, error) {\n\tuserID := user.Username()\n\tregion, _ := user.Password()\n\treturn cloudRequest(userID, region, apiURL, params)\n}\n\nfunc getCameraURL(url *url.URL) (string, error) {\n\tmodel := url.Query().Get(\"model\")\n\n\t// It is not known which models need to be awakened.\n\t// Probably all the doorbells and all the battery cameras.\n\tif strings.Contains(model, \".cateye.\") {\n\t\t_ = wakeUpCamera(url)\n\t}\n\n\t// The getMissURL request has a fallback to getP2PURL.\n\t// But for known models we can save one request to the cloud.\n\tif xiaomi.IsLegacy(model) {\n\t\treturn getLegacyURL(url)\n\t}\n\treturn getMissURL(url)\n}\n\nfunc getLegacyURL(url *url.URL) (string, error) {\n\tquery := url.Query()\n\n\tclientPublic, clientPrivate, err := crypto.GenerateKey()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tparams := fmt.Sprintf(`{\"did\":\"%s\",\"toSignAppData\":\"%x\"}`, query.Get(\"did\"), clientPublic)\n\n\tuserID := url.User.Username()\n\tregion, _ := url.User.Password()\n\tres, err := cloudRequest(userID, region, \"/device/devicepass\", params)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar v struct {\n\t\tUID       string `json:\"p2p_id\"`\n\t\tPassword  string `json:\"password\"`\n\t\tPublicKey string `json:\"p2p_dev_public_key\"`\n\t\tSign      string `json:\"signForAppData\"`\n\t}\n\tif err = json.Unmarshal(res, &v); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tquery.Set(\"uid\", v.UID)\n\n\tif v.Sign != \"\" {\n\t\tquery.Set(\"client_public\", hex.EncodeToString(clientPublic))\n\t\tquery.Set(\"client_private\", hex.EncodeToString(clientPrivate))\n\t\tquery.Set(\"device_public\", v.PublicKey)\n\t\tquery.Set(\"sign\", v.Sign)\n\t} else {\n\t\tquery.Set(\"password\", v.Password)\n\t}\n\n\turl.RawQuery = query.Encode()\n\treturn url.String(), nil\n}\n\nfunc getMissURL(url *url.URL) (string, error) {\n\tclientPublic, clientPrivate, err := crypto.GenerateKey()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tquery := url.Query()\n\tparams := fmt.Sprintf(\n\t\t`{\"app_pubkey\":\"%x\",\"did\":\"%s\",\"support_vendors\":\"TUTK_CS2_MTP\"}`,\n\t\tclientPublic, query.Get(\"did\"),\n\t)\n\n\tres, err := cloudUserRequest(url.User, \"/v2/device/miss_get_vendor\", params)\n\tif err != nil {\n\t\tif strings.Contains(err.Error(), \"no available vendor support\") {\n\t\t\treturn getLegacyURL(url)\n\t\t}\n\t\treturn \"\", err\n\t}\n\n\tvar v struct {\n\t\tVendor struct {\n\t\t\tID     byte `json:\"vendor\"`\n\t\t\tParams struct {\n\t\t\t\tUID string `json:\"p2p_id\"`\n\t\t\t} `json:\"vendor_params\"`\n\t\t} `json:\"vendor\"`\n\t\tPublicKey string `json:\"public_key\"`\n\t\tSign      string `json:\"sign\"`\n\t}\n\tif err = json.Unmarshal(res, &v); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tquery.Set(\"client_public\", hex.EncodeToString(clientPublic))\n\tquery.Set(\"client_private\", hex.EncodeToString(clientPrivate))\n\tquery.Set(\"device_public\", v.PublicKey)\n\tquery.Set(\"sign\", v.Sign)\n\tquery.Set(\"vendor\", getVendorName(v.Vendor.ID))\n\n\tif v.Vendor.ID == 1 {\n\t\tquery.Set(\"uid\", v.Vendor.Params.UID)\n\t}\n\n\turl.RawQuery = query.Encode()\n\treturn url.String(), nil\n}\n\nfunc getVendorName(i byte) string {\n\tswitch i {\n\tcase 1:\n\t\treturn \"tutk\"\n\tcase 3:\n\t\treturn \"agora\"\n\tcase 4:\n\t\treturn \"cs2\"\n\tcase 6:\n\t\treturn \"mtp\"\n\t}\n\treturn fmt.Sprintf(\"%d\", i)\n}\n\nfunc wakeUpCamera(url *url.URL) error {\n\tconst params = `{\"id\":1,\"method\":\"wakeup\",\"params\":{\"video\":\"1\"}}`\n\tdid := url.Query().Get(\"did\")\n\t_, err := cloudUserRequest(url.User, \"/home/rpc/\"+did, params)\n\treturn err\n}\n\nfunc apiXiaomi(w http.ResponseWriter, r *http.Request) {\n\tswitch r.Method {\n\tcase \"GET\":\n\t\tapiDeviceList(w, r)\n\tcase \"POST\":\n\t\tapiAuth(w, r)\n\t}\n}\n\nfunc apiDeviceList(w http.ResponseWriter, r *http.Request) {\n\tquery := r.URL.Query()\n\n\tuser := query.Get(\"id\")\n\tif user == \"\" {\n\t\tcloudsMu.Lock()\n\t\tusers := make([]string, 0, len(tokens))\n\t\tfor s := range tokens {\n\t\t\tusers = append(users, s)\n\t\t}\n\t\tcloudsMu.Unlock()\n\n\t\tapi.ResponseJSON(w, users)\n\t\treturn\n\t}\n\n\terr := func() error {\n\t\tregion := query.Get(\"region\")\n\t\tres, err := cloudRequest(user, region, \"/v2/home/device_list_page\", \"{}\")\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tvar v struct {\n\t\t\tList []*Device `json:\"list\"`\n\t\t}\n\n\t\tlog.Trace().Str(\"user\", user).Msgf(\"[xiaomi] devices list: %s\", res)\n\n\t\tif err = json.Unmarshal(res, &v); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tvar items []*api.Source\n\n\t\tfor _, device := range v.List {\n\t\t\tif !device.HasCamera() {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\titems = append(items, &api.Source{\n\t\t\t\tName: device.Name,\n\t\t\t\tInfo: fmt.Sprintf(\"ip: %s, mac: %s\", device.IP, device.MAC),\n\t\t\t\tURL:  fmt.Sprintf(\"xiaomi://%s:%s@%s?did=%s&model=%s\", user, region, device.IP, device.Did, device.Model),\n\t\t\t})\n\t\t}\n\n\t\tapi.ResponseSources(w, items)\n\t\treturn nil\n\t}()\n\n\tif err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t}\n}\n\ntype Device struct {\n\tDid   string `json:\"did\"`\n\tName  string `json:\"name\"`\n\tModel string `json:\"model\"`\n\tMAC   string `json:\"mac\"`\n\tIP    string `json:\"localip\"`\n}\n\nfunc (d *Device) HasCamera() bool {\n\treturn strings.Contains(d.Model, \".camera.\") ||\n\t\tstrings.Contains(d.Model, \".cateye.\") ||\n\t\tstrings.Contains(d.Model, \".feeder.\")\n}\n\nvar auth *xiaomi.Cloud\n\nfunc apiAuth(w http.ResponseWriter, r *http.Request) {\n\tif err := r.ParseForm(); err != nil {\n\t\thttp.Error(w, err.Error(), http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tusername := r.Form.Get(\"username\")\n\tpassword := r.Form.Get(\"password\")\n\tcaptcha := r.Form.Get(\"captcha\")\n\tverify := r.Form.Get(\"verify\")\n\n\tvar err error\n\n\tswitch {\n\tcase username != \"\" || password != \"\":\n\t\tauth = xiaomi.NewCloud(AppXiaomiHome)\n\t\terr = auth.Login(username, password)\n\tcase captcha != \"\":\n\t\terr = auth.LoginWithCaptcha(captcha)\n\tcase verify != \"\":\n\t\terr = auth.LoginWithVerify(verify)\n\tdefault:\n\t\thttp.Error(w, \"wrong request\", http.StatusBadRequest)\n\t\treturn\n\t}\n\n\tif err == nil {\n\t\tuserID, token := auth.UserToken()\n\t\tauth = nil\n\n\t\tcloudsMu.Lock()\n\t\tif tokens == nil {\n\t\t\ttokens = map[string]string{userID: token}\n\t\t} else {\n\t\t\ttokens[userID] = token\n\t\t}\n\t\tcloudsMu.Unlock()\n\n\t\terr = app.PatchConfig([]string{\"xiaomi\", userID}, token)\n\t}\n\n\tif err != nil {\n\t\tvar login *xiaomi.LoginError\n\t\tif errors.As(err, &login) {\n\t\t\tw.Header().Set(\"Content-Type\", api.MimeJSON)\n\t\t\tw.WriteHeader(http.StatusUnauthorized)\n\t\t\t_ = json.NewEncoder(w).Encode(err)\n\t\t\treturn\n\t\t}\n\n\t\thttp.Error(w, err.Error(), http.StatusInternalServerError)\n\t}\n}\n\nconst AppXiaomiHome = \"xiaomiio\"\n\nfunc GetBaseURL(region string) string {\n\tswitch region {\n\tcase \"de\", \"i2\", \"ru\", \"sg\", \"us\":\n\t\treturn \"https://\" + region + \".api.io.mi.com/app\"\n\t}\n\treturn \"https://api.io.mi.com/app\"\n}\n"
  },
  {
    "path": "internal/yandex/README.md",
    "content": "# Yandex\n\nSource for receiving stream from new [Yandex IP camera](https://alice.yandex.ru/smart-home/security/ipcamera).\n\n## Get Yandex token\n\n1. Install HomeAssistant integration [YandexStation](https://github.com/AlexxIT/YandexStation).\n2. Copy token from HomeAssistant config folder: `/config/.storage/core.config_entries`, key: `\"x_token\"`.\n\n## Get device ID\n\n1. Open this link in any browser: https://iot.quasar.yandex.ru/m/v3/user/devices\n2. Copy ID of your camera, key: `\"id\"`.\n\n## Configuration\n\n```yaml\nstreams:\n  yandex_stream: yandex:?x_token=XXXX&device_id=XXXX\n  yandex_snapshot: yandex:?x_token=XXXX&device_id=XXXX&snapshot\n  yandex_snapshot_custom_size: yandex:?x_token=XXXX&device_id=XXXX&snapshot=h=540\n```\n"
  },
  {
    "path": "internal/yandex/goloom.go",
    "content": "package yandex\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/webrtc\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\txwebrtc \"github.com/AlexxIT/go2rtc/pkg/webrtc\"\n\t\"github.com/google/uuid\"\n\t\"github.com/gorilla/websocket\"\n\tpion \"github.com/pion/webrtc/v4\"\n)\n\nfunc goloomClient(serviceURL, serviceName, roomId, participantId, credentials string) (core.Producer, error) {\n\tconn, _, err := websocket.DefaultDialer.Dial(serviceURL, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer func() {\n\t\ttime.Sleep(time.Second)\n\t\t_ = conn.Close()\n\t}()\n\n\ts := fmt.Sprintf(`{\"hello\": {\n\"credentials\":\"%s\",\"participantId\":\"%s\",\"roomId\":\"%s\",\"serviceName\":\"%s\",\"sdkInitializationId\":\"%s\",\n\"capabilitiesOffer\":{},\"sendAudio\":false,\"sendSharing\":false,\"sendVideo\":false,\n\"sdkInfo\":{\"hwConcurrency\":4,\"implementation\":\"browser\",\"version\":\"5.4.0\"},\n\"participantAttributes\":{\"description\":\"\",\"name\":\"mike\",\"role\":\"SPEAKER\"},\n\"participantMeta\":{\"description\":\"\",\"name\":\"mike\",\"role\":\"SPEAKER\",\"sendAudio\":false,\"sendVideo\":false}\n},\"uid\":\"%s\"}`,\n\t\tcredentials, participantId, roomId, serviceName,\n\t\tuuid.NewString(), uuid.NewString(),\n\t)\n\n\terr = conn.WriteMessage(websocket.TextMessage, []byte(s))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif _, _, err = conn.ReadMessage(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tpc, err := webrtc.PeerConnection(true)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tprod := xwebrtc.NewConn(pc)\n\tprod.FormatName = \"yandex\"\n\tprod.Mode = core.ModeActiveProducer\n\tprod.Protocol = \"wss\"\n\n\tvar connState core.Waiter\n\n\tprod.Listen(func(msg any) {\n\t\tswitch msg := msg.(type) {\n\t\tcase pion.PeerConnectionState:\n\t\t\tswitch msg {\n\t\t\tcase pion.PeerConnectionStateConnecting:\n\t\t\tcase pion.PeerConnectionStateConnected:\n\t\t\t\tconnState.Done(nil)\n\t\t\tdefault:\n\t\t\t\tconnState.Done(errors.New(\"webrtc: \" + msg.String()))\n\t\t\t}\n\t\t}\n\t})\n\n\tgo func() {\n\t\tfor {\n\t\t\tvar msg map[string]json.RawMessage\n\t\t\tif err = conn.ReadJSON(&msg); err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tfor k, v := range msg {\n\t\t\t\tswitch k {\n\t\t\t\tcase \"uid\":\n\t\t\t\t\tcontinue\n\t\t\t\tcase \"serverHello\":\n\t\t\t\tcase \"subscriberSdpOffer\":\n\t\t\t\t\tvar sdp subscriberSdp\n\t\t\t\t\tif err = json.Unmarshal(v, &sdp); err != nil {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\t//log.Trace().Msgf(\"offer:\\n%s\", sdp.Sdp)\n\t\t\t\t\tif err = prod.SetOffer(sdp.Sdp); err != nil {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tif sdp.Sdp, err = prod.GetAnswer(); err != nil {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\t//log.Trace().Msgf(\"answer:\\n%s\", sdp.Sdp)\n\n\t\t\t\t\tvar raw []byte\n\t\t\t\t\tif raw, err = json.Marshal(sdp); err != nil {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\ts = fmt.Sprintf(`{\"uid\":\"%s\",\"subscriberSdpAnswer\":%s}`, uuid.NewString(), raw)\n\t\t\t\t\tif err = conn.WriteMessage(websocket.TextMessage, []byte(s)); err != nil {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\tcase \"webrtcIceCandidate\":\n\t\t\t\t\tvar candidate webrtcIceCandidate\n\t\t\t\t\tif err = json.Unmarshal(v, &candidate); err != nil {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t\tif err = prod.AddCandidate(candidate.Candidate); err != nil {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t//log.Trace().Msgf(\"%s : %s\", k, v)\n\t\t\t}\n\n\t\t\tif msg[\"ack\"] != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ts = fmt.Sprintf(`{\"uid\":%s,\"ack\":{\"status\":{\"code\":\"OK\"}}}`, msg[\"uid\"])\n\t\t\tif err = conn.WriteMessage(websocket.TextMessage, []byte(s)); err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\n\tif err = connState.Wait(); err != nil {\n\t\treturn nil, err\n\t}\n\n\ts = fmt.Sprintf(`{\"uid\":\"%s\",\"setSlots\":{\"slots\":[{\"width\":0,\"height\":0}],\"audioSlotsCount\":0,\"key\":1,\"shutdownAllVideo\":false,\"withSelfView\":false,\"selfViewVisibility\":\"ON_LOADING_THEN_HIDE\",\"gridConfig\":{}}}`, uuid.NewString())\n\tif err = conn.WriteMessage(websocket.TextMessage, []byte(s)); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn prod, nil\n}\n\ntype subscriberSdp struct {\n\tPcSeq int    `json:\"pcSeq\"`\n\tSdp   string `json:\"sdp\"`\n}\n\ntype webrtcIceCandidate struct {\n\tPcSeq         int    `json:\"pcSeq\"`\n\tTarget        string `json:\"target\"`\n\tCandidate     string `json:\"candidate\"`\n\tSdpMid        string `json:\"sdpMid\"`\n\tSdpMlineIndex int    `json:\"sdpMlineIndex\"`\n}\n"
  },
  {
    "path": "internal/yandex/yandex.go",
    "content": "package yandex\n\nimport (\n\t\"net/url\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/yandex\"\n)\n\nfunc Init() {\n\tstreams.HandleFunc(\"yandex\", func(source string) (core.Producer, error) {\n\t\tu, err := url.Parse(source)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tquery := u.Query()\n\t\ttoken := query.Get(\"x_token\")\n\n\t\tsession, err := yandex.GetSession(token)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tdeviceID := query.Get(\"device_id\")\n\n\t\tif query.Has(\"snapshot\") {\n\t\t\trawURL, err := session.GetSnapshotURL(deviceID)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\trawURL += \"/current.jpg?\" + query.Get(\"snapshot\") + \"#header=Cookie:\" + session.GetCookieString(rawURL)\n\t\t\treturn streams.GetProducer(rawURL)\n\t\t}\n\n\t\troom, err := session.WebrtcCreateRoom(deviceID)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn goloomClient(room.ServiceUrl, room.ServiceName, room.RoomId, room.ParticipantId, room.Credentials)\n\t})\n}\n"
  },
  {
    "path": "main.go",
    "content": "package main\n\nimport (\n\t\"slices\"\n\n\t\"github.com/AlexxIT/go2rtc/internal/alsa\"\n\t\"github.com/AlexxIT/go2rtc/internal/api\"\n\t\"github.com/AlexxIT/go2rtc/internal/api/ws\"\n\t\"github.com/AlexxIT/go2rtc/internal/app\"\n\t\"github.com/AlexxIT/go2rtc/internal/bubble\"\n\t\"github.com/AlexxIT/go2rtc/internal/debug\"\n\t\"github.com/AlexxIT/go2rtc/internal/doorbird\"\n\t\"github.com/AlexxIT/go2rtc/internal/dvrip\"\n\t\"github.com/AlexxIT/go2rtc/internal/echo\"\n\t\"github.com/AlexxIT/go2rtc/internal/eseecloud\"\n\t\"github.com/AlexxIT/go2rtc/internal/exec\"\n\t\"github.com/AlexxIT/go2rtc/internal/expr\"\n\t\"github.com/AlexxIT/go2rtc/internal/ffmpeg\"\n\t\"github.com/AlexxIT/go2rtc/internal/flussonic\"\n\t\"github.com/AlexxIT/go2rtc/internal/gopro\"\n\t\"github.com/AlexxIT/go2rtc/internal/hass\"\n\t\"github.com/AlexxIT/go2rtc/internal/hls\"\n\t\"github.com/AlexxIT/go2rtc/internal/homekit\"\n\t\"github.com/AlexxIT/go2rtc/internal/http\"\n\t\"github.com/AlexxIT/go2rtc/internal/isapi\"\n\t\"github.com/AlexxIT/go2rtc/internal/ivideon\"\n\t\"github.com/AlexxIT/go2rtc/internal/kasa\"\n\t\"github.com/AlexxIT/go2rtc/internal/mjpeg\"\n\t\"github.com/AlexxIT/go2rtc/internal/mp4\"\n\t\"github.com/AlexxIT/go2rtc/internal/mpeg\"\n\t\"github.com/AlexxIT/go2rtc/internal/multitrans\"\n\t\"github.com/AlexxIT/go2rtc/internal/nest\"\n\t\"github.com/AlexxIT/go2rtc/internal/ngrok\"\n\t\"github.com/AlexxIT/go2rtc/internal/onvif\"\n\t\"github.com/AlexxIT/go2rtc/internal/pinggy\"\n\t\"github.com/AlexxIT/go2rtc/internal/ring\"\n\t\"github.com/AlexxIT/go2rtc/internal/roborock\"\n\t\"github.com/AlexxIT/go2rtc/internal/rtmp\"\n\t\"github.com/AlexxIT/go2rtc/internal/rtsp\"\n\t\"github.com/AlexxIT/go2rtc/internal/srtp\"\n\t\"github.com/AlexxIT/go2rtc/internal/streams\"\n\t\"github.com/AlexxIT/go2rtc/internal/tapo\"\n\t\"github.com/AlexxIT/go2rtc/internal/tuya\"\n\t\"github.com/AlexxIT/go2rtc/internal/v4l2\"\n\t\"github.com/AlexxIT/go2rtc/internal/webrtc\"\n\t\"github.com/AlexxIT/go2rtc/internal/webtorrent\"\n\t\"github.com/AlexxIT/go2rtc/internal/wyoming\"\n\t\"github.com/AlexxIT/go2rtc/internal/wyze\"\n\t\"github.com/AlexxIT/go2rtc/internal/xiaomi\"\n\t\"github.com/AlexxIT/go2rtc/internal/yandex\"\n\t\"github.com/AlexxIT/go2rtc/pkg/shell\"\n)\n\nfunc main() {\n\t// version will be set later from -buildvcs info, this used only as fallback\n\tapp.Version = \"1.9.14\"\n\n\ttype module struct {\n\t\tname string\n\t\tinit func()\n\t}\n\n\tmodules := []module{\n\t\t{\"\", app.Init},    // init config and logs\n\t\t{\"api\", api.Init}, // init API before all others\n\t\t{\"ws\", ws.Init},   // init WS API endpoint\n\t\t{\"\", streams.Init},\n\t\t// Main sources and servers\n\t\t{\"http\", http.Init},     // rtsp source, HTTP server\n\t\t{\"rtsp\", rtsp.Init},     // rtsp source, RTSP server\n\t\t{\"webrtc\", webrtc.Init}, // webrtc source, WebRTC server\n\t\t// Main API\n\t\t{\"mp4\", mp4.Init},     // MP4 API\n\t\t{\"hls\", hls.Init},     // HLS API\n\t\t{\"mjpeg\", mjpeg.Init}, // MJPEG API\n\t\t// Other sources and servers\n\t\t{\"hass\", hass.Init},             // hass source, Hass API server\n\t\t{\"homekit\", homekit.Init},       // homekit source, HomeKit server\n\t\t{\"onvif\", onvif.Init},           // onvif source, ONVIF API server\n\t\t{\"rtmp\", rtmp.Init},             // rtmp source, RTMP server\n\t\t{\"webtorrent\", webtorrent.Init}, // webtorrent source, WebTorrent module\n\t\t{\"wyoming\", wyoming.Init},\n\t\t// Exec and script sources\n\t\t{\"echo\", echo.Init},\n\t\t{\"exec\", exec.Init},\n\t\t{\"expr\", expr.Init},\n\t\t{\"ffmpeg\", ffmpeg.Init},\n\t\t// Hardware sources\n\t\t{\"alsa\", alsa.Init},\n\t\t{\"v4l2\", v4l2.Init},\n\t\t// Other sources\n\t\t{\"bubble\", bubble.Init},\n\t\t{\"doorbird\", doorbird.Init},\n\t\t{\"dvrip\", dvrip.Init},\n\t\t{\"eseecloud\", eseecloud.Init},\n\t\t{\"flussonic\", flussonic.Init},\n\t\t{\"gopro\", gopro.Init},\n\t\t{\"isapi\", isapi.Init},\n\t\t{\"ivideon\", ivideon.Init},\n\t\t{\"kasa\", kasa.Init},\n\t\t{\"mpegts\", mpeg.Init},\n\t\t{\"multitrans\", multitrans.Init},\n\t\t{\"nest\", nest.Init},\n\t\t{\"ring\", ring.Init},\n\t\t{\"roborock\", roborock.Init},\n\t\t{\"tapo\", tapo.Init},\n\t\t{\"tuya\", tuya.Init},\n\t\t{\"wyze\", wyze.Init},\n\t\t{\"xiaomi\", xiaomi.Init},\n\t\t{\"yandex\", yandex.Init},\n\t\t// Helper modules\n\t\t{\"debug\", debug.Init},\n\t\t{\"ngrok\", ngrok.Init},\n\t\t{\"pinggy\", pinggy.Init},\n\t\t{\"srtp\", srtp.Init},\n\t}\n\n\tfor _, m := range modules {\n\t\tif app.Modules == nil || m.name == \"\" || slices.Contains(app.Modules, m.name) {\n\t\t\tm.init()\n\t\t}\n\t}\n\n\tshell.RunUntilSignal()\n}\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"devDependencies\": {\n    \"@types/node\": \"^25.2.0\",\n    \"eslint\": \"^9.39.2\",\n    \"eslint-plugin-html\": \"^8.1.4\",\n    \"vitepress\": \"^2.0.0-alpha.16\"\n  },\n  \"scripts\": {\n    \"docs:dev\": \"vitepress dev website --host\",\n    \"docs:build\": \"vitepress build website\",\n    \"docs:preview\": \"vitepress preview website\"\n  },\n  \"eslintConfig\": {\n    \"env\": {\n      \"browser\": true,\n      \"es6\": true\n    },\n    \"parserOptions\": {\n      \"ecmaVersion\": 2017,\n      \"sourceType\": \"module\"\n    },\n    \"rules\": {\n      \"no-var\": \"error\",\n      \"no-undef\": \"error\",\n      \"no-unused-vars\": \"warn\",\n      \"prefer-const\": \"error\",\n      \"quotes\": [\n        \"error\",\n        \"single\"\n      ],\n      \"semi\": \"error\"\n    },\n    \"plugins\": [\n      \"html\"\n    ],\n    \"overrides\": [\n      {\n        \"files\": [\n          \"*.html\"\n        ],\n        \"parserOptions\": {\n          \"sourceType\": \"script\"\n        }\n      }\n    ]\n  }\n}\n"
  },
  {
    "path": "pkg/README.md",
    "content": "# Notes\n\ngo2rtc tries to name formats, protocols and codecs the same way they are named in FFmpeg.\nSome formats and protocols go2rtc supports exclusively. They have no equivalent in FFmpeg.\n\n## Producers (input)\n\n- The initiator of the connection can be go2rtc - **Source protocols**\n- The initiator of the connection can be an external program - **Ingress protocols**\n- Codecs can be incoming - **Receiver codecs**\n- Codecs can be outgoing (two way audio) - **Sender codecs**\n\n| Group      | Format       | Protocols       | Ingress | Receiver codecs                 | Sender codecs      | Example       |\n|------------|--------------|-----------------|---------|---------------------------------|---------------------|---------------|\n| Devices    | alsa         | pipe            |         |                                 | pcm                 | `alsa:`       |\n| Devices    | v4l2         | pipe            |         |                                 |                     | `v4l2:`       |\n| Files      | adts         | http, tcp, pipe | http    | aac                             |                     | `http:`       |\n| Files      | flv          | http, tcp, pipe | http    | h264, aac                       |                     | `http:`       |\n| Files      | h264         | http, tcp, pipe | http    | h264                            |                     | `http:`       |\n| Files      | hevc         | http, tcp, pipe | http    | hevc                            |                     | `http:`       |\n| Files      | hls          | http            |         | h264, h265, aac, opus           |                     | `http:`       |\n| Files      | mjpeg        | http, tcp, pipe | http    | mjpeg                           |                     | `http:`       |\n| Files      | mpegts       | http, tcp, pipe | http    | h264, hevc, aac, opus           |                     | `http:`       |\n| Files      | wav          | http, tcp, pipe | http    | pcm_alaw, pcm_mulaw             |                     | `http:`       |\n| Net (pub)  | mpjpeg       | http, tcp, pipe | http    | mjpeg                           |                     | `http:`       |\n| Net (pub)  | onvif        | rtsp            |         |                                 |                     | `onvif:`      |\n| Net (pub)  | rtmp         | rtmp            | rtmp    | h264, aac                       |                     | `rtmp:`       |\n| Net (pub)  | rtsp         | rtsp, ws        | rtsp    | h264, hevc, aac, pcm*, opus     | pcm*, opus          | `rtsp:`       |\n| Net (pub)  | webrtc*      | webrtc          | webrtc  | h264, pcm_alaw, pcm_mulaw, opus | pcm_alaw, pcm_mulaw | `webrtc:`     |\n| Net (pub)  | yuv4mpegpipe | http, tcp, pipe | http    | rawvideo                        |                     | `http:`       |\n| Net (priv) | bubble       | http            |         | h264, hevc, pcm_alaw            |                     | `bubble:`     |\n| Net (priv) | doorbird     | http            |         |                                 |                     | `doorbird:`   |\n| Net (priv) | dvrip        | tcp             |         | h264, hevc, pcm_alaw, pcm_mulaw | pcm_alaw            | `dvrip:`      |\n| Net (priv) | eseecloud    | http            |         |                                 |                     | `eseecloud:`  |\n| Net (priv) | gopro        | udp             |         | TODO                            |                     | `gopro:`      |\n| Net (priv) | hass         | webrtc          |         | TODO                            |                     | `hass:`       |\n| Net (priv) | homekit      | hap             |         | h264, eld*                      |                     | `homekit:`    |\n| Net (priv) | isapi        | http            |         |                                 | pcm_alaw, pcm_mulaw | `isapi:`      |\n| Net (priv) | kasa         | http            |         | h264, pcm_mulaw                 |                     | `kasa:`       |\n| Net (priv) | nest         | rtsp, webrtc    |         | TODO                            |                     | `nest:`       |\n| Net (priv) | ring         | webrtc          |         |                                 |                     | `ring:`       |\n| Net (priv) | roborock     | webrtc          |         | h264, opus                      | opus                | `roborock:`   |\n| Net (priv) | tapo         | http            |         | h264, pcma                      | pcm_alaw            | `tapo:`       |\n| Net (priv) | tuya         | webrtc          |         |                                 |                     | `tuya:`       |\n| Net (priv) | vigi         | http            |         |                                 |                     | `vigi:`       |\n| Net (priv) | webtorrent   | webrtc          | TODO    | TODO                            | TODO                | `webtorrent:` |\n| Net (priv) | xiaomi*      | cs2, tutk       |         |                                 |                     | `xiaomi:`     |\n| Services   | flussonic    | ws              |         |                                 |                     | `flussonic:`  |\n| Services   | ivideon      | ws              |         | h264                            |                     | `ivideon:`    |\n| Services   | yandex       | webrtc          |         |                                 |                     | `yandex:`     |\n| Other      | echo         | *               |         |                                 |                     | `echo:`       |\n| Other      | exec         | pipe, rtsp      |         |                                 |                     | `exec:`       |\n| Other      | expr         | *               |         |                                 |                     | `expr:`       |\n| Other      | ffmpeg       | pipe, rtsp      |         |                                 |                     | `ffmpeg:`     |\n| Other      | stdin        | pipe            |         |                                 | pcm_alaw, pcm_mulaw | `stdin:`      |\n\n- **eld** - rare variant of aac codec\n- **pcm** - pcm_alaw pcm_mulaw pcm_s16be pcm_s16le\n- **webrtc** - webrtc/kinesis, webrtc/openipc, webrtc/milestone, webrtc/wyze, webrtc/whep\n\n## Consumers (output)\n\n| Format       | Protocol | Send codecs                     | Recv codecs               | Example                               |\n|--------------|----------|---------------------------------|---------------------------|---------------------------------------|\n| adts         | http     | aac                             |                           | `GET /api/stream.adts`                |\n| ascii        | http     | mjpeg                           |                           | `GET /api/stream.ascii`               |\n| flv          | http     | h264, aac                       |                           | `GET /api/stream.flv`                 |\n| hls/mpegts   | http     | h264, hevc, aac                 |                           | `GET /api/stream.m3u8`                |\n| hls/fmp4     | http     | h264, hevc, aac, pcm*, opus     |                           | `GET /api/stream.m3u8?mp4`            |\n| homekit      | hap      | h264, opus                      |                           | Apple HomeKit app                     |\n| mjpeg        | ws       | mjpeg                           |                           | `{\"type\":\"mjpeg\"}` -> `/api/ws`       |\n| mpjpeg       | http     | mjpeg                           |                           | `GET /api/stream.mjpeg`               |\n| mp4          | http     | h264, hevc, aac, pcm*, opus     |                           | `GET /api/stream.mp4`                 |\n| mse/fmp4     | ws       | h264, hevc, aac, pcm*, opus     |                           | `{\"type\":\"mse\"}` -> `/api/ws`         |\n| mpegts       | http     | h264, hevc, aac                 |                           | `GET /api/stream.ts`                  |\n| rtmp         | rtmp     | h264, aac                       |                           | `rtmp://localhost:1935/{stream_name}` |\n| rtsp         | rtsp     | h264, hevc, aac, pcm*, opus     |                           | `rtsp://localhost:8554/{stream_name}` |\n| webrtc       | webrtc   | h264, pcm_alaw, pcm_mulaw, opus | pcm_alaw, pcm_mulaw, opus | `{\"type\":\"webrtc\"}` -> `/api/ws`      |\n| yuv4mpegpipe | http     | rawvideo                        |                           | `GET /api/stream.y4m`                 |\n\n- **pcm** - pcm_alaw pcm_mulaw pcm_s16be pcm_s16le\n\n## Snapshots\n\n| Format | Protocol | Send codecs | Example               |\n|--------|----------|-------------|-----------------------|\n| jpeg   | http     | mjpeg       | `GET /api/frame.jpeg` |\n| mp4    | http     | h264,hevc   | `GET /api/frame.mp4`  |\n\n## Developers\n\n**File naming:**\n\n- `pkg/{format}/producer.go` - producer for this format (also if support backchannel)\n- `pkg/{format}/consumer.go` - consumer for this format\n- `pkg/{format}/backchannel.go` - producer with only backchannel func\n\n**Mentioning modules:**\n\n- [`main.go`](../main.go)\n- [`README.md`](../README.md)\n- [`internal/README.md`](../internal/README.md)\n- [`website/.vitepress/config.js`](../website/.vitepress/config.js)\n- [`website/api/openapi.yaml`](../website/api/openapi.yaml)\n- [`www/schema.json`](../www/schema.json)\n\n## Useful links\n\n- https://www.wowza.com/blog/streaming-protocols\n- https://vimeo.com/blog/post/rtmp-stream/\n- https://sanjeev-pandey.medium.com/understanding-the-mpeg-4-moov-atom-pseudo-streaming-in-mp4-93935e1b9e9a\n- [Android Supported media formats](https://developer.android.com/guide/topics/media/media-formats)\n- [THEOplayer](https://www.theoplayer.com/test-your-stream-hls-dash-hesp)\n- [How Generate DTS/PTS](https://www.ramugedia.com/how-generate-dts-pts-from-elementary-stream)\n"
  },
  {
    "path": "pkg/aac/README.md",
    "content": "## AAC-LD and AAC-ELD\n\n| Codec   | Rate  | QuickTime | ffmpeg | VLC |\n|---------|-------|-----------|--------|-----|\n| AAC-LD  | 8000  | yes       | no     | no  |\n| AAC-LD  | 16000 | yes       | no     | no  |\n| AAC-LD  | 22050 | yes       | yes    | no  |\n| AAC-LD  | 24000 | yes       | yes    | no  |\n| AAC-LD  | 32000 | yes       | yes    | no  |\n| AAC-ELD | 8000  | yes       | no     | no  |\n| AAC-ELD | 16000 | yes       | no     | no  |\n| AAC-ELD | 22050 | yes       | yes    | yes |\n| AAC-ELD | 24000 | yes       | yes    | yes |\n| AAC-ELD | 32000 | yes       | yes    | yes |\n\n## Useful links\n\n- [4.6.20 Enhanced Low Delay Codec](https://csclub.uwaterloo.ca/~ehashman/ISO14496-3-2009.pdf)\n- https://stackoverflow.com/questions/40014508/aac-adts-for-aacobject-eld-packets\n- https://code.videolan.org/videolan/vlc/-/blob/master/modules/packetizer/mpeg4audio.c\n"
  },
  {
    "path": "pkg/aac/aac.go",
    "content": "package aac\n\nimport (\n\t\"encoding/hex\"\n\t\"fmt\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/bits\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\nconst (\n\tTypeAACMain = 1\n\tTypeAACLC   = 2  // Low Complexity\n\tTypeAACLD   = 23 // Low Delay (48000, 44100, 32000, 24000, 22050)\n\tTypeESCAPE  = 31\n\tTypeAACELD  = 39 // Enhanced Low Delay\n\n\tAUTime = 1024\n\n\t// FMTP streamtype=5 - audio stream\n\tFMTP = \"streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=\"\n)\n\nvar sampleRates = [16]uint32{\n\t96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000, 12000, 11025, 8000, 7350,\n\t0, 0, 0, // protection from request sampleRates[15]\n}\n\nfunc ConfigToCodec(conf []byte) *core.Codec {\n\t// https://en.wikipedia.org/wiki/MPEG-4_Part_3#MPEG-4_Audio_Object_Types\n\trd := bits.NewReader(conf)\n\n\tcodec := &core.Codec{\n\t\tFmtpLine:    FMTP + hex.EncodeToString(conf),\n\t\tPayloadType: core.PayloadTypeRAW,\n\t}\n\n\tobjType := rd.ReadBits(5)\n\tif objType == TypeESCAPE {\n\t\tobjType = 32 + rd.ReadBits(6)\n\t}\n\n\tswitch objType {\n\tcase TypeAACLC, TypeAACLD, TypeAACELD:\n\t\tcodec.Name = core.CodecAAC\n\tdefault:\n\t\tcodec.Name = fmt.Sprintf(\"AAC-%X\", objType)\n\t}\n\n\tif sampleRateIdx := rd.ReadBits8(4); sampleRateIdx < 0x0F {\n\t\tcodec.ClockRate = sampleRates[sampleRateIdx]\n\t} else {\n\t\tcodec.ClockRate = rd.ReadBits(24)\n\t}\n\n\tcodec.Channels = rd.ReadBits8(4)\n\n\treturn codec\n}\n\nfunc DecodeConfig(b []byte) (objType, sampleFreqIdx, channels byte, sampleRate uint32) {\n\trd := bits.NewReader(b)\n\n\tobjType = rd.ReadBits8(5)\n\tif objType == 0b11111 {\n\t\tobjType = 32 + rd.ReadBits8(6)\n\t}\n\n\tsampleFreqIdx = rd.ReadBits8(4)\n\tif sampleFreqIdx == 0b1111 {\n\t\tsampleRate = rd.ReadBits(24)\n\t} else {\n\t\tsampleRate = sampleRates[sampleFreqIdx]\n\t}\n\n\tchannels = rd.ReadBits8(4)\n\treturn\n}\n\nfunc EncodeConfig(objType byte, sampleRate uint32, channels byte, shortFrame bool) []byte {\n\twr := bits.NewWriter(nil)\n\n\tif objType < TypeESCAPE {\n\t\twr.WriteBits8(objType, 5)\n\t} else {\n\t\twr.WriteBits8(TypeESCAPE, 5)\n\t\twr.WriteBits8(objType-32, 6)\n\t}\n\n\ti := indexUint32(sampleRates[:], sampleRate)\n\tif i >= 0 {\n\t\twr.WriteBits8(byte(i), 4)\n\t} else {\n\t\twr.WriteBits8(0xF, 4)\n\t\twr.WriteBits(sampleRate, 24)\n\t}\n\n\twr.WriteBits8(channels, 4)\n\n\tswitch objType {\n\tcase TypeAACLD:\n\t\t// https://github.com/FFmpeg/FFmpeg/blob/67d392b97941bb51fb7af3a3c9387f5ab895fa46/libavcodec/aacdec_template.c#L841\n\t\twr.WriteBool(shortFrame)\n\t\twr.WriteBit(0)      // dependsOnCoreCoder\n\t\twr.WriteBit(0)      // extension_flag\n\t\twr.WriteBits8(0, 2) // ep_config\n\tcase TypeAACELD:\n\t\t// https://github.com/FFmpeg/FFmpeg/blob/67d392b97941bb51fb7af3a3c9387f5ab895fa46/libavcodec/aacdec_template.c#L922\n\t\twr.WriteBool(shortFrame)\n\t\twr.WriteBits8(0, 3) // res_flags\n\t\twr.WriteBit(0)      // ldSbrPresentFlag\n\t\twr.WriteBits8(0, 4) // ELDEXT_TERM\n\t\twr.WriteBits8(0, 2) // ep_config\n\t}\n\n\treturn wr.Bytes()\n}\n\nfunc indexUint32(s []uint32, v uint32) int {\n\tfor i := range s {\n\t\tif v == s[i] {\n\t\t\treturn i\n\t\t}\n\t}\n\treturn -1\n}\n"
  },
  {
    "path": "pkg/aac/aac_test.go",
    "content": "package aac\n\nimport (\n\t\"encoding/hex\"\n\t\"testing\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestConfigToCodec(t *testing.T) {\n\ts := \"profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=F8EC3000\"\n\ts = core.Between(s, \"config=\", \";\")\n\tsrc, err := hex.DecodeString(s)\n\trequire.Nil(t, err)\n\n\tcodec := ConfigToCodec(src)\n\trequire.Equal(t, core.CodecAAC, codec.Name)\n\trequire.Equal(t, uint32(24000), codec.ClockRate)\n\trequire.Equal(t, uint16(1), codec.Channels)\n\n\tdst := EncodeConfig(TypeAACELD, 24000, 1, true)\n\trequire.Equal(t, src, dst)\n}\n\nfunc TestADTS(t *testing.T) {\n\t// FFmpeg MPEG-TS AAC (one packet)\n\ts := \"fff15080021ffc210049900219002380fff15080021ffc212049900219002380\" //...\n\tsrc, err := hex.DecodeString(s)\n\trequire.Nil(t, err)\n\n\tcodec := ADTSToCodec(src)\n\trequire.Equal(t, uint32(44100), codec.ClockRate)\n\trequire.Equal(t, uint16(2), codec.Channels)\n\n\tsize := ReadADTSSize(src)\n\trequire.Equal(t, uint16(16), size)\n\n\tdst := CodecToADTS(codec)\n\tWriteADTSSize(dst, size)\n\n\trequire.Equal(t, src[:len(dst)], dst)\n}\n\nfunc TestEncodeConfig(t *testing.T) {\n\tconf := EncodeConfig(TypeAACLC, 48000, 1, false)\n\trequire.Equal(t, \"1188\", hex.EncodeToString(conf))\n\tconf = EncodeConfig(TypeAACLC, 16000, 1, false)\n\trequire.Equal(t, \"1408\", hex.EncodeToString(conf))\n\tconf = EncodeConfig(TypeAACLC, 8000, 1, false)\n\trequire.Equal(t, \"1588\", hex.EncodeToString(conf))\n}\n"
  },
  {
    "path": "pkg/aac/adts.go",
    "content": "package aac\n\nimport (\n\t\"encoding/hex\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/bits\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/pion/rtp\"\n)\n\nconst ADTSHeaderSize = 7\n\nfunc ADTSHeaderLen(b []byte) int {\n\tif HasCRC(b) {\n\t\treturn 9 // 7 bytes header + 2 bytes CRC\n\t}\n\treturn ADTSHeaderSize\n}\n\nfunc IsADTS(b []byte) bool {\n\t// AAAAAAAA AAAABCCD EEFFFFGH HHIJKLMM MMMMMMMM MMMOOOOO OOOOOOPP (QQQQQQQQ QQQQQQQQ)\n\t// A\t12\tSyncword, all bits must be set to 1.\n\t// C\t2\tLayer, always set to 0.\n\treturn len(b) >= ADTSHeaderSize && b[0] == 0xFF && b[1]&0b1111_0110 == 0xF0\n}\n\nfunc HasCRC(b []byte) bool {\n\t// AAAAAAAA AAAABCCD EEFFFFGH HHIJKLMM MMMMMMMM MMMOOOOO OOOOOOPP (QQQQQQQQ QQQQQQQQ)\n\t// D\t1\tProtection absence, set to 1 if there is no CRC and 0 if there is CRC.\n\treturn b[1]&0b1 == 0\n}\n\nfunc ADTSToCodec(b []byte) *core.Codec {\n\t// 1. Check ADTS header\n\tif !IsADTS(b) {\n\t\treturn nil\n\t}\n\n\t// 2. Decode ADTS params\n\t// https://wiki.multimedia.cx/index.php/ADTS\n\trd := bits.NewReader(b)\n\t_ = rd.ReadBits(12)              // Syncword, all bits must be set to 1\n\t_ = rd.ReadBit()                 // MPEG Version, set to 0 for MPEG-4 and 1 for MPEG-2\n\t_ = rd.ReadBits(2)               // Layer, always set to 0\n\t_ = rd.ReadBit()                 // Protection absence, set to 1 if there is no CRC and 0 if there is CRC\n\tobjType := rd.ReadBits8(2) + 1   // Profile, the MPEG-4 Audio Object Type minus 1\n\tsampleRateIdx := rd.ReadBits8(4) // MPEG-4 Sampling Frequency Index\n\t_ = rd.ReadBit()                 // Private bit, guaranteed never to be used by MPEG, set to 0 when encoding, ignore when decoding\n\tchannels := rd.ReadBits8(3)      // MPEG-4 Channel Configuration\n\n\t//_ = rd.ReadBit()    // Originality, set to 1 to signal originality of the audio and 0 otherwise\n\t//_ = rd.ReadBit()    // Home, set to 1 to signal home usage of the audio and 0 otherwise\n\t//_ = rd.ReadBit()    // Copyright ID bit\n\t//_ = rd.ReadBit()    // Copyright ID start\n\t//_ = rd.ReadBits(13) // Frame length\n\t//_ = rd.ReadBits(11) // Buffer fullness\n\t//_ = rd.ReadBits(2)  // Number of AAC frames (Raw Data Blocks) in ADTS frame minus 1\n\t//_ = rd.ReadBits(16) // CRC check\n\n\t// 3. Encode RTP config\n\twr := bits.NewWriter(nil)\n\twr.WriteBits8(objType, 5)\n\twr.WriteBits8(sampleRateIdx, 4)\n\twr.WriteBits8(channels, 4)\n\tconf := wr.Bytes()\n\n\tcodec := &core.Codec{\n\t\tName:      core.CodecAAC,\n\t\tClockRate: sampleRates[sampleRateIdx],\n\t\tChannels:  channels,\n\t\tFmtpLine:  FMTP + hex.EncodeToString(conf),\n\t}\n\treturn codec\n}\n\nfunc ReadADTSSize(b []byte) uint16 {\n\t// AAAAAAAA AAAABCCD EEFFFFGH HHIJKLMM MMMMMMMM MMMOOOOO OOOOOOPP (QQQQQQQQ QQQQQQQQ)\n\t_ = b[5] // bounds\n\treturn uint16(b[3]&0b11)<<11 | uint16(b[4])<<3 | uint16(b[5]>>5)\n}\n\nfunc WriteADTSSize(b []byte, size uint16) {\n\t// AAAAAAAA AAAABCCD EEFFFFGH HHIJKLMM MMMMMMMM MMMOOOOO OOOOOOPP (QQQQQQQQ QQQQQQQQ)\n\t_ = b[5] // bounds\n\tb[3] |= byte(size >> (8 + 3))\n\tb[4] = byte(size >> 3)\n\tb[5] |= byte(size << 5)\n\treturn\n}\n\nfunc ADTSTimeSize(b []byte) uint32 {\n\tvar units uint32\n\tfor len(b) > ADTSHeaderSize {\n\t\tauSize := ReadADTSSize(b)\n\t\tb = b[auSize:]\n\t\tunits++\n\t}\n\treturn units * AUTime\n}\n\nfunc CodecToADTS(codec *core.Codec) []byte {\n\ts := core.Between(codec.FmtpLine, \"config=\", \";\")\n\tconf, err := hex.DecodeString(s)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tobjType, sampleFreqIdx, channels, _ := DecodeConfig(conf)\n\tprofile := objType - 1\n\n\twr := bits.NewWriter(nil)\n\twr.WriteAllBits(1, 12)          // Syncword, all bits must be set to 1\n\twr.WriteBit(0)                  // MPEG Version, set to 0 for MPEG-4 and 1 for MPEG-2\n\twr.WriteBits8(0, 2)             // Layer, always set to 0\n\twr.WriteBit(1)                  // Protection absence, set to 1 if there is no CRC and 0 if there is CRC\n\twr.WriteBits8(profile, 2)       // Profile, the MPEG-4 Audio Object Type minus 1\n\twr.WriteBits8(sampleFreqIdx, 4) // MPEG-4 Sampling Frequency Index\n\twr.WriteBit(0)                  // Private bit, guaranteed never to be used by MPEG, set to 0 when encoding, ignore when decoding\n\twr.WriteBits8(channels, 3)      // MPEG-4 Channel Configuration\n\twr.WriteBit(0)                  // Originality, set to 1 to signal originality of the audio and 0 otherwise\n\twr.WriteBit(0)                  // Home, set to 1 to signal home usage of the audio and 0 otherwise\n\twr.WriteBit(0)                  // Copyright ID bit\n\twr.WriteBit(0)                  // Copyright ID start\n\twr.WriteBits16(0, 13)           // Frame length\n\twr.WriteAllBits(1, 11)          // Buffer fullness (variable bitrate)\n\twr.WriteBits8(0, 2)             // Number of AAC frames (Raw Data Blocks) in ADTS frame minus 1\n\n\treturn wr.Bytes()\n}\n\nfunc EncodeToADTS(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {\n\tadts := CodecToADTS(codec)\n\n\treturn func(packet *rtp.Packet) {\n\t\tif !IsADTS(packet.Payload) {\n\t\t\tb := make([]byte, ADTSHeaderSize+len(packet.Payload))\n\t\t\tcopy(b, adts)\n\t\t\tcopy(b[ADTSHeaderSize:], packet.Payload)\n\t\t\tWriteADTSSize(b, uint16(len(b)))\n\n\t\t\tclone := *packet\n\t\t\tclone.Payload = b\n\t\t\thandler(&clone)\n\t\t} else {\n\t\t\thandler(packet)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/aac/consumer.go",
    "content": "package aac\n\nimport (\n\t\"io\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Consumer struct {\n\tcore.Connection\n\twr *core.WriteBuffer\n}\n\nfunc NewConsumer() *Consumer {\n\tmedias := []*core.Media{\n\t\t{\n\t\t\tKind:      core.KindAudio,\n\t\t\tDirection: core.DirectionSendonly,\n\t\t\tCodecs: []*core.Codec{\n\t\t\t\t{Name: core.CodecAAC},\n\t\t\t},\n\t\t},\n\t}\n\twr := core.NewWriteBuffer(nil)\n\treturn &Consumer{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"adts\",\n\t\t\tMedias:     medias,\n\t\t\tTransport:  wr,\n\t\t},\n\t\twr: wr,\n\t}\n}\n\nfunc (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {\n\tsender := core.NewSender(media, track.Codec)\n\n\tsender.Handler = func(pkt *rtp.Packet) {\n\t\tif n, err := c.wr.Write(pkt.Payload); err == nil {\n\t\t\tc.Send += n\n\t\t}\n\t}\n\n\tif track.Codec.IsRTP() {\n\t\tsender.Handler = RTPToADTS(track.Codec, sender.Handler)\n\t} else {\n\t\tsender.Handler = EncodeToADTS(track.Codec, sender.Handler)\n\t}\n\n\tsender.HandleRTP(track)\n\tc.Senders = append(c.Senders, sender)\n\treturn nil\n}\n\nfunc (c *Consumer) WriteTo(wr io.Writer) (int64, error) {\n\treturn c.wr.WriteTo(wr)\n}\n"
  },
  {
    "path": "pkg/aac/producer.go",
    "content": "package aac\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"io\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Producer struct {\n\tcore.Connection\n\trd *bufio.Reader\n}\n\nfunc Open(r io.Reader) (*Producer, error) {\n\trd := bufio.NewReader(r)\n\n\tb, err := rd.Peek(ADTSHeaderSize)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcodec := ADTSToCodec(b)\n\tif codec == nil {\n\t\treturn nil, errors.New(\"adts: wrong header\")\n\t}\n\tcodec.PayloadType = core.PayloadTypeRAW\n\n\tmedias := []*core.Media{\n\t\t{\n\t\t\tKind:      core.KindAudio,\n\t\t\tDirection: core.DirectionRecvonly,\n\t\t\tCodecs:    []*core.Codec{codec},\n\t\t},\n\t}\n\treturn &Producer{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"adts\",\n\t\t\tMedias:     medias,\n\t\t\tTransport:  r,\n\t\t},\n\t\trd: rd,\n\t}, nil\n}\n\nfunc (c *Producer) Start() error {\n\tfor {\n\t\t// read ADTS header\n\t\tadts := make([]byte, ADTSHeaderSize)\n\t\tif _, err := io.ReadFull(c.rd, adts); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tauSize := ReadADTSSize(adts) - ADTSHeaderSize\n\n\t\tif HasCRC(adts) {\n\t\t\t// skip CRC after header\n\t\t\tif _, err := c.rd.Discard(2); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tauSize -= 2\n\t\t}\n\n\t\t// read AAC payload after header\n\t\tpayload := make([]byte, auSize)\n\t\tif _, err := io.ReadFull(c.rd, payload); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tc.Recv += int(auSize)\n\n\t\tif len(c.Receivers) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tpkt := &rtp.Packet{\n\t\t\tHeader:  rtp.Header{Timestamp: core.Now90000()},\n\t\t\tPayload: payload,\n\t\t}\n\t\tc.Receivers[0].WriteRTP(pkt)\n\t}\n}\n"
  },
  {
    "path": "pkg/aac/rtp.go",
    "content": "package aac\n\nimport (\n\t\"encoding/binary\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/pion/rtp\"\n)\n\nconst RTPPacketVersionAAC = 0\n\nfunc RTPDepay(handler core.HandlerFunc) core.HandlerFunc {\n\tvar timestamp uint32\n\n\treturn func(packet *rtp.Packet) {\n\t\t// support ONLY 2 bytes header size!\n\t\t// streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408\n\t\t// https://datatracker.ietf.org/doc/html/rfc3640\n\t\theadersSize := binary.BigEndian.Uint16(packet.Payload) >> 3\n\n\t\t//log.Printf(\"[RTP/AAC] units: %d, size: %4d, ts: %10d, %t\", headersSize/2, len(packet.Payload), packet.Timestamp, packet.Marker)\n\n\t\tif len(packet.Payload) < int(2+headersSize) {\n\t\t\t// In very rare cases noname cameras may send data not according to the standard\n\t\t\t// https://github.com/AlexxIT/go2rtc/issues/1328\n\t\t\tif IsADTS(packet.Payload) {\n\t\t\t\tclone := *packet\n\t\t\t\tclone.Version = RTPPacketVersionAAC\n\t\t\t\tclone.Timestamp = timestamp\n\t\t\t\tclone.Payload = clone.Payload[ADTSHeaderSize:]\n\t\t\t\thandler(&clone)\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\theaders := packet.Payload[2 : 2+headersSize]\n\t\tunits := packet.Payload[2+headersSize:]\n\n\t\tfor len(headers) >= 2 {\n\t\t\tunitSize := binary.BigEndian.Uint16(headers) >> 3\n\n\t\t\tif len(units) < int(unitSize) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tunit := units[:unitSize]\n\n\t\t\theaders = headers[2:]\n\t\t\tunits = units[unitSize:]\n\n\t\t\ttimestamp += AUTime\n\n\t\t\tclone := *packet\n\t\t\tclone.Version = RTPPacketVersionAAC\n\t\t\tclone.Timestamp = timestamp\n\t\t\tif IsADTS(unit) {\n\t\t\t\tclone.Payload = unit[ADTSHeaderSize:]\n\t\t\t} else {\n\t\t\t\tclone.Payload = unit\n\t\t\t}\n\t\t\thandler(&clone)\n\t\t}\n\t}\n}\n\nfunc RTPPay(handler core.HandlerFunc) core.HandlerFunc {\n\tvar seq uint16\n\tvar ts uint32\n\n\treturn func(packet *rtp.Packet) {\n\t\tif packet.Version != RTPPacketVersionAAC {\n\t\t\thandler(packet)\n\t\t\treturn\n\t\t}\n\n\t\t// support ONLY one unit in payload\n\t\tauSize := uint16(len(packet.Payload))\n\t\t// 2 bytes header size + 2 bytes first payload size\n\t\tpayload := make([]byte, 2+2+auSize)\n\t\tpayload[1] = 16 // header size in bits\n\t\tbinary.BigEndian.PutUint16(payload[2:], auSize<<3)\n\t\tcopy(payload[4:], packet.Payload)\n\n\t\tclone := rtp.Packet{\n\t\t\tHeader: rtp.Header{\n\t\t\t\tVersion:        2,\n\t\t\t\tMarker:         true,\n\t\t\t\tSequenceNumber: seq,\n\t\t\t\tTimestamp:      ts,\n\t\t\t},\n\t\t\tPayload: payload,\n\t\t}\n\t\thandler(&clone)\n\n\t\tseq++\n\t\tts += AUTime\n\t}\n}\n\nfunc ADTStoRTP(src []byte) (dst []byte) {\n\tdst = make([]byte, 2) // header bytes\n\tfor i, n := 0, len(src)-ADTSHeaderSize; i < n; {\n\t\tauSize := ReadADTSSize(src[i:])\n\t\tdst = append(dst, byte(auSize>>5), byte(auSize<<3)) // size in bits\n\t\ti += int(auSize)\n\t}\n\thdrSize := uint16(len(dst) - 2)\n\tbinary.BigEndian.PutUint16(dst, hdrSize<<3) // size in bits\n\treturn append(dst, src...)\n}\n\nfunc RTPTimeSize(b []byte) uint32 {\n\t// convert RTP header size to units count\n\tunits := binary.BigEndian.Uint16(b) >> 4\n\treturn uint32(units) * AUTime\n}\n\nfunc RTPToADTS(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {\n\tadts := CodecToADTS(codec)\n\n\treturn func(packet *rtp.Packet) {\n\t\tsrc := packet.Payload\n\t\tdst := make([]byte, 0, len(src))\n\n\t\theadersSize := binary.BigEndian.Uint16(src) >> 3\n\t\theaders := src[2 : 2+headersSize]\n\t\tunits := src[2+headersSize:]\n\n\t\tfor len(headers) > 0 {\n\t\t\tunitSize := binary.BigEndian.Uint16(headers) >> 3\n\t\t\theaders = headers[2:]\n\t\t\tunit := units[:unitSize]\n\t\t\tunits = units[unitSize:]\n\n\t\t\tif !IsADTS(unit) {\n\t\t\t\ti := len(dst)\n\t\t\t\tdst = append(dst, adts...)\n\t\t\t\tWriteADTSSize(dst[i:], ADTSHeaderSize+uint16(len(unit)))\n\t\t\t}\n\n\t\t\tdst = append(dst, unit...)\n\t\t}\n\n\t\tclone := *packet\n\t\tclone.Version = RTPPacketVersionAAC\n\t\tclone.Payload = dst\n\t\thandler(&clone)\n\t}\n}\n\nfunc RTPToCodec(b []byte) *core.Codec {\n\thdrSize := binary.BigEndian.Uint16(b) / 8\n\treturn ADTSToCodec(b[2+hdrSize:])\n}\n"
  },
  {
    "path": "pkg/aac/rtp_test.go",
    "content": "package aac\n\nimport (\n\t\"encoding/hex\"\n\t\"testing\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/pion/rtp\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestBuggy_RTSP_AAC(t *testing.T) {\n\t// https: //github.com/AlexxIT/go2rtc/issues/1328\n\tpayload, _ := hex.DecodeString(\"fff16080431ffc211ad4458aa309a1c0a8761a230502b7c74b2b5499252a010555e32e460128303c8ace4fd3260d654a424f7e7c65eddc96735fc6f1ac0edf94fdefa0e0bd6370da1c07b9c0e77a9d6e86b196a1ac7439dcafadcffcf6d89f60ac67f8884868e931383ad3e40cf5495470d1f606ef6f7624d285b951ebfa0e42641ab98f1371182b237d14f1bd16ad714fa2f1c6a7d23ebde7a0e34a2eca156a608a4caec49d9dca4b6fe2a09e9cdbf762c5b4148a3914abb7959c991228b0837b5988334b9fc18b8fac689b5ca1e4661573bbb8b253a86cae7ec14ace49969a9a76fd571ab6e650764cb59114d61dcedf07ac61b39e4ac66adebfd0d0ab45d518dd3c161049823f150864d977cf0855172ac8482e4b25fe911325d19617558c5405af74aff5492e4599bee53f2dbdf0503730af37078550f84c956b7ee89aae83c154fa2fa6e6792c5ddd5cd5cf6bb96bf055fee7f93bed59ffb039daee5ea7e5593cb194e9091e417c67d8f73026a6a6ae056e808f7c65c03d1b9197d3709ceb63bc7b979f7ba71df5e7c6395d99d6ea229000a6bc16fb4346d6b27d32f5d8d1200736d9366d59c0c9547210813b602473da9c46f9015bbb37594c1dd90cd6a36e96bd5d6a1445ab93c9e65505ec2c722bb4cc27a10600139a48c83594dde145253c386f6627d8c6e5102fe3828a590c709bc87f55b37e97d1ae72b017b09c6bb2c13299817bb45cc67318e10b6822075b97c6a03ec1c0\")\n\tpacket := &rtp.Packet{\n\t\tHeader: rtp.Header{\n\t\t\tVersion:        2,\n\t\t\tMarker:         true,\n\t\t\tSequenceNumber: 36944,\n\t\t\tTimestamp:      4217191328,\n\t\t\tSSRC:           12892774,\n\t\t},\n\t\tPayload: payload,\n\t}\n\n\tvar size int\n\n\tRTPDepay(func(packet *core.Packet) {\n\t\tsize = len(packet.Payload)\n\t})(packet)\n\n\trequire.Equal(t, len(payload), size+ADTSHeaderSize)\n}\n"
  },
  {
    "path": "pkg/alsa/README.md",
    "content": "## Build\n\n```shell\nx86_64-linux-gnu-gcc -w -static asound_arch.c -o asound_amd64\ni686-linux-gnu-gcc -w -static asound_arch.c -o asound_i386\naarch64-linux-gnu-gcc -w -static asound_arch.c -o asound_arm64\narm-linux-gnueabihf-gcc -w -static asound_arch.c -o asound_arm\nmipsel-linux-gnu-gcc -w -static asound_arch.c -o asound_mipsle -D_TIME_BITS=32\n```\n\n## Useful links\n\n- https://github.com/torvalds/linux/blob/master/include/uapi/sound/asound.h\n- https://github.com/yobert/alsa\n- https://github.com/Narsil/alsa-go\n- https://github.com/alsa-project/alsa-lib\n- https://github.com/anisse/alsa\n- https://github.com/tinyalsa/tinyalsa\n\n**Broken pipe**\n\n- https://stackoverflow.com/questions/26545139/alsa-cannot-recovery-from-underrun-prepare-failed-broken-pipe\n- https://klipspringer.avadeaux.net/alsa-broken-pipe-errors/\n"
  },
  {
    "path": "pkg/alsa/capture_linux.go",
    "content": "package alsa\n\nimport (\n\t\"github.com/AlexxIT/go2rtc/pkg/alsa/device\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/pcm\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Capture struct {\n\tcore.Connection\n\tdev    *device.Device\n\tclosed core.Waiter\n}\n\nfunc newCapture(dev *device.Device) (*Capture, error) {\n\tmedias := []*core.Media{\n\t\t{\n\t\t\tKind:      core.KindAudio,\n\t\t\tDirection: core.DirectionRecvonly,\n\t\t\tCodecs: []*core.Codec{\n\t\t\t\t{Name: core.CodecPCML, ClockRate: 16000},\n\t\t\t},\n\t\t},\n\t}\n\treturn &Capture{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"alsa\",\n\t\t\tMedias:     medias,\n\t\t\tTransport:  dev,\n\t\t},\n\t\tdev: dev,\n\t}, nil\n}\n\nfunc (c *Capture) Start() error {\n\tdst := c.Medias[0].Codecs[0]\n\tsrc := &core.Codec{\n\t\tName:      dst.Name,\n\t\tClockRate: c.dev.GetRateNear(dst.ClockRate),\n\t\tChannels:  c.dev.GetChannelsNear(dst.Channels),\n\t}\n\n\tif err := c.dev.SetHWParams(device.SNDRV_PCM_FORMAT_S16_LE, src.ClockRate, src.Channels); err != nil {\n\t\treturn err\n\t}\n\n\ttranscode := transcodeFunc(dst, src)\n\tframeBytes := int(pcm.BytesPerFrame(src))\n\n\tvar ts uint32\n\n\t// readBufferSize for 20ms interval\n\treadBufferSize := 20 * frameBytes * int(src.ClockRate) / 1000\n\tb := make([]byte, readBufferSize)\n\tfor {\n\t\tn, err := c.dev.Read(b)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tc.Recv += n\n\n\t\tif len(c.Receivers) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tpkt := &rtp.Packet{\n\t\t\tHeader: rtp.Header{\n\t\t\t\tVersion:   2,\n\t\t\t\tMarker:    true,\n\t\t\t\tTimestamp: ts,\n\t\t\t},\n\t\t\tPayload: transcode(b[:n]),\n\t\t}\n\t\tc.Receivers[0].WriteRTP(pkt)\n\n\t\tts += uint32(n / frameBytes)\n\t}\n}\n\nfunc transcodeFunc(dst, src *core.Codec) func([]byte) []byte {\n\tif dst.ClockRate == src.ClockRate && dst.Channels == src.Channels {\n\t\treturn func(b []byte) []byte {\n\t\t\treturn b\n\t\t}\n\t}\n\treturn pcm.Transcode(dst, src)\n}\n"
  },
  {
    "path": "pkg/alsa/device/asound_32bit.go",
    "content": "//go:build 386 || arm\n\npackage device\n\ntype unsigned_char = byte\ntype signed_int = int32\ntype unsigned_int = uint32\ntype signed_long = int64\ntype unsigned_long = uint64\ntype __u32 = uint32\ntype void__user = uintptr\n\nconst (\n\tSNDRV_PCM_STREAM_PLAYBACK = 0\n\tSNDRV_PCM_STREAM_CAPTURE  = 1\n\n\tSNDRV_PCM_ACCESS_MMAP_INTERLEAVED    = 0\n\tSNDRV_PCM_ACCESS_MMAP_NONINTERLEAVED = 1\n\tSNDRV_PCM_ACCESS_MMAP_COMPLEX        = 2\n\tSNDRV_PCM_ACCESS_RW_INTERLEAVED      = 3\n\tSNDRV_PCM_ACCESS_RW_NONINTERLEAVED   = 4\n\n\tSNDRV_PCM_FORMAT_S8         = 0\n\tSNDRV_PCM_FORMAT_U8         = 1\n\tSNDRV_PCM_FORMAT_S16_LE     = 2\n\tSNDRV_PCM_FORMAT_S16_BE     = 3\n\tSNDRV_PCM_FORMAT_U16_LE     = 4\n\tSNDRV_PCM_FORMAT_U16_BE     = 5\n\tSNDRV_PCM_FORMAT_S24_LE     = 6\n\tSNDRV_PCM_FORMAT_S24_BE     = 7\n\tSNDRV_PCM_FORMAT_U24_LE     = 8\n\tSNDRV_PCM_FORMAT_U24_BE     = 9\n\tSNDRV_PCM_FORMAT_S32_LE     = 10\n\tSNDRV_PCM_FORMAT_S32_BE     = 11\n\tSNDRV_PCM_FORMAT_U32_LE     = 12\n\tSNDRV_PCM_FORMAT_U32_BE     = 13\n\tSNDRV_PCM_FORMAT_FLOAT_LE   = 14\n\tSNDRV_PCM_FORMAT_FLOAT_BE   = 15\n\tSNDRV_PCM_FORMAT_FLOAT64_LE = 16\n\tSNDRV_PCM_FORMAT_FLOAT64_BE = 17\n\tSNDRV_PCM_FORMAT_MU_LAW     = 20\n\tSNDRV_PCM_FORMAT_A_LAW      = 21\n\tSNDRV_PCM_FORMAT_MPEG       = 23\n\n\tSNDRV_PCM_IOCTL_PVERSION      = 0x80044100\n\tSNDRV_PCM_IOCTL_INFO          = 0x81204101\n\tSNDRV_PCM_IOCTL_HW_REFINE     = 0xc25c4110\n\tSNDRV_PCM_IOCTL_HW_PARAMS     = 0xc25c4111\n\tSNDRV_PCM_IOCTL_SW_PARAMS     = 0xc0684113\n\tSNDRV_PCM_IOCTL_PREPARE       = 0x00004140\n\tSNDRV_PCM_IOCTL_WRITEI_FRAMES = 0x400c4150\n\tSNDRV_PCM_IOCTL_READI_FRAMES  = 0x800c4151\n)\n\ntype snd_pcm_info struct { // size 288\n\tdevice           unsigned_int      // offset 0, size 4\n\tsubdevice        unsigned_int      // offset 4, size 4\n\tstream           signed_int        // offset 8, size 4\n\tcard             signed_int        // offset 12, size 4\n\tid               [64]unsigned_char // offset 16, size 64\n\tname             [80]unsigned_char // offset 80, size 80\n\tsubname          [32]unsigned_char // offset 160, size 32\n\tdev_class        signed_int        // offset 192, size 4\n\tdev_subclass     signed_int        // offset 196, size 4\n\tsubdevices_count unsigned_int      // offset 200, size 4\n\tsubdevices_avail unsigned_int      // offset 204, size 4\n\tpad1             [16]unsigned_char\n\treserved         [64]unsigned_char // offset 224, size 64\n}\n\ntype snd_pcm_uframes_t = unsigned_long\ntype snd_pcm_sframes_t = signed_long\n\ntype snd_xferi struct { // size 12\n\tresult snd_pcm_sframes_t // offset 0, size 4\n\tbuf    void__user        // offset 4, size 4\n\tframes snd_pcm_uframes_t // offset 8, size 4\n}\n\nconst (\n\tSNDRV_PCM_HW_PARAM_ACCESS     = 0\n\tSNDRV_PCM_HW_PARAM_FORMAT     = 1\n\tSNDRV_PCM_HW_PARAM_SUBFORMAT  = 2\n\tSNDRV_PCM_HW_PARAM_FIRST_MASK = 0\n\tSNDRV_PCM_HW_PARAM_LAST_MASK  = 2\n\n\tSNDRV_PCM_HW_PARAM_SAMPLE_BITS    = 8\n\tSNDRV_PCM_HW_PARAM_FRAME_BITS     = 9\n\tSNDRV_PCM_HW_PARAM_CHANNELS       = 10\n\tSNDRV_PCM_HW_PARAM_RATE           = 11\n\tSNDRV_PCM_HW_PARAM_PERIOD_TIME    = 12\n\tSNDRV_PCM_HW_PARAM_PERIOD_SIZE    = 13\n\tSNDRV_PCM_HW_PARAM_PERIOD_BYTES   = 14\n\tSNDRV_PCM_HW_PARAM_PERIODS        = 15\n\tSNDRV_PCM_HW_PARAM_BUFFER_TIME    = 16\n\tSNDRV_PCM_HW_PARAM_BUFFER_SIZE    = 17\n\tSNDRV_PCM_HW_PARAM_BUFFER_BYTES   = 18\n\tSNDRV_PCM_HW_PARAM_TICK_TIME      = 19\n\tSNDRV_PCM_HW_PARAM_FIRST_INTERVAL = 8\n\tSNDRV_PCM_HW_PARAM_LAST_INTERVAL  = 19\n\n\tSNDRV_MASK_MAX = 256\n\n\tSNDRV_PCM_TSTAMP_NONE   = 0\n\tSNDRV_PCM_TSTAMP_ENABLE = 1\n)\n\ntype snd_mask struct { // size 32\n\tbits [(SNDRV_MASK_MAX + 31) / 32]__u32 // offset 0, size 32\n}\n\ntype snd_interval struct { // size 12\n\tmin unsigned_int // offset 0, size 4\n\tmax unsigned_int // offset 4, size 4\n\tbit unsigned_int\n}\n\ntype snd_pcm_hw_params struct { // size 604\n\tflags     unsigned_int                                                                           // offset 0, size 4\n\tmasks     [SNDRV_PCM_HW_PARAM_LAST_MASK - SNDRV_PCM_HW_PARAM_FIRST_MASK + 1]snd_mask             // offset 4, size 96\n\tmres      [5]snd_mask                                                                            // offset 100, size 160\n\tintervals [SNDRV_PCM_HW_PARAM_LAST_INTERVAL - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL + 1]snd_interval // offset 260, size 144\n\tires      [9]snd_interval                                                                        // offset 404, size 108\n\trmask     unsigned_int                                                                           // offset 512, size 4\n\tcmask     unsigned_int                                                                           // offset 516, size 4\n\tinfo      unsigned_int                                                                           // offset 520, size 4\n\tmsbits    unsigned_int                                                                           // offset 524, size 4\n\trate_num  unsigned_int                                                                           // offset 528, size 4\n\trate_den  unsigned_int                                                                           // offset 532, size 4\n\tfifo_size snd_pcm_uframes_t                                                                      // offset 536, size 4\n\treserved  [64]unsigned_char                                                                      // offset 540, size 64\n}\n\ntype snd_pcm_sw_params struct { // size 104\n\ttstamp_mode       signed_int        // offset 0, size 4\n\tperiod_step       unsigned_int      // offset 4, size 4\n\tsleep_min         unsigned_int      // offset 8, size 4\n\tavail_min         snd_pcm_uframes_t // offset 12, size 4\n\txfer_align        snd_pcm_uframes_t // offset 16, size 4\n\tstart_threshold   snd_pcm_uframes_t // offset 20, size 4\n\tstop_threshold    snd_pcm_uframes_t // offset 24, size 4\n\tsilence_threshold snd_pcm_uframes_t // offset 28, size 4\n\tsilence_size      snd_pcm_uframes_t // offset 32, size 4\n\tboundary          snd_pcm_uframes_t // offset 36, size 4\n\tproto             unsigned_int      // offset 40, size 4\n\ttstamp_type       unsigned_int      // offset 44, size 4\n\treserved          [56]unsigned_char // offset 48, size 56\n}\n"
  },
  {
    "path": "pkg/alsa/device/asound_64bit.go",
    "content": "//go:build amd64 || arm64\n\npackage device\n\ntype unsigned_char = byte\ntype signed_int = int32\ntype unsigned_int = uint32\ntype signed_long = int64\ntype unsigned_long = uint64\ntype __u32 = uint32\ntype void__user = uintptr\n\nconst (\n\tSNDRV_PCM_STREAM_PLAYBACK = 0\n\tSNDRV_PCM_STREAM_CAPTURE  = 1\n\n\tSNDRV_PCM_ACCESS_MMAP_INTERLEAVED    = 0\n\tSNDRV_PCM_ACCESS_MMAP_NONINTERLEAVED = 1\n\tSNDRV_PCM_ACCESS_MMAP_COMPLEX        = 2\n\tSNDRV_PCM_ACCESS_RW_INTERLEAVED      = 3\n\tSNDRV_PCM_ACCESS_RW_NONINTERLEAVED   = 4\n\n\tSNDRV_PCM_FORMAT_S8         = 0\n\tSNDRV_PCM_FORMAT_U8         = 1\n\tSNDRV_PCM_FORMAT_S16_LE     = 2\n\tSNDRV_PCM_FORMAT_S16_BE     = 3\n\tSNDRV_PCM_FORMAT_U16_LE     = 4\n\tSNDRV_PCM_FORMAT_U16_BE     = 5\n\tSNDRV_PCM_FORMAT_S24_LE     = 6\n\tSNDRV_PCM_FORMAT_S24_BE     = 7\n\tSNDRV_PCM_FORMAT_U24_LE     = 8\n\tSNDRV_PCM_FORMAT_U24_BE     = 9\n\tSNDRV_PCM_FORMAT_S32_LE     = 10\n\tSNDRV_PCM_FORMAT_S32_BE     = 11\n\tSNDRV_PCM_FORMAT_U32_LE     = 12\n\tSNDRV_PCM_FORMAT_U32_BE     = 13\n\tSNDRV_PCM_FORMAT_FLOAT_LE   = 14\n\tSNDRV_PCM_FORMAT_FLOAT_BE   = 15\n\tSNDRV_PCM_FORMAT_FLOAT64_LE = 16\n\tSNDRV_PCM_FORMAT_FLOAT64_BE = 17\n\tSNDRV_PCM_FORMAT_MU_LAW     = 20\n\tSNDRV_PCM_FORMAT_A_LAW      = 21\n\tSNDRV_PCM_FORMAT_MPEG       = 23\n\n\tSNDRV_PCM_IOCTL_PVERSION      = 0x80044100\n\tSNDRV_PCM_IOCTL_INFO          = 0x81204101\n\tSNDRV_PCM_IOCTL_HW_REFINE     = 0xc2604110\n\tSNDRV_PCM_IOCTL_HW_PARAMS     = 0xc2604111\n\tSNDRV_PCM_IOCTL_SW_PARAMS     = 0xc0884113\n\tSNDRV_PCM_IOCTL_PREPARE       = 0x00004140\n\tSNDRV_PCM_IOCTL_WRITEI_FRAMES = 0x40184150\n\tSNDRV_PCM_IOCTL_READI_FRAMES  = 0x80184151\n)\n\ntype snd_pcm_info struct { // size 288\n\tdevice           unsigned_int      // offset 0, size 4\n\tsubdevice        unsigned_int      // offset 4, size 4\n\tstream           signed_int        // offset 8, size 4\n\tcard             signed_int        // offset 12, size 4\n\tid               [64]unsigned_char // offset 16, size 64\n\tname             [80]unsigned_char // offset 80, size 80\n\tsubname          [32]unsigned_char // offset 160, size 32\n\tdev_class        signed_int        // offset 192, size 4\n\tdev_subclass     signed_int        // offset 196, size 4\n\tsubdevices_count unsigned_int      // offset 200, size 4\n\tsubdevices_avail unsigned_int      // offset 204, size 4\n\tpad1             [16]unsigned_char\n\treserved         [64]unsigned_char // offset 224, size 64\n}\n\ntype snd_pcm_uframes_t = unsigned_long\ntype snd_pcm_sframes_t = signed_long\n\ntype snd_xferi struct { // size 24\n\tresult snd_pcm_sframes_t // offset 0, size 8\n\tbuf    void__user        // offset 8, size 8\n\tframes snd_pcm_uframes_t // offset 16, size 8\n}\n\nconst (\n\tSNDRV_PCM_HW_PARAM_ACCESS     = 0\n\tSNDRV_PCM_HW_PARAM_FORMAT     = 1\n\tSNDRV_PCM_HW_PARAM_SUBFORMAT  = 2\n\tSNDRV_PCM_HW_PARAM_FIRST_MASK = 0\n\tSNDRV_PCM_HW_PARAM_LAST_MASK  = 2\n\n\tSNDRV_PCM_HW_PARAM_SAMPLE_BITS    = 8\n\tSNDRV_PCM_HW_PARAM_FRAME_BITS     = 9\n\tSNDRV_PCM_HW_PARAM_CHANNELS       = 10\n\tSNDRV_PCM_HW_PARAM_RATE           = 11\n\tSNDRV_PCM_HW_PARAM_PERIOD_TIME    = 12\n\tSNDRV_PCM_HW_PARAM_PERIOD_SIZE    = 13\n\tSNDRV_PCM_HW_PARAM_PERIOD_BYTES   = 14\n\tSNDRV_PCM_HW_PARAM_PERIODS        = 15\n\tSNDRV_PCM_HW_PARAM_BUFFER_TIME    = 16\n\tSNDRV_PCM_HW_PARAM_BUFFER_SIZE    = 17\n\tSNDRV_PCM_HW_PARAM_BUFFER_BYTES   = 18\n\tSNDRV_PCM_HW_PARAM_TICK_TIME      = 19\n\tSNDRV_PCM_HW_PARAM_FIRST_INTERVAL = 8\n\tSNDRV_PCM_HW_PARAM_LAST_INTERVAL  = 19\n\n\tSNDRV_MASK_MAX = 256\n\n\tSNDRV_PCM_TSTAMP_NONE   = 0\n\tSNDRV_PCM_TSTAMP_ENABLE = 1\n)\n\ntype snd_mask struct { // size 32\n\tbits [(SNDRV_MASK_MAX + 31) / 32]__u32 // offset 0, size 32\n}\n\ntype snd_interval struct { // size 12\n\tmin unsigned_int // offset 0, size 4\n\tmax unsigned_int // offset 4, size 4\n\tbit unsigned_int\n}\n\ntype snd_pcm_hw_params struct { // size 608\n\tflags     unsigned_int                                                                           // offset 0, size 4\n\tmasks     [SNDRV_PCM_HW_PARAM_LAST_MASK - SNDRV_PCM_HW_PARAM_FIRST_MASK + 1]snd_mask             // offset 4, size 96\n\tmres      [5]snd_mask                                                                            // offset 100, size 160\n\tintervals [SNDRV_PCM_HW_PARAM_LAST_INTERVAL - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL + 1]snd_interval // offset 260, size 144\n\tires      [9]snd_interval                                                                        // offset 404, size 108\n\trmask     unsigned_int                                                                           // offset 512, size 4\n\tcmask     unsigned_int                                                                           // offset 516, size 4\n\tinfo      unsigned_int                                                                           // offset 520, size 4\n\tmsbits    unsigned_int                                                                           // offset 524, size 4\n\trate_num  unsigned_int                                                                           // offset 528, size 4\n\trate_den  unsigned_int                                                                           // offset 532, size 4\n\tfifo_size snd_pcm_uframes_t                                                                      // offset 536, size 8\n\treserved  [64]unsigned_char                                                                      // offset 544, size 64\n}\n\ntype snd_pcm_sw_params struct { // size 136\n\ttstamp_mode       signed_int        // offset 0, size 4\n\tperiod_step       unsigned_int      // offset 4, size 4\n\tsleep_min         unsigned_int      // offset 8, size 4\n\tavail_min         snd_pcm_uframes_t // offset 16, size 8\n\txfer_align        snd_pcm_uframes_t // offset 24, size 8\n\tstart_threshold   snd_pcm_uframes_t // offset 32, size 8\n\tstop_threshold    snd_pcm_uframes_t // offset 40, size 8\n\tsilence_threshold snd_pcm_uframes_t // offset 48, size 8\n\tsilence_size      snd_pcm_uframes_t // offset 56, size 8\n\tboundary          snd_pcm_uframes_t // offset 64, size 8\n\tproto             unsigned_int      // offset 72, size 4\n\ttstamp_type       unsigned_int      // offset 76, size 4\n\treserved          [56]unsigned_char // offset 80, size 56\n}\n"
  },
  {
    "path": "pkg/alsa/device/asound_arch.c",
    "content": "//go:build ignore\n#include <stdio.h>\n#include <stddef.h>\n#include <sys/ioctl.h>\n#include <sound/asound.h>\n\n#define print_line(text) printf(\"%s\\n\", text)\n#define print_hex_const(name) printf(\"\\t%s = 0x%08lx\\n\", #name, name)\n#define print_int_const(con) printf(\"\\t%s = %d\\n\", #con, con)\n\n#define print_struct_header(str) printf(\"type %s struct { // size %lu\\n\", #str, sizeof(struct str))\n#define print_struct_member(str, mem, typ) printf(\"\\t%s %s // offset %lu, size %lu\\n\", #mem == \"type\" ? \"typ\" : #mem, typ, offsetof(struct str, mem), sizeof((struct str){0}.mem))\n\n// https://github.com/torvalds/linux/blob/master/include/uapi/sound/asound.h\nint main() {\n    print_line(\"package device\\n\");\n\n    print_line(\"type unsigned_char = byte\");\n    print_line(\"type signed_int = int32\");\n    print_line(\"type unsigned_int = uint32\");\n    print_line(\"type signed_long = int64\");\n    print_line(\"type unsigned_long = uint64\");\n    print_line(\"type __u32 = uint32\");\n    print_line(\"type void__user = uintptr\\n\");\n\n    print_line(\"const (\");\n    print_int_const(SNDRV_PCM_STREAM_PLAYBACK);\n    print_int_const(SNDRV_PCM_STREAM_CAPTURE);\n\tprint_line(\"\");\n\tprint_int_const(SNDRV_PCM_ACCESS_MMAP_INTERLEAVED);\n\tprint_int_const(SNDRV_PCM_ACCESS_MMAP_NONINTERLEAVED);\n\tprint_int_const(SNDRV_PCM_ACCESS_MMAP_COMPLEX);\n\tprint_int_const(SNDRV_PCM_ACCESS_RW_INTERLEAVED);\n\tprint_int_const(SNDRV_PCM_ACCESS_RW_NONINTERLEAVED);\n\tprint_line(\"\");\n\tprint_int_const(SNDRV_PCM_FORMAT_S8);\n\tprint_int_const(SNDRV_PCM_FORMAT_U8);\n\tprint_int_const(SNDRV_PCM_FORMAT_S16_LE);\n\tprint_int_const(SNDRV_PCM_FORMAT_S16_BE);\n\tprint_int_const(SNDRV_PCM_FORMAT_U16_LE);\n\tprint_int_const(SNDRV_PCM_FORMAT_U16_BE);\n\tprint_int_const(SNDRV_PCM_FORMAT_S24_LE);\n\tprint_int_const(SNDRV_PCM_FORMAT_S24_BE);\n\tprint_int_const(SNDRV_PCM_FORMAT_U24_LE);\n\tprint_int_const(SNDRV_PCM_FORMAT_U24_BE);\n\tprint_int_const(SNDRV_PCM_FORMAT_S32_LE);\n\tprint_int_const(SNDRV_PCM_FORMAT_S32_BE);\n\tprint_int_const(SNDRV_PCM_FORMAT_U32_LE);\n\tprint_int_const(SNDRV_PCM_FORMAT_U32_BE);\n\tprint_int_const(SNDRV_PCM_FORMAT_FLOAT_LE);\n\tprint_int_const(SNDRV_PCM_FORMAT_FLOAT_BE);\n\tprint_int_const(SNDRV_PCM_FORMAT_FLOAT64_LE);\n\tprint_int_const(SNDRV_PCM_FORMAT_FLOAT64_BE);\n\tprint_int_const(SNDRV_PCM_FORMAT_MU_LAW);\n\tprint_int_const(SNDRV_PCM_FORMAT_A_LAW);\n\tprint_int_const(SNDRV_PCM_FORMAT_MPEG);\n\tprint_line(\"\");\n    print_hex_const(SNDRV_PCM_IOCTL_PVERSION);        // A 0x00\n    print_hex_const(SNDRV_PCM_IOCTL_INFO);            // A 0x01\n    print_hex_const(SNDRV_PCM_IOCTL_HW_REFINE);       // A 0x10\n    print_hex_const(SNDRV_PCM_IOCTL_HW_PARAMS);       // A 0x11\n    print_hex_const(SNDRV_PCM_IOCTL_SW_PARAMS);       // A 0x13\n    print_hex_const(SNDRV_PCM_IOCTL_PREPARE);         // A 0x40\n    print_hex_const(SNDRV_PCM_IOCTL_WRITEI_FRAMES);   // A 0x50\n    print_hex_const(SNDRV_PCM_IOCTL_READI_FRAMES);    // A 0x51\n    print_line(\")\\n\");\n\n\tprint_struct_header(snd_pcm_info);\n\tprint_struct_member(snd_pcm_info, device, \"unsigned_int\");\n\tprint_struct_member(snd_pcm_info, subdevice, \"unsigned_int\");\n\tprint_struct_member(snd_pcm_info, stream, \"signed_int\");\n\tprint_struct_member(snd_pcm_info, card, \"signed_int\");\n\tprint_struct_member(snd_pcm_info, id, \"[64]unsigned_char\");\n\tprint_struct_member(snd_pcm_info, name, \"[80]unsigned_char\");\n\tprint_struct_member(snd_pcm_info, subname, \"[32]unsigned_char\");\n\tprint_struct_member(snd_pcm_info, dev_class, \"signed_int\");\n\tprint_struct_member(snd_pcm_info, dev_subclass, \"signed_int\");\n\tprint_struct_member(snd_pcm_info, subdevices_count, \"unsigned_int\");\n\tprint_struct_member(snd_pcm_info, subdevices_avail, \"unsigned_int\");\n\tprint_line(\"\\tpad1 [16]unsigned_char\");\n\tprint_struct_member(snd_pcm_info, reserved, \"[64]unsigned_char\");\n\tprint_line(\"}\\n\");\n\n\tprint_line(\"type snd_pcm_uframes_t = unsigned_long\");\n\tprint_line(\"type snd_pcm_sframes_t = signed_long\\n\");\n\n\tprint_struct_header(snd_xferi);\n\tprint_struct_member(snd_xferi, result, \"snd_pcm_sframes_t\");\n\tprint_struct_member(snd_xferi, buf, \"void__user\");\n\tprint_struct_member(snd_xferi, frames, \"snd_pcm_uframes_t\");\n\tprint_line(\"}\\n\");\n\n\tprint_line(\"const (\");\n\tprint_int_const(SNDRV_PCM_HW_PARAM_ACCESS);\n\tprint_int_const(SNDRV_PCM_HW_PARAM_FORMAT);\n\tprint_int_const(SNDRV_PCM_HW_PARAM_SUBFORMAT);\n\tprint_int_const(SNDRV_PCM_HW_PARAM_FIRST_MASK);\n\tprint_int_const(SNDRV_PCM_HW_PARAM_LAST_MASK);\n\tprint_line(\"\");\n\tprint_int_const(SNDRV_PCM_HW_PARAM_SAMPLE_BITS);\n\tprint_int_const(SNDRV_PCM_HW_PARAM_FRAME_BITS);\n\tprint_int_const(SNDRV_PCM_HW_PARAM_CHANNELS);\n\tprint_int_const(SNDRV_PCM_HW_PARAM_RATE);\n\tprint_int_const(SNDRV_PCM_HW_PARAM_PERIOD_TIME);\n\tprint_int_const(SNDRV_PCM_HW_PARAM_PERIOD_SIZE);\n\tprint_int_const(SNDRV_PCM_HW_PARAM_PERIOD_BYTES);\n\tprint_int_const(SNDRV_PCM_HW_PARAM_PERIODS);\n\tprint_int_const(SNDRV_PCM_HW_PARAM_BUFFER_TIME);\n\tprint_int_const(SNDRV_PCM_HW_PARAM_BUFFER_SIZE);\n\tprint_int_const(SNDRV_PCM_HW_PARAM_BUFFER_BYTES);\n\tprint_int_const(SNDRV_PCM_HW_PARAM_TICK_TIME);\n\tprint_int_const(SNDRV_PCM_HW_PARAM_FIRST_INTERVAL);\n\tprint_int_const(SNDRV_PCM_HW_PARAM_LAST_INTERVAL);\n\tprint_line(\"\");\n\tprint_int_const(SNDRV_MASK_MAX);\n\tprint_line(\"\");\n\tprint_int_const(SNDRV_PCM_TSTAMP_NONE);\n\tprint_int_const(SNDRV_PCM_TSTAMP_ENABLE);\n\tprint_line(\")\\n\");\n\n\tprint_struct_header(snd_mask);\n\tprint_struct_member(snd_mask, bits, \"[(SNDRV_MASK_MAX+31)/32]__u32\");\n\tprint_line(\"}\\n\");\n\n\tprint_struct_header(snd_interval);\n\tprint_struct_member(snd_interval, min, \"unsigned_int\");\n\tprint_struct_member(snd_interval, max, \"unsigned_int\");\n\tprint_line(\"\\tbit unsigned_int\");\n\tprint_line(\"}\\n\");\n\n\tprint_struct_header(snd_pcm_hw_params);\n\tprint_struct_member(snd_pcm_hw_params, flags, \"unsigned_int\");\n\tprint_struct_member(snd_pcm_hw_params, masks, \"[SNDRV_PCM_HW_PARAM_LAST_MASK-SNDRV_PCM_HW_PARAM_FIRST_MASK+1]snd_mask\");\n\tprint_struct_member(snd_pcm_hw_params, mres, \"[5]snd_mask\");\n\tprint_struct_member(snd_pcm_hw_params, intervals, \"[SNDRV_PCM_HW_PARAM_LAST_INTERVAL-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL+1]snd_interval\");\n\tprint_struct_member(snd_pcm_hw_params, ires, \"[9]snd_interval\");\n\tprint_struct_member(snd_pcm_hw_params, rmask, \"unsigned_int\");\n\tprint_struct_member(snd_pcm_hw_params, cmask, \"unsigned_int\");\n\tprint_struct_member(snd_pcm_hw_params, info, \"unsigned_int\");\n\tprint_struct_member(snd_pcm_hw_params, msbits, \"unsigned_int\");\n\tprint_struct_member(snd_pcm_hw_params, rate_num, \"unsigned_int\");\n\tprint_struct_member(snd_pcm_hw_params, rate_den, \"unsigned_int\");\n\tprint_struct_member(snd_pcm_hw_params, fifo_size, \"snd_pcm_uframes_t\");\n\tprint_struct_member(snd_pcm_hw_params, reserved, \"[64]unsigned_char\");\n\tprint_line(\"}\\n\");\n\n\tprint_struct_header(snd_pcm_sw_params);\n\tprint_struct_member(snd_pcm_sw_params, tstamp_mode, \"signed_int\");\n\tprint_struct_member(snd_pcm_sw_params, period_step, \"unsigned_int\");\n\tprint_struct_member(snd_pcm_sw_params, sleep_min, \"unsigned_int\");\n\tprint_struct_member(snd_pcm_sw_params, avail_min, \"snd_pcm_uframes_t\");\n\tprint_struct_member(snd_pcm_sw_params, xfer_align, \"snd_pcm_uframes_t\");\n\tprint_struct_member(snd_pcm_sw_params, start_threshold, \"snd_pcm_uframes_t\");\n\tprint_struct_member(snd_pcm_sw_params, stop_threshold, \"snd_pcm_uframes_t\");\n\tprint_struct_member(snd_pcm_sw_params, silence_threshold, \"snd_pcm_uframes_t\");\n\tprint_struct_member(snd_pcm_sw_params, silence_size, \"snd_pcm_uframes_t\");\n\tprint_struct_member(snd_pcm_sw_params, boundary, \"snd_pcm_uframes_t\");\n\tprint_struct_member(snd_pcm_sw_params, proto, \"unsigned_int\");\n\tprint_struct_member(snd_pcm_sw_params, tstamp_type, \"unsigned_int\");\n\tprint_struct_member(snd_pcm_sw_params, reserved, \"[56]unsigned_char\");\n\tprint_line(\"}\\n\");\n\n\treturn 0;\n}"
  },
  {
    "path": "pkg/alsa/device/asound_mipsle.go",
    "content": "package device\n\ntype unsigned_char = byte\ntype signed_int = int32\ntype unsigned_int = uint32\ntype signed_long = int64\ntype unsigned_long = uint64\ntype __u32 = uint32\ntype void__user = uintptr\n\nconst (\n\tSNDRV_PCM_STREAM_PLAYBACK = 0\n\tSNDRV_PCM_STREAM_CAPTURE  = 1\n\n\tSNDRV_PCM_ACCESS_MMAP_INTERLEAVED    = 0\n\tSNDRV_PCM_ACCESS_MMAP_NONINTERLEAVED = 1\n\tSNDRV_PCM_ACCESS_MMAP_COMPLEX        = 2\n\tSNDRV_PCM_ACCESS_RW_INTERLEAVED      = 3\n\tSNDRV_PCM_ACCESS_RW_NONINTERLEAVED   = 4\n\n\tSNDRV_PCM_FORMAT_S8         = 0\n\tSNDRV_PCM_FORMAT_U8         = 1\n\tSNDRV_PCM_FORMAT_S16_LE     = 2\n\tSNDRV_PCM_FORMAT_S16_BE     = 3\n\tSNDRV_PCM_FORMAT_U16_LE     = 4\n\tSNDRV_PCM_FORMAT_U16_BE     = 5\n\tSNDRV_PCM_FORMAT_S24_LE     = 6\n\tSNDRV_PCM_FORMAT_S24_BE     = 7\n\tSNDRV_PCM_FORMAT_U24_LE     = 8\n\tSNDRV_PCM_FORMAT_U24_BE     = 9\n\tSNDRV_PCM_FORMAT_S32_LE     = 10\n\tSNDRV_PCM_FORMAT_S32_BE     = 11\n\tSNDRV_PCM_FORMAT_U32_LE     = 12\n\tSNDRV_PCM_FORMAT_U32_BE     = 13\n\tSNDRV_PCM_FORMAT_FLOAT_LE   = 14\n\tSNDRV_PCM_FORMAT_FLOAT_BE   = 15\n\tSNDRV_PCM_FORMAT_FLOAT64_LE = 16\n\tSNDRV_PCM_FORMAT_FLOAT64_BE = 17\n\tSNDRV_PCM_FORMAT_MU_LAW     = 20\n\tSNDRV_PCM_FORMAT_A_LAW      = 21\n\tSNDRV_PCM_FORMAT_MPEG       = 23\n\n\tSNDRV_PCM_IOCTL_PVERSION      = 0x40044100\n\tSNDRV_PCM_IOCTL_INFO          = 0x41204101\n\tSNDRV_PCM_IOCTL_HW_REFINE     = 0xc25c4110\n\tSNDRV_PCM_IOCTL_HW_PARAMS     = 0xc25c4111\n\tSNDRV_PCM_IOCTL_SW_PARAMS     = 0xc0684113\n\tSNDRV_PCM_IOCTL_PREPARE       = 0x20004140\n\tSNDRV_PCM_IOCTL_WRITEI_FRAMES = 0x800c4150\n\tSNDRV_PCM_IOCTL_READI_FRAMES  = 0x400c4151\n)\n\ntype snd_pcm_info struct { // size 288\n\tdevice           unsigned_int      // offset 0, size 4\n\tsubdevice        unsigned_int      // offset 4, size 4\n\tstream           signed_int        // offset 8, size 4\n\tcard             signed_int        // offset 12, size 4\n\tid               [64]unsigned_char // offset 16, size 64\n\tname             [80]unsigned_char // offset 80, size 80\n\tsubname          [32]unsigned_char // offset 160, size 32\n\tdev_class        signed_int        // offset 192, size 4\n\tdev_subclass     signed_int        // offset 196, size 4\n\tsubdevices_count unsigned_int      // offset 200, size 4\n\tsubdevices_avail unsigned_int      // offset 204, size 4\n\tpad1             [16]unsigned_char\n\treserved         [64]unsigned_char // offset 224, size 64\n}\n\ntype snd_pcm_uframes_t = unsigned_long\ntype snd_pcm_sframes_t = signed_long\n\ntype snd_xferi struct { // size 12\n\tresult snd_pcm_sframes_t // offset 0, size 4\n\tbuf    void__user        // offset 4, size 4\n\tframes snd_pcm_uframes_t // offset 8, size 4\n}\n\nconst (\n\tSNDRV_PCM_HW_PARAM_ACCESS     = 0\n\tSNDRV_PCM_HW_PARAM_FORMAT     = 1\n\tSNDRV_PCM_HW_PARAM_SUBFORMAT  = 2\n\tSNDRV_PCM_HW_PARAM_FIRST_MASK = 0\n\tSNDRV_PCM_HW_PARAM_LAST_MASK  = 2\n\n\tSNDRV_PCM_HW_PARAM_SAMPLE_BITS    = 8\n\tSNDRV_PCM_HW_PARAM_FRAME_BITS     = 9\n\tSNDRV_PCM_HW_PARAM_CHANNELS       = 10\n\tSNDRV_PCM_HW_PARAM_RATE           = 11\n\tSNDRV_PCM_HW_PARAM_PERIOD_TIME    = 12\n\tSNDRV_PCM_HW_PARAM_PERIOD_SIZE    = 13\n\tSNDRV_PCM_HW_PARAM_PERIOD_BYTES   = 14\n\tSNDRV_PCM_HW_PARAM_PERIODS        = 15\n\tSNDRV_PCM_HW_PARAM_BUFFER_TIME    = 16\n\tSNDRV_PCM_HW_PARAM_BUFFER_SIZE    = 17\n\tSNDRV_PCM_HW_PARAM_BUFFER_BYTES   = 18\n\tSNDRV_PCM_HW_PARAM_TICK_TIME      = 19\n\tSNDRV_PCM_HW_PARAM_FIRST_INTERVAL = 8\n\tSNDRV_PCM_HW_PARAM_LAST_INTERVAL  = 19\n\n\tSNDRV_MASK_MAX = 256\n\n\tSNDRV_PCM_TSTAMP_NONE   = 0\n\tSNDRV_PCM_TSTAMP_ENABLE = 1\n)\n\ntype snd_mask struct { // size 32\n\tbits [(SNDRV_MASK_MAX + 31) / 32]__u32 // offset 0, size 32\n}\n\ntype snd_interval struct { // size 12\n\tmin unsigned_int // offset 0, size 4\n\tmax unsigned_int // offset 4, size 4\n\tbit unsigned_int\n}\n\ntype snd_pcm_hw_params struct { // size 604\n\tflags     unsigned_int                                                                           // offset 0, size 4\n\tmasks     [SNDRV_PCM_HW_PARAM_LAST_MASK - SNDRV_PCM_HW_PARAM_FIRST_MASK + 1]snd_mask             // offset 4, size 96\n\tmres      [5]snd_mask                                                                            // offset 100, size 160\n\tintervals [SNDRV_PCM_HW_PARAM_LAST_INTERVAL - SNDRV_PCM_HW_PARAM_FIRST_INTERVAL + 1]snd_interval // offset 260, size 144\n\tires      [9]snd_interval                                                                        // offset 404, size 108\n\trmask     unsigned_int                                                                           // offset 512, size 4\n\tcmask     unsigned_int                                                                           // offset 516, size 4\n\tinfo      unsigned_int                                                                           // offset 520, size 4\n\tmsbits    unsigned_int                                                                           // offset 524, size 4\n\trate_num  unsigned_int                                                                           // offset 528, size 4\n\trate_den  unsigned_int                                                                           // offset 532, size 4\n\tfifo_size snd_pcm_uframes_t                                                                      // offset 536, size 4\n\treserved  [64]unsigned_char                                                                      // offset 540, size 64\n}\n\ntype snd_pcm_sw_params struct { // size 104\n\ttstamp_mode       signed_int        // offset 0, size 4\n\tperiod_step       unsigned_int      // offset 4, size 4\n\tsleep_min         unsigned_int      // offset 8, size 4\n\tavail_min         snd_pcm_uframes_t // offset 12, size 4\n\txfer_align        snd_pcm_uframes_t // offset 16, size 4\n\tstart_threshold   snd_pcm_uframes_t // offset 20, size 4\n\tstop_threshold    snd_pcm_uframes_t // offset 24, size 4\n\tsilence_threshold snd_pcm_uframes_t // offset 28, size 4\n\tsilence_size      snd_pcm_uframes_t // offset 32, size 4\n\tboundary          snd_pcm_uframes_t // offset 36, size 4\n\tproto             unsigned_int      // offset 40, size 4\n\ttstamp_type       unsigned_int      // offset 44, size 4\n\treserved          [56]unsigned_char // offset 48, size 56\n}\n"
  },
  {
    "path": "pkg/alsa/device/device_linux.go",
    "content": "package device\n\nimport (\n\t\"fmt\"\n\t\"syscall\"\n\t\"unsafe\"\n)\n\ntype Device struct {\n\tfd   uintptr\n\tpath string\n\n\thwparams   snd_pcm_hw_params\n\tframeBytes int // sample size * channels\n}\n\nfunc Open(path string) (*Device, error) {\n\t// important to use nonblock because can get lock\n\tfd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_NONBLOCK, 0)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// important to remove nonblock because better to handle reads and writes\n\tif err = syscall.SetNonblock(fd, false); err != nil {\n\t\treturn nil, err\n\t}\n\n\td := &Device{fd: uintptr(fd), path: path}\n\td.init()\n\n\t// load all supported formats, channels, rates, etc.\n\tif err = ioctl(d.fd, SNDRV_PCM_IOCTL_HW_REFINE, &d.hwparams); err != nil {\n\t\t_ = d.Close()\n\t\treturn nil, err\n\t}\n\n\td.setMask(SNDRV_PCM_HW_PARAM_ACCESS, SNDRV_PCM_ACCESS_RW_INTERLEAVED)\n\n\treturn d, nil\n}\n\nfunc (d *Device) Close() error {\n\treturn syscall.Close(int(d.fd))\n}\n\nfunc (d *Device) IsCapture() bool {\n\t// path: /dev/snd/pcmC0D0c, where p - playback, c - capture\n\treturn d.path[len(d.path)-1] == 'c'\n}\n\ntype Info struct {\n\tCard      int\n\tDevice    int\n\tSubDevice int\n\tStream    int\n\tID        string\n\tName      string\n\tSubName   string\n}\n\nfunc (d *Device) Info() (*Info, error) {\n\tvar info snd_pcm_info\n\tif err := ioctl(d.fd, SNDRV_PCM_IOCTL_INFO, &info); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &Info{\n\t\tCard:      int(info.card),\n\t\tDevice:    int(info.device),\n\t\tSubDevice: int(info.subdevice),\n\t\tStream:    int(info.stream),\n\t\tID:        str(info.id[:]),\n\t\tName:      str(info.name[:]),\n\t\tSubName:   str(info.subname[:]),\n\t}, nil\n}\n\nfunc (d *Device) CheckFormat(format byte) bool {\n\treturn d.checkMask(SNDRV_PCM_HW_PARAM_FORMAT, uint32(format))\n}\n\nfunc (d *Device) ListFormats() (formats []byte) {\n\tfor i := byte(0); i <= 28; i++ {\n\t\tif d.CheckFormat(i) {\n\t\t\tformats = append(formats, i)\n\t\t}\n\t}\n\treturn\n}\n\nfunc (d *Device) RangeRates() (uint32, uint32) {\n\treturn d.getInterval(SNDRV_PCM_HW_PARAM_RATE)\n}\n\nfunc (d *Device) RangeChannels() (byte, byte) {\n\tminCh, maxCh := d.getInterval(SNDRV_PCM_HW_PARAM_CHANNELS)\n\treturn byte(minCh), byte(maxCh)\n}\n\nfunc (d *Device) GetRateNear(rate uint32) uint32 {\n\tr1, r2 := d.RangeRates()\n\tif rate < r1 {\n\t\treturn r1\n\t}\n\tif rate > r2 {\n\t\treturn r2\n\t}\n\treturn rate\n}\n\nfunc (d *Device) GetChannelsNear(channels byte) byte {\n\tc1, c2 := d.RangeChannels()\n\tif channels < c1 {\n\t\treturn c1\n\t}\n\tif channels > c2 {\n\t\treturn c2\n\t}\n\treturn channels\n}\n\nconst bufferSize = 4096\n\nfunc (d *Device) SetHWParams(format byte, rate uint32, channels byte) error {\n\td.setInterval(SNDRV_PCM_HW_PARAM_CHANNELS, uint32(channels))\n\td.setInterval(SNDRV_PCM_HW_PARAM_RATE, rate)\n\td.setMask(SNDRV_PCM_HW_PARAM_FORMAT, uint32(format))\n\t//d.setMask(SNDRV_PCM_HW_PARAM_SUBFORMAT, 0)\n\n\t// important for smooth playback\n\td.setInterval(SNDRV_PCM_HW_PARAM_BUFFER_SIZE, bufferSize)\n\t//d.setInterval(SNDRV_PCM_HW_PARAM_PERIOD_SIZE, 2000)\n\n\tif err := ioctl(d.fd, SNDRV_PCM_IOCTL_HW_PARAMS, &d.hwparams); err != nil {\n\t\treturn fmt.Errorf(\"[alsa] set hw_params: %w\", err)\n\t}\n\n\t_, i := d.getInterval(SNDRV_PCM_HW_PARAM_FRAME_BITS)\n\td.frameBytes = int(i / 8)\n\n\t_, periods := d.getInterval(SNDRV_PCM_HW_PARAM_PERIODS)\n\t_, periodSize := d.getInterval(SNDRV_PCM_HW_PARAM_PERIOD_SIZE)\n\tthreshold := snd_pcm_uframes_t(periods * periodSize) // same as bufferSize\n\n\tswparams := snd_pcm_sw_params{\n\t\t//tstamp_mode: SNDRV_PCM_TSTAMP_ENABLE,\n\t\tperiod_step:    1,\n\t\tavail_min:      1, // start as soon as possible\n\t\tstop_threshold: threshold,\n\t}\n\n\tif d.IsCapture() {\n\t\tswparams.start_threshold = 1\n\t} else {\n\t\tswparams.start_threshold = threshold\n\t}\n\n\tif err := ioctl(d.fd, SNDRV_PCM_IOCTL_SW_PARAMS, &swparams); err != nil {\n\t\treturn fmt.Errorf(\"[alsa] set sw_params: %w\", err)\n\t}\n\n\tif err := ioctl(d.fd, SNDRV_PCM_IOCTL_PREPARE, nil); err != nil {\n\t\treturn fmt.Errorf(\"[alsa] prepare: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (d *Device) Write(b []byte) (n int, err error) {\n\txfer := &snd_xferi{\n\t\tbuf:    uintptr(unsafe.Pointer(&b[0])),\n\t\tframes: snd_pcm_uframes_t(len(b) / d.frameBytes),\n\t}\n\terr = ioctl(d.fd, SNDRV_PCM_IOCTL_WRITEI_FRAMES, xfer)\n\tif err == syscall.EPIPE {\n\t\t// auto handle underrun state\n\t\t// https://stackoverflow.com/questions/59396728/how-to-properly-handle-xrun-in-alsa-programming-when-playing-audio-with-snd-pcm\n\t\terr = ioctl(d.fd, SNDRV_PCM_IOCTL_PREPARE, nil)\n\t}\n\tn = int(xfer.result) * d.frameBytes\n\treturn\n}\n\nfunc (d *Device) Read(b []byte) (n int, err error) {\n\txfer := &snd_xferi{\n\t\tbuf:    uintptr(unsafe.Pointer(&b[0])),\n\t\tframes: snd_pcm_uframes_t(len(b) / d.frameBytes),\n\t}\n\terr = ioctl(d.fd, SNDRV_PCM_IOCTL_READI_FRAMES, xfer)\n\tn = int(xfer.result) * d.frameBytes\n\treturn\n}\n\nfunc (d *Device) init() {\n\tfor i := range d.hwparams.masks {\n\t\td.hwparams.masks[i].bits[0] = 0xFFFFFFFF\n\t\td.hwparams.masks[i].bits[1] = 0xFFFFFFFF\n\t}\n\tfor i := range d.hwparams.intervals {\n\t\td.hwparams.intervals[i].max = 0xFFFFFFFF\n\t}\n\n\td.hwparams.rmask = 0xFFFFFFFF\n\td.hwparams.cmask = 0\n\td.hwparams.info = 0xFFFFFFFF\n}\n\nfunc (d *Device) setInterval(param, val uint32) {\n\td.hwparams.intervals[param-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].min = val\n\td.hwparams.intervals[param-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].max = val\n\td.hwparams.intervals[param-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].bit = 0b0100 // integer\n}\n\nfunc (d *Device) setIntervalMin(param, val uint32) {\n\td.hwparams.intervals[param-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].min = val\n}\n\nfunc (d *Device) getInterval(param uint32) (uint32, uint32) {\n\treturn d.hwparams.intervals[param-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].min,\n\t\td.hwparams.intervals[param-SNDRV_PCM_HW_PARAM_FIRST_INTERVAL].max\n}\n\nfunc (d *Device) setMask(mask, val uint32) {\n\td.hwparams.masks[mask].bits[0] = 0\n\td.hwparams.masks[mask].bits[1] = 0\n\td.hwparams.masks[mask].bits[val>>5] = 1 << (val & 0x1F)\n}\n\nfunc (d *Device) checkMask(mask, val uint32) bool {\n\treturn d.hwparams.masks[mask].bits[val>>5]&(1<<(val&0x1F)) > 0\n}\n"
  },
  {
    "path": "pkg/alsa/device/ioctl_linux.go",
    "content": "package device\n\nimport (\n\t\"bytes\"\n\t\"reflect\"\n\t\"syscall\"\n)\n\nfunc ioctl(fd, req uintptr, arg any) error {\n\tvar ptr uintptr\n\tif arg != nil {\n\t\tptr = reflect.ValueOf(arg).Pointer()\n\t}\n\t_, _, err := syscall.Syscall(syscall.SYS_IOCTL, fd, req, ptr)\n\tif err != 0 {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc str(b []byte) string {\n\tif i := bytes.IndexByte(b, 0); i >= 0 {\n\t\treturn string(b[:i])\n\t}\n\treturn string(b)\n}\n"
  },
  {
    "path": "pkg/alsa/open_linux.go",
    "content": "package alsa\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"net/url\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/alsa/device\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\nfunc Open(rawURL string) (core.Producer, error) {\n\t// Example (ffmpeg source compatible):\n\t// alsa:device?audio=/dev/snd/pcmC0D0p\n\t// TODO: ?audio=default\n\t// TODO: ?audio=hw:0,0\n\t// TODO: &sample_rate=48000&channels=2\n\t// TODO: &backchannel=1\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpath := u.Query().Get(\"audio\")\n\tdev, err := device.Open(path)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif !dev.CheckFormat(device.SNDRV_PCM_FORMAT_S16_LE) {\n\t\t_ = dev.Close()\n\t\treturn nil, errors.New(\"alsa: format S16LE not supported\")\n\t}\n\n\tswitch path[len(path)-1] {\n\tcase 'p': // playback\n\t\treturn newPlayback(dev)\n\tcase 'c': // capture\n\t\treturn newCapture(dev)\n\t}\n\n\t_ = dev.Close()\n\treturn nil, fmt.Errorf(\"alsa: unknown path: %s\", path)\n}\n"
  },
  {
    "path": "pkg/alsa/playback_linux.go",
    "content": "package alsa\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/alsa/device\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/pcm\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Playback struct {\n\tcore.Connection\n\tdev    *device.Device\n\tclosed core.Waiter\n}\n\nfunc newPlayback(dev *device.Device) (*Playback, error) {\n\tmedias := []*core.Media{\n\t\t{\n\t\t\tKind:      core.KindAudio,\n\t\t\tDirection: core.DirectionSendonly,\n\t\t\tCodecs: []*core.Codec{\n\t\t\t\t{Name: core.CodecPCML},                  // support ffmpeg producer (auto transcode)\n\t\t\t\t{Name: core.CodecPCMA, ClockRate: 8000}, // support webrtc producer\n\t\t\t},\n\t\t},\n\t}\n\treturn &Playback{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"alsa\",\n\t\t\tMedias:     medias,\n\t\t\tTransport:  dev,\n\t\t},\n\t\tdev: dev,\n\t}, nil\n}\n\nfunc (p *Playback) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {\n\treturn nil, core.ErrCantGetTrack\n}\n\nfunc (p *Playback) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {\n\tsrc := track.Codec\n\tdst := &core.Codec{\n\t\tName:      core.CodecPCML,\n\t\tClockRate: p.dev.GetRateNear(src.ClockRate),\n\t\tChannels:  p.dev.GetChannelsNear(src.Channels),\n\t}\n\tsender := core.NewSender(media, dst)\n\n\tsender.Handler = func(pkt *rtp.Packet) {\n\t\tif n, err := p.dev.Write(pkt.Payload); err == nil {\n\t\t\tp.Send += n\n\t\t}\n\t}\n\n\tif sender.Handler = pcm.TranscodeHandler(dst, src, sender.Handler); sender.Handler == nil {\n\t\treturn fmt.Errorf(\"alsa: can't convert %s to %s\", src, dst)\n\t}\n\n\t// typical card support:\n\t// - Formats: S16_LE, S32_LE\n\t// - ClockRates: 8000 - 192000\n\t// - Channels: 2 - 10\n\terr := p.dev.SetHWParams(device.SNDRV_PCM_FORMAT_S16_LE, dst.ClockRate, byte(dst.Channels))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tsender.HandleRTP(track)\n\tp.Senders = append(p.Senders, sender)\n\treturn nil\n}\n\nfunc (p *Playback) Start() (err error) {\n\treturn p.closed.Wait()\n}\n\nfunc (p *Playback) Stop() error {\n\tp.closed.Done(nil)\n\treturn p.Connection.Stop()\n}\n"
  },
  {
    "path": "pkg/ascii/README.md",
    "content": "## Useful links\n\n- https://en.wikipedia.org/wiki/ANSI_escape_code\n- https://paulbourke.net/dataformats/asciiart/\n- https://github.com/kutuluk/xterm-color-chart\n- https://github.com/hugomd/parrot.live\n"
  },
  {
    "path": "pkg/ascii/ascii.go",
    "content": "package ascii\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"image/jpeg\"\n\t\"io\"\n\t\"net/http\"\n\t\"unicode/utf8\"\n)\n\nfunc NewWriter(w io.Writer, foreground, background, text string) io.Writer {\n\t// once clear screen\n\t_, _ = w.Write([]byte(csiClear))\n\n\t// every frame - move to home\n\ta := &writer{wr: w, buf: []byte(csiHome)}\n\n\t// https://en.wikipedia.org/wiki/ANSI_escape_code\n\tswitch foreground {\n\tcase \"\":\n\tcase \"8\":\n\t\ta.color = func(r, g, b uint8) {\n\t\t\tidx := xterm256color(r, g, b, 8)\n\t\t\ta.appendEsc(fmt.Sprintf(\"\\033[%dm\", 30+idx))\n\n\t\t}\n\tcase \"256\":\n\t\ta.color = func(r, g, b uint8) {\n\t\t\tidx := xterm256color(r, g, b, 255)\n\t\t\ta.appendEsc(fmt.Sprintf(\"\\033[38;5;%dm\", idx))\n\t\t}\n\tcase \"rgb\":\n\t\ta.color = func(r, g, b uint8) {\n\t\t\ta.appendEsc(fmt.Sprintf(\"\\033[38;2;%d;%d;%dm\", r, g, b))\n\t\t}\n\tdefault:\n\t\ta.buf = append(a.buf, \"\\033[\"+foreground+\"m\"...)\n\t}\n\n\tswitch background {\n\tcase \"\":\n\tcase \"8\":\n\t\ta.color = func(r, g, b uint8) {\n\t\t\tidx := xterm256color(r, g, b, 8)\n\t\t\ta.appendEsc(fmt.Sprintf(\"\\033[%dm\", 40+idx))\n\t\t}\n\tcase \"256\":\n\t\ta.color = func(r, g, b uint8) {\n\t\t\tidx := xterm256color(r, g, b, 255)\n\t\t\ta.appendEsc(fmt.Sprintf(\"\\033[48;5;%dm\", idx))\n\t\t}\n\tcase \"rgb\":\n\t\ta.color = func(r, g, b uint8) {\n\t\t\ta.appendEsc(fmt.Sprintf(\"\\033[48;2;%d;%d;%dm\", r, g, b))\n\t\t}\n\tdefault:\n\t\ta.buf = append(a.buf, \"\\033[\"+background+\"m\"...)\n\t}\n\n\ta.pre = len(a.buf) // save prefix size\n\n\tif len(text) == 1 {\n\t\t// fast 1 symbol version\n\t\ta.text = func(_, _, _ uint32) {\n\t\t\ta.buf = append(a.buf, text[0])\n\t\t}\n\t} else {\n\t\tswitch text {\n\t\tcase \"\":\n\t\t\ttext = ` .::--~~==++**##%%$@` // default for empty text\n\t\tcase \"block\":\n\t\t\ttext = \" ░░▒▒▓▓█\" // https://en.wikipedia.org/wiki/Block_Elements\n\t\t}\n\n\t\tif runes := []rune(text); len(runes) != len(text) {\n\t\t\tk := float32(len(runes)-1) / 255\n\t\t\ta.text = func(r, g, b uint32) {\n\t\t\t\ti := gray(r, g, b, k)\n\t\t\t\ta.buf = utf8.AppendRune(a.buf, runes[i])\n\t\t\t}\n\t\t} else {\n\t\t\tk := float32(len(text)-1) / 255\n\t\t\ta.text = func(r, g, b uint32) {\n\t\t\t\ti := gray(r, g, b, k)\n\t\t\t\ta.buf = append(a.buf, text[i])\n\t\t\t}\n\t\t}\n\t}\n\n\treturn a\n}\n\ntype writer struct {\n\twr    io.Writer\n\tbuf   []byte\n\tpre   int\n\tesc   string\n\tcolor func(r, g, b uint8)\n\ttext  func(r, g, b uint32)\n}\n\n// https://stackoverflow.com/questions/37774983/clearing-the-screen-by-printing-a-character\nconst csiClear = \"\\033[2J\"\nconst csiHome = \"\\033[H\"\n\nfunc (a *writer) Write(p []byte) (n int, err error) {\n\timg, err := jpeg.Decode(bytes.NewReader(p))\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\ta.buf = a.buf[:a.pre] // restore prefix\n\n\tw := img.Bounds().Dx()\n\th := img.Bounds().Dy()\n\tfor y := 0; y < h; y++ {\n\t\tfor x := 0; x < w; x++ {\n\t\t\tr, g, b, _ := img.At(x, y).RGBA()\n\t\t\tif a.color != nil {\n\t\t\t\ta.color(uint8(r>>8), uint8(g>>8), uint8(b>>8))\n\t\t\t}\n\t\t\ta.text(r, g, b)\n\t\t}\n\t\ta.buf = append(a.buf, '\\n')\n\t}\n\n\ta.appendEsc(\"\\033[0m\")\n\n\tif _, err = a.wr.Write(a.buf); err != nil {\n\t\treturn 0, err\n\t}\n\n\ta.wr.(http.Flusher).Flush()\n\n\treturn len(p), nil\n}\n\n// appendEsc - append ESC code to buffer, and skip duplicates\nfunc (a *writer) appendEsc(s string) {\n\tif a.esc != s {\n\t\ta.esc = s\n\t\ta.buf = append(a.buf, s...)\n\t}\n}\n\nfunc gray(r, g, b uint32, k float32) uint8 {\n\tgr := (19595*r + 38470*g + 7471*b + 1<<15) >> 24 // uint8\n\treturn uint8(float32(gr) * k)\n}\n\nconst x256r = \"\\x00\\x80\\x00\\x80\\x00\\x80\\x00\\xc0\\x80\\xff\\x00\\xff\\x00\\xff\\x00\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x00\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x87\\x87\\x87\\x87\\x87\\x87\\x87\\x87\\x87\\x87\\x87\\x87\\x87\\x87\\x87\\x87\\x87\\x87\\x87\\x87\\x87\\x87\\x87\\x87\\x87\\x87\\x87\\x87\\x87\\x87\\x87\\x87\\x87\\x87\\x87\\x87\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\xff\\x08\\x12\\x1c\\x26\\x30\\x3a\\x44\\x4e\\x58\\x60\\x66\\x76\\x80\\x8a\\x94\\x9e\\xa8\\xb2\\xbc\\xc6\\xd0\\xda\\xe4\\xee\"\nconst x256g = \"\\x00\\x00\\x80\\x80\\x00\\x00\\x80\\xc0\\x80\\x00\\xff\\xff\\x00\\x00\\xff\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x87\\x87\\x87\\x87\\x87\\x87\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xff\\xff\\xff\\xff\\xff\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x87\\x87\\x87\\x87\\x87\\x87\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xff\\xff\\xff\\xff\\xff\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x87\\x87\\x87\\x87\\x87\\x87\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xff\\xff\\xff\\xff\\xff\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x87\\x87\\x87\\x87\\x87\\x87\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xff\\xff\\xff\\xff\\xff\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x87\\x87\\x87\\x87\\x87\\x87\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xff\\xff\\xff\\xff\\xff\\xff\\x00\\x00\\x00\\x00\\x00\\x00\\x5f\\x5f\\x5f\\x5f\\x5f\\x5f\\x87\\x87\\x87\\x87\\x87\\x87\\xaf\\xaf\\xaf\\xaf\\xaf\\xaf\\xd7\\xd7\\xd7\\xd7\\xd7\\xd7\\xff\\xff\\xff\\xff\\xff\\xff\\x08\\x12\\x1c\\x26\\x30\\x3a\\x44\\x4e\\x58\\x60\\x66\\x76\\x80\\x8a\\x94\\x9e\\xa8\\xb2\\xbc\\xc6\\xd0\\xda\\xe4\\xee\"\nconst x256b = \"\\x00\\x00\\x00\\x00\\x80\\x80\\x80\\xc0\\x80\\x00\\x00\\x00\\xff\\xff\\xff\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x00\\x5f\\x87\\xaf\\xd7\\xff\\x08\\x12\\x1c\\x26\\x30\\x3a\\x44\\x4e\\x58\\x60\\x66\\x76\\x80\\x8a\\x94\\x9e\\xa8\\xb2\\xbc\\xc6\\xd0\\xda\\xe4\\xee\"\n\nfunc xterm256color(r, g, b uint8, n int) (index uint8) {\n\tbest := uint16(0xFFFF)\n\tfor i := 0; i < n; i++ {\n\t\tdiff := sqDiff(r, x256r[i]) + sqDiff(g, x256g[i]) + sqDiff(b, x256b[i])\n\t\tif diff < best {\n\t\t\tbest = diff\n\t\t\tindex = uint8(i)\n\t\t}\n\t}\n\treturn\n}\n\n// sqDiff - just like from image/color/color.go\nfunc sqDiff(x, y uint8) uint16 {\n\td := uint16(x - y)\n\t//return d\n\treturn (d * d) >> 2\n}\n"
  },
  {
    "path": "pkg/bits/reader.go",
    "content": "package bits\n\ntype Reader struct {\n\tEOF bool // if end of buffer raised during reading\n\n\tbuf  []byte // total buf\n\tbyte byte   // current byte\n\tbits byte   // bits left in byte\n\tpos  int    // current pos in buf\n}\n\nfunc NewReader(b []byte) *Reader {\n\treturn &Reader{buf: b}\n}\n\n//goland:noinspection GoStandardMethods\nfunc (r *Reader) ReadByte() byte {\n\tif r.bits != 0 {\n\t\treturn r.ReadBits8(8)\n\t}\n\n\tif r.pos >= len(r.buf) {\n\t\tr.EOF = true\n\t\treturn 0\n\t}\n\n\tb := r.buf[r.pos]\n\tr.pos++\n\treturn b\n}\n\nfunc (r *Reader) ReadUint16() uint16 {\n\tif r.bits != 0 {\n\t\treturn r.ReadBits16(16)\n\t}\n\treturn uint16(r.ReadByte())<<8 | uint16(r.ReadByte())\n}\n\nfunc (r *Reader) ReadUint24() uint32 {\n\tif r.bits != 0 {\n\t\treturn r.ReadBits(24)\n\t}\n\treturn uint32(r.ReadByte())<<16 | uint32(r.ReadByte())<<8 | uint32(r.ReadByte())\n}\n\nfunc (r *Reader) ReadUint32() uint32 {\n\tif r.bits != 0 {\n\t\treturn r.ReadBits(32)\n\t}\n\treturn uint32(r.ReadByte())<<24 | uint32(r.ReadByte())<<16 | uint32(r.ReadByte())<<8 | uint32(r.ReadByte())\n}\n\nfunc (r *Reader) ReadBit() byte {\n\tif r.bits == 0 {\n\t\tr.byte = r.ReadByte()\n\t\tr.bits = 7\n\t} else {\n\t\tr.bits--\n\t}\n\n\treturn (r.byte >> r.bits) & 0b1\n}\n\nfunc (r *Reader) ReadBits(n byte) (res uint32) {\n\tfor i := n - 1; i != 255; i-- {\n\t\tres |= uint32(r.ReadBit()) << i\n\t}\n\treturn\n}\n\nfunc (r *Reader) ReadBits8(n byte) (res uint8) {\n\tfor i := n - 1; i != 255; i-- {\n\t\tres |= r.ReadBit() << i\n\t}\n\treturn\n}\n\nfunc (r *Reader) ReadBits16(n byte) (res uint16) {\n\tfor i := n - 1; i != 255; i-- {\n\t\tres |= uint16(r.ReadBit()) << i\n\t}\n\treturn\n}\n\nfunc (r *Reader) ReadBits64(n byte) (res uint64) {\n\tfor i := n - 1; i != 255; i-- {\n\t\tres |= uint64(r.ReadBit()) << i\n\t}\n\treturn\n}\n\nfunc (r *Reader) ReadFloat32() float64 {\n\ti := r.ReadUint16()\n\tf := r.ReadUint16()\n\treturn float64(i) + float64(f)/65536\n}\n\nfunc (r *Reader) ReadBytes(n int) (b []byte) {\n\tif r.bits == 0 {\n\t\tif r.pos+n > len(r.buf) {\n\t\t\tr.EOF = true\n\t\t\treturn nil\n\t\t}\n\n\t\tb = r.buf[r.pos : r.pos+n]\n\t\tr.pos += n\n\t} else {\n\t\tb = make([]byte, n)\n\t\tfor i := 0; i < n; i++ {\n\t\t\tb[i] = r.ReadByte()\n\t\t}\n\t}\n\n\treturn\n}\n\n// ReadUEGolomb - ReadExponentialGolomb (unsigned)\nfunc (r *Reader) ReadUEGolomb() uint32 {\n\tvar size byte\n\tfor size = 0; size < 32; size++ {\n\t\tif b := r.ReadBit(); b != 0 || r.EOF {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn r.ReadBits(size) + (1 << size) - 1\n}\n\n// ReadSEGolomb - ReadSignedExponentialGolomb\nfunc (r *Reader) ReadSEGolomb() int32 {\n\tif b := r.ReadUEGolomb(); b%2 == 0 {\n\t\treturn -int32(b / 2)\n\t} else {\n\t\treturn int32((b + 1) / 2)\n\t}\n}\n\nfunc (r *Reader) Left() []byte {\n\treturn r.buf[r.pos:]\n}\n\nfunc (r *Reader) Pos() (int, byte) {\n\treturn r.pos - 1, r.bits\n}\n"
  },
  {
    "path": "pkg/bits/writer.go",
    "content": "package bits\n\ntype Writer struct {\n\tbuf  []byte // total buf\n\tbyte *byte  // pointer to current byte\n\tbits byte   // bits left in byte\n}\n\nfunc NewWriter(buf []byte) *Writer {\n\treturn &Writer{buf: buf}\n}\n\n//goland:noinspection GoStandardMethods\nfunc (w *Writer) WriteByte(b byte) {\n\tif w.bits != 0 {\n\t\tw.WriteBits8(b, 8)\n\t}\n\n\tw.buf = append(w.buf, b)\n}\n\nfunc (w *Writer) WriteBit(b byte) {\n\tif w.bits == 0 {\n\t\tw.buf = append(w.buf, 0)\n\t\tw.byte = &w.buf[len(w.buf)-1]\n\t\tw.bits = 7\n\t} else {\n\t\tw.bits--\n\t}\n\n\t*w.byte |= (b & 1) << w.bits\n}\n\nfunc (w *Writer) WriteBits(v uint32, n byte) {\n\tfor i := n - 1; i != 255; i-- {\n\t\tw.WriteBit(byte(v>>i) & 0b1)\n\t}\n}\n\nfunc (w *Writer) WriteBits16(v uint16, n byte) {\n\tfor i := n - 1; i != 255; i-- {\n\t\tw.WriteBit(byte(v>>i) & 0b1)\n\t}\n}\n\nfunc (w *Writer) WriteBits8(v, n byte) {\n\tfor i := n - 1; i != 255; i-- {\n\t\tw.WriteBit((v >> i) & 0b1)\n\t}\n}\n\nfunc (w *Writer) WriteAllBits(bit, n byte) {\n\tfor i := byte(0); i < n; i++ {\n\t\tw.WriteBit(bit)\n\t}\n}\n\nfunc (w *Writer) WriteBool(b bool) {\n\tif b {\n\t\tw.WriteBit(1)\n\t} else {\n\t\tw.WriteBit(0)\n\t}\n}\n\nfunc (w *Writer) WriteUint16(v uint16) {\n\tif w.bits != 0 {\n\t\tw.WriteBits16(v, 16)\n\t}\n\n\tw.buf = append(w.buf, byte(v>>8), byte(v))\n}\n\nfunc (w *Writer) WriteBytes(bytes ...byte) {\n\tif w.bits != 0 {\n\t\tfor _, b := range bytes {\n\t\t\tw.WriteByte(b)\n\t\t}\n\t}\n\n\tw.buf = append(w.buf, bytes...)\n}\n\nfunc (w *Writer) Bytes() []byte {\n\treturn w.buf\n}\n\nfunc (w *Writer) Len() int {\n\treturn len(w.buf)\n}\n\nfunc (w *Writer) Reset() {\n\tw.buf = w.buf[:0]\n\tw.bits = 0\n}\n"
  },
  {
    "path": "pkg/bubble/client.go",
    "content": "// Package bubble, because:\n// Request URL: /bubble/live?ch=0&stream=0\n// Response Conten-Type: video/bubble\n// https://github.com/Lynch234ok/lynch-git/blob/master/app_rebulid/src/bubble.c\npackage bubble\n\nimport (\n\t\"bufio\"\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264/annexb\"\n\t\"github.com/AlexxIT/go2rtc/pkg/tcp\"\n\t\"github.com/pion/rtp\"\n)\n\n// Deprecated: should be rewritten to core.Connection\ntype Client struct {\n\tcore.Listener\n\n\turl  string\n\tconn net.Conn\n\n\tvideoCodec string\n\tchannel    int\n\tstream     int\n\n\tr *bufio.Reader\n\n\tmedias    []*core.Media\n\treceivers []*core.Receiver\n\n\tvideoTrack *core.Receiver\n\taudioTrack *core.Receiver\n\n\trecv int\n}\n\nfunc Dial(rawURL string) (*Client, error) {\n\tclient := &Client{url: rawURL}\n\tif err := client.Dial(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn client, nil\n}\n\nconst (\n\tSyncByte    = 0xAA\n\tPacketAuth  = 0x00\n\tPacketMedia = 0x01\n\tPacketStart = 0x0A\n)\n\nconst Timeout = time.Second * 5\n\nfunc (c *Client) Dial() (err error) {\n\tu, err := url.Parse(c.url)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tif c.conn, err = net.DialTimeout(\"tcp\", u.Host, Timeout); err != nil {\n\t\treturn\n\t}\n\n\tif err = c.conn.SetDeadline(time.Now().Add(Timeout)); err != nil {\n\t\treturn\n\t}\n\n\treq := &tcp.Request{Method: \"GET\", URL: &url.URL{Path: u.Path, RawQuery: u.RawQuery}, Proto: \"HTTP/1.1\"}\n\tif err = req.Write(c.conn); err != nil {\n\t\treturn\n\t}\n\n\tc.r = bufio.NewReader(c.conn)\n\tres, err := tcp.ReadResponse(c.r)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tif res.StatusCode != http.StatusOK {\n\t\treturn errors.New(\"wrong response: \" + res.Status)\n\t}\n\n\t// 1. Read 1024 bytes with XML, some cameras returns exact 1024, but some - 923\n\txml := make([]byte, 1024)\n\tif _, err = c.r.Read(xml); err != nil {\n\t\treturn\n\t}\n\n\t// 2. Write size uint32 + unknown 4b + user 20b + pass 20b\n\tb := make([]byte, 48)\n\tbinary.BigEndian.PutUint32(b, 44)\n\n\tif u.User != nil {\n\t\tcopy(b[8:], u.User.Username())\n\t\tpass, _ := u.User.Password()\n\t\tcopy(b[28:], pass)\n\t} else {\n\t\tcopy(b[8:], \"admin\")\n\t}\n\n\tif err = c.Write(PacketAuth, 0x0E16C271, b); err != nil {\n\t\treturn\n\t}\n\n\t// 3. Read response\n\tcmd, b, err := c.Read()\n\tif err != nil {\n\t\treturn\n\t}\n\n\tif cmd != PacketAuth || len(b) != 44 || b[4] != 3 || b[8] != 1 {\n\t\treturn errors.New(\"wrong auth response\")\n\t}\n\n\t// 4. Parse XML (from 1)\n\tquery := u.Query()\n\n\tstream := query.Get(\"stream\")\n\tif stream != \"\" {\n\t\tc.stream = core.Atoi(stream)\n\t} else {\n\t\tstream = \"0\"\n\t}\n\n\t// <bubble version=\"1.0\" vin=\"1\"><vin0 stream=\"2\">\n\t// <stream0 name=\"720p.264\" size=\"2304x1296\" x1=\"yes\" x2=\"yes\" x4=\"yes\" />\n\t// <stream1 name=\"360p.265\" size=\"640x360\" x1=\"yes\" x2=\"yes\" x4=\"yes\" />\n\t// <vin0>\n\t// </bubble>\n\tre := regexp.MustCompile(\"<stream\" + stream + \" [^>]+\")\n\tstream = re.FindString(string(xml))\n\tif strings.Contains(stream, \".265\") {\n\t\tc.videoCodec = core.CodecH265\n\t} else {\n\t\tc.videoCodec = core.CodecH264\n\t}\n\n\tif ch := query.Get(\"ch\"); ch != \"\" {\n\t\tc.channel = core.Atoi(ch)\n\t}\n\n\treturn\n}\n\nfunc (c *Client) Write(command byte, timestamp uint32, payload []byte) error {\n\tif err := c.conn.SetWriteDeadline(time.Now().Add(Timeout)); err != nil {\n\t\treturn err\n\t}\n\n\t// 0xAA + size uint32 + cmd byte + ts uint32 + payload\n\tb := make([]byte, 14+len(payload))\n\tb[0] = SyncByte\n\tbinary.BigEndian.PutUint32(b[1:], uint32(5+len(payload)))\n\tb[5] = command\n\tbinary.BigEndian.PutUint32(b[6:], timestamp)\n\tcopy(b[10:], payload)\n\n\t_, err := c.conn.Write(b)\n\treturn err\n}\n\nfunc (c *Client) Read() (byte, []byte, error) {\n\tif err := c.conn.SetReadDeadline(time.Now().Add(Timeout)); err != nil {\n\t\treturn 0, nil, err\n\t}\n\n\t// 0xAA + size uint32 + cmd byte + ts uint32 + payload\n\tb := make([]byte, 10)\n\tif _, err := io.ReadFull(c.r, b); err != nil {\n\t\treturn 0, nil, err\n\t}\n\n\tif b[0] != SyncByte {\n\t\treturn 0, nil, errors.New(\"wrong start byte\")\n\t}\n\n\tsize := binary.BigEndian.Uint32(b[1:])\n\tpayload := make([]byte, size-1-4)\n\tif _, err := io.ReadFull(c.r, payload); err != nil {\n\t\treturn 0, nil, err\n\t}\n\n\t//timestamp := binary.BigEndian.Uint32(b[6:]) // in ms\n\n\treturn b[5], payload, nil\n}\n\nfunc (c *Client) Play() error {\n\t// yeah, there's no mistake about the little endian\n\tb := make([]byte, 16)\n\tbinary.LittleEndian.PutUint32(b, uint32(c.channel))\n\tbinary.LittleEndian.PutUint32(b[4:], uint32(c.stream))\n\tbinary.LittleEndian.PutUint32(b[8:], 1) // opened\n\treturn c.Write(PacketStart, 0x0E16C2DF, b)\n}\n\nfunc (c *Client) Handle() error {\n\tvar audioTS uint32\n\n\tfor {\n\t\tcmd, b, err := c.Read()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tc.recv += len(b)\n\n\t\tif cmd != PacketMedia {\n\t\t\tcontinue\n\t\t}\n\n\t\t// size uint32 + type 1b + channel 1b\n\t\t// type = 1 for keyframe, 2 for other frame, 0 for audio\n\n\t\tif b[4] > 0 {\n\t\t\tif c.videoTrack == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tpkt := &rtp.Packet{\n\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\tTimestamp: core.Now90000(),\n\t\t\t\t},\n\t\t\t\tPayload: annexb.EncodeToAVCC(b[6:]),\n\t\t\t}\n\t\t\tc.videoTrack.WriteRTP(pkt)\n\t\t} else {\n\t\t\tif c.audioTrack == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t//binary.LittleEndian.Uint32(b[6:])          // entries (always 1)\n\t\t\t//size := binary.LittleEndian.Uint32(b[10:]) // size\n\t\t\t//mk := binary.LittleEndian.Uint64(b[14:]) // pts (uint64_t)\n\t\t\t//binary.LittleEndian.Uint32(b[22:])         // gtime (time_t)\n\t\t\t//name := b[26:34] // g711\n\t\t\t//rate := binary.LittleEndian.Uint32(b[34:])  // sample rate\n\t\t\t//width := binary.LittleEndian.Uint32(b[38:]) // samplewidth\n\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\tTimestamp: audioTS,\n\t\t\t\t},\n\t\t\t\tPayload: b[6+36:],\n\t\t\t}\n\t\t\taudioTS += uint32(len(pkt.Payload))\n\t\t\tc.audioTrack.WriteRTP(pkt)\n\t\t}\n\t}\n}\n\nfunc (c *Client) Close() error {\n\treturn c.conn.Close()\n}\n"
  },
  {
    "path": "pkg/bubble/producer.go",
    "content": "package bubble\n\nimport (\n\t\"encoding/json\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\nfunc (c *Client) GetMedias() []*core.Media {\n\tif c.medias == nil {\n\t\tc.medias = []*core.Media{\n\t\t\t{\n\t\t\t\tKind:      core.KindVideo,\n\t\t\t\tDirection: core.DirectionRecvonly,\n\t\t\t\tCodecs: []*core.Codec{\n\t\t\t\t\t{Name: c.videoCodec, ClockRate: 90000, PayloadType: core.PayloadTypeRAW},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tKind:      core.KindAudio,\n\t\t\t\tDirection: core.DirectionRecvonly,\n\t\t\t\tCodecs: []*core.Codec{\n\t\t\t\t\t{Name: core.CodecPCMA, ClockRate: 8000, PayloadType: 8},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t}\n\n\treturn c.medias\n}\n\nfunc (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {\n\tfor _, track := range c.receivers {\n\t\tif track.Codec == codec {\n\t\t\treturn track, nil\n\t\t}\n\t}\n\n\ttrack := core.NewReceiver(media, codec)\n\n\tswitch media.Kind {\n\tcase core.KindVideo:\n\t\tc.videoTrack = track\n\tcase core.KindAudio:\n\t\tc.audioTrack = track\n\t}\n\n\tc.receivers = append(c.receivers, track)\n\n\treturn track, nil\n}\n\nfunc (c *Client) Start() error {\n\tif err := c.Play(); err != nil {\n\t\treturn err\n\t}\n\treturn c.Handle()\n}\n\nfunc (c *Client) Stop() error {\n\tfor _, receiver := range c.receivers {\n\t\treceiver.Close()\n\t}\n\treturn c.Close()\n}\n\nfunc (c *Client) MarshalJSON() ([]byte, error) {\n\tinfo := &core.Connection{\n\t\tID:         core.ID(c),\n\t\tFormatName: \"bubble\",\n\t\tProtocol:   \"http\",\n\t\tMedias:     c.medias,\n\t\tRecv:       c.recv,\n\t\tReceivers:  c.receivers,\n\t}\n\tif c.conn != nil {\n\t\tinfo.RemoteAddr = c.conn.RemoteAddr().String()\n\t}\n\treturn json.Marshal(info)\n}\n"
  },
  {
    "path": "pkg/core/README.md",
    "content": "## PCM\n\n**RTSP**\n\n- PayloadType=10 - L16/44100/2 - Linear PCM 16-bit big endian\n- PayloadType=11 - L16/44100/1 - Linear PCM 16-bit big endian\n\nhttps://en.wikipedia.org/wiki/RTP_payload_formats\n\n**Apple QuickTime**\n\n- `raw` - 16-bit data is stored in little endian format\n- `twos` - 16-bit data is stored in big endian format\n- `sowt` - 16-bit data is stored in little endian format\n- `in24` - denotes 24-bit, big endian\n- `in32` - denotes 32-bit, big endian\n- `fl32` - denotes 32-bit floating point PCM\n- `fl64` - denotes 64-bit floating point PCM\n- `alaw` - denotes A-law logarithmic PCM\n- `ulaw` - denotes mu-law logarithmic PCM\n\nhttps://wiki.multimedia.cx/index.php/PCM\n\n**FFmpeg RTSP**\n\n```\npcm_s16be, 44100 Hz, stereo => 10\npcm_s16be, 48000 Hz, stereo => 96 L16/48000/2\npcm_s16be, 44100 Hz, mono   => 11\n\npcm_s16le, 48000 Hz, stereo => 96 (b=AS:1536)\npcm_s16le, 44100 Hz, stereo => 96 (b=AS:1411)\npcm_s16le, 16000 Hz, stereo => 96 (b=AS:512)\npcm_s16le, 8000 Hz, stereo  => 96 (b=AS:256)\n\npcm_s16le, 48000 Hz, mono   => 96 (b=AS:768)\npcm_s16le, 44100 Hz, mono   => 96 (b=AS:705)\npcm_s16le, 16000 Hz, mono   => 96 (b=AS:256)\npcm_s16le, 8000 Hz, mono    => 96 (b=AS:128)\n```"
  },
  {
    "path": "pkg/core/codec.go",
    "content": "package core\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\t\"unicode\"\n\n\t\"github.com/pion/sdp/v3\"\n)\n\ntype Codec struct {\n\tName        string // H264, PCMU, PCMA, opus...\n\tClockRate   uint32 // 90000, 8000, 16000...\n\tChannels    uint8  // 0, 1, 2\n\tFmtpLine    string\n\tPayloadType uint8\n}\n\n// MarshalJSON - return FFprobe compatible output\nfunc (c *Codec) MarshalJSON() ([]byte, error) {\n\tinfo := map[string]any{}\n\tif name := FFmpegCodecName(c.Name); name != \"\" {\n\t\tinfo[\"codec_name\"] = name\n\t\tinfo[\"codec_type\"] = c.Kind()\n\t}\n\tif c.Name == CodecH264 {\n\t\tprofile, level := DecodeH264(c.FmtpLine)\n\t\tif profile != \"\" {\n\t\t\tinfo[\"profile\"] = profile\n\t\t\tinfo[\"level\"] = level\n\t\t}\n\t}\n\tif c.ClockRate != 0 && c.ClockRate != 90000 {\n\t\tinfo[\"sample_rate\"] = c.ClockRate\n\t}\n\tif c.Channels > 0 {\n\t\tinfo[\"channels\"] = c.Channels\n\t}\n\treturn json.Marshal(info)\n}\n\nfunc FFmpegCodecName(name string) string {\n\tswitch name {\n\tcase CodecH264:\n\t\treturn \"h264\"\n\tcase CodecH265:\n\t\treturn \"hevc\"\n\tcase CodecJPEG:\n\t\treturn \"mjpeg\"\n\tcase CodecRAW:\n\t\treturn \"rawvideo\"\n\tcase CodecPCMA:\n\t\treturn \"pcm_alaw\"\n\tcase CodecPCMU:\n\t\treturn \"pcm_mulaw\"\n\tcase CodecPCM:\n\t\treturn \"pcm_s16be\"\n\tcase CodecPCML:\n\t\treturn \"pcm_s16le\"\n\tcase CodecAAC:\n\t\treturn \"aac\"\n\tcase CodecOpus:\n\t\treturn \"opus\"\n\tcase CodecVP8:\n\t\treturn \"vp8\"\n\tcase CodecVP9:\n\t\treturn \"vp9\"\n\tcase CodecAV1:\n\t\treturn \"av1\"\n\tcase CodecELD:\n\t\treturn \"aac/eld\"\n\tcase CodecFLAC:\n\t\treturn \"flac\"\n\tcase CodecMP3:\n\t\treturn \"mp3\"\n\t}\n\treturn name\n}\n\nfunc (c *Codec) String() (s string) {\n\ts = c.Name\n\tif c.ClockRate != 0 && c.ClockRate != 90000 {\n\t\ts += fmt.Sprintf(\"/%d\", c.ClockRate)\n\t}\n\tif c.Channels > 0 {\n\t\ts += fmt.Sprintf(\"/%d\", c.Channels)\n\t}\n\treturn\n}\n\nfunc (c *Codec) IsRTP() bool {\n\treturn c.PayloadType != PayloadTypeRAW\n}\n\nfunc (c *Codec) IsVideo() bool {\n\treturn c.Kind() == KindVideo\n}\n\nfunc (c *Codec) IsAudio() bool {\n\treturn c.Kind() == KindAudio\n}\n\nfunc (c *Codec) Kind() string {\n\treturn GetKind(c.Name)\n}\n\nfunc (c *Codec) PrintName() string {\n\tswitch c.Name {\n\tcase CodecAAC:\n\t\treturn \"AAC\"\n\tcase CodecPCM:\n\t\treturn \"S16B\"\n\tcase CodecPCML:\n\t\treturn \"S16L\"\n\t}\n\treturn c.Name\n}\n\nfunc (c *Codec) Clone() *Codec {\n\tclone := *c\n\treturn &clone\n}\n\nfunc (c *Codec) Match(remote *Codec) bool {\n\tswitch remote.Name {\n\tcase CodecAll, CodecAny:\n\t\treturn true\n\t}\n\n\treturn c.Name == remote.Name &&\n\t\t(c.ClockRate == remote.ClockRate || remote.ClockRate == 0) &&\n\t\t(c.Channels == remote.Channels || remote.Channels == 0)\n}\n\nfunc UnmarshalCodec(md *sdp.MediaDescription, payloadType string) *Codec {\n\tc := &Codec{PayloadType: byte(Atoi(payloadType))}\n\n\tfor _, attr := range md.Attributes {\n\t\tswitch {\n\t\tcase c.Name == \"\" && attr.Key == \"rtpmap\" && strings.HasPrefix(attr.Value, payloadType):\n\t\t\ti := strings.IndexByte(attr.Value, ' ')\n\t\t\tss := strings.Split(attr.Value[i+1:], \"/\")\n\n\t\t\tc.Name = strings.ToUpper(ss[0])\n\t\t\t// fix tailing space: `a=rtpmap:96 H264/90000 `\n\t\t\tc.ClockRate = uint32(Atoi(strings.TrimRightFunc(ss[1], unicode.IsSpace)))\n\n\t\t\tif len(ss) == 3 && ss[2] == \"2\" {\n\t\t\t\tc.Channels = 2\n\t\t\t}\n\t\tcase c.FmtpLine == \"\" && attr.Key == \"fmtp\" && strings.HasPrefix(attr.Value, payloadType):\n\t\t\tif i := strings.IndexByte(attr.Value, ' '); i > 0 {\n\t\t\t\tc.FmtpLine = attr.Value[i+1:]\n\t\t\t}\n\t\t}\n\t}\n\n\tswitch c.Name {\n\tcase \"PCM\":\n\t\t// https://www.reddit.com/r/Hikvision/comments/17elxex/comment/k642g2r/\n\t\t// check pkg/rtsp/rtsp_test.go TestHikvisionPCM\n\t\tc.Name = CodecPCML\n\tcase \"\":\n\t\t// https://en.wikipedia.org/wiki/RTP_payload_formats\n\t\tswitch payloadType {\n\t\tcase \"0\":\n\t\t\tc.Name = CodecPCMU\n\t\t\tc.ClockRate = 8000\n\t\tcase \"8\":\n\t\t\tc.Name = CodecPCMA\n\t\t\tc.ClockRate = 8000\n\t\tcase \"10\":\n\t\t\tc.Name = CodecPCM\n\t\t\tc.ClockRate = 44100\n\t\t\tc.Channels = 2\n\t\tcase \"11\":\n\t\t\tc.Name = CodecPCM\n\t\t\tc.ClockRate = 44100\n\t\tcase \"14\":\n\t\t\tc.Name = CodecMP3\n\t\t\tc.ClockRate = 90000 // it's not real sample rate\n\t\tcase \"26\":\n\t\t\tc.Name = CodecJPEG\n\t\t\tc.ClockRate = 90000\n\t\tcase \"96\", \"97\", \"98\":\n\t\t\tif len(md.Bandwidth) == 0 {\n\t\t\t\tc.Name = payloadType\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\t// FFmpeg + RTSP + pcm_s16le = doesn't pass info about codec name and params\n\t\t\t// so try to guess the codec based on bitrate\n\t\t\t// https://github.com/AlexxIT/go2rtc/issues/523\n\t\t\tswitch md.Bandwidth[0].Bandwidth {\n\t\t\tcase 128:\n\t\t\t\tc.ClockRate = 8000\n\t\t\tcase 256:\n\t\t\t\tc.ClockRate = 16000\n\t\t\tcase 384:\n\t\t\t\tc.ClockRate = 24000\n\t\t\tcase 512:\n\t\t\t\tc.ClockRate = 32000\n\t\t\tcase 705:\n\t\t\t\tc.ClockRate = 44100\n\t\t\tcase 768:\n\t\t\t\tc.ClockRate = 48000\n\t\t\tcase 1411:\n\t\t\t\t// default Windows DShow\n\t\t\t\tc.ClockRate = 44100\n\t\t\t\tc.Channels = 2\n\t\t\tcase 1536:\n\t\t\t\t// default Linux ALSA\n\t\t\t\tc.ClockRate = 48000\n\t\t\t\tc.Channels = 2\n\t\t\tdefault:\n\t\t\t\tc.Name = payloadType\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tc.Name = CodecPCML\n\t\tdefault:\n\t\t\tc.Name = payloadType\n\t\t}\n\t}\n\n\treturn c\n}\n\nfunc DecodeH264(fmtp string) (profile string, level byte) {\n\tif ps := Between(fmtp, \"sprop-parameter-sets=\", \",\"); ps != \"\" {\n\t\tif sps, _ := base64.StdEncoding.DecodeString(ps); len(sps) >= 4 {\n\t\t\tswitch sps[1] {\n\t\t\tcase 0x42:\n\t\t\t\tprofile = \"Baseline\"\n\t\t\tcase 0x4D:\n\t\t\t\tprofile = \"Main\"\n\t\t\tcase 0x58:\n\t\t\t\tprofile = \"Extended\"\n\t\t\tcase 0x64:\n\t\t\t\tprofile = \"High\"\n\t\t\tdefault:\n\t\t\t\tprofile = fmt.Sprintf(\"0x%02X\", sps[1])\n\t\t\t}\n\n\t\t\tlevel = sps[3]\n\t\t}\n\t}\n\treturn\n}\n\nfunc ParseCodecString(s string) *Codec {\n\tvar codec Codec\n\n\tss := strings.Split(s, \"/\")\n\tswitch strings.ToLower(ss[0]) {\n\tcase \"pcm_s16be\", \"s16be\", \"pcm\":\n\t\tcodec.Name = CodecPCM\n\tcase \"pcm_s16le\", \"s16le\", \"pcml\":\n\t\tcodec.Name = CodecPCML\n\tcase \"pcm_alaw\", \"alaw\", \"pcma\", \"g711a\":\n\t\tcodec.Name = CodecPCMA\n\tcase \"pcm_mulaw\", \"mulaw\", \"pcmu\", \"g711u\":\n\t\tcodec.Name = CodecPCMU\n\tcase \"aac\", \"mpeg4-generic\":\n\t\tcodec.Name = CodecAAC\n\tcase \"opus\":\n\t\tcodec.Name = CodecOpus\n\tcase \"flac\":\n\t\tcodec.Name = CodecFLAC\n\tdefault:\n\t\treturn nil\n\t}\n\n\tif len(ss) >= 2 {\n\t\tcodec.ClockRate = uint32(Atoi(ss[1]))\n\t}\n\tif len(ss) >= 3 {\n\t\tcodec.Channels = uint8(Atoi(ss[2]))\n\t}\n\n\treturn &codec\n}\n"
  },
  {
    "path": "pkg/core/connection.go",
    "content": "package core\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"reflect\"\n\t\"sync/atomic\"\n)\n\nfunc NewID() uint32 {\n\treturn id.Add(1)\n}\n\n// Deprecated: use NewID instead\nfunc ID(v any) uint32 {\n\tp := uintptr(reflect.ValueOf(v).UnsafePointer())\n\treturn 0x8000_0000 | uint32(p)\n}\n\nvar id atomic.Uint32\n\ntype Info interface {\n\tSetProtocol(string)\n\tSetRemoteAddr(string)\n\tSetSource(string)\n\tSetURL(string)\n\tWithRequest(*http.Request)\n\tGetSource() string\n}\n\n// Connection just like webrtc.PeerConnection\n// - ID and RemoteAddr used for building Connection(s) graph\n// - FormatName, Protocol, RemoteAddr, Source, URL, SDP, UserAgent used for info about Connection\n// - FormatName and Protocol has FFmpeg compatible names\n// - Transport used for auto closing on Stop\ntype Connection struct {\n\tID         uint32 `json:\"id,omitempty\"`\n\tFormatName string `json:\"format_name,omitempty\"` // rtsp, webrtc, mp4, mjpeg, mpjpeg...\n\tProtocol   string `json:\"protocol,omitempty\"`    // tcp, udp, http, ws, pipe...\n\tRemoteAddr string `json:\"remote_addr,omitempty\"` // host:port other info\n\tSource     string `json:\"source,omitempty\"`\n\tURL        string `json:\"url,omitempty\"`\n\tSDP        string `json:\"sdp,omitempty\"`\n\tUserAgent  string `json:\"user_agent,omitempty\"`\n\n\tMedias    []*Media    `json:\"medias,omitempty\"`\n\tReceivers []*Receiver `json:\"receivers,omitempty\"`\n\tSenders   []*Sender   `json:\"senders,omitempty\"`\n\tRecv      int         `json:\"bytes_recv,omitempty\"`\n\tSend      int         `json:\"bytes_send,omitempty\"`\n\n\tTransport any `json:\"-\"`\n}\n\nfunc (c *Connection) GetMedias() []*Media {\n\treturn c.Medias\n}\n\nfunc (c *Connection) GetTrack(media *Media, codec *Codec) (*Receiver, error) {\n\tfor _, receiver := range c.Receivers {\n\t\tif receiver.Codec == codec {\n\t\t\treturn receiver, nil\n\t\t}\n\t}\n\treceiver := NewReceiver(media, codec)\n\tc.Receivers = append(c.Receivers, receiver)\n\treturn receiver, nil\n}\n\nfunc (c *Connection) Stop() error {\n\tfor _, receiver := range c.Receivers {\n\t\treceiver.Close()\n\t}\n\tfor _, sender := range c.Senders {\n\t\tsender.Close()\n\t}\n\tif closer, ok := c.Transport.(io.Closer); ok {\n\t\treturn closer.Close()\n\t}\n\treturn nil\n}\n\n// Deprecated:\nfunc (c *Connection) Codecs() []*Codec {\n\tcodecs := make([]*Codec, len(c.Senders))\n\tfor i, sender := range c.Senders {\n\t\tcodecs[i] = sender.Codec\n\t}\n\treturn codecs\n}\n\nfunc (c *Connection) SetProtocol(s string) {\n\tc.Protocol = s\n}\n\nfunc (c *Connection) SetRemoteAddr(s string) {\n\tif c.RemoteAddr == \"\" {\n\t\tc.RemoteAddr = s\n\t} else {\n\t\tc.RemoteAddr += \" forwarded \" + s\n\t}\n}\n\nfunc (c *Connection) SetSource(s string) {\n\tc.Source = s\n}\n\nfunc (c *Connection) SetURL(s string) {\n\tc.URL = s\n}\n\nfunc (c *Connection) WithRequest(r *http.Request) {\n\tif r.Header.Get(\"Upgrade\") == \"websocket\" {\n\t\tc.Protocol = \"ws\"\n\t} else {\n\t\tc.Protocol = \"http\"\n\t}\n\n\tc.RemoteAddr = r.RemoteAddr\n\tif remote := r.Header.Get(\"X-Forwarded-For\"); remote != \"\" {\n\t\tc.RemoteAddr += \" forwarded \" + remote\n\t}\n\n\tc.UserAgent = r.UserAgent()\n}\n\nfunc (c *Connection) GetSource() string {\n\treturn c.Source\n}\n\n// Create like os.Create, init Consumer with existing Transport\nfunc Create(w io.Writer) (*Connection, error) {\n\treturn &Connection{Transport: w}, nil\n}\n\n// Open like os.Open, init Producer from existing Transport\nfunc Open(r io.Reader) (*Connection, error) {\n\treturn &Connection{Transport: r}, nil\n}\n\n// Dial like net.Dial, init Producer via Dialing\nfunc Dial(rawURL string) (*Connection, error) {\n\treturn &Connection{}, nil\n}\n"
  },
  {
    "path": "pkg/core/core.go",
    "content": "package core\n\nimport \"encoding/json\"\n\nconst (\n\tDirectionRecvonly = \"recvonly\"\n\tDirectionSendonly = \"sendonly\"\n\tDirectionSendRecv = \"sendrecv\"\n)\n\nconst (\n\tKindVideo = \"video\"\n\tKindAudio = \"audio\"\n)\n\nconst (\n\tCodecH264 = \"H264\" // payloadType: 96\n\tCodecH265 = \"H265\"\n\tCodecVP8  = \"VP8\"\n\tCodecVP9  = \"VP9\"\n\tCodecAV1  = \"AV1\"\n\tCodecJPEG = \"JPEG\" // payloadType: 26\n\tCodecRAW  = \"RAW\"\n\n\tCodecPCMU = \"PCMU\" // payloadType: 0\n\tCodecPCMA = \"PCMA\" // payloadType: 8\n\tCodecAAC  = \"MPEG4-GENERIC\"\n\tCodecOpus = \"OPUS\" // payloadType: 111\n\tCodecG722 = \"G722\"\n\tCodecMP3  = \"MPA\" // payload: 14, aka MPEG-1 Layer III\n\tCodecPCM  = \"L16\" // Linear PCM (big endian)\n\n\tCodecPCML = \"PCML\" // Linear PCM (little endian)\n\n\tCodecELD  = \"ELD\" // AAC-ELD\n\tCodecFLAC = \"FLAC\"\n\n\tCodecAll = \"ALL\"\n\tCodecAny = \"ANY\"\n)\n\nconst PayloadTypeRAW byte = 255\n\ntype Producer interface {\n\t// GetMedias - return Media(s) with local Media.Direction:\n\t// - recvonly for Producer Video/Audio\n\t// - sendonly for Producer backchannel\n\tGetMedias() []*Media\n\n\t// GetTrack - return Receiver, that can only produce rtp.Packet(s)\n\tGetTrack(media *Media, codec *Codec) (*Receiver, error)\n\n\t// Deprecated: rename to Run()\n\tStart() error\n\n\t// Deprecated: rename to Close()\n\tStop() error\n}\n\ntype Consumer interface {\n\t// GetMedias - return Media(s) with local Media.Direction:\n\t// - sendonly for Consumer Video/Audio\n\t// - recvonly for Consumer backchannel\n\tGetMedias() []*Media\n\n\tAddTrack(media *Media, codec *Codec, track *Receiver) error\n\n\t// Deprecated: rename to Close()\n\tStop() error\n}\n\ntype Mode byte\n\nconst (\n\tModeActiveProducer Mode = iota + 1 // typical source (client)\n\tModePassiveConsumer\n\tModePassiveProducer\n\tModeActiveConsumer\n)\n\nfunc (m Mode) String() string {\n\tswitch m {\n\tcase ModeActiveProducer:\n\t\treturn \"active producer\"\n\tcase ModePassiveConsumer:\n\t\treturn \"passive consumer\"\n\tcase ModePassiveProducer:\n\t\treturn \"passive producer\"\n\tcase ModeActiveConsumer:\n\t\treturn \"active consumer\"\n\t}\n\treturn \"unknown\"\n}\n\nfunc (m Mode) MarshalJSON() ([]byte, error) {\n\treturn json.Marshal(m.String())\n}\n"
  },
  {
    "path": "pkg/core/core_test.go",
    "content": "package core\n\nimport (\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\ntype producer struct {\n\tMedias    []*Media\n\tReceivers []*Receiver\n\n\tid byte\n}\n\nfunc (p *producer) GetMedias() []*Media {\n\treturn p.Medias\n}\n\nfunc (p *producer) GetTrack(_ *Media, codec *Codec) (*Receiver, error) {\n\tfor _, receiver := range p.Receivers {\n\t\tif receiver.Codec == codec {\n\t\t\treturn receiver, nil\n\t\t}\n\t}\n\treceiver := NewReceiver(nil, codec)\n\tp.Receivers = append(p.Receivers, receiver)\n\treturn receiver, nil\n}\n\nfunc (p *producer) Start() error {\n\tpkt := &Packet{Payload: []byte{p.id}}\n\tp.Receivers[0].Input(pkt)\n\treturn nil\n}\n\nfunc (p *producer) Stop() error {\n\tfor _, receiver := range p.Receivers {\n\t\treceiver.Close()\n\t}\n\treturn nil\n}\n\ntype consumer struct {\n\tMedias  []*Media\n\tSenders []*Sender\n\n\tcache chan byte\n}\n\nfunc (c *consumer) GetMedias() []*Media {\n\treturn c.Medias\n}\n\nfunc (c *consumer) AddTrack(_ *Media, _ *Codec, track *Receiver) error {\n\tc.cache = make(chan byte, 1)\n\tsender := NewSender(nil, track.Codec)\n\tsender.Output = func(packet *Packet) {\n\t\tc.cache <- packet.Payload[0]\n\t}\n\tsender.HandleRTP(track)\n\tc.Senders = append(c.Senders, sender)\n\treturn nil\n}\n\nfunc (c *consumer) Stop() error {\n\tfor _, sender := range c.Senders {\n\t\tsender.Close()\n\t}\n\treturn nil\n}\n\nfunc (c *consumer) read() byte {\n\treturn <-c.cache\n}\n\nfunc TestName(t *testing.T) {\n\tGetProducer := func(b byte) Producer {\n\t\treturn &producer{\n\t\t\tMedias: []*Media{\n\t\t\t\t{\n\t\t\t\t\tKind:      KindVideo,\n\t\t\t\t\tDirection: DirectionRecvonly,\n\t\t\t\t\tCodecs: []*Codec{\n\t\t\t\t\t\t{Name: CodecH264},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tid: b,\n\t\t}\n\t}\n\n\t// stage1\n\tprod1 := GetProducer(1)\n\tcons2 := &consumer{}\n\n\tmedia1 := prod1.GetMedias()[0]\n\ttrack1, _ := prod1.GetTrack(media1, media1.Codecs[0])\n\n\t_ = cons2.AddTrack(nil, nil, track1)\n\n\t_ = prod1.Start()\n\trequire.Equal(t, byte(1), cons2.read())\n\n\t// stage2\n\tprod2 := GetProducer(2)\n\tmedia2 := prod2.GetMedias()[0]\n\trequire.NotEqual(t, fmt.Sprintf(\"%p\", media1), fmt.Sprintf(\"%p\", media2))\n\ttrack2, _ := prod2.GetTrack(media2, media2.Codecs[0])\n\ttrack1.Replace(track2)\n\n\t_ = prod1.Stop()\n\n\t_ = prod2.Start()\n\trequire.Equal(t, byte(2), cons2.read())\n\n\t// stage3\n\t_ = prod2.Stop()\n}\n\nfunc TestStripUserinfo(t *testing.T) {\n\ts := `streams:\n  test:\n    - ffmpeg:rtsp://username:password@10.1.2.3:554/stream1\n    - ffmpeg:rtsp://10.1.2.3:554/stream1@#video=copy\n`\n\ts = StripUserinfo(s)\n\trequire.Equal(t, `streams:\n  test:\n    - ffmpeg:rtsp://***@10.1.2.3:554/stream1\n    - ffmpeg:rtsp://10.1.2.3:554/stream1@#video=copy\n`, s)\n}\n"
  },
  {
    "path": "pkg/core/helpers.go",
    "content": "package core\n\nimport (\n\t\"crypto/rand\"\n\t\"runtime\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\nconst (\n\tBufferSize      = 64 * 1024 // 64K\n\tConnDialTimeout = 5 * time.Second\n\tConnDeadline    = 5 * time.Second\n\tProbeTimeout    = 5 * time.Second\n)\n\n// Now90000 - timestamp for Video (clock rate = 90000 samples per second)\nfunc Now90000() uint32 {\n\treturn uint32(time.Duration(time.Now().UnixNano()) * 90000 / time.Second)\n}\n\nconst symbols = \"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-_\"\n\n// RandString base10 - numbers, base16 - hex, base36 - digits+letters\n// base64 - URL safe symbols, base0 - crypto random\nfunc RandString(size, base byte) string {\n\tb := make([]byte, size)\n\tif _, err := rand.Read(b); err != nil {\n\t\tpanic(err)\n\t}\n\tif base == 0 {\n\t\treturn string(b)\n\t}\n\tfor i := byte(0); i < size; i++ {\n\t\tb[i] = symbols[b[i]%base]\n\t}\n\treturn string(b)\n}\n\nfunc Before(s, sep string) string {\n\tif i := strings.Index(s, sep); i > 0 {\n\t\treturn s[:i]\n\t}\n\treturn s\n}\n\nfunc Between(s, sub1, sub2 string) string {\n\ti := strings.Index(s, sub1)\n\tif i < 0 {\n\t\treturn \"\"\n\t}\n\ts = s[i+len(sub1):]\n\n\tif i = strings.Index(s, sub2); i >= 0 {\n\t\treturn s[:i]\n\t}\n\n\treturn s\n}\n\nfunc Atoi(s string) (i int) {\n\tif s != \"\" {\n\t\ti, _ = strconv.Atoi(s)\n\t}\n\treturn\n}\n\n// ParseByte - fast parsing string to byte function\nfunc ParseByte(s string) (b byte) {\n\tfor i, ch := range []byte(s) {\n\t\tch -= '0'\n\t\tif ch > 9 {\n\t\t\treturn 0\n\t\t}\n\t\tif i > 0 {\n\t\t\tb *= 10\n\t\t}\n\t\tb += ch\n\t}\n\treturn\n}\n\nfunc Assert(ok bool) {\n\tif !ok {\n\t\t_, file, line, _ := runtime.Caller(1)\n\t\tpanic(file + \":\" + strconv.Itoa(line))\n\t}\n}\n\nfunc Caller() string {\n\t_, file, line, _ := runtime.Caller(1)\n\treturn file + \":\" + strconv.Itoa(line)\n}\n"
  },
  {
    "path": "pkg/core/listener.go",
    "content": "package core\n\ntype EventFunc func(msg any)\n\n// Listener base struct for all classes with support feedback\ntype Listener struct {\n\tevents []EventFunc\n}\n\nfunc (l *Listener) Listen(f EventFunc) {\n\tl.events = append(l.events, f)\n}\n\nfunc (l *Listener) Fire(msg any) {\n\tfor _, f := range l.events {\n\t\tf(msg)\n\t}\n}\n"
  },
  {
    "path": "pkg/core/media.go",
    "content": "package core\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/pion/sdp/v3\"\n)\n\n// Media take best from:\n// - deepch/vdk/format/rtsp/sdp.Media\n// - pion/sdp.MediaDescription\ntype Media struct {\n\tKind      string   `json:\"kind,omitempty\"`      // video or audio\n\tDirection string   `json:\"direction,omitempty\"` // sendonly, recvonly\n\tCodecs    []*Codec `json:\"codecs,omitempty\"`\n\n\tID string `json:\"id,omitempty\"` // MID for WebRTC, Control for RTSP\n}\n\nfunc (m *Media) String() string {\n\ts := fmt.Sprintf(\"%s, %s\", m.Kind, m.Direction)\n\tfor _, codec := range m.Codecs {\n\t\tname := codec.String()\n\n\t\tif strings.Contains(s, name) {\n\t\t\tcontinue\n\t\t}\n\n\t\ts += \", \" + name\n\t}\n\treturn s\n}\n\nfunc (m *Media) MarshalJSON() ([]byte, error) {\n\treturn json.Marshal(m.String())\n}\n\nfunc (m *Media) Clone() *Media {\n\tclone := *m\n\tclone.Codecs = make([]*Codec, len(m.Codecs))\n\tfor i, codec := range m.Codecs {\n\t\tclone.Codecs[i] = codec.Clone()\n\t}\n\treturn &clone\n}\n\nfunc (m *Media) MatchMedia(remote *Media) (codec, remoteCodec *Codec) {\n\t// check same kind and opposite dirrection\n\tif m.Kind != remote.Kind ||\n\t\tm.Direction == DirectionSendonly && remote.Direction != DirectionRecvonly ||\n\t\tm.Direction == DirectionRecvonly && remote.Direction != DirectionSendonly {\n\t\treturn nil, nil\n\t}\n\n\tfor _, codec = range m.Codecs {\n\t\tfor _, remoteCodec = range remote.Codecs {\n\t\t\tif codec.Match(remoteCodec) {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil, nil\n}\n\nfunc (m *Media) MatchCodec(remote *Codec) *Codec {\n\tfor _, codec := range m.Codecs {\n\t\tif codec.Match(remote) {\n\t\t\treturn codec\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (m *Media) MatchAll() bool {\n\tfor _, codec := range m.Codecs {\n\t\tif codec.Name == CodecAll {\n\t\t\treturn true\n\t\t}\n\t}\n\treturn false\n}\n\nfunc (m *Media) Equal(media *Media) bool {\n\tif media.ID != \"\" {\n\t\treturn m.ID == media.ID\n\t}\n\treturn m.String() == media.String()\n}\n\nfunc GetKind(name string) string {\n\tswitch name {\n\tcase CodecH264, CodecH265, CodecVP8, CodecVP9, CodecAV1, CodecJPEG, CodecRAW:\n\t\treturn KindVideo\n\tcase CodecPCMU, CodecPCMA, CodecAAC, CodecOpus, CodecG722, CodecMP3, CodecPCM, CodecPCML, CodecELD, CodecFLAC:\n\t\treturn KindAudio\n\t}\n\treturn \"\"\n}\n\nfunc MarshalSDP(name string, medias []*Media) ([]byte, error) {\n\tsd := &sdp.SessionDescription{\n\t\tOrigin: sdp.Origin{\n\t\t\tUsername: \"-\", SessionID: 1, SessionVersion: 1,\n\t\t\tNetworkType: \"IN\", AddressType: \"IP4\", UnicastAddress: \"0.0.0.0\",\n\t\t},\n\t\tSessionName: sdp.SessionName(name),\n\t\tConnectionInformation: &sdp.ConnectionInformation{\n\t\t\tNetworkType: \"IN\", AddressType: \"IP4\", Address: &sdp.Address{\n\t\t\t\tAddress: \"0.0.0.0\",\n\t\t\t},\n\t\t},\n\t\tTimeDescriptions: []sdp.TimeDescription{\n\t\t\t{Timing: sdp.Timing{}},\n\t\t},\n\t}\n\n\tfor _, media := range medias {\n\t\tif media.Codecs == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tcodec := media.Codecs[0]\n\n\t\tswitch codec.Name {\n\t\tcase CodecELD:\n\t\t\tname = CodecAAC\n\t\tcase CodecPCML:\n\t\t\tname = CodecPCM // beacuse we using pcm.LittleToBig for RTSP server\n\t\tdefault:\n\t\t\tname = codec.Name\n\t\t}\n\n\t\tmd := &sdp.MediaDescription{\n\t\t\tMediaName: sdp.MediaName{\n\t\t\t\tMedia:  media.Kind,\n\t\t\t\tProtos: []string{\"RTP\", \"AVP\"},\n\t\t\t},\n\t\t}\n\t\tmd.WithCodec(codec.PayloadType, name, codec.ClockRate, uint16(codec.Channels), codec.FmtpLine)\n\n\t\tif media.Direction != \"\" {\n\t\t\tmd.WithPropertyAttribute(media.Direction)\n\t\t}\n\n\t\tif media.ID != \"\" {\n\t\t\tmd.WithValueAttribute(\"control\", media.ID)\n\t\t}\n\n\t\tsd.MediaDescriptions = append(sd.MediaDescriptions, md)\n\t}\n\n\treturn sd.Marshal()\n}\n\nfunc UnmarshalMedia(md *sdp.MediaDescription) *Media {\n\tm := &Media{\n\t\tKind: md.MediaName.Media,\n\t}\n\n\tfor _, attr := range md.Attributes {\n\t\tswitch attr.Key {\n\t\tcase DirectionSendonly, DirectionRecvonly, DirectionSendRecv:\n\t\t\tm.Direction = attr.Key\n\t\tcase \"control\", \"mid\":\n\t\t\tm.ID = attr.Value\n\t\t}\n\t}\n\n\tfor _, format := range md.MediaName.Formats {\n\t\tm.Codecs = append(m.Codecs, UnmarshalCodec(md, format))\n\t}\n\n\treturn m\n}\n\nfunc ParseQuery(query map[string][]string) (medias []*Media) {\n\t// set media candidates from query list\n\tfor key, values := range query {\n\t\tswitch key {\n\t\tcase KindVideo, KindAudio:\n\t\t\tfor _, value := range values {\n\t\t\t\tmedia := &Media{Kind: key, Direction: DirectionSendonly}\n\n\t\t\t\tfor _, name := range strings.Split(value, \",\") {\n\t\t\t\t\tname = strings.ToUpper(name)\n\n\t\t\t\t\t// check aliases\n\t\t\t\t\tswitch name {\n\t\t\t\t\tcase \"\", \"COPY\":\n\t\t\t\t\t\tname = CodecAny\n\t\t\t\t\tcase \"MJPEG\":\n\t\t\t\t\t\tname = CodecJPEG\n\t\t\t\t\tcase \"AAC\":\n\t\t\t\t\t\tname = CodecAAC\n\t\t\t\t\tcase \"MP3\":\n\t\t\t\t\t\tname = CodecMP3\n\t\t\t\t\t}\n\n\t\t\t\t\tmedia.Codecs = append(media.Codecs, &Codec{Name: name})\n\t\t\t\t}\n\n\t\t\t\tmedias = append(medias, media)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn\n}\n"
  },
  {
    "path": "pkg/core/media_test.go",
    "content": "package core\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"testing\"\n\n\t\"github.com/pion/sdp/v3\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestSDP(t *testing.T) {\n\tmedias := []*Media{{\n\t\tKind: KindAudio, Direction: DirectionSendonly,\n\t\tCodecs: []*Codec{\n\t\t\t{Name: CodecPCMU, ClockRate: 8000},\n\t\t},\n\t}}\n\n\tdata, err := MarshalSDP(\"go2rtc/1.0.0\", medias)\n\tassert.Empty(t, err)\n\n\tsd := &sdp.SessionDescription{}\n\terr = sd.Unmarshal(data)\n\tassert.Empty(t, err)\n}\n\nfunc TestParseQuery(t *testing.T) {\n\tu, _ := url.Parse(\"rtsp://localhost:8554/camera1\")\n\tmedias := ParseQuery(u.Query())\n\tassert.Nil(t, medias)\n\n\tfor _, rawULR := range []string{\n\t\t\"rtsp://localhost:8554/camera1?video\",\n\t\t\"rtsp://localhost:8554/camera1?video=copy\",\n\t\t\"rtsp://localhost:8554/camera1?video=any\",\n\t} {\n\t\tu, _ = url.Parse(rawULR)\n\t\tmedias = ParseQuery(u.Query())\n\t\tassert.Equal(t, []*Media{\n\t\t\t{Kind: KindVideo, Direction: DirectionSendonly, Codecs: []*Codec{{Name: CodecAny}}},\n\t\t}, medias)\n\t}\n}\n\nfunc TestClone(t *testing.T) {\n\tmedia1 := &Media{\n\t\tKind:      KindVideo,\n\t\tDirection: DirectionRecvonly,\n\t\tCodecs: []*Codec{\n\t\t\t{Name: CodecPCMU, ClockRate: 8000},\n\t\t},\n\t}\n\tmedia2 := media1.Clone()\n\n\tp1 := fmt.Sprintf(\"%p\", media1)\n\tp2 := fmt.Sprintf(\"%p\", media2)\n\trequire.NotEqualValues(t, p1, p2)\n\n\tp3 := fmt.Sprintf(\"%p\", media1.Codecs[0])\n\tp4 := fmt.Sprintf(\"%p\", media2.Codecs[0])\n\trequire.NotEqualValues(t, p3, p4)\n}\n"
  },
  {
    "path": "pkg/core/node.go",
    "content": "package core\n\nimport (\n\t\"sync\"\n\n\t\"github.com/pion/rtp\"\n)\n\n//type Packet struct {\n//\tPayload     []byte\n//\tTimestamp   uint32 // PTS if DTS == 0 else DTS\n//\tComposition uint32 // CTS = PTS-DTS (for support B-frames)\n//\tSequence    uint16\n//}\n\ntype Packet = rtp.Packet\n\n// HandlerFunc - process input packets (just like http.HandlerFunc)\ntype HandlerFunc func(packet *Packet)\n\n// Filter - a decorator for any HandlerFunc\ntype Filter func(handler HandlerFunc) HandlerFunc\n\n// Node - Receiver or Sender or Filter (transform)\ntype Node struct {\n\tCodec  *Codec\n\tInput  HandlerFunc\n\tOutput HandlerFunc\n\n\tid     uint32\n\tchilds []*Node\n\tparent *Node\n\n\tmu sync.Mutex\n}\n\nfunc (n *Node) WithParent(parent *Node) *Node {\n\tparent.AppendChild(n)\n\treturn n\n}\n\nfunc (n *Node) AppendChild(child *Node) {\n\tn.mu.Lock()\n\tn.childs = append(n.childs, child)\n\tn.mu.Unlock()\n\n\tchild.parent = n\n}\n\nfunc (n *Node) RemoveChild(child *Node) {\n\tn.mu.Lock()\n\tfor i, ch := range n.childs {\n\t\tif ch == child {\n\t\t\tn.childs = append(n.childs[:i], n.childs[i+1:]...)\n\t\t\tbreak\n\t\t}\n\t}\n\tn.mu.Unlock()\n}\n\nfunc (n *Node) Close() {\n\tif parent := n.parent; parent != nil {\n\t\tparent.RemoveChild(n)\n\n\t\tif len(parent.childs) == 0 {\n\t\t\tparent.Close()\n\t\t}\n\t} else {\n\t\tfor _, childs := range n.childs {\n\t\t\tchilds.Close()\n\t\t}\n\t}\n}\n\nfunc MoveNode(dst, src *Node) {\n\tsrc.mu.Lock()\n\tchilds := src.childs\n\tsrc.childs = nil\n\tsrc.mu.Unlock()\n\n\tdst.mu.Lock()\n\tdst.childs = childs\n\tdst.mu.Unlock()\n\n\tfor _, child := range childs {\n\t\tchild.parent = dst\n\t}\n}\n"
  },
  {
    "path": "pkg/core/readbuffer.go",
    "content": "package core\n\nimport (\n\t\"errors\"\n\t\"io\"\n)\n\n// ProbeSize\n// in my tests MPEG-TS 40Mbit/s 4K-video require more than 1MB for probe\nconst ProbeSize = 5 * 1024 * 1024 // 5MB\n\nconst (\n\tBufferDisable       = 0\n\tBufferDrainAndClear = -1\n)\n\n// ReadBuffer support buffering and Seek over buffer\n// positive BufferSize will enable buffering mode\n// Seek to negative offset will clear buffer\n// Seek with a positive BufferSize will continue buffering after the last read from the buffer\n// Seek with a negative BufferSize will clear buffer after the last read from the buffer\n// Read more than BufferSize will raise error\ntype ReadBuffer struct {\n\tio.Reader\n\n\tBufferSize int\n\n\tbuf []byte\n\tpos int\n}\n\nfunc NewReadBuffer(rd io.Reader) *ReadBuffer {\n\tif rs, ok := rd.(*ReadBuffer); ok {\n\t\treturn rs\n\t}\n\treturn &ReadBuffer{Reader: rd}\n}\n\nfunc (r *ReadBuffer) Read(p []byte) (n int, err error) {\n\t// with zero buffer - read as usual\n\tif r.BufferSize == BufferDisable {\n\t\treturn r.Reader.Read(p)\n\t}\n\n\t// if buffer not empty - read from it\n\tif r.pos < len(r.buf) {\n\t\tn = copy(p, r.buf[r.pos:])\n\t\tr.pos += n\n\t\treturn\n\t}\n\n\t// with negative buffer - empty it and read as usual\n\tif r.BufferSize < 0 {\n\t\tr.BufferSize = BufferDisable\n\t\tr.buf = nil\n\t\tr.pos = 0\n\n\t\treturn r.Reader.Read(p)\n\t}\n\n\tn, err = r.Reader.Read(p)\n\tif len(r.buf)+n > r.BufferSize {\n\t\treturn 0, errors.New(\"probe reader overflow\")\n\t}\n\tr.buf = append(r.buf, p[:n]...)\n\tr.pos += n\n\treturn\n}\n\nfunc (r *ReadBuffer) Close() error {\n\tif closer, ok := r.Reader.(io.Closer); ok {\n\t\treturn closer.Close()\n\t}\n\treturn nil\n}\n\nfunc (r *ReadBuffer) Seek(offset int64, whence int) (int64, error) {\n\tvar pos int\n\tswitch whence {\n\tcase io.SeekStart:\n\t\tpos = int(offset)\n\tcase io.SeekCurrent:\n\t\tpos = r.pos + int(offset)\n\tcase io.SeekEnd:\n\t\tpos = len(r.buf) + int(offset)\n\t}\n\n\t// negative offset - empty buffer\n\tif pos < 0 {\n\t\tr.buf = nil\n\t\tr.pos = 0\n\t} else if pos >= len(r.buf) {\n\t\tr.pos = len(r.buf)\n\t} else {\n\t\tr.pos = pos\n\t}\n\n\treturn int64(r.pos), nil\n}\n\nfunc (r *ReadBuffer) Peek(n int) ([]byte, error) {\n\tr.BufferSize = n\n\tb := make([]byte, n)\n\tif _, err := io.ReadAtLeast(r, b, n); err != nil {\n\t\treturn nil, err\n\t}\n\tr.Reset()\n\treturn b, nil\n}\n\nfunc (r *ReadBuffer) Reset() {\n\tr.BufferSize = BufferDrainAndClear\n\tr.pos = 0\n}\n"
  },
  {
    "path": "pkg/core/readbuffer_test.go",
    "content": "package core\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestReadSeeker(t *testing.T) {\n\tb := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}\n\tbuf := bytes.NewReader(b)\n\n\trd := NewReadBuffer(buf)\n\trd.BufferSize = ProbeSize\n\n\t// 1. Read to buffer\n\tb = make([]byte, 3)\n\tn, err := rd.Read(b)\n\trequire.Nil(t, err)\n\trequire.Equal(t, []byte{0, 1, 2}, b[:n])\n\n\t// 2. Seek to start\n\t_, err = rd.Seek(0, io.SeekStart)\n\trequire.Nil(t, err)\n\n\t// 3. Read from buffer\n\tb = make([]byte, 2)\n\tn, err = rd.Read(b)\n\trequire.Nil(t, err)\n\trequire.Equal(t, []byte{0, 1}, b[:n])\n\n\t// 4. Read from buffer\n\tn, err = rd.Read(b)\n\trequire.Nil(t, err)\n\trequire.Equal(t, []byte{2}, b[:n])\n\n\t// 5. Read to buffer\n\tn, err = rd.Read(b)\n\trequire.Nil(t, err)\n\trequire.Equal(t, []byte{3, 4}, b[:n])\n\n\t// 6. Seek to start\n\t_, err = rd.Seek(0, io.SeekStart)\n\trequire.Nil(t, err)\n\n\t// 7. Disable buffer\n\trd.BufferSize = -1\n\n\t// 8. Read from buffer\n\tb = make([]byte, 10)\n\tn, err = rd.Read(b)\n\trequire.Nil(t, err)\n\trequire.Equal(t, []byte{0, 1, 2, 3, 4}, b[:n])\n\n\t// 9. Direct read\n\tn, err = rd.Read(b)\n\trequire.Nil(t, err)\n\trequire.Equal(t, []byte{5, 6, 7, 8, 9}, b[:n])\n\n\t// 10. Check buffer empty\n\trequire.Nil(t, rd.buf)\n}\n"
  },
  {
    "path": "pkg/core/slices.go",
    "content": "package core\n\n// This code copied from go1.21 for backward support in go1.20.\n// We need to support go1.20 for Windows 7\n\n// Index returns the index of the first occurrence of v in s,\n// or -1 if not present.\nfunc Index[S ~[]E, E comparable](s S, v E) int {\n\tfor i := range s {\n\t\tif v == s[i] {\n\t\t\treturn i\n\t\t}\n\t}\n\treturn -1\n}\n\n// Contains reports whether v is present in s.\nfunc Contains[S ~[]E, E comparable](s S, v E) bool {\n\treturn Index(s, v) >= 0\n}\n\ntype Ordered interface {\n\t~int | ~int8 | ~int16 | ~int32 | ~int64 |\n\t\t~uint | ~uint8 | ~uint16 | ~uint32 | ~uint64 | ~uintptr |\n\t\t~float32 | ~float64 |\n\t\t~string\n}\n\n// Max returns the maximal value in x. It panics if x is empty.\n// For floating-point E, Max propagates NaNs (any NaN value in x\n// forces the output to be NaN).\nfunc Max[S ~[]E, E Ordered](x S) E {\n\tif len(x) < 1 {\n\t\tpanic(\"slices.Max: empty list\")\n\t}\n\tm := x[0]\n\tfor i := 1; i < len(x); i++ {\n\t\tif x[i] > m {\n\t\t\tm = x[i]\n\t\t}\n\t}\n\treturn m\n}\n"
  },
  {
    "path": "pkg/core/track.go",
    "content": "package core\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\n\t\"github.com/pion/rtp\"\n)\n\nvar ErrCantGetTrack = errors.New(\"can't get track\")\n\ntype Receiver struct {\n\tNode\n\n\t// Deprecated: should be removed\n\tMedia *Media `json:\"-\"`\n\t// Deprecated: should be removed\n\tID byte `json:\"-\"` // Channel for RTSP, PayloadType for MPEG-TS\n\n\tBytes   int `json:\"bytes,omitempty\"`\n\tPackets int `json:\"packets,omitempty\"`\n}\n\nfunc NewReceiver(media *Media, codec *Codec) *Receiver {\n\tr := &Receiver{\n\t\tNode:  Node{id: NewID(), Codec: codec},\n\t\tMedia: media,\n\t}\n\tr.Input = func(packet *Packet) {\n\t\tr.Bytes += len(packet.Payload)\n\t\tr.Packets++\n\t\tfor _, child := range r.childs {\n\t\t\tchild.Input(packet)\n\t\t}\n\t}\n\treturn r\n}\n\n// Deprecated: should be removed\nfunc (r *Receiver) WriteRTP(packet *rtp.Packet) {\n\tr.Input(packet)\n}\n\n// Deprecated: should be removed\nfunc (r *Receiver) Senders() []*Sender {\n\tif len(r.childs) > 0 {\n\t\treturn []*Sender{{}}\n\t} else {\n\t\treturn nil\n\t}\n}\n\n// Deprecated: should be removed\nfunc (r *Receiver) Replace(target *Receiver) {\n\tMoveNode(&target.Node, &r.Node)\n}\n\nfunc (r *Receiver) Close() {\n\tr.Node.Close()\n}\n\ntype Sender struct {\n\tNode\n\n\t// Deprecated:\n\tMedia *Media `json:\"-\"`\n\t// Deprecated:\n\tHandler HandlerFunc `json:\"-\"`\n\n\tBytes   int `json:\"bytes,omitempty\"`\n\tPackets int `json:\"packets,omitempty\"`\n\tDrops   int `json:\"drops,omitempty\"`\n\n\tbuf  chan *Packet\n\tdone chan struct{}\n}\n\nfunc NewSender(media *Media, codec *Codec) *Sender {\n\tvar bufSize uint16\n\n\tif GetKind(codec.Name) == KindVideo {\n\t\tif codec.IsRTP() {\n\t\t\t// in my tests 40Mbit/s 4K-video can generate up to 1500 items\n\t\t\t// for the h264.RTPDepay => RTPPay queue\n\t\t\tbufSize = 4096\n\t\t} else {\n\t\t\tbufSize = 64\n\t\t}\n\t} else {\n\t\tbufSize = 128\n\t}\n\n\tbuf := make(chan *Packet, bufSize)\n\ts := &Sender{\n\t\tNode:  Node{id: NewID(), Codec: codec},\n\t\tMedia: media,\n\t\tbuf:   buf,\n\t}\n\ts.Input = func(packet *Packet) {\n\t\ts.mu.Lock()\n\t\t// unblock write to nil chan - OK, write to closed chan - panic\n\t\tselect {\n\t\tcase s.buf <- packet:\n\t\t\ts.Bytes += len(packet.Payload)\n\t\t\ts.Packets++\n\t\tdefault:\n\t\t\ts.Drops++\n\t\t}\n\t\ts.mu.Unlock()\n\t}\n\ts.Output = func(packet *Packet) {\n\t\ts.Handler(packet)\n\t}\n\treturn s\n}\n\n// Deprecated: should be removed\nfunc (s *Sender) HandleRTP(parent *Receiver) {\n\ts.WithParent(parent)\n\ts.Start()\n}\n\n// Deprecated: should be removed\nfunc (s *Sender) Bind(parent *Receiver) {\n\ts.WithParent(parent)\n}\n\nfunc (s *Sender) WithParent(parent *Receiver) *Sender {\n\ts.Node.WithParent(&parent.Node)\n\treturn s\n}\n\nfunc (s *Sender) Start() {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\tif s.buf == nil || s.done != nil {\n\t\treturn\n\t}\n\ts.done = make(chan struct{})\n\n\t// pass buf directly so that it's impossible for buf to be nil\n\tgo func(buf chan *Packet) {\n\t\tfor packet := range buf {\n\t\t\ts.Output(packet)\n\t\t}\n\t\tclose(s.done)\n\t}(s.buf)\n}\n\nfunc (s *Sender) Wait() {\n\tif done := s.done; done != nil {\n\t\t<-done\n\t}\n}\n\nfunc (s *Sender) State() string {\n\tif s.buf == nil {\n\t\treturn \"closed\"\n\t}\n\tif s.done == nil {\n\t\treturn \"new\"\n\t}\n\treturn \"connected\"\n}\n\nfunc (s *Sender) Close() {\n\t// close buffer if exists\n\ts.mu.Lock()\n\tif s.buf != nil {\n\t\tclose(s.buf) // exit from for range loop\n\t\ts.buf = nil  // prevent writing to closed chan\n\t}\n\ts.mu.Unlock()\n\n\ts.Node.Close()\n}\n\nfunc (r *Receiver) MarshalJSON() ([]byte, error) {\n\tv := struct {\n\t\tID      uint32   `json:\"id\"`\n\t\tCodec   *Codec   `json:\"codec\"`\n\t\tChilds  []uint32 `json:\"childs,omitempty\"`\n\t\tBytes   int      `json:\"bytes,omitempty\"`\n\t\tPackets int      `json:\"packets,omitempty\"`\n\t}{\n\t\tID:      r.Node.id,\n\t\tCodec:   r.Node.Codec,\n\t\tBytes:   r.Bytes,\n\t\tPackets: r.Packets,\n\t}\n\tfor _, child := range r.childs {\n\t\tv.Childs = append(v.Childs, child.id)\n\t}\n\treturn json.Marshal(v)\n}\n\nfunc (s *Sender) MarshalJSON() ([]byte, error) {\n\tv := struct {\n\t\tID      uint32 `json:\"id\"`\n\t\tCodec   *Codec `json:\"codec\"`\n\t\tParent  uint32 `json:\"parent,omitempty\"`\n\t\tBytes   int    `json:\"bytes,omitempty\"`\n\t\tPackets int    `json:\"packets,omitempty\"`\n\t\tDrops   int    `json:\"drops,omitempty\"`\n\t}{\n\t\tID:      s.Node.id,\n\t\tCodec:   s.Node.Codec,\n\t\tBytes:   s.Bytes,\n\t\tPackets: s.Packets,\n\t\tDrops:   s.Drops,\n\t}\n\tif s.parent != nil {\n\t\tv.Parent = s.parent.id\n\t}\n\treturn json.Marshal(v)\n}\n"
  },
  {
    "path": "pkg/core/track_test.go",
    "content": "package core\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestSenser(t *testing.T) {\n\trecv := make(chan *Packet) // blocking receiver\n\n\tsender := NewSender(nil, &Codec{})\n\tsender.Output = func(packet *Packet) {\n\t\trecv <- packet\n\t}\n\trequire.Equal(t, \"new\", sender.State())\n\n\tsender.Start()\n\trequire.Equal(t, \"connected\", sender.State())\n\n\tsender.Input(&Packet{})\n\tsender.Input(&Packet{})\n\n\trequire.Equal(t, 2, sender.Packets)\n\trequire.Equal(t, 0, sender.Drops)\n\n\t// important to read one before close\n\t// because goroutine in Start() can run with nil chan\n\t// it's OK in real life, but bad for test\n\t_, ok := <-recv\n\trequire.True(t, ok)\n\n\tsender.Close()\n\trequire.Equal(t, \"closed\", sender.State())\n\n\tsender.Input(&Packet{})\n\n\trequire.Equal(t, 2, sender.Packets)\n\trequire.Equal(t, 1, sender.Drops)\n\n\t// read 2nd\n\t_, ok = <-recv\n\trequire.True(t, ok)\n\n\t// read 3rd\n\tselect {\n\tcase <-recv:\n\t\tok = true\n\tdefault:\n\t\tok = false\n\t}\n\trequire.False(t, ok)\n}\n"
  },
  {
    "path": "pkg/core/waiter.go",
    "content": "package core\n\nimport (\n\t\"sync\"\n)\n\n// Waiter support:\n// - autotart on first Wait\n// - block new waiters after last Done\n// - safe Done after finish\ntype Waiter struct {\n\tsync.WaitGroup\n\tmu    sync.Mutex\n\tstate int // state < 0 means finish\n\terr   error\n}\n\nfunc (w *Waiter) Add(delta int) {\n\tw.mu.Lock()\n\tif w.state >= 0 {\n\t\tw.state += delta\n\t\tw.WaitGroup.Add(delta)\n\t}\n\tw.mu.Unlock()\n}\n\nfunc (w *Waiter) Wait() error {\n\tw.mu.Lock()\n\t// first wait auto start waiter\n\tif w.state == 0 {\n\t\tw.state++\n\t\tw.WaitGroup.Add(1)\n\t}\n\tw.mu.Unlock()\n\n\tw.WaitGroup.Wait()\n\n\treturn w.err\n}\n\nfunc (w *Waiter) Done(err error) {\n\tw.mu.Lock()\n\n\t// safe run Done only when have tasks\n\tif w.state > 0 {\n\t\tw.state--\n\t\tw.WaitGroup.Done()\n\t}\n\n\t// block waiter for any operations after last done\n\tif w.state == 0 {\n\t\tw.state = -1\n\t\tw.err = err\n\t}\n\n\tw.mu.Unlock()\n}\n\nfunc (w *Waiter) WaitChan() <-chan error {\n\tvar ch chan error\n\n\tw.mu.Lock()\n\n\tif w.state >= 0 {\n\t\tch = make(chan error)\n\t\tgo func() {\n\t\t\tch <- w.Wait()\n\t\t}()\n\t}\n\n\tw.mu.Unlock()\n\n\treturn ch\n}\n"
  },
  {
    "path": "pkg/core/worker.go",
    "content": "package core\n\nimport (\n\t\"time\"\n)\n\ntype Worker struct {\n\ttimer *time.Timer\n\tdone  chan struct{}\n}\n\n// NewWorker run f after d\nfunc NewWorker(d time.Duration, f func() time.Duration) *Worker {\n\ttimer := time.NewTimer(d)\n\tdone := make(chan struct{})\n\n\tgo func() {\n\t\tfor {\n\t\t\tselect {\n\t\t\tcase <-timer.C:\n\t\t\t\tif d = f(); d > 0 {\n\t\t\t\t\ttimer.Reset(d)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\tcase <-done:\n\t\t\t\ttimer.Stop()\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\t}()\n\n\treturn &Worker{timer: timer, done: done}\n}\n\n// Do - instant timer run\nfunc (w *Worker) Do() {\n\tif w == nil {\n\t\treturn\n\t}\n\tw.timer.Reset(0)\n}\n\nfunc (w *Worker) Stop() {\n\tif w == nil {\n\t\treturn\n\t}\n\n\tselect {\n\tcase w.done <- struct{}{}:\n\tdefault:\n\t}\n}\n"
  },
  {
    "path": "pkg/core/writebuffer.go",
    "content": "package core\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"net/http\"\n\t\"sync\"\n)\n\n// WriteBuffer by defaul Write(s) to bytes.Buffer.\n// But after WriteTo to new io.Writer - calls Reset.\n// Reset will flush current buffer data to new writer and starts to Write to new io.Writer\n// WriteTo will be locked until Write fails or Close will be called.\ntype WriteBuffer struct {\n\tio.Writer\n\terr   error\n\tmu    sync.Mutex\n\twg    sync.WaitGroup\n\tstate byte\n}\n\nfunc NewWriteBuffer(wr io.Writer) *WriteBuffer {\n\tif wr == nil {\n\t\twr = bytes.NewBuffer(nil)\n\t}\n\treturn &WriteBuffer{Writer: wr}\n}\n\nfunc (w *WriteBuffer) Write(p []byte) (n int, err error) {\n\tw.mu.Lock()\n\tif w.err != nil {\n\t\terr = w.err\n\t} else if n, err = w.Writer.Write(p); err != nil {\n\t\tw.err = err\n\t\tw.done()\n\t} else if f, ok := w.Writer.(http.Flusher); ok {\n\t\tf.Flush()\n\t}\n\tw.mu.Unlock()\n\treturn\n}\n\nfunc (w *WriteBuffer) WriteTo(wr io.Writer) (n int64, err error) {\n\tw.Reset(wr)\n\tw.wg.Wait()\n\treturn 0, w.err // TODO: fix counter\n}\n\nfunc (w *WriteBuffer) Close() error {\n\tif closer, ok := w.Writer.(io.Closer); ok {\n\t\treturn closer.Close()\n\t}\n\tw.mu.Lock()\n\tw.done()\n\tw.mu.Unlock()\n\treturn nil\n}\n\nfunc (w *WriteBuffer) Reset(wr io.Writer) {\n\tw.mu.Lock()\n\tw.add()\n\tif buf, ok := w.Writer.(*bytes.Buffer); ok && buf.Len() != 0 {\n\t\tif _, err := io.Copy(wr, buf); err != nil {\n\t\t\tw.err = err\n\t\t\tw.done()\n\t\t}\n\t}\n\tw.Writer = wr\n\tw.mu.Unlock()\n}\n\nconst (\n\tnone = iota\n\tstart\n\tend\n)\n\nfunc (w *WriteBuffer) add() {\n\tif w.state == none {\n\t\tw.state = start\n\t\tw.wg.Add(1)\n\t}\n}\n\nfunc (w *WriteBuffer) done() {\n\tif w.state == start {\n\t\tw.state = end\n\t\tw.wg.Done()\n\t}\n}\n\n// OnceBuffer will catch only first message\ntype OnceBuffer struct {\n\tbuf []byte\n}\n\nfunc (o *OnceBuffer) Write(p []byte) (n int, err error) {\n\tif o.buf == nil {\n\t\to.buf = p\n\t}\n\treturn 0, io.EOF\n}\n\nfunc (o *OnceBuffer) WriteTo(w io.Writer) (n int64, err error) {\n\treturn io.Copy(w, bytes.NewReader(o.buf))\n}\n\nfunc (o *OnceBuffer) Buffer() []byte {\n\treturn o.buf\n}\n\nfunc (o *OnceBuffer) Len() int {\n\treturn len(o.buf)\n}\n"
  },
  {
    "path": "pkg/creds/README.md",
    "content": "# Credentials\n\nThis module allows you to get variables:\n\n- from custom storage (ex. config file)\n- from [credential files](https://systemd.io/CREDENTIALS/)\n- from environment variables\n"
  },
  {
    "path": "pkg/creds/creds.go",
    "content": "package creds\n\nimport (\n\t\"errors\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n)\n\ntype Storage interface {\n\tSetValue(name, value string) error\n\tGetValue(name string) (string, bool)\n}\n\nvar storage Storage\n\nfunc SetStorage(s Storage) {\n\tstorage = s\n}\n\nfunc SetValue(name, value string) error {\n\tif storage == nil {\n\t\treturn errors.New(\"credentials: storage not initialized\")\n\t}\n\tif err := storage.SetValue(name, value); err != nil {\n\t\treturn err\n\t}\n\tAddSecret(value)\n\treturn nil\n}\n\nfunc GetValue(name string) (value string, ok bool) {\n\tvalue, ok = getValue(name)\n\tAddSecret(value)\n\treturn\n}\n\nfunc getValue(name string) (string, bool) {\n\tif storage != nil {\n\t\tif value, ok := storage.GetValue(name); ok {\n\t\t\treturn value, true\n\t\t}\n\t}\n\n\tif dir, ok := os.LookupEnv(\"CREDENTIALS_DIRECTORY\"); ok {\n\t\tif value, _ := os.ReadFile(filepath.Join(dir, name)); value != nil {\n\t\t\treturn strings.TrimSpace(string(value)), true\n\t\t}\n\t}\n\n\treturn os.LookupEnv(name)\n}\n\n// ReplaceVars - support format ${CAMERA_PASSWORD} and ${RTSP_USER:admin}\nfunc ReplaceVars(data []byte) []byte {\n\tre := regexp.MustCompile(`\\${([^}{]+)}`)\n\treturn re.ReplaceAllFunc(data, func(match []byte) []byte {\n\t\tkey := string(match[2 : len(match)-1])\n\n\t\tvar def string\n\t\tvar defok bool\n\n\t\tif i := strings.IndexByte(key, ':'); i > 0 {\n\t\t\tkey, def = key[:i], key[i+1:]\n\t\t\tdefok = true\n\t\t}\n\n\t\tif value, ok := GetValue(key); ok {\n\t\t\treturn []byte(value)\n\t\t}\n\n\t\tif defok {\n\t\t\treturn []byte(def)\n\t\t}\n\n\t\treturn match\n\t})\n}\n"
  },
  {
    "path": "pkg/creds/secrets.go",
    "content": "package creds\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"slices\"\n\t\"strings\"\n\t\"sync\"\n)\n\nfunc AddSecret(value string) {\n\tif value == \"\" {\n\t\treturn\n\t}\n\n\tsecretsMu.Lock()\n\tdefer secretsMu.Unlock()\n\n\tif slices.Contains(secrets, value) {\n\t\treturn\n\t}\n\n\tsecrets = append(secrets, value)\n\tsecretsReplacer = nil\n}\n\nvar secrets []string\nvar secretsMu sync.Mutex\nvar secretsReplacer *strings.Replacer\nvar userinfoRegexp *regexp.Regexp\n\nfunc getReplacer() *strings.Replacer {\n\tsecretsMu.Lock()\n\tdefer secretsMu.Unlock()\n\n\tif secretsReplacer == nil {\n\t\toldnew := make([]string, 0, 2*len(secrets))\n\t\tfor _, s := range secrets {\n\t\t\toldnew = append(oldnew, s, \"***\")\n\t\t}\n\t\tsecretsReplacer = strings.NewReplacer(oldnew...)\n\t}\n\n\tif userinfoRegexp == nil {\n\t\tuserinfoRegexp = regexp.MustCompile(`://[` + userinfo + `]+@`)\n\t}\n\n\treturn secretsReplacer\n}\n\n// Uniform Resource Identifier (URI)\n// https://datatracker.ietf.org/doc/html/rfc3986\nconst (\n\tunreserved = `A-Za-z0-9-._~`\n\tsubdelims  = `!$&'()*+,;=`\n\tuserinfo   = unreserved + subdelims + `%:`\n)\n\nfunc SecretString(s string) string {\n\tre := getReplacer()\n\ts = userinfoRegexp.ReplaceAllString(s, `://***@`)\n\treturn re.Replace(s)\n}\n\nfunc SecretWrite(w io.Writer, s string) (n int, err error) {\n\tre := getReplacer()\n\ts = userinfoRegexp.ReplaceAllString(s, `://***@`)\n\treturn re.WriteString(w, s)\n}\n\nfunc SecretWriter(w io.Writer) io.Writer {\n\treturn &secretWriter{w}\n}\n\ntype secretWriter struct {\n\tw io.Writer\n}\n\nfunc (s *secretWriter) Write(b []byte) (int, error) {\n\treturn SecretWrite(s.w, string(b))\n}\n\nfunc SecretResponse(w http.ResponseWriter) http.ResponseWriter {\n\treturn &secretResponse{w}\n}\n\ntype secretResponse struct {\n\thttp.ResponseWriter\n}\n\nfunc (s *secretResponse) Write(b []byte) (int, error) {\n\treturn SecretWrite(s.ResponseWriter, string(b))\n}\n"
  },
  {
    "path": "pkg/creds/secrets_test.go",
    "content": "package creds\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestString(t *testing.T) {\n\tAddSecret(\"admin\")\n\tAddSecret(\"pa$$word\")\n\n\ts := SecretString(\"rtsp://admin:pa$$word@192.168.1.123/stream1\")\n\trequire.Equal(t, \"rtsp://***:***@192.168.1.123/stream1\", s)\n}\n"
  },
  {
    "path": "pkg/debug/conn.go",
    "content": "package debug\n\nimport (\n\t\"bytes\"\n\t\"math/rand\"\n\t\"net\"\n)\n\ntype badConn struct {\n\tnet.Conn\n\tdelay int\n\tbuf   []byte\n}\n\nfunc NewBadConn(conn net.Conn) net.Conn {\n\treturn &badConn{Conn: conn}\n}\n\nconst (\n\tmissChance  = 0.05\n\tdelayChance = 0.1\n)\n\nfunc (c *badConn) Read(b []byte) (n int, err error) {\n\tif rand.Float32() < missChance {\n\t\tif _, err = c.Conn.Read(b); err != nil {\n\t\t\treturn\n\t\t}\n\t\t//log.Printf(\"bad conn: miss\")\n\t}\n\n\tif c.delay > 0 {\n\t\tif c.delay--; c.delay == 0 {\n\t\t\tn = copy(b, c.buf)\n\t\t\treturn\n\t\t}\n\t} else if rand.Float32() < delayChance {\n\t\tif n, err = c.Conn.Read(b); err != nil {\n\t\t\treturn\n\t\t}\n\t\tc.delay = 1 + rand.Intn(5)\n\t\tc.buf = bytes.Clone(b[:n])\n\t\t//log.Printf(\"bad conn: delay %d\", c.delay)\n\t}\n\n\treturn c.Conn.Read(b)\n}\n"
  },
  {
    "path": "pkg/debug/debug.go",
    "content": "package debug\n\nimport (\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/pion/rtp\"\n)\n\nfunc Logger(include func(packet *rtp.Packet) bool) func(packet *rtp.Packet) {\n\tvar lastTime = time.Now()\n\tvar lastTS uint32\n\n\tvar secCnt int\n\tvar secSize int\n\tvar secTS uint32\n\tvar secTime time.Time\n\n\treturn func(packet *rtp.Packet) {\n\t\tif include != nil && !include(packet) {\n\t\t\treturn\n\t\t}\n\n\t\tnow := time.Now()\n\n\t\tfmt.Printf(\n\t\t\t\"%s: size=%6d ts=%10d type=%2d ssrc=%d seq=%5d mark=%t dts=%4d dtime=%3dms\\n\",\n\t\t\tnow.Format(\"15:04:05.000\"),\n\t\t\tlen(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker,\n\t\t\tpacket.Timestamp-lastTS, now.Sub(lastTime).Milliseconds(),\n\t\t)\n\n\t\tlastTS = packet.Timestamp\n\t\tlastTime = now\n\n\t\tif secTS == 0 {\n\t\t\tsecTS = lastTS\n\t\t\tsecTime = now\n\t\t\treturn\n\t\t}\n\n\t\tif dt := now.Sub(secTime); dt > time.Second {\n\t\t\tfmt.Printf(\n\t\t\t\t\"%s: size=%6d cnt=%d dts=%d dtime=%3dms\\n\",\n\t\t\t\tnow.Format(\"15:04:05.000\"),\n\t\t\t\tsecSize, secCnt, lastTS-secTS, dt.Milliseconds(),\n\t\t\t)\n\n\t\t\tsecCnt = 0\n\t\t\tsecSize = 0\n\t\t\tsecTS = lastTS\n\t\t\tsecTime = now\n\t\t}\n\n\t\tsecCnt++\n\t\tsecSize += len(packet.Payload)\n\t}\n}\n"
  },
  {
    "path": "pkg/doorbird/backchannel.go",
    "content": "package doorbird\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Client struct {\n\tcore.Connection\n\tconn net.Conn\n}\n\nfunc Dial(rawURL string) (*Client, error) {\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tuser := u.User.Username()\n\tpass, _ := u.User.Password()\n\n\tif u.Port() == \"\" {\n\t\tu.Host += \":80\"\n\t}\n\n\tconn, err := net.DialTimeout(\"tcp\", u.Host, core.ConnDialTimeout)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ts := fmt.Sprintf(\"POST /bha-api/audio-transmit.cgi?http-user=%s&http-password=%s HTTP/1.0\\r\\n\", user, pass) +\n\t\t\"Content-Type: audio/basic\\r\\n\" +\n\t\t\"Content-Length: 9999999\\r\\n\" +\n\t\t\"Connection: Keep-Alive\\r\\n\" +\n\t\t\"Cache-Control: no-cache\\r\\n\" +\n\t\t\"\\r\\n\"\n\n\t_ = conn.SetWriteDeadline(time.Now().Add(core.ConnDeadline))\n\tif _, err = conn.Write([]byte(s)); err != nil {\n\t\treturn nil, err\n\t}\n\n\tmedias := []*core.Media{\n\t\t{\n\t\t\tKind:      core.KindAudio,\n\t\t\tDirection: core.DirectionSendonly,\n\t\t\tCodecs: []*core.Codec{\n\t\t\t\t{Name: core.CodecPCMU, ClockRate: 8000},\n\t\t\t},\n\t\t},\n\t}\n\n\treturn &Client{\n\t\tcore.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"doorbird\",\n\t\t\tProtocol:   \"http\",\n\t\t\tURL:        rawURL,\n\t\t\tMedias:     medias,\n\t\t\tTransport:  conn,\n\t\t},\n\t\tconn,\n\t}, nil\n}\n\nfunc (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {\n\treturn nil, core.ErrCantGetTrack\n}\n\nfunc (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {\n\tsender := core.NewSender(media, track.Codec)\n\n\tsender.Handler = func(pkt *rtp.Packet) {\n\t\t_ = c.conn.SetWriteDeadline(time.Now().Add(core.ConnDeadline))\n\t\tif n, err := c.conn.Write(pkt.Payload); err == nil {\n\t\t\tc.Send += n\n\t\t}\n\t}\n\n\tsender.HandleRTP(track)\n\tc.Senders = append(c.Senders, sender)\n\treturn nil\n}\n\nfunc (c *Client) Start() (err error) {\n\t// just block until c.conn closed\n\tb := make([]byte, 1)\n\t_, err = c.conn.Read(b)\n\treturn\n}\n"
  },
  {
    "path": "pkg/dvrip/backchannel.go",
    "content": "package dvrip\n\nimport (\n\t\"encoding/binary\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Backchannel struct {\n\tcore.Connection\n\tclient *Client\n}\n\nfunc (c *Backchannel) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {\n\treturn nil, core.ErrCantGetTrack\n}\n\nfunc (c *Backchannel) Start() error {\n\tif err := c.client.conn.SetReadDeadline(time.Time{}); err != nil {\n\t\treturn err\n\t}\n\n\tb := make([]byte, 4096)\n\tfor {\n\t\tif _, err := c.client.rd.Read(b); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n}\n\nfunc (c *Backchannel) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {\n\tif err := c.client.Talk(); err != nil {\n\t\treturn err\n\t}\n\n\tconst PacketSize = 320\n\n\tbuf := make([]byte, 8+PacketSize)\n\tbinary.BigEndian.PutUint32(buf, 0x1FA)\n\n\tswitch track.Codec.Name {\n\tcase core.CodecPCMU:\n\t\tbuf[4] = 10\n\tcase core.CodecPCMA:\n\t\tbuf[4] = 14\n\t}\n\n\t//for i, rate := range sampleRates {\n\t//\tif rate == track.Codec.ClockRate {\n\t//\t\tbuf[5] = byte(i) + 1\n\t//\t\tbreak\n\t//\t}\n\t//}\n\tbuf[5] = 2 // ClockRate=8000\n\n\tbinary.LittleEndian.PutUint16(buf[6:], PacketSize)\n\n\tvar payload []byte\n\n\tsender := core.NewSender(media, track.Codec)\n\tsender.Handler = func(packet *rtp.Packet) {\n\t\tpayload = append(payload, packet.Payload...)\n\n\t\tfor len(payload) >= PacketSize {\n\t\t\tbuf = append(buf[:8], payload[:PacketSize]...)\n\t\t\tif n, err := c.client.WriteCmd(OPTalkData, buf); err != nil {\n\t\t\t\tc.Send += n\n\t\t\t}\n\n\t\t\tpayload = payload[PacketSize:]\n\t\t}\n\t}\n\n\tsender.HandleRTP(track)\n\tc.Senders = append(c.Senders, sender)\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/dvrip/client.go",
    "content": "package dvrip\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"crypto/md5\"\n\t\"encoding/binary\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/url\"\n\t\"time\"\n)\n\nconst (\n\tLogin          = 1000\n\tOPMonitorClaim = 1413\n\tOPMonitorStart = 1410\n\tOPTalkClaim    = 1434\n\tOPTalkStart    = 1430\n\tOPTalkData     = 1432\n)\n\ntype Client struct {\n\tconn    net.Conn\n\tsession uint32\n\tseq     uint32\n\tstream  string\n\n\trd  io.Reader\n\tbuf []byte\n}\n\nfunc (c *Client) Dial(rawURL string) (err error) {\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tif u.Port() == \"\" {\n\t\t// add default TCP port\n\t\tu.Host += \":34567\"\n\t}\n\n\tc.conn, err = net.DialTimeout(\"tcp\", u.Host, time.Second*3)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tif query := u.Query(); query.Get(\"backchannel\") != \"1\" {\n\t\tchannel := query.Get(\"channel\")\n\t\tif channel == \"\" {\n\t\t\tchannel = \"0\"\n\t\t}\n\n\t\tsubtype := query.Get(\"subtype\")\n\t\tswitch subtype {\n\t\tcase \"\", \"0\":\n\t\t\tsubtype = \"Main\"\n\t\tcase \"1\":\n\t\t\tsubtype = \"Extra1\"\n\t\t}\n\n\t\tc.stream = fmt.Sprintf(\n\t\t\t`{\"Channel\":%s,\"CombinMode\":\"NONE\",\"StreamType\":\"%s\",\"TransMode\":\"TCP\"}`,\n\t\t\tchannel, subtype,\n\t\t)\n\t}\n\n\tc.rd = bufio.NewReader(c.conn)\n\n\tif u.User != nil {\n\t\tpass, _ := u.User.Password()\n\t\treturn c.Login(u.User.Username(), pass)\n\t} else {\n\t\treturn c.Login(\"admin\", \"admin\")\n\t}\n}\n\nfunc (c *Client) Close() error {\n\treturn c.conn.Close()\n}\n\nfunc (c *Client) Login(user, pass string) (err error) {\n\tdata := fmt.Sprintf(\n\t\t`{\"EncryptType\":\"MD5\",\"LoginType\":\"DVRIP-Web\",\"PassWord\":\"%s\",\"UserName\":\"%s\"}`+\"\\x0A\\x00\",\n\t\tSofiaHash(pass), user,\n\t)\n\n\tif _, err = c.WriteCmd(Login, []byte(data)); err != nil {\n\t\treturn\n\t}\n\n\t_, err = c.ReadJSON()\n\treturn\n}\n\nfunc (c *Client) Play() error {\n\tformat := `{\"Name\":\"OPMonitor\",\"SessionID\":\"0x%08X\",\"OPMonitor\":{\"Action\":\"%s\",\"Parameter\":%s}}` + \"\\x0A\\x00\"\n\n\tdata := fmt.Sprintf(format, c.session, \"Claim\", c.stream)\n\tif _, err := c.WriteCmd(OPMonitorClaim, []byte(data)); err != nil {\n\t\treturn err\n\t}\n\tif _, err := c.ReadJSON(); err != nil {\n\t\treturn err\n\t}\n\n\tdata = fmt.Sprintf(format, c.session, \"Start\", c.stream)\n\t_, err := c.WriteCmd(OPMonitorStart, []byte(data))\n\treturn err\n}\n\nfunc (c *Client) Talk() error {\n\tformat := `{\"Name\":\"OPTalk\",\"SessionID\":\"0x%08X\",\"OPTalk\":{\"Action\":\"%s\",\"AudioFormat\":{\"EncodeType\":\"G711_ALAW\"}}}` + \"\\x0A\\x00\"\n\n\tdata := fmt.Sprintf(format, c.session, \"Claim\")\n\tif _, err := c.WriteCmd(OPTalkClaim, []byte(data)); err != nil {\n\t\treturn err\n\t}\n\tif _, err := c.ReadJSON(); err != nil {\n\t\treturn err\n\t}\n\n\tdata = fmt.Sprintf(format, c.session, \"Start\")\n\t_, err := c.WriteCmd(OPTalkStart, []byte(data))\n\treturn err\n}\n\nfunc (c *Client) WriteCmd(cmd uint16, payload []byte) (n int, err error) {\n\tb := make([]byte, 20, 128)\n\tb[0] = 255\n\tbinary.LittleEndian.PutUint32(b[4:], c.session)\n\tbinary.LittleEndian.PutUint32(b[8:], c.seq)\n\tbinary.LittleEndian.PutUint16(b[14:], cmd)\n\tbinary.LittleEndian.PutUint32(b[16:], uint32(len(payload)))\n\tb = append(b, payload...)\n\n\tc.seq++\n\n\tif err = c.conn.SetWriteDeadline(time.Now().Add(time.Second * 5)); err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn c.conn.Write(b)\n}\n\nfunc (c *Client) ReadChunk() (b []byte, err error) {\n\tif err = c.conn.SetReadDeadline(time.Now().Add(time.Second * 5)); err != nil {\n\t\treturn\n\t}\n\n\tb = make([]byte, 20)\n\tif _, err = io.ReadFull(c.rd, b); err != nil {\n\t\treturn\n\t}\n\n\tif b[0] != 255 {\n\t\treturn nil, errors.New(\"read error\")\n\t}\n\n\tc.session = binary.LittleEndian.Uint32(b[4:])\n\tsize := binary.LittleEndian.Uint32(b[16:])\n\n\tb = make([]byte, size)\n\tif _, err = io.ReadFull(c.rd, b); err != nil {\n\t\treturn\n\t}\n\n\treturn\n}\n\nfunc (c *Client) ReadPacket() (pType byte, payload []byte, err error) {\n\tvar b []byte\n\n\t// many cameras may split packet to multiple chunks\n\t// some rare cameras may put multiple packets to single chunk\n\tfor len(c.buf) < 16 {\n\t\tif b, err = c.ReadChunk(); err != nil {\n\t\t\treturn 0, nil, err\n\t\t}\n\t\tc.buf = append(c.buf, b...)\n\t}\n\n\tif !bytes.HasPrefix(c.buf, []byte{0, 0, 1}) {\n\t\treturn 0, nil, fmt.Errorf(\"dvrip: wrong packet: %0.16x\", c.buf)\n\t}\n\n\tvar size int\n\n\tswitch pType = c.buf[3]; pType {\n\tcase 0xFC, 0xFE:\n\t\tsize = int(binary.LittleEndian.Uint32(c.buf[12:])) + 16\n\tcase 0xFD: // PFrame\n\t\tsize = int(binary.LittleEndian.Uint32(c.buf[4:])) + 8\n\tcase 0xFA, 0xF9:\n\t\tsize = int(binary.LittleEndian.Uint16(c.buf[6:])) + 8\n\tdefault:\n\t\treturn 0, nil, fmt.Errorf(\"dvrip: unknown packet type: %X\", pType)\n\t}\n\n\tfor len(c.buf) < size {\n\t\tif b, err = c.ReadChunk(); err != nil {\n\t\t\treturn 0, nil, err\n\t\t}\n\t\tc.buf = append(c.buf, b...)\n\t}\n\n\tpayload = c.buf[:size]\n\tc.buf = c.buf[size:]\n\n\treturn\n}\n\ntype Response map[string]any\n\nfunc (c *Client) ReadJSON() (res Response, err error) {\n\tb, err := c.ReadChunk()\n\tif err != nil {\n\t\treturn\n\t}\n\n\tres = Response{}\n\tif err = json.Unmarshal(b[:len(b)-2], &res); err != nil {\n\t\treturn\n\t}\n\n\tif v, ok := res[\"Ret\"].(float64); !ok || (v != 100 && v != 515) {\n\t\terr = fmt.Errorf(\"wrong response: %s\", b)\n\t}\n\treturn\n}\n\nfunc SofiaHash(password string) string {\n\tconst chars = \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz\"\n\n\tsofia := make([]byte, 0, 8)\n\thash := md5.Sum([]byte(password))\n\tfor i := 0; i < md5.Size; i += 2 {\n\t\tj := uint16(hash[i]) + uint16(hash[i+1])\n\t\tsofia = append(sofia, chars[j%62])\n\t}\n\n\treturn string(sofia)\n}\n"
  },
  {
    "path": "pkg/dvrip/dvrip.go",
    "content": "package dvrip\n\nimport \"github.com/AlexxIT/go2rtc/pkg/core\"\n\nfunc Dial(url string) (core.Producer, error) {\n\tclient := &Client{}\n\tif err := client.Dial(url); err != nil {\n\t\treturn nil, err\n\t}\n\n\tconn := core.Connection{\n\t\tID:         core.NewID(),\n\t\tFormatName: \"dvrip\",\n\t\tProtocol:   \"tcp\",\n\t\tRemoteAddr: client.conn.RemoteAddr().String(),\n\t\tTransport:  client.conn,\n\t}\n\n\tif client.stream != \"\" {\n\t\tprod := &Producer{Connection: conn, client: client}\n\t\tif err := prod.probe(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\treturn prod, nil\n\t} else {\n\t\tconn.Medias = []*core.Media{\n\t\t\t{\n\t\t\t\tKind:      core.KindAudio,\n\t\t\t\tDirection: core.DirectionSendonly,\n\t\t\t\tCodecs: []*core.Codec{\n\t\t\t\t\t// leave only one codec here for better compatibility with cameras\n\t\t\t\t\t// https://github.com/AlexxIT/go2rtc/issues/1111\n\t\t\t\t\t{Name: core.CodecPCMA, ClockRate: 8000, PayloadType: 8},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t\treturn &Backchannel{Connection: conn, client: client}, nil\n\t}\n}\n"
  },
  {
    "path": "pkg/dvrip/producer.go",
    "content": "package dvrip\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"fmt\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264/annexb\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h265\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Producer struct {\n\tcore.Connection\n\n\tclient *Client\n\n\tvideo, audio *core.Receiver\n\n\tvideoTS  uint32\n\tvideoDT  uint32\n\taudioTS  uint32\n\taudioSeq uint16\n}\n\nfunc (c *Producer) Start() error {\n\tfor {\n\t\tpType, b, err := c.client.ReadPacket()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t//log.Printf(\"[DVR] type: %d, len: %d\", dataType, len(b))\n\n\t\tswitch pType {\n\t\tcase 0xFC, 0xFE, 0xFD:\n\t\t\tif c.video == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tvar payload []byte\n\t\t\tif pType != 0xFD {\n\t\t\t\tpayload = b[16:] // iframe\n\t\t\t} else {\n\t\t\t\tpayload = b[8:] // pframe\n\t\t\t}\n\n\t\t\tc.videoTS += c.videoDT\n\n\t\t\tpacket := &rtp.Packet{\n\t\t\t\tHeader:  rtp.Header{Timestamp: c.videoTS},\n\t\t\t\tPayload: annexb.EncodeToAVCC(payload),\n\t\t\t}\n\n\t\t\t//log.Printf(\"[AVC] %v, len: %d, ts: %10d\", h265.Types(payload), len(payload), packet.Timestamp)\n\n\t\t\tc.video.WriteRTP(packet)\n\n\t\tcase 0xFA: // audio\n\t\t\tif c.audio == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tpayload := b[8:]\n\n\t\t\tc.audioTS += uint32(len(payload))\n\t\t\tc.audioSeq++\n\n\t\t\tpacket := &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\tSequenceNumber: c.audioSeq,\n\t\t\t\t\tTimestamp:      c.audioTS,\n\t\t\t\t},\n\t\t\t\tPayload: payload,\n\t\t\t}\n\n\t\t\t//log.Printf(\"[DVR] len: %d, ts: %10d\", len(packet.Payload), packet.Timestamp)\n\n\t\t\tc.audio.WriteRTP(packet)\n\n\t\tcase 0xF9: // unknown\n\n\t\tdefault:\n\t\t\tprintln(fmt.Sprintf(\"dvrip: unknown packet type: %d\", pType))\n\t\t}\n\t}\n}\n\nfunc (c *Producer) probe() error {\n\tif err := c.client.Play(); err != nil {\n\t\treturn err\n\t}\n\n\trd := core.NewReadBuffer(c.client.rd)\n\trd.BufferSize = core.ProbeSize\n\tdefer func() {\n\t\tc.client.buf = nil\n\t\trd.Reset()\n\t}()\n\n\tc.client.rd = rd\n\n\t// some awful cameras has VERY rare keyframes\n\t// so we wait video+audio for default probe time\n\t// and wait anything for 15 seconds\n\ttimeoutBoth := time.Now().Add(core.ProbeTimeout)\n\ttimeoutAny := time.Now().Add(time.Second * 15)\n\n\tfor {\n\t\tif now := time.Now(); now.Before(timeoutBoth) {\n\t\t\tif c.video != nil && c.audio != nil {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t} else if now.Before(timeoutAny) {\n\t\t\tif c.video != nil || c.audio != nil {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t} else {\n\t\t\treturn errors.New(\"dvrip: can't probe medias\")\n\t\t}\n\n\t\ttag, b, err := c.client.ReadPacket()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tswitch tag {\n\t\tcase 0xFC, 0xFE: // video\n\t\t\tif c.video != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tfps := b[5]\n\t\t\t//width := uint16(b[6]) * 8\n\t\t\t//height := uint16(b[7]) * 8\n\t\t\t//println(width, height)\n\t\t\tts := b[8:]\n\n\t\t\t// the exact value of the start TS does not matter\n\t\t\tc.videoTS = binary.LittleEndian.Uint32(ts)\n\t\t\tc.videoDT = 90000 / uint32(fps)\n\n\t\t\tpayload := annexb.EncodeToAVCC(b[16:])\n\t\t\tc.addVideoTrack(b[4], payload)\n\n\t\tcase 0xFA: // audio\n\t\t\tif c.audio != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// the exact value of the start TS does not matter\n\t\t\tc.audioTS = c.videoTS\n\n\t\t\tc.addAudioTrack(b[4], b[5])\n\t\t}\n\t}\n}\n\nfunc (c *Producer) addVideoTrack(mediaCode byte, payload []byte) {\n\tvar codec *core.Codec\n\tswitch mediaCode {\n\tcase 0x02, 0x12:\n\t\tcodec = &core.Codec{\n\t\t\tName:        core.CodecH264,\n\t\t\tClockRate:   90000,\n\t\t\tPayloadType: core.PayloadTypeRAW,\n\t\t\tFmtpLine:    h264.GetFmtpLine(payload),\n\t\t}\n\n\tcase 0x03, 0x13, 0x43, 0x53:\n\t\tcodec = &core.Codec{\n\t\t\tName:        core.CodecH265,\n\t\t\tClockRate:   90000,\n\t\t\tPayloadType: core.PayloadTypeRAW,\n\t\t\tFmtpLine:    \"profile-id=1\",\n\t\t}\n\n\t\tfor {\n\t\t\tsize := 4 + int(binary.BigEndian.Uint32(payload))\n\n\t\t\tswitch h265.NALUType(payload) {\n\t\t\tcase h265.NALUTypeVPS:\n\t\t\t\tcodec.FmtpLine += \";sprop-vps=\" + base64.StdEncoding.EncodeToString(payload[4:size])\n\t\t\tcase h265.NALUTypeSPS:\n\t\t\t\tcodec.FmtpLine += \";sprop-sps=\" + base64.StdEncoding.EncodeToString(payload[4:size])\n\t\t\tcase h265.NALUTypePPS:\n\t\t\t\tcodec.FmtpLine += \";sprop-pps=\" + base64.StdEncoding.EncodeToString(payload[4:size])\n\t\t\t}\n\n\t\t\tif size < len(payload) {\n\t\t\t\tpayload = payload[size:]\n\t\t\t} else {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\tdefault:\n\t\tprintln(\"[DVRIP] unsupported video codec:\", mediaCode)\n\t\treturn\n\t}\n\n\tmedia := &core.Media{\n\t\tKind:      core.KindVideo,\n\t\tDirection: core.DirectionRecvonly,\n\t\tCodecs:    []*core.Codec{codec},\n\t}\n\tc.Medias = append(c.Medias, media)\n\n\tc.video = core.NewReceiver(media, codec)\n\tc.Receivers = append(c.Receivers, c.video)\n}\n\nvar sampleRates = []uint32{4000, 8000, 11025, 16000, 20000, 22050, 32000, 44100, 48000}\n\nfunc (c *Producer) addAudioTrack(mediaCode byte, sampleRate byte) {\n\t// https://github.com/vigoss30611/buildroot-ltc/blob/master/system/qm/ipc/ProtocolService/src/ZhiNuo/inc/zn_dh_base_type.h\n\t// PCM8 = 7, G729, IMA_ADPCM, G711U, G721, PCM8_VWIS, MS_ADPCM, G711A, PCM16\n\tvar codec *core.Codec\n\tswitch mediaCode {\n\tcase 10: // G711U\n\t\tcodec = &core.Codec{\n\t\t\tName: core.CodecPCMU,\n\t\t}\n\tcase 14: // G711A\n\t\tcodec = &core.Codec{\n\t\t\tName: core.CodecPCMA,\n\t\t}\n\tdefault:\n\t\tprintln(\"[DVRIP] unsupported audio codec:\", mediaCode)\n\t\treturn\n\t}\n\n\tif sampleRate <= byte(len(sampleRates)) {\n\t\tcodec.ClockRate = sampleRates[sampleRate-1]\n\t}\n\n\tmedia := &core.Media{\n\t\tKind:      core.KindAudio,\n\t\tDirection: core.DirectionRecvonly,\n\t\tCodecs:    []*core.Codec{codec},\n\t}\n\tc.Medias = append(c.Medias, media)\n\n\tc.audio = core.NewReceiver(media, codec)\n\tc.Receivers = append(c.Receivers, c.audio)\n}\n\n//func (c *Client) MarshalJSON() ([]byte, error) {\n//\tinfo := &core.Info{\n//\t\tType:       \"DVRIP active producer\",\n//\t\tRemoteAddr: c.conn.RemoteAddr().String(),\n//\t\tMedias:     c.Medias,\n//\t\tReceivers:  c.Receivers,\n//\t\tRecv:       c.Recv,\n//\t}\n//\treturn json.Marshal(info)\n//}\n"
  },
  {
    "path": "pkg/eseecloud/eseecloud.go",
    "content": "package eseecloud\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264/annexb\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Producer struct {\n\tcore.Connection\n\trd *core.ReadBuffer\n\n\tvideoPT, audioPT uint8\n}\n\nfunc Dial(rawURL string) (core.Producer, error) {\n\trawURL, _ = strings.CutPrefix(rawURL, \"eseecloud\")\n\tres, err := http.Get(\"http\" + rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tprod, err := Open(res.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif info, ok := prod.(core.Info); ok {\n\t\tinfo.SetProtocol(\"http\")\n\t\tinfo.SetURL(rawURL)\n\t}\n\n\treturn prod, nil\n}\n\nfunc Open(r io.Reader) (core.Producer, error) {\n\tprod := &Producer{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"eseecloud\",\n\t\t\tTransport:  r,\n\t\t},\n\t\trd: core.NewReadBuffer(r),\n\t}\n\n\tif err := prod.probe(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn prod, nil\n}\n\nfunc (p *Producer) probe() error {\n\tb, err := p.rd.Peek(1024)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ti := bytes.Index(b, []byte(\"\\r\\n\\r\\n\"))\n\tif i == -1 {\n\t\treturn io.EOF\n\t}\n\n\tb = make([]byte, i+4)\n\t_, _ = p.rd.Read(b)\n\n\tre := regexp.MustCompile(`m=(video|audio) (\\d+) (\\w+)/(\\d+)\\S*`)\n\tfor _, item := range re.FindAllStringSubmatch(string(b), 2) {\n\t\tp.SDP += item[0] + \"\\n\"\n\n\t\tswitch item[3] {\n\t\tcase \"H264\", \"H265\":\n\t\t\tp.Medias = append(p.Medias, &core.Media{\n\t\t\t\tKind:      core.KindVideo,\n\t\t\t\tDirection: core.DirectionRecvonly,\n\t\t\t\tCodecs: []*core.Codec{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:        item[3],\n\t\t\t\t\t\tClockRate:   90000,\n\t\t\t\t\t\tPayloadType: core.PayloadTypeRAW,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t})\n\t\t\tp.videoPT = byte(core.Atoi(item[2]))\n\n\t\tcase \"G711\":\n\t\t\tp.Medias = append(p.Medias, &core.Media{\n\t\t\t\tKind:      core.KindAudio,\n\t\t\t\tDirection: core.DirectionRecvonly,\n\t\t\t\tCodecs: []*core.Codec{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:      core.CodecPCMA,\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\tp.audioPT = byte(core.Atoi(item[2]))\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (p *Producer) Start() error {\n\treceivers := make(map[uint8]*core.Receiver)\n\n\tfor _, receiver := range p.Receivers {\n\t\tswitch receiver.Codec.Kind() {\n\t\tcase core.KindVideo:\n\t\t\treceivers[p.videoPT] = receiver\n\t\tcase core.KindAudio:\n\t\t\treceivers[p.audioPT] = receiver\n\t\t}\n\t}\n\n\tfor {\n\t\tpkt, err := p.readPacket()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif recv := receivers[pkt.PayloadType]; recv != nil {\n\t\t\tswitch recv.Codec.Name {\n\t\t\tcase core.CodecH264, core.CodecH265:\n\t\t\t\t// timestamp = seconds x 1000000\n\t\t\t\tpkt = &rtp.Packet{\n\t\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\t\tTimestamp: uint32(uint64(pkt.Timestamp) * 90000 / 1000000),\n\t\t\t\t\t},\n\t\t\t\t\tPayload: annexb.EncodeToAVCC(pkt.Payload),\n\t\t\t\t}\n\t\t\tcase core.CodecPCMA:\n\t\t\t\tpkt = &rtp.Packet{\n\t\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\t\tVersion:        2,\n\t\t\t\t\t\tSequenceNumber: pkt.SequenceNumber,\n\t\t\t\t\t\tTimestamp:      uint32(uint64(pkt.Timestamp) * 8000 / 1000000),\n\t\t\t\t\t},\n\t\t\t\t\tPayload: pkt.Payload,\n\t\t\t\t}\n\t\t\t}\n\t\t\trecv.WriteRTP(pkt)\n\t\t}\n\t}\n}\n\nfunc (p *Producer) readPacket() (*core.Packet, error) {\n\tb := make([]byte, 8)\n\n\tif _, err := io.ReadFull(p.rd, b); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif b[0] != '$' {\n\t\treturn nil, errors.New(\"eseecloud: wrong start byte\")\n\t}\n\n\tsize := binary.BigEndian.Uint32(b[4:])\n\tb = make([]byte, size)\n\tif _, err := io.ReadFull(p.rd, b); err != nil {\n\t\treturn nil, err\n\t}\n\n\tpkt := &core.Packet{}\n\tif err := pkt.Unmarshal(b); err != nil {\n\t\treturn nil, err\n\t}\n\n\tp.Recv += int(size)\n\n\treturn pkt, nil\n}\n"
  },
  {
    "path": "pkg/expr/expr.go",
    "content": "package expr\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/cookiejar\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/expr-lang/expr\"\n\t\"github.com/expr-lang/expr/vm\"\n)\n\nfunc newRequest(rawURL string, options map[string]any) (*http.Request, error) {\n\tvar method, contentType string\n\tvar rd io.Reader\n\n\t// method from js fetch\n\tif s, ok := options[\"method\"].(string); ok {\n\t\tmethod = s\n\t} else {\n\t\tmethod = \"GET\"\n\t}\n\n\t// params key from python requests\n\tif kv, ok := options[\"params\"].(map[string]any); ok {\n\t\trawURL += \"?\" + url.Values(kvToString(kv)).Encode()\n\t}\n\n\t// json key from python requests\n\t// data key from python requests\n\t// body key from js fetch\n\tif v, ok := options[\"json\"]; ok {\n\t\tb, err := json.Marshal(v)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcontentType = \"application/json\"\n\t\trd = bytes.NewReader(b)\n\t} else if kv, ok := options[\"data\"].(map[string]any); ok {\n\t\tcontentType = \"application/x-www-form-urlencoded\"\n\t\trd = strings.NewReader(url.Values(kvToString(kv)).Encode())\n\t} else if s, ok := options[\"body\"].(string); ok {\n\t\trd = strings.NewReader(s)\n\t}\n\n\treq, err := http.NewRequest(method, rawURL, rd)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif kv, ok := options[\"headers\"].(map[string]any); ok {\n\t\treq.Header = kvToString(kv)\n\t}\n\n\tif contentType != \"\" && req.Header.Get(\"Content-Type\") == \"\" {\n\t\treq.Header.Set(\"Content-Type\", contentType)\n\t}\n\n\treturn req, nil\n}\n\nfunc kvToString(kv map[string]any) map[string][]string {\n\tdst := make(map[string][]string, len(kv))\n\tfor k, v := range kv {\n\t\tdst[k] = []string{fmt.Sprintf(\"%v\", v)}\n\t}\n\treturn dst\n}\n\nfunc regExp(params ...any) (*regexp.Regexp, error) {\n\texp := params[0].(string)\n\tif len(params) >= 2 {\n\t\t// support:\n\t\t//   i  case-insensitive (default false)\n\t\t//   m  multi-line mode: ^ and $ match begin/end line (default false)\n\t\t//   s  let . match \\n (default false)\n\t\t// https://pkg.go.dev/regexp/syntax\n\t\tflags := params[1].(string)\n\t\texp = \"(?\" + flags + \")\" + exp\n\t}\n\treturn regexp.Compile(exp)\n}\n\nfunc Compile(input string) (*vm.Program, error) {\n\t// support http sessions\n\tjar, _ := cookiejar.New(nil)\n\tclient := http.Client{\n\t\tJar:     jar,\n\t\tTimeout: 5 * time.Second,\n\t}\n\n\treturn expr.Compile(\n\t\tinput,\n\t\texpr.Function(\n\t\t\t\"fetch\",\n\t\t\tfunc(params ...any) (any, error) {\n\t\t\t\tvar req *http.Request\n\t\t\t\tvar err error\n\n\t\t\t\trawURL := params[0].(string)\n\n\t\t\t\tif len(params) == 2 {\n\t\t\t\t\toptions := params[1].(map[string]any)\n\t\t\t\t\treq, err = newRequest(rawURL, options)\n\t\t\t\t} else {\n\t\t\t\t\treq, err = http.NewRequest(\"GET\", rawURL, nil)\n\t\t\t\t}\n\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\tres, err := client.Do(req)\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\tb, _ := io.ReadAll(res.Body)\n\n\t\t\t\treturn map[string]any{\n\t\t\t\t\t\"ok\":     res.StatusCode < 400,\n\t\t\t\t\t\"status\": res.Status,\n\t\t\t\t\t\"text\":   string(b),\n\t\t\t\t\t\"json\": func() (v any) {\n\t\t\t\t\t\t_ = json.Unmarshal(b, &v)\n\t\t\t\t\t\treturn\n\t\t\t\t\t},\n\t\t\t\t}, nil\n\t\t\t},\n\t\t\t//new(func(url string) map[string]any),\n\t\t\t//new(func(url string, options map[string]any) map[string]any),\n\t\t),\n\t\texpr.Function(\n\t\t\t\"match\",\n\t\t\tfunc(params ...any) (any, error) {\n\t\t\t\tre, err := regExp(params[1:]...)\n\t\t\t\tif err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\t\t\t\tstr := params[0].(string)\n\t\t\t\treturn re.FindStringSubmatch(str), nil\n\t\t\t},\n\t\t\t//new(func(str, expr string) []string),\n\t\t\t//new(func(str, expr, flags string) []string),\n\t\t),\n\t)\n}\n\nfunc Eval(input string, env any) (any, error) {\n\tprogram, err := Compile(input)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn expr.Run(program, env)\n}\n\nfunc Run(program *vm.Program, env any) (any, error) {\n\treturn vm.Run(program, env)\n}\n"
  },
  {
    "path": "pkg/expr/expr_test.go",
    "content": "package expr\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestMatchHost(t *testing.T) {\n\tv, err := Eval(`\nlet url = \"rtsp://user:pass@192.168.1.123/cam/realmonitor?...\";\nlet host = match(url, \"//[^/]+\")[0][2:];\nhost\n`, nil)\n\trequire.Nil(t, err)\n\trequire.Equal(t, \"user:pass@192.168.1.123\", v)\n}\n"
  },
  {
    "path": "pkg/ffmpeg/README.md",
    "content": "## FFplay output\n\n[FFplay](https://stackoverflow.com/questions/27778678/what-are-mv-fd-aq-vq-sq-and-f-in-a-video-stream) `7.11 A-V:  0.003 fd=   1 aq=   21KB vq=  321KB sq=    0B f=0/0`:\n\n- `7.11` - master clock, is the time from start of the stream/video\n- `A-V` - av_diff, difference between audio and video timestamps\n- `fd` - frames dropped\n- `aq` - audio queue (0 - no delay)\n- `vq` - video queue (0 - no delay)\n- `sq` - subtitle queue\n- `f` - timestamp error correction rate (Not 100% sure)\n\n`M-V`, `M-A` means video stream only, audio stream only respectively.\n\n## Devices Windows\n\n```\n>ffmpeg -hide_banner -f dshow -list_options true -i video=\"VMware Virtual USB Video Device\"\n[dshow @ 0000025695e52900] DirectShow video device options (from video devices)\n[dshow @ 0000025695e52900]  Pin \"Record\" (alternative pin name \"0\")\n[dshow @ 0000025695e52900]   pixel_format=yuyv422  min s=1280x720 fps=1 max s=1280x720 fps=10\n[dshow @ 0000025695e52900]   pixel_format=yuyv422  min s=1280x720 fps=1 max s=1280x720 fps=10 (tv, bt470bg/bt709/unknown, topleft)\n[dshow @ 0000025695e52900]   pixel_format=nv12  min s=1280x720 fps=1 max s=1280x720 fps=23\n[dshow @ 0000025695e52900]   pixel_format=nv12  min s=1280x720 fps=1 max s=1280x720 fps=23 (tv, bt470bg/bt709/unknown, topleft)\n```\n\n## Devices Mac\n\n```\n% ./ffmpeg -hide_banner -f avfoundation -list_devices true -i \"\"\n[AVFoundation indev @ 0x7f8b1f504d80] AVFoundation video devices:\n[AVFoundation indev @ 0x7f8b1f504d80] [0] FaceTime HD Camera\n[AVFoundation indev @ 0x7f8b1f504d80] [1] Capture screen 0\n[AVFoundation indev @ 0x7f8b1f504d80] AVFoundation audio devices:\n[AVFoundation indev @ 0x7f8b1f504d80] [0] Soundflower (2ch)\n[AVFoundation indev @ 0x7f8b1f504d80] [1] Built-in Microphone\n[AVFoundation indev @ 0x7f8b1f504d80] [2] Soundflower (64ch)\n```\n\n## Devices Linux\n\n```\n# ffmpeg -hide_banner -f v4l2 -list_formats all -i /dev/video0\n[video4linux2,v4l2 @ 0x7f7de7c58bc0] Raw       :     yuyv422 :           YUYV 4:2:2 : 640x480 160x120 176x144 320x176 320x240 352x288 432x240 544x288 640x360 752x416 800x448 800x600 864x480 960x544 960x720 1024x576 1184x656 1280x720 1280x960\n[video4linux2,v4l2 @ 0x7f7de7c58bc0] Compressed:       mjpeg :          Motion-JPEG : 640x480 160x120 176x144 320x176 320x240 352x288 432x240 544x288 640x360 752x416 800x448 800x600 864x480 960x544 960x720 1024x576 1184x656 1280x720 1280x960\n```\n\n## TTS\n\n```yaml\nstreams:\n  tts: ffmpeg:#input=-readrate 1 -readrate_initial_burst 0.001 -f lavfi -i \"flite=text='1 2 3 4 5 6 7 8 9 0'\"#audio=pcma\n```\n\n## Useful links\n\n- https://superuser.com/questions/564402/explanation-of-x264-tune\n- https://stackoverflow.com/questions/33624016/why-sliced-thread-affect-so-much-on-realtime-encoding-using-ffmpeg-x264\n- https://codec.fandom.com/ru/wiki/X264_-_описание_ключей_кодирования\n- https://html5test.com/\n- https://trac.ffmpeg.org/wiki/Capture/Webcam\n- https://trac.ffmpeg.org/wiki/DirectShow\n- https://stackoverflow.com/questions/53207692/libav-mjpeg-encoding-and-huffman-table\n- https://github.com/tuupola/esp_video/blob/master/README.md\n- https://github.com/leandromoreira/ffmpeg-libav-tutorial\n- https://www.reddit.com/user/VeritablePornocopium/comments/okw130/ffmpeg_with_libfdk_aac_for_windows_x64/\n- https://slhck.info/video/2017/02/24/vbr-settings.html\n- [HomeKit audio samples problem](https://superuser.com/questions/1290996/non-monotonous-dts-with-igndts-flag)\n"
  },
  {
    "path": "pkg/ffmpeg/ffmpeg.go",
    "content": "package ffmpeg\n\nimport (\n\t\"bytes\"\n\t\"strconv\"\n\t\"strings\"\n)\n\n// correlation of libavformat versions with ffmpeg versions\nconst (\n\tVersion50 = \"59. 16\"\n\tVersion51 = \"59. 27\"\n\tVersion60 = \"60.  3\"\n\tVersion61 = \"60. 16\"\n\tVersion70 = \"61.  1\"\n)\n\ntype Args struct {\n\tBin     string   // ffmpeg\n\tGlobal  string   // -hide_banner -v error\n\tInput   string   // -re -stream_loop -1 -i /media/bunny.mp4\n\tCodecs  []string // -c:v libx264 -g:v 30 -preset:v ultrafast -tune:v zerolatency\n\tFilters []string // scale=1920:1080\n\tOutput  string   // -f rtsp {output}\n\tVersion string   // libavformat version, it's more reliable than the ffmpeg version\n\n\tVideo, Audio int // count of Video and Audio params\n}\n\nfunc (a *Args) AddCodec(codec string) {\n\ta.Codecs = append(a.Codecs, codec)\n}\n\nfunc (a *Args) AddFilter(filter string) {\n\ta.Filters = append(a.Filters, filter)\n}\n\nfunc (a *Args) InsertFilter(filter string) {\n\ta.Filters = append([]string{filter}, a.Filters...)\n}\n\nfunc (a *Args) HasFilters(filters ...string) bool {\n\tfor _, f1 := range a.Filters {\n\t\tfor _, f2 := range filters {\n\t\t\tif strings.HasPrefix(f1, f2) {\n\t\t\t\treturn true\n\t\t\t}\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc (a *Args) String() string {\n\tb := bytes.NewBuffer(make([]byte, 0, 512))\n\n\tb.WriteString(a.Bin)\n\n\tif a.Global != \"\" {\n\t\tb.WriteByte(' ')\n\t\tb.WriteString(a.Global)\n\t}\n\n\tb.WriteByte(' ')\n\t// starting from FFmpeg 6.1 readrate=1 has default initial bust 0.5 sec\n\t// it might make us miss the first couple seconds of the file\n\tif strings.HasPrefix(a.Input, \"-re \") && a.Version >= Version61 {\n\t\tb.WriteString(\"-readrate_initial_burst 0.001 \")\n\t}\n\tb.WriteString(a.Input)\n\n\tmultimode := a.Video > 1 || a.Audio > 1\n\tvar iv, ia int\n\n\tfor _, codec := range a.Codecs {\n\t\t// support multiple video and/or audio codecs\n\t\tif multimode && len(codec) >= 5 {\n\t\t\tswitch codec[:5] {\n\t\t\tcase \"-c:v \":\n\t\t\t\tcodec = \"-map 0:v:0? \" + strings.ReplaceAll(codec, \":v \", \":v:\"+strconv.Itoa(iv)+\" \")\n\t\t\t\tiv++\n\t\t\tcase \"-c:a \":\n\t\t\t\tcodec = \"-map 0:a:0? \" + strings.ReplaceAll(codec, \":a \", \":a:\"+strconv.Itoa(ia)+\" \")\n\t\t\t\tia++\n\t\t\t}\n\t\t}\n\n\t\tb.WriteByte(' ')\n\t\tb.WriteString(codec)\n\t}\n\n\tif len(a.Filters) > 0 {\n\t\tfor i, filter := range a.Filters {\n\t\t\tif i == 0 {\n\t\t\t\tb.WriteString(` -vf \"`)\n\t\t\t} else {\n\t\t\t\tb.WriteByte(',')\n\t\t\t}\n\t\t\tb.WriteString(filter)\n\t\t}\n\t\tb.WriteByte('\"')\n\t}\n\n\tb.WriteByte(' ')\n\tb.WriteString(a.Output)\n\n\treturn b.String()\n}\n\nfunc ParseVersion(b []byte) (ffmpeg string, libavformat string) {\n\tif len(b) > 100 {\n\t\t// ffmpeg version n7.0-30-g8b0fe91754-20240520 Copyright (c) 2000-2024 the FFmpeg developers\n\t\tif i := bytes.IndexByte(b[15:], ' '); i > 0 {\n\t\t\tffmpeg = string(b[15 : 15+i])\n\t\t}\n\n\t\t// libavformat    60. 16.100 / 60. 16.100\n\t\tif i := strings.Index(string(b), \"libavformat\"); i > 0 {\n\t\t\tlibavformat = string(b[i+15 : i+25])\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "pkg/flussonic/flussonic.go",
    "content": "package flussonic\n\nimport (\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/aac\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264\"\n\t\"github.com/AlexxIT/go2rtc/pkg/iso\"\n\t\"github.com/gorilla/websocket\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Producer struct {\n\tcore.Connection\n\tconn *websocket.Conn\n\n\tvideoTrackID, audioTrackID     uint32\n\tvideoTimeScale, audioTimeScale float32\n}\n\nfunc Dial(source string) (core.Producer, error) {\n\turl, _ := strings.CutPrefix(source, \"flussonic:\")\n\tconn, _, err := websocket.DefaultDialer.Dial(url, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tprod := &Producer{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"flussonic\",\n\t\t\tProtocol:   core.Before(url, \":\"), // wss\n\t\t\tRemoteAddr: conn.RemoteAddr().String(),\n\t\t\tURL:        url,\n\t\t\tTransport:  conn,\n\t\t},\n\t\tconn: conn,\n\t}\n\n\tif err = prod.probe(); err != nil {\n\t\t_ = conn.Close()\n\t\treturn nil, err\n\t}\n\n\treturn prod, nil\n}\n\nfunc (p *Producer) probe() error {\n\tvar init struct {\n\t\t//Metadata struct {\n\t\t//\tTracks []struct {\n\t\t//\t\tWidth   int    `json:\"width,omitempty\"`\n\t\t//\t\tHeight  int    `json:\"height,omitempty\"`\n\t\t//\t\tFps     int    `json:\"fps,omitempty\"`\n\t\t//\t\tContent string `json:\"content\"`\n\t\t//\t\tTrackId string `json:\"trackId\"`\n\t\t//\t\tBitrate int    `json:\"bitrate\"`\n\t\t//\t} `json:\"tracks\"`\n\t\t//} `json:\"metadata\"`\n\t\tTracks []struct {\n\t\t\tContent string `json:\"content\"`\n\t\t\tId      uint32 `json:\"id\"`\n\t\t\tPayload []byte `json:\"payload\"`\n\t\t} `json:\"tracks\"`\n\t\t//Type string `json:\"type\"`\n\t}\n\n\tif err := p.conn.ReadJSON(&init); err != nil {\n\t\treturn err\n\t}\n\n\tvar timeScale uint32\n\n\tfor _, track := range init.Tracks {\n\t\tatoms, _ := iso.DecodeAtoms(track.Payload)\n\t\tfor _, atom := range atoms {\n\t\t\tswitch atom := atom.(type) {\n\t\t\tcase *iso.AtomMdhd:\n\t\t\t\ttimeScale = atom.TimeScale\n\t\t\tcase *iso.AtomVideo:\n\t\t\t\tswitch atom.Name {\n\t\t\t\tcase \"avc1\":\n\t\t\t\t\tcodec := h264.AVCCToCodec(atom.Config)\n\t\t\t\t\tp.Medias = append(p.Medias, &core.Media{\n\t\t\t\t\t\tKind:      core.KindVideo,\n\t\t\t\t\t\tDirection: core.DirectionRecvonly,\n\t\t\t\t\t\tCodecs:    []*core.Codec{codec},\n\t\t\t\t\t})\n\t\t\t\t\tp.videoTrackID = track.Id\n\t\t\t\t\tp.videoTimeScale = float32(codec.ClockRate) / float32(timeScale)\n\t\t\t\t}\n\t\t\tcase *iso.AtomAudio:\n\t\t\t\tswitch atom.Name {\n\t\t\t\tcase \"mp4a\":\n\t\t\t\t\tcodec := aac.ConfigToCodec(atom.Config)\n\t\t\t\t\tp.Medias = append(p.Medias, &core.Media{\n\t\t\t\t\t\tKind:      core.KindAudio,\n\t\t\t\t\t\tDirection: core.DirectionRecvonly,\n\t\t\t\t\t\tCodecs:    []*core.Codec{codec},\n\t\t\t\t\t})\n\t\t\t\t\tp.audioTrackID = track.Id\n\t\t\t\t\tp.audioTimeScale = float32(codec.ClockRate) / float32(timeScale)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (p *Producer) Start() error {\n\tif err := p.conn.WriteMessage(websocket.TextMessage, []byte(\"resume\")); err != nil {\n\t\treturn err\n\t}\n\n\treceivers := make(map[uint32]*core.Receiver)\n\ttimeScales := make(map[uint32]float32)\n\n\tfor _, receiver := range p.Receivers {\n\t\tswitch receiver.Codec.Kind() {\n\t\tcase core.KindVideo:\n\t\t\treceivers[p.videoTrackID] = receiver\n\t\t\ttimeScales[p.videoTrackID] = p.videoTimeScale\n\t\tcase core.KindAudio:\n\t\t\treceivers[p.audioTrackID] = receiver\n\t\t\ttimeScales[p.audioTrackID] = p.audioTimeScale\n\t\t}\n\t}\n\n\tch := make(chan []byte, 10)\n\tdefer close(ch)\n\n\tgo func() {\n\t\tfor b := range ch {\n\t\t\tatoms, err := iso.DecodeAtoms(b)\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tvar trackID uint32\n\t\t\tvar decodeTime uint64\n\n\t\t\tfor _, atom := range atoms {\n\t\t\t\tswitch atom := atom.(type) {\n\t\t\t\tcase *iso.AtomTfhd:\n\t\t\t\t\ttrackID = atom.TrackID\n\t\t\t\tcase *iso.AtomTfdt:\n\t\t\t\t\tdecodeTime = atom.DecodeTime\n\t\t\t\tcase *iso.AtomMdat:\n\t\t\t\t\tb = atom.Data\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif recv := receivers[trackID]; recv != nil {\n\t\t\t\ttimestamp := uint32(float32(decodeTime) * timeScales[trackID])\n\t\t\t\tpacket := &rtp.Packet{\n\t\t\t\t\tHeader:  rtp.Header{Timestamp: timestamp},\n\t\t\t\t\tPayload: b,\n\t\t\t\t}\n\t\t\t\trecv.WriteRTP(packet)\n\t\t\t}\n\t\t}\n\t}()\n\n\tfor {\n\t\tmType, b, err := p.conn.ReadMessage()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif mType == websocket.BinaryMessage {\n\t\t\tp.Recv += len(b)\n\t\t\tch <- b\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/flv/amf/amf.go",
    "content": "package amf\n\nimport (\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"math\"\n)\n\nconst (\n\tTypeNumber byte = iota\n\tTypeBoolean\n\tTypeString\n\tTypeObject\n\tTypeNull      = 5\n\tTypeEcmaArray = 8\n\tTypeObjectEnd = 9\n)\n\n// AMF spec: http://download.macromedia.com/pub/labs/amf/amf0_spec_121207.pdf\ntype AMF struct {\n\tbuf []byte\n\tpos int\n}\n\nvar ErrRead = errors.New(\"amf: read error\")\n\nfunc NewReader(b []byte) *AMF {\n\treturn &AMF{buf: b}\n}\n\nfunc (a *AMF) ReadItems() ([]any, error) {\n\tvar items []any\n\tfor a.pos < len(a.buf) {\n\t\tv, err := a.ReadItem()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\titems = append(items, v)\n\t}\n\treturn items, nil\n}\n\nfunc (a *AMF) ReadItem() (any, error) {\n\tdataType, err := a.ReadByte()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tswitch dataType {\n\tcase TypeNumber:\n\t\treturn a.ReadNumber()\n\n\tcase TypeBoolean:\n\t\tb, err := a.ReadByte()\n\t\treturn b != 0, err\n\n\tcase TypeString:\n\t\treturn a.ReadString()\n\n\tcase TypeObject:\n\t\treturn a.ReadObject()\n\n\tcase TypeEcmaArray:\n\t\treturn a.ReadEcmaArray()\n\n\tcase TypeNull:\n\t\treturn nil, nil\n\n\tcase TypeObjectEnd:\n\t\treturn nil, nil\n\t}\n\n\treturn nil, ErrRead\n}\n\nfunc (a *AMF) ReadByte() (byte, error) {\n\tif a.pos >= len(a.buf) {\n\t\treturn 0, ErrRead\n\t}\n\n\tv := a.buf[a.pos]\n\ta.pos++\n\treturn v, nil\n}\n\nfunc (a *AMF) ReadNumber() (float64, error) {\n\tif a.pos+8 > len(a.buf) {\n\t\treturn 0, ErrRead\n\t}\n\n\tv := binary.BigEndian.Uint64(a.buf[a.pos : a.pos+8])\n\ta.pos += 8\n\treturn math.Float64frombits(v), nil\n}\n\nfunc (a *AMF) ReadString() (string, error) {\n\tif a.pos+2 > len(a.buf) {\n\t\treturn \"\", ErrRead\n\t}\n\n\tsize := int(binary.BigEndian.Uint16(a.buf[a.pos:]))\n\ta.pos += 2\n\n\tif a.pos+size > len(a.buf) {\n\t\treturn \"\", ErrRead\n\t}\n\n\ts := string(a.buf[a.pos : a.pos+size])\n\ta.pos += size\n\n\treturn s, nil\n}\n\nfunc (a *AMF) ReadObject() (map[string]any, error) {\n\tobj := make(map[string]any)\n\n\tfor {\n\t\tk, err := a.ReadString()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tv, err := a.ReadItem()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif k == \"\" {\n\t\t\tbreak\n\t\t}\n\n\t\tobj[k] = v\n\t}\n\n\treturn obj, nil\n}\n\nfunc (a *AMF) ReadEcmaArray() (map[string]any, error) {\n\tif a.pos+4 > len(a.buf) {\n\t\treturn nil, ErrRead\n\t}\n\ta.pos += 4 // skip size\n\n\treturn a.ReadObject()\n}\n\nfunc NewWriter() *AMF {\n\treturn &AMF{}\n}\n\nfunc (a *AMF) Bytes() []byte {\n\treturn a.buf\n}\n\nfunc (a *AMF) WriteNumber(n float64) {\n\tb := math.Float64bits(n)\n\ta.buf = append(\n\t\ta.buf, TypeNumber,\n\t\tbyte(b>>56), byte(b>>48), byte(b>>40), byte(b>>32),\n\t\tbyte(b>>24), byte(b>>16), byte(b>>8), byte(b),\n\t)\n}\n\nfunc (a *AMF) WriteBool(b bool) {\n\tif b {\n\t\ta.buf = append(a.buf, TypeBoolean, 1)\n\t} else {\n\t\ta.buf = append(a.buf, TypeBoolean, 0)\n\t}\n}\n\nfunc (a *AMF) WriteString(s string) {\n\tn := len(s)\n\ta.buf = append(a.buf, TypeString, byte(n>>8), byte(n))\n\ta.buf = append(a.buf, s...)\n}\n\nfunc (a *AMF) WriteObject(obj map[string]any) {\n\ta.buf = append(a.buf, TypeObject)\n\ta.writeKV(obj)\n\ta.buf = append(a.buf, 0, 0, TypeObjectEnd)\n}\n\nfunc (a *AMF) WriteEcmaArray(obj map[string]any) {\n\tn := len(obj)\n\ta.buf = append(a.buf, TypeEcmaArray, byte(n>>24), byte(n>>16), byte(n>>8), byte(n))\n\ta.writeKV(obj)\n\ta.buf = append(a.buf, 0, 0, TypeObjectEnd)\n}\n\nfunc (a *AMF) writeKV(obj map[string]any) {\n\tfor k, v := range obj {\n\t\tn := len(k)\n\t\ta.buf = append(a.buf, byte(n>>8), byte(n))\n\t\ta.buf = append(a.buf, k...)\n\n\t\tswitch v := v.(type) {\n\t\tcase string:\n\t\t\ta.WriteString(v)\n\t\tcase int:\n\t\t\ta.WriteNumber(float64(v))\n\t\tcase uint16:\n\t\t\ta.WriteNumber(float64(v))\n\t\tcase uint32:\n\t\t\ta.WriteNumber(float64(v))\n\t\tcase float64:\n\t\t\ta.WriteNumber(v)\n\t\tcase bool:\n\t\t\ta.WriteBool(v)\n\t\tdefault:\n\t\t\tpanic(v)\n\t\t}\n\t}\n}\n\nfunc (a *AMF) WriteNull() {\n\ta.buf = append(a.buf, TypeNull)\n}\n\nfunc EncodeItems(items ...any) []byte {\n\ta := &AMF{}\n\tfor _, item := range items {\n\t\tswitch v := item.(type) {\n\t\tcase float64:\n\t\t\ta.WriteNumber(v)\n\t\tcase int:\n\t\t\ta.WriteNumber(float64(v))\n\t\tcase string:\n\t\t\ta.WriteString(v)\n\t\tcase map[string]any:\n\t\t\ta.WriteObject(v)\n\t\tcase nil:\n\t\t\ta.WriteNull()\n\t\tdefault:\n\t\t\tpanic(v)\n\t\t}\n\t}\n\treturn a.Bytes()\n}\n"
  },
  {
    "path": "pkg/flv/amf/amf_test.go",
    "content": "package amf\n\nimport (\n\t\"encoding/hex\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNewReader(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\tactual string\n\t\texpect []any\n\t}{\n\t\t{\n\t\t\tname:   \"ffmpeg-http\",\n\t\t\tactual: \"02000a6f6e4d65746144617461080000001000086475726174696f6e000000000000000000000577696474680040940000000000000006686569676874004086800000000000000d766964656f646174617261746500409e62770000000000096672616d6572617465004038000000000000000c766964656f636f646563696400401c000000000000000d617564696f646174617261746500405ea93000000000000f617564696f73616d706c65726174650040e5888000000000000f617564696f73616d706c6573697a65004030000000000000000673746572656f0101000c617564696f636f6465636964004024000000000000000b6d616a6f725f6272616e640200046d703432000d6d696e6f725f76657273696f6e020001300011636f6d70617469626c655f6272616e647302000c69736f6d617663316d7034320007656e636f64657202000c4c61766636302e352e313030000866696c6573697a65000000000000000000000009\",\n\t\t\texpect: []any{\n\t\t\t\t\"onMetaData\",\n\t\t\t\tmap[string]any{\n\t\t\t\t\t\"compatible_brands\": \"isomavc1mp42\",\n\t\t\t\t\t\"major_brand\":       \"mp42\",\n\t\t\t\t\t\"minor_version\":     \"0\",\n\t\t\t\t\t\"encoder\":           \"Lavf60.5.100\",\n\n\t\t\t\t\t\"filesize\": float64(0),\n\t\t\t\t\t\"duration\": float64(0),\n\n\t\t\t\t\t\"videocodecid\":  float64(7),\n\t\t\t\t\t\"width\":         float64(1280),\n\t\t\t\t\t\"height\":        float64(720),\n\t\t\t\t\t\"framerate\":     float64(24),\n\t\t\t\t\t\"videodatarate\": 1944.6162109375,\n\n\t\t\t\t\t\"audiocodecid\":    float64(10),\n\t\t\t\t\t\"audiosamplerate\": float64(44100),\n\t\t\t\t\t\"stereo\":          true,\n\t\t\t\t\t\"audiosamplesize\": float64(16),\n\t\t\t\t\t\"audiodatarate\":   122.6435546875,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"ffmpeg-file\",\n\t\t\tactual: \"02000a6f6e4d65746144617461080000000800086475726174696f6e004000000000000000000577696474680040940000000000000006686569676874004086800000000000000d766964656f646174617261746500000000000000000000096672616d6572617465004039000000000000000c766964656f636f646563696400401c0000000000000007656e636f64657202000c4c61766636302e352e313030000866696c6573697a6500411f541400000000000009\",\n\t\t\texpect: []any{\n\t\t\t\t\"onMetaData\",\n\t\t\t\tmap[string]any{\n\t\t\t\t\t\"encoder\": \"Lavf60.5.100\",\n\n\t\t\t\t\t\"filesize\": float64(513285),\n\t\t\t\t\t\"duration\": float64(2),\n\n\t\t\t\t\t\"videocodecid\":  float64(7),\n\t\t\t\t\t\"width\":         float64(1280),\n\t\t\t\t\t\"height\":        float64(720),\n\t\t\t\t\t\"framerate\":     float64(25),\n\t\t\t\t\t\"videodatarate\": float64(0),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"reolink-1\",\n\t\t\tactual: \"0200075f726573756c74003ff0000000000000030006666d7356657202000d464d532f332c302c312c313233000c6361706162696c697469657300403f0000000000000000090300056c6576656c0200067374617475730004636f646502001d4e6574436f6e6e656374696f6e2e436f6e6e6563742e53756363657373000b6465736372697074696f6e020015436f6e6e656374696f6e207375636365656465642e000e6f626a656374456e636f64696e67000000000000000000000009\",\n\t\t\texpect: []any{\n\t\t\t\t\"_result\", float64(1),\n\t\t\t\tmap[string]any{\n\t\t\t\t\t\"capabilities\": float64(31),\n\t\t\t\t\t\"fmsVer\":       \"FMS/3,0,1,123\",\n\t\t\t\t},\n\t\t\t\tmap[string]any{\n\t\t\t\t\t\"code\":           \"NetConnection.Connect.Success\",\n\t\t\t\t\t\"description\":    \"Connection succeeded.\",\n\t\t\t\t\t\"level\":          \"status\",\n\t\t\t\t\t\"objectEncoding\": float64(0),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"reolink-2\",\n\t\t\tactual: \"0200075f726573756c7400400000000000000005003ff0000000000000\",\n\t\t\texpect: []any{\n\t\t\t\t\"_result\", float64(2), nil, float64(1),\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"reolink-3\",\n\t\t\tactual: \"0200086f6e537461747573000000000000000000050300056c6576656c0200067374617475730004636f64650200144e657453747265616d2e506c61792e5374617274000b6465736372697074696f6e020015537461727420766964656f206f6e2064656d616e64000009\",\n\t\t\texpect: []any{\n\t\t\t\t\"onStatus\", float64(0), nil,\n\t\t\t\tmap[string]any{\n\t\t\t\t\t\"code\":        \"NetStream.Play.Start\",\n\t\t\t\t\t\"description\": \"Start video on demand\",\n\t\t\t\t\t\"level\":       \"status\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"reolink-4\",\n\t\t\tactual: \"0200117c52746d7053616d706c6541636365737301010101\",\n\t\t\texpect: []any{\n\t\t\t\t\"|RtmpSampleAccess\", true, true,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"reolink-5\",\n\t\t\tactual: \"02000a6f6e4d6574614461746103000577696474680040a4000000000000000668656967687400409e000000000000000c646973706c617957696474680040a4000000000000000d646973706c617948656967687400409e00000000000000086475726174696f6e000000000000000000000c766964656f636f646563696400401c000000000000000c617564696f636f6465636964004024000000000000000f617564696f73616d706c65726174650040cf40000000000000096672616d657261746500403e000000000000000009\",\n\t\t\texpect: []any{\n\t\t\t\t\"onMetaData\",\n\t\t\t\tmap[string]any{\n\t\t\t\t\t\"duration\": float64(0),\n\n\t\t\t\t\t\"videocodecid\":  float64(7),\n\t\t\t\t\t\"width\":         float64(2560),\n\t\t\t\t\t\"height\":        float64(1920),\n\t\t\t\t\t\"displayWidth\":  float64(2560),\n\t\t\t\t\t\"displayHeight\": float64(1920),\n\t\t\t\t\t\"framerate\":     float64(30),\n\n\t\t\t\t\t\"audiocodecid\":    float64(10),\n\t\t\t\t\t\"audiosamplerate\": float64(16000),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"mediamtx\",\n\t\t\tactual: \"02000d40736574446174614672616d6502000a6f6e4d6574614461746103000d766964656f6461746172617465000000000000000000000c766964656f636f646563696400401c000000000000000d617564696f6461746172617465000000000000000000000c617564696f636f6465636964004024000000000000000009\",\n\t\t\texpect: []any{\n\t\t\t\t\"@setDataFrame\",\n\t\t\t\t\"onMetaData\",\n\t\t\t\tmap[string]any{\n\t\t\t\t\t\"videocodecid\":  float64(7),\n\t\t\t\t\t\"videodatarate\": float64(0),\n\t\t\t\t\t\"audiocodecid\":  float64(10),\n\t\t\t\t\t\"audiodatarate\": float64(0),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"mediamtx\",\n\t\t\tactual: \"0200075f726573756c74003ff0000000000000030006666d7356657202000d4c4e5820392c302c3132342c32000c6361706162696c697469657300403f0000000000000000090300056c6576656c0200067374617475730004636f646502001d4e6574436f6e6e656374696f6e2e436f6e6e6563742e53756363657373000b6465736372697074696f6e020015436f6e6e656374696f6e207375636365656465642e000e6f626a656374456e636f64696e67000000000000000000000009\",\n\t\t\texpect: []any{\n\t\t\t\t\"_result\", float64(1), map[string]any{\n\t\t\t\t\t\"capabilities\": float64(31),\n\t\t\t\t\t\"fmsVer\":       \"LNX 9,0,124,2\",\n\t\t\t\t}, map[string]any{\n\t\t\t\t\t\"code\":           \"NetConnection.Connect.Success\",\n\t\t\t\t\t\"description\":    \"Connection succeeded.\",\n\t\t\t\t\t\"level\":          \"status\",\n\t\t\t\t\t\"objectEncoding\": float64(0),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"mediamtx\",\n\t\t\tactual: \"0200075f726573756c7400401000000000000005003ff0000000000000\",\n\t\t\texpect: []any{\"_result\", float64(4), any(nil), float64(1)},\n\t\t},\n\t\t{\n\t\t\tname:   \"mediamtx\",\n\t\t\tactual: \"0200086f6e537461747573004014000000000000050300056c6576656c0200067374617475730004636f64650200144e657453747265616d2e506c61792e5265736574000b6465736372697074696f6e02000a706c6179207265736574000009\",\n\t\t\texpect: []any{\n\t\t\t\t\"onStatus\", float64(5), any(nil), map[string]any{\n\t\t\t\t\t\"code\":        \"NetStream.Play.Reset\",\n\t\t\t\t\t\"description\": \"play reset\",\n\t\t\t\t\t\"level\":       \"status\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"mediamtx\",\n\t\t\tactual: \"0200086f6e537461747573004014000000000000050300056c6576656c0200067374617475730004636f64650200144e657453747265616d2e506c61792e5374617274000b6465736372697074696f6e02000a706c6179207374617274000009\",\n\t\t\texpect: []any{\n\t\t\t\t\"onStatus\", float64(5), any(nil), map[string]any{\n\t\t\t\t\t\"code\":        \"NetStream.Play.Start\",\n\t\t\t\t\t\"description\": \"play start\",\n\t\t\t\t\t\"level\":       \"status\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"mediamtx\",\n\t\t\tactual: \"0200086f6e537461747573004014000000000000050300056c6576656c0200067374617475730004636f64650200144e657453747265616d2e446174612e5374617274000b6465736372697074696f6e02000a64617461207374617274000009\",\n\t\t\texpect: []any{\n\t\t\t\t\"onStatus\", float64(5), any(nil), map[string]any{\n\t\t\t\t\t\"code\":        \"NetStream.Data.Start\",\n\t\t\t\t\t\"description\": \"data start\",\n\t\t\t\t\t\"level\":       \"status\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"mediamtx\",\n\t\t\tactual: \"0200086f6e537461747573004014000000000000050300056c6576656c0200067374617475730004636f646502001c4e657453747265616d2e506c61792e5075626c6973684e6f74696679000b6465736372697074696f6e02000e7075626c697368206e6f74696679000009\",\n\t\t\texpect: []any{\n\t\t\t\t\"onStatus\", float64(5), any(nil), map[string]any{\n\t\t\t\t\t\"code\":        \"NetStream.Play.PublishNotify\",\n\t\t\t\t\t\"description\": \"publish notify\",\n\t\t\t\t\t\"level\":       \"status\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"obs-connect\",\n\t\t\tactual: \"020007636f6e6e656374003ff000000000000003000361707002000c617070312f73747265616d3100047479706502000a6e6f6e70726976617465000e737570706f727473476f4177617901010008666c61736856657202001f464d4c452f332e302028636f6d70617469626c653b20464d53632f312e3029000673776655726c02002272746d703a2f2f3139322e3136382e31302e3130312f617070312f73747265616d310005746355726c02002272746d703a2f2f3139322e3136382e31302e3130312f617070312f73747265616d31000009\",\n\t\t\texpect: []any{\n\t\t\t\t\"connect\", float64(1),\n\t\t\t\tmap[string]any{\n\t\t\t\t\t\"app\":            \"app1/stream1\",\n\t\t\t\t\t\"flashVer\":       \"FMLE/3.0 (compatible; FMSc/1.0)\",\n\t\t\t\t\t\"supportsGoAway\": true,\n\t\t\t\t\t\"swfUrl\":         \"rtmp://192.168.10.101/app1/stream1\",\n\t\t\t\t\t\"tcUrl\":          \"rtmp://192.168.10.101/app1/stream1\",\n\t\t\t\t\t\"type\":           \"nonprivate\",\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"obs-key\",\n\t\t\tactual: \"02000d72656c6561736553747265616d004000000000000000050200046b657931\",\n\t\t\texpect: []any{\n\t\t\t\t\"releaseStream\", float64(2), nil, \"key1\",\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"obs\",\n\t\t\tactual: \"02000d40736574446174614672616d6502000a6f6e4d65746144617461080000001400086475726174696f6e000000000000000000000866696c6553697a65000000000000000000000577696474680040840000000000000006686569676874004076800000000000000c766964656f636f646563696400401c000000000000000d766964656f64617461726174650040a388000000000000096672616d6572617465004039000000000000000c617564696f636f6465636964004024000000000000000d617564696f6461746172617465004064000000000000000f617564696f73616d706c65726174650040e5888000000000000f617564696f73616d706c6573697a65004030000000000000000d617564696f6368616e6e656c73004000000000000000000673746572656f01010003322e3101000003332e3101000003342e3001000003342e3101000003352e3101000003372e3101000007656e636f6465720200376f62732d6f7574707574206d6f64756c6520286c69626f62732076657273696f6e2032392e302e302d36322d6739303031323131663829000009\",\n\t\t\texpect: []any{\n\t\t\t\t\"@setDataFrame\", \"onMetaData\", map[string]any{\n\t\t\t\t\t\"2.1\":             false,\n\t\t\t\t\t\"3.1\":             false,\n\t\t\t\t\t\"4.0\":             false,\n\t\t\t\t\t\"4.1\":             false,\n\t\t\t\t\t\"5.1\":             false,\n\t\t\t\t\t\"7.1\":             false,\n\t\t\t\t\t\"audiochannels\":   float64(2),\n\t\t\t\t\t\"audiocodecid\":    float64(10),\n\t\t\t\t\t\"audiodatarate\":   float64(160),\n\t\t\t\t\t\"audiosamplerate\": float64(44100),\n\t\t\t\t\t\"audiosamplesize\": float64(16),\n\t\t\t\t\t\"duration\":        float64(0),\n\t\t\t\t\t\"encoder\":         \"obs-output module (libobs version 29.0.0-62-g9001211f8)\",\n\t\t\t\t\t\"fileSize\":        float64(0),\n\t\t\t\t\t\"framerate\":       float64(25),\n\t\t\t\t\t\"height\":          float64(360),\n\t\t\t\t\t\"stereo\":          true,\n\t\t\t\t\t\"videocodecid\":    float64(7),\n\t\t\t\t\t\"videodatarate\":   float64(2500),\n\t\t\t\t\t\"width\":           float64(640),\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"telegram-2\",\n\t\t\tactual: \"0200075f726573756c7400400000000000000005\",\n\t\t\texpect: []any{\n\t\t\t\t\"_result\", float64(2), nil,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"telegram-4\",\n\t\t\tactual: \"0200075f726573756c7400401000000000000005003ff0000000000000\",\n\t\t\texpect: []any{\n\t\t\t\t\"_result\", float64(4), nil, float64(1),\n\t\t\t},\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tb, err := hex.DecodeString(test.actual)\n\t\t\trequire.Nil(t, err)\n\n\t\t\trd := NewReader(b)\n\t\t\tv, err := rd.ReadItems()\n\t\t\trequire.Nil(t, err)\n\n\t\t\trequire.Equal(t, test.expect, v)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/flv/consumer.go",
    "content": "package flv\n\nimport (\n\t\"io\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/aac\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Consumer struct {\n\tcore.Connection\n\twr    *core.WriteBuffer\n\tmuxer *Muxer\n}\n\nfunc NewConsumer() *Consumer {\n\tmedias := []*core.Media{\n\t\t{\n\t\t\tKind:      core.KindVideo,\n\t\t\tDirection: core.DirectionSendonly,\n\t\t\tCodecs: []*core.Codec{\n\t\t\t\t{Name: core.CodecH264},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tKind:      core.KindAudio,\n\t\t\tDirection: core.DirectionSendonly,\n\t\t\tCodecs: []*core.Codec{\n\t\t\t\t{Name: core.CodecAAC},\n\t\t\t},\n\t\t},\n\t}\n\twr := core.NewWriteBuffer(nil)\n\treturn &Consumer{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"flv\",\n\t\t\tMedias:     medias,\n\t\t\tTransport:  wr,\n\t\t},\n\t\twr:    wr,\n\t\tmuxer: &Muxer{},\n\t}\n}\n\nfunc (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {\n\tsender := core.NewSender(media, track.Codec)\n\n\tswitch track.Codec.Name {\n\tcase core.CodecH264:\n\t\tpayload := c.muxer.GetPayloader(track.Codec)\n\n\t\tsender.Handler = func(pkt *rtp.Packet) {\n\t\t\tb := payload(pkt)\n\t\t\tif n, err := c.wr.Write(b); err == nil {\n\t\t\t\tc.Send += n\n\t\t\t}\n\t\t}\n\n\t\tif track.Codec.IsRTP() {\n\t\t\tsender.Handler = h264.RTPDepay(track.Codec, sender.Handler)\n\t\t} else {\n\t\t\tsender.Handler = h264.RepairAVCC(track.Codec, sender.Handler)\n\t\t}\n\n\tcase core.CodecAAC:\n\t\tpayload := c.muxer.GetPayloader(track.Codec)\n\n\t\tsender.Handler = func(pkt *rtp.Packet) {\n\t\t\tb := payload(pkt)\n\t\t\tif n, err := c.wr.Write(b); err == nil {\n\t\t\t\tc.Send += n\n\t\t\t}\n\t\t}\n\n\t\tif track.Codec.IsRTP() {\n\t\t\tsender.Handler = aac.RTPDepay(sender.Handler)\n\t\t}\n\t}\n\n\tsender.HandleRTP(track)\n\tc.Senders = append(c.Senders, sender)\n\treturn nil\n}\n\nfunc (c *Consumer) WriteTo(wr io.Writer) (int64, error) {\n\tb := c.muxer.GetInit()\n\tif _, err := wr.Write(b); err != nil {\n\t\treturn 0, err\n\t}\n\treturn c.wr.WriteTo(wr)\n}\n"
  },
  {
    "path": "pkg/flv/flv_test.go",
    "content": "package flv\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestTimeToRTP(t *testing.T) {\n\t// Reolink camera has 20 FPS\n\t// Video timestamp increases by 50ms, SampleRate 90000, RTP timestamp increases by 4500\n\t// Audio timestamp increases by 64ms, SampleRate 16000, RTP timestamp increases by 1024\n\tframeN := 1\n\tfor i := 0; i < 32; i++ {\n\t\t// 1000ms/(90000/4500) = 50ms\n\t\trequire.Equal(t, uint32(frameN*4500), TimeToRTP(uint32(frameN*50), 90000))\n\t\t// 1000ms/(16000/1024) = 64ms\n\t\trequire.Equal(t, uint32(frameN*1024), TimeToRTP(uint32(frameN*64), 16000))\n\t\tframeN *= 2\n\t}\n}\n"
  },
  {
    "path": "pkg/flv/muxer.go",
    "content": "package flv\n\nimport (\n\t\"encoding/binary\"\n\t\"encoding/hex\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/flv/amf\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Muxer struct {\n\tcodecs []*core.Codec\n}\n\nconst (\n\tFlagsVideo = 0b001\n\tFlagsAudio = 0b100\n)\n\nfunc (m *Muxer) GetInit() []byte {\n\tb := []byte{\n\t\t'F', 'L', 'V', // signature\n\t\t1,          // version\n\t\t0,          // flags (has video/audio)\n\t\t0, 0, 0, 9, // header size\n\t\t0, 0, 0, 0, // tag 0 size\n\t}\n\n\tobj := map[string]any{}\n\n\tfor _, codec := range m.codecs {\n\t\tswitch codec.Name {\n\t\tcase core.CodecH264:\n\t\t\tb[4] |= FlagsVideo\n\t\t\tobj[\"videocodecid\"] = CodecH264\n\n\t\tcase core.CodecAAC:\n\t\t\tb[4] |= FlagsAudio\n\t\t\tobj[\"audiocodecid\"] = CodecAAC\n\t\t\tobj[\"audiosamplerate\"] = codec.ClockRate\n\t\t\tobj[\"audiosamplesize\"] = 16\n\t\t\tobj[\"stereo\"] = codec.Channels == 2\n\t\t}\n\t}\n\n\tdata := amf.EncodeItems(\"@setDataFrame\", \"onMetaData\", obj)\n\tb = append(b, EncodeTag(TagData, 0, data)...)\n\n\tfor _, codec := range m.codecs {\n\t\tswitch codec.Name {\n\t\tcase core.CodecH264:\n\t\t\tsps, pps := h264.GetParameterSet(codec.FmtpLine)\n\t\t\tif len(sps) == 0 {\n\t\t\t\tsps = []byte{0x67, 0x42, 0x00, 0x0a, 0xf8, 0x41, 0xa2}\n\t\t\t} else {\n\t\t\t\th264.FixPixFmt(sps)\n\t\t\t}\n\t\t\tif len(pps) == 0 {\n\t\t\t\tpps = []byte{0x68, 0xce, 0x38, 0x80}\n\t\t\t}\n\n\t\t\tconfig := h264.EncodeConfig(sps, pps)\n\t\t\tvideo := append(encodeAVData(codec, 0), config...)\n\t\t\tb = append(b, EncodeTag(TagVideo, 0, video)...)\n\n\t\tcase core.CodecAAC:\n\t\t\ts := core.Between(codec.FmtpLine, \"config=\", \";\")\n\t\t\tconfig, _ := hex.DecodeString(s)\n\t\t\taudio := append(encodeAVData(codec, 0), config...)\n\t\t\tb = append(b, EncodeTag(TagAudio, 0, audio)...)\n\t\t}\n\t}\n\n\treturn b\n}\n\nfunc (m *Muxer) GetPayloader(codec *core.Codec) func(packet *rtp.Packet) []byte {\n\tm.codecs = append(m.codecs, codec)\n\n\tvar ts0 uint32\n\tvar k = codec.ClockRate / 1000\n\n\tswitch codec.Name {\n\tcase core.CodecH264:\n\t\tbuf := encodeAVData(codec, 1)\n\n\t\treturn func(packet *rtp.Packet) []byte {\n\t\t\tif h264.IsKeyframe(packet.Payload) {\n\t\t\t\tbuf[0] = 1<<4 | 7\n\t\t\t} else {\n\t\t\t\tbuf[0] = 2<<4 | 7\n\t\t\t}\n\n\t\t\tbuf = append(buf[:5], packet.Payload...) // reset buffer to previous place\n\n\t\t\tif ts0 == 0 {\n\t\t\t\tts0 = packet.Timestamp\n\t\t\t}\n\n\t\t\ttimeMS := (packet.Timestamp - ts0) / k\n\t\t\treturn EncodeTag(TagVideo, timeMS, buf)\n\t\t}\n\n\tcase core.CodecAAC:\n\t\tbuf := encodeAVData(codec, 1)\n\n\t\treturn func(packet *rtp.Packet) []byte {\n\t\t\tbuf = append(buf[:2], packet.Payload...)\n\n\t\t\tif ts0 == 0 {\n\t\t\t\tts0 = packet.Timestamp\n\t\t\t}\n\n\t\t\ttimeMS := (packet.Timestamp - ts0) / k\n\t\t\treturn EncodeTag(TagAudio, timeMS, buf)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc EncodeTag(tagType byte, timeMS uint32, payload []byte) []byte {\n\tpayloadSize := uint32(len(payload))\n\ttagSize := payloadSize + 11\n\n\tb := make([]byte, tagSize+4)\n\tb[0] = tagType\n\tb[1] = byte(payloadSize >> 16)\n\tb[2] = byte(payloadSize >> 8)\n\tb[3] = byte(payloadSize)\n\tb[4] = byte(timeMS >> 16)\n\tb[5] = byte(timeMS >> 8)\n\tb[6] = byte(timeMS)\n\tb[7] = byte(timeMS >> 24)\n\tcopy(b[11:], payload)\n\n\tbinary.BigEndian.PutUint32(b[tagSize:], tagSize)\n\treturn b\n}\n\nfunc encodeAVData(codec *core.Codec, isFrame byte) []byte {\n\tswitch codec.Name {\n\tcase core.CodecH264:\n\t\treturn []byte{\n\t\t\t1<<4 | 7, // keyframe + AVC\n\t\t\tisFrame,  // 0 - config, 1 - frame\n\t\t\t0, 0, 0,  // composition time = 0\n\t\t}\n\n\tcase core.CodecAAC:\n\t\tvar b0 byte = 10 << 4 // AAC\n\n\t\tswitch codec.ClockRate {\n\t\tcase 11025:\n\t\t\tb0 |= 1 << 2\n\t\tcase 22050:\n\t\t\tb0 |= 2 << 2\n\t\tcase 44100:\n\t\t\tb0 |= 3 << 2\n\t\t}\n\n\t\tb0 |= 1 << 1 // 16 bits\n\n\t\tif codec.Channels == 2 {\n\t\t\tb0 |= 1\n\t\t}\n\n\t\treturn []byte{b0, isFrame} // 0 - config, 1 - frame\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/flv/producer.go",
    "content": "package flv\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"io\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/aac\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h265\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Producer struct {\n\tcore.Connection\n\trd *core.ReadBuffer\n\n\tvideo, audio *core.Receiver\n}\n\nfunc Open(rd io.Reader) (*Producer, error) {\n\tprod := &Producer{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"flv\",\n\t\t\tTransport:  rd,\n\t\t},\n\t\trd: core.NewReadBuffer(rd),\n\t}\n\tif err := prod.probe(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn prod, nil\n}\n\nconst (\n\tSignature = \"FLV\"\n\n\tTagAudio = 8\n\tTagVideo = 9\n\tTagData  = 18\n\n\tCodecAAC = 10\n\n\tCodecH264 = 7\n\tCodecHEVC = 12\n)\n\nconst (\n\tPacketTypeAVCHeader = iota\n\tPacketTypeAVCNALU\n\tPacketTypeAVCEnd\n)\n\nconst (\n\tPacketTypeSequenceStart = iota\n\tPacketTypeCodedFrames\n\tPacketTypeSequenceEnd\n\tPacketTypeCodedFramesX\n\tPacketTypeMetadata\n\tPacketTypeMPEG2TSSequenceStart\n)\n\nfunc (c *Producer) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {\n\treceiver, _ := c.Connection.GetTrack(media, codec)\n\tif media.Kind == core.KindVideo {\n\t\tc.video = receiver\n\t} else {\n\t\tc.audio = receiver\n\t}\n\treturn receiver, nil\n}\n\nfunc (c *Producer) Start() error {\n\tfor {\n\t\tpkt, err := c.readPacket()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tc.Recv += len(pkt.Payload)\n\n\t\tswitch pkt.PayloadType {\n\t\tcase TagAudio:\n\t\t\tif c.audio == nil || pkt.Payload[1] == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tpkt.Timestamp = TimeToRTP(pkt.Timestamp, c.audio.Codec.ClockRate)\n\t\t\tpkt.Payload = pkt.Payload[2:]\n\t\t\tc.audio.WriteRTP(pkt)\n\n\t\tcase TagVideo:\n\t\t\tif c.video == nil {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif isExHeader(pkt.Payload) {\n\t\t\t\tswitch packetType := pkt.Payload[0] & 0b1111; packetType {\n\t\t\t\tcase PacketTypeCodedFrames:\n\t\t\t\t\t// frame type 4b, packet type 4b, fourCC 32b, composition time 24b\n\t\t\t\t\tpkt.Payload = pkt.Payload[8:]\n\t\t\t\tcase PacketTypeCodedFramesX:\n\t\t\t\t\t// frame type 4b, packet type 4b, fourCC 32b\n\t\t\t\t\tpkt.Payload = pkt.Payload[5:]\n\t\t\t\tdefault:\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tswitch pkt.Payload[1] {\n\t\t\t\tcase PacketTypeAVCNALU:\n\t\t\t\t\t// frame type 4b, codecID 4b, avc packet type 8b, composition time 24b\n\t\t\t\t\tpkt.Payload = pkt.Payload[5:]\n\t\t\t\tdefault:\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tpkt.Timestamp = TimeToRTP(pkt.Timestamp, c.video.Codec.ClockRate)\n\t\t\tc.video.WriteRTP(pkt)\n\t\t}\n\t}\n}\n\nfunc (c *Producer) probe() error {\n\tif err := c.readHeader(); err != nil {\n\t\treturn err\n\t}\n\n\tc.rd.BufferSize = core.ProbeSize\n\tdefer c.rd.Reset()\n\n\t// Normal software sends:\n\t// 1. Video/audio flag in header\n\t// 2. MetaData as first tag (with video/audio codec info)\n\t// 3. Video/audio headers in 2nd and 3rd tag\n\n\t// Reolink camera sends:\n\t// 1. Empty video/audio flag\n\t// 2. MedaData without stereo key for AAC\n\t// 3. Audio header after Video keyframe tag\n\n\t// OpenIPC camera (on old firmwares) sends:\n\t// 1. Empty video/audio flag\n\t// 2. No MetaData packet\n\t// 3. Sends a video packet in more than 3 seconds\n\twaitVideo := true\n\twaitAudio := true\n\ttimeout := time.Now().Add(time.Second * 5)\n\n\tfor (waitVideo || waitAudio) && time.Now().Before(timeout) {\n\t\tpkt, err := c.readPacket()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t//log.Printf(\"%d %0.20s\", pkt.PayloadType, pkt.Payload)\n\n\t\tswitch pkt.PayloadType {\n\t\tcase TagAudio:\n\t\t\tif !waitAudio {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t_ = pkt.Payload[1] // bounds\n\n\t\t\tcodecID := pkt.Payload[0] >> 4 // SoundFormat\n\t\t\t_ = pkt.Payload[0] & 0b1100    // SoundRate\n\t\t\t_ = pkt.Payload[0] & 0b0010    // SoundSize\n\t\t\t_ = pkt.Payload[0] & 0b0001    // SoundType\n\n\t\t\tif codecID != CodecAAC {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif pkt.Payload[1] != 0 { // check if header\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tcodec := aac.ConfigToCodec(pkt.Payload[2:])\n\t\t\tmedia := &core.Media{\n\t\t\t\tKind:      core.KindAudio,\n\t\t\t\tDirection: core.DirectionRecvonly,\n\t\t\t\tCodecs:    []*core.Codec{codec},\n\t\t\t}\n\t\t\tc.Medias = append(c.Medias, media)\n\t\t\twaitAudio = false\n\n\t\tcase TagVideo:\n\t\t\tif !waitVideo {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tvar codec *core.Codec\n\n\t\t\tif isExHeader(pkt.Payload) {\n\t\t\t\tif string(pkt.Payload[1:5]) != \"hvc1\" {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tif packetType := pkt.Payload[0] & 0b1111; packetType != PacketTypeSequenceStart {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tcodec = h265.ConfigToCodec(pkt.Payload[5:])\n\t\t\t} else {\n\t\t\t\t_ = pkt.Payload[0] >> 4 // FrameType\n\n\t\t\t\tif packetType := pkt.Payload[1]; packetType != PacketTypeAVCHeader { // check if header\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tswitch codecID := pkt.Payload[0] & 0b1111; codecID {\n\t\t\t\tcase CodecH264:\n\t\t\t\t\tcodec = h264.ConfigToCodec(pkt.Payload[5:])\n\t\t\t\tcase CodecHEVC:\n\t\t\t\t\tcodec = h265.ConfigToCodec(pkt.Payload[5:])\n\t\t\t\tdefault:\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmedia := &core.Media{\n\t\t\t\tKind:      core.KindVideo,\n\t\t\t\tDirection: core.DirectionRecvonly,\n\t\t\t\tCodecs:    []*core.Codec{codec},\n\t\t\t}\n\t\t\tc.Medias = append(c.Medias, media)\n\t\t\twaitVideo = false\n\n\t\tcase TagData:\n\t\t\tif !bytes.Contains(pkt.Payload, []byte(\"onMetaData\")) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t// Dahua cameras doesn't send videocodecid\n\t\t\tif !bytes.Contains(pkt.Payload, []byte(\"videocodecid\")) &&\n\t\t\t\t!bytes.Contains(pkt.Payload, []byte(\"width\")) &&\n\t\t\t\t!bytes.Contains(pkt.Payload, []byte(\"framerate\")) {\n\t\t\t\twaitVideo = false\n\t\t\t}\n\t\t\tif !bytes.Contains(pkt.Payload, []byte(\"audiocodecid\")) {\n\t\t\t\twaitAudio = false\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (c *Producer) readHeader() error {\n\tb := make([]byte, 9)\n\tif _, err := io.ReadFull(c.rd, b); err != nil {\n\t\treturn err\n\t}\n\n\tif string(b[:3]) != Signature {\n\t\treturn errors.New(\"flv: wrong header\")\n\t}\n\n\t_ = b[4] // flags (skip because unsupported by Reolink cameras)\n\n\tif skip := binary.BigEndian.Uint32(b[5:]) - 9; skip > 0 {\n\t\tif _, err := io.ReadFull(c.rd, make([]byte, skip)); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (c *Producer) readPacket() (*rtp.Packet, error) {\n\t// https://rtmp.veriskope.com/pdf/video_file_format_spec_v10.pdf\n\tb := make([]byte, 4+11)\n\tif _, err := io.ReadFull(c.rd, b); err != nil {\n\t\treturn nil, err\n\t}\n\n\tb = b[4 : 4+11] // skip previous tag size\n\n\tsize := uint32(b[1])<<16 | uint32(b[2])<<8 | uint32(b[3])\n\n\tpkt := &rtp.Packet{\n\t\tHeader: rtp.Header{\n\t\t\tPayloadType: b[0],\n\t\t\tTimestamp:   uint32(b[4])<<16 | uint32(b[5])<<8 | uint32(b[6]) | uint32(b[7])<<24,\n\t\t},\n\t\tPayload: make([]byte, size),\n\t}\n\n\tif _, err := io.ReadFull(c.rd, pkt.Payload); err != nil {\n\t\treturn nil, err\n\t}\n\n\t//log.Printf(\"[FLV] %d %.40x\", pkt.PayloadType, pkt.Payload)\n\n\treturn pkt, nil\n}\n\n// TimeToRTP convert time in milliseconds to RTP time\nfunc TimeToRTP(timeMS, clockRate uint32) uint32 {\n\t// for clockRates 90000, 16000, 8000, etc. - we can use:\n\t//     return timeMS * (clockRate / 1000)\n\t// but for clockRates 44100, 22050, 11025 - we should use:\n\treturn uint32(uint64(timeMS) * uint64(clockRate) / 1000)\n}\n\nfunc isExHeader(data []byte) bool {\n\treturn data[0]&0b1000_0000 != 0\n}\n"
  },
  {
    "path": "pkg/gopro/discovery.go",
    "content": "package gopro\n\nimport (\n\t\"net\"\n\t\"net/http\"\n\t\"regexp\"\n)\n\nfunc Discovery() (urls []string) {\n\tints, err := net.Interfaces()\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\t// The socket address for USB connections is 172.2X.1YZ.51:8080\n\t// https://gopro.github.io/OpenGoPro/http_2_0#socket-address\n\tre := regexp.MustCompile(`^172\\.2\\d\\.1\\d\\d\\.`)\n\n\tfor _, itf := range ints {\n\t\taddrs, err := itf.Addrs()\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, addr := range addrs {\n\t\t\thost := addr.String()\n\t\t\tif !re.MatchString(host) {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\thost = host[:11] + \"51\" // 172.2x.1xx.xxx\n\t\t\tres, err := http.Get(\"http://\" + host + \":8080/gopro/webcam/status\")\n\t\t\tif err != nil {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\t_ = res.Body.Close()\n\n\t\t\turls = append(urls, host)\n\t\t}\n\t}\n\n\treturn\n}\n"
  },
  {
    "path": "pkg/gopro/producer.go",
    "content": "package gopro\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/mpegts\"\n)\n\nfunc Dial(rawURL string) (*mpegts.Producer, error) {\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tr := &listener{host: u.Host}\n\n\tif err = r.command(\"/gopro/webcam/stop\"); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err = r.listen(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err = r.command(\"/gopro/webcam/start\"); err != nil {\n\t\treturn nil, err\n\t}\n\n\tprod, err := mpegts.Open(r)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tprod.FormatName = \"gopro\"\n\tprod.RemoteAddr = u.Host\n\n\treturn prod, nil\n}\n\ntype listener struct {\n\tconn    net.PacketConn\n\thost    string\n\tpacket  []byte\n\tpackets chan []byte\n}\n\nfunc (r *listener) Read(p []byte) (n int, err error) {\n\tif r.packet == nil {\n\t\tvar ok bool\n\t\tif r.packet, ok = <-r.packets; !ok {\n\t\t\treturn 0, io.EOF // channel closed\n\t\t}\n\t}\n\n\tn = copy(p, r.packet)\n\n\tif n < len(r.packet) {\n\t\tr.packet = r.packet[n:]\n\t} else {\n\t\tr.packet = nil\n\t}\n\n\treturn\n}\n\nfunc (r *listener) Close() error {\n\treturn r.conn.Close()\n}\n\nfunc (r *listener) command(api string) error {\n\tclient := &http.Client{Timeout: 5 * time.Second}\n\n\tres, err := client.Get(\"http://\" + r.host + \":8080\" + api)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_ = res.Body.Close()\n\n\tif res.StatusCode != http.StatusOK {\n\t\treturn errors.New(\"gopro: wrong response: \" + res.Status)\n\t}\n\n\treturn nil\n}\n\nfunc (r *listener) listen() (err error) {\n\tif r.conn, err = net.ListenPacket(\"udp4\", \":8554\"); err != nil {\n\t\treturn\n\t}\n\n\tr.packets = make(chan []byte, 1024)\n\tgo r.worker()\n\n\treturn\n}\n\nfunc (r *listener) worker() {\n\tb := make([]byte, 1500)\n\tfor {\n\t\tif err := r.conn.SetReadDeadline(time.Now().Add(3 * time.Second)); err != nil {\n\t\t\tbreak\n\t\t}\n\n\t\tn, _, err := r.conn.ReadFrom(b)\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\n\t\tpacket := make([]byte, n)\n\t\tcopy(packet, b)\n\n\t\tr.packets <- packet\n\t}\n\n\tclose(r.packets)\n\n\t_ = r.command(\"/gopro/webcam/stop\")\n}\n"
  },
  {
    "path": "pkg/h264/README.md",
    "content": "# H264\n\nPayloader code taken from [pion](https://github.com/pion/rtp) library and changed to AVC packets support.\n\n## Useful Links\n\n- [RTP Payload Format for H.264 Video](https://datatracker.ietf.org/doc/html/rfc6184)\n- [The H264 Sequence parameter set](https://www.cardinalpeak.com/blog/the-h-264-sequence-parameter-set)\n- [H.264 Video Types (Microsoft)](https://docs.microsoft.com/en-us/windows/win32/directshow/h-264-video-types)\n- [Automatic Generation of H.264 Parameter Sets to Recover Video File Fragments](https://arxiv.org/pdf/2104.14522.pdf)\n- [Chromium sources](https://chromium.googlesource.com/external/webrtc/+/HEAD/common_video/h264)\n- [AVC levels](https://en.wikipedia.org/wiki/Advanced_Video_Coding#Levels)\n- [AVC profiles table](https://developer.mozilla.org/ru/docs/Web/Media/Formats/codecs_parameter)\n- [Supported Media for Google Cast](https://developers.google.com/cast/docs/media)\n- [Two stream formats, Annex-B, AVCC (H.264) and HVCC (H.265)](https://www.programmersought.com/article/3901815022/)\n- https://docs.aws.amazon.com/kinesisvideostreams/latest/dg/producer-reference-nal.html\n"
  },
  {
    "path": "pkg/h264/annexb/annexb.go",
    "content": "// Package annexb - universal for H264 and H265\npackage annexb\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n)\n\nconst StartCode = \"\\x00\\x00\\x00\\x01\"\nconst startAUD = StartCode + \"\\x09\\xF0\"\nconst startAUDstart = startAUD + StartCode\n\n// EncodeToAVCC\n//\n// FFmpeg MPEG-TS: 00000001 AUD 00000001 SPS 00000001 PPS 000001 IFrame\n// FFmpeg H264:    00000001 SPS 00000001 PPS 000001 IFrame 00000001 PFrame\n// Reolink:        000001 AUD 000001 VPS 00000001 SPS 00000001 PPS 00000001 IDR 00000001 IDR\nfunc EncodeToAVCC(annexb []byte) (avc []byte) {\n\tvar start int\n\n\tavc = make([]byte, 0, len(annexb)+4) // init memory with little overhead\n\n\tfor i := 0; ; i++ {\n\t\tvar offset int\n\n\t\tif i+3 < len(annexb) {\n\t\t\t// search next separator\n\t\t\tif annexb[i] == 0 && annexb[i+1] == 0 {\n\t\t\t\tif annexb[i+2] == 1 {\n\t\t\t\t\toffset = 3 // 00 00 01\n\t\t\t\t} else if annexb[i+2] == 0 && annexb[i+3] == 1 {\n\t\t\t\t\toffset = 4 // 00 00 00 01\n\t\t\t\t} else {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t} else {\n\t\t\ti = len(annexb) // move i to data end\n\t\t}\n\n\t\tif start != 0 {\n\t\t\tsize := uint32(i - start)\n\t\t\tavc = binary.BigEndian.AppendUint32(avc, size)\n\t\t\tavc = append(avc, annexb[start:i]...)\n\t\t}\n\n\t\t// sometimes FFmpeg put separator at the end\n\t\tif i += offset; i == len(annexb) {\n\t\t\tbreak\n\t\t}\n\n\t\tif isAUD(annexb[i]) {\n\t\t\tstart = 0 // skip this NALU\n\t\t} else {\n\t\t\tstart = i // save this position\n\t\t}\n\t}\n\n\treturn\n}\n\nfunc isAUD(b byte) bool {\n\tconst h264 = 9\n\tconst h265 = 35 << 1\n\treturn b&0b0001_1111 == h264 || b&0b0111_1110 == h265\n}\n\nfunc DecodeAVCC(b []byte, safeClone bool) []byte {\n\tif safeClone {\n\t\tb = bytes.Clone(b)\n\t}\n\tfor i := 0; i < len(b); {\n\t\tsize := int(binary.BigEndian.Uint32(b[i:]))\n\t\tb[i] = 0\n\t\tb[i+1] = 0\n\t\tb[i+2] = 0\n\t\tb[i+3] = 1\n\t\ti += 4 + size\n\t}\n\treturn b\n}\n\n// DecodeAVCCWithAUD - AUD doesn't important for FFmpeg, but important for Safari\nfunc DecodeAVCCWithAUD(src []byte) []byte {\n\tdst := make([]byte, len(startAUD)+len(src))\n\tcopy(dst, startAUD)\n\tcopy(dst[len(startAUD):], src)\n\tDecodeAVCC(dst[len(startAUD):], false)\n\treturn dst\n}\n\nconst (\n\th264PFrame = 1\n\th264IFrame = 5\n\th264SPS    = 7\n\th264PPS    = 8\n\n\th265VPS    = 32\n\th265PFrame = 1\n)\n\n// IndexFrame - get new frame start position in the AnnexB stream\nfunc IndexFrame(b []byte) int {\n\tif len(b) < len(startAUDstart) {\n\t\treturn -1\n\t}\n\n\tfor i := len(startAUDstart); ; {\n\t\tif di := bytes.Index(b[i:], []byte(StartCode)); di < 0 {\n\t\t\tbreak\n\t\t} else {\n\t\t\ti += di + 4 // move to NALU start\n\t\t}\n\n\t\tif i >= len(b) {\n\t\t\tbreak\n\t\t}\n\n\t\th264Type := b[i] & 0b1_1111\n\t\tswitch h264Type {\n\t\tcase h264PFrame, h264SPS:\n\t\t\treturn i - 4 // move to start code\n\t\tcase h264IFrame, h264PPS:\n\t\t\tcontinue\n\t\t}\n\n\t\th265Type := (b[i] >> 1) & 0b11_1111\n\t\tswitch h265Type {\n\t\tcase h265PFrame, h265VPS:\n\t\t\treturn i - 4 // move to start code\n\t\t}\n\t}\n\n\treturn -1\n}\n\nfunc FixAnnexBInAVCC(b []byte) []byte {\n\tfor i := 0; i < len(b); {\n\t\tif i+4 >= len(b) {\n\t\t\tbreak\n\t\t}\n\n\t\tsize := bytes.Index(b[i+4:], []byte{0, 0, 0, 1})\n\t\tif size < 0 {\n\t\t\tsize = len(b) - (i + 4)\n\t\t}\n\n\t\tbinary.BigEndian.PutUint32(b[i:], uint32(size))\n\n\t\ti += size + 4\n\t}\n\n\treturn b\n}\n"
  },
  {
    "path": "pkg/h264/annexb/annexb_test.go",
    "content": "package annexb\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"encoding/hex\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc decode(s string) []byte {\n\tb, _ := hex.DecodeString(strings.ReplaceAll(s, \" \", \"\"))\n\treturn b\n}\n\nfunc naluTypes(avcc []byte) (types []byte) {\n\tfor {\n\t\ttypes = append(types, avcc[4])\n\n\t\tsize := 4 + binary.BigEndian.Uint32(avcc)\n\t\tif size < uint32(len(avcc)) {\n\t\t\tavcc = avcc[size:]\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn\n}\n\nfunc TestFFmpegH264(t *testing.T) {\n\t// ffmpeg -re -i bbb.mp4 -c copy -f h264 -\n\ts := \"000000016764001fac2484014016ec0440000003004000000c23c60c92 0000000168ee32c8b0 00000165888080033ffef5f8454f32cb1bb4203f854dd69bc2ca91b2bce1fb3527440000030000030000030000030050999841d1afd324aea000000300000f600011c0001b40004e40011f0003b80010800059000238000be0005e000220001100000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300004041 00000001\"\n\tb := EncodeToAVCC(decode(s))\n\trequire.True(t, bytes.HasSuffix(b, []byte{0x40, 0x41}))\n\tn := naluTypes(b)\n\trequire.Equal(t, []byte{0x67, 0x68, 0x65}, n)\n}\n\nfunc TestFFmpegMPEGTSH264(t *testing.T) {\n\t// ffmpeg -re -i bbb.mp4 -c copy -f mpegts -\n\ts := \"00000001 09f0 000000016764001fac2484014016ec0440000003004000000c23c60c92 0000000168ee32c8b0 00000165888080033ffef5f8454f32cb1bb4203f854dd69bc2ca91b2bce1fb3527440000030000030000030000030050999841d1afd324aea000000300000f600011c0001b40004e40011f0003b80010800059000238000be0005e000220001100000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300000300004041\"\n\tb := EncodeToAVCC(decode(s))\n\tn := naluTypes(b)\n\trequire.Equal(t, []byte{0x67, 0x68, 0x65}, n)\n}\n\nfunc TestFFmpegHEVC(t *testing.T) {\n\t// ffmpeg -re -i bbb.mp4 -c libx265 -preset superfast -tune zerolatency -f hevc -\n\ts := \"0000000140010c01ffff01600000030090000003000003005dba0240 0000000142010101600000030090000003000003005da00280802d165bab930bc05a7080000003008000000c04 000000014401c1718312 0000014e0105ffffffffffffffffff2b2ca2de09b51747dbbb55a4fe7fc2fc4e7832363520286275696c642032303829202d20332e352b3131312d6330616631386464353a5b57696e646f77735d5b4743432031332e322e305d5b3634206269745d20386269742b31306269742b3132626974202d20482e3236352f4845564320636f646563202d20436f7079726967687420323031332d3230313820286329204d756c7469636f7265776172\"\n\tb := EncodeToAVCC(decode(s))\n\tn := naluTypes(b)\n\trequire.Equal(t, []byte{0x40, 0x42, 0x44, 0x4e}, n)\n}\n\nfunc TestFFmpegHEVC2(t *testing.T) {\n\t// ffmpeg -re -i bbb.mp4 -c libx265 -preset superfast -tune zerolatency -f hevc -\n\ts := \"0000000140010c01ffff01600000030090000003000003005dba0240 0000000142010101600000030090000003000003005da00280802d165bab930bc05a7080000003008000000c04 000000014401c1718312 0000014e0105ffffffffffffffffff2b2ca2de09b51747dbbb55a4fe7fc2fc4e7832363520286275696c642032303829202d20332e352b3131312d6330616631386464353a5b57696e646f77735d5b4743432031332e322e305d5b3634206269745d20386269742b31306269742b3132626974202d20482e3236352f4845564320636f646563202d20436f7079726967687420323031332d3230313820286329204d756c7469636f7265776172652c20496e63202d20687474703a2f2f783236352e6f7267202d206f7074696f6e733a2063707569643d31303439303731206672616d652d746872656164733d32206e756d612d706f6f6c733d3420777070206e6f2d706d6f6465206e6f2d706d65206e6f2d70736e72206e6f2d7373696d206c6f672d6c6576656c3d322062697464657074683d3820696e7075742d6373703d31206670733d32342f3120696e7075742d7265733d313238307837323020696e7465726c6163653d3020746f74616c2d6672616d65733d30206c6576656c2d6964633d3020686967682d746965723d31207568642d62643d30207265663d31206e6f2d616c6c6f772d6e6f6e2d636f6e666f726d616e6365207265706561742d6865616465727320616e6e657862206e6f2d617564206e6f2d656f62206e6f2d656f73206e6f2d68726420696e666f20686173683d302074656d706f72616c2d6c61796572733d30206f70656e2d676f70206d696e2d6b6579696e743d3234206b6579696e743d32353020676f702d6c6f6f6b61686561643d3020626672616d65733d3020622d61646170743d30206e6f2d622d707972616d696420626672616d652d626961733d302072632d6c6f6f6b61686561643d30206c6f6f6b61686561642d736c696365733d34207363656e656375743d30206e6f2d686973742d7363656e65637574207261646c3d30206e6f2d73706c696365206e6f2d696e7472612d72656672657368206374753d3332206d696e2d63752d73697a653d38206e6f2d72656374206e6f2d616d70206d61782d74752d73697a653d33322074752d696e7465722d64657074683d312074752d696e7472612d64657074683d31206c696d69742d74753d302072646f712d6c6576656c3d302064796e616d69632d72643d302e3030206e6f2d7373696d2d7264207369676e68696465206e6f2d74736b6970206e722d696e7472613d30206e722d696e7465723d30206e6f2d636f6e73747261696e65642d696e747261207374726f6e672d696e7472612d736d6f6f7468696e67206d61782d6d657267653d32206c696d69742d726566733d30206e6f2d6c696d69742d6d6f646573206d653d31207375626d653d31206d6572616e67653d35372074656d706f72616c2d6d7670206e6f2d6672616d652d647570206e6f2d686d65206e6f2d77656967687470206e6f2d77656967687462206e6f2d616e616c797a652d7372632d70696373206465626c6f636b3d303a30206e6f2d73616f206e6f2d73616f2d6e6f6e2d6465626c6f636b2072643d322073656c6563746976652d73616f3d30206561726c792d736b69702072736b697020666173742d696e747261206e6f2d74736b69702d66617374206e6f2d63752d6c6f73736c657373206e6f2d622d696e747261206e6f2d73706c697472642d736b697020726470656e616c74793d30207073792d72643d322e3030207073792d72646f713d302e3030206e6f2d72642d726566696e65206e6f2d6c6f73736c65737320636271706f6666733d3020637271706f6666733d302072633d637266206372663d32382e302071636f6d703d302e3630207170737465703d342073746174732d77726974653d302073746174732d726561643d30206970726174696f3d312e34302061712d6d6f64653d302061712d737472656e6774683d302e3030206e6f2d637574726565207a6f6e652d636f756e743d30206e6f2d7374726963742d6362722071672d73697a653d3332206e6f2d72632d677261696e2071706d61783d36392071706d696e3d30206e6f2d636f6e73742d766276207361723d31206f7665727363616e3d3020766964656f666f726d61743d352072616e67653d3020636f6c6f727072696d3d32207472616e736665723d3220636f6c6f726d61747269783d32206368726f6d616c6f633d31206368726f6d616c6f632d746f703d30206368726f6d616c6f632d626f74746f6d3d3020646973706c61792d77696e646f773d3020636c6c3d302c30206d696e2d6c756d613d30206d61782d6c756d613d323535206c6f67322d6d61782d706f632d6c73623d38207675692d74696d696e672d696e666f207675692d6872642d696e666f20736c696365733d31206e6f2d6f70742d71702d707073206e6f2d6f70742d7265662d6c6973742d6c656e6774682d707073206e6f2d6d756c74692d706173732d6f70742d727073207363656e656375742d626961733d302e3035206e6f2d6f70742d63752d64656c74612d7170206e6f2d61712d6d6f74696f6e206e6f2d6864723130206e6f2d68647231302d6f7074206e6f2d6468647231302d6f7074206e6f2d6964722d7265636f766572792d73656920616e616c797369732d72657573652d6c6576656c3d3020616e616c797369732d736176652d72657573652d6c6576656c3d3020616e616c797369732d6c6f61642d72657573652d6c6576656c3d30207363616c652d666163746f723d3020726566696e652d696e7472613d3020726566696e652d696e7465723d3020726566696e652d6d763d3120726566696e652d6374752d646973746f7274696f6e3d30206e6f2d6c696d69742d73616f206374752d696e666f3d30206e6f2d6c6f77706173732d64637420726566696e652d616e616c797369732d747970653d3020636f70792d7069633d31206d61782d617573697a652d666163746f723d312e30206e6f2d64796e616d69632d726566696e65206e6f2d73696e676c652d736569206e6f2d686576632d6171206e6f2d737674206e6f2d6669656c642071702d61646170746174696f6e2d72616e67653d312e3030207363656e656375742d61776172652d71703d30636f6e666f726d616e63652d77696e646f772d6f6666736574732072696768743d3020626f74746f6d3d30206465636f6465722d6d61782d726174653d30206e6f2d7662762d6c6976652d6d756c74692d70617373206e6f2d6d63737466206e6f2d7362726380 0000012801adc2e5bca307b9ce6b18b5ad6a525294a6d117ffd3917322eebaeda718a0000003000003000003021207706824da718a00000300000300000300044408d5db4e31400000030000030000030012500c2725a000000300000300000300002a600e4880000003000003000003000019301180000003000003000003000007d400000300000300000300000300010b000003000003000003000003001810000003000003000003000003019100000300000300000300000d38000003000003000003000067c000000300000300000300025e000003000003000003000c58000003000003000003002b60000003000003000003007f80000003000003000003016300000300000300000303b2000003000003000006e400000300000300000e18000003000003000018d00000030000030000292000000300000300003ce00000030000030000030000030000030000bb80\"\n\tb := EncodeToAVCC(decode(s))\n\tn := naluTypes(b)\n\trequire.Equal(t, []byte{0x40, 0x42, 0x44, 0x4e, 0x28}, n)\n}\n\nfunc TestFFmpegMPEGTSHEVC(t *testing.T) {\n\t// ffmpeg -re -i bbb.mp4 -c libx265 -preset superfast -tune zerolatency -an -f mpegts -\n\ts := \"00000001460150 0000000140010c01ffff01600000030090000003000003005dba0240 0000000142010101600000030090000003000003005da00280802d165bab930bc05a7080000003008000000c04 000000014401c1718312 0000014e0105ffffffffffffffffff2b2ca2de09b51747dbbb55a4fe7fc2fc4e7832363520286275696c642032303829202d20332e352b3131312d6330616631386464353a5b57696e646f77735d5b4743432031332e322e305d5b3634206269745d20386269742b31306269742b3132626974202d20482e3236352f4845564320636f646563202d20436f7079726967687420323031332d3230313820286329204d756c7469636f7265776172652c20496e63202d20687474703a2f2f783236352e6f7267202d206f7074696f6e733a2063707569643d31303439303731206672616d652d746872656164733d32206e756d612d706f6f6c733d3420777070206e6f2d706d6f6465206e6f2d706d65206e6f2d70736e72206e6f2d7373696d206c6f672d6c6576656c3d322062697464657074683d3820696e7075742d6373703d31206670733d32342f3120696e7075742d7265733d313238307837323020696e7465726c6163653d3020746f74616c2d6672616d65733d30206c6576656c2d6964633d3020686967682d746965723d31207568642d62643d30207265663d31206e6f2d616c6c6f772d6e6f6e2d636f6e666f726d616e6365207265706561742d6865616465727320616e6e657862206e6f2d617564206e6f2d656f62206e6f2d656f73206e6f2d68726420696e666f20686173683d302074656d706f72616c2d6c61796572733d30206f70656e2d676f70206d696e2d6b6579696e743d3234206b6579696e743d32353020676f702d6c6f6f6b61686561643d3020626672616d65733d3020622d61646170743d30206e6f2d622d707972616d696420626672616d652d626961733d302072632d6c6f6f6b61686561643d30206c6f6f6b61686561642d736c696365733d34207363656e656375743d30206e6f2d686973742d7363656e65637574207261646c3d30206e6f2d73706c696365206e6f2d696e7472612d72656672657368206374753d3332206d696e2d63752d73697a653d38206e6f2d72656374206e6f2d616d70206d61782d74752d73697a653d33322074752d696e7465722d64657074683d312074752d696e7472612d64657074683d31206c696d69742d74753d302072646f712d6c6576656c3d302064796e616d69632d72643d302e3030206e6f2d7373696d2d7264207369676e68696465206e6f2d74736b6970206e722d696e7472613d30206e722d696e7465723d30206e6f2d636f6e73747261696e65642d696e747261207374726f6e672d696e7472612d736d6f6f7468696e67206d61782d6d657267653d32206c696d69742d726566733d30206e6f2d6c696d69742d6d6f646573206d653d31207375626d653d31206d6572616e67653d35372074656d706f72616c2d6d7670206e6f2d6672616d652d647570206e6f2d686d65206e6f2d77656967687470206e6f2d77656967687462206e6f2d616e616c797a652d7372632d70696373206465626c6f636b3d303a30206e6f2d73616f206e6f2d73616f2d6e6f6e2d6465626c6f636b2072643d322073656c6563746976652d73616f3d30206561726c792d736b69702072736b697020666173742d696e747261206e6f2d74736b69702d66617374206e6f2d63752d6c6f73736c657373206e6f2d622d696e747261206e6f2d73706c697472642d736b697020726470656e616c74793d30207073792d72643d322e3030207073792d72646f713d302e3030206e6f2d72642d726566696e65206e6f2d6c6f73736c65737320636271706f6666733d3020637271706f6666733d302072633d637266206372663d32382e302071636f6d703d302e3630207170737465703d342073746174732d77726974653d302073746174732d726561643d30206970726174696f3d312e34302061712d6d6f64653d302061712d737472656e6774683d302e3030206e6f2d637574726565207a6f6e652d636f756e743d30206e6f2d7374726963742d6362722071672d73697a653d3332206e6f2d72632d677261696e2071706d61783d36392071706d696e3d30206e6f2d636f6e73742d766276207361723d31206f7665727363616e3d3020766964656f666f726d61743d352072616e67653d3020636f6c6f727072696d3d32207472616e736665723d3220636f6c6f726d61747269783d32206368726f6d616c6f633d31206368726f6d616c6f632d746f703d30206368726f6d616c6f632d626f74746f6d3d3020646973706c61792d77696e646f773d3020636c6c3d302c30206d696e2d6c756d613d30206d61782d6c756d613d323535206c6f67322d6d61782d706f632d6c73623d38207675692d74696d696e672d696e666f207675692d6872642d696e666f20736c696365733d31206e6f2d6f70742d71702d707073206e6f2d6f70742d7265662d6c6973742d6c656e6774682d707073206e6f2d6d756c74692d706173732d6f70742d727073207363656e656375742d626961733d302e3035206e6f2d6f70742d63752d64656c74612d7170206e6f2d61712d6d6f74696f6e206e6f2d6864723130206e6f2d68647231302d6f7074206e6f2d6468647231302d6f7074206e6f2d6964722d7265636f766572792d73656920616e616c797369732d72657573652d6c6576656c3d3020616e616c797369732d736176652d72657573652d6c6576656c3d3020616e616c797369732d6c6f61642d72657573652d6c6576656c3d30207363616c652d666163746f723d3020726566696e652d696e7472613d3020726566696e652d696e7465723d3020726566696e652d6d763d3120726566696e652d6374752d646973746f7274696f6e3d30206e6f2d6c696d69742d73616f206374752d696e666f3d30206e6f2d6c6f77706173732d64637420726566696e652d616e616c797369732d747970653d3020636f70792d7069633d31206d61782d617573697a652d666163746f723d312e30206e6f2d64796e616d69632d726566696e65206e6f2d73696e676c652d736569206e6f2d686576632d6171206e6f2d737674206e6f2d6669656c642071702d61646170746174696f6e2d72616e67653d312e3030207363656e656375742d61776172652d71703d30636f6e666f726d616e63652d77696e646f772d6f6666736574732072696768743d3020626f74746f6d3d30206465636f6465722d6d61782d726174653d30206e6f2d7662762d6c6976652d6d756c74692d70617373206e6f2d6d63737466206e6f2d7362726380 0000012801adc2e5bca307b9ce6b18b5ad6a525294a6d117ffd3917322eebaeda718a0000003000003000003021207706824da718a00000300000300000300044408d5db4e31400000030000030000030012500c2725a000000300000300000300002a600e4880000003000003000003000019301180000003000003000003000007d400000300000300000300000300010b000003000003000003000003001810000003000003000003000003019100000300000300000300000d38000003000003000003000067c000000300000300000300025e000003000003000003000c58000003000003000003002b60000003000003000003007f80000003000003000003016300000300000300000303b2000003000003000006e400000300000300000e18000003000003000018d00000030000030000292000000300000300003ce00000030000030000030000030000030000bb80\"\n\tb := EncodeToAVCC(decode(s))\n\tn := naluTypes(b)\n\trequire.Equal(t, []byte{0x40, 0x42, 0x44, 0x4e, 0x28}, n)\n}\n\nfunc TestReolink(t *testing.T) {\n\ts := \"000001460150 00000140010C01FFFF01600000030000030000030000030096AC09 0000000142010101600000030000030000030000030096A001E020021C7F8AAD3BA24BB804000013D800018CE008 000000014401C072F0941E3648 000000012601\"\n\tb := EncodeToAVCC(decode(s))\n\tn := naluTypes(b)\n\trequire.Equal(t, []byte{0x40, 0x42, 0x44, 0x26}, n)\n}\n\nfunc TestDahua(t *testing.T) {\n\ts := \"00000001460150 00000140010c01ffff01400000030000030000030000030099ac0900 0000000142010101400000030000030000030000030099a001402005a1fe5aee46c1ae550400 000000014401c073c04c9000 000000012601\"\n\tb := EncodeToAVCC(decode(s))\n\tn := naluTypes(b)\n\trequire.Equal(t, []byte{0x40, 0x42, 0x44, 0x26}, n)\n}\n\nfunc TestUSB(t *testing.T) {\n\ts := \"00 00 00 01 67 4D 00 1F 8D 8D 40 28 02 DD 37 01 01 01 40 00 01 C2 00 00 57 E4 01 00 00 00 01 68 EE 3C 80 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 65 88 80 00\"\n\tb := EncodeToAVCC(decode(s))\n\tn := naluTypes(b)\n\trequire.Equal(t, []byte{0x67, 0x68, 0x65}, n)\n\n\ts = \"00 00 00 00 00 00 00 00 00 00 00 00 00 00 01 41 9A 00 4C\"\n\tb = EncodeToAVCC(decode(s))\n\tn = naluTypes(b)\n\trequire.Equal(t, []byte{0x41}, n)\n}\n"
  },
  {
    "path": "pkg/h264/avc.go",
    "content": "package h264\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n)\n\nconst forbiddenZeroBit = 0x80\nconst nalUnitType = 0x1F\n\n// Deprecated: DecodeStream - find and return first AU in AVC format\n// useful for processing live streams with unknown separator size\nfunc DecodeStream(annexb []byte) ([]byte, int) {\n\tstartPos := -1\n\n\ti := 0\n\tfor {\n\t\t// search next separator\n\t\tif i = IndexFrom(annexb, []byte{0, 0, 1}, i); i < 0 {\n\t\t\tbreak\n\t\t}\n\n\t\t// move i to next AU\n\t\tif i += 3; i >= len(annexb) {\n\t\t\tbreak\n\t\t}\n\n\t\t// check if AU type valid\n\t\toctet := annexb[i]\n\t\tif octet&forbiddenZeroBit != 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\t// 0 => AUD => SPS/IF/PF => AUD\n\t\t// 0 => SPS/PF => SPS/PF\n\t\tnalType := octet & nalUnitType\n\t\tif startPos >= 0 {\n\t\t\tswitch nalType {\n\t\t\tcase NALUTypeAUD, NALUTypeSPS, NALUTypePFrame:\n\t\t\t\tif annexb[i-4] == 0 {\n\t\t\t\t\treturn DecodeAnnexB(annexb[startPos : i-4]), i - 4\n\t\t\t\t} else {\n\t\t\t\t\treturn DecodeAnnexB(annexb[startPos : i-3]), i - 3\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tswitch nalType {\n\t\t\tcase NALUTypeSPS, NALUTypePFrame:\n\t\t\t\tif i >= 4 && annexb[i-4] == 0 {\n\t\t\t\t\tstartPos = i - 4\n\t\t\t\t} else {\n\t\t\t\t\tstartPos = i - 3\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil, 0\n}\n\n// DecodeAnnexB - convert AnnexB to AVC format\n// support unknown separator size\nfunc DecodeAnnexB(b []byte) []byte {\n\tif b[2] == 1 {\n\t\t// convert: 0 0 1 => 0 0 0 1\n\t\tb = append([]byte{0}, b...)\n\t}\n\n\tstartPos := 0\n\n\ti := 4\n\tfor {\n\t\t// search next separato\n\t\tif i = IndexFrom(b, []byte{0, 0, 1}, i); i < 0 {\n\t\t\tbreak\n\t\t}\n\n\t\t// move i to next AU\n\t\tif i += 3; i >= len(b) {\n\t\t\tbreak\n\t\t}\n\n\t\t// check if AU type valid\n\t\toctet := b[i]\n\t\tif octet&forbiddenZeroBit != 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tswitch octet & nalUnitType {\n\t\tcase NALUTypePFrame, NALUTypeIFrame, NALUTypeSPS, NALUTypePPS:\n\t\t\tif b[i-4] != 0 {\n\t\t\t\t// prefix: 0 0 1\n\t\t\t\tbinary.BigEndian.PutUint32(b[startPos:], uint32(i-startPos-7))\n\t\t\t\ttmp := make([]byte, 0, len(b)+1)\n\t\t\t\ttmp = append(tmp, b[:i]...)\n\t\t\t\ttmp = append(tmp, 0)\n\t\t\t\tb = append(tmp, b[i:]...)\n\t\t\t\tstartPos = i - 3\n\t\t\t} else {\n\t\t\t\t// prefix: 0 0 0 1\n\t\t\t\tbinary.BigEndian.PutUint32(b[startPos:], uint32(i-startPos-8))\n\t\t\t\tstartPos = i - 4\n\t\t\t}\n\t\t}\n\t}\n\n\tbinary.BigEndian.PutUint32(b[startPos:], uint32(len(b)-startPos-4))\n\treturn b\n}\n\nfunc IndexFrom(b []byte, sep []byte, from int) int {\n\tif from > 0 {\n\t\tif from < len(b) {\n\t\t\tif i := bytes.Index(b[from:], sep); i >= 0 {\n\t\t\t\treturn from + i\n\t\t\t}\n\t\t}\n\t\treturn -1\n\t}\n\n\treturn bytes.Index(b, sep)\n}\n"
  },
  {
    "path": "pkg/h264/avcc.go",
    "content": "// Package h264 - AVCC format related functions\npackage h264\n\nimport (\n\t\"bytes\"\n\t\"encoding/base64\"\n\t\"encoding/binary\"\n\t\"encoding/hex\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/pion/rtp\"\n)\n\nfunc RepairAVCC(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {\n\tsps, pps := GetParameterSet(codec.FmtpLine)\n\tps := JoinNALU(sps, pps)\n\n\treturn func(packet *rtp.Packet) {\n\t\t// this can happen for FLV from FFmpeg\n\t\tif NALUType(packet.Payload) == NALUTypeSEI {\n\t\t\tsize := int(binary.BigEndian.Uint32(packet.Payload)) + 4\n\t\t\tpacket.Payload = packet.Payload[size:]\n\t\t}\n\t\tif NALUType(packet.Payload) == NALUTypeIFrame {\n\t\t\tpacket.Payload = Join(ps, packet.Payload)\n\t\t}\n\t\thandler(packet)\n\t}\n}\n\nfunc JoinNALU(nalus ...[]byte) (avcc []byte) {\n\tvar i, n int\n\n\tfor _, nalu := range nalus {\n\t\tif i = len(nalu); i > 0 {\n\t\t\tn += 4 + i\n\t\t}\n\t}\n\n\tavcc = make([]byte, n)\n\n\tn = 0\n\tfor _, nal := range nalus {\n\t\tif i = len(nal); i > 0 {\n\t\t\tbinary.BigEndian.PutUint32(avcc[n:], uint32(i))\n\t\t\tn += 4 + copy(avcc[n+4:], nal)\n\t\t}\n\t}\n\n\treturn\n}\n\nfunc SplitNALU(avcc []byte) [][]byte {\n\tvar nals [][]byte\n\tfor {\n\t\t// get AVC length\n\t\tsize := int(binary.BigEndian.Uint32(avcc)) + 4\n\n\t\t// check if multiple items in one packet\n\t\tif size < len(avcc) {\n\t\t\tnals = append(nals, avcc[:size])\n\t\t\tavcc = avcc[size:]\n\t\t} else {\n\t\t\tnals = append(nals, avcc)\n\t\t\tbreak\n\t\t}\n\t}\n\treturn nals\n}\n\nfunc NALUTypes(avcc []byte) []byte {\n\tvar types []byte\n\tfor {\n\t\ttypes = append(types, NALUType(avcc))\n\n\t\tsize := 4 + int(binary.BigEndian.Uint32(avcc))\n\t\tif size < len(avcc) {\n\t\t\tavcc = avcc[size:]\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn types\n}\n\nfunc AVCCToCodec(avcc []byte) *core.Codec {\n\tbuf := bytes.NewBufferString(\"packetization-mode=1\")\n\n\tfor {\n\t\tn := len(avcc)\n\t\tif n < 4 {\n\t\t\tbreak\n\t\t}\n\n\t\tsize := 4 + int(binary.BigEndian.Uint32(avcc))\n\t\tif n < size {\n\t\t\tbreak\n\t\t}\n\n\t\tswitch NALUType(avcc) {\n\t\tcase NALUTypeSPS:\n\t\t\tbuf.WriteString(\";profile-level-id=\")\n\t\t\tbuf.WriteString(hex.EncodeToString(avcc[5:8]))\n\t\t\tbuf.WriteString(\";sprop-parameter-sets=\")\n\t\t\tbuf.WriteString(base64.StdEncoding.EncodeToString(avcc[4:size]))\n\t\tcase NALUTypePPS:\n\t\t\tbuf.WriteString(\",\")\n\t\t\tbuf.WriteString(base64.StdEncoding.EncodeToString(avcc[4:size]))\n\t\t}\n\n\t\tavcc = avcc[size:]\n\t}\n\n\treturn &core.Codec{\n\t\tName:        core.CodecH264,\n\t\tClockRate:   90000,\n\t\tFmtpLine:    buf.String(),\n\t\tPayloadType: core.PayloadTypeRAW,\n\t}\n}\n"
  },
  {
    "path": "pkg/h264/h264.go",
    "content": "package h264\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/binary\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\nconst (\n\tNALUTypePFrame = 1 // Coded slice of a non-IDR picture\n\tNALUTypeIFrame = 5 // Coded slice of an IDR picture\n\tNALUTypeSEI    = 6 // Supplemental enhancement information (SEI)\n\tNALUTypeSPS    = 7 // Sequence parameter set\n\tNALUTypePPS    = 8 // Picture parameter set\n\tNALUTypeAUD    = 9 // Access unit delimiter\n)\n\nfunc NALUType(b []byte) byte {\n\treturn b[4] & 0x1F\n}\n\n// IsKeyframe - check if any NALU in one AU is Keyframe\nfunc IsKeyframe(b []byte) bool {\n\tfor {\n\t\tswitch NALUType(b) {\n\t\tcase NALUTypePFrame:\n\t\t\treturn false\n\t\tcase NALUTypeIFrame:\n\t\t\treturn true\n\t\t}\n\n\t\tsize := int(binary.BigEndian.Uint32(b)) + 4\n\t\tif size < len(b) {\n\t\t\tb = b[size:]\n\t\t\tcontinue\n\t\t} else {\n\t\t\treturn false\n\t\t}\n\t}\n}\n\nfunc Join(ps, iframe []byte) []byte {\n\tb := make([]byte, len(ps)+len(iframe))\n\ti := copy(b, ps)\n\tcopy(b[i:], iframe)\n\treturn b\n}\n\n// https://developers.google.com/cast/docs/media\nconst (\n\tProfileBaseline    = 0x42\n\tProfileMain        = 0x4D\n\tProfileHigh        = 0x64\n\tCapabilityBaseline = 0xE0\n\tCapabilityMain     = 0x40\n)\n\n// GetProfileLevelID - get profile from fmtp line\n// Some devices won't play video with high level, so limit max profile and max level.\n// And return some profile even if fmtp line is empty.\nfunc GetProfileLevelID(fmtp string) string {\n\t// avc1.640029 - H.264 high 4.1 (Chromecast 1st and 2nd Gen)\n\tprofile := byte(ProfileHigh)\n\tcapab := byte(0)\n\tlevel := byte(41)\n\n\tif fmtp != \"\" {\n\t\tvar conf []byte\n\t\t// some cameras has wrong profile-level-id\n\t\t// https://github.com/AlexxIT/go2rtc/issues/155\n\t\tif s := core.Between(fmtp, \"sprop-parameter-sets=\", \",\"); s != \"\" {\n\t\t\tif sps, _ := base64.StdEncoding.DecodeString(s); len(sps) >= 4 {\n\t\t\t\tconf = sps[1:4]\n\t\t\t}\n\t\t} else if s = core.Between(fmtp, \"profile-level-id=\", \";\"); s != \"\" {\n\t\t\tconf, _ = hex.DecodeString(s)\n\t\t}\n\n\t\tif len(conf) == 3 {\n\t\t\t// sanitize profile, capab and level to supported values\n\t\t\tswitch conf[0] {\n\t\t\tcase ProfileBaseline, ProfileMain:\n\t\t\t\tprofile = conf[0]\n\t\t\t}\n\t\t\tswitch conf[1] {\n\t\t\tcase CapabilityBaseline, CapabilityMain:\n\t\t\t\tcapab = conf[1]\n\t\t\t}\n\t\t\tswitch conf[2] {\n\t\t\tcase 30, 31, 40:\n\t\t\t\tlevel = conf[2]\n\t\t\t}\n\t\t}\n\t}\n\n\treturn fmt.Sprintf(\"%02X%02X%02X\", profile, capab, level)\n}\n\nfunc GetParameterSet(fmtp string) (sps, pps []byte) {\n\tif fmtp == \"\" {\n\t\treturn\n\t}\n\n\ts := core.Between(fmtp, \"sprop-parameter-sets=\", \";\")\n\tif s == \"\" {\n\t\treturn\n\t}\n\n\ti := strings.IndexByte(s, ',')\n\tif i < 0 {\n\t\treturn\n\t}\n\n\tsps, _ = base64.StdEncoding.DecodeString(s[:i])\n\tpps, _ = base64.StdEncoding.DecodeString(s[i+1:])\n\n\treturn\n}\n\n// GetFmtpLine from SPS+PPS+IFrame in AVC format\nfunc GetFmtpLine(avc []byte) string {\n\ts := \"packetization-mode=1\"\n\n\tfor {\n\t\tsize := 4 + int(binary.BigEndian.Uint32(avc))\n\n\t\tswitch NALUType(avc) {\n\t\tcase NALUTypeSPS:\n\t\t\ts += \";profile-level-id=\" + hex.EncodeToString(avc[5:8])\n\t\t\ts += \";sprop-parameter-sets=\" + base64.StdEncoding.EncodeToString(avc[4:size])\n\t\tcase NALUTypePPS:\n\t\t\ts += \",\" + base64.StdEncoding.EncodeToString(avc[4:size])\n\t\t}\n\n\t\tif size < len(avc) {\n\t\t\tavc = avc[size:]\n\t\t} else {\n\t\t\treturn s\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/h264/h264_test.go",
    "content": "package h264\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDecodeConfig(t *testing.T) {\n\ts := \"01640033ffe1000c67640033ac1514a02800f19001000468ee3cb0\"\n\tsrc, err := hex.DecodeString(s)\n\trequire.Nil(t, err)\n\n\tprofile, sps, pps := DecodeConfig(src)\n\trequire.NotNil(t, profile)\n\trequire.NotNil(t, sps)\n\trequire.NotNil(t, pps)\n\n\tdst := EncodeConfig(sps, pps)\n\trequire.Equal(t, src, dst)\n}\n\nfunc TestDecodeSPS(t *testing.T) {\n\ts := \"Z0IAMukAUAHjQgAAB9IAAOqcCAA=\" // Amcrest AD410\n\tb, err := base64.StdEncoding.DecodeString(s)\n\trequire.Nil(t, err)\n\n\tsps := DecodeSPS(b)\n\trequire.Equal(t, uint16(2560), sps.Width())\n\trequire.Equal(t, uint16(1920), sps.Height())\n\n\ts = \"R00AKZmgHgCJ+WEAAAMD6AAATiCE\" // Sonoff\n\tb, err = base64.StdEncoding.DecodeString(s)\n\trequire.Nil(t, err)\n\n\tsps = DecodeSPS(b)\n\trequire.Equal(t, uint16(1920), sps.Width())\n\trequire.Equal(t, uint16(1080), sps.Height())\n\n\ts = \"Z01AMqaAKAC1kAA=\" // Dahua\n\tb, err = base64.StdEncoding.DecodeString(s)\n\trequire.Nil(t, err)\n\n\tsps = DecodeSPS(b)\n\trequire.Equal(t, uint16(2560), sps.Width())\n\trequire.Equal(t, uint16(1440), sps.Height())\n\n\ts = \"Z2QAM6wVFKAoAPGQ\" // Reolink\n\tb, err = base64.StdEncoding.DecodeString(s)\n\trequire.Nil(t, err)\n\n\tsps = DecodeSPS(b)\n\trequire.Equal(t, uint16(2560), sps.Width())\n\trequire.Equal(t, uint16(1920), sps.Height())\n\n\ts = \"Z2QAKKwa0AoAt03AQEBQAAADABAAAAMB6PFCKg==\" // TP-Link\n\tb, err = base64.StdEncoding.DecodeString(s)\n\trequire.Nil(t, err)\n\n\tsps = DecodeSPS(b)\n\trequire.Equal(t, uint16(1280), sps.Width())\n\trequire.Equal(t, uint16(720), sps.Height())\n\n\ts = \"Z2QAFqwa0BQF/yzcBAQFAAADAAEAAAMAHo8UIqA=\" // TP-Link sub\n\tb, err = base64.StdEncoding.DecodeString(s)\n\trequire.Nil(t, err)\n\n\tsps = DecodeSPS(b)\n\trequire.Equal(t, uint16(640), sps.Width())\n\trequire.Equal(t, uint16(360), sps.Height())\n}\n\nfunc TestGetProfileLevelID(t *testing.T) {\n\t// OpenIPC https://github.com/OpenIPC\n\ts := \"profile-level-id=0033e7; packetization-mode=1; \"\n\tprofile := GetProfileLevelID(s)\n\trequire.Equal(t, \"640029\", profile)\n\n\t// Eufy T8400 https://github.com/AlexxIT/go2rtc/issues/155\n\ts = \"packetization-mode=1;profile-level-id=276400\"\n\tprofile = GetProfileLevelID(s)\n\trequire.Equal(t, \"640029\", profile)\n}\n\nfunc TestDecodeSPS2(t *testing.T) {\n\ts := \"6764001fad84010c20086100430802184010c200843b50740932\"\n\tb, err := hex.DecodeString(s)\n\trequire.Nil(t, err)\n\n\tsps := DecodeSPS(b)\n\trequire.Equal(t, uint16(928), sps.Width())\n\trequire.Equal(t, uint16(576), sps.Height())\n\n\ts = \"Z2QAHq2EAQwgCGEAQwgCGEAQwgCEO1BQF/yzcBAQFAAAD6AAAXcCEA==\" // unknown\n\tb, err = base64.StdEncoding.DecodeString(s)\n\trequire.Nil(t, err)\n\n\tsps = DecodeSPS(b)\n\trequire.Equal(t, uint16(640), sps.Width())\n\trequire.Equal(t, uint16(360), sps.Height())\n}\n\nfunc TestAVCCToCodec(t *testing.T) {\n\ts := \"000000196764001fac2484014016ec0440000003004000000c23c60c920000000568ee32c8b0000000d365\"\n\tb, _ := hex.DecodeString(s)\n\tcodec := AVCCToCodec(b)\n\trequire.Equal(t, \"packetization-mode=1;profile-level-id=64001f;sprop-parameter-sets=Z2QAH6wkhAFAFuwEQAAAAwBAAAAMI8YMkg==,aO4yyLA=\", codec.FmtpLine)\n}\n"
  },
  {
    "path": "pkg/h264/mpeg4.go",
    "content": "// Package h264 - MPEG4 format related functions\npackage h264\n\nimport (\n\t\"bytes\"\n\t\"encoding/base64\"\n\t\"encoding/binary\"\n\t\"encoding/hex\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\n// DecodeConfig - extract profile, SPS and PPS from MPEG4 config\nfunc DecodeConfig(conf []byte) (profile []byte, sps []byte, pps []byte) {\n\tif len(conf) < 6 || conf[0] != 1 {\n\t\treturn\n\t}\n\n\tprofile = conf[1:4]\n\n\tcount := conf[5] & 0x1F\n\tconf = conf[6:]\n\tfor i := byte(0); i < count; i++ {\n\t\tif len(conf) < 2 {\n\t\t\treturn\n\t\t}\n\t\tsize := 2 + int(binary.BigEndian.Uint16(conf))\n\t\tif len(conf) < size {\n\t\t\treturn\n\t\t}\n\t\tif sps == nil {\n\t\t\tsps = conf[2:size]\n\t\t}\n\t\tconf = conf[size:]\n\t}\n\n\tcount = conf[0]\n\tconf = conf[1:]\n\tfor i := byte(0); i < count; i++ {\n\t\tif len(conf) < 2 {\n\t\t\treturn\n\t\t}\n\t\tsize := 2 + int(binary.BigEndian.Uint16(conf))\n\t\tif len(conf) < size {\n\t\t\treturn\n\t\t}\n\t\tif pps == nil {\n\t\t\tpps = conf[2:size]\n\t\t}\n\t\tconf = conf[size:]\n\t}\n\n\treturn\n}\n\nfunc EncodeConfig(sps, pps []byte) []byte {\n\tspsSize := uint16(len(sps))\n\tppsSize := uint16(len(pps))\n\n\tbuf := make([]byte, 5+3+spsSize+3+ppsSize)\n\tbuf[0] = 1\n\tcopy(buf[1:], sps[1:4]) // profile\n\tbuf[4] = 3 | 0xFC       // ? LengthSizeMinusOne\n\n\tb := buf[5:]\n\t_ = b[3]\n\tb[0] = 1 | 0xE0 // ? sps count\n\tbinary.BigEndian.PutUint16(b[1:], spsSize)\n\tcopy(b[3:], sps)\n\n\tb = buf[5+3+spsSize:]\n\t_ = b[3]\n\tb[0] = 1 // pps count\n\tbinary.BigEndian.PutUint16(b[1:], ppsSize)\n\tcopy(b[3:], pps)\n\n\treturn buf\n}\n\nfunc ConfigToCodec(conf []byte) *core.Codec {\n\tbuf := bytes.NewBufferString(\"packetization-mode=1\")\n\n\tprofile, sps, pps := DecodeConfig(conf)\n\tif profile != nil {\n\t\tbuf.WriteString(\";profile-level-id=\")\n\t\tbuf.WriteString(hex.EncodeToString(profile))\n\t}\n\tif sps != nil && pps != nil {\n\t\tbuf.WriteString(\";sprop-parameter-sets=\")\n\t\tbuf.WriteString(base64.StdEncoding.EncodeToString(sps))\n\t\tbuf.WriteString(\",\")\n\t\tbuf.WriteString(base64.StdEncoding.EncodeToString(pps))\n\t}\n\n\treturn &core.Codec{\n\t\tName:        core.CodecH264,\n\t\tClockRate:   90000,\n\t\tFmtpLine:    buf.String(),\n\t\tPayloadType: core.PayloadTypeRAW,\n\t}\n}\n"
  },
  {
    "path": "pkg/h264/payloader.go",
    "content": "package h264\n\nimport \"encoding/binary\"\n\n// Payloader payloads H264 packets\ntype Payloader struct {\n\tIsAVC     bool\n\tstapANalu []byte\n}\n\nconst (\n\tstapaNALUType  = 24\n\tfuaNALUType    = 28\n\tfubNALUType    = 29\n\tspsNALUType    = 7\n\tppsNALUType    = 8\n\taudNALUType    = 9\n\tfillerNALUType = 12\n\n\tfuaHeaderSize = 2\n\t//stapaHeaderSize     = 1\n\t//stapaNALULengthSize = 2\n\n\tnaluTypeBitmask   = 0x1F\n\tnaluRefIdcBitmask = 0x60\n\t//fuStartBitmask    = 0x80\n\t//fuEndBitmask      = 0x40\n\n\toutputStapAHeader = 0x78\n)\n\n//func annexbNALUStartCode() []byte { return []byte{0x00, 0x00, 0x00, 0x01} }\n\nfunc EmitNalus(nals []byte, isAVC bool, emit func([]byte)) {\n\tif !isAVC {\n\t\tnextInd := func(nalu []byte, start int) (indStart int, indLen int) {\n\t\t\tzeroCount := 0\n\n\t\t\tfor i, b := range nalu[start:] {\n\t\t\t\tif b == 0 {\n\t\t\t\t\tzeroCount++\n\t\t\t\t\tcontinue\n\t\t\t\t} else if b == 1 {\n\t\t\t\t\tif zeroCount >= 2 {\n\t\t\t\t\t\treturn start + i - zeroCount, zeroCount + 1\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tzeroCount = 0\n\t\t\t}\n\t\t\treturn -1, -1\n\t\t}\n\n\t\tnextIndStart, nextIndLen := nextInd(nals, 0)\n\t\tif nextIndStart == -1 {\n\t\t\temit(nals)\n\t\t} else {\n\t\t\tfor nextIndStart != -1 {\n\t\t\t\tprevStart := nextIndStart + nextIndLen\n\t\t\t\tnextIndStart, nextIndLen = nextInd(nals, prevStart)\n\t\t\t\tif nextIndStart != -1 {\n\t\t\t\t\temit(nals[prevStart:nextIndStart])\n\t\t\t\t} else {\n\t\t\t\t\t// Emit until end of stream, no end indicator found\n\t\t\t\t\temit(nals[prevStart:])\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\tfor {\n\t\t\tn := uint32(len(nals))\n\t\t\tif n < 4 {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tend := 4 + binary.BigEndian.Uint32(nals)\n\t\t\tif n < end {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\temit(nals[4:end])\n\t\t\tnals = nals[end:]\n\t\t}\n\t}\n}\n\n// Payload fragments a H264 packet across one or more byte arrays\nfunc (p *Payloader) Payload(mtu uint16, payload []byte) [][]byte {\n\tvar payloads [][]byte\n\tif len(payload) == 0 {\n\t\treturn payloads\n\t}\n\n\tEmitNalus(payload, p.IsAVC, func(nalu []byte) {\n\t\tif len(nalu) == 0 {\n\t\t\treturn\n\t\t}\n\n\t\tnaluType := nalu[0] & naluTypeBitmask\n\t\tnaluRefIdc := nalu[0] & naluRefIdcBitmask\n\n\t\tswitch naluType {\n\t\tcase audNALUType, fillerNALUType:\n\t\t\treturn\n\t\tcase spsNALUType, ppsNALUType:\n\t\t\tif p.stapANalu == nil {\n\t\t\t\tp.stapANalu = []byte{outputStapAHeader}\n\t\t\t}\n\t\t\tp.stapANalu = append(p.stapANalu, byte(len(nalu)>>8), byte(len(nalu)))\n\t\t\tp.stapANalu = append(p.stapANalu, nalu...)\n\t\t\treturn\n\t\t}\n\n\t\tif p.stapANalu != nil {\n\t\t\t// Pack current NALU with SPS and PPS as STAP-A\n\t\t\t// Supports multiple PPS in a row\n\t\t\tif len(p.stapANalu) <= int(mtu) {\n\t\t\t\tpayloads = append(payloads, p.stapANalu)\n\t\t\t}\n\t\t\tp.stapANalu = nil\n\t\t}\n\n\t\t// Single NALU\n\t\tif len(nalu) <= int(mtu) {\n\t\t\tout := make([]byte, len(nalu))\n\t\t\tcopy(out, nalu)\n\t\t\tpayloads = append(payloads, out)\n\t\t\treturn\n\t\t}\n\n\t\t// FU-A\n\t\tmaxFragmentSize := int(mtu) - fuaHeaderSize\n\n\t\t// The FU payload consists of fragments of the payload of the fragmented\n\t\t// NAL unit so that if the fragmentation unit payloads of consecutive\n\t\t// FUs are sequentially concatenated, the payload of the fragmented NAL\n\t\t// unit can be reconstructed.  The NAL unit type octet of the fragmented\n\t\t// NAL unit is not included as such in the fragmentation unit payload,\n\t\t// \tbut rather the information of the NAL unit type octet of the\n\t\t// fragmented NAL unit is conveyed in the F and NRI fields of the FU\n\t\t// indicator octet of the fragmentation unit and in the type field of\n\t\t// the FU header.  An FU payload MAY have any number of octets and MAY\n\t\t// be empty.\n\n\t\tnaluData := nalu\n\t\t// According to the RFC, the first octet is skipped due to redundant information\n\t\tnaluDataIndex := 1\n\t\tnaluDataLength := len(nalu) - naluDataIndex\n\t\tnaluDataRemaining := naluDataLength\n\n\t\tif min(maxFragmentSize, naluDataRemaining) <= 0 {\n\t\t\treturn\n\t\t}\n\n\t\tfor naluDataRemaining > 0 {\n\t\t\tcurrentFragmentSize := min(maxFragmentSize, naluDataRemaining)\n\t\t\tout := make([]byte, fuaHeaderSize+currentFragmentSize)\n\n\t\t\t// +---------------+\n\t\t\t// |0|1|2|3|4|5|6|7|\n\t\t\t// +-+-+-+-+-+-+-+-+\n\t\t\t// |F|NRI|  Type   |\n\t\t\t// +---------------+\n\t\t\tout[0] = fuaNALUType\n\t\t\tout[0] |= naluRefIdc\n\n\t\t\t// +---------------+\n\t\t\t// |0|1|2|3|4|5|6|7|\n\t\t\t// +-+-+-+-+-+-+-+-+\n\t\t\t// |S|E|R|  Type   |\n\t\t\t// +---------------+\n\n\t\t\tout[1] = naluType\n\t\t\tif naluDataRemaining == naluDataLength {\n\t\t\t\t// Set start bit\n\t\t\t\tout[1] |= 1 << 7\n\t\t\t} else if naluDataRemaining-currentFragmentSize == 0 {\n\t\t\t\t// Set end bit\n\t\t\t\tout[1] |= 1 << 6\n\t\t\t}\n\n\t\t\tcopy(out[fuaHeaderSize:], naluData[naluDataIndex:naluDataIndex+currentFragmentSize])\n\t\t\tpayloads = append(payloads, out)\n\n\t\t\tnaluDataRemaining -= currentFragmentSize\n\t\t\tnaluDataIndex += currentFragmentSize\n\t\t}\n\t})\n\n\treturn payloads\n}\n\nfunc min(a, b int) int {\n\tif a < b {\n\t\treturn a\n\t}\n\treturn b\n}\n"
  },
  {
    "path": "pkg/h264/rtp.go",
    "content": "package h264\n\nimport (\n\t\"encoding/binary\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264/annexb\"\n\t\"github.com/pion/rtp\"\n\t\"github.com/pion/rtp/codecs\"\n)\n\nconst RTPPacketVersionAVC = 0\n\nconst PSMaxSize = 128 // the biggest SPS I've seen is 48 (EZVIZ CS-CV210)\n\nfunc RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {\n\tdepack := &codecs.H264Packet{IsAVC: true}\n\n\tsps, pps := GetParameterSet(codec.FmtpLine)\n\tps := JoinNALU(sps, pps)\n\n\tbuf := make([]byte, 0, 512*1024) // 512K\n\n\treturn func(packet *rtp.Packet) {\n\t\t//log.Printf(\"[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v\", codec.Name, packet.Payload[0]&0x1F, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)\n\n\t\tpayload, err := depack.Unmarshal(packet.Payload)\n\t\tif len(payload) == 0 || err != nil {\n\t\t\treturn\n\t\t}\n\n\t\t// Memory overflow protection. Can happen if we miss a lot of packets with the marker.\n\t\t// https://github.com/AlexxIT/go2rtc/issues/675\n\t\tif len(buf) > 5*1024*1024 {\n\t\t\tbuf = buf[: 0 : 512*1024]\n\t\t}\n\n\t\t// Fix TP-Link Tapo TC70: sends SPS and PPS with packet.Marker = true\n\t\t// Reolink Duo 2: sends SPS with Marker and PPS without\n\t\tif packet.Marker && len(payload) < PSMaxSize {\n\t\t\tswitch NALUType(payload) {\n\t\t\tcase NALUTypeSPS, NALUTypePPS:\n\t\t\t\tbuf = append(buf, payload...)\n\t\t\t\treturn\n\t\t\tcase NALUTypeSEI:\n\t\t\t\t// RtspServer https://github.com/AlexxIT/go2rtc/issues/244\n\t\t\t\t// sends, marked SPS, marked PPS, marked SEI, marked IFrame\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tif len(buf) == 0 {\n\t\t\tfor {\n\t\t\t\t// Amcrest IP4M-1051: 9, 7, 8, 6, 28...\n\t\t\t\t// Amcrest IP4M-1051: 9, 6, 1\n\t\t\t\tswitch NALUType(payload) {\n\t\t\t\tcase NALUTypeIFrame:\n\t\t\t\t\t// fix IFrame without SPS,PPS\n\t\t\t\t\tbuf = append(buf, ps...)\n\t\t\t\tcase NALUTypeSEI, NALUTypeAUD:\n\t\t\t\t\t// fix ffmpeg with transcoding first frame\n\t\t\t\t\ti := int(4 + binary.BigEndian.Uint32(payload))\n\n\t\t\t\t\t// check if only one NAL (fix ffmpeg transcoding for Reolink RLC-510A)\n\t\t\t\t\tif i == len(payload) {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\n\t\t\t\t\tpayload = payload[i:]\n\t\t\t\t\tcontinue\n\t\t\t\tcase NALUTypePFrame, NALUTypeSPS, NALUTypePPS: // pass\n\t\t\t\tdefault:\n\t\t\t\t\treturn // skip any unknown NAL unit type\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\t// collect all NALs for Access Unit\n\t\tif !packet.Marker {\n\t\t\tbuf = append(buf, payload...)\n\t\t\treturn\n\t\t}\n\n\t\tif len(buf) > 0 {\n\t\t\tpayload = append(buf, payload...)\n\t\t\tbuf = buf[:0]\n\t\t}\n\n\t\t// should not be that huge SPS\n\t\tif NALUType(payload) == NALUTypeSPS && binary.BigEndian.Uint32(payload) >= PSMaxSize {\n\t\t\t// some Chinese buggy cameras have a single packet with SPS+PPS+IFrame separated by 00 00 00 01\n\t\t\t// https://github.com/AlexxIT/WebRTC/issues/391\n\t\t\t// https://github.com/AlexxIT/WebRTC/issues/392\n\t\t\tpayload = annexb.FixAnnexBInAVCC(payload)\n\t\t}\n\n\t\t//log.Printf(\"[AVC] %v, len: %d, ts: %10d, seq: %d\", NALUTypes(payload), len(payload), packet.Timestamp, packet.SequenceNumber)\n\n\t\tclone := *packet\n\t\tclone.Version = RTPPacketVersionAVC\n\t\tclone.Payload = payload\n\t\thandler(&clone)\n\t}\n}\n\nfunc RTPPay(mtu uint16, handler core.HandlerFunc) core.HandlerFunc {\n\tif mtu == 0 {\n\t\tmtu = 1472\n\t}\n\n\tpayloader := &Payloader{IsAVC: true}\n\tsequencer := rtp.NewRandomSequencer()\n\tmtu -= 12 // rtp.Header size\n\n\treturn func(packet *rtp.Packet) {\n\t\tif packet.Version != RTPPacketVersionAVC {\n\t\t\thandler(packet)\n\t\t\treturn\n\t\t}\n\n\t\tpayloads := payloader.Payload(mtu, packet.Payload)\n\t\tlast := len(payloads) - 1\n\t\tfor i, payload := range payloads {\n\t\t\tclone := rtp.Packet{\n\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\tVersion:        2,\n\t\t\t\t\tMarker:         i == last,\n\t\t\t\t\tSequenceNumber: sequencer.NextSequenceNumber(),\n\t\t\t\t\tTimestamp:      packet.Timestamp,\n\t\t\t\t},\n\t\t\t\tPayload: payload,\n\t\t\t}\n\t\t\thandler(&clone)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/h264/sps.go",
    "content": "package h264\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/bits\"\n)\n\n// http://www.itu.int/rec/T-REC-H.264\n// https://webrtc.googlesource.com/src/+/refs/heads/main/common_video/h264/sps_parser.cc\n\n//goland:noinspection GoSnakeCaseUsage\ntype SPS struct {\n\tprofile_idc uint8\n\tprofile_iop uint8\n\tlevel_idc   uint8\n\n\tseq_parameter_set_id uint32\n\n\tchroma_format_idc                    uint32\n\tseparate_colour_plane_flag           byte\n\tbit_depth_luma_minus8                uint32\n\tbit_depth_chroma_minus8              uint32\n\tqpprime_y_zero_transform_bypass_flag byte\n\tseq_scaling_matrix_present_flag      byte\n\n\tlog2_max_frame_num_minus4             uint32\n\tpic_order_cnt_type                    uint32\n\tlog2_max_pic_order_cnt_lsb_minus4     uint32\n\tdelta_pic_order_always_zero_flag      byte\n\toffset_for_non_ref_pic                int32\n\toffset_for_top_to_bottom_field        int32\n\tnum_ref_frames_in_pic_order_cnt_cycle uint32\n\tnum_ref_frames                        uint32\n\tgaps_in_frame_num_value_allowed_flag  byte\n\n\tpic_width_in_mbs_minus_1        uint32\n\tpic_height_in_map_units_minus_1 uint32\n\tframe_mbs_only_flag             byte\n\tmb_adaptive_frame_field_flag    byte\n\tdirect_8x8_inference_flag       byte\n\n\tframe_cropping_flag      byte\n\tframe_crop_left_offset   uint32\n\tframe_crop_right_offset  uint32\n\tframe_crop_top_offset    uint32\n\tframe_crop_bottom_offset uint32\n\n\tvui_parameters_present_flag    byte\n\taspect_ratio_info_present_flag byte\n\taspect_ratio_idc               byte\n\tsar_width                      uint16\n\tsar_height                     uint16\n\n\toverscan_info_present_flag byte\n\toverscan_appropriate_flag  byte\n\n\tvideo_signal_type_present_flag byte\n\tvideo_format                   uint8\n\tvideo_full_range_flag          byte\n\n\tcolour_description_present_flag byte\n\tcolour_description              uint32\n\n\tchroma_loc_info_present_flag        byte\n\tchroma_sample_loc_type_top_field    uint32\n\tchroma_sample_loc_type_bottom_field uint32\n\n\ttiming_info_present_flag byte\n\tnum_units_in_tick        uint32\n\ttime_scale               uint32\n\tfixed_frame_rate_flag    byte\n}\n\nfunc (s *SPS) Width() uint16 {\n\twidth := 16 * (s.pic_width_in_mbs_minus_1 + 1)\n\tcrop := 2 * (s.frame_crop_left_offset + s.frame_crop_right_offset)\n\treturn uint16(width - crop)\n}\n\nfunc (s *SPS) Height() uint16 {\n\theight := 16 * (s.pic_height_in_map_units_minus_1 + 1)\n\tcrop := 2 * (s.frame_crop_top_offset + s.frame_crop_bottom_offset)\n\tif s.frame_mbs_only_flag == 0 {\n\t\theight *= 2\n\t}\n\treturn uint16(height - crop)\n}\n\nfunc DecodeSPS(sps []byte) *SPS {\n\t// https://developer.ridgerun.com/wiki/index.php/H264_Analysis_Tools\n\t// ffmpeg -i file.h264 -c copy -bsf:v trace_headers -f null -\n\tr := bits.NewReader(sps)\n\n\thdr := r.ReadByte()\n\tif hdr&0x1F != NALUTypeSPS {\n\t\treturn nil\n\t}\n\n\ts := &SPS{\n\t\tprofile_idc:          r.ReadByte(),\n\t\tprofile_iop:          r.ReadByte(),\n\t\tlevel_idc:            r.ReadByte(),\n\t\tseq_parameter_set_id: r.ReadUEGolomb(),\n\t}\n\n\tswitch s.profile_idc {\n\tcase 100, 110, 122, 244, 44, 83, 86, 118, 128, 138, 139, 134, 135:\n\t\tn := byte(8)\n\n\t\ts.chroma_format_idc = r.ReadUEGolomb()\n\t\tif s.chroma_format_idc == 3 {\n\t\t\ts.separate_colour_plane_flag = r.ReadBit()\n\t\t\tn = 12\n\t\t}\n\n\t\ts.bit_depth_luma_minus8 = r.ReadUEGolomb()\n\t\ts.bit_depth_chroma_minus8 = r.ReadUEGolomb()\n\t\ts.qpprime_y_zero_transform_bypass_flag = r.ReadBit()\n\n\t\ts.seq_scaling_matrix_present_flag = r.ReadBit()\n\t\tif s.seq_scaling_matrix_present_flag != 0 {\n\t\t\tfor i := byte(0); i < n; i++ {\n\t\t\t\t//goland:noinspection GoSnakeCaseUsage\n\t\t\t\tseq_scaling_list_present_flag := r.ReadBit()\n\t\t\t\tif seq_scaling_list_present_flag != 0 {\n\t\t\t\t\tif i < 6 {\n\t\t\t\t\t\ts.scaling_list(r, 16)\n\t\t\t\t\t} else {\n\t\t\t\t\t\ts.scaling_list(r, 64)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\ts.log2_max_frame_num_minus4 = r.ReadUEGolomb()\n\n\ts.pic_order_cnt_type = r.ReadUEGolomb()\n\tswitch s.pic_order_cnt_type {\n\tcase 0:\n\t\ts.log2_max_pic_order_cnt_lsb_minus4 = r.ReadUEGolomb()\n\tcase 1:\n\t\ts.delta_pic_order_always_zero_flag = r.ReadBit()\n\t\ts.offset_for_non_ref_pic = r.ReadSEGolomb()\n\t\ts.offset_for_top_to_bottom_field = r.ReadSEGolomb()\n\n\t\ts.num_ref_frames_in_pic_order_cnt_cycle = r.ReadUEGolomb()\n\t\tfor i := uint32(0); i < s.num_ref_frames_in_pic_order_cnt_cycle; i++ {\n\t\t\t_ = r.ReadSEGolomb() // offset_for_ref_frame[i]\n\t\t}\n\t}\n\n\ts.num_ref_frames = r.ReadUEGolomb()\n\ts.gaps_in_frame_num_value_allowed_flag = r.ReadBit()\n\n\ts.pic_width_in_mbs_minus_1 = r.ReadUEGolomb()\n\ts.pic_height_in_map_units_minus_1 = r.ReadUEGolomb()\n\n\ts.frame_mbs_only_flag = r.ReadBit()\n\tif s.frame_mbs_only_flag == 0 {\n\t\ts.mb_adaptive_frame_field_flag = r.ReadBit()\n\t}\n\n\ts.direct_8x8_inference_flag = r.ReadBit()\n\n\ts.frame_cropping_flag = r.ReadBit()\n\tif s.frame_cropping_flag != 0 {\n\t\ts.frame_crop_left_offset = r.ReadUEGolomb()\n\t\ts.frame_crop_right_offset = r.ReadUEGolomb()\n\t\ts.frame_crop_top_offset = r.ReadUEGolomb()\n\t\ts.frame_crop_bottom_offset = r.ReadUEGolomb()\n\t}\n\n\ts.vui_parameters_present_flag = r.ReadBit()\n\tif s.vui_parameters_present_flag != 0 {\n\t\ts.aspect_ratio_info_present_flag = r.ReadBit()\n\t\tif s.aspect_ratio_info_present_flag != 0 {\n\t\t\ts.aspect_ratio_idc = r.ReadByte()\n\t\t\tif s.aspect_ratio_idc == 255 {\n\t\t\t\ts.sar_width = r.ReadUint16()\n\t\t\t\ts.sar_height = r.ReadUint16()\n\t\t\t}\n\t\t}\n\n\t\ts.overscan_info_present_flag = r.ReadBit()\n\t\tif s.overscan_info_present_flag != 0 {\n\t\t\ts.overscan_appropriate_flag = r.ReadBit()\n\t\t}\n\n\t\ts.video_signal_type_present_flag = r.ReadBit()\n\t\tif s.video_signal_type_present_flag != 0 {\n\t\t\ts.video_format = r.ReadBits8(3)\n\t\t\ts.video_full_range_flag = r.ReadBit()\n\n\t\t\ts.colour_description_present_flag = r.ReadBit()\n\t\t\tif s.colour_description_present_flag != 0 {\n\t\t\t\ts.colour_description = r.ReadUint24()\n\t\t\t}\n\t\t}\n\n\t\ts.chroma_loc_info_present_flag = r.ReadBit()\n\t\tif s.chroma_loc_info_present_flag != 0 {\n\t\t\ts.chroma_sample_loc_type_top_field = r.ReadUEGolomb()\n\t\t\ts.chroma_sample_loc_type_bottom_field = r.ReadUEGolomb()\n\t\t}\n\n\t\ts.timing_info_present_flag = r.ReadBit()\n\t\tif s.timing_info_present_flag != 0 {\n\t\t\ts.num_units_in_tick = r.ReadUint32()\n\t\t\ts.time_scale = r.ReadUint32()\n\t\t\ts.fixed_frame_rate_flag = r.ReadBit()\n\t\t}\n\t\t//...\n\t}\n\n\tif r.EOF {\n\t\treturn nil\n\t}\n\n\treturn s\n}\n\n//goland:noinspection GoSnakeCaseUsage\nfunc (s *SPS) scaling_list(r *bits.Reader, sizeOfScalingList int) {\n\tlastScale := int32(8)\n\tnextScale := int32(8)\n\tfor j := 0; j < sizeOfScalingList; j++ {\n\t\tif nextScale != 0 {\n\t\t\tdelta_scale := r.ReadSEGolomb()\n\t\t\tnextScale = (lastScale + delta_scale + 256) % 256\n\t\t}\n\t\tif nextScale != 0 {\n\t\t\tlastScale = nextScale\n\t\t}\n\t}\n}\n\nfunc (s *SPS) Profile() string {\n\tswitch s.profile_idc {\n\tcase 0x42:\n\t\treturn \"Baseline\"\n\tcase 0x4D:\n\t\treturn \"Main\"\n\tcase 0x58:\n\t\treturn \"Extended\"\n\tcase 0x64:\n\t\treturn \"High\"\n\t}\n\treturn fmt.Sprintf(\"0x%02X\", s.profile_idc)\n}\n\nfunc (s *SPS) PixFmt() string {\n\tif s.bit_depth_luma_minus8 == 0 {\n\t\tswitch s.chroma_format_idc {\n\t\tcase 1:\n\t\t\tif s.video_full_range_flag == 1 {\n\t\t\t\treturn \"yuvj420p\"\n\t\t\t}\n\t\t\treturn \"yuv420p\"\n\t\tcase 2:\n\t\t\treturn \"yuv422p\"\n\t\tcase 3:\n\t\t\treturn \"yuv444p\"\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc (s *SPS) String() string {\n\treturn fmt.Sprintf(\n\t\t\"%s %d.%d, %s, %dx%d\",\n\t\ts.Profile(), s.level_idc/10, s.level_idc%10, s.PixFmt(), s.Width(), s.Height(),\n\t)\n}\n\n// FixPixFmt - change yuvj420p to yuv420p in SPS\n// same as \"-c:v copy -bsf:v h264_metadata=video_full_range_flag=0\"\nfunc FixPixFmt(sps []byte) {\n\tr := bits.NewReader(sps)\n\n\t_ = r.ReadByte()\n\n\tprofile := r.ReadByte()\n\t_ = r.ReadByte()\n\t_ = r.ReadByte()\n\t_ = r.ReadUEGolomb()\n\n\tswitch profile {\n\tcase 100, 110, 122, 244, 44, 83, 86, 118, 128, 138, 139, 134, 135:\n\t\tn := byte(8)\n\n\t\tif r.ReadUEGolomb() == 3 {\n\t\t\t_ = r.ReadBit()\n\t\t\tn = 12\n\t\t}\n\n\t\t_ = r.ReadUEGolomb()\n\t\t_ = r.ReadUEGolomb()\n\t\t_ = r.ReadBit()\n\n\t\tif r.ReadBit() != 0 {\n\t\t\tfor i := byte(0); i < n; i++ {\n\t\t\t\tif r.ReadBit() != 0 {\n\t\t\t\t\treturn // skip\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t_ = r.ReadUEGolomb()\n\n\tswitch r.ReadUEGolomb() {\n\tcase 0:\n\t\t_ = r.ReadUEGolomb()\n\tcase 1:\n\t\t_ = r.ReadBit()\n\t\t_ = r.ReadSEGolomb()\n\t\t_ = r.ReadSEGolomb()\n\n\t\tn := r.ReadUEGolomb()\n\t\tfor i := uint32(0); i < n; i++ {\n\t\t\t_ = r.ReadSEGolomb()\n\t\t}\n\t}\n\n\t_ = r.ReadUEGolomb()\n\t_ = r.ReadBit()\n\n\t_ = r.ReadUEGolomb()\n\t_ = r.ReadUEGolomb()\n\n\tif r.ReadBit() == 0 {\n\t\t_ = r.ReadBit()\n\t}\n\n\t_ = r.ReadBit()\n\n\tif r.ReadBit() != 0 {\n\t\t_ = r.ReadUEGolomb()\n\t\t_ = r.ReadUEGolomb()\n\t\t_ = r.ReadUEGolomb()\n\t\t_ = r.ReadUEGolomb()\n\t}\n\n\tif r.ReadBit() != 0 {\n\t\tif r.ReadBit() != 0 {\n\t\t\tif r.ReadByte() == 255 {\n\t\t\t\t_ = r.ReadUint16()\n\t\t\t\t_ = r.ReadUint16()\n\t\t\t}\n\t\t}\n\n\t\tif r.ReadBit() != 0 {\n\t\t\t_ = r.ReadBit()\n\t\t}\n\n\t\tif r.ReadBit() != 0 {\n\t\t\t_ = r.ReadBits8(3)\n\t\t\tif r.ReadBit() == 1 {\n\t\t\t\tpos, bit := r.Pos()\n\t\t\t\tsps[pos] &= ^byte(1 << bit)\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/h265/README.md",
    "content": "# H265\n\nPayloader code taken from [pion](https://github.com/pion/rtp) library branch [h265](https://github.com/pion/rtp/tree/h265), because it's still not in release. Thanks to [@kevmo314](https://github.com/kevmo314).\n\n## Useful links\n\n- https://datatracker.ietf.org/doc/html/rfc7798\n- [Add initial support for WebRTC HEVC](https://trac.webkit.org/changeset/259452/webkit)\n"
  },
  {
    "path": "pkg/h265/avc.go",
    "content": "package h265\n\nimport \"github.com/AlexxIT/go2rtc/pkg/h264\"\n\nconst forbiddenZeroBit = 0x80\nconst nalUnitType = 0x3F\n\n// Deprecated: DecodeStream - find and return first AU in AVC format\n// useful for processing live streams with unknown separator size\nfunc DecodeStream(annexb []byte) ([]byte, int) {\n\tstartPos := -1\n\n\ti := 0\n\tfor {\n\t\t// search next separator\n\t\tif i = h264.IndexFrom(annexb, []byte{0, 0, 1}, i); i < 0 {\n\t\t\tbreak\n\t\t}\n\n\t\t// move i to next AU\n\t\tif i += 3; i >= len(annexb) {\n\t\t\tbreak\n\t\t}\n\n\t\t// check if AU type valid\n\t\toctet := annexb[i]\n\t\tif octet&forbiddenZeroBit != 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tnalType := (octet >> 1) & nalUnitType\n\t\tif startPos >= 0 {\n\t\t\tswitch nalType {\n\t\t\tcase NALUTypeVPS, NALUTypePFrame:\n\t\t\t\tif annexb[i-4] == 0 {\n\t\t\t\t\treturn h264.DecodeAnnexB(annexb[startPos : i-4]), i - 4\n\t\t\t\t} else {\n\t\t\t\t\treturn h264.DecodeAnnexB(annexb[startPos : i-3]), i - 3\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tswitch nalType {\n\t\t\tcase NALUTypeVPS, NALUTypePFrame:\n\t\t\t\tif i >= 4 && annexb[i-4] == 0 {\n\t\t\t\t\tstartPos = i - 4\n\t\t\t\t} else {\n\t\t\t\t\tstartPos = i - 3\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil, 0\n}\n"
  },
  {
    "path": "pkg/h265/avcc.go",
    "content": "// Package h265 - AVCC format related functions\npackage h265\n\nimport (\n\t\"bytes\"\n\t\"encoding/base64\"\n\t\"encoding/binary\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264\"\n\t\"github.com/pion/rtp\"\n)\n\nfunc RepairAVCC(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {\n\tvds, sps, pps := GetParameterSet(codec.FmtpLine)\n\tps := h264.JoinNALU(vds, sps, pps)\n\n\treturn func(packet *rtp.Packet) {\n\t\tswitch NALUType(packet.Payload) {\n\t\tcase NALUTypeIFrame, NALUTypeIFrame2, NALUTypeIFrame3:\n\t\t\tclone := *packet\n\t\t\tclone.Payload = h264.Join(ps, packet.Payload)\n\t\t\thandler(&clone)\n\t\tdefault:\n\t\t\thandler(packet)\n\t\t}\n\t}\n}\n\nfunc AVCCToCodec(avcc []byte) *core.Codec {\n\tbuf := bytes.NewBufferString(\"profile-id=1\")\n\n\tfor {\n\t\tsize := 4 + int(binary.BigEndian.Uint32(avcc))\n\n\t\tswitch NALUType(avcc) {\n\t\tcase NALUTypeVPS:\n\t\t\tbuf.WriteString(\";sprop-vps=\")\n\t\t\tbuf.WriteString(base64.StdEncoding.EncodeToString(avcc[4:size]))\n\t\tcase NALUTypeSPS:\n\t\t\tbuf.WriteString(\";sprop-sps=\")\n\t\t\tbuf.WriteString(base64.StdEncoding.EncodeToString(avcc[4:size]))\n\t\tcase NALUTypePPS:\n\t\t\tbuf.WriteString(\";sprop-pps=\")\n\t\t\tbuf.WriteString(base64.StdEncoding.EncodeToString(avcc[4:size]))\n\t\t}\n\n\t\tif size < len(avcc) {\n\t\t\tavcc = avcc[size:]\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\t}\n\n\treturn &core.Codec{\n\t\tName:        core.CodecH265,\n\t\tClockRate:   90000,\n\t\tFmtpLine:    buf.String(),\n\t\tPayloadType: core.PayloadTypeRAW,\n\t}\n}\n"
  },
  {
    "path": "pkg/h265/h265_test.go",
    "content": "package h265\n\nimport (\n\t\"encoding/base64\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDecodeSPS(t *testing.T) {\n\ts := \"QgEBAWAAAAMAAAMAAAMAAAMAmaAAoAgBaH+KrTuiS7/8AAQABbAgApMuADN/mAE=\"\n\tb, err := base64.StdEncoding.DecodeString(s)\n\trequire.Nil(t, err)\n\n\tsps := DecodeSPS(b)\n\trequire.NotNil(t, sps)\n\trequire.Equal(t, uint16(5120), sps.Width())\n\trequire.Equal(t, uint16(1440), sps.Height())\n}\n\nfunc TestDecodeSPS2(t *testing.T) {\n\ts := \"QgEBIUAAAAMAkAAAAwAAAwCWoAUCAWlnpbkShc1AQIC4QAAAAwBAAAAFFEn/eEAOpgAV+V8IBBA=\"\n\tb, err := base64.StdEncoding.DecodeString(s)\n\trequire.Nil(t, err)\n\n\tsps := DecodeSPS(b)\n\trequire.NotNil(t, sps)\n\trequire.Equal(t, uint16(640), sps.Width())\n\trequire.Equal(t, uint16(360), sps.Height())\n}\n"
  },
  {
    "path": "pkg/h265/helper.go",
    "content": "package h265\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/binary\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\nconst (\n\tNALUTypePFrame    = 1\n\tNALUTypeIFrame    = 19\n\tNALUTypeIFrame2   = 20\n\tNALUTypeIFrame3   = 21\n\tNALUTypeVPS       = 32\n\tNALUTypeSPS       = 33\n\tNALUTypePPS       = 34\n\tNALUTypePrefixSEI = 39\n\tNALUTypeSuffixSEI = 40\n\tNALUTypeFU        = 49\n)\n\nfunc NALUType(b []byte) byte {\n\treturn (b[4] >> 1) & 0x3F\n}\n\nfunc IsKeyframe(b []byte) bool {\n\tfor {\n\t\tswitch NALUType(b) {\n\t\tcase NALUTypePFrame:\n\t\t\treturn false\n\t\tcase NALUTypeIFrame, NALUTypeIFrame2, NALUTypeIFrame3:\n\t\t\treturn true\n\t\t}\n\n\t\tsize := int(binary.BigEndian.Uint32(b)) + 4\n\t\tif size < len(b) {\n\t\t\tb = b[size:]\n\t\t\tcontinue\n\t\t} else {\n\t\t\treturn false\n\t\t}\n\t}\n}\n\nfunc Types(data []byte) []byte {\n\tvar types []byte\n\tfor {\n\t\ttypes = append(types, NALUType(data))\n\n\t\tsize := 4 + int(binary.BigEndian.Uint32(data))\n\t\tif size < len(data) {\n\t\t\tdata = data[size:]\n\t\t} else {\n\t\t\tbreak\n\t\t}\n\t}\n\treturn types\n}\n\nfunc GetParameterSet(fmtp string) (vps, sps, pps []byte) {\n\tif fmtp == \"\" {\n\t\treturn\n\t}\n\n\ts := core.Between(fmtp, \"sprop-vps=\", \";\")\n\tvps, _ = base64.StdEncoding.DecodeString(s)\n\n\ts = core.Between(fmtp, \"sprop-sps=\", \";\")\n\tsps, _ = base64.StdEncoding.DecodeString(s)\n\n\ts = core.Between(fmtp, \"sprop-pps=\", \";\")\n\tpps, _ = base64.StdEncoding.DecodeString(s)\n\n\treturn\n}\n"
  },
  {
    "path": "pkg/h265/mpeg4.go",
    "content": "// Package h265 - MPEG4 format related functions\npackage h265\n\nimport (\n\t\"bytes\"\n\t\"encoding/base64\"\n\t\"encoding/binary\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\nfunc DecodeConfig(conf []byte) (profile, vps, sps, pps []byte) {\n\tprofile = conf[1:4]\n\n\tb := conf[23:]\n\tif binary.BigEndian.Uint16(b[1:]) != 1 {\n\t\treturn\n\t}\n\tvpsSize := binary.BigEndian.Uint16(b[3:])\n\tvps = b[5 : 5+vpsSize]\n\n\tb = conf[23+5+vpsSize:]\n\tif binary.BigEndian.Uint16(b[1:]) != 1 {\n\t\treturn\n\t}\n\tspsSize := binary.BigEndian.Uint16(b[3:])\n\tsps = b[5 : 5+spsSize]\n\n\tb = conf[23+5+vpsSize+5+spsSize:]\n\tif binary.BigEndian.Uint16(b[1:]) != 1 {\n\t\treturn\n\t}\n\tppsSize := binary.BigEndian.Uint16(b[3:])\n\tpps = b[5 : 5+ppsSize]\n\n\treturn\n}\n\nfunc EncodeConfig(vps, sps, pps []byte) []byte {\n\tvpsSize := uint16(len(vps))\n\tspsSize := uint16(len(sps))\n\tppsSize := uint16(len(pps))\n\n\tbuf := make([]byte, 23+5+vpsSize+5+spsSize+5+ppsSize)\n\n\tbuf[0] = 1\n\tcopy(buf[1:], sps[3:6]) // profile\n\tbuf[21] = 3             // ?\n\tbuf[22] = 3             // ?\n\n\tb := buf[23:]\n\t_ = b[5]\n\tb[0] = (vps[0] >> 1) & 0x3F\n\tbinary.BigEndian.PutUint16(b[1:], 1) // VPS count\n\tbinary.BigEndian.PutUint16(b[3:], vpsSize)\n\tcopy(b[5:], vps)\n\n\tb = buf[23+5+vpsSize:]\n\t_ = b[5]\n\tb[0] = (sps[0] >> 1) & 0x3F\n\tbinary.BigEndian.PutUint16(b[1:], 1) // SPS count\n\tbinary.BigEndian.PutUint16(b[3:], spsSize)\n\tcopy(b[5:], sps)\n\n\tb = buf[23+5+vpsSize+5+spsSize:]\n\t_ = b[5]\n\tb[0] = (pps[0] >> 1) & 0x3F\n\tbinary.BigEndian.PutUint16(b[1:], 1) // PPS count\n\tbinary.BigEndian.PutUint16(b[3:], ppsSize)\n\tcopy(b[5:], pps)\n\n\treturn buf\n}\n\nfunc ConfigToCodec(conf []byte) *core.Codec {\n\tbuf := bytes.NewBufferString(\"profile-id=1\")\n\n\t_, vps, sps, pps := DecodeConfig(conf)\n\tif vps != nil {\n\t\tbuf.WriteString(\";sprop-vps=\")\n\t\tbuf.WriteString(base64.StdEncoding.EncodeToString(vps))\n\t}\n\tif sps != nil {\n\t\tbuf.WriteString(\";sprop-sps=\")\n\t\tbuf.WriteString(base64.StdEncoding.EncodeToString(sps))\n\t}\n\tif pps != nil {\n\t\tbuf.WriteString(\";sprop-pps=\")\n\t\tbuf.WriteString(base64.StdEncoding.EncodeToString(pps))\n\t}\n\n\treturn &core.Codec{\n\t\tName:        core.CodecH265,\n\t\tClockRate:   90000,\n\t\tFmtpLine:    buf.String(),\n\t\tPayloadType: core.PayloadTypeRAW,\n\t}\n}\n"
  },
  {
    "path": "pkg/h265/payloader.go",
    "content": "package h265\n\nimport (\n\t\"encoding/binary\"\n\t\"math\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/h264\"\n)\n\n//\n// Network Abstraction Unit Header implementation\n//\n\nconst (\n\t// sizeof(uint16)\n\th265NaluHeaderSize = 2\n\t// https://datatracker.ietf.org/doc/html/rfc7798#section-4.4.2\n\th265NaluAggregationPacketType = 48\n\t// https://datatracker.ietf.org/doc/html/rfc7798#section-4.4.3\n\th265NaluFragmentationUnitType = 49\n\t// https://datatracker.ietf.org/doc/html/rfc7798#section-4.4.4\n\th265NaluPACIPacketType = 50\n)\n\n// H265NALUHeader is a H265 NAL Unit Header\n// https://datatracker.ietf.org/doc/html/rfc7798#section-1.1.4\n// +---------------+---------------+\n//\n//\t|0|1|2|3|4|5|6|7|0|1|2|3|4|5|6|7|\n//\t+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+\n//\t|F|   Type    |  LayerID  | TID |\n//\t+-------------+-----------------+\ntype H265NALUHeader uint16\n\nfunc newH265NALUHeader(highByte, lowByte uint8) H265NALUHeader {\n\treturn H265NALUHeader((uint16(highByte) << 8) | uint16(lowByte))\n}\n\n// F is the forbidden bit, should always be 0.\nfunc (h H265NALUHeader) F() bool {\n\treturn (uint16(h) >> 15) != 0\n}\n\n// Type of NAL Unit.\nfunc (h H265NALUHeader) Type() uint8 {\n\t// 01111110 00000000\n\tconst mask = 0b01111110 << 8\n\treturn uint8((uint16(h) & mask) >> (8 + 1))\n}\n\n// IsTypeVCLUnit returns whether or not the NAL Unit type is a VCL NAL unit.\nfunc (h H265NALUHeader) IsTypeVCLUnit() bool {\n\t// Type is coded on 6 bits\n\tconst msbMask = 0b00100000\n\treturn (h.Type() & msbMask) == 0\n}\n\n// LayerID should always be 0 in non-3D HEVC context.\nfunc (h H265NALUHeader) LayerID() uint8 {\n\t// 00000001 11111000\n\tconst mask = (0b00000001 << 8) | 0b11111000\n\treturn uint8((uint16(h) & mask) >> 3)\n}\n\n// TID is the temporal identifier of the NAL unit +1.\nfunc (h H265NALUHeader) TID() uint8 {\n\tconst mask = 0b00000111\n\treturn uint8(uint16(h) & mask)\n}\n\n// IsAggregationPacket returns whether or not the packet is an Aggregation packet.\nfunc (h H265NALUHeader) IsAggregationPacket() bool {\n\treturn h.Type() == h265NaluAggregationPacketType\n}\n\n// IsFragmentationUnit returns whether or not the packet is a Fragmentation Unit packet.\nfunc (h H265NALUHeader) IsFragmentationUnit() bool {\n\treturn h.Type() == h265NaluFragmentationUnitType\n}\n\n// IsPACIPacket returns whether or not the packet is a PACI packet.\nfunc (h H265NALUHeader) IsPACIPacket() bool {\n\treturn h.Type() == h265NaluPACIPacketType\n}\n\n//\n// Fragmentation Unit implementation\n//\n\nconst (\n\t// sizeof(uint8)\n\th265FragmentationUnitHeaderSize = 1\n)\n\n// H265FragmentationUnitHeader is a H265 FU Header\n// +---------------+\n// |0|1|2|3|4|5|6|7|\n// +-+-+-+-+-+-+-+-+\n// |S|E|  FuType   |\n// +---------------+\ntype H265FragmentationUnitHeader uint8\n\n// S represents the start of a fragmented NAL unit.\nfunc (h H265FragmentationUnitHeader) S() bool {\n\tconst mask = 0b10000000\n\treturn ((h & mask) >> 7) != 0\n}\n\n// E represents the end of a fragmented NAL unit.\nfunc (h H265FragmentationUnitHeader) E() bool {\n\tconst mask = 0b01000000\n\treturn ((h & mask) >> 6) != 0\n}\n\n// FuType MUST be equal to the field Type of the fragmented NAL unit.\nfunc (h H265FragmentationUnitHeader) FuType() uint8 {\n\tconst mask = 0b00111111\n\treturn uint8(h) & mask\n}\n\n// Payloader payloads H265 packets\ntype Payloader struct {\n\tAddDONL         bool\n\tSkipAggregation bool\n\tdonl            uint16\n}\n\n// Payload fragments a H265 packet across one or more byte arrays\nfunc (p *Payloader) Payload(mtu uint16, payload []byte) [][]byte {\n\tvar payloads [][]byte\n\tif len(payload) == 0 {\n\t\treturn payloads\n\t}\n\n\tbufferedNALUs := make([][]byte, 0)\n\taggregationBufferSize := 0\n\n\tflushBufferedNals := func() {\n\t\tif len(bufferedNALUs) == 0 {\n\t\t\treturn\n\t\t}\n\t\tif len(bufferedNALUs) == 1 {\n\t\t\t// emit this as a single NALU packet\n\t\t\tnalu := bufferedNALUs[0]\n\n\t\t\tif p.AddDONL {\n\t\t\t\tbuf := make([]byte, len(nalu)+2)\n\n\t\t\t\t// copy the NALU header to the payload header\n\t\t\t\tcopy(buf[0:h265NaluHeaderSize], nalu[0:h265NaluHeaderSize])\n\n\t\t\t\t// copy the DONL into the header\n\t\t\t\tbinary.BigEndian.PutUint16(buf[h265NaluHeaderSize:h265NaluHeaderSize+2], p.donl)\n\n\t\t\t\t// write the payload\n\t\t\t\tcopy(buf[h265NaluHeaderSize+2:], nalu[h265NaluHeaderSize:])\n\n\t\t\t\tp.donl++\n\n\t\t\t\tpayloads = append(payloads, buf)\n\t\t\t} else {\n\t\t\t\t// write the nalu directly to the payload\n\t\t\t\tpayloads = append(payloads, nalu)\n\t\t\t}\n\t\t} else {\n\t\t\t// construct an aggregation packet\n\t\t\taggregationPacketSize := aggregationBufferSize + 2\n\t\t\tbuf := make([]byte, aggregationPacketSize)\n\n\t\t\tlayerID := uint8(math.MaxUint8)\n\t\t\ttid := uint8(math.MaxUint8)\n\t\t\tfor _, nalu := range bufferedNALUs {\n\t\t\t\theader := newH265NALUHeader(nalu[0], nalu[1])\n\t\t\t\theaderLayerID := header.LayerID()\n\t\t\t\theaderTID := header.TID()\n\t\t\t\tif headerLayerID < layerID {\n\t\t\t\t\tlayerID = headerLayerID\n\t\t\t\t}\n\t\t\t\tif headerTID < tid {\n\t\t\t\t\ttid = headerTID\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tbinary.BigEndian.PutUint16(buf[0:2], (uint16(h265NaluAggregationPacketType)<<9)|(uint16(layerID)<<3)|uint16(tid))\n\n\t\t\tindex := 2\n\t\t\tfor i, nalu := range bufferedNALUs {\n\t\t\t\tif p.AddDONL {\n\t\t\t\t\tif i == 0 {\n\t\t\t\t\t\tbinary.BigEndian.PutUint16(buf[index:index+2], p.donl)\n\t\t\t\t\t\tindex += 2\n\t\t\t\t\t} else {\n\t\t\t\t\t\tbuf[index] = byte(i - 1)\n\t\t\t\t\t\tindex++\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbinary.BigEndian.PutUint16(buf[index:index+2], uint16(len(nalu)))\n\t\t\t\tindex += 2\n\t\t\t\tindex += copy(buf[index:], nalu)\n\t\t\t}\n\t\t\tpayloads = append(payloads, buf)\n\t\t}\n\t\t// clear the buffered NALUs\n\t\tbufferedNALUs = make([][]byte, 0)\n\t\taggregationBufferSize = 0\n\t}\n\n\th264.EmitNalus(payload, true, func(nalu []byte) {\n\t\tif len(nalu) == 0 {\n\t\t\treturn\n\t\t}\n\n\t\tif len(nalu) <= int(mtu) {\n\t\t\t// this nalu fits into a single packet, either it can be emitted as\n\t\t\t// a single nalu or appended to the previous aggregation packet\n\n\t\t\tmarginalAggregationSize := len(nalu) + 2\n\t\t\tif p.AddDONL {\n\t\t\t\tmarginalAggregationSize += 1\n\t\t\t}\n\n\t\t\tif aggregationBufferSize+marginalAggregationSize > int(mtu) {\n\t\t\t\tflushBufferedNals()\n\t\t\t}\n\t\t\tbufferedNALUs = append(bufferedNALUs, nalu)\n\t\t\taggregationBufferSize += marginalAggregationSize\n\t\t\tif p.SkipAggregation {\n\t\t\t\t// emit this immediately.\n\t\t\t\tflushBufferedNals()\n\t\t\t}\n\t\t} else {\n\t\t\t// if this nalu doesn't fit in the current mtu, it needs to be fragmented\n\t\t\tfuPacketHeaderSize := h265FragmentationUnitHeaderSize + 2 /* payload header size */\n\t\t\tif p.AddDONL {\n\t\t\t\tfuPacketHeaderSize += 2\n\t\t\t}\n\n\t\t\t// then, fragment the nalu\n\t\t\tmaxFUPayloadSize := int(mtu) - fuPacketHeaderSize\n\n\t\t\tnaluHeader := newH265NALUHeader(nalu[0], nalu[1])\n\n\t\t\t// the nalu header is omitted from the fragmentation packet payload\n\t\t\tnalu = nalu[h265NaluHeaderSize:]\n\n\t\t\tif maxFUPayloadSize == 0 || len(nalu) == 0 {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// flush any buffered aggregation packets.\n\t\t\tflushBufferedNals()\n\n\t\t\tfullNALUSize := len(nalu)\n\t\t\tfor len(nalu) > 0 {\n\t\t\t\tcurentFUPayloadSize := len(nalu)\n\t\t\t\tif curentFUPayloadSize > maxFUPayloadSize {\n\t\t\t\t\tcurentFUPayloadSize = maxFUPayloadSize\n\t\t\t\t}\n\n\t\t\t\tout := make([]byte, fuPacketHeaderSize+curentFUPayloadSize)\n\n\t\t\t\t// write the payload header\n\t\t\t\tbinary.BigEndian.PutUint16(out[0:2], uint16(naluHeader))\n\t\t\t\tout[0] = (out[0] & 0b10000001) | h265NaluFragmentationUnitType<<1\n\n\t\t\t\t// write the fragment header\n\t\t\t\tout[2] = byte(H265FragmentationUnitHeader(naluHeader.Type()))\n\t\t\t\tif len(nalu) == fullNALUSize {\n\t\t\t\t\t// Set start bit\n\t\t\t\t\tout[2] |= 1 << 7\n\t\t\t\t} else if len(nalu)-curentFUPayloadSize == 0 {\n\t\t\t\t\t// Set end bit\n\t\t\t\t\tout[2] |= 1 << 6\n\t\t\t\t}\n\n\t\t\t\tif p.AddDONL {\n\t\t\t\t\t// write the DONL header\n\t\t\t\t\tbinary.BigEndian.PutUint16(out[3:5], p.donl)\n\n\t\t\t\t\tp.donl++\n\n\t\t\t\t\t// copy the fragment payload\n\t\t\t\t\tcopy(out[5:], nalu[0:curentFUPayloadSize])\n\t\t\t\t} else {\n\t\t\t\t\t// copy the fragment payload\n\t\t\t\t\tcopy(out[3:], nalu[0:curentFUPayloadSize])\n\t\t\t\t}\n\n\t\t\t\t// append the fragment to the payload\n\t\t\t\tpayloads = append(payloads, out)\n\n\t\t\t\t// advance the nalu data pointer\n\t\t\t\tnalu = nalu[curentFUPayloadSize:]\n\t\t\t}\n\t\t}\n\t})\n\n\tflushBufferedNals()\n\n\treturn payloads\n}\n"
  },
  {
    "path": "pkg/h265/rtp.go",
    "content": "package h265\n\nimport (\n\t\"encoding/binary\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264\"\n\t\"github.com/pion/rtp\"\n)\n\nfunc RTPDepay(codec *core.Codec, handler core.HandlerFunc) core.HandlerFunc {\n\tvps, sps, pps := GetParameterSet(codec.FmtpLine)\n\tps := h264.JoinNALU(vps, sps, pps)\n\n\tbuf := make([]byte, 0, 512*1024) // 512K\n\tvar nuStart int\n\tvar seqNum uint16\n\n\treturn func(packet *rtp.Packet) {\n\t\tdata := packet.Payload\n\t\tif len(data) < 3 {\n\t\t\treturn\n\t\t}\n\n\t\tnuType := (data[0] >> 1) & 0x3F\n\t\t//log.Printf(\"[RTP] codec: %s, nalu: %2d, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, %v\", track.Codec.Name, nuType, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)\n\n\t\t// Fix for RtspServer https://github.com/AlexxIT/go2rtc/issues/244\n\t\tif packet.Marker && len(data) < h264.PSMaxSize {\n\t\t\tswitch nuType {\n\t\t\tcase NALUTypeVPS, NALUTypeSPS, NALUTypePPS:\n\t\t\t\tpacket.Marker = false\n\t\t\tcase NALUTypePrefixSEI, NALUTypeSuffixSEI:\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\t// when we collect data into one buffer, we need to make sure\n\t\t// that all of it falls into the same sequence\n\t\tif len(buf) > 0 && packet.SequenceNumber-seqNum != 1 {\n\t\t\t//log.Printf(\"broken H265 sequence\")\n\t\t\tbuf = buf[:0] // drop data\n\t\t\treturn\n\t\t}\n\n\t\tseqNum = packet.SequenceNumber\n\n\t\tif nuType == NALUTypeFU {\n\t\t\tswitch data[2] >> 6 {\n\t\t\tcase 0b10: // begin\n\t\t\t\tnuType = data[2] & 0x3F\n\n\t\t\t\t// push PS data before keyframe\n\t\t\t\tif len(buf) == 0 && nuType >= 19 && nuType <= 21 {\n\t\t\t\t\tbuf = append(buf, ps...)\n\t\t\t\t}\n\n\t\t\t\tnuStart = len(buf)\n\t\t\t\tbuf = append(buf, 0, 0, 0, 0) // NAL unit size\n\t\t\t\tbuf = append(buf, (data[0]&0x81)|(nuType<<1), data[1])\n\t\t\t\tbuf = append(buf, data[3:]...)\n\t\t\t\treturn\n\t\t\tcase 0b00: // continue\n\t\t\t\tif len(buf) == 0 {\n\t\t\t\t\t//log.Printf(\"broken H265 fragment\")\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tbuf = append(buf, data[3:]...)\n\t\t\t\treturn\n\t\t\tcase 0b01: // end\n\t\t\t\tif len(buf) == 0 {\n\t\t\t\t\t//log.Printf(\"broken H265 fragment\")\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tbuf = append(buf, data[3:]...)\n\n\t\t\t\tif nuStart > len(buf)+4 {\n\t\t\t\t\t//log.Printf(\"broken H265 fragment\")\n\t\t\t\t\tbuf = buf[:0] // drop data\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tbinary.BigEndian.PutUint32(buf[nuStart:], uint32(len(buf)-nuStart-4))\n\t\t\tcase 0b11: // wrong RFC 7798 realisation from OpenIPC project\n\t\t\t\t// A non-fragmented NAL unit MUST NOT be transmitted in one FU; i.e.,\n\t\t\t\t// the Start bit and End bit must not both be set to 1 in the same FU\n\t\t\t\t// header.\n\t\t\t\tnuType = data[2] & 0x3F\n\t\t\t\tbuf = binary.BigEndian.AppendUint32(buf, uint32(len(data))-1) // NAL unit size\n\t\t\t\tbuf = append(buf, (data[0]&0x81)|(nuType<<1), data[1])\n\t\t\t\tbuf = append(buf, data[3:]...)\n\t\t\t}\n\t\t} else {\n\t\t\tbuf = binary.BigEndian.AppendUint32(buf, uint32(len(data))) // NAL unit size\n\t\t\tbuf = append(buf, data...)\n\t\t}\n\n\t\t// collect all NAL Units for Access Unit\n\t\tif !packet.Marker {\n\t\t\treturn\n\t\t}\n\n\t\t//log.Printf(\"[HEVC] %v, len: %d\", Types(buf), len(buf))\n\n\t\tclone := *packet\n\t\tclone.Version = h264.RTPPacketVersionAVC\n\t\tclone.Payload = buf\n\n\t\tbuf = buf[:0]\n\n\t\thandler(&clone)\n\t}\n}\n\nfunc RTPPay(mtu uint16, handler core.HandlerFunc) core.HandlerFunc {\n\tif mtu == 0 {\n\t\tmtu = 1472\n\t}\n\n\tpayloader := &Payloader{}\n\tsequencer := rtp.NewRandomSequencer()\n\tmtu -= 12 // rtp.Header size\n\n\treturn func(packet *rtp.Packet) {\n\t\tif packet.Version != h264.RTPPacketVersionAVC {\n\t\t\thandler(packet)\n\t\t\treturn\n\t\t}\n\n\t\tpayloads := payloader.Payload(mtu, packet.Payload)\n\t\tlast := len(payloads) - 1\n\t\tfor i, payload := range payloads {\n\t\t\tclone := rtp.Packet{\n\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\tVersion:        2,\n\t\t\t\t\tMarker:         i == last,\n\t\t\t\t\tSequenceNumber: sequencer.NextSequenceNumber(),\n\t\t\t\t\tTimestamp:      packet.Timestamp,\n\t\t\t\t},\n\t\t\t\tPayload: payload,\n\t\t\t}\n\t\t\thandler(&clone)\n\t\t}\n\t}\n}\n\n// SafariPay - generate Safari friendly payload for H265\n// https://github.com/AlexxIT/Blog/issues/5\nfunc SafariPay(mtu uint16, handler core.HandlerFunc) core.HandlerFunc {\n\tsequencer := rtp.NewRandomSequencer()\n\tsize := int(mtu - 12) // rtp.Header size\n\n\treturn func(packet *rtp.Packet) {\n\t\tif packet.Version != h264.RTPPacketVersionAVC {\n\t\t\thandler(packet)\n\t\t\treturn\n\t\t}\n\n\t\t// protect original packets from modification\n\t\tau := make([]byte, len(packet.Payload))\n\t\tcopy(au, packet.Payload)\n\n\t\tvar start byte\n\n\t\tfor i := 0; i < len(au); {\n\t\t\tsize := int(binary.BigEndian.Uint32(au[i:])) + 4\n\n\t\t\t// convert AVC to Annex-B\n\t\t\tau[i] = 0\n\t\t\tau[i+1] = 0\n\t\t\tau[i+2] = 0\n\t\t\tau[i+3] = 1\n\n\t\t\tswitch NALUType(au[i:]) {\n\t\t\tcase NALUTypeIFrame, NALUTypeIFrame2, NALUTypeIFrame3:\n\t\t\t\tstart = 3\n\t\t\tdefault:\n\t\t\t\tif start == 0 {\n\t\t\t\t\tstart = 2\n\t\t\t\t}\n\t\t\t}\n\n\t\t\ti += size\n\t\t}\n\n\t\t// rtp.Packet payload\n\t\tb := make([]byte, 1, size)\n\t\tsize-- // minus header byte\n\n\t\tfor au != nil {\n\t\t\tb[0] = start\n\n\t\t\tif start > 1 {\n\t\t\t\tstart -= 2\n\t\t\t}\n\n\t\t\tif len(au) > size {\n\t\t\t\tb = append(b, au[:size]...)\n\t\t\t\tau = au[size:]\n\t\t\t} else {\n\t\t\t\tb = append(b, au...)\n\t\t\t\tau = nil\n\t\t\t}\n\n\t\t\tclone := rtp.Packet{\n\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\tVersion:        2,\n\t\t\t\t\tMarker:         au == nil,\n\t\t\t\t\tSequenceNumber: sequencer.NextSequenceNumber(),\n\t\t\t\t\tTimestamp:      packet.Timestamp,\n\t\t\t\t},\n\t\t\t\tPayload: b,\n\t\t\t}\n\t\t\thandler(&clone)\n\n\t\t\tb = b[:1] // clear buffer\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/h265/sps.go",
    "content": "package h265\n\nimport (\n\t\"bytes\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/bits\"\n)\n\n// http://www.itu.int/rec/T-REC-H.265\n\n//goland:noinspection GoSnakeCaseUsage\ntype SPS struct {\n\tsps_video_parameter_set_id   uint8\n\tsps_max_sub_layers_minus1    uint8\n\tsps_temporal_id_nesting_flag byte\n\n\tgeneral_profile_space               uint8\n\tgeneral_tier_flag                   byte\n\tgeneral_profile_idc                 uint8\n\tgeneral_profile_compatibility_flags uint32\n\n\tgeneral_level_idc              uint8\n\tsub_layer_profile_present_flag []byte\n\tsub_layer_level_present_flag   []byte\n\n\tsps_seq_parameter_set_id   uint32\n\tchroma_format_idc          uint32\n\tseparate_colour_plane_flag byte\n\n\tpic_width_in_luma_samples  uint32\n\tpic_height_in_luma_samples uint32\n}\n\nfunc (s *SPS) Width() uint16 {\n\treturn uint16(s.pic_width_in_luma_samples)\n}\n\nfunc (s *SPS) Height() uint16 {\n\treturn uint16(s.pic_height_in_luma_samples)\n}\n\nfunc DecodeSPS(nalu []byte) *SPS {\n\trbsp := bytes.ReplaceAll(nalu[2:], []byte{0, 0, 3}, []byte{0, 0})\n\n\tr := bits.NewReader(rbsp)\n\ts := &SPS{}\n\n\ts.sps_video_parameter_set_id = r.ReadBits8(4)\n\ts.sps_max_sub_layers_minus1 = r.ReadBits8(3)\n\ts.sps_temporal_id_nesting_flag = r.ReadBit()\n\n\tif !s.profile_tier_level(r) {\n\t\treturn nil\n\t}\n\n\ts.sps_seq_parameter_set_id = r.ReadUEGolomb()\n\ts.chroma_format_idc = r.ReadUEGolomb()\n\tif s.chroma_format_idc == 3 {\n\t\ts.separate_colour_plane_flag = r.ReadBit()\n\t}\n\n\ts.pic_width_in_luma_samples = r.ReadUEGolomb()\n\ts.pic_height_in_luma_samples = r.ReadUEGolomb()\n\n\t//...\n\n\tif r.EOF {\n\t\treturn nil\n\t}\n\n\treturn s\n}\n\n// profile_tier_level supports ONLY general_profile_idc == 1\n// over variants very complicated...\n//\n//goland:noinspection GoSnakeCaseUsage\nfunc (s *SPS) profile_tier_level(r *bits.Reader) bool {\n\ts.general_profile_space = r.ReadBits8(2)\n\ts.general_tier_flag = r.ReadBit()\n\ts.general_profile_idc = r.ReadBits8(5)\n\n\ts.general_profile_compatibility_flags = r.ReadBits(32)\n\t_ = r.ReadBits64(48) // other flags\n\n\tif s.general_profile_idc != 1 {\n\t\treturn false\n\t}\n\n\ts.general_level_idc = r.ReadBits8(8)\n\n\ts.sub_layer_profile_present_flag = make([]byte, s.sps_max_sub_layers_minus1)\n\ts.sub_layer_level_present_flag = make([]byte, s.sps_max_sub_layers_minus1)\n\n\tfor i := byte(0); i < s.sps_max_sub_layers_minus1; i++ {\n\t\ts.sub_layer_profile_present_flag[i] = r.ReadBit()\n\t\ts.sub_layer_level_present_flag[i] = r.ReadBit()\n\t}\n\n\tif s.sps_max_sub_layers_minus1 > 0 {\n\t\tfor i := s.sps_max_sub_layers_minus1; i < 8; i++ {\n\t\t\t_ = r.ReadBits8(2) // reserved_zero_2bits\n\t\t}\n\t}\n\n\tfor i := byte(0); i < s.sps_max_sub_layers_minus1; i++ {\n\t\tif s.sub_layer_profile_present_flag[i] != 0 {\n\t\t\t_ = r.ReadBits8(2)                      // sub_layer_profile_space\n\t\t\t_ = r.ReadBit()                         // sub_layer_tier_flag\n\t\t\tsub_layer_profile_idc := r.ReadBits8(5) // sub_layer_profile_idc\n\n\t\t\t_ = r.ReadBits(32)   // sub_layer_profile_compatibility_flag\n\t\t\t_ = r.ReadBits64(48) // other flags\n\n\t\t\tif sub_layer_profile_idc != 1 {\n\t\t\t\treturn false\n\t\t\t}\n\t\t}\n\n\t\tif s.sub_layer_level_present_flag[i] != 0 {\n\t\t\t_ = r.ReadBits8(8)\n\t\t}\n\t}\n\n\treturn true\n}\n"
  },
  {
    "path": "pkg/hap/README.md",
    "content": "# Home Accessory Protocol\n\n> PS. Character = Characteristic\n\n**Device** - HomeKit end device (swith, camera, etc)\n\n- mDNS name: `MyCamera._hap._tcp.local.`\n- DeviceID - mac-like: `0E:AA:CE:2B:35:71`\n- HomeKit device is described by:\n  - one or more `Accessories` - has `AID` and `Services`  \n  - `Services` - has `IID`, `Type` and `Characters`  \n  - `Characters` - has `IID`, `Type`, `Format` and `Value`\n\n**Client** - HomeKit client (iPhone, iPad, MacBook or opensource library)\n\n- ClientID - static random UUID\n- ClientPublic/ClientPrivate - static random 32 byte keypair\n- can pair with Device (exchange ClientID/ClientPublic, ServerID/ServerPublic using Pin)\n- can auth to Device using ClientPrivate\n- holding persistant Secure connection to device\n- can read device Accessories\n- can read and write device Characters\n- can subscribe on device Characters change (Event)\n\n**Server** - HomeKit server (soft on end device or opensource library)\n\n- ServerID - same as DeviceID (using for Client auth)\n- ServerPublic/ServerPrivate - static random 32 byte keypair\n\n## AAC ELD \n\nRequires ffmpeg built with `--enable-libfdk-aac`\n\n```\n-acodec libfdk_aac -aprofile aac_eld \n```\n\n| SampleRate | RTPTime | constantDuration   | objectType   |\n|------------|---------|--------------------|--------------|\n| 8000       | 60      | =8000/1000*60=480  | 39 (AAC ELD) |\n| 16000      | 30      | =16000/1000*30=480 | 39 (AAC ELD) |\n| 24000      | 20      | =24000/1000*20=480 | 39 (AAC ELD) |\n| 16000      | 60      | =16000/1000*60=960 | 23 (AAC LD)  |\n| 24000      | 40      | =24000/1000*40=960 | 23 (AAC LD)  |\n\n## Useful links\n\n- https://github.com/apple/HomeKitADK/blob/master/Documentation/crypto.md\n- https://github.com/apple/HomeKitADK/blob/master/HAP/HAPPairingPairSetup.c\n- [Extracting HomeKit Pairing Keys](https://pvieito.com/2019/12/extract-homekit-pairing-keys)\n- [HAP in AirPlay2 receiver](https://github.com/openairplay/airplay2-receiver/blob/master/ap2/pairing/hap.py)\n- [HomeKit Secure Video Unofficial Specification](https://github.com/Supereg/secure-video-specification)\n- [Homebridge Camera FFmpeg](https://sunoo.github.io/homebridge-camera-ffmpeg/configs/)\n- https://github.com/ljezny/Particle-HAP/blob/master/HAP-Specification-Non-Commercial-Version.pdf"
  },
  {
    "path": "pkg/hap/accessory.go",
    "content": "package hap\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n)\n\nconst (\n\tFormatString = \"string\"\n\tFormatBool   = \"bool\"\n\tFormatFloat  = \"float\"\n\tFormatUInt8  = \"uint8\"\n\tFormatUInt16 = \"uint16\"\n\tFormatUInt32 = \"uint32\"\n\tFormatInt32  = \"int32\"\n\tFormatUInt64 = \"uint64\"\n\tFormatData   = \"data\"\n\tFormatTLV8   = \"tlv8\"\n\n\tUnitPercentage = \"percentage\"\n)\n\nvar PR = []string{\"pr\"}\nvar PW = []string{\"pw\"}\nvar PRPW = []string{\"pr\", \"pw\"}\nvar EVPRPW = []string{\"ev\", \"pr\", \"pw\"}\nvar EVPR = []string{\"ev\", \"pr\"}\n\ntype Accessory struct {\n\tAID      uint8      `json:\"aid\"` // 150 unique accessories per bridge\n\tServices []*Service `json:\"services\"`\n}\n\nfunc (a *Accessory) InitIID() {\n\tserviceN := map[string]byte{}\n\tfor _, service := range a.Services {\n\t\tif len(service.Type) > 3 {\n\t\t\tpanic(service.Type)\n\t\t}\n\n\t\tn := serviceN[service.Type] + 1\n\t\tserviceN[service.Type] = n\n\n\t\tif n > 15 {\n\t\t\tpanic(n)\n\t\t}\n\n\t\t// ServiceID   = ANSSS000\n\t\ts := fmt.Sprintf(\"%x%x%03s000\", a.AID, n, service.Type)\n\t\tservice.IID, _ = strconv.ParseUint(s, 16, 64)\n\n\t\tfor _, character := range service.Characters {\n\t\t\tif len(character.Type) > 3 {\n\t\t\t\tpanic(character.Type)\n\t\t\t}\n\n\t\t\t// CharacterID = ANSSSCCC\n\t\t\tcharacter.IID, _ = strconv.ParseUint(character.Type, 16, 64)\n\t\t\tcharacter.IID += service.IID\n\t\t}\n\t}\n}\n\nfunc (a *Accessory) GetService(servType string) *Service {\n\tfor _, serv := range a.Services {\n\t\tif serv.Type == servType {\n\t\t\treturn serv\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (a *Accessory) GetCharacter(charType string) *Character {\n\tfor _, serv := range a.Services {\n\t\tfor _, char := range serv.Characters {\n\t\t\tif char.Type == charType {\n\t\t\t\treturn char\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (a *Accessory) GetCharacterByID(iid uint64) *Character {\n\tfor _, serv := range a.Services {\n\t\tfor _, char := range serv.Characters {\n\t\t\tif char.IID == iid {\n\t\t\t\treturn char\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\ntype Service struct {\n\tDesc string `json:\"description,omitempty\"`\n\n\tType       string       `json:\"type\"`\n\tIID        uint64       `json:\"iid\"`\n\tPrimary    bool         `json:\"primary,omitempty\"`\n\tCharacters []*Character `json:\"characteristics\"`\n\tLinked     []int        `json:\"linked,omitempty\"`\n}\n\nfunc (s *Service) GetCharacter(charType string) *Character {\n\tfor _, char := range s.Characters {\n\t\tif char.Type == charType {\n\t\t\treturn char\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc ServiceAccessoryInformation(manuf, model, name, serial, firmware string) *Service {\n\treturn &Service{\n\t\tType: \"3E\", // AccessoryInformation\n\t\tCharacters: []*Character{\n\t\t\t{\n\t\t\t\tType:   \"14\",\n\t\t\t\tFormat: FormatBool,\n\t\t\t\tPerms:  PW,\n\t\t\t\t//Descr:  \"Identify\",\n\t\t\t}, {\n\t\t\t\tType:   \"20\",\n\t\t\t\tFormat: FormatString,\n\t\t\t\tValue:  manuf,\n\t\t\t\tPerms:  PR,\n\t\t\t\t//Descr:  \"Manufacturer\",\n\t\t\t\t//MaxLen: 64,\n\t\t\t}, {\n\t\t\t\tType:   \"21\",\n\t\t\t\tFormat: FormatString,\n\t\t\t\tValue:  model,\n\t\t\t\tPerms:  PR,\n\t\t\t\t//Descr:  \"Model\",\n\t\t\t\t//MaxLen: 64,\n\t\t\t}, {\n\t\t\t\tType:   \"23\",\n\t\t\t\tFormat: FormatString,\n\t\t\t\tValue:  name,\n\t\t\t\tPerms:  PR,\n\t\t\t\t//Descr:  \"Name\",\n\t\t\t\t//MaxLen: 64,\n\t\t\t}, {\n\t\t\t\tType:   \"30\",\n\t\t\t\tFormat: FormatString,\n\t\t\t\tValue:  serial,\n\t\t\t\tPerms:  PR,\n\t\t\t\t//Descr:  \"Serial Number\",\n\t\t\t\t//MaxLen: 64,\n\t\t\t}, {\n\t\t\t\tType:   \"52\",\n\t\t\t\tFormat: FormatString,\n\t\t\t\tValue:  firmware,\n\t\t\t\tPerms:  PR,\n\t\t\t\t//Descr:  \"Firmware Revision\",\n\t\t\t},\n\t\t},\n\t}\n}\n\nfunc ServiceHAPProtocolInformation() *Service {\n\treturn &Service{\n\t\tType: \"A2\", // 'HAPProtocolInformation'\n\t\tCharacters: []*Character{\n\t\t\t{\n\t\t\t\tType:   \"37\",\n\t\t\t\tFormat: FormatString,\n\t\t\t\tValue:  \"1.1.0\",\n\t\t\t\tPerms:  PR,\n\t\t\t\t//Descr:  \"Version\",\n\t\t\t\t//MaxLen: 64,\n\t\t\t},\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "pkg/hap/camera/README.md",
    "content": "## Useful links\n\n- https://github.com/bauer-andreas/secure-video-specification\n"
  },
  {
    "path": "pkg/hap/camera/accessory.go",
    "content": "package camera\n\nimport (\n\t\"github.com/AlexxIT/go2rtc/pkg/hap\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hap/tlv8\"\n)\n\nfunc NewAccessory(manuf, model, name, serial, firmware string) *hap.Accessory {\n\tacc := &hap.Accessory{\n\t\tAID: hap.DeviceAID,\n\t\tServices: []*hap.Service{\n\t\t\thap.ServiceAccessoryInformation(manuf, model, name, serial, firmware),\n\t\t\tServiceCameraRTPStreamManagement(),\n\t\t\t//hap.ServiceHAPProtocolInformation(),\n\t\t\tServiceMicrophone(),\n\t\t},\n\t}\n\tacc.InitIID()\n\treturn acc\n}\n\nfunc ServiceMicrophone() *hap.Service {\n\treturn &hap.Service{\n\t\tType: \"112\", // 'Microphone'\n\t\tCharacters: []*hap.Character{\n\t\t\t{\n\t\t\t\tType:   \"11A\",\n\t\t\t\tFormat: hap.FormatBool,\n\t\t\t\tValue:  0,\n\t\t\t\tPerms:  hap.EVPRPW,\n\t\t\t\t//Descr:  \"Mute\",\n\t\t\t},\n\t\t\t//{\n\t\t\t//\tType:   \"119\",\n\t\t\t//\tFormat: hap.FormatUInt8,\n\t\t\t//\tValue:  100,\n\t\t\t//\tPerms:  hap.EVPRPW,\n\t\t\t//\t//Descr:    \"Volume\",\n\t\t\t//\t//Unit:     hap.UnitPercentage,\n\t\t\t//\t//MinValue: 0,\n\t\t\t//\t//MaxValue: 100,\n\t\t\t//\t//MinStep:  1,\n\t\t\t//},\n\t\t},\n\t}\n}\n\nfunc ServiceCameraRTPStreamManagement() *hap.Service {\n\tval120, _ := tlv8.MarshalBase64(StreamingStatus{\n\t\tStatus: StreamingStatusAvailable,\n\t})\n\tval114, _ := tlv8.MarshalBase64(SupportedVideoStreamConfiguration{\n\t\tCodecs: []VideoCodecConfiguration{\n\t\t\t{\n\t\t\t\tCodecType: VideoCodecTypeH264,\n\t\t\t\tCodecParams: []VideoCodecParameters{\n\t\t\t\t\t{\n\t\t\t\t\t\tProfileID: []byte{VideoCodecProfileMain},\n\t\t\t\t\t\tLevel:     []byte{VideoCodecLevel31, VideoCodecLevel40},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tVideoAttrs: []VideoCodecAttributes{\n\t\t\t\t\t{Width: 1920, Height: 1080, Framerate: 30},\n\t\t\t\t\t{Width: 1280, Height: 720, Framerate: 30}, // important for iPhones\n\t\t\t\t\t{Width: 320, Height: 240, Framerate: 15},  // apple watch\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t})\n\tval115, _ := tlv8.MarshalBase64(SupportedAudioStreamConfiguration{\n\t\tCodecs: []AudioCodecConfiguration{\n\t\t\t{\n\t\t\t\tCodecType: AudioCodecTypeOpus,\n\t\t\t\tCodecParams: []AudioCodecParameters{\n\t\t\t\t\t{\n\t\t\t\t\t\tChannels:    1,\n\t\t\t\t\t\tBitrateMode: AudioCodecBitrateVariable,\n\t\t\t\t\t\tSampleRate:  []byte{AudioCodecSampleRate16Khz},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\tComfortNoiseSupport: 0,\n\t})\n\tval116, _ := tlv8.MarshalBase64(SupportedRTPConfiguration{\n\t\tSRTPCryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80},\n\t})\n\n\tservice := &hap.Service{\n\t\tType: \"110\", // 'CameraRTPStreamManagement'\n\t\tCharacters: []*hap.Character{\n\t\t\t{\n\t\t\t\tType:   TypeStreamingStatus,\n\t\t\t\tFormat: hap.FormatTLV8,\n\t\t\t\tValue:  val120,\n\t\t\t\tPerms:  hap.EVPR,\n\t\t\t\t//Descr:  \"Streaming Status\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tType:   TypeSupportedVideoStreamConfiguration,\n\t\t\t\tFormat: hap.FormatTLV8,\n\t\t\t\tValue:  val114,\n\t\t\t\tPerms:  hap.PR,\n\t\t\t\t//Descr:  \"Supported Video Stream Configuration\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tType:   TypeSupportedAudioStreamConfiguration,\n\t\t\t\tFormat: hap.FormatTLV8,\n\t\t\t\tValue:  val115,\n\t\t\t\tPerms:  hap.PR,\n\t\t\t\t//Descr:  \"Supported Audio Stream Configuration\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tType:   TypeSupportedRTPConfiguration,\n\t\t\t\tFormat: hap.FormatTLV8,\n\t\t\t\tValue:  val116,\n\t\t\t\tPerms:  hap.PR,\n\t\t\t\t//Descr:  \"Supported RTP Configuration\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tType:   \"B0\",\n\t\t\t\tFormat: hap.FormatUInt8,\n\t\t\t\tValue:  1,\n\t\t\t\tPerms:  hap.EVPRPW,\n\t\t\t\t//Descr:    \"Active\",\n\t\t\t\t//MinValue: 0,\n\t\t\t\t//MaxValue: 1,\n\t\t\t\t//MinStep:  1,\n\t\t\t\t//ValidVal: []any{0, 1},\n\t\t\t},\n\t\t\t{\n\t\t\t\tType:   TypeSelectedStreamConfiguration,\n\t\t\t\tFormat: hap.FormatTLV8,\n\t\t\t\tValue:  \"\", // important empty\n\t\t\t\tPerms:  hap.PRPW,\n\t\t\t\t//Descr:  \"Selected RTP Stream Configuration\",\n\t\t\t},\n\t\t\t{\n\t\t\t\tType:   TypeSetupEndpoints,\n\t\t\t\tFormat: hap.FormatTLV8,\n\t\t\t\tValue:  \"\", // important empty\n\t\t\t\tPerms:  hap.PRPW,\n\t\t\t\t//Descr:  \"Setup Endpoints\",\n\t\t\t},\n\t\t},\n\t}\n\n\treturn service\n}\n"
  },
  {
    "path": "pkg/hap/camera/accessory_test.go",
    "content": "package camera\n\nimport (\n\t\"encoding/base64\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/hap\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestNilCharacter(t *testing.T) {\n\tvar res SetupEndpoints\n\tchar := &hap.Character{}\n\terr := char.ReadTLV8(&res)\n\trequire.NotNil(t, err)\n\trequire.NotNil(t, strings.Contains(err.Error(), \"can't read value\"))\n}\n\ntype testTLV8 struct {\n\tname    string\n\tvalue   string\n\tactual  any\n\texpect  any\n\tnoequal bool\n}\n\nfunc (test testTLV8) run(t *testing.T) {\n\tif test.actual == nil {\n\t\treturn\n\t}\n\n\tsrc := &hap.Character{Value: test.value, Format: hap.FormatTLV8}\n\terr := src.ReadTLV8(test.actual)\n\trequire.Nil(t, err)\n\n\trequire.Equal(t, test.expect, test.actual)\n\n\tdst := &hap.Character{Format: hap.FormatTLV8}\n\terr = dst.Write(test.actual)\n\trequire.Nil(t, err)\n\n\ta, _ := base64.StdEncoding.DecodeString(test.value)\n\tb, _ := base64.StdEncoding.DecodeString(dst.Value.(string))\n\tt.Logf(\"%x\\n\", a)\n\tt.Logf(\"%x\\n\", b)\n\n\tif !test.noequal {\n\t\trequire.Equal(t, test.value, dst.Value)\n\t}\n}\n\nfunc TestAqaraG3(t *testing.T) {\n\ttests := []testTLV8{\n\t\t{\n\t\t\tname:   \"120\",\n\t\t\tvalue:  \"AQEA\",\n\t\t\tactual: &StreamingStatus{},\n\t\t\texpect: &StreamingStatus{\n\t\t\t\tStatus: StreamingStatusAvailable,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"114\",\n\t\t\tvalue:  \"AaoBAQACEQEBAQIBAAAAAgECAwEABAEAAwsBAoAHAgI4BAMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAkABAgK0AAMBHgAAAwsBAgAFAgLAAwMBHgAAAwsBAgAEAgIAAwMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAkABAgLwAAMBHg==\",\n\t\t\tactual: &SupportedVideoStreamConfiguration{},\n\t\t\texpect: &SupportedVideoStreamConfiguration{\n\t\t\t\tCodecs: []VideoCodecConfiguration{\n\t\t\t\t\t{\n\t\t\t\t\t\tCodecType: VideoCodecTypeH264,\n\t\t\t\t\t\tCodecParams: []VideoCodecParameters{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tProfileID:  []byte{VideoCodecProfileMain},\n\t\t\t\t\t\t\t\tLevel:      []byte{VideoCodecLevel31, VideoCodecLevel40},\n\t\t\t\t\t\t\t\tCVOEnabled: []byte{0},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tVideoAttrs: []VideoCodecAttributes{\n\t\t\t\t\t\t\t{Width: 1920, Height: 1080, Framerate: 30},\n\t\t\t\t\t\t\t{Width: 1280, Height: 720, Framerate: 30},\n\t\t\t\t\t\t\t{Width: 640, Height: 360, Framerate: 30},\n\t\t\t\t\t\t\t{Width: 480, Height: 270, Framerate: 30},\n\t\t\t\t\t\t\t{Width: 320, Height: 180, Framerate: 30},\n\t\t\t\t\t\t\t{Width: 1280, Height: 960, Framerate: 30},\n\t\t\t\t\t\t\t{Width: 1024, Height: 768, Framerate: 30},\n\t\t\t\t\t\t\t{Width: 640, Height: 480, Framerate: 30},\n\t\t\t\t\t\t\t{Width: 480, Height: 360, Framerate: 30},\n\t\t\t\t\t\t\t{Width: 320, Height: 240, Framerate: 30},\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:   \"115\",\n\t\t\tvalue:  \"AQ4BAQICCQEBAQIBAAMBAQIBAA==\",\n\t\t\tactual: &SupportedAudioStreamConfiguration{},\n\t\t\texpect: &SupportedAudioStreamConfiguration{\n\t\t\t\tCodecs: []AudioCodecConfiguration{\n\t\t\t\t\t{\n\t\t\t\t\t\tCodecType: AudioCodecTypeAACELD,\n\t\t\t\t\t\tCodecParams: []AudioCodecParameters{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tChannels:    1,\n\t\t\t\t\t\t\t\tBitrateMode: AudioCodecBitrateVariable,\n\t\t\t\t\t\t\t\tSampleRate:  []byte{AudioCodecSampleRate16Khz},\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\tComfortNoiseSupport: 0,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"116\",\n\t\t\tvalue:  \"AgEAAAACAQEAAAIBAg==\",\n\t\t\tactual: &SupportedRTPConfiguration{},\n\t\t\texpect: &SupportedRTPConfiguration{\n\t\t\t\tSRTPCryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80, CryptoAES_CM_256_HMAC_SHA1_80, CryptoDisabled},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, test.run)\n\t}\n}\n\nfunc TestHomebridge(t *testing.T) {\n\ttests := []testTLV8{\n\t\t{\n\t\t\tname:   \"114\",\n\t\t\tvalue:  \"AcUBAQACHQEBAAAAAQEBAAABAQICAQAAAAIBAQAAAgECAwEAAwsBAkABAgK0AAMBHgAAAwsBAkABAgLwAAMBDwAAAwsBAkABAgLwAAMBHgAAAwsBAuABAgIOAQMBHgAAAwsBAuABAgJoAQMBHgAAAwsBAoACAgJoAQMBHgAAAwsBAoACAgLgAQMBHgAAAwsBAgAFAgLQAgMBHgAAAwsBAgAFAgLAAwMBHgAAAwsBAoAHAgI4BAMBHgAAAwsBAkAGAgKwBAMBHg==\",\n\t\t\tactual: &SupportedVideoStreamConfiguration{},\n\t\t\texpect: &SupportedVideoStreamConfiguration{\n\t\t\t\tCodecs: []VideoCodecConfiguration{\n\t\t\t\t\t{\n\t\t\t\t\t\tCodecType: VideoCodecTypeH264,\n\t\t\t\t\t\tCodecParams: []VideoCodecParameters{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tProfileID: []byte{VideoCodecProfileConstrainedBaseline, VideoCodecProfileMain, VideoCodecProfileHigh},\n\t\t\t\t\t\t\t\tLevel:     []byte{VideoCodecLevel31, VideoCodecLevel32, VideoCodecLevel40},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tVideoAttrs: []VideoCodecAttributes{\n\n\t\t\t\t\t\t\t{Width: 320, Height: 180, Framerate: 30},\n\t\t\t\t\t\t\t{Width: 320, Height: 240, Framerate: 15},\n\t\t\t\t\t\t\t{Width: 320, Height: 240, Framerate: 30},\n\t\t\t\t\t\t\t{Width: 480, Height: 270, Framerate: 30},\n\t\t\t\t\t\t\t{Width: 480, Height: 360, Framerate: 30},\n\t\t\t\t\t\t\t{Width: 640, Height: 360, Framerate: 30},\n\t\t\t\t\t\t\t{Width: 640, Height: 480, Framerate: 30},\n\t\t\t\t\t\t\t{Width: 1280, Height: 720, Framerate: 30},\n\t\t\t\t\t\t\t{Width: 1280, Height: 960, Framerate: 30},\n\t\t\t\t\t\t\t{Width: 1920, Height: 1080, Framerate: 30},\n\t\t\t\t\t\t\t{Width: 1600, Height: 1200, Framerate: 30},\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:   \"116\",\n\t\t\tvalue:  \"AgEA\",\n\t\t\tactual: &SupportedRTPConfiguration{},\n\t\t\texpect: &SupportedRTPConfiguration{\n\t\t\t\tSRTPCryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, test.run)\n\t}\n}\n\nfunc TestScrypted(t *testing.T) {\n\ttests := []testTLV8{\n\t\t{\n\t\t\tname:   \"114\",\n\t\t\tvalue:  \"AVIBAQACEwEBAQIBAAAAAgEBAAACAQIDAQADCwECAA8CAnAIAwEeAAADCwECgAcCAjgEAwEeAAADCwECAAUCAtACAwEeAAADCwECQAECAvAAAwEP\",\n\t\t\tactual: &SupportedVideoStreamConfiguration{},\n\t\t\texpect: &SupportedVideoStreamConfiguration{\n\t\t\t\tCodecs: []VideoCodecConfiguration{\n\t\t\t\t\t{\n\t\t\t\t\t\tCodecType: VideoCodecTypeH264,\n\t\t\t\t\t\tCodecParams: []VideoCodecParameters{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tProfileID: []byte{VideoCodecProfileMain},\n\t\t\t\t\t\t\t\tLevel:     []byte{VideoCodecLevel31, VideoCodecLevel32, VideoCodecLevel40},\n\t\t\t\t\t\t\t},\n\t\t\t\t\t\t},\n\t\t\t\t\t\tVideoAttrs: []VideoCodecAttributes{\n\t\t\t\t\t\t\t{Width: 3840, Height: 2160, Framerate: 30},\n\t\t\t\t\t\t\t{Width: 1920, Height: 1080, Framerate: 30},\n\t\t\t\t\t\t\t{Width: 1280, Height: 720, Framerate: 30},\n\t\t\t\t\t\t\t{Width: 320, Height: 240, Framerate: 15},\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:   \"115\",\n\t\t\tvalue:  \"AScBAQMCIgEBAQIBAAMBAAAAAwEAAAADAQEAAAMBAQAAAwECAAADAQICAQA=\",\n\t\t\tactual: &SupportedAudioStreamConfiguration{},\n\t\t\texpect: &SupportedAudioStreamConfiguration{\n\t\t\t\tCodecs: []AudioCodecConfiguration{\n\t\t\t\t\t{\n\t\t\t\t\t\tCodecType: AudioCodecTypeOpus,\n\t\t\t\t\t\tCodecParams: []AudioCodecParameters{\n\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\tChannels:    1,\n\t\t\t\t\t\t\t\tBitrateMode: AudioCodecBitrateVariable,\n\t\t\t\t\t\t\t\tSampleRate: []byte{\n\t\t\t\t\t\t\t\t\tAudioCodecSampleRate8Khz, AudioCodecSampleRate8Khz,\n\t\t\t\t\t\t\t\t\tAudioCodecSampleRate16Khz, AudioCodecSampleRate16Khz,\n\t\t\t\t\t\t\t\t\tAudioCodecSampleRate24Khz, AudioCodecSampleRate24Khz,\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\tComfortNoiseSupport: 0,\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tname:   \"116\",\n\t\t\tvalue:  \"AgEAAAACAQI=\",\n\t\t\tactual: &SupportedRTPConfiguration{},\n\t\t\texpect: &SupportedRTPConfiguration{\n\t\t\t\tSRTPCryptoType: []byte{CryptoAES_CM_128_HMAC_SHA1_80, CryptoDisabled},\n\t\t\t},\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, test.run)\n\t}\n}\n\nfunc TestHass(t *testing.T) {\n\ttests := []testTLV8{\n\t\t{\n\t\t\tname:  \"114\",\n\t\t\tvalue: \"AdABAQACFQMBAAEBAAEBAQEBAgIBAAIBAQIBAgMMAQJAAQICtAADAg8AAwwBAkABAgLwAAMCDwADDAECQAECArQAAwIeAAMMAQJAAQIC8AADAh4AAwwBAuABAgIOAQMCHgADDAEC4AECAmgBAwIeAAMMAQKAAgICaAEDAh4AAwwBAoACAgLgAQMCHgADDAECAAQCAkACAwIeAAMMAQIABAICAAMDAh4AAwwBAgAFAgLQAgMCHgADDAECAAUCAsADAwIeAAMMAQKABwICOAQDAh4A\",\n\t\t},\n\t\t{\n\t\t\tname:  \"115\",\n\t\t\tvalue: \"AQ4BAQMCCQEBAQIBAAMBAgEOAQEDAgkBAQECAQADAQECAQA=\",\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, test.run)\n\t}\n}\n"
  },
  {
    "path": "pkg/hap/camera/ch114_supported_video.go",
    "content": "package camera\n\nconst TypeSupportedVideoStreamConfiguration = \"114\"\n\ntype SupportedVideoStreamConfiguration struct {\n\tCodecs []VideoCodecConfiguration `tlv8:\"1\"`\n}\n\ntype VideoCodecConfiguration struct {\n\tCodecType   byte                   `tlv8:\"1\"`\n\tCodecParams []VideoCodecParameters `tlv8:\"2\"`\n\tVideoAttrs  []VideoCodecAttributes `tlv8:\"3\"`\n\tRTPParams   []RTPParams            `tlv8:\"4\"`\n}\n\n//goland:noinspection ALL\nconst (\n\tVideoCodecTypeH264 = 0\n\n\tVideoCodecProfileConstrainedBaseline = 0\n\tVideoCodecProfileMain                = 1\n\tVideoCodecProfileHigh                = 2\n\n\tVideoCodecLevel31 = 0\n\tVideoCodecLevel32 = 1\n\tVideoCodecLevel40 = 2\n\n\tVideoCodecPacketizationModeNonInterleaved = 0\n\n\tVideoCodecCvoNotSuppported = 0\n\tVideoCodecCvoSuppported    = 1\n)\n\ntype VideoCodecParameters struct {\n\tProfileID         []byte `tlv8:\"1\"` // 0 - baseline, 1 - main, 2 - high\n\tLevel             []byte `tlv8:\"2\"` // 0 - 3.1, 1 - 3.2, 2 - 4.0\n\tPacketizationMode byte   `tlv8:\"3\"` // only 0 - non interleaved\n\tCVOEnabled        []byte `tlv8:\"4\"` // 0 - not supported, 1 - supported\n\tCVOID             []byte `tlv8:\"5\"` // ID for CVO RTP extensio\n}\n\ntype VideoCodecAttributes struct {\n\tWidth     uint16 `tlv8:\"1\"`\n\tHeight    uint16 `tlv8:\"2\"`\n\tFramerate uint8  `tlv8:\"3\"`\n}\n"
  },
  {
    "path": "pkg/hap/camera/ch115_supported_audio.go",
    "content": "package camera\n\nconst TypeSupportedAudioStreamConfiguration = \"115\"\n\ntype SupportedAudioStreamConfiguration struct {\n\tCodecs              []AudioCodecConfiguration `tlv8:\"1\"`\n\tComfortNoiseSupport byte                      `tlv8:\"2\"`\n}\n\n//goland:noinspection ALL\nconst (\n\tAudioCodecTypePCMU   = 0\n\tAudioCodecTypePCMA   = 1\n\tAudioCodecTypeAACELD = 2\n\tAudioCodecTypeOpus   = 3\n\tAudioCodecTypeMSBC   = 4\n\tAudioCodecTypeAMR    = 5\n\tAudioCodecTypeARMWB  = 6\n\n\tAudioCodecBitrateVariable = 0\n\tAudioCodecBitrateConstant = 1\n\n\tAudioCodecSampleRate8Khz  = 0\n\tAudioCodecSampleRate16Khz = 1\n\tAudioCodecSampleRate24Khz = 2\n\n\tRTPTimeAACELD8  = 60 // 8000/1000*60=480\n\tRTPTimeAACELD16 = 30 // 16000/1000*30=480\n\tRTPTimeAACELD24 = 20 // 24000/1000*20=480\n\tRTPTimeAACLD16  = 60 // 16000/1000*60=960\n\tRTPTimeAACLD24  = 40 // 24000/1000*40=960\n)\n\ntype AudioCodecConfiguration struct {\n\tCodecType    byte                   `tlv8:\"1\"`\n\tCodecParams  []AudioCodecParameters `tlv8:\"2\"`\n\tRTPParams    []RTPParams            `tlv8:\"3\"`\n\tComfortNoise []byte                 `tlv8:\"4\"`\n}\n\ntype AudioCodecParameters struct {\n\tChannels    uint8   `tlv8:\"1\"`\n\tBitrateMode byte    `tlv8:\"2\"` // 0 - variable, 1 - constant\n\tSampleRate  []byte  `tlv8:\"3\"` // 0 - 8000, 1 - 16000, 2 - 24000\n\tRTPTime     []uint8 `tlv8:\"4\"` // 20, 30, 40, 60\n}\n"
  },
  {
    "path": "pkg/hap/camera/ch116_supported_rtp.go",
    "content": "package camera\n\nconst TypeSupportedRTPConfiguration = \"116\"\n\n//goland:noinspection ALL\nconst (\n\tCryptoAES_CM_128_HMAC_SHA1_80 = 0\n\tCryptoAES_CM_256_HMAC_SHA1_80 = 1\n\tCryptoDisabled                = 2\n)\n\ntype SupportedRTPConfiguration struct {\n\tSRTPCryptoType []byte `tlv8:\"2\"`\n}\n"
  },
  {
    "path": "pkg/hap/camera/ch117_selected_stream.go",
    "content": "package camera\n\nconst TypeSelectedStreamConfiguration = \"117\"\n\ntype SelectedStreamConfiguration struct {\n\tControl    SessionControl          `tlv8:\"1\"`\n\tVideoCodec VideoCodecConfiguration `tlv8:\"2\"`\n\tAudioCodec AudioCodecConfiguration `tlv8:\"3\"`\n}\n\n//goland:noinspection ALL\nconst (\n\tSessionCommandEnd         = 0\n\tSessionCommandStart       = 1\n\tSessionCommandSuspend     = 2\n\tSessionCommandResume      = 3\n\tSessionCommandReconfigure = 4\n)\n\ntype SessionControl struct {\n\tSessionID string `tlv8:\"1\"`\n\tCommand   byte   `tlv8:\"2\"`\n}\n\ntype RTPParams struct {\n\tPayloadType             uint8    `tlv8:\"1\"`\n\tSSRC                    uint32   `tlv8:\"2\"`\n\tMaxBitrate              uint16   `tlv8:\"3\"`\n\tRTCPInterval            float32  `tlv8:\"4\"`\n\tMaxMTU                  []uint16 `tlv8:\"5\"`\n\tComfortNoisePayloadType []uint8  `tlv8:\"6\"`\n}\n"
  },
  {
    "path": "pkg/hap/camera/ch118_setup_endpoints.go",
    "content": "package camera\n\nconst TypeSetupEndpoints = \"118\"\n\ntype SetupEndpointsRequest struct {\n\tSessionID   string          `tlv8:\"1\"`\n\tAddress     Address         `tlv8:\"3\"`\n\tVideoCrypto SRTPCryptoSuite `tlv8:\"4\"`\n\tAudioCrypto SRTPCryptoSuite `tlv8:\"5\"`\n}\n\ntype SetupEndpointsResponse struct {\n\tSessionID   string          `tlv8:\"1\"`\n\tStatus      byte            `tlv8:\"2\"`\n\tAddress     Address         `tlv8:\"3\"`\n\tVideoCrypto SRTPCryptoSuite `tlv8:\"4\"`\n\tAudioCrypto SRTPCryptoSuite `tlv8:\"5\"`\n\tVideoSSRC   uint32          `tlv8:\"6\"`\n\tAudioSSRC   uint32          `tlv8:\"7\"`\n}\n\ntype Address struct {\n\tIPVersion    byte   `tlv8:\"1\"`\n\tIPAddr       string `tlv8:\"2\"`\n\tVideoRTPPort uint16 `tlv8:\"3\"`\n\tAudioRTPPort uint16 `tlv8:\"4\"`\n}\n\ntype SRTPCryptoSuite struct {\n\tCryptoSuite byte   `tlv8:\"1\"`\n\tMasterKey   string `tlv8:\"2\"` // 16 (AES_CM_128) or 32 (AES_256_CM)\n\tMasterSalt  string `tlv8:\"3\"` // 14 byte\n}\n"
  },
  {
    "path": "pkg/hap/camera/ch120_streaming_status.go",
    "content": "package camera\n\nconst TypeStreamingStatus = \"120\"\n\ntype StreamingStatus struct {\n\tStatus byte `tlv8:\"1\"`\n}\n\n//goland:noinspection ALL\nconst (\n\tStreamingStatusAvailable   = 0\n\tStreamingStatusInUse       = 1\n\tStreamingStatusUnavailable = 2\n)\n"
  },
  {
    "path": "pkg/hap/camera/ch130_data_stream_transport.go",
    "content": "package camera\n\nconst TypeSupportedDataStreamTransportConfiguration = \"130\"\n\ntype SupportedDataStreamTransportConfiguration struct {\n\tConfigs []TransferTransportConfiguration `tlv8:\"1\"`\n}\n\ntype TransferTransportConfiguration struct {\n\tTransportType byte `tlv8:\"1\"`\n}\n"
  },
  {
    "path": "pkg/hap/camera/ch131_data_stream.go",
    "content": "package camera\n\nconst TypeSetupDataStreamTransport = \"131\"\n\ntype SetupDataStreamTransportRequest struct {\n\tSessionCommandType byte   `tlv8:\"1\"`\n\tTransportType      byte   `tlv8:\"2\"`\n\tControllerKeySalt  string `tlv8:\"3\"`\n}\n\ntype SetupDataStreamTransportResponse struct {\n\tStatus                         byte `tlv8:\"1\"`\n\tTransportTypeSessionParameters struct {\n\t\tTCPListeningPort uint16 `tlv8:\"1\"`\n\t} `tlv8:\"2\"`\n\tAccessoryKeySalt string `tlv8:\"3\"`\n}\n"
  },
  {
    "path": "pkg/hap/camera/ch205.go",
    "content": "package camera\n\nconst TypeSupportedCameraRecordingConfiguration = \"205\"\n\ntype SupportedCameraRecordingConfiguration struct {\n\tPrebufferLength              uint32 `tlv8:\"1\"`\n\tEventTriggerOptions          uint64 `tlv8:\"2\"`\n\tMediaContainerConfigurations `tlv8:\"3\"`\n}\n\ntype MediaContainerConfigurations struct {\n\tMediaContainerType       uint8 `tlv8:\"1\"`\n\tMediaContainerParameters `tlv8:\"2\"`\n}\n\ntype MediaContainerParameters struct {\n\tFragmentLength uint32 `tlv8:\"1\"`\n}\n"
  },
  {
    "path": "pkg/hap/camera/ch206.go",
    "content": "package camera\n\nconst TypeSupportedVideoRecordingConfiguration = \"206\"\n\ntype SupportedVideoRecordingConfiguration struct {\n\tCodecConfigs []VideoRecordingCodecConfiguration `tlv8:\"1\"`\n}\n\ntype VideoRecordingCodecConfiguration struct {\n\tCodecType   uint8                         `tlv8:\"1\"`\n\tCodecParams VideoRecordingCodecParameters `tlv8:\"2\"`\n\tCodecAttrs  VideoCodecAttributes          `tlv8:\"3\"`\n}\n\ntype VideoRecordingCodecParameters struct {\n\tProfileID      uint8  `tlv8:\"1\"`\n\tLevel          uint8  `tlv8:\"2\"`\n\tBitrate        uint32 `tlv8:\"3\"`\n\tIFrameInterval uint32 `tlv8:\"4\"`\n}\n"
  },
  {
    "path": "pkg/hap/camera/ch207.go",
    "content": "package camera\n\nconst TypeSupportedAudioRecordingConfiguration = \"207\"\n\ntype SupportedAudioRecordingConfiguration struct {\n\tCodecConfigs []AudioRecordingCodecConfiguration `tlv8:\"1\"`\n}\n\ntype AudioRecordingCodecConfiguration struct {\n\tCodecType   byte                            `tlv8:\"1\"`\n\tCodecParams []AudioRecordingCodecParameters `tlv8:\"2\"`\n}\n\ntype AudioRecordingCodecParameters struct {\n\tChannels        uint8    `tlv8:\"1\"`\n\tBitrateMode     []byte   `tlv8:\"2\"`\n\tSampleRate      []byte   `tlv8:\"3\"`\n\tMaxAudioBitrate []uint32 `tlv8:\"4\"`\n}\n"
  },
  {
    "path": "pkg/hap/camera/ch209.go",
    "content": "package camera\n\nconst TypeSelectedCameraRecordingConfiguration = \"209\"\n\ntype SelectedCameraRecordingConfiguration struct {\n\tGeneralConfig SupportedCameraRecordingConfiguration `tlv8:\"1\"`\n\tVideoConfig   SupportedVideoRecordingConfiguration  `tlv8:\"2\"`\n\tAudioConfig   SupportedAudioRecordingConfiguration  `tlv8:\"3\"`\n}\n"
  },
  {
    "path": "pkg/hap/camera/stream.go",
    "content": "package camera\n\nimport (\n\t\"errors\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hap\"\n\t\"github.com/AlexxIT/go2rtc/pkg/srtp\"\n)\n\ntype Stream struct {\n\tid      string\n\tclient  *hap.Client\n\tservice *hap.Service\n}\n\nfunc NewStream(\n\tclient *hap.Client, videoCodec *VideoCodecConfiguration, audioCodec *AudioCodecConfiguration,\n\tvideoSession, audioSession *srtp.Session, bitrate int,\n) (*Stream, error) {\n\tstream := &Stream{\n\t\tid:     core.RandString(16, 0),\n\t\tclient: client,\n\t}\n\n\tif err := stream.GetFreeStream(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err := stream.ExchangeEndpoints(videoSession, audioSession); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif bitrate != 0 {\n\t\tbitrate /= 1024 // convert bps to kbps\n\t} else {\n\t\tbitrate = 4096 // default kbps for general FullHD camera\n\t}\n\n\tvideoCodec.RTPParams = []RTPParams{\n\t\t{\n\t\t\tPayloadType:  99,\n\t\t\tSSRC:         videoSession.Local.SSRC,\n\t\t\tMaxBitrate:   uint16(bitrate), // iPhone query 299Kbps, iPad/AppleTV query 802Kbps\n\t\t\tRTCPInterval: 0.5,\n\t\t\tMaxMTU:       []uint16{1378},\n\t\t},\n\t}\n\taudioCodec.RTPParams = []RTPParams{\n\t\t{\n\t\t\tPayloadType:  110,\n\t\t\tSSRC:         audioSession.Local.SSRC,\n\t\t\tMaxBitrate:   24, // any iDevice query 24Kbps (this is OK for 16KHz and 1 channel)\n\t\t\tRTCPInterval: 5,\n\n\t\t\tComfortNoisePayloadType: []uint8{13},\n\t\t},\n\t}\n\taudioCodec.ComfortNoise = []byte{0}\n\n\tconfig := &SelectedStreamConfiguration{\n\t\tControl: SessionControl{\n\t\t\tSessionID: stream.id,\n\t\t\tCommand:   SessionCommandStart,\n\t\t},\n\t\tVideoCodec: *videoCodec,\n\t\tAudioCodec: *audioCodec,\n\t}\n\n\tif err := stream.SetStreamConfig(config); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn stream, nil\n}\n\n// GetFreeStream search free streaming service.\n// Usual every HomeKit camera can stream only to two clients simultaniosly.\n// So it has two similar services for streaming.\nfunc (s *Stream) GetFreeStream() error {\n\tacc, err := s.client.GetFirstAccessory()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor _, srv := range acc.Services {\n\t\tfor _, char := range srv.Characters {\n\t\t\tif char.Type == TypeStreamingStatus {\n\t\t\t\tvar status StreamingStatus\n\t\t\t\tif err = char.ReadTLV8(&status); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif status.Status == StreamingStatusAvailable {\n\t\t\t\t\ts.service = srv\n\t\t\t\t\treturn nil\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\treturn errors.New(\"hap: no free streams\")\n}\n\nfunc (s *Stream) ExchangeEndpoints(videoSession, audioSession *srtp.Session) error {\n\treq := SetupEndpointsRequest{\n\t\tSessionID: s.id,\n\t\tAddress: Address{\n\t\t\tIPVersion:    0,\n\t\t\tIPAddr:       videoSession.Local.Addr,\n\t\t\tVideoRTPPort: videoSession.Local.Port,\n\t\t\tAudioRTPPort: audioSession.Local.Port,\n\t\t},\n\t\tVideoCrypto: SRTPCryptoSuite{\n\t\t\tMasterKey:  string(videoSession.Local.MasterKey),\n\t\t\tMasterSalt: string(videoSession.Local.MasterSalt),\n\t\t},\n\t\tAudioCrypto: SRTPCryptoSuite{\n\t\t\tMasterKey:  string(audioSession.Local.MasterKey),\n\t\t\tMasterSalt: string(audioSession.Local.MasterSalt),\n\t\t},\n\t}\n\n\tchar := s.service.GetCharacter(TypeSetupEndpoints)\n\tif err := char.Write(&req); err != nil {\n\t\treturn err\n\t}\n\tif err := s.client.PutCharacters(char); err != nil {\n\t\treturn err\n\t}\n\n\tvar res SetupEndpointsResponse\n\tif err := s.client.GetCharacter(char); err != nil {\n\t\treturn err\n\t}\n\tif err := char.ReadTLV8(&res); err != nil {\n\t\treturn err\n\t}\n\n\tvideoSession.Remote = &srtp.Endpoint{\n\t\tAddr:       res.Address.IPAddr,\n\t\tPort:       res.Address.VideoRTPPort,\n\t\tMasterKey:  []byte(res.VideoCrypto.MasterKey),\n\t\tMasterSalt: []byte(res.VideoCrypto.MasterSalt),\n\t\tSSRC:       res.VideoSSRC,\n\t}\n\n\taudioSession.Remote = &srtp.Endpoint{\n\t\tAddr:       res.Address.IPAddr,\n\t\tPort:       res.Address.AudioRTPPort,\n\t\tMasterKey:  []byte(res.AudioCrypto.MasterKey),\n\t\tMasterSalt: []byte(res.AudioCrypto.MasterSalt),\n\t\tSSRC:       res.AudioSSRC,\n\t}\n\n\treturn nil\n}\n\nfunc (s *Stream) SetStreamConfig(config *SelectedStreamConfiguration) error {\n\tchar := s.service.GetCharacter(TypeSelectedStreamConfiguration)\n\tif err := char.Write(config); err != nil {\n\t\treturn err\n\t}\n\tif err := s.client.PutCharacters(char); err != nil {\n\t\treturn err\n\t}\n\n\treturn s.client.GetCharacter(char)\n}\n\nfunc (s *Stream) Close() error {\n\tconfig := &SelectedStreamConfiguration{\n\t\tControl: SessionControl{\n\t\t\tSessionID: s.id,\n\t\t\tCommand:   SessionCommandEnd,\n\t\t},\n\t}\n\n\tchar := s.service.GetCharacter(TypeSelectedStreamConfiguration)\n\tif err := char.Write(config); err != nil {\n\t\treturn err\n\t}\n\treturn s.client.PutCharacters(char)\n}\n"
  },
  {
    "path": "pkg/hap/chacha20poly1305/chacha20poly1305.go",
    "content": "package chacha20poly1305\n\nimport (\n\t\"errors\"\n\n\t\"golang.org/x/crypto/chacha20poly1305\"\n)\n\nvar ErrInvalidParams = errors.New(\"chacha20poly1305: invalid params\")\n\n// Decrypt - decrypt without verify\nfunc Decrypt(key32 []byte, nonce8 string, ciphertext []byte) ([]byte, error) {\n\treturn DecryptAndVerify(key32, nil, []byte(nonce8), ciphertext, nil)\n}\n\n// Encrypt - encrypt without seal\nfunc Encrypt(key32 []byte, nonce8 string, plaintext []byte) ([]byte, error) {\n\treturn EncryptAndSeal(key32, nil, []byte(nonce8), plaintext, nil)\n}\n\nfunc DecryptAndVerify(key32, dst, nonce8, ciphertext, verify []byte) ([]byte, error) {\n\tif len(key32) != chacha20poly1305.KeySize || len(nonce8) != 8 {\n\t\treturn nil, ErrInvalidParams\n\t}\n\n\taead, err := chacha20poly1305.New(key32)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tnonce := make([]byte, chacha20poly1305.NonceSize)\n\tcopy(nonce[4:], nonce8)\n\n\treturn aead.Open(dst, nonce, ciphertext, verify)\n}\n\nfunc EncryptAndSeal(key32, dst, nonce8, plaintext, verify []byte) ([]byte, error) {\n\tif len(key32) != chacha20poly1305.KeySize || len(nonce8) != 8 {\n\t\treturn nil, ErrInvalidParams\n\t}\n\n\taead, err := chacha20poly1305.New(key32)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tnonce := make([]byte, chacha20poly1305.NonceSize)\n\tcopy(nonce[4:], nonce8)\n\n\treturn aead.Seal(dst, nonce, plaintext, verify), nil\n}\n"
  },
  {
    "path": "pkg/hap/character.go",
    "content": "package hap\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/hap/tlv8\"\n)\n\n// Character - Aqara props order\n// Value should be omit for PW\n// Value may be empty for PR\ntype Character struct {\n\tDesc string `json:\"description,omitempty\"`\n\n\tIID    uint64   `json:\"iid\"`\n\tType   string   `json:\"type\"`\n\tFormat string   `json:\"format\"`\n\tValue  any      `json:\"value,omitempty\"`\n\tPerms  []string `json:\"perms\"`\n\n\t//MaxLen   int    `json:\"maxLen,omitempty\"`\n\t//Unit     string `json:\"unit,omitempty\"`\n\t//MinValue any    `json:\"minValue,omitempty\"`\n\t//MaxValue any    `json:\"maxValue,omitempty\"`\n\t//MinStep  any    `json:\"minStep,omitempty\"`\n\t//ValidVal []any  `json:\"valid-values,omitempty\"`\n\n\tlisteners map[io.Writer]bool\n}\n\nfunc (c *Character) AddListener(w io.Writer) {\n\t// TODO: sync.Mutex\n\tif c.listeners == nil {\n\t\tc.listeners = map[io.Writer]bool{}\n\t}\n\tc.listeners[w] = true\n}\n\nfunc (c *Character) RemoveListener(w io.Writer) {\n\tdelete(c.listeners, w)\n\n\tif len(c.listeners) == 0 {\n\t\tc.listeners = nil\n\t}\n}\n\nfunc (c *Character) NotifyListeners(ignore io.Writer) error {\n\tif c.listeners == nil {\n\t\treturn nil\n\t}\n\n\tdata, err := c.GenerateEvent()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfor w := range c.listeners {\n\t\tif w == ignore {\n\t\t\tcontinue\n\t\t}\n\t\tif _, err = w.Write(data); err != nil {\n\t\t\t// error not a problem - just remove listener\n\t\t\tc.RemoveListener(w)\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// GenerateEvent with raw HTTP headers\nfunc (c *Character) GenerateEvent() (data []byte, err error) {\n\tv := JSONCharacters{\n\t\tValue: []JSONCharacter{\n\t\t\t{AID: DeviceAID, IID: c.IID, Value: c.Value},\n\t\t},\n\t}\n\tif data, err = json.Marshal(v); err != nil {\n\t\treturn\n\t}\n\n\tres := http.Response{\n\t\tStatusCode:    http.StatusOK,\n\t\tProtoMajor:    1,\n\t\tProtoMinor:    0,\n\t\tHeader:        http.Header{\"Content-Type\": []string{MimeJSON}},\n\t\tContentLength: int64(len(data)),\n\t\tBody:          io.NopCloser(bytes.NewReader(data)),\n\t}\n\n\tbuf := bytes.NewBuffer([]byte{0})\n\tif err = res.Write(buf); err != nil {\n\t\treturn\n\t}\n\tcopy(buf.Bytes(), \"EVENT\")\n\n\treturn buf.Bytes(), err\n}\n\n// Set new value and NotifyListeners\nfunc (c *Character) Set(v any) (err error) {\n\tif err = c.Write(v); err != nil {\n\t\treturn\n\t}\n\treturn c.NotifyListeners(nil)\n}\n\n// Write new value with right format\nfunc (c *Character) Write(v any) (err error) {\n\tswitch c.Format {\n\tcase \"tlv8\":\n\t\tc.Value, err = tlv8.MarshalBase64(v)\n\n\tcase \"bool\":\n\t\tswitch v := v.(type) {\n\t\tcase bool:\n\t\t\tc.Value = v\n\t\tcase float64:\n\t\t\tc.Value = v != 0\n\t\t}\n\t}\n\treturn\n}\n\n// ReadTLV8 value to right struct\nfunc (c *Character) ReadTLV8(v any) (err error) {\n\tif s, ok := c.Value.(string); ok {\n\t\treturn tlv8.UnmarshalBase64(s, v)\n\t}\n\treturn fmt.Errorf(\"hap: can't read value: %v\", v)\n}\n\nfunc (c *Character) ReadBool() (bool, error) {\n\tif v, ok := c.Value.(bool); ok {\n\t\treturn v, nil\n\t}\n\treturn false, fmt.Errorf(\"hap: can't read value: %v\", c.Value)\n}\n\nfunc (c *Character) String() string {\n\tdata, err := json.Marshal(c)\n\tif err != nil {\n\t\treturn \"ERROR\"\n\t}\n\treturn string(data)\n}\n"
  },
  {
    "path": "pkg/hap/client.go",
    "content": "package hap\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hap/curve25519\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hap/ed25519\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hap/hkdf\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hap/tlv8\"\n\t\"github.com/AlexxIT/go2rtc/pkg/mdns\"\n)\n\nconst (\n\tConnDialTimeout = time.Second * 3\n\tConnDeadline    = time.Second * 3\n)\n\n// Client for HomeKit. DevicePublic can be null.\ntype Client struct {\n\tDeviceAddress string // including port\n\tDeviceID      string // aka. Accessory\n\tDevicePublic  []byte\n\tClientID      string // aka. Controller\n\tClientPrivate []byte\n\n\tOnEvent func(res *http.Response)\n\t//Output  func(msg any)\n\n\tConn   net.Conn\n\treader *bufio.Reader\n\n\tres chan *http.Response\n\terr error\n}\n\nfunc Dial(rawURL string) (*Client, error) {\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tquery := u.Query()\n\tc := &Client{\n\t\tDeviceAddress: u.Host,\n\t\tDeviceID:      query.Get(\"device_id\"),\n\t\tDevicePublic:  DecodeKey(query.Get(\"device_public\")),\n\t\tClientID:      query.Get(\"client_id\"),\n\t\tClientPrivate: DecodeKey(query.Get(\"client_private\")),\n\t}\n\n\tif err = c.Dial(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn c, nil\n}\n\nfunc (c *Client) ClientPublic() []byte {\n\treturn c.ClientPrivate[32:]\n}\n\nfunc (c *Client) URL() string {\n\treturn fmt.Sprintf(\n\t\t\"homekit://%s?device_id=%s&device_public=%16x&client_id=%s&client_private=%32x\",\n\t\tc.DeviceAddress, c.DeviceID, c.DevicePublic, c.ClientID, c.ClientPrivate,\n\t)\n}\n\nfunc (c *Client) DeviceHost() string {\n\tif i := strings.IndexByte(c.DeviceAddress, ':'); i > 0 {\n\t\treturn c.DeviceAddress[:i]\n\t}\n\treturn c.DeviceAddress\n}\n\nfunc (c *Client) Dial() (err error) {\n\tif len(c.ClientID) == 0 || len(c.ClientPrivate) == 0 {\n\t\treturn errors.New(\"hap: can't dial witout client_id or client_private\")\n\t}\n\n\t// update device address (host and/or port) before dial\n\t_ = mdns.QueryOrDiscovery(c.DeviceHost(), mdns.ServiceHAP, func(entry *mdns.ServiceEntry) bool {\n\t\tif entry.Complete() && entry.Info[\"id\"] == c.DeviceID {\n\t\t\tc.DeviceAddress = entry.Addr()\n\t\t\treturn true\n\t\t}\n\t\treturn false\n\t})\n\n\t// TODO: close conn on error\n\tif c.Conn, err = net.DialTimeout(\"tcp\", c.DeviceAddress, ConnDialTimeout); err != nil {\n\t\treturn\n\t}\n\n\tc.reader = bufio.NewReader(c.Conn)\n\n\t// STEP M1: send our session public to device\n\tsessionPublic, sessionPrivate := curve25519.GenerateKeyPair()\n\n\t// 1. Send sessionPublic\n\tplainM1 := struct {\n\t\tPublicKey string `tlv8:\"3\"`\n\t\tState     byte   `tlv8:\"6\"`\n\t}{\n\t\tPublicKey: string(sessionPublic),\n\t\tState:     StateM1,\n\t}\n\tres, err := c.Post(PathPairVerify, MimeTLV8, tlv8.MarshalReader(plainM1))\n\tif err != nil {\n\t\treturn\n\t}\n\n\t// STEP M2: unpack deviceID from response\n\tvar cipherM2 struct {\n\t\tPublicKey     string `tlv8:\"3\"`\n\t\tEncryptedData string `tlv8:\"5\"`\n\t\tState         byte   `tlv8:\"6\"`\n\t}\n\tif err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &cipherM2); err != nil {\n\t\treturn err\n\t}\n\tif cipherM2.State != StateM2 {\n\t\treturn newResponseError(plainM1, cipherM2)\n\t}\n\n\t// 1. generate session shared key\n\tsessionShared, err := curve25519.SharedSecret(sessionPrivate, []byte(cipherM2.PublicKey))\n\tif err != nil {\n\t\treturn\n\t}\n\n\tsessionKey, err := hkdf.Sha512(\n\t\tsessionShared, \"Pair-Verify-Encrypt-Salt\", \"Pair-Verify-Encrypt-Info\",\n\t)\n\tif err != nil {\n\t\treturn\n\t}\n\n\t// 2. decrypt M2 response with session key\n\tb, err := chacha20poly1305.Decrypt(sessionKey, \"PV-Msg02\", []byte(cipherM2.EncryptedData))\n\tif err != nil {\n\t\treturn\n\t}\n\n\t// 3. unpack payload from TLV8\n\tvar plainM2 struct {\n\t\tIdentifier string `tlv8:\"1\"`\n\t\tSignature  string `tlv8:\"10\"`\n\t}\n\tif err = tlv8.Unmarshal(b, &plainM2); err != nil {\n\t\treturn\n\t}\n\n\t// 4. verify signature for M2 response with device public\n\t// device session + device id + our session\n\tif c.DevicePublic != nil {\n\t\tb = Append(cipherM2.PublicKey, plainM2.Identifier, sessionPublic)\n\t\tif !ed25519.ValidateSignature(c.DevicePublic, b, []byte(plainM2.Signature)) {\n\t\t\treturn errors.New(\"hap: ValidateSignature\")\n\t\t}\n\t}\n\n\t// STEP M3: send our clientID to device\n\t// 1. generate signature with our private key\n\t// (our session + our ID + device session)\n\tb = Append(sessionPublic, c.ClientID, cipherM2.PublicKey)\n\tif b, err = ed25519.Signature(c.ClientPrivate, b); err != nil {\n\t\treturn\n\t}\n\n\t// 2. generate payload\n\tplainM3 := struct {\n\t\tIdentifier string `tlv8:\"1\"`\n\t\tSignature  string `tlv8:\"10\"`\n\t}{\n\t\tIdentifier: c.ClientID,\n\t\tSignature:  string(b),\n\t}\n\tif b, err = tlv8.Marshal(plainM3); err != nil {\n\t\treturn\n\t}\n\n\t// 4. encrypt payload with session key\n\tif b, err = chacha20poly1305.Encrypt(sessionKey, \"PV-Msg03\", b); err != nil {\n\t\treturn\n\t}\n\n\t// 4. generate request\n\tcipherM3 := struct {\n\t\tEncryptedData string `tlv8:\"5\"`\n\t\tState         byte   `tlv8:\"6\"`\n\t}{\n\t\tState:         StateM3,\n\t\tEncryptedData: string(b),\n\t}\n\tif res, err = c.Post(PathPairVerify, MimeTLV8, tlv8.MarshalReader(cipherM3)); err != nil {\n\t\treturn\n\t}\n\n\t// STEP M4. Read response\n\tvar plainM4 struct {\n\t\tState byte `tlv8:\"6\"`\n\t}\n\tif err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &plainM4); err != nil {\n\t\treturn\n\t}\n\tif plainM4.State != StateM4 {\n\t\treturn newResponseError(cipherM3, plainM4)\n\t}\n\n\trw := bufio.NewReadWriter(c.reader, bufio.NewWriter(c.Conn))\n\n\t// like tls.Client wrapper over net.Conn\n\tif c.Conn, err = NewConn(c.Conn, rw, sessionShared, true); err != nil {\n\t\treturn\n\t}\n\t// new reader for new conn\n\tc.reader = bufio.NewReader(c.Conn)\n\n\treturn\n}\n\nfunc (c *Client) Close() error {\n\tif c.Conn == nil {\n\t\treturn nil\n\t}\n\treturn c.Conn.Close()\n}\n\nfunc (c *Client) eventsReader() {\n\tc.res = make(chan *http.Response)\n\n\tfor {\n\t\tvar res *http.Response\n\t\tif res, c.err = ReadResponse(c.reader, nil); c.err != nil {\n\t\t\tbreak\n\t\t}\n\n\t\tvar body []byte\n\t\tif body, c.err = io.ReadAll(res.Body); c.err != nil {\n\t\t\tbreak\n\t\t}\n\n\t\tres.Body = io.NopCloser(bytes.NewReader(body))\n\n\t\tif res.Proto != ProtoEvent {\n\t\t\tc.res <- res\n\t\t} else if c.OnEvent != nil {\n\t\t\tc.OnEvent(res)\n\t\t}\n\t}\n\n\tclose(c.res)\n}\n\nfunc (c *Client) GetAccessories() ([]*Accessory, error) {\n\tres, err := c.Get(PathAccessories)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar v JSONAccessories\n\tif err = json.NewDecoder(res.Body).Decode(&v); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn v.Value, nil\n}\n\nfunc (c *Client) GetFirstAccessory() (*Accessory, error) {\n\taccs, err := c.GetAccessories()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif len(accs) == 0 {\n\t\treturn nil, errors.New(\"hap: GetAccessories zero answer\")\n\t}\n\treturn accs[0], nil\n}\n\nfunc (c *Client) GetCharacters(query string) ([]JSONCharacter, error) {\n\tres, err := c.Get(PathCharacteristics + \"?id=\" + query)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdata, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar v JSONCharacters\n\tif err = json.Unmarshal(data, &v); err != nil {\n\t\treturn nil, err\n\t}\n\treturn v.Value, nil\n}\n\nfunc (c *Client) GetCharacter(char *Character) error {\n\tquery := fmt.Sprintf(\"%d.%d\", DeviceAID, char.IID)\n\tchars, err := c.GetCharacters(query)\n\tif err != nil {\n\t\treturn err\n\t}\n\tchar.Value = chars[0].Value\n\treturn nil\n}\n\nfunc (c *Client) PutCharacters(characters ...*Character) error {\n\tvar v JSONCharacters\n\tfor i, char := range characters {\n\t\tv.Value = append(v.Value, JSONCharacter{\n\t\t\tAID:   1,\n\t\t\tIID:   char.IID,\n\t\t\tValue: char.Value,\n\t\t})\n\t\tcharacters[i] = char\n\t}\n\tbody, err := json.Marshal(v)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tres, err := c.Put(PathCharacteristics, MimeJSON, bytes.NewReader(body))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, _ = io.ReadAll(res.Body) // important to \"clear\" body\n\n\treturn nil\n}\n\nfunc (c *Client) GetImage(width, height int) ([]byte, error) {\n\ts := fmt.Sprintf(\n\t\t`{\"image-width\":%d,\"image-height\":%d,\"resource-type\":\"image\",\"reason\":0}`,\n\t\twidth, height,\n\t)\n\tres, err := c.Post(PathResource, MimeJSON, bytes.NewBufferString(s))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn io.ReadAll(res.Body)\n}\n\nfunc (c *Client) LocalIP() string {\n\tif c.Conn == nil {\n\t\treturn \"\"\n\t}\n\taddr := c.Conn.LocalAddr().(*net.TCPAddr)\n\treturn addr.IP.String()\n}\n\nfunc DecodeKey(s string) []byte {\n\tif s == \"\" {\n\t\treturn nil\n\t}\n\tdata, err := hex.DecodeString(s)\n\tif err != nil {\n\t\treturn nil\n\t}\n\treturn data\n}\n"
  },
  {
    "path": "pkg/hap/client_http.go",
    "content": "package hap\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n)\n\nconst (\n\tMimeTLV8 = \"application/pairing+tlv8\"\n\tMimeJSON = \"application/hap+json\"\n\n\tPathPairSetup       = \"/pair-setup\"\n\tPathPairVerify      = \"/pair-verify\"\n\tPathPairings        = \"/pairings\"\n\tPathAccessories     = \"/accessories\"\n\tPathCharacteristics = \"/characteristics\"\n\tPathResource        = \"/resource\"\n)\n\nfunc (c *Client) Do(req *http.Request) (*http.Response, error) {\n\tif err := req.Write(c.Conn); err != nil {\n\t\treturn nil, err\n\t}\n\tif c.res != nil {\n\t\treturn <-c.res, c.err\n\t}\n\treturn http.ReadResponse(c.reader, req)\n}\n\nfunc (c *Client) Request(method, path, contentType string, body io.Reader) (*http.Response, error) {\n\treq, err := http.NewRequest(method, \"http://\"+c.DeviceAddress+path, body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif contentType != \"\" {\n\t\treq.Header.Set(\"Content-Type\", contentType)\n\t}\n\n\tres, err := c.Do(req)\n\tif err == nil && res.StatusCode >= http.StatusBadRequest {\n\t\terr = errors.New(\"hap: wrong http status: \" + res.Status)\n\t}\n\n\treturn res, err\n}\n\nfunc (c *Client) Get(path string) (*http.Response, error) {\n\treturn c.Request(\"GET\", path, \"\", nil)\n}\n\nfunc (c *Client) Post(path, contentType string, body io.Reader) (*http.Response, error) {\n\treturn c.Request(\"POST\", path, contentType, body)\n}\n\nfunc (c *Client) Put(path, contentType string, body io.Reader) (*http.Response, error) {\n\treturn c.Request(\"PUT\", path, contentType, body)\n}\n\nconst ProtoEvent = \"EVENT/1.0\"\n\nfunc ReadResponse(r *bufio.Reader, req *http.Request) (*http.Response, error) {\n\tb, err := r.Peek(9)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif string(b) != ProtoEvent {\n\t\treturn http.ReadResponse(r, req)\n\t}\n\n\tcopy(b, \"HTTP/1.1 \")\n\n\tres, err := http.ReadResponse(r, req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tres.Proto = ProtoEvent\n\n\treturn res, nil\n}\n\nfunc WriteEvent(w io.Writer, res *http.Response) error {\n\treturn res.Write(&eventWriter{w: w})\n}\n\ntype eventWriter struct {\n\tw    io.Writer\n\tdone bool\n}\n\nfunc (e *eventWriter) Write(p []byte) (n int, err error) {\n\tif !e.done {\n\t\tp = append([]byte(\"EVENT/1.0\"), p[8:]...)\n\t\te.done = true\n\t}\n\treturn e.w.Write(p)\n}\n"
  },
  {
    "path": "pkg/hap/client_pairing.go",
    "content": "package hap\n\nimport (\n\t\"bufio\"\n\t\"crypto/sha512\"\n\t\"errors\"\n\t\"net\"\n\t\"net/url\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hap/ed25519\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hap/hkdf\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hap/tlv8\"\n\t\"github.com/tadglines/go-pkgs/crypto/srp\"\n)\n\n// Pair homekit\nfunc Pair(rawURL string) (*Client, error) {\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tquery := u.Query()\n\n\tc := &Client{\n\t\tDeviceAddress: u.Host,\n\t\tDeviceID:      query.Get(\"device_id\"),\n\t\tClientID:      query.Get(\"client_id\"),\n\t\tClientPrivate: DecodeKey(query.Get(\"client_private\")),\n\t}\n\n\tif c.ClientID == \"\" {\n\t\tc.ClientID = GenerateUUID()\n\t}\n\tif c.ClientPrivate == nil {\n\t\tc.ClientPrivate = GenerateKey()\n\t}\n\n\tif err = c.Pair(query.Get(\"feature\"), query.Get(\"pin\")); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn c, nil\n}\n\nfunc Unpair(rawURL string) error {\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tquery := u.Query()\n\tconn := &Client{\n\t\tDeviceAddress: u.Host,\n\t\tDeviceID:      query.Get(\"device_id\"),\n\t\tDevicePublic:  DecodeKey(query.Get(\"device_public\")),\n\t\tClientID:      query.Get(\"client_id\"),\n\t\tClientPrivate: DecodeKey(query.Get(\"client_private\")),\n\t}\n\n\tif err = conn.Dial(); err != nil {\n\t\treturn err\n\t}\n\n\tdefer conn.Close()\n\n\tif err = conn.ListPairings(); err != nil {\n\t\treturn err\n\t}\n\n\treturn conn.DeletePairing(conn.ClientID)\n}\n\nfunc (c *Client) Pair(feature, pin string) (err error) {\n\tif pin, err = SanitizePin(pin); err != nil {\n\t\treturn err\n\t}\n\n\tc.Conn, err = net.DialTimeout(\"tcp\", c.DeviceAddress, ConnDialTimeout)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tc.reader = bufio.NewReader(c.Conn)\n\n\t// STEP M1. Send HELLO\n\tplainM1 := struct {\n\t\tMethod byte `tlv8:\"0\"`\n\t\tState  byte `tlv8:\"6\"`\n\t}{\n\t\tMethod: MethodPair,\n\t\tState:  StateM1,\n\t}\n\tif feature == \"1\" {\n\t\tplainM1.Method = MethodPairMFi // ff=1 => method=1, ff=2 => method=0\n\t}\n\tres, err := c.Post(PathPairSetup, MimeTLV8, tlv8.MarshalReader(plainM1))\n\tif err != nil {\n\t\treturn\n\t}\n\n\t// STEP M2. Read Device Salt and session PublicKey\n\tvar plainM2 struct {\n\t\tSalt       string `tlv8:\"2\"`\n\t\tSessionKey string `tlv8:\"3\"` // server public key, aka session.B\n\t\tState      byte   `tlv8:\"6\"`\n\t\tError      byte   `tlv8:\"7\"`\n\t}\n\tif err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &plainM2); err != nil {\n\t\treturn\n\t}\n\tif plainM2.State != StateM2 {\n\t\treturn newResponseError(plainM1, plainM2)\n\t}\n\tif plainM2.Error != 0 {\n\t\treturn newPairingError(plainM2.Error)\n\t}\n\n\t// STEP M3. Generate SRP Session using pin\n\tusername := []byte(\"Pair-Setup\")\n\n\t// Stanford Secure Remote Password (SRP) / Password Authenticated Key Exchange (PAKE)\n\tpake, err := srp.NewSRP(\"rfc5054.3072\", sha512.New, keyDerivativeFuncRFC2945(username))\n\tif err != nil {\n\t\treturn\n\t}\n\n\tpake.SaltLength = 16\n\n\t// username: \"Pair-Setup\", password: PIN (with dashes)\n\tsession := pake.NewClientSession(username, []byte(pin))\n\n\tsessionShared, err := session.ComputeKey([]byte(plainM2.Salt), []byte(plainM2.SessionKey))\n\tif err != nil {\n\t\treturn\n\t}\n\n\t// STEP M3. Send request\n\tplainM3 := struct {\n\t\tSessionKey string `tlv8:\"3\"`\n\t\tProof      string `tlv8:\"4\"`\n\t\tState      byte   `tlv8:\"6\"`\n\t}{\n\t\tSessionKey: string(session.GetA()), // client public key, aka session.A\n\t\tProof:      string(session.ComputeAuthenticator()),\n\t\tState:      StateM3,\n\t}\n\tif res, err = c.Post(PathPairSetup, MimeTLV8, tlv8.MarshalReader(plainM3)); err != nil {\n\t\treturn\n\t}\n\n\t// STEP M4. Read response\n\tvar plainM4 struct {\n\t\tProof string `tlv8:\"4\"` // server proof\n\t\tState byte   `tlv8:\"6\"`\n\t\tError byte   `tlv8:\"7\"`\n\n\t\tEncryptedData string `tlv8:\"5\"` // skip EncryptedData validation (for MFi devices)\n\t}\n\tif err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &plainM4); err != nil {\n\t\treturn\n\t}\n\tif plainM4.State != StateM4 {\n\t\treturn newResponseError(plainM3, plainM4)\n\t}\n\tif plainM4.Error != 0 {\n\t\treturn newPairingError(plainM4.Error)\n\t}\n\n\t// STEP M4. Verify response\n\tif !session.VerifyServerAuthenticator([]byte(plainM4.Proof)) {\n\t\treturn errors.New(\"hap: VerifyServerAuthenticator\")\n\t}\n\n\t// STEP M5. Generate signature\n\tlocalSign, err := hkdf.Sha512(\n\t\tsessionShared, \"Pair-Setup-Controller-Sign-Salt\", \"Pair-Setup-Controller-Sign-Info\",\n\t)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tb := Append(localSign, c.ClientID, c.ClientPublic())\n\tsignature, err := ed25519.Signature(c.ClientPrivate, b)\n\tif err != nil {\n\t\treturn\n\t}\n\n\t// STEP M5. Generate payload\n\tplainM5 := struct {\n\t\tIdentifier string `tlv8:\"1\"`\n\t\tPublicKey  string `tlv8:\"3\"`\n\t\tSignature  string `tlv8:\"10\"`\n\t}{\n\t\tIdentifier: c.ClientID,\n\t\tPublicKey:  string(c.ClientPublic()),\n\t\tSignature:  string(signature),\n\t}\n\tif b, err = tlv8.Marshal(plainM5); err != nil {\n\t\treturn\n\t}\n\n\t// STEP M5. Encrypt payload\n\tencryptKey, err := hkdf.Sha512(\n\t\tsessionShared, \"Pair-Setup-Encrypt-Salt\", \"Pair-Setup-Encrypt-Info\",\n\t)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tif b, err = chacha20poly1305.Encrypt(encryptKey, \"PS-Msg05\", b); err != nil {\n\t\treturn\n\t}\n\n\t// STEP M5. Send request\n\tcipherM5 := struct {\n\t\tEncryptedData string `tlv8:\"5\"`\n\t\tState         byte   `tlv8:\"6\"`\n\t}{\n\t\tEncryptedData: string(b),\n\t\tState:         StateM5,\n\t}\n\tif res, err = c.Post(PathPairSetup, MimeTLV8, tlv8.MarshalReader(cipherM5)); err != nil {\n\t\treturn\n\t}\n\n\t// STEP M6. Read response\n\tcipherM6 := struct {\n\t\tEncryptedData string `tlv8:\"5\"`\n\t\tState         byte   `tlv8:\"6\"`\n\t\tError         byte   `tlv8:\"7\"`\n\t}{}\n\tif err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &cipherM6); err != nil {\n\t\treturn\n\t}\n\tif cipherM6.State != StateM6 || cipherM6.Error != 0 {\n\t\treturn newResponseError(plainM5, cipherM6)\n\t}\n\n\t// STEP M6. Decrypt payload\n\tb, err = chacha20poly1305.Decrypt(encryptKey, \"PS-Msg06\", []byte(cipherM6.EncryptedData))\n\tif err != nil {\n\t\treturn\n\t}\n\n\tplainM6 := struct {\n\t\tIdentifier string `tlv8:\"1\"`\n\t\tPublicKey  string `tlv8:\"3\"`\n\t\tSignature  string `tlv8:\"10\"`\n\t}{}\n\tif err = tlv8.Unmarshal(b, &plainM6); err != nil {\n\t\treturn\n\t}\n\n\t// STEP M6. Verify payload\n\tremoteSign, err := hkdf.Sha512(\n\t\tsessionShared, \"Pair-Setup-Accessory-Sign-Salt\", \"Pair-Setup-Accessory-Sign-Info\",\n\t)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tb = Append(remoteSign, plainM6.Identifier, plainM6.PublicKey)\n\tif !ed25519.ValidateSignature([]byte(plainM6.PublicKey), b, []byte(plainM6.Signature)) {\n\t\treturn errors.New(\"hap: ValidateSignature\")\n\t}\n\n\tif c.DeviceID != plainM6.Identifier {\n\t\treturn errors.New(\"hap: wrong DeviceID: \" + plainM6.Identifier)\n\t}\n\n\tc.DevicePublic = []byte(plainM6.PublicKey)\n\n\treturn nil\n}\n\nfunc (c *Client) ListPairings() error {\n\tplainM1 := struct {\n\t\tMethod byte `tlv8:\"0\"`\n\t\tState  byte `tlv8:\"6\"`\n\t}{\n\t\tMethod: MethodListPairings,\n\t\tState:  StateM1,\n\t}\n\tres, err := c.Post(PathPairings, MimeTLV8, tlv8.MarshalReader(plainM1))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// TODO: don't know how to fix array of items\n\tvar plainM2 struct {\n\t\tIdentifier string `tlv8:\"1\"`\n\t\tPublicKey  string `tlv8:\"3\"`\n\t\tState      byte   `tlv8:\"6\"`\n\t\tPermission byte   `tlv8:\"11\"`\n\t}\n\tif err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &plainM2); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) PairingsAdd(clientID string, clientPublic []byte, admin bool) error {\n\tplainM1 := struct {\n\t\tMethod     byte   `tlv8:\"0\"`\n\t\tIdentifier string `tlv8:\"1\"`\n\t\tPublicKey  string `tlv8:\"3\"`\n\t\tState      byte   `tlv8:\"6\"`\n\t\tPermission byte   `tlv8:\"11\"`\n\t}{\n\t\tMethod:     MethodAddPairing,\n\t\tIdentifier: clientID,\n\t\tPublicKey:  string(clientPublic),\n\t\tState:      StateM1,\n\t\tPermission: PermissionUser,\n\t}\n\tif admin {\n\t\tplainM1.Permission = PermissionAdmin\n\t}\n\tres, err := c.Post(PathPairings, MimeTLV8, tlv8.MarshalReader(plainM1))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar plainM2 struct {\n\t\tState   byte `tlv8:\"6\"`\n\t\tUnknown byte `tlv8:\"7\"`\n\t}\n\tif err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &plainM2); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) DeletePairing(id string) error {\n\tplainM1 := struct {\n\t\tMethod     byte   `tlv8:\"0\"`\n\t\tIdentifier string `tlv8:\"1\"`\n\t\tState      byte   `tlv8:\"6\"`\n\t}{\n\t\tMethod:     MethodDeletePairing,\n\t\tIdentifier: id,\n\t\tState:      StateM1,\n\t}\n\tres, err := c.Post(PathPairings, MimeTLV8, tlv8.MarshalReader(plainM1))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar plainM2 struct {\n\t\tState byte `tlv8:\"6\"`\n\t}\n\tif err = tlv8.UnmarshalReader(res.Body, res.ContentLength, &plainM2); err != nil {\n\t\treturn err\n\t}\n\tif plainM2.State != StateM2 {\n\t\treturn newResponseError(plainM1, plainM2)\n\t}\n\n\treturn nil\n}\n\nfunc newPairingError(code byte) error {\n\tvar text string\n\t// https://github.com/apple/HomeKitADK/blob/fb201f98f5fdc7fef6a455054f08b59cca5d1ec8/HAP/HAPPairing.h#L89\n\tswitch code {\n\tcase 1:\n\t\ttext = \"Generic error to handle unexpected errors\"\n\tcase 2:\n\t\ttext = \"Setup code or signature verification failed\"\n\tcase 3:\n\t\ttext = \"Client must look at the retry delay TLV item and wait that many seconds before retrying\"\n\tcase 4:\n\t\ttext = \"Server cannot accept any more pairings\"\n\tcase 5:\n\t\ttext = \"Server reached its maximum number of authentication attempts\"\n\tcase 6:\n\t\ttext = \"Server pairing method is unavailable\"\n\tcase 7:\n\t\ttext = \"Server is busy and cannot accept a pairing request at this time\"\n\tdefault:\n\t\ttext = \"Unknown pairing error\"\n\t}\n\treturn errors.New(\"hap: \" + text)\n}\n\nfunc keyDerivativeFuncRFC2945(username []byte) srp.KeyDerivationFunc {\n\treturn func(salt, password []byte) []byte {\n\t\th1 := sha512.New()\n\t\th1.Write(username)\n\t\th1.Write([]byte(\":\"))\n\t\th1.Write(password)\n\n\t\th2 := sha512.New()\n\t\th2.Write(salt)\n\t\th2.Write(h1.Sum(nil))\n\n\t\treturn h2.Sum(nil)\n\t}\n}\n"
  },
  {
    "path": "pkg/hap/conn.go",
    "content": "package hap\n\nimport (\n\t\"bufio\"\n\t\"encoding/binary\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hap/hkdf\"\n)\n\ntype Conn struct {\n\tconn net.Conn\n\trw   *bufio.ReadWriter\n\twmu  sync.Mutex\n\n\tencryptKey []byte\n\tdecryptKey []byte\n\tencryptCnt uint64\n\tdecryptCnt uint64\n\n\t//ClientID  string\n\tSharedKey []byte\n\n\trecv int\n\tsend int\n}\n\nfunc (c *Conn) MarshalJSON() ([]byte, error) {\n\tconn := core.Connection{\n\t\tID:         core.ID(c),\n\t\tFormatName: \"homekit\",\n\t\tProtocol:   \"hap\",\n\t\tRemoteAddr: c.conn.RemoteAddr().String(),\n\t\tRecv:       c.recv,\n\t\tSend:       c.send,\n\t}\n\treturn json.Marshal(conn)\n}\n\nfunc NewConn(conn net.Conn, rw *bufio.ReadWriter, sharedKey []byte, isClient bool) (*Conn, error) {\n\tkey1, err := hkdf.Sha512(sharedKey, \"Control-Salt\", \"Control-Read-Encryption-Key\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tkey2, err := hkdf.Sha512(sharedKey, \"Control-Salt\", \"Control-Write-Encryption-Key\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tc := &Conn{\n\t\tconn: conn,\n\t\trw:   rw,\n\n\t\tSharedKey: sharedKey,\n\t}\n\n\tif isClient {\n\t\tc.encryptKey, c.decryptKey = key2, key1\n\t} else {\n\t\tc.encryptKey, c.decryptKey = key1, key2\n\t}\n\n\treturn c, nil\n}\n\nconst (\n\t// packetSizeMax is the max length of encrypted packets\n\tpacketSizeMax = 0x400\n\n\tVerifySize = 2\n\tNonceSize  = 8\n\tOverhead   = 16 // chacha20poly1305.Overhead\n)\n\nfunc (c *Conn) Read(b []byte) (n int, err error) {\n\tif cap(b) < packetSizeMax {\n\t\treturn 0, errors.New(\"hap: read buffer is too small\")\n\t}\n\n\tverify := make([]byte, VerifySize) // verify = plain message size\n\tif _, err = io.ReadFull(c.rw, verify); err != nil {\n\t\treturn\n\t}\n\n\tn = int(binary.LittleEndian.Uint16(verify))\n\n\tciphertext := make([]byte, n+Overhead)\n\tif _, err = io.ReadFull(c.rw, ciphertext); err != nil {\n\t\treturn\n\t}\n\n\tnonce := make([]byte, NonceSize)\n\tbinary.LittleEndian.PutUint64(nonce, c.decryptCnt)\n\tc.decryptCnt++\n\n\t_, err = chacha20poly1305.DecryptAndVerify(c.decryptKey, b[:0], nonce, ciphertext, verify)\n\n\tc.recv += n\n\treturn\n}\n\nfunc (c *Conn) Write(b []byte) (n int, err error) {\n\tc.wmu.Lock()\n\tdefer c.wmu.Unlock()\n\n\tbuf := make([]byte, 0, packetSizeMax+Overhead)\n\tnonce := make([]byte, NonceSize)\n\tverify := make([]byte, VerifySize)\n\n\tfor len(b) > 0 {\n\t\tsize := len(b)\n\t\tif size > packetSizeMax {\n\t\t\tsize = packetSizeMax\n\t\t}\n\n\t\tbinary.LittleEndian.PutUint16(verify, uint16(size))\n\t\tif _, err = c.rw.Write(verify); err != nil {\n\t\t\treturn\n\t\t}\n\n\t\tbinary.LittleEndian.PutUint64(nonce, c.encryptCnt)\n\t\tc.encryptCnt++\n\n\t\t_, err = chacha20poly1305.EncryptAndSeal(c.encryptKey, buf, nonce, b[:size], verify)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\n\t\tif _, err = c.rw.Write(buf[:size+Overhead]); err != nil {\n\t\t\treturn\n\t\t}\n\n\t\tb = b[size:]\n\t\tn += size\n\t}\n\n\terr = c.rw.Flush()\n\n\tc.send += n\n\treturn\n}\n\nfunc (c *Conn) Close() error {\n\treturn c.conn.Close()\n}\n\nfunc (c *Conn) LocalAddr() net.Addr {\n\treturn c.conn.LocalAddr()\n}\n\nfunc (c *Conn) RemoteAddr() net.Addr {\n\treturn c.conn.RemoteAddr()\n}\n\nfunc (c *Conn) SetDeadline(t time.Time) error {\n\treturn c.conn.SetDeadline(t)\n}\n\nfunc (c *Conn) SetReadDeadline(t time.Time) error {\n\treturn c.conn.SetReadDeadline(t)\n}\n\nfunc (c *Conn) SetWriteDeadline(t time.Time) error {\n\treturn c.conn.SetWriteDeadline(t)\n}\n"
  },
  {
    "path": "pkg/hap/curve25519/curve25519.go",
    "content": "package curve25519\n\nimport (\n\t\"crypto/rand\"\n\n\t\"golang.org/x/crypto/curve25519\"\n)\n\nfunc GenerateKeyPair() ([]byte, []byte) {\n\tvar publicKey, privateKey [32]byte\n\t_, _ = rand.Read(privateKey[:])\n\tcurve25519.ScalarBaseMult(&publicKey, &privateKey)\n\treturn publicKey[:], privateKey[:]\n}\n\nfunc SharedSecret(privateKey, otherPublicKey []byte) ([]byte, error) {\n\treturn curve25519.X25519(privateKey, otherPublicKey)\n}\n"
  },
  {
    "path": "pkg/hap/ed25519/ed25519.go",
    "content": "package ed25519\n\nimport (\n\t\"crypto/ed25519\"\n\t\"errors\"\n)\n\nvar ErrInvalidParams = errors.New(\"ed25519: invalid params\")\n\nfunc ValidateSignature(key, data, signature []byte) bool {\n\tif len(key) != ed25519.PublicKeySize || len(signature) != ed25519.SignatureSize {\n\t\treturn false\n\t}\n\n\treturn ed25519.Verify(key, data, signature)\n}\n\nfunc Signature(key, data []byte) ([]byte, error) {\n\tif len(key) != ed25519.PrivateKeySize {\n\t\treturn nil, ErrInvalidParams\n\t}\n\n\treturn ed25519.Sign(key, data), nil\n}\n"
  },
  {
    "path": "pkg/hap/hds/hds.go",
    "content": "// Package hds - HomeKit Data Stream\npackage hds\n\nimport (\n\t\"bufio\"\n\t\"encoding/binary\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hap\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hap/hkdf\"\n)\n\nfunc NewConn(conn net.Conn, key []byte, salt string, controller bool) (*Conn, error) {\n\twriteKey, err := hkdf.Sha512(key, salt, \"HDS-Write-Encryption-Key\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treadKey, err := hkdf.Sha512(key, salt, \"HDS-Read-Encryption-Key\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tc := &Conn{\n\t\tconn: conn,\n\t\trd:   bufio.NewReaderSize(conn, 32*1024),\n\t\twr:   bufio.NewWriterSize(conn, 32*1024),\n\t}\n\n\tif controller {\n\t\tc.decryptKey, c.encryptKey = readKey, writeKey\n\t} else {\n\t\tc.decryptKey, c.encryptKey = writeKey, readKey\n\t}\n\n\treturn c, nil\n}\n\ntype Conn struct {\n\tconn net.Conn\n\n\trd *bufio.Reader\n\twr *bufio.Writer\n\n\tdecryptKey []byte\n\tencryptKey []byte\n\tdecryptCnt uint64\n\tencryptCnt uint64\n\n\trecv int\n\tsend int\n}\n\nfunc (c *Conn) MarshalJSON() ([]byte, error) {\n\tconn := core.Connection{\n\t\tID:         core.ID(c),\n\t\tFormatName: \"homekit\",\n\t\tProtocol:   \"hds\",\n\t\tRemoteAddr: c.conn.RemoteAddr().String(),\n\t\tRecv:       c.recv,\n\t\tSend:       c.send,\n\t}\n\treturn json.Marshal(conn)\n}\n\nfunc (c *Conn) read() (b []byte, err error) {\n\tverify := make([]byte, 4)\n\tif _, err = io.ReadFull(c.rd, verify); err != nil {\n\t\treturn\n\t}\n\n\tn := int(binary.BigEndian.Uint32(verify) & 0xFFFFFF)\n\n\tciphertext := make([]byte, n+hap.Overhead)\n\tif _, err = io.ReadFull(c.rd, ciphertext); err != nil {\n\t\treturn\n\t}\n\n\tnonce := make([]byte, hap.NonceSize)\n\tbinary.LittleEndian.PutUint64(nonce, c.decryptCnt)\n\tc.decryptCnt++\n\n\tc.recv += n\n\n\treturn chacha20poly1305.DecryptAndVerify(c.decryptKey, ciphertext[:0], nonce, ciphertext, verify)\n}\n\nfunc (c *Conn) Read(p []byte) (n int, err error) {\n\tb, err := c.read()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tn = copy(p, b)\n\tif len(b) > n {\n\t\terr = errors.New(\"hds: read buffer too small\")\n\t}\n\treturn\n}\n\nfunc (c *Conn) WriteTo(w io.Writer) (int64, error) {\n\tvar total int64\n\tfor {\n\t\tb, err := c.read()\n\t\tif err != nil {\n\t\t\treturn total, err\n\t\t}\n\n\t\tn, err := w.Write(b)\n\t\ttotal += int64(n)\n\t\tif err != nil {\n\t\t\treturn total, err\n\t\t}\n\t}\n}\n\nfunc (c *Conn) Write(b []byte) (n int, err error) {\n\tn = len(b)\n\n\tif n > 0xFFFFFF {\n\t\treturn 0, errors.New(\"hds: write buffer too big\")\n\t}\n\n\tverify := make([]byte, 4)\n\tbinary.BigEndian.PutUint32(verify, 0x01000000|uint32(n))\n\tif _, err = c.wr.Write(verify); err != nil {\n\t\treturn\n\t}\n\n\tnonce := make([]byte, hap.NonceSize)\n\tbinary.LittleEndian.PutUint64(nonce, c.encryptCnt)\n\tc.encryptCnt++\n\n\tbuf := make([]byte, n+hap.Overhead)\n\tif _, err = chacha20poly1305.EncryptAndSeal(c.encryptKey, buf[:0], nonce, b, verify); err != nil {\n\t\treturn\n\t}\n\n\tif _, err = c.wr.Write(buf); err != nil {\n\t\treturn\n\t}\n\n\terr = c.wr.Flush()\n\n\tc.send += n\n\treturn\n}\n\nfunc (c *Conn) Close() error {\n\treturn c.conn.Close()\n}\n\nfunc (c *Conn) LocalAddr() net.Addr {\n\treturn c.conn.LocalAddr()\n}\n\nfunc (c *Conn) RemoteAddr() net.Addr {\n\treturn c.conn.RemoteAddr()\n}\n\nfunc (c *Conn) SetDeadline(t time.Time) error {\n\treturn c.conn.SetDeadline(t)\n}\n\nfunc (c *Conn) SetReadDeadline(t time.Time) error {\n\treturn c.conn.SetReadDeadline(t)\n}\n\nfunc (c *Conn) SetWriteDeadline(t time.Time) error {\n\treturn c.conn.SetWriteDeadline(t)\n}\n"
  },
  {
    "path": "pkg/hap/hds/hds_test.go",
    "content": "package hds\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"testing\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestEncryption(t *testing.T) {\n\tkey := []byte(core.RandString(16, 0))\n\tsalt := core.RandString(32, 0)\n\n\tc, err := Client(nil, key, salt, true)\n\trequire.NoError(t, err)\n\n\tbuf := bytes.NewBuffer(nil)\n\tc.wr = bufio.NewWriter(buf)\n\n\tn, err := c.Write([]byte(\"test\"))\n\trequire.NoError(t, err)\n\trequire.Equal(t, 4, n)\n\n\tc, err = Client(nil, key, salt, false)\n\tc.rd = bufio.NewReader(buf)\n\trequire.NoError(t, err)\n\n\tb := make([]byte, 32)\n\tn, err = c.Read(b)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, \"test\", string(b[:n]))\n}\n"
  },
  {
    "path": "pkg/hap/helpers.go",
    "content": "package hap\n\nimport (\n\t\"crypto/ed25519\"\n\t\"crypto/rand\"\n\t\"crypto/sha512\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n)\n\nconst (\n\tTXTConfigNumber = \"c#\" // Current configuration number (ex. 1, 2, 3)\n\tTXTDeviceID     = \"id\" // Device ID of the accessory (ex. 77:75:87:A0:7D:F4)\n\tTXTModel        = \"md\" // Model name of the accessory (ex. MJCTD02YL)\n\tTXTProtoVersion = \"pv\" // Protocol version string (ex. 1.1)\n\tTXTStateNumber  = \"s#\" // Current state number (ex. 1)\n\tTXTCategory     = \"ci\" // Accessory Category Identifier (ex. 2, 5, 17)\n\tTXTSetupHash    = \"sh\" // Setup hash (ex. Y9w9hQ==)\n\n\t// TXTFeatureFlags\n\t//  - 0001b - Supports Apple Authentication Coprocessor\n\t//  - 0010b - Supports Software Authentication\n\tTXTFeatureFlags = \"ff\" // Pairing Feature flags (ex. 0, 1, 2)\n\n\t// TXTStatusFlags\n\t//  - 0001b - Accessory has not been paired with any controllers\n\t//  - 0100b - A problem has been detected on the accessory\n\tTXTStatusFlags = \"sf\" // Status flags (ex. 0, 1)\n\n\tStatusPaired    = \"0\"\n\tStatusNotPaired = \"1\"\n\n\tCategoryBridge   = \"2\"\n\tCategoryCamera   = \"17\"\n\tCategoryDoorbell = \"18\"\n\n\tStateM1 = 1\n\tStateM2 = 2\n\tStateM3 = 3\n\tStateM4 = 4\n\tStateM5 = 5\n\tStateM6 = 6\n\n\tMethodPair          = 0\n\tMethodPairMFi       = 1 // if device has MFI cert\n\tMethodVerifyPair    = 2\n\tMethodAddPairing    = 3\n\tMethodDeletePairing = 4\n\tMethodListPairings  = 5\n\n\tPermissionUser  = 0\n\tPermissionAdmin = 1\n)\n\nconst DeviceAID = 1 // TODO: fix someday\n\ntype JSONAccessories struct {\n\tValue []*Accessory `json:\"accessories\"`\n}\n\ntype JSONCharacters struct {\n\tValue []JSONCharacter `json:\"characteristics\"`\n}\n\ntype JSONCharacter struct {\n\tAID    uint8  `json:\"aid\"`\n\tIID    uint64 `json:\"iid\"`\n\tStatus any    `json:\"status,omitempty\"`\n\tValue  any    `json:\"value,omitempty\"`\n\tEvent  any    `json:\"ev,omitempty\"`\n}\n\n// 4.2.1.2 Invalid Setup Codes\nconst insecurePINs = \"00000000 11111111 22222222 33333333 44444444 55555555 66666666 77777777 88888888 99999999 12345678 87654321\"\n\nfunc SanitizePin(pin string) (string, error) {\n\ts := strings.ReplaceAll(pin, \"-\", \"\")\n\tif len(s) != 8 {\n\t\treturn \"\", errors.New(\"hap: wrong PIN format: \" + pin)\n\t}\n\tif strings.Contains(insecurePINs, s) {\n\t\treturn \"\", errors.New(\"hap: insecure PIN: \" + pin)\n\t}\n\t// 123-45-678\n\treturn s[:3] + \"-\" + s[3:5] + \"-\" + s[5:], nil\n}\n\nfunc GenerateKey() []byte {\n\t_, key, _ := ed25519.GenerateKey(nil)\n\treturn key\n}\n\nfunc GenerateUUID() string {\n\t//12345678-9012-3456-7890-123456789012\n\tdata := make([]byte, 16)\n\t_, _ = rand.Read(data)\n\ts := hex.EncodeToString(data)\n\treturn s[:8] + \"-\" + s[8:12] + \"-\" + s[12:16] + \"-\" + s[16:20] + \"-\" + s[20:]\n}\n\nfunc SetupHash(setupID, deviceID string) string {\n\t// should be setup_id (random 4 alphanum) + device_id (mac address)\n\tb := sha512.Sum512([]byte(setupID + deviceID))\n\treturn base64.StdEncoding.EncodeToString(b[:4])\n}\n\nfunc Append(items ...any) (b []byte) {\n\tfor _, item := range items {\n\t\tswitch v := item.(type) {\n\t\tcase string:\n\t\t\tb = append(b, v...)\n\t\tcase []byte:\n\t\t\tb = append(b, v[:]...)\n\t\tdefault:\n\t\t\tpanic(v)\n\t\t}\n\t}\n\treturn\n}\n\nfunc newRequestError(req any) error {\n\treturn fmt.Errorf(\"hap: wrong request: %#v\", req)\n}\n\nfunc newResponseError(req, res any) error {\n\treturn fmt.Errorf(\"hap: wrong response: %#v, on request: %#v\", res, req)\n}\n"
  },
  {
    "path": "pkg/hap/hkdf/hkdf.go",
    "content": "package hkdf\n\nimport (\n\t\"crypto/sha512\"\n\t\"io\"\n\n\t\"golang.org/x/crypto/hkdf\"\n)\n\nfunc Sha512(key []byte, salt, info string) ([]byte, error) {\n\tr := hkdf.New(sha512.New, key, []byte(salt), []byte(info))\n\n\tbuf := make([]byte, 32)\n\t_, err := io.ReadFull(r, buf)\n\n\treturn buf, err\n}\n"
  },
  {
    "path": "pkg/hap/server.go",
    "content": "package hap\n\nimport (\n\t\"bufio\"\n\t\"crypto/sha512\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/hap/chacha20poly1305\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hap/curve25519\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hap/ed25519\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hap/hkdf\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hap/tlv8\"\n\t\"github.com/tadglines/go-pkgs/crypto/srp\"\n)\n\ntype Server struct {\n\tPin           string\n\tDeviceID      string\n\tDevicePrivate []byte\n\n\t// GetClientPublic may be nil, so client validation will be disabled\n\tGetClientPublic func(id string) []byte\n}\n\nfunc (s *Server) ServerPublic() []byte {\n\treturn s.DevicePrivate[32:]\n}\n\n//func (s *Server) Status() string {\n//\tif len(s.Pairings) == 0 {\n//\t\treturn StatusNotPaired\n//\t}\n//\treturn StatusPaired\n//}\n\nfunc (s *Server) PairSetup(req *http.Request, rw *bufio.ReadWriter) (id string, publicKey []byte, err error) {\n\t// STEP 1. Request from iPhone\n\tvar plainM1 struct {\n\t\tState  byte   `tlv8:\"6\"`\n\t\tMethod byte   `tlv8:\"0\"`\n\t\tFlags  uint32 `tlv8:\"19\"`\n\t}\n\tif err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &plainM1); err != nil {\n\t\treturn\n\t}\n\tif plainM1.State != StateM1 {\n\t\terr = newRequestError(plainM1)\n\t\treturn\n\t}\n\n\tusername := []byte(\"Pair-Setup\")\n\n\t// Stanford Secure Remote Password (SRP) / Password Authenticated Key Exchange (PAKE)\n\tpake, err := srp.NewSRP(\"rfc5054.3072\", sha512.New, keyDerivativeFuncRFC2945(username))\n\tif err != nil {\n\t\treturn\n\t}\n\n\tpake.SaltLength = 16\n\n\tsalt, verifier, err := pake.ComputeVerifier([]byte(s.Pin))\n\tif err != nil {\n\t\treturn\n\t}\n\n\tsession := pake.NewServerSession(username, salt, verifier)\n\n\t// STEP 2. Response to iPhone\n\tplainM2 := struct {\n\t\tState     byte   `tlv8:\"6\"`\n\t\tPublicKey string `tlv8:\"3\"`\n\t\tSalt      string `tlv8:\"2\"`\n\t}{\n\t\tState:     StateM2,\n\t\tPublicKey: string(session.GetB()),\n\t\tSalt:      string(salt),\n\t}\n\tbody, err := tlv8.Marshal(plainM2)\n\tif err != nil {\n\t\treturn\n\t}\n\tif err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil {\n\t\treturn\n\t}\n\n\t// STEP 3. Request from iPhone\n\tif req, err = http.ReadRequest(rw.Reader); err != nil {\n\t\treturn\n\t}\n\n\tvar plainM3 struct {\n\t\tState     byte   `tlv8:\"6\"`\n\t\tPublicKey string `tlv8:\"3\"`\n\t\tProof     string `tlv8:\"4\"`\n\t}\n\tif err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &plainM3); err != nil {\n\t\treturn\n\t}\n\tif plainM3.State != StateM3 {\n\t\terr = newRequestError(plainM3)\n\t\treturn\n\t}\n\n\t// important to compute key before verify client\n\tsessionShared, err := session.ComputeKey([]byte(plainM3.PublicKey))\n\tif err != nil {\n\t\treturn\n\t}\n\n\tif !session.VerifyClientAuthenticator([]byte(plainM3.Proof)) {\n\t\terr = errors.New(\"hap: VerifyClientAuthenticator\")\n\t\treturn\n\t}\n\n\tproof := session.ComputeAuthenticator([]byte(plainM3.Proof)) // server proof\n\n\t// STEP 4. Response to iPhone\n\tpayloadM4 := struct {\n\t\tState byte   `tlv8:\"6\"`\n\t\tProof string `tlv8:\"4\"`\n\t}{\n\t\tState: StateM4,\n\t\tProof: string(proof),\n\t}\n\tif body, err = tlv8.Marshal(payloadM4); err != nil {\n\t\treturn\n\t}\n\tif err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil {\n\t\treturn\n\t}\n\n\t// STEP 5. Request from iPhone\n\tif req, err = http.ReadRequest(rw.Reader); err != nil {\n\t\treturn\n\t}\n\tvar cipherM5 struct {\n\t\tState         byte   `tlv8:\"6\"`\n\t\tEncryptedData string `tlv8:\"5\"`\n\t}\n\tif err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &cipherM5); err != nil {\n\t\treturn\n\t}\n\tif cipherM5.State != StateM5 {\n\t\terr = newRequestError(cipherM5)\n\t\treturn\n\t}\n\n\t// decrypt message using session shared\n\tencryptKey, err := hkdf.Sha512(sessionShared, \"Pair-Setup-Encrypt-Salt\", \"Pair-Setup-Encrypt-Info\")\n\tif err != nil {\n\t\treturn\n\t}\n\n\tb, err := chacha20poly1305.Decrypt(encryptKey, \"PS-Msg05\", []byte(cipherM5.EncryptedData))\n\tif err != nil {\n\t\treturn\n\t}\n\n\t// unpack message from TLV8\n\tvar plainM5 struct {\n\t\tIdentifier string `tlv8:\"1\"`\n\t\tPublicKey  string `tlv8:\"3\"`\n\t\tSignature  string `tlv8:\"10\"`\n\t}\n\tif err = tlv8.Unmarshal(b, &plainM5); err != nil {\n\t\treturn\n\t}\n\n\t// 3. verify client ID and Public\n\tremoteSign, err := hkdf.Sha512(\n\t\tsessionShared, \"Pair-Setup-Controller-Sign-Salt\", \"Pair-Setup-Controller-Sign-Info\",\n\t)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tb = Append(remoteSign, plainM5.Identifier, plainM5.PublicKey)\n\tif !ed25519.ValidateSignature([]byte(plainM5.PublicKey), b, []byte(plainM5.Signature)) {\n\t\terr = errors.New(\"hap: ValidateSignature\")\n\t\treturn\n\t}\n\n\t// 4. generate signature to our ID and Public\n\tlocalSign, err := hkdf.Sha512(\n\t\tsessionShared, \"Pair-Setup-Accessory-Sign-Salt\", \"Pair-Setup-Accessory-Sign-Info\",\n\t)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tb = Append(localSign, s.DeviceID, s.ServerPublic()) // ServerPublic\n\tsignature, err := ed25519.Signature(s.DevicePrivate, b)\n\tif err != nil {\n\t\treturn\n\t}\n\n\t// 5. pack our ID and Public\n\tplainM6 := struct {\n\t\tIdentifier string `tlv8:\"1\"`\n\t\tPublicKey  string `tlv8:\"3\"`\n\t\tSignature  string `tlv8:\"10\"`\n\t}{\n\t\tIdentifier: s.DeviceID,\n\t\tPublicKey:  string(s.ServerPublic()),\n\t\tSignature:  string(signature),\n\t}\n\tif b, err = tlv8.Marshal(plainM6); err != nil {\n\t\treturn\n\t}\n\n\t// 6. encrypt message\n\tb, err = chacha20poly1305.Encrypt(encryptKey, \"PS-Msg06\", b)\n\tif err != nil {\n\t\treturn\n\t}\n\n\t// STEP 6. Response to iPhone\n\tcipherM6 := struct {\n\t\tState         byte   `tlv8:\"6\"`\n\t\tEncryptedData string `tlv8:\"5\"`\n\t}{\n\t\tState:         StateM6,\n\t\tEncryptedData: string(b),\n\t}\n\tif body, err = tlv8.Marshal(cipherM6); err != nil {\n\t\treturn\n\t}\n\tif err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil {\n\t\treturn\n\t}\n\n\tid = plainM5.Identifier\n\tpublicKey = []byte(plainM5.PublicKey)\n\n\treturn\n}\n\nfunc (s *Server) PairVerify(req *http.Request, rw *bufio.ReadWriter) (id string, sessionKey []byte, err error) {\n\t// Request from iPhone\n\tvar plainM1 struct {\n\t\tState     byte   `tlv8:\"6\"`\n\t\tPublicKey string `tlv8:\"3\"`\n\t}\n\tif err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &plainM1); err != nil {\n\t\treturn\n\t}\n\tif plainM1.State != StateM1 {\n\t\terr = newRequestError(plainM1)\n\t\treturn\n\t}\n\n\t// Generate the key pair\n\tsessionPublic, sessionPrivate := curve25519.GenerateKeyPair()\n\tsessionShared, err := curve25519.SharedSecret(sessionPrivate, []byte(plainM1.PublicKey))\n\tif err != nil {\n\t\treturn\n\t}\n\n\tencryptKey, err := hkdf.Sha512(\n\t\tsessionShared, \"Pair-Verify-Encrypt-Salt\", \"Pair-Verify-Encrypt-Info\",\n\t)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tb := Append(sessionPublic, s.DeviceID, plainM1.PublicKey)\n\tsignature, err := ed25519.Signature(s.DevicePrivate, b)\n\tif err != nil {\n\t\treturn\n\t}\n\n\t// STEP M2. Response to iPhone\n\tplainM2 := struct {\n\t\tIdentifier string `tlv8:\"1\"`\n\t\tSignature  string `tlv8:\"10\"`\n\t}{\n\t\tIdentifier: s.DeviceID,\n\t\tSignature:  string(signature),\n\t}\n\tif b, err = tlv8.Marshal(plainM2); err != nil {\n\t\treturn\n\t}\n\n\tb, err = chacha20poly1305.Encrypt(encryptKey, \"PV-Msg02\", b)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tcipherM2 := struct {\n\t\tState         byte   `tlv8:\"6\"`\n\t\tPublicKey     string `tlv8:\"3\"`\n\t\tEncryptedData string `tlv8:\"5\"`\n\t}{\n\t\tState:         StateM2,\n\t\tPublicKey:     string(sessionPublic),\n\t\tEncryptedData: string(b),\n\t}\n\tbody, err := tlv8.Marshal(cipherM2)\n\tif err != nil {\n\t\treturn\n\t}\n\tif err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil {\n\t\treturn\n\t}\n\n\t// STEP M3. Request from iPhone\n\tif req, err = http.ReadRequest(rw.Reader); err != nil {\n\t\treturn\n\t}\n\n\tvar cipherM3 struct {\n\t\tState         byte   `tlv8:\"6\"`\n\t\tEncryptedData string `tlv8:\"5\"`\n\t}\n\tif err = tlv8.UnmarshalReader(req.Body, req.ContentLength, &cipherM3); err != nil {\n\t\treturn\n\t}\n\tif cipherM3.State != StateM3 {\n\t\terr = newRequestError(cipherM3)\n\t\treturn\n\t}\n\n\tb, err = chacha20poly1305.Decrypt(encryptKey, \"PV-Msg03\", []byte(cipherM3.EncryptedData))\n\tif err != nil {\n\t\treturn\n\t}\n\n\tvar plainM3 struct {\n\t\tIdentifier string `tlv8:\"1\"`\n\t\tSignature  string `tlv8:\"10\"`\n\t}\n\tif err = tlv8.Unmarshal(b, &plainM3); err != nil {\n\t\treturn\n\t}\n\n\tif s.GetClientPublic != nil {\n\t\tclientPublic := s.GetClientPublic(plainM3.Identifier)\n\t\tif clientPublic == nil {\n\t\t\terr = errors.New(\"hap: PairVerify with unknown client_id: \" + plainM3.Identifier)\n\t\t\treturn\n\t\t}\n\n\t\tb = Append(plainM1.PublicKey, plainM3.Identifier, sessionPublic)\n\t\tif !ed25519.ValidateSignature(clientPublic, b, []byte(plainM3.Signature)) {\n\t\t\terr = errors.New(\"hap: ValidateSignature\")\n\t\t\treturn\n\t\t}\n\t}\n\n\t// STEP M4. Response to iPhone\n\tpayloadM4 := struct {\n\t\tState byte `tlv8:\"6\"`\n\t}{\n\t\tState: StateM4,\n\t}\n\tif body, err = tlv8.Marshal(payloadM4); err != nil {\n\t\treturn\n\t}\n\tif err = WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body); err != nil {\n\t\treturn\n\t}\n\n\tid = plainM3.Identifier\n\tsessionKey = sessionShared\n\n\treturn\n}\n\nfunc WriteResponse(w *bufio.Writer, statusCode int, contentType string, body []byte) error {\n\theader := fmt.Sprintf(\n\t\t\"HTTP/1.1 %d %s\\r\\nContent-Type: %s\\r\\nContent-Length: %d\\r\\n\\r\\n\",\n\t\tstatusCode, http.StatusText(statusCode), contentType, len(body),\n\t)\n\tbody = append([]byte(header), body...)\n\tif _, err := w.Write(body); err != nil {\n\t\treturn err\n\t}\n\treturn w.Flush()\n}\n\n//func WriteBackoff(rw *bufio.ReadWriter) error {\n//\tplainM2 := struct {\n//\t\tState byte `tlv8:\"6\"`\n//\t\tError byte `tlv8:\"7\"`\n//\t}{\n//\t\tState: StateM2,\n//\t\tError: 3, // BackoffError\n//\t}\n//\tbody, err := tlv8.Marshal(plainM2)\n//\tif err != nil {\n//\t\treturn err\n//\t}\n//\treturn WriteResponse(rw.Writer, http.StatusOK, MimeTLV8, body)\n//}\n"
  },
  {
    "path": "pkg/hap/setup/setup.go",
    "content": "package setup\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n)\n\nconst (\n\tFlagNFC = 1\n\tFlagIP  = 2\n\tFlagBLE = 4\n\tFlagWAC = 8 // Wireless Accessory Configuration (WAC)/Apples MFi\n)\n\nfunc GenerateSetupURI(category, pin, setupID string) string {\n\tc, _ := strconv.Atoi(category)\n\tp, _ := strconv.Atoi(strings.ReplaceAll(pin, \"-\", \"\"))\n\tpayload := int64(c&0xFF)<<31 | int64(FlagIP&0xF)<<27 | int64(p&0x7FFFFFF)\n\treturn \"X-HM://\" + FormatInt36(payload, 9) + setupID\n}\n\n// FormatInt36 equal to strings.ToUpper(fmt.Sprintf(\"%0\"+strconv.Itoa(n)+\"s\", strconv.FormatInt(value, 36)))\nfunc FormatInt36(value int64, n int) string {\n\tb := make([]byte, n)\n\tfor i := n - 1; 0 <= i; i-- {\n\t\tb[i] = digits[value%36]\n\t\tvalue /= 36\n\t}\n\treturn string(b)\n}\n\nconst digits = \"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n"
  },
  {
    "path": "pkg/hap/setup/setup_test.go",
    "content": "package setup\n\nimport (\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestFormatAlphaNum(t *testing.T) {\n\tvalue := int64(999)\n\tn := 5\n\ts1 := strings.ToUpper(fmt.Sprintf(\"%0\"+strconv.Itoa(n)+\"s\", strconv.FormatInt(value, 36)))\n\ts2 := FormatInt36(value, n)\n\trequire.Equal(t, s1, s2)\n}\n"
  },
  {
    "path": "pkg/hap/tlv8/tlv8.go",
    "content": "package tlv8\n\nimport (\n\t\"bytes\"\n\t\"encoding/base64\"\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"math\"\n\t\"reflect\"\n\t\"strconv\"\n)\n\ntype errReader struct {\n\terr error\n}\n\nfunc (e *errReader) Read([]byte) (int, error) {\n\treturn 0, e.err\n}\n\nfunc MarshalBase64(v any) (string, error) {\n\tb, err := Marshal(v)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\treturn base64.StdEncoding.EncodeToString(b), nil\n}\n\nfunc MarshalReader(v any) io.Reader {\n\tb, err := Marshal(v)\n\tif err != nil {\n\t\treturn &errReader{err: err}\n\t}\n\treturn bytes.NewReader(b)\n}\n\nfunc Marshal(v any) ([]byte, error) {\n\tvalue := reflect.ValueOf(v)\n\tkind := value.Type().Kind()\n\n\tif kind == reflect.Pointer {\n\t\tvalue = value.Elem()\n\t\tkind = value.Type().Kind()\n\t}\n\n\tswitch kind {\n\tcase reflect.Slice:\n\t\treturn appendSlice(nil, value)\n\tcase reflect.Struct:\n\t\treturn appendStruct(nil, value)\n\t}\n\n\treturn nil, errors.New(\"tlv8: not implemented: \" + kind.String())\n}\n\n// separator the most confusing meaning in the documentation.\n// It can have a value of 0x00 or 0xFF or even 0x05.\nconst separator = 0xFF\n\nfunc appendSlice(b []byte, value reflect.Value) ([]byte, error) {\n\tfor i := 0; i < value.Len(); i++ {\n\t\tif i > 0 {\n\t\t\tb = append(b, separator, 0)\n\t\t}\n\t\tvar err error\n\t\tif b, err = appendStruct(b, value.Index(i)); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\treturn b, nil\n}\n\nfunc appendStruct(b []byte, value reflect.Value) ([]byte, error) {\n\tvalueType := value.Type()\n\n\tfor i := 0; i < value.NumField(); i++ {\n\t\trefField := value.Field(i)\n\t\ts, ok := valueType.Field(i).Tag.Lookup(\"tlv8\")\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\ttag, err := strconv.Atoi(s)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tb, err = appendValue(b, byte(tag), refField)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn b, nil\n}\n\nfunc appendValue(b []byte, tag byte, value reflect.Value) ([]byte, error) {\n\tvar err error\n\n\tswitch value.Kind() {\n\tcase reflect.Uint8:\n\t\tv := value.Uint()\n\t\treturn append(b, tag, 1, byte(v)), nil\n\n\tcase reflect.Uint16:\n\t\tv := value.Uint()\n\t\treturn append(b, tag, 2, byte(v), byte(v>>8)), nil\n\n\tcase reflect.Uint32:\n\t\tv := value.Uint()\n\t\treturn append(b, tag, 4, byte(v), byte(v>>8), byte(v>>16), byte(v>>24)), nil\n\n\tcase reflect.Uint64:\n\t\tv := value.Uint()\n\t\treturn binary.LittleEndian.AppendUint64(append(b, tag, 8), v), nil\n\n\tcase reflect.Float32:\n\t\tv := math.Float32bits(float32(value.Float()))\n\t\treturn append(b, tag, 4, byte(v), byte(v>>8), byte(v>>16), byte(v>>24)), nil\n\n\tcase reflect.String:\n\t\tv := value.String()\n\t\tl := len(v) // support \"big\" string\n\t\tfor ; l > 255; l -= 255 {\n\t\t\tb = append(b, tag, 255)\n\t\t\tb = append(b, v[:255]...)\n\t\t\tv = v[255:]\n\t\t}\n\t\tb = append(b, tag, byte(l))\n\t\treturn append(b, v...), nil\n\n\tcase reflect.Array:\n\t\tif value.Type().Elem().Kind() == reflect.Uint8 {\n\t\t\tn := value.Len()\n\t\t\tb = append(b, tag, byte(n))\n\t\t\tfor i := 0; i < n; i++ {\n\t\t\t\tb = append(b, byte(value.Index(i).Uint()))\n\t\t\t}\n\t\t\treturn b, nil\n\t\t}\n\n\tcase reflect.Slice:\n\t\tfor i := 0; i < value.Len(); i++ {\n\t\t\tif i > 0 {\n\t\t\t\tb = append(b, separator, 0)\n\t\t\t}\n\t\t\tif b, err = appendValue(b, tag, value.Index(i)); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\t\treturn b, nil\n\n\tcase reflect.Struct:\n\t\tb = append(b, tag, 0)\n\t\ti := len(b)\n\t\tif b, err = appendStruct(b, value); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tb[i-1] = byte(len(b) - i) // set struct size\n\t\treturn b, nil\n\t}\n\n\treturn nil, errors.New(\"tlv8: not implemented: \" + value.Kind().String())\n}\n\nfunc UnmarshalBase64(in any, out any) error {\n\ts, _ := in.(string) // protect from in == nil\n\tdata, err := base64.StdEncoding.DecodeString(s)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn Unmarshal(data, out)\n}\n\nfunc UnmarshalReader(r io.Reader, n int64, v any) error {\n\tvar data []byte\n\tvar err error\n\n\tif n > 0 {\n\t\tdata = make([]byte, n)\n\t\t_, err = io.ReadFull(r, data)\n\t} else {\n\t\tdata, err = io.ReadAll(r)\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treturn Unmarshal(data, v)\n}\n\nfunc Unmarshal(data []byte, v any) error {\n\tif len(data) == 0 {\n\t\treturn errors.New(\"tlv8: unmarshal zero data\")\n\t}\n\n\tvalue := reflect.ValueOf(v)\n\tkind := value.Kind()\n\n\tif kind != reflect.Pointer {\n\t\treturn errors.New(\"tlv8: value should be pointer: \" + kind.String())\n\t}\n\n\tvalue = value.Elem()\n\tkind = value.Kind()\n\n\tif kind == reflect.Interface {\n\t\tvalue = value.Elem()\n\t\tkind = value.Kind()\n\t}\n\n\tswitch kind {\n\tcase reflect.Slice:\n\t\treturn unmarshalSlice(data, value)\n\tcase reflect.Struct:\n\t\treturn unmarshalStruct(data, value)\n\t}\n\n\treturn errors.New(\"tlv8: not implemented: \" + kind.String())\n}\n\n// unmarshalTLV can return two types of errors:\n// - critical and then the value of []byte will be nil\n// - not critical and then []byte will contain the value\nfunc unmarshalTLV(b []byte, value reflect.Value) ([]byte, error) {\n\tif len(b) < 2 {\n\t\treturn nil, errors.New(\"tlv8: wrong size: \" + value.Type().Name())\n\t}\n\n\tt := b[0]\n\tl := int(b[1])\n\n\t// array item divider (t == 0x00 || t == 0xFF)\n\tif l == 0 {\n\t\treturn b[2:], errors.New(\"tlv8: zero item\")\n\t}\n\n\tvar v []byte\n\n\tfor {\n\t\tif len(b) < 2+l {\n\t\t\treturn nil, errors.New(\"tlv8: wrong size: \" + value.Type().Name())\n\t\t}\n\n\t\tv = append(v, b[2:2+l]...)\n\t\tb = b[2+l:]\n\n\t\t// if size == 255 and same tag - continue read big payload\n\t\tif l < 255 || len(b) < 2 || b[0] != t {\n\t\t\tbreak\n\t\t}\n\n\t\tl = int(b[1])\n\t}\n\n\ttag := strconv.Itoa(int(t))\n\n\tvalueField, ok := getStructField(value, tag)\n\tif !ok {\n\t\treturn b, fmt.Errorf(\"tlv8: can't find T=%d,L=%d,V=%x for: %s\", t, l, v, value.Type().Name())\n\t}\n\n\tif err := unmarshalValue(v, valueField); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn b, nil\n}\n\nfunc unmarshalSlice(b []byte, value reflect.Value) error {\n\tvalueIndex := value.Index(growSlice(value))\n\tfor len(b) > 0 {\n\t\tvar err error\n\t\tif b, err = unmarshalTLV(b, valueIndex); err != nil {\n\t\t\tif b != nil {\n\t\t\t\tvalueIndex = value.Index(growSlice(value))\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc unmarshalStruct(b []byte, value reflect.Value) error {\n\tfor len(b) > 0 {\n\t\tvar err error\n\t\tif b, err = unmarshalTLV(b, value); b == nil && err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc unmarshalValue(v []byte, value reflect.Value) error {\n\tswitch value.Kind() {\n\tcase reflect.Uint8:\n\t\tif len(v) != 1 {\n\t\t\treturn errors.New(\"tlv8: wrong size: \" + value.Type().Name())\n\t\t}\n\t\tvalue.SetUint(uint64(v[0]))\n\n\tcase reflect.Uint16:\n\t\tif len(v) != 2 {\n\t\t\treturn errors.New(\"tlv8: wrong size: \" + value.Type().Name())\n\t\t}\n\t\tvalue.SetUint(uint64(v[0]) | uint64(v[1])<<8)\n\n\tcase reflect.Uint32:\n\t\tif len(v) != 4 {\n\t\t\treturn errors.New(\"tlv8: wrong size: \" + value.Type().Name())\n\t\t}\n\t\tvalue.SetUint(uint64(v[0]) | uint64(v[1])<<8 | uint64(v[2])<<16 | uint64(v[3])<<24)\n\n\tcase reflect.Uint64:\n\t\tif len(v) != 8 {\n\t\t\treturn errors.New(\"tlv8: wrong size: \" + value.Type().Name())\n\t\t}\n\t\tvalue.SetUint(binary.LittleEndian.Uint64(v))\n\n\tcase reflect.Float32:\n\t\tf := math.Float32frombits(binary.LittleEndian.Uint32(v))\n\t\tvalue.SetFloat(float64(f))\n\n\tcase reflect.String:\n\t\tvalue.SetString(string(v))\n\n\tcase reflect.Array:\n\t\tif kind := value.Type().Elem().Kind(); kind != reflect.Uint8 {\n\t\t\treturn errors.New(\"tlv8: unsupported array: \" + kind.String())\n\t\t}\n\n\t\tfor i, b := range v {\n\t\t\tvalue.Index(i).SetUint(uint64(b))\n\t\t}\n\t\treturn nil\n\n\tcase reflect.Slice:\n\t\ti := growSlice(value)\n\t\treturn unmarshalValue(v, value.Index(i))\n\n\tcase reflect.Struct:\n\t\treturn unmarshalStruct(v, value)\n\n\tdefault:\n\t\treturn errors.New(\"tlv8: not implemented: \" + value.Kind().String())\n\t}\n\n\treturn nil\n}\n\nfunc getStructField(value reflect.Value, tag string) (reflect.Value, bool) {\n\tvalueType := value.Type()\n\n\tfor i := 0; i < value.NumField(); i++ {\n\t\tvalueField := value.Field(i)\n\n\t\tif s, ok := valueType.Field(i).Tag.Lookup(\"tlv8\"); ok && s == tag {\n\t\t\treturn valueField, true\n\t\t}\n\t}\n\n\treturn reflect.Value{}, false\n}\n\nfunc growSlice(value reflect.Value) int {\n\tsize := value.Len()\n\n\tif size >= value.Cap() {\n\t\tnewcap := value.Cap() + value.Cap()/2\n\t\tif newcap < 4 {\n\t\t\tnewcap = 4\n\t\t}\n\t\tnewValue := reflect.MakeSlice(value.Type(), value.Len(), newcap)\n\t\treflect.Copy(newValue, value)\n\t\tvalue.Set(newValue)\n\t}\n\n\tif size >= value.Len() {\n\t\tvalue.SetLen(size + 1)\n\t}\n\n\treturn size\n}\n"
  },
  {
    "path": "pkg/hap/tlv8/tlv8_test.go",
    "content": "package tlv8\n\nimport (\n\t\"encoding/hex\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestMarshal(t *testing.T) {\n\ttype Struct struct {\n\t\tByte    byte    `tlv8:\"1\"`\n\t\tUint16  uint16  `tlv8:\"2\"`\n\t\tUint32  uint32  `tlv8:\"3\"`\n\t\tFloat32 float32 `tlv8:\"4\"`\n\t\tString  string  `tlv8:\"5\"`\n\t\tSlice   []byte  `tlv8:\"6\"`\n\t\tArray   [4]byte `tlv8:\"7\"`\n\t}\n\n\tsrc := Struct{\n\t\tByte:    1,\n\t\tUint16:  2,\n\t\tUint32:  3,\n\t\tFloat32: 1.23,\n\t\tString:  \"123\",\n\t\tSlice:   []byte{1, 2, 3},\n\t\tArray:   [4]byte{1, 2, 3, 4},\n\t}\n\n\tb, err := Marshal(src)\n\trequire.Nil(t, err)\n\n\tvar dst Struct\n\terr = Unmarshal(b, &dst)\n\trequire.Nil(t, err)\n\n\trequire.Equal(t, src, dst)\n}\n\nfunc TestBytes(t *testing.T) {\n\tbytes := make([]byte, 255)\n\tfor i := 0; i < len(bytes); i++ {\n\t\tbytes[i] = byte(i)\n\t}\n\n\ttype Struct struct {\n\t\tString string `tlv8:\"1\"`\n\t}\n\tsrc := Struct{\n\t\tString: string(bytes),\n\t}\n\n\tb, err := Marshal(src)\n\trequire.Nil(t, err)\n\n\tvar dst Struct\n\terr = Unmarshal(b, &dst)\n\trequire.Nil(t, err)\n\n\trequire.Equal(t, src, dst)\n\trequire.Equal(t, bytes, []byte(dst.String))\n}\n\nfunc TestVideoCodecParams(t *testing.T) {\n\ttype VideoCodecParams struct {\n\t\tProfileID         []byte `tlv8:\"1\"`\n\t\tLevel             []byte `tlv8:\"2\"`\n\t\tPacketizationMode byte   `tlv8:\"3\"`\n\t\tCVOEnabled        []byte `tlv8:\"4\"`\n\t\tCVOID             []byte `tlv8:\"5\"`\n\t}\n\n\tsrc, err := hex.DecodeString(\"0101010201000000020102030100040100\")\n\trequire.Nil(t, err)\n\n\tvar v VideoCodecParams\n\terr = Unmarshal(src, &v)\n\trequire.Nil(t, err)\n\n\tdst, err := Marshal(v)\n\trequire.Nil(t, err)\n\n\trequire.Equal(t, src, dst)\n}\n\nfunc TestInterface(t *testing.T) {\n\ttype Struct struct {\n\t\tByte byte `tlv8:\"1\"`\n\t}\n\n\tsrc := Struct{\n\t\tByte: 1,\n\t}\n\tvar v1 any = &src\n\n\tb, err := Marshal(v1)\n\trequire.Nil(t, err)\n\n\trequire.Equal(t, []byte{1, 1, 1}, b)\n\n\tvar dst Struct\n\tvar v2 any = &dst\n\n\terr = Unmarshal(b, v2)\n\trequire.Nil(t, err)\n\n\trequire.Equal(t, src, dst)\n}\n\nfunc TestSlice1(t *testing.T) {\n\tvar v struct {\n\t\tVideoAttrs []struct {\n\t\t\tWidth     uint16 `tlv8:\"1\"`\n\t\t\tHeight    uint16 `tlv8:\"2\"`\n\t\t\tFramerate uint8  `tlv8:\"3\"`\n\t\t} `tlv8:\"3\"`\n\t}\n\n\ts := `030b010280070202380403011e ff00 030b010200050202d00203011e`\n\tb1, err := hex.DecodeString(strings.ReplaceAll(s, \" \", \"\"))\n\trequire.NoError(t, err)\n\n\terr = Unmarshal(b1, &v)\n\trequire.NoError(t, err)\n\n\trequire.Len(t, v.VideoAttrs, 2)\n\n\tb2, err := Marshal(v)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, b1, b2)\n}\n\nfunc TestSlice2(t *testing.T) {\n\tvar v []struct {\n\t\tWidth     uint16 `tlv8:\"1\"`\n\t\tHeight    uint16 `tlv8:\"2\"`\n\t\tFramerate uint8  `tlv8:\"3\"`\n\t}\n\n\ts := `010280070202380403011e ff00 010200050202d00203011e`\n\tb1, err := hex.DecodeString(strings.ReplaceAll(s, \" \", \"\"))\n\trequire.NoError(t, err)\n\n\terr = Unmarshal(b1, &v)\n\trequire.NoError(t, err)\n\n\trequire.Len(t, v, 2)\n\n\tb2, err := Marshal(v)\n\trequire.NoError(t, err)\n\n\trequire.Equal(t, b1, b2)\n}\n"
  },
  {
    "path": "pkg/hass/api.go",
    "content": "package hass\n\nimport (\n\t\"errors\"\n\t\"os\"\n\n\t\"github.com/gorilla/websocket\"\n)\n\ntype API struct {\n\tws *websocket.Conn\n}\n\nfunc NewAPI(url, token string) (*API, error) {\n\tws, _, err := websocket.DefaultDialer.Dial(url, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tapi := &API{ws: ws}\n\tif err = api.Auth(token); err != nil {\n\t\t_ = ws.Close()\n\t\treturn nil, err\n\t}\n\n\treturn api, nil\n}\n\nfunc (a *API) Auth(token string) error {\n\tvar res ResponseAuth\n\n\tif err := a.ws.ReadJSON(&res); err != nil {\n\t\treturn err\n\t}\n\tif res.Type != \"auth_required\" {\n\t\treturn errors.New(\"hass: wrong type: \" + res.Type)\n\t}\n\n\ts := `{\"type\":\"auth\",\"access_token\":\"` + token + `\"}`\n\tif err := a.ws.WriteMessage(websocket.TextMessage, []byte(s)); err != nil {\n\t\treturn err\n\t}\n\tif err := a.ws.ReadJSON(&res); err != nil {\n\t\treturn err\n\t}\n\tif res.Type != \"auth_ok\" {\n\t\treturn errors.New(\"hass: wrong type: \" + res.Type)\n\t}\n\n\treturn nil\n}\n\nfunc (a *API) Close() error {\n\treturn a.ws.Close()\n}\n\nfunc (a *API) ExchangeSDP(entityID, offer string) (string, error) {\n\tvar msg = map[string]any{\n\t\t\"id\":        1,\n\t\t\"type\":      \"camera/web_rtc_offer\",\n\t\t\"entity_id\": entityID,\n\t\t\"offer\":     offer,\n\t}\n\tif err := a.ws.WriteJSON(msg); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar res ResponseOffer\n\tif err := a.ws.ReadJSON(&res); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif res.Type != \"result\" || !res.Success {\n\t\treturn \"\", errors.New(\"hass: wrong response\")\n\t}\n\n\treturn res.Result.Answer, nil\n}\n\nfunc (a *API) GetWebRTCEntities() (map[string]string, error) {\n\ts := `{\"id\":1,\"type\":\"get_states\"}`\n\tif err := a.ws.WriteMessage(websocket.TextMessage, []byte(s)); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar res ResponseStates\n\tif err := a.ws.ReadJSON(&res); err != nil {\n\t\treturn nil, err\n\t}\n\tif res.Type != \"result\" || !res.Success {\n\t\treturn nil, errors.New(\"hass: wrong response\")\n\t}\n\n\tentities := map[string]string{}\n\n\tfor _, entity := range res.Result {\n\t\tif entity.Attributes.FrontendStreamType == \"web_rtc\" {\n\t\t\tentities[entity.Attributes.FriendlyName] = entity.EntityId\n\t\t}\n\t}\n\n\treturn entities, nil\n}\n\ntype ResponseAuth struct {\n\tType string `json:\"type\"`\n}\n\ntype ResponseStates struct {\n\t//Id      int    `json:\"id\"`\n\tType    string `json:\"type\"`\n\tSuccess bool   `json:\"success\"`\n\tResult  []struct {\n\t\tEntityId string `json:\"entity_id\"`\n\t\t//State      string `json:\"state\"`\n\t\tAttributes struct {\n\t\t\t//ModelName          string `json:\"model_name\"`\n\t\t\t//Brand              string `json:\"brand\"`\n\t\t\tFrontendStreamType string `json:\"frontend_stream_type\"`\n\t\t\tFriendlyName       string `json:\"friendly_name\"`\n\t\t\t//SupportedFeatures  int    `json:\"supported_features\"`\n\t\t} `json:\"attributes\"`\n\t\t//LastChanged time.Time `json:\"last_changed\"`\n\t\t//LastUpdated time.Time `json:\"last_updated\"`\n\t\t//Context     struct {\n\t\t//\tId       string      `json:\"id\"`\n\t\t//\tParentId interface{} `json:\"parent_id\"`\n\t\t//\tUserId   interface{} `json:\"user_id\"`\n\t\t//} `json:\"context\"`\n\t} `json:\"result\"`\n}\n\ntype ResponseOffer struct {\n\t//Id      int    `json:\"id\"`\n\tType    string `json:\"type\"`\n\tSuccess bool   `json:\"success\"`\n\tResult  struct {\n\t\tAnswer string `json:\"answer\"`\n\t} `json:\"result\"`\n}\n\nfunc SupervisorToken() string {\n\treturn os.Getenv(\"SUPERVISOR_TOKEN\")\n}\n"
  },
  {
    "path": "pkg/hass/client.go",
    "content": "package hass\n\nimport (\n\t\"errors\"\n\t\"net/url\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/webrtc\"\n\tpion \"github.com/pion/webrtc/v4\"\n)\n\ntype Client struct {\n\tconn *webrtc.Conn\n}\n\nfunc NewClient(rawURL string) (*Client, error) {\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tquery := u.Query()\n\n\tentityID := query.Get(\"entity_id\")\n\tif entityID == \"\" {\n\t\treturn nil, errors.New(\"hass: no entity_id\")\n\t}\n\n\tvar uri, token string\n\n\tif u.Host == \"supervisor\" {\n\t\turi = \"ws://supervisor/core/websocket\"\n\t\ttoken = SupervisorToken()\n\t} else {\n\t\turi = \"ws://\" + u.Host + \"/api/websocket\"\n\t\ttoken = query.Get(\"token\")\n\t}\n\n\tif token == \"\" {\n\t\treturn nil, errors.New(\"hass: no token\")\n\t}\n\n\t// 1. Check connection to Hass\n\thassAPI, err := NewAPI(uri, token)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdefer hassAPI.Close()\n\n\t// 2. Create WebRTC client\n\trtcAPI, err := webrtc.NewAPI()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tconf := pion.Configuration{}\n\tpc, err := rtcAPI.NewPeerConnection(conf)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tconn := webrtc.NewConn(pc)\n\tconn.FormatName = \"hass/webrtc\"\n\tconn.Mode = core.ModeActiveProducer\n\tconn.Protocol = \"ws\"\n\tconn.URL = rawURL\n\n\t// https://developers.google.com/nest/device-access/traits/device/camera-live-stream#generatewebrtcstream-request-fields\n\tmedias := []*core.Media{\n\t\t{Kind: core.KindAudio, Direction: core.DirectionRecvonly},\n\t\t{Kind: core.KindVideo, Direction: core.DirectionRecvonly},\n\t\t{Kind: \"app\"}, // important for Nest\n\t}\n\n\t// 3. Create offer with candidates\n\toffer, err := conn.CreateCompleteOffer(medias)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 4. Exchange SDP via Hass\n\tanswer, err := hassAPI.ExchangeSDP(entityID, offer)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 5. Set answer with remote medias\n\tif err = conn.SetAnswer(answer); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &Client{conn: conn}, nil\n}\n\nfunc (c *Client) GetMedias() []*core.Media {\n\treturn c.conn.GetMedias()\n}\n\nfunc (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {\n\treturn c.conn.GetTrack(media, codec)\n}\n\nfunc (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {\n\treturn c.conn.AddTrack(media, codec, track)\n}\n\nfunc (c *Client) Start() error {\n\treturn c.conn.Start()\n}\n\nfunc (c *Client) Stop() error {\n\treturn c.conn.Stop()\n}\n\nfunc (c *Client) MarshalJSON() ([]byte, error) {\n\treturn c.conn.MarshalJSON()\n}\n"
  },
  {
    "path": "pkg/hls/producer.go",
    "content": "package hls\n\nimport (\n\t\"io\"\n\t\"net/url\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/mpegts\"\n)\n\nfunc OpenURL(u *url.URL, body io.ReadCloser) (*mpegts.Producer, error) {\n\trd, err := NewReader(u, body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tprod, err := mpegts.Open(rd)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tprod.FormatName = \"hls/mpegts\"\n\tprod.RemoteAddr = u.Host\n\treturn prod, nil\n}\n"
  },
  {
    "path": "pkg/hls/reader.go",
    "content": "package hls\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\ntype reader struct {\n\tclient  *http.Client\n\trequest *http.Request\n\n\tplaylist    []byte\n\tlastSegment []byte\n\tlastTime    time.Time\n\n\tbuf []byte\n}\n\nfunc NewReader(u *url.URL, body io.ReadCloser) (io.Reader, error) {\n\tb, err := io.ReadAll(body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar rawURL string\n\n\tre := regexp.MustCompile(`#EXT-X-STREAM-INF.+?\\n(\\S+)`)\n\tm := re.FindSubmatch(b)\n\tif m != nil {\n\t\tref, err := url.Parse(string(m[1]))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\trawURL = u.ResolveReference(ref).String()\n\t} else {\n\t\trawURL = u.String()\n\t}\n\n\treq, err := http.NewRequest(\"GET\", rawURL, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trd := &reader{\n\t\tclient:  &http.Client{Timeout: core.ConnDialTimeout},\n\t\trequest: req,\n\t}\n\treturn rd, nil\n}\n\nfunc (r *reader) Read(dst []byte) (n int, err error) {\n\t// 1. Check temporary tempbuffer\n\tif len(r.buf) == 0 {\n\t\tsrc, err2 := r.getSegment()\n\t\tif err2 != nil {\n\t\t\treturn 0, err2\n\t\t}\n\n\t\t// 2. Check if the message fits in the buffer\n\t\tif len(src) <= len(dst) {\n\t\t\treturn copy(dst, src), nil\n\t\t}\n\n\t\t// 3. Put the message into a temporary buffer\n\t\tr.buf = src\n\t}\n\n\t// 4. Send temporary buffer\n\tn = copy(dst, r.buf)\n\tr.buf = r.buf[n:]\n\treturn\n}\n\nfunc (r *reader) Close() error {\n\tr.client.Transport = r // after close we fail on next request\n\treturn nil\n}\n\nfunc (r *reader) RoundTrip(_ *http.Request) (*http.Response, error) {\n\treturn nil, io.EOF\n}\n\nfunc (r *reader) getSegment() ([]byte, error) {\n\tfor i := 0; i < 10; i++ {\n\t\tif r.playlist == nil {\n\t\t\tif wait := time.Second - time.Since(r.lastTime); wait > 0 {\n\t\t\t\ttime.Sleep(wait)\n\t\t\t}\n\n\t\t\t// 1. Load playlist\n\t\t\tres, err := r.client.Do(r.request)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tr.playlist, err = io.ReadAll(res.Body)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tr.lastTime = time.Now()\n\n\t\t\t//log.Printf(\"[hls] load playlist\\n%s\", r.playlist)\n\t\t}\n\n\t\tfor r.playlist != nil {\n\t\t\t// 2. Remove all previous segments from playlist\n\t\t\tif i := bytes.Index(r.playlist, r.lastSegment); i > 0 {\n\t\t\t\tr.playlist = r.playlist[i:]\n\t\t\t}\n\n\t\t\t// 3. Get link to new segment\n\t\t\tsegment := getSegment(r.playlist)\n\t\t\tif segment == nil {\n\t\t\t\tr.playlist = nil\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\t//log.Printf(\"[hls] load segment: %s\", segment)\n\n\t\t\tref, err := url.Parse(string(segment))\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tref = r.request.URL.ResolveReference(ref)\n\t\t\tres, err := r.client.Get(ref.String())\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tr.lastSegment = segment\n\n\t\t\treturn io.ReadAll(res.Body)\n\t\t}\n\t}\n\n\treturn nil, io.EOF\n}\n\nfunc getSegment(src []byte) []byte {\n\tfor ok := false; !ok; {\n\t\tok = bytes.HasPrefix(src, []byte(\"#EXTINF\"))\n\n\t\ti := bytes.IndexByte(src, '\\n') + 1\n\t\tif i == 0 {\n\t\t\treturn nil\n\t\t}\n\n\t\tsrc = src[i:]\n\t}\n\n\tif i := bytes.IndexByte(src, '\\n'); i > 0 {\n\t\treturn src[:i]\n\t}\n\n\treturn src\n}\n"
  },
  {
    "path": "pkg/homekit/consumer.go",
    "content": "package homekit\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"math/rand\"\n\t\"net\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hap/camera\"\n\t\"github.com/AlexxIT/go2rtc/pkg/opus\"\n\t\"github.com/AlexxIT/go2rtc/pkg/srtp\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Consumer struct {\n\tcore.Connection\n\tconn net.Conn\n\tsrtp *srtp.Server\n\n\tdeadline *time.Timer\n\n\tsessionID    string\n\tvideoSession *srtp.Session\n\taudioSession *srtp.Session\n\taudioRTPTime byte\n}\n\nfunc NewConsumer(conn net.Conn, server *srtp.Server) *Consumer {\n\tmedias := []*core.Media{\n\t\t{\n\t\t\tKind:      core.KindVideo,\n\t\t\tDirection: core.DirectionSendonly,\n\t\t\tCodecs: []*core.Codec{\n\t\t\t\t{Name: core.CodecH264},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tKind:      core.KindAudio,\n\t\t\tDirection: core.DirectionSendonly,\n\t\t\tCodecs: []*core.Codec{\n\t\t\t\t{Name: core.CodecOpus},\n\t\t\t},\n\t\t},\n\t}\n\treturn &Consumer{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"homekit\",\n\t\t\tProtocol:   \"rtp\",\n\t\t\tRemoteAddr: conn.RemoteAddr().String(),\n\t\t\tMedias:     medias,\n\t\t\tTransport:  conn,\n\t\t},\n\t\tconn: conn,\n\t\tsrtp: server,\n\t}\n}\n\nfunc (c *Consumer) SessionID() string {\n\treturn c.sessionID\n}\n\nfunc (c *Consumer) SetOffer(offer *camera.SetupEndpointsRequest) {\n\tc.sessionID = offer.SessionID\n\tc.videoSession = &srtp.Session{\n\t\tRemote: &srtp.Endpoint{\n\t\t\tAddr:       offer.Address.IPAddr,\n\t\t\tPort:       offer.Address.VideoRTPPort,\n\t\t\tMasterKey:  []byte(offer.VideoCrypto.MasterKey),\n\t\t\tMasterSalt: []byte(offer.VideoCrypto.MasterSalt),\n\t\t},\n\t}\n\tc.audioSession = &srtp.Session{\n\t\tRemote: &srtp.Endpoint{\n\t\t\tAddr:       offer.Address.IPAddr,\n\t\t\tPort:       offer.Address.AudioRTPPort,\n\t\t\tMasterKey:  []byte(offer.AudioCrypto.MasterKey),\n\t\t\tMasterSalt: []byte(offer.AudioCrypto.MasterSalt),\n\t\t},\n\t}\n}\n\nfunc (c *Consumer) GetAnswer() *camera.SetupEndpointsResponse {\n\tc.videoSession.Local = c.srtpEndpoint()\n\tc.audioSession.Local = c.srtpEndpoint()\n\n\treturn &camera.SetupEndpointsResponse{\n\t\tSessionID: c.sessionID,\n\t\tStatus:    camera.StreamingStatusAvailable,\n\t\tAddress: camera.Address{\n\t\t\tIPAddr:       c.videoSession.Local.Addr,\n\t\t\tVideoRTPPort: c.videoSession.Local.Port,\n\t\t\tAudioRTPPort: c.audioSession.Local.Port,\n\t\t},\n\t\tVideoCrypto: camera.SRTPCryptoSuite{\n\t\t\tMasterKey:  string(c.videoSession.Local.MasterKey),\n\t\t\tMasterSalt: string(c.videoSession.Local.MasterSalt),\n\t\t},\n\t\tAudioCrypto: camera.SRTPCryptoSuite{\n\t\t\tMasterKey:  string(c.audioSession.Local.MasterKey),\n\t\t\tMasterSalt: string(c.audioSession.Local.MasterSalt),\n\t\t},\n\t\tVideoSSRC: c.videoSession.Local.SSRC,\n\t\tAudioSSRC: c.audioSession.Local.SSRC,\n\t}\n}\n\nfunc (c *Consumer) SetConfig(conf *camera.SelectedStreamConfiguration) bool {\n\tif c.sessionID != conf.Control.SessionID {\n\t\treturn false\n\t}\n\n\tc.SDP = fmt.Sprintf(\"%+v\\n%+v\", conf.VideoCodec, conf.AudioCodec)\n\n\tc.videoSession.Remote.SSRC = conf.VideoCodec.RTPParams[0].SSRC\n\tc.videoSession.PayloadType = conf.VideoCodec.RTPParams[0].PayloadType\n\tc.videoSession.RTCPInterval = toDuration(conf.VideoCodec.RTPParams[0].RTCPInterval)\n\n\tc.audioSession.Remote.SSRC = conf.AudioCodec.RTPParams[0].SSRC\n\tc.audioSession.PayloadType = conf.AudioCodec.RTPParams[0].PayloadType\n\tc.audioSession.RTCPInterval = toDuration(conf.AudioCodec.RTPParams[0].RTCPInterval)\n\tc.audioRTPTime = conf.AudioCodec.CodecParams[0].RTPTime[0]\n\n\tc.srtp.AddSession(c.videoSession)\n\tc.srtp.AddSession(c.audioSession)\n\n\treturn true\n}\n\nfunc (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {\n\tvar session *srtp.Session\n\tif codec.Kind() == core.KindVideo {\n\t\tsession = c.videoSession\n\t} else {\n\t\tsession = c.audioSession\n\t}\n\n\tsender := core.NewSender(media, track.Codec)\n\n\tif c.deadline == nil {\n\t\tc.deadline = time.NewTimer(time.Second * 30)\n\n\t\tsender.Handler = func(packet *rtp.Packet) {\n\t\t\tc.deadline.Reset(core.ConnDeadline)\n\t\t\tif n, err := session.WriteRTP(packet); err == nil {\n\t\t\t\tc.Send += n\n\t\t\t}\n\t\t}\n\t} else {\n\t\tsender.Handler = func(packet *rtp.Packet) {\n\t\t\tif n, err := session.WriteRTP(packet); err == nil {\n\t\t\t\tc.Send += n\n\t\t\t}\n\t\t}\n\t}\n\n\tswitch codec.Name {\n\tcase core.CodecH264:\n\t\tsender.Handler = h264.RTPPay(1378, sender.Handler)\n\t\tif track.Codec.IsRTP() {\n\t\t\tsender.Handler = h264.RTPDepay(track.Codec, sender.Handler)\n\t\t} else {\n\t\t\tsender.Handler = h264.RepairAVCC(track.Codec, sender.Handler)\n\t\t}\n\tcase core.CodecOpus:\n\t\tsender.Handler = opus.RepackToHAP(c.audioRTPTime, sender.Handler)\n\t}\n\n\tsender.HandleRTP(track)\n\tc.Senders = append(c.Senders, sender)\n\treturn nil\n}\n\nfunc (c *Consumer) WriteTo(io.Writer) (int64, error) {\n\tif c.deadline != nil {\n\t\t<-c.deadline.C\n\t}\n\treturn 0, nil\n}\n\nfunc (c *Consumer) Stop() error {\n\tif c.deadline != nil {\n\t\tc.deadline.Reset(0)\n\t}\n\treturn c.Connection.Stop()\n}\n\nfunc (c *Consumer) srtpEndpoint() *srtp.Endpoint {\n\taddr := c.conn.LocalAddr().(*net.TCPAddr)\n\treturn &srtp.Endpoint{\n\t\tAddr:       addr.IP.To4().String(),\n\t\tPort:       uint16(c.srtp.Port()),\n\t\tMasterKey:  []byte(core.RandString(16, 0)),\n\t\tMasterSalt: []byte(core.RandString(14, 0)),\n\t\tSSRC:       rand.Uint32(),\n\t}\n}\n\nfunc toDuration(seconds float32) time.Duration {\n\treturn time.Duration(seconds * float32(time.Second))\n}\n"
  },
  {
    "path": "pkg/homekit/helpers.go",
    "content": "package homekit\n\nimport (\n\t\"encoding/hex\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/aac\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hap/camera\"\n)\n\nvar videoCodecs = [...]string{core.CodecH264}\nvar videoProfiles = [...]string{\"4200\", \"4D00\", \"6400\"}\nvar videoLevels = [...]string{\"1F\", \"20\", \"28\"}\n\nfunc videoToMedia(codecs []camera.VideoCodecConfiguration) *core.Media {\n\tmedia := &core.Media{\n\t\tKind: core.KindVideo, Direction: core.DirectionRecvonly,\n\t}\n\n\tfor _, codec := range codecs {\n\t\tfor _, param := range codec.CodecParams {\n\t\t\t// get best profile and level\n\t\t\tprofileID := core.Max(param.ProfileID)\n\t\t\tlevel := core.Max(param.Level)\n\t\t\tprofile := videoProfiles[profileID] + videoLevels[level]\n\t\t\tmediaCodec := &core.Codec{\n\t\t\t\tName:      videoCodecs[codec.CodecType],\n\t\t\t\tClockRate: 90000,\n\t\t\t\tFmtpLine:  \"profile-level-id=\" + profile,\n\t\t\t}\n\t\t\tmedia.Codecs = append(media.Codecs, mediaCodec)\n\t\t}\n\t}\n\n\treturn media\n}\n\nvar audioCodecs = [...]string{core.CodecPCMU, core.CodecPCMA, core.CodecELD, core.CodecOpus}\nvar audioSampleRates = [...]uint32{8000, 16000, 24000}\n\nfunc audioToMedia(codecs []camera.AudioCodecConfiguration) *core.Media {\n\tmedia := &core.Media{\n\t\tKind: core.KindAudio, Direction: core.DirectionRecvonly,\n\t}\n\n\tfor _, codec := range codecs {\n\t\tfor _, param := range codec.CodecParams {\n\t\t\tfor _, sampleRate := range param.SampleRate {\n\t\t\t\tmediaCodec := &core.Codec{\n\t\t\t\t\tName:      audioCodecs[codec.CodecType],\n\t\t\t\t\tClockRate: audioSampleRates[sampleRate],\n\t\t\t\t\tChannels:  param.Channels,\n\t\t\t\t}\n\n\t\t\t\tif mediaCodec.Name == core.CodecELD {\n\t\t\t\t\t// only this version works with FFmpeg\n\t\t\t\t\tconf := aac.EncodeConfig(aac.TypeAACELD, 24000, 1, true)\n\t\t\t\t\tmediaCodec.FmtpLine = aac.FMTP + hex.EncodeToString(conf)\n\t\t\t\t}\n\n\t\t\t\tmedia.Codecs = append(media.Codecs, mediaCodec)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn media\n}\n\nfunc trackToVideo(track *core.Receiver, video0 *camera.VideoCodecConfiguration, maxWidth, maxHeight int) *camera.VideoCodecConfiguration {\n\tprofileID := video0.CodecParams[0].ProfileID[0]\n\tlevel := video0.CodecParams[0].Level[0]\n\tvar attrs camera.VideoCodecAttributes\n\n\tif track != nil {\n\t\tprofile := h264.GetProfileLevelID(track.Codec.FmtpLine)\n\n\t\tfor i, s := range videoProfiles {\n\t\t\tif s == profile[:4] {\n\t\t\t\tprofileID = byte(i)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tfor i, s := range videoLevels {\n\t\t\tif s == profile[4:] {\n\t\t\t\tlevel = byte(i)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tfor _, s := range video0.VideoAttrs {\n\t\t\tif (maxWidth > 0 && int(s.Width) > maxWidth) || (maxHeight > 0 && int(s.Height) > maxHeight) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif s.Width > attrs.Width || s.Height > attrs.Height {\n\t\t\t\tattrs = s\n\t\t\t}\n\t\t}\n\t}\n\n\treturn &camera.VideoCodecConfiguration{\n\t\tCodecType: video0.CodecType,\n\t\tCodecParams: []camera.VideoCodecParameters{\n\t\t\t{\n\t\t\t\tProfileID: []byte{profileID},\n\t\t\t\tLevel:     []byte{level},\n\t\t\t},\n\t\t},\n\t\tVideoAttrs: []camera.VideoCodecAttributes{attrs},\n\t}\n}\n\nfunc trackToAudio(track *core.Receiver, audio0 *camera.AudioCodecConfiguration) *camera.AudioCodecConfiguration {\n\tcodecType := audio0.CodecType\n\tchannels := audio0.CodecParams[0].Channels\n\tsampleRate := audio0.CodecParams[0].SampleRate[0]\n\n\tif track != nil {\n\t\tchannels = uint8(track.Codec.Channels)\n\n\t\tfor i, s := range audioCodecs {\n\t\t\tif s == track.Codec.Name {\n\t\t\t\tcodecType = byte(i)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tfor i, s := range audioSampleRates {\n\t\t\tif s == track.Codec.ClockRate {\n\t\t\t\tsampleRate = byte(i)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\treturn &camera.AudioCodecConfiguration{\n\t\tCodecType: codecType,\n\t\tCodecParams: []camera.AudioCodecParameters{\n\t\t\t{\n\t\t\t\tChannels:   channels,\n\t\t\t\tSampleRate: []byte{sampleRate},\n\t\t\t\tRTPTime:    []uint8{20},\n\t\t\t},\n\t\t},\n\t}\n}\n"
  },
  {
    "path": "pkg/homekit/log/debug.go",
    "content": "package log\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"log\"\n\t\"net/http\"\n)\n\nfunc Debug(v any) {\n\tswitch v := v.(type) {\n\tcase *http.Request:\n\t\tif v == nil {\n\t\t\treturn\n\t\t}\n\t\tif v.ContentLength != 0 {\n\t\t\tb, err := io.ReadAll(v.Body)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\tv.Body = io.NopCloser(bytes.NewReader(b))\n\t\t\tlog.Printf(\"[homekit] request: %s %s\\n%s\", v.Method, v.RequestURI, b)\n\t\t} else {\n\t\t\tlog.Printf(\"[homekit] request: %s %s <nobody>\", v.Method, v.RequestURI)\n\t\t}\n\tcase *http.Response:\n\t\tif v == nil {\n\t\t\treturn\n\t\t}\n\t\tif v.Header.Get(\"Content-Type\") == \"image/jpeg\" {\n\t\t\tlog.Printf(\"[homekit] response: %d <jpeg>\", v.StatusCode)\n\t\t\treturn\n\t\t}\n\t\tif v.ContentLength != 0 {\n\t\t\tb, err := io.ReadAll(v.Body)\n\t\t\tif err != nil {\n\t\t\t\tpanic(err)\n\t\t\t}\n\t\t\tv.Body = io.NopCloser(bytes.NewReader(b))\n\t\t\tlog.Printf(\"[homekit] response: %s %d\\n%s\", v.Proto, v.StatusCode, b)\n\t\t} else {\n\t\t\tlog.Printf(\"[homekit] response: %s %d <nobody>\", v.Proto, v.StatusCode)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/homekit/producer.go",
    "content": "package homekit\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"net\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hap\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hap/camera\"\n\t\"github.com/AlexxIT/go2rtc/pkg/srtp\"\n\t\"github.com/pion/rtp\"\n)\n\n// Deprecated: rename to Producer\ntype Client struct {\n\tcore.Connection\n\n\thap  *hap.Client\n\tsrtp *srtp.Server\n\n\tvideoConfig camera.SupportedVideoStreamConfiguration\n\taudioConfig camera.SupportedAudioStreamConfiguration\n\n\tvideoSession *srtp.Session\n\taudioSession *srtp.Session\n\n\tstream *camera.Stream\n\n\tMaxWidth  int `json:\"-\"`\n\tMaxHeight int `json:\"-\"`\n\tBitrate   int `json:\"-\"` // in bits/s\n}\n\nfunc Dial(rawURL string, server *srtp.Server) (*Client, error) {\n\tconn, err := hap.Dial(rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient := &Client{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"homekit\",\n\t\t\tProtocol:   \"udp\",\n\t\t\tRemoteAddr: conn.Conn.RemoteAddr().String(),\n\t\t\tSource:     rawURL,\n\t\t\tTransport:  conn,\n\t\t},\n\t\thap:  conn,\n\t\tsrtp: server,\n\t}\n\n\treturn client, nil\n}\n\nfunc (c *Client) Conn() net.Conn {\n\treturn c.hap.Conn\n}\n\nfunc (c *Client) GetMedias() []*core.Media {\n\tif c.Medias != nil {\n\t\treturn c.Medias\n\t}\n\n\tacc, err := c.hap.GetFirstAccessory()\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tchar := acc.GetCharacter(camera.TypeSupportedVideoStreamConfiguration)\n\tif char == nil {\n\t\treturn nil\n\t}\n\tif err = char.ReadTLV8(&c.videoConfig); err != nil {\n\t\treturn nil\n\t}\n\n\tchar = acc.GetCharacter(camera.TypeSupportedAudioStreamConfiguration)\n\tif char == nil {\n\t\treturn nil\n\t}\n\tif err = char.ReadTLV8(&c.audioConfig); err != nil {\n\t\treturn nil\n\t}\n\n\tc.SDP = fmt.Sprintf(\"%+v\\n%+v\", c.videoConfig, c.audioConfig)\n\n\tc.Medias = []*core.Media{\n\t\tvideoToMedia(c.videoConfig.Codecs),\n\t\taudioToMedia(c.audioConfig.Codecs),\n\t\t{\n\t\t\tKind:      core.KindVideo,\n\t\t\tDirection: core.DirectionRecvonly,\n\t\t\tCodecs: []*core.Codec{\n\t\t\t\t{\n\t\t\t\t\tName:        core.CodecJPEG,\n\t\t\t\t\tClockRate:   90000,\n\t\t\t\t\tPayloadType: core.PayloadTypeRAW,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\treturn c.Medias\n}\n\nfunc (c *Client) Start() error {\n\tif c.Receivers == nil {\n\t\treturn errors.New(\"producer without tracks\")\n\t}\n\n\tif c.Receivers[0].Codec.Name == core.CodecJPEG {\n\t\treturn c.startMJPEG()\n\t}\n\n\tvideoTrack := c.trackByKind(core.KindVideo)\n\tvideoCodec := trackToVideo(videoTrack, &c.videoConfig.Codecs[0], c.MaxWidth, c.MaxHeight)\n\n\taudioTrack := c.trackByKind(core.KindAudio)\n\taudioCodec := trackToAudio(audioTrack, &c.audioConfig.Codecs[0])\n\n\tc.videoSession = &srtp.Session{Local: c.srtpEndpoint()}\n\tc.audioSession = &srtp.Session{Local: c.srtpEndpoint()}\n\n\tvar err error\n\tc.stream, err = camera.NewStream(c.hap, videoCodec, audioCodec, c.videoSession, c.audioSession, c.Bitrate)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tc.srtp.AddSession(c.videoSession)\n\tc.srtp.AddSession(c.audioSession)\n\n\tdeadline := time.NewTimer(core.ConnDeadline)\n\n\tif videoTrack != nil {\n\t\tc.videoSession.OnReadRTP = func(packet *rtp.Packet) {\n\t\t\tdeadline.Reset(core.ConnDeadline)\n\t\t\tvideoTrack.WriteRTP(packet)\n\t\t\tc.Recv += len(packet.Payload)\n\t\t}\n\n\t\tif audioTrack != nil {\n\t\t\tc.audioSession.OnReadRTP = func(packet *rtp.Packet) {\n\t\t\t\taudioTrack.WriteRTP(packet)\n\t\t\t\tc.Recv += len(packet.Payload)\n\t\t\t}\n\t\t}\n\t} else {\n\t\tc.audioSession.OnReadRTP = func(packet *rtp.Packet) {\n\t\t\tdeadline.Reset(core.ConnDeadline)\n\t\t\taudioTrack.WriteRTP(packet)\n\t\t\tc.Recv += len(packet.Payload)\n\t\t}\n\t}\n\n\tif c.audioSession.OnReadRTP != nil {\n\t\tc.audioSession.OnReadRTP = timekeeper(c.audioSession.OnReadRTP)\n\t}\n\n\t<-deadline.C\n\n\treturn nil\n}\n\nfunc (c *Client) Stop() error {\n\tif c.videoSession != nil && c.videoSession.Remote != nil {\n\t\tc.srtp.DelSession(c.videoSession)\n\t}\n\tif c.audioSession != nil && c.audioSession.Remote != nil {\n\t\tc.srtp.DelSession(c.audioSession)\n\t}\n\n\treturn c.Connection.Stop()\n}\n\nfunc (c *Client) trackByKind(kind string) *core.Receiver {\n\tfor _, receiver := range c.Receivers {\n\t\tif receiver.Codec.Kind() == kind {\n\t\t\treturn receiver\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (c *Client) startMJPEG() error {\n\treceiver := c.Receivers[0]\n\n\tfor {\n\t\tb, err := c.hap.GetImage(1920, 1080)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tc.Recv += len(b)\n\n\t\tpacket := &rtp.Packet{\n\t\t\tHeader:  rtp.Header{Timestamp: core.Now90000()},\n\t\t\tPayload: b,\n\t\t}\n\t\treceiver.WriteRTP(packet)\n\t}\n}\n\nfunc (c *Client) srtpEndpoint() *srtp.Endpoint {\n\treturn &srtp.Endpoint{\n\t\tAddr:       c.hap.LocalIP(),\n\t\tPort:       uint16(c.srtp.Port()),\n\t\tMasterKey:  []byte(core.RandString(16, 0)),\n\t\tMasterSalt: []byte(core.RandString(14, 0)),\n\t\tSSRC:       rand.Uint32(),\n\t}\n}\n\nfunc timekeeper(handler core.HandlerFunc) core.HandlerFunc {\n\tconst sampleRate = 16000\n\tconst sampleSize = 480\n\n\tvar send time.Duration\n\tvar firstTime time.Time\n\n\treturn func(packet *rtp.Packet) {\n\t\tnow := time.Now()\n\n\t\tif send != 0 {\n\t\t\telapsed := now.Sub(firstTime) * sampleRate / time.Second\n\t\t\tif send+sampleSize > elapsed {\n\t\t\t\treturn // drop overflow frame\n\t\t\t}\n\t\t} else {\n\t\t\tfirstTime = now\n\t\t}\n\n\t\tsend += sampleSize\n\n\t\tpacket.Timestamp = uint32(send)\n\n\t\thandler(packet)\n\t}\n}\n"
  },
  {
    "path": "pkg/homekit/proxy.go",
    "content": "package homekit\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/hap\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hap/camera\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hap/hds\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hap/tlv8\"\n)\n\ntype ServerProxy interface {\n\tServerPair\n\tAddConn(conn any)\n\tDelConn(conn any)\n}\n\nfunc ProxyHandler(srv ServerProxy, acc net.Conn) HandlerFunc {\n\treturn func(con net.Conn) error {\n\t\tdefer con.Close()\n\n\t\tpr := &Proxy{\n\t\t\tcon: con.(*hap.Conn),\n\t\t\tacc: acc.(*hap.Conn),\n\t\t\tres: make(chan *http.Response),\n\t\t}\n\n\t\t// accessory (ex. Camera) => controller (ex. iPhone)\n\t\tgo pr.handleAcc()\n\n\t\t// controller => accessory\n\t\treturn pr.handleCon(srv)\n\t}\n}\n\ntype Proxy struct {\n\tcon *hap.Conn\n\tacc *hap.Conn\n\tres chan *http.Response\n}\n\nfunc (p *Proxy) handleCon(srv ServerProxy) error {\n\tvar hdsCharIID uint64\n\n\trd := bufio.NewReader(p.con)\n\tfor {\n\t\treq, err := http.ReadRequest(rd)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tvar hdsConSalt string\n\n\t\tswitch {\n\t\tcase req.Method == \"POST\" && req.URL.Path == hap.PathPairings:\n\t\t\tvar res *http.Response\n\t\t\tif res, err = handlePairings(req, srv); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err = res.Write(p.con); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tcontinue\n\t\tcase req.Method == \"PUT\" && req.URL.Path == hap.PathCharacteristics && hdsCharIID != 0:\n\t\t\tbody, _ := io.ReadAll(req.Body)\n\t\t\tvar v hap.JSONCharacters\n\t\t\t_ = json.Unmarshal(body, &v)\n\t\t\tfor _, char := range v.Value {\n\t\t\t\tif char.IID == hdsCharIID {\n\t\t\t\t\tvar hdsReq camera.SetupDataStreamTransportRequest\n\t\t\t\t\t_ = tlv8.UnmarshalBase64(char.Value, &hdsReq)\n\t\t\t\t\thdsConSalt = hdsReq.ControllerKeySalt\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\treq.Body = io.NopCloser(bytes.NewReader(body))\n\t\t}\n\n\t\tif err = req.Write(p.acc); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tres := <-p.res\n\n\t\tswitch {\n\t\tcase req.Method == \"GET\" && req.URL.Path == hap.PathAccessories:\n\t\t\tbody, _ := io.ReadAll(res.Body)\n\t\t\tvar v hap.JSONAccessories\n\t\t\tif err = json.Unmarshal(body, &v); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tfor _, acc := range v.Value {\n\t\t\t\tif char := acc.GetCharacter(camera.TypeSetupDataStreamTransport); char != nil {\n\t\t\t\t\thdsCharIID = char.IID\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tres.Body = io.NopCloser(bytes.NewReader(body))\n\n\t\tcase hdsConSalt != \"\":\n\t\t\tbody, _ := io.ReadAll(res.Body)\n\t\t\tvar v hap.JSONCharacters\n\t\t\t_ = json.Unmarshal(body, &v)\n\t\t\tfor i, char := range v.Value {\n\t\t\t\tif char.IID == hdsCharIID {\n\t\t\t\t\tvar hdsRes camera.SetupDataStreamTransportResponse\n\t\t\t\t\t_ = tlv8.UnmarshalBase64(char.Value, &hdsRes)\n\n\t\t\t\t\thdsAccSalt := hdsRes.AccessoryKeySalt\n\t\t\t\t\thdsPort := int(hdsRes.TransportTypeSessionParameters.TCPListeningPort)\n\n\t\t\t\t\t// swtich accPort to conPort\n\t\t\t\t\thdsPort, err = p.listenHDS(srv, hdsPort, hdsConSalt+hdsAccSalt)\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\thdsRes.TransportTypeSessionParameters.TCPListeningPort = uint16(hdsPort)\n\t\t\t\t\tif v.Value[i].Value, err = tlv8.MarshalBase64(hdsRes); err != nil {\n\t\t\t\t\t\treturn err\n\t\t\t\t\t}\n\t\t\t\t\tbody, _ = json.Marshal(v)\n\t\t\t\t\tres.ContentLength = int64(len(body))\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t\tres.Body = io.NopCloser(bytes.NewReader(body))\n\t\t}\n\n\t\tif err = res.Write(p.con); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n}\n\nfunc (p *Proxy) handleAcc() error {\n\trd := bufio.NewReader(p.acc)\n\tfor {\n\t\tres, err := hap.ReadResponse(rd, nil)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif res.Proto == hap.ProtoEvent {\n\t\t\tif err = hap.WriteEvent(p.con, res); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\t// important to read body before next read response\n\t\tbody, err := io.ReadAll(res.Body)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tres.Body = io.NopCloser(bytes.NewReader(body))\n\n\t\tp.res <- res\n\t}\n}\n\nfunc (p *Proxy) listenHDS(srv ServerProxy, accPort int, salt string) (int, error) {\n\t// The TCP port range for HDS must be >= 32768.\n\tln, err := net.ListenTCP(\"tcp\", nil)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tgo func() {\n\t\tdefer ln.Close()\n\n\t\t_ = ln.SetDeadline(time.Now().Add(30 * time.Second))\n\n\t\t// raw controller conn\n\t\tconn1, err := ln.Accept()\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\n\t\tdefer conn1.Close()\n\n\t\t// secured controller conn (controlle=false because we are accessory)\n\t\tcon, err := hds.NewConn(conn1, p.con.SharedKey, salt, false)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\n\t\tsrv.AddConn(con)\n\t\tdefer srv.DelConn(con)\n\n\t\taccIP := p.acc.RemoteAddr().(*net.TCPAddr).IP\n\n\t\t// raw accessory conn\n\t\tconn2, err := net.DialTCP(\"tcp\", nil, &net.TCPAddr{IP: accIP, Port: accPort})\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\t\tdefer conn2.Close()\n\n\t\t// secured accessory conn (controller=true because we are controller)\n\t\tacc, err := hds.NewConn(conn2, p.acc.SharedKey, salt, true)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\n\t\tgo io.Copy(con, acc)\n\t\t_, _ = io.Copy(acc, con)\n\t}()\n\n\tconPort := ln.Addr().(*net.TCPAddr).Port\n\treturn conPort, nil\n}\n"
  },
  {
    "path": "pkg/homekit/server.go",
    "content": "package homekit\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/hap\"\n\t\"github.com/AlexxIT/go2rtc/pkg/hap/tlv8\"\n)\n\ntype HandlerFunc func(net.Conn) error\n\ntype Server interface {\n\tServerPair\n\tServerAccessory\n}\n\ntype ServerPair interface {\n\tGetPair(id string) []byte\n\tAddPair(id string, public []byte, permissions byte)\n\tDelPair(id string)\n}\n\ntype ServerAccessory interface {\n\tGetAccessories(conn net.Conn) []*hap.Accessory\n\tGetCharacteristic(conn net.Conn, aid uint8, iid uint64) any\n\tSetCharacteristic(conn net.Conn, aid uint8, iid uint64, value any)\n\tGetImage(conn net.Conn, width, height int) []byte\n}\n\nfunc ServerHandler(server Server) HandlerFunc {\n\treturn handleRequest(func(conn net.Conn, req *http.Request) (*http.Response, error) {\n\t\tswitch req.URL.Path {\n\t\tcase hap.PathPairings:\n\t\t\treturn handlePairings(req, server)\n\n\t\tcase hap.PathAccessories:\n\t\t\tbody := hap.JSONAccessories{Value: server.GetAccessories(conn)}\n\t\t\treturn makeResponse(hap.MimeJSON, body)\n\n\t\tcase hap.PathCharacteristics:\n\t\t\tswitch req.Method {\n\t\t\tcase \"GET\":\n\t\t\t\tvar v hap.JSONCharacters\n\n\t\t\t\tid := req.URL.Query().Get(\"id\")\n\t\t\t\tfor _, id = range strings.Split(id, \",\") {\n\t\t\t\t\ts1, s2, _ := strings.Cut(id, \".\")\n\t\t\t\t\taid, _ := strconv.Atoi(s1)\n\t\t\t\t\tiid, _ := strconv.ParseUint(s2, 10, 64)\n\t\t\t\t\tval := server.GetCharacteristic(conn, uint8(aid), iid)\n\n\t\t\t\t\tv.Value = append(v.Value, hap.JSONCharacter{AID: uint8(aid), IID: iid, Value: val})\n\t\t\t\t}\n\n\t\t\t\treturn makeResponse(hap.MimeJSON, v)\n\n\t\t\tcase \"PUT\":\n\t\t\t\tvar v struct {\n\t\t\t\t\tValue []struct {\n\t\t\t\t\t\tAID   uint8  `json:\"aid\"`\n\t\t\t\t\t\tIID   uint64 `json:\"iid\"`\n\t\t\t\t\t\tValue any    `json:\"value\"`\n\t\t\t\t\t} `json:\"characteristics\"`\n\t\t\t\t}\n\t\t\t\tif err := json.NewDecoder(req.Body).Decode(&v); err != nil {\n\t\t\t\t\treturn nil, err\n\t\t\t\t}\n\n\t\t\t\tfor _, char := range v.Value {\n\t\t\t\t\tserver.SetCharacteristic(conn, char.AID, char.IID, char.Value)\n\t\t\t\t}\n\n\t\t\t\tres := &http.Response{\n\t\t\t\t\tStatusCode: http.StatusNoContent,\n\t\t\t\t\tProto:      \"HTTP\",\n\t\t\t\t\tProtoMajor: 1,\n\t\t\t\t\tProtoMinor: 1,\n\t\t\t\t}\n\t\t\t\treturn res, nil\n\t\t\t}\n\n\t\tcase hap.PathResource:\n\t\t\tvar v struct {\n\t\t\t\tWidth  int    `json:\"image-width\"`\n\t\t\t\tHeight int    `json:\"image-height\"`\n\t\t\t\tType   string `json:\"resource-type\"`\n\t\t\t}\n\t\t\tif err := json.NewDecoder(req.Body).Decode(&v); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tbody := server.GetImage(conn, v.Width, v.Height)\n\t\t\treturn makeResponse(\"image/jpeg\", body)\n\t\t}\n\n\t\treturn nil, errors.New(\"hap: unsupported path: \" + req.RequestURI)\n\t})\n}\n\nfunc handleRequest(handle func(conn net.Conn, req *http.Request) (*http.Response, error)) HandlerFunc {\n\treturn func(conn net.Conn) error {\n\t\trw := bufio.NewReaderSize(conn, 16*1024)\n\t\twr := bufio.NewWriterSize(conn, 16*1024)\n\t\tfor {\n\t\t\treq, err := http.ReadRequest(rw)\n\t\t\t//debug(req)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tres, err := handle(conn, req)\n\t\t\t//debug(res)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif err = res.Write(wr); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif err = wr.Flush(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc handlePairings(req *http.Request, srv ServerPair) (*http.Response, error) {\n\tcmd := struct {\n\t\tMethod      byte   `tlv8:\"0\"`\n\t\tIdentifier  string `tlv8:\"1\"`\n\t\tPublicKey   string `tlv8:\"3\"`\n\t\tState       byte   `tlv8:\"6\"`\n\t\tPermissions byte   `tlv8:\"11\"`\n\t}{}\n\n\tif err := tlv8.UnmarshalReader(req.Body, req.ContentLength, &cmd); err != nil {\n\t\treturn nil, err\n\t}\n\n\tswitch cmd.Method {\n\tcase 3: // add\n\t\tsrv.AddPair(cmd.Identifier, []byte(cmd.PublicKey), cmd.Permissions)\n\tcase 4: // delete\n\t\tsrv.DelPair(cmd.Identifier)\n\t}\n\n\tbody := struct {\n\t\tState byte `tlv8:\"6\"`\n\t}{\n\t\tState: hap.StateM2,\n\t}\n\n\treturn makeResponse(hap.MimeTLV8, body)\n}\n\nfunc makeResponse(mime string, v any) (*http.Response, error) {\n\tvar body []byte\n\tvar err error\n\n\tswitch mime {\n\tcase hap.MimeJSON:\n\t\tbody, err = json.Marshal(v)\n\tcase hap.MimeTLV8:\n\t\tbody, err = tlv8.Marshal(v)\n\tcase \"image/jpeg\":\n\t\tbody = v.([]byte)\n\t}\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tres := &http.Response{\n\t\tStatusCode: http.StatusOK,\n\t\tProto:      \"HTTP\",\n\t\tProtoMajor: 1,\n\t\tProtoMinor: 1,\n\t\tHeader: http.Header{\n\t\t\t\"Content-Type\":   []string{mime},\n\t\t\t\"Content-Length\": []string{strconv.Itoa(len(body))},\n\t\t},\n\t\tContentLength: int64(len(body)),\n\t\tBody:          io.NopCloser(bytes.NewReader(body)),\n\t}\n\treturn res, nil\n}\n"
  },
  {
    "path": "pkg/image/producer.go",
    "content": "package image\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/tcp\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Producer struct {\n\tcore.Connection\n\n\tclosed bool\n\tres    *http.Response\n}\n\nfunc Open(res *http.Response) (*Producer, error) {\n\treturn &Producer{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"image\",\n\t\t\tProtocol:   \"http\",\n\t\t\tRemoteAddr: res.Request.URL.Host,\n\t\t\tTransport:  res.Body,\n\t\t\tMedias: []*core.Media{\n\t\t\t\t{\n\t\t\t\t\tKind:      core.KindVideo,\n\t\t\t\t\tDirection: core.DirectionRecvonly,\n\t\t\t\t\tCodecs: []*core.Codec{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:        core.CodecJPEG,\n\t\t\t\t\t\t\tClockRate:   90000,\n\t\t\t\t\t\t\tPayloadType: core.PayloadTypeRAW,\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\tres: res,\n\t}, nil\n}\n\nfunc (c *Producer) Start() error {\n\tbody, err := io.ReadAll(c.res.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpkt := &rtp.Packet{\n\t\tHeader:  rtp.Header{Timestamp: core.Now90000()},\n\t\tPayload: body,\n\t}\n\tc.Receivers[0].WriteRTP(pkt)\n\n\tc.Recv += len(body)\n\n\treq := c.res.Request\n\n\tfor !c.closed {\n\t\tres, err := tcp.Do(req)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif res.StatusCode != http.StatusOK {\n\t\t\treturn errors.New(\"wrong status: \" + res.Status)\n\t\t}\n\n\t\tbody, err = io.ReadAll(res.Body)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tc.Recv += len(body)\n\n\t\tpkt = &rtp.Packet{\n\t\t\tHeader:  rtp.Header{Timestamp: core.Now90000()},\n\t\t\tPayload: body,\n\t\t}\n\t\tc.Receivers[0].WriteRTP(pkt)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Producer) Stop() error {\n\tc.closed = true\n\treturn c.Connection.Stop()\n}\n"
  },
  {
    "path": "pkg/ioctl/README.md",
    "content": "# IOCTL\n\nThis is just an example how Linux IOCTL constants works.\n"
  },
  {
    "path": "pkg/ioctl/ioctl.go",
    "content": "package ioctl\n\nimport (\n\t\"bytes\"\n)\n\nfunc Str(b []byte) string {\n\tif i := bytes.IndexByte(b, 0); i >= 0 {\n\t\treturn string(b[:i])\n\t}\n\treturn string(b)\n}\n\nfunc io(mode byte, type_ byte, number byte, size uint16) uintptr {\n\treturn uintptr(mode)<<30 | uintptr(size)<<16 | uintptr(type_)<<8 | uintptr(number)\n}\n\nfunc IOR(type_ byte, number byte, size uint16) uintptr {\n\treturn io(read, type_, number, size)\n}\n\nfunc IOW(type_ byte, number byte, size uint16) uintptr {\n\treturn io(write, type_, number, size)\n}\n\nfunc IORW(type_ byte, number byte, size uint16) uintptr {\n\treturn io(read|write, type_, number, size)\n}\n"
  },
  {
    "path": "pkg/ioctl/ioctl_be.go",
    "content": "//go:build arm || arm64 || 386 || amd64\n\npackage ioctl\n\nconst (\n\twrite = 1\n\tread  = 2\n)\n"
  },
  {
    "path": "pkg/ioctl/ioctl_le.go",
    "content": "//go:build mipsle\n\npackage ioctl\n\nconst (\n\tread  = 1\n\twrite = 2\n)\n"
  },
  {
    "path": "pkg/ioctl/ioctl_linux.go",
    "content": "package ioctl\n\nimport (\n\t\"syscall\"\n\t\"unsafe\"\n)\n\nfunc Ioctl(fd int, req uint, arg unsafe.Pointer) error {\n\t_, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(arg))\n\tif err != 0 {\n\t\treturn err\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/ioctl/ioctl_test.go",
    "content": "package ioctl\n\nimport (\n\t\"runtime\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestIOR(t *testing.T) {\n\t// #define SNDRV_PCM_IOCTL_INFO\t\t_IOR('A', 0x01, struct snd_pcm_info)\n\tif runtime.GOARCH == \"arm64\" {\n\t\tc := IOR('A', 0x01, 288)\n\t\trequire.Equal(t, uintptr(0x81204101), c)\n\t}\n}\n"
  },
  {
    "path": "pkg/isapi/backchannel.go",
    "content": "package isapi\n\nimport (\n\t\"encoding/json\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/pion/rtp\"\n)\n\nfunc (c *Client) GetMedias() []*core.Media {\n\treturn c.medias\n}\n\nfunc (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {\n\treturn nil, core.ErrCantGetTrack\n}\n\nfunc (c *Client) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {\n\tif c.sender == nil {\n\t\tc.sender = core.NewSender(media, track.Codec)\n\t\tc.sender.Handler = func(packet *rtp.Packet) {\n\t\t\tif c.conn == nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tc.send += len(packet.Payload)\n\t\t\t_, _ = c.conn.Write(packet.Payload)\n\t\t}\n\t}\n\n\tc.sender.HandleRTP(track)\n\treturn nil\n}\n\nfunc (c *Client) Start() (err error) {\n\tif err = c.Open(); err != nil {\n\t\treturn\n\t}\n\treturn\n}\n\nfunc (c *Client) Stop() (err error) {\n\tif c.sender != nil {\n\t\tc.sender.Close()\n\t}\n\n\tif c.conn != nil {\n\t\t_ = c.Close()\n\t\treturn c.conn.Close()\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) MarshalJSON() ([]byte, error) {\n\tinfo := &core.Connection{\n\t\tID:         core.ID(c),\n\t\tFormatName: \"isapi\",\n\t\tProtocol:   \"http\",\n\t\tMedias:     c.medias,\n\t\tSend:       c.send,\n\t}\n\tif c.conn != nil {\n\t\tinfo.RemoteAddr = c.conn.RemoteAddr().String()\n\t}\n\tif c.sender != nil {\n\t\tinfo.Senders = []*core.Sender{c.sender}\n\t}\n\treturn json.Marshal(info)\n}\n"
  },
  {
    "path": "pkg/isapi/client.go",
    "content": "package isapi\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/tcp\"\n)\n\n// Deprecated: should be rewritten to core.Connection\ntype Client struct {\n\tcore.Listener\n\n\turl     string\n\tchannel string\n\tconn    net.Conn\n\n\tmedias []*core.Media\n\tsender *core.Sender\n\tsend   int\n}\n\nfunc Dial(rawURL string) (*Client, error) {\n\t// check if url is valid url\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tu.Scheme = \"http\"\n\tu.Path = \"\"\n\n\tclient := &Client{url: u.String()}\n\tif err = client.Dial(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn client, err\n}\n\nfunc (c *Client) Dial() (err error) {\n\tlink := c.url + \"/ISAPI/System/TwoWayAudio/channels\"\n\treq, err := http.NewRequest(\"GET\", link, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tres, err := tcp.Do(req)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tif res.StatusCode != http.StatusOK {\n\t\ttcp.Close(res)\n\t\treturn errors.New(res.Status)\n\t}\n\n\tb, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\txml := string(b)\n\n\tcodec := core.Between(xml, `<audioCompressionType>`, `<`)\n\tswitch codec {\n\tcase \"G.711ulaw\":\n\t\tcodec = core.CodecPCMU\n\tcase \"G.711alaw\":\n\t\tcodec = core.CodecPCMA\n\tdefault:\n\t\treturn nil\n\t}\n\n\tc.channel = core.Between(xml, `<id>`, `<`)\n\n\tmedia := &core.Media{\n\t\tKind:      core.KindAudio,\n\t\tDirection: core.DirectionSendonly,\n\t\tCodecs: []*core.Codec{\n\t\t\t{Name: codec, ClockRate: 8000},\n\t\t},\n\t}\n\tc.medias = append(c.medias, media)\n\n\treturn nil\n}\n\nfunc (c *Client) Open() (err error) {\n\t// Hikvision ISAPI may not accept a new open request if the previous one was not closed (e.g.\n\t// using the test button on-camera or via curl command) but a close request can be sent even if\n\t// the audio is already closed. So, we send a close request first and then open it again. Seems\n\t// janky but it works.\n\tif err = c.Close(); err != nil {\n\t\treturn err\n\t}\n\n\tlink := c.url + \"/ISAPI/System/TwoWayAudio/channels/\" + c.channel\n\treq, err := http.NewRequest(\"PUT\", link+\"/open\", nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tres, err := tcp.Do(req)\n\tif err != nil {\n\t\treturn\n\t}\n\n\ttcp.Close(res)\n\n\tctx, pconn := tcp.WithConn()\n\treq, err = http.NewRequestWithContext(ctx, \"PUT\", link+\"/audioData\", nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/octet-stream\")\n\treq.Header.Set(\"Content-Length\", \"0\")\n\n\tres, err = tcp.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tc.conn = *pconn\n\n\t// just block until c.conn closed\n\tb := make([]byte, 1)\n\t_, _ = c.conn.Read(b)\n\n\ttcp.Close(res)\n\n\treturn nil\n}\n\nfunc (c *Client) Close() (err error) {\n\tlink := c.url + \"/ISAPI/System/TwoWayAudio/channels/\" + c.channel\n\treq, err := http.NewRequest(\"PUT\", link+\"/close\", nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tres, err := tcp.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttcp.Close(res)\n\n\treturn nil\n}\n\n//type XMLChannels struct {\n//\tChannels []Channel `xml:\"TwoWayAudioChannel\"`\n//}\n\n//type Channel struct {\n//\tID      string `xml:\"id\"`\n//\tEnabled string `xml:\"enabled\"`\n//\tCodec   string `xml:\"audioCompressionType\"`\n//}\n"
  },
  {
    "path": "pkg/iso/atoms.go",
    "content": "package iso\n\nconst (\n\tFtyp                        = \"ftyp\"\n\tMoov                        = \"moov\"\n\tMoovMvhd                    = \"mvhd\"\n\tMoovTrak                    = \"trak\"\n\tMoovTrakTkhd                = \"tkhd\"\n\tMoovTrakMdia                = \"mdia\"\n\tMoovTrakMdiaMdhd            = \"mdhd\"\n\tMoovTrakMdiaHdlr            = \"hdlr\"\n\tMoovTrakMdiaMinf            = \"minf\"\n\tMoovTrakMdiaMinfVmhd        = \"vmhd\"\n\tMoovTrakMdiaMinfSmhd        = \"smhd\"\n\tMoovTrakMdiaMinfDinf        = \"dinf\"\n\tMoovTrakMdiaMinfDinfDref    = \"dref\"\n\tMoovTrakMdiaMinfDinfDrefUrl = \"url \"\n\tMoovTrakMdiaMinfStbl        = \"stbl\"\n\tMoovTrakMdiaMinfStblStsd    = \"stsd\"\n\tMoovTrakMdiaMinfStblStts    = \"stts\"\n\tMoovTrakMdiaMinfStblStsc    = \"stsc\"\n\tMoovTrakMdiaMinfStblStsz    = \"stsz\"\n\tMoovTrakMdiaMinfStblStco    = \"stco\"\n\tMoovMvex                    = \"mvex\"\n\tMoovMvexTrex                = \"trex\"\n\tMoof                        = \"moof\"\n\tMoofMfhd                    = \"mfhd\"\n\tMoofTraf                    = \"traf\"\n\tMoofTrafTfhd                = \"tfhd\"\n\tMoofTrafTfdt                = \"tfdt\"\n\tMoofTrafTrun                = \"trun\"\n\tMdat                        = \"mdat\"\n)\n\nconst (\n\tsampleIsNonSync  = 0x10000\n\tsampleDependsOn1 = 0x1000000\n\tsampleDependsOn2 = 0x2000000\n\n\tSampleVideoIFrame    = sampleDependsOn2\n\tSampleVideoNonIFrame = sampleDependsOn1 | sampleIsNonSync\n\tSampleAudio          = sampleDependsOn2 //sampleIsNonSync\n)\n\nfunc (m *Movie) WriteFileType() {\n\tm.StartAtom(Ftyp)\n\tm.WriteString(\"iso5\")\n\tm.WriteUint32(512)\n\tm.WriteString(\"iso5\")\n\tm.WriteString(\"iso6\")\n\tm.WriteString(\"mp41\")\n\tm.EndAtom()\n}\n\nfunc (m *Movie) WriteMovieHeader() {\n\tm.StartAtom(MoovMvhd)\n\tm.Skip(1)           // version\n\tm.Skip(3)           // flags\n\tm.Skip(4)           // create time\n\tm.Skip(4)           // modify time\n\tm.WriteUint32(1000) // time scale\n\tm.Skip(4)           // duration\n\tm.WriteFloat32(1)   // preferred rate\n\tm.WriteFloat16(1)   // preferred volume\n\tm.Skip(10)          // reserved\n\tm.WriteMatrix()\n\tm.Skip(6 * 4)             // predefined?\n\tm.WriteUint32(0xFFFFFFFF) // next track ID\n\tm.EndAtom()\n}\n\nfunc (m *Movie) WriteTrackHeader(id uint32, width, height uint16) {\n\tconst (\n\t\tTkhdTrackEnabled   = 0x0001\n\t\tTkhdTrackInMovie   = 0x0002\n\t\tTkhdTrackInPreview = 0x0004\n\t\tTkhdTrackInPoster  = 0x0008\n\t)\n\n\t// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-32963\n\tm.StartAtom(MoovTrakTkhd)\n\tm.Skip(1) // version\n\tm.WriteUint24(TkhdTrackEnabled | TkhdTrackInMovie)\n\tm.Skip(4)         // create time\n\tm.Skip(4)         // modify time\n\tm.WriteUint32(id) // trackID\n\tm.Skip(4)         // reserved\n\tm.Skip(4)         // duration\n\tm.Skip(8)         // reserved\n\tm.Skip(2)         // layer\n\tif width > 0 {\n\t\tm.Skip(2)\n\t\tm.Skip(2)\n\t} else {\n\t\tm.WriteUint16(1)  // alternate group\n\t\tm.WriteFloat16(1) // volume\n\t}\n\tm.Skip(2) // reserved\n\tm.WriteMatrix()\n\tif width > 0 {\n\t\tm.WriteFloat32(float64(width))\n\t\tm.WriteFloat32(float64(height))\n\t} else {\n\t\tm.Skip(4)\n\t\tm.Skip(4)\n\t}\n\tm.EndAtom()\n}\n\nfunc (m *Movie) WriteMediaHeader(timescale uint32) {\n\t// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-32999\n\tm.StartAtom(MoovTrakMdiaMdhd)\n\tm.Skip(1)                // version\n\tm.Skip(3)                // flags\n\tm.Skip(4)                // creation time\n\tm.Skip(4)                // modification time\n\tm.WriteUint32(timescale) // timescale\n\tm.Skip(4)                // duration\n\tm.WriteUint16(0x55C4)    // language (Unspecified)\n\tm.Skip(2)                // quality\n\tm.EndAtom()\n}\n\nfunc (m *Movie) WriteMediaHandler(s, name string) {\n\t// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-33004\n\tm.StartAtom(MoovTrakMdiaHdlr)\n\tm.Skip(1) // version\n\tm.Skip(3) // flags\n\tm.Skip(4)\n\tm.WriteString(s)    // handler type (4 byte!)\n\tm.Skip(3 * 4)       // reserved\n\tm.WriteString(name) // handler name (any len)\n\tm.Skip(1)           // end string\n\tm.EndAtom()\n}\n\nfunc (m *Movie) WriteVideoMediaInfo() {\n\t// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-33012\n\tm.StartAtom(MoovTrakMdiaMinfVmhd)\n\tm.Skip(1)        // version\n\tm.WriteUint24(1) // flags (You should always set this flag to 1)\n\tm.Skip(2)        // graphics mode\n\tm.Skip(3 * 2)    // op color\n\tm.EndAtom()\n}\n\nfunc (m *Movie) WriteAudioMediaInfo() {\n\tm.StartAtom(MoovTrakMdiaMinfSmhd)\n\tm.Skip(1) // version\n\tm.Skip(3) // flags\n\tm.Skip(4) // balance\n\tm.EndAtom()\n}\n\nfunc (m *Movie) WriteDataInfo() {\n\t// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-25680\n\tm.StartAtom(MoovTrakMdiaMinfDinf)\n\tm.StartAtom(MoovTrakMdiaMinfDinfDref)\n\tm.Skip(1)        // version\n\tm.Skip(3)        // flags\n\tm.WriteUint32(1) // childrens\n\n\tm.StartAtom(MoovTrakMdiaMinfDinfDrefUrl)\n\tm.Skip(1)        // version\n\tm.WriteUint24(1) // flags (self reference)\n\tm.EndAtom()\n\n\tm.EndAtom() // DREF\n\tm.EndAtom() // DINF\n}\n\nfunc (m *Movie) WriteSampleTable(writeSampleDesc func()) {\n\t// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap2/qtff2.html#//apple_ref/doc/uid/TP40000939-CH204-33040\n\tm.StartAtom(MoovTrakMdiaMinfStbl)\n\n\tm.StartAtom(MoovTrakMdiaMinfStblStsd)\n\tm.Skip(1)        // version\n\tm.Skip(3)        // flags\n\tm.WriteUint32(1) // entry count\n\twriteSampleDesc()\n\tm.EndAtom()\n\n\tm.StartAtom(MoovTrakMdiaMinfStblStts)\n\tm.Skip(1) // version\n\tm.Skip(3) // flags\n\tm.Skip(4) // entry count\n\tm.EndAtom()\n\n\tm.StartAtom(MoovTrakMdiaMinfStblStsc)\n\tm.Skip(1) // version\n\tm.Skip(3) // flags\n\tm.Skip(4) // entry count\n\tm.EndAtom()\n\n\tm.StartAtom(MoovTrakMdiaMinfStblStsz)\n\tm.Skip(1) // version\n\tm.Skip(3) // flags\n\tm.Skip(4) // sample size\n\tm.Skip(4) // entry count\n\tm.EndAtom()\n\n\tm.StartAtom(MoovTrakMdiaMinfStblStco)\n\tm.Skip(1) // version\n\tm.Skip(3) // flags\n\tm.Skip(4) // entry count\n\tm.EndAtom()\n\n\tm.EndAtom()\n}\n\nfunc (m *Movie) WriteTrackExtend(id uint32) {\n\tm.StartAtom(MoovMvexTrex)\n\tm.Skip(1)         // version\n\tm.Skip(3)         // flags\n\tm.WriteUint32(id) // trackID\n\tm.WriteUint32(1)  // default sample description index\n\tm.Skip(4)         // default sample duration\n\tm.Skip(4)         // default sample size\n\tm.Skip(4)         // default sample flags\n\tm.EndAtom()\n}\n\nfunc (m *Movie) WriteVideoTrack(id uint32, codec string, timescale uint32, width, height uint16, conf []byte) {\n\tm.StartAtom(MoovTrak)\n\tm.WriteTrackHeader(id, width, height)\n\n\tm.StartAtom(MoovTrakMdia)\n\tm.WriteMediaHeader(timescale)\n\tm.WriteMediaHandler(\"vide\", \"VideoHandler\")\n\n\tm.StartAtom(MoovTrakMdiaMinf)\n\tm.WriteVideoMediaInfo()\n\tm.WriteDataInfo()\n\tm.WriteSampleTable(func() {\n\t\tm.WriteVideo(codec, width, height, conf)\n\t})\n\tm.EndAtom() // MINF\n\n\tm.EndAtom() // MDIA\n\tm.EndAtom() // TRAK\n}\n\nfunc (m *Movie) WriteAudioTrack(id uint32, codec string, timescale uint32, channels uint16, conf []byte) {\n\tm.StartAtom(MoovTrak)\n\tm.WriteTrackHeader(id, 0, 0)\n\n\tm.StartAtom(MoovTrakMdia)\n\tm.WriteMediaHeader(timescale)\n\tm.WriteMediaHandler(\"soun\", \"SoundHandler\")\n\n\tm.StartAtom(MoovTrakMdiaMinf)\n\tm.WriteAudioMediaInfo()\n\tm.WriteDataInfo()\n\tm.WriteSampleTable(func() {\n\t\tm.WriteAudio(codec, channels, timescale, conf)\n\t})\n\tm.EndAtom() // MINF\n\n\tm.EndAtom() // MDIA\n\tm.EndAtom() // TRAK\n}\n\nconst (\n\tTfhdDefaultSampleDuration = 0x000008\n\tTfhdDefaultSampleSize     = 0x000010\n\tTfhdDefaultSampleFlags    = 0x000020\n\tTfhdDefaultBaseIsMoof     = 0x020000\n)\n\nconst (\n\tTrunDataOffset       = 0x000001\n\tTrunFirstSampleFlags = 0x000004\n\tTrunSampleDuration   = 0x0000100\n\tTrunSampleSize       = 0x0000200\n\tTrunSampleFlags      = 0x0000400\n\tTrunSampleCTS        = 0x0000800\n)\n\nfunc (m *Movie) WriteMovieFragment(seq, tid, duration, size, flags uint32, dts uint64, cts uint32) {\n\tm.StartAtom(Moof)\n\n\tm.StartAtom(MoofMfhd)\n\tm.Skip(1)          // version\n\tm.Skip(3)          // flags\n\tm.WriteUint32(seq) // sequence number\n\tm.EndAtom()\n\n\tm.StartAtom(MoofTraf)\n\n\tm.StartAtom(MoofTrafTfhd)\n\tm.Skip(1) // version\n\tm.WriteUint24(\n\t\tTfhdDefaultSampleDuration |\n\t\t\tTfhdDefaultSampleSize |\n\t\t\tTfhdDefaultSampleFlags |\n\t\t\tTfhdDefaultBaseIsMoof,\n\t)\n\tm.WriteUint32(tid)      // track id\n\tm.WriteUint32(duration) // default sample duration\n\tm.WriteUint32(size)     // default sample size\n\tm.WriteUint32(flags)    // default sample flags\n\tm.EndAtom()\n\n\tm.StartAtom(MoofTrafTfdt)\n\tm.WriteBytes(1)    // version\n\tm.Skip(3)          // flags\n\tm.WriteUint64(dts) // base media decode time\n\tm.EndAtom()\n\n\tm.StartAtom(MoofTrafTrun)\n\tm.Skip(1) // version\n\n\tif cts == 0 {\n\t\tm.WriteUint24(TrunDataOffset) // flags\n\t\tm.WriteUint32(1)              // sample count\n\n\t\t// data offset: current pos + uint32 len + MDAT header len\n\t\tm.WriteUint32(uint32(len(m.b)) + 4 + 8)\n\t} else {\n\t\tm.WriteUint24(TrunDataOffset | TrunSampleCTS)\n\t\tm.WriteUint32(1)\n\n\t\t// data offset: current pos + uint32 len + CTS + MDAT header len\n\t\tm.WriteUint32(uint32(len(m.b)) + 4 + 4 + 8)\n\t\tm.WriteUint32(cts)\n\t}\n\n\tm.EndAtom() // TRUN\n\n\tm.EndAtom() // TRAF\n\n\tm.EndAtom() // MOOF\n}\n\nfunc (m *Movie) WriteData(b []byte) {\n\tm.StartAtom(Mdat)\n\tm.Write(b)\n\tm.EndAtom()\n}\n"
  },
  {
    "path": "pkg/iso/codecs.go",
    "content": "package iso\n\nimport (\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/pcm\"\n)\n\nfunc (m *Movie) WriteVideo(codec string, width, height uint16, conf []byte) {\n\t// https://developer.apple.com/library/archive/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html\n\tswitch codec {\n\tcase core.CodecH264:\n\t\tm.StartAtom(\"avc1\")\n\tcase core.CodecH265:\n\t\tm.StartAtom(\"hev1\")\n\tdefault:\n\t\tpanic(\"unsupported iso video: \" + codec)\n\t}\n\tm.Skip(6)\n\tm.WriteUint16(1)      // data_reference_index\n\tm.Skip(2)             // version\n\tm.Skip(2)             // revision\n\tm.Skip(4)             // vendor\n\tm.Skip(4)             // temporal quality\n\tm.Skip(4)             // spatial quality\n\tm.WriteUint16(width)  // width\n\tm.WriteUint16(height) // height\n\tm.WriteFloat32(72)    // horizontal resolution\n\tm.WriteFloat32(72)    // vertical resolution\n\tm.Skip(4)             // reserved\n\tm.WriteUint16(1)      // frame count\n\tm.Skip(32)            // compressor name\n\tm.WriteUint16(24)     // depth\n\tm.WriteUint16(0xFFFF) // color table id (-1)\n\n\tswitch codec {\n\tcase core.CodecH264:\n\t\tm.StartAtom(\"avcC\")\n\tcase core.CodecH265:\n\t\tm.StartAtom(\"hvcC\")\n\t}\n\tm.Write(conf)\n\tm.EndAtom() // AVCC\n\n\tm.StartAtom(\"pasp\") // Pixel Aspect Ratio\n\tm.WriteUint32(1)    // hSpacing\n\tm.WriteUint32(1)    // vSpacing\n\tm.EndAtom()\n\n\tm.EndAtom() // AVC1\n}\n\nfunc (m *Movie) WriteAudio(codec string, channels uint16, sampleRate uint32, conf []byte) {\n\tswitch codec {\n\tcase core.CodecAAC, core.CodecMP3:\n\t\tm.StartAtom(\"mp4a\") // supported in all players and browsers\n\tcase core.CodecFLAC:\n\t\tm.StartAtom(\"fLaC\") // supported in all players and browsers\n\tcase core.CodecOpus:\n\t\tm.StartAtom(\"Opus\") // supported in Chrome and Firefox\n\tcase core.CodecPCMU:\n\t\tm.StartAtom(\"ulaw\")\n\tcase core.CodecPCMA:\n\t\tm.StartAtom(\"alaw\")\n\tdefault:\n\t\tpanic(\"unsupported iso audio: \" + codec)\n\t}\n\n\tif channels == 0 {\n\t\tchannels = 1\n\t}\n\n\tm.Skip(6)\n\tm.WriteUint16(1)                    // data_reference_index\n\tm.Skip(2)                           // version\n\tm.Skip(2)                           // revision\n\tm.Skip(4)                           // vendor\n\tm.WriteUint16(channels)             // channel_count\n\tm.WriteUint16(16)                   // sample_size\n\tm.Skip(2)                           // compression id\n\tm.Skip(2)                           // reserved\n\tm.WriteFloat32(float64(sampleRate)) // sample_rate\n\n\tswitch codec {\n\tcase core.CodecAAC:\n\t\tm.WriteEsdsAAC(conf)\n\tcase core.CodecMP3:\n\t\tm.WriteEsdsMP3()\n\tcase core.CodecFLAC:\n\t\tm.StartAtom(\"dfLa\")\n\t\tm.Write(pcm.FLACHeader(false, sampleRate))\n\t\tm.EndAtom()\n\tcase core.CodecOpus:\n\t\tm.WriteOpus(channels, sampleRate)\n\tcase core.CodecPCMU, core.CodecPCMA:\n\t\t// don't know what means this magic\n\t\tm.StartAtom(\"chan\")\n\t\tm.WriteBytes(0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 4, 0, 0, 0, 0)\n\t\tm.EndAtom()\n\t}\n\n\tm.EndAtom() // MP4A/OPUS\n}\n\nfunc (m *Movie) WriteEsdsAAC(conf []byte) {\n\tm.StartAtom(\"esds\")\n\tm.Skip(1) // version\n\tm.Skip(3) // flags\n\n\t// MP4ESDescrTag[3]:\n\t// - MP4DecConfigDescrTag[4]:\n\t//   - MP4DecSpecificDescrTag[5]: conf\n\t// - Other[6]\n\tconst header = 5\n\tconst size3 = 3\n\tconst size4 = 13\n\tsize5 := byte(len(conf))\n\tconst size6 = 1\n\n\tm.WriteBytes(3, 0x80, 0x80, 0x80, size3+header+size4+header+size5+header+size6)\n\tm.Skip(2) // es id\n\tm.Skip(1) // es flags\n\n\t// https://learn.microsoft.com/en-us/windows/win32/medfound/mpeg-4-file-sink#aac-audio\n\tm.WriteBytes(4, 0x80, 0x80, 0x80, size4+header+size5)\n\tm.WriteBytes(0x40) // object id\n\tm.WriteBytes(0x15) // stream type\n\tm.Skip(3)          // buffer size db\n\tm.Skip(4)          // max bitraga\n\tm.Skip(4)          // avg bitraga\n\n\tm.WriteBytes(5, 0x80, 0x80, 0x80, size5)\n\tm.Write(conf)\n\n\tm.WriteBytes(6, 0x80, 0x80, 0x80, 1)\n\tm.WriteBytes(2) // ?\n\n\tm.EndAtom() // ESDS\n}\n\nfunc (m *Movie) WriteEsdsMP3() {\n\tm.StartAtom(\"esds\")\n\tm.Skip(1) // version\n\tm.Skip(3) // flags\n\n\t// MP4ESDescrTag[3]:\n\t// - MP4DecConfigDescrTag[4]:\n\t// - Other[6]\n\tconst header = 5\n\tconst size3 = 3\n\tconst size4 = 13\n\tconst size6 = 1\n\n\tm.WriteBytes(3, 0x80, 0x80, 0x80, size3+header+size4+header+size6)\n\tm.Skip(2) // es id\n\tm.Skip(1) // es flags\n\n\t// https://learn.microsoft.com/en-us/windows/win32/medfound/mpeg-4-file-sink#mp3-audio\n\tm.WriteBytes(4, 0x80, 0x80, 0x80, size4)\n\tm.WriteBytes(0x6B) // object id\n\tm.WriteBytes(0x15) // stream type\n\tm.Skip(3)          // buffer size db\n\tm.Skip(4)          // max bitraga\n\tm.Skip(4)          // avg bitraga\n\n\tm.WriteBytes(6, 0x80, 0x80, 0x80, 1)\n\tm.WriteBytes(2) // ?\n\n\tm.EndAtom() // ESDS\n}\n\nfunc (m *Movie) WriteOpus(channels uint16, sampleRate uint32) {\n\t// https://www.opus-codec.org/docs/opus_in_isobmff.html\n\tm.StartAtom(\"dOps\")\n\tm.Skip(1) // version\n\tm.WriteBytes(byte(channels))\n\tm.WriteUint16(0) // PreSkip ???\n\tm.WriteUint32(sampleRate)\n\tm.Skip(2) // OutputGain\n\tm.Skip(1) // signed int(16) OutputGain;\n\tm.EndAtom()\n}\n"
  },
  {
    "path": "pkg/iso/iso.go",
    "content": "package iso\n\nimport (\n\t\"encoding/binary\"\n\t\"math\"\n)\n\ntype Movie struct {\n\tb     []byte\n\tstart []int\n}\n\nfunc NewMovie(size int) *Movie {\n\treturn &Movie{b: make([]byte, 0, size)}\n}\n\nfunc (m *Movie) Bytes() []byte {\n\treturn m.b\n}\n\nfunc (m *Movie) StartAtom(name string) {\n\tm.start = append(m.start, len(m.b))\n\tm.b = append(m.b, 0, 0, 0, 0)\n\tm.b = append(m.b, name...)\n}\n\nfunc (m *Movie) EndAtom() {\n\tn := len(m.start) - 1\n\n\ti := m.start[n]\n\tsize := uint32(len(m.b) - i)\n\tbinary.BigEndian.PutUint32(m.b[i:], size)\n\n\tm.start = m.start[:n]\n}\n\nfunc (m *Movie) Write(b []byte) {\n\tm.b = append(m.b, b...)\n}\n\nfunc (m *Movie) WriteBytes(b ...byte) {\n\tm.b = append(m.b, b...)\n}\n\nfunc (m *Movie) WriteString(s string) {\n\tm.b = append(m.b, s...)\n}\n\nfunc (m *Movie) Skip(n int) {\n\tm.b = append(m.b, make([]byte, n)...)\n}\n\nfunc (m *Movie) WriteUint16(v uint16) {\n\tm.b = append(m.b, byte(v>>8), byte(v))\n}\n\nfunc (m *Movie) WriteUint24(v uint32) {\n\tm.b = append(m.b, byte(v>>16), byte(v>>8), byte(v))\n}\n\nfunc (m *Movie) WriteUint32(v uint32) {\n\tm.b = append(m.b, byte(v>>24), byte(v>>16), byte(v>>8), byte(v))\n}\n\nfunc (m *Movie) WriteUint64(v uint64) {\n\tm.b = append(m.b, byte(v>>56), byte(v>>48), byte(v>>40), byte(v>>32), byte(v>>24), byte(v>>16), byte(v>>8), byte(v))\n}\n\nfunc (m *Movie) WriteFloat16(f float64) {\n\ti, f := math.Modf(f)\n\tf *= 256\n\tm.b = append(m.b, byte(i), byte(f))\n}\n\nfunc (m *Movie) WriteFloat32(f float64) {\n\ti, f := math.Modf(f)\n\tf *= 65536\n\tm.b = append(m.b, byte(uint16(i)>>8), byte(i), byte(uint16(f)>>8), byte(f))\n}\n\nfunc (m *Movie) WriteMatrix() {\n\tm.WriteUint32(0x00010000)\n\tm.Skip(4)\n\tm.Skip(4)\n\tm.Skip(4)\n\tm.WriteUint32(0x00010000)\n\tm.Skip(4)\n\tm.Skip(4)\n\tm.Skip(4)\n\tm.WriteUint32(0x40000000)\n}\n"
  },
  {
    "path": "pkg/iso/reader.go",
    "content": "package iso\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"io\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/bits\"\n)\n\ntype Atom struct {\n\tName string\n\tData []byte\n}\n\ntype AtomTkhd struct {\n\tTrackID uint32\n}\n\ntype AtomMdhd struct {\n\tTimeScale uint32\n}\n\ntype AtomVideo struct {\n\tName   string\n\tConfig []byte\n}\n\ntype AtomAudio struct {\n\tName       string\n\tChannels   uint16\n\tSampleRate uint32\n\tConfig     []byte\n}\n\ntype AtomMfhd struct {\n\tSequence uint32\n}\n\ntype AtomMdat struct {\n\tData []byte\n}\n\ntype AtomTfhd struct {\n\tTrackID        uint32\n\tSampleDuration uint32\n\tSampleSize     uint32\n\tSampleFlags    uint32\n}\ntype AtomTfdt struct {\n\tDecodeTime uint64\n}\n\ntype AtomTrun struct {\n\tDataOffset       uint32\n\tFirstSampleFlags uint32\n\tSamplesDuration  []uint32\n\tSamplesSize      []uint32\n\tSamplesFlags     []uint32\n\tSamplesCTS       []uint32\n}\n\nfunc DecodeAtom(b []byte) (any, error) {\n\tsize := binary.BigEndian.Uint32(b)\n\tif len(b) < int(size) {\n\t\treturn nil, io.EOF\n\t}\n\n\tname := string(b[4:8])\n\tdata := b[8:size]\n\n\tswitch name {\n\t// useful containers\n\tcase Moov, MoovTrak, MoovTrakMdia, MoovTrakMdiaMinf, MoovTrakMdiaMinfStbl, Moof, MoofTraf:\n\t\treturn DecodeAtoms(data)\n\n\tcase MoovTrakTkhd:\n\t\treturn &AtomTkhd{TrackID: binary.BigEndian.Uint32(data[1+3+4+4:])}, nil\n\n\tcase MoovTrakMdiaMdhd:\n\t\treturn &AtomMdhd{TimeScale: binary.BigEndian.Uint32(data[1+3+4+4:])}, nil\n\n\tcase MoovTrakMdiaMinfStblStsd:\n\t\t// support only 1 codec entry\n\t\tif n := binary.BigEndian.Uint32(data[1+3:]); n == 1 {\n\t\t\treturn DecodeAtom(data[1+3+4:])\n\t\t}\n\n\tcase \"avc1\", \"hev1\":\n\t\tb = data[6+2+2+2+4+4+4+2+2+4+4+4+2+32+2+2:]\n\t\tatom, err := DecodeAtom(b)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif conf, ok := atom.(*Atom); ok {\n\t\t\treturn &AtomVideo{Name: name, Config: conf.Data}, nil\n\t\t}\n\n\tcase \"mp4a\":\n\t\tatom := &AtomAudio{Name: name}\n\n\t\trd := bits.NewReader(data)\n\t\trd.ReadBytes(6 + 2 + 2 + 2 + 4) // skip\n\t\tatom.Channels = rd.ReadUint16()\n\t\trd.ReadBytes(2 + 2 + 2) // skip\n\t\tatom.SampleRate = uint32(rd.ReadFloat32())\n\n\t\tatom2, _ := DecodeAtom(rd.Left())\n\t\tif conf, ok := atom2.(*Atom); ok {\n\t\t\t_, b, _ = bytes.Cut(conf.Data, []byte{5, 0x80, 0x80, 0x80})\n\t\t\tif n := len(b); n > 0 && n > 1+int(b[0]) {\n\t\t\t\tatom.Config = b[1 : 1+b[0]]\n\t\t\t}\n\t\t}\n\n\t\treturn atom, nil\n\n\tcase MoofMfhd:\n\t\treturn &AtomMfhd{Sequence: binary.BigEndian.Uint32(data[4:])}, nil\n\n\tcase MoofTrafTfhd:\n\t\trd := bits.NewReader(data)\n\t\t_ = rd.ReadByte() // version\n\t\tflags := rd.ReadUint24()\n\n\t\tatom := &AtomTfhd{\n\t\t\tTrackID: rd.ReadUint32(),\n\t\t}\n\n\t\tif flags&TfhdDefaultSampleDuration != 0 {\n\t\t\tatom.SampleDuration = rd.ReadUint32()\n\n\t\t}\n\t\tif flags&TfhdDefaultSampleSize != 0 {\n\t\t\tatom.SampleSize = rd.ReadUint32()\n\t\t}\n\t\tif flags&TfhdDefaultSampleFlags != 0 {\n\t\t\tatom.SampleFlags = rd.ReadUint32() // skip\n\t\t}\n\n\t\treturn atom, nil\n\n\tcase MoofTrafTfdt:\n\t\treturn &AtomTfdt{DecodeTime: binary.BigEndian.Uint64(data[4:])}, nil\n\n\tcase MoofTrafTrun:\n\t\trd := bits.NewReader(data)\n\t\t_ = rd.ReadByte() // version\n\t\tflags := rd.ReadUint24()\n\t\tsamples := rd.ReadUint32()\n\n\t\tatom := &AtomTrun{}\n\n\t\tif flags&TrunDataOffset != 0 {\n\t\t\tatom.DataOffset = rd.ReadUint32()\n\t\t}\n\t\tif flags&TrunFirstSampleFlags != 0 {\n\t\t\tatom.FirstSampleFlags = rd.ReadUint32()\n\t\t}\n\n\t\tfor i := uint32(0); i < samples; i++ {\n\t\t\tif flags&TrunSampleDuration != 0 {\n\t\t\t\tatom.SamplesDuration = append(atom.SamplesDuration, rd.ReadUint32())\n\t\t\t}\n\t\t\tif flags&TrunSampleSize != 0 {\n\t\t\t\tatom.SamplesSize = append(atom.SamplesSize, rd.ReadUint32())\n\t\t\t}\n\t\t\tif flags&TrunSampleFlags != 0 {\n\t\t\t\tatom.SamplesFlags = append(atom.SamplesFlags, rd.ReadUint32())\n\t\t\t}\n\t\t\tif flags&TrunSampleCTS != 0 {\n\t\t\t\tatom.SamplesCTS = append(atom.SamplesCTS, rd.ReadUint32())\n\t\t\t}\n\t\t}\n\n\t\treturn atom, nil\n\n\tcase Mdat:\n\t\treturn &AtomMdat{Data: data}, nil\n\t}\n\n\treturn &Atom{Name: name, Data: data}, nil\n}\n\nfunc DecodeAtoms(b []byte) (atoms []any, err error) {\n\tfor len(b) > 0 {\n\t\tatom, err := DecodeAtom(b)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif childs, ok := atom.([]any); ok {\n\t\t\tatoms = append(atoms, childs...)\n\t\t} else {\n\t\t\tatoms = append(atoms, atom)\n\t\t}\n\n\t\tsize := binary.BigEndian.Uint32(b)\n\t\tb = b[size:]\n\t}\n\n\treturn atoms, nil\n}\n"
  },
  {
    "path": "pkg/ivideon/ivideon.go",
    "content": "package ivideon\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/mp4\"\n\t\"github.com/gorilla/websocket\"\n)\n\ntype Producer struct {\n\tcore.Connection\n\tconn *websocket.Conn\n\n\tbuf []byte\n\n\tdem *mp4.Demuxer\n}\n\nfunc Dial(source string) (core.Producer, error) {\n\tid := strings.Replace(source[8:], \"/\", \":\", 1)\n\n\turl, err := GetLiveStream(id)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tconn, _, err := websocket.DefaultDialer.Dial(url, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tprod := &Producer{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"ivideon\",\n\t\t\tProtocol:   core.Before(url, \":\"), // wss\n\t\t\tRemoteAddr: conn.RemoteAddr().String(),\n\t\t\tSource:     source,\n\t\t\tURL:        url,\n\t\t\tTransport:  conn,\n\t\t},\n\t\tconn: conn,\n\t}\n\n\tif err = prod.probe(); err != nil {\n\t\t_ = conn.Close()\n\t\treturn nil, err\n\t}\n\n\treturn prod, nil\n}\n\nfunc GetLiveStream(id string) (string, error) {\n\t// &video_codecs=h264,h265&audio_codecs=aac,mp3,pcma,pcmu,none\n\tresp, err := http.Get(\n\t\t\"https://openapi-alpha.ivideon.com/cameras/\" + id +\n\t\t\t\"/live_stream?op=GET&access_token=public&q=2&video_codecs=h264&format=ws-fmp4\",\n\t)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar v struct {\n\t\tMessage string `json:\"message\"`\n\t\tResult  struct {\n\t\t\tURL string `json:\"url\"`\n\t\t} `json:\"result\"`\n\t\tSuccess bool `json:\"success\"`\n\t}\n\tif err = json.NewDecoder(resp.Body).Decode(&v); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif !v.Success {\n\t\treturn \"\", fmt.Errorf(\"ivideon: can't get live_stream: \" + v.Message)\n\t}\n\n\treturn v.Result.URL, nil\n}\n\nfunc (p *Producer) Start() error {\n\treceivers := make(map[uint32]*core.Receiver)\n\tfor _, receiver := range p.Receivers {\n\t\ttrackID := p.dem.GetTrackID(receiver.Codec)\n\t\treceivers[trackID] = receiver\n\t}\n\n\tch := make(chan []byte, 10)\n\tdefer close(ch)\n\n\tch <- p.buf\n\n\tgo func() {\n\t\t// add delay to the stream for smooth playing (not a best solution)\n\t\tt0 := time.Now()\n\n\t\tfor data := range ch {\n\t\t\ttrackID, packets := p.dem.Demux(data)\n\t\t\tif receiver := receivers[trackID]; receiver != nil {\n\t\t\t\tclockRate := time.Duration(receiver.Codec.ClockRate)\n\t\t\t\tfor _, packet := range packets {\n\t\t\t\t\t// synchronize framerate for WebRTC and MSE\n\t\t\t\t\tts := time.Second * time.Duration(packet.Timestamp) / clockRate\n\t\t\t\t\td := ts - time.Since(t0)\n\t\t\t\t\tif d < 0 {\n\t\t\t\t\t\td = 10 * time.Millisecond\n\t\t\t\t\t}\n\t\t\t\t\ttime.Sleep(d)\n\n\t\t\t\t\treceiver.WriteRTP(packet)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}()\n\n\tfor {\n\t\tvar msg message\n\t\tif err := p.conn.ReadJSON(&msg); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tswitch msg.Type {\n\t\tcase \"stream-init\", \"metadata\":\n\t\t\tcontinue\n\n\t\tcase \"fragment\":\n\t\t\t_, b, err := p.conn.ReadMessage()\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tp.Recv += len(b)\n\t\t\tch <- b\n\n\t\tdefault:\n\t\t\treturn errors.New(\"ivideon: wrong message type: \" + msg.Type)\n\t\t}\n\t}\n}\n\nfunc (p *Producer) probe() (err error) {\n\tp.dem = &mp4.Demuxer{}\n\n\tfor {\n\t\tvar msg message\n\t\tif err = p.conn.ReadJSON(&msg); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tswitch msg.Type {\n\t\tcase \"metadata\":\n\t\t\tcontinue\n\n\t\tcase \"stream-init\":\n\t\t\t// it's difficult to maintain audio\n\t\t\tif strings.HasPrefix(msg.CodecString, \"avc1\") {\n\t\t\t\tmedias := p.dem.Probe(msg.Data)\n\t\t\t\tp.Medias = append(p.Medias, medias...)\n\t\t\t}\n\n\t\tcase \"fragment\":\n\t\t\t_, p.buf, err = p.conn.ReadMessage()\n\t\t\treturn\n\n\t\tdefault:\n\t\t\treturn errors.New(\"ivideon: wrong message type: \" + msg.Type)\n\t\t}\n\t}\n}\n\ntype message struct {\n\tType        string `json:\"type\"`\n\tCodecString string `json:\"codec_string\"`\n\tData        []byte `json:\"data\"`\n\t//TrackID     byte    `json:\"track_id\"`\n\t//Track       byte    `json:\"track\"`\n\t//StartTime   float32 `json:\"start_time\"`\n\t//Duration    float32 `json:\"duration\"`\n\t//IsKey       bool    `json:\"is_key\"`\n\t//DataOffset  uint32  `json:\"data_offset\"`\n}\n"
  },
  {
    "path": "pkg/kasa/producer.go",
    "content": "package kasa\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/httputil\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264/annexb\"\n\t\"github.com/AlexxIT/go2rtc/pkg/mpjpeg\"\n\t\"github.com/AlexxIT/go2rtc/pkg/tcp\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Producer struct {\n\tcore.Connection\n\trd *core.ReadBuffer\n\n\treader *bufio.Reader\n}\n\nfunc Dial(url string) (*Producer, error) {\n\treq, err := http.NewRequest(\"GET\", url, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq.URL.Scheme = \"httpx\"\n\n\tres, err := tcp.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// KC200\n\t//   HTTP/1.0 200 OK\n\t//   Content-Type: multipart/x-mixed-replace;boundary=data-boundary--\n\t// KD110, KC401, KC420WS:\n\t//   HTTP/1.0 200 OK\n\t//   Content-Type: multipart/x-mixed-replace;boundary=data-boundary--\n\t//   Transfer-Encoding: chunked\n\t// HTTP/1.0 + chunked = out of standard, so golang remove this header\n\t// and we need to check first two bytes\n\tbuf := bufio.NewReader(res.Body)\n\n\tb, err := buf.Peek(2)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trd := struct {\n\t\tio.Reader\n\t\tio.Closer\n\t}{\n\t\tbuf,\n\t\tres.Body,\n\t}\n\n\tif string(b) != \"--\" {\n\t\trd.Reader = httputil.NewChunkedReader(buf)\n\t}\n\n\tprod := &Producer{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"kasa\",\n\t\t\tProtocol:   \"http\",\n\t\t\tTransport:  rd,\n\t\t},\n\t\trd: core.NewReadBuffer(rd),\n\t}\n\tif err = prod.probe(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn prod, nil\n}\n\nfunc (c *Producer) Start() error {\n\tif len(c.Receivers) == 0 {\n\t\treturn errors.New(\"multipart: no receivers\")\n\t}\n\n\tvar video, audio *core.Receiver\n\n\tfor _, receiver := range c.Receivers {\n\t\tswitch receiver.Codec.Name {\n\t\tcase core.CodecH264:\n\t\t\tvideo = receiver\n\t\tcase core.CodecPCMU:\n\t\t\taudio = receiver\n\t\t}\n\t}\n\n\tfor {\n\t\theader, body, err := mpjpeg.Next(c.reader)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tc.Recv += len(body)\n\n\t\tct := header.Get(\"Content-Type\")\n\t\tswitch ct {\n\t\tcase MimeVideo:\n\t\t\tif video != nil {\n\t\t\t\tts := GetTimestamp(header)\n\t\t\t\tpkt := &rtp.Packet{\n\t\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\t\tTimestamp: uint32(ts * 90000),\n\t\t\t\t\t},\n\t\t\t\t\tPayload: annexb.EncodeToAVCC(body),\n\t\t\t\t}\n\t\t\t\tvideo.WriteRTP(pkt)\n\t\t\t}\n\n\t\tcase MimeG711U:\n\t\t\tif audio != nil {\n\t\t\t\tts := GetTimestamp(header)\n\t\t\t\tpkt := &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\tTimestamp: uint32(ts * 8000),\n\t\t\t\t\t},\n\t\t\t\t\tPayload: body,\n\t\t\t\t}\n\t\t\t\taudio.WriteRTP(pkt)\n\t\t\t}\n\t\t}\n\t}\n}\n\nconst (\n\tMimeVideo = \"video/x-h264\"\n\tMimeG711U = \"audio/g711u\"\n)\n\nfunc (c *Producer) probe() error {\n\tc.rd.BufferSize = core.ProbeSize\n\tc.reader = bufio.NewReader(c.rd)\n\n\tdefer func() {\n\t\tc.rd.Reset()\n\t\tc.reader = bufio.NewReader(c.rd)\n\t}()\n\n\twaitVideo, waitAudio := true, true\n\ttimeout := time.Now().Add(core.ProbeTimeout)\n\n\tfor (waitVideo || waitAudio) && time.Now().Before(timeout) {\n\t\theader, body, err := mpjpeg.Next(c.reader)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tvar media *core.Media\n\n\t\tct := header.Get(\"Content-Type\")\n\t\tswitch ct {\n\t\tcase MimeVideo:\n\t\t\tif !waitVideo {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\twaitVideo = false\n\n\t\t\tbody = annexb.EncodeToAVCC(body)\n\t\t\tcodec := h264.AVCCToCodec(body)\n\t\t\tmedia = &core.Media{\n\t\t\t\tKind:      core.KindVideo,\n\t\t\t\tDirection: core.DirectionRecvonly,\n\t\t\t\tCodecs:    []*core.Codec{codec},\n\t\t\t}\n\n\t\tcase MimeG711U:\n\t\t\tif !waitAudio {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\twaitAudio = false\n\n\t\t\tmedia = &core.Media{\n\t\t\t\tKind:      core.KindAudio,\n\t\t\t\tDirection: core.DirectionRecvonly,\n\t\t\t\tCodecs: []*core.Codec{\n\t\t\t\t\t{\n\t\t\t\t\t\tName:      core.CodecPCMU,\n\t\t\t\t\t\tClockRate: 8000,\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t}\n\n\t\tdefault:\n\t\t\treturn errors.New(\"kasa: unsupported type: \" + ct)\n\t\t}\n\n\t\tc.Medias = append(c.Medias, media)\n\t}\n\n\treturn nil\n}\n\n// GetTimestamp - return timestamp in seconds\nfunc GetTimestamp(header http.Header) float64 {\n\tif s := header.Get(\"X-Timestamp\"); s != \"\" {\n\t\tif f, _ := strconv.ParseFloat(s, 32); f != 0 {\n\t\t\treturn f\n\t\t}\n\t}\n\n\treturn float64(time.Duration(time.Now().UnixNano()) / time.Second)\n}\n"
  },
  {
    "path": "pkg/magic/bitstream/producer.go",
    "content": "package bitstream\n\nimport (\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"io\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264/annexb\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h265\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Producer struct {\n\tcore.Connection\n\trd *core.ReadBuffer\n}\n\nfunc Open(r io.Reader) (*Producer, error) {\n\trd := core.NewReadBuffer(r)\n\n\tbuf, err := rd.Peek(256)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tbuf = annexb.EncodeToAVCC(buf) // won't break original buffer\n\n\tvar codec *core.Codec\n\tvar format string\n\n\tswitch {\n\tcase h264.NALUType(buf) == h264.NALUTypeSPS:\n\t\tcodec = h264.AVCCToCodec(buf)\n\t\tformat = \"h264\"\n\tcase h265.NALUType(buf) == h265.NALUTypeVPS:\n\t\tcodec = h265.AVCCToCodec(buf)\n\t\tformat = \"hevc\"\n\tdefault:\n\t\treturn nil, errors.New(\"bitstream: unsupported header: \" + hex.EncodeToString(buf[:8]))\n\t}\n\n\tmedias := []*core.Media{\n\t\t{\n\t\t\tKind:      core.KindVideo,\n\t\t\tDirection: core.DirectionRecvonly,\n\t\t\tCodecs:    []*core.Codec{codec},\n\t\t},\n\t}\n\treturn &Producer{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: format,\n\t\t\tMedias:     medias,\n\t\t\tTransport:  r,\n\t\t},\n\t\trd: rd,\n\t}, nil\n}\n\nfunc (c *Producer) Start() error {\n\tvar buf []byte\n\n\tb := make([]byte, core.BufferSize)\n\tfor {\n\t\tn, err := c.rd.Read(b)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tc.Recv += n\n\n\t\tbuf = append(buf, b[:n]...)\n\n\t\tfor {\n\t\t\ti := annexb.IndexFrame(buf)\n\t\t\tif i < 0 {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tif len(c.Receivers) > 0 {\n\t\t\t\tpkt := &rtp.Packet{\n\t\t\t\t\tHeader:  rtp.Header{Timestamp: core.Now90000()},\n\t\t\t\t\tPayload: annexb.EncodeToAVCC(buf[:i]),\n\t\t\t\t}\n\t\t\t\tc.Receivers[0].WriteRTP(pkt)\n\n\t\t\t\t//log.Printf(\"[AVC] %v, len: %d\", h264.Types(pkt.Payload), len(pkt.Payload))\n\t\t\t}\n\n\t\t\tbuf = buf[i:]\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/magic/keyframe.go",
    "content": "package magic\n\nimport (\n\t\"io\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264/annexb\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h265\"\n\t\"github.com/AlexxIT/go2rtc/pkg/mjpeg\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Keyframe struct {\n\tcore.Connection\n\twr *core.WriteBuffer\n}\n\n// Deprecated: should be rewritten\nfunc NewKeyframe() *Keyframe {\n\tmedias := []*core.Media{\n\t\t{\n\t\t\tKind:      core.KindVideo,\n\t\t\tDirection: core.DirectionSendonly,\n\t\t\tCodecs: []*core.Codec{\n\t\t\t\t{Name: core.CodecJPEG},\n\t\t\t\t{Name: core.CodecRAW},\n\t\t\t\t{Name: core.CodecH264},\n\t\t\t\t{Name: core.CodecH265},\n\t\t\t},\n\t\t},\n\t}\n\twr := core.NewWriteBuffer(nil)\n\treturn &Keyframe{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"keyframe\",\n\t\t\tMedias:     medias,\n\t\t\tTransport:  wr,\n\t\t},\n\t\twr: wr,\n\t}\n}\n\nfunc (k *Keyframe) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {\n\tsender := core.NewSender(media, track.Codec)\n\n\tswitch track.Codec.Name {\n\tcase core.CodecH264:\n\t\tsender.Handler = func(packet *rtp.Packet) {\n\t\t\tif !h264.IsKeyframe(packet.Payload) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tb := annexb.DecodeAVCC(packet.Payload, true)\n\t\t\tif n, err := k.wr.Write(b); err == nil {\n\t\t\t\tk.Send += n\n\t\t\t}\n\t\t}\n\n\t\tif track.Codec.IsRTP() {\n\t\t\tsender.Handler = h264.RTPDepay(track.Codec, sender.Handler)\n\t\t} else {\n\t\t\tsender.Handler = h264.RepairAVCC(track.Codec, sender.Handler)\n\t\t}\n\n\tcase core.CodecH265:\n\t\tsender.Handler = func(packet *rtp.Packet) {\n\t\t\tif !h265.IsKeyframe(packet.Payload) {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tb := annexb.DecodeAVCC(packet.Payload, true)\n\t\t\tif n, err := k.wr.Write(b); err == nil {\n\t\t\t\tk.Send += n\n\t\t\t}\n\t\t}\n\n\t\tif track.Codec.IsRTP() {\n\t\t\tsender.Handler = h265.RTPDepay(track.Codec, sender.Handler)\n\t\t}\n\n\tcase core.CodecJPEG:\n\t\tsender.Handler = func(packet *rtp.Packet) {\n\t\t\tif n, err := k.wr.Write(packet.Payload); err == nil {\n\t\t\t\tk.Send += n\n\t\t\t}\n\t\t}\n\n\t\tif track.Codec.IsRTP() {\n\t\t\tsender.Handler = mjpeg.RTPDepay(sender.Handler)\n\t\t}\n\n\tcase core.CodecRAW:\n\t\tsender.Handler = func(packet *rtp.Packet) {\n\t\t\tif n, err := k.wr.Write(packet.Payload); err == nil {\n\t\t\t\tk.Send += n\n\t\t\t}\n\t\t}\n\n\t\tsender.Handler = mjpeg.Encoder(track.Codec, 5, sender.Handler)\n\t}\n\n\tsender.HandleRTP(track)\n\tk.Senders = append(k.Senders, sender)\n\treturn nil\n}\n\nfunc (k *Keyframe) CodecName() string {\n\tif len(k.Senders) != 1 {\n\t\treturn \"\"\n\t}\n\treturn k.Senders[0].Codec.Name\n}\n\nfunc (k *Keyframe) WriteTo(wr io.Writer) (int64, error) {\n\treturn k.wr.WriteTo(wr)\n}\n"
  },
  {
    "path": "pkg/magic/mjpeg/producer.go",
    "content": "package mjpeg\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Producer struct {\n\tcore.Connection\n\trd *core.ReadBuffer\n}\n\nfunc Open(rd io.Reader) (*Producer, error) {\n\tmedias := []*core.Media{\n\t\t{\n\t\t\tKind:      core.KindVideo,\n\t\t\tDirection: core.DirectionRecvonly,\n\t\t\tCodecs: []*core.Codec{\n\t\t\t\t{\n\t\t\t\t\tName:        core.CodecJPEG,\n\t\t\t\t\tClockRate:   90000,\n\t\t\t\t\tPayloadType: core.PayloadTypeRAW,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\treturn &Producer{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"mjpeg\",\n\t\t\tMedias:     medias,\n\t\t\tTransport:  rd,\n\t\t},\n\t\trd: core.NewReadBuffer(rd),\n\t}, nil\n}\n\nfunc (c *Producer) Start() error {\n\tvar buf []byte                     // total bufer\n\tb := make([]byte, core.BufferSize) // reading buffer\n\n\tfor {\n\t\t// one JPEG end and next start\n\t\ti := bytes.Index(buf, []byte{0xFF, 0xD9, 0xFF, 0xD8})\n\t\tif i < 0 {\n\t\t\tn, err := c.rd.Read(b)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tc.Recv += n\n\n\t\t\tbuf = append(buf, b[:n]...)\n\n\t\t\t// if we receive frame\n\t\t\tif n >= 2 && b[n-2] == 0xFF && b[n-1] == 0xD9 {\n\t\t\t\ti = len(buf)\n\t\t\t} else {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t} else {\n\t\t\ti += 2\n\t\t}\n\n\t\tpkt := &rtp.Packet{\n\t\t\tHeader:  rtp.Header{Timestamp: core.Now90000()},\n\t\t\tPayload: buf[:i],\n\t\t}\n\t\tc.Receivers[0].WriteRTP(pkt)\n\n\t\t//log.Printf(\"[mjpeg] ts=%d size=%d\", pkt.Header.Timestamp, len(pkt.Payload))\n\n\t\tbuf = buf[i:]\n\t}\n}\n"
  },
  {
    "path": "pkg/magic/producer.go",
    "content": "package magic\n\nimport (\n\t\"bytes\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"io\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/aac\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/flv\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264/annexb\"\n\t\"github.com/AlexxIT/go2rtc/pkg/magic/bitstream\"\n\t\"github.com/AlexxIT/go2rtc/pkg/magic/mjpeg\"\n\t\"github.com/AlexxIT/go2rtc/pkg/mpegts\"\n\t\"github.com/AlexxIT/go2rtc/pkg/mpjpeg\"\n\t\"github.com/AlexxIT/go2rtc/pkg/wav\"\n\t\"github.com/AlexxIT/go2rtc/pkg/y4m\"\n)\n\nfunc Open(r io.Reader) (core.Producer, error) {\n\trd := core.NewReadBuffer(r)\n\n\tb, err := rd.Peek(4)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tswitch string(b) {\n\tcase annexb.StartCode:\n\t\treturn bitstream.Open(rd)\n\tcase wav.FourCC:\n\t\treturn wav.Open(rd)\n\tcase y4m.FourCC:\n\t\treturn y4m.Open(rd)\n\t}\n\n\tswitch string(b[:3]) {\n\tcase flv.Signature:\n\t\treturn flv.Open(rd)\n\t}\n\n\tswitch string(b[:2]) {\n\tcase \"\\xFF\\xD8\":\n\t\treturn mjpeg.Open(rd)\n\tcase \"\\xFF\\xF1\", \"\\xFF\\xF9\":\n\t\treturn aac.Open(rd)\n\tcase \"--\":\n\t\treturn mpjpeg.Open(rd)\n\t}\n\n\tswitch b[0] {\n\tcase mpegts.SyncByte:\n\t\treturn mpegts.Open(rd)\n\t}\n\n\t// support MJPEG with trash on start\n\t// https://github.com/AlexxIT/go2rtc/issues/747\n\tif b, err = rd.Peek(4096); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif i := bytes.Index(b, []byte{0xFF, 0xD8, 0xFF, 0xDB}); i > 0 {\n\t\t_, _ = io.ReadFull(rd, make([]byte, i))\n\t\treturn mjpeg.Open(rd)\n\t}\n\n\treturn nil, errors.New(\"magic: unsupported header: \" + hex.EncodeToString(b[:4]))\n}\n"
  },
  {
    "path": "pkg/mdns/README.md",
    "content": "# Useful links\n\n- https://grouper.ieee.org/groups/1722/contributions/2009/Bonjour%20Device%20Discovery.pdf"
  },
  {
    "path": "pkg/mdns/client.go",
    "content": "package mdns\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"strings\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/xnet\"\n\t\"github.com/miekg/dns\" // awesome library for parsing mDNS records\n)\n\nconst (\n\tServiceDNSSD = \"_services._dns-sd._udp.local.\"\n\tServiceHAP   = \"_hap._tcp.local.\" // HomeKit Accessory Protocol\n)\n\ntype ServiceEntry struct {\n\tName string            `json:\"name,omitempty\"`\n\tIP   net.IP            `json:\"ip,omitempty\"`\n\tPort uint16            `json:\"port,omitempty\"`\n\tInfo map[string]string `json:\"info,omitempty\"`\n}\n\nfunc (e *ServiceEntry) String() string {\n\tb, err := json.Marshal(e)\n\tif err != nil {\n\t\treturn err.Error()\n\t}\n\treturn string(b)\n}\n\nfunc (e *ServiceEntry) TXT() []string {\n\tvar txt []string\n\tfor k, v := range e.Info {\n\t\ttxt = append(txt, k+\"=\"+v)\n\t}\n\treturn txt\n}\n\nfunc (e *ServiceEntry) Complete() bool {\n\treturn e.IP != nil && e.Port > 0 && e.Info != nil\n}\n\nfunc (e *ServiceEntry) Addr() string {\n\treturn fmt.Sprintf(\"%s:%d\", e.IP, e.Port)\n}\n\nfunc (e *ServiceEntry) Host(service string) string {\n\treturn e.name() + \".\" + strings.TrimRight(service, \".\")\n}\n\nfunc (e *ServiceEntry) name() string {\n\tb := []byte(e.Name)\n\tfor i, c := range b {\n\t\tif 'a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' || '0' <= c && c <= '9' {\n\t\t\tcontinue\n\t\t}\n\t\tb[i] = '-'\n\t}\n\treturn string(b)\n}\n\nvar MulticastAddr = &net.UDPAddr{\n\tIP:   net.IP{224, 0, 0, 251},\n\tPort: 5353,\n}\n\nconst sendTimeout = time.Millisecond * 505\nconst respTimeout = time.Second * 3\n\n// BasicDiscovery - default golang Multicast UDP listener.\n// Does not work well with multiple interfaces.\nfunc BasicDiscovery(service string, onentry func(*ServiceEntry) bool) error {\n\tconn, err := net.ListenMulticastUDP(\"udp4\", nil, MulticastAddr)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tb := Browser{\n\t\tService:     service,\n\t\tAddr:        MulticastAddr,\n\t\tRecv:        conn,\n\t\tSends:       []net.PacketConn{conn},\n\t\tRecvTimeout: respTimeout,\n\t\tSendTimeout: sendTimeout,\n\t}\n\n\tdefer b.Close()\n\n\treturn b.Browse(onentry)\n}\n\n// Discovery - better discovery version. Works well with multiple interfaces.\nfunc Discovery(service string, onentry func(*ServiceEntry) bool) error {\n\tb := Browser{\n\t\tService:     service,\n\t\tAddr:        MulticastAddr,\n\t\tRecvTimeout: respTimeout,\n\t\tSendTimeout: sendTimeout,\n\t}\n\n\tif err := b.ListenMulticastUDP(); err != nil {\n\t\treturn err\n\t}\n\n\tdefer b.Close()\n\n\treturn b.Browse(onentry)\n}\n\n// Query - direct Discovery request on device IP-address. Works even over VPN.\nfunc Query(host, service string) (entry *ServiceEntry, err error) {\n\tconn, err := net.ListenPacket(\"udp4\", \":0\") // shouldn't use \":5353\"\n\tif err != nil {\n\t\treturn\n\t}\n\n\tbr := Browser{\n\t\tService: service,\n\t\tAddr: &net.UDPAddr{\n\t\t\tIP:   net.ParseIP(host),\n\t\t\tPort: 5353,\n\t\t},\n\t\tRecv:        conn,\n\t\tSends:       []net.PacketConn{conn},\n\t\tSendTimeout: time.Millisecond * 255,\n\t\tRecvTimeout: time.Second,\n\t}\n\n\tdefer br.Close()\n\n\terr = br.Browse(func(en *ServiceEntry) bool {\n\t\tentry = en\n\t\treturn true\n\t})\n\n\treturn\n}\n\n// QueryOrDiscovery - useful if we know previous device host and want\n// to update port or any other information. Will work even over VPN.\nfunc QueryOrDiscovery(host, service string, onentry func(*ServiceEntry) bool) error {\n\tentry, _ := Query(host, service)\n\tif entry != nil && onentry(entry) {\n\t\treturn nil\n\t}\n\n\treturn Discovery(service, onentry)\n}\n\ntype Browser struct {\n\tService string\n\n\tAddr  net.Addr\n\tNets  []*net.IPNet\n\tRecv  net.PacketConn\n\tSends []net.PacketConn\n\n\tRecvTimeout time.Duration\n\tSendTimeout time.Duration\n}\n\n// ListenMulticastUDP - creates multiple senders socket (each for IP4 interface).\n// And one receiver with multicast membership for each sender.\n// Receiver will get multicast responses on senders requests.\nfunc (b *Browser) ListenMulticastUDP() error {\n\t// 1. Collect IPv4 interfaces\n\tnets, err := xnet.IPNets(func(ip net.IP) bool {\n\t\treturn !xnet.Docker.Contains(ip)\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// 2. Create senders\n\tlc1 := net.ListenConfig{\n\t\tControl: func(network, address string, c syscall.RawConn) error {\n\t\t\treturn c.Control(func(fd uintptr) {\n\t\t\t\t// 1. Allow multicast UDP to listen concurrently across multiple listeners\n\t\t\t\t_ = SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1)\n\t\t\t})\n\t\t},\n\t}\n\n\tctx := context.Background()\n\n\tfor _, ipn := range nets {\n\t\tconn, err := lc1.ListenPacket(ctx, \"udp4\", ipn.IP.String()+\":5353\") // same port important\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tb.Nets = append(b.Nets, ipn)\n\t\tb.Sends = append(b.Sends, conn)\n\t}\n\n\tif b.Sends == nil {\n\t\treturn errors.New(\"no interfaces for listen\")\n\t}\n\n\t// 3. Create receiver\n\tlc2 := net.ListenConfig{\n\t\tControl: func(network, address string, c syscall.RawConn) error {\n\t\t\treturn c.Control(func(fd uintptr) {\n\t\t\t\t// 1. Allow multicast UDP to listen concurrently across multiple listeners\n\t\t\t\t_ = SetsockoptInt(fd, syscall.SOL_SOCKET, syscall.SO_REUSEADDR, 1)\n\n\t\t\t\t// 2. Disable loop responses\n\t\t\t\t_ = SetsockoptInt(fd, syscall.IPPROTO_IP, syscall.IP_MULTICAST_LOOP, 0)\n\n\t\t\t\t// 3. Allow receive multicast responses on all this addresses\n\t\t\t\tmreq := &syscall.IPMreq{\n\t\t\t\t\tMultiaddr: [4]byte{224, 0, 0, 251},\n\t\t\t\t}\n\t\t\t\t_ = SetsockoptIPMreq(fd, syscall.IPPROTO_IP, syscall.IP_ADD_MEMBERSHIP, mreq)\n\n\t\t\t\tfor _, send := range b.Sends {\n\t\t\t\t\taddr := send.LocalAddr().(*net.UDPAddr)\n\t\t\t\t\tmreq.Interface = [4]byte(addr.IP.To4())\n\t\t\t\t\t_ = SetsockoptIPMreq(fd, syscall.IPPROTO_IP, syscall.IP_ADD_MEMBERSHIP, mreq)\n\t\t\t\t}\n\t\t\t})\n\t\t},\n\t}\n\n\tb.Recv, err = lc2.ListenPacket(ctx, \"udp4\", \":5353\")\n\n\treturn err\n}\n\nfunc (b *Browser) Browse(onentry func(*ServiceEntry) bool) error {\n\tmsg := &dns.Msg{\n\t\tQuestion: []dns.Question{\n\t\t\t{Name: b.Service, Qtype: dns.TypePTR, Qclass: dns.ClassINET},\n\t\t},\n\t}\n\n\tquery, err := msg.Pack()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif err = b.Recv.SetDeadline(time.Now().Add(b.RecvTimeout)); err != nil {\n\t\treturn err\n\t}\n\n\tgo func() {\n\t\tfor {\n\t\t\tfor _, send := range b.Sends {\n\t\t\t\tif _, err := send.WriteTo(query, b.Addr); err != nil {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t}\n\t\t\ttime.Sleep(b.SendTimeout)\n\t\t}\n\t}()\n\n\tprocessed := map[string]struct{}{\"\": {}}\n\n\tb2 := make([]byte, 1500)\n\tfor {\n\t\t// in the Hass docker network can receive same msg from different address\n\t\tn, addr, err := b.Recv.ReadFrom(b2)\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\n\t\tif err = msg.Unpack(b2[:n]); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tptr := GetPTR(msg, b.Service)\n\n\t\tif _, ok := processed[ptr]; ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tip := addr.(*net.UDPAddr).IP\n\n\t\tfor _, entry := range NewServiceEntries(msg, ip) {\n\t\t\tif onentry(entry) {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\n\t\tprocessed[ptr] = struct{}{}\n\t}\n\n\treturn nil\n}\n\nfunc (b *Browser) Close() error {\n\tif b.Recv != nil {\n\t\t_ = b.Recv.Close()\n\t}\n\tfor _, send := range b.Sends {\n\t\t_ = send.Close()\n\t}\n\treturn nil\n}\n\nfunc GetPTR(msg *dns.Msg, service string) string {\n\tfor _, record := range msg.Answer {\n\t\tif ptr, ok := record.(*dns.PTR); ok && ptr.Hdr.Name == service {\n\t\t\treturn ptr.Ptr\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc NewServiceEntries(msg *dns.Msg, ip net.IP) (entries []*ServiceEntry) {\n\trecords := make([]dns.RR, 0, len(msg.Answer)+len(msg.Ns)+len(msg.Extra))\n\trecords = append(records, msg.Answer...)\n\trecords = append(records, msg.Ns...)\n\trecords = append(records, msg.Extra...)\n\n\t// PTR ptr=SomeName._hap._tcp.local. hdr=_hap._tcp.local.\n\t// TXT txt=...                       hdr=SomeName._hap._tcp.local.\n\t// SRV target=SomeName.local.        hdr=SomeName._hap._tcp.local.\n\t// A   a=192.168.1.123               hdr=SomeName.local.\n\n\tfor _, record := range records {\n\t\tptr, ok := record.(*dns.PTR)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tentry := &ServiceEntry{}\n\n\t\tif i := strings.IndexByte(ptr.Ptr, '.'); i > 0 {\n\t\t\tentry.Name = strings.ReplaceAll(ptr.Ptr[:i], `\\ `, \" \")\n\t\t}\n\n\t\tvar txt *dns.TXT\n\t\tvar srv *dns.SRV\n\t\tvar a *dns.A\n\n\t\tfor _, record = range records {\n\t\t\tif txt, ok = record.(*dns.TXT); ok && txt.Hdr.Name == ptr.Ptr {\n\t\t\t\tentry.Info = make(map[string]string, len(txt.Txt))\n\t\t\t\tfor _, s := range txt.Txt {\n\t\t\t\t\tk, v, _ := strings.Cut(s, \"=\")\n\t\t\t\t\tentry.Info[k] = v\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tfor _, record = range records {\n\t\t\tif srv, ok = record.(*dns.SRV); ok && srv.Hdr.Name == ptr.Ptr {\n\t\t\t\tentry.Port = srv.Port\n\n\t\t\t\tfor _, record = range records {\n\t\t\t\t\tif a, ok = record.(*dns.A); ok && a.Hdr.Name == srv.Target {\n\t\t\t\t\t\t// device can send multiple IP addresses (ex. Homebridge)\n\t\t\t\t\t\t// use first IP from the list or same IP from sender\n\t\t\t\t\t\tif entry.IP == nil || ip.Equal(a.A) {\n\t\t\t\t\t\t\tentry.IP = a.A\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tentries = append(entries, entry)\n\t}\n\n\treturn\n}\n"
  },
  {
    "path": "pkg/mdns/mdns_test.go",
    "content": "package mdns\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestDiscovery(t *testing.T) {\n\tonentry := func(entry *ServiceEntry) bool {\n\t\treturn true\n\t}\n\terr := Discovery(ServiceHAP, onentry)\n\t//err := Discovery(\"_ewelink._tcp.local.\", time.Second, onentry)\n\t// err := Discovery(\"_googlecast._tcp.local.\", time.Second, onentry)\n\trequire.Nil(t, err)\n}\n"
  },
  {
    "path": "pkg/mdns/server.go",
    "content": "package mdns\n\nimport (\n\t\"net\"\n\n\t\"github.com/miekg/dns\"\n)\n\n// ClassCacheFlush https://datatracker.ietf.org/doc/html/rfc6762#section-10.2\nconst ClassCacheFlush = 0x8001\n\nfunc Serve(service string, entries []*ServiceEntry) error {\n\tb := Browser{Service: service}\n\n\tif err := b.ListenMulticastUDP(); err != nil {\n\t\treturn err\n\t}\n\n\treturn b.Serve(entries)\n}\n\nfunc (b *Browser) Serve(entries []*ServiceEntry) error {\n\tnames := make(map[string]*ServiceEntry, len(entries))\n\tfor _, entry := range entries {\n\t\tname := entry.name() + \".\" + b.Service\n\t\tnames[name] = entry\n\t}\n\n\tbuf := make([]byte, 1500)\n\tfor {\n\t\tn, addr, err := b.Recv.ReadFrom(buf)\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\n\t\tvar req dns.Msg // request\n\t\tif err = req.Unpack(buf[:n]); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// skip messages without Questions\n\t\tif req.Question == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tremoteIP := addr.(*net.UDPAddr).IP\n\t\tlocalIP := b.MatchLocalIP(remoteIP)\n\n\t\t// skip messages from unknown networks (can be docker network)\n\t\tif localIP == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar res dns.Msg // response\n\t\tfor _, q := range req.Question {\n\t\t\tif q.Qtype != dns.TypePTR || q.Qclass != dns.ClassINET {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif q.Name == ServiceDNSSD {\n\t\t\t\tAppendDNSSD(&res, b.Service)\n\t\t\t} else if q.Name == b.Service {\n\t\t\t\tfor _, entry := range entries {\n\t\t\t\t\tAppendEntry(&res, entry, b.Service, localIP)\n\t\t\t\t}\n\t\t\t} else if entry, ok := names[q.Name]; ok {\n\t\t\t\tAppendEntry(&res, entry, b.Service, localIP)\n\t\t\t}\n\t\t}\n\n\t\tif res.Answer == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tres.MsgHdr.Response = true\n\t\tres.MsgHdr.Authoritative = true\n\n\t\tdata, err := res.Pack()\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, send := range b.Sends {\n\t\t\t_, _ = send.WriteTo(data, MulticastAddr)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (b *Browser) MatchLocalIP(remote net.IP) net.IP {\n\tfor _, ipn := range b.Nets {\n\t\tif ipn.Contains(remote) {\n\t\t\treturn ipn.IP\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc AppendDNSSD(msg *dns.Msg, service string) {\n\tmsg.Answer = append(\n\t\tmsg.Answer,\n\t\t&dns.PTR{\n\t\t\tHdr: dns.RR_Header{\n\t\t\t\tName:   ServiceDNSSD,  // _services._dns-sd._udp.local.\n\t\t\t\tRrtype: dns.TypePTR,   // 12\n\t\t\t\tClass:  dns.ClassINET, // 1\n\t\t\t\tTtl:    4500,\n\t\t\t},\n\t\t\tPtr: service, // _home-assistant._tcp.local.\n\t\t},\n\t)\n}\n\nfunc AppendEntry(msg *dns.Msg, entry *ServiceEntry, service string, ip net.IP) {\n\tptrName := entry.name() + \".\" + service\n\tsrvName := entry.name() + \".local.\"\n\n\tmsg.Answer = append(\n\t\tmsg.Answer,\n\t\t&dns.PTR{\n\t\t\tHdr: dns.RR_Header{\n\t\t\t\tName:   service,       // _home-assistant._tcp.local.\n\t\t\t\tRrtype: dns.TypePTR,   // 12\n\t\t\t\tClass:  dns.ClassINET, // 1\n\t\t\t\tTtl:    4500,\n\t\t\t},\n\t\t\tPtr: ptrName, // Home\\ Assistant._home-assistant._tcp.local.\n\t\t},\n\t)\n\tmsg.Extra = append(\n\t\tmsg.Extra,\n\t\t&dns.TXT{\n\t\t\tHdr: dns.RR_Header{\n\t\t\t\tName:   ptrName,         // Home\\ Assistant._home-assistant._tcp.local.\n\t\t\t\tRrtype: dns.TypeTXT,     // 16\n\t\t\t\tClass:  ClassCacheFlush, // 32769\n\t\t\t\tTtl:    4500,\n\t\t\t},\n\t\t\tTxt: entry.TXT(),\n\t\t},\n\t\t&dns.SRV{\n\t\t\tHdr: dns.RR_Header{\n\t\t\t\tName:   ptrName,         // Home\\ Assistant._home-assistant._tcp.local.\n\t\t\t\tRrtype: dns.TypeSRV,     // 33\n\t\t\t\tClass:  ClassCacheFlush, // 32769\n\t\t\t\tTtl:    120,\n\t\t\t},\n\t\t\tPort:   entry.Port, // 8123\n\t\t\tTarget: srvName,    // 963f1fa82b7142809711cebe7c826322.local.\n\t\t},\n\t\t&dns.A{\n\t\t\tHdr: dns.RR_Header{\n\t\t\t\tName:   srvName,         // 963f1fa82b7142809711cebe7c826322.local.\n\t\t\t\tRrtype: dns.TypeA,       // 1\n\t\t\t\tClass:  ClassCacheFlush, // 32769\n\t\t\t\tTtl:    120,\n\t\t\t},\n\t\t\tA: ip,\n\t\t},\n\t)\n}\n"
  },
  {
    "path": "pkg/mdns/syscall.go",
    "content": "//go:build !(darwin || ios || freebsd || openbsd || netbsd || dragonfly || windows)\n\npackage mdns\n\nimport (\n\t\"syscall\"\n)\n\nfunc SetsockoptInt(fd uintptr, level, opt int, value int) (err error) {\n\treturn syscall.SetsockoptInt(int(fd), level, opt, value)\n}\n\nfunc SetsockoptIPMreq(fd uintptr, level, opt int, mreq *syscall.IPMreq) (err error) {\n\treturn syscall.SetsockoptIPMreq(int(fd), level, opt, mreq)\n}\n"
  },
  {
    "path": "pkg/mdns/syscall_bsd.go",
    "content": "//go:build darwin || ios || freebsd || openbsd || netbsd || dragonfly\n\npackage mdns\n\nimport (\n\t\"syscall\"\n)\n\nfunc SetsockoptInt(fd uintptr, level, opt int, value int) (err error) {\n\t// change SO_REUSEADDR and REUSEPORT flags simultaneously for BSD-like OS\n\t// https://github.com/AlexxIT/go2rtc/issues/626\n\t// https://stackoverflow.com/questions/14388706/how-do-so-reuseaddr-and-so-reuseport-differ/14388707\n\tif opt == syscall.SO_REUSEADDR {\n\t\tif err = syscall.SetsockoptInt(int(fd), level, opt, value); err != nil {\n\t\t\treturn\n\t\t}\n\n\t\topt = syscall.SO_REUSEPORT\n\t}\n\n\treturn syscall.SetsockoptInt(int(fd), level, opt, value)\n}\n\nfunc SetsockoptIPMreq(fd uintptr, level, opt int, mreq *syscall.IPMreq) (err error) {\n\treturn syscall.SetsockoptIPMreq(int(fd), level, opt, mreq)\n}\n"
  },
  {
    "path": "pkg/mdns/syscall_windows.go",
    "content": "//go:build windows\n\npackage mdns\n\nimport \"syscall\"\n\nfunc SetsockoptInt(fd uintptr, level, opt int, value int) (err error) {\n\treturn syscall.SetsockoptInt(syscall.Handle(fd), level, opt, value)\n}\n\nfunc SetsockoptIPMreq(fd uintptr, level, opt int, mreq *syscall.IPMreq) (err error) {\n\treturn syscall.SetsockoptIPMreq(syscall.Handle(fd), level, opt, mreq)\n}\n"
  },
  {
    "path": "pkg/mjpeg/README.md",
    "content": "## Useful links\n\n- https://www.rfc-editor.org/rfc/rfc2435\n- https://github.com/GStreamer/gst-plugins-good/blob/master/gst/rtp/gstrtpjpegdepay.c\n- https://mjpeg.sanford.io/\n"
  },
  {
    "path": "pkg/mjpeg/consumer.go",
    "content": "package mjpeg\n\nimport (\n\t\"io\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Consumer struct {\n\tcore.Connection\n\twr *core.WriteBuffer\n}\n\nfunc NewConsumer() *Consumer {\n\tmedias := []*core.Media{\n\t\t{\n\t\t\tKind:      core.KindVideo,\n\t\t\tDirection: core.DirectionSendonly,\n\t\t\tCodecs: []*core.Codec{\n\t\t\t\t{Name: core.CodecJPEG},\n\t\t\t\t{Name: core.CodecRAW},\n\t\t\t},\n\t\t},\n\t}\n\twr := core.NewWriteBuffer(nil)\n\treturn &Consumer{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"mjpeg\",\n\t\t\tMedias:     medias,\n\t\t\tTransport:  wr,\n\t\t},\n\t\twr: wr,\n\t}\n}\n\nfunc (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {\n\tsender := core.NewSender(media, track.Codec)\n\tsender.Handler = func(packet *rtp.Packet) {\n\t\tif n, err := c.wr.Write(packet.Payload); err == nil {\n\t\t\tc.Send += n\n\t\t}\n\t}\n\n\tif track.Codec.IsRTP() {\n\t\tsender.Handler = RTPDepay(sender.Handler)\n\t} else if track.Codec.Name == core.CodecRAW {\n\t\tsender.Handler = Encoder(track.Codec, 0, sender.Handler)\n\t}\n\n\tsender.HandleRTP(track)\n\tc.Senders = append(c.Senders, sender)\n\treturn nil\n}\n\nfunc (c *Consumer) WriteTo(wr io.Writer) (int64, error) {\n\treturn c.wr.WriteTo(wr)\n}\n"
  },
  {
    "path": "pkg/mjpeg/helpers.go",
    "content": "package mjpeg\n\nimport (\n\t\"bytes\"\n\t\"image/jpeg\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/y4m\"\n\t\"github.com/pion/rtp\"\n)\n\nfunc FixJPEG(b []byte) []byte {\n\t// skip non-JPEG\n\tif len(b) < 10 || b[0] != 0xFF || b[1] != markerSOI {\n\t\treturn b\n\t}\n\n\t// skip JPEG without app marker\n\tif b[2] == 0xFF && b[3] == markerDQT {\n\t\treturn b\n\t}\n\n\tswitch string(b[6:10]) {\n\tcase \"JFIF\", \"Exif\":\n\t\t// skip if header OK for imghdr library\n\t\t// - https://docs.python.org/3/library/imghdr.html\n\t\treturn b\n\tcase \"AVI1\":\n\t\t// adds DHT tables to JPEG file before SOS marker\n\t\t// useful when you want to save a JPEG frame from an MJPEG stream\n\t\t// - https://github.com/image-rs/jpeg-decoder/issues/76\n\t\t// - https://github.com/pion/mediadevices/pull/493\n\t\t// - https://bugzilla.mozilla.org/show_bug.cgi?id=963907#c18\n\t\treturn InjectDHT(b)\n\t}\n\n\t// reencode JPEG if it has wrong header\n\t//\n\t// for example, this app produce \"bad\" images:\n\t// https://github.com/jacksonliam/mjpg-streamer\n\t//\n\t// and they can't be uploaded to the Telegram servers:\n\t// {\"ok\":false,\"error_code\":400,\"description\":\"Bad Request: IMAGE_PROCESS_FAILED\"}\n\timg, err := jpeg.Decode(bytes.NewReader(b))\n\tif err != nil {\n\t\treturn b\n\t}\n\tbuf := bytes.NewBuffer(nil)\n\tif err = jpeg.Encode(buf, img, nil); err != nil {\n\t\treturn b\n\t}\n\treturn buf.Bytes()\n}\n\n// Encoder convert YUV frame to Img.\n// Support skipping empty frames, for example if USB cam needs time to start.\nfunc Encoder(codec *core.Codec, skipEmpty int, handler core.HandlerFunc) core.HandlerFunc {\n\tnewImage := y4m.NewImage(codec.FmtpLine)\n\n\treturn func(packet *rtp.Packet) {\n\t\timg := newImage(packet.Payload)\n\n\t\tif skipEmpty != 0 && y4m.HasSameColor(img) {\n\t\t\tskipEmpty--\n\t\t\treturn\n\t\t}\n\n\t\tbuf := bytes.NewBuffer(nil)\n\t\tif err := jpeg.Encode(buf, img, nil); err != nil {\n\t\t\treturn\n\t\t}\n\n\t\tclone := *packet\n\t\tclone.Payload = buf.Bytes()\n\t\thandler(&clone)\n\t}\n}\n\nconst dhtSize = 432 // known size for 4 default tables\n\nfunc InjectDHT(b []byte) []byte {\n\tif bytes.Index(b, []byte{0xFF, markerDHT}) > 0 {\n\t\treturn b // already exist\n\t}\n\n\ti := bytes.Index(b, []byte{0xFF, markerSOS})\n\tif i < 0 {\n\t\treturn b\n\t}\n\n\tdht := make([]byte, 0, dhtSize)\n\tdht = MakeHuffmanHeaders(dht)\n\n\ttmp := make([]byte, len(b)+dhtSize)\n\tcopy(tmp, b[:i])\n\tcopy(tmp[i:], dht)\n\tcopy(tmp[i+dhtSize:], b[i:])\n\n\treturn tmp\n}\n"
  },
  {
    "path": "pkg/mjpeg/jpeg.go",
    "content": "package mjpeg\n\nconst (\n\tmarkerSOF = 0xC0 // Start Of Frame (Baseline Sequential)\n\tmarkerSOI = 0xD8 // Start Of Image\n\tmarkerEOI = 0xD9 // End Of Image\n\tmarkerSOS = 0xDA // Start Of Scan\n\tmarkerDQT = 0xDB // Define Quantization Table\n\tmarkerDHT = 0xC4 // Define Huffman Table\n)\n"
  },
  {
    "path": "pkg/mjpeg/mjpeg_test.go",
    "content": "package mjpeg\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestRFC2435(t *testing.T) {\n\tlqt, cqt := MakeTables(71)\n\trequire.Equal(t, byte(9), lqt[0])\n\trequire.Equal(t, byte(10), cqt[0])\n}\n"
  },
  {
    "path": "pkg/mjpeg/rfc2435.go",
    "content": "package mjpeg\n\n// RFC 2435. Appendix A\n\n// don't know why two tables are not respect RFC\n// https://github.com/FFmpeg/FFmpeg/blob/master/libavformat/rtpdec_jpeg.c\n\nvar jpeg_luma_quantizer = [64]byte{\n\t16, 11, 12, 14, 12, 10, 16, 14,\n\t13, 14, 18, 17, 16, 19, 24, 40,\n\t26, 24, 22, 22, 24, 49, 35, 37,\n\t29, 40, 58, 51, 61, 60, 57, 51,\n\t56, 55, 64, 72, 92, 78, 64, 68,\n\t87, 69, 55, 56, 80, 109, 81, 87,\n\t95, 98, 103, 104, 103, 62, 77, 113,\n\t121, 112, 100, 120, 92, 101, 103, 99,\n}\nvar jpeg_chroma_quantizer = [64]byte{\n\t17, 18, 18, 24, 21, 24, 47, 26,\n\t26, 47, 99, 66, 56, 66, 99, 99,\n\t99, 99, 99, 99, 99, 99, 99, 99,\n\t99, 99, 99, 99, 99, 99, 99, 99,\n\t99, 99, 99, 99, 99, 99, 99, 99,\n\t99, 99, 99, 99, 99, 99, 99, 99,\n\t99, 99, 99, 99, 99, 99, 99, 99,\n\t99, 99, 99, 99, 99, 99, 99, 99,\n}\n\nfunc MakeTables(q byte) (lqt, cqt []byte) {\n\tvar factor int\n\n\tswitch {\n\tcase q < 1:\n\t\tfactor = 1\n\tcase q > 99:\n\t\tfactor = 99\n\tdefault:\n\t\tfactor = int(q)\n\t}\n\n\tif q < 50 {\n\t\tfactor = 5000 / factor\n\t} else {\n\t\tfactor = 200 - factor*2\n\t}\n\n\tlqt = make([]byte, 64)\n\tcqt = make([]byte, 64)\n\n\tfor i := 0; i < 64; i++ {\n\t\tlq := (int(jpeg_luma_quantizer[i])*factor + 50) / 100\n\t\tcq := (int(jpeg_chroma_quantizer[i])*factor + 50) / 100\n\n\t\t/* Limit the quantizers to 1 <= q <= 255 */\n\t\tswitch {\n\t\tcase lq < 1:\n\t\t\tlqt[i] = 1\n\t\tcase lq > 255:\n\t\t\tlqt[i] = 255\n\t\tdefault:\n\t\t\tlqt[i] = byte(lq)\n\t\t}\n\n\t\tswitch {\n\t\tcase cq < 1:\n\t\t\tcqt[i] = 1\n\t\tcase cq > 255:\n\t\t\tcqt[i] = 255\n\t\tdefault:\n\t\t\tcqt[i] = byte(cq)\n\t\t}\n\t}\n\n\treturn\n}\n\n// RFC 2435. Appendix B\n\nvar lum_dc_codelens = []byte{\n\t0, 1, 5, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0,\n}\nvar lum_dc_symbols = []byte{\n\t0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,\n}\nvar lum_ac_codelens = []byte{\n\t0, 2, 1, 3, 3, 2, 4, 3, 5, 5, 4, 4, 0, 0, 1, 0x7d,\n}\nvar lum_ac_symbols = []byte{\n\t0x01, 0x02, 0x03, 0x00, 0x04, 0x11, 0x05, 0x12,\n\t0x21, 0x31, 0x41, 0x06, 0x13, 0x51, 0x61, 0x07,\n\t0x22, 0x71, 0x14, 0x32, 0x81, 0x91, 0xa1, 0x08,\n\t0x23, 0x42, 0xb1, 0xc1, 0x15, 0x52, 0xd1, 0xf0,\n\t0x24, 0x33, 0x62, 0x72, 0x82, 0x09, 0x0a, 0x16,\n\t0x17, 0x18, 0x19, 0x1a, 0x25, 0x26, 0x27, 0x28,\n\t0x29, 0x2a, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39,\n\t0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49,\n\t0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58, 0x59,\n\t0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68, 0x69,\n\t0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79,\n\t0x7a, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88, 0x89,\n\t0x8a, 0x92, 0x93, 0x94, 0x95, 0x96, 0x97, 0x98,\n\t0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5, 0xa6, 0xa7,\n\t0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4, 0xb5, 0xb6,\n\t0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3, 0xc4, 0xc5,\n\t0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2, 0xd3, 0xd4,\n\t0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda, 0xe1, 0xe2,\n\t0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9, 0xea,\n\t0xf1, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,\n\t0xf9, 0xfa,\n}\nvar chm_dc_codelens = []byte{\n\t0, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0,\n}\nvar chm_dc_symbols = []byte{\n\t0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11,\n}\nvar chm_ac_codelens = []byte{\n\t0, 2, 1, 2, 4, 4, 3, 4, 7, 5, 4, 4, 0, 1, 2, 0x77,\n}\nvar chm_ac_symbols = []byte{\n\t0x00, 0x01, 0x02, 0x03, 0x11, 0x04, 0x05, 0x21,\n\t0x31, 0x06, 0x12, 0x41, 0x51, 0x07, 0x61, 0x71,\n\t0x13, 0x22, 0x32, 0x81, 0x08, 0x14, 0x42, 0x91,\n\t0xa1, 0xb1, 0xc1, 0x09, 0x23, 0x33, 0x52, 0xf0,\n\t0x15, 0x62, 0x72, 0xd1, 0x0a, 0x16, 0x24, 0x34,\n\t0xe1, 0x25, 0xf1, 0x17, 0x18, 0x19, 0x1a, 0x26,\n\t0x27, 0x28, 0x29, 0x2a, 0x35, 0x36, 0x37, 0x38,\n\t0x39, 0x3a, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48,\n\t0x49, 0x4a, 0x53, 0x54, 0x55, 0x56, 0x57, 0x58,\n\t0x59, 0x5a, 0x63, 0x64, 0x65, 0x66, 0x67, 0x68,\n\t0x69, 0x6a, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78,\n\t0x79, 0x7a, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87,\n\t0x88, 0x89, 0x8a, 0x92, 0x93, 0x94, 0x95, 0x96,\n\t0x97, 0x98, 0x99, 0x9a, 0xa2, 0xa3, 0xa4, 0xa5,\n\t0xa6, 0xa7, 0xa8, 0xa9, 0xaa, 0xb2, 0xb3, 0xb4,\n\t0xb5, 0xb6, 0xb7, 0xb8, 0xb9, 0xba, 0xc2, 0xc3,\n\t0xc4, 0xc5, 0xc6, 0xc7, 0xc8, 0xc9, 0xca, 0xd2,\n\t0xd3, 0xd4, 0xd5, 0xd6, 0xd7, 0xd8, 0xd9, 0xda,\n\t0xe2, 0xe3, 0xe4, 0xe5, 0xe6, 0xe7, 0xe8, 0xe9,\n\t0xea, 0xf2, 0xf3, 0xf4, 0xf5, 0xf6, 0xf7, 0xf8,\n\t0xf9, 0xfa,\n}\n\nfunc MakeHeaders(p []byte, t byte, w, h uint16, lqt, cqt []byte) []byte {\n\t// Appendix A from https://www.rfc-editor.org/rfc/rfc2435\n\tp = append(p, 0xFF, markerSOI)\n\n\tp = MakeQuantHeader(p, lqt, 0)\n\tp = MakeQuantHeader(p, cqt, 1)\n\n\tif t == 0 {\n\t\tt = 0x21 // hsamp = 2, vsamp = 1\n\t} else {\n\t\tt = 0x22 // hsamp = 2, vsamp = 2\n\t}\n\n\tp = append(p, 0xFF, markerSOF,\n\t\t0, 17, // size\n\t\t8, // bits per component\n\t\tbyte(h>>8), byte(h&0xFF),\n\t\tbyte(w>>8), byte(w&0xFF),\n\t\t3, // number of components\n\t\t0, // comp 0\n\t\tt,\n\t\t0,    // quant table 0\n\t\t1,    // comp 1\n\t\t0x11, // hsamp = 1, vsamp = 1\n\t\t1,    // quant table 1\n\t\t2,    // comp 2\n\t\t0x11, // hsamp = 1, vsamp = 1\n\t\t1,    // quant table 1\n\t)\n\n\tp = MakeHuffmanHeaders(p)\n\n\treturn append(p, 0xFF, markerSOS,\n\t\t0, 12, // size\n\t\t3,    // 3 components\n\t\t0,    // comp 0\n\t\t0,    // huffman table 0\n\t\t1,    // comp 1\n\t\t0x11, // huffman table 1\n\t\t2,    // comp 2\n\t\t0x11, // huffman table 1\n\t\t0,    // first DCT coeff\n\t\t63,   // last DCT coeff\n\t\t0,    // sucessive approx\n\t)\n}\n\nfunc MakeQuantHeader(p []byte, qt []byte, tableNo byte) []byte {\n\tp = append(p, 0xFF, markerDQT, 0, 67, tableNo)\n\treturn append(p, qt...)\n}\n\nfunc MakeHuffmanHeader(p, codelens, symbols []byte, tableNo, tableClass byte) []byte {\n\tp = append(p, 0xFF, markerDHT,\n\t\t0, byte(3+len(codelens)+len(symbols)), // size\n\t\t(tableClass<<4)|tableNo,\n\t)\n\tp = append(p, codelens...)\n\treturn append(p, symbols...)\n}\n\nfunc MakeHuffmanHeaders(p []byte) []byte {\n\tp = MakeHuffmanHeader(p, lum_dc_codelens, lum_dc_symbols, 0, 0)\n\tp = MakeHuffmanHeader(p, lum_ac_codelens, lum_ac_symbols, 0, 1)\n\tp = MakeHuffmanHeader(p, chm_dc_codelens, chm_dc_symbols, 1, 0)\n\tp = MakeHuffmanHeader(p, chm_ac_codelens, chm_ac_symbols, 1, 1)\n\treturn p\n}\n"
  },
  {
    "path": "pkg/mjpeg/rtp.go",
    "content": "package mjpeg\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"image\"\n\t\"image/jpeg\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/pion/rtp\"\n)\n\nfunc RTPDepay(handlerFunc core.HandlerFunc) core.HandlerFunc {\n\tbuf := make([]byte, 0, 512*1024) // 512K\n\n\treturn func(packet *rtp.Packet) {\n\t\t//log.Printf(\"[RTP] codec: %s, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, mark: %v\", track.Codec.Name, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)\n\n\t\t// https://www.rfc-editor.org/rfc/rfc2435#section-3.1\n\t\tb := packet.Payload\n\n\t\t// 3.1.  JPEG header\n\t\tt := b[4]\n\n\t\t// 3.1.7.  Restart Marker header\n\t\tif 64 <= t && t <= 127 {\n\t\t\tb = b[12:] // skip it\n\t\t} else {\n\t\t\tb = b[8:]\n\t\t}\n\n\t\tif len(buf) == 0 {\n\t\t\tvar lqt, cqt []byte\n\n\t\t\t// 3.1.8.  Quantization Table header\n\t\t\tq := packet.Payload[5]\n\t\t\tif q >= 128 {\n\t\t\t\tlqt = b[4:68]\n\t\t\t\tcqt = b[68:132]\n\t\t\t\tb = b[132:]\n\t\t\t} else {\n\t\t\t\tlqt, cqt = MakeTables(q)\n\t\t\t}\n\n\t\t\t// https://www.rfc-editor.org/rfc/rfc2435#section-3.1.5\n\t\t\t// The maximum width is 2040 pixels.\n\t\t\tw := uint16(packet.Payload[6]) << 3\n\t\t\th := uint16(packet.Payload[7]) << 3\n\n\t\t\t// fix sizes more than 2040\n\t\t\tswitch {\n\t\t\t// 512x1920 512x1440\n\t\t\tcase w == cutSize(2560) && (h == 1920 || h == 1440):\n\t\t\t\tw = 2560\n\t\t\t// 1792x112\n\t\t\tcase w == cutSize(3840) && h == cutSize(2160):\n\t\t\t\tw = 3840\n\t\t\t\th = 2160\n\t\t\t// 256x1296\n\t\t\tcase w == cutSize(2304) && h == 1296:\n\t\t\t\tw = 2304\n\t\t\t}\n\n\t\t\t//fmt.Printf(\"t: %d, q: %d, w: %d, h: %d\\n\", t, q, w, h)\n\t\t\tbuf = MakeHeaders(buf, t, w, h, lqt, cqt)\n\t\t}\n\n\t\t// 3.1.9.  JPEG Payload\n\t\tbuf = append(buf, b...)\n\n\t\tif !packet.Marker {\n\t\t\treturn\n\t\t}\n\n\t\tif end := buf[len(buf)-2:]; end[0] != 0xFF && end[1] != 0xD9 {\n\t\t\tbuf = append(buf, 0xFF, 0xD9)\n\t\t}\n\n\t\tclone := *packet\n\t\tclone.Payload = buf\n\n\t\tbuf = buf[:0] // clear buffer\n\n\t\thandlerFunc(&clone)\n\t}\n}\n\nfunc cutSize(size uint16) uint16 {\n\treturn ((size >> 3) & 0xFF) << 3\n}\n\nfunc RTPPay(handlerFunc core.HandlerFunc) core.HandlerFunc {\n\tconst packetSize = 1436\n\n\tsequencer := rtp.NewRandomSequencer()\n\n\treturn func(packet *rtp.Packet) {\n\t\t// reincode image to more common form\n\t\tp, err := Transcode(packet.Payload)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\n\t\th1 := make([]byte, 8)\n\t\th1[4] = 1   // Type\n\t\th1[5] = 255 // Q\n\n\t\t// MBZ=0, Precision=0, Length=128\n\t\th2 := make([]byte, 4, 132)\n\t\th2[3] = 128\n\n\t\tvar jpgData []byte\n\t\tfor jpgData == nil {\n\t\t\t// 2 bytes h1\n\t\t\tif p[0] != 0xFF {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tsize := binary.BigEndian.Uint16(p[2:]) + 2\n\n\t\t\t// 2 bytes payload size (include 2 bytes)\n\t\t\tswitch p[1] {\n\t\t\tcase 0xD8: // 0. Start Of Image (size=0)\n\t\t\t\tp = p[2:]\n\t\t\t\tcontinue\n\t\t\tcase 0xDB: // 1. Define Quantization Table (size=130)\n\t\t\t\tfor i := uint16(4 + 1); i < size; i += 1 + 64 {\n\t\t\t\t\th2 = append(h2, p[i:i+64]...)\n\t\t\t\t}\n\t\t\tcase 0xC0: // 2. Start Of Frame (size=15)\n\t\t\t\tif p[4] != 8 {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\th := binary.BigEndian.Uint16(p[5:])\n\t\t\t\tw := binary.BigEndian.Uint16(p[7:])\n\t\t\t\th1[6] = uint8(w >> 3)\n\t\t\t\th1[7] = uint8(h >> 3)\n\t\t\tcase 0xC4: // 3. Define Huffman Table (size=416)\n\t\t\tcase 0xDA: // 4. Start Of Scan (size=10)\n\t\t\t\tjpgData = p[size:]\n\t\t\t}\n\n\t\t\tp = p[size:]\n\t\t}\n\n\t\toffset := 0\n\t\tp = make([]byte, 0)\n\n\t\tfor jpgData != nil {\n\t\t\tp = p[:0]\n\n\t\t\tif offset > 0 {\n\t\t\t\th1[1] = byte(offset >> 16)\n\t\t\t\th1[2] = byte(offset >> 8)\n\t\t\t\th1[3] = byte(offset)\n\t\t\t\tp = append(p, h1...)\n\t\t\t} else {\n\t\t\t\tp = append(p, h1...)\n\t\t\t\tp = append(p, h2...)\n\t\t\t}\n\n\t\t\tdataLen := packetSize - len(p)\n\t\t\tif dataLen < len(jpgData) {\n\t\t\t\tp = append(p, jpgData[:dataLen]...)\n\t\t\t\tjpgData = jpgData[dataLen:]\n\t\t\t\toffset += dataLen\n\t\t\t} else {\n\t\t\t\tp = append(p, jpgData...)\n\t\t\t\tjpgData = nil\n\t\t\t}\n\n\t\t\tclone := rtp.Packet{\n\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\tVersion:        2,\n\t\t\t\t\tMarker:         jpgData == nil,\n\t\t\t\t\tSequenceNumber: sequencer.NextSequenceNumber(),\n\t\t\t\t\tTimestamp:      packet.Timestamp,\n\t\t\t\t},\n\t\t\t\tPayload: p,\n\t\t\t}\n\t\t\thandlerFunc(&clone)\n\t\t}\n\t}\n}\n\nfunc Transcode(b []byte) ([]byte, error) {\n\timg, err := jpeg.Decode(bytes.NewReader(b))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\twh := img.Bounds().Size()\n\tw := wh.X\n\th := wh.Y\n\n\tif w > 2040 {\n\t\tw = 2040\n\t} else if w&3 > 0 {\n\t\tw &= 3\n\t}\n\tif h > 2040 {\n\t\th = 2040\n\t} else if h&3 > 0 {\n\t\th &= 3\n\t}\n\n\tif w != wh.X || h != wh.Y {\n\t\tx0 := (wh.X - w) / 2\n\t\ty0 := (wh.Y - h) / 2\n\t\trect := image.Rect(x0, y0, x0+w, y0+h)\n\t\timg = img.(*image.YCbCr).SubImage(rect)\n\t}\n\n\tbuf := bytes.NewBuffer(nil)\n\tif err = jpeg.Encode(buf, img, nil); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn buf.Bytes(), nil\n}\n"
  },
  {
    "path": "pkg/mjpeg/writer.go",
    "content": "package mjpeg\n\nimport (\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n)\n\nfunc NewWriter(w io.Writer) io.Writer {\n\th := w.(http.ResponseWriter).Header()\n\th.Set(\"Content-Type\", \"multipart/x-mixed-replace; boundary=frame\")\n\treturn &writer{wr: w, buf: []byte(header)}\n}\n\nconst header = \"--frame\\r\\nContent-Type: image/jpeg\\r\\nContent-Length: \"\n\ntype writer struct {\n\twr  io.Writer\n\tbuf []byte\n}\n\nfunc (w *writer) Write(p []byte) (n int, err error) {\n\tw.buf = w.buf[:len(header)]\n\tw.buf = append(w.buf, strconv.Itoa(len(p))...)\n\tw.buf = append(w.buf, \"\\r\\n\\r\\n\"...)\n\tw.buf = append(w.buf, p...)\n\tw.buf = append(w.buf, \"\\r\\n\"...)\n\n\t// Chrome bug: mjpeg image always shows the second to last image\n\t// https://bugs.chromium.org/p/chromium/issues/detail?id=527446\n\tif _, err = w.wr.Write(w.buf); err != nil {\n\t\treturn 0, err\n\t}\n\n\tw.wr.(http.Flusher).Flush()\n\n\treturn len(p), nil\n}\n"
  },
  {
    "path": "pkg/mp4/README.md",
    "content": "## Fragmented MP4\n\n```\nffmpeg -i \"rtsp://...\" -movflags +frag_keyframe+separate_moof+default_base_moof+empty_moov -frag_duration 1 -c copy -t 5 sample.mp4\n```\n\n- movflags frag_keyframe \n  Start a new fragment at each video keyframe.\n- frag_duration duration\n  Create fragments that are duration microseconds long.\n- movflags separate_moof\n  Write a separate moof (movie fragment) atom for each track.\n- movflags default_base_moof\n  Similarly to the omit_tfhd_offset, this flag avoids writing the absolute base_data_offset field in tfhd atoms, but does so by using the new default-base-is-moof flag instead.\n\nhttps://ffmpeg.org/ffmpeg-formats.html#Options-13\n\n## HEVC\n\n| Browser     | avc1 | hvc1 | hev1 |\n|-------------|------|------|------|\n | Mac Chrome  | +    | -    | +    |\n | Mac Safari  | +    | +    | -    |\n | iOS 15?     | +    | +    | -    |\n | Mac Firefox | +    | -    | -    |\n | iOS 12      | +    | -    | -    |\n | Android 13  | +    | -    | -    |\n\n## Useful links\n\n- https://stackoverflow.com/questions/63468587/what-hevc-codec-tag-to-use-with-fmp4-hvc1-or-hev1\n- https://stackoverflow.com/questions/32152090/encode-h265-to-hvc1-codec\n- https://jellyfin.org/docs/general/clients/codec-support.html\n- https://github.com/StaZhu/enable-chromium-hevc-hardware-decoding\n- https://developer.mozilla.org/ru/docs/Web/Media/Formats/codecs_parameter\n- https://gstreamer-devel.narkive.com/rhkUolp2/rtp-dts-pts-result-in-varying-mp4-frame-durations\n"
  },
  {
    "path": "pkg/mp4/consumer.go",
    "content": "package mp4\n\nimport (\n\t\"errors\"\n\t\"io\"\n\t\"sync\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/aac\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h265\"\n\t\"github.com/AlexxIT/go2rtc/pkg/pcm\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Consumer struct {\n\tcore.Connection\n\twr    *core.WriteBuffer\n\tmuxer *Muxer\n\tmu    sync.Mutex\n\tstart bool\n\n\tRotate int `json:\"-\"`\n\tScaleX int `json:\"-\"`\n\tScaleY int `json:\"-\"`\n}\n\nfunc NewConsumer(medias []*core.Media) *Consumer {\n\tif medias == nil {\n\t\t// default local medias\n\t\tmedias = []*core.Media{\n\t\t\t{\n\t\t\t\tKind:      core.KindVideo,\n\t\t\t\tDirection: core.DirectionSendonly,\n\t\t\t\tCodecs: []*core.Codec{\n\t\t\t\t\t{Name: core.CodecH264},\n\t\t\t\t\t{Name: core.CodecH265},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tKind:      core.KindAudio,\n\t\t\t\tDirection: core.DirectionSendonly,\n\t\t\t\tCodecs: []*core.Codec{\n\t\t\t\t\t{Name: core.CodecAAC},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t}\n\n\twr := core.NewWriteBuffer(nil)\n\treturn &Consumer{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"mp4\",\n\t\t\tMedias:     medias,\n\t\t\tTransport:  wr,\n\t\t},\n\t\tmuxer: &Muxer{},\n\t\twr:    wr,\n\t}\n}\n\nfunc (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {\n\ttrackID := byte(len(c.Senders))\n\n\tcodec := track.Codec.Clone()\n\thandler := core.NewSender(media, codec)\n\n\tswitch track.Codec.Name {\n\tcase core.CodecH264:\n\t\thandler.Handler = func(packet *rtp.Packet) {\n\t\t\tif !c.start {\n\t\t\t\tif !h264.IsKeyframe(packet.Payload) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tc.start = true\n\t\t\t}\n\n\t\t\t// important to use Mutex because right fragment order\n\t\t\tc.mu.Lock()\n\t\t\tb := c.muxer.GetPayload(trackID, packet)\n\t\t\tif n, err := c.wr.Write(b); err == nil {\n\t\t\t\tc.Send += n\n\t\t\t}\n\t\t\tc.mu.Unlock()\n\t\t}\n\n\t\tif track.Codec.IsRTP() {\n\t\t\thandler.Handler = h264.RTPDepay(track.Codec, handler.Handler)\n\t\t} else {\n\t\t\thandler.Handler = h264.RepairAVCC(track.Codec, handler.Handler)\n\t\t}\n\n\tcase core.CodecH265:\n\t\thandler.Handler = func(packet *rtp.Packet) {\n\t\t\tif !c.start {\n\t\t\t\tif !h265.IsKeyframe(packet.Payload) {\n\t\t\t\t\treturn\n\t\t\t\t}\n\t\t\t\tc.start = true\n\t\t\t}\n\n\t\t\t// important to use Mutex because right fragment order\n\t\t\tc.mu.Lock()\n\t\t\tb := c.muxer.GetPayload(trackID, packet)\n\t\t\tif n, err := c.wr.Write(b); err == nil {\n\t\t\t\tc.Send += n\n\t\t\t}\n\t\t\tc.mu.Unlock()\n\t\t}\n\n\t\tif track.Codec.IsRTP() {\n\t\t\thandler.Handler = h265.RTPDepay(track.Codec, handler.Handler)\n\t\t} else {\n\t\t\thandler.Handler = h265.RepairAVCC(track.Codec, handler.Handler)\n\t\t}\n\n\tdefault:\n\t\thandler.Handler = func(packet *rtp.Packet) {\n\t\t\tif !c.start {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// important to use Mutex because right fragment order\n\t\t\tc.mu.Lock()\n\t\t\tb := c.muxer.GetPayload(trackID, packet)\n\t\t\tif n, err := c.wr.Write(b); err == nil {\n\t\t\t\tc.Send += n\n\t\t\t}\n\t\t\tc.mu.Unlock()\n\t\t}\n\n\t\tswitch track.Codec.Name {\n\t\tcase core.CodecAAC:\n\t\t\tif track.Codec.IsRTP() {\n\t\t\t\thandler.Handler = aac.RTPDepay(handler.Handler)\n\t\t\t}\n\t\tcase core.CodecOpus, core.CodecMP3: // no changes\n\t\tcase core.CodecPCMA, core.CodecPCMU, core.CodecPCM, core.CodecPCML:\n\t\t\tcodec.Name = core.CodecFLAC\n\t\t\tif codec.Channels == 2 {\n\t\t\t\t// hacky way for support two channels audio\n\t\t\t\tcodec.Channels = 1\n\t\t\t\tcodec.ClockRate *= 2\n\t\t\t}\n\t\t\thandler.Handler = pcm.FLACEncoder(track.Codec.Name, codec.ClockRate, handler.Handler)\n\n\t\tdefault:\n\t\t\thandler.Handler = nil\n\t\t}\n\t}\n\n\tif handler.Handler == nil {\n\t\ts := \"mp4: unsupported codec: \" + track.Codec.String()\n\t\tprintln(s)\n\t\treturn errors.New(s)\n\t}\n\n\tc.muxer.AddTrack(codec)\n\n\thandler.HandleRTP(track)\n\tc.Senders = append(c.Senders, handler)\n\n\treturn nil\n}\n\nfunc (c *Consumer) WriteTo(wr io.Writer) (int64, error) {\n\tif len(c.Senders) == 1 && c.Senders[0].Codec.IsAudio() {\n\t\tc.start = true\n\t}\n\n\tinit, err := c.muxer.GetInit()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tif c.Rotate != 0 {\n\t\tPatchVideoRotate(init, c.Rotate)\n\t}\n\tif c.ScaleX != 0 && c.ScaleY != 0 {\n\t\tPatchVideoScale(init, c.ScaleX, c.ScaleY)\n\t}\n\n\tif _, err = wr.Write(init); err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn c.wr.WriteTo(wr)\n}\n"
  },
  {
    "path": "pkg/mp4/demuxer.go",
    "content": "package mp4\n\nimport (\n\t\"github.com/AlexxIT/go2rtc/pkg/aac\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264\"\n\t\"github.com/AlexxIT/go2rtc/pkg/iso\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Demuxer struct {\n\tcodecs     map[uint32]*core.Codec\n\ttimeScales map[uint32]float32\n}\n\nfunc (d *Demuxer) Probe(init []byte) (medias []*core.Media) {\n\tvar trackID, timeScale uint32\n\n\tif d.codecs == nil {\n\t\td.codecs = make(map[uint32]*core.Codec)\n\t\td.timeScales = make(map[uint32]float32)\n\t}\n\n\tatoms, _ := iso.DecodeAtoms(init)\n\tfor _, atom := range atoms {\n\t\tvar codec *core.Codec\n\n\t\tswitch atom := atom.(type) {\n\t\tcase *iso.AtomTkhd:\n\t\t\ttrackID = atom.TrackID\n\t\tcase *iso.AtomMdhd:\n\t\t\ttimeScale = atom.TimeScale\n\t\tcase *iso.AtomVideo:\n\t\t\tswitch atom.Name {\n\t\t\tcase \"avc1\":\n\t\t\t\tcodec = h264.ConfigToCodec(atom.Config)\n\t\t\t}\n\t\tcase *iso.AtomAudio:\n\t\t\tswitch atom.Name {\n\t\t\tcase \"mp4a\":\n\t\t\t\tcodec = aac.ConfigToCodec(atom.Config)\n\t\t\t}\n\t\t}\n\n\t\tif codec != nil {\n\t\t\td.codecs[trackID] = codec\n\t\t\td.timeScales[trackID] = float32(codec.ClockRate) / float32(timeScale)\n\n\t\t\tmedias = append(medias, &core.Media{\n\t\t\t\tKind:      codec.Kind(),\n\t\t\t\tDirection: core.DirectionRecvonly,\n\t\t\t\tCodecs:    []*core.Codec{codec},\n\t\t\t})\n\t\t}\n\t}\n\n\treturn\n}\n\nfunc (d *Demuxer) GetTrackID(codec *core.Codec) uint32 {\n\tfor trackID, c := range d.codecs {\n\t\tif c == codec {\n\t\t\treturn trackID\n\t\t}\n\t}\n\treturn 0\n}\n\nfunc (d *Demuxer) Demux(data2 []byte) (trackID uint32, packets []*core.Packet) {\n\tatoms, err := iso.DecodeAtoms(data2)\n\tif err != nil {\n\t\treturn 0, nil\n\t}\n\n\tvar ts uint32\n\tvar trun *iso.AtomTrun\n\tvar data []byte\n\n\tfor _, atom := range atoms {\n\t\tswitch atom := atom.(type) {\n\t\tcase *iso.AtomTfhd:\n\t\t\ttrackID = atom.TrackID\n\t\tcase *iso.AtomTfdt:\n\t\t\tts = uint32(atom.DecodeTime)\n\t\tcase *iso.AtomTrun:\n\t\t\ttrun = atom\n\t\tcase *iso.AtomMdat:\n\t\t\tdata = atom.Data\n\t\t}\n\t}\n\n\ttimeScale := d.timeScales[trackID]\n\tif timeScale == 0 {\n\t\treturn 0, nil\n\t}\n\n\tn := len(trun.SamplesDuration)\n\tpackets = make([]*core.Packet, n)\n\n\tfor i := 0; i < n; i++ {\n\t\tduration := trun.SamplesDuration[i]\n\t\tsize := trun.SamplesSize[i]\n\n\t\t// can be SPS, PPS and IFrame in one packet\n\t\ttimestamp := uint32(float32(ts) * timeScale)\n\t\tpackets[i] = &rtp.Packet{\n\t\t\tHeader:  rtp.Header{Timestamp: timestamp},\n\t\t\tPayload: data[:size],\n\t\t}\n\n\t\tdata = data[size:]\n\t\tts += duration\n\t}\n\n\treturn\n}\n"
  },
  {
    "path": "pkg/mp4/helpers.go",
    "content": "package mp4\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\n// ParseQuery - like usual parse, but with mp4 param handler\nfunc ParseQuery(query map[string][]string) []*core.Media {\n\tif v := query[\"mp4\"]; len(v) != 0 {\n\t\tmedias := []*core.Media{\n\t\t\t{\n\t\t\t\tKind:      core.KindVideo,\n\t\t\t\tDirection: core.DirectionSendonly,\n\t\t\t\tCodecs: []*core.Codec{\n\t\t\t\t\t{Name: core.CodecH264},\n\t\t\t\t\t{Name: core.CodecH265},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tKind:      core.KindAudio,\n\t\t\t\tDirection: core.DirectionSendonly,\n\t\t\t\tCodecs: []*core.Codec{\n\t\t\t\t\t{Name: core.CodecAAC},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\n\t\tif v[0] == \"\" {\n\t\t\treturn medias // legacy\n\t\t}\n\n\t\tmedias[1].Codecs = append(medias[1].Codecs,\n\t\t\t&core.Codec{Name: core.CodecPCMA},\n\t\t\t&core.Codec{Name: core.CodecPCMU},\n\t\t\t&core.Codec{Name: core.CodecPCM},\n\t\t\t&core.Codec{Name: core.CodecPCML},\n\t\t)\n\n\t\tif v[0] == \"flac\" {\n\t\t\treturn medias // modern browsers\n\t\t}\n\n\t\tmedias[1].Codecs = append(medias[1].Codecs,\n\t\t\t&core.Codec{Name: core.CodecOpus},\n\t\t\t&core.Codec{Name: core.CodecMP3},\n\t\t)\n\n\t\treturn medias // Chrome, FFmpeg, VLC\n\t}\n\n\treturn core.ParseQuery(query)\n}\n\nfunc ParseCodecs(codecs string, parseAudio bool) (medias []*core.Media) {\n\tvar videos []*core.Codec\n\tvar audios []*core.Codec\n\n\tfor _, name := range strings.Split(codecs, \",\") {\n\t\tswitch name {\n\t\tcase MimeH264:\n\t\t\tcodec := &core.Codec{Name: core.CodecH264}\n\t\t\tvideos = append(videos, codec)\n\t\tcase MimeH265:\n\t\t\tcodec := &core.Codec{Name: core.CodecH265}\n\t\t\tvideos = append(videos, codec)\n\t\tcase MimeAAC:\n\t\t\tcodec := &core.Codec{Name: core.CodecAAC}\n\t\t\taudios = append(audios, codec)\n\t\tcase MimeFlac:\n\t\t\taudios = append(audios,\n\t\t\t\t&core.Codec{Name: core.CodecPCMA},\n\t\t\t\t&core.Codec{Name: core.CodecPCMU},\n\t\t\t\t&core.Codec{Name: core.CodecPCM},\n\t\t\t\t&core.Codec{Name: core.CodecPCML},\n\t\t\t)\n\t\tcase MimeOpus:\n\t\t\tcodec := &core.Codec{Name: core.CodecOpus}\n\t\t\taudios = append(audios, codec)\n\t\t}\n\t}\n\n\tif videos != nil {\n\t\tmedia := &core.Media{\n\t\t\tKind:      core.KindVideo,\n\t\t\tDirection: core.DirectionSendonly,\n\t\t\tCodecs:    videos,\n\t\t}\n\t\tmedias = append(medias, media)\n\t}\n\n\tif audios != nil && parseAudio {\n\t\tmedia := &core.Media{\n\t\t\tKind:      core.KindAudio,\n\t\t\tDirection: core.DirectionSendonly,\n\t\t\tCodecs:    audios,\n\t\t}\n\t\tmedias = append(medias, media)\n\t}\n\n\treturn\n}\n\n// PatchVideoRotate - update video track transformation matrix.\n// Rotation supported by many players and browsers (except Safari).\n// Scale has low support and better not to use it.\n// Supported only 0, 90, 180, 270 degrees.\nfunc PatchVideoRotate(init []byte, degrees int) bool {\n\t// search video atom\n\ti := bytes.Index(init, []byte(\"vide\"))\n\tif i < 0 {\n\t\treturn false\n\t}\n\n\t// seek to video matrix position\n\ti -= 4 + 3 + 1 + 8 + 32 + 8 + 4 + 4 + 4*9\n\n\t// Rotation matrix:\n\t// [   cos   sin     0]\n\t// [  -sin   cos     0]\n\t// [     0     0 16384]\n\tvar cos, sin uint16\n\n\tswitch degrees {\n\tcase 0:\n\t\tcos = 1\n\t\tsin = 0\n\tcase 90:\n\t\tcos = 0\n\t\tsin = 1\n\tcase 180:\n\t\tcos = 0xFFFF // -1\n\t\tsin = 0\n\tcase 270:\n\t\tcos = 0\n\t\tsin = 0xFFFF // -1\n\tdefault:\n\t\treturn false\n\t}\n\n\tbinary.BigEndian.PutUint16(init[i:], cos)\n\tbinary.BigEndian.PutUint16(init[i+4:], sin)\n\tbinary.BigEndian.PutUint16(init[i+12:], -sin)\n\tbinary.BigEndian.PutUint16(init[i+16:], cos)\n\n\treturn true\n}\n\n// PatchVideoScale - update \"Pixel Aspect Ratio\" atom.\n// Supported by many players and browsers (except Firefox).\n// Supported only positive integers.\nfunc PatchVideoScale(init []byte, scaleX, scaleY int) bool {\n\t// search video atom\n\ti := bytes.Index(init, []byte(\"pasp\"))\n\tif i < 0 {\n\t\treturn false\n\t}\n\n\tbinary.BigEndian.PutUint32(init[i+4:], uint32(scaleX))\n\tbinary.BigEndian.PutUint32(init[i+8:], uint32(scaleY))\n\n\treturn true\n}\n"
  },
  {
    "path": "pkg/mp4/keyframe.go",
    "content": "package mp4\n\nimport (\n\t\"io\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h265\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Keyframe struct {\n\tcore.Connection\n\twr    *core.WriteBuffer\n\tmuxer *Muxer\n}\n\n// Deprecated: should be rewritten\nfunc NewKeyframe(medias []*core.Media) *Keyframe {\n\tif medias == nil {\n\t\tmedias = []*core.Media{\n\t\t\t{\n\t\t\t\tKind:      core.KindVideo,\n\t\t\t\tDirection: core.DirectionSendonly,\n\t\t\t\tCodecs: []*core.Codec{\n\t\t\t\t\t{Name: core.CodecH264},\n\t\t\t\t\t{Name: core.CodecH265},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t}\n\n\twr := core.NewWriteBuffer(nil)\n\tcons := &Keyframe{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"mp4\",\n\t\t\tTransport:  wr,\n\t\t},\n\t\tmuxer: &Muxer{},\n\t\twr:    wr,\n\t}\n\tcons.Medias = medias\n\treturn cons\n}\n\nfunc (c *Keyframe) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {\n\tc.muxer.AddTrack(track.Codec)\n\tinit, err := c.muxer.GetInit()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\thandler := core.NewSender(media, track.Codec)\n\n\tswitch track.Codec.Name {\n\tcase core.CodecH264:\n\t\thandler.Handler = func(packet *rtp.Packet) {\n\t\t\tif !h264.IsKeyframe(packet.Payload) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// important to use Mutex because right fragment order\n\t\t\tb := c.muxer.GetPayload(0, packet)\n\t\t\tb = append(init, b...)\n\t\t\tif n, err := c.wr.Write(b); err == nil {\n\t\t\t\tc.Send += n\n\t\t\t}\n\t\t}\n\n\t\tif track.Codec.IsRTP() {\n\t\t\thandler.Handler = h264.RTPDepay(track.Codec, handler.Handler)\n\t\t} else {\n\t\t\thandler.Handler = h264.RepairAVCC(track.Codec, handler.Handler)\n\t\t}\n\n\tcase core.CodecH265:\n\t\thandler.Handler = func(packet *rtp.Packet) {\n\t\t\tif !h265.IsKeyframe(packet.Payload) {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\t// important to use Mutex because right fragment order\n\t\t\tb := c.muxer.GetPayload(0, packet)\n\t\t\tb = append(init, b...)\n\t\t\tif n, err := c.wr.Write(b); err == nil {\n\t\t\t\tc.Send += n\n\t\t\t}\n\t\t}\n\n\t\tif track.Codec.IsRTP() {\n\t\t\thandler.Handler = h265.RTPDepay(track.Codec, handler.Handler)\n\t\t}\n\t}\n\n\thandler.HandleRTP(track)\n\tc.Senders = append(c.Senders, handler)\n\n\treturn nil\n}\n\nfunc (c *Keyframe) WriteTo(wr io.Writer) (int64, error) {\n\treturn c.wr.WriteTo(wr)\n}\n"
  },
  {
    "path": "pkg/mp4/mime.go",
    "content": "package mp4\n\nimport (\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264\"\n)\n\nconst (\n\tMimeH264 = \"avc1.640029\"\n\tMimeH265 = \"hvc1.1.6.L153.B0\"\n\tMimeAAC  = \"mp4a.40.2\"\n\tMimeFlac = \"flac\"\n\tMimeOpus = \"opus\"\n)\n\nfunc MimeCodecs(codecs []*core.Codec) string {\n\tvar s string\n\n\tfor i, codec := range codecs {\n\t\tif i > 0 {\n\t\t\ts += \",\"\n\t\t}\n\n\t\tswitch codec.Name {\n\t\tcase core.CodecH264:\n\t\t\ts += \"avc1.\" + h264.GetProfileLevelID(codec.FmtpLine)\n\t\tcase core.CodecH265:\n\t\t\t// H.265 profile=main level=5.1\n\t\t\t// hvc1 - supported in Safari, hev1 - doesn't, both supported in Chrome\n\t\t\ts += MimeH265\n\t\tcase core.CodecAAC:\n\t\t\ts += MimeAAC\n\t\tcase core.CodecOpus:\n\t\t\ts += MimeOpus\n\t\tcase core.CodecFLAC:\n\t\t\ts += MimeFlac\n\t\t}\n\t}\n\n\treturn s\n}\n\nfunc ContentType(codecs []*core.Codec) string {\n\treturn `video/mp4; codecs=\"` + MimeCodecs(codecs) + `\"`\n}\n"
  },
  {
    "path": "pkg/mp4/muxer.go",
    "content": "package mp4\n\nimport (\n\t\"encoding/hex\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h265\"\n\t\"github.com/AlexxIT/go2rtc/pkg/iso\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Muxer struct {\n\tindex  uint32\n\tdts    []uint64\n\tpts    []uint32\n\tcodecs []*core.Codec\n}\n\nfunc (m *Muxer) AddTrack(codec *core.Codec) {\n\tm.dts = append(m.dts, 0)\n\tm.pts = append(m.pts, 0)\n\tm.codecs = append(m.codecs, codec)\n}\n\nfunc (m *Muxer) GetInit() ([]byte, error) {\n\tmv := iso.NewMovie(1024)\n\tmv.WriteFileType()\n\n\tmv.StartAtom(iso.Moov)\n\tmv.WriteMovieHeader()\n\n\tfor i, codec := range m.codecs {\n\t\tswitch codec.Name {\n\t\tcase core.CodecH264:\n\t\t\tsps, pps := h264.GetParameterSet(codec.FmtpLine)\n\t\t\t// some dummy SPS and PPS not a problem for MP4, but problem for HLS :(\n\t\t\tif len(sps) == 0 {\n\t\t\t\tsps = []byte{0x67, 0x42, 0x00, 0x0a, 0xf8, 0x41, 0xa2}\n\t\t\t}\n\t\t\tif len(pps) == 0 {\n\t\t\t\tpps = []byte{0x68, 0xce, 0x38, 0x80}\n\t\t\t}\n\n\t\t\tvar width, height uint16\n\t\t\tif s := h264.DecodeSPS(sps); s != nil {\n\t\t\t\twidth = s.Width()\n\t\t\t\theight = s.Height()\n\t\t\t} else {\n\t\t\t\twidth = 1920\n\t\t\t\theight = 1080\n\t\t\t}\n\n\t\t\tmv.WriteVideoTrack(\n\t\t\t\tuint32(i+1), codec.Name, codec.ClockRate, width, height, h264.EncodeConfig(sps, pps),\n\t\t\t)\n\n\t\tcase core.CodecH265:\n\t\t\tvps, sps, pps := h265.GetParameterSet(codec.FmtpLine)\n\t\t\t// some dummy SPS and PPS not a problem\n\t\t\tif len(vps) == 0 {\n\t\t\t\tvps = []byte{0x40, 0x01, 0x0c, 0x01, 0xff, 0xff, 0x01, 0x40, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x99, 0xac, 0x09}\n\t\t\t}\n\t\t\tif len(sps) == 0 {\n\t\t\t\tsps = []byte{0x42, 0x01, 0x01, 0x01, 0x40, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03, 0x00, 0x99, 0xa0, 0x01, 0x40, 0x20, 0x05, 0xa1, 0xfe, 0x5a, 0xee, 0x46, 0xc1, 0xae, 0x55, 0x04}\n\t\t\t}\n\t\t\tif len(pps) == 0 {\n\t\t\t\tpps = []byte{0x44, 0x01, 0xc0, 0x73, 0xc0, 0x4c, 0x90}\n\t\t\t}\n\n\t\t\tvar width, height uint16\n\t\t\tif s := h265.DecodeSPS(sps); s != nil {\n\t\t\t\twidth = s.Width()\n\t\t\t\theight = s.Height()\n\t\t\t} else {\n\t\t\t\twidth = 1920\n\t\t\t\theight = 1080\n\t\t\t}\n\n\t\t\tmv.WriteVideoTrack(\n\t\t\t\tuint32(i+1), codec.Name, codec.ClockRate, width, height, h265.EncodeConfig(vps, sps, pps),\n\t\t\t)\n\n\t\tcase core.CodecAAC:\n\t\t\ts := core.Between(codec.FmtpLine, \"config=\", \";\")\n\t\t\tb, err := hex.DecodeString(s)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tmv.WriteAudioTrack(\n\t\t\t\tuint32(i+1), codec.Name, codec.ClockRate, uint16(codec.Channels), b,\n\t\t\t)\n\n\t\tcase core.CodecOpus, core.CodecMP3, core.CodecPCMA, core.CodecPCMU, core.CodecFLAC:\n\t\t\tmv.WriteAudioTrack(\n\t\t\t\tuint32(i+1), codec.Name, codec.ClockRate, uint16(codec.Channels), nil,\n\t\t\t)\n\t\t}\n\t}\n\n\tmv.StartAtom(iso.MoovMvex)\n\tfor i := range m.codecs {\n\t\tmv.WriteTrackExtend(uint32(i + 1))\n\t}\n\tmv.EndAtom() // MVEX\n\n\tmv.EndAtom() // MOOV\n\n\treturn mv.Bytes(), nil\n}\n\nfunc (m *Muxer) Reset() {\n\tm.index = 0\n\tfor i := range m.dts {\n\t\tm.dts[i] = 0\n\t\tm.pts[i] = 0\n\t}\n}\n\nfunc (m *Muxer) GetPayload(trackID byte, packet *rtp.Packet) []byte {\n\tcodec := m.codecs[trackID]\n\n\tm.index++\n\n\tduration := packet.Timestamp - m.pts[trackID]\n\tm.pts[trackID] = packet.Timestamp\n\n\t// flags important for Apple Finder video preview\n\tvar flags uint32\n\n\tswitch codec.Name {\n\tcase core.CodecH264:\n\t\tif h264.IsKeyframe(packet.Payload) {\n\t\t\tflags = iso.SampleVideoIFrame\n\t\t} else {\n\t\t\tflags = iso.SampleVideoNonIFrame\n\t\t}\n\tcase core.CodecH265:\n\t\tif h265.IsKeyframe(packet.Payload) {\n\t\t\tflags = iso.SampleVideoIFrame\n\t\t} else {\n\t\t\tflags = iso.SampleVideoNonIFrame\n\t\t}\n\tcase core.CodecAAC:\n\t\tduration = 1024         // important for Apple Finder and QuickTime\n\t\tflags = iso.SampleAudio // not important?\n\tdefault:\n\t\tflags = iso.SampleAudio // important for FLAC on Android Telegram\n\t}\n\n\t// minumum duration important for MSE in Apple Safari\n\tif duration == 0 || duration > codec.ClockRate {\n\t\tduration = codec.ClockRate/1000 + 1\n\t\tm.pts[trackID] += duration\n\t}\n\n\tsize := len(packet.Payload)\n\n\tmv := iso.NewMovie(1024 + size)\n\tmv.WriteMovieFragment(\n\t\t// ExtensionProfile - wrong place for CTS (supported by mpegts.Demuxer)\n\t\tm.index, uint32(trackID+1), duration, uint32(size), flags, m.dts[trackID], uint32(packet.ExtensionProfile),\n\t)\n\tmv.WriteData(packet.Payload)\n\n\t//log.Printf(\"[MP4] idx:%3d trk:%d dts:%6d cts:%4d dur:%5d time:%10d len:%5d\", m.index, trackID+1, m.dts[trackID], packet.SSRC, duration, packet.Timestamp, len(packet.Payload))\n\n\tm.dts[trackID] += uint64(duration)\n\n\treturn mv.Bytes()\n}\n"
  },
  {
    "path": "pkg/mpegts/README.md",
    "content": "## PTS/DTS/CTS\n\n```\nif DTS == 0 {\n    // for I and P frames\n\tpacket.Timestamp = PTS (presentation time)\n} else {\n    // for B frames\n    packet.Timestamp = DTS (decode time)\n    CTS = PTS-DTS (composition time)\n}\n```\n\n- MPEG-TS container uses PTS and optional DTS.\n- MP4 container uses DTS and CTS\n- RTP container uses PTS\n\n## MPEG-TS\n\nFFmpeg:\n- PMTID=4096\n- H264: PESID=256, StreamType=27, StreamID=224\n- H265: PESID=256, StreamType=36, StreamID=224\n- AAC: PESID=257, StreamType=15, StreamID=192\n\nTapo:\n- PMTID=18\n- H264: PESID=68, StreamType=27, StreamID=224\n- AAC: PESID=69, StreamType=144, StreamID=192\n\n## Useful links\n\n- https://github.com/theREDspace/video-onboarding/blob/main/MPEGTS%20Knowledge.md\n- https://en.wikipedia.org/wiki/MPEG_transport_stream\n- https://en.wikipedia.org/wiki/Program-specific_information\n"
  },
  {
    "path": "pkg/mpegts/checksum.go",
    "content": "package mpegts\n\n// have to create this table manually because it is in another endian\n// https://github.com/arturvt/TSreader/blob/master/src/br/ufpe/cin/tool/mpegts/CRC32.java\nvar table = [256]uint32{\n\t0x00000000, 0xB71DC104, 0x6E3B8209, 0xD926430D, 0xDC760413, 0x6B6BC517,\n\t0xB24D861A, 0x0550471E, 0xB8ED0826, 0x0FF0C922, 0xD6D68A2F, 0x61CB4B2B,\n\t0x649B0C35, 0xD386CD31, 0x0AA08E3C, 0xBDBD4F38, 0x70DB114C, 0xC7C6D048,\n\t0x1EE09345, 0xA9FD5241, 0xACAD155F, 0x1BB0D45B, 0xC2969756, 0x758B5652,\n\t0xC836196A, 0x7F2BD86E, 0xA60D9B63, 0x11105A67, 0x14401D79, 0xA35DDC7D,\n\t0x7A7B9F70, 0xCD665E74, 0xE0B62398, 0x57ABE29C, 0x8E8DA191, 0x39906095,\n\t0x3CC0278B, 0x8BDDE68F, 0x52FBA582, 0xE5E66486, 0x585B2BBE, 0xEF46EABA,\n\t0x3660A9B7, 0x817D68B3, 0x842D2FAD, 0x3330EEA9, 0xEA16ADA4, 0x5D0B6CA0,\n\t0x906D32D4, 0x2770F3D0, 0xFE56B0DD, 0x494B71D9, 0x4C1B36C7, 0xFB06F7C3,\n\t0x2220B4CE, 0x953D75CA, 0x28803AF2, 0x9F9DFBF6, 0x46BBB8FB, 0xF1A679FF,\n\t0xF4F63EE1, 0x43EBFFE5, 0x9ACDBCE8, 0x2DD07DEC, 0x77708634, 0xC06D4730,\n\t0x194B043D, 0xAE56C539, 0xAB068227, 0x1C1B4323, 0xC53D002E, 0x7220C12A,\n\t0xCF9D8E12, 0x78804F16, 0xA1A60C1B, 0x16BBCD1F, 0x13EB8A01, 0xA4F64B05,\n\t0x7DD00808, 0xCACDC90C, 0x07AB9778, 0xB0B6567C, 0x69901571, 0xDE8DD475,\n\t0xDBDD936B, 0x6CC0526F, 0xB5E61162, 0x02FBD066, 0xBF469F5E, 0x085B5E5A,\n\t0xD17D1D57, 0x6660DC53, 0x63309B4D, 0xD42D5A49, 0x0D0B1944, 0xBA16D840,\n\t0x97C6A5AC, 0x20DB64A8, 0xF9FD27A5, 0x4EE0E6A1, 0x4BB0A1BF, 0xFCAD60BB,\n\t0x258B23B6, 0x9296E2B2, 0x2F2BAD8A, 0x98366C8E, 0x41102F83, 0xF60DEE87,\n\t0xF35DA999, 0x4440689D, 0x9D662B90, 0x2A7BEA94, 0xE71DB4E0, 0x500075E4,\n\t0x892636E9, 0x3E3BF7ED, 0x3B6BB0F3, 0x8C7671F7, 0x555032FA, 0xE24DF3FE,\n\t0x5FF0BCC6, 0xE8ED7DC2, 0x31CB3ECF, 0x86D6FFCB, 0x8386B8D5, 0x349B79D1,\n\t0xEDBD3ADC, 0x5AA0FBD8, 0xEEE00C69, 0x59FDCD6D, 0x80DB8E60, 0x37C64F64,\n\t0x3296087A, 0x858BC97E, 0x5CAD8A73, 0xEBB04B77, 0x560D044F, 0xE110C54B,\n\t0x38368646, 0x8F2B4742, 0x8A7B005C, 0x3D66C158, 0xE4408255, 0x535D4351,\n\t0x9E3B1D25, 0x2926DC21, 0xF0009F2C, 0x471D5E28, 0x424D1936, 0xF550D832,\n\t0x2C769B3F, 0x9B6B5A3B, 0x26D61503, 0x91CBD407, 0x48ED970A, 0xFFF0560E,\n\t0xFAA01110, 0x4DBDD014, 0x949B9319, 0x2386521D, 0x0E562FF1, 0xB94BEEF5,\n\t0x606DADF8, 0xD7706CFC, 0xD2202BE2, 0x653DEAE6, 0xBC1BA9EB, 0x0B0668EF,\n\t0xB6BB27D7, 0x01A6E6D3, 0xD880A5DE, 0x6F9D64DA, 0x6ACD23C4, 0xDDD0E2C0,\n\t0x04F6A1CD, 0xB3EB60C9, 0x7E8D3EBD, 0xC990FFB9, 0x10B6BCB4, 0xA7AB7DB0,\n\t0xA2FB3AAE, 0x15E6FBAA, 0xCCC0B8A7, 0x7BDD79A3, 0xC660369B, 0x717DF79F,\n\t0xA85BB492, 0x1F467596, 0x1A163288, 0xAD0BF38C, 0x742DB081, 0xC3307185,\n\t0x99908A5D, 0x2E8D4B59, 0xF7AB0854, 0x40B6C950, 0x45E68E4E, 0xF2FB4F4A,\n\t0x2BDD0C47, 0x9CC0CD43, 0x217D827B, 0x9660437F, 0x4F460072, 0xF85BC176,\n\t0xFD0B8668, 0x4A16476C, 0x93300461, 0x242DC565, 0xE94B9B11, 0x5E565A15,\n\t0x87701918, 0x306DD81C, 0x353D9F02, 0x82205E06, 0x5B061D0B, 0xEC1BDC0F,\n\t0x51A69337, 0xE6BB5233, 0x3F9D113E, 0x8880D03A, 0x8DD09724, 0x3ACD5620,\n\t0xE3EB152D, 0x54F6D429, 0x7926A9C5, 0xCE3B68C1, 0x171D2BCC, 0xA000EAC8,\n\t0xA550ADD6, 0x124D6CD2, 0xCB6B2FDF, 0x7C76EEDB, 0xC1CBA1E3, 0x76D660E7,\n\t0xAFF023EA, 0x18EDE2EE, 0x1DBDA5F0, 0xAAA064F4, 0x738627F9, 0xC49BE6FD,\n\t0x09FDB889, 0xBEE0798D, 0x67C63A80, 0xD0DBFB84, 0xD58BBC9A, 0x62967D9E,\n\t0xBBB03E93, 0x0CADFF97, 0xB110B0AF, 0x060D71AB, 0xDF2B32A6, 0x6836F3A2,\n\t0x6D66B4BC, 0xDA7B75B8, 0x035D36B5, 0xB440F7B1,\n}\n\nfunc checksum(data []byte) uint32 {\n\tcrc := uint32(0xFFFFFFFF)\n\tfor _, b := range data {\n\t\tcrc = table[b^byte(crc)] ^ (crc >> 8)\n\t}\n\treturn crc\n}\n"
  },
  {
    "path": "pkg/mpegts/consumer.go",
    "content": "package mpegts\n\nimport (\n\t\"io\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/aac\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h265\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Consumer struct {\n\tcore.Connection\n\tmuxer *Muxer\n\twr    *core.WriteBuffer\n}\n\nfunc NewConsumer() *Consumer {\n\tmedias := []*core.Media{\n\t\t{\n\t\t\tKind:      core.KindVideo,\n\t\t\tDirection: core.DirectionSendonly,\n\t\t\tCodecs: []*core.Codec{\n\t\t\t\t{Name: core.CodecH264},\n\t\t\t\t{Name: core.CodecH265},\n\t\t\t},\n\t\t},\n\t\t{\n\t\t\tKind:      core.KindAudio,\n\t\t\tDirection: core.DirectionSendonly,\n\t\t\tCodecs: []*core.Codec{\n\t\t\t\t{Name: core.CodecAAC},\n\t\t\t},\n\t\t},\n\t}\n\twr := core.NewWriteBuffer(nil)\n\treturn &Consumer{\n\t\tcore.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"mpegts\",\n\t\t\tMedias:     medias,\n\t\t\tTransport:  wr,\n\t\t},\n\t\tNewMuxer(),\n\t\twr,\n\t}\n}\n\nfunc (c *Consumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {\n\tsender := core.NewSender(media, track.Codec)\n\n\tswitch track.Codec.Name {\n\tcase core.CodecH264:\n\t\tpid := c.muxer.AddTrack(StreamTypeH264)\n\n\t\tsender.Handler = func(pkt *rtp.Packet) {\n\t\t\tb := c.muxer.GetPayload(pid, pkt.Timestamp, pkt.Payload)\n\t\t\tif n, err := c.wr.Write(b); err == nil {\n\t\t\t\tc.Send += n\n\t\t\t}\n\t\t}\n\n\t\tif track.Codec.IsRTP() {\n\t\t\tsender.Handler = h264.RTPDepay(track.Codec, sender.Handler)\n\t\t} else {\n\t\t\tsender.Handler = h264.RepairAVCC(track.Codec, sender.Handler)\n\t\t}\n\n\tcase core.CodecH265:\n\t\tpid := c.muxer.AddTrack(StreamTypeH265)\n\n\t\tsender.Handler = func(pkt *rtp.Packet) {\n\t\t\tb := c.muxer.GetPayload(pid, pkt.Timestamp, pkt.Payload)\n\t\t\tif n, err := c.wr.Write(b); err == nil {\n\t\t\t\tc.Send += n\n\t\t\t}\n\t\t}\n\n\t\tif track.Codec.IsRTP() {\n\t\t\tsender.Handler = h265.RTPDepay(track.Codec, sender.Handler)\n\t\t}\n\n\tcase core.CodecAAC:\n\t\tpid := c.muxer.AddTrack(StreamTypeAAC)\n\n\t\t// convert timestamp to 90000Hz clock\n\t\tdt := 90000 / float64(track.Codec.ClockRate)\n\n\t\tsender.Handler = func(pkt *rtp.Packet) {\n\t\t\tpts := uint32(float64(pkt.Timestamp) * dt)\n\t\t\tb := c.muxer.GetPayload(pid, pts, pkt.Payload)\n\t\t\tif n, err := c.wr.Write(b); err == nil {\n\t\t\t\tc.Send += n\n\t\t\t}\n\t\t}\n\n\t\tif track.Codec.IsRTP() {\n\t\t\tsender.Handler = aac.RTPToADTS(track.Codec, sender.Handler)\n\t\t} else {\n\t\t\tsender.Handler = aac.EncodeToADTS(track.Codec, sender.Handler)\n\t\t}\n\t}\n\n\tsender.HandleRTP(track)\n\tc.Senders = append(c.Senders, sender)\n\treturn nil\n}\n\nfunc (c *Consumer) WriteTo(wr io.Writer) (int64, error) {\n\tb := c.muxer.GetHeader()\n\tif _, err := wr.Write(b); err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn c.wr.WriteTo(wr)\n}\n\n//func TimestampFromRTP(rtp *rtp.Packet, codec *core.Codec) {\n//\tif codec.ClockRate == ClockRate {\n//\t\treturn\n//\t}\n//\trtp.Timestamp = uint32(float64(rtp.Timestamp) / float64(codec.ClockRate) * ClockRate)\n//}\n"
  },
  {
    "path": "pkg/mpegts/demuxer.go",
    "content": "package mpegts\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"io\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/aac\"\n\t\"github.com/AlexxIT/go2rtc/pkg/bits\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264/annexb\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Demuxer struct {\n\tbuf [PacketSize]byte // total buf\n\n\tbyte byte // current byte\n\tbits byte // bits left in byte\n\tpos  byte // current pos in buf\n\tend  byte // end position\n\n\tpmtID uint16 // Program Map Table (PMT) PID\n\tpes   map[uint16]*PES\n}\n\nfunc NewDemuxer() *Demuxer {\n\treturn &Demuxer{}\n}\n\nconst skipRead = 0xFF\n\nfunc (d *Demuxer) ReadPacket(rd io.Reader) (*rtp.Packet, error) {\n\tfor {\n\t\tif d.pos != skipRead {\n\t\t\tif _, err := io.ReadFull(rd, d.buf[:]); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\n\t\tpid, start, err := d.readPacketHeader()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif d.pes == nil {\n\t\t\tswitch pid {\n\t\t\tcase 0: // PAT ID\n\t\t\t\td.readPAT() // PAT: Program Association Table\n\t\t\tcase d.pmtID:\n\t\t\t\td.readPMT() // PMT : Program Map Table\n\n\t\t\t\tpkt := &rtp.Packet{\n\t\t\t\t\tPayload: make([]byte, 0, len(d.pes)),\n\t\t\t\t}\n\t\t\t\tfor _, pes := range d.pes {\n\t\t\t\t\tpkt.Payload = append(pkt.Payload, pes.StreamType)\n\t\t\t\t}\n\t\t\t\treturn pkt, nil\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\tif pkt := d.readPES(pid, start); pkt != nil {\n\t\t\treturn pkt, nil\n\t\t}\n\t}\n}\n\nfunc (d *Demuxer) readPacketHeader() (pid uint16, start bool, err error) {\n\td.reset()\n\n\tsb := d.readByte() // Sync byte\n\tif sb != SyncByte {\n\t\treturn 0, false, errors.New(\"mpegts: wrong sync byte\")\n\t}\n\n\t_ = d.readBit()        // Transport error indicator (TEI)\n\tpusi := d.readBit()    // Payload unit start indicator (PUSI)\n\t_ = d.readBit()        // Transport priority\n\tpid = d.readBits16(13) // PID\n\n\t_ = d.readBits(2) // Transport scrambling control (TSC)\n\taf := d.readBit() // Adaptation field\n\t_ = d.readBit()   // Payload\n\t_ = d.readBits(4) // Continuity counter\n\n\tif af != 0 {\n\t\tadSize := d.readByte() // Adaptation field length\n\t\tif adSize > PacketSize-6 {\n\t\t\treturn 0, false, errors.New(\"mpegts: wrong adaptation size\")\n\t\t}\n\t\td.skip(adSize)\n\t}\n\n\treturn pid, pusi != 0, nil\n}\n\nfunc (d *Demuxer) skip(i byte) {\n\td.pos += i\n}\n\nfunc (d *Demuxer) readBytes(i byte) []byte {\n\td.pos += i\n\treturn d.buf[d.pos-i : d.pos]\n}\n\nfunc (d *Demuxer) readPSIHeader() {\n\t// https://en.wikipedia.org/wiki/Program-specific_information#Table_Sections\n\tpointer := d.readByte() // Pointer field\n\td.skip(pointer)         // Pointer filler bytes\n\n\t_ = d.readByte()       // Table ID\n\t_ = d.readBit()        // Section syntax indicator\n\t_ = d.readBit()        // Private bit\n\t_ = d.readBits(2)      // Reserved bits\n\t_ = d.readBits(2)      // Section length unused bits\n\tsize := d.readBits(10) // Section length\n\td.setSize(byte(size))\n\n\t_ = d.readBits(16) // Table ID extension\n\t_ = d.readBits(2)  // Reserved bits\n\t_ = d.readBits(5)  // Version number\n\t_ = d.readBit()    // Current/next indicator\n\t_ = d.readByte()   // Section number\n\t_ = d.readByte()   // Last section number\n}\n\n// ReadPAT (Program Association Table)\nfunc (d *Demuxer) readPAT() {\n\t// https://en.wikipedia.org/wiki/Program-specific_information#PAT_(Program_Association_Table)\n\td.readPSIHeader()\n\n\tconst CRCSize = 4\n\tfor d.left() > CRCSize {\n\t\tnum := d.readBits(16)   // Program num\n\t\t_ = d.readBits(3)       // Reserved bits\n\t\tpid := d.readBits16(13) // Program map PID\n\t\tif num != 0 {\n\t\t\td.pmtID = pid\n\t\t}\n\t}\n\n\td.skip(4) // CRC32\n}\n\n// ReadPMT (Program map specific data)\nfunc (d *Demuxer) readPMT() {\n\t// https://en.wikipedia.org/wiki/Program-specific_information#PMT_(Program_map_specific_data)\n\td.readPSIHeader()\n\n\t_ = d.readBits(3)      // Reserved bits\n\t_ = d.readBits(13)     // PCR PID\n\t_ = d.readBits(4)      // Reserved bits\n\t_ = d.readBits(2)      // Program info length unused bits\n\tsize := d.readBits(10) // Program info length\n\td.skip(byte(size))\n\n\td.pes = map[uint16]*PES{}\n\n\tconst CRCSize = 4\n\tfor d.left() > CRCSize {\n\t\tstreamType := d.readByte() // Stream type\n\t\t_ = d.readBits(3)          // Reserved bits\n\t\tpid := d.readBits16(13)    // Elementary PID\n\t\t_ = d.readBits(4)          // Reserved bits\n\t\t_ = d.readBits(2)          // ES Info length unused bits\n\t\tsize = d.readBits(10)      // ES Info length\n\t\tinfo := d.readBytes(byte(size))\n\n\t\tif streamType == StreamTypePrivate && bytes.HasPrefix(info, opusInfo) {\n\t\t\tstreamType = StreamTypePrivateOPUS\n\t\t}\n\n\t\td.pes[pid] = &PES{StreamType: streamType}\n\t}\n\n\td.skip(4) // CRC32\n}\n\nfunc (d *Demuxer) readPES(pid uint16, start bool) *rtp.Packet {\n\tpes := d.pes[pid]\n\tif pes == nil {\n\t\treturn nil\n\t}\n\n\t// if new payload beging\n\tif start {\n\t\tif len(pes.Payload) != 0 {\n\t\t\td.pos = skipRead\n\t\t\treturn pes.GetPacket() // finish previous packet\n\t\t}\n\n\t\t// https://en.wikipedia.org/wiki/Packetized_elementary_stream\n\t\t// Packet start code prefix\n\t\tif d.readByte() != 0 || d.readByte() != 0 || d.readByte() != 1 {\n\t\t\treturn nil\n\t\t}\n\n\t\tpes.StreamID = d.readByte()    // Stream id\n\t\tpacketSize := d.readBits16(16) // PES Packet length\n\n\t\t_ = d.readBits(2) // Marker bits\n\t\t_ = d.readBits(2) // Scrambling control\n\t\t_ = d.readBit()   // Priority\n\t\t_ = d.readBit()   // Data alignment indicator\n\t\t_ = d.readBit()   // Copyright\n\t\t_ = d.readBit()   // Original or Copy\n\n\t\tptsi := d.readBit() // PTS indicator\n\t\tdtsi := d.readBit() // DTS indicator\n\t\t_ = d.readBit()     // ESCR flag\n\t\t_ = d.readBit()     // ES rate flag\n\t\t_ = d.readBit()     // DSM trick mode flag\n\t\t_ = d.readBit()     // Additional copy info flag\n\t\t_ = d.readBit()     // CRC flag\n\t\t_ = d.readBit()     // extension flag\n\n\t\theaderSize := d.readByte() // PES header length\n\n\t\tif packetSize != 0 {\n\t\t\tpacketSize -= uint16(3 + headerSize)\n\t\t}\n\n\t\tif ptsi != 0 {\n\t\t\tpes.PTS = d.readTime()\n\t\t\theaderSize -= 5\n\t\t} else {\n\t\t\tpes.PTS = 0\n\t\t}\n\n\t\tif dtsi != 0 {\n\t\t\tpes.DTS = d.readTime()\n\t\t\theaderSize -= 5\n\t\t} else {\n\t\t\tpes.DTS = 0\n\t\t}\n\n\t\td.skip(headerSize)\n\n\t\tpes.SetBuffer(packetSize, d.bytes())\n\t} else {\n\t\tpes.AppendBuffer(d.bytes())\n\t}\n\n\tif pes.Size != 0 && len(pes.Payload) >= pes.Size {\n\t\treturn pes.GetPacket() // finish current packet\n\t}\n\n\treturn nil\n}\n\nfunc (d *Demuxer) reset() {\n\td.pos = 0\n\td.end = PacketSize\n\td.bits = 0\n}\n\n//goland:noinspection GoStandardMethods\nfunc (d *Demuxer) readByte() byte {\n\tif d.bits != 0 {\n\t\treturn byte(d.readBits(8))\n\t}\n\n\tb := d.buf[d.pos]\n\td.pos++\n\treturn b\n}\n\nfunc (d *Demuxer) readBit() byte {\n\tif d.bits == 0 {\n\t\td.byte = d.readByte()\n\t\td.bits = 7\n\t} else {\n\t\td.bits--\n\t}\n\n\treturn (d.byte >> d.bits) & 0b1\n}\n\nfunc (d *Demuxer) readBits(n byte) (res uint32) {\n\tfor i := n - 1; i != 255; i-- {\n\t\tres |= uint32(d.readBit()) << i\n\t}\n\treturn\n}\n\nfunc (d *Demuxer) readBits16(n byte) (res uint16) {\n\tfor i := n - 1; i != 255; i-- {\n\t\tres |= uint16(d.readBit()) << i\n\t}\n\treturn\n}\n\nfunc (d *Demuxer) readTime() uint32 {\n\t// https://en.wikipedia.org/wiki/Packetized_elementary_stream\n\t// xxxxAAAx BBBBBBBB BBBBBBBx CCCCCCCC CCCCCCCx\n\t_ = d.readBits(4) // 0010b or 0011b or 0001b\n\tts := d.readBits(3) << 30\n\t_ = d.readBits(1) // 1b\n\tts |= d.readBits(15) << 15\n\t_ = d.readBits(1) // 1b\n\tts |= d.readBits(15)\n\t_ = d.readBits(1) // 1b\n\treturn ts\n}\n\nfunc (d *Demuxer) bytes() []byte {\n\treturn d.buf[d.pos:PacketSize]\n}\n\nfunc (d *Demuxer) left() byte {\n\treturn d.end - d.pos\n}\n\nfunc (d *Demuxer) setSize(size byte) {\n\td.end = d.pos + size\n}\n\nconst (\n\tPacketSize = 188\n\tSyncByte   = 0x47  // Uppercase G\n\tClockRate  = 90000 // fixed clock rate for PTS/DTS of any type\n)\n\n// https://en.wikipedia.org/wiki/Program-specific_information#Elementary_stream_types\nconst (\n\tStreamTypeMetadata    = 0    // Reserved\n\tStreamTypePrivate     = 0x06 // PCMU or PCMA or FLAC from FFmpeg\n\tStreamTypeAAC         = 0x0F\n\tStreamTypeH264        = 0x1B\n\tStreamTypeH265        = 0x24\n\tStreamTypePCMATapo    = 0x90\n\tStreamTypePCMUTapo    = 0x91\n\tStreamTypePrivateOPUS = 0xEB\n)\n\n// PES - Packetized Elementary Stream\ntype PES struct {\n\tStreamID   byte   // from each PES header\n\tStreamType byte   // from PMT table\n\tSequence   uint16 // manual\n\tTimestamp  uint32 // manual\n\tPTS        uint32 // from extra header, always 90000Hz\n\tDTS        uint32\n\tPayload    []byte // from PES body\n\tSize       int    // from PES header, can be 0\n\n\twr *bits.Writer\n}\n\nfunc (p *PES) SetBuffer(size uint16, b []byte) {\n\tp.Payload = make([]byte, 0, size)\n\tp.Payload = append(p.Payload, b...)\n\tp.Size = int(size)\n}\n\nfunc (p *PES) AppendBuffer(b []byte) {\n\tp.Payload = append(p.Payload, b...)\n}\n\nfunc (p *PES) GetPacket() (pkt *rtp.Packet) {\n\tswitch p.StreamType {\n\tcase StreamTypeH264, StreamTypeH265:\n\t\tpkt = &rtp.Packet{\n\t\t\tHeader: rtp.Header{\n\t\t\t\tPayloadType: p.StreamType,\n\t\t\t},\n\t\t\tPayload: annexb.EncodeToAVCC(p.Payload),\n\t\t}\n\n\t\tif p.DTS != 0 {\n\t\t\tpkt.Timestamp = p.DTS\n\t\t\t// wrong place for CTS, but we don't have another one\n\t\t\tpkt.ExtensionProfile = uint16(p.PTS - p.DTS)\n\t\t} else {\n\t\t\tpkt.Timestamp = p.PTS\n\t\t}\n\n\tcase StreamTypeAAC:\n\t\tp.Sequence++\n\n\t\tpkt = &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:    p.StreamType,\n\t\t\t\tSequenceNumber: p.Sequence,\n\t\t\t\tTimestamp:      p.PTS,\n\t\t\t\t//Timestamp:      p.Timestamp,\n\t\t\t},\n\t\t\tPayload: aac.ADTStoRTP(p.Payload),\n\t\t}\n\n\t\t//p.Timestamp += aac.RTPTimeSize(pkt.Payload) // update next timestamp!\n\n\tcase StreamTypePCMATapo, StreamTypePCMUTapo:\n\t\tp.Sequence++\n\n\t\tpkt = &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:    p.StreamType,\n\t\t\t\tSequenceNumber: p.Sequence,\n\t\t\t\tTimestamp:      p.PTS,\n\t\t\t\t//Timestamp:      p.Timestamp,\n\t\t\t},\n\t\t\tPayload: p.Payload,\n\t\t}\n\n\t\t//p.Timestamp += uint32(len(p.Payload)) // update next timestamp!\n\n\tcase StreamTypePrivateOPUS:\n\t\tp.Sequence++\n\n\t\tpkt = &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:    p.StreamType,\n\t\t\t\tSequenceNumber: p.Sequence,\n\t\t\t\tTimestamp:      p.PTS,\n\t\t\t},\n\t\t}\n\n\t\tpkt.Payload, p.Payload = CutOPUSPacket(p.Payload)\n\t\tp.PTS += opusDT\n\t\treturn\n\t}\n\n\tp.Payload = nil\n\n\treturn\n}\n"
  },
  {
    "path": "pkg/mpegts/muxer.go",
    "content": "package mpegts\n\nimport (\n\t\"encoding/binary\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/bits\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264/annexb\"\n)\n\ntype Muxer struct {\n\tpes map[uint16]*PES\n}\n\nfunc NewMuxer() *Muxer {\n\treturn &Muxer{\n\t\tpes: map[uint16]*PES{},\n\t}\n}\n\nfunc (m *Muxer) AddTrack(streamType byte) (pid uint16) {\n\tpes := &PES{StreamType: streamType}\n\n\t// Audio streams (0xC0-0xDF), Video streams (0xE0-0xEF)\n\tswitch streamType {\n\tcase StreamTypeH264, StreamTypeH265:\n\t\tpes.StreamID = 0xE0\n\tcase StreamTypeAAC, StreamTypePCMATapo:\n\t\tpes.StreamID = 0xC0\n\t}\n\n\tpid = pes0PID + uint16(len(m.pes))\n\tm.pes[pid] = pes\n\n\treturn\n}\n\nfunc (m *Muxer) GetHeader() []byte {\n\tbw := bits.NewWriter(nil)\n\tm.writePAT(bw)\n\tm.writePMT(bw)\n\treturn bw.Bytes()\n}\n\n// GetPayload - safe to run concurently with different pid\nfunc (m *Muxer) GetPayload(pid uint16, timestamp uint32, payload []byte) []byte {\n\tpes := m.pes[pid]\n\n\tswitch pes.StreamType {\n\tcase StreamTypeH264, StreamTypeH265:\n\t\tpayload = annexb.DecodeAVCCWithAUD(payload)\n\t}\n\n\tif pes.Timestamp != 0 {\n\t\tpes.PTS += timestamp - pes.Timestamp\n\t}\n\tpes.Timestamp = timestamp\n\n\t// min header size (3 byte) + adv header size (PES)\n\tsize := 3 + 5 + len(payload)\n\n\tb := make([]byte, 6+3+5)\n\n\tb[0], b[1], b[2] = 0, 0, 1 // Packet start code prefix\n\tb[3] = pes.StreamID        // Stream ID\n\n\t// PES Packet length (zero value OK for video)\n\tif size <= 0xFFFF {\n\t\tbinary.BigEndian.PutUint16(b[4:], uint16(size))\n\t}\n\n\t// Optional PES header:\n\tb[6] = 0x80 // Marker bits (binary)\n\tb[7] = 0x80 // PTS indicator\n\tb[8] = 5    // PES header length\n\n\tWriteTime(b[9:], pes.PTS)\n\n\tpes.Payload = append(b, payload...)\n\tpes.Size = 1 // set PUSI in first PES\n\n\tif pes.wr == nil {\n\t\tpes.wr = bits.NewWriter(nil)\n\t} else {\n\t\tpes.wr.Reset()\n\t}\n\n\tfor len(pes.Payload) > 0 {\n\t\tm.writePES(pes.wr, pid, pes)\n\t\tpes.Sequence++\n\t\tpes.Size = 0\n\t}\n\n\treturn pes.wr.Bytes()\n}\n\nconst patPID = 0\nconst pmtPID = 0x1000\nconst pes0PID = 0x100\n\nfunc (m *Muxer) writePAT(wr *bits.Writer) {\n\tm.writeHeader(wr, patPID)\n\ti := wr.Len() + 1 // start for CRC32\n\tm.writePSIHeader(wr, 0, 4)\n\n\twr.WriteUint16(1)          // Program num\n\twr.WriteBits8(0b111, 3)    // Reserved bits (all to 1)\n\twr.WriteBits16(pmtPID, 13) // Program map PID\n\n\tcrc := checksum(wr.Bytes()[i:])\n\twr.WriteBytes(byte(crc), byte(crc>>8), byte(crc>>16), byte(crc>>24)) // CRC32 (little endian)\n\n\tm.WriteTail(wr)\n}\n\nfunc (m *Muxer) writePMT(wr *bits.Writer) {\n\tm.writeHeader(wr, pmtPID)\n\ti := wr.Len() + 1                               // start for CRC32\n\tm.writePSIHeader(wr, 2, 4+uint16(len(m.pes))*5) // 4 bytes below + 5 bytes each PES\n\n\twr.WriteBits8(0b111, 3)    // Reserved bits (all to 1)\n\twr.WriteBits16(0x1FFF, 13) // Program map PID (not used)\n\n\twr.WriteBits8(0b1111, 4) // Reserved bits (all to 1)\n\twr.WriteBits8(0, 2)      // Program info length unused bits (all to 0)\n\twr.WriteBits16(0, 10)    // Program info length\n\n\tfor pid := uint16(pes0PID); ; pid++ {\n\t\tpes, ok := m.pes[pid]\n\t\tif !ok {\n\t\t\tbreak\n\t\t}\n\t\twr.WriteByte(pes.StreamType) // Stream type\n\t\twr.WriteBits8(0b111, 3)      // Reserved bits (all to 1)\n\t\twr.WriteBits16(pid, 13)      // Elementary PID\n\t\twr.WriteBits8(0b1111, 4)     // Reserved bits (all to 1)\n\t\twr.WriteBits(0, 2)           // ES Info length unused bits\n\t\twr.WriteBits16(0, 10)        // ES Info length\n\t}\n\n\tcrc := checksum(wr.Bytes()[i:])\n\twr.WriteBytes(byte(crc), byte(crc>>8), byte(crc>>16), byte(crc>>24)) // CRC32 (little endian)\n\n\tm.WriteTail(wr)\n}\n\nfunc (m *Muxer) writePES(wr *bits.Writer, pid uint16, pes *PES) {\n\tconst flagPUSI = 0b01000000_00000000\n\tconst flagAdaptation = 0b00100000\n\tconst flagPayload = 0b00010000\n\n\twr.WriteByte(SyncByte)\n\n\tif pes.Size != 0 {\n\t\tpid |= flagPUSI // Payload unit start indicator (PUSI)\n\t}\n\n\twr.WriteUint16(pid)\n\n\tcounter := byte(pes.Sequence) & 0xF\n\n\tif size := len(pes.Payload); size < PacketSize-4 {\n\t\twr.WriteByte(flagAdaptation | flagPayload | counter) // adaptation + payload\n\n\t\t// for 183 payload will be zero\n\t\tadSize := PacketSize - 4 - 1 - byte(size)\n\t\twr.WriteByte(adSize)\n\t\twr.WriteBytes(make([]byte, adSize)...)\n\n\t\twr.WriteBytes(pes.Payload...)\n\t\tpes.Payload = nil\n\t} else {\n\t\twr.WriteByte(flagPayload | counter) // only payload\n\n\t\twr.WriteBytes(pes.Payload[:PacketSize-4]...)\n\t\tpes.Payload = pes.Payload[PacketSize-4:]\n\t}\n}\n\nfunc (m *Muxer) writeHeader(wr *bits.Writer, pid uint16) {\n\twr.WriteByte(SyncByte)\n\n\twr.WriteBit(0)          // Transport error indicator (TEI)\n\twr.WriteBit(1)          // Payload unit start indicator (PUSI)\n\twr.WriteBit(0)          // Transport priority\n\twr.WriteBits16(pid, 13) // PID\n\n\twr.WriteBits8(0, 2) // Transport scrambling control (TSC)\n\twr.WriteBit(0)      // Adaptation field\n\twr.WriteBit(1)      // Payload\n\twr.WriteBits8(0, 4) // Continuity counter\n}\n\nfunc (m *Muxer) writePSIHeader(wr *bits.Writer, tableID byte, size uint16) {\n\twr.WriteByte(0) // Pointer field\n\n\twr.WriteByte(tableID) // Table ID\n\n\twr.WriteBit(1)               // Section syntax indicator\n\twr.WriteBit(0)               // Private bit\n\twr.WriteBits8(0b11, 2)       // Reserved bits (all to 1)\n\twr.WriteBits8(0, 2)          // Section length unused bits (all to 0)\n\twr.WriteBits16(5+size+4, 10) // Section length (5 bytes below + content + 4 bytes CRC32)\n\n\twr.WriteUint16(1)      // Table ID extension\n\twr.WriteBits8(0b11, 2) // Reserved bits (all to 1)\n\twr.WriteBits8(0, 5)    // Version number\n\twr.WriteBit(1)         // Current/next indicator\n\n\twr.WriteByte(0) // Section number\n\twr.WriteByte(0) // Last section number\n}\n\nfunc (m *Muxer) WriteTail(wr *bits.Writer) {\n\tsize := PacketSize - wr.Len()%PacketSize\n\twr.WriteBytes(make([]byte, size)...)\n}\n\nfunc WriteTime(b []byte, t uint32) {\n\t_ = b[4] // bounds\n\tconst onlyPTS = 0x20\n\tb[0] = onlyPTS | byte(t>>(32-3)) | 1\n\tb[1] = byte(t >> (24 - 2))\n\tb[2] = byte(t>>(16-2)) | 1\n\tb[3] = byte(t >> (8 - 1))\n\tb[4] = byte(t<<1) | 1 // t>>(0-1)\n}\n"
  },
  {
    "path": "pkg/mpegts/opus.go",
    "content": "package mpegts\n\nimport (\n\t\"github.com/AlexxIT/go2rtc/pkg/bits\"\n)\n\n// opusDT - each AU from FFmpeg has 5 OPUS packets. Each packet len = 960 in the 48000 clock.\nconst opusDT = 960 * ClockRate / 48000\n\n// https://opus-codec.org/docs/\nvar opusInfo = []byte{ // registration_descriptor\n\t0x05,               // descriptor_tag\n\t0x04,               // descriptor_length\n\t'O', 'p', 'u', 's', // format_identifier\n}\n\n//goland:noinspection GoSnakeCaseUsage\nfunc CutOPUSPacket(b []byte) (packet []byte, left []byte) {\n\tr := bits.NewReader(b)\n\n\tsize := opus_control_header(r)\n\tif size == 0 {\n\t\treturn nil, nil\n\t}\n\n\tpacket = r.ReadBytes(size)\n\tleft = r.Left()\n\treturn\n}\n\n//goland:noinspection GoSnakeCaseUsage\nfunc opus_control_header(r *bits.Reader) int {\n\tcontrol_header_prefix := r.ReadBits(11)\n\tif control_header_prefix != 0x3FF {\n\t\treturn 0\n\t}\n\n\tstart_trim_flag := r.ReadBit()\n\tend_trim_flag := r.ReadBit()\n\tcontrol_extension_flag := r.ReadBit()\n\t_ = r.ReadBits(2) // reserved\n\n\tvar payload_size int\n\tfor {\n\t\ti := r.ReadByte()\n\t\tpayload_size += int(i)\n\t\tif i < 255 {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif start_trim_flag != 0 {\n\t\t_ = r.ReadBits(3)\n\t\t_ = r.ReadBits(13)\n\t}\n\tif end_trim_flag != 0 {\n\t\t_ = r.ReadBits(3)\n\t\t_ = r.ReadBits(13)\n\t}\n\tif control_extension_flag != 0 {\n\t\tcontrol_extension_length := r.ReadByte()\n\t\t_ = r.ReadBytes(int(control_extension_length)) // reserved\n\t}\n\n\treturn payload_size\n}\n"
  },
  {
    "path": "pkg/mpegts/producer.go",
    "content": "package mpegts\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/aac\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h265\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Producer struct {\n\tcore.Connection\n\trd *core.ReadBuffer\n}\n\nfunc Open(rd io.Reader) (*Producer, error) {\n\tprod := &Producer{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"mpegts\",\n\t\t\tTransport:  rd,\n\t\t},\n\t\trd: core.NewReadBuffer(rd),\n\t}\n\tif err := prod.probe(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn prod, nil\n}\n\nfunc (c *Producer) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {\n\treceiver, _ := c.Connection.GetTrack(media, codec)\n\treceiver.ID = StreamType(codec)\n\treturn receiver, nil\n}\n\nfunc (c *Producer) Start() error {\n\trd := NewDemuxer()\n\n\tfor {\n\t\tpkt, err := rd.ReadPacket(c.rd)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tc.Recv += len(pkt.Payload)\n\n\t\t//log.Printf(\"[mpegts] size: %6d, muxer: %10d, pt: %2d\", len(pkt.Payload), pkt.Timestamp, pkt.PayloadType)\n\n\t\tfor _, receiver := range c.Receivers {\n\t\t\tif receiver.ID == pkt.PayloadType {\n\t\t\t\tTimestampToRTP(pkt, receiver.Codec)\n\t\t\t\treceiver.WriteRTP(pkt)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (c *Producer) probe() error {\n\tc.rd.BufferSize = core.ProbeSize\n\tdefer c.rd.Reset()\n\n\trd := NewDemuxer()\n\n\t// Strategy:\n\t// 1. Wait packet with metadata, init other packets for wait\n\t// 2. Wait other packets\n\t// 3. Stop after timeout\n\twaitType := []byte{StreamTypeMetadata}\n\ttimeout := time.Now().Add(core.ProbeTimeout)\n\n\tfor len(waitType) != 0 && time.Now().Before(timeout) {\n\t\tpkt, err := rd.ReadPacket(c.rd)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// check if we wait this type\n\t\tif i := bytes.IndexByte(waitType, pkt.PayloadType); i < 0 {\n\t\t\tcontinue\n\t\t} else {\n\t\t\twaitType = append(waitType[:i], waitType[i+1:]...)\n\t\t}\n\n\t\tswitch pkt.PayloadType {\n\t\tcase StreamTypeMetadata:\n\t\t\tfor _, streamType := range pkt.Payload {\n\t\t\t\tswitch streamType {\n\t\t\t\tcase StreamTypeH264, StreamTypeH265, StreamTypeAAC, StreamTypePrivateOPUS, StreamTypePCMATapo:\n\t\t\t\t\twaitType = append(waitType, streamType)\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase StreamTypeH264:\n\t\t\tcodec := h264.AVCCToCodec(pkt.Payload)\n\t\t\tmedia := &core.Media{\n\t\t\t\tKind:      core.KindVideo,\n\t\t\t\tDirection: core.DirectionRecvonly,\n\t\t\t\tCodecs:    []*core.Codec{codec},\n\t\t\t}\n\t\t\tc.Medias = append(c.Medias, media)\n\n\t\tcase StreamTypeH265:\n\t\t\tcodec := h265.AVCCToCodec(pkt.Payload)\n\t\t\tmedia := &core.Media{\n\t\t\t\tKind:      core.KindVideo,\n\t\t\t\tDirection: core.DirectionRecvonly,\n\t\t\t\tCodecs:    []*core.Codec{codec},\n\t\t\t}\n\t\t\tc.Medias = append(c.Medias, media)\n\n\t\tcase StreamTypeAAC:\n\t\t\tcodec := aac.RTPToCodec(pkt.Payload)\n\t\t\tmedia := &core.Media{\n\t\t\t\tKind:      core.KindAudio,\n\t\t\t\tDirection: core.DirectionRecvonly,\n\t\t\t\tCodecs:    []*core.Codec{codec},\n\t\t\t}\n\t\t\tc.Medias = append(c.Medias, media)\n\n\t\tcase StreamTypePrivateOPUS:\n\t\t\tcodec := &core.Codec{\n\t\t\t\tName:      core.CodecOpus,\n\t\t\t\tClockRate: 48000,\n\t\t\t\tChannels:  2,\n\t\t\t}\n\t\t\tmedia := &core.Media{\n\t\t\t\tKind:      core.KindAudio,\n\t\t\t\tDirection: core.DirectionRecvonly,\n\t\t\t\tCodecs:    []*core.Codec{codec},\n\t\t\t}\n\t\t\tc.Medias = append(c.Medias, media)\n\n\t\tcase StreamTypePCMATapo:\n\t\t\tcodec := &core.Codec{\n\t\t\t\tName:      core.CodecPCMA,\n\t\t\t\tClockRate: 8000,\n\t\t\t}\n\t\t\tmedia := &core.Media{\n\t\t\t\tKind:      core.KindAudio,\n\t\t\t\tDirection: core.DirectionRecvonly,\n\t\t\t\tCodecs:    []*core.Codec{codec},\n\t\t\t}\n\t\t\tc.Medias = append(c.Medias, media)\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc StreamType(codec *core.Codec) uint8 {\n\tswitch codec.Name {\n\tcase core.CodecH264:\n\t\treturn StreamTypeH264\n\tcase core.CodecH265:\n\t\treturn StreamTypeH265\n\tcase core.CodecAAC:\n\t\treturn StreamTypeAAC\n\tcase core.CodecPCMA:\n\t\treturn StreamTypePCMATapo\n\tcase core.CodecOpus:\n\t\treturn StreamTypePrivateOPUS\n\t}\n\treturn 0\n}\n\nfunc TimestampToRTP(rtp *rtp.Packet, codec *core.Codec) {\n\tif codec.ClockRate == ClockRate {\n\t\treturn\n\t}\n\trtp.Timestamp = uint32(float64(rtp.Timestamp) * float64(codec.ClockRate) / ClockRate)\n}\n"
  },
  {
    "path": "pkg/mpjpeg/multipart.go",
    "content": "package mpjpeg\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/textproto\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nfunc Next(rd *bufio.Reader) (http.Header, []byte, error) {\n\tfor {\n\t\t// search next boundary and skip empty lines\n\t\ts, err := rd.ReadString('\\n')\n\t\tif err != nil {\n\t\t\treturn nil, nil, err\n\t\t}\n\n\t\tif s == \"\\r\\n\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tif !strings.HasPrefix(s, \"--\") {\n\t\t\treturn nil, nil, errors.New(\"multipart: wrong boundary: \" + s)\n\t\t}\n\n\t\t// Foscam G2 has a awful implementation of MJPEG\n\t\t// https://github.com/AlexxIT/go2rtc/issues/1258\n\t\tif b, _ := rd.Peek(2); string(b) == \"--\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tbreak\n\t}\n\n\ttp := textproto.NewReader(rd)\n\theader, err := tp.ReadMIMEHeader()\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\ts := header.Get(\"Content-Length\")\n\tif s == \"\" {\n\t\treturn nil, nil, errors.New(\"multipart: no content length\")\n\t}\n\n\tsize, err := strconv.Atoi(s)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tbuf := make([]byte, size)\n\tif _, err = io.ReadFull(rd, buf); err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\treturn http.Header(header), buf, nil\n}\n"
  },
  {
    "path": "pkg/mpjpeg/producer.go",
    "content": "package mpjpeg\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"io\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Producer struct {\n\tcore.Connection\n\trd *bufio.Reader\n}\n\nfunc Open(rd io.Reader) (*Producer, error) {\n\treturn &Producer{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"mpjpeg\", // Multipart JPEG\n\t\t\tTransport:  rd,\n\t\t\tMedias: []*core.Media{\n\t\t\t\t{\n\t\t\t\t\tKind:      core.KindVideo,\n\t\t\t\t\tDirection: core.DirectionRecvonly,\n\t\t\t\t\tCodecs: []*core.Codec{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:        core.CodecJPEG,\n\t\t\t\t\t\t\tClockRate:   90000,\n\t\t\t\t\t\t\tPayloadType: core.PayloadTypeRAW,\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}, nil\n}\n\nfunc (c *Producer) Start() error {\n\tif len(c.Receivers) != 1 {\n\t\treturn errors.New(\"mjpeg: no receivers\")\n\t}\n\n\trd := bufio.NewReader(c.Transport.(io.Reader))\n\n\tmjpeg := c.Receivers[0]\n\n\tfor {\n\t\t_, body, err := Next(rd)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tc.Recv += len(body)\n\n\t\tif mjpeg != nil {\n\t\t\tpacket := &rtp.Packet{\n\t\t\t\tHeader:  rtp.Header{Timestamp: core.Now90000()},\n\t\t\t\tPayload: body,\n\t\t\t}\n\t\t\tmjpeg.WriteRTP(packet)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/mqtt/client.go",
    "content": "package mqtt\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"io\"\n\t\"net\"\n\t\"time\"\n)\n\nconst Timeout = time.Second * 5\n\ntype Client struct {\n\tconn net.Conn\n\tmid  uint16\n}\n\nfunc NewClient(conn net.Conn) *Client {\n\treturn &Client{conn: conn, mid: 2}\n}\n\nfunc (c *Client) Connect(clientID, username, password string) (err error) {\n\tif err = c.conn.SetDeadline(time.Now().Add(Timeout)); err != nil {\n\t\treturn\n\t}\n\n\tmsg := NewConnect(clientID, username, password)\n\tif _, err = c.conn.Write(msg.b); err != nil {\n\t\treturn\n\t}\n\n\tb := make([]byte, 4)\n\tif _, err = io.ReadFull(c.conn, b); err != nil {\n\t\treturn\n\t}\n\n\tif !bytes.Equal(b, []byte{CONNACK, 2, 0, 0}) {\n\t\treturn errors.New(\"wrong login\")\n\t}\n\n\treturn\n}\n\nfunc (c *Client) Subscribe(topic string) (err error) {\n\tif err = c.conn.SetDeadline(time.Now().Add(Timeout)); err != nil {\n\t\treturn\n\t}\n\n\tc.mid++\n\tmsg := NewSubscribe(c.mid, topic, 1)\n\t_, err = c.conn.Write(msg.b)\n\treturn\n}\n\nfunc (c *Client) Publish(topic string, payload []byte) (err error) {\n\tif err = c.conn.SetDeadline(time.Now().Add(Timeout)); err != nil {\n\t\treturn\n\t}\n\n\tc.mid++\n\tmsg := NewPublishQOS1(c.mid, topic, payload)\n\t_, err = c.conn.Write(msg.b)\n\treturn\n}\n\nfunc (c *Client) Read() (string, []byte, error) {\n\tif err := c.conn.SetDeadline(time.Now().Add(Timeout)); err != nil {\n\t\treturn \"\", nil, err\n\t}\n\n\tb := make([]byte, 1)\n\tif _, err := io.ReadFull(c.conn, b); err != nil {\n\t\treturn \"\", nil, err\n\t}\n\n\tsize, err := ReadLen(c.conn)\n\tif err != nil {\n\t\treturn \"\", nil, err\n\t}\n\n\tb0 := b[0]\n\tb = make([]byte, size)\n\tif _, err = io.ReadFull(c.conn, b); err != nil {\n\t\treturn \"\", nil, err\n\t}\n\n\tif b0&0xF0 != PUBLISH {\n\t\treturn \"\", nil, nil\n\t}\n\n\ti := binary.BigEndian.Uint16(b)\n\tif uint32(i) > size {\n\t\treturn \"\", nil, errors.New(\"wrong topic size\")\n\t}\n\n\tb = b[2:]\n\n\tif qos := (b0 >> 1) & 0b11; qos == 0 {\n\t\treturn string(b[:i]), b[i:], nil\n\t}\n\n\t// response with packet ID\n\t_, _ = c.conn.Write([]byte{PUBACK, 2, b[i], b[i+1]})\n\n\treturn string(b[2:i]), b[i+2:], nil\n}\n\nfunc (c *Client) Close() error {\n\t// TODO: Teardown\n\treturn c.conn.Close()\n}\n"
  },
  {
    "path": "pkg/mqtt/message.go",
    "content": "package mqtt\n\nimport (\n\t\"io\"\n)\n\ntype Message struct {\n\tb []byte\n}\n\n// https://docs.oasis-open.org/mqtt/mqtt/v5.0/mqtt-v5.0.html\nconst (\n\tCONNECT   = 0x10\n\tCONNACK   = 0x20\n\tPUBLISH   = 0x30\n\tPUBACK    = 0x40\n\tSUBSCRIBE = 0x82\n\tSUBACK    = 0x90\n\tQOS1      = 0x02\n)\n\nfunc (m *Message) WriteByte(b byte) {\n\tm.b = append(m.b, b)\n}\n\nfunc (m *Message) WriteBytes(b []byte) {\n\tm.b = append(m.b, b...)\n}\n\nfunc (m *Message) WriteUint16(i uint16) {\n\tm.b = append(m.b, byte(i>>8), byte(i))\n}\n\nfunc (m *Message) WriteLen(i int) {\n\tfor i > 0 {\n\t\tb := byte(i % 128)\n\t\tif i /= 128; i > 0 {\n\t\t\tb |= 0x80\n\t\t}\n\t\tm.WriteByte(b)\n\t}\n}\n\nfunc (m *Message) WriteString(s string) {\n\tm.WriteUint16(uint16(len(s)))\n\tm.b = append(m.b, s...)\n}\n\nfunc (m *Message) Bytes() []byte {\n\treturn m.b\n}\n\nconst (\n\tflagCleanStart = 0x02\n\tflagUsername   = 0x80\n\tflagPassword   = 0x40\n)\n\nfunc NewConnect(clientID, username, password string) *Message {\n\tm := &Message{}\n\tm.WriteByte(CONNECT)\n\tm.WriteLen(16 + len(clientID) + len(username) + len(password))\n\n\tm.WriteString(\"MQTT\")\n\tm.WriteByte(4) // MQTT version\n\tm.WriteByte(flagCleanStart | flagUsername | flagPassword)\n\tm.WriteUint16(30) // keepalive\n\n\tm.WriteString(clientID)\n\tm.WriteString(username)\n\tm.WriteString(password)\n\treturn m\n}\n\nfunc NewSubscribe(mid uint16, topic string, qos byte) *Message {\n\tm := &Message{}\n\tm.WriteByte(SUBSCRIBE)\n\tm.WriteLen(5 + len(topic))\n\n\tm.WriteUint16(mid)\n\tm.WriteString(topic)\n\tm.WriteByte(qos)\n\treturn m\n}\n\nfunc NewPublish(topic string, payload []byte) *Message {\n\tm := &Message{}\n\tm.WriteByte(PUBLISH)\n\tm.WriteLen(2 + len(topic) + len(payload))\n\n\tm.WriteString(topic)\n\tm.WriteBytes(payload)\n\treturn m\n}\n\nfunc NewPublishQOS1(mid uint16, topic string, payload []byte) *Message {\n\tm := &Message{}\n\tm.WriteByte(PUBLISH | QOS1)\n\tm.WriteLen(4 + len(topic) + len(payload))\n\n\tm.WriteString(topic)\n\tm.WriteUint16(mid)\n\tm.WriteBytes(payload)\n\treturn m\n}\n\nfunc ReadLen(r io.Reader) (uint32, error) {\n\tvar i uint32\n\tvar shift byte\n\n\tb := []byte{0x80}\n\tfor b[0]&0x80 != 0 {\n\t\tif _, err := r.Read(b); err != nil {\n\t\t\treturn 0, err\n\t\t}\n\n\t\ti += uint32(b[0]&0x7F) << shift\n\t\tshift += 7\n\t}\n\n\treturn i, nil\n}\n"
  },
  {
    "path": "pkg/multitrans/client.go",
    "content": "package multitrans\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/tcp\"\n\t\"github.com/google/uuid\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Client struct {\n\tcore.Connection\n\tconn   net.Conn\n\trd     *bufio.Reader\n\tclosed core.Waiter\n}\n\nfunc Dial(rawURL string) (core.Producer, error) {\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif u.Port() == \"\" {\n\t\tu.Host += \":554\"\n\t}\n\n\tconn, err := net.DialTimeout(\"tcp\", u.Host, core.ConnDialTimeout)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tc := &Client{\n\t\tconn: conn,\n\t\trd:   bufio.NewReader(conn),\n\t}\n\n\tif err = c.handshake(u); err != nil {\n\t\t_ = conn.Close()\n\t\treturn nil, err\n\t}\n\n\tc.Connection = core.Connection{\n\t\tID:         core.NewID(),\n\t\tFormatName: \"multitrans\",\n\t\tProtocol:   \"rtsp\",\n\t\tRemoteAddr: conn.RemoteAddr().String(),\n\t\tSource:     rawURL,\n\t\tMedias: []*core.Media{\n\t\t\t{\n\t\t\t\tKind:      core.KindAudio,\n\t\t\t\tDirection: core.DirectionSendonly,\n\t\t\t\tCodecs:    []*core.Codec{{Name: core.CodecPCMA, ClockRate: 8000, PayloadType: 8}},\n\t\t\t},\n\t\t},\n\t\tTransport: conn,\n\t}\n\n\treturn c, nil\n}\n\nfunc (c *Client) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {\n\tsender := core.NewSender(media, track.Codec)\n\tsender.Handler = func(packet *rtp.Packet) {\n\t\tclone := rtp.Packet{\n\t\t\tHeader: rtp.Header{\n\t\t\t\tVersion:        2,\n\t\t\t\tMarker:         packet.Marker,\n\t\t\t\tPayloadType:    8,\n\t\t\t\tSequenceNumber: packet.SequenceNumber,\n\t\t\t\tTimestamp:      packet.Timestamp,\n\t\t\t\tSSRC:           packet.SSRC,\n\t\t\t},\n\t\t\tPayload: packet.Payload,\n\t\t}\n\n\t\t// Encapsulate in RTSP Interleaved Frame (Channel 1)\n\t\t// $ + Channel(1 byte) + Length(2 bytes) + Packet\n\t\tsize := 12 + len(clone.Payload)\n\t\tb := make([]byte, 4+size)\n\t\tb[0] = '$'\n\t\tb[1] = 1 // Channel 1 for audio\n\t\tb[2] = byte(size >> 8)\n\t\tb[3] = byte(size)\n\t\tif _, err := clone.MarshalTo(b[4:]); err != nil {\n\t\t\treturn\n\t\t}\n\t\tif _, err := c.conn.Write(b); err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\tsender.HandleRTP(track)\n\tc.Senders = append(c.Senders, sender)\n\treturn nil\n}\n\nfunc (c *Client) handshake(u *url.URL) error {\n\t// Step 1: Get Challenge\n\tuid := uuid.New().String()\n\n\turi := fmt.Sprintf(\"rtsp://%s/multitrans\", u.Host)\n\tdata := fmt.Sprintf(\"MULTITRANS %s RTSP/1.0\\r\\nCSeq: 0\\r\\nX-Client-UUID: %s\\r\\n\\r\\n\", uri, uid)\n\n\tif _, err := c.conn.Write([]byte(data)); err != nil {\n\t\treturn err\n\t}\n\n\tres, err := tcp.ReadResponse(c.rd)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif res.StatusCode != http.StatusUnauthorized {\n\t\treturn errors.New(\"multitrans: expected 401, got \" + res.Status)\n\t}\n\n\tauth := res.Header.Get(\"WWW-Authenticate\")\n\trealm := tcp.Between(auth, `realm=\"`, `\"`)\n\tnonce := tcp.Between(auth, `nonce=\"`, `\"`)\n\n\t// Step 2: Send Auth\n\tuser := u.User.Username()\n\tpass, _ := u.User.Password()\n\n\tha1 := tcp.HexMD5(user, realm, pass)\n\tha2 := tcp.HexMD5(\"MULTITRANS\", uri)\n\tresponse := tcp.HexMD5(ha1, nonce, ha2)\n\n\tauthHeader := fmt.Sprintf(`Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", response=\"%s\"`,\n\t\tuser, realm, nonce, uri, response)\n\n\tdata = fmt.Sprintf(\"MULTITRANS %s RTSP/1.0\\r\\nCSeq: 1\\r\\nAuthorization: %s\\r\\nX-Client-UUID: %s\\r\\n\\r\\n\",\n\t\turi, authHeader, uid)\n\n\tif _, err = c.conn.Write([]byte(data)); err != nil {\n\t\treturn err\n\t}\n\n\tres, err = tcp.ReadResponse(c.rd)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif res.StatusCode != http.StatusOK {\n\t\treturn errors.New(\"multitrans: auth failed: \" + res.Status)\n\t}\n\n\t// Session: 7116520596809429228\n\tsession := res.Header.Get(\"Session\")\n\tif session == \"\" {\n\t\treturn errors.New(\"multitrans: no session\")\n\t}\n\n\treturn c.openTalkChannel(uri, session)\n}\n\nfunc (c *Client) openTalkChannel(uri, session string) error {\n\tpayload := `{\"type\":\"request\",\"seq\":0,\"params\":{\"method\":\"get\",\"talk\":{\"mode\":\"full_duplex\"}}}`\n\n\tdata := fmt.Sprintf(\"MULTITRANS %s RTSP/1.0\\r\\nCSeq: 2\\r\\nSession: %s\\r\\nContent-Type: application/json\\r\\nContent-Length: %d\\r\\n\\r\\n%s\",\n\t\turi, session, len(payload), payload)\n\n\tif _, err := c.conn.Write([]byte(data)); err != nil {\n\t\treturn err\n\t}\n\n\tres, err := tcp.ReadResponse(c.rd)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif res.StatusCode != http.StatusOK {\n\t\treturn errors.New(\"multitrans: talkback failed: \" + res.Status)\n\t}\n\n\t// Python checks for \"error_code\":0 in body.\n\tif !bytes.Contains(res.Body, []byte(`\"error_code\":0`)) {\n\t\treturn fmt.Errorf(\"multitrans: talkback error: %s\", string(res.Body))\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {\n\treturn nil, core.ErrCantGetTrack\n}\n\nfunc (c *Client) Start() error {\n\t_ = c.closed.Wait()\n\treturn nil\n}\n\nfunc (c *Client) Stop() error {\n\tc.closed.Done(nil)\n\treturn c.Connection.Stop()\n}\n"
  },
  {
    "path": "pkg/nest/api.go",
    "content": "package nest\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n)\n\ntype API struct {\n\tToken     string\n\tExpiresAt time.Time\n\n\tStreamProjectID string\n\tStreamDeviceID  string\n\tStreamExpiresAt time.Time\n\n\t// WebRTC\n\tStreamSessionID string\n\n\t// RTSP\n\tStreamToken          string\n\tStreamExtensionToken string\n\n\textendTimer *time.Timer\n}\n\ntype Auth struct {\n\tAccessToken string\n}\n\ntype DeviceInfo struct {\n\tName      string\n\tDeviceID  string\n\tProtocols []string\n}\n\nvar cache = map[string]*API{}\nvar cacheMu sync.Mutex\n\nfunc NewAPI(clientID, clientSecret, refreshToken string) (*API, error) {\n\tcacheMu.Lock()\n\tdefer cacheMu.Unlock()\n\n\tkey := clientID + \":\" + clientSecret + \":\" + refreshToken\n\tnow := time.Now()\n\n\tif api := cache[key]; api != nil && now.Before(api.ExpiresAt) {\n\t\treturn api, nil\n\t}\n\n\tdata := url.Values{\n\t\t\"grant_type\":    []string{\"refresh_token\"},\n\t\t\"client_id\":     []string{clientID},\n\t\t\"client_secret\": []string{clientSecret},\n\t\t\"refresh_token\": []string{refreshToken},\n\t}\n\n\tclient := &http.Client{Timeout: time.Second * 5000}\n\tres, err := client.PostForm(\"https://www.googleapis.com/oauth2/v4/token\", data)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer res.Body.Close()\n\n\tif res.StatusCode != 200 {\n\t\treturn nil, errors.New(\"nest: wrong status: \" + res.Status)\n\t}\n\n\tvar resv struct {\n\t\tAccessToken string        `json:\"access_token\"`\n\t\tExpiresIn   time.Duration `json:\"expires_in\"`\n\t\tScope       string        `json:\"scope\"`\n\t\tTokenType   string        `json:\"token_type\"`\n\t}\n\n\tif err = json.NewDecoder(res.Body).Decode(&resv); err != nil {\n\t\treturn nil, err\n\t}\n\n\tapi := &API{\n\t\tToken:     resv.AccessToken,\n\t\tExpiresAt: now.Add(resv.ExpiresIn * time.Second),\n\t}\n\n\tcache[key] = api\n\n\treturn api, nil\n}\n\nfunc (a *API) GetDevices(projectID string) ([]DeviceInfo, error) {\n\turi := \"https://smartdevicemanagement.googleapis.com/v1/enterprises/\" + projectID + \"/devices\"\n\treq, err := http.NewRequest(\"GET\", uri, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq.Header.Set(\"Authorization\", \"Bearer \"+a.Token)\n\n\tclient := &http.Client{Timeout: time.Second * 5000}\n\tres, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer res.Body.Close()\n\n\tif res.StatusCode != 200 {\n\t\treturn nil, errors.New(\"nest: wrong status: \" + res.Status)\n\t}\n\n\tvar resv struct {\n\t\tDevices []Device\n\t}\n\n\tif err = json.NewDecoder(res.Body).Decode(&resv); err != nil {\n\t\treturn nil, err\n\t}\n\n\tdevices := make([]DeviceInfo, 0, len(resv.Devices))\n\n\tfor _, device := range resv.Devices {\n\t\t// only RTSP and WEB_RTC available (both supported)\n\t\tif len(device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\ti := strings.LastIndexByte(device.Name, '/')\n\t\tif i <= 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tname := device.Traits.SdmDevicesTraitsInfo.CustomName\n\t\t// Devices configured through the Nest app use the container/room name as opposed to the customName trait\n\t\tif name == \"\" && len(device.ParentRelations) > 0 {\n\t\t\tname = device.ParentRelations[0].DisplayName\n\t\t}\n\n\t\tdevices = append(devices, DeviceInfo{\n\t\t\tName:      name,\n\t\t\tDeviceID:  device.Name[i+1:],\n\t\t\tProtocols: device.Traits.SdmDevicesTraitsCameraLiveStream.SupportedProtocols,\n\t\t})\n\t}\n\n\treturn devices, nil\n}\n\nfunc (a *API) ExchangeSDP(projectID, deviceID, offer string) (string, error) {\n\tvar reqv struct {\n\t\tCommand string `json:\"command\"`\n\t\tParams  struct {\n\t\t\tOffer string `json:\"offerSdp\"`\n\t\t} `json:\"params\"`\n\t}\n\treqv.Command = \"sdm.devices.commands.CameraLiveStream.GenerateWebRtcStream\"\n\treqv.Params.Offer = offer\n\n\tb, err := json.Marshal(reqv)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\turi := \"https://smartdevicemanagement.googleapis.com/v1/enterprises/\" +\n\t\tprojectID + \"/devices/\" + deviceID + \":executeCommand\"\n\n\tmaxRetries := 3\n\tretryDelay := time.Second * 30\n\n\tfor attempt := 0; attempt < maxRetries; attempt++ {\n\t\treq, err := http.NewRequest(\"POST\", uri, bytes.NewReader(b))\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\treq.Header.Set(\"Authorization\", \"Bearer \"+a.Token)\n\n\t\tclient := &http.Client{Timeout: time.Second * 5000}\n\t\tres, err := client.Do(req)\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\t// Handle 409 (Conflict), 429 (Too Many Requests), and 401 (Unauthorized)\n\t\tif res.StatusCode == 409 || res.StatusCode == 429 || res.StatusCode == 401 {\n\t\t\tres.Body.Close()\n\t\t\tif attempt < maxRetries-1 {\n\t\t\t\t// Get new token from Google\n\t\t\t\tif err := a.refreshToken(); err != nil {\n\t\t\t\t\treturn \"\", err\n\t\t\t\t}\n\t\t\t\ttime.Sleep(retryDelay)\n\t\t\t\tretryDelay *= 2 // exponential backoff\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tdefer res.Body.Close()\n\n\t\tif res.StatusCode != 200 {\n\t\t\treturn \"\", errors.New(\"nest: wrong status: \" + res.Status)\n\t\t}\n\n\t\tvar resv struct {\n\t\t\tResults struct {\n\t\t\t\tAnswer         string    `json:\"answerSdp\"`\n\t\t\t\tExpiresAt      time.Time `json:\"expiresAt\"`\n\t\t\t\tMediaSessionID string    `json:\"mediaSessionId\"`\n\t\t\t} `json:\"results\"`\n\t\t}\n\n\t\tif err = json.NewDecoder(res.Body).Decode(&resv); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\ta.StreamProjectID = projectID\n\t\ta.StreamDeviceID = deviceID\n\t\ta.StreamSessionID = resv.Results.MediaSessionID\n\t\ta.StreamExpiresAt = resv.Results.ExpiresAt\n\n\t\treturn resv.Results.Answer, nil\n\t}\n\n\treturn \"\", errors.New(\"nest: max retries exceeded\")\n}\n\nfunc (a *API) refreshToken() error {\n\t// Get the cached API with matching token to get credentials\n\tvar refreshKey string\n\tcacheMu.Lock()\n\tfor key, api := range cache {\n\t\tif api.Token == a.Token {\n\t\t\trefreshKey = key\n\t\t\tbreak\n\t\t}\n\t}\n\tcacheMu.Unlock()\n\n\tif refreshKey == \"\" {\n\t\treturn errors.New(\"nest: unable to find cached credentials\")\n\t}\n\n\t// Parse credentials from cache key\n\tparts := strings.Split(refreshKey, \":\")\n\tif len(parts) != 3 {\n\t\treturn errors.New(\"nest: invalid cache key format\")\n\t}\n\tclientID, clientSecret, refreshToken := parts[0], parts[1], parts[2]\n\n\t// Get new API instance which will refresh the token\n\tnewAPI, err := NewAPI(clientID, clientSecret, refreshToken)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Update current API with new token\n\ta.Token = newAPI.Token\n\ta.ExpiresAt = newAPI.ExpiresAt\n\treturn nil\n}\n\nfunc (a *API) ExtendStream() error {\n\tvar reqv struct {\n\t\tCommand string `json:\"command\"`\n\t\tParams  struct {\n\t\t\tMediaSessionID       string `json:\"mediaSessionId,omitempty\"`\n\t\t\tStreamExtensionToken string `json:\"streamExtensionToken,omitempty\"`\n\t\t} `json:\"params\"`\n\t}\n\n\tif a.StreamToken != \"\" {\n\t\t// RTSP\n\t\treqv.Command = \"sdm.devices.commands.CameraLiveStream.ExtendRtspStream\"\n\t\treqv.Params.StreamExtensionToken = a.StreamExtensionToken\n\t} else {\n\t\t// WebRTC\n\t\treqv.Command = \"sdm.devices.commands.CameraLiveStream.ExtendWebRtcStream\"\n\t\treqv.Params.MediaSessionID = a.StreamSessionID\n\t}\n\n\tb, err := json.Marshal(reqv)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\turi := \"https://smartdevicemanagement.googleapis.com/v1/enterprises/\" +\n\t\ta.StreamProjectID + \"/devices/\" + a.StreamDeviceID + \":executeCommand\"\n\treq, err := http.NewRequest(\"POST\", uri, bytes.NewReader(b))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treq.Header.Set(\"Authorization\", \"Bearer \"+a.Token)\n\n\tclient := &http.Client{Timeout: time.Second * 5000}\n\tres, err := client.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer res.Body.Close()\n\n\tif res.StatusCode != 200 {\n\t\treturn errors.New(\"nest: wrong status: \" + res.Status)\n\t}\n\n\tvar resv struct {\n\t\tResults struct {\n\t\t\tExpiresAt            time.Time `json:\"expiresAt\"`\n\t\t\tMediaSessionID       string    `json:\"mediaSessionId\"`\n\t\t\tStreamExtensionToken string    `json:\"streamExtensionToken\"`\n\t\t\tStreamToken          string    `json:\"streamToken\"`\n\t\t} `json:\"results\"`\n\t}\n\n\tif err = json.NewDecoder(res.Body).Decode(&resv); err != nil {\n\t\treturn err\n\t}\n\n\ta.StreamSessionID = resv.Results.MediaSessionID\n\ta.StreamExpiresAt = resv.Results.ExpiresAt\n\ta.StreamExtensionToken = resv.Results.StreamExtensionToken\n\ta.StreamToken = resv.Results.StreamToken\n\n\treturn nil\n}\n\nfunc (a *API) GenerateRtspStream(projectID, deviceID string) (string, error) {\n\tvar reqv struct {\n\t\tCommand string   `json:\"command\"`\n\t\tParams  struct{} `json:\"params\"`\n\t}\n\treqv.Command = \"sdm.devices.commands.CameraLiveStream.GenerateRtspStream\"\n\n\tb, err := json.Marshal(reqv)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\turi := \"https://smartdevicemanagement.googleapis.com/v1/enterprises/\" +\n\t\tprojectID + \"/devices/\" + deviceID + \":executeCommand\"\n\treq, err := http.NewRequest(\"POST\", uri, bytes.NewReader(b))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treq.Header.Set(\"Authorization\", \"Bearer \"+a.Token)\n\n\tclient := &http.Client{Timeout: time.Second * 5000}\n\tres, err := client.Do(req)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif res.StatusCode != 200 {\n\t\treturn \"\", errors.New(\"nest: wrong status: \" + res.Status)\n\t}\n\n\tvar resv struct {\n\t\tResults struct {\n\t\t\tStreamURLs           map[string]string `json:\"streamUrls\"`\n\t\t\tStreamExtensionToken string            `json:\"streamExtensionToken\"`\n\t\t\tStreamToken          string            `json:\"streamToken\"`\n\t\t\tExpiresAt            time.Time         `json:\"expiresAt\"`\n\t\t} `json:\"results\"`\n\t}\n\n\tif err = json.NewDecoder(res.Body).Decode(&resv); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif _, ok := resv.Results.StreamURLs[\"rtspUrl\"]; !ok {\n\t\treturn \"\", errors.New(\"nest: failed to generate rtsp url\")\n\t}\n\n\ta.StreamProjectID = projectID\n\ta.StreamDeviceID = deviceID\n\ta.StreamToken = resv.Results.StreamToken\n\ta.StreamExtensionToken = resv.Results.StreamExtensionToken\n\ta.StreamExpiresAt = resv.Results.ExpiresAt\n\n\treturn resv.Results.StreamURLs[\"rtspUrl\"], nil\n}\n\nfunc (a *API) StopRTSPStream() error {\n\tif a.StreamProjectID == \"\" || a.StreamDeviceID == \"\" {\n\t\treturn errors.New(\"nest: tried to stop rtsp stream without a project or device ID\")\n\t}\n\n\tvar reqv struct {\n\t\tCommand string `json:\"command\"`\n\t\tParams  struct {\n\t\t\tStreamExtensionToken string `json:\"streamExtensionToken\"`\n\t\t} `json:\"params\"`\n\t}\n\treqv.Command = \"sdm.devices.commands.CameraLiveStream.StopRtspStream\"\n\treqv.Params.StreamExtensionToken = a.StreamExtensionToken\n\n\tb, err := json.Marshal(reqv)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\turi := \"https://smartdevicemanagement.googleapis.com/v1/enterprises/\" +\n\t\ta.StreamProjectID + \"/devices/\" + a.StreamDeviceID + \":executeCommand\"\n\treq, err := http.NewRequest(\"POST\", uri, bytes.NewReader(b))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treq.Header.Set(\"Authorization\", \"Bearer \"+a.Token)\n\n\tclient := &http.Client{Timeout: time.Second * 5000}\n\tres, err := client.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif res.StatusCode != 200 {\n\t\treturn errors.New(\"nest: wrong status: \" + res.Status)\n\t}\n\n\ta.StreamProjectID = \"\"\n\ta.StreamDeviceID = \"\"\n\ta.StreamExtensionToken = \"\"\n\ta.StreamToken = \"\"\n\n\treturn nil\n}\n\ntype Device struct {\n\tName string `json:\"name\"`\n\tType string `json:\"type\"`\n\t//Assignee string `json:\"assignee\"`\n\tTraits struct {\n\t\tSdmDevicesTraitsInfo struct {\n\t\t\tCustomName string `json:\"customName\"`\n\t\t} `json:\"sdm.devices.traits.Info\"`\n\t\tSdmDevicesTraitsCameraLiveStream struct {\n\t\t\tVideoCodecs        []string `json:\"videoCodecs\"`\n\t\t\tAudioCodecs        []string `json:\"audioCodecs\"`\n\t\t\tSupportedProtocols []string `json:\"supportedProtocols\"`\n\t\t} `json:\"sdm.devices.traits.CameraLiveStream\"`\n\t\t//SdmDevicesTraitsCameraImage struct {\n\t\t//\tMaxImageResolution struct {\n\t\t//\t\tWidth  int `json:\"width\"`\n\t\t//\t\tHeight int `json:\"height\"`\n\t\t//\t} `json:\"maxImageResolution\"`\n\t\t//} `json:\"sdm.devices.traits.CameraImage\"`\n\t\t//SdmDevicesTraitsCameraPerson struct {\n\t\t//} `json:\"sdm.devices.traits.CameraPerson\"`\n\t\t//SdmDevicesTraitsCameraMotion struct {\n\t\t//} `json:\"sdm.devices.traits.CameraMotion\"`\n\t\t//SdmDevicesTraitsDoorbellChime struct {\n\t\t//} `json:\"sdm.devices.traits.DoorbellChime\"`\n\t\t//SdmDevicesTraitsCameraClipPreview struct {\n\t\t//} `json:\"sdm.devices.traits.CameraClipPreview\"`\n\t} `json:\"traits\"`\n\tParentRelations []struct {\n\t\tParent      string `json:\"parent\"`\n\t\tDisplayName string `json:\"displayName\"`\n\t} `json:\"parentRelations\"`\n}\n\nfunc (a *API) StartExtendStreamTimer() {\n\tif a.extendTimer != nil {\n\t\treturn\n\t}\n\n\ta.extendTimer = time.NewTimer(time.Until(a.StreamExpiresAt) - time.Minute)\n\tgo func() {\n\t\t<-a.extendTimer.C\n\t\tif err := a.ExtendStream(); err != nil {\n\t\t\treturn\n\t\t}\n\t}()\n}\n\nfunc (a *API) StopExtendStreamTimer() {\n\tif a.extendTimer != nil {\n\t\ta.extendTimer.Stop()\n\t\ta.extendTimer = nil\n\t}\n}\n"
  },
  {
    "path": "pkg/nest/client.go",
    "content": "package nest\n\nimport (\n\t\"errors\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/rtsp\"\n\t\"github.com/AlexxIT/go2rtc/pkg/webrtc\"\n\tpion \"github.com/pion/webrtc/v4\"\n)\n\ntype WebRTCClient struct {\n\tconn *webrtc.Conn\n\tapi  *API\n}\n\ntype RTSPClient struct {\n\tconn *rtsp.Conn\n\tapi  *API\n}\n\nfunc Dial(rawURL string) (core.Producer, error) {\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tquery := u.Query()\n\tcliendID := query.Get(\"client_id\")\n\tcliendSecret := query.Get(\"client_secret\")\n\trefreshToken := query.Get(\"refresh_token\")\n\tprojectID := query.Get(\"project_id\")\n\tdeviceID := query.Get(\"device_id\")\n\n\tif cliendID == \"\" || cliendSecret == \"\" || refreshToken == \"\" || projectID == \"\" || deviceID == \"\" {\n\t\treturn nil, errors.New(\"nest: wrong query\")\n\t}\n\n\tmaxRetries := 3\n\tretryDelay := time.Second * 30\n\n\tvar nestAPI *API\n\tvar lastErr error\n\n\tfor attempt := 0; attempt < maxRetries; attempt++ {\n\t\tnestAPI, err = NewAPI(cliendID, cliendSecret, refreshToken)\n\t\tif err == nil {\n\t\t\tbreak\n\t\t}\n\t\tlastErr = err\n\t\tif attempt < maxRetries-1 {\n\t\t\ttime.Sleep(retryDelay)\n\t\t\tretryDelay *= 2 // exponential backoff\n\t\t}\n\t}\n\n\tif nestAPI == nil {\n\t\treturn nil, lastErr\n\t}\n\n\tprotocols := strings.Split(query.Get(\"protocols\"), \",\")\n\tif len(protocols) > 0 && protocols[0] == \"RTSP\" {\n\t\treturn rtspConn(nestAPI, rawURL, projectID, deviceID)\n\t}\n\n\t// Default to WEB_RTC for backwards compataiility\n\treturn rtcConn(nestAPI, rawURL, projectID, deviceID)\n}\n\nfunc (c *WebRTCClient) GetMedias() []*core.Media {\n\treturn c.conn.GetMedias()\n}\n\nfunc (c *WebRTCClient) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {\n\treturn c.conn.GetTrack(media, codec)\n}\n\nfunc (c *WebRTCClient) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {\n\treturn c.conn.AddTrack(media, codec, track)\n}\n\nfunc (c *WebRTCClient) Start() error {\n\tc.api.StartExtendStreamTimer()\n\treturn c.conn.Start()\n}\n\nfunc (c *WebRTCClient) Stop() error {\n\tc.api.StopExtendStreamTimer()\n\treturn c.conn.Stop()\n}\n\nfunc (c *WebRTCClient) MarshalJSON() ([]byte, error) {\n\treturn c.conn.MarshalJSON()\n}\n\nfunc rtcConn(nestAPI *API, rawURL, projectID, deviceID string) (*WebRTCClient, error) {\n\tmaxRetries := 3\n\tretryDelay := time.Second * 30\n\tvar lastErr error\n\n\tfor attempt := 0; attempt < maxRetries; attempt++ {\n\t\trtcAPI, err := webrtc.NewAPI()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tconf := pion.Configuration{}\n\t\tpc, err := rtcAPI.NewPeerConnection(conf)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tconn := webrtc.NewConn(pc)\n\t\tconn.FormatName = \"nest/webrtc\"\n\t\tconn.Mode = core.ModeActiveProducer\n\t\tconn.Protocol = \"http\"\n\t\tconn.URL = rawURL\n\n\t\t// https://developers.google.com/nest/device-access/traits/device/camera-live-stream#generatewebrtcstream-request-fields\n\t\tmedias := []*core.Media{\n\t\t\t{Kind: core.KindAudio, Direction: core.DirectionRecvonly},\n\t\t\t{Kind: core.KindVideo, Direction: core.DirectionRecvonly},\n\t\t\t{Kind: \"app\"}, // important for Nest\n\t\t}\n\n\t\t// 3. Create offer with candidates\n\t\toffer, err := conn.CreateCompleteOffer(medias)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// 4. Exchange SDP via Hass\n\t\tanswer, err := nestAPI.ExchangeSDP(projectID, deviceID, offer)\n\t\tif err != nil {\n\t\t\tlastErr = err\n\t\t\tif attempt < maxRetries-1 {\n\t\t\t\ttime.Sleep(retryDelay)\n\t\t\t\tretryDelay *= 2\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// 5. Set answer with remote medias\n\t\tif err = conn.SetAnswer(answer); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn &WebRTCClient{conn: conn, api: nestAPI}, nil\n\t}\n\n\treturn nil, lastErr\n}\n\nfunc rtspConn(nestAPI *API, rawURL, projectID, deviceID string) (*RTSPClient, error) {\n\trtspURL, err := nestAPI.GenerateRtspStream(projectID, deviceID)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\trtspClient := rtsp.NewClient(rtspURL)\n\tif err := rtspClient.Dial(); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := rtspClient.Describe(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &RTSPClient{conn: rtspClient, api: nestAPI}, nil\n}\n\nfunc (c *RTSPClient) GetMedias() []*core.Media {\n\tresult := c.conn.GetMedias()\n\treturn result\n}\n\nfunc (c *RTSPClient) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {\n\treturn c.conn.GetTrack(media, codec)\n}\n\nfunc (c *RTSPClient) Start() error {\n\tc.api.StartExtendStreamTimer()\n\treturn c.conn.Start()\n}\n\nfunc (c *RTSPClient) Stop() error {\n\tc.api.StopRTSPStream()\n\tc.api.StopExtendStreamTimer()\n\treturn c.conn.Stop()\n}\n\nfunc (c *RTSPClient) MarshalJSON() ([]byte, error) {\n\treturn c.conn.MarshalJSON()\n}\n"
  },
  {
    "path": "pkg/ngrok/ngrok.go",
    "content": "package ngrok\n\nimport (\n\t\"bufio\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"os/exec\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\ntype Ngrok struct {\n\tcore.Listener\n\n\tTunnels map[string]string\n\n\treader *bufio.Reader\n}\n\ntype Message struct {\n\tMsg  string `json:\"msg\"`\n\tAddr string `json:\"addr\"`\n\tURL  string `json:\"url\"`\n\tLine string\n}\n\nfunc NewNgrok(command any) (*Ngrok, error) {\n\tvar arg []string\n\tswitch command.(type) {\n\tcase string:\n\t\targ = strings.Split(command.(string), \" \")\n\tcase []string:\n\t\targ = command.([]string)\n\t}\n\n\targ = append(arg, \"--log\", \"stdout\", \"--log-format\", \"json\")\n\n\tcmd := exec.Command(arg[0], arg[1:]...)\n\n\tr, err := cmd.StdoutPipe()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tcmd.Stderr = cmd.Stdout\n\n\tn := &Ngrok{\n\t\tTunnels: map[string]string{},\n\t\treader:  bufio.NewReader(r),\n\t}\n\n\tif err = cmd.Start(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn n, nil\n}\n\nfunc (n *Ngrok) Serve() error {\n\tfor {\n\t\tline, _, err := n.reader.ReadLine()\n\t\tif err != nil {\n\t\t\tif err != io.EOF {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\treturn nil\n\t\t}\n\n\t\tmsg := new(Message)\n\t\t_ = json.Unmarshal(line, msg)\n\n\t\tif msg.Msg == \"started tunnel\" {\n\t\t\tn.Tunnels[msg.Addr] = msg.URL\n\t\t}\n\n\t\tmsg.Line = string(line)\n\n\t\tn.Fire(msg)\n\t}\n}\n"
  },
  {
    "path": "pkg/onvif/README.md",
    "content": "## Profiles\n\n- Profile A - For access control configuration\n- Profile C - For door control and event management\n- Profile S - For basic video streaming\n  - Video streaming and configuration\n- Profile T - For advanced video streaming\n  - H.264 / H.265 video compression\n  - Imaging settings\n  - Motion alarm and tampering events\n  - Metadata streaming\n  - Bi-directional audio\n\n## Services\n\nhttps://www.onvif.org/profiles/specifications/\n\n- https://www.onvif.org/ver10/device/wsdl/devicemgmt.wsdl\n- https://www.onvif.org/ver20/imaging/wsdl/imaging.wsdl\n- https://www.onvif.org/ver10/media/wsdl/media.wsdl\n\n## TMP\n\n|                        | Dahua   | Reolink | TP-Link |\n|------------------------|---------|---------|---------|\n| GetCapabilities        | no auth | no auth | no auth |\n| GetServices            | no auth | no auth | no auth |\n| GetServiceCapabilities | no auth | no auth | auth    |\n| GetSystemDateAndTime   | no auth | no auth | no auth |\n| GetNetworkInterfaces   | auth    | auth    | auth    |\n| GetDeviceInformation   | auth    | auth    | auth    |\n| GetProfiles            | auth    | auth    | auth    |\n| GetScopes              | auth    | auth    | auth    |\n\n- Dahua - onvif://192.168.10.90:80\n- Reolink - onvif://192.168.10.92:8000\n- TP-Link - onvif://192.168.10.91:2020/onvif/device_service\n- "
  },
  {
    "path": "pkg/onvif/client.go",
    "content": "package onvif\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"html\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n)\n\nconst PathDevice = \"/onvif/device_service\"\n\ntype Client struct {\n\turl *url.URL\n\n\tdeviceURL string\n\tmediaURL  string\n\timaginURL string\n}\n\nfunc NewClient(rawURL string) (*Client, error) {\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tbaseURL := \"http://\" + u.Host\n\n\tclient := &Client{url: u}\n\tclient.deviceURL = baseURL + GetPath(u.Path, PathDevice)\n\n\tb, err := client.DeviceRequest(DeviceGetCapabilities)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ts := FindTagValue(b, \"Media.+?XAddr\")\n\tclient.mediaURL = baseURL + GetPath(s, \"/onvif/media_service\")\n\n\ts = FindTagValue(b, \"Imaging.+?XAddr\")\n\tclient.imaginURL = baseURL + GetPath(s, \"/onvif/imaging_service\")\n\n\treturn client, nil\n}\n\nfunc (c *Client) GetURI() (string, error) {\n\tquery := c.url.Query()\n\n\ttoken := query.Get(\"subtype\")\n\n\t// support empty\n\tif i := atoi(token); i >= 0 {\n\t\ttokens, err := c.GetProfilesTokens()\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\tif i >= len(tokens) {\n\t\t\treturn \"\", errors.New(\"onvif: wrong subtype\")\n\t\t}\n\t\ttoken = tokens[i]\n\t}\n\n\tgetUri := c.GetStreamUri\n\tif query.Has(\"snapshot\") {\n\t\tgetUri = c.GetSnapshotUri\n\t}\n\n\tb, err := getUri(token)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\trawURL := FindTagValue(b, \"Uri\")\n\trawURL = strings.TrimSpace(html.UnescapeString(rawURL))\n\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif u.User == nil && c.url.User != nil {\n\t\tu.User = c.url.User\n\t}\n\n\treturn u.String(), nil\n}\n\nfunc (c *Client) GetName() (string, error) {\n\tb, err := c.DeviceRequest(DeviceGetDeviceInformation)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn FindTagValue(b, \"Manufacturer\") + \" \" + FindTagValue(b, \"Model\"), nil\n}\n\nfunc (c *Client) GetProfilesTokens() ([]string, error) {\n\tb, err := c.MediaRequest(MediaGetProfiles)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar tokens []string\n\n\tre := regexp.MustCompile(`Profiles.+?token=\"([^\"]+)`)\n\tfor _, s := range re.FindAllStringSubmatch(string(b), 10) {\n\t\ttokens = append(tokens, s[1])\n\t}\n\n\treturn tokens, nil\n}\n\nfunc (c *Client) HasSnapshots() bool {\n\tb, err := c.GetServiceCapabilities()\n\tif err != nil {\n\t\treturn false\n\t}\n\treturn strings.Contains(string(b), `SnapshotUri=\"true\"`)\n}\n\nfunc (c *Client) GetProfile(token string) ([]byte, error) {\n\treturn c.Request(\n\t\tc.mediaURL, `<trt:GetProfile><trt:ProfileToken>`+token+`</trt:ProfileToken></trt:GetProfile>`,\n\t)\n}\n\nfunc (c *Client) GetVideoSourceConfiguration(token string) ([]byte, error) {\n\treturn c.Request(c.mediaURL, `<trt:GetVideoSourceConfiguration>\n\t<trt:ConfigurationToken>`+token+`</trt:ConfigurationToken>\n</trt:GetVideoSourceConfiguration>`)\n}\n\nfunc (c *Client) GetStreamUri(token string) ([]byte, error) {\n\treturn c.Request(c.mediaURL, `<trt:GetStreamUri>\n\t<trt:StreamSetup>\n\t\t<tt:Stream>RTP-Unicast</tt:Stream>\n\t\t<tt:Transport><tt:Protocol>RTSP</tt:Protocol></tt:Transport>\n\t</trt:StreamSetup>\n\t<trt:ProfileToken>`+token+`</trt:ProfileToken>\n</trt:GetStreamUri>`)\n}\n\nfunc (c *Client) GetSnapshotUri(token string) ([]byte, error) {\n\treturn c.Request(\n\t\tc.imaginURL, `<trt:GetSnapshotUri><trt:ProfileToken>`+token+`</trt:ProfileToken></trt:GetSnapshotUri>`,\n\t)\n}\n\nfunc (c *Client) GetServiceCapabilities() ([]byte, error) {\n\t// some cameras answer GetServiceCapabilities for media only for path = \"/onvif/media\"\n\treturn c.Request(\n\t\tc.mediaURL, `<trt:GetServiceCapabilities />`,\n\t)\n}\n\nfunc (c *Client) DeviceRequest(operation string) ([]byte, error) {\n\tswitch operation {\n\tcase DeviceGetServices:\n\t\toperation = `<tds:GetServices><tds:IncludeCapability>true</tds:IncludeCapability></tds:GetServices>`\n\tcase DeviceGetCapabilities:\n\t\toperation = `<tds:GetCapabilities><tds:Category>All</tds:Category></tds:GetCapabilities>`\n\tdefault:\n\t\toperation = `<tds:` + operation + `/>`\n\t}\n\treturn c.Request(c.deviceURL, operation)\n}\n\nfunc (c *Client) MediaRequest(operation string) ([]byte, error) {\n\toperation = `<trt:` + operation + `/>`\n\treturn c.Request(c.mediaURL, operation)\n}\n\nfunc (c *Client) Request(url, body string) ([]byte, error) {\n\tif url == \"\" {\n\t\treturn nil, errors.New(\"onvif: unsupported service\")\n\t}\n\n\te := NewEnvelopeWithUser(c.url.User)\n\te.Append(body)\n\n\tclient := &http.Client{Timeout: time.Second * 5000}\n\tres, err := client.Post(url, `application/soap+xml;charset=utf-8`, bytes.NewReader(e.Bytes()))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer res.Body.Close()\n\n\tif res.StatusCode != http.StatusOK {\n\t\treturn nil, errors.New(\"onvif: wrong response \" + res.Status)\n\t}\n\n\treturn io.ReadAll(res.Body)\n}\n"
  },
  {
    "path": "pkg/onvif/envelope.go",
    "content": "package onvif\n\nimport (\n\t\"crypto/sha1\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\ntype Envelope struct {\n\tbuf []byte\n}\n\nconst (\n\tprefix1 = `<?xml version=\"1.0\" encoding=\"utf-8\"?><s:Envelope xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\" xmlns:tt=\"http://www.onvif.org/ver10/schema\" xmlns:tds=\"http://www.onvif.org/ver10/device/wsdl\" xmlns:trt=\"http://www.onvif.org/ver10/media/wsdl\">`\n\tprefix2 = `<s:Body>`\n\tsuffix  = `</s:Body></s:Envelope>`\n)\n\nfunc NewEnvelope() *Envelope {\n\te := &Envelope{buf: make([]byte, 0, 1024)}\n\te.Append(prefix1, prefix2)\n\treturn e\n}\n\nfunc NewEnvelopeWithUser(user *url.Userinfo) *Envelope {\n\tif user == nil {\n\t\treturn NewEnvelope()\n\t}\n\n\tnonce := core.RandString(16, 36)\n\tcreated := time.Now().UTC().Format(time.RFC3339Nano)\n\tpass, _ := user.Password()\n\n\th := sha1.New()\n\th.Write([]byte(nonce + created + pass))\n\n\te := &Envelope{buf: make([]byte, 0, 1024)}\n\te.Append(prefix1)\n\te.Appendf(`<s:Header>\n\t<wsse:Security xmlns:wsse=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd\">\n\t\t<wsse:UsernameToken>\n\t\t\t<wsse:Username>%s</wsse:Username>\n\t\t\t<wsse:Password Type=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest\">%s</wsse:Password>\n\t\t\t<wsse:Nonce EncodingType=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-soap-message-security-1.0#Base64Binary\">%s</wsse:Nonce>\n\t\t\t<wsu:Created xmlns:wsu=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\">%s</wsu:Created>\n\t\t</wsse:UsernameToken>\n\t</wsse:Security>\n</s:Header>`,\n\t\tuser.Username(),\n\t\tbase64.StdEncoding.EncodeToString(h.Sum(nil)),\n\t\tbase64.StdEncoding.EncodeToString([]byte(nonce)),\n\t\tcreated)\n\te.Append(prefix2)\n\treturn e\n}\n\nfunc (e *Envelope) Append(args ...string) {\n\tfor _, s := range args {\n\t\te.buf = append(e.buf, s...)\n\t}\n}\n\nfunc (e *Envelope) Appendf(format string, args ...any) {\n\te.buf = fmt.Appendf(e.buf, format, args...)\n}\n\nfunc (e *Envelope) Bytes() []byte {\n\treturn append(e.buf, suffix...)\n}\n"
  },
  {
    "path": "pkg/onvif/helpers.go",
    "content": "package onvif\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\ntype DiscoveryDevice struct {\n\tURL      string\n\tName     string\n\tHardware string\n}\n\nfunc FindTagValue(b []byte, tag string) string {\n\tre := regexp.MustCompile(`(?s)<(?:\\w+:)?` + tag + `\\b[^>]*>([^<]+)`)\n\tm := re.FindSubmatch(b)\n\tif len(m) != 2 {\n\t\treturn \"\"\n\t}\n\treturn string(m[1])\n}\n\n// UUID - generate something like 44302cbf-0d18-4feb-79b3-33b575263da3\nfunc UUID() string {\n\ts := core.RandString(32, 16)\n\treturn s[:8] + \"-\" + s[8:12] + \"-\" + s[12:16] + \"-\" + s[16:20] + \"-\" + s[20:]\n}\n\n// DiscoveryStreamingDevices return list of tuple (onvif_url, name, hardware)\nfunc DiscoveryStreamingDevices() ([]DiscoveryDevice, error) {\n\tconn, err := net.ListenUDP(\"udp4\", nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdefer conn.Close()\n\n\t// https://www.onvif.org/wp-content/uploads/2016/12/ONVIF_Feature_Discovery_Specification_16.07.pdf\n\t// 5.3 Discovery Procedure:\n\tmsg := `<?xml version=\"1.0\" ?>\n<s:Envelope xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\">\n\t<s:Header xmlns:a=\"http://schemas.xmlsoap.org/ws/2004/08/addressing\">\n\t\t<a:Action>http://schemas.xmlsoap.org/ws/2005/04/discovery/Probe</a:Action>\n\t\t<a:MessageID>urn:uuid:` + UUID() + `</a:MessageID>\n\t\t<a:To>urn:schemas-xmlsoap-org:ws:2005:04:discovery</a:To>\n\t</s:Header>\n\t<s:Body>\n\t\t<d:Probe xmlns:d=\"http://schemas.xmlsoap.org/ws/2005/04/discovery\">\n\t\t\t<d:Types />\n\t\t\t<d:Scopes />\n\t\t</d:Probe>\n\t</s:Body>\n</s:Envelope>`\n\n\taddr := &net.UDPAddr{\n\t\tIP:   net.IP{239, 255, 255, 250},\n\t\tPort: 3702,\n\t}\n\n\tif _, err = conn.WriteTo([]byte(msg), addr); err != nil {\n\t\treturn nil, err\n\t}\n\n\t_ = conn.SetReadDeadline(time.Now().Add(5 * time.Second))\n\n\tvar devices []DiscoveryDevice\n\n\tb := make([]byte, 8192)\n\tfor {\n\t\tn, addr, err := conn.ReadFromUDP(b)\n\t\tif err != nil {\n\t\t\tbreak\n\t\t}\n\n\t\t//log.Printf(\"[onvif] discovery response addr=%s:\\n%s\", addr, b[:n])\n\n\t\t// ignore printers, etc\n\t\tif !strings.Contains(string(b[:n]), \"onvif\") {\n\t\t\tcontinue\n\t\t}\n\n\t\tdevice := DiscoveryDevice{\n\t\t\tURL: FindTagValue(b[:n], \"XAddrs\"),\n\t\t}\n\n\t\tif device.URL == \"\" {\n\t\t\tcontinue\n\t\t}\n\n\t\t// fix some buggy cameras\n\t\t// <wsdd:XAddrs>http://0.0.0.0:8080/onvif/device_service</wsdd:XAddrs>\n\t\tif s, ok := strings.CutPrefix(device.URL, \"http://0.0.0.0\"); ok {\n\t\t\tdevice.URL = \"http://\" + addr.IP.String() + s\n\t\t}\n\n\t\t// try to find the camera name and model (hardware)\n\t\tscopes := FindTagValue(b[:n], \"Scopes\")\n\t\tdevice.Name = findScope(scopes, \"onvif://www.onvif.org/name/\")\n\t\tdevice.Hardware = findScope(scopes, \"onvif://www.onvif.org/hardware/\")\n\n\t\tdevices = append(devices, device)\n\t}\n\n\treturn devices, nil\n}\n\nfunc findScope(s, prefix string) string {\n\ts = core.Between(s, prefix, \" \")\n\ts, _ = url.QueryUnescape(s)\n\treturn s\n}\n\nfunc atoi(s string) int {\n\tif s == \"\" {\n\t\treturn 0\n\t}\n\ti, err := strconv.Atoi(s)\n\tif err != nil {\n\t\treturn -1\n\t}\n\treturn i\n}\n\nfunc GetPosixTZ(current time.Time) string {\n\t// Thanks to https://github.com/Path-Variable/go-posix-time\n\t_, offset := current.Zone()\n\n\tif current.IsDST() {\n\t\t_, end := current.ZoneBounds()\n\t\tendPlus1 := end.Add(time.Hour * 25)\n\t\t_, offset = endPlus1.Zone()\n\t}\n\n\tvar prefix string\n\tif offset < 0 {\n\t\tprefix = \"GMT+\"\n\t\toffset = -offset / 60\n\t} else {\n\t\tprefix = \"GMT-\"\n\t\toffset = offset / 60\n\t}\n\n\treturn prefix + fmt.Sprintf(\"%02d:%02d\", offset/60, offset%60)\n}\n\nfunc GetPath(urlOrPath, defPath string) string {\n\tif urlOrPath == \"\" || urlOrPath[0] == '/' {\n\t\treturn defPath\n\t}\n\tu, err := url.Parse(urlOrPath)\n\tif err != nil {\n\t\treturn defPath\n\t}\n\treturn GetPath(u.Path, defPath)\n}\n"
  },
  {
    "path": "pkg/onvif/onvif_test.go",
    "content": "package onvif\n\nimport (\n\t\"html\"\n\t\"net/url\"\n\t\"strings\"\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestGetStreamUri(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\txml  string\n\t\turl  string\n\t}{\n\t\t{\n\t\t\tname: \"Dahua stream default\",\n\t\t\txml:  `<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?><s:Envelope xmlns:sc=\"http://www.w3.org/2003/05/soap-encoding\" xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\" xmlns:tt=\"http://www.onvif.org/ver10/schema\" xmlns:trt=\"http://www.onvif.org/ver10/media/wsdl\"><s:Header/><s:Body><trt:GetStreamUriResponse><trt:MediaUri><tt:Uri>rtsp://192.168.1.123:554/cam/realmonitor?channel=1&amp;subtype=1&amp;unicast=true&amp;proto=Onvif</tt:Uri><tt:InvalidAfterConnect>true</tt:InvalidAfterConnect><tt:InvalidAfterReboot>true</tt:InvalidAfterReboot><tt:Timeout>PT0S</tt:Timeout></trt:MediaUri></trt:GetStreamUriResponse></s:Body></s:Envelope>`,\n\t\t\turl:  \"rtsp://192.168.1.123:554/cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif\",\n\t\t},\n\t\t{\n\t\t\tname: \"Dahua snapshot default\",\n\t\t\txml:  `<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?><s:Envelope xmlns:sc=\"http://www.w3.org/2003/05/soap-encoding\" xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\" xmlns:tt=\"http://www.onvif.org/ver10/schema\" xmlns:trt=\"http://www.onvif.org/ver10/media/wsdl\"><s:Header/><s:Body><trt:GetSnapshotUriResponse><trt:MediaUri><tt:Uri>http://192.168.1.123/onvifsnapshot/media_service/snapshot?channel=1&amp;subtype=1</tt:Uri><tt:InvalidAfterConnect>false</tt:InvalidAfterConnect><tt:InvalidAfterReboot>false</tt:InvalidAfterReboot><tt:Timeout>PT0S</tt:Timeout></trt:MediaUri></trt:GetSnapshotUriResponse></s:Body></s:Envelope>`,\n\t\t\turl:  \"http://192.168.1.123/onvifsnapshot/media_service/snapshot?channel=1&subtype=1\",\n\t\t},\n\t\t{\n\t\t\tname: \"Dahua stream formatted\",\n\t\t\txml: `<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>\n<s:Envelope xmlns:sc=\"http://www.w3.org/2003/05/soap-encoding\"\n    xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\" xmlns:tt=\"http://www.onvif.org/ver10/schema\"\n    xmlns:trt=\"http://www.onvif.org/ver10/media/wsdl\">\n    <s:Header />\n    <s:Body>\n        <trt:GetStreamUriResponse>\n            <trt:MediaUri>\n                <tt:Uri>\n                    rtsp://192.168.1.123:554/cam/realmonitor?channel=1&amp;subtype=1&amp;unicast=true&amp;proto=Onvif</tt:Uri>\n                <tt:InvalidAfterConnect>true</tt:InvalidAfterConnect>\n                <tt:InvalidAfterReboot>true</tt:InvalidAfterReboot>\n                <tt:Timeout>PT0S</tt:Timeout>\n            </trt:MediaUri>\n        </trt:GetStreamUriResponse>\n    </s:Body>\n</s:Envelope>`,\n\t\t\turl: \"rtsp://192.168.1.123:554/cam/realmonitor?channel=1&subtype=1&unicast=true&proto=Onvif\",\n\t\t},\n\t\t{\n\t\t\tname: \"Dahua snapshot formatted\",\n\t\t\txml: `<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>\n<s:Envelope xmlns:sc=\"http://www.w3.org/2003/05/soap-encoding\"\n    xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\" xmlns:tt=\"http://www.onvif.org/ver10/schema\"\n    xmlns:trt=\"http://www.onvif.org/ver10/media/wsdl\">\n    <s:Header />\n    <s:Body>\n        <trt:GetSnapshotUriResponse>\n            <trt:MediaUri>\n                <tt:Uri>\n                    http://192.168.1.123/onvifsnapshot/media_service/snapshot?channel=1&amp;subtype=1</tt:Uri>\n                <tt:InvalidAfterConnect>false</tt:InvalidAfterConnect>\n                <tt:InvalidAfterReboot>false</tt:InvalidAfterReboot>\n                <tt:Timeout>PT0S</tt:Timeout>\n            </trt:MediaUri>\n        </trt:GetSnapshotUriResponse>\n    </s:Body>\n</s:Envelope>`,\n\t\t\turl: \"http://192.168.1.123/onvifsnapshot/media_service/snapshot?channel=1&subtype=1\",\n\t\t},\n\t\t{\n\t\t\tname: \"Unknown\",\n\t\t\txml: `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<SOAP-ENV:Envelope ...>\n   <SOAP-ENV:Header></SOAP-ENV:Header>\n   <SOAP-ENV:Body>\n\t   <MC1:GetStreamUriResponse>\n\t\t   <MC1:MediaUri>\n\t\t\t   <MC2:Uri>\n\t\t\t\t\trtsp://192.168.5.53:8090/profile1=r\n\t\t\t\t</MC2:Uri>\n\t\t   </MC1:MediaUri>\n\t   </MC1:GetStreamUriResponse>\n   </SOAP-ENV:Body>\n</SOAP-ENV:Envelope>`,\n\t\t\turl: \"rtsp://192.168.5.53:8090/profile1=r\",\n\t\t},\n\t\t{\n\t\t\tname: \"go2rtc 1.9.4\",\n\t\t\txml: `<?xml version=\"1.0\" encoding=\"utf-8\"?><s:Envelope xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\">\n    <s:Body>\n        <trt:GetStreamUriResponse xmlns:trt=\"http://www.onvif.org/ver10/media/wsdl\">\n            <trt:MediaUri>\n                <tt:Uri xmlns:tt=\"http://www.onvif.org/ver10/schema\">rtsp://192.168.1.123:8554/rtsp-dahua1</tt:Uri>\n            </trt:MediaUri>\n        </trt:GetStreamUriResponse>\n    </s:Body>\n</s:Envelope>`,\n\t\t\turl: \"rtsp://192.168.1.123:8554/rtsp-dahua1\",\n\t\t},\n\t\t{\n\t\t\tname: \"go2rtc 1.9.8\",\n\t\t\txml: `<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>\n<s:Envelope xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\" xmlns:tds=\"http://www.onvif.org/ver10/device/wsdl\" xmlns:trt=\"http://www.onvif.org/ver10/media/wsdl\" xmlns:tt=\"http://www.onvif.org/ver10/schema\">\n    <s:Body>\n        <trt:GetStreamUriResponse>\n            <trt:MediaUri>\n                <tt:Uri>rtsp://192.168.1.123:8554/rtsp-dahua2</tt:Uri>\n            </trt:MediaUri>\n        </trt:GetStreamUriResponse>\n    </s:Body>\n</s:Envelope>\n`,\n\t\t\turl: \"rtsp://192.168.1.123:8554/rtsp-dahua2\",\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\turi := FindTagValue([]byte(test.xml), \"Uri\")\n\t\t\turi = strings.TrimSpace(html.UnescapeString(uri))\n\t\t\tu, err := url.Parse(uri)\n\t\t\trequire.Nil(t, err)\n\t\t\trequire.Equal(t, test.url, u.String())\n\t\t})\n\t}\n}\n\nfunc TestGetCapabilities(t *testing.T) {\n\ttests := []struct {\n\t\tname string\n\t\txml  string\n\t}{\n\t\t{\n\t\t\tname: \"Dahua default\",\n\t\t\txml:  `<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\" ?><s:Envelope xmlns:sc=\"http://www.w3.org/2003/05/soap-encoding\" xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\" xmlns:tt=\"http://www.onvif.org/ver10/schema\" xmlns:tds=\"http://www.onvif.org/ver10/device/wsdl\"><s:Header/><s:Body><tds:GetCapabilitiesResponse><tds:Capabilities><tt:Analytics><tt:XAddr>http://192.168.1.123/onvif/analytics_service</tt:XAddr><tt:RuleSupport>true</tt:RuleSupport><tt:AnalyticsModuleSupport>true</tt:AnalyticsModuleSupport></tt:Analytics><tt:Device><tt:XAddr>http://192.168.1.123/onvif/device_service</tt:XAddr><tt:Network><tt:IPFilter>false</tt:IPFilter><tt:ZeroConfiguration>false</tt:ZeroConfiguration><tt:IPVersion6>false</tt:IPVersion6><tt:DynDNS>false</tt:DynDNS><tt:Extension><tt:Dot11Configuration>false</tt:Dot11Configuration></tt:Extension></tt:Network><tt:System><tt:DiscoveryResolve>false</tt:DiscoveryResolve><tt:DiscoveryBye>true</tt:DiscoveryBye><tt:RemoteDiscovery>false</tt:RemoteDiscovery><tt:SystemBackup>false</tt:SystemBackup><tt:SystemLogging>true</tt:SystemLogging><tt:FirmwareUpgrade>true</tt:FirmwareUpgrade><tt:SupportedVersions><tt:Major>2</tt:Major><tt:Minor>00</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>2</tt:Major><tt:Minor>10</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>2</tt:Major><tt:Minor>20</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>2</tt:Major><tt:Minor>30</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>2</tt:Major><tt:Minor>40</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>2</tt:Major><tt:Minor>42</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>16</tt:Major><tt:Minor>12</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>18</tt:Major><tt:Minor>06</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>18</tt:Major><tt:Minor>12</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>19</tt:Major><tt:Minor>06</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>19</tt:Major><tt:Minor>12</tt:Minor></tt:SupportedVersions><tt:SupportedVersions><tt:Major>20</tt:Major><tt:Minor>06</tt:Minor></tt:SupportedVersions><tt:Extension><tt:HttpFirmwareUpgrade>true</tt:HttpFirmwareUpgrade><tt:HttpSystemBackup>false</tt:HttpSystemBackup><tt:HttpSystemLogging>false</tt:HttpSystemLogging><tt:HttpSupportInformation>false</tt:HttpSupportInformation></tt:Extension></tt:System><tt:IO><tt:InputConnectors>2</tt:InputConnectors><tt:RelayOutputs>1</tt:RelayOutputs><tt:Extension><tt:Auxiliary>false</tt:Auxiliary><tt:AuxiliaryCommands></tt:AuxiliaryCommands><tt:Extension></tt:Extension></tt:Extension></tt:IO><tt:Security><tt:TLS1.1>false</tt:TLS1.1><tt:TLS1.2>false</tt:TLS1.2><tt:OnboardKeyGeneration>false</tt:OnboardKeyGeneration><tt:AccessPolicyConfig>false</tt:AccessPolicyConfig><tt:X.509Token>false</tt:X.509Token><tt:SAMLToken>false</tt:SAMLToken><tt:KerberosToken>false</tt:KerberosToken><tt:RELToken>false</tt:RELToken><tt:Extension><tt:TLS1.0>false</tt:TLS1.0><tt:Extension><tt:Dot1X>false</tt:Dot1X><tt:SupportedEAPMethod>0</tt:SupportedEAPMethod><tt:RemoteUserHandling>false</tt:RemoteUserHandling></tt:Extension></tt:Extension></tt:Security></tt:Device><tt:Events><tt:XAddr>http://192.168.1.123/onvif/event_service</tt:XAddr><tt:WSSubscriptionPolicySupport>true</tt:WSSubscriptionPolicySupport><tt:WSPullPointSupport>true</tt:WSPullPointSupport><tt:WSPausableSubscriptionManagerInterfaceSupport>false</tt:WSPausableSubscriptionManagerInterfaceSupport></tt:Events><tt:Imaging><tt:XAddr>http://192.168.1.123/onvif/imaging_service</tt:XAddr></tt:Imaging><tt:Media><tt:XAddr>http://192.168.1.123/onvif/media_service</tt:XAddr><tt:StreamingCapabilities><tt:RTPMulticast>true</tt:RTPMulticast><tt:RTP_TCP>true</tt:RTP_TCP><tt:RTP_RTSP_TCP>true</tt:RTP_RTSP_TCP></tt:StreamingCapabilities><tt:Extension><tt:ProfileCapabilities><tt:MaximumNumberOfProfiles>6</tt:MaximumNumberOfProfiles></tt:ProfileCapabilities></tt:Extension></tt:Media><tt:Extension><tt:DeviceIO><tt:XAddr>http://192.168.1.123/onvif/deviceIO_service</tt:XAddr><tt:VideoSources>1</tt:VideoSources><tt:VideoOutputs>0</tt:VideoOutputs><tt:AudioSources>1</tt:AudioSources><tt:AudioOutputs>1</tt:AudioOutputs><tt:RelayOutputs>1</tt:RelayOutputs></tt:DeviceIO></tt:Extension></tds:Capabilities></tds:GetCapabilitiesResponse></s:Body></s:Envelope>`,\n\t\t},\n\t\t{\n\t\t\tname: \"Dahua formatted\",\n\t\t\txml: `<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"yes\"?>\n<s:Envelope xmlns:sc=\"http://www.w3.org/2003/05/soap-encoding\"\n    xmlns:s=\"http://www.w3.org/2003/05/soap-envelope\" xmlns:tt=\"http://www.onvif.org/ver10/schema\"\n    xmlns:tds=\"http://www.onvif.org/ver10/device/wsdl\">\n    <s:Header />\n    <s:Body>\n        <tds:GetCapabilitiesResponse>\n            <tds:Capabilities>\n                <tt:Analytics>\n                    <tt:XAddr>http://192.168.1.123/onvif/analytics_service</tt:XAddr>\n                    <tt:RuleSupport>true</tt:RuleSupport>\n                    <tt:AnalyticsModuleSupport>true</tt:AnalyticsModuleSupport>\n                </tt:Analytics>\n                <tt:Device>\n                    <tt:XAddr>http://192.168.1.123/onvif/device_service</tt:XAddr>\n                    <tt:Network>\n                        <tt:IPFilter>false</tt:IPFilter>\n                        <tt:ZeroConfiguration>false</tt:ZeroConfiguration>\n                        <tt:IPVersion6>false</tt:IPVersion6>\n                        <tt:DynDNS>false</tt:DynDNS>\n                        <tt:Extension>\n                            <tt:Dot11Configuration>false</tt:Dot11Configuration>\n                        </tt:Extension>\n                    </tt:Network>\n                    <tt:System>\n                        ...\n                    </tt:System>\n                    <tt:IO>\n                        <tt:InputConnectors>2</tt:InputConnectors>\n                        <tt:RelayOutputs>1</tt:RelayOutputs>\n                        <tt:Extension>\n                            <tt:Auxiliary>false</tt:Auxiliary>\n                            <tt:AuxiliaryCommands></tt:AuxiliaryCommands>\n                            <tt:Extension></tt:Extension>\n                        </tt:Extension>\n                    </tt:IO>\n                    <tt:Security>\n                        ...\n                    </tt:Security>\n                </tt:Device>\n                <tt:Events>\n                    <tt:XAddr>http://192.168.1.123/onvif/event_service</tt:XAddr>\n                    <tt:WSSubscriptionPolicySupport>true</tt:WSSubscriptionPolicySupport>\n                    <tt:WSPullPointSupport>true</tt:WSPullPointSupport>\n                    <tt:WSPausableSubscriptionManagerInterfaceSupport>false</tt:WSPausableSubscriptionManagerInterfaceSupport>\n                </tt:Events>\n                <tt:Imaging>\n                    <tt:XAddr>http://192.168.1.123/onvif/imaging_service</tt:XAddr>\n                </tt:Imaging>\n                <tt:Media>\n                    <tt:XAddr>http://192.168.1.123/onvif/media_service</tt:XAddr>\n                    <tt:StreamingCapabilities>\n                        <tt:RTPMulticast>true</tt:RTPMulticast>\n                        <tt:RTP_TCP>true</tt:RTP_TCP>\n                        <tt:RTP_RTSP_TCP>true</tt:RTP_RTSP_TCP>\n                    </tt:StreamingCapabilities>\n                    <tt:Extension>\n                        <tt:ProfileCapabilities>\n                            <tt:MaximumNumberOfProfiles>6</tt:MaximumNumberOfProfiles>\n                        </tt:ProfileCapabilities>\n                    </tt:Extension>\n                </tt:Media>\n                <tt:Extension>\n                    <tt:DeviceIO>\n                        <tt:XAddr>http://192.168.1.123/onvif/deviceIO_service</tt:XAddr>\n                        <tt:VideoSources>1</tt:VideoSources>\n                        <tt:VideoOutputs>0</tt:VideoOutputs>\n                        <tt:AudioSources>1</tt:AudioSources>\n                        <tt:AudioOutputs>1</tt:AudioOutputs>\n                        <tt:RelayOutputs>1</tt:RelayOutputs>\n                    </tt:DeviceIO>\n                </tt:Extension>\n            </tds:Capabilities>\n        </tds:GetCapabilitiesResponse>\n    </s:Body>\n</s:Envelope>`,\n\t\t},\n\t}\n\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\trawURL := FindTagValue([]byte(test.xml), \"Media.+?XAddr\")\n\t\t\trequire.Equal(t, \"http://192.168.1.123/onvif/media_service\", rawURL)\n\n\t\t\trawURL = FindTagValue([]byte(test.xml), \"Imaging.+?XAddr\")\n\t\t\trequire.Equal(t, \"http://192.168.1.123/onvif/imaging_service\", rawURL)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/onvif/server.go",
    "content": "package onvif\n\nimport (\n\t\"bytes\"\n\t\"regexp\"\n\t\"time\"\n)\n\nconst ServiceGetServiceCapabilities = \"GetServiceCapabilities\"\n\nconst (\n\tDeviceGetCapabilities          = \"GetCapabilities\"\n\tDeviceGetDeviceInformation     = \"GetDeviceInformation\"\n\tDeviceGetDiscoveryMode         = \"GetDiscoveryMode\"\n\tDeviceGetDNS                   = \"GetDNS\"\n\tDeviceGetHostname              = \"GetHostname\"\n\tDeviceGetNetworkDefaultGateway = \"GetNetworkDefaultGateway\"\n\tDeviceGetNetworkInterfaces     = \"GetNetworkInterfaces\"\n\tDeviceGetNetworkProtocols      = \"GetNetworkProtocols\"\n\tDeviceGetNTP                   = \"GetNTP\"\n\tDeviceGetScopes                = \"GetScopes\"\n\tDeviceGetServices              = \"GetServices\"\n\tDeviceGetSystemDateAndTime     = \"GetSystemDateAndTime\"\n\tDeviceSetSystemDateAndTime     = \"SetSystemDateAndTime\"\n\tDeviceSystemReboot             = \"SystemReboot\"\n)\n\nconst (\n\tMediaGetAudioEncoderConfigurations       = \"GetAudioEncoderConfigurations\"\n\tMediaGetAudioSources                     = \"GetAudioSources\"\n\tMediaGetAudioSourceConfigurations        = \"GetAudioSourceConfigurations\"\n\tMediaGetProfile                          = \"GetProfile\"\n\tMediaGetProfiles                         = \"GetProfiles\"\n\tMediaGetSnapshotUri                      = \"GetSnapshotUri\"\n\tMediaGetStreamUri                        = \"GetStreamUri\"\n\tMediaGetVideoEncoderConfiguration        = \"GetVideoEncoderConfiguration\"\n\tMediaGetVideoEncoderConfigurations       = \"GetVideoEncoderConfigurations\"\n\tMediaGetVideoEncoderConfigurationOptions = \"GetVideoEncoderConfigurationOptions\"\n\tMediaGetVideoSources                     = \"GetVideoSources\"\n\tMediaGetVideoSourceConfiguration         = \"GetVideoSourceConfiguration\"\n\tMediaGetVideoSourceConfigurations        = \"GetVideoSourceConfigurations\"\n)\n\nfunc GetRequestAction(b []byte) string {\n\t// <soap-env:Body><ns0:GetCapabilities xmlns:ns0=\"http://www.onvif.org/ver10/device/wsdl\">\n\t// <v:Body><GetSystemDateAndTime xmlns=\"http://www.onvif.org/ver10/device/wsdl\" /></v:Body>\n\tre := regexp.MustCompile(`Body[^<]+<([^ />]+)`)\n\tm := re.FindSubmatch(b)\n\tif len(m) != 2 {\n\t\treturn \"\"\n\t}\n\tif i := bytes.IndexByte(m[1], ':'); i > 0 {\n\t\treturn string(m[1][i+1:])\n\t}\n\treturn string(m[1])\n}\n\nfunc GetCapabilitiesResponse(host string) []byte {\n\te := NewEnvelope()\n\te.Appendf(`<tds:GetCapabilitiesResponse>\n\t<tds:Capabilities>\n\t\t<tt:Device>\n\t\t\t<tt:XAddr>http://%s/onvif/device_service</tt:XAddr>\n\t\t</tt:Device>\n\t\t<tt:Media>\n\t\t\t<tt:XAddr>http://%s/onvif/media_service</tt:XAddr>\n\t\t\t<tt:StreamingCapabilities>\n\t\t\t\t<tt:RTPMulticast>false</tt:RTPMulticast>\n\t\t\t\t<tt:RTP_TCP>false</tt:RTP_TCP>\n\t\t\t\t<tt:RTP_RTSP_TCP>true</tt:RTP_RTSP_TCP>\n\t\t\t</tt:StreamingCapabilities>\n\t\t</tt:Media>\n\t</tds:Capabilities>\n</tds:GetCapabilitiesResponse>`, host, host)\n\treturn e.Bytes()\n}\n\nfunc GetServicesResponse(host string) []byte {\n\te := NewEnvelope()\n\te.Appendf(`<tds:GetServicesResponse>\n\t<tds:Service>\n\t\t<tds:Namespace>http://www.onvif.org/ver10/device/wsdl</tds:Namespace>\n\t\t<tds:XAddr>http://%s/onvif/device_service</tds:XAddr>\n\t\t<tds:Version><tt:Major>2</tt:Major><tt:Minor>5</tt:Minor></tds:Version>\n\t</tds:Service>\n\t<tds:Service>\n\t\t<tds:Namespace>http://www.onvif.org/ver10/media/wsdl</tds:Namespace>\n\t\t<tds:XAddr>http://%s/onvif/media_service</tds:XAddr>\n\t\t<tds:Version><tt:Major>2</tt:Major><tt:Minor>5</tt:Minor></tds:Version>\n\t</tds:Service>\n</tds:GetServicesResponse>`, host, host)\n\treturn e.Bytes()\n}\n\nfunc GetSystemDateAndTimeResponse() []byte {\n\tloc := time.Now()\n\tutc := loc.UTC()\n\n\te := NewEnvelope()\n\te.Appendf(`<tds:GetSystemDateAndTimeResponse>\n\t<tds:SystemDateAndTime>\n\t\t<tt:DateTimeType>NTP</tt:DateTimeType>\n\t\t<tt:DaylightSavings>true</tt:DaylightSavings>\n\t\t<tt:TimeZone>\n\t\t\t<tt:TZ>%s</tt:TZ>\n\t\t</tt:TimeZone>\n\t\t<tt:UTCDateTime>\n\t\t\t<tt:Time><tt:Hour>%d</tt:Hour><tt:Minute>%d</tt:Minute><tt:Second>%d</tt:Second></tt:Time>\n\t\t\t<tt:Date><tt:Year>%d</tt:Year><tt:Month>%d</tt:Month><tt:Day>%d</tt:Day></tt:Date>\n\t\t</tt:UTCDateTime>\n\t\t<tt:LocalDateTime>\n\t\t\t<tt:Time><tt:Hour>%d</tt:Hour><tt:Minute>%d</tt:Minute><tt:Second>%d</tt:Second></tt:Time>\n\t\t\t<tt:Date><tt:Year>%d</tt:Year><tt:Month>%d</tt:Month><tt:Day>%d</tt:Day></tt:Date>\n\t\t</tt:LocalDateTime>\n\t</tds:SystemDateAndTime>\n</tds:GetSystemDateAndTimeResponse>`,\n\t\tGetPosixTZ(loc),\n\t\tutc.Hour(), utc.Minute(), utc.Second(), utc.Year(), utc.Month(), utc.Day(),\n\t\tloc.Hour(), loc.Minute(), loc.Second(), loc.Year(), loc.Month(), loc.Day(),\n\t)\n\treturn e.Bytes()\n}\n\nfunc GetDeviceInformationResponse(manuf, model, firmware, serial string) []byte {\n\te := NewEnvelope()\n\te.Appendf(`<tds:GetDeviceInformationResponse>\n\t<tds:Manufacturer>%s</tds:Manufacturer>\n\t<tds:Model>%s</tds:Model>\n\t<tds:FirmwareVersion>%s</tds:FirmwareVersion>\n\t<tds:SerialNumber>%s</tds:SerialNumber>\n\t<tds:HardwareId>1.00</tds:HardwareId>\n</tds:GetDeviceInformationResponse>`, manuf, model, firmware, serial)\n\treturn e.Bytes()\n}\n\nfunc GetProfilesResponse(names []string) []byte {\n\te := NewEnvelope()\n\te.Append(`<trt:GetProfilesResponse>`)\n\tfor _, name := range names {\n\t\tappendProfile(e, \"Profiles\", name)\n\t}\n\te.Append(`</trt:GetProfilesResponse>`)\n\treturn e.Bytes()\n}\n\nfunc GetProfileResponse(name string) []byte {\n\te := NewEnvelope()\n\te.Append(`<trt:GetProfileResponse>`)\n\tappendProfile(e, \"Profile\", name)\n\te.Append(`</trt:GetProfileResponse>`)\n\treturn e.Bytes()\n}\n\nfunc appendProfile(e *Envelope, tag, name string) {\n\t// go2rtc name = ONVIF Profile Name = ONVIF Profile token\n\te.Appendf(`<trt:%s token=\"%s\" fixed=\"true\">`, tag, name)\n\te.Appendf(`<tt:Name>%s</tt:Name>`, name)\n\tappendVideoSourceConfiguration(e, \"VideoSourceConfiguration\", name)\n\tappendVideoEncoderConfiguration(e, \"VideoEncoderConfiguration\")\n\te.Appendf(`</trt:%s>`, tag)\n}\n\nfunc GetVideoSourcesResponse(names []string) []byte {\n\t// go2rtc name = ONVIF VideoSource token\n\te := NewEnvelope()\n\te.Append(`<trt:GetVideoSourcesResponse>`)\n\tfor _, name := range names {\n\t\te.Appendf(`<trt:VideoSources token=\"%s\">\n\t<tt:Framerate>30.000000</tt:Framerate>\n\t<tt:Resolution><tt:Width>1920</tt:Width><tt:Height>1080</tt:Height></tt:Resolution>\n</trt:VideoSources>`, name)\n\t}\n\te.Append(`</trt:GetVideoSourcesResponse>`)\n\treturn e.Bytes()\n}\n\nfunc GetVideoSourceConfigurationsResponse(names []string) []byte {\n\te := NewEnvelope()\n\te.Append(`<trt:GetVideoSourceConfigurationsResponse>`)\n\tfor _, name := range names {\n\t\tappendVideoSourceConfiguration(e, \"Configurations\", name)\n\t}\n\te.Append(`</trt:GetVideoSourceConfigurationsResponse>`)\n\treturn e.Bytes()\n}\n\nfunc GetVideoSourceConfigurationResponse(name string) []byte {\n\te := NewEnvelope()\n\te.Append(`<trt:GetVideoSourceConfigurationResponse>`)\n\tappendVideoSourceConfiguration(e, \"Configuration\", name)\n\te.Append(`</trt:GetVideoSourceConfigurationResponse>`)\n\treturn e.Bytes()\n}\n\nfunc appendVideoSourceConfiguration(e *Envelope, tag, name string) {\n\t// go2rtc name = ONVIF VideoSourceConfiguration token\n\te.Appendf(`<tt:%s token=\"%s\" fixed=\"true\">\n\t<tt:Name>VSC</tt:Name>\n\t<tt:SourceToken>%s</tt:SourceToken>\n\t<tt:Bounds x=\"0\" y=\"0\" width=\"1920\" height=\"1080\"></tt:Bounds>\n</tt:%s>`, tag, name, name, tag)\n}\n\nfunc GetVideoEncoderConfigurationsResponse() []byte {\n\te := NewEnvelope()\n\te.Append(`<trt:GetVideoEncoderConfigurationsResponse>`)\n\tappendVideoEncoderConfiguration(e, \"VideoEncoderConfigurations\")\n\te.Append(`</trt:GetVideoEncoderConfigurationsResponse>`)\n\treturn e.Bytes()\n}\n\nfunc GetVideoEncoderConfigurationResponse() []byte {\n\te := NewEnvelope()\n\te.Append(`<trt:GetVideoEncoderConfigurationResponse>`)\n\tappendVideoEncoderConfiguration(e, \"VideoEncoderConfiguration\")\n\te.Append(`</trt:GetVideoEncoderConfigurationResponse>`)\n\treturn e.Bytes()\n}\n\nfunc appendVideoEncoderConfiguration(e *Envelope, tag string) {\n\t// empty `RateControl` important for UniFi Protect\n\te.Appendf(`<tt:%s token=\"vec\">\n\t\t<tt:Name>VEC</tt:Name>\n        <tt:UseCount>1</tt:UseCount>\n\t\t<tt:Encoding>H264</tt:Encoding>\n\t\t<tt:Resolution><tt:Width>1920</tt:Width><tt:Height>1080</tt:Height></tt:Resolution>\n        <tt:Quality>0</tt:Quality>\n\t\t<tt:RateControl><tt:FrameRateLimit>30</tt:FrameRateLimit><tt:EncodingInterval>1</tt:EncodingInterval><tt:BitrateLimit>8192</tt:BitrateLimit></tt:RateControl>\n        <tt:H264><tt:GovLength>10</tt:GovLength><tt:H264Profile>Main</tt:H264Profile></tt:H264>\n        <tt:SessionTimeout>PT10S</tt:SessionTimeout>\n\t</tt:%s>`, tag, tag)\n}\n\nfunc GetStreamUriResponse(uri string) []byte {\n\te := NewEnvelope()\n\te.Appendf(`<trt:GetStreamUriResponse><trt:MediaUri><tt:Uri>%s</tt:Uri></trt:MediaUri></trt:GetStreamUriResponse>`, uri)\n\treturn e.Bytes()\n}\n\nfunc GetSnapshotUriResponse(uri string) []byte {\n\te := NewEnvelope()\n\te.Appendf(`<trt:GetSnapshotUriResponse><trt:MediaUri><tt:Uri>%s</tt:Uri></trt:MediaUri></trt:GetSnapshotUriResponse>`, uri)\n\treturn e.Bytes()\n}\n\nfunc StaticResponse(operation string) []byte {\n\tswitch operation {\n\tcase DeviceGetSystemDateAndTime:\n\t\treturn GetSystemDateAndTimeResponse()\n\tcase MediaGetVideoEncoderConfiguration:\n\t\treturn GetVideoEncoderConfigurationResponse()\n\tcase MediaGetVideoEncoderConfigurations:\n\t\treturn GetVideoEncoderConfigurationsResponse()\n\t}\n\n\te := NewEnvelope()\n\te.Append(responses[operation])\n\treturn e.Bytes()\n}\n\nvar responses = map[string]string{\n\tServiceGetServiceCapabilities: `<trt:GetServiceCapabilitiesResponse>\n\t<trt:Capabilities SnapshotUri=\"true\" Rotation=\"false\" VideoSourceMode=\"false\" OSD=\"false\" TemporaryOSDText=\"false\" EXICompression=\"false\">\n\t\t<trt:StreamingCapabilities RTPMulticast=\"false\" RTP_TCP=\"false\" RTP_RTSP_TCP=\"true\" NonAggregateControl=\"false\" NoRTSPStreaming=\"false\" />\n\t</trt:Capabilities>\n</trt:GetServiceCapabilitiesResponse>`,\n\n\tDeviceGetDiscoveryMode:         `<tds:GetDiscoveryModeResponse><tds:DiscoveryMode>Discoverable</tds:DiscoveryMode></tds:GetDiscoveryModeResponse>`,\n\tDeviceGetDNS:                   `<tds:GetDNSResponse><tds:DNSInformation /></tds:GetDNSResponse>`,\n\tDeviceGetHostname:              `<tds:GetHostnameResponse><tds:HostnameInformation /></tds:GetHostnameResponse>`,\n\tDeviceGetNetworkDefaultGateway: `<tds:GetNetworkDefaultGatewayResponse><tds:NetworkGateway /></tds:GetNetworkDefaultGatewayResponse>`,\n\tDeviceGetNTP:                   `<tds:GetNTPResponse><tds:NTPInformation /></tds:GetNTPResponse>`,\n\tDeviceSetSystemDateAndTime:     `<tds:SetSystemDateAndTimeResponse />`,\n\tDeviceSystemReboot:             `<tds:SystemRebootResponse><tds:Message>OK</tds:Message></tds:SystemRebootResponse>`,\n\n\tDeviceGetNetworkInterfaces: `<tds:GetNetworkInterfacesResponse />`,\n\tDeviceGetNetworkProtocols:  `<tds:GetNetworkProtocolsResponse />`,\n\tDeviceGetScopes: `<tds:GetScopesResponse>\n\t<tds:Scopes><tt:ScopeDef>Fixed</tt:ScopeDef><tt:ScopeItem>onvif://www.onvif.org/name/go2rtc</tt:ScopeItem></tds:Scopes>\n\t<tds:Scopes><tt:ScopeDef>Fixed</tt:ScopeDef><tt:ScopeItem>onvif://www.onvif.org/location/github</tt:ScopeItem></tds:Scopes>\n\t<tds:Scopes><tt:ScopeDef>Fixed</tt:ScopeDef><tt:ScopeItem>onvif://www.onvif.org/Profile/Streaming</tt:ScopeItem></tds:Scopes>\n\t<tds:Scopes><tt:ScopeDef>Fixed</tt:ScopeDef><tt:ScopeItem>onvif://www.onvif.org/type/Network_Video_Transmitter</tt:ScopeItem></tds:Scopes>\n</tds:GetScopesResponse>`,\n\n\tMediaGetAudioEncoderConfigurations: `<trt:GetAudioEncoderConfigurationsResponse />`,\n\tMediaGetAudioSources:               `<trt:GetAudioSourcesResponse />`,\n\tMediaGetAudioSourceConfigurations:  `<trt:GetAudioSourceConfigurationsResponse />`,\n\n\tMediaGetVideoEncoderConfigurationOptions: `<trt:GetVideoEncoderConfigurationOptionsResponse>\n   <trt:Options>\n       <tt:QualityRange><tt:Min>1</tt:Min><tt:Max>6</tt:Max></tt:QualityRange>\n\t   <tt:H264>\n\t\t   <tt:ResolutionsAvailable><tt:Width>1920</tt:Width><tt:Height>1080</tt:Height></tt:ResolutionsAvailable>\n\t\t   <tt:GovLengthRange><tt:Min>0</tt:Min><tt:Max>100</tt:Max></tt:GovLengthRange>\n\t\t   <tt:FrameRateRange><tt:Min>1</tt:Min><tt:Max>30</tt:Max></tt:FrameRateRange>\n\t\t   <tt:EncodingIntervalRange><tt:Min>1</tt:Min><tt:Max>100</tt:Max></tt:EncodingIntervalRange>\n           <tt:H264ProfilesSupported>Main</tt:H264ProfilesSupported>\n\t   </tt:H264>\n   </trt:Options>\n</trt:GetVideoEncoderConfigurationOptionsResponse>`,\n}\n"
  },
  {
    "path": "pkg/opus/README.md",
    "content": "## Useful links\n\n- [RFC 3550: RTP: A Transport Protocol for Real-Time Applications](https://datatracker.ietf.org/doc/html/rfc3550)\n- [RFC 6716: Definition of the Opus Audio Codec](https://datatracker.ietf.org/doc/html/rfc6716)\n- [RFC 7587: RTP Payload Format for the Opus Speech and Audio Codec](https://datatracker.ietf.org/doc/html/rfc7587)\n"
  },
  {
    "path": "pkg/opus/homekit.go",
    "content": "package opus\n\nimport (\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/pion/rtp\"\n)\n\n// Some info about this magic:\n// - Apple has no respect for RFC 7587 standard and using RFC 3550 for RTP timestamps\n// - Apple can request packets with 20ms duration over LAN connection and 60ms over LTE\n// - FFmpeg produce packets with 20ms duration by default and only one frame per packet\n// - FFmpeg should use \"-min_comp 0\" option, so every packet will be same duration\n// - Apple doesn't care about real sample rate of track\n// - Apple only cares about proper timestamp based on REQUESTED sample rate\n\n// RepackToHAP - convert standart RTP packet with OPUS to HAP packet\n// We expect that:\n// - incoming packet will be 20ms duration and only one frame per packet\n// - outgouing packet will be 20ms or 60ms duration\n// - incoming sample rate will be any (but not very big if we needs 60ms packets for output)\n// - outgouing sample rate will be 16000\n// https://github.com/AlexxIT/go2rtc/issues/667\nfunc RepackToHAP(rtpTime byte, handler core.HandlerFunc) core.HandlerFunc {\n\tswitch rtpTime {\n\tcase 20:\n\t\treturn repackToHAP20(handler)\n\tcase 60:\n\t\treturn repackToHAP60(handler)\n\t}\n\treturn handler\n}\n\n// we using only one sample rate in the pkg/hap/camera/accessory.go\nconst (\n\ttimestamp20 = 16000 * 0.020\n\ttimestamp60 = 16000 * 0.060\n)\n\n// repackToHAP20 - just fix RTP timestamp from RFC 7587 to RFC 3550\nfunc repackToHAP20(handler core.HandlerFunc) core.HandlerFunc {\n\tvar timestamp uint32\n\n\treturn func(pkt *rtp.Packet) {\n\t\ttimestamp += timestamp20\n\n\t\tclone := *pkt\n\t\tclone.Timestamp = timestamp\n\t\thandler(&clone)\n\t}\n}\n\n// repackToHAP60 - collect 20ms frames to single 60ms packet\n// thanks to @civita idea https://github.com/AlexxIT/go2rtc/pull/843\nfunc repackToHAP60(handler core.HandlerFunc) core.HandlerFunc {\n\tvar sequence uint16\n\tvar timestamp uint32\n\n\tvar framesCount byte\n\tvar framesSize []byte\n\tvar framesData []byte\n\n\treturn func(pkt *rtp.Packet) {\n\t\tframesData = append(framesData, pkt.Payload[1:]...)\n\n\t\tif framesCount++; framesCount < 3 {\n\t\t\tif frameSize := len(pkt.Payload) - 1; frameSize >= 252 {\n\t\t\t\tb0 := 252 + byte(frameSize)&0b11\n\t\t\t\tframesSize = append(framesSize, b0, byte(frameSize/4)-b0)\n\t\t\t} else {\n\t\t\t\tframesSize = append(framesSize, byte(frameSize))\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\ttoc := pkt.Payload[0]\n\n\t\tpayload := make([]byte, 2, 2+len(framesSize)+len(framesData))\n\t\tpayload[0] = toc | 0b11  // code 3 (multiple frames per packet)\n\t\tpayload[1] = 0b1000_0011 // VBR, no padding, 3 frames\n\t\tpayload = append(payload, framesSize...)\n\t\tpayload = append(payload, framesData...)\n\n\t\tsequence++\n\t\ttimestamp += timestamp60\n\n\t\tclone := *pkt\n\t\tclone.Payload = payload\n\t\tclone.SequenceNumber = sequence\n\t\tclone.Timestamp = timestamp\n\t\thandler(&clone)\n\n\t\tframesCount = 0\n\t\tframesSize = framesSize[:0]\n\t\tframesData = framesData[:0]\n\t}\n}\n"
  },
  {
    "path": "pkg/opus/opus.go",
    "content": "package opus\n\nimport (\n\t\"time\"\n)\n\ntype Header struct {\n\tMode       string\n\tSampleRate uint16\n\tFrameSize  time.Duration\n\tChannels   byte\n\tFrames     byte\n}\n\nfunc UnmarshalHeader(b []byte) *Header {\n\t// https://datatracker.ietf.org/doc/html/rfc6716#section-3.1\n\tb0 := b[0]\n\tconfig := b0 >> 3\n\treturn &Header{\n\t\tMode:       parseMode(config),\n\t\tSampleRate: parseSampleRate(config),\n\t\tFrameSize:  parseFrameSize(config),\n\t\tChannels:   parseChannels(b0 >> 2 & 0b1),\n\t\tFrames:     parseFrames(b0 & 0b11),\n\t}\n}\n\nfunc parseMode(config byte) string {\n\tif config <= 11 {\n\t\treturn \"silk\"\n\t}\n\tif config <= 15 {\n\t\treturn \"hybrid\"\n\t}\n\treturn \"celt\"\n}\n\nfunc parseSampleRate(config byte) uint16 {\n\tswitch config {\n\tcase 0, 1, 2, 3, 16, 17, 18, 19:\n\t\treturn 8000 // NB (narrowband)\n\tcase 4, 5, 6, 7:\n\t\treturn 12000 // MB (medium-band)\n\tcase 8, 9, 10, 11, 20, 21, 22, 23:\n\t\treturn 16000 // WB (wideband)\n\tcase 12, 13, 24, 25, 26, 27:\n\t\treturn 24000 // SWB (super-wideband)\n\tcase 14, 15, 28, 29, 30, 31:\n\t\treturn 48000 // FB (fullband)\n\t}\n\treturn 0\n}\n\nfunc parseFrameSize(config byte) time.Duration {\n\tswitch config {\n\tcase 0, 4, 8, 12, 14, 18, 22, 26, 30:\n\t\treturn 10_000_000\n\tcase 1, 5, 9, 13, 15, 19, 23, 27, 31:\n\t\treturn 20_000_000\n\tcase 2, 6, 10:\n\t\treturn 40_000_000\n\tcase 3, 7, 11:\n\t\treturn 60_000_000\n\tcase 16, 20, 24, 28:\n\t\treturn 2_500_000\n\tcase 17, 21, 25, 29:\n\t\treturn 5_000_000\n\t}\n\treturn 0\n}\n\nfunc parseChannels(s byte) byte {\n\tif s == 1 {\n\t\treturn 2\n\t}\n\treturn 1\n}\n\nfunc parseFrames(c byte) byte {\n\tswitch c {\n\tcase 0:\n\t\treturn 1\n\tcase 1, 2:\n\t\treturn 2\n\t}\n\treturn 0xFF\n}\n\nfunc JoinFrames(b1, b2 []byte) []byte {\n\t// can't join\n\tif b1[0]&0b11 != 0 || b2[0]&0b11 != 0 {\n\t\treturn append(b1, b2...)\n\t}\n\n\tsize1, size2 := len(b1)-1, len(b2)-1\n\n\t// join same sizes\n\tif size1 == size2 {\n\t\tb := make([]byte, 1+size1+size2)\n\t\tcopy(b, b1)\n\t\tcopy(b[1+size1:], b2[1:])\n\t\tb[0] |= 0b01\n\t\treturn b\n\t}\n\n\tb := make([]byte, 1, 3+size1+size2)\n\tb[0] = b1[0] | 0b10\n\tif size1 >= 252 {\n\t\tb0 := 252 + byte(size1)&0b11\n\t\tb = append(b, b0, byte(size1/4)-b0)\n\t} else {\n\t\tb = append(b, byte(size1))\n\t}\n\n\tb = append(b, b1[1:]...)\n\tb = append(b, b2[1:]...)\n\treturn b\n}\n"
  },
  {
    "path": "pkg/pcm/backchannel.go",
    "content": "package pcm\n\nimport (\n\t\"errors\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/shell\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Backchannel struct {\n\tcore.Connection\n\tcmd *shell.Command\n}\n\nfunc NewBackchannel(cmd *shell.Command, audio string) (core.Producer, error) {\n\tvar codec *core.Codec\n\n\tif audio == \"\" {\n\t\t// default codec\n\t\tcodec = &core.Codec{Name: core.CodecPCML, ClockRate: 16000}\n\t} else if codec = core.ParseCodecString(audio); codec == nil {\n\t\treturn nil, errors.New(\"pcm: unsupported audio format: \" + audio)\n\t}\n\n\tmedias := []*core.Media{\n\t\t{\n\t\t\tKind:      core.KindAudio,\n\t\t\tDirection: core.DirectionSendonly,\n\t\t\tCodecs:    []*core.Codec{codec},\n\t\t},\n\t}\n\n\treturn &Backchannel{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"pcm\",\n\t\t\tProtocol:   \"pipe\",\n\t\t\tMedias:     medias,\n\t\t\tTransport:  cmd,\n\t\t},\n\t\tcmd: cmd,\n\t}, nil\n}\n\nfunc (c *Backchannel) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {\n\treturn nil, core.ErrCantGetTrack\n}\n\nfunc (c *Backchannel) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {\n\twr, err := c.cmd.StdinPipe()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tsender := core.NewSender(media, track.Codec)\n\tsender.Handler = func(packet *rtp.Packet) {\n\t\tif n, err := wr.Write(packet.Payload); err != nil {\n\t\t\tc.Send += n\n\t\t}\n\t}\n\tsender.HandleRTP(track)\n\tc.Senders = append(c.Senders, sender)\n\treturn nil\n}\n\nfunc (c *Backchannel) Start() error {\n\treturn c.cmd.Run()\n}\n"
  },
  {
    "path": "pkg/pcm/flac.go",
    "content": "// Package pcm - support raw (verbatim) PCM 16 bit in the FLAC container:\n// - only 1 channel\n// - only 16 bit per sample\n// - only 8000, 16000, 24000, 48000 sample rate\npackage pcm\n\nimport (\n\t\"encoding/binary\"\n\t\"unicode/utf8\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/pion/rtp\"\n\t\"github.com/sigurn/crc16\"\n\t\"github.com/sigurn/crc8\"\n)\n\nfunc FLACHeader(magic bool, sampleRate uint32) []byte {\n\tb := make([]byte, 42)\n\n\tif magic {\n\t\tcopy(b, \"fLaC\") // [0..3]\n\t}\n\n\t// https://xiph.org/flac/format.html#metadata_block_header\n\tb[4] = 0x80 // [4] lastMetadata=1 (1 bit), blockType=0 - STREAMINFO (7 bit)\n\tb[7] = 0x22 // [5..7] blockLength=34 (24 bit)\n\n\t// Important for Apple QuickTime player:\n\t// 1. Both values should be same\n\t// 2. Maximum value = 32768\n\tbinary.BigEndian.PutUint16(b[8:], 32768)  // [8..9] info.BlockSizeMin=16 (16 bit)\n\tbinary.BigEndian.PutUint16(b[10:], 32768) // [10..11] info.BlockSizeMin=65535 (16 bit)\n\n\t// [12..14] info.FrameSizeMin=0 (24 bit)\n\t// [15..17] info.FrameSizeMax=0 (24 bit)\n\n\tb[18] = byte(sampleRate >> 12)\n\tb[19] = byte(sampleRate >> 4)\n\tb[20] = byte(sampleRate << 4) // [18..20] info.SampleRate=8000 (20 bit), info.NChannels=1-1 (3 bit)\n\n\tb[21] = 0xF0 // [21..25] info.BitsPerSample=16-1 (5 bit), info.NSamples (36 bit)\n\n\t// [26..41] MD5sum (16 bytes)\n\n\treturn b\n}\n\nvar table8 *crc8.Table\nvar table16 *crc16.Table\n\nfunc FLACEncoder(codecName string, clockRate uint32, handler core.HandlerFunc) core.HandlerFunc {\n\tvar sr byte\n\tswitch clockRate {\n\tcase 8000:\n\t\tsr = 0b0100\n\tcase 16000:\n\t\tsr = 0b0101\n\tcase 22050:\n\t\tsr = 0b0110\n\tcase 24000:\n\t\tsr = 0b0111\n\tcase 32000:\n\t\tsr = 0b1000\n\tcase 44100:\n\t\tsr = 0b1001\n\tcase 48000:\n\t\tsr = 0b1010\n\tcase 96000:\n\t\tsr = 0b1011\n\tdefault:\n\t\treturn nil\n\t}\n\n\tif table8 == nil {\n\t\ttable8 = crc8.MakeTable(crc8.CRC8)\n\t}\n\tif table16 == nil {\n\t\ttable16 = crc16.MakeTable(crc16.CRC16_BUYPASS)\n\t}\n\n\tvar sampleNumber int32\n\n\treturn func(packet *rtp.Packet) {\n\t\tsamples := uint16(len(packet.Payload))\n\n\t\tif codecName == core.CodecPCM || codecName == core.CodecPCML {\n\t\t\tsamples /= 2\n\t\t}\n\n\t\t// https://xiph.org/flac/format.html#frame_header\n\t\tbuf := make([]byte, samples*2+30)\n\n\t\t// 1. Frame header\n\t\tbuf[0] = 0xFF\n\t\tbuf[1] = 0xF9      // [0..1] syncCode=0xFFF8 - reserved (15 bit), blockStrategy=1 - variable-blocksize (1 bit)\n\t\tbuf[2] = 0x70 | sr // blockSizeType=7 (4 bit), sampleRate=4 - 8000 (4 bit)\n\t\tbuf[3] = 0x08      // channels=1-1 (4 bit), sampleSize=4 - 16 (3 bit), reserved=0 (1 bit)\n\n\t\tn := 4 + utf8.EncodeRune(buf[4:], sampleNumber) // 4 bytes max\n\t\tsampleNumber += int32(samples)\n\n\t\t// this is wrong but very simple frame block size value\n\t\tbinary.BigEndian.PutUint16(buf[n:], samples-1)\n\t\tn += 2\n\n\t\tbuf[n] = crc8.Checksum(buf[:n], table8)\n\t\tn += 1\n\n\t\t// 2. Subframe header\n\t\tbuf[n] = 0x02 // padding=0 (1 bit), subframeType=1 - verbatim (6 bit), wastedFlag=0 (1 bit)\n\t\tn += 1\n\n\t\t// 3. Subframe\n\t\tswitch codecName {\n\t\tcase core.CodecPCMA:\n\t\t\tfor _, b := range packet.Payload {\n\t\t\t\ts16 := PCMAtoPCM(b)\n\t\t\t\tbuf[n] = byte(s16 >> 8)\n\t\t\t\tbuf[n+1] = byte(s16)\n\t\t\t\tn += 2\n\t\t\t}\n\t\tcase core.CodecPCMU:\n\t\t\tfor _, b := range packet.Payload {\n\t\t\t\ts16 := PCMUtoPCM(b)\n\t\t\t\tbuf[n] = byte(s16 >> 8)\n\t\t\t\tbuf[n+1] = byte(s16)\n\t\t\t\tn += 2\n\t\t\t}\n\t\tcase core.CodecPCM:\n\t\t\tn += copy(buf[n:], packet.Payload)\n\t\tcase core.CodecPCML:\n\t\t\t// reverse endian from little to big\n\t\t\tsize := len(packet.Payload)\n\t\t\tfor i := 0; i < size; i += 2 {\n\t\t\t\tbuf[n] = packet.Payload[i+1]\n\t\t\t\tbuf[n+1] = packet.Payload[i]\n\t\t\t\tn += 2\n\t\t\t}\n\t\t}\n\n\t\t// 4. Frame footer\n\t\tcrc := crc16.Checksum(buf[:n], table16)\n\t\tbinary.BigEndian.PutUint16(buf[n:], crc)\n\t\tn += 2\n\n\t\tclone := *packet\n\t\tclone.Payload = buf[:n]\n\n\t\thandler(&clone)\n\t}\n}\n"
  },
  {
    "path": "pkg/pcm/handlers.go",
    "content": "package pcm\n\nimport (\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/pion/rtp\"\n)\n\n// RepackG711 - Repack G.711 PCMA/PCMU into frames of size 1024\n//  1. Fixes WebRTC audio quality issue (monotonic timestamp)\n//  2. Fixes Reolink Doorbell backchannel issue (zero timestamp)\n//     https://github.com/AlexxIT/go2rtc/issues/331\nfunc RepackG711(zeroTS bool, handler core.HandlerFunc) core.HandlerFunc {\n\tconst PacketSize = 1024\n\n\tvar buf []byte\n\tvar seq uint16\n\tvar ts uint32\n\n\t// fix https://github.com/AlexxIT/go2rtc/issues/432\n\tvar mu sync.Mutex\n\n\treturn func(packet *rtp.Packet) {\n\t\tmu.Lock()\n\n\t\tbuf = append(buf, packet.Payload...)\n\t\tif len(buf) < PacketSize {\n\t\t\tmu.Unlock()\n\t\t\treturn\n\t\t}\n\n\t\tpkt := &rtp.Packet{\n\t\t\tHeader: rtp.Header{\n\t\t\t\tVersion:        2,\n\t\t\t\tMarker:         true,               // should be true\n\t\t\t\tPayloadType:    packet.PayloadType, // will be owerwriten\n\t\t\t\tSequenceNumber: seq,\n\t\t\t\tSSRC:           packet.SSRC,\n\t\t\t},\n\t\t\tPayload: buf[:PacketSize],\n\t\t}\n\n\t\tseq++\n\n\t\t// don't know if zero TS important for Reolink Doorbell\n\t\t// don't have this strange devices for tests\n\t\tif !zeroTS {\n\t\t\tpkt.Timestamp = ts\n\t\t\tts += PacketSize\n\t\t}\n\n\t\tbuf = buf[PacketSize:]\n\n\t\tmu.Unlock()\n\n\t\thandler(pkt)\n\t}\n}\n\n// LittleToBig - convert PCM little endian to PCM big endian\nfunc LittleToBig(handler core.HandlerFunc) core.HandlerFunc {\n\treturn func(packet *rtp.Packet) {\n\t\tclone := *packet\n\t\tclone.Payload = FlipEndian(packet.Payload)\n\t\thandler(&clone)\n\t}\n}\n\nfunc TranscodeHandler(dst, src *core.Codec, handler core.HandlerFunc) core.HandlerFunc {\n\tvar ts uint32\n\tk := float32(BytesPerFrame(dst)) / float32(BytesPerFrame(src))\n\tf := Transcode(dst, src)\n\n\treturn func(packet *rtp.Packet) {\n\t\tts += uint32(k * float32(len(packet.Payload)))\n\n\t\tclone := *packet\n\t\tclone.Payload = f(packet.Payload)\n\t\tclone.Timestamp = ts\n\t\thandler(&clone)\n\t}\n}\n\nfunc BytesPerSample(codec *core.Codec) int {\n\tswitch codec.Name {\n\tcase core.CodecPCML, core.CodecPCM:\n\t\treturn 2\n\tcase core.CodecPCMU, core.CodecPCMA:\n\t\treturn 1\n\t}\n\treturn 0\n}\n\nfunc BytesPerFrame(codec *core.Codec) int {\n\tif codec.Channels <= 1 {\n\t\treturn BytesPerSample(codec)\n\t}\n\treturn int(codec.Channels) * BytesPerSample(codec)\n}\n\nfunc FramesPerDuration(codec *core.Codec, duration time.Duration) int {\n\treturn int(time.Duration(codec.ClockRate) * duration / time.Second)\n}\n\nfunc BytesPerDuration(codec *core.Codec, duration time.Duration) int {\n\treturn BytesPerFrame(codec) * FramesPerDuration(codec, duration)\n}\n"
  },
  {
    "path": "pkg/pcm/pcm.go",
    "content": "package pcm\n\nimport (\n\t\"math\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\nfunc ceil(x float32) int {\n\td, fract := math.Modf(float64(x))\n\tif fract == 0.0 {\n\t\treturn int(d)\n\t}\n\treturn int(d) + 1\n}\n\nfunc Downsample(k float32) func([]int16) []int16 {\n\tvar sampleN, sampleSum float32\n\n\treturn func(src []int16) (dst []int16) {\n\t\tvar i int\n\t\tdst = make([]int16, ceil((float32(len(src))+sampleN)/k))\n\t\tfor _, sample := range src {\n\t\t\tsampleSum += float32(sample)\n\t\t\tsampleN++\n\t\t\tif sampleN >= k {\n\t\t\t\tdst[i] = int16(sampleSum / k)\n\t\t\t\ti++\n\n\t\t\t\tsampleSum = 0\n\t\t\t\tsampleN -= k\n\t\t\t}\n\t\t}\n\t\treturn\n\t}\n}\n\nfunc Upsample(k float32) func([]int16) []int16 {\n\tvar sampleN float32\n\n\treturn func(src []int16) (dst []int16) {\n\t\tvar i int\n\t\tdst = make([]int16, ceil(k*float32(len(src))))\n\t\tfor _, sample := range src {\n\t\t\tsampleN += k\n\t\t\tfor sampleN > 0 {\n\t\t\t\tdst[i] = sample\n\t\t\t\ti++\n\n\t\t\t\tsampleN -= 1\n\t\t\t}\n\t\t}\n\t\treturn\n\t}\n}\n\nfunc FlipEndian(src []byte) (dst []byte) {\n\tvar i, j int\n\tn := len(src)\n\tdst = make([]byte, n)\n\tfor i < n {\n\t\tx := src[i]\n\t\ti++\n\t\tdst[j] = src[i]\n\t\tj++\n\t\ti++\n\t\tdst[j] = x\n\t\tj++\n\t}\n\treturn\n}\n\nfunc Transcode(dst, src *core.Codec) func([]byte) []byte {\n\tvar reader func([]byte) []int16\n\tvar writer func([]int16) []byte\n\tvar filters []func([]int16) []int16\n\n\tswitch src.Name {\n\tcase core.CodecPCML:\n\t\treader = func(src []byte) (dst []int16) {\n\t\t\tvar i, j int\n\t\t\tn := len(src)\n\t\t\tdst = make([]int16, n/2)\n\t\t\tfor i < n {\n\t\t\t\tlo := src[i]\n\t\t\t\ti++\n\t\t\t\thi := src[i]\n\t\t\t\ti++\n\t\t\t\tdst[j] = int16(hi)<<8 | int16(lo)\n\t\t\t\tj++\n\t\t\t}\n\t\t\treturn\n\t\t}\n\tcase core.CodecPCM:\n\t\treader = func(src []byte) (dst []int16) {\n\t\t\tvar i, j int\n\t\t\tn := len(src)\n\t\t\tdst = make([]int16, n/2)\n\t\t\tfor i < n {\n\t\t\t\thi := src[i]\n\t\t\t\ti++\n\t\t\t\tlo := src[i]\n\t\t\t\ti++\n\t\t\t\tdst[j] = int16(hi)<<8 | int16(lo)\n\t\t\t\tj++\n\t\t\t}\n\t\t\treturn\n\t\t}\n\tcase core.CodecPCMU:\n\t\treader = func(src []byte) (dst []int16) {\n\t\t\tvar i int\n\t\t\tdst = make([]int16, len(src))\n\t\t\tfor _, sample := range src {\n\t\t\t\tdst[i] = PCMUtoPCM(sample)\n\t\t\t\ti++\n\t\t\t}\n\t\t\treturn\n\t\t}\n\tcase core.CodecPCMA:\n\t\treader = func(src []byte) (dst []int16) {\n\t\t\tvar i int\n\t\t\tdst = make([]int16, len(src))\n\t\t\tfor _, sample := range src {\n\t\t\t\tdst[i] = PCMAtoPCM(sample)\n\t\t\t\ti++\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t}\n\n\tif src.Channels > 1 {\n\t\tfilters = append(filters, Downsample(float32(src.Channels)))\n\t}\n\n\tif src.ClockRate > dst.ClockRate {\n\t\tfilters = append(filters, Downsample(float32(src.ClockRate)/float32(dst.ClockRate)))\n\t} else if src.ClockRate < dst.ClockRate {\n\t\tfilters = append(filters, Upsample(float32(dst.ClockRate)/float32(src.ClockRate)))\n\t}\n\n\tif dst.Channels > 1 {\n\t\tfilters = append(filters, Upsample(float32(dst.Channels)))\n\t}\n\n\tswitch dst.Name {\n\tcase core.CodecPCML:\n\t\twriter = func(src []int16) (dst []byte) {\n\t\t\tvar i int\n\t\t\tdst = make([]byte, len(src)*2)\n\t\t\tfor _, sample := range src {\n\t\t\t\tdst[i] = byte(sample)\n\t\t\t\ti++\n\t\t\t\tdst[i] = byte(sample >> 8)\n\t\t\t\ti++\n\t\t\t}\n\t\t\treturn\n\t\t}\n\tcase core.CodecPCM:\n\t\twriter = func(src []int16) (dst []byte) {\n\t\t\tvar i int\n\t\t\tdst = make([]byte, len(src)*2)\n\t\t\tfor _, sample := range src {\n\t\t\t\tdst[i] = byte(sample >> 8)\n\t\t\t\ti++\n\t\t\t\tdst[i] = byte(sample)\n\t\t\t\ti++\n\t\t\t}\n\t\t\treturn\n\t\t}\n\tcase core.CodecPCMU:\n\t\twriter = func(src []int16) (dst []byte) {\n\t\t\tvar i int\n\t\t\tdst = make([]byte, len(src))\n\t\t\tfor _, sample := range src {\n\t\t\t\tdst[i] = PCMtoPCMU(sample)\n\t\t\t\ti++\n\t\t\t}\n\t\t\treturn\n\t\t}\n\tcase core.CodecPCMA:\n\t\twriter = func(src []int16) (dst []byte) {\n\t\t\tvar i int\n\t\t\tdst = make([]byte, len(src))\n\t\t\tfor _, sample := range src {\n\t\t\t\tdst[i] = PCMtoPCMA(sample)\n\t\t\t\ti++\n\t\t\t}\n\t\t\treturn\n\t\t}\n\t}\n\n\treturn func(b []byte) []byte {\n\t\tsamples := reader(b)\n\t\tfor _, filter := range filters {\n\t\t\tsamples = filter(samples)\n\t\t}\n\t\treturn writer(samples)\n\t}\n}\n\nfunc ConsumerCodecs() []*core.Codec {\n\treturn []*core.Codec{\n\t\t{Name: core.CodecPCML},\n\t\t{Name: core.CodecPCM},\n\t\t{Name: core.CodecPCMA},\n\t\t{Name: core.CodecPCMU},\n\t}\n}\n\nfunc ProducerCodecs() []*core.Codec {\n\treturn []*core.Codec{\n\t\t{Name: core.CodecPCML, ClockRate: 16000},\n\t\t{Name: core.CodecPCM, ClockRate: 16000},\n\t\t{Name: core.CodecPCML, ClockRate: 8000},\n\t\t{Name: core.CodecPCM, ClockRate: 8000},\n\t\t{Name: core.CodecPCMA, ClockRate: 8000},\n\t\t{Name: core.CodecPCMU, ClockRate: 8000},\n\t\t{Name: core.CodecPCML, ClockRate: 22050}, // wyoming-snd-external\n\t}\n}\n"
  },
  {
    "path": "pkg/pcm/pcm_test.go",
    "content": "package pcm\n\nimport (\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"testing\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestTranscode(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\tsrc    core.Codec\n\t\tdst    core.Codec\n\t\tsource string\n\t\texpect string\n\t}{\n\t\t{\n\t\t\tname:   \"s16be->s16be\",\n\t\t\tsrc:    core.Codec{Name: core.CodecPCM, ClockRate: 8000, Channels: 1},\n\t\t\tdst:    core.Codec{Name: core.CodecPCM, ClockRate: 8000, Channels: 1},\n\t\t\tsource: \"FCCA00130343062808130B510D9E0F7610DA111113EA15BD16F2168215D41561\",\n\t\t\texpect: \"FCCA00130343062808130B510D9E0F7610DA111113EA15BD16F2168215D41561\",\n\t\t},\n\t\t{\n\t\t\tname:   \"s16be->s16le\",\n\t\t\tsrc:    core.Codec{Name: core.CodecPCM, ClockRate: 8000, Channels: 1},\n\t\t\tdst:    core.Codec{Name: core.CodecPCML, ClockRate: 8000, Channels: 1},\n\t\t\tsource: \"FCCA00130343062808130B510D9E0F7610DA111113EA15BD16F2168215D41561\",\n\t\t\texpect: \"CAFC1300430328061308510B9E0D760FDA101111EA13BD15F2168216D4156115\",\n\t\t},\n\t\t{\n\t\t\tname:   \"s16be->mulaw\",\n\t\t\tsrc:    core.Codec{Name: core.CodecPCM, ClockRate: 8000, Channels: 1},\n\t\t\tdst:    core.Codec{Name: core.CodecPCMU, ClockRate: 8000, Channels: 1},\n\t\t\tsource: \"FCCA00130343062808130B510D9E0F7610DA111113EA15BD16F2168215D41561\",\n\t\t\texpect: \"52FDD1C5BEB8B3B0AEAEABA9A8A8A9AA\",\n\t\t},\n\t\t{\n\t\t\tname:   \"s16be->alaw\",\n\t\t\tsrc:    core.Codec{Name: core.CodecPCM, ClockRate: 8000, Channels: 1},\n\t\t\tdst:    core.Codec{Name: core.CodecPCMA, ClockRate: 8000, Channels: 1},\n\t\t\tsource: \"FCCA00130343062808130B510D9E0F7610DA111113EA15BD16F2168215D41561\",\n\t\t\texpect: \"7CD4FFED95939E9B8584868083838080\",\n\t\t},\n\t\t{\n\t\t\tname:   \"2ch->1ch\",\n\t\t\tsrc:    core.Codec{Name: core.CodecPCM, ClockRate: 8000, Channels: 2},\n\t\t\tdst:    core.Codec{Name: core.CodecPCM, ClockRate: 8000, Channels: 1},\n\t\t\tsource: \"FCCAFCCA001300130343034306280628081308130B510B510D9E0D9E0F760F76\",\n\t\t\texpect: \"FCCA00130343062808130B510D9E0F76\",\n\t\t},\n\t\t{\n\t\t\tname:   \"1ch->2ch\",\n\t\t\tsrc:    core.Codec{Name: core.CodecPCM, ClockRate: 8000, Channels: 1},\n\t\t\tdst:    core.Codec{Name: core.CodecPCM, ClockRate: 8000, Channels: 2},\n\t\t\tsource: \"FCCA00130343062808130B510D9E0F76\",\n\t\t\texpect: \"FCCAFCCA001300130343034306280628081308130B510B510D9E0D9E0F760F76\",\n\t\t},\n\t\t{\n\t\t\tname:   \"16khz->8khz\",\n\t\t\tsrc:    core.Codec{Name: core.CodecPCM, ClockRate: 16000, Channels: 1},\n\t\t\tdst:    core.Codec{Name: core.CodecPCM, ClockRate: 8000, Channels: 1},\n\t\t\tsource: \"FCCAFCCA001300130343034306280628081308130B510B510D9E0D9E0F760F76\",\n\t\t\texpect: \"FCCA00130343062808130B510D9E0F76\",\n\t\t},\n\t}\n\tfor _, test := range tests {\n\t\tt.Run(test.name, func(t *testing.T) {\n\t\t\tf := Transcode(&test.dst, &test.src)\n\t\t\tb, _ := hex.DecodeString(test.source)\n\t\t\tb = f(b)\n\t\t\ts := fmt.Sprintf(\"%X\", b)\n\t\t\trequire.Equal(t, test.expect, s)\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/pcm/pcma.go",
    "content": "// Package pcm\n// https://www.codeproject.com/Articles/14237/Using-the-G711-standard\npackage pcm\n\nconst alawMax = 0x7FFF\n\nfunc PCMAtoPCM(alaw byte) int16 {\n\talaw ^= 0xD5\n\n\tdata := int16(((alaw & 0x0F) << 4) + 8)\n\texponent := (alaw & 0x70) >> 4\n\n\tif exponent != 0 {\n\t\tdata |= 0x100\n\t}\n\n\tif exponent > 1 {\n\t\tdata <<= exponent - 1\n\t}\n\n\t// sign\n\tif alaw&0x80 == 0 {\n\t\treturn data\n\t} else {\n\t\treturn -data\n\t}\n}\n\nfunc PCMtoPCMA(pcm int16) byte {\n\tvar alaw byte\n\n\tif pcm < 0 {\n\t\tpcm = -pcm\n\t\talaw = 0x80\n\t}\n\n\tif pcm > alawMax {\n\t\tpcm = alawMax\n\t}\n\n\texponent := byte(7)\n\tfor expMask := int16(0x4000); (pcm&expMask) == 0 && exponent > 0; expMask >>= 1 {\n\t\texponent--\n\t}\n\n\tif exponent == 0 {\n\t\talaw |= byte(pcm>>4) & 0x0F\n\t} else {\n\t\talaw |= (exponent << 4) | (byte(pcm>>(exponent+3)) & 0x0F)\n\t}\n\n\treturn alaw ^ 0xD5\n}\n"
  },
  {
    "path": "pkg/pcm/pcmu.go",
    "content": "// Package pcm\n// https://www.codeproject.com/Articles/14237/Using-the-G711-standard\npackage pcm\n\nconst bias = 0x84 // 132 or 1000 0100\nconst ulawMax = alawMax - bias\n\nfunc PCMUtoPCM(ulaw byte) int16 {\n\tulaw = ^ulaw\n\n\texponent := (ulaw & 0x70) >> 4\n\tdata := (int16((((ulaw&0x0F)|0x10)<<1)+1) << (exponent + 2)) - bias\n\n\t// sign\n\tif ulaw&0x80 == 0 {\n\t\treturn data\n\t} else if data == 0 {\n\t\treturn -1\n\t} else {\n\t\treturn -data\n\t}\n}\n\nfunc PCMtoPCMU(pcm int16) byte {\n\tvar ulaw byte\n\n\tif pcm < 0 {\n\t\tpcm = -pcm\n\t\tulaw = 0x80\n\t}\n\n\tif pcm > ulawMax {\n\t\tpcm = ulawMax\n\t}\n\n\tpcm += bias\n\n\texponent := byte(7)\n\tfor expMask := int16(0x4000); (pcm & expMask) == 0; expMask >>= 1 {\n\t\texponent--\n\t}\n\n\t// mantisa\n\tulaw |= byte(pcm>>(exponent+3)) & 0x0F\n\n\tif exponent > 0 {\n\t\tulaw |= exponent << 4\n\t}\n\n\treturn ^ulaw\n}\n"
  },
  {
    "path": "pkg/pcm/producer.go",
    "content": "package pcm\n\nimport (\n\t\"io\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Producer struct {\n\tcore.Connection\n\trd io.Reader\n}\n\nfunc Open(rd io.Reader) (*Producer, error) {\n\tmedias := []*core.Media{\n\t\t{\n\t\t\tKind:      core.KindAudio,\n\t\t\tDirection: core.DirectionRecvonly,\n\t\t\tCodecs: []*core.Codec{\n\t\t\t\t{Name: core.CodecPCMU, ClockRate: 8000},\n\t\t\t},\n\t\t},\n\t}\n\treturn &Producer{\n\t\tcore.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"pcm\",\n\t\t\tMedias:     medias,\n\t\t\tTransport:  rd,\n\t\t},\n\t\trd,\n\t}, nil\n}\n\nfunc (c *Producer) Start() error {\n\tfor {\n\t\tpayload := make([]byte, 1024)\n\t\tif _, err := io.ReadFull(c.rd, payload); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tc.Recv += 1024\n\n\t\tif len(c.Receivers) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tpkt := &rtp.Packet{\n\t\t\tHeader:  rtp.Header{Timestamp: core.Now90000()},\n\t\t\tPayload: payload,\n\t\t}\n\t\tc.Receivers[0].WriteRTP(pkt)\n\t}\n}\n"
  },
  {
    "path": "pkg/pcm/producer_sync.go",
    "content": "package pcm\n\nimport (\n\t\"io\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/pion/rtp\"\n)\n\ntype ProducerSync struct {\n\tcore.Connection\n\tsrc     *core.Codec\n\trd      io.Reader\n\tonClose func()\n}\n\nfunc OpenSync(codec *core.Codec, rd io.Reader) *ProducerSync {\n\tmedias := []*core.Media{\n\t\t{\n\t\t\tKind:      core.KindAudio,\n\t\t\tDirection: core.DirectionRecvonly,\n\t\t\tCodecs:    ProducerCodecs(),\n\t\t},\n\t}\n\n\treturn &ProducerSync{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"pcm\",\n\t\t\tMedias:     medias,\n\t\t\tTransport:  rd,\n\t\t},\n\t\tsrc: codec,\n\t\trd:  rd,\n\t}\n}\n\nfunc (p *ProducerSync) OnClose(f func()) {\n\tp.onClose = f\n}\n\nfunc (p *ProducerSync) Start() error {\n\tif len(p.Receivers) == 0 {\n\t\treturn nil\n\t}\n\n\tvar pktSeq uint16\n\tvar pktTS uint32          // time in frames\n\tvar pktTime time.Duration // time in seconds\n\n\tt0 := time.Now()\n\n\tdst := p.Receivers[0].Codec\n\ttranscode := Transcode(dst, p.src)\n\n\tconst chunkDuration = 20 * time.Millisecond\n\tchunkBytes := BytesPerDuration(p.src, chunkDuration)\n\tchunkFrames := uint32(FramesPerDuration(dst, chunkDuration))\n\n\tfor {\n\t\tbuf := make([]byte, chunkBytes)\n\t\tn, _ := io.ReadFull(p.rd, buf)\n\n\t\tif n == 0 {\n\t\t\tbreak\n\t\t}\n\n\t\tpkt := &core.Packet{\n\t\t\tHeader: rtp.Header{\n\t\t\t\tVersion:        2,\n\t\t\t\tMarker:         true,\n\t\t\t\tSequenceNumber: pktSeq,\n\t\t\t\tTimestamp:      pktTS,\n\t\t\t},\n\t\t\tPayload: transcode(buf[:n]),\n\t\t}\n\n\t\tif d := pktTime - time.Since(t0); d > 0 {\n\t\t\ttime.Sleep(d)\n\t\t}\n\n\t\tp.Receivers[0].WriteRTP(pkt)\n\t\tp.Recv += n\n\n\t\tpktSeq++\n\t\tpktTS += chunkFrames\n\t\tpktTime += chunkDuration\n\t}\n\n\tif p.onClose != nil {\n\t\tp.onClose()\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/pcm/s16le/s16le.go",
    "content": "package s16le\n\nfunc PeaksRMS(b []byte) int16 {\n\t// RMS of sine wave = peak / sqrt2\n\t// https://en.wikipedia.org/wiki/Root_mean_square\n\t// https://www.youtube.com/watch?v=MUDkL4KZi0I\n\tvar peaks int32\n\tvar peaksSum int32\n\tvar prevSample int16\n\tvar prevUp bool\n\n\tvar i int\n\tfor n := len(b); i < n; {\n\t\tlo := b[i]\n\t\ti++\n\t\thi := b[i]\n\t\ti++\n\n\t\tsample := int16(hi)<<8 | int16(lo)\n\t\tup := sample >= prevSample\n\n\t\tif i >= 4 {\n\t\t\tif up != prevUp {\n\t\t\t\tif prevSample >= 0 {\n\t\t\t\t\tpeaksSum += int32(prevSample)\n\t\t\t\t} else {\n\t\t\t\t\tpeaksSum -= int32(prevSample)\n\t\t\t\t}\n\t\t\t\tpeaks++\n\t\t\t}\n\t\t}\n\n\t\tprevSample = sample\n\t\tprevUp = up\n\t}\n\n\tif peaks == 0 {\n\t\treturn 0\n\t}\n\n\treturn int16(peaksSum / peaks)\n}\n"
  },
  {
    "path": "pkg/pcm/v1/pcm.go",
    "content": "// Package v1\n// http://web.archive.org/web/20110719132013/http://hazelware.luggle.com/tutorials/mulawcompression.html\npackage v1\n\nconst cBias = 0x84\nconst cClip = 32635\n\nvar MuLawCompressTable = [256]byte{\n\t0, 0, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3,\n\t4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4,\n\t5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,\n\t5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5, 5,\n\t6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,\n\t6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,\n\t6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,\n\t6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6,\n\t7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,\n\t7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,\n\t7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,\n\t7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,\n\t7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,\n\t7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,\n\t7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,\n\t7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7, 7,\n}\n\nfunc LinearToMuLawSample(sample int16) byte {\n\tsign := byte(sample>>8) & 0x80\n\tif sign != 0 {\n\t\tsample = -sample\n\t}\n\n\tif sample > cClip {\n\t\tsample = cClip\n\t}\n\tsample = sample + cBias\n\n\texponent := MuLawCompressTable[(sample>>7)&0xFF]\n\tmantissa := byte(sample>>(exponent+3)) & 0x0F\n\n\tcompressedByte := ^(sign | (exponent << 4) | mantissa)\n\n\treturn compressedByte\n}\n\nvar ALawCompressTable = [128]byte{\n\t1, 1, 2, 2, 3, 3, 3, 3,\n\t4, 4, 4, 4, 4, 4, 4, 4,\n\t5, 5, 5, 5, 5, 5, 5, 5,\n\t5, 5, 5, 5, 5, 5, 5, 5,\n\t6, 6, 6, 6, 6, 6, 6, 6,\n\t6, 6, 6, 6, 6, 6, 6, 6,\n\t6, 6, 6, 6, 6, 6, 6, 6,\n\t6, 6, 6, 6, 6, 6, 6, 6,\n\t7, 7, 7, 7, 7, 7, 7, 7,\n\t7, 7, 7, 7, 7, 7, 7, 7,\n\t7, 7, 7, 7, 7, 7, 7, 7,\n\t7, 7, 7, 7, 7, 7, 7, 7,\n\t7, 7, 7, 7, 7, 7, 7, 7,\n\t7, 7, 7, 7, 7, 7, 7, 7,\n\t7, 7, 7, 7, 7, 7, 7, 7,\n\t7, 7, 7, 7, 7, 7, 7, 7,\n}\n\nfunc LinearToALawSample(sample int16) byte {\n\tsign := byte((^sample)>>8) & 0x80\n\tif sign == 0 {\n\t\tsample = -sample\n\t}\n\n\tif sample > cClip {\n\t\tsample = cClip\n\t}\n\n\tvar compressedByte byte\n\tif sample >= 256 {\n\t\texponent := ALawCompressTable[(sample>>8)&0x7F]\n\t\tmantissa := byte(sample>>(exponent+3)) & 0x0F\n\t\tcompressedByte = (exponent << 4) | mantissa\n\t} else {\n\t\tcompressedByte = byte(sample >> 4)\n\t}\n\tcompressedByte ^= sign ^ 0x55\n\treturn compressedByte\n}\n\nvar MuLawDecompressTable = [256]int16{\n\t-32124, -31100, -30076, -29052, -28028, -27004, -25980, -24956,\n\t-23932, -22908, -21884, -20860, -19836, -18812, -17788, -16764,\n\t-15996, -15484, -14972, -14460, -13948, -13436, -12924, -12412,\n\t-11900, -11388, -10876, -10364, -9852, -9340, -8828, -8316,\n\t-7932, -7676, -7420, -7164, -6908, -6652, -6396, -6140,\n\t-5884, -5628, -5372, -5116, -4860, -4604, -4348, -4092,\n\t-3900, -3772, -3644, -3516, -3388, -3260, -3132, -3004,\n\t-2876, -2748, -2620, -2492, -2364, -2236, -2108, -1980,\n\t-1884, -1820, -1756, -1692, -1628, -1564, -1500, -1436,\n\t-1372, -1308, -1244, -1180, -1116, -1052, -988, -924,\n\t-876, -844, -812, -780, -748, -716, -684, -652,\n\t-620, -588, -556, -524, -492, -460, -428, -396,\n\t-372, -356, -340, -324, -308, -292, -276, -260,\n\t-244, -228, -212, -196, -180, -164, -148, -132,\n\t-120, -112, -104, -96, -88, -80, -72, -64,\n\t-56, -48, -40, -32, -24, -16, -8, -1,\n\t32124, 31100, 30076, 29052, 28028, 27004, 25980, 24956,\n\t23932, 22908, 21884, 20860, 19836, 18812, 17788, 16764,\n\t15996, 15484, 14972, 14460, 13948, 13436, 12924, 12412,\n\t11900, 11388, 10876, 10364, 9852, 9340, 8828, 8316,\n\t7932, 7676, 7420, 7164, 6908, 6652, 6396, 6140,\n\t5884, 5628, 5372, 5116, 4860, 4604, 4348, 4092,\n\t3900, 3772, 3644, 3516, 3388, 3260, 3132, 3004,\n\t2876, 2748, 2620, 2492, 2364, 2236, 2108, 1980,\n\t1884, 1820, 1756, 1692, 1628, 1564, 1500, 1436,\n\t1372, 1308, 1244, 1180, 1116, 1052, 988, 924,\n\t876, 844, 812, 780, 748, 716, 684, 652,\n\t620, 588, 556, 524, 492, 460, 428, 396,\n\t372, 356, 340, 324, 308, 292, 276, 260,\n\t244, 228, 212, 196, 180, 164, 148, 132,\n\t120, 112, 104, 96, 88, 80, 72, 64,\n\t56, 48, 40, 32, 24, 16, 8, 0,\n}\n\nvar ALawDecompressTable = [256]int16{\n\t-5504, -5248, -6016, -5760, -4480, -4224, -4992, -4736,\n\t-7552, -7296, -8064, -7808, -6528, -6272, -7040, -6784,\n\t-2752, -2624, -3008, -2880, -2240, -2112, -2496, -2368,\n\t-3776, -3648, -4032, -3904, -3264, -3136, -3520, -3392,\n\t-22016, -20992, -24064, -23040, -17920, -16896, -19968, -18944,\n\t-30208, -29184, -32256, -31232, -26112, -25088, -28160, -27136,\n\t-11008, -10496, -12032, -11520, -8960, -8448, -9984, -9472,\n\t-15104, -14592, -16128, -15616, -13056, -12544, -14080, -13568,\n\t-344, -328, -376, -360, -280, -264, -312, -296,\n\t-472, -456, -504, -488, -408, -392, -440, -424,\n\t-88, -72, -120, -104, -24, -8, -56, -40,\n\t-216, -200, -248, -232, -152, -136, -184, -168,\n\t-1376, -1312, -1504, -1440, -1120, -1056, -1248, -1184,\n\t-1888, -1824, -2016, -1952, -1632, -1568, -1760, -1696,\n\t-688, -656, -752, -720, -560, -528, -624, -592,\n\t-944, -912, -1008, -976, -816, -784, -880, -848,\n\t5504, 5248, 6016, 5760, 4480, 4224, 4992, 4736,\n\t7552, 7296, 8064, 7808, 6528, 6272, 7040, 6784,\n\t2752, 2624, 3008, 2880, 2240, 2112, 2496, 2368,\n\t3776, 3648, 4032, 3904, 3264, 3136, 3520, 3392,\n\t22016, 20992, 24064, 23040, 17920, 16896, 19968, 18944,\n\t30208, 29184, 32256, 31232, 26112, 25088, 28160, 27136,\n\t11008, 10496, 12032, 11520, 8960, 8448, 9984, 9472,\n\t15104, 14592, 16128, 15616, 13056, 12544, 14080, 13568,\n\t344, 328, 376, 360, 280, 264, 312, 296,\n\t472, 456, 504, 488, 408, 392, 440, 424,\n\t88, 72, 120, 104, 24, 8, 56, 40,\n\t216, 200, 248, 232, 152, 136, 184, 168,\n\t1376, 1312, 1504, 1440, 1120, 1056, 1248, 1184,\n\t1888, 1824, 2016, 1952, 1632, 1568, 1760, 1696,\n\t688, 656, 752, 720, 560, 528, 624, 592,\n\t944, 912, 1008, 976, 816, 784, 880, 848,\n}\n"
  },
  {
    "path": "pkg/pcm/v1/pcm_test.go",
    "content": "package v1\n\nimport (\n\t\"testing\"\n\n\tv2 \"github.com/AlexxIT/go2rtc/pkg/pcm\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPCMUtoPCM(t *testing.T) {\n\tfor pcmu := byte(0); pcmu < 255; pcmu++ {\n\t\tpcm1 := MuLawDecompressTable[pcmu]\n\t\tpcm2 := v2.PCMUtoPCM(pcmu)\n\t\trequire.Equal(t, pcm1, pcm2)\n\t}\n}\n\nfunc TestPCMAtoPCM(t *testing.T) {\n\tfor pcma := byte(0); pcma < 255; pcma++ {\n\t\tpcm1 := ALawDecompressTable[pcma]\n\t\tpcm2 := v2.PCMAtoPCM(pcma)\n\t\trequire.Equal(t, pcm1, pcm2)\n\t}\n}\n\nfunc TestPCMtoPCMU(t *testing.T) {\n\tfor pcm := int16(-32768); pcm < 32767; pcm++ {\n\t\tpcmu1 := LinearToMuLawSample(pcm)\n\t\tpcmu2 := v2.PCMtoPCMU(pcm)\n\t\trequire.Equal(t, pcmu1, pcmu2)\n\t}\n}\n\nfunc TestPCMtoPCMA(t *testing.T) {\n\tfor pcm := int16(-32768); pcm < 32767; pcm++ {\n\t\tpcma1 := LinearToALawSample(pcm)\n\t\tpcma2 := v2.PCMtoPCMA(pcm)\n\t\trequire.Equal(t, pcma1, pcma2)\n\t}\n}\n"
  },
  {
    "path": "pkg/pinggy/pinggy.go",
    "content": "package pinggy\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"golang.org/x/crypto/ssh\"\n)\n\ntype Client struct {\n\tSSH *ssh.Client\n\tTCP net.Listener\n\tAPI *http.Client\n}\n\nfunc NewClient(proto string) (*Client, error) {\n\tswitch proto {\n\tcase \"http\", \"tcp\", \"tls\", \"tlstcp\":\n\tcase \"\":\n\t\tproto = \"http\"\n\tdefault:\n\t\treturn nil, errors.New(\"pinggy: unsupported proto: \" + proto)\n\t}\n\n\tconfig := &ssh.ClientConfig{\n\t\tUser:            \"auth+\" + proto,\n\t\tAuth:            []ssh.AuthMethod{ssh.Password(\"nopass\")},\n\t\tHostKeyCallback: ssh.InsecureIgnoreHostKey(),\n\t\tTimeout:         5 * time.Second,\n\t}\n\n\tclient, err := ssh.Dial(\"tcp\", \"a.pinggy.io:443\", config)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tln, err := client.Listen(\"tcp\", \"0.0.0.0:0\")\n\tif err != nil {\n\t\t_ = client.Close()\n\t\treturn nil, err\n\t}\n\n\tc := &Client{\n\t\tSSH: client,\n\t\tTCP: ln,\n\t\tAPI: &http.Client{\n\t\t\tTransport: &http.Transport{\n\t\t\t\tDialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {\n\t\t\t\t\treturn client.Dial(network, addr)\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\tif proto == \"http\" {\n\t\tif err = c.NewSession(); err != nil {\n\t\t\t_ = client.Close()\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn c, nil\n}\n\nfunc (c *Client) Close() error {\n\treturn errors.Join(c.SSH.Close(), c.TCP.Close())\n}\n\nfunc (c *Client) NewSession() error {\n\tsession, err := c.SSH.NewSession()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn session.Shell()\n}\n\nfunc (c *Client) GetURLs() ([]string, error) {\n\tres, err := c.API.Get(\"http://localhost:4300/urls\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer res.Body.Close()\n\n\tvar v struct {\n\t\tURLs []string `json:\"urls\"`\n\t}\n\n\tif err = json.NewDecoder(res.Body).Decode(&v); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn v.URLs, nil\n}\n\nfunc (c *Client) Proxy(address string) error {\n\tdefer c.TCP.Close()\n\n\tfor {\n\t\tconn, err := c.TCP.Accept()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tgo proxy(conn, address)\n\t}\n}\n\nfunc proxy(conn1 net.Conn, address string) {\n\tdefer conn1.Close()\n\n\tconn2, err := net.Dial(\"tcp\", address)\n\tif err != nil {\n\t\treturn\n\t}\n\tdefer conn2.Close()\n\n\tgo io.Copy(conn2, conn1)\n\tio.Copy(conn1, conn2)\n}\n\n// DialTLS like ssh.Dial but with TLS\n//func DialTLS(network, addr, sni string, config *ssh.ClientConfig) (*ssh.Client, error) {\n//\tconn, err := net.DialTimeout(network, addr, config.Timeout)\n//\tif err != nil {\n//\t\treturn nil, err\n//\t}\n//\tconn = tls.Client(conn, &tls.Config{ServerName: sni, InsecureSkipVerify: sni == \"\"})\n//\tc, chans, reqs, err := ssh.NewClientConn(conn, addr, config)\n//\tif err != nil {\n//\t\treturn nil, err\n//\t}\n//\treturn ssh.NewClient(c, chans, reqs), nil\n//}\n"
  },
  {
    "path": "pkg/probe/consumer.go",
    "content": "package probe\n\nimport (\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\ntype Probe struct {\n\tcore.Connection\n}\n\nfunc Create(name string, query url.Values) *Probe {\n\tmedias := core.ParseQuery(query)\n\n\tfor _, value := range query[\"microphone\"] {\n\t\tmedia := &core.Media{Kind: core.KindAudio, Direction: core.DirectionRecvonly}\n\n\t\tfor _, name := range strings.Split(value, \",\") {\n\t\t\tname = strings.ToUpper(name)\n\t\t\tswitch name {\n\t\t\tcase \"\", \"COPY\":\n\t\t\t\tname = core.CodecAny\n\t\t\t}\n\t\t\tmedia.Codecs = append(media.Codecs, &core.Codec{Name: name})\n\t\t}\n\n\t\tmedias = append(medias, media)\n\t}\n\n\treturn &Probe{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: name,\n\t\t\tMedias:     medias,\n\t\t},\n\t}\n}\n\nfunc (p *Probe) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {\n\tsender := core.NewSender(media, track.Codec)\n\tsender.Handler = func(pkt *core.Packet) {\n\t\tp.Send += len(pkt.Payload)\n\t}\n\tsender.HandleRTP(track)\n\tp.Senders = append(p.Senders, sender)\n\treturn nil\n}\n\nfunc (p *Probe) Start() error {\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/ring/api.go",
    "content": "package ring\n\nimport (\n\t\"bytes\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"reflect\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n)\n\nvar clientCache = map[string]*RingApi{}\nvar cacheMutex sync.Mutex\n\ntype RefreshTokenAuth struct {\n\tRefreshToken string\n}\n\ntype EmailAuth struct {\n\tEmail    string\n\tPassword string\n}\n\ntype AuthConfig struct {\n\tRT  string `json:\"rt\"`  // Refresh Token\n\tHID string `json:\"hid\"` // Hardware ID\n}\n\ntype AuthTokenResponse struct {\n\tAccessToken  string `json:\"access_token\"`\n\tExpiresIn    int    `json:\"expires_in\"`\n\tRefreshToken string `json:\"refresh_token\"`\n\tScope        string `json:\"scope\"`      // Always \"client\"\n\tTokenType    string `json:\"token_type\"` // Always \"Bearer\"\n}\n\ntype Auth2faResponse struct {\n\tError            string `json:\"error\"`\n\tErrorDescription string `json:\"error_description\"`\n\tTSVState         string `json:\"tsv_state\"`\n\tPhone            string `json:\"phone\"`\n\tNextTimeInSecs   int    `json:\"next_time_in_secs\"`\n}\n\ntype SocketTicketResponse struct {\n\tTicket            string `json:\"ticket\"`\n\tResponseTimestamp int64  `json:\"response_timestamp\"`\n}\n\ntype SessionResponse struct {\n\tProfile struct {\n\t\tID        int64  `json:\"id\"`\n\t\tEmail     string `json:\"email\"`\n\t\tFirstName string `json:\"first_name\"`\n\t\tLastName  string `json:\"last_name\"`\n\t} `json:\"profile\"`\n}\n\ntype RingApi struct {\n\thttpClient     *http.Client\n\tauthConfig     *AuthConfig\n\thardwareID     string\n\tauthToken      *AuthTokenResponse\n\ttokenExpiry    time.Time\n\tUsing2FA       bool\n\tPromptFor2FA   string\n\tRefreshToken   string\n\tauth           interface{} // EmailAuth or RefreshTokenAuth\n\tonTokenRefresh func(string)\n\tauthMutex      sync.Mutex\n\tsession        *SessionResponse\n\tsessionExpiry  time.Time\n\tsessionMutex   sync.Mutex\n\tcacheKey       string\n}\n\ntype CameraKind string\n\ntype CameraData struct {\n\tID          int    `json:\"id\"`\n\tDescription string `json:\"description\"`\n\tDeviceID    string `json:\"device_id\"`\n\tKind        string `json:\"kind\"`\n\tLocationID  string `json:\"location_id\"`\n}\n\ntype RingDeviceType string\n\ntype RingDevicesResponse struct {\n\tDoorbots           []CameraData             `json:\"doorbots\"`\n\tAuthorizedDoorbots []CameraData             `json:\"authorized_doorbots\"`\n\tStickupCams        []CameraData             `json:\"stickup_cams\"`\n\tAllCameras         []CameraData             `json:\"all_cameras\"`\n\tChimes             []CameraData             `json:\"chimes\"`\n\tOther              []map[string]interface{} `json:\"other\"`\n}\n\nconst (\n\tDoorbot             CameraKind = \"doorbot\"\n\tDoorbell            CameraKind = \"doorbell\"\n\tDoorbellV3          CameraKind = \"doorbell_v3\"\n\tDoorbellV4          CameraKind = \"doorbell_v4\"\n\tDoorbellV5          CameraKind = \"doorbell_v5\"\n\tDoorbellOyster      CameraKind = \"doorbell_oyster\"\n\tDoorbellPortal      CameraKind = \"doorbell_portal\"\n\tDoorbellScallop     CameraKind = \"doorbell_scallop\"\n\tDoorbellScallopLite CameraKind = \"doorbell_scallop_lite\"\n\tDoorbellGraham      CameraKind = \"doorbell_graham_cracker\"\n\tLpdV1               CameraKind = \"lpd_v1\"\n\tLpdV2               CameraKind = \"lpd_v2\"\n\tLpdV4               CameraKind = \"lpd_v4\"\n\tJboxV1              CameraKind = \"jbox_v1\"\n\tStickupCam          CameraKind = \"stickup_cam\"\n\tStickupCamV3        CameraKind = \"stickup_cam_v3\"\n\tStickupCamElite     CameraKind = \"stickup_cam_elite\"\n\tStickupCamLongfin   CameraKind = \"stickup_cam_longfin\"\n\tStickupCamLunar     CameraKind = \"stickup_cam_lunar\"\n\tSpotlightV2         CameraKind = \"spotlightw_v2\"\n\tHpCamV1             CameraKind = \"hp_cam_v1\"\n\tHpCamV2             CameraKind = \"hp_cam_v2\"\n\tStickupCamV4        CameraKind = \"stickup_cam_v4\"\n\tFloodlightV1        CameraKind = \"floodlight_v1\"\n\tFloodlightV2        CameraKind = \"floodlight_v2\"\n\tFloodlightPro       CameraKind = \"floodlight_pro\"\n\tCocoaCamera         CameraKind = \"cocoa_camera\"\n\tCocoaDoorbell       CameraKind = \"cocoa_doorbell\"\n\tCocoaFloodlight     CameraKind = \"cocoa_floodlight\"\n\tCocoaSpotlight      CameraKind = \"cocoa_spotlight\"\n\tStickupCamMini      CameraKind = \"stickup_cam_mini\"\n\tOnvifCamera         CameraKind = \"onvif_camera\"\n)\n\nconst (\n\tIntercomHandsetAudio RingDeviceType = \"intercom_handset_audio\"\n\tOnvifCameraType      RingDeviceType = \"onvif_camera\"\n)\n\nconst (\n\tclientAPIBaseURL   = \"https://api.ring.com/clients_api/\"\n\tdeviceAPIBaseURL   = \"https://api.ring.com/devices/v1/\"\n\tcommandsAPIBaseURL = \"https://api.ring.com/commands/v1/\"\n\tappAPIBaseURL      = \"https://prd-api-us.prd.rings.solutions/api/v1/\"\n\toauthURL           = \"https://oauth.ring.com/oauth/token\"\n\tapiVersion         = 11\n\tdefaultTimeout     = 20 * time.Second\n\tmaxRetries         = 3\n\tsessionValidTime   = 12 * time.Hour\n)\n\nfunc NewRestClient(auth interface{}, onTokenRefresh func(string)) (*RingApi, error) {\n\tvar cacheKey string\n\n\t// Create cache key based on auth data\n\tswitch a := auth.(type) {\n\tcase RefreshTokenAuth:\n\t\tif a.RefreshToken == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"refresh token is required\")\n\t\t}\n\t\tcacheKey = \"refresh:\" + a.RefreshToken\n\tcase EmailAuth:\n\t\tif a.Email == \"\" || a.Password == \"\" {\n\t\t\treturn nil, fmt.Errorf(\"email and password are required\")\n\t\t}\n\t\tcacheKey = \"email:\" + a.Email + \":\" + a.Password\n\tdefault:\n\t\treturn nil, fmt.Errorf(\"invalid auth type\")\n\t}\n\n\tcacheMutex.Lock()\n\tdefer cacheMutex.Unlock()\n\n\tif cachedClient, ok := clientCache[cacheKey]; ok {\n\t\t// Check if token is not nil and not expired\n\t\tif cachedClient.authToken != nil && time.Now().Before(cachedClient.tokenExpiry) {\n\t\t\tcachedClient.onTokenRefresh = onTokenRefresh\n\t\t\treturn cachedClient, nil\n\t\t}\n\t}\n\n\tclient := &RingApi{\n\t\thttpClient:     &http.Client{Timeout: defaultTimeout},\n\t\tonTokenRefresh: onTokenRefresh,\n\t\thardwareID:     generateHardwareID(),\n\t\tauth:           auth,\n\t\tcacheKey:       cacheKey,\n\t}\n\n\tswitch a := auth.(type) {\n\tcase RefreshTokenAuth:\n\t\tconfig, err := parseAuthConfig(a.RefreshToken)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to parse refresh token: %w\", err)\n\t\t}\n\n\t\tclient.authConfig = config\n\t\tclient.hardwareID = config.HID\n\t\tclient.RefreshToken = a.RefreshToken\n\t}\n\n\tclientCache[cacheKey] = client\n\n\treturn client, nil\n}\n\nfunc ClientAPI(path string) string {\n\treturn clientAPIBaseURL + path\n}\n\nfunc DeviceAPI(path string) string {\n\treturn deviceAPIBaseURL + path\n}\n\nfunc CommandsAPI(path string) string {\n\treturn commandsAPIBaseURL + path\n}\n\nfunc AppAPI(path string) string {\n\treturn appAPIBaseURL + path\n}\n\nfunc (c *RingApi) GetAuth(twoFactorAuthCode string) (*AuthTokenResponse, error) {\n\tvar grantData map[string]string\n\n\tif c.authConfig != nil && twoFactorAuthCode == \"\" {\n\t\tgrantData = map[string]string{\n\t\t\t\"grant_type\":    \"refresh_token\",\n\t\t\t\"refresh_token\": c.authConfig.RT,\n\t\t}\n\t} else {\n\t\tauthEmail, ok := c.auth.(EmailAuth)\n\t\tif !ok {\n\t\t\treturn nil, fmt.Errorf(\"invalid auth type for email authentication\")\n\t\t}\n\t\tgrantData = map[string]string{\n\t\t\t\"grant_type\": \"password\",\n\t\t\t\"username\":   authEmail.Email,\n\t\t\t\"password\":   authEmail.Password,\n\t\t}\n\t}\n\n\tgrantData[\"client_id\"] = \"ring_official_android\"\n\tgrantData[\"scope\"] = \"client\"\n\n\tbody, err := json.Marshal(grantData)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to marshal auth request: %w\", err)\n\t}\n\n\treq, err := http.NewRequest(\"POST\", oauthURL, bytes.NewReader(body))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Accept\", \"application/json\")\n\treq.Header.Set(\"hardware_id\", c.hardwareID)\n\treq.Header.Set(\"User-Agent\", \"android:com.ringapp\")\n\treq.Header.Set(\"2fa-support\", \"true\")\n\tif twoFactorAuthCode != \"\" {\n\t\treq.Header.Set(\"2fa-code\", twoFactorAuthCode)\n\t}\n\n\tresp, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\t// Handle 2FA Responses\n\tif resp.StatusCode == http.StatusPreconditionFailed ||\n\t\t(resp.StatusCode == http.StatusBadRequest && strings.Contains(resp.Header.Get(\"WWW-Authenticate\"), \"Verification Code\")) {\n\n\t\tvar tfaResp Auth2faResponse\n\t\tif err := json.NewDecoder(resp.Body).Decode(&tfaResp); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tc.Using2FA = true\n\t\tif resp.StatusCode == http.StatusBadRequest {\n\t\t\tc.PromptFor2FA = \"Invalid 2fa code entered. Please try again.\"\n\t\t\treturn nil, fmt.Errorf(\"invalid 2FA code\")\n\t\t}\n\n\t\tif tfaResp.TSVState != \"\" {\n\t\t\tprompt := \"from your authenticator app\"\n\t\t\tif tfaResp.TSVState != \"totp\" {\n\t\t\t\tprompt = fmt.Sprintf(\"sent to %s via %s\", tfaResp.Phone, tfaResp.TSVState)\n\t\t\t}\n\t\t\tc.PromptFor2FA = fmt.Sprintf(\"Please enter the code %s\", prompt)\n\t\t} else {\n\t\t\tc.PromptFor2FA = \"Please enter the code sent to your text/email\"\n\t\t}\n\n\t\treturn nil, fmt.Errorf(\"2FA required\")\n\t}\n\n\t// Handle errors\n\tif resp.StatusCode != http.StatusOK {\n\t\tbody, _ := io.ReadAll(resp.Body)\n\t\treturn nil, fmt.Errorf(\"auth request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar authResp AuthTokenResponse\n\tif err := json.NewDecoder(resp.Body).Decode(&authResp); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to decode auth response: %w\", err)\n\t}\n\n\t// Refresh token and expiry\n\tc.authToken = &authResp\n\tc.authConfig = &AuthConfig{\n\t\tRT:  authResp.RefreshToken,\n\t\tHID: c.hardwareID,\n\t}\n\t// Set token expiry (1 minute before actual expiry)\n\texpiresIn := time.Duration(authResp.ExpiresIn-60) * time.Second\n\tc.tokenExpiry = time.Now().Add(expiresIn)\n\n\tc.RefreshToken = encodeAuthConfig(c.authConfig)\n\tif c.onTokenRefresh != nil {\n\t\tc.onTokenRefresh(c.RefreshToken)\n\t}\n\n\t// Refresh the cached client\n\tcacheMutex.Lock()\n\tclientCache[c.cacheKey] = c\n\tcacheMutex.Unlock()\n\n\treturn c.authToken, nil\n}\n\nfunc (c *RingApi) FetchRingDevices() (*RingDevicesResponse, error) {\n\tresponse, err := c.Request(\"GET\", ClientAPI(\"ring_devices\"), nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to fetch ring devices: %w\", err)\n\t}\n\n\tvar devices RingDevicesResponse\n\tif err := json.Unmarshal(response, &devices); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to unmarshal devices response: %w\", err)\n\t}\n\n\t// Process \"other\" devices\n\tvar onvifCameras []CameraData\n\tvar intercoms []CameraData\n\n\tfor _, device := range devices.Other {\n\t\tkind, ok := device[\"kind\"].(string)\n\t\tif !ok {\n\t\t\tcontinue\n\t\t}\n\n\t\tswitch RingDeviceType(kind) {\n\t\tcase OnvifCameraType:\n\t\t\tvar camera CameraData\n\t\t\tif deviceJson, err := json.Marshal(device); err == nil {\n\t\t\t\tif err := json.Unmarshal(deviceJson, &camera); err == nil {\n\t\t\t\t\tonvifCameras = append(onvifCameras, camera)\n\t\t\t\t}\n\t\t\t}\n\t\tcase IntercomHandsetAudio:\n\t\t\tvar intercom CameraData\n\t\t\tif deviceJson, err := json.Marshal(device); err == nil {\n\t\t\t\tif err := json.Unmarshal(deviceJson, &intercom); err == nil {\n\t\t\t\t\tintercoms = append(intercoms, intercom)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\t// Combine all cameras into AllCameras slice\n\tallCameras := make([]CameraData, 0)\n\tallCameras = append(allCameras, interfaceSlice(devices.Doorbots)...)\n\tallCameras = append(allCameras, interfaceSlice(devices.StickupCams)...)\n\tallCameras = append(allCameras, interfaceSlice(devices.AuthorizedDoorbots)...)\n\tallCameras = append(allCameras, interfaceSlice(onvifCameras)...)\n\tallCameras = append(allCameras, interfaceSlice(intercoms)...)\n\n\tdevices.AllCameras = allCameras\n\n\treturn &devices, nil\n}\n\nfunc (c *RingApi) GetSocketTicket() (*SocketTicketResponse, error) {\n\tresponse, err := c.Request(\"POST\", AppAPI(\"clap/ticket/request/signalsocket\"), nil)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to fetch socket ticket: %w\", err)\n\t}\n\n\tvar ticket SocketTicketResponse\n\tif err := json.Unmarshal(response, &ticket); err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to unmarshal socket ticket response: %w\", err)\n\t}\n\n\treturn &ticket, nil\n}\n\nfunc (c *RingApi) Request(method, url string, body interface{}) ([]byte, error) {\n\t// Ensure we have a valid session\n\tif err := c.ensureSession(); err != nil {\n\t\treturn nil, fmt.Errorf(\"session validation failed: %w\", err)\n\t}\n\n\tvar bodyReader io.Reader\n\tif body != nil {\n\t\tjsonBody, err := json.Marshal(body)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to marshal request body: %w\", err)\n\t\t}\n\t\tbodyReader = bytes.NewReader(jsonBody)\n\t}\n\n\t// Create request\n\treq, err := http.NewRequest(method, url, bodyReader)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"failed to create request: %w\", err)\n\t}\n\n\t// Set headers\n\treq.Header.Set(\"Authorization\", \"Bearer \"+c.authToken.AccessToken)\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Accept\", \"application/json\")\n\treq.Header.Set(\"hardware_id\", c.hardwareID)\n\treq.Header.Set(\"User-Agent\", \"android:com.ringapp\")\n\n\t// Make request with retries\n\tvar resp *http.Response\n\tvar responseBody []byte\n\n\tfor attempt := 0; attempt <= maxRetries; attempt++ {\n\t\tresp, err = c.httpClient.Do(req)\n\t\tif err != nil {\n\t\t\tif attempt == maxRetries {\n\t\t\t\treturn nil, fmt.Errorf(\"request failed after %d retries: %w\", maxRetries, err)\n\t\t\t}\n\t\t\ttime.Sleep(5 * time.Second)\n\t\t\tcontinue\n\t\t}\n\t\tdefer resp.Body.Close()\n\n\t\tresponseBody, err = io.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"failed to read response body: %w\", err)\n\t\t}\n\n\t\t// Handle 401 by refreshing auth and retrying\n\t\tif resp.StatusCode == http.StatusUnauthorized {\n\t\t\t// Reset token to force refresh\n\t\t\tc.authMutex.Lock()\n\t\t\tc.authToken = nil\n\t\t\tc.tokenExpiry = time.Time{} // Reset token expiry\n\t\t\tc.authMutex.Unlock()\n\n\t\t\tif attempt == maxRetries {\n\t\t\t\treturn nil, fmt.Errorf(\"authentication failed after %d retries\", maxRetries)\n\t\t\t}\n\n\t\t\t// By 401 with Auth AND Session start over\n\t\t\tc.sessionMutex.Lock()\n\t\t\tc.session = nil\n\t\t\tc.sessionExpiry = time.Time{} // Reset session expiry\n\t\t\tc.sessionMutex.Unlock()\n\n\t\t\tif err := c.ensureSession(); err != nil {\n\t\t\t\treturn nil, fmt.Errorf(\"failed to refresh session: %w\", err)\n\t\t\t}\n\n\t\t\treq.Header.Set(\"Authorization\", \"Bearer \"+c.authToken.AccessToken)\n\t\t\tcontinue\n\t\t}\n\n\t\t// Handle 404 error with hardware_id reference - session issue\n\t\tif resp.StatusCode == 404 && strings.Contains(url, clientAPIBaseURL) {\n\t\t\tvar errorBody map[string]interface{}\n\t\t\tif err := json.Unmarshal(responseBody, &errorBody); err == nil {\n\t\t\t\tif errorStr, ok := errorBody[\"error\"].(string); ok && strings.Contains(errorStr, c.hardwareID) {\n\t\t\t\t\t// Session with hardware_id not found, refresh session\n\t\t\t\t\tc.sessionMutex.Lock()\n\t\t\t\t\tc.session = nil\n\t\t\t\t\tc.sessionExpiry = time.Time{} // Reset session expiry\n\t\t\t\t\tc.sessionMutex.Unlock()\n\n\t\t\t\t\tif attempt == maxRetries {\n\t\t\t\t\t\treturn nil, fmt.Errorf(\"session refresh failed after %d retries\", maxRetries)\n\t\t\t\t\t}\n\n\t\t\t\t\tif err := c.ensureSession(); err != nil {\n\t\t\t\t\t\treturn nil, fmt.Errorf(\"failed to refresh session: %w\", err)\n\t\t\t\t\t}\n\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\t// Handle other error status codes\n\t\tif resp.StatusCode >= 400 {\n\t\t\treturn nil, fmt.Errorf(\"request failed with status %d: %s\", resp.StatusCode, string(responseBody))\n\t\t}\n\n\t\tbreak\n\t}\n\n\treturn responseBody, nil\n}\n\nfunc (c *RingApi) ensureSession() error {\n\tc.sessionMutex.Lock()\n\tdefer c.sessionMutex.Unlock()\n\n\t// If session is still valid, use it\n\tif c.session != nil && time.Now().Before(c.sessionExpiry) {\n\t\treturn nil\n\t}\n\n\t// Make sure we have a valid auth token\n\tif err := c.ensureAuth(); err != nil {\n\t\treturn fmt.Errorf(\"authentication failed while creating session: %w\", err)\n\t}\n\n\tsessionPayload := map[string]interface{}{\n\t\t\"device\": map[string]interface{}{\n\t\t\t\"hardware_id\": c.hardwareID,\n\t\t\t\"metadata\": map[string]interface{}{\n\t\t\t\t\"api_version\":  apiVersion,\n\t\t\t\t\"device_model\": \"ring-client-go\",\n\t\t\t},\n\t\t\t\"os\": \"android\",\n\t\t},\n\t}\n\n\tbody, err := json.Marshal(sessionPayload)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to marshal session request: %w\", err)\n\t}\n\n\treq, err := http.NewRequest(\"POST\", ClientAPI(\"session\"), bytes.NewReader(body))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Accept\", \"application/json\")\n\treq.Header.Set(\"Authorization\", \"Bearer \"+c.authToken.AccessToken)\n\treq.Header.Set(\"hardware_id\", c.hardwareID)\n\treq.Header.Set(\"User-Agent\", \"android:com.ringapp\")\n\n\tresp, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode < 200 || resp.StatusCode >= 300 {\n\t\trespBody, _ := io.ReadAll(resp.Body)\n\t\treturn fmt.Errorf(\"session request failed with status %d: %s\", resp.StatusCode, string(respBody))\n\t}\n\n\tvar sessionResp SessionResponse\n\tif err := json.NewDecoder(resp.Body).Decode(&sessionResp); err != nil {\n\t\treturn fmt.Errorf(\"failed to decode session response: %w\", err)\n\t}\n\n\tc.session = &sessionResp\n\tc.sessionExpiry = time.Now().Add(sessionValidTime)\n\n\t// Aktualisiere den gecachten Client\n\tcacheMutex.Lock()\n\tclientCache[c.cacheKey] = c\n\tcacheMutex.Unlock()\n\n\treturn nil\n}\n\nfunc (c *RingApi) ensureAuth() error {\n\tc.authMutex.Lock()\n\tdefer c.authMutex.Unlock()\n\n\t// If token exists and is not expired, use it\n\tif c.authToken != nil && time.Now().Before(c.tokenExpiry) {\n\t\treturn nil\n\t}\n\n\tvar grantData = map[string]string{\n\t\t\"grant_type\":    \"refresh_token\",\n\t\t\"refresh_token\": c.authConfig.RT,\n\t}\n\n\t// Add common fields\n\tgrantData[\"client_id\"] = \"ring_official_android\"\n\tgrantData[\"scope\"] = \"client\"\n\n\t// Make auth request\n\tbody, err := json.Marshal(grantData)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to marshal auth request: %w\", err)\n\t}\n\n\treq, err := http.NewRequest(\"POST\", oauthURL, bytes.NewReader(body))\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to create auth request: %w\", err)\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Accept\", \"application/json\")\n\treq.Header.Set(\"hardware_id\", c.hardwareID)\n\treq.Header.Set(\"User-Agent\", \"android:com.ringapp\")\n\treq.Header.Set(\"2fa-support\", \"true\")\n\n\tresp, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"auth request failed: %w\", err)\n\t}\n\tdefer resp.Body.Close()\n\n\tif resp.StatusCode == http.StatusPreconditionFailed {\n\t\treturn fmt.Errorf(\"2FA required. Please see documentation for handling 2FA\")\n\t}\n\n\tif resp.StatusCode != http.StatusOK {\n\t\tbody, _ := io.ReadAll(resp.Body)\n\t\treturn fmt.Errorf(\"auth request failed with status %d: %s\", resp.StatusCode, string(body))\n\t}\n\n\tvar authResp AuthTokenResponse\n\tif err := json.NewDecoder(resp.Body).Decode(&authResp); err != nil {\n\t\treturn fmt.Errorf(\"failed to decode auth response: %w\", err)\n\t}\n\n\t// Update auth config and refresh token\n\tc.authToken = &authResp\n\tc.authConfig = &AuthConfig{\n\t\tRT:  authResp.RefreshToken,\n\t\tHID: c.hardwareID,\n\t}\n\n\t// Set token expiry (1 minute before actual expiry)\n\texpiresIn := time.Duration(authResp.ExpiresIn-60) * time.Second\n\tc.tokenExpiry = time.Now().Add(expiresIn)\n\n\t// Encode and notify about new refresh token\n\tif c.onTokenRefresh != nil {\n\t\tnewRefreshToken := encodeAuthConfig(c.authConfig)\n\t\tc.onTokenRefresh(newRefreshToken)\n\t}\n\n\t// Refreshn the token in the client\n\tc.RefreshToken = encodeAuthConfig(c.authConfig)\n\n\t// Refresh the cached client\n\tcacheMutex.Lock()\n\tclientCache[c.cacheKey] = c\n\tcacheMutex.Unlock()\n\n\treturn nil\n}\n\nfunc parseAuthConfig(refreshToken string) (*AuthConfig, error) {\n\tdecoded, err := base64.StdEncoding.DecodeString(refreshToken)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar config AuthConfig\n\tif err := json.Unmarshal(decoded, &config); err != nil {\n\t\t// Handle legacy format where refresh token is the raw token\n\t\treturn &AuthConfig{RT: refreshToken}, nil\n\t}\n\n\treturn &config, nil\n}\n\nfunc encodeAuthConfig(config *AuthConfig) string {\n\tjsonBytes, _ := json.Marshal(config)\n\treturn base64.StdEncoding.EncodeToString(jsonBytes)\n}\n\nfunc generateHardwareID() string {\n\th := sha256.New()\n\th.Write([]byte(\"ring-client-go2rtc\"))\n\treturn hex.EncodeToString(h.Sum(nil)[:16])\n}\n\nfunc interfaceSlice(slice interface{}) []CameraData {\n\ts := reflect.ValueOf(slice)\n\tif s.Kind() != reflect.Slice {\n\t\treturn nil\n\t}\n\n\tret := make([]CameraData, s.Len())\n\tfor i := 0; i < s.Len(); i++ {\n\t\tif camera, ok := s.Index(i).Interface().(CameraData); ok {\n\t\t\tret[i] = camera\n\t\t}\n\t}\n\treturn ret\n}\n"
  },
  {
    "path": "pkg/ring/client.go",
    "content": "package ring\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"strconv\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/webrtc\"\n\t\"github.com/google/uuid\"\n\tpion \"github.com/pion/webrtc/v4\"\n)\n\ntype Client struct {\n\tapi       *RingApi\n\twsClient  *WSClient\n\tprod      core.Producer\n\tcameraID  int\n\tdialogID  string\n\tconnected core.Waiter\n\tclosed    bool\n}\n\nfunc Dial(rawURL string) (*Client, error) {\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tquery := u.Query()\n\tencodedToken := query.Get(\"refresh_token\")\n\tcameraID := query.Get(\"camera_id\")\n\tdeviceID := query.Get(\"device_id\")\n\t_, isSnapshot := query[\"snapshot\"]\n\n\tif encodedToken == \"\" || deviceID == \"\" || cameraID == \"\" {\n\t\treturn nil, errors.New(\"ring: wrong query\")\n\t}\n\n\tclient := &Client{\n\t\tdialogID: uuid.NewString(),\n\t}\n\n\tclient.cameraID, err = strconv.Atoi(cameraID)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ring: invalid camera_id: %w\", err)\n\t}\n\n\trefreshToken, err := url.QueryUnescape(encodedToken)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"ring: invalid refresh token encoding: %w\", err)\n\t}\n\n\tclient.api, err = NewRestClient(RefreshTokenAuth{RefreshToken: refreshToken}, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Snapshot Flow\n\tif isSnapshot {\n\t\tclient.prod = NewSnapshotProducer(client.api, client.cameraID)\n\t\treturn client, nil\n\t}\n\n\tclient.wsClient, err = StartWebsocket(client.cameraID, client.api)\n\tif err != nil {\n\t\tclient.Stop()\n\t\treturn nil, err\n\t}\n\n\t// Create Peer Connection\n\tconf := pion.Configuration{\n\t\tICEServers: []pion.ICEServer{\n\t\t\t{URLs: []string{\n\t\t\t\t\"stun:stun.kinesisvideo.us-east-1.amazonaws.com:443\",\n\t\t\t\t\"stun:stun.kinesisvideo.us-east-2.amazonaws.com:443\",\n\t\t\t\t\"stun:stun.kinesisvideo.us-west-2.amazonaws.com:443\",\n\t\t\t\t\"stun:stun.l.google.com:19302\",\n\t\t\t\t\"stun:stun1.l.google.com:19302\",\n\t\t\t\t\"stun:stun2.l.google.com:19302\",\n\t\t\t\t\"stun:stun3.l.google.com:19302\",\n\t\t\t\t\"stun:stun4.l.google.com:19302\",\n\t\t\t}},\n\t\t},\n\t\tICETransportPolicy: pion.ICETransportPolicyAll,\n\t\tBundlePolicy:       pion.BundlePolicyBalanced,\n\t}\n\n\tapi, err := webrtc.NewAPI()\n\tif err != nil {\n\t\tclient.Stop()\n\t\treturn nil, err\n\t}\n\n\tpc, err := api.NewPeerConnection(conf)\n\tif err != nil {\n\t\tclient.Stop()\n\t\treturn nil, err\n\t}\n\n\t// protect from sending ICE candidate before Offer\n\tvar sendOffer core.Waiter\n\n\t// protect from blocking on errors\n\tdefer sendOffer.Done(nil)\n\n\tprod := webrtc.NewConn(pc)\n\tprod.FormatName = \"ring/webrtc\"\n\tprod.Mode = core.ModeActiveProducer\n\tprod.Protocol = \"ws\"\n\tprod.URL = rawURL\n\n\tclient.wsClient.onMessage = func(msg WSMessage) {\n\t\tclient.onWSMessage(msg)\n\t}\n\n\tclient.wsClient.onError = func(err error) {\n\t\t// fmt.Printf(\"ring: error: %s\\n\", err.Error())\n\t\tclient.Stop()\n\t\tclient.connected.Done(err)\n\t}\n\n\tclient.wsClient.onClose = func() {\n\t\t// fmt.Println(\"ring: disconnect\")\n\t\tclient.Stop()\n\t\tclient.connected.Done(errors.New(\"ring: disconnect\"))\n\t}\n\n\tprod.Listen(func(msg any) {\n\t\tswitch msg := msg.(type) {\n\t\tcase *pion.ICECandidate:\n\t\t\t_ = sendOffer.Wait()\n\n\t\t\ticeCandidate := msg.ToJSON()\n\n\t\t\t// skip empty ICE candidates\n\t\t\tif iceCandidate.Candidate == \"\" {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\ticePayload := map[string]interface{}{\n\t\t\t\t\"ice\":        iceCandidate.Candidate,\n\t\t\t\t\"mlineindex\": iceCandidate.SDPMLineIndex,\n\t\t\t}\n\n\t\t\tif err = client.wsClient.sendSessionMessage(\"ice\", icePayload); err != nil {\n\t\t\t\tclient.connected.Done(err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\tcase pion.PeerConnectionState:\n\t\t\tswitch msg {\n\t\t\tcase pion.PeerConnectionStateNew:\n\t\t\t\tbreak\n\t\t\tcase pion.PeerConnectionStateConnecting:\n\t\t\t\tbreak\n\t\t\tcase pion.PeerConnectionStateConnected:\n\t\t\t\tclient.connected.Done(nil)\n\t\t\tdefault:\n\t\t\t\tclient.Stop()\n\t\t\t\tclient.connected.Done(errors.New(\"ring: \" + msg.String()))\n\t\t\t}\n\t\t}\n\t})\n\n\tclient.prod = prod\n\n\t// Setup media configuration\n\tmedias := []*core.Media{\n\t\t{\n\t\t\tKind:      core.KindAudio,\n\t\t\tDirection: core.DirectionSendRecv,\n\t\t\tCodecs: []*core.Codec{\n\t\t\t\t{\n\t\t\t\t\tName:      \"opus\",\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\t{\n\t\t\tKind:      core.KindVideo,\n\t\t\tDirection: core.DirectionRecvonly,\n\t\t\tCodecs: []*core.Codec{\n\t\t\t\t{\n\t\t\t\t\tName:      \"H264\",\n\t\t\t\t\tClockRate: 90000,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\n\t// Create offer\n\toffer, err := prod.CreateOffer(medias)\n\tif err != nil {\n\t\tclient.Stop()\n\t\treturn nil, err\n\t}\n\n\t// Send offer\n\tofferPayload := map[string]interface{}{\n\t\t\"stream_options\": map[string]bool{\n\t\t\t\"audio_enabled\": true,\n\t\t\t\"video_enabled\": true,\n\t\t},\n\t\t\"sdp\": offer,\n\t}\n\n\tif err = client.wsClient.sendSessionMessage(\"live_view\", offerPayload); err != nil {\n\t\tclient.Stop()\n\t\treturn nil, err\n\t}\n\n\tsendOffer.Done(nil)\n\n\tif err = client.connected.Wait(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n\nfunc (c *Client) onWSMessage(msg WSMessage) {\n\trawMsg, _ := json.Marshal(msg)\n\n\t// fmt.Printf(\"ring: onWSMessage: %s\\n\", string(rawMsg))\n\n\t// check if \"doorbot_id\" is present\n\tif _, ok := msg.Body[\"doorbot_id\"]; !ok {\n\t\treturn\n\t}\n\n\t// check if the message is from the correct doorbot\n\tdoorbotID := msg.Body[\"doorbot_id\"].(float64)\n\tif int(doorbotID) != c.cameraID {\n\t\treturn\n\t}\n\n\tif msg.Method == \"session_created\" || msg.Method == \"session_started\" {\n\t\tif _, ok := msg.Body[\"session_id\"]; ok && c.wsClient.sessionID == \"\" {\n\t\t\tc.wsClient.sessionID = msg.Body[\"session_id\"].(string)\n\t\t}\n\t}\n\n\t// check if the message is from the correct session\n\tif _, ok := msg.Body[\"session_id\"]; ok {\n\t\tif msg.Body[\"session_id\"].(string) != c.wsClient.sessionID {\n\t\t\treturn\n\t\t}\n\t}\n\n\tswitch msg.Method {\n\tcase \"sdp\":\n\t\tif prod, ok := c.prod.(*webrtc.Conn); ok {\n\t\t\t// Get answer\n\t\t\tvar msg AnswerMessage\n\t\t\tif err := json.Unmarshal(rawMsg, &msg); err != nil {\n\t\t\t\tc.Stop()\n\t\t\t\tc.connected.Done(err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif err := prod.SetAnswer(msg.Body.SDP); err != nil {\n\t\t\t\tc.Stop()\n\t\t\t\tc.connected.Done(err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif err := c.wsClient.activateSession(); err != nil {\n\t\t\t\tc.Stop()\n\t\t\t\tc.connected.Done(err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tprod.SDP = msg.Body.SDP\n\t\t}\n\n\tcase \"ice\":\n\t\tif prod, ok := c.prod.(*webrtc.Conn); ok {\n\t\t\tvar msg IceCandidateMessage\n\t\t\tif err := json.Unmarshal(rawMsg, &msg); err != nil {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\t// Skip empty candidates\n\t\t\tif msg.Body.Ice == \"\" {\n\t\t\t\tbreak\n\t\t\t}\n\n\t\t\tif err := prod.AddCandidate(msg.Body.Ice); err != nil {\n\t\t\t\tc.Stop()\n\t\t\t\tc.connected.Done(err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\tcase \"close\":\n\t\tc.Stop()\n\t\tc.connected.Done(errors.New(\"ring: close\"))\n\n\tcase \"pong\":\n\t\t// Ignore\n\t}\n}\n\nfunc (c *Client) GetMedias() []*core.Media {\n\treturn c.prod.GetMedias()\n}\n\nfunc (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {\n\treturn c.prod.GetTrack(media, codec)\n}\n\nfunc (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {\n\tif webrtcProd, ok := c.prod.(*webrtc.Conn); ok {\n\t\tif media.Kind == core.KindAudio {\n\t\t\t// Enable speaker\n\t\t\tspeakerPayload := map[string]interface{}{\n\t\t\t\t\"stealth_mode\": false,\n\t\t\t}\n\t\t\t_ = c.wsClient.sendSessionMessage(\"camera_options\", speakerPayload)\n\t\t}\n\t\treturn webrtcProd.AddTrack(media, codec, track)\n\t}\n\n\treturn fmt.Errorf(\"add track not supported for snapshot\")\n}\n\nfunc (c *Client) Start() error {\n\treturn c.prod.Start()\n}\n\nfunc (c *Client) Stop() error {\n\tif c.closed {\n\t\treturn nil\n\t}\n\n\tc.closed = true\n\n\tif c.prod != nil {\n\t\t_ = c.prod.Stop()\n\t}\n\n\tif c.wsClient != nil {\n\t\t_ = c.wsClient.Close()\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) MarshalJSON() ([]byte, error) {\n\treturn json.Marshal(c.prod)\n}\n"
  },
  {
    "path": "pkg/ring/snapshot.go",
    "content": "package ring\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/pion/rtp\"\n)\n\ntype SnapshotProducer struct {\n\tcore.Connection\n\n\tclient   *RingApi\n\tcameraID int\n}\n\nfunc NewSnapshotProducer(client *RingApi, cameraID int) *SnapshotProducer {\n\treturn &SnapshotProducer{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"ring/snapshot\",\n\t\t\tProtocol:   \"https\",\n\t\t\tRemoteAddr: \"app-snaps.ring.com\",\n\t\t\tMedias: []*core.Media{\n\t\t\t\t{\n\t\t\t\t\tKind:      core.KindVideo,\n\t\t\t\t\tDirection: core.DirectionRecvonly,\n\t\t\t\t\tCodecs: []*core.Codec{\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tName:        core.CodecJPEG,\n\t\t\t\t\t\t\tClockRate:   90000,\n\t\t\t\t\t\t\tPayloadType: core.PayloadTypeRAW,\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\tclient:   client,\n\t\tcameraID: cameraID,\n\t}\n}\n\nfunc (p *SnapshotProducer) Start() error {\n\tresponse, err := p.client.Request(\"GET\", fmt.Sprintf(\"https://app-snaps.ring.com/snapshots/next/%d\", p.cameraID), nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpkt := &rtp.Packet{\n\t\tHeader:  rtp.Header{Timestamp: core.Now90000()},\n\t\tPayload: response,\n\t}\n\n\tp.Receivers[0].WriteRTP(pkt)\n\n\treturn nil\n}\n\nfunc (p *SnapshotProducer) Stop() error {\n\treturn p.Connection.Stop()\n}\n"
  },
  {
    "path": "pkg/ring/ws.go",
    "content": "package ring\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/google/uuid\"\n\t\"github.com/gorilla/websocket\"\n)\n\ntype SessionBody struct {\n\tDoorbotID int    `json:\"doorbot_id\"`\n\tSessionID string `json:\"session_id\"`\n}\n\ntype AnswerMessage struct {\n\tMethod string `json:\"method\"` // \"sdp\"\n\tBody   struct {\n\t\tSessionBody\n\t\tSDP  string `json:\"sdp\"`\n\t\tType string `json:\"type\"` // \"answer\"\n\t} `json:\"body\"`\n}\n\ntype IceCandidateMessage struct {\n\tMethod string `json:\"method\"` // \"ice\"\n\tBody   struct {\n\t\tSessionBody\n\t\tIce        string `json:\"ice\"`\n\t\tMLineIndex int    `json:\"mlineindex\"`\n\t} `json:\"body\"`\n}\n\ntype SessionMessage struct {\n\tMethod string      `json:\"method\"` // \"session_created\" or \"session_started\"\n\tBody   SessionBody `json:\"body\"`\n}\n\ntype PongMessage struct {\n\tMethod string      `json:\"method\"` // \"pong\"\n\tBody   SessionBody `json:\"body\"`\n}\n\ntype NotificationMessage struct {\n\tMethod string `json:\"method\"` // \"notification\"\n\tBody   struct {\n\t\tSessionBody\n\t\tIsOK bool   `json:\"is_ok\"`\n\t\tText string `json:\"text\"`\n\t} `json:\"body\"`\n}\n\ntype StreamInfoMessage struct {\n\tMethod string `json:\"method\"` // \"stream_info\"\n\tBody   struct {\n\t\tSessionBody\n\t\tTranscoding       bool   `json:\"transcoding\"`\n\t\tTranscodingReason string `json:\"transcoding_reason\"`\n\t} `json:\"body\"`\n}\n\ntype CloseRequest struct {\n\tMethod string `json:\"method\"` // \"close\"\n\tBody   struct {\n\t\tSessionBody\n\t\tReason struct {\n\t\t\tCode int    `json:\"code\"`\n\t\t\tText string `json:\"text\"`\n\t\t} `json:\"reason\"`\n\t} `json:\"body\"`\n}\n\ntype WSMessage struct {\n\tMethod string         `json:\"method\"`\n\tBody   map[string]any `json:\"body\"`\n}\n\ntype WSClient struct {\n\tws        *websocket.Conn\n\tapi       *RingApi\n\twsMutex   sync.Mutex\n\tcameraID  int\n\tdialogID  string\n\tsessionID string\n\n\tonMessage func(msg WSMessage)\n\tonError   func(err error)\n\tonClose   func()\n\n\tclosed chan struct{}\n}\n\nconst (\n\tCloseReasonNormalClose          = 0\n\tCloseReasonAuthenticationFailed = 5\n\tCloseReasonTimeout              = 6\n)\n\nfunc StartWebsocket(cameraID int, api *RingApi) (*WSClient, error) {\n\tclient := &WSClient{\n\t\tapi:      api,\n\t\tcameraID: cameraID,\n\t\tdialogID: uuid.NewString(),\n\t\tclosed:   make(chan struct{}),\n\t}\n\n\tticket, err := client.api.GetSocketTicket()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\turl := fmt.Sprintf(\"wss://api.prod.signalling.ring.devices.a2z.com/ws?api_version=4.0&auth_type=ring_solutions&client_id=ring_site-%s&token=%s\",\n\t\tuuid.NewString(), url.QueryEscape(ticket.Ticket))\n\n\thttpHeader := http.Header{}\n\thttpHeader.Set(\"User-Agent\", \"android:com.ringapp\")\n\n\tclient.ws, _, err = websocket.DefaultDialer.Dial(url, httpHeader)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient.ws.SetCloseHandler(func(code int, text string) error {\n\t\tclient.onWsClose()\n\t\treturn nil\n\t})\n\n\tgo client.startPingLoop()\n\tgo client.startMessageLoop()\n\n\treturn client, nil\n}\n\nfunc (c *WSClient) Close() error {\n\tselect {\n\tcase <-c.closed:\n\t\treturn nil\n\tdefault:\n\t\tclose(c.closed)\n\t}\n\n\tclosePayload := map[string]interface{}{\n\t\t\"reason\": map[string]interface{}{\n\t\t\t\"code\": CloseReasonNormalClose,\n\t\t\t\"text\": \"\",\n\t\t},\n\t}\n\n\t_ = c.sendSessionMessage(\"close\", closePayload)\n\n\treturn c.ws.Close()\n}\n\nfunc (c *WSClient) startPingLoop() {\n\tticker := time.NewTicker(5 * time.Second)\n\tdefer ticker.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase <-c.closed:\n\t\t\treturn\n\t\tcase <-ticker.C:\n\t\t\tif err := c.sendSessionMessage(\"ping\", nil); err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (c *WSClient) startMessageLoop() {\n\tfor {\n\t\tselect {\n\t\tcase <-c.closed:\n\t\t\treturn\n\t\tdefault:\n\t\t\tvar res WSMessage\n\t\t\tif err := c.ws.ReadJSON(&res); err != nil {\n\t\t\t\tselect {\n\t\t\t\tcase <-c.closed:\n\t\t\t\t\t// Ignore error if closed\n\t\t\t\tdefault:\n\t\t\t\t\tc.onWsError(err)\n\t\t\t\t}\n\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tc.onWsMessage(res)\n\t\t}\n\t}\n}\n\nfunc (c *WSClient) activateSession() error {\n\tif err := c.sendSessionMessage(\"activate_session\", nil); err != nil {\n\t\treturn err\n\t}\n\n\tstreamPayload := map[string]interface{}{\n\t\t\"audio_enabled\": true,\n\t\t\"video_enabled\": true,\n\t}\n\n\tif err := c.sendSessionMessage(\"stream_options\", streamPayload); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (c *WSClient) sendSessionMessage(method string, payload map[string]interface{}) error {\n\tselect {\n\tcase <-c.closed:\n\t\treturn nil\n\tdefault:\n\t\t// continue\n\t}\n\n\tc.wsMutex.Lock()\n\tdefer c.wsMutex.Unlock()\n\n\tif payload == nil {\n\t\tpayload = make(map[string]interface{})\n\t}\n\n\tpayload[\"doorbot_id\"] = c.cameraID\n\tif c.sessionID != \"\" {\n\t\tpayload[\"session_id\"] = c.sessionID\n\t}\n\n\tmsg := map[string]interface{}{\n\t\t\"method\":    method,\n\t\t\"dialog_id\": c.dialogID,\n\t\t\"body\":      payload,\n\t}\n\n\t// rawMsg, _ := json.Marshal(msg)\n\t// fmt.Printf(\"ring: sendSessionMessage: %s: %s\\n\", method, string(rawMsg))\n\n\tif err := c.ws.WriteJSON(msg); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (c *WSClient) onWsMessage(msg WSMessage) {\n\tif c.onMessage != nil {\n\t\tc.onMessage(msg)\n\t}\n}\n\nfunc (c *WSClient) onWsError(err error) {\n\tif c.onError != nil {\n\t\tc.onError(err)\n\t}\n}\n\nfunc (c *WSClient) onWsClose() {\n\tif c.onClose != nil {\n\t\tc.onClose()\n\t}\n}\n"
  },
  {
    "path": "pkg/roborock/api.go",
    "content": "package roborock\n\nimport (\n\t\"crypto/hmac\"\n\t\"crypto/md5\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\ntype UserInfo struct {\n\tToken string `json:\"token\"`\n\tIoT   struct {\n\t\tUser   string `json:\"u\"`\n\t\tPass   string `json:\"s\"`\n\t\tHash   string `json:\"h\"`\n\t\tDomain string `json:\"k\"`\n\t\tURL    struct {\n\t\t\tAPI  string `json:\"a\"`\n\t\t\tMQTT string `json:\"m\"`\n\t\t} `json:\"r\"`\n\t} `json:\"rriot\"`\n}\n\nfunc GetBaseURL(username string) (string, error) {\n\tu := \"https://euiot.roborock.com/api/v1/getUrlByEmail?email=\" + url.QueryEscape(username)\n\treq, err := http.NewRequest(\"POST\", u, nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tclient := http.Client{Timeout: time.Second * 5000}\n\tres, err := client.Do(req)\n\n\tvar v struct {\n\t\tMsg  string `json:\"msg\"`\n\t\tCode int    `json:\"code\"`\n\t\tData struct {\n\t\t\tURL string `json:\"url\"`\n\t\t} `json:\"data\"`\n\t}\n\tif err = json.NewDecoder(res.Body).Decode(&v); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif v.Code != 200 {\n\t\treturn \"\", fmt.Errorf(\"%d: %s\", v.Code, v.Msg)\n\t}\n\n\treturn v.Data.URL, nil\n}\n\nfunc Login(baseURL, username, password string) (*UserInfo, error) {\n\tu := baseURL + \"/api/v1/login?username=\" + url.QueryEscape(username) +\n\t\t\"&password=\" + url.QueryEscape(password) + \"&needtwostepauth=false\"\n\treq, err := http.NewRequest(\"POST\", u, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclientID := core.RandString(16, 64)\n\tclientID = base64.StdEncoding.EncodeToString([]byte(clientID))\n\treq.Header.Set(\"header_clientid\", clientID)\n\n\tclient := http.Client{Timeout: time.Second * 5000}\n\tres, err := client.Do(req)\n\n\tvar v struct {\n\t\tMsg  string   `json:\"msg\"`\n\t\tCode int      `json:\"code\"`\n\t\tData UserInfo `json:\"data\"`\n\t}\n\tif err = json.NewDecoder(res.Body).Decode(&v); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif v.Code != 200 {\n\t\treturn nil, fmt.Errorf(\"%d: %s\", v.Code, v.Msg)\n\t}\n\n\treturn &v.Data, nil\n}\n\nfunc GetHomeID(baseURL, token string) (int, error) {\n\treq, err := http.NewRequest(\"GET\", baseURL+\"/api/v1/getHomeDetail\", nil)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treq.Header.Set(\"Authorization\", token)\n\n\tclient := http.Client{Timeout: time.Second * 5000}\n\tres, err := client.Do(req)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\tvar v struct {\n\t\tMsg  string `json:\"msg\"`\n\t\tCode int    `json:\"code\"`\n\t\tData struct {\n\t\t\tHomeID int `json:\"rrHomeId\"`\n\t\t} `json:\"data\"`\n\t}\n\tif err = json.NewDecoder(res.Body).Decode(&v); err != nil {\n\t\treturn 0, err\n\t}\n\n\tif v.Code != 200 {\n\t\treturn 0, fmt.Errorf(\"%d: %s\", v.Code, v.Msg)\n\t}\n\n\treturn v.Data.HomeID, nil\n}\n\ntype DeviceInfo struct {\n\tDID  string `json:\"duid\"`\n\tName string `json:\"name\"`\n\tKey  string `json:\"localKey\"`\n}\n\nfunc GetDevices(ui *UserInfo, homeID int) ([]DeviceInfo, error) {\n\tnonce := core.RandString(6, 64)\n\tts := time.Now().Unix()\n\tpath := \"/user/homes/\" + strconv.Itoa(homeID)\n\n\tmac := fmt.Sprintf(\n\t\t\"%s:%s:%s:%d:%x::\", ui.IoT.User, ui.IoT.Pass, nonce, ts, md5.Sum([]byte(path)),\n\t)\n\thash := hmac.New(sha256.New, []byte(ui.IoT.Hash))\n\thash.Write([]byte(mac))\n\tmac = base64.StdEncoding.EncodeToString(hash.Sum(nil))\n\n\tauth := fmt.Sprintf(\n\t\t`Hawk id=\"%s\", s=\"%s\", ts=\"%d\", nonce=\"%s\", mac=\"%s\"`,\n\t\tui.IoT.User, ui.IoT.Pass, ts, nonce, mac,\n\t)\n\n\treq, err := http.NewRequest(\"GET\", ui.IoT.URL.API+path, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header.Set(\"Authorization\", auth)\n\n\tclient := http.Client{Timeout: time.Second * 5000}\n\tres, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar v struct {\n\t\tResult struct {\n\t\t\tDevices []DeviceInfo `json:\"devices\"`\n\t\t} `json:\"result\"`\n\t}\n\tif err = json.NewDecoder(res.Body).Decode(&v); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn v.Result.Devices, nil\n}\n"
  },
  {
    "path": "pkg/roborock/client.go",
    "content": "package roborock\n\nimport (\n\t\"crypto/md5\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/rpc\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/roborock/iot\"\n\t\"github.com/AlexxIT/go2rtc/pkg/webrtc\"\n\tpion \"github.com/pion/webrtc/v4\"\n)\n\n// Deprecated: should be rewritten to core.Connection\ntype Client struct {\n\tcore.Listener\n\n\turl string\n\n\tconn *webrtc.Conn\n\tiot  *rpc.Client\n\n\tdevKey   string\n\tpin      string\n\tdevTopic string\n\n\taudio       bool\n\tbackchannel bool\n}\n\nfunc Dial(rawURL string) (*Client, error) {\n\tclient := &Client{url: rawURL}\n\tif err := client.Dial(); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := client.Connect(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn client, nil\n}\n\nfunc (c *Client) Dial() error {\n\tu, err := url.Parse(c.url)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif c.iot, err = iot.Dial(c.url); err != nil {\n\t\treturn err\n\t}\n\n\tc.pin = u.Query().Get(\"pin\")\n\tif c.pin != \"\" {\n\t\tc.pin = fmt.Sprintf(\"%x\", md5.Sum([]byte(c.pin)))\n\t\treturn c.CheckHomesecPassword()\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) Connect() error {\n\t// 1. Check if camera ready for connection\n\tfor i := 0; ; i++ {\n\t\tclientID, err := c.GetHomesecConnectStatus()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif clientID == \"none\" {\n\t\t\tbreak\n\t\t}\n\t\tif err = c.StopCameraPreview(clientID); err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif i == 5 {\n\t\t\treturn errors.New(\"camera not ready\")\n\t\t}\n\t\ttime.Sleep(time.Second)\n\t}\n\n\t// 2. Start camera\n\tif err := c.StartCameraPreview(); err != nil {\n\t\treturn err\n\t}\n\n\t// 3. Get TURN config\n\tconf := pion.Configuration{}\n\n\tif turn, _ := c.GetTurnServer(); turn != nil {\n\t\tconf.ICEServers = append(conf.ICEServers, *turn)\n\t}\n\n\t// 4. Create Peer Connection\n\tapi, err := webrtc.NewAPI()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tpc, err := api.NewPeerConnection(conf)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar connected = make(chan bool)\n\tvar sendOffer sync.WaitGroup\n\n\tc.conn = webrtc.NewConn(pc)\n\tc.conn.FormatName = \"roborock\"\n\tc.conn.Mode = core.ModeActiveProducer\n\tc.conn.Protocol = \"mqtt\"\n\tc.conn.URL = c.url\n\tc.conn.Listen(func(msg any) {\n\t\tswitch msg := msg.(type) {\n\t\tcase *pion.ICECandidate:\n\t\t\tif msg != nil && msg.Component == 1 {\n\t\t\t\tsendOffer.Wait()\n\t\t\t\t_ = c.SendICEtoRobot(msg.ToJSON().Candidate, \"0\")\n\t\t\t}\n\t\tcase pion.PeerConnectionState:\n\t\t\tif msg == pion.PeerConnectionStateConnecting {\n\t\t\t\treturn\n\t\t\t}\n\t\t\t// unblocking write to channel\n\t\t\tselect {\n\t\t\tcase connected <- msg == pion.PeerConnectionStateConnected:\n\t\t\tdefault:\n\t\t\t}\n\t\t}\n\t})\n\n\t// 5. Send Offer\n\tsendOffer.Add(1)\n\n\tmedias := []*core.Media{\n\t\t{Kind: core.KindVideo, Direction: core.DirectionRecvonly},\n\t\t{Kind: core.KindAudio, Direction: core.DirectionSendRecv},\n\t}\n\n\tif _, err = c.conn.CreateOffer(medias); err != nil {\n\t\treturn err\n\t}\n\n\toffer := pc.LocalDescription()\n\t//log.Printf(\"[roborock] offer\\n%s\", offer.SDP)\n\tif err = c.SendSDPtoRobot(offer); err != nil {\n\t\treturn err\n\t}\n\n\tsendOffer.Done()\n\n\t// 6. Receive answer\n\tts := time.Now().Add(time.Second * 5)\n\tfor {\n\t\ttime.Sleep(time.Second)\n\n\t\tif desc, _ := c.GetDeviceSDP(); desc != nil {\n\t\t\t//log.Printf(\"[roborock] answer\\n%s\", desc.SDP)\n\t\t\tif err = c.conn.SetAnswer(desc.SDP); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\n\t\tif time.Now().After(ts) {\n\t\t\treturn errors.New(\"can't get device SDP\")\n\t\t}\n\t}\n\n\tticker := time.NewTicker(time.Second * 2)\n\tfor {\n\t\tselect {\n\t\tcase <-ticker.C:\n\t\t\t// 7. Receive remote candidates\n\t\t\tif pc.ICEConnectionState() == pion.ICEConnectionStateCompleted {\n\t\t\t\tticker.Stop()\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif ice, _ := c.GetDeviceICE(); ice != nil {\n\t\t\t\tfor _, candidate := range ice {\n\t\t\t\t\t_ = c.conn.AddCandidate(candidate)\n\t\t\t\t}\n\t\t\t}\n\n\t\tcase ok := <-connected:\n\t\t\t// 8. Wait connected result (true or false)\n\t\t\tif !ok {\n\t\t\t\treturn errors.New(\"can't connect\")\n\t\t\t}\n\n\t\t\treturn nil\n\t\t}\n\t}\n}\n\nfunc (c *Client) CheckHomesecPassword() (err error) {\n\tvar ok bool\n\n\tparams := `{\"password\":\"` + c.pin + `\"}`\n\tif err = c.iot.Call(\"check_homesec_password\", params, &ok); err != nil {\n\t\treturn\n\t}\n\n\tif !ok {\n\t\treturn errors.New(\"wrong pin code\")\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) GetHomesecConnectStatus() (clientID string, err error) {\n\tvar res []byte\n\n\tif err = c.iot.Call(\"get_homesec_connect_status\", nil, &res); err != nil {\n\t\treturn\n\t}\n\n\tvar v struct {\n\t\tStatus   int    `json:\"status\"`\n\t\tClientID string `json:\"client_id\"`\n\t}\n\tif err = json.Unmarshal(res, &v); err != nil {\n\t\treturn\n\t}\n\n\treturn v.ClientID, nil\n}\n\nfunc (c *Client) StartCameraPreview() error {\n\tparams := `{\"client_id\":\"676f32727463\",\"quality\":\"HD\",\"password\":\"` + c.pin + `\"}`\n\treturn c.Request(\"start_camera_preview\", params)\n}\n\nfunc (c *Client) StopCameraPreview(clientID string) error {\n\tparams := `{\"client_id\":\"` + clientID + `\"}`\n\treturn c.Request(\"stop_camera_preview\", params)\n}\n\nfunc (c *Client) GetTurnServer() (turn *pion.ICEServer, err error) {\n\tvar res []byte\n\n\tif err = c.iot.Call(\"get_turn_server\", nil, &res); err != nil {\n\t\treturn\n\t}\n\n\tvar v struct {\n\t\tURL  string `json:\"url\"`\n\t\tUser string `json:\"user\"`\n\t\tPwd  string `json:\"pwd\"`\n\t}\n\tif err = json.Unmarshal(res, &v); err != nil {\n\t\treturn nil, err\n\t}\n\n\tturn = &pion.ICEServer{\n\t\tURLs:       []string{v.URL},\n\t\tUsername:   v.User,\n\t\tCredential: v.Pwd,\n\t}\n\n\treturn\n}\n\nfunc (c *Client) SendSDPtoRobot(offer *pion.SessionDescription) (err error) {\n\tb, err := json.Marshal(offer)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tparams := `{\"app_sdp\":\"` + base64.StdEncoding.EncodeToString(b) + `\"}`\n\treturn c.iot.Call(\"send_sdp_to_robot\", params, nil)\n}\n\nfunc (c *Client) SendICEtoRobot(candidate string, mid string) (err error) {\n\tb := []byte(`{\"candidate\":\"` + candidate + `\",\"sdpMLineIndex\":` + mid + `,\"sdpMid\":\"` + mid + `\"}`)\n\n\tparams := `{\"app_ice\":\"` + base64.StdEncoding.EncodeToString(b) + `\"}`\n\treturn c.iot.Call(\"send_ice_to_robot\", params, nil)\n}\n\nfunc (c *Client) GetDeviceSDP() (sd *pion.SessionDescription, err error) {\n\tvar res []byte\n\n\tif err = c.iot.Call(\"get_device_sdp\", nil, &res); err != nil {\n\t\treturn\n\t}\n\n\tif string(res) == `{\"dev_sdp\":\"retry\"}` {\n\t\treturn nil, nil\n\t}\n\n\tvar v struct {\n\t\tSDP []byte `json:\"dev_sdp\"`\n\t}\n\tif err = json.Unmarshal(res, &v); err != nil {\n\t\treturn nil, err\n\t}\n\n\tsd = &pion.SessionDescription{}\n\tif err = json.Unmarshal(v.SDP, sd); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn\n}\n\nfunc (c *Client) GetDeviceICE() (ice []string, err error) {\n\tvar res []byte\n\n\tif err = c.iot.Call(\"get_device_ice\", nil, &res); err != nil {\n\t\treturn\n\t}\n\n\tif string(res) == `{\"dev_ice\":\"retry\"}` {\n\t\treturn nil, nil\n\t}\n\n\tvar v struct {\n\t\tICE [][]byte `json:\"dev_ice\"`\n\t}\n\tif err = json.Unmarshal(res, &v); err != nil {\n\t\treturn\n\t}\n\n\tfor _, b := range v.ICE {\n\t\tinit := pion.ICECandidateInit{}\n\t\tif err = json.Unmarshal(b, &init); err != nil {\n\t\t\treturn\n\t\t}\n\t\tice = append(ice, init.Candidate)\n\t}\n\n\treturn\n}\n\nfunc (c *Client) StartVoiceChat() error {\n\t// record - audio from robot, play - audio to robot?\n\tparams := fmt.Sprintf(`{\"record\":%t,\"play\":%t}`, c.audio, c.backchannel)\n\treturn c.Request(\"start_voice_chat\", params)\n}\n\nfunc (c *Client) SwitchVideoQuality(hd bool) error {\n\tif hd {\n\t\treturn c.Request(\"switch_video_quality\", `{\"quality\":\"HD\"}`)\n\t} else {\n\t\treturn c.Request(\"switch_video_quality\", `{\"quality\":\"SD\"}`)\n\t}\n}\n\nfunc (c *Client) SetVoiceChatVolume(volume int) error {\n\tparams := `{\"volume\":` + strconv.Itoa(volume) + `}`\n\treturn c.Request(\"set_voice_chat_volume\", params)\n}\n\nfunc (c *Client) EnableHomesecVoice(enable bool) error {\n\tif enable {\n\t\treturn c.Request(\"enable_homesec_voice\", `{\"enable\":true}`)\n\t} else {\n\t\treturn c.Request(\"enable_homesec_voice\", `{\"enable\":false}`)\n\t}\n}\n\nfunc (c *Client) Request(method string, args any) (err error) {\n\tvar reply string\n\n\tif err = c.iot.Call(method, args, &reply); err != nil {\n\t\treturn\n\t}\n\n\tif reply != `[\"ok\"]` {\n\t\treturn errors.New(reply)\n\t}\n\n\treturn\n}\n"
  },
  {
    "path": "pkg/roborock/iot/client.go",
    "content": "package iot\n\nimport (\n\t\"crypto/md5\"\n\t\"crypto/tls\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/rpc\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/mqtt\"\n)\n\ntype Codec struct {\n\tmqtt *mqtt.Client\n\n\tdevTopic string\n\tdevKey   string\n\n\tbody json.RawMessage\n}\n\ntype dps struct {\n\tDps struct {\n\t\tReq string `json:\"101,omitempty\"`\n\t\tRes string `json:\"102,omitempty\"`\n\t} `json:\"dps\"`\n\tT uint32 `json:\"t\"`\n}\n\ntype response struct {\n\tID     uint64          `json:\"id\"`\n\tResult json.RawMessage `json:\"result\"`\n\tError  struct {\n\t\tCode    int    `json:\"code\"`\n\t\tMessage string `json:\"message\"`\n\t} `json:\"error\"`\n}\n\nfunc (c *Codec) WriteRequest(r *rpc.Request, v any) error {\n\tif v == nil {\n\t\tv = \"[]\"\n\t}\n\n\tts := uint32(time.Now().Unix())\n\tmsg := dps{T: ts}\n\tmsg.Dps.Req = fmt.Sprintf(\n\t\t`{\"id\":%d,\"method\":\"%s\",\"params\":%s}`, r.Seq, r.ServiceMethod, v,\n\t)\n\n\tpayload, err := json.Marshal(msg)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t//log.Printf(\"[roborock] send: %s\", payload)\n\n\tpayload = c.Encrypt(payload, ts, ts, ts)\n\n\treturn c.mqtt.Publish(\"rr/m/i/\"+c.devTopic, payload)\n}\n\nfunc (c *Codec) ReadResponseHeader(r *rpc.Response) error {\n\tfor {\n\t\t// receive any message from MQTT\n\t\t_, payload, err := c.mqtt.Read()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// skip if it is not PUBLISH message\n\t\tif payload == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// decrypt MQTT PUBLISH payload\n\t\tif payload, err = c.Decrypt(payload); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// skip if we can't decrypt this payload (ex. binary payload)\n\t\tif payload == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t//log.Printf(\"[roborock] recv %s\", payload)\n\n\t\t// get content from response payload:\n\t\t// {\"t\":1676871268,\"dps\":{\"102\":\"{\\\"id\\\":315003,\\\"result\\\":[\\\"ok\\\"]}\"}}\n\t\tvar msg dps\n\t\tif err = json.Unmarshal(payload, &msg); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar res response\n\t\tif err = json.Unmarshal([]byte(msg.Dps.Res), &res); err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tr.Seq = res.ID\n\t\tif res.Error.Code != 0 {\n\t\t\tr.Error = res.Error.Message\n\t\t} else {\n\t\t\tc.body = res.Result\n\t\t}\n\n\t\treturn nil\n\t}\n}\n\nfunc (c *Codec) ReadResponseBody(v any) error {\n\tswitch vv := v.(type) {\n\tcase *[]byte:\n\t\t*vv = c.body\n\tcase *string:\n\t\t*vv = string(c.body)\n\tcase *bool:\n\t\t*vv = string(c.body) == `[\"ok\"]`\n\t}\n\treturn nil\n}\n\nfunc (c *Codec) Close() error {\n\treturn c.mqtt.Close()\n}\n\nfunc Dial(rawURL string) (*rpc.Client, error) {\n\tlink, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// dial to MQTT\n\tconn, err := net.DialTimeout(\"tcp\", link.Host, time.Second*5)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// process MQTT SSL\n\tconf := &tls.Config{ServerName: link.Hostname()}\n\tsconn := tls.Client(conn, conf)\n\tif err = sconn.Handshake(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tquery := link.Query()\n\n\t// send MQTT login\n\tuk := md5.Sum([]byte(query.Get(\"u\") + \":\" + query.Get(\"k\")))\n\tsk := md5.Sum([]byte(query.Get(\"s\") + \":\" + query.Get(\"k\")))\n\tuser := hex.EncodeToString(uk[1:5])\n\tpass := hex.EncodeToString(sk[8:])\n\n\tc := &Codec{\n\t\tmqtt:     mqtt.NewClient(sconn),\n\t\tdevKey:   query.Get(\"key\"),\n\t\tdevTopic: query.Get(\"u\") + \"/\" + user + \"/\" + query.Get(\"did\"),\n\t}\n\n\tif err = c.mqtt.Connect(\"com.roborock.smart:mbrriot\", user, pass); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// subscribe on device topic\n\tif err = c.mqtt.Subscribe(\"rr/m/o/\" + c.devTopic); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn rpc.NewClientWithCodec(c), nil\n}\n"
  },
  {
    "path": "pkg/roborock/iot/crypto.go",
    "content": "package iot\n\nimport (\n\t\"crypto/aes\"\n\t\"crypto/md5\"\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"hash/crc32\"\n)\n\n// key - convert timestamp to key\nfunc (c *Codec) key(timestamp uint32) []byte {\n\tconst salt = \"TXdfu$jyZ#TZHsg4\"\n\tkey := md5.Sum([]byte(encodeTimestamp(timestamp) + c.devKey + salt))\n\treturn key[:]\n}\n\nfunc (c *Codec) Decrypt(cipherText []byte) ([]byte, error) {\n\tif len(cipherText) < 32 || string(cipherText[:3]) != \"1.0\" {\n\t\treturn nil, errors.New(\"wrong message prefix\")\n\t}\n\n\ti := len(cipherText) - 4\n\tif binary.BigEndian.Uint32(cipherText[i:]) != crc32.ChecksumIEEE(cipherText[:i]) {\n\t\treturn nil, errors.New(\"wrong message checksum\")\n\t}\n\n\tif proto := binary.BigEndian.Uint16(cipherText[15:]); proto != 102 {\n\t\treturn nil, nil\n\t}\n\n\ttimestamp := binary.BigEndian.Uint32(cipherText[11:])\n\treturn decryptECB(cipherText[19:i], c.key(timestamp)), nil\n}\n\nfunc (c *Codec) Encrypt(plainText []byte, seq, random, timestamp uint32) []byte {\n\tconst proto = 101\n\n\tcipherText := encryptECB(plainText, c.key(timestamp))\n\n\tsize := uint16(len(cipherText))\n\n\tmsg := make([]byte, 23+size)\n\tcopy(msg, \"1.0\")\n\tbinary.BigEndian.PutUint32(msg[3:], seq)\n\tbinary.BigEndian.PutUint32(msg[7:], random)\n\tbinary.BigEndian.PutUint32(msg[11:], timestamp)\n\tbinary.BigEndian.PutUint16(msg[15:], proto)\n\tbinary.BigEndian.PutUint16(msg[17:], size)\n\tcopy(msg[19:], cipherText)\n\n\tcrc := crc32.ChecksumIEEE(msg[:19+size])\n\n\tbinary.BigEndian.PutUint32(msg[19+size:], crc)\n\treturn msg\n}\n\nfunc encodeTimestamp(i uint32) string {\n\tconst hextable = \"0123456789abcdef\"\n\tb := []byte{\n\t\thextable[i>>8&0xF], hextable[i>>4&0xF],\n\t\thextable[i>>16&0xF], hextable[i&0xF],\n\t\thextable[i>>24&0xF], hextable[i>>20&0xF],\n\t\thextable[i>>28&0xF], hextable[i>>12&0xF],\n\t}\n\treturn string(b)\n}\n\nfunc pad(plainText []byte, blockSize int) []byte {\n\tb0 := byte(blockSize - len(plainText)%blockSize)\n\tfor i := byte(0); i < b0; i++ {\n\t\tplainText = append(plainText, b0)\n\t}\n\treturn plainText\n}\n\nfunc unpad(paddedText []byte) []byte {\n\tpadSize := int(paddedText[len(paddedText)-1])\n\treturn paddedText[:len(paddedText)-padSize]\n}\n\nfunc encryptECB(plainText, key []byte) []byte {\n\tblock, err := aes.NewCipher(key)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tblockSize := block.BlockSize()\n\tplainText = pad(plainText, blockSize)\n\tcipherText := plainText\n\n\tfor len(plainText) > 0 {\n\t\tblock.Encrypt(plainText, plainText)\n\t\tplainText = plainText[blockSize:]\n\t}\n\n\treturn cipherText\n}\n\nfunc decryptECB(cipherText, key []byte) []byte {\n\tblock, err := aes.NewCipher(key)\n\tif err != nil {\n\t\tpanic(err)\n\t}\n\n\tblockSize := block.BlockSize()\n\tpaddedText := cipherText\n\n\tfor len(cipherText) > 0 {\n\t\tblock.Decrypt(cipherText, cipherText)\n\t\tcipherText = cipherText[blockSize:]\n\t}\n\n\treturn unpad(paddedText)\n}\n"
  },
  {
    "path": "pkg/roborock/producer.go",
    "content": "package roborock\n\nimport (\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\nfunc (c *Client) GetMedias() []*core.Media {\n\treturn c.conn.GetMedias()\n}\n\nfunc (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {\n\tif media.Kind == core.KindAudio {\n\t\tc.audio = true\n\t}\n\n\treturn c.conn.GetTrack(media, codec)\n}\n\nfunc (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {\n\tc.backchannel = true\n\treturn c.conn.AddTrack(media, codec, track)\n}\n\nfunc (c *Client) Start() error {\n\tif c.audio || c.backchannel {\n\t\tif err := c.StartVoiceChat(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif c.backchannel {\n\t\t\tif err := c.SetVoiceChatVolume(80); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t}\n\t}\n\treturn c.conn.Start()\n}\n\nfunc (c *Client) Stop() error {\n\t_ = c.iot.Close()\n\treturn c.conn.Stop()\n}\n\nfunc (c *Client) MarshalJSON() ([]byte, error) {\n\treturn c.conn.MarshalJSON()\n}\n"
  },
  {
    "path": "pkg/rtmp/README.md",
    "content": "## Tests\n\n- go2rtc rtmp client => Reolink\n- go2rtc rtmp server <= Dahua\n- go2rtc rtmp publish => YouTube\n- go2rtc rtmp publish => Telegram\n\n## Logs\n\n```\nrequest  []interface {}{\"connect\", 1, map[string]interface {}{\"app\":\"s\", \"flashVer\":\"FMLE/3.0 (compatible; FMSc/1.0)\", \"tcUrl\":\"rtmps://xxx.rtmp.t.me/s/xxxxx\"}}\nresponse []interface {}{\"_result\", 1, map[string]interface {}{\"capabilities\":31, \"fmsVer\":\"FMS/3,0,1,123\"}, map[string]interface {}{\"code\":\"NetConnection.Connect.Success\", \"description\":\"Connection succeeded.\", \"level\":\"status\", \"objectEncoding\":0}}\nrequest  []interface {}{\"releaseStream\", 2, interface {}(nil), \"xxxxx\"}\nrequest  []interface {}{\"FCPublish\", 3, interface {}(nil), \"xxxxx\"}\nrequest  []interface {}{\"createStream\", 4, interface {}(nil)}\nresponse []interface {}{\"_result\", 2, interface {}(nil)}\nresponse []interface {}{\"_result\", 4, interface {}(nil), 1}\nrequest  []interface {}{\"publish\", 5, interface {}(nil), \"xxxxx\", \"live\"}\nresponse []interface {}{\"onStatus\", 0, interface {}(nil), map[string]interface {}{\"code\":\"NetStream.Publish.Start\", \"description\":\"xxxxx is now published\", \"detail\":\"xxxxx\", \"level\":\"status\"}}\n```\n\n## Useful links\n\n- https://en.wikipedia.org/wiki/Flash_Video\n- https://en.wikipedia.org/wiki/Real-Time_Messaging_Protocol\n- https://rtmp.veriskope.com/pdf/rtmp_specification_1.0.pdf\n- https://rtmp.veriskope.com/docs/spec/\n"
  },
  {
    "path": "pkg/rtmp/client.go",
    "content": "package rtmp\n\nimport (\n\t\"bufio\"\n\t\"io\"\n\t\"net\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/flv\"\n\t\"github.com/AlexxIT/go2rtc/pkg/tcp\"\n)\n\nfunc DialPlay(rawURL string) (*flv.Producer, error) {\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tconn, err := tcp.Dial(u, core.ConnDialTimeout)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient, err := NewClient(conn, u)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err = client.play(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn client.Producer()\n}\n\nfunc DialPublish(rawURL string, cons *flv.Consumer) (io.Writer, error) {\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tconn, err := tcp.Dial(u, core.ConnDialTimeout)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tclient, err := NewClient(conn, u)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err = client.publish(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tcons.FormatName = \"rtmp\"\n\tcons.Protocol = \"rtmp\"\n\tcons.RemoteAddr = conn.RemoteAddr().String()\n\tcons.URL = rawURL\n\n\treturn client, nil\n}\n\nfunc NewClient(conn net.Conn, u *url.URL) (*Conn, error) {\n\tc := &Conn{\n\t\turl: u.String(),\n\n\t\tconn: conn,\n\t\trd:   bufio.NewReaderSize(conn, core.BufferSize),\n\t\twr:   conn,\n\n\t\tchunks: map[uint8]*chunk{},\n\n\t\trdPacketSize: 128,\n\t\twrPacketSize: 4096, // OBS - 4096, Reolink - 4096\n\t}\n\n\tif args := strings.Split(u.Path, \"/\"); len(args) >= 2 {\n\t\tc.App = args[1]\n\t\tif len(args) >= 3 {\n\t\t\tc.Stream = args[2]\n\t\t\tif u.RawQuery != \"\" {\n\t\t\t\tc.Stream += \"?\" + u.RawQuery\n\t\t\t}\n\t\t}\n\t}\n\n\tif err := c.clienHandshake(); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := c.writePacketSize(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn c, nil\n}\n\nfunc (c *Conn) clienHandshake() error {\n\t// simple handshake without real random and check response\n\tb := make([]byte, 1+1536)\n\tb[0] = 0x03\n\t// write C0+C1\n\tif _, err := c.conn.Write(b); err != nil {\n\t\treturn err\n\t}\n\t// read S0+S1\n\tif _, err := io.ReadFull(c.rd, b); err != nil {\n\t\treturn err\n\t}\n\t// write S1\n\tif _, err := c.conn.Write(b[1:]); err != nil {\n\t\treturn err\n\t}\n\t// read C1, skip check\n\tif _, err := io.ReadFull(c.rd, b[1:]); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (c *Conn) play() error {\n\tif err := c.writeConnect(); err != nil {\n\t\treturn err\n\t}\n\tif err := c.writeCreateStream(); err != nil {\n\t\treturn err\n\t}\n\tif err := c.writePlay(); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc (c *Conn) publish() error {\n\tif err := c.writeConnect(); err != nil {\n\t\treturn err\n\t}\n\tif err := c.writeReleaseStream(); err != nil {\n\t\treturn err\n\t}\n\tif err := c.writeCreateStream(); err != nil {\n\t\treturn err\n\t}\n\tif err := c.writePublish(); err != nil {\n\t\treturn err\n\t}\n\n\tgo func() {\n\t\tfor {\n\t\t\t_, _, _, err := c.readMessage()\n\t\t\t//log.Printf(\"!!! %d %d %.30x\", msgType, timeMS, b)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}()\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/rtmp/conn.go",
    "content": "package rtmp\n\nimport (\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/flv/amf\"\n)\n\nconst (\n\tTypeSetPacketSize   = 1\n\tTypeServerBandwidth = 5\n\tTypeClientBandwidth = 6\n\tTypeAudio           = 8\n\tTypeVideo           = 9\n\tTypeData            = 18\n\tTypeCommand         = 20\n)\n\ntype Conn struct {\n\tApp    string\n\tStream string\n\tIntent string\n\n\trdPacketSize uint32\n\twrPacketSize uint32\n\n\tchunks   map[byte]*chunk\n\tstreamID byte\n\turl      string\n\n\tconn net.Conn\n\trd   io.Reader\n\twr   io.Writer\n\n\trdBuf []byte\n\twrBuf []byte\n\tmu    sync.Mutex\n}\n\nfunc (c *Conn) Close() error {\n\treturn c.conn.Close()\n}\n\nfunc (c *Conn) readResponse(wait func(items []any) bool) ([]any, error) {\n\tfor {\n\t\tmsgType, _, b, err := c.readMessage()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\t//log.Printf(\"[rtmp] type=%d data=%s\", msgType, b)\n\n\t\tswitch msgType {\n\t\tcase TypeSetPacketSize:\n\t\t\tc.rdPacketSize = binary.BigEndian.Uint32(b)\n\t\tcase TypeCommand:\n\t\t\titems, _ := amf.NewReader(b).ReadItems()\n\t\t\tif wait(items) {\n\t\t\t\treturn items, nil\n\t\t\t}\n\t\t}\n\t}\n}\n\ntype chunk struct {\n\tconn     *Conn\n\trawTime  uint32\n\tdataSize uint32\n\ttagType  byte\n\tstreamID uint32\n\ttimeMS   uint32\n}\n\nfunc (c *chunk) readHeader(typ byte) error {\n\tswitch typ {\n\tcase 0: // 12 byte header (full header)\n\t\tb, err := c.conn.readSize(11)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tc.rawTime = Uint24(b)\n\t\tc.dataSize = Uint24(b[3:])\n\t\tc.tagType = b[6]\n\t\tc.streamID = binary.LittleEndian.Uint32(b[7:])\n\t\tc.timeMS = c.readExtendedTime()\n\n\tcase 1: // 8 bytes - like type b00, not including message ID (4 last bytes)\n\t\tb, err := c.conn.readSize(7)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tc.rawTime = Uint24(b)\n\t\tc.dataSize = Uint24(b[3:]) // msgdatalen\n\t\tc.tagType = b[6]           // msgtypeid\n\t\tc.timeMS += c.readExtendedTime()\n\n\tcase 2: // 4 bytes - Basic Header and timestamp (3 bytes) are included\n\t\tb, err := c.conn.readSize(3)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tc.rawTime = Uint24(b) // timestamp\n\t\tc.timeMS += c.readExtendedTime()\n\n\tcase 3: // 1 byte - only the Basic Header is included\n\t\t// use here hdr from previous msg with same session ID (sid)\n\t}\n\treturn nil\n}\n\nfunc (c *chunk) readExtendedTime() uint32 {\n\tif c.rawTime == 0xFFFFFF {\n\t\tif b, err := c.conn.readSize(4); err == nil {\n\t\t\treturn binary.BigEndian.Uint32(b)\n\t\t}\n\t}\n\treturn c.rawTime\n}\n\n//var ErrNotImplemented = errors.New(\"rtmp: not implemented\")\n\nfunc (c *Conn) readMessage() (byte, uint32, []byte, error) {\n\tb, err := c.readSize(1) // doesn't support big chunkID!!!\n\tif err != nil {\n\t\treturn 0, 0, nil, err\n\t}\n\n\thdrType := b[0] >> 6\n\tchunkID := b[0] & 0b111111\n\n\t// storing header information for support header type 3\n\tch, ok := c.chunks[chunkID]\n\tif !ok {\n\t\tch = &chunk{conn: c}\n\t\tc.chunks[chunkID] = ch\n\t}\n\n\tif err = ch.readHeader(hdrType); err != nil {\n\t\treturn 0, 0, nil, err\n\t}\n\n\t//log.Printf(\"[rtmp] hdr=%d chunkID=%d timeMS=%d size=%d tagType=%d streamID=%d\", hdrType, chunkID, ch.timeMS, ch.dataSize, ch.tagType, ch.streamID)\n\n\t// 1. Response zero size\n\tif ch.dataSize == 0 {\n\t\treturn ch.tagType, ch.timeMS, nil, nil\n\t}\n\n\tdata := make([]byte, ch.dataSize)\n\n\t// 2. Response small packet\n\tif ch.dataSize <= c.rdPacketSize {\n\t\tif _, err = io.ReadFull(c.rd, data); err != nil {\n\t\t\treturn 0, 0, nil, err\n\t\t}\n\t\treturn ch.tagType, ch.timeMS, data, nil\n\t}\n\n\t// 3. Response big packet\n\tvar i0 uint32\n\tfor i1 := c.rdPacketSize; i1 < ch.dataSize; i1 += c.rdPacketSize {\n\t\tif _, err = io.ReadFull(c.rd, data[i0:i1]); err != nil {\n\t\t\treturn 0, 0, nil, err\n\t\t}\n\n\t\t// hopefully this will be hdrType=3 with same chunkID\n\t\tif _, err = c.readSize(1); err != nil {\n\t\t\treturn 0, 0, nil, err\n\t\t}\n\n\t\t_ = ch.readExtendedTime()\n\n\t\ti0 = i1\n\t}\n\n\tif _, err = io.ReadFull(c.rd, data[i0:]); err != nil {\n\t\treturn 0, 0, nil, err\n\t}\n\n\treturn ch.tagType, ch.timeMS, data, nil\n}\n\nfunc (c *Conn) writeMessage(chunkID, tagType byte, timeMS uint32, payload []byte) error {\n\tc.mu.Lock()\n\tc.resetBuffer()\n\n\tb := payload\n\tsize := uint32(len(b))\n\n\tif size > c.wrPacketSize {\n\t\tc.appendType0(chunkID, tagType, timeMS, size, b[:c.wrPacketSize])\n\n\t\tfor {\n\t\t\tb = b[c.wrPacketSize:]\n\t\t\tif uint32(len(b)) > c.wrPacketSize {\n\t\t\t\tc.appendType3(chunkID, b[:c.wrPacketSize])\n\t\t\t} else {\n\t\t\t\tc.appendType3(chunkID, b)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t} else {\n\t\tc.appendType0(chunkID, tagType, timeMS, size, b)\n\t}\n\n\t//log.Printf(\"%d %2d %5d %6d %.32x\", chunkID, tagType, timeMS, size, payload)\n\n\t_, err := c.wr.Write(c.wrBuf)\n\tc.mu.Unlock()\n\treturn err\n}\n\nfunc (c *Conn) resetBuffer() {\n\tc.wrBuf = c.wrBuf[:0]\n}\n\nfunc (c *Conn) appendType0(chunkID, tagType byte, timeMS, size uint32, payload []byte) {\n\t// TODO: timeMS more than 24 bit\n\tc.wrBuf = append(c.wrBuf,\n\t\tchunkID,\n\t\tbyte(timeMS>>16), byte(timeMS>>8), byte(timeMS),\n\t\tbyte(size>>16), byte(size>>8), byte(size),\n\t\ttagType,\n\t\tc.streamID, 0, 0, 0, // little endian streamID\n\t)\n\tc.wrBuf = append(c.wrBuf, payload...)\n}\n\nfunc (c *Conn) appendType3(chunkID byte, payload []byte) {\n\tc.wrBuf = append(c.wrBuf, 3<<6|chunkID)\n\tc.wrBuf = append(c.wrBuf, payload...)\n}\n\nfunc (c *Conn) writePacketSize() error {\n\tb := binary.BigEndian.AppendUint32(nil, c.wrPacketSize)\n\treturn c.writeMessage(2, TypeSetPacketSize, 0, b)\n}\n\nfunc (c *Conn) writeConnect() error {\n\tb := amf.EncodeItems(\"connect\", 1, map[string]any{\n\t\t\"app\":      c.App,\n\t\t\"flashVer\": \"FMLE/3.0 (compatible; FMSc/1.0)\",\n\t\t\"tcUrl\":    c.url,\n\t})\n\tif err := c.writeMessage(3, TypeCommand, 0, b); err != nil {\n\t\treturn err\n\t}\n\n\tv, err := c.readResponse(func(items []any) bool {\n\t\treturn len(items) >= 3 && items[0] == \"_result\" && items[1] == float64(1)\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tcode := getString(v, 3, \"code\")\n\tif code != \"NetConnection.Connect.Success\" {\n\t\treturn fmt.Errorf(\"rtmp: wrong response %#v\", v)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Conn) writeReleaseStream() error {\n\tb := amf.EncodeItems(\"releaseStream\", 2, nil, c.Stream)\n\tif err := c.writeMessage(3, TypeCommand, 0, b); err != nil {\n\t\treturn err\n\t}\n\tb = amf.EncodeItems(\"FCPublish\", 3, nil, c.Stream)\n\tif err := c.writeMessage(3, TypeCommand, 0, b); err != nil {\n\t\treturn err\n\t}\n\treturn nil\n}\nfunc (c *Conn) writeCreateStream() error {\n\tb := amf.EncodeItems(\"createStream\", 4, nil)\n\tif err := c.writeMessage(3, TypeCommand, 0, b); err != nil {\n\t\treturn err\n\t}\n\n\tv, err := c.readResponse(func(items []any) bool {\n\t\treturn len(items) >= 3 && items[0] == \"_result\" && items[1] == float64(4)\n\t})\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif len(v) == 4 {\n\t\tif f, ok := v[3].(float64); ok {\n\t\t\tc.streamID = byte(f)\n\t\t\treturn nil\n\t\t}\n\t}\n\n\treturn fmt.Errorf(\"rtmp: wrong response %#v\", v)\n}\n\nfunc (c *Conn) writePublish() error {\n\tb := amf.EncodeItems(\"publish\", 5, nil, c.Stream, \"live\")\n\tif err := c.writeMessage(3, TypeCommand, 0, b); err != nil {\n\t\treturn err\n\t}\n\n\t// YouTube can response with \"onBWDone 0\"\n\tv, err := c.readResponse(func(items []any) bool {\n\t\treturn len(items) >= 3 && items[0] == \"onStatus\"\n\t})\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tcode := getString(v, 3, \"code\")\n\tif code != \"NetStream.Publish.Start\" {\n\t\treturn fmt.Errorf(\"rtmp: wrong response %#v\", v)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Conn) writePlay() error {\n\tb := amf.EncodeItems(\"play\", 5, nil, c.Stream)\n\tif err := c.writeMessage(3, TypeCommand, 0, b); err != nil {\n\t\treturn err\n\t}\n\n\t// Reolink response with ID=0, other software respose with ID=5\n\tv, err := c.readResponse(func(items []any) bool {\n\t\treturn len(items) >= 3 && items[0] == \"onStatus\"\n\t})\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tcode := getString(v, 3, \"code\")\n\tif !strings.HasPrefix(code, \"NetStream.Play.\") {\n\t\treturn fmt.Errorf(\"rtmp: wrong response %#v\", v)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Conn) readSize(n uint32) ([]byte, error) {\n\tb := make([]byte, n)\n\tif _, err := io.ReadFull(c.rd, b); err != nil {\n\t\treturn nil, err\n\t}\n\treturn b, nil\n}\n\nfunc PutUint24(b []byte, v uint32) {\n\t_ = b[2]\n\tb[0] = byte(v >> 16)\n\tb[1] = byte(v >> 8)\n\tb[2] = byte(v)\n}\n\nfunc Uint24(b []byte) uint32 {\n\t_ = b[2]\n\treturn uint32(b[0])<<16 | uint32(b[1])<<8 | uint32(b[2])\n}\n\nfunc getString(v []any, i int, key string) string {\n\tif len(v) <= i {\n\t\treturn \"\"\n\t}\n\tif v, ok := v[i].(map[string]any); ok {\n\t\tif s, ok := v[key].(string); ok {\n\t\t\treturn s\n\t\t}\n\t}\n\treturn \"\"\n}\n"
  },
  {
    "path": "pkg/rtmp/flv.go",
    "content": "package rtmp\n\nimport (\n\t\"github.com/AlexxIT/go2rtc/pkg/flv\"\n)\n\nfunc (c *Conn) Producer() (*flv.Producer, error) {\n\tc.rdBuf = []byte{\n\t\t'F', 'L', 'V', // signature\n\t\t1,          // version\n\t\t0,          // flags (has video/audio)\n\t\t0, 0, 0, 9, // header size\n\t}\n\n\tprod, err := flv.Open(c)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tprod.FormatName = \"rtmp\"\n\tprod.Protocol = \"rtmp\"\n\tprod.RemoteAddr = c.conn.RemoteAddr().String()\n\tprod.URL = c.url\n\n\treturn prod, nil\n}\n\n// Read - convert RTMP to FLV format\nfunc (c *Conn) Read(p []byte) (n int, err error) {\n\t// 1. Check temporary tempbuffer\n\tif len(c.rdBuf) == 0 {\n\t\tmsgType, timeMS, payload, err2 := c.readMessage()\n\t\tif err2 != nil {\n\t\t\treturn 0, err2\n\t\t}\n\n\t\t// previous tag size (4 byte) + header (11 byte) + payload\n\t\tn = 4 + 11 + len(payload)\n\n\t\t// 2. Check if the message fits in the buffer\n\t\tif n <= len(p) {\n\t\t\tencodeFLV(p, msgType, timeMS, payload)\n\t\t\treturn\n\t\t}\n\n\t\t// 3. Put the message into a temporary buffer\n\t\tc.rdBuf = make([]byte, n)\n\t\tencodeFLV(c.rdBuf, msgType, timeMS, payload)\n\t}\n\n\t// 4. Send temporary buffer\n\tn = copy(p, c.rdBuf)\n\tc.rdBuf = c.rdBuf[n:]\n\treturn\n}\n\nfunc encodeFLV(b []byte, msgType byte, time uint32, payload []byte) {\n\t_ = b[4+11]\n\n\tb[0] = 0\n\tb[1] = 0\n\tb[2] = 0\n\tb[3] = 0\n\tb[4+0] = msgType\n\tPutUint24(b[4+1:], uint32(len(payload)))\n\tPutUint24(b[4+4:], time)\n\tb[4+7] = byte(time >> 24)\n\n\tcopy(b[4+11:], payload)\n}\n\n// Write - convert FLV format to RTMP format\nfunc (c *Conn) Write(p []byte) (n int, err error) {\n\tn = len(p)\n\n\tif p[0] == 'F' {\n\t\tp = p[9+4:] // skip first msg with FLV header\n\n\t\tfor len(p) > 0 {\n\t\t\tsize := 11 + uint16(p[2])<<8 + uint16(p[3]) + 4\n\t\t\tif _, err = c.Write(p[:size]); err != nil {\n\t\t\t\treturn 0, err\n\t\t\t}\n\t\t\tp = p[size:]\n\t\t}\n\t\treturn\n\t}\n\n\t// decode FLV: 11 bytes header + payload + 4 byte size\n\ttagType := p[0]\n\ttimeMS := uint32(p[4])<<16 | uint32(p[5])<<8 | uint32(p[6]) | uint32(p[7])<<24\n\tpayload := p[11 : len(p)-4]\n\n\terr = c.writeMessage(4, tagType, timeMS, payload)\n\treturn\n}\n"
  },
  {
    "path": "pkg/rtmp/server.go",
    "content": "package rtmp\n\nimport (\n\t\"bufio\"\n\t\"crypto/rand\"\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/flv/amf\"\n)\n\nfunc NewServer(conn net.Conn) (*Conn, error) {\n\tc := &Conn{\n\t\tconn: conn,\n\t\trd:   bufio.NewReaderSize(conn, core.BufferSize),\n\t\twr:   conn,\n\n\t\tchunks: map[uint8]*chunk{},\n\n\t\trdPacketSize: 128,\n\t\twrPacketSize: 4096,\n\t}\n\n\tif err := c.serverHandshake(); err != nil {\n\t\treturn nil, err\n\t}\n\tif err := c.writePacketSize(); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn c, nil\n}\n\nfunc (c *Conn) serverHandshake() error {\n\t// based on https://rtmp.veriskope.com/docs/spec/\n\t_ = c.conn.SetDeadline(time.Now().Add(core.ConnDeadline))\n\n\t// read C0\n\tb := make([]byte, 1)\n\tif _, err := io.ReadFull(c.rd, b); err != nil {\n\t\treturn err\n\t}\n\n\tif b[0] != 3 {\n\t\treturn errors.New(\"rtmp: wrong handshake\")\n\t}\n\n\t// write S0\n\tif _, err := c.conn.Write([]byte{3}); err != nil {\n\t\treturn err\n\t}\n\n\tb = make([]byte, 1536)\n\n\t// write S1\n\ttsS1 := nowMS()\n\tbinary.BigEndian.PutUint32(b, tsS1)\n\tbinary.BigEndian.PutUint32(b[4:], 0)\n\t_, _ = rand.Read(b[8:])\n\tif _, err := c.conn.Write(b); err != nil {\n\t\treturn err\n\t}\n\n\t// read C1\n\tif _, err := io.ReadFull(c.rd, b); err != nil {\n\t\treturn err\n\t}\n\n\t// write S2\n\ttsS2 := nowMS()\n\tbinary.BigEndian.PutUint32(b, tsS1)\n\tbinary.BigEndian.PutUint32(b[4:], tsS2)\n\tif _, err := c.conn.Write(b); err != nil {\n\t\treturn err\n\t}\n\n\t// read C2\n\tif _, err := io.ReadFull(c.rd, b); err != nil {\n\t\treturn err\n\t}\n\n\t_ = c.conn.SetDeadline(time.Time{})\n\treturn nil\n}\n\nfunc (c *Conn) ReadCommands() error {\n\tfor {\n\t\tmsgType, _, b, err := c.readMessage()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t//log.Printf(\"%d %.256x\", msgType, b)\n\n\t\tswitch msgType {\n\t\tcase TypeSetPacketSize:\n\t\t\tc.rdPacketSize = binary.BigEndian.Uint32(b)\n\t\tcase TypeCommand:\n\t\t\tif err = c.acceptCommand(b); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tif c.Intent != \"\" {\n\t\t\t\treturn nil\n\t\t\t}\n\t\t}\n\t}\n}\n\nconst (\n\tCommandConnect       = \"connect\"\n\tCommandReleaseStream = \"releaseStream\"\n\tCommandFCPublish     = \"FCPublish\"\n\tCommandCreateStream  = \"createStream\"\n\tCommandPublish       = \"publish\"\n\tCommandPlay          = \"play\"\n)\n\nfunc (c *Conn) acceptCommand(b []byte) error {\n\titems, err := amf.NewReader(b).ReadItems()\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\t//log.Printf(\"%#v\", items)\n\n\tif len(items) < 2 {\n\t\treturn fmt.Errorf(\"rtmp: read command %x\", b)\n\t}\n\n\tcmd, ok := items[0].(string)\n\tif !ok {\n\t\treturn fmt.Errorf(\"rtmp: read command %x\", b)\n\t}\n\n\ttID, ok := items[1].(float64) // transaction ID\n\tif !ok {\n\t\treturn fmt.Errorf(\"rtmp: read command %x\", b)\n\t}\n\n\tswitch cmd {\n\tcase CommandConnect:\n\t\tif len(items) == 3 {\n\t\t\tif v, ok := items[2].(map[string]any); ok {\n\t\t\t\tc.App, _ = v[\"app\"].(string)\n\t\t\t}\n\t\t}\n\n\t\tpayload := amf.EncodeItems(\n\t\t\t\"_result\", tID,\n\t\t\tmap[string]any{\"fmsVer\": \"FMS/3,0,1,123\"},\n\t\t\tmap[string]any{\"code\": \"NetConnection.Connect.Success\"},\n\t\t)\n\t\treturn c.writeMessage(3, TypeCommand, 0, payload)\n\n\tcase CommandReleaseStream:\n\t\t// if app is empty - will use key as app\n\t\tif c.App == \"\" && len(items) == 4 {\n\t\t\tc.App, _ = items[3].(string)\n\t\t}\n\n\t\tpayload := amf.EncodeItems(\"_result\", tID, nil)\n\t\treturn c.writeMessage(3, TypeCommand, 0, payload)\n\n\tcase CommandFCPublish: // no response\n\n\tcase CommandCreateStream:\n\t\tpayload := amf.EncodeItems(\"_result\", tID, nil, 1)\n\t\treturn c.writeMessage(3, TypeCommand, 0, payload)\n\n\tcase CommandPublish, CommandPlay: // response later\n\t\tc.Intent = cmd\n\t\tc.streamID = 1\n\n\tdefault:\n\t\tprintln(\"rtmp: unknown command: \" + cmd)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Conn) WriteStart() error {\n\tvar code string\n\tif c.Intent == CommandPublish {\n\t\tcode = \"NetStream.Publish.Start\"\n\t} else {\n\t\tcode = \"NetStream.Play.Start\"\n\t}\n\n\tpayload := amf.EncodeItems(\"onStatus\", 0, nil, map[string]any{\"code\": code})\n\treturn c.writeMessage(3, TypeCommand, 0, payload)\n}\n\nfunc nowMS() uint32 {\n\treturn uint32(time.Now().UnixNano() / int64(time.Millisecond))\n}\n"
  },
  {
    "path": "pkg/rtsp/README.md",
    "content": "## Useful links\n\n- https://www.kurento.org/blog/rtp-i-intro-rtp-and-sdp"
  },
  {
    "path": "pkg/rtsp/client.go",
    "content": "package rtsp\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/tcp/websocket\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/tcp\"\n)\n\nvar Timeout = time.Second * 5\n\nfunc NewClient(uri string) *Conn {\n\treturn &Conn{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"rtsp\",\n\t\t},\n\t\turi: uri,\n\t}\n}\n\nfunc (c *Conn) Dial() (err error) {\n\tif c.URL, err = url.Parse(c.uri); err != nil {\n\t\treturn\n\t}\n\n\tvar conn net.Conn\n\n\tswitch c.Transport {\n\tcase \"\", \"tcp\", \"udp\":\n\t\tvar timeout time.Duration\n\t\tif c.Timeout != 0 {\n\t\t\ttimeout = time.Second * time.Duration(c.Timeout)\n\t\t} else {\n\t\t\ttimeout = core.ConnDialTimeout\n\t\t}\n\t\tconn, err = tcp.Dial(c.URL, timeout)\n\n\t\tif c.Transport != \"udp\" {\n\t\t\tc.Protocol = \"rtsp+tcp\"\n\t\t} else {\n\t\t\tc.Protocol = \"rtsp+udp\"\n\t\t}\n\tdefault:\n\t\tconn, err = websocket.Dial(c.Transport)\n\t\tc.Protocol = \"ws\"\n\t}\n\tif err != nil {\n\t\treturn\n\t}\n\n\t// remove UserInfo from URL\n\tc.auth = tcp.NewAuth(c.URL.User)\n\tc.URL.User = nil\n\n\tc.conn = conn\n\tc.reader = bufio.NewReaderSize(conn, core.BufferSize)\n\tc.session = \"\"\n\tc.sequence = 0\n\tc.state = StateConn\n\n\tc.udpConn = nil\n\tc.udpAddr = nil\n\n\tc.Connection.RemoteAddr = conn.RemoteAddr().String()\n\tc.Connection.Transport = conn\n\tc.Connection.URL = c.uri\n\n\treturn nil\n}\n\n// Do send WriteRequest and receive and process WriteResponse\nfunc (c *Conn) Do(req *tcp.Request) (*tcp.Response, error) {\n\tif err := c.WriteRequest(req); err != nil {\n\t\treturn nil, err\n\t}\n\n\tres, err := c.ReadResponse()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tc.Fire(res)\n\n\tswitch res.StatusCode {\n\tcase http.StatusOK:\n\t\treturn res, nil\n\n\tcase http.StatusMovedPermanently, http.StatusFound:\n\t\trawURL := res.Header.Get(\"Location\")\n\n\t\tvar u *url.URL\n\t\tif u, err = url.Parse(rawURL); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif u.User == nil {\n\t\t\tu.User = c.auth.UserInfo() // restore auth if we don't have it in the new URL\n\t\t}\n\n\t\tc.uri = u.String() // so auth will be saved on reconnect\n\n\t\t_ = c.conn.Close()\n\n\t\tif err = c.Dial(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treq.URL = c.URL // because path was changed\n\n\t\treturn c.Do(req)\n\n\tcase http.StatusUnauthorized:\n\t\tswitch c.auth.Method {\n\t\tcase tcp.AuthNone:\n\t\t\tif c.auth.ReadNone(res) {\n\t\t\t\treturn c.Do(req)\n\t\t\t}\n\t\t\treturn nil, errors.New(\"user/pass not provided\")\n\t\tcase tcp.AuthUnknown:\n\t\t\tif c.auth.Read(res) {\n\t\t\t\treturn c.Do(req)\n\t\t\t}\n\t\tdefault:\n\t\t\treturn nil, errors.New(\"wrong user/pass\")\n\t\t}\n\t}\n\n\treturn res, fmt.Errorf(\"wrong response on %s\", req.Method)\n}\n\nfunc (c *Conn) Options() error {\n\treq := &tcp.Request{Method: MethodOptions, URL: c.URL}\n\n\tres, err := c.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif val := res.Header.Get(\"Content-Base\"); val != \"\" {\n\t\tc.URL, err = urlParse(val)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (c *Conn) Describe() error {\n\t// 5.3 Back channel connection\n\t// https://www.onvif.org/specs/stream/ONVIF-Streaming-Spec.pdf\n\treq := &tcp.Request{\n\t\tMethod: MethodDescribe,\n\t\tURL:    c.URL,\n\t\tHeader: map[string][]string{\n\t\t\t\"Accept\": {\"application/sdp\"},\n\t\t},\n\t}\n\n\tif c.Backchannel {\n\t\treq.Header.Set(\"Require\", \"www.onvif.org/ver20/backchannel\")\n\t}\n\n\tif c.UserAgent != \"\" {\n\t\t// this camera will answer with 401 on DESCRIBE without User-Agent\n\t\t// https://github.com/AlexxIT/go2rtc/issues/235\n\t\treq.Header.Set(\"User-Agent\", c.UserAgent)\n\t}\n\n\tres, err := c.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif val := res.Header.Get(\"Content-Base\"); val != \"\" {\n\t\tc.URL, err = urlParse(val)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tc.SDP = string(res.Body) // for info\n\n\tmedias, err := UnmarshalSDP(res.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif c.Media != \"\" {\n\t\tclone := make([]*core.Media, 0, len(medias))\n\t\tfor _, media := range medias {\n\t\t\tif strings.Contains(c.Media, media.Kind) {\n\t\t\t\tclone = append(clone, media)\n\t\t\t}\n\t\t}\n\t\tmedias = clone\n\t}\n\n\t// TODO: rewrite more smart\n\tif c.Medias == nil {\n\t\tc.Medias = medias\n\t} else if len(c.Medias) > len(medias) {\n\t\tc.Medias = c.Medias[:len(medias)]\n\t}\n\n\tc.mode = core.ModeActiveProducer\n\n\treturn nil\n}\n\nfunc (c *Conn) Announce() (err error) {\n\treq := &tcp.Request{\n\t\tMethod: MethodAnnounce,\n\t\tURL:    c.URL,\n\t\tHeader: map[string][]string{\n\t\t\t\"Content-Type\": {\"application/sdp\"},\n\t\t},\n\t}\n\n\treq.Body, err = core.MarshalSDP(c.SessionName, c.Medias)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t_, err = c.Do(req)\n\treturn\n}\n\nfunc (c *Conn) Record() (err error) {\n\treq := &tcp.Request{\n\t\tMethod: MethodRecord,\n\t\tURL:    c.URL,\n\t\tHeader: map[string][]string{\n\t\t\t\"Range\": {\"npt=0.000-\"},\n\t\t},\n\t}\n\n\t_, err = c.Do(req)\n\treturn\n}\n\nfunc (c *Conn) SetupMedia(media *core.Media) (byte, error) {\n\tvar transport string\n\n\tif c.Transport == \"udp\" {\n\t\tconn1, conn2, err := ListenUDPPair()\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\n\t\tc.udpConn = append(c.udpConn, conn1, conn2)\n\n\t\tport := conn1.LocalAddr().(*net.UDPAddr).Port\n\t\ttransport = fmt.Sprintf(\"RTP/AVP;unicast;client_port=%d-%d\", port, port+1)\n\t} else {\n\t\t// try to use media position as channel number\n\t\tfor i, m := range c.Medias {\n\t\t\tif m.Equal(media) {\n\t\t\t\ttransport = fmt.Sprintf(\n\t\t\t\t\t// i   - RTP (data channel)\n\t\t\t\t\t// i+1 - RTCP (control channel)\n\t\t\t\t\t\"RTP/AVP/TCP;unicast;interleaved=%d-%d\", i*2, i*2+1,\n\t\t\t\t)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\tif transport == \"\" {\n\t\treturn 0, fmt.Errorf(\"wrong media: %v\", media)\n\t}\n\n\trawURL := media.ID // control\n\tif !strings.Contains(rawURL, \"://\") {\n\t\trawURL = c.URL.String()\n\t\t// prefix check for https://github.com/AlexxIT/go2rtc/issues/1236\n\t\tif !strings.HasSuffix(rawURL, \"/\") && !strings.HasPrefix(media.ID, \"/\") {\n\t\t\trawURL += \"/\"\n\t\t}\n\t\trawURL += media.ID\n\t}\n\ttrackURL, err := urlParse(rawURL)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\treq := &tcp.Request{\n\t\tMethod: MethodSetup,\n\t\tURL:    trackURL,\n\t\tHeader: map[string][]string{\n\t\t\t\"Transport\": {transport},\n\t\t},\n\t}\n\n\tres, err := c.Do(req)\n\tif err != nil {\n\t\t// some Dahua/Amcrest cameras fail here because two simultaneous\n\t\t// backchannel connections\n\t\tif c.Backchannel {\n\t\t\tc.Backchannel = false\n\t\t\tif err = c.Reconnect(); err != nil {\n\t\t\t\treturn 0, err\n\t\t\t}\n\t\t\treturn c.SetupMedia(media)\n\t\t}\n\n\t\treturn 0, err\n\t}\n\n\tif c.session == \"\" {\n\t\t// Session: 7116520596809429228\n\t\t// Session: 216525287999;timeout=60\n\t\tif s := res.Header.Get(\"Session\"); s != \"\" {\n\t\t\tif i := strings.IndexByte(s, ';'); i > 0 {\n\t\t\t\tc.session = s[:i]\n\t\t\t\tif i = strings.Index(s, \"timeout=\"); i > 0 {\n\t\t\t\t\tc.keepalive, _ = strconv.Atoi(s[i+8:])\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tc.session = s\n\t\t\t}\n\t\t}\n\t}\n\n\t// Parse server response\n\ttransport = res.Header.Get(\"Transport\")\n\n\tif c.Transport == \"udp\" {\n\t\tchannel := byte(len(c.udpConn) - 2)\n\n\t\t// Dahua:   RTP/AVP/UDP;unicast;client_port=49292-49293;server_port=43670-43671;ssrc=7CB694B4\n\t\t// OpenIPC: RTP/AVP/UDP;unicast;client_port=59612-59613\n\t\tif s := core.Between(transport, \"server_port=\", \";\"); s != \"\" {\n\t\t\ts1, s2, _ := strings.Cut(s, \"-\")\n\t\t\tport1 := core.Atoi(s1)\n\t\t\tport2 := core.Atoi(s2)\n\t\t\t// TODO: more smart handling empty server ports\n\t\t\tif port1 > 0 && port2 > 0 {\n\t\t\t\tremoteIP := c.conn.RemoteAddr().(*net.TCPAddr).IP\n\t\t\t\tc.udpAddr = append(c.udpAddr,\n\t\t\t\t\t&net.UDPAddr{IP: remoteIP, Port: port1},\n\t\t\t\t\t&net.UDPAddr{IP: remoteIP, Port: port2},\n\t\t\t\t)\n\n\t\t\t\tgo func() {\n\t\t\t\t\t// Try to open a hole in the NAT router (to allow incoming UDP packets)\n\t\t\t\t\t// by send a UDP packet for RTP and RTCP to the remote RTSP server.\n\t\t\t\t\t// https://github.com/FFmpeg/FFmpeg/blob/aa91ae25b88e195e6af4248e0ab30605735ca1cd/libavformat/rtpdec.c#L416-L438\n\t\t\t\t\t_, _ = c.WriteToUDP([]byte{0x80, 0x00, 0x00, 0x00}, channel)\n\t\t\t\t\t_, _ = c.WriteToUDP([]byte{0x80, 0xC8, 0x00, 0x01}, channel+1)\n\t\t\t\t}()\n\t\t\t}\n\t\t}\n\n\t\treturn channel, nil\n\t} else {\n\t\t// we send our `interleaved`, but camera can answer with another\n\n\t\t// Transport: RTP/AVP/TCP;unicast;interleaved=10-11;ssrc=10117CB7\n\t\t// Transport: RTP/AVP/TCP;unicast;destination=192.168.1.111;source=192.168.1.222;interleaved=0\n\t\t// Transport: RTP/AVP/TCP;ssrc=22345682;interleaved=0-1\n\t\t// Escam Q6 has a bug:\n\t\t// Transport: RTP/AVP;unicast;destination=192.168.1.111;source=192.168.1.222;interleaved=0-1\n\t\ts := core.Between(transport, \"interleaved=\", \"-\")\n\t\ti, err := strconv.Atoi(s)\n\t\tif err != nil {\n\t\t\treturn 0, fmt.Errorf(\"wrong transport: %s\", transport)\n\t\t}\n\n\t\treturn byte(i), nil\n\t}\n}\n\nfunc (c *Conn) Play() (err error) {\n\treq := &tcp.Request{Method: MethodPlay, URL: c.URL}\n\treturn c.WriteRequest(req)\n}\n\nfunc (c *Conn) Teardown() (err error) {\n\t// allow TEARDOWN from any state (ex. ANNOUNCE > SETUP)\n\treq := &tcp.Request{Method: MethodTeardown, URL: c.URL}\n\treturn c.WriteRequest(req)\n}\n\nfunc (c *Conn) Close() error {\n\tif c.mode == core.ModeActiveProducer {\n\t\t_ = c.Teardown()\n\t}\n\tif c.OnClose != nil {\n\t\t_ = c.OnClose()\n\t}\n\tfor _, conn := range c.udpConn {\n\t\t_ = conn.Close()\n\t}\n\treturn c.conn.Close()\n}\n\nfunc (c *Conn) WriteToUDP(b []byte, channel byte) (int, error) {\n\treturn c.udpConn[channel].WriteToUDP(b, c.udpAddr[channel])\n}\n\nconst listenUDPAttemps = 10\n\nvar listenUDPMu sync.Mutex\n\nfunc ListenUDPPair() (*net.UDPConn, *net.UDPConn, error) {\n\tlistenUDPMu.Lock()\n\tdefer listenUDPMu.Unlock()\n\n\tfor i := 0; i < listenUDPAttemps; i++ {\n\t\t// Get a random even port from the OS\n\t\tln1, err := net.ListenUDP(\"udp\", &net.UDPAddr{IP: nil, Port: 0})\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar port1 = ln1.LocalAddr().(*net.UDPAddr).Port\n\t\tvar port2 int\n\n\t\t// 11. RTP over Network and Transport Protocols (https://www.ietf.org/rfc/rfc3550.txt)\n\t\t// For UDP and similar protocols,\n\t\t// RTP SHOULD use an even destination port number and the corresponding\n\t\t// RTCP stream SHOULD use the next higher (odd) destination port number\n\t\tif port1&1 > 0 {\n\t\t\tport2 = port1 - 1\n\t\t} else {\n\t\t\tport2 = port1 + 1\n\t\t}\n\n\t\tln2, err := net.ListenUDP(\"udp\", &net.UDPAddr{IP: nil, Port: port2})\n\t\tif err != nil {\n\t\t\t_ = ln1.Close()\n\t\t\tcontinue\n\t\t}\n\n\t\tif port1 < port2 {\n\t\t\treturn ln1, ln2, nil\n\t\t} else {\n\t\t\treturn ln2, ln1, nil\n\t\t}\n\t}\n\n\treturn nil, nil, fmt.Errorf(\"can't open two UDP ports\")\n}\n"
  },
  {
    "path": "pkg/rtsp/client_test.go",
    "content": "package rtsp\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 TestTimeout(t *testing.T) {\n\tTimeout = time.Millisecond\n\n\tln, err := net.Listen(\"tcp\", \"localhost:0\")\n\trequire.Nil(t, err)\n\n\tclient := NewClient(\"rtsp://\" + ln.Addr().String() + \"/stream\")\n\tclient.Backchannel = true\n\n\terr = client.Dial()\n\trequire.Nil(t, err)\n\n\terr = client.Describe()\n\trequire.ErrorIs(t, err, os.ErrDeadlineExceeded)\n}\n\nfunc TestMissedControl(t *testing.T) {\n\tTimeout = time.Millisecond\n\n\tln, err := net.Listen(\"tcp\", \"localhost:0\")\n\trequire.Nil(t, err)\n\n\tgo func() {\n\t\tconn, err := ln.Accept()\n\t\trequire.Nil(t, err)\n\n\t\tb := make([]byte, 8192)\n\t\tfor {\n\t\t\tn, err := conn.Read(b)\n\t\t\trequire.Nil(t, err)\n\n\t\t\treq := string(b[:n])\n\n\t\t\tswitch req[:4] {\n\t\t\tcase \"DESC\":\n\t\t\t\t_, _ = conn.Write([]byte(`RTSP/1.0 200 OK\nCseq: 1\nContent-Length: 495\nContent-Type: application/sdp\n\nv=0\no=- 1 1 IN IP4 0.0.0.0\ns=go2rtc/1.2.0\nc=IN IP4 0.0.0.0\nt=0 0\nm=audio 0 RTP/AVP 96\na=rtpmap:96 MPEG4-GENERIC/48000/2\na=fmtp:96 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3; config=119056E500\nm=audio 0 RTP/AVP 97\na=rtpmap:97 OPUS/48000/2\na=fmtp:97 sprop-stereo=1\nm=video 0 RTP/AVP 98\na=rtpmap:98 H264/90000\na=fmtp:98 packetization-mode=1; sprop-parameter-sets=Z2QAKaw0yAeAIn5cBagICAoAAAfQAAE4gdDAAjhAACOEF3lxoYAEcIAARwgu8uFA,aO48MAA=; profile-level-id=640029\n`))\n\n\t\t\tcase \"SETU\":\n\t\t\t\t_, _ = conn.Write([]byte(`RTSP/1.0 200 OK\nTransport: RTP/AVP/TCP;unicast;interleaved=4-5\nCseq: 3\nSession: 1\n\n`))\n\n\t\t\tdefault:\n\t\t\t\tt.Fail()\n\t\t\t}\n\t\t}\n\t}()\n\n\tclient := NewClient(\"rtsp://\" + ln.Addr().String() + \"/stream\")\n\tclient.Backchannel = true\n\n\terr = client.Dial()\n\trequire.Nil(t, err)\n\n\terr = client.Describe()\n\trequire.Nil(t, err)\n\trequire.Len(t, client.Medias, 3)\n\n\tch, err := client.SetupMedia(client.Medias[2])\n\trequire.Nil(t, err)\n\trequire.Equal(t, ch, byte(4))\n}\n"
  },
  {
    "path": "pkg/rtsp/conn.go",
    "content": "package rtsp\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/tcp\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Conn struct {\n\tcore.Connection\n\tcore.Listener\n\n\t// public\n\n\tBackchannel bool\n\tMedia       string\n\tOnClose     func() error\n\tPacketSize  uint16\n\tSessionName string\n\tTimeout     int\n\tTransport   string // custom transport support, ex. RTSP over WebSocket\n\n\tURL *url.URL\n\n\t// internal\n\n\tauth      *tcp.Auth\n\tconn      net.Conn\n\tkeepalive int\n\tmode      core.Mode\n\tplayOK    bool\n\tplayErr   error\n\treader    *bufio.Reader\n\tsequence  int\n\tsession   string\n\turi       string\n\n\tstate   State\n\tstateMu sync.Mutex\n\n\tudpConn []*net.UDPConn\n\tudpAddr []*net.UDPAddr\n}\n\nconst (\n\tProtoRTSP      = \"RTSP/1.0\"\n\tMethodOptions  = \"OPTIONS\"\n\tMethodSetup    = \"SETUP\"\n\tMethodTeardown = \"TEARDOWN\"\n\tMethodDescribe = \"DESCRIBE\"\n\tMethodPlay     = \"PLAY\"\n\tMethodPause    = \"PAUSE\"\n\tMethodAnnounce = \"ANNOUNCE\"\n\tMethodRecord   = \"RECORD\"\n)\n\ntype State byte\n\nfunc (s State) String() string {\n\tswitch s {\n\tcase StateNone:\n\t\treturn \"NONE\"\n\tcase StateConn:\n\t\treturn \"CONN\"\n\tcase StateSetup:\n\t\treturn MethodSetup\n\tcase StatePlay:\n\t\treturn MethodPlay\n\t}\n\treturn strconv.Itoa(int(s))\n}\n\nconst (\n\tStateNone State = iota\n\tStateConn\n\tStateSetup\n\tStatePlay\n)\n\nfunc (c *Conn) Handle() (err error) {\n\tvar timeout time.Duration\n\n\tswitch c.mode {\n\tcase core.ModeActiveProducer:\n\t\tvar keepaliveDT time.Duration\n\n\t\tif c.keepalive > 5 {\n\t\t\tkeepaliveDT = time.Duration(c.keepalive-5) * time.Second\n\t\t} else {\n\t\t\tkeepaliveDT = 25 * time.Second\n\t\t}\n\n\t\tctx, cancel := context.WithCancel(context.Background())\n\t\tgo c.handleKeepalive(ctx, keepaliveDT)\n\t\tdefer cancel()\n\n\t\tif c.Timeout == 0 {\n\t\t\t// polling frames from remote RTSP Server (ex Camera)\n\t\t\ttimeout = time.Second * 5\n\n\t\t\tif len(c.Receivers) == 0 || c.Transport == \"udp\" {\n\t\t\t\t// if we only send audio to camera\n\t\t\t\t// https://github.com/AlexxIT/go2rtc/issues/659\n\t\t\t\ttimeout += keepaliveDT\n\t\t\t}\n\t\t} else {\n\t\t\ttimeout = time.Second * time.Duration(c.Timeout)\n\t\t}\n\n\tcase core.ModePassiveProducer:\n\t\t// polling frames from remote RTSP Client (ex FFmpeg)\n\t\tif c.Timeout == 0 {\n\t\t\ttimeout = time.Second * 15\n\t\t} else {\n\t\t\ttimeout = time.Second * time.Duration(c.Timeout)\n\t\t}\n\n\tcase core.ModePassiveConsumer:\n\t\t// pushing frames to remote RTSP Client (ex VLC)\n\t\ttimeout = time.Second * 60\n\n\tdefault:\n\t\treturn fmt.Errorf(\"wrong RTSP conn mode: %d\", c.mode)\n\t}\n\n\tfor i := 0; i < len(c.udpConn); i++ {\n\t\tgo c.handleUDPData(byte(i))\n\t}\n\n\tfor c.state != StateNone {\n\t\tts := time.Now()\n\n\t\t_ = c.conn.SetReadDeadline(ts.Add(timeout))\n\n\t\tif err = c.handleTCPData(); err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\n\treturn\n}\n\nfunc (c *Conn) handleKeepalive(ctx context.Context, d time.Duration) {\n\tticker := time.NewTicker(d)\n\tfor {\n\t\tselect {\n\t\tcase <-ticker.C:\n\t\t\treq := &tcp.Request{Method: MethodOptions, URL: c.URL}\n\t\t\tif err := c.WriteRequest(req); err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\tcase <-ctx.Done():\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (c *Conn) handleUDPData(channel byte) {\n\t// TODO: handle timeouts and drop TCP connection after any error\n\tconn := c.udpConn[channel]\n\n\tfor {\n\t\t// TP-Link Tapo camera has crazy 10000 bytes packet size\n\t\tbuf := make([]byte, 10240)\n\n\t\tn, _, err := conn.ReadFromUDP(buf)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\n\t\tif err = c.handleRawPacket(channel, buf[:n]); err != nil {\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (c *Conn) handleTCPData() error {\n\t// we can read:\n\t// 1. RTP interleaved: `$` + 1B channel number + 2B size\n\t// 2. RTSP response:   RTSP/1.0 200 OK\n\t// 3. RTSP request:    OPTIONS ...\n\tvar buf4 []byte // `$` + 1B channel number + 2B size\n\tvar err error\n\n\tbuf4, err = c.reader.Peek(4)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar channel byte\n\tvar size uint16\n\n\tif buf4[0] != '$' {\n\t\tswitch string(buf4) {\n\t\tcase \"RTSP\":\n\t\t\tvar res *tcp.Response\n\t\t\tif res, err = c.ReadResponse(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tc.Fire(res)\n\t\t\t// for playing backchannel only after OK response on play\n\t\t\tc.playOK = true\n\t\t\treturn nil\n\n\t\tcase \"OPTI\", \"TEAR\", \"DESC\", \"SETU\", \"PLAY\", \"PAUS\", \"RECO\", \"ANNO\", \"GET_\", \"SET_\":\n\t\t\tvar req *tcp.Request\n\t\t\tif req, err = c.ReadRequest(); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tc.Fire(req)\n\t\t\tif req.Method == MethodOptions {\n\t\t\t\tres := &tcp.Response{Request: req}\n\t\t\t\tif err = c.WriteResponse(res); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn nil\n\n\t\tdefault:\n\t\t\tc.Fire(\"RTSP wrong input\")\n\n\t\t\tfor i := 0; ; i++ {\n\t\t\t\t// search next start symbol\n\t\t\t\tif _, err = c.reader.ReadBytes('$'); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\tif channel, err = c.reader.ReadByte(); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\t// TODO: better check maximum good channel ID\n\t\t\t\tif channel >= 20 {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\tbuf4 = make([]byte, 2)\n\t\t\t\tif _, err = io.ReadFull(c.reader, buf4); err != nil {\n\t\t\t\t\treturn err\n\t\t\t\t}\n\n\t\t\t\t// check if size good for RTP\n\t\t\t\tsize = binary.BigEndian.Uint16(buf4)\n\t\t\t\tif size <= 1500 {\n\t\t\t\t\tbreak\n\t\t\t\t}\n\n\t\t\t\t// 10 tries to find good packet\n\t\t\t\tif i >= 10 {\n\t\t\t\t\treturn fmt.Errorf(\"RTSP wrong input\")\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// hope that the odd channels are always RTCP\n\t\tchannel = buf4[1]\n\n\t\t// get data size\n\t\tsize = binary.BigEndian.Uint16(buf4[2:])\n\n\t\t// skip 4 bytes from c.reader.Peek\n\t\tif _, err = c.reader.Discard(4); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\t// init memory for data\n\tbuf := make([]byte, size)\n\tif _, err = io.ReadFull(c.reader, buf); err != nil {\n\t\treturn err\n\t}\n\n\tc.Recv += int(size)\n\n\treturn c.handleRawPacket(channel, buf)\n}\n\nfunc (c *Conn) handleRawPacket(channel byte, buf []byte) error {\n\tif channel&1 == 0 {\n\t\tpacket := &rtp.Packet{}\n\t\tif err := packet.Unmarshal(buf); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tfor _, receiver := range c.Receivers {\n\t\t\tif receiver.ID == channel {\n\t\t\t\treceiver.WriteRTP(packet)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t} else {\n\t\tmsg := &RTCP{Channel: channel}\n\n\t\tif err := msg.Header.Unmarshal(buf); err != nil {\n\t\t\treturn nil\n\t\t}\n\n\t\t//var err error\n\t\t//msg.Packets, err = rtcp.Unmarshal(buf)\n\t\t//if err != nil {\n\t\t//\treturn nil\n\t\t//}\n\n\t\tc.Fire(msg)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Conn) WriteRequest(req *tcp.Request) error {\n\tif req.Proto == \"\" {\n\t\treq.Proto = ProtoRTSP\n\t}\n\n\tif req.Header == nil {\n\t\treq.Header = make(map[string][]string)\n\t}\n\n\tc.sequence++\n\t// important to send case sensitive CSeq\n\t// https://github.com/AlexxIT/go2rtc/issues/7\n\treq.Header[\"CSeq\"] = []string{strconv.Itoa(c.sequence)}\n\n\tc.auth.Write(req)\n\n\tif c.session != \"\" {\n\t\treq.Header.Set(\"Session\", c.session)\n\t}\n\n\tif req.Body != nil {\n\t\tval := strconv.Itoa(len(req.Body))\n\t\treq.Header.Set(\"Content-Length\", val)\n\t}\n\n\tc.Fire(req)\n\n\tif err := c.conn.SetWriteDeadline(time.Now().Add(Timeout)); err != nil {\n\t\treturn err\n\t}\n\n\treturn req.Write(c.conn)\n}\n\nfunc (c *Conn) ReadRequest() (*tcp.Request, error) {\n\tif err := c.conn.SetReadDeadline(time.Now().Add(Timeout)); err != nil {\n\t\treturn nil, err\n\t}\n\treturn tcp.ReadRequest(c.reader)\n}\n\nfunc (c *Conn) WriteResponse(res *tcp.Response) error {\n\tif res.Proto == \"\" {\n\t\tres.Proto = ProtoRTSP\n\t}\n\n\tif res.Status == \"\" {\n\t\tres.Status = \"200 OK\"\n\t}\n\n\tif res.Header == nil {\n\t\tres.Header = make(map[string][]string)\n\t}\n\n\tif res.Request != nil && res.Request.Header != nil {\n\t\tseq := res.Request.Header.Get(\"CSeq\")\n\t\tif seq != \"\" {\n\t\t\tres.Header.Set(\"CSeq\", seq)\n\t\t}\n\t}\n\n\tif c.session != \"\" {\n\t\tif res.Request != nil && res.Request.Method == MethodSetup {\n\t\t\tres.Header.Set(\"Session\", c.session+\";timeout=60\")\n\t\t} else {\n\t\t\tres.Header.Set(\"Session\", c.session)\n\t\t}\n\t}\n\n\tif res.Body != nil {\n\t\tval := strconv.Itoa(len(res.Body))\n\t\tres.Header.Set(\"Content-Length\", val)\n\t}\n\n\tc.Fire(res)\n\n\tif err := c.conn.SetWriteDeadline(time.Now().Add(Timeout)); err != nil {\n\t\treturn err\n\t}\n\n\treturn res.Write(c.conn)\n}\n\nfunc (c *Conn) ReadResponse() (*tcp.Response, error) {\n\tif err := c.conn.SetReadDeadline(time.Now().Add(Timeout)); err != nil {\n\t\treturn nil, err\n\t}\n\treturn tcp.ReadResponse(c.reader)\n}\n"
  },
  {
    "path": "pkg/rtsp/consumer.go",
    "content": "package rtsp\n\nimport (\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/aac\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h265\"\n\t\"github.com/AlexxIT/go2rtc/pkg/mjpeg\"\n\t\"github.com/AlexxIT/go2rtc/pkg/pcm\"\n\t\"github.com/pion/rtp\"\n)\n\nfunc (c *Conn) GetMedias() []*core.Media {\n\t//core.Assert(c.Medias != nil)\n\treturn c.Medias\n}\n\nfunc (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) (err error) {\n\tvar channel byte\n\n\tswitch c.mode {\n\tcase core.ModeActiveProducer: // backchannel\n\t\tc.stateMu.Lock()\n\t\tdefer c.stateMu.Unlock()\n\n\t\tif c.state == StatePlay {\n\t\t\tif err = c.Reconnect(); err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\n\t\tif channel, err = c.SetupMedia(media); err != nil {\n\t\t\treturn\n\t\t}\n\n\t\tc.state = StateSetup\n\n\tcase core.ModePassiveConsumer:\n\t\tchannel = byte(len(c.Senders)) * 2\n\n\t\t// for consumer is better to use original track codec\n\t\tcodec = track.Codec.Clone()\n\t\t// generate new payload type, starting from 96\n\t\tcodec.PayloadType = byte(96 + len(c.Senders))\n\n\tdefault:\n\t\tpanic(core.Caller())\n\t}\n\n\t// save original codec to sender (can have Codec.Name = ANY)\n\tsender := core.NewSender(media, codec)\n\t// important to send original codec for valid IsRTP check\n\tsender.Handler = c.packetWriter(track.Codec, channel, codec.PayloadType)\n\n\tif c.mode == core.ModeActiveProducer && track.Codec.Name == core.CodecPCMA {\n\t\t// Fix Reolink Doorbell https://github.com/AlexxIT/go2rtc/issues/331\n\t\tsender.Handler = pcm.RepackG711(true, sender.Handler)\n\t}\n\n\tsender.HandleRTP(track)\n\n\tc.Senders = append(c.Senders, sender)\n\treturn nil\n}\n\nconst (\n\tstartVideoBuf = 32 * 1024   // 32KB\n\tstartAudioBuf = 2 * 1024    // 2KB\n\tmaxBuf        = 1024 * 1024 // 1MB\n\trtpHdr        = 12          // basic RTP header size\n\tintHdr        = 4           // interleaved header size\n)\n\nfunc (c *Conn) packetWriter(codec *core.Codec, channel, payloadType uint8) core.HandlerFunc {\n\tvar buf []byte\n\tvar n int\n\n\tvideo := codec.IsVideo()\n\tif video {\n\t\tbuf = make([]byte, startVideoBuf)\n\t} else {\n\t\tbuf = make([]byte, startAudioBuf)\n\t}\n\n\tflushBuf := func() {\n\t\t//log.Printf(\"[rtsp] channel:%2d write_size:%6d buffer_size:%6d\", channel, n, len(buf))\n\t\tif err := c.writeInterleavedData(buf[:n]); err != nil {\n\t\t\tc.Send += n\n\t\t}\n\t\tn = 0\n\t}\n\n\thandlerFunc := func(packet *rtp.Packet) {\n\t\tif c.state == StateNone {\n\t\t\treturn\n\t\t}\n\n\t\tclone := rtp.Packet{\n\t\t\tHeader: rtp.Header{\n\t\t\t\tVersion:        2,\n\t\t\t\tMarker:         packet.Marker,\n\t\t\t\tPayloadType:    payloadType,\n\t\t\t\tSequenceNumber: packet.SequenceNumber,\n\t\t\t\tTimestamp:      packet.Timestamp,\n\t\t\t\tSSRC:           packet.SSRC,\n\t\t\t},\n\t\t\tPayload: packet.Payload,\n\t\t}\n\n\t\tif !video {\n\t\t\tpacket.Marker = true // better to have marker on all audio packets\n\t\t}\n\n\t\tsize := rtpHdr + len(packet.Payload)\n\n\t\tif l := len(buf); n+intHdr+size > l {\n\t\t\tif l < maxBuf {\n\t\t\t\tbuf = append(buf, make([]byte, l)...) // double buffer size\n\t\t\t} else {\n\t\t\t\tflushBuf()\n\t\t\t}\n\t\t}\n\n\t\t//log.Printf(\"[RTP] codec: %s, size: %6d, ts: %10d, pt: %2d, ssrc: %d, seq: %d, mark: %v\", codec.Name, len(packet.Payload), packet.Timestamp, packet.PayloadType, packet.SSRC, packet.SequenceNumber, packet.Marker)\n\n\t\tchunk := buf[n:]\n\t\t_ = chunk[4] // bounds\n\t\tchunk[0] = '$'\n\t\tchunk[1] = channel\n\t\tchunk[2] = byte(size >> 8)\n\t\tchunk[3] = byte(size)\n\n\t\tif _, err := clone.MarshalTo(chunk[4:]); err != nil {\n\t\t\treturn\n\t\t}\n\n\t\tn += 4 + size\n\n\t\tif !packet.Marker || !c.playOK {\n\t\t\t// collect continious video packets to buffer\n\t\t\t// or wait OK for PLAY command for backchannel\n\t\t\t//log.Printf(\"[rtsp] collecting buffer ok=%t\", c.playOK)\n\t\t\treturn\n\t\t}\n\n\t\tflushBuf()\n\t}\n\n\tif !codec.IsRTP() {\n\t\tswitch codec.Name {\n\t\tcase core.CodecH264:\n\t\t\thandlerFunc = h264.RTPPay(c.PacketSize, handlerFunc)\n\t\tcase core.CodecH265:\n\t\t\thandlerFunc = h265.RTPPay(c.PacketSize, handlerFunc)\n\t\tcase core.CodecAAC:\n\t\t\thandlerFunc = aac.RTPPay(handlerFunc)\n\t\tcase core.CodecJPEG:\n\t\t\thandlerFunc = mjpeg.RTPPay(handlerFunc)\n\t\t}\n\t} else if codec.Name == core.CodecPCML {\n\t\thandlerFunc = pcm.LittleToBig(handlerFunc)\n\t} else if c.PacketSize != 0 {\n\t\tswitch codec.Name {\n\t\tcase core.CodecH264:\n\t\t\thandlerFunc = h264.RTPPay(c.PacketSize, handlerFunc)\n\t\t\thandlerFunc = h264.RTPDepay(codec, handlerFunc)\n\t\tcase core.CodecH265:\n\t\t\thandlerFunc = h265.RTPPay(c.PacketSize, handlerFunc)\n\t\t\thandlerFunc = h265.RTPDepay(codec, handlerFunc)\n\t\t}\n\t}\n\n\treturn handlerFunc\n}\n\nfunc (c *Conn) writeInterleavedData(data []byte) error {\n\tif c.Transport != \"udp\" {\n\t\t_ = c.conn.SetWriteDeadline(time.Now().Add(Timeout))\n\t\t_, err := c.conn.Write(data)\n\t\treturn err\n\t}\n\n\tfor len(data) >= 4 && data[0] == '$' {\n\t\tchannel := data[1]\n\t\tsize := uint16(data[2])<<8 | uint16(data[3])\n\t\trtpData := data[4 : 4+size]\n\n\t\tif _, err := c.WriteToUDP(rtpData, channel); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tdata = data[4+size:]\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/rtsp/helpers.go",
    "content": "package rtsp\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/pion/rtcp\"\n\t\"github.com/pion/sdp/v3\"\n)\n\ntype RTCP struct {\n\tChannel byte\n\tHeader  rtcp.Header\n\tPackets []rtcp.Packet\n}\n\nconst sdpHeader = `v=0\no=- 0 0 IN IP4 0.0.0.0\ns=-\nt=0 0`\n\nfunc UnmarshalSDP(rawSDP []byte) ([]*core.Media, error) {\n\tsd := &sdp.SessionDescription{}\n\tif err := sd.Unmarshal(rawSDP); err != nil {\n\t\t// fix multiple `s=` https://github.com/AlexxIT/WebRTC/issues/417\n\t\trawSDP = regexp.MustCompile(\"\\ns=[^\\n]+\").ReplaceAll(rawSDP, nil)\n\n\t\t// fix broken `c=` https://github.com/AlexxIT/go2rtc/issues/1426\n\t\trawSDP = regexp.MustCompile(\"\\nc=[^\\n]+\").ReplaceAll(rawSDP, nil)\n\n\t\t// fix SDP header for some cameras\n\t\tif i := bytes.Index(rawSDP, []byte(\"\\nm=\")); i > 0 {\n\t\t\trawSDP = append([]byte(sdpHeader), rawSDP[i:]...)\n\t\t}\n\n\t\t// Fix invalid media type (errSDPInvalidValue) caused by\n\t\t// some TP-LINK IP camera, e.g. TL-IPC44GW\n\t\tfor _, b := range regexp.MustCompile(\"m=[^ ]+ \").FindAll(rawSDP, -1) {\n\t\t\tswitch string(b[2 : len(b)-1]) {\n\t\t\tcase \"audio\", \"video\", \"application\":\n\t\t\tdefault:\n\t\t\t\trawSDP = bytes.Replace(rawSDP, b, []byte(\"m=application \"), 1)\n\t\t\t}\n\t\t}\n\n\t\tif err == io.EOF {\n\t\t\trawSDP = append(rawSDP, '\\n')\n\t\t}\n\n\t\tsd = &sdp.SessionDescription{}\n\t\terr = sd.Unmarshal(rawSDP)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\t// fix buggy camera https://github.com/AlexxIT/go2rtc/issues/771\n\tforceDirection := sd.Origin.Username == \"CV-RTSPHandler\"\n\n\tvar medias []*core.Media\n\n\tfor _, md := range sd.MediaDescriptions {\n\t\tmedia := core.UnmarshalMedia(md)\n\n\t\t// Check buggy SDP with fmtp for H264 on another track\n\t\t// https://github.com/AlexxIT/WebRTC/issues/419\n\t\tfor _, codec := range media.Codecs {\n\t\t\tswitch codec.Name {\n\t\t\tcase core.CodecH264:\n\t\t\t\tif codec.FmtpLine == \"\" {\n\t\t\t\t\tcodec.FmtpLine = findFmtpLine(codec.PayloadType, sd.MediaDescriptions)\n\t\t\t\t}\n\t\t\tcase core.CodecH265:\n\t\t\t\tif codec.FmtpLine != \"\" {\n\t\t\t\t\t// all three parameters are needed for a valid fmtp line\n\t\t\t\t\t// https://github.com/AlexxIT/go2rtc/pull/1588\n\t\t\t\t\tif !strings.Contains(codec.FmtpLine, \"sprop-vps=\") ||\n\t\t\t\t\t\t!strings.Contains(codec.FmtpLine, \"sprop-sps=\") ||\n\t\t\t\t\t\t!strings.Contains(codec.FmtpLine, \"sprop-pps=\") {\n\t\t\t\t\t\tcodec.FmtpLine = \"\"\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\tcase core.CodecOpus:\n\t\t\t\t// fix OPUS for some cameras https://datatracker.ietf.org/doc/html/rfc7587\n\t\t\t\tcodec.ClockRate = 48000\n\t\t\t\tcodec.Channels = 2\n\t\t\t}\n\t\t}\n\n\t\tif media.Direction == \"\" || forceDirection {\n\t\t\tmedia.Direction = core.DirectionRecvonly\n\t\t}\n\n\t\tmedias = append(medias, media)\n\t}\n\n\treturn medias, nil\n}\n\nfunc findFmtpLine(payloadType uint8, descriptions []*sdp.MediaDescription) string {\n\ts := strconv.Itoa(int(payloadType))\n\tfor _, md := range descriptions {\n\t\tcodec := core.UnmarshalCodec(md, s)\n\t\tif codec.FmtpLine != \"\" {\n\t\t\treturn codec.FmtpLine\n\t\t}\n\t}\n\treturn \"\"\n}\n\n// urlParse fix bugs:\n// 1. Content-Base: rtsp://::ffff:192.168.1.123/onvif/profile.1/\n// 2. Content-Base: rtsp://rtsp://turret2-cam.lan:554/stream1/\n// 3. Content-Base: 192.168.253.220:1935/\nfunc urlParse(rawURL string) (*url.URL, error) {\n\t// fix https://github.com/AlexxIT/go2rtc/issues/830\n\tif strings.HasPrefix(rawURL, \"rtsp://rtsp://\") {\n\t\trawURL = rawURL[7:]\n\t}\n\n\t// fix https://github.com/AlexxIT/go2rtc/issues/1852\n\tif !strings.Contains(rawURL, \"://\") {\n\t\trawURL = \"rtsp://\" + rawURL\n\t}\n\n\tu, err := url.Parse(rawURL)\n\tif err != nil && strings.HasSuffix(err.Error(), \"after host\") {\n\t\tif i := indexN(rawURL, '/', 3); i > 0 {\n\t\t\treturn urlParse(rawURL[:i] + \":\" + rawURL[i:])\n\t\t}\n\t}\n\n\treturn u, err\n}\n\nfunc indexN(s string, c byte, n int) int {\n\tvar offset int\n\tfor {\n\t\ti := strings.IndexByte(s[offset:], c)\n\t\tif i < 0 {\n\t\t\tbreak\n\t\t}\n\t\tif n--; n == 0 {\n\t\t\treturn offset + i\n\t\t}\n\t\toffset += i + 1\n\t}\n\treturn -1\n}\n"
  },
  {
    "path": "pkg/rtsp/producer.go",
    "content": "package rtsp\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\nfunc (c *Conn) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {\n\tcore.Assert(media.Direction == core.DirectionRecvonly)\n\n\tfor _, track := range c.Receivers {\n\t\tif track.Codec == codec {\n\t\t\treturn track, nil\n\t\t}\n\t}\n\n\tc.stateMu.Lock()\n\tdefer c.stateMu.Unlock()\n\n\tvar channel byte\n\n\tswitch c.mode {\n\tcase core.ModeActiveProducer:\n\t\tif c.state == StatePlay {\n\t\t\tif err := c.Reconnect(); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t}\n\n\t\tvar err error\n\t\tchannel, err = c.SetupMedia(media)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tc.state = StateSetup\n\tcase core.ModePassiveConsumer:\n\t\t// Backchannel\n\t\tchannel = byte(len(c.Senders)) * 2\n\tdefault:\n\t\treturn nil, errors.New(\"rtsp: wrong mode for GetTrack\")\n\t}\n\n\ttrack := core.NewReceiver(media, codec)\n\ttrack.ID = channel\n\tc.Receivers = append(c.Receivers, track)\n\n\treturn track, nil\n}\n\nfunc (c *Conn) Start() (err error) {\n\tcore.Assert(c.mode == core.ModeActiveProducer || c.mode == core.ModePassiveProducer)\n\n\tfor {\n\t\tok := false\n\n\t\tc.stateMu.Lock()\n\t\tswitch c.state {\n\t\tcase StateNone:\n\t\t\terr = nil\n\t\tcase StateConn:\n\t\t\terr = errors.New(\"start from CONN state\")\n\t\tcase StateSetup:\n\t\t\tswitch c.mode {\n\t\t\tcase core.ModeActiveProducer:\n\t\t\t\terr = c.Play()\n\t\t\tcase core.ModePassiveProducer:\n\t\t\t\terr = nil\n\t\t\tdefault:\n\t\t\t\terr = errors.New(\"start from wrong mode: \" + c.mode.String())\n\t\t\t}\n\n\t\t\tif err == nil {\n\t\t\t\tc.state = StatePlay\n\t\t\t\tok = true\n\t\t\t}\n\t\t}\n\t\tc.stateMu.Unlock()\n\n\t\tif !ok {\n\t\t\treturn\n\t\t}\n\n\t\t// Handler can return different states:\n\t\t// 1. None after PLAY should exit without error\n\t\t// 2. Play after PLAY should exit from Start with error\n\t\t// 3. Setup after PLAY should Play once again\n\t\terr = c.Handle()\n\t}\n}\n\nfunc (c *Conn) Stop() (err error) {\n\tfor _, receiver := range c.Receivers {\n\t\treceiver.Close()\n\t}\n\tfor _, sender := range c.Senders {\n\t\tsender.Close()\n\t}\n\n\tc.stateMu.Lock()\n\tif c.state != StateNone {\n\t\tc.state = StateNone\n\t\terr = c.Close()\n\t}\n\tc.stateMu.Unlock()\n\n\treturn\n}\n\nfunc (c *Conn) MarshalJSON() ([]byte, error) {\n\treturn json.Marshal(c.Connection)\n}\n\nfunc (c *Conn) Reconnect() error {\n\tc.Fire(\"RTSP reconnect\")\n\n\t// close current session\n\t_ = c.Close()\n\n\t// start new session\n\tif err := c.Dial(); err != nil {\n\t\treturn err\n\t}\n\tif err := c.Describe(); err != nil {\n\t\treturn err\n\t}\n\n\t// restore previous medias\n\tfor _, receiver := range c.Receivers {\n\t\tif _, err := c.SetupMedia(receiver.Media); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\tfor _, sender := range c.Senders {\n\t\tif _, err := c.SetupMedia(sender.Media); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/rtsp/rtsp_test.go",
    "content": "package rtsp\n\nimport (\n\t\"testing\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/stretchr/testify/assert\"\n)\n\nfunc TestURLParse(t *testing.T) {\n\t// https://github.com/AlexxIT/WebRTC/issues/395\n\tbase := \"rtsp://::ffff:192.168.1.123/onvif/profile.1/\"\n\tu, err := urlParse(base)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"::ffff:192.168.1.123:\", u.Host)\n\n\t// https://github.com/AlexxIT/go2rtc/issues/208\n\tbase = \"rtsp://rtsp://turret2-cam.lan:554/stream1/\"\n\tu, err = urlParse(base)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"turret2-cam.lan:554\", u.Host)\n\n\t// https://github.com/AlexxIT/go2rtc/issues/1852\n\tbase = \"192.168.253.220:1935/\"\n\tu, err = urlParse(base)\n\tassert.NoError(t, err)\n\tassert.Equal(t, \"192.168.253.220:1935\", u.Host)\n}\n\nfunc TestBugSDP1(t *testing.T) {\n\t// https://github.com/AlexxIT/WebRTC/issues/417\n\ts := `v=0\no=- 91674849066 1 IN IP4 192.168.1.123\ns=RtspServer\ni=live\nt=0 0\na=control:*\na=range:npt=0-\nm=video 0 RTP/AVP 96\nc=IN IP4 0.0.0.0\ns=RtspServer\ni=live\na=control:track0\na=range:npt=0-\na=rtpmap:96 H264/90000\na=fmtp:96 packetization-mode=1;profile-level-id=42001E;sprop-parameter-sets=Z0IAHvQCgC3I,aM48gA==\na=control:track0\nm=audio 0 RTP/AVP 97\nc=IN IP4 0.0.0.0\ns=RtspServer\ni=live\na=control:track1\na=range:npt=0-\na=rtpmap:97 MPEG4-GENERIC/8000/1\na=fmtp:97 profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1588\na=control:track1\n`\n\tmedias, err := UnmarshalSDP([]byte(s))\n\tassert.Nil(t, err)\n\tassert.NotNil(t, medias)\n}\n\nfunc TestBugSDP2(t *testing.T) {\n\t// https://github.com/AlexxIT/WebRTC/issues/419\n\ts := `v=0\no=- 1675628282 1675628283 IN IP4 192.168.1.123\ns=streamed by the RTSP server\nt=0 0\nm=video 0 RTP/AVP 96\na=rtpmap:96 H264/90000\na=control:track0\nm=audio 0 RTP/AVP 8\na=rtpmap:0 pcma/8000/1\na=control:track1\na=framerate:25\na=range:npt=now-\na=fmtp:96 packetization-mode=1;profile-level-id=64001F;sprop-parameter-sets=Z0IAH5WoFAFuQA==,aM48gA==\n`\n\tmedias, err := UnmarshalSDP([]byte(s))\n\tassert.Nil(t, err)\n\tassert.NotNil(t, medias)\n\tassert.NotEqual(t, \"\", medias[0].Codecs[0].FmtpLine)\n}\n\nfunc TestBugSDP3(t *testing.T) {\n\ts := `v=0\no=- 1680614126554766 1 IN IP4 192.168.0.3\ns=Session streamed by \"preview\"\nt=0 0\na=tool:BC Streaming Media v202210012022.10.01\na=type:broadcast\na=control:*\na=range:npt=now-\na=x-qt-text-nam:Session streamed by \"preview\"\nm=video 0 RTP/AVP 96\nc=IN IP4 0.0.0.0\nb=AS:8192\na=rtpmap:96 H264/90000\na=range:npt=now-\na=fmtp:96 packetization-mode=1;profile-level-id=640033;sprop-parameter-sets=Z2QAM6wVFKAoAPGQ,aO48sA==\na=recvonly\na=control:track1\nm=audio 0 RTP/AVP 97\nc=IN IP4 0.0.0.0\nb=AS:8192\na=rtpmap:97 MPEG4-GENERIC/16000\na=fmtp:97 streamtype=5;profile-level-id=1;mode=AAC-hbr;sizelength=13;indexlength=3;indexdeltalength=3;config=1408;\na=recvonly\na=control:track2\nm=audio 0 RTP/AVP 8\na=control:track3\na=rtpmap:8 PCMA/8000\na=sendonly`\n\tmedias, err := UnmarshalSDP([]byte(s))\n\tassert.Nil(t, err)\n\tassert.Len(t, medias, 3)\n}\n\nfunc TestBugSDP4(t *testing.T) {\n\ts := `v=0\no=- 14665860 31787219 1 IN IP4 10.0.0.94\ns=Session streamed by \"MERCURY RTSP Server\"\nt=0 0\nm=video 0 RTP/AVP 96\nc=IN IP4 0.0.0.0\nb=AS:4096\na=range:npt=0-\na=control:track1\na=rtpmap:96 H264/90000\na=fmtp:96 packetization-mode=1; profile-level-id=640016; sprop-parameter-sets=Z2QAFqzGoCgPaEAAAAMAQAAAB6E=,aOqPLA==\nm=audio 0 RTP/AVP 8\na=rtpmap:8 PCMA/8000\na=control:track2\nm=application/MERCURY 0 RTP/AVP smart/1/90000\na=rtpmap:95 MERCURY/90000\na=control:track3\n`\n\tmedias, err := UnmarshalSDP([]byte(s))\n\tassert.Nil(t, err)\n\tassert.Len(t, medias, 3)\n}\n\nfunc TestBugSDP5(t *testing.T) {\n\ts := `v=0\no=CV-RTSPHandler 1123412 0 IN IP4 192.168.1.22\ns=Camera\nc=IN IP4 192.168.1.22\nt=0 0\na=charset:Shift_JIS\na=range:npt=0-\na=control:*\na=etag:1234567890\nm=video 0 RTP/AVP 99\na=rtpmap:99 H264/90000\na=fmtp:99 profile-level-id=42A01E;packetization-mode=1;sprop-parameter-sets=Z0KgKedAPAET8uAIEAABd2AAK/IGAAADAC+vCAAAHc1lP//jAAADABfXhAAADuayn//wIA==,aN48gA==\na=control:trackID=1\na=sendonly\nm=audio 0 RTP/AVP 127\na=rtpmap:127 mpeg4-generic/8000/1\na=fmtp:127 streamtype=5; profile-level-id=15; mode=AAC-hbr; sizeLength=13; indexLength=3; indexDeltalength=3; config=1588; CTSDeltaLength=0; DTSDeltaLength=0;\na=control:trackID=2\n`\n\tmedias, err := UnmarshalSDP([]byte(s))\n\tassert.Nil(t, err)\n\tassert.Len(t, medias, 2)\n\tassert.Equal(t, \"recvonly\", medias[0].Direction)\n\tassert.Equal(t, \"recvonly\", medias[1].Direction)\n}\n\nfunc TestBugSDP6(t *testing.T) {\n\t// https://github.com/AlexxIT/go2rtc/issues/1278\n\ts := `v=0\no=- 3730506281693 1 IN IP4 172.20.0.215\ns=IP camera Live streaming\ni=stream1\nt=0 0\na=tool:LIVE555 Streaming Media v2014.02.04\na=type:broadcast\na=control:*\na=range:npt=0-\na=x-qt-text-nam:IP camera Live streaming\na=x-qt-text-inf:stream1\nm=video 0 RTP/AVP 26\nc=IN IP4 172.20.0.215\nb=AS:1500\na=x-bufferdelay:0.55000\na=x-dimensions:1280,960\na=control:track1\nm=audio 0 RTP/AVP 0\nc=IN IP4 172.20.0.215\nb=AS:64\na=x-bufferdelay:0.55000\na=control:track2\nm=application 0 RTP/AVP 107\nc=IN IP4 172.20.0.215\nb=AS:1\na=x-bufferdelay:0.55000\na=rtpmap:107 vnd.onvif.metadata/90000/500\na=control:track4\nm=vana 0 RTP/AVP 108\nc=IN IP4 172.20.0.215\nb=AS:1\na=x-bufferdelay:0.55000\na=rtpmap:108 video.analysis/90000/500\na=control:track5\n`\n\tmedias, err := UnmarshalSDP([]byte(s))\n\tassert.Nil(t, err)\n\tassert.Len(t, medias, 4)\n}\n\nfunc TestBugSDP7(t *testing.T) {\n\t// https://github.com/AlexxIT/go2rtc/issues/1426\n\ts := `v=0\no=- 1001 1 IN\ns=VCP IPC Realtime stream\nm=video 0 RTP/AVP 105\nc=IN\na=control:rtsp://1.0.1.113/media/video2/video\na=rtpmap:105 H264/90000\na=fmtp:105 profile-level-id=640016; packetization-mode=1; sprop-parameter-sets=Z2QAFqw7UFAX/LCAAAH0AABOIEI=,aOqPLA==\na=recvonly\nm=audio 0 RTP/AVP 0\nc=IN\na=fmtp:0 RTCP=0\na=control:rtsp://1.0.1.113/media/video2/audio1\na=recvonly\nm=audio 0 RTP/AVP 0\nc=IN\na=control:rtsp://1.0.1.113/media/video2/backchannel\na=rtpmap:0 PCMA/8000\na=rtpmap:0 PCMU/8000\na=sendonly\nm=application 0 RTP/AVP 107\nc=IN\na=control:rtsp://1.0.1.113/media/video2/metadata\na=rtpmap:107 vnd.onvif.metadata/90000\na=fmtp:107 DecoderTag=h3c-v3 RTCP=0\na=recvonly\n`\n\tmedias, err := UnmarshalSDP([]byte(s))\n\tassert.Nil(t, err)\n\tassert.Len(t, medias, 4)\n}\n\nfunc TestHikvisionPCM(t *testing.T) {\n\ts := `v=0\no=- 1721969533379665 1721969533379665 IN IP4 192.168.1.12\ns=Media Presentation\ne=NONE\nb=AS:5100\nt=0 0\na=control:rtsp://192.168.1.12:554/Streaming/channels/101/\nm=video 0 RTP/AVP 96\nc=IN IP4 0.0.0.0\nb=AS:5000\na=recvonly\na=x-dimensions:3200,1800\na=control:rtsp://192.168.1.12:554/Streaming/channels/101/trackID=1\na=rtpmap:96 H264/90000\na=fmtp:96 profile-level-id=420029; packetization-mode=1; sprop-parameter-sets=Z2QAM6wVFKAyAOP5f/AAEAAWyAAAH0AAB1MAIA==,aO48sA==\nm=audio 0 RTP/AVP 11\nc=IN IP4 0.0.0.0\nb=AS:50\na=recvonly\na=control:rtsp://192.168.1.12:554/Streaming/channels/101/trackID=2\na=rtpmap:11 PCM/48000\na=Media_header:MEDIAINFO=494D4B4801030000040000010170011080BB0000007D000000000000000000000000000000000000;\na=appversion:1.0\n`\n\tmedias, err := UnmarshalSDP([]byte(s))\n\tassert.Nil(t, err)\n\tassert.Len(t, medias, 2)\n\tassert.Equal(t, core.CodecPCML, medias[1].Codecs[0].Name)\n}\n"
  },
  {
    "path": "pkg/rtsp/server.go",
    "content": "package rtsp\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/tcp\"\n)\n\nvar FailedAuth = errors.New(\"failed authentication\")\n\nfunc NewServer(conn net.Conn) *Conn {\n\treturn &Conn{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"rtsp\",\n\t\t\tProtocol:   \"rtsp+tcp\",\n\t\t\tRemoteAddr: conn.RemoteAddr().String(),\n\t\t},\n\t\tconn:   conn,\n\t\treader: bufio.NewReader(conn),\n\t}\n}\n\nfunc (c *Conn) Auth(username, password string) {\n\tinfo := url.UserPassword(username, password)\n\tc.auth = tcp.NewAuth(info)\n}\n\nfunc (c *Conn) Accept() error {\n\tfor {\n\t\treq, err := c.ReadRequest()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif c.URL == nil {\n\t\t\tc.URL = req.URL\n\t\t\tc.UserAgent = req.Header.Get(\"User-Agent\")\n\t\t}\n\n\t\tc.Fire(req)\n\n\t\tif valid, empty := c.auth.Validate(req); !valid {\n\t\t\tres := &tcp.Response{\n\t\t\t\tStatus:  \"401 Unauthorized\",\n\t\t\t\tHeader:  map[string][]string{\"Www-Authenticate\": {`Basic realm=\"go2rtc\"`}},\n\t\t\t\tRequest: req,\n\t\t\t}\n\t\t\tif err = c.WriteResponse(res); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tif empty {\n\t\t\t\t// eliminate false positive: ffmpeg sends first request without\n\t\t\t\t// authorization header even if the user provides credentials\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn FailedAuth\n\t\t}\n\n\t\t// Receiver: OPTIONS > DESCRIBE > SETUP... > PLAY > TEARDOWN\n\t\t// Sender: OPTIONS > ANNOUNCE > SETUP... > RECORD > TEARDOWN\n\t\tswitch req.Method {\n\t\tcase MethodOptions:\n\t\t\tres := &tcp.Response{\n\t\t\t\tHeader: map[string][]string{\n\t\t\t\t\t\"Public\": {\"OPTIONS, SETUP, TEARDOWN, DESCRIBE, PLAY, PAUSE, ANNOUNCE, RECORD\"},\n\t\t\t\t},\n\t\t\t\tRequest: req,\n\t\t\t}\n\t\t\tif err = c.WriteResponse(res); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\tcase MethodAnnounce:\n\t\t\tif req.Header.Get(\"Content-Type\") != \"application/sdp\" {\n\t\t\t\treturn errors.New(\"wrong content type\")\n\t\t\t}\n\n\t\t\tc.SDP = string(req.Body) // for info\n\n\t\t\tc.Medias, err = UnmarshalSDP(req.Body)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\t// TODO: fix someday...\n\t\t\tfor i, media := range c.Medias {\n\t\t\t\ttrack := core.NewReceiver(media, media.Codecs[0])\n\t\t\t\ttrack.ID = byte(i * 2)\n\t\t\t\tc.Receivers = append(c.Receivers, track)\n\t\t\t}\n\n\t\t\tc.mode = core.ModePassiveProducer\n\t\t\tc.Fire(MethodAnnounce)\n\n\t\t\tres := &tcp.Response{Request: req}\n\t\t\tif err = c.WriteResponse(res); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\tcase MethodDescribe:\n\t\t\tc.mode = core.ModePassiveConsumer\n\t\t\tc.Fire(MethodDescribe)\n\n\t\t\tif c.Senders == nil {\n\t\t\t\tres := &tcp.Response{\n\t\t\t\t\tStatus:  \"404 Not Found\",\n\t\t\t\t\tRequest: req,\n\t\t\t\t}\n\t\t\t\treturn c.WriteResponse(res)\n\t\t\t}\n\n\t\t\tres := &tcp.Response{\n\t\t\t\tHeader: map[string][]string{\n\t\t\t\t\t\"Content-Type\": {\"application/sdp\"},\n\t\t\t\t},\n\t\t\t\tRequest: req,\n\t\t\t}\n\n\t\t\t// convert tracks to real output medias medias\n\t\t\tvar medias []*core.Media\n\t\t\tfor i, track := range c.Senders {\n\t\t\t\tmedia := &core.Media{\n\t\t\t\t\tKind:      core.GetKind(track.Codec.Name),\n\t\t\t\t\tDirection: core.DirectionRecvonly,\n\t\t\t\t\tCodecs:    []*core.Codec{track.Codec},\n\t\t\t\t\tID:        \"trackID=\" + strconv.Itoa(i),\n\t\t\t\t}\n\t\t\t\tmedias = append(medias, media)\n\t\t\t}\n\n\t\t\tfor i, track := range c.Receivers {\n\t\t\t\tmedia := &core.Media{\n\t\t\t\t\tKind:      core.GetKind(track.Codec.Name),\n\t\t\t\t\tDirection: core.DirectionSendonly,\n\t\t\t\t\tCodecs:    []*core.Codec{track.Codec},\n\t\t\t\t\tID:        \"trackID=\" + strconv.Itoa(i+len(c.Senders)),\n\t\t\t\t}\n\t\t\t\tmedias = append(medias, media)\n\t\t\t}\n\n\t\t\tres.Body, err = core.MarshalSDP(c.SessionName, medias)\n\t\t\tif err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\t\tc.SDP = string(res.Body) // for info\n\n\t\t\tif err = c.WriteResponse(res); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\tcase MethodSetup:\n\t\t\tres := &tcp.Response{\n\t\t\t\tHeader:  map[string][]string{},\n\t\t\t\tRequest: req,\n\t\t\t}\n\n\t\t\t// Test if client requests TCP transport, otherwise return 461 Transport not supported\n\t\t\t// This allows smart clients who initially requested UDP to fall back on TCP transport\n\t\t\tif tr := req.Header.Get(\"Transport\"); strings.HasPrefix(tr, \"RTP/AVP/TCP\") {\n\t\t\t\tc.session = core.RandString(8, 10)\n\t\t\t\tc.state = StateSetup\n\n\t\t\t\tif c.mode == core.ModePassiveConsumer {\n\t\t\t\t\tif i := reqTrackID(req); i >= 0 && i < len(c.Senders)+len(c.Receivers) {\n\t\t\t\t\t\tif i < len(c.Senders) {\n\t\t\t\t\t\t\tc.Senders[i].Media.ID = MethodSetup\n\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\tc.Receivers[i-len(c.Senders)].Media.ID = MethodSetup\n\t\t\t\t\t\t}\n\t\t\t\t\t\ttr = fmt.Sprintf(\"RTP/AVP/TCP;unicast;interleaved=%d-%d\", i*2, i*2+1)\n\t\t\t\t\t\tres.Header.Set(\"Transport\", tr)\n\t\t\t\t\t} else {\n\t\t\t\t\t\tres.Status = \"400 Bad Request\"\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tres.Header.Set(\"Transport\", tr)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\tres.Status = \"461 Unsupported transport\"\n\t\t\t}\n\n\t\t\tif err = c.WriteResponse(res); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\n\t\tcase MethodRecord, MethodPlay:\n\t\t\tif c.mode == core.ModePassiveConsumer {\n\t\t\t\t// stop unconfigured senders\n\t\t\t\tfor _, track := range c.Senders {\n\t\t\t\t\tif track.Media.ID != MethodSetup {\n\t\t\t\t\t\ttrack.Close()\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tres := &tcp.Response{Request: req}\n\t\t\terr = c.WriteResponse(res)\n\t\t\tc.playOK = true\n\t\t\treturn err\n\n\t\tcase MethodTeardown:\n\t\t\tres := &tcp.Response{Request: req}\n\t\t\t_ = c.WriteResponse(res)\n\t\t\tc.state = StateNone\n\t\t\treturn c.conn.Close()\n\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"unsupported method: %s\", req.Method)\n\t\t}\n\t}\n}\n\nfunc reqTrackID(req *tcp.Request) int {\n\tvar s string\n\tif req.URL.RawQuery != \"\" {\n\t\ts = req.URL.RawQuery\n\t} else {\n\t\ts = req.URL.Path\n\t}\n\tif i := strings.LastIndexByte(s, '='); i > 0 {\n\t\tif i, err := strconv.Atoi(s[i+1:]); err == nil {\n\t\t\treturn i\n\t\t}\n\t}\n\treturn -1\n}\n"
  },
  {
    "path": "pkg/shell/command.go",
    "content": "package shell\n\nimport (\n\t\"context\"\n\t\"os/exec\"\n)\n\n// Command like exec.Cmd, but with support:\n// - io.Closer interface\n// - Wait from multiple places\n// - Done channel\ntype Command struct {\n\t*exec.Cmd\n\tctx    context.Context\n\tcancel context.CancelFunc\n\terr    error\n}\n\nfunc NewCommand(s string) *Command {\n\tctx, cancel := context.WithCancel(context.Background())\n\targs := QuoteSplit(s)\n\tcmd := exec.CommandContext(ctx, args[0], args[1:]...)\n\tcmd.SysProcAttr = procAttr\n\treturn &Command{cmd, ctx, cancel, nil}\n}\n\nfunc (c *Command) Start() error {\n\tif err := c.Cmd.Start(); err != nil {\n\t\treturn err\n\t}\n\n\tgo func() {\n\t\tc.err = c.Cmd.Wait()\n\t\tc.cancel() // release context resources\n\t}()\n\n\treturn nil\n}\n\nfunc (c *Command) Wait() error {\n\t<-c.ctx.Done()\n\treturn c.err\n}\n\nfunc (c *Command) Run() error {\n\tif err := c.Start(); err != nil {\n\t\treturn err\n\t}\n\treturn c.Wait()\n}\n\nfunc (c *Command) Done() <-chan struct{} {\n\treturn c.ctx.Done()\n}\n\nfunc (c *Command) Close() error {\n\tc.cancel()\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/shell/procattr.go",
    "content": "//go:build !linux\n\npackage shell\n\nimport \"syscall\"\n\nvar procAttr *syscall.SysProcAttr\n"
  },
  {
    "path": "pkg/shell/procattr_linux.go",
    "content": "package shell\n\nimport \"syscall\"\n\n// will stop child if parent died (even with SIGKILL)\nvar procAttr = &syscall.SysProcAttr{Pdeathsig: syscall.SIGTERM}\n"
  },
  {
    "path": "pkg/shell/shell.go",
    "content": "package shell\n\nimport (\n\t\"os\"\n\t\"os/signal\"\n\t\"strings\"\n\t\"syscall\"\n)\n\nfunc QuoteSplit(s string) []string {\n\tvar a []string\n\n\tfor len(s) > 0 {\n\t\tswitch c := s[0]; c {\n\t\tcase '\\t', '\\n', '\\r', ' ': // unicode.IsSpace\n\t\t\ts = s[1:]\n\t\tcase '\"', '\\'': // quote chars\n\t\t\tif i := strings.IndexByte(s[1:], c); i > 0 {\n\t\t\t\ta = append(a, s[1:i+1])\n\t\t\t\ts = s[i+2:]\n\t\t\t} else {\n\t\t\t\treturn nil // error\n\t\t\t}\n\t\tdefault:\n\t\t\ti := strings.IndexAny(s, \"\\t\\n\\r \")\n\t\t\tif i > 0 {\n\t\t\t\ta = append(a, s[:i])\n\t\t\t\ts = s[i:]\n\t\t\t} else {\n\t\t\t\ta = append(a, s)\n\t\t\t\ts = \"\"\n\t\t\t}\n\t\t}\n\t}\n\n\treturn a\n}\n\nfunc RunUntilSignal() {\n\tsigs := make(chan os.Signal, 1)\n\tsignal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)\n\tprintln(\"exit with signal:\", (<-sigs).String())\n}\n"
  },
  {
    "path": "pkg/shell/shell_test.go",
    "content": "package shell\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestQuoteSplit(t *testing.T) {\n\ts := `\npython \"-c\" 'import time\nprint(\"time\", time.time())'\n`\n\trequire.Equal(t, []string{\"python\", \"-c\", \"import time\\nprint(\\\"time\\\", time.time())\"}, QuoteSplit(s))\n\n\ts = `ffmpeg -i \"video=FaceTime HD Camera\" -i \"DeckLink SDI (2)\"`\n\trequire.Equal(t, []string{\"ffmpeg\", \"-i\", `video=FaceTime HD Camera`, \"-i\", \"DeckLink SDI (2)\"}, QuoteSplit(s))\n}\n"
  },
  {
    "path": "pkg/srtp/server.go",
    "content": "package srtp\n\nimport (\n\t\"encoding/binary\"\n\t\"net\"\n\t\"strconv\"\n\t\"sync\"\n)\n\ntype Server struct {\n\taddress  string\n\tconn     net.PacketConn\n\tsessions map[uint32]*Session\n\tmu       sync.Mutex\n}\n\nfunc NewServer(address string) *Server {\n\treturn &Server{\n\t\taddress:  address,\n\t\tsessions: map[uint32]*Session{},\n\t}\n}\n\nfunc (s *Server) Port() int {\n\tif s.conn != nil {\n\t\treturn s.conn.LocalAddr().(*net.UDPAddr).Port\n\t}\n\n\t_, a, _ := net.SplitHostPort(s.address)\n\ti, _ := strconv.Atoi(a)\n\treturn i\n}\n\nfunc (s *Server) AddSession(session *Session) {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\tif err := session.init(); err != nil {\n\t\treturn\n\t}\n\n\tif len(s.sessions) == 0 {\n\t\tvar err error\n\t\tif s.conn, err = net.ListenPacket(\"udp\", s.address); err != nil {\n\t\t\treturn\n\t\t}\n\t\tgo s.handle()\n\t}\n\n\tsession.conn = s.conn\n\n\ts.sessions[session.Remote.SSRC] = session\n}\n\nfunc (s *Server) DelSession(session *Session) {\n\ts.mu.Lock()\n\n\tdelete(s.sessions, session.Remote.SSRC)\n\n\t// check s.conn for https://github.com/AlexxIT/go2rtc/issues/734\n\tif len(s.sessions) == 0 && s.conn != nil {\n\t\t_ = s.conn.Close()\n\t}\n\n\ts.mu.Unlock()\n}\n\nfunc (s *Server) GetSession(ssrc uint32) (session *Session) {\n\ts.mu.Lock()\n\tsession = s.sessions[ssrc]\n\ts.mu.Unlock()\n\treturn\n}\n\nfunc (s *Server) handle() error {\n\tb := make([]byte, 2048)\n\tfor {\n\t\tn, _, err := s.conn.ReadFrom(b)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\t// Multiplexing RTP Data and Control Packets on a Single Port\n\t\t// https://datatracker.ietf.org/doc/html/rfc5761\n\n\t\tswitch packetType := b[1]; packetType {\n\t\tcase 99, 110, 0x80 | 99, 0x80 | 110:\n\t\t\t// this is default position for SSRC in RTP packet\n\t\t\tssrc := binary.BigEndian.Uint32(b[8:])\n\t\t\tif session := s.GetSession(ssrc); session != nil {\n\t\t\t\tsession.ReadRTP(b[:n])\n\t\t\t}\n\n\t\tcase 200, 201, 202, 203, 204, 205, 206, 207:\n\t\t\t// this is default position for SSRC in RTCP packet\n\t\t\tssrc := binary.BigEndian.Uint32(b[4:])\n\t\t\tif session := s.GetSession(ssrc); session != nil {\n\t\t\t\tsession.ReadRTCP(b[:n])\n\t\t\t}\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/srtp/session.go",
    "content": "package srtp\n\nimport (\n\t\"net\"\n\t\"time\"\n\n\t\"github.com/pion/rtcp\"\n\t\"github.com/pion/rtp\"\n\t\"github.com/pion/srtp/v3\"\n)\n\ntype Session struct {\n\tLocal  *Endpoint\n\tRemote *Endpoint\n\n\tOnReadRTP func(packet *rtp.Packet)\n\n\tRecv int // bytes recv\n\tSend int // bytes send\n\n\tconn net.PacketConn // local conn endpoint\n\n\tPayloadType  uint8\n\tRTCPInterval time.Duration\n\n\tsenderRTCP rtcp.SenderReport\n\tsenderTime time.Time\n}\n\ntype Endpoint struct {\n\tAddr       string\n\tPort       uint16\n\tMasterKey  []byte\n\tMasterSalt []byte\n\tSSRC       uint32\n\n\taddr net.Addr\n\tsrtp *srtp.Context\n}\n\nfunc (e *Endpoint) init() (err error) {\n\te.addr = &net.UDPAddr{IP: net.ParseIP(e.Addr), Port: int(e.Port)}\n\te.srtp, err = srtp.CreateContext(e.MasterKey, e.MasterSalt, profile(e.MasterKey))\n\treturn\n}\n\nfunc profile(key []byte) srtp.ProtectionProfile {\n\tswitch len(key) {\n\tcase 16:\n\t\treturn srtp.ProtectionProfileAes128CmHmacSha1_80\n\t\t//case 32:\n\t\t//\treturn srtp.ProtectionProfileAes256CmHmacSha1_80\n\t}\n\treturn 0\n}\n\nfunc (s *Session) init() error {\n\tif err := s.Local.init(); err != nil {\n\t\treturn err\n\t}\n\tif err := s.Remote.init(); err != nil {\n\t\treturn err\n\t}\n\n\ts.senderRTCP.SSRC = s.Local.SSRC\n\ts.senderTime = time.Now().Add(s.RTCPInterval)\n\n\treturn nil\n}\n\nfunc (s *Session) WriteRTP(packet *rtp.Packet) (int, error) {\n\tif s.Local.srtp == nil {\n\t\treturn 0, nil // before init call\n\t}\n\n\tif now := time.Now(); now.After(s.senderTime) {\n\t\ts.senderRTCP.NTPTime = uint64(now.UnixNano())\n\t\ts.senderTime = now.Add(s.RTCPInterval)\n\t\t_, _ = s.WriteRTCP(&s.senderRTCP)\n\t}\n\n\tclone := rtp.Packet{\n\t\tHeader: rtp.Header{\n\t\t\tVersion:        2,\n\t\t\tMarker:         packet.Marker,\n\t\t\tPayloadType:    s.PayloadType,\n\t\t\tSequenceNumber: packet.SequenceNumber,\n\t\t\tTimestamp:      packet.Timestamp,\n\t\t\tSSRC:           s.Local.SSRC,\n\t\t},\n\t\tPayload: packet.Payload,\n\t}\n\n\tb, err := clone.Marshal()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\n\ts.senderRTCP.PacketCount++\n\ts.senderRTCP.RTPTime = clone.Timestamp\n\ts.senderRTCP.OctetCount += uint32(len(clone.Payload))\n\n\tif b, err = s.Local.srtp.EncryptRTP(nil, b, nil); err != nil {\n\t\treturn 0, err\n\t}\n\n\treturn s.conn.WriteTo(b, s.Remote.addr)\n}\n\nfunc (s *Session) WriteRTCP(packet rtcp.Packet) (int, error) {\n\tb, err := packet.Marshal()\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tb, err = s.Local.srtp.EncryptRTCP(nil, b, nil)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\treturn s.conn.WriteTo(b, s.Remote.addr)\n}\n\nfunc (s *Session) ReadRTP(b []byte) {\n\tpacket := &rtp.Packet{}\n\n\tb, err := s.Remote.srtp.DecryptRTP(nil, b, &packet.Header)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tif err = packet.Unmarshal(b); err != nil {\n\t\treturn\n\t}\n\n\tif s.OnReadRTP != nil {\n\t\ts.OnReadRTP(packet)\n\t}\n}\n\nfunc (s *Session) ReadRTCP(b []byte) {\n\theader := rtcp.Header{}\n\tb, err := s.Remote.srtp.DecryptRTCP(nil, b, &header)\n\tif err != nil {\n\t\treturn\n\t}\n\n\t//packets, err := rtcp.Unmarshal(b)\n\t//if err != nil {\n\t//\treturn\n\t//}\n\t//if report, ok := packets[0].(*rtcp.SenderReport); ok {\n\t//\tlog.Printf(\"[srtp] rtcp type=%d report=%v\", header.Type, report)\n\t//}\n\n\tif header.Type != rtcp.TypeSenderReport {\n\t\treturn\n\t}\n\n\treceiverRTCP := rtcp.ReceiverReport{SSRC: s.Local.SSRC}\n\t_, _ = s.WriteRTCP(&receiverRTCP)\n}\n"
  },
  {
    "path": "pkg/tapo/backchannel.go",
    "content": "package tapo\n\nimport (\n\t\"bytes\"\n\t\"strconv\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/mpegts\"\n\t\"github.com/pion/rtp\"\n)\n\nfunc (c *Client) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {\n\tif c.sender == nil {\n\t\tif err := c.SetupBackchannel(); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tmuxer := mpegts.NewMuxer()\n\t\tpid := muxer.AddTrack(mpegts.StreamTypePCMATapo)\n\t\tif err := c.WriteBackchannel(muxer.GetHeader()); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tc.sender = core.NewSender(media, track.Codec)\n\t\tc.sender.Handler = func(packet *rtp.Packet) {\n\t\t\tb := muxer.GetPayload(pid, packet.Timestamp, packet.Payload)\n\t\t\t_ = c.WriteBackchannel(b)\n\t\t}\n\t}\n\n\tc.sender.HandleRTP(track)\n\treturn nil\n}\n\nfunc (c *Client) SetupBackchannel() (err error) {\n\t// if conn1 is not used - we will use it for backchannel\n\t// or we need to start another conn for session2\n\tif c.session1 != \"\" {\n\t\tif c.conn2, err = c.newConn(); err != nil {\n\t\t\treturn\n\t\t}\n\t} else {\n\t\tc.conn2 = c.conn1\n\t}\n\n\tc.session2, err = c.Request(c.conn2, []byte(`{\"params\":{\"talk\":{\"mode\":\"aec\"},\"method\":\"get\"},\"seq\":3,\"type\":\"request\"}`))\n\treturn\n}\n\nfunc (c *Client) WriteBackchannel(body []byte) (err error) {\n\t// TODO: fixme (size)\n\tbuf := bytes.NewBuffer(nil)\n\tbuf.WriteString(\"----client-stream-boundary--\\r\\n\")\n\tbuf.WriteString(\"Content-Type: audio/mp2t\\r\\n\")\n\tbuf.WriteString(\"X-If-Encrypt: 0\\r\\n\")\n\tbuf.WriteString(\"X-Session-Id: \" + c.session2 + \"\\r\\n\")\n\tbuf.WriteString(\"Content-Length: \" + strconv.Itoa(len(body)) + \"\\r\\n\\r\\n\")\n\tbuf.Write(body)\n\n\t_, err = buf.WriteTo(c.conn2)\n\treturn\n}\n"
  },
  {
    "path": "pkg/tapo/client.go",
    "content": "package tapo\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"crypto/aes\"\n\t\"crypto/cipher\"\n\t\"crypto/md5\"\n\t\"crypto/sha256\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime/multipart\"\n\t\"net\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/mpegts\"\n\t\"github.com/AlexxIT/go2rtc/pkg/pcm\"\n\t\"github.com/AlexxIT/go2rtc/pkg/tcp\"\n)\n\n// Deprecated: should be rewritten to core.Connection\ntype Client struct {\n\tcore.Listener\n\n\turl *url.URL\n\n\tmedias    []*core.Media\n\treceivers []*core.Receiver\n\tsender    *core.Sender\n\n\tconn1 net.Conn\n\tconn2 net.Conn\n\n\tdecrypt func(b []byte) []byte\n\n\tsession1 string\n\tsession2 string\n\trequest  string\n\n\trecv int\n\tsend int\n}\n\n// block ciphers using cipher block chaining.\ntype cbcMode interface {\n\tcipher.BlockMode\n\tSetIV([]byte)\n}\n\n// Dial support different urls:\n//   - tapo://{cloud-password}@192.168.1.123 - auth to Tapo cameras\n//     with cloud password (autodetect hash method)\n//   - tapo://admin:{hashed-cloud-password}@192.168.1.123 - auth to Tapo cameras\n//     with pre-hashed cloud password\n//   - vigi://admin:{password}@192.168.1.123 - auth to Vigi cameras with password\n//     for admin account (other not supported)\nfunc Dial(rawURL string) (*Client, error) {\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif u.Port() == \"\" {\n\t\tu.Host += \":8800\"\n\t}\n\n\tc := &Client{url: u}\n\tif c.conn1, err = c.newConn(); err != nil {\n\t\treturn nil, err\n\t}\n\treturn c, nil\n}\n\nfunc (c *Client) newConn() (net.Conn, error) {\n\treq, err := http.NewRequest(\"POST\", \"http://\"+c.url.Host+\"/stream\", nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tquery := c.url.Query()\n\n\tif deviceId := query.Get(\"deviceId\"); deviceId != \"\" {\n\t\treq.URL.RawQuery = \"deviceId=\" + deviceId\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"multipart/mixed; boundary=--client-stream-boundary--\")\n\n\tusername := c.url.User.Username()\n\tpassword, _ := c.url.User.Password()\n\n\tconn, res, err := dial(req, c.url.Scheme, username, password)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif res.StatusCode != http.StatusOK {\n\t\treturn nil, errors.New(res.Status)\n\t}\n\n\tif c.decrypt == nil {\n\t\tc.newDectypter(res, c.url.Scheme, username, password)\n\t}\n\n\tchannel := query.Get(\"channel\")\n\tif channel == \"\" {\n\t\tchannel = \"0\"\n\t}\n\n\tsubtype := query.Get(\"subtype\")\n\tswitch subtype {\n\tcase \"\", \"0\":\n\t\tsubtype = \"HD\"\n\tcase \"1\":\n\t\tsubtype = \"VGA\"\n\t}\n\n\tc.request = fmt.Sprintf(\n\t\t`{\"params\":{\"preview\":{\"audio\":[\"default\"],\"channels\":[%s],\"resolutions\":[\"%s\"]},\"method\":\"get\"},\"seq\":1,\"type\":\"request\"}`,\n\t\tchannel, subtype,\n\t)\n\n\treturn conn, nil\n}\n\nfunc (c *Client) newDectypter(res *http.Response, brand, username, password string) {\n\texchange := res.Header.Get(\"Key-Exchange\")\n\tnonce := core.Between(exchange, `nonce=\"`, `\"`)\n\n\tif brand == \"tapo\" && password == \"\" {\n\t\tif strings.Contains(exchange, `encrypt_type=\"3\"`) {\n\t\t\tpassword = fmt.Sprintf(\"%32X\", sha256.Sum256([]byte(username)))\n\t\t} else {\n\t\t\tpassword = fmt.Sprintf(\"%16X\", md5.Sum([]byte(username)))\n\t\t}\n\t\tusername = \"admin\"\n\t}\n\n\tif strings.Contains(exchange, `username=\"none\"`) {\n\t\t// https://nvd.nist.gov/vuln/detail/CVE-2022-37255\n\t\tusername = \"none\"\n\t\tpassword = \"TPL075526460603\"\n\t}\n\n\tkey := md5.Sum([]byte(nonce + \":\" + password))\n\tiv := md5.Sum([]byte(username + \":\" + nonce))\n\n\tblock, err := aes.NewCipher(key[:])\n\tif err != nil {\n\t\treturn\n\t}\n\n\tcbc := cipher.NewCBCDecrypter(block, iv[:]).(cbcMode)\n\n\tc.decrypt = func(b []byte) []byte {\n\t\t// restore IV\n\t\tcbc.SetIV(iv[:])\n\n\t\t// decrypt\n\t\tcbc.CryptBlocks(b, b)\n\n\t\t// unpad\n\t\tn := len(b)\n\t\tpadSize := int(b[n-1])\n\t\treturn b[:n-padSize]\n\t}\n}\n\nfunc (c *Client) SetupStream() (err error) {\n\tif c.session1 != \"\" {\n\t\treturn\n\t}\n\n\t// audio: default, disable, enable\n\tc.session1, err = c.Request(c.conn1, []byte(c.request))\n\treturn\n}\n\n// Handle - first run will be in probe state\nfunc (c *Client) Handle() error {\n\trd := multipart.NewReader(c.conn1, \"--device-stream-boundary--\")\n\tdemux := mpegts.NewDemuxer()\n\n\tvar transcode func([]byte) []byte\n\n\tfor {\n\t\tp, err := rd.NextRawPart()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif ct := p.Header.Get(\"Content-Type\"); ct != \"video/mp2t\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tcl := p.Header.Get(\"Content-Length\")\n\t\tsize, err := strconv.Atoi(cl)\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tc.recv += size\n\n\t\tbody := make([]byte, size)\n\n\t\tb := body\n\t\tfor {\n\t\t\tif n, err2 := p.Read(b); err2 == nil {\n\t\t\t\tb = b[n:]\n\t\t\t} else {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\tbody = c.decrypt(body)\n\t\tbytesRd := bytes.NewReader(body)\n\n\t\tfor {\n\t\t\tpkt, err2 := demux.ReadPacket(bytesRd)\n\t\t\tif pkt == nil || err2 == io.EOF {\n\t\t\t\tbreak\n\t\t\t}\n\t\t\tif err2 != nil {\n\t\t\t\treturn err2\n\t\t\t}\n\n\t\t\tif pkt.PayloadType == mpegts.StreamTypePCMUTapo {\n\t\t\t\t// TODO: rewrite this part in the future\n\t\t\t\t// Some cameras in the new firmware began to use PCMU/16000.\n\t\t\t\t// https://github.com/AlexxIT/go2rtc/issues/1954\n\t\t\t\t// I don't know why Tapo considers this an improvement. The codec is no better than the previous one.\n\t\t\t\t// Unfortunately, we don't know in advance what codec the camera will use.\n\t\t\t\t// Therefore, it's easier to transcode to a standard codec that all Tapo cameras have.\n\t\t\t\tif transcode == nil {\n\t\t\t\t\ttranscode = pcm.Transcode(\n\t\t\t\t\t\t&core.Codec{Name: core.CodecPCMA, ClockRate: 8000},\n\t\t\t\t\t\t&core.Codec{Name: core.CodecPCMU, ClockRate: 16000},\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t\tpkt.PayloadType = mpegts.StreamTypePCMATapo\n\t\t\t\tpkt.Payload = transcode(pkt.Payload)\n\t\t\t}\n\n\t\t\tfor _, receiver := range c.receivers {\n\t\t\t\tif receiver.ID == pkt.PayloadType {\n\t\t\t\t\tmpegts.TimestampToRTP(pkt, receiver.Codec)\n\t\t\t\t\treceiver.WriteRTP(pkt)\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (c *Client) Close() (err error) {\n\tif c.conn1 != nil {\n\t\terr = c.conn1.Close()\n\t}\n\tif c.conn2 != nil {\n\t\t_ = c.conn2.Close()\n\t}\n\treturn\n}\n\nfunc (c *Client) Request(conn net.Conn, body []byte) (string, error) {\n\t// TODO: fixme (size)\n\tbuf := bytes.NewBuffer(nil)\n\tbuf.WriteString(\"----client-stream-boundary--\\r\\n\")\n\tbuf.WriteString(\"Content-Type: application/json\\r\\n\")\n\tbuf.WriteString(\"Content-Length: \" + strconv.Itoa(len(body)) + \"\\r\\n\\r\\n\")\n\tbuf.Write(body)\n\tbuf.WriteString(\"\\r\\n\")\n\n\tif _, err := buf.WriteTo(conn); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tmpReader := multipart.NewReader(conn, \"--device-stream-boundary--\")\n\n\tfor {\n\t\tp, err := mpReader.NextRawPart()\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\tvar v struct {\n\t\t\tParams struct {\n\t\t\t\tSessionID string `json:\"session_id\"`\n\t\t\t} `json:\"params\"`\n\t\t}\n\n\t\tif err = json.NewDecoder(p).Decode(&v); err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\n\t\treturn v.Params.SessionID, nil\n\t}\n}\n\nfunc dial(req *http.Request, brand, username, password string) (net.Conn, *http.Response, error) {\n\tconn, err := net.DialTimeout(\"tcp\", req.URL.Host, core.ConnDialTimeout)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tif err = req.Write(conn); err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tr := bufio.NewReader(conn)\n\n\tres, err := http.ReadResponse(r, req)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\t_, _ = io.Copy(io.Discard, res.Body) // discard leftovers\n\t_ = res.Body.Close()                 // ignore response body\n\n\tauth := res.Header.Get(\"WWW-Authenticate\")\n\n\tif res.StatusCode != http.StatusUnauthorized || !strings.HasPrefix(auth, \"Digest\") {\n\t\treturn nil, nil, errors.New(\"tapo: wrond status: \" + res.Status)\n\t}\n\n\tif brand == \"tapo\" && password == \"\" {\n\t\t// support cloud password in place of username\n\t\tif strings.Contains(auth, `encrypt_type=\"3\"`) {\n\t\t\tpassword = fmt.Sprintf(\"%32X\", sha256.Sum256([]byte(username)))\n\t\t} else {\n\t\t\tpassword = fmt.Sprintf(\"%16X\", md5.Sum([]byte(username)))\n\t\t}\n\t\tusername = \"admin\"\n\t} else if brand == \"vigi\" && username == \"admin\" {\n\t\tpassword = securityEncode(password)\n\t}\n\n\trealm := tcp.Between(auth, `realm=\"`, `\"`)\n\tnonce := tcp.Between(auth, `nonce=\"`, `\"`)\n\tqop := tcp.Between(auth, `qop=\"`, `\"`)\n\turi := req.URL.RequestURI()\n\tha1 := tcp.HexMD5(username, realm, password)\n\tha2 := tcp.HexMD5(req.Method, uri)\n\tnc := \"00000001\"\n\tcnonce := core.RandString(32, 64)\n\tresponse := tcp.HexMD5(ha1, nonce, nc, cnonce, qop, ha2)\n\n\t// https://datatracker.ietf.org/doc/html/rfc7616\n\theader := fmt.Sprintf(\n\t\t`Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", qop=%s, nc=%s, cnonce=\"%s\", response=\"%s\"`,\n\t\tusername, realm, nonce, uri, qop, nc, cnonce, response,\n\t)\n\n\tif opaque := tcp.Between(auth, `opaque=\"`, `\"`); opaque != \"\" {\n\t\theader += fmt.Sprintf(`, opaque=\"%s\", algorithm=MD5`, opaque)\n\t}\n\n\treq.Header.Set(\"Authorization\", header)\n\n\tif err = req.Write(conn); err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\tif res, err = http.ReadResponse(r, req); err != nil {\n\t\treturn nil, nil, err\n\t}\n\n\treturn conn, res, nil\n}\n\nconst (\n\tkeyShort = \"RDpbLfCPsJZ7fiv\"\n\tkeyLong  = \"yLwVl0zKqws7LgKPRQ84Mdt708T1qQ3Ha7xv3H7NyU84p21BriUWBU43odz3iP4rBL3cD02KZciXTysVXiV8ngg6vL48rPJyAUw0HurW20xqxv9aYb4M9wK1Ae0wlro510qXeU07kV57fQMc8L6aLgMLwygtc0F10a0Dg70TOoouyFhdysuRMO51yY5ZlOZZLEal1h0t9YQW0Ko7oBwmCAHoic4HYbUyVeU3sfQ1xtXcPcf1aT303wAQhv66qzW\"\n)\n\nfunc securityEncode(s string) string {\n\tsize := len(s)\n\n\tvar n int // max\n\tif size > len(keyShort) {\n\t\tn = size\n\t} else {\n\t\tn = len(keyShort)\n\t}\n\n\tb := make([]byte, n)\n\n\tfor i := 0; i < n; i++ {\n\t\tc1 := 187\n\t\tc2 := 187\n\t\tif i >= size {\n\t\t\tc1 = int(keyShort[i])\n\t\t} else if i >= len(keyShort) {\n\t\t\tc2 = int(s[i])\n\t\t} else {\n\t\t\tc1 = int(keyShort[i])\n\t\t\tc2 = int(s[i])\n\t\t}\n\t\tb[i] = keyLong[(c1^c2)%len(keyLong)]\n\t}\n\n\treturn string(b)\n}\n"
  },
  {
    "path": "pkg/tapo/producer.go",
    "content": "package tapo\n\nimport (\n\t\"encoding/json\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/mpegts\"\n)\n\nfunc (c *Client) GetMedias() []*core.Media {\n\tif c.medias == nil {\n\t\t// don't know if all Tapo has this capabilities...\n\t\tc.medias = []*core.Media{\n\t\t\t{\n\t\t\t\tKind:      core.KindVideo,\n\t\t\t\tDirection: core.DirectionRecvonly,\n\t\t\t\tCodecs: []*core.Codec{\n\t\t\t\t\t{Name: core.CodecH264, ClockRate: 90000, PayloadType: core.PayloadTypeRAW},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tKind:      core.KindAudio,\n\t\t\t\tDirection: core.DirectionRecvonly,\n\t\t\t\tCodecs: []*core.Codec{\n\t\t\t\t\t{Name: core.CodecPCMA, ClockRate: 8000, PayloadType: 8},\n\t\t\t\t},\n\t\t\t},\n\t\t\t{\n\t\t\t\tKind:      core.KindAudio,\n\t\t\t\tDirection: core.DirectionSendonly,\n\t\t\t\tCodecs: []*core.Codec{\n\t\t\t\t\t{Name: core.CodecPCMA, ClockRate: 8000, PayloadType: 8},\n\t\t\t\t},\n\t\t\t},\n\t\t}\n\t}\n\n\treturn c.medias\n}\n\nfunc (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {\n\tfor _, track := range c.receivers {\n\t\tif track.Codec == codec {\n\t\t\treturn track, nil\n\t\t}\n\t}\n\n\tif err := c.SetupStream(); err != nil {\n\t\treturn nil, err\n\t}\n\n\ttrack := core.NewReceiver(media, codec)\n\tswitch media.Kind {\n\tcase core.KindVideo:\n\t\ttrack.ID = mpegts.StreamTypeH264\n\tcase core.KindAudio:\n\t\ttrack.ID = mpegts.StreamTypePCMATapo\n\t}\n\tc.receivers = append(c.receivers, track)\n\treturn track, nil\n}\n\nfunc (c *Client) Start() error {\n\treturn c.Handle()\n}\n\nfunc (c *Client) Stop() error {\n\tfor _, receiver := range c.receivers {\n\t\treceiver.Close()\n\t}\n\tif c.sender != nil {\n\t\tc.sender.Close()\n\t}\n\treturn c.Close()\n}\n\nfunc (c *Client) MarshalJSON() ([]byte, error) {\n\tinfo := &core.Connection{\n\t\tID:         core.ID(c),\n\t\tFormatName: c.url.Scheme,\n\t\tProtocol:   \"http\",\n\t\tMedias:     c.medias,\n\t\tRecv:       c.recv,\n\t\tReceivers:  c.receivers,\n\t\tSend:       c.send,\n\t}\n\tif c.sender != nil {\n\t\tinfo.Senders = []*core.Sender{c.sender}\n\t}\n\tif c.conn1 != nil {\n\t\tinfo.RemoteAddr = c.conn1.RemoteAddr().String()\n\t}\n\treturn json.Marshal(info)\n}\n"
  },
  {
    "path": "pkg/tcp/auth.go",
    "content": "package tcp\n\nimport (\n\t\"crypto/md5\"\n\t\"encoding/base64\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n)\n\ntype Auth struct {\n\tMethod  byte\n\tuser    string\n\tpass    string\n\theader  string\n\th1nonce string\n}\n\nconst (\n\tAuthNone byte = iota\n\tAuthUnknown\n\tAuthBasic\n\tAuthDigest\n\tAuthTPLink // https://drmnsamoliu.github.io/video.html\n)\n\nfunc NewAuth(user *url.Userinfo) *Auth {\n\ta := new(Auth)\n\ta.user = user.Username()\n\ta.pass, _ = user.Password()\n\tif a.user != \"\" {\n\t\ta.Method = AuthUnknown\n\t}\n\treturn a\n}\n\nfunc (a *Auth) Read(res *Response) bool {\n\tauth := res.Header.Get(\"WWW-Authenticate\")\n\tif len(auth) < 6 {\n\t\treturn false\n\t}\n\n\tswitch auth[:6] {\n\tcase \"Basic \":\n\t\ta.header = \"Basic \" + B64(a.user, a.pass)\n\t\ta.Method = AuthBasic\n\t\treturn true\n\tcase \"Digest\":\n\t\trealm := Between(auth, `realm=\"`, `\"`)\n\t\tnonce := Between(auth, `nonce=\"`, `\"`)\n\n\t\ta.h1nonce = HexMD5(a.user, realm, a.pass) + \":\" + nonce\n\t\ta.header = fmt.Sprintf(\n\t\t\t`Digest username=\"%s\", realm=\"%s\", nonce=\"%s\"`,\n\t\t\ta.user, realm, nonce,\n\t\t)\n\t\ta.Method = AuthDigest\n\t\treturn true\n\tdefault:\n\t\treturn false\n\t}\n}\n\nfunc (a *Auth) Write(req *Request) {\n\tif a == nil {\n\t\treturn\n\t}\n\n\tswitch a.Method {\n\tcase AuthBasic:\n\t\treq.Header.Set(\"Authorization\", a.header)\n\tcase AuthDigest:\n\t\t// important to use String except RequestURL for RtspServer:\n\t\t// https://github.com/AlexxIT/go2rtc/issues/244\n\t\turi := req.URL.String()\n\t\th2 := HexMD5(req.Method, uri)\n\t\tresponse := HexMD5(a.h1nonce, h2)\n\t\theader := a.header + fmt.Sprintf(\n\t\t\t`, uri=\"%s\", response=\"%s\"`, uri, response,\n\t\t)\n\t\treq.Header.Set(\"Authorization\", header)\n\tcase AuthTPLink:\n\t\treq.URL.Host = \"127.0.0.1\"\n\t}\n}\n\nfunc (a *Auth) Validate(req *Request) (valid, empty bool) {\n\tif a == nil {\n\t\treturn true, true\n\t}\n\n\theader := req.Header.Get(\"Authorization\")\n\tif header == \"\" {\n\t\treturn false, true\n\t}\n\n\tif a.Method == AuthUnknown {\n\t\ta.Method = AuthBasic\n\t\ta.header = \"Basic \" + B64(a.user, a.pass)\n\t}\n\n\treturn header == a.header, false\n}\n\nfunc (a *Auth) ReadNone(res *Response) bool {\n\tauth := res.Header.Get(\"WWW-Authenticate\")\n\tif strings.Contains(auth, \"TP-LINK Streaming Media\") {\n\t\ta.Method = AuthTPLink\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc (a *Auth) UserInfo() *url.Userinfo {\n\treturn url.UserPassword(a.user, a.pass)\n}\n\nfunc Between(s, sub1, sub2 string) string {\n\ti := strings.Index(s, sub1)\n\tif i < 0 {\n\t\treturn \"\"\n\t}\n\ts = s[i+len(sub1):]\n\ti = strings.Index(s, sub2)\n\tif i < 0 {\n\t\treturn \"\"\n\t}\n\treturn s[:i]\n}\n\nfunc HexMD5(s ...string) string {\n\tb := md5.Sum([]byte(strings.Join(s, \":\")))\n\treturn hex.EncodeToString(b[:])\n}\n\nfunc B64(s ...string) string {\n\tb := []byte(strings.Join(s, \":\"))\n\treturn base64.StdEncoding.EncodeToString(b)\n}\n"
  },
  {
    "path": "pkg/tcp/dial.go",
    "content": "package tcp\n\nimport (\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"net\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n)\n\n// Dial - for RTSP(S|X) and RTMP(S|X)\nfunc Dial(u *url.URL, timeout time.Duration) (net.Conn, error) {\n\tvar address string\n\tvar hostname string // without port\n\tif i := strings.IndexByte(u.Host, ':'); i > 0 {\n\t\taddress = u.Host\n\t\thostname = u.Host[:i]\n\t} else {\n\t\tswitch u.Scheme {\n\t\tcase \"rtsp\", \"rtsps\", \"rtspx\":\n\t\t\taddress = u.Host + \":554\"\n\t\tcase \"rtmp\":\n\t\t\taddress = u.Host + \":1935\"\n\t\tcase \"rtmps\", \"rtmpx\":\n\t\t\taddress = u.Host + \":443\"\n\t\t}\n\t\thostname = u.Host\n\t}\n\n\tvar secure *tls.Config\n\n\tswitch u.Scheme {\n\tcase \"rtsp\", \"rtmp\":\n\tcase \"rtsps\", \"rtspx\", \"rtmps\", \"rtmpx\":\n\t\tif u.Scheme[4] == 'x' || IsIP(hostname) {\n\t\t\tsecure = &tls.Config{InsecureSkipVerify: true}\n\t\t} else {\n\t\t\tsecure = &tls.Config{ServerName: hostname}\n\t\t}\n\tdefault:\n\t\treturn nil, errors.New(\"unsupported scheme: \" + u.Scheme)\n\t}\n\n\tconn, err := net.DialTimeout(\"tcp\", address, timeout)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif secure == nil {\n\t\treturn conn, nil\n\t}\n\n\ttlsConn := tls.Client(conn, secure)\n\tif err = tlsConn.Handshake(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif u.Scheme[4] == 'x' {\n\t\tu.Scheme = u.Scheme[:4] + \"s\"\n\t}\n\n\treturn tlsConn, nil\n}\n"
  },
  {
    "path": "pkg/tcp/request.go",
    "content": "package tcp\n\nimport (\n\t\"context\"\n\t\"crypto/tls\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\n// Do - http.Client with support Digest Authorization\nfunc Do(req *http.Request) (*http.Response, error) {\n\tvar secure *tls.Config\n\n\tswitch req.URL.Scheme {\n\tcase \"httpx\":\n\t\tsecure = insecureConfig\n\t\treq.URL.Scheme = \"https\"\n\tcase \"https\":\n\t\tif hostname := req.URL.Hostname(); IsIP(hostname) {\n\t\t\tsecure = insecureConfig\n\t\t}\n\t}\n\n\tif secure != nil {\n\t\tctx := context.WithValue(req.Context(), secureKey, secure)\n\t\treq = req.WithContext(ctx)\n\t}\n\n\tif client == nil {\n\t\ttransport := http.DefaultTransport.(*http.Transport).Clone()\n\n\t\tdial := transport.DialContext\n\t\ttransport.DialContext = func(ctx context.Context, network, addr string) (net.Conn, error) {\n\t\t\tconn, err := dial(ctx, network, addr)\n\t\t\tif pconn, ok := ctx.Value(connKey).(*net.Conn); ok {\n\t\t\t\t*pconn = conn\n\t\t\t}\n\t\t\treturn conn, err\n\t\t}\n\t\ttransport.DialTLSContext = func(ctx context.Context, network, addr string) (net.Conn, error) {\n\t\t\tconn, err := dial(ctx, network, addr)\n\t\t\tif err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tvar conf *tls.Config\n\t\t\tif v, ok := ctx.Value(secureKey).(*tls.Config); ok {\n\t\t\t\tconf = v\n\t\t\t} else if host, _, err := net.SplitHostPort(addr); err != nil {\n\t\t\t\tconf = &tls.Config{ServerName: addr}\n\t\t\t} else {\n\t\t\t\tconf = &tls.Config{ServerName: host}\n\t\t\t}\n\n\t\t\ttlsConn := tls.Client(conn, conf)\n\t\t\tif err = tlsConn.Handshake(); err != nil {\n\t\t\t\treturn nil, err\n\t\t\t}\n\n\t\t\tif pconn, ok := ctx.Value(connKey).(*net.Conn); ok {\n\t\t\t\t*pconn = tlsConn\n\t\t\t}\n\t\t\treturn tlsConn, err\n\t\t}\n\n\t\tclient = &http.Client{Transport: transport}\n\t}\n\n\tuser := req.URL.User\n\n\t// Hikvision won't answer on Basic auth with any headers\n\tif strings.HasPrefix(req.URL.Path, \"/ISAPI/\") {\n\t\treq.URL.User = nil\n\t}\n\n\tres, err := client.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif res.StatusCode == http.StatusUnauthorized && user != nil {\n\t\tClose(res)\n\n\t\tauth := res.Header.Get(\"WWW-Authenticate\")\n\t\tif !strings.HasPrefix(auth, \"Digest\") {\n\t\t\treturn nil, errors.New(\"unsupported auth: \" + auth)\n\t\t}\n\n\t\trealm := Between(auth, `realm=\"`, `\"`)\n\t\tnonce := Between(auth, `nonce=\"`, `\"`)\n\t\tqop := Between(auth, `qop=\"`, `\"`)\n\n\t\tusername := user.Username()\n\t\tpassword, _ := user.Password()\n\t\tha1 := HexMD5(username, realm, password)\n\n\t\turi := req.URL.RequestURI()\n\t\tha2 := HexMD5(req.Method, uri)\n\n\t\tvar header string\n\n\t\tswitch qop {\n\t\tcase \"\":\n\t\t\tresponse := HexMD5(ha1, nonce, ha2)\n\t\t\theader = fmt.Sprintf(\n\t\t\t\t`Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", response=\"%s\"`,\n\t\t\t\tusername, realm, nonce, uri, response,\n\t\t\t)\n\t\tcase \"auth\":\n\t\t\tnc := \"00000001\"\n\t\t\tcnonce := core.RandString(32, 64)\n\t\t\tresponse := HexMD5(ha1, nonce, nc, cnonce, qop, ha2)\n\t\t\theader = fmt.Sprintf(\n\t\t\t\t`Digest username=\"%s\", realm=\"%s\", nonce=\"%s\", uri=\"%s\", qop=%s, nc=%s, cnonce=\"%s\", response=\"%s\"`,\n\t\t\t\tusername, realm, nonce, uri, qop, nc, cnonce, response,\n\t\t\t)\n\t\tdefault:\n\t\t\treturn nil, errors.New(\"unsupported qop: \" + auth)\n\t\t}\n\n\t\treq.Header.Set(\"Authorization\", header)\n\n\t\tif res, err = client.Do(req); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn res, nil\n}\n\nvar client *http.Client\n\ntype key string\n\nvar connKey = key(\"conn\")\nvar secureKey = key(\"secure\")\n\nvar insecureConfig = &tls.Config{\n\tInsecureSkipVerify: true,\n\tCipherSuites: []uint16{\n\t\ttls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,\n\t\ttls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,\n\t\ttls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,\n\t\ttls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,\n\t\ttls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,\n\n\t\t// this cipher suites disabled starting from https://tip.golang.org/doc/go1.22\n\t\t// but cameras can't work without them https://github.com/AlexxIT/go2rtc/issues/1172\n\t\ttls.TLS_RSA_WITH_AES_128_GCM_SHA256, // insecure\n\t\ttls.TLS_RSA_WITH_AES_256_GCM_SHA384, // insecure\n\t},\n}\n\nfunc WithConn() (context.Context, *net.Conn) {\n\tpconn := new(net.Conn)\n\treturn context.WithValue(context.Background(), connKey, pconn), pconn\n}\n\nfunc Close(res *http.Response) {\n\tif res.Body != nil {\n\t\t_ = res.Body.Close()\n\t}\n}\n\nfunc IsIP(hostname string) bool {\n\treturn net.ParseIP(hostname) != nil\n}\n"
  },
  {
    "path": "pkg/tcp/textproto.go",
    "content": "package tcp\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/textproto\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n)\n\nconst EndLine = \"\\r\\n\"\n\n// Response like http.Response, but with any proto\ntype Response struct {\n\tStatus     string\n\tStatusCode int\n\tProto      string\n\tHeader     textproto.MIMEHeader\n\tBody       []byte\n\tRequest    *Request\n}\n\nfunc (r Response) String() string {\n\ts := r.Proto + \" \" + r.Status + EndLine\n\tfor k, v := range r.Header {\n\t\ts += k + \": \" + v[0] + EndLine\n\t}\n\ts += EndLine\n\tif r.Body != nil {\n\t\ts += string(r.Body)\n\t}\n\treturn s\n}\n\nfunc (r *Response) Write(w io.Writer) (err error) {\n\t_, err = w.Write([]byte(r.String()))\n\treturn\n}\n\nfunc ReadResponse(r *bufio.Reader) (*Response, error) {\n\ttp := textproto.NewReader(r)\n\n\tline, err := tp.ReadLine()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif line == \"\" {\n\t\treturn nil, errors.New(\"empty response on RTSP request\")\n\t}\n\n\tss := strings.SplitN(line, \" \", 3)\n\tif len(ss) != 3 {\n\t\treturn nil, fmt.Errorf(\"malformed response: %s\", line)\n\t}\n\n\tres := &Response{\n\t\tStatus: ss[1] + \" \" + ss[2],\n\t\tProto:  ss[0],\n\t}\n\n\tres.StatusCode, err = strconv.Atoi(ss[1])\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tres.Header, err = tp.ReadMIMEHeader()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif val := res.Header.Get(\"Content-Length\"); val != \"\" {\n\t\tvar i int\n\t\ti, err = strconv.Atoi(val)\n\t\tres.Body = make([]byte, i)\n\t\tif _, err = io.ReadAtLeast(r, res.Body, i); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn res, nil\n}\n\n// Request like http.Request, but with any proto\ntype Request struct {\n\tMethod string\n\tURL    *url.URL\n\tProto  string\n\tHeader textproto.MIMEHeader\n\tBody   []byte\n}\n\nfunc (r *Request) String() string {\n\ts := r.Method + \" \" + r.URL.String() + \" \" + r.Proto + EndLine\n\tfor k, v := range r.Header {\n\t\ts += k + \": \" + v[0] + EndLine\n\t}\n\ts += EndLine\n\tif r.Body != nil {\n\t\ts += string(r.Body)\n\t}\n\treturn s\n}\n\nfunc (r *Request) Write(w io.Writer) (err error) {\n\t_, err = w.Write([]byte(r.String()))\n\treturn\n}\n\nfunc ReadRequest(r *bufio.Reader) (*Request, error) {\n\ttp := textproto.NewReader(r)\n\n\tline, err := tp.ReadLine()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tss := strings.SplitN(line, \" \", 3)\n\tif len(ss) != 3 {\n\t\treturn nil, fmt.Errorf(\"wrong request: %s\", line)\n\t}\n\n\treq := &Request{\n\t\tMethod: ss[0],\n\t\tProto:  ss[2],\n\t}\n\n\treq.URL, err = url.Parse(ss[1])\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq.Header, err = tp.ReadMIMEHeader()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif val := req.Header.Get(\"Content-Length\"); val != \"\" {\n\t\tvar i int\n\t\ti, err = strconv.Atoi(val)\n\t\treq.Body = make([]byte, i)\n\t\tif _, err = io.ReadAtLeast(r, req.Body, i); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn req, nil\n}\n"
  },
  {
    "path": "pkg/tcp/textproto_test.go",
    "content": "package tcp\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"net/http\"\n\t\"testing\"\n)\n\nfunc assert(t *testing.T, one, two any) {\n\tif one != two {\n\t\tt.FailNow()\n\t}\n}\n\nfunc TestName(t *testing.T) {\n\tdata := []byte(`RTSP/1.0 401 Unauthorized\nWWW-Authenticate: Digest realm=\"testrealm@host.com\",\n                        nonce=\"dcd98b7102dd2f0e8b11d0f600bfb0c093\",\n\n`)\n\n\tbuf := bytes.NewBuffer(data)\n\tr := bufio.NewReader(buf)\n\n\tres, err := ReadResponse(r)\n\tassert(t, err, nil)\n\n\tassert(t, res.StatusCode, http.StatusUnauthorized)\n}\n"
  },
  {
    "path": "pkg/tcp/websocket/client.go",
    "content": "package websocket\n\nimport (\n\tcryptorand \"crypto/rand\"\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"time\"\n)\n\nconst BinaryMessage = 2\n\ntype Client struct {\n\tconn   net.Conn\n\tremain int\n}\n\nfunc NewClient(conn net.Conn) *Client {\n\treturn &Client{conn: conn}\n}\n\nconst finalBit = 0x80\nconst maskBit = 0x80\n\nfunc (w *Client) Read(b []byte) (n int, err error) {\n\tif w.remain == 0 {\n\t\tb2 := make([]byte, 2)\n\t\tif _, err = io.ReadFull(w.conn, b2); err != nil {\n\t\t\treturn 0, err\n\t\t}\n\n\t\tframeType := b2[0] & 0xF\n\t\tw.remain = int(b2[1] & 0x7F)\n\n\t\tswitch frameType {\n\t\tcase BinaryMessage:\n\t\tdefault:\n\t\t\treturn 0, fmt.Errorf(\"unsupported frame type: %d\", frameType)\n\t\t}\n\n\t\tswitch w.remain {\n\t\tcase 126:\n\t\t\tif _, err = io.ReadFull(w.conn, b2); err != nil {\n\t\t\t\treturn 0, err\n\t\t\t}\n\t\t\tw.remain = int(binary.BigEndian.Uint16(b2))\n\t\tcase 127:\n\t\t\tb8 := make([]byte, 8)\n\t\t\tif _, err = io.ReadFull(w.conn, b8); err != nil {\n\t\t\t\treturn 0, err\n\t\t\t}\n\t\t\tw.remain = int(binary.BigEndian.Uint64(b8))\n\t\t}\n\t}\n\n\tif w.remain > len(b) {\n\t\tn, err = io.ReadFull(w.conn, b)\n\t\tw.remain -= n\n\t\treturn\n\t}\n\n\tn, err = io.ReadFull(w.conn, b[:w.remain])\n\tw.remain = 0\n\n\treturn\n}\n\nfunc (w *Client) Write(b []byte) (n int, err error) {\n\tvar data []byte\n\tvar start byte\n\n\tsize := len(b)\n\n\tswitch {\n\tcase size > 65535:\n\t\tstart = 10\n\t\tdata = make([]byte, size+14)\n\t\tdata[1] = maskBit | 127\n\t\tbinary.BigEndian.PutUint64(data[2:], uint64(size))\n\tcase size > 125:\n\t\tstart = 4\n\t\tdata = make([]byte, size+8)\n\t\tdata[1] = maskBit | 126\n\t\tbinary.BigEndian.PutUint16(data[2:], uint16(size))\n\tdefault:\n\t\tstart = 2\n\t\tdata = make([]byte, size+6)\n\t\tdata[1] = maskBit | byte(size)\n\t}\n\n\tdata[0] = BinaryMessage | finalBit\n\n\tmask := data[start : start+4]\n\tmsg := data[start+4:]\n\n\tif _, err = cryptorand.Read(mask); err != nil {\n\t\treturn 0, err\n\t}\n\n\tfor i := 0; i < len(b); i++ {\n\t\tmsg[i] = b[i] ^ mask[i%4]\n\t}\n\n\treturn w.conn.Write(data)\n}\n\nfunc (w *Client) Close() error {\n\treturn w.conn.Close()\n}\n\nfunc (w *Client) LocalAddr() net.Addr {\n\treturn w.conn.LocalAddr()\n}\n\nfunc (w *Client) RemoteAddr() net.Addr {\n\treturn w.conn.RemoteAddr()\n}\n\nfunc (w *Client) SetDeadline(t time.Time) error {\n\treturn w.conn.SetDeadline(t)\n}\n\nfunc (w *Client) SetReadDeadline(t time.Time) error {\n\treturn w.conn.SetReadDeadline(t)\n}\n\nfunc (w *Client) SetWriteDeadline(t time.Time) error {\n\treturn w.conn.SetWriteDeadline(t)\n}\n"
  },
  {
    "path": "pkg/tcp/websocket/dial.go",
    "content": "package websocket\n\nimport (\n\tcryptorand \"crypto/rand\"\n\t\"crypto/sha1\"\n\t\"encoding/base64\"\n\t\"errors\"\n\t\"net\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/tcp\"\n)\n\nfunc Dial(address string) (net.Conn, error) {\n\tif strings.HasPrefix(address, \"ws\") {\n\t\taddress = \"http\" + address[2:] // support http and https\n\t}\n\n\t// using custom client for support Digest Auth\n\t// https://github.com/AlexxIT/go2rtc/issues/415\n\tctx, pconn := tcp.WithConn()\n\n\treq, err := http.NewRequestWithContext(ctx, \"GET\", address, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tkey, accept := GetKeyAccept()\n\n\t// Version, Key, Protocol important for Axis cameras\n\treq.Header.Set(\"Connection\", \"Upgrade\")\n\treq.Header.Set(\"Upgrade\", \"websocket\")\n\treq.Header.Set(\"Sec-WebSocket-Version\", \"13\")\n\treq.Header.Set(\"Sec-WebSocket-Key\", key)\n\treq.Header.Set(\"Sec-WebSocket-Protocol\", \"binary\")\n\n\tres, err := tcp.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif res.StatusCode != http.StatusSwitchingProtocols {\n\t\treturn nil, errors.New(\"wrong status: \" + res.Status)\n\t}\n\n\tif res.Header.Get(\"Sec-Websocket-Accept\") != accept {\n\t\treturn nil, errors.New(\"wrong websocket accept\")\n\t}\n\n\treturn NewClient(*pconn), nil\n}\n\nfunc GetKeyAccept() (key, accept string) {\n\tb := make([]byte, 16)\n\t_, _ = cryptorand.Read(b)\n\tkey = base64.StdEncoding.EncodeToString(b)\n\n\th := sha1.New()\n\th.Write([]byte(key))\n\th.Write([]byte(\"258EAFA5-E914-47DA-95CA-C5AB0DC85B11\"))\n\taccept = base64.StdEncoding.EncodeToString(h.Sum(nil))\n\n\treturn\n}\n"
  },
  {
    "path": "pkg/tutk/codec.go",
    "content": "package tutk\n\n// https://github.com/seydx/tutk_wyze#11-codec-reference\nconst (\n\tCodecMPEG4 byte = 0x4C\n\tCodecH263  byte = 0x4D\n\tCodecH264  byte = 0x4E\n\tCodecMJPEG byte = 0x4F\n\tCodecH265  byte = 0x50\n)\n\nconst (\n\tCodecAACRaw  byte = 0x86\n\tCodecAACADTS byte = 0x87\n\tCodecAACLATM byte = 0x88\n\tCodecPCMU    byte = 0x89\n\tCodecPCMA    byte = 0x8A\n\tCodecADPCM   byte = 0x8B\n\tCodecPCML    byte = 0x8C\n\tCodecSPEEX   byte = 0x8D\n\tCodecMP3     byte = 0x8E\n\tCodecG726    byte = 0x8F\n\tCodecAACAlt  byte = 0x90\n\tCodecOpus    byte = 0x92\n)\n\nvar sampleRates = [9]uint32{8000, 11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000}\n\nfunc GetSampleRateIndex(sampleRate uint32) uint8 {\n\tfor i, rate := range sampleRates {\n\t\tif rate == sampleRate {\n\t\t\treturn uint8(i)\n\t\t}\n\t}\n\treturn 3 // default 16kHz\n}\n\nfunc GetSamplesPerFrame(codecID byte) uint32 {\n\tswitch codecID {\n\tcase CodecAACRaw, CodecAACADTS, CodecAACLATM, CodecAACAlt:\n\t\treturn 1024\n\tcase CodecPCMU, CodecPCMA, CodecPCML, CodecADPCM, CodecSPEEX, CodecG726:\n\t\treturn 160\n\tcase CodecMP3:\n\t\treturn 1152\n\tcase CodecOpus:\n\t\treturn 960\n\tdefault:\n\t\treturn 1024\n\t}\n}\n\nfunc IsVideoCodec(id byte) bool {\n\treturn id >= CodecMPEG4 && id <= CodecH265\n}\n\nfunc IsAudioCodec(id byte) bool {\n\treturn id >= CodecAACRaw && id <= CodecOpus\n}\n"
  },
  {
    "path": "pkg/tutk/conn.go",
    "content": "package tutk\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n)\n\nfunc Dial(host, uid, username, password string) (*Conn, error) {\n\taddr, err := net.ResolveUDPAddr(\"udp\", host)\n\tif err != nil {\n\t\t// Default port for listening incoming LAN connections.\n\t\t// Important. It's not using for real connection.\n\t\taddr = &net.UDPAddr{IP: net.ParseIP(host), Port: 32761}\n\t}\n\n\tudpConn, err := net.ListenUDP(\"udp\", nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tc := &Conn{UDPConn: udpConn, addr: addr}\n\n\tsid := GenSessionID()\n\n\t_ = c.SetDeadline(time.Now().Add(5 * time.Second))\n\n\tif addr.Port != 10001 {\n\t\terr = c.connectDirect(uid, sid)\n\t} else {\n\t\terr = c.connectRemote(uid, sid)\n\t}\n\tif err != nil {\n\t\t_ = c.Close()\n\t\treturn nil, err\n\t}\n\n\tif c.ver[0] >= 25 {\n\t\tc.session = NewSession25(c, sid)\n\t} else {\n\t\tc.session = NewSession16(c, sid)\n\t}\n\n\tif err = c.clientStart(username, password); err != nil {\n\t\t_ = c.Close()\n\t\treturn nil, err\n\t}\n\n\tgo c.worker()\n\n\treturn c, nil\n}\n\ntype Conn struct {\n\t*net.UDPConn\n\taddr    *net.UDPAddr\n\tsession Session\n\n\tver    []byte\n\terr    error\n\tcmdMu  sync.Mutex\n\tcmdAck func()\n}\n\n// Read overwrite net.Conn\nfunc (c *Conn) Read(buf []byte) (n int, err error) {\n\tfor {\n\t\tvar addr *net.UDPAddr\n\t\tif n, addr, err = c.UDPConn.ReadFromUDP(buf); err != nil {\n\t\t\treturn 0, err\n\t\t}\n\n\t\tif string(c.addr.IP) != string(addr.IP) || n < 16 {\n\t\t\tcontinue // skip messages from another IP\n\t\t}\n\n\t\tif c.addr.Port != addr.Port {\n\t\t\tc.addr.Port = addr.Port\n\t\t}\n\n\t\tReverseTransCodePartial(buf, buf[:n])\n\t\t//log.Printf(\"<- %x\", buf[:n])\n\t\treturn n, nil\n\t}\n}\n\n// Write overwrite net.Conn\nfunc (c *Conn) Write(b []byte) (n int, err error) {\n\t//log.Printf(\"-> %x\", b)\n\treturn c.UDPConn.WriteToUDP(TransCodePartial(nil, b), c.addr)\n}\n\n// RemoteAddr overwrite net.Conn\nfunc (c *Conn) RemoteAddr() net.Addr {\n\treturn c.addr\n}\n\nfunc (c *Conn) Protocol() string {\n\treturn \"tutk+udp\"\n}\n\nfunc (c *Conn) Version() string {\n\tif len(c.ver) == 1 {\n\t\treturn fmt.Sprintf(\"TUTK/%d\", c.ver[0])\n\t}\n\treturn fmt.Sprintf(\"TUTK/%d SDK %d.%d.%d.%d\", c.ver[0], c.ver[1], c.ver[2], c.ver[3], c.ver[4])\n}\n\nfunc (c *Conn) ReadCommand() (ctrlType uint32, ctrlData []byte, err error) {\n\treturn c.session.RecvIOCtrl()\n}\n\nfunc (c *Conn) WriteCommand(ctrlType uint32, ctrlData []byte) error {\n\tc.cmdMu.Lock()\n\tdefer c.cmdMu.Unlock()\n\n\tvar repeat atomic.Int32\n\trepeat.Store(5)\n\n\ttimeout := time.NewTicker(time.Second)\n\tdefer timeout.Stop()\n\n\tc.cmdAck = func() {\n\t\trepeat.Store(0)\n\t\ttimeout.Reset(1)\n\t}\n\n\tbuf := c.session.SendIOCtrl(ctrlType, ctrlData)\n\n\tfor {\n\t\tif err := c.session.SessionWrite(0, buf); err != nil {\n\t\t\treturn err\n\t\t}\n\t\t<-timeout.C\n\t\tr := repeat.Add(-1)\n\t\tif r < 0 {\n\t\t\treturn nil\n\t\t}\n\t\tif r == 0 {\n\t\t\treturn fmt.Errorf(\"%s: can't send command %d\", \"tutk\", ctrlType)\n\t\t}\n\t}\n}\n\nfunc (c *Conn) ReadPacket() (hdr, payload []byte, err error) {\n\treturn c.session.RecvFrameData()\n}\n\nfunc (c *Conn) WritePacket(hdr, payload []byte) error {\n\tbuf := c.session.SendFrameData(hdr, payload)\n\treturn c.session.SessionWrite(1, buf)\n}\n\nfunc (c *Conn) Error() error {\n\tif c.err != nil {\n\t\treturn c.err\n\t}\n\treturn io.EOF\n}\n\nfunc (c *Conn) worker() {\n\tdefer c.session.Close()\n\n\tbuf := make([]byte, 1200)\n\n\tfor {\n\t\tn, err := c.Read(buf)\n\t\tif err != nil {\n\t\t\tc.err = fmt.Errorf(\"%s: %w\", \"tutk\", err)\n\t\t\treturn\n\t\t}\n\n\t\tswitch c.handleMsg(buf[:n]) {\n\t\tcase msgUnknown:\n\t\t\tfmt.Printf(\"tutk: unknown msg: %x\\n\", buf[:n])\n\t\tcase msgError:\n\t\t\treturn\n\t\tcase msgCommandAck:\n\t\t\tif c.cmdAck != nil {\n\t\t\t\tc.cmdAck()\n\t\t\t}\n\t\t}\n\t}\n}\n\nconst (\n\tmsgUnknown = iota\n\tmsgError\n\tmsgPing\n\tmsgUnknownPing\n\tmsgClientStart\n\tmsgClientStart2\n\tmsgClientStartAck2\n\tmsgCommand\n\tmsgCommandAck\n\tmsgCounters\n\tmsgMediaChunk\n\tmsgMediaFrame\n\tmsgMediaReorder\n\tmsgMediaLost\n\tmsgCh5\n\n\tmsgUnknown0007 // time sync without data?\n\tmsgUnknown0008 // time sync with data?\n\tmsgUnknown0010\n\tmsgUnknown0013\n\tmsgUnknown0900\n\tmsgUnknown0a08\n\tmsgUnknownCh1c\n\tmsgDafang0012\n)\n\nfunc (c *Conn) handleMsg(msg []byte) int {\n\t// off sample\n\t// 0   0402      tutk magic\n\t// 2   120a      tutk version (120a, 190a...)\n\t// 4   0800      msg size = len(b)-16\n\t// 6   0000      channel seq\n\t// 8   28041200  msg type\n\t// 14  0100      channel (not all msg)\n\t// 28  0700      msg data (not all msg)\n\tswitch msg[8] {\n\tcase 0x08:\n\t\tswitch ch := msg[14]; ch {\n\t\tcase 0, 1:\n\t\t\treturn c.session.SessionRead(ch, msg[28:])\n\t\tcase 5:\n\t\t\tif len(msg) == 48 {\n\t\t\t\t_, _ = c.Write(msgAckCh5(msg))\n\t\t\t\treturn msgCh5\n\t\t\t}\n\t\tcase 0x1c:\n\t\t\treturn msgUnknownCh1c\n\t\t}\n\tcase 0x18:\n\t\treturn msgUnknownPing\n\tcase 0x28:\n\t\tif len(msg) == 24 {\n\t\t\t_, _ = c.Write(msgAckPing(msg))\n\t\t\treturn msgPing\n\t\t}\n\t}\n\treturn msgUnknown\n}\n\nfunc msgAckPing(msg []byte) []byte {\n\t// <- [24] 0402120a 08000000 28041200 000000005b0d4202070aa8c0\n\t// -> [24] 04021a0a 08000000 27042100 000000005b0d4202070aa8c0\n\tmsg[8] = 0x27\n\tmsg[10] = 0x21\n\treturn msg\n}\n\nfunc msgAckCh5(msg []byte) []byte {\n\t// <- [48] 0402190a 20000400 07042100 7ecc05000c0000007ecc93c456c2561f 5a97c2f101050000000000000000000000010000\n\t// -> [48] 0402190a 20000400 08041200 7ecc05000c0000007ecc93c456c2561f 5a97c2f141050000000000000000000000010000\n\tmsg[8] = 0x07\n\tmsg[10] = 0x21\n\tmsg[32] = 0x41\n\treturn msg\n}\n"
  },
  {
    "path": "pkg/tutk/crypto.go",
    "content": "package tutk\n\nimport (\n\t\"encoding/binary\"\n\t\"math/bits\"\n)\n\n// I'd like to say hello to Charlie. Your name is forever etched into the history of streaming software.\nconst charlie = \"Charlie is the designer of P2P!!\"\n\nfunc ReverseTransCodePartial(dst, src []byte) []byte {\n\tn := len(src)\n\ttmp := make([]byte, n)\n\tif len(dst) < n {\n\t\tdst = make([]byte, n)\n\t}\n\n\tsrc16 := src\n\ttmp16 := tmp\n\tdst16 := dst\n\n\tfor ; n >= 16; n -= 16 {\n\t\tfor i := 0; i != 16; i += 4 {\n\t\t\tx := binary.LittleEndian.Uint32(src16[i:])\n\t\t\tbinary.LittleEndian.PutUint32(tmp16[i:], bits.RotateLeft32(x, i+3))\n\t\t}\n\n\t\tswap(dst16, tmp16, 16)\n\n\t\tfor i := 0; i != 16; i++ {\n\t\t\ttmp16[i] = dst16[i] ^ charlie[i]\n\t\t}\n\n\t\tfor i := 0; i != 16; i += 4 {\n\t\t\tx := binary.LittleEndian.Uint32(tmp16[i:])\n\t\t\tbinary.LittleEndian.PutUint32(dst16[i:], bits.RotateLeft32(x, i+1))\n\t\t}\n\n\t\ttmp16 = tmp16[16:]\n\t\tdst16 = dst16[16:]\n\t\tsrc16 = src16[16:]\n\t}\n\n\tswap(tmp16, src16, n)\n\n\tfor i := 0; i < n; i++ {\n\t\tdst16[i] = tmp16[i] ^ charlie[i]\n\t}\n\n\treturn dst\n}\n\nfunc ReverseTransCodeBlob(src []byte) []byte {\n\tif len(src) < 16 {\n\t\treturn ReverseTransCodePartial(nil, src)\n\t}\n\n\tdst := make([]byte, len(src))\n\theader := ReverseTransCodePartial(nil, src[:16])\n\tcopy(dst, header)\n\n\tif len(src) > 16 {\n\t\tif dst[3]&1 != 0 { // Partial encryption (check decrypted header)\n\t\t\tremaining := len(src) - 16\n\t\t\tdecryptLen := min(remaining, 48)\n\t\t\tif decryptLen > 0 {\n\t\t\t\tdecrypted := ReverseTransCodePartial(nil, src[16:16+decryptLen])\n\t\t\t\tcopy(dst[16:], decrypted)\n\t\t\t}\n\t\t\tif remaining > 48 {\n\t\t\t\tcopy(dst[64:], src[64:])\n\t\t\t}\n\t\t} else { // Full decryption\n\t\t\tdecrypted := ReverseTransCodePartial(nil, src[16:])\n\t\t\tcopy(dst[16:], decrypted)\n\t\t}\n\t}\n\treturn dst\n}\n\nfunc TransCodePartial(dst, src []byte) []byte {\n\tn := len(src)\n\ttmp := make([]byte, n)\n\tif len(dst) < n {\n\t\tdst = make([]byte, n)\n\t}\n\n\tsrc16 := src\n\ttmp16 := tmp\n\tdst16 := dst\n\n\tfor ; n >= 16; n -= 16 {\n\t\tfor i := 0; i != 16; i += 4 {\n\t\t\tx := binary.LittleEndian.Uint32(src16[i:])\n\t\t\tbinary.LittleEndian.PutUint32(tmp16[i:], bits.RotateLeft32(x, -i-1))\n\t\t}\n\n\t\tfor i := 0; i != 16; i++ {\n\t\t\tdst16[i] = tmp16[i] ^ charlie[i]\n\t\t}\n\n\t\tswap(tmp16, dst16, 16)\n\n\t\tfor i := 0; i != 16; i += 4 {\n\t\t\tx := binary.LittleEndian.Uint32(tmp16[i:])\n\t\t\tbinary.LittleEndian.PutUint32(dst16[i:], bits.RotateLeft32(x, -i-3))\n\t\t}\n\n\t\ttmp16 = tmp16[16:]\n\t\tdst16 = dst16[16:]\n\t\tsrc16 = src16[16:]\n\t}\n\n\tfor i := 0; i < n; i++ {\n\t\ttmp16[i] = src16[i] ^ charlie[i]\n\t}\n\n\tswap(dst16, tmp16, n)\n\n\treturn dst\n}\n\nfunc TransCodeBlob(src []byte) []byte {\n\tif len(src) < 16 {\n\t\treturn TransCodePartial(nil, src)\n\t}\n\n\tdst := make([]byte, len(src))\n\theader := TransCodePartial(nil, src[:16])\n\tcopy(dst, header)\n\n\tif len(src) > 16 {\n\t\tif src[3]&1 != 0 { // Partial encryption\n\t\t\tremaining := len(src) - 16\n\t\t\tencryptLen := min(remaining, 48)\n\t\t\tif encryptLen > 0 {\n\t\t\t\tencrypted := TransCodePartial(nil, src[16:16+encryptLen])\n\t\t\t\tcopy(dst[16:], encrypted)\n\t\t\t}\n\t\t\tif remaining > 48 {\n\t\t\t\tcopy(dst[64:], src[64:])\n\t\t\t}\n\t\t} else { // Full encryption\n\t\t\tencrypted := TransCodePartial(nil, src[16:])\n\t\t\tcopy(dst[16:], encrypted)\n\t\t}\n\t}\n\treturn dst\n}\n\nfunc swap(dst, src []byte, n int) {\n\tswitch n {\n\tcase 2:\n\t\t_, _ = src[1], dst[1]\n\t\tdst[0] = src[1]\n\t\tdst[1] = src[0]\n\t\treturn\n\tcase 4:\n\t\t_, _ = src[3], dst[3]\n\t\tdst[0] = src[2]\n\t\tdst[1] = src[3]\n\t\tdst[2] = src[0]\n\t\tdst[3] = src[1]\n\t\treturn\n\tcase 8:\n\t\t_, _ = src[7], dst[7]\n\t\tdst[0] = src[7]\n\t\tdst[1] = src[4]\n\t\tdst[2] = src[3]\n\t\tdst[3] = src[2]\n\t\tdst[4] = src[1]\n\t\tdst[5] = src[6]\n\t\tdst[6] = src[5]\n\t\tdst[7] = src[0]\n\t\treturn\n\tcase 16:\n\t\t_, _ = src[15], dst[15]\n\t\tdst[0] = src[11]\n\t\tdst[1] = src[9]\n\t\tdst[2] = src[8]\n\t\tdst[3] = src[15]\n\t\tdst[4] = src[13]\n\t\tdst[5] = src[10]\n\t\tdst[6] = src[12]\n\t\tdst[7] = src[14]\n\t\tdst[8] = src[2]\n\t\tdst[9] = src[1]\n\t\tdst[10] = src[5]\n\t\tdst[11] = src[0]\n\t\tdst[12] = src[6]\n\t\tdst[13] = src[4]\n\t\tdst[14] = src[7]\n\t\tdst[15] = src[3]\n\t\treturn\n\t}\n\tcopy(dst, src[:n])\n}\n\nconst delta = 0x9e3779b9\n\nfunc XXTEADecrypt(dst, src, key []byte) {\n\tconst n = int8(4) // support only 16 bytes src\n\n\tvar w, k [n]uint32\n\tfor i := int8(0); i < n; i++ {\n\t\tw[i] = binary.LittleEndian.Uint32(src)\n\t\tk[i] = binary.LittleEndian.Uint32(key)\n\t\tsrc = src[4:]\n\t\tkey = key[4:]\n\t}\n\n\trounds := 52/n + 6\n\tsum := uint32(rounds) * delta\n\tfor ; rounds > 0; rounds-- {\n\t\tw0 := w[0]\n\t\ti2 := int8((sum >> 2) & 3)\n\t\tfor i := n - 1; i >= 0; i-- {\n\t\t\twi := w[(i-1)&3]\n\t\t\tki := k[i^i2]\n\t\t\tt1 := (w0 ^ sum) + (wi ^ ki)\n\t\t\tt2 := (wi >> 5) ^ (w0 << 2)\n\t\t\tt3 := (w0 >> 3) ^ (wi << 4)\n\t\t\tw[i] -= t1 ^ (t2 + t3)\n\t\t\tw0 = w[i]\n\t\t}\n\t\tsum -= delta\n\t}\n\n\tfor _, i := range w {\n\t\tbinary.LittleEndian.PutUint32(dst, i)\n\t\tdst = dst[4:]\n\t}\n}\n\nfunc XXTEADecryptVar(data, key []byte) []byte {\n\tif len(data) < 8 || len(key) < 16 {\n\t\treturn nil\n\t}\n\n\tk := make([]uint32, 4)\n\tfor i := range 4 {\n\t\tk[i] = binary.LittleEndian.Uint32(key[i*4:])\n\t}\n\n\tn := max(len(data)/4, 2)\n\tv := make([]uint32, n)\n\tfor i := 0; i < len(data)/4; i++ {\n\t\tv[i] = binary.LittleEndian.Uint32(data[i*4:])\n\t}\n\n\trounds := 6 + 52/n\n\tsum := uint32(rounds) * delta\n\ty := v[0]\n\n\tfor rounds > 0 {\n\t\te := (sum >> 2) & 3\n\t\tfor p := n - 1; p > 0; p-- {\n\t\t\tz := v[p-1]\n\t\t\tv[p] -= xxteaMX(sum, y, z, p, e, k)\n\t\t\ty = v[p]\n\t\t}\n\t\tz := v[n-1]\n\t\tv[0] -= xxteaMX(sum, y, z, 0, e, k)\n\t\ty = v[0]\n\t\tsum -= delta\n\t\trounds--\n\t}\n\n\tresult := make([]byte, n*4)\n\tfor i := range n {\n\t\tbinary.LittleEndian.PutUint32(result[i*4:], v[i])\n\t}\n\n\treturn result[:len(data)]\n}\n\nfunc xxteaMX(sum, y, z uint32, p int, e uint32, k []uint32) uint32 {\n\treturn ((z>>5 ^ y<<2) + (y>>3 ^ z<<4)) ^ ((sum ^ y) + (k[(p&3)^int(e)] ^ z))\n}\n"
  },
  {
    "path": "pkg/tutk/crypto_test.go",
    "content": "package tutk\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestXXTEADecrypt(t *testing.T) {\n\tbuf := []byte(\"WERhJxb87WF3zgPa\")\n\tkey := []byte(\"GAgDiwVPg2E4GMke\")\n\tXXTEADecrypt(buf, buf, key)\n\trequire.Equal(t, \"\\xc4\\xa6\\x2c\\xa1\\x10\\x64\\x17\\xa5\\xda\\x02\\xe1\\x62\\xa5\\xf0\\x62\\x71\", string(buf))\n}\n"
  },
  {
    "path": "pkg/tutk/dtls/auth.go",
    "content": "package dtls\n\nimport (\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"strings\"\n)\n\nfunc CalculateAuthKey(enr, mac string) []byte {\n\tdata := enr + strings.ToUpper(mac)\n\thash := sha256.Sum256([]byte(data))\n\tb64 := base64.StdEncoding.EncodeToString(hash[:6])\n\tb64 = strings.ReplaceAll(b64, \"+\", \"Z\")\n\tb64 = strings.ReplaceAll(b64, \"/\", \"9\")\n\tb64 = strings.ReplaceAll(b64, \"=\", \"A\")\n\treturn []byte(b64)\n}\n\nfunc DerivePSK(enr string) []byte {\n\t// DerivePSK derives the DTLS PSK from ENR\n\t// TUTK SDK treats the PSK as a NULL-terminated C string, so if SHA256(ENR)\n\t// contains a 0x00 byte, the PSK is truncated at that position.\n\thash := sha256.Sum256([]byte(enr))\n\tpskLen := 32\n\tfor i := range 32 {\n\t\tif hash[i] == 0x00 {\n\t\t\tpskLen = i\n\t\t\tbreak\n\t\t}\n\t}\n\n\tpsk := make([]byte, 32)\n\tcopy(psk[:pskLen], hash[:pskLen])\n\treturn psk\n}\n"
  },
  {
    "path": "pkg/tutk/dtls/cipher.go",
    "content": "package dtls\n\nimport (\n\t\"crypto/cipher\"\n\t\"crypto/sha256\"\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"fmt\"\n\t\"hash\"\n\t\"sync/atomic\"\n\n\t\"github.com/pion/dtls/v3\"\n\t\"github.com/pion/dtls/v3/pkg/crypto/clientcertificate\"\n\t\"github.com/pion/dtls/v3/pkg/crypto/prf\"\n\t\"github.com/pion/dtls/v3/pkg/protocol\"\n\t\"github.com/pion/dtls/v3/pkg/protocol/recordlayer\"\n\t\"golang.org/x/crypto/chacha20poly1305\"\n)\n\nconst CipherSuiteID_CCAC dtls.CipherSuiteID = 0xCCAC\n\nconst (\n\tchachaTagLength   = 16\n\tchachaNonceLength = 12\n)\n\nvar (\n\terrDecryptPacket      = &protocol.TemporaryError{Err: errors.New(\"failed to decrypt packet\")}\n\terrCipherSuiteNotInit = &protocol.TemporaryError{Err: errors.New(\"CipherSuite not initialized\")}\n)\n\ntype ChaCha20Poly1305Cipher struct {\n\tlocalCipher, remoteCipher   cipher.AEAD\n\tlocalWriteIV, remoteWriteIV []byte\n}\n\nfunc NewChaCha20Poly1305Cipher(localKey, localWriteIV, remoteKey, remoteWriteIV []byte) (*ChaCha20Poly1305Cipher, error) {\n\tlocalCipher, err := chacha20poly1305.New(localKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tremoteCipher, err := chacha20poly1305.New(remoteKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &ChaCha20Poly1305Cipher{\n\t\tlocalCipher:   localCipher,\n\t\tlocalWriteIV:  localWriteIV,\n\t\tremoteCipher:  remoteCipher,\n\t\tremoteWriteIV: remoteWriteIV,\n\t}, nil\n}\n\nfunc generateAEADAdditionalData(h *recordlayer.Header, payloadLen int) []byte {\n\tvar additionalData [13]byte\n\n\tbinary.BigEndian.PutUint64(additionalData[:], h.SequenceNumber)\n\tbinary.BigEndian.PutUint16(additionalData[:], h.Epoch)\n\tadditionalData[8] = byte(h.ContentType)\n\tadditionalData[9] = h.Version.Major\n\tadditionalData[10] = h.Version.Minor\n\tbinary.BigEndian.PutUint16(additionalData[11:], uint16(payloadLen))\n\n\treturn additionalData[:]\n}\n\nfunc computeNonce(iv []byte, epoch uint16, sequenceNumber uint64) []byte {\n\tnonce := make([]byte, chachaNonceLength)\n\n\tbinary.BigEndian.PutUint64(nonce[4:], sequenceNumber)\n\tbinary.BigEndian.PutUint16(nonce[4:], epoch)\n\n\tfor i := range chachaNonceLength {\n\t\tnonce[i] ^= iv[i]\n\t}\n\n\treturn nonce\n}\n\nfunc (c *ChaCha20Poly1305Cipher) Encrypt(pkt *recordlayer.RecordLayer, raw []byte) ([]byte, error) {\n\tpayload := raw[pkt.Header.Size():]\n\traw = raw[:pkt.Header.Size()]\n\n\tnonce := computeNonce(c.localWriteIV, pkt.Header.Epoch, pkt.Header.SequenceNumber)\n\tadditionalData := generateAEADAdditionalData(&pkt.Header, len(payload))\n\tencryptedPayload := c.localCipher.Seal(nil, nonce, payload, additionalData)\n\n\tr := make([]byte, len(raw)+len(encryptedPayload))\n\tcopy(r, raw)\n\tcopy(r[len(raw):], encryptedPayload)\n\n\tbinary.BigEndian.PutUint16(r[pkt.Header.Size()-2:], uint16(len(r)-pkt.Header.Size()))\n\n\treturn r, nil\n}\n\nfunc (c *ChaCha20Poly1305Cipher) Decrypt(header recordlayer.Header, in []byte) ([]byte, error) {\n\terr := header.Unmarshal(in)\n\tswitch {\n\tcase err != nil:\n\t\treturn nil, err\n\tcase header.ContentType == protocol.ContentTypeChangeCipherSpec:\n\t\treturn in, nil\n\tcase len(in) <= header.Size()+chachaTagLength:\n\t\treturn nil, fmt.Errorf(\"ciphertext too short: %d <= %d\", len(in), header.Size()+chachaTagLength)\n\t}\n\n\tnonce := computeNonce(c.remoteWriteIV, header.Epoch, header.SequenceNumber)\n\tout := in[header.Size():]\n\tadditionalData := generateAEADAdditionalData(&header, len(out)-chachaTagLength)\n\n\tout, err = c.remoteCipher.Open(out[:0], nonce, out, additionalData)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"%w: %v\", errDecryptPacket, err)\n\t}\n\n\treturn append(in[:header.Size()], out...), nil\n}\n\ntype TLSEcdhePskWithChacha20Poly1305Sha256 struct {\n\taead atomic.Value\n}\n\nfunc NewTLSEcdhePskWithChacha20Poly1305Sha256() *TLSEcdhePskWithChacha20Poly1305Sha256 {\n\treturn &TLSEcdhePskWithChacha20Poly1305Sha256{}\n}\n\nfunc (c *TLSEcdhePskWithChacha20Poly1305Sha256) CertificateType() clientcertificate.Type {\n\treturn clientcertificate.Type(0)\n}\n\nfunc (c *TLSEcdhePskWithChacha20Poly1305Sha256) KeyExchangeAlgorithm() dtls.CipherSuiteKeyExchangeAlgorithm {\n\treturn dtls.CipherSuiteKeyExchangeAlgorithmPsk | dtls.CipherSuiteKeyExchangeAlgorithmEcdhe\n}\n\nfunc (c *TLSEcdhePskWithChacha20Poly1305Sha256) ECC() bool {\n\treturn true\n}\n\nfunc (c *TLSEcdhePskWithChacha20Poly1305Sha256) ID() dtls.CipherSuiteID {\n\treturn CipherSuiteID_CCAC\n}\n\nfunc (c *TLSEcdhePskWithChacha20Poly1305Sha256) String() string {\n\treturn \"TLS_ECDHE_PSK_WITH_CHACHA20_POLY1305_SHA256\"\n}\n\nfunc (c *TLSEcdhePskWithChacha20Poly1305Sha256) HashFunc() func() hash.Hash {\n\treturn sha256.New\n}\n\nfunc (c *TLSEcdhePskWithChacha20Poly1305Sha256) AuthenticationType() dtls.CipherSuiteAuthenticationType {\n\treturn dtls.CipherSuiteAuthenticationTypePreSharedKey\n}\n\nfunc (c *TLSEcdhePskWithChacha20Poly1305Sha256) IsInitialized() bool {\n\treturn c.aead.Load() != nil\n}\n\nfunc (c *TLSEcdhePskWithChacha20Poly1305Sha256) Init(masterSecret, clientRandom, serverRandom []byte, isClient bool) error {\n\tconst (\n\t\tprfMacLen = 0\n\t\tprfKeyLen = 32\n\t\tprfIvLen  = 12\n\t)\n\n\tkeys, err := prf.GenerateEncryptionKeys(\n\t\tmasterSecret, clientRandom, serverRandom,\n\t\tprfMacLen, prfKeyLen, prfIvLen,\n\t\tc.HashFunc(),\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar aead *ChaCha20Poly1305Cipher\n\tif isClient {\n\t\taead, err = NewChaCha20Poly1305Cipher(\n\t\t\tkeys.ClientWriteKey, keys.ClientWriteIV,\n\t\t\tkeys.ServerWriteKey, keys.ServerWriteIV,\n\t\t)\n\t} else {\n\t\taead, err = NewChaCha20Poly1305Cipher(\n\t\t\tkeys.ServerWriteKey, keys.ServerWriteIV,\n\t\t\tkeys.ClientWriteKey, keys.ClientWriteIV,\n\t\t)\n\t}\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tc.aead.Store(aead)\n\treturn nil\n}\n\nfunc (c *TLSEcdhePskWithChacha20Poly1305Sha256) Encrypt(pkt *recordlayer.RecordLayer, raw []byte) ([]byte, error) {\n\taead, ok := c.aead.Load().(*ChaCha20Poly1305Cipher)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"%w: unable to encrypt\", errCipherSuiteNotInit)\n\t}\n\treturn aead.Encrypt(pkt, raw)\n}\n\nfunc (c *TLSEcdhePskWithChacha20Poly1305Sha256) Decrypt(h recordlayer.Header, raw []byte) ([]byte, error) {\n\taead, ok := c.aead.Load().(*ChaCha20Poly1305Cipher)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"%w: unable to decrypt\", errCipherSuiteNotInit)\n\t}\n\treturn aead.Decrypt(h, raw)\n}\n\nfunc CustomCipherSuites() []dtls.CipherSuite {\n\treturn []dtls.CipherSuite{\n\t\tNewTLSEcdhePskWithChacha20Poly1305Sha256(),\n\t}\n}\n"
  },
  {
    "path": "pkg/tutk/dtls/conn_dtls.go",
    "content": "package dtls\n\nimport (\n\t\"context\"\n\t\"crypto/hmac\"\n\t\"crypto/sha1\"\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/tutk\"\n\t\"github.com/pion/dtls/v3\"\n)\n\nconst (\n\tmagicCC51    = \"\\x51\\xcc\"         // (wyze specific?)\n\tsdkVersion42 = \"\\x01\\x01\\x02\\x04\" // 4.2.1.1\n\tsdkVersion43 = \"\\x00\\x08\\x03\\x04\" // 4.3.8.0\n)\n\nconst (\n\tcmdDiscoReq     uint16 = 0x0601\n\tcmdDiscoRes     uint16 = 0x0602\n\tcmdSessionReq   uint16 = 0x0402\n\tcmdSessionRes   uint16 = 0x0404\n\tcmdDataTX       uint16 = 0x0407\n\tcmdDataRX       uint16 = 0x0408\n\tcmdKeepaliveReq uint16 = 0x0427\n\tcmdKeepaliveRes uint16 = 0x0428\n\n\theaderSize    = 16\n\tdiscoBodySize = 72\n\tdiscoSize     = headerSize + discoBodySize\n\tsessionBody   = 36\n\tsessionSize   = headerSize + sessionBody\n)\n\nconst (\n\tcmdDiscoCC51      uint16 = 0x1002\n\tcmdKeepaliveCC51  uint16 = 0x1202\n\tcmdDTLSCC51       uint16 = 0x1502\n\tpayloadSizeCC51   uint16 = 0x0028\n\tpacketSizeCC51           = 52\n\theaderSizeCC51           = 28\n\tauthSizeCC51             = 20\n\tkeepaliveSizeCC51        = 48\n)\n\nconst (\n\tmagicAVLoginResp uint16 = 0x2100\n\tmagicIOCtrl      uint16 = 0x7000\n\tmagicChannelMsg  uint16 = 0x1000\n\tmagicACK         uint16 = 0x0009\n\tmagicAVLogin1    uint16 = 0x0000\n\tmagicAVLogin2    uint16 = 0x2000\n)\n\nconst (\n\tprotoVersion uint16 = 0x000c\n\tdefaultCaps  uint32 = 0x001f07fb\n)\n\nconst (\n\tiotcChannelMain = 0 // Main AV (we = DTLS Client)\n\tiotcChannelBack = 1 // Backchannel (we = DTLS Server)\n)\n\ntype DTLSConn struct {\n\tconn    *net.UDPConn\n\taddr    *net.UDPAddr\n\tframes  *tutk.FrameHandler\n\terr     error\n\tverbose bool\n\tctx     context.Context\n\tcancel  context.CancelFunc\n\twg      sync.WaitGroup\n\tmu      sync.RWMutex\n\n\t// DTLS\n\tclientConn *dtls.Conn\n\tserverConn *dtls.Conn\n\tclientBuf  chan []byte\n\tserverBuf  chan []byte\n\trawCmd     chan []byte\n\n\t// Identity\n\tuid     string\n\tauthKey string\n\tenr     string\n\tpsk     []byte\n\n\t// Session\n\tsid                []byte\n\tticket             uint16\n\thasTwoWayStreaming bool\n\n\t// Protocol\n\tisCC51       bool\n\tseq          uint16\n\tseqCmd       uint16\n\tavSeq        uint32\n\tkaSeq        uint32\n\taudioSeq     uint32\n\taudioFrameNo uint32\n\n\t// Ack\n\tackFlags   uint16\n\trxSeqStart uint16\n\trxSeqEnd   uint16\n\trxSeqInit  bool\n\tcmdAck     func()\n}\n\nfunc DialDTLS(host string, port int, uid, authKey, enr string, verbose bool) (*DTLSConn, error) {\n\tudp, err := net.ListenUDP(\"udp\", nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t_ = udp.SetReadBuffer(2 * 1024 * 1024)\n\n\tctx, cancel := context.WithCancel(context.Background())\n\tpsk := DerivePSK(enr)\n\n\tif port == 0 {\n\t\tport = 32761\n\t}\n\n\tc := &DTLSConn{\n\t\tconn:       udp,\n\t\taddr:       &net.UDPAddr{IP: net.ParseIP(host), Port: port},\n\t\tuid:        uid,\n\t\tauthKey:    authKey,\n\t\tenr:        enr,\n\t\tpsk:        psk,\n\t\tverbose:    verbose,\n\t\tctx:        ctx,\n\t\tcancel:     cancel,\n\t\trxSeqStart: 0xffff,\n\t\trxSeqEnd:   0xffff,\n\t}\n\n\tif err = c.discovery(); err != nil {\n\t\t_ = c.Close()\n\t\treturn nil, err\n\t}\n\n\tc.clientBuf = make(chan []byte, 64)\n\tc.serverBuf = make(chan []byte, 64)\n\tc.rawCmd = make(chan []byte, 16)\n\tc.frames = tutk.NewFrameHandler(c.verbose)\n\n\tc.wg.Add(1)\n\tgo c.reader()\n\n\tif err = c.connect(); err != nil {\n\t\t_ = c.Close()\n\t\treturn nil, err\n\t}\n\n\tc.wg.Add(1)\n\tgo c.worker()\n\n\treturn c, nil\n}\n\nfunc (c *DTLSConn) AVClientStart(timeout time.Duration) error {\n\trandomID := tutk.GenSessionID()\n\tpkt1 := c.msgAVLogin(magicAVLogin1, 570, 0x0001, randomID)\n\tpkt2 := c.msgAVLogin(magicAVLogin2, 572, 0x0000, randomID)\n\tpkt2[20]++ // pkt2 has randomID incremented by 1\n\n\tif _, err := c.clientConn.Write(pkt1); err != nil {\n\t\treturn fmt.Errorf(\"av login 1 failed: %w\", err)\n\t}\n\n\ttime.Sleep(10 * time.Millisecond)\n\n\tif _, err := c.clientConn.Write(pkt2); err != nil {\n\t\treturn fmt.Errorf(\"av login 2 failed: %w\", err)\n\t}\n\n\t// Wait for response\n\ttimer := time.NewTimer(timeout)\n\tdefer timer.Stop()\n\tfor {\n\t\tselect {\n\t\tcase data, ok := <-c.rawCmd:\n\t\t\tif !ok {\n\t\t\t\treturn io.EOF\n\t\t\t}\n\t\t\tif len(data) >= 32 && binary.LittleEndian.Uint16(data) == magicAVLoginResp {\n\t\t\t\tc.hasTwoWayStreaming = data[31] == 1\n\n\t\t\t\tack := c.msgACK()\n\t\t\t\tc.clientConn.Write(ack)\n\n\t\t\t\t// Start ACK sender for continuous streaming\n\t\t\t\tc.wg.Add(1)\n\t\t\t\tgo func() {\n\t\t\t\t\tdefer c.wg.Done()\n\t\t\t\t\tackTicker := time.NewTicker(100 * time.Millisecond)\n\t\t\t\t\tdefer ackTicker.Stop()\n\n\t\t\t\t\tfor {\n\t\t\t\t\t\tselect {\n\t\t\t\t\t\tcase <-c.ctx.Done():\n\t\t\t\t\t\t\treturn\n\t\t\t\t\t\tcase <-ackTicker.C:\n\t\t\t\t\t\t\tif c.clientConn != nil {\n\t\t\t\t\t\t\t\tack := c.msgACK()\n\t\t\t\t\t\t\t\tc.clientConn.Write(ack)\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\treturn nil\n\t\t\t}\n\t\tcase <-timer.C:\n\t\t\treturn context.DeadlineExceeded\n\t\t}\n\t}\n}\n\nfunc (c *DTLSConn) AVServStart() error {\n\tconn, err := NewDTLSServer(c.ctx, iotcChannelBack, c.addr, c.WriteDTLS, c.serverBuf, c.psk)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dtls: server handshake failed: %w\", err)\n\t}\n\n\tif c.verbose {\n\t\tfmt.Printf(\"[DTLS] Server handshake complete on channel %d\\n\", iotcChannelBack)\n\t\tfmt.Printf(\"[SERVER] Waiting for AV Login request from camera...\\n\")\n\t}\n\n\t// Wait for AV Login request from camera\n\tbuf := make([]byte, 1024)\n\tconn.SetReadDeadline(time.Now().Add(5 * time.Second))\n\tn, err := conn.Read(buf)\n\tif err != nil {\n\t\tgo conn.Close()\n\t\treturn fmt.Errorf(\"read av login: %w\", err)\n\t}\n\n\tif c.verbose {\n\t\tfmt.Printf(\"[SERVER] AV Login request len=%d data:\\n%s\", n, hexDump(buf[:n]))\n\t}\n\n\tif n < 24 {\n\t\tgo conn.Close()\n\t\treturn fmt.Errorf(\"av login too short: %d bytes\", n)\n\t}\n\n\tchecksum := binary.LittleEndian.Uint32(buf[20:])\n\tresp := c.msgAVLoginResponse(checksum)\n\n\tif c.verbose {\n\t\tfmt.Printf(\"[SERVER] Sending AV Login response: %d bytes\\n\", len(resp))\n\t}\n\n\tif _, err = conn.Write(resp); err != nil {\n\t\tgo conn.Close()\n\t\treturn fmt.Errorf(\"write av login response: %w\", err)\n\t}\n\n\tif c.verbose {\n\t\tfmt.Printf(\"[SERVER] AV Login response sent, waiting for possible resend...\\n\")\n\t}\n\n\t// Camera may resend, respond again\n\tconn.SetReadDeadline(time.Now().Add(500 * time.Millisecond))\n\tif n, _ = conn.Read(buf); n > 0 {\n\t\tif c.verbose {\n\t\t\tfmt.Printf(\"[SERVER] Received AV Login resend: %d bytes\\n\", n)\n\t\t}\n\t\tconn.Write(resp)\n\t}\n\n\tconn.SetReadDeadline(time.Time{})\n\n\tif c.verbose {\n\t\tfmt.Printf(\"[SERVER] AV Login complete, ready for two way streaming\\n\")\n\t}\n\n\tc.mu.Lock()\n\tc.serverConn = conn\n\tc.mu.Unlock()\n\n\treturn nil\n}\n\nfunc (c *DTLSConn) AVServStop() error {\n\tc.mu.Lock()\n\tserverConn := c.serverConn\n\tc.serverConn = nil\n\n\t// Reset audio TX state\n\tc.audioSeq = 0\n\tc.audioFrameNo = 0\n\tc.mu.Unlock()\n\n\tif serverConn == nil {\n\t\treturn nil\n\t}\n\n\tgo serverConn.Close()\n\n\treturn nil\n}\n\nfunc (c *DTLSConn) AVRecvFrameData() (*tutk.Packet, error) {\n\tselect {\n\tcase pkt, ok := <-c.frames.Recv():\n\t\tif !ok {\n\t\t\treturn nil, c.Error()\n\t\t}\n\t\treturn pkt, nil\n\tcase <-c.ctx.Done():\n\t\treturn nil, c.Error()\n\t}\n}\n\nfunc (c *DTLSConn) AVSendAudioData(codec byte, payload []byte, timestampUS uint32, sampleRate uint32, channels uint8) error {\n\tc.mu.Lock()\n\tconn := c.serverConn\n\tif conn == nil {\n\t\tc.mu.Unlock()\n\t\treturn fmt.Errorf(\"av server not ready\")\n\t}\n\n\tframe := c.msgAudioFrame(payload, timestampUS, codec, sampleRate, channels)\n\n\tc.mu.Unlock()\n\n\tn, err := conn.Write(frame)\n\tif c.verbose {\n\t\tif err != nil {\n\t\t\tfmt.Printf(\"[SERVER TX] DTLS Write ERROR: %v\\n\", err)\n\t\t} else {\n\t\t\tfmt.Printf(\"[SERVER TX] len=%d, data:\\n%s\", n, hexDump(frame))\n\t\t}\n\t}\n\treturn err\n}\n\nfunc (c *DTLSConn) Write(data []byte) error {\n\tif c.isCC51 {\n\t\t_, err := c.conn.WriteToUDP(data, c.addr)\n\t\treturn err\n\t}\n\t_, err := c.conn.WriteToUDP(tutk.TransCodeBlob(data), c.addr)\n\treturn err\n}\n\nfunc (c *DTLSConn) WriteDTLS(payload []byte, channel byte) error {\n\tvar frame []byte\n\tif c.isCC51 {\n\t\tframe = c.msgTxDataCC51(payload, channel)\n\t} else {\n\t\tframe = c.msgTxData(payload, channel)\n\t}\n\n\treturn c.Write(frame)\n}\n\nfunc (c *DTLSConn) WriteIOCtrl(payload []byte) error {\n\t_, err := c.conn.Write(c.msgIOCtrl(payload))\n\treturn err\n}\n\nfunc (c *DTLSConn) WriteAndWait(req []byte, ok func(res []byte) bool) ([]byte, error) {\n\tvar t *time.Timer\n\tt = time.AfterFunc(1, func() {\n\t\tif err := c.Write(req); err == nil && t != nil {\n\t\t\tt.Reset(time.Second)\n\t\t}\n\t})\n\tdefer t.Stop()\n\n\t_ = c.conn.SetDeadline(time.Now().Add(5 * time.Second))\n\tdefer c.conn.SetDeadline(time.Time{})\n\n\tbuf := make([]byte, 2048)\n\tfor {\n\t\tn, addr, err := c.conn.ReadFromUDP(buf)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tif string(addr.IP) != string(c.addr.IP) || n < 16 {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar res []byte\n\t\tif c.isCC51 {\n\t\t\tres = buf[:n]\n\t\t} else {\n\t\t\tres = tutk.ReverseTransCodeBlob(buf[:n])\n\t\t}\n\n\t\tif ok(res) {\n\t\t\tc.addr.Port = addr.Port\n\t\t\treturn res, nil\n\t\t}\n\t}\n}\n\nfunc (c *DTLSConn) WriteAndWaitIOCtrl(payload []byte, match func([]byte) bool, timeout time.Duration) ([]byte, error) {\n\tframe := c.msgIOCtrl(payload)\n\tvar t *time.Timer\n\tt = time.AfterFunc(1, func() {\n\t\tc.mu.RLock()\n\t\tconn := c.clientConn\n\t\tc.mu.RUnlock()\n\t\tif conn != nil {\n\t\t\tif _, err := conn.Write(frame); err == nil && t != nil {\n\t\t\t\tt.Reset(time.Second)\n\t\t\t}\n\t\t}\n\t})\n\tdefer t.Stop()\n\n\ttimer := time.NewTimer(timeout)\n\tdefer timer.Stop()\n\n\tfor {\n\t\tselect {\n\t\tcase data, ok := <-c.rawCmd:\n\t\t\tif !ok {\n\t\t\t\treturn nil, io.EOF\n\t\t\t}\n\n\t\t\tack := c.msgACK()\n\t\t\tc.clientConn.Write(ack)\n\n\t\t\tif match(data) {\n\t\t\t\treturn data, nil\n\t\t\t}\n\t\tcase <-timer.C:\n\t\t\treturn nil, fmt.Errorf(\"timeout waiting for response\")\n\t\t}\n\t}\n}\n\nfunc (c *DTLSConn) HasTwoWayStreaming() bool {\n\treturn c.hasTwoWayStreaming\n}\n\nfunc (c *DTLSConn) IsBackchannelReady() bool {\n\tc.mu.RLock()\n\tdefer c.mu.RUnlock()\n\treturn c.serverConn != nil\n}\n\nfunc (c *DTLSConn) RemoteAddr() *net.UDPAddr {\n\treturn c.addr\n}\n\nfunc (c *DTLSConn) LocalAddr() *net.UDPAddr {\n\treturn c.conn.LocalAddr().(*net.UDPAddr)\n}\n\nfunc (c *DTLSConn) SetDeadline(t time.Time) error {\n\treturn c.conn.SetDeadline(t)\n}\n\nfunc (c *DTLSConn) Close() error {\n\tc.cancel()\n\n\tc.mu.Lock()\n\tif conn := c.serverConn; conn != nil {\n\t\tc.serverConn = nil\n\t\tgo conn.Close()\n\t}\n\tif conn := c.clientConn; conn != nil {\n\t\tc.clientConn = nil\n\t\tgo conn.Close()\n\t}\n\tif c.frames != nil {\n\t\tc.frames.Close()\n\t}\n\tc.mu.Unlock()\n\n\tc.wg.Wait()\n\n\treturn c.conn.Close()\n}\n\nfunc (c *DTLSConn) Error() error {\n\tif c.err != nil {\n\t\treturn c.err\n\t}\n\treturn io.EOF\n}\n\nfunc (c *DTLSConn) discovery() error {\n\tc.sid = tutk.GenSessionID()\n\n\tpktIOTC := tutk.TransCodeBlob(c.msgDisco(1))\n\tpktCC51 := c.msgDiscoCC51(0, 0, false)\n\n\tbuf := make([]byte, 2048)\n\tdeadline := time.Now().Add(5 * time.Second)\n\n\tfor time.Now().Before(deadline) {\n\t\tc.conn.WriteToUDP(pktIOTC, c.addr)\n\t\tc.conn.WriteToUDP(pktCC51, c.addr)\n\n\t\tc.conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond))\n\t\tn, addr, err := c.conn.ReadFromUDP(buf)\n\t\tif err != nil {\n\t\t\tcontinue\n\t\t}\n\t\tif !addr.IP.Equal(c.addr.IP) {\n\t\t\tcontinue\n\t\t}\n\n\t\t// CC51 protocol\n\t\tif n >= packetSizeCC51 && string(buf[:2]) == magicCC51 {\n\t\t\tif binary.LittleEndian.Uint16(buf[4:]) == cmdDiscoCC51 {\n\t\t\t\tc.addr, c.isCC51, c.ticket = addr, true, binary.LittleEndian.Uint16(buf[14:])\n\t\t\t\tif n >= 24 {\n\t\t\t\t\tcopy(c.sid, buf[16:24])\n\t\t\t\t}\n\t\t\t\treturn c.discoDoneCC51()\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\t// IOTC Protocol (Basis)\n\t\tdata := tutk.ReverseTransCodeBlob(buf[:n])\n\t\tif len(data) >= 16 && binary.LittleEndian.Uint16(data[8:]) == cmdDiscoRes {\n\t\t\tc.addr, c.isCC51 = addr, false\n\t\t\treturn c.discoDone()\n\t\t}\n\t}\n\n\treturn fmt.Errorf(\"discovery timeout\")\n}\n\nfunc (c *DTLSConn) discoDone() error {\n\tc.Write(c.msgDisco(2))\n\ttime.Sleep(100 * time.Millisecond)\n\t_, err := c.WriteAndWait(c.msgSession(), func(res []byte) bool {\n\t\treturn len(res) >= 16 && binary.LittleEndian.Uint16(res[8:]) == cmdSessionRes\n\t})\n\treturn err\n}\n\nfunc (c *DTLSConn) discoDoneCC51() error {\n\t_, err := c.WriteAndWait(c.msgDiscoCC51(2, c.ticket, false), func(res []byte) bool {\n\t\tif len(res) < packetSizeCC51 || string(res[:2]) != magicCC51 {\n\t\t\treturn false\n\t\t}\n\t\tcmd := binary.LittleEndian.Uint16(res[4:])\n\t\tdir := binary.LittleEndian.Uint16(res[8:])\n\t\tseq := binary.LittleEndian.Uint16(res[12:])\n\t\treturn cmd == cmdDiscoCC51 && dir == 0xFFFF && seq == 3\n\t})\n\treturn err\n}\n\nfunc (c *DTLSConn) connect() error {\n\tconn, err := NewDTLSClient(c.ctx, iotcChannelMain, c.addr, c.WriteDTLS, c.clientBuf, c.psk)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"dtls: client handshake failed: %w\", err)\n\t}\n\n\tc.mu.Lock()\n\tc.clientConn = conn\n\tc.mu.Unlock()\n\n\tif c.verbose {\n\t\tfmt.Printf(\"[DTLS] Client handshake complete on channel %d\\n\", iotcChannelMain)\n\t}\n\n\treturn nil\n}\n\nfunc (c *DTLSConn) worker() {\n\tdefer c.wg.Done()\n\n\tbuf := make([]byte, 2048)\n\n\tfor {\n\t\tselect {\n\t\tcase <-c.ctx.Done():\n\t\t\treturn\n\t\tdefault:\n\t\t}\n\n\t\tn, err := c.clientConn.Read(buf)\n\t\tif err != nil {\n\t\t\tc.err = err\n\t\t\treturn\n\t\t}\n\n\t\tif n < 2 {\n\t\t\tcontinue\n\t\t}\n\n\t\tdata := buf[:n]\n\t\tmagic := binary.LittleEndian.Uint16(data)\n\n\t\tif c.verbose {\n\t\t\tfmt.Printf(\"[DTLS RX] magic=0x%04x len=%d\\n\", magic, n)\n\t\t}\n\n\t\tswitch magic {\n\t\tcase magicAVLoginResp:\n\t\t\tc.queue(c.rawCmd, data)\n\n\t\tcase magicIOCtrl, magicChannelMsg:\n\t\t\tc.queue(c.rawCmd, data)\n\n\t\tcase protoVersion:\n\t\t\t// Seq-Tracking\n\t\t\tif len(data) >= 8 {\n\t\t\t\tseq := binary.LittleEndian.Uint16(data[4:])\n\t\t\t\tif !c.rxSeqInit {\n\t\t\t\t\tc.rxSeqInit = true\n\t\t\t\t}\n\t\t\t\tif seq > c.rxSeqEnd || c.rxSeqEnd == 0xffff {\n\t\t\t\t\tc.rxSeqEnd = seq\n\t\t\t\t}\n\t\t\t}\n\t\t\tc.queue(c.rawCmd, data)\n\n\t\tcase magicACK:\n\t\t\tc.mu.RLock()\n\t\t\tack := c.cmdAck\n\t\t\tc.mu.RUnlock()\n\t\t\tif ack != nil {\n\t\t\t\tack()\n\t\t\t}\n\n\t\tdefault:\n\t\t\tchannel := data[0]\n\t\t\tif channel == tutk.ChannelAudio || channel == tutk.ChannelIVideo || channel == tutk.ChannelPVideo {\n\t\t\t\tc.frames.Handle(data)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (c *DTLSConn) reader() {\n\tdefer c.wg.Done()\n\n\tbuf := make([]byte, 2048)\n\n\tfor {\n\t\tselect {\n\t\tcase <-c.ctx.Done():\n\t\t\treturn\n\t\tdefault:\n\t\t}\n\n\t\tc.conn.SetReadDeadline(time.Now().Add(100 * time.Millisecond))\n\t\tn, addr, err := c.conn.ReadFromUDP(buf)\n\t\tif err != nil {\n\t\t\tif netErr, ok := err.(net.Error); ok && netErr.Timeout() {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\treturn\n\t\t}\n\n\t\tif !addr.IP.Equal(c.addr.IP) {\n\t\t\tif c.verbose {\n\t\t\t\tfmt.Printf(\"Ignored packet from unknown IP: %s\\n\", addr.IP.String())\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\t\tif addr.Port != c.addr.Port {\n\t\t\tc.addr.Port = addr.Port\n\t\t}\n\n\t\t// CC51 Protocol\n\t\tif c.isCC51 && n >= 12 && string(buf[:2]) == magicCC51 {\n\t\t\tcmd := binary.LittleEndian.Uint16(buf[4:])\n\t\t\tswitch cmd {\n\t\t\tcase cmdKeepaliveCC51:\n\t\t\t\tif n >= keepaliveSizeCC51 {\n\t\t\t\t\t_ = c.Write(c.msgKeepaliveCC51())\n\t\t\t\t}\n\t\t\tcase cmdDTLSCC51:\n\t\t\t\tif n >= headerSizeCC51+authSizeCC51 {\n\t\t\t\t\tch := byte(binary.LittleEndian.Uint16(buf[12:]) >> 8)\n\t\t\t\t\tdtlsData := buf[headerSizeCC51 : n-authSizeCC51]\n\t\t\t\t\tswitch ch {\n\t\t\t\t\tcase iotcChannelMain:\n\t\t\t\t\t\tc.queue(c.clientBuf, dtlsData)\n\t\t\t\t\tcase iotcChannelBack:\n\t\t\t\t\t\tc.queue(c.serverBuf, dtlsData)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t\tcontinue\n\t\t}\n\n\t\t// IOTC Protocol (Basis)\n\t\tdata := tutk.ReverseTransCodeBlob(buf[:n])\n\t\tif len(data) < 16 {\n\t\t\tcontinue\n\t\t}\n\n\t\tswitch binary.LittleEndian.Uint16(data[8:]) {\n\t\tcase cmdKeepaliveRes:\n\t\t\tif len(data) > 24 {\n\t\t\t\t_ = c.Write(c.msgKeepalive(data[16:]))\n\t\t\t}\n\t\tcase cmdDataRX:\n\t\t\tif len(data) > 28 {\n\t\t\t\tch := data[14]\n\t\t\t\tswitch ch {\n\t\t\t\tcase iotcChannelMain:\n\t\t\t\t\tc.queue(c.clientBuf, data[28:])\n\t\t\t\tcase iotcChannelBack:\n\t\t\t\t\tc.queue(c.serverBuf, data[28:])\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (c *DTLSConn) queue(ch chan []byte, data []byte) {\n\tb := make([]byte, len(data))\n\tcopy(b, data)\n\tselect {\n\tcase ch <- b:\n\tdefault:\n\t\tselect {\n\t\tcase <-ch:\n\t\tdefault:\n\t\t}\n\t\tch <- b\n\t}\n}\n\nfunc (c *DTLSConn) msgDisco(stage byte) []byte {\n\tb := make([]byte, discoSize)\n\tcopy(b, \"\\x04\\x02\\x1a\\x02\")                         // marker + mode\n\tbinary.LittleEndian.PutUint16(b[4:], discoBodySize) // body size\n\tbinary.LittleEndian.PutUint16(b[8:], cmdDiscoReq)   // 0x0601\n\tbinary.LittleEndian.PutUint16(b[10:], 0x0021)       // flags\n\tbody := b[headerSize:]\n\tcopy(body[:20], c.uid)\n\tcopy(body[36:], sdkVersion42) // SDK 4.2.1.1\n\tcopy(body[40:], c.sid)\n\tbody[48] = stage\n\tif stage == 1 && len(c.authKey) > 0 {\n\t\tcopy(body[58:], c.authKey)\n\t}\n\treturn b\n}\n\nfunc (c *DTLSConn) msgDiscoCC51(seq, ticket uint16, isResponse bool) []byte {\n\tb := make([]byte, packetSizeCC51)\n\tcopy(b[:2], magicCC51)\n\tbinary.LittleEndian.PutUint16(b[4:], cmdDiscoCC51)    // 0x1002\n\tbinary.LittleEndian.PutUint16(b[6:], payloadSizeCC51) // 40 bytes\n\tif isResponse {\n\t\tbinary.LittleEndian.PutUint16(b[8:], 0xFFFF) // response\n\t}\n\tbinary.LittleEndian.PutUint16(b[12:], seq)\n\tbinary.LittleEndian.PutUint16(b[14:], ticket)\n\tcopy(b[16:24], c.sid)\n\tcopy(b[24:28], sdkVersion43) // SDK 4.3.8.0\n\tb[28] = 0x1d                 // unknown field (capability/build flag?)\n\th := hmac.New(sha1.New, append([]byte(c.uid), c.authKey...))\n\th.Write(b[:32])\n\tcopy(b[32:52], h.Sum(nil))\n\treturn b\n}\n\nfunc (c *DTLSConn) msgKeepaliveCC51() []byte {\n\tc.kaSeq += 2\n\tb := make([]byte, keepaliveSizeCC51)\n\tcopy(b[:2], magicCC51)\n\tbinary.LittleEndian.PutUint16(b[4:], cmdKeepaliveCC51) // 0x1202\n\tbinary.LittleEndian.PutUint16(b[6:], 0x0024)           // 36 bytes payload\n\tbinary.LittleEndian.PutUint32(b[16:], c.kaSeq)         // counter\n\tcopy(b[20:28], c.sid)                                  // session ID\n\th := hmac.New(sha1.New, append([]byte(c.uid), c.authKey...))\n\th.Write(b[:28])\n\tcopy(b[28:48], h.Sum(nil))\n\treturn b\n}\n\nfunc (c *DTLSConn) msgSession() []byte {\n\tb := make([]byte, sessionSize)\n\tcopy(b, \"\\x04\\x02\\x1a\\x02\")                         // marker + mode\n\tbinary.LittleEndian.PutUint16(b[4:], sessionBody)   // body size\n\tbinary.LittleEndian.PutUint16(b[8:], cmdSessionReq) // 0x0402\n\tbinary.LittleEndian.PutUint16(b[10:], 0x0033)       // flags\n\tbody := b[headerSize:]\n\tcopy(body[:20], c.uid)\n\tcopy(body[20:], c.sid)\n\tbinary.LittleEndian.PutUint32(body[32:], uint32(time.Now().Unix()))\n\treturn b\n}\n\nfunc (c *DTLSConn) msgAVLogin(magic uint16, size int, flags uint16, randomID []byte) []byte {\n\tb := make([]byte, size)\n\tbinary.LittleEndian.PutUint16(b, magic)\n\tbinary.LittleEndian.PutUint16(b[2:], protoVersion)\n\tbinary.LittleEndian.PutUint16(b[16:], uint16(size-24)) // payload size\n\tbinary.LittleEndian.PutUint16(b[18:], flags)\n\tcopy(b[20:], randomID[:4])\n\tcopy(b[24:], \"admin\")                               // username\n\tcopy(b[280:], c.enr)                                // password/ENR\n\tbinary.LittleEndian.PutUint32(b[540:], 4)           // security_mode ?\n\tbinary.LittleEndian.PutUint32(b[552:], defaultCaps) // capabilities\n\treturn b\n}\n\nfunc (c *DTLSConn) msgAVLoginResponse(checksum uint32) []byte {\n\tb := make([]byte, 60)\n\tbinary.LittleEndian.PutUint16(b, 0x2100)        // magic\n\tbinary.LittleEndian.PutUint16(b[2:], 0x000c)    // version\n\tb[4] = 0x10                                     // success\n\tbinary.LittleEndian.PutUint32(b[16:], 0x24)     // payload size\n\tbinary.LittleEndian.PutUint32(b[20:], checksum) // echo checksum\n\tb[29] = 0x01                                    // enable flag\n\tb[31] = 0x01                                    // two-way streaming\n\tbinary.LittleEndian.PutUint32(b[36:], 0x04)     // buffer config\n\tbinary.LittleEndian.PutUint32(b[40:], defaultCaps)\n\tbinary.LittleEndian.PutUint16(b[54:], 0x0003) // channel info\n\tbinary.LittleEndian.PutUint16(b[56:], 0x0002)\n\treturn b\n}\n\nfunc (c *DTLSConn) msgAudioFrame(payload []byte, timestampUS uint32, codec byte, sampleRate uint32, channels uint8) []byte {\n\tc.audioSeq++\n\tc.audioFrameNo++\n\tprevFrame := uint32(0)\n\tif c.audioFrameNo > 1 {\n\t\tprevFrame = c.audioFrameNo - 1\n\t}\n\n\ttotalPayload := len(payload) + 16 // payload + frameinfo\n\tb := make([]byte, 36+totalPayload)\n\n\t// Outer header (36 bytes)\n\tb[0] = tutk.ChannelAudio      // 0x03\n\tb[1] = tutk.FrameTypeStartAlt // 0x09\n\tbinary.LittleEndian.PutUint16(b[2:], protoVersion)\n\tbinary.LittleEndian.PutUint32(b[4:], c.audioSeq)\n\tbinary.LittleEndian.PutUint32(b[8:], timestampUS)\n\tif c.audioFrameNo == 1 {\n\t\tbinary.LittleEndian.PutUint32(b[12:], 0x00000001)\n\t} else {\n\t\tbinary.LittleEndian.PutUint32(b[12:], 0x00100001)\n\t}\n\n\t// Inner header\n\tb[16] = tutk.ChannelAudio\n\tb[17] = tutk.FrameTypeEndSingle\n\tbinary.LittleEndian.PutUint16(b[18:], uint16(prevFrame))\n\tbinary.LittleEndian.PutUint16(b[20:], 0x0001) // pkt_total\n\tbinary.LittleEndian.PutUint16(b[22:], 0x0010) // flags\n\tbinary.LittleEndian.PutUint32(b[24:], uint32(totalPayload))\n\tbinary.LittleEndian.PutUint32(b[28:], prevFrame)\n\tbinary.LittleEndian.PutUint32(b[32:], c.audioFrameNo)\n\tcopy(b[36:], payload) // Payload + FrameInfo\n\tfi := b[36+len(payload):]\n\tfi[0] = codec // Codec ID (low byte)\n\tfi[1] = 0     // Codec ID (high byte, unused)\n\t// Audio flags: [3:2]=sampleRateIdx [1]=16bit [0]=stereo\n\tsrIdx := tutk.GetSampleRateIndex(sampleRate)\n\tfi[2] = (srIdx << 2) | 0x02 // 16-bit always set\n\tif channels == 2 {\n\t\tfi[2] |= 0x01\n\t}\n\tfi[4] = 1 // online\n\tbinary.LittleEndian.PutUint32(fi[12:], (c.audioFrameNo-1)*tutk.GetSamplesPerFrame(codec)*1000/sampleRate)\n\treturn b\n}\n\nfunc (c *DTLSConn) msgTxData(payload []byte, channel byte) []byte {\n\tbodySize := 12 + len(payload)\n\tb := make([]byte, 16+bodySize)\n\tcopy(b, \"\\x04\\x02\\x1a\\x0b\")                            // marker + mode=data\n\tbinary.LittleEndian.PutUint16(b[4:], uint16(bodySize)) // body size\n\tbinary.LittleEndian.PutUint16(b[6:], c.seq)            // sequence\n\tc.seq++\n\tbinary.LittleEndian.PutUint16(b[8:], cmdDataTX)   // 0x0407\n\tbinary.LittleEndian.PutUint16(b[10:], 0x0021)     // flags\n\tcopy(b[12:], c.sid[:2])                           // rid[0:2]\n\tb[14] = channel                                   // channel\n\tb[15] = 0x01                                      // marker\n\tbinary.LittleEndian.PutUint32(b[16:], 0x0000000c) // const\n\tcopy(b[20:], c.sid[:8])                           // rid\n\tcopy(b[28:], payload)\n\treturn b\n}\n\nfunc (c *DTLSConn) msgTxDataCC51(payload []byte, channel byte) []byte {\n\tpayloadSize := uint16(16 + len(payload) + authSizeCC51)\n\tb := make([]byte, headerSizeCC51+len(payload)+authSizeCC51)\n\tcopy(b[:2], magicCC51)\n\tbinary.LittleEndian.PutUint16(b[4:], cmdDTLSCC51) // 0x1502\n\tbinary.LittleEndian.PutUint16(b[6:], payloadSize)\n\tbinary.LittleEndian.PutUint16(b[12:], uint16(0x0010)|(uint16(channel)<<8)) // channel in high byte\n\tbinary.LittleEndian.PutUint16(b[14:], c.ticket)\n\tcopy(b[16:24], c.sid)\n\tbinary.LittleEndian.PutUint32(b[24:], 1) // const\n\tcopy(b[headerSizeCC51:], payload)\n\th := hmac.New(sha1.New, append([]byte(c.uid), c.authKey...))\n\th.Write(b[:headerSizeCC51])\n\tcopy(b[headerSizeCC51+len(payload):], h.Sum(nil))\n\treturn b\n}\n\nfunc (c *DTLSConn) msgACK() []byte {\n\tc.ackFlags++\n\tb := make([]byte, 24)\n\tbinary.LittleEndian.PutUint16(b[0:], magicACK)     // 0x0009\n\tbinary.LittleEndian.PutUint16(b[2:], protoVersion) // 0x000c\n\tbinary.LittleEndian.PutUint32(b[4:], c.avSeq)      // TX seq\n\tc.avSeq++\n\tbinary.LittleEndian.PutUint16(b[8:], c.rxSeqStart) // RX start (last acked)\n\tbinary.LittleEndian.PutUint16(b[10:], c.rxSeqEnd)  // RX end (highest received)\n\tif c.rxSeqInit {\n\t\tc.rxSeqStart = c.rxSeqEnd\n\t}\n\tbinary.LittleEndian.PutUint16(b[12:], c.ackFlags)             // AckFlags\n\tbinary.LittleEndian.PutUint32(b[16:], uint32(c.ackFlags)<<16) // AckCounter\n\tts := uint32(time.Now().UnixMilli() & 0xFFFF)\n\tbinary.LittleEndian.PutUint16(b[20:], uint16(ts)) // Timestamp\n\treturn b\n}\n\nfunc (c *DTLSConn) msgKeepalive(incoming []byte) []byte {\n\tb := make([]byte, 24)\n\tcopy(b, \"\\x04\\x02\\x1a\\x0a\")                           // marker + mode\n\tbinary.LittleEndian.PutUint16(b[4:], 8)               // body size\n\tbinary.LittleEndian.PutUint16(b[8:], cmdKeepaliveReq) // 0x0427\n\tbinary.LittleEndian.PutUint16(b[10:], 0x0021)         // flags\n\tif len(incoming) >= 8 {\n\t\tcopy(b[16:], incoming[:8]) // echo payload\n\t}\n\treturn b\n}\n\nfunc (c *DTLSConn) msgIOCtrl(payload []byte) []byte {\n\tb := make([]byte, 40+len(payload))\n\tbinary.LittleEndian.PutUint16(b, protoVersion)     // magic\n\tbinary.LittleEndian.PutUint16(b[2:], protoVersion) // version\n\tbinary.LittleEndian.PutUint32(b[4:], c.avSeq)      // av seq\n\tc.avSeq++\n\tbinary.LittleEndian.PutUint16(b[16:], magicIOCtrl)            // 0x7000\n\tbinary.LittleEndian.PutUint16(b[18:], c.seqCmd)               // sub channel\n\tbinary.LittleEndian.PutUint32(b[20:], 1)                      // ioctl seq\n\tbinary.LittleEndian.PutUint32(b[24:], uint32(len(payload)+4)) // payload size\n\tbinary.LittleEndian.PutUint32(b[28:], uint32(c.seqCmd))       // flag\n\tb[37] = 0x01\n\tcopy(b[40:], payload)\n\tc.seqCmd++\n\treturn b\n}\n\nfunc hexDump(data []byte) string {\n\tconst maxBytes = 650\n\ttotalLen := len(data)\n\ttruncated := totalLen > maxBytes\n\tif truncated {\n\t\tdata = data[:maxBytes]\n\t}\n\n\tvar result string\n\tfor i := 0; i < len(data); i += 16 {\n\t\tend := min(i+16, len(data))\n\t\tline := fmt.Sprintf(\"    %04x:\", i)\n\t\tfor j := i; j < end; j++ {\n\t\t\tline += fmt.Sprintf(\" %02x\", data[j])\n\t\t}\n\t\tresult += line + \"\\n\"\n\t}\n\n\tif truncated {\n\t\tresult += fmt.Sprintf(\"    ... (truncated, showing %d of %d bytes)\\n\", maxBytes, totalLen)\n\t}\n\treturn result\n}\n"
  },
  {
    "path": "pkg/tutk/dtls/dtls.go",
    "content": "package dtls\n\nimport (\n\t\"context\"\n\t\"net\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/pion/dtls/v3\"\n)\n\nfunc NewDTLSClient(ctx context.Context, channel uint8, addr net.Addr, writeFn func([]byte, uint8) error, readChan chan []byte, psk []byte) (*dtls.Conn, error) {\n\treturn dialDTLS(ctx, channel, addr, writeFn, readChan, psk, false)\n}\n\nfunc NewDTLSServer(ctx context.Context, channel uint8, addr net.Addr, writeFn func([]byte, uint8) error, readChan chan []byte, psk []byte) (*dtls.Conn, error) {\n\treturn dialDTLS(ctx, channel, addr, writeFn, readChan, psk, true)\n}\n\nfunc dialDTLS(ctx context.Context, channel uint8, addr net.Addr, writeFn func([]byte, uint8) error, readChan chan []byte, psk []byte, isServer bool) (*dtls.Conn, error) {\n\tadapter := &channelAdapter{\n\t\tctx:      ctx,\n\t\tchannel:  channel,\n\t\taddr:     addr,\n\t\twriteFn:  writeFn,\n\t\treadChan: readChan,\n\t}\n\n\tvar conn *dtls.Conn\n\tvar err error\n\n\tif isServer {\n\t\tconn, err = dtls.Server(adapter, addr, buildDTLSConfig(psk, true))\n\t} else {\n\t\tconn, err = dtls.Client(adapter, addr, buildDTLSConfig(psk, false))\n\t}\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttimeout := 5 * time.Second\n\tadapter.SetReadDeadline(time.Now().Add(timeout))\n\thsCtx, cancel := context.WithTimeout(ctx, timeout)\n\tdefer cancel()\n\n\tif err := conn.HandshakeContext(hsCtx); err != nil {\n\t\tgo conn.Close()\n\t\treturn nil, err\n\t}\n\n\tadapter.SetReadDeadline(time.Time{})\n\treturn conn, nil\n}\n\nfunc buildDTLSConfig(psk []byte, isServer bool) *dtls.Config {\n\tconfig := &dtls.Config{\n\t\tPSK: func(hint []byte) ([]byte, error) {\n\t\t\treturn psk, nil\n\t\t},\n\t\tPSKIdentityHint:         []byte(\"AUTHPWD_admin\"),\n\t\tInsecureSkipVerify:      true,\n\t\tInsecureSkipVerifyHello: true,\n\t\tMTU:                     1200,\n\t\tFlightInterval:          300 * time.Millisecond,\n\t\tExtendedMasterSecret:    dtls.DisableExtendedMasterSecret,\n\t}\n\n\tif isServer {\n\t\tconfig.CipherSuites = []dtls.CipherSuiteID{dtls.TLS_PSK_WITH_AES_128_CBC_SHA256}\n\t} else {\n\t\tconfig.CustomCipherSuites = CustomCipherSuites\n\t}\n\n\treturn config\n}\n\ntype channelAdapter struct {\n\tctx          context.Context\n\tchannel      uint8\n\twriteFn      func([]byte, uint8) error\n\treadChan     chan []byte\n\taddr         net.Addr\n\tmu           sync.Mutex\n\treadDeadline time.Time\n}\n\nfunc (a *channelAdapter) ReadFrom(p []byte) (n int, addr net.Addr, err error) {\n\ta.mu.Lock()\n\tdeadline := a.readDeadline\n\ta.mu.Unlock()\n\n\tif !deadline.IsZero() {\n\t\ttimeout := time.Until(deadline)\n\t\tif timeout <= 0 {\n\t\t\treturn 0, nil, &timeoutError{}\n\t\t}\n\n\t\ttimer := time.NewTimer(timeout)\n\t\tdefer timer.Stop()\n\n\t\tselect {\n\t\tcase data := <-a.readChan:\n\t\t\treturn copy(p, data), a.addr, nil\n\t\tcase <-timer.C:\n\t\t\treturn 0, nil, &timeoutError{}\n\t\tcase <-a.ctx.Done():\n\t\t\treturn 0, nil, net.ErrClosed\n\t\t}\n\t}\n\n\tselect {\n\tcase data := <-a.readChan:\n\t\treturn copy(p, data), a.addr, nil\n\tcase <-a.ctx.Done():\n\t\treturn 0, nil, net.ErrClosed\n\t}\n}\n\nfunc (a *channelAdapter) WriteTo(p []byte, _ net.Addr) (int, error) {\n\tif err := a.writeFn(p, a.channel); err != nil {\n\t\treturn 0, err\n\t}\n\treturn len(p), nil\n}\n\nfunc (a *channelAdapter) Close() error        { return nil }\nfunc (a *channelAdapter) LocalAddr() net.Addr { return &net.UDPAddr{} }\nfunc (a *channelAdapter) SetDeadline(t time.Time) error {\n\ta.mu.Lock()\n\ta.readDeadline = t\n\ta.mu.Unlock()\n\treturn nil\n}\nfunc (a *channelAdapter) SetReadDeadline(t time.Time) error {\n\ta.mu.Lock()\n\ta.readDeadline = t\n\ta.mu.Unlock()\n\treturn nil\n}\nfunc (a *channelAdapter) SetWriteDeadline(time.Time) error { return nil }\n\ntype timeoutError struct{}\n\nfunc (e *timeoutError) Error() string   { return \"i/o timeout\" }\nfunc (e *timeoutError) Timeout() bool   { return true }\nfunc (e *timeoutError) Temporary() bool { return true }\n"
  },
  {
    "path": "pkg/tutk/frame.go",
    "content": "package tutk\n\nimport (\n\t\"encoding/binary\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"sync\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/aac\"\n)\n\nconst (\n\tFrameTypeStart     uint8 = 0x08 // Extended start (36-byte header)\n\tFrameTypeStartAlt  uint8 = 0x09 // StartAlt (36-byte header)\n\tFrameTypeCont      uint8 = 0x00 // Continuation (28-byte header)\n\tFrameTypeContAlt   uint8 = 0x04 // Continuation alt\n\tFrameTypeEndSingle uint8 = 0x01 // Single-packet frame (28-byte)\n\tFrameTypeEndMulti  uint8 = 0x05 // Multi-packet end (28-byte)\n\tFrameTypeEndExt    uint8 = 0x0d // Extended end (36-byte)\n)\n\nconst (\n\tChannelIVideo uint8 = 0x05\n\tChannelAudio  uint8 = 0x03\n\tChannelPVideo uint8 = 0x07\n)\n\nconst frameInfoSize = 40\n\n// FrameInfo - Wyze extended FRAMEINFO (40 bytes at end of packet)\n// Video: 40 bytes, Audio: 16 bytes (uses same struct, fields 16+ are zero)\n//\n// Offset  Size  Field\n// 0-1     2     CodecID     - 0x4E=H264, 0x7B=H265, 0x90=AAC_WYZE\n// 2       1     Flags       - Video: 1=Keyframe, 0=P-frame | Audio: sample rate/bits/channels\n// 3       1     CamIndex    - Camera index\n// 4       1     OnlineNum   - Online number\n// 5       1     FPS         - Framerate (e.g. 20)\n// 6       1     ResTier     - Video: 1=Low(360P), 4=High(HD/2K) | Audio: 0\n// 7       1     Bitrate     - Video: 30=360P, 100=HD, 200=2K | Audio: 1\n// 8-11    4     Timestamp   - Timestamp (increases ~50000/frame for 20fps video)\n// 12-15   4     SessionID   - Session marker (constant per stream)\n// 16-19   4     PayloadSize - Frame payload size in bytes\n// 20-23   4     FrameNo     - Global frame number\n// 24-35   12    DeviceID    - MAC address (ASCII) - video only\n// 36-39   4     Padding     - Always 0 - video only\ntype FrameInfo struct {\n\tCodecID     byte   // 0 (only low byte used)\n\tFlags       uint8  // 2\n\tCamIndex    uint8  // 3\n\tOnlineNum   uint8  // 4\n\tFPS         uint8  // 5: Framerate\n\tResTier     uint8  // 6: Resolution tier (1=Low, 4=High)\n\tBitrate     uint8  // 7: Bitrate index (30=360P, 100=HD, 200=2K)\n\tTimestamp   uint32 // 8-11: Timestamp\n\tSessionID   uint32 // 12-15: Session marker (constant)\n\tPayloadSize uint32 // 16-19: Payload size\n\tFrameNo     uint32 // 20-23: Frame number\n}\n\nfunc (fi *FrameInfo) IsKeyframe() bool {\n\treturn fi.Flags == 0x01\n}\n\nfunc (fi *FrameInfo) SampleRate() uint32 {\n\tidx := (fi.Flags >> 2) & 0x0F\n\tif idx < uint8(len(sampleRates)) {\n\t\treturn sampleRates[idx]\n\t}\n\treturn 16000\n}\n\nfunc (fi *FrameInfo) Channels() uint8 {\n\tif fi.Flags&0x01 == 1 {\n\t\treturn 2\n\t}\n\treturn 1\n}\n\nfunc ParseFrameInfo(data []byte) *FrameInfo {\n\tif len(data) < frameInfoSize {\n\t\treturn nil\n\t}\n\n\toffset := len(data) - frameInfoSize\n\tfi := data[offset:]\n\n\treturn &FrameInfo{\n\t\tCodecID:     fi[0],\n\t\tFlags:       fi[2],\n\t\tCamIndex:    fi[3],\n\t\tOnlineNum:   fi[4],\n\t\tFPS:         fi[5],\n\t\tResTier:     fi[6],\n\t\tBitrate:     fi[7],\n\t\tTimestamp:   binary.LittleEndian.Uint32(fi[8:]),\n\t\tSessionID:   binary.LittleEndian.Uint32(fi[12:]),\n\t\tPayloadSize: binary.LittleEndian.Uint32(fi[16:]),\n\t\tFrameNo:     binary.LittleEndian.Uint32(fi[20:]),\n\t}\n}\n\ntype Packet struct {\n\tChannel    uint8\n\tCodec      byte\n\tTimestamp  uint32\n\tPayload    []byte\n\tIsKeyframe bool\n\tFrameNo    uint32\n\tSampleRate uint32\n\tChannels   uint8\n}\n\ntype PacketHeader struct {\n\tChannel      byte\n\tFrameType    byte\n\tHeaderSize   int\n\tFrameNo      uint32\n\tPktIdx       uint16\n\tPktTotal     uint16\n\tPayloadSize  uint16\n\tHasFrameInfo bool\n}\n\nfunc ParsePacketHeader(data []byte) *PacketHeader {\n\tif len(data) < 28 {\n\t\treturn nil\n\t}\n\n\tframeType := data[1]\n\thdr := &PacketHeader{\n\t\tChannel:   data[0],\n\t\tFrameType: frameType,\n\t}\n\n\tswitch frameType {\n\tcase FrameTypeStart, FrameTypeStartAlt, FrameTypeEndExt:\n\t\thdr.HeaderSize = 36\n\tdefault:\n\t\thdr.HeaderSize = 28\n\t}\n\n\tif len(data) < hdr.HeaderSize {\n\t\treturn nil\n\t}\n\n\tif hdr.HeaderSize == 28 {\n\t\thdr.PktTotal = binary.LittleEndian.Uint16(data[12:])\n\t\tpktIdxOrMarker := binary.LittleEndian.Uint16(data[14:])\n\t\thdr.PayloadSize = binary.LittleEndian.Uint16(data[16:])\n\t\thdr.FrameNo = binary.LittleEndian.Uint32(data[24:])\n\n\t\tif pktIdxOrMarker == 0x0028 && (IsEndFrame(frameType) || hdr.PktTotal == 1) {\n\t\t\thdr.HasFrameInfo = true\n\t\t\tif hdr.PktTotal > 0 {\n\t\t\t\thdr.PktIdx = hdr.PktTotal - 1\n\t\t\t}\n\t\t} else {\n\t\t\thdr.PktIdx = pktIdxOrMarker\n\t\t}\n\t} else {\n\t\thdr.PktTotal = binary.LittleEndian.Uint16(data[20:])\n\t\tpktIdxOrMarker := binary.LittleEndian.Uint16(data[22:])\n\t\thdr.PayloadSize = binary.LittleEndian.Uint16(data[24:])\n\t\thdr.FrameNo = binary.LittleEndian.Uint32(data[32:])\n\n\t\tif pktIdxOrMarker == 0x0028 && (IsEndFrame(frameType) || hdr.PktTotal == 1) {\n\t\t\thdr.HasFrameInfo = true\n\t\t\tif hdr.PktTotal > 0 {\n\t\t\t\thdr.PktIdx = hdr.PktTotal - 1\n\t\t\t}\n\t\t} else {\n\t\t\thdr.PktIdx = pktIdxOrMarker\n\t\t}\n\t}\n\n\treturn hdr\n}\n\nfunc IsStartFrame(frameType uint8) bool {\n\treturn frameType == FrameTypeStart || frameType == FrameTypeStartAlt\n}\n\nfunc IsEndFrame(frameType uint8) bool {\n\treturn frameType == FrameTypeEndSingle ||\n\t\tframeType == FrameTypeEndMulti ||\n\t\tframeType == FrameTypeEndExt\n}\n\nfunc IsContinuationFrame(frameType uint8) bool {\n\treturn frameType == FrameTypeCont || frameType == FrameTypeContAlt\n}\n\ntype channelState struct {\n\tframeNo    uint32     // current frame being assembled\n\tpktTotal   uint16     // expected total packets\n\twaitSeq    uint16     // next expected packet index (0, 1, 2, ...)\n\twaitData   []byte     // accumulated payload data\n\tframeInfo  *FrameInfo // frame info (from end packet)\n\thasStarted bool       // received first packet of frame\n\tlastPktIdx uint16     // last received packet index (for OOO detection)\n}\n\nfunc (cs *channelState) reset() {\n\tcs.frameNo = 0\n\tcs.pktTotal = 0\n\tcs.waitSeq = 0\n\tcs.waitData = cs.waitData[:0]\n\tcs.frameInfo = nil\n\tcs.hasStarted = false\n\tcs.lastPktIdx = 0\n}\n\nconst tsWrapPeriod uint32 = 1000000\n\ntype tsTracker struct {\n\tlastRawTS uint32\n\taccumUS   uint64\n\tfirstTS   bool\n}\n\nfunc (t *tsTracker) update(rawTS uint32) uint64 {\n\tif !t.firstTS {\n\t\tt.firstTS = true\n\t\tt.lastRawTS = rawTS\n\t\treturn 0\n\t}\n\n\tvar delta uint32\n\tif rawTS >= t.lastRawTS {\n\t\tdelta = rawTS - t.lastRawTS\n\t} else {\n\t\t// Wrapped: delta = (wrap - last) + new\n\t\tdelta = (tsWrapPeriod - t.lastRawTS) + rawTS\n\t}\n\n\tt.accumUS += uint64(delta)\n\tt.lastRawTS = rawTS\n\n\treturn t.accumUS\n}\n\ntype FrameHandler struct {\n\tchannels map[byte]*channelState\n\tvideoTS  tsTracker\n\taudioTS  tsTracker\n\toutput   chan *Packet\n\tverbose  bool\n\tclosed   bool\n\tcloseMu  sync.Mutex\n}\n\nfunc NewFrameHandler(verbose bool) *FrameHandler {\n\treturn &FrameHandler{\n\t\tchannels: make(map[byte]*channelState),\n\t\toutput:   make(chan *Packet, 128),\n\t\tverbose:  verbose,\n\t}\n}\n\nfunc (h *FrameHandler) Recv() <-chan *Packet {\n\treturn h.output\n}\n\nfunc (h *FrameHandler) Close() {\n\th.closeMu.Lock()\n\tdefer h.closeMu.Unlock()\n\n\tif h.closed {\n\t\treturn\n\t}\n\th.closed = true\n\tclose(h.output)\n}\n\nfunc (h *FrameHandler) Handle(data []byte) {\n\thdr := ParsePacketHeader(data)\n\tif hdr == nil {\n\t\treturn\n\t}\n\n\tpayload, fi := h.extractPayload(data, hdr.Channel)\n\tif payload == nil {\n\t\treturn\n\t}\n\n\tif h.verbose {\n\t\tfiStr := \"\"\n\t\tif hdr.HasFrameInfo {\n\t\t\tfiStr = \" +FI\"\n\t\t}\n\t\tfmt.Printf(\"[RX] ch=0x%02x type=0x%02x #%d pkt=%d/%d data=%dB%s\\n\",\n\t\t\thdr.Channel, hdr.FrameType,\n\t\t\thdr.FrameNo, hdr.PktIdx, hdr.PktTotal, len(payload), fiStr)\n\t}\n\n\tswitch hdr.Channel {\n\tcase ChannelAudio:\n\t\th.handleAudio(payload, fi)\n\tcase ChannelIVideo, ChannelPVideo:\n\t\th.handleVideo(hdr.Channel, hdr, payload, fi)\n\t}\n}\n\nfunc (h *FrameHandler) extractPayload(data []byte, channel byte) ([]byte, *FrameInfo) {\n\tif len(data) < 2 {\n\t\treturn nil, nil\n\t}\n\n\tframeType := data[1]\n\n\theaderSize := 28\n\tfiSize := 0\n\n\tswitch frameType {\n\tcase FrameTypeStart:\n\t\theaderSize = 36\n\tcase FrameTypeStartAlt:\n\t\theaderSize = 36\n\t\tif len(data) >= 22 {\n\t\t\tpktTotal := binary.LittleEndian.Uint16(data[20:])\n\t\t\tif pktTotal == 1 {\n\t\t\t\tfiSize = frameInfoSize\n\t\t\t}\n\t\t}\n\tcase FrameTypeCont, FrameTypeContAlt:\n\t\theaderSize = 28\n\tcase FrameTypeEndSingle, FrameTypeEndMulti:\n\t\theaderSize = 28\n\t\tfiSize = frameInfoSize\n\tcase FrameTypeEndExt:\n\t\theaderSize = 36\n\t\tfiSize = frameInfoSize\n\tdefault:\n\t\theaderSize = 28\n\t}\n\n\tif len(data) < headerSize {\n\t\treturn nil, nil\n\t}\n\n\tif fiSize == 0 {\n\t\treturn data[headerSize:], nil\n\t}\n\n\tif len(data) < headerSize+fiSize {\n\t\treturn data[headerSize:], nil\n\t}\n\n\tfi := ParseFrameInfo(data)\n\n\tvalidCodec := false\n\tswitch channel {\n\tcase ChannelIVideo, ChannelPVideo:\n\t\tvalidCodec = IsVideoCodec(fi.CodecID)\n\tcase ChannelAudio:\n\t\tvalidCodec = IsAudioCodec(fi.CodecID)\n\t}\n\n\tif validCodec {\n\t\tpayload := data[headerSize : len(data)-fiSize]\n\t\treturn payload, fi\n\t}\n\n\treturn data[headerSize:], nil\n}\n\nfunc (h *FrameHandler) handleVideo(channel byte, hdr *PacketHeader, payload []byte, fi *FrameInfo) {\n\tcs := h.channels[channel]\n\tif cs == nil {\n\t\tcs = &channelState{}\n\t\th.channels[channel] = cs\n\t}\n\n\t// New frame number - reset and start fresh\n\tif hdr.FrameNo != cs.frameNo {\n\t\t// Check if previous frame was incomplete\n\t\tif cs.hasStarted && cs.waitSeq < cs.pktTotal {\n\t\t\tfmt.Printf(\"[DROP] ch=0x%02x #%d INCOMPLETE: got %d/%d pkts\\n\",\n\t\t\t\tchannel, cs.frameNo, cs.waitSeq, cs.pktTotal)\n\t\t}\n\t\tcs.reset()\n\t\tcs.frameNo = hdr.FrameNo\n\t\tcs.pktTotal = hdr.PktTotal\n\t}\n\n\t// If packet index doesn't match expected, reset (data loss)\n\tif hdr.PktIdx != cs.waitSeq {\n\t\tfmt.Printf(\"[OOO] ch=0x%02x #%d frameType=0x%02x pktTotal=%d expected pkt %d, got %d - reset\\n\",\n\t\t\tchannel, hdr.FrameNo, hdr.FrameType, hdr.PktTotal, cs.waitSeq, hdr.PktIdx)\n\t\tcs.reset()\n\t\treturn\n\t}\n\n\t// First packet - mark as started\n\tif cs.waitSeq == 0 {\n\t\tcs.hasStarted = true\n\t}\n\n\tcs.waitData = append(cs.waitData, payload...)\n\tcs.waitSeq++\n\n\t// Store frame info if present\n\tif fi != nil {\n\t\tcs.frameInfo = fi\n\t}\n\n\t// Check if frame is complete\n\tif cs.waitSeq != cs.pktTotal || cs.frameInfo == nil {\n\t\treturn\n\t}\n\n\tfi = cs.frameInfo\n\tdefer cs.reset()\n\n\tif fi.PayloadSize > 0 && uint32(len(cs.waitData)) != fi.PayloadSize {\n\t\tfmt.Printf(\"[SIZE] ch=0x%02x #%d mismatch: expected %d, got %d\\n\",\n\t\t\tchannel, cs.frameNo, fi.PayloadSize, len(cs.waitData))\n\t\treturn\n\t}\n\n\tif len(cs.waitData) == 0 {\n\t\treturn\n\t}\n\n\taccumUS := h.videoTS.update(fi.Timestamp)\n\trtpTS := uint32(accumUS * 90000 / 1000000)\n\n\tpkt := &Packet{\n\t\tChannel:    channel,\n\t\tPayload:    append([]byte{}, cs.waitData...),\n\t\tCodec:      fi.CodecID,\n\t\tTimestamp:  rtpTS,\n\t\tIsKeyframe: fi.IsKeyframe(),\n\t\tFrameNo:    fi.FrameNo,\n\t}\n\n\tif h.verbose {\n\t\tframeType := \"P\"\n\t\tif fi.IsKeyframe() {\n\t\t\tframeType = \"KEY\"\n\t\t}\n\t\tfmt.Printf(\"[OK] ch=0x%02x #%d codec=0x%02x %s size=%d\\n\",\n\t\t\tchannel, fi.FrameNo, fi.CodecID, frameType, len(pkt.Payload))\n\t\tfmt.Printf(\"  [0-1]codec=0x%02x [2]flags=0x%x [3]=%d [4]=%d\\n\",\n\t\t\tfi.CodecID, fi.Flags, fi.CamIndex, fi.OnlineNum)\n\t\tfmt.Printf(\"  [5]=%d [6]=%d [7]=%d [8-11]ts=%d\\n\",\n\t\t\tfi.FPS, fi.ResTier, fi.Bitrate, fi.Timestamp)\n\t\tfmt.Printf(\"  [12-15]=0x%x [16-19]payload=%d [20-23]frameNo=%d\\n\",\n\t\t\tfi.SessionID, fi.PayloadSize, fi.FrameNo)\n\t\tfmt.Printf(\"  rtp_ts=%d accum_us=%d\\n\", rtpTS, accumUS)\n\t\tfmt.Printf(\"  hex: %s\\n\", dumpHex(fi))\n\t}\n\n\th.queue(pkt)\n}\n\nfunc (h *FrameHandler) handleAudio(payload []byte, fi *FrameInfo) {\n\tif len(payload) == 0 || fi == nil {\n\t\treturn\n\t}\n\n\tvar sampleRate uint32\n\tvar channels uint8\n\n\tswitch fi.CodecID {\n\tcase CodecAACRaw, CodecAACADTS, CodecAACLATM, CodecAACAlt:\n\t\tsampleRate, channels = parseAudioParams(payload, fi)\n\tdefault:\n\t\tsampleRate = fi.SampleRate()\n\t\tchannels = fi.Channels()\n\t}\n\n\taccumUS := h.audioTS.update(fi.Timestamp)\n\trtpTS := uint32(accumUS * uint64(sampleRate) / 1000000)\n\n\tpayloadCopy := make([]byte, len(payload))\n\tcopy(payloadCopy, payload)\n\n\tpkt := &Packet{\n\t\tChannel:    ChannelAudio,\n\t\tPayload:    payloadCopy,\n\t\tCodec:      fi.CodecID,\n\t\tTimestamp:  rtpTS,\n\t\tSampleRate: sampleRate,\n\t\tChannels:   channels,\n\t\tFrameNo:    fi.FrameNo,\n\t}\n\n\tif h.verbose {\n\t\tbits := 8\n\t\tif fi.Flags&0x02 != 0 {\n\t\t\tbits = 16\n\t\t}\n\t\tfmt.Printf(\"[OK] Audio #%d codec=0x%02x size=%d\\n\",\n\t\t\tfi.FrameNo, fi.CodecID, len(payload))\n\t\tfmt.Printf(\"  [0-1]codec=0x%02x [2]flags=0x%x(%dHz/%dbit/%dch)\\n\",\n\t\t\tfi.CodecID, fi.Flags, sampleRate, bits, channels)\n\t\tfmt.Printf(\"  [8-11]ts=%d [12-15]=0x%x rtp_ts=%d\\n\",\n\t\t\tfi.Timestamp, fi.SessionID, rtpTS)\n\t\tfmt.Printf(\"  hex: %s\\n\", dumpHex(fi))\n\t}\n\n\th.queue(pkt)\n}\n\nfunc (h *FrameHandler) queue(pkt *Packet) {\n\th.closeMu.Lock()\n\tdefer h.closeMu.Unlock()\n\n\tif h.closed {\n\t\treturn\n\t}\n\n\tselect {\n\tcase h.output <- pkt:\n\tdefault:\n\t\t// Queue full - drop oldest\n\t\tselect {\n\t\tcase <-h.output:\n\t\tdefault:\n\t\t}\n\t\tselect {\n\t\tcase h.output <- pkt:\n\t\tdefault:\n\t\t\t// Queue still full, drop this packet\n\t\t}\n\t}\n}\n\nfunc parseAudioParams(payload []byte, fi *FrameInfo) (sampleRate uint32, channels uint8) {\n\tif aac.IsADTS(payload) {\n\t\tcodec := aac.ADTSToCodec(payload)\n\t\tif codec != nil {\n\t\t\treturn codec.ClockRate, codec.Channels\n\t\t}\n\t}\n\n\tif fi != nil {\n\t\treturn fi.SampleRate(), fi.Channels()\n\t}\n\n\treturn 16000, 1\n}\n\nfunc dumpHex(fi *FrameInfo) string {\n\tb := make([]byte, frameInfoSize)\n\tb[0] = fi.CodecID\n\tb[1] = 0 // High byte (unused)\n\tb[2] = fi.Flags\n\tb[3] = fi.CamIndex\n\tb[4] = fi.OnlineNum\n\tb[5] = fi.FPS\n\tb[6] = fi.ResTier\n\tb[7] = fi.Bitrate\n\tbinary.LittleEndian.PutUint32(b[8:], fi.Timestamp)\n\tbinary.LittleEndian.PutUint32(b[12:], fi.SessionID)\n\tbinary.LittleEndian.PutUint32(b[16:], fi.PayloadSize)\n\tbinary.LittleEndian.PutUint32(b[20:], fi.FrameNo)\n\t// Bytes 24-39 are DeviceID and Padding (not stored in struct)\n\n\thexStr := hex.EncodeToString(b)\n\tformatted := \"\"\n\tfor i := 0; i < len(hexStr); i += 2 {\n\t\tif i > 0 {\n\t\t\tformatted += \" \"\n\t\t}\n\t\tformatted += hexStr[i : i+2]\n\t}\n\treturn formatted\n}\n"
  },
  {
    "path": "pkg/tutk/helpers.go",
    "content": "package tutk\n\nimport (\n\t\"encoding/binary\"\n\t\"time\"\n)\n\nfunc GenSessionID() []byte {\n\tb := make([]byte, 8)\n\tbinary.LittleEndian.PutUint64(b, uint64(time.Now().UnixNano()))\n\treturn b\n}\n\nfunc ICAM(cmd uint32, args ...byte) []byte {\n\t// 0   4943414d  ICAM\n\t// 4   d807ff00  command\n\t// 8   00000000000000\n\t// 15  02        args count\n\t// 16  00000000000000\n\t// 23  0101      args\n\tn := byte(len(args))\n\tb := make([]byte, 23+n)\n\tcopy(b, \"ICAM\")\n\tbinary.LittleEndian.PutUint32(b[4:], cmd)\n\tb[15] = n\n\tcopy(b[23:], args)\n\treturn b\n}\n\nfunc HL(cmdID uint16, payload []byte) []byte {\n\t// 0-1   \"HL\"       magic\n\t// 2     version    (typically 5)\n\t// 3     reserved\n\t// 4-5   cmdID      command ID (uint16 LE)\n\t// 6-7   payloadLen payload length (uint16 LE)\n\t// 8-15  reserved\n\t// 16+   payload\n\tconst headerSize = 16\n\tconst version = 5\n\n\tb := make([]byte, headerSize+len(payload))\n\tcopy(b, \"HL\")\n\tb[2] = version\n\tbinary.LittleEndian.PutUint16(b[4:], cmdID)\n\tbinary.LittleEndian.PutUint16(b[6:], uint16(len(payload)))\n\tcopy(b[headerSize:], payload)\n\treturn b\n}\n\nfunc ParseHL(data []byte) (cmdID uint16, payload []byte, ok bool) {\n\tif len(data) < 16 || data[0] != 'H' || data[1] != 'L' {\n\t\treturn 0, nil, false\n\t}\n\tcmdID = binary.LittleEndian.Uint16(data[4:])\n\tpayloadLen := binary.LittleEndian.Uint16(data[6:])\n\tif len(data) >= 16+int(payloadLen) {\n\t\tpayload = data[16 : 16+payloadLen]\n\t} else if len(data) > 16 {\n\t\tpayload = data[16:]\n\t}\n\treturn cmdID, payload, true\n}\n\nfunc FindHL(data []byte, offset int) []byte {\n\tfor i := offset; i+16 <= len(data); i++ {\n\t\tif data[i] == 'H' && data[i+1] == 'L' {\n\t\t\treturn data[i:]\n\t\t}\n\t}\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/tutk/session0.go",
    "content": "package tutk\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"net\"\n\t\"time\"\n)\n\nfunc (c *Conn) connectDirect(uid string, sid []byte) error {\n\tres, err := writeAndWait(\n\t\tc, func(res []byte) bool { return bytes.Index(res, []byte(\"\\x02\\x06\\x12\\x00\")) == 8 },\n\t\tConnectByUID(stageBroadcast, uid, sid),\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tn := len(res) // should be 200\n\tc.ver = []byte{res[2], res[n-13], res[n-14], res[n-15], res[n-16]}\n\n\t_, err = c.Write(ConnectByUID(stageDirect, uid, sid))\n\treturn err\n}\n\nfunc (c *Conn) connectRemote(uid string, sid []byte) error {\n\tres, err := writeAndWait(\n\t\tc, func(res []byte) bool { return bytes.Index(res, []byte(\"\\x01\\x03\\x43\")) == 8 },\n\t\tConnectByUID(stageGetRemoteIP, uid, sid),\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// Read real IP from cloud server response.\n\t// Important ot use net.IPv4 because slice will be 16 bytes.\n\tc.addr.IP = net.IPv4(res[40], res[41], res[42], res[43])\n\tc.addr.Port = int(binary.BigEndian.Uint16(res[38:]))\n\n\tres, err = writeAndWait(\n\t\tc, func(res []byte) bool { return bytes.Index(res, []byte(\"\\x04\\x04\\x33\")) == 8 },\n\t\tConnectByUID(stageRemoteAck, uid, sid),\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif len(res) == 52 {\n\t\tc.ver = []byte{res[2], res[51], res[50], res[49], res[48]}\n\t} else {\n\t\tc.ver = []byte{res[2]}\n\t}\n\n\t_, err = c.Write(ConnectByUID(stageRemoteOK, uid, sid))\n\treturn err\n}\n\nfunc (c *Conn) clientStart(username, password string) error {\n\t_, err := writeAndWait(\n\t\tc, func(res []byte) bool {\n\t\t\treturn len(res) >= 84 && res[28] == 0 && (res[29] == 0x14 || res[29] == 0x21)\n\t\t},\n\t\tc.session.ClientStart(0, username, password),\n\t\tc.session.ClientStart(1, username, password),\n\t)\n\treturn err\n}\n\nfunc writeAndWait(conn net.Conn, ok func(res []byte) bool, req ...[]byte) ([]byte, error) {\n\tvar t *time.Timer\n\tt = time.AfterFunc(1, func() {\n\t\tfor _, b := range req {\n\t\t\tif _, err := conn.Write(b); err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t\tif t != nil {\n\t\t\tt.Reset(time.Second)\n\t\t}\n\t})\n\tdefer t.Stop()\n\n\tbuf := make([]byte, 1200)\n\n\tfor {\n\t\tn, err := conn.Read(buf)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif ok(buf[:n]) {\n\t\t\treturn buf[:n], nil\n\t\t}\n\t}\n}\n\nconst (\n\tmagic      = \"\\x04\\x02\\x19\"     // include version 0x19\n\tsdkVersion = \"\\x06\\x00\\x03\\x03\" // 3.3.0.6\n)\n\nconst (\n\tstageBroadcast = iota + 1\n\tstageDirect\n\tstageGetPublicIP\n\tstageGetRemoteIP\n\tstageRemoteReq\n\tstageRemoteAck\n\tstageRemoteOK\n)\n\nfunc ConnectByUID(stage byte, uid string, sid8 []byte) []byte {\n\tvar b []byte\n\n\tswitch stage {\n\tcase stageBroadcast, stageDirect:\n\t\tb = make([]byte, 68)\n\t\tcopy(b[8:], \"\\x01\\x06\\x21\")\n\t\tcopy(b[52:], sdkVersion)\n\t\tcopy(b[56:], sid8)\n\t\tb[64] = stage // 1 or 2\n\n\tcase stageGetPublicIP:\n\t\tb = make([]byte, 54)\n\t\tcopy(b[8:], \"\\x07\\x10\\x18\")\n\n\tcase stageGetRemoteIP:\n\t\tb = make([]byte, 112)\n\t\tcopy(b[8:], \"\\x03\\x02\\x34\")\n\t\tcopy(b[100:], sid8)\n\t\tb[108] = stageDirect\n\n\tcase stageRemoteReq:\n\t\tb = make([]byte, 52)\n\t\tcopy(b[8:], \"\\x01\\x04\\x33\")\n\t\tcopy(b[36:], sid8)\n\t\tcopy(b[48:], sdkVersion)\n\n\tcase stageRemoteAck:\n\t\tb = make([]byte, 44)\n\t\tcopy(b[8:], \"\\x02\\x04\\x33\")\n\t\tcopy(b[36:], sid8)\n\n\tcase stageRemoteOK:\n\t\tb = make([]byte, 52)\n\t\tcopy(b[8:], \"\\x04\\x04\\x33\")\n\t\tcopy(b[36:], sid8)\n\t\tcopy(b[48:], sdkVersion)\n\t}\n\n\tcopy(b, magic)\n\tb[3] = 0x02 // connection stage\n\tbinary.LittleEndian.PutUint16(b[4:], uint16(len(b))-16)\n\tcopy(b[16:], uid)\n\n\treturn b\n}\n"
  },
  {
    "path": "pkg/tutk/session16.go",
    "content": "package tutk\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"io\"\n\t\"net\"\n\t\"time\"\n)\n\ntype Session interface {\n\tClose() error\n\n\tClientStart(i byte, username, password string) []byte\n\n\tSendIOCtrl(ctrlType uint32, ctrlData []byte) []byte\n\tSendFrameData(frameInfo, frameData []byte) []byte\n\n\tRecvIOCtrl() (ctrlType uint32, ctrlData []byte, err error)\n\tRecvFrameData() (frameInfo, frameData []byte, err error)\n\n\tSessionRead(chID byte, buf []byte) int\n\tSessionWrite(chID byte, buf []byte) error\n}\n\nfunc NewSession16(conn net.Conn, sid8 []byte) *Session16 {\n\tsid16 := make([]byte, 16)\n\tcopy(sid16[8:], sid8)\n\tcopy(sid16, sid8[:2])\n\tsid16[4] = 0x0c\n\n\treturn &Session16{\n\t\tconn:   conn,\n\t\tsid16:  sid16,\n\t\trawCmd: make(chan []byte, 10),\n\t\trawPkt: make(chan [2][]byte, 100),\n\t}\n}\n\ntype Session16 struct {\n\tconn  net.Conn\n\tsid16 []byte\n\n\trawCmd chan []byte\n\trawPkt chan [2][]byte\n\n\tseqSendCh0 uint16\n\tseqSendCh1 uint16\n\n\tseqSendCmd1 uint16\n\tseqSendAud  uint16\n\n\twaitFSeq uint16\n\twaitCSeq uint16\n\twaitSize int\n\twaitData []byte\n}\n\nfunc (s *Session16) Close() error {\n\tclose(s.rawCmd)\n\tclose(s.rawPkt)\n\treturn nil\n}\n\nfunc (s *Session16) Msg(size uint16) []byte {\n\tb := make([]byte, size)\n\tcopy(b, magic)\n\tb[3] = 0x0a // connected stage\n\tbinary.LittleEndian.PutUint16(b[4:], size-16)\n\tcopy(b[8:], \"\\x07\\x04\\x21\") // client request\n\tcopy(b[12:], s.sid16)\n\treturn b\n}\n\nconst (\n\tmsgHhrSize = 28\n\tcmdHdrSize = 24\n)\n\nfunc (s *Session16) ClientStart(i byte, username, password string) []byte {\n\tconst size = 566 + 32\n\tmsg := s.Msg(size)\n\n\t// 0    00000b0000000000000000000000000022020000fcfc7284\n\t// 24   4d69737300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\n\t// 281  636c69656e740000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\n\t// 538  0100000004000000fb071f000000000000000000000003000000000001000000\n\tcmd := msg[msgHhrSize:]\n\tcopy(cmd, \"\\x00\\x00\\x0b\\x00\")\n\tbinary.LittleEndian.PutUint16(cmd[16:], size-52)\n\tif i == 0 {\n\t\tcmd[18] = 1\n\t} else {\n\t\tcmd[1] = 0x20\n\t}\n\tbinary.LittleEndian.PutUint32(cmd[20:], uint32(time.Now().UnixMilli()))\n\n\t// important values for some cameras (not for df3)\n\tdata := cmd[cmdHdrSize:]\n\tcopy(data, username)\n\tcopy(data[257:], password)\n\n\t// 0100000004000000fb071f000000000000000000000003000000000001000000\n\tcfg := data[257+257:]\n\t//cfg[0] = 1 // 0 - simple proto, 1 - complex proto with \"0Cxx\" commands\n\tcfg[4] = 4\n\tcopy(cfg[8:], \"\\xfb\\x07\\x1f\\x00\")\n\tcfg[22] = 3\n\t//cfg[28] = 1 // unknown\n\treturn msg\n}\n\nfunc (s *Session16) SendIOCtrl(ctrlType uint32, ctrlData []byte) []byte {\n\tdataSize := 4 + uint16(len(ctrlData))\n\tmsg := s.Msg(msgHhrSize + cmdHdrSize + dataSize)\n\n\tcmd := msg[msgHhrSize:]\n\tcopy(cmd, \"\\x00\\x70\\x0b\\x00\")\n\n\ts.seqSendCmd1++ // start from 1, important!\n\tbinary.LittleEndian.PutUint16(cmd[4:], s.seqSendCmd1)\n\n\tbinary.LittleEndian.PutUint16(cmd[16:], dataSize)\n\tbinary.LittleEndian.PutUint32(cmd[20:], uint32(time.Now().UnixMilli()))\n\n\tdata := cmd[cmdHdrSize:]\n\tbinary.LittleEndian.PutUint32(data, ctrlType)\n\tcopy(data[4:], ctrlData)\n\treturn msg\n}\n\nfunc (s *Session16) SendFrameData(frameInfo, frameData []byte) []byte {\n\t// -> 01030b001d0000008802000000002800b0020bf501000000 ... 4f4455412000000088020000030400001d000000000000000bf51f7a9b0100000000000000000000\n\n\tn := uint16(len(frameData))\n\tdataSize := n + 8 + 32\n\tmsg := s.Msg(msgHhrSize + cmdHdrSize + dataSize)\n\n\t// 0   01030b00  command + version\n\t// 4   1d000000  seq\n\t// 8   8802      media size (648)\n\t// 10  00000000\n\t// 14  2800      tail (pkt header) size?\n\t// 16  b002      size (648 + 8 + 32)\n\t// 18  0bf5      random msg id (unixms)\n\t// 20  01000000  fixed\n\tcmd := msg[msgHhrSize:]\n\tcopy(cmd, \"\\x01\\x03\\x0b\\x00\")\n\tbinary.LittleEndian.PutUint16(cmd[4:], s.seqSendAud)\n\ts.seqSendAud++\n\tbinary.LittleEndian.PutUint16(cmd[8:], n)\n\tcmd[14] = 0x28 // important!\n\tbinary.LittleEndian.PutUint16(cmd[16:], dataSize)\n\tbinary.LittleEndian.PutUint16(cmd[18:], uint16(time.Now().UnixMilli()))\n\tcmd[20] = 1\n\n\tdata := cmd[cmdHdrSize:]\n\tcopy(data, frameData)\n\tcopy(data[n:], \"ODUA\\x20\\x00\\x00\\x00\")\n\tcopy(data[n+8:], frameInfo)\n\n\treturn msg\n}\n\nfunc (s *Session16) RecvIOCtrl() (ctrlType uint32, ctrlData []byte, err error) {\n\tbuf, ok := <-s.rawCmd\n\tif !ok {\n\t\treturn 0, nil, io.EOF\n\t}\n\treturn binary.LittleEndian.Uint32(buf), buf[4:], nil\n}\n\nfunc (s *Session16) RecvFrameData() (frameInfo, frameData []byte, err error) {\n\tbuf, ok := <-s.rawPkt\n\tif !ok {\n\t\treturn nil, nil, io.EOF\n\t}\n\treturn buf[0], buf[1], nil\n}\n\nfunc (s *Session16) SessionRead(chID byte, cmd []byte) int {\n\tif chID != 0 {\n\t\treturn s.handleCh1(cmd)\n\t}\n\n\t// 0  01030800  command + version\n\t// 4  00000000  frame seq\n\t// 8  ac880100  total size\n\t// 12 6200      chunk seq\n\t// 14 2000      tail (pkt header) size\n\t// 16 cc00      size\n\t// 18 0000\n\t// 20 01000000  fixed\n\n\tswitch cmd[0] {\n\tcase 0x01:\n\t\tvar packetData [2][]byte\n\n\t\tswitch cmd[1] {\n\t\tcase 0x03:\n\t\t\tframeSeq := binary.LittleEndian.Uint16(cmd[4:])\n\t\t\tchunkSeq := binary.LittleEndian.Uint16(cmd[12:])\n\t\t\tif chunkSeq == 0 {\n\t\t\t\ts.waitFSeq = frameSeq\n\t\t\t\ts.waitCSeq = 0\n\t\t\t\ts.waitData = s.waitData[:0]\n\t\t\t\tpayloadSize := binary.LittleEndian.Uint32(cmd[8:])\n\t\t\t\thdrSize := binary.LittleEndian.Uint16(cmd[14:])\n\t\t\t\ts.waitSize = int(hdrSize) + int(payloadSize)\n\t\t\t} else if frameSeq != s.waitFSeq || chunkSeq != s.waitCSeq {\n\t\t\t\ts.waitCSeq = 0\n\t\t\t\treturn msgMediaLost\n\t\t\t}\n\n\t\t\ts.waitData = append(s.waitData, cmd[24:]...)\n\t\t\tif n := len(s.waitData); n < s.waitSize {\n\t\t\t\ts.waitCSeq++\n\t\t\t\treturn msgMediaChunk\n\t\t\t}\n\n\t\t\ts.waitCSeq = 0\n\n\t\t\tpayloadSize := binary.LittleEndian.Uint32(cmd[8:])\n\t\t\tpacketData[0] = bytes.Clone(s.waitData[payloadSize:])\n\t\t\tpacketData[1] = bytes.Clone(s.waitData[:payloadSize])\n\n\t\tcase 0x04:\n\t\t\tdata := cmd[24:]\n\t\t\thdrSize := binary.LittleEndian.Uint16(cmd[14:])\n\t\t\tpacketData[0] = bytes.Clone(data[:hdrSize])\n\t\t\tpacketData[1] = bytes.Clone(data[hdrSize:])\n\n\t\tdefault:\n\t\t\treturn msgUnknown\n\t\t}\n\n\t\tselect {\n\t\tcase s.rawPkt <- packetData:\n\t\tdefault:\n\t\t\treturn msgError\n\t\t}\n\t\treturn msgMediaFrame\n\n\tcase 0x00:\n\t\tswitch cmd[1] {\n\t\tcase 0x70:\n\t\t\t_ = s.SessionWrite(0, s.msgAck0070(cmd))\n\t\t\tselect {\n\t\t\tcase s.rawCmd <- append([]byte{}, cmd[24:]...):\n\t\t\tdefault:\n\t\t\t}\n\n\t\t\treturn msgCommand\n\t\tcase 0x12:\n\t\t\t_ = s.SessionWrite(0, s.msgAck0012(cmd))\n\t\t\treturn msgDafang0012\n\t\tcase 0x71:\n\t\t\treturn msgCommandAck\n\t\t}\n\t}\n\n\treturn msgUnknown\n}\n\nfunc (s *Session16) msgAck0070(msg28 []byte) []byte {\n\t// <- 00700800010000000000000000000000340000007625a02f ...\n\t// -> 00710800010000000000000000000000000000007625a02f\n\tmsg := s.Msg(msgHhrSize + cmdHdrSize)\n\n\tcmd := msg[msgHhrSize:]\n\tcopy(cmd, \"\\x00\\x71\")\n\tcopy(cmd[2:], msg28[2:6])    // same version and seq\n\tcopy(cmd[20:], msg28[20:24]) // same msg random\n\n\treturn msg\n}\n\nfunc (s *Session16) msgAck0012(msg28 []byte) []byte {\n\t// <- 001208000000000000000000000000000c00000000000000 020000000100000001000000\n\t// -> 00130b000000000000000000000000001400000000000000 0200000001000000010000000000000000000000\n\tconst dataSize = 20\n\tmsg := s.Msg(msgHhrSize + cmdHdrSize + dataSize)\n\n\tcmd := msg[msgHhrSize:]\n\tcopy(cmd, \"\\x00\\x13\\x0b\\x00\")\n\tcmd[16] = dataSize\n\n\tdata := cmd[cmdHdrSize:]\n\tcopy(data, msg28[cmdHdrSize:])\n\n\treturn msg\n}\n\nfunc (s *Session16) handleCh1(cmd []byte) int {\n\t// Channel 1 used for two-way audio. It's important:\n\t// - answer on 0000 command with exact config response (can't set simple proto)\n\t// - send 0012 command at start\n\t// - respond on every 0008 command for smooth playback\n\tswitch cid := string(cmd[:2]); cid {\n\tcase \"\\x00\\x00\": // client start\n\t\t_ = s.SessionWrite(1, s.msgAck0000(cmd))\n\t\t_ = s.SessionWrite(1, s.msg0012())\n\t\treturn msgClientStart\n\tcase \"\\x00\\x07\": // time sync without data\n\t\t_ = s.SessionWrite(1, s.msgAck0007(cmd))\n\t\treturn msgUnknown0007\n\tcase \"\\x00\\x08\": // time sync with data\n\t\t_ = s.SessionWrite(1, s.msgAck0008(cmd))\n\t\treturn msgUnknown0008\n\tcase \"\\x00\\x13\": // ack for 0012\n\t\treturn msgUnknown0013\n\t}\n\treturn msgUnknown\n}\n\nfunc (s *Session16) msgAck0000(msg28 []byte) []byte {\n\t// <- 000008000000000000000000000000001a0200004f47c714 ... 00000000000000000100000004000000fb071f00000000000000000000000300\n\t// -> 00140b00000000000000000000000000200000004f47c714     00000000000000000100000004000000fb071f00000000000000000000000300\n\tconst cmdDataSize = 32\n\tmsg := s.Msg(msgHhrSize + cmdHdrSize + cmdDataSize)\n\n\tcmd := msg[msgHhrSize:]\n\tcopy(cmd, \"\\x00\\x14\\x0b\\x00\")\n\tcmd[16] = cmdDataSize\n\tcopy(cmd[20:], msg28[20:24]) // request id (random)\n\n\t// Important to answer with same data.\n\tdata := cmd[cmdHdrSize:]\n\tcopy(data, msg28[len(msg28)-32:])\n\treturn msg\n}\n\nfunc (s *Session16) msg0012() []byte {\n\t// -> 00120b000000000000000000000000000c00000000000000020000000100000001000000\n\tconst dataSize = 12\n\tmsg := s.Msg(msgHhrSize + cmdHdrSize + dataSize)\n\tcmd := msg[msgHhrSize:]\n\n\tcopy(cmd, \"\\x00\\x12\\x0b\\x00\")\n\tcmd[16] = dataSize\n\tdata := cmd[cmdHdrSize:]\n\n\tdata[0] = 2\n\tdata[4] = 1\n\tdata[9] = 1\n\treturn msg\n}\n\nfunc (s *Session16) msgAck0007(msg28 []byte) []byte {\n\t// <- 000708000000000000000000000000000c00000001000000000000001c551f7a00000000\n\t// -> 010a0b00000000000000000000000000000000000100000000000000\n\tmsg := s.Msg(msgHhrSize + 28)\n\tcmd := msg[msgHhrSize:]\n\tcopy(cmd, \"\\x01\\x0a\\x0b\\x00\")\n\tcmd[20] = 1\n\treturn msg\n}\n\nfunc (s *Session16) msgAck0008(msg28 []byte) []byte {\n\t// <- 000808000000000000000000000000000000f9f0010000000200000050f31f7a\n\t// -> 01090b0000000000000000000000000000000000010000000200000050f31f7a\n\tmsg := s.Msg(msgHhrSize + 28)\n\tcmd := msg[msgHhrSize:]\n\tcopy(cmd, \"\\x01\\x09\\x0b\\x00\")\n\tcopy(cmd[20:], msg28[20:])\n\treturn msg\n}\n\nfunc (s *Session16) SessionWrite(chID byte, buf []byte) error {\n\tswitch chID {\n\tcase 0:\n\t\tbinary.LittleEndian.PutUint16(buf[6:], s.seqSendCh0)\n\t\ts.seqSendCh0++\n\tcase 1:\n\t\tbinary.LittleEndian.PutUint16(buf[6:], s.seqSendCh1)\n\t\ts.seqSendCh1++\n\t\tbuf[14] = 1 // channel\n\t}\n\t_, err := s.conn.Write(buf)\n\treturn err\n}\n"
  },
  {
    "path": "pkg/tutk/session25.go",
    "content": "package tutk\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"net\"\n\t\"time\"\n)\n\nfunc NewSession25(conn net.Conn, sid []byte) *Session25 {\n\treturn &Session25{\n\t\tSession16: NewSession16(conn, sid),\n\t\trb:        NewReorderBuffer(5),\n\t}\n}\n\ntype Session25 struct {\n\t*Session16\n\n\trb *ReorderBuffer\n\n\tseqSendCmd2 uint16\n\tseqSendCnt  uint16\n\n\tseqRecvPkt0 uint16\n\tseqRecvPkt1 uint16\n\tseqRecvCmd2 uint16\n}\n\nconst cmdHdrSize25 = 28\n\nfunc (s *Session25) SendIOCtrl(ctrlType uint32, ctrlData []byte) []byte {\n\tsize := msgHhrSize + cmdHdrSize25 + 4 + uint16(len(ctrlData))\n\tmsg := s.Msg(size)\n\n\t// 0  0070      command\n\t// 2  0b00      version\n\t// 4  1000      seq\n\t// 6  0076      ???\n\tcmd := msg[msgHhrSize:]\n\tcopy(cmd, \"\\x00\\x70\\x0b\\x00\")\n\tbinary.LittleEndian.PutUint16(cmd[4:], s.seqSendCmd1)\n\ts.seqSendCmd1++\n\n\t// 8  0070      command (second time)\n\t// 10 0300      seq\n\t// 12 0100      chunks count\n\t// 14 0000      chunk seq (starts from 0)\n\t// 16 5500      size\n\t// 18 0000      random msg id (always 0)\n\t// 20 03000000  seq (second time)\n\t// 24 00000000\n\t// 28 01010000  ctrlType\n\tcmd[9] = 0x70\n\tcmd[12] = 1\n\tbinary.LittleEndian.PutUint16(cmd[16:], size-52)\n\n\tbinary.LittleEndian.PutUint16(cmd[10:], s.seqSendCmd2)\n\tbinary.LittleEndian.PutUint16(cmd[20:], s.seqSendCmd2)\n\ts.seqSendCmd2++\n\n\tdata := cmd[28:]\n\tbinary.LittleEndian.PutUint32(data, ctrlType)\n\tcopy(data[4:], ctrlData)\n\treturn msg\n}\n\nfunc (s *Session25) SendFrameData(frameInfo, frameData []byte) []byte {\n\treturn nil\n}\n\nfunc (s *Session25) SessionRead(chID byte, cmd []byte) (res int) {\n\tif chID != 0 {\n\t\treturn s.handleCh1(cmd)\n\t}\n\n\tswitch cmd[0] {\n\tcase 0x03, 0x05, 0x07:\n\t\tfor i := 0; cmd != nil; i++ {\n\t\t\tres = s.handleChunk(cmd, i == 0)\n\t\t\tcmd = s.rb.Pop()\n\t\t}\n\t\treturn\n\n\tcase 0x00:\n\t\t_ = s.SessionWrite(0, s.msgAckCounters())\n\t\ts.seqRecvCmd2 = binary.LittleEndian.Uint16(cmd[2:])\n\n\t\tswitch cmd[1] {\n\t\tcase 0x10:\n\t\t\treturn msgUnknown0010 // unknown\n\t\tcase 0x21:\n\t\t\treturn msgClientStartAck2\n\t\tcase 0x70:\n\t\t\tselect {\n\t\t\tcase s.rawCmd <- cmd[28:]:\n\t\t\tdefault:\n\t\t\t}\n\t\t\treturn msgCommand // cmd from camera\n\t\tcase 0x71:\n\t\t\treturn msgCommandAck\n\t\t}\n\n\tcase 0x09:\n\t\t// off  sample\n\t\t// 0    09000b00  cmd1\n\t\t// 4    0d000000  seqCmd1\n\t\t// 12   0000      seqRecvCmd2\n\t\tseq := binary.LittleEndian.Uint16(cmd[12:])\n\t\tif s.seqSendCmd1 > seq {\n\t\t\treturn msgCommandAck\n\t\t}\n\t\treturn msgCounters\n\n\tcase 0x0a:\n\t\t// seq sample\n\t\t// 0   0a080b00\n\t\t// 4   03000000\n\t\t// 8   e2043200\n\t\t// 12  01000000\n\t\t_ = s.SessionWrite(0, s.msgAck0A08(cmd))\n\t\treturn msgUnknown0a08\n\t}\n\n\treturn msgUnknown\n}\n\nfunc (s *Session25) handleChunk(cmd []byte, checkSeq bool) int {\n\tvar cmd2 []byte\n\n\tflags := cmd[1]\n\tif flags&0b1000 == 0 {\n\t\t// off sample\n\t\t// 0   0700      command\n\t\t// 2   0b00      version\n\t\t// 4   2700      seq\n\t\t// 6   0000      ???\n\t\t// 8   0700      command (second time)\n\t\t// 10  1400      seq\n\t\t// 12  1300      chunks count per this frame\n\t\t// 14  1100      chunk seq, starts from 0 (0x20 for last chunk)\n\t\t// 16  0004      frame data size\n\t\t// 18  0000      random msg id (always 0)\n\t\t// 20  02000000  previous frame seq, starts from 0\n\t\t// 24  03000000  current frame seq, starts from 1\n\t\tcmd2 = cmd[8:]\n\t} else {\n\t\t// off sample\n\t\t// 0   070d0b00\n\t\t// 4   30000000\n\t\t// 8   5c965500  ???\n\t\t// 12  ffff0000  ???\n\t\t// 16  0701      fixed command\n\t\t// 18  190001002000a802000006000000070000000\n\t\tcmd2 = cmd[16:]\n\t}\n\n\tseq := binary.LittleEndian.Uint16(cmd2[2:])\n\n\tif checkSeq {\n\t\tif s.rb.Check(seq) {\n\t\t\ts.rb.Next()\n\t\t} else {\n\t\t\ts.rb.Push(seq, cmd)\n\t\t\treturn msgMediaReorder\n\t\t}\n\t}\n\n\t// Check if this is first chunk for frame.\n\t// Handle protocol bug \"0x20 chunk seq for last chunk\" and sometimes\n\t// \"0x20 chunk seq for first chunk if only one chunk\".\n\tif binary.LittleEndian.Uint16(cmd2[6:]) == 0 || binary.LittleEndian.Uint16(cmd2[4:]) == 1 {\n\t\ts.waitData = s.waitData[:0]\n\t\ts.waitCSeq = seq\n\t} else if seq != s.waitCSeq {\n\t\treturn msgMediaLost\n\t}\n\n\ts.waitData = append(s.waitData, cmd2[20:]...)\n\n\tif flags&0b0001 == 0 {\n\t\ts.waitCSeq++\n\t\treturn msgMediaChunk\n\t}\n\n\ts.seqRecvPkt1 = seq\n\t_ = s.SessionWrite(0, s.msgAckCounters())\n\n\tn := len(s.waitData) - 32\n\tpacketData := [2][]byte{bytes.Clone(s.waitData[n:]), bytes.Clone(s.waitData[:n])}\n\n\tselect {\n\tcase s.rawPkt <- packetData:\n\tdefault:\n\t\treturn msgError\n\t}\n\treturn msgMediaFrame\n}\n\nfunc (s *Session25) msgAckCounters() []byte {\n\tmsg := s.Msg(msgHhrSize + cmdHdrSize)\n\n\t// off  sample\n\t// 0    09000b00  cmd1\n\t// 4    2700      seqCmd1\n\t// 6    0000\n\t// 8    1300      seqRecvPkt0\n\t// 10   2600      seqRecvPkt1\n\t// 12   0400      seqRecvCmd2\n\t// 14   00000000\n\t// 18   1400      seqSendCnt\n\t// 20   d91a      random\n\t// 22   0000\n\tcmd := msg[msgHhrSize:]\n\tcopy(cmd, \"\\x09\\x00\\x0b\\x00\")\n\n\tbinary.LittleEndian.PutUint16(cmd[4:], s.seqSendCmd1)\n\ts.seqSendCmd1++\n\n\t// seqRecvPkt0 stores previous value of seqRecvPkt1\n\t// don't understand why this needs\n\tbinary.LittleEndian.PutUint16(cmd[8:], s.seqRecvPkt0)\n\ts.seqRecvPkt0 = s.seqRecvPkt1\n\tbinary.LittleEndian.PutUint16(cmd[10:], s.seqRecvPkt1)\n\tbinary.LittleEndian.PutUint16(cmd[12:], s.seqRecvCmd2)\n\n\tbinary.LittleEndian.PutUint16(cmd[18:], s.seqSendCnt)\n\ts.seqSendCnt++\n\tbinary.LittleEndian.PutUint16(cmd[20:], uint16(time.Now().UnixMilli()))\n\treturn msg\n}\n\nfunc (s *Session25) handleCh1(cmd []byte) int {\n\tswitch cid := string(cmd[:2]); cid {\n\tcase \"\\x00\\x00\": // client start\n\t\treturn msgClientStart\n\tcase \"\\x00\\x07\": // time sync without data\n\t\t_ = s.SessionWrite(1, s.msgAck0007(cmd))\n\t\treturn msgUnknown0007\n\tcase \"\\x00\\x20\": // client start2\n\t\t_ = s.SessionWrite(1, s.msgAck0020(cmd))\n\t\treturn msgClientStart2\n\tcase \"\\x09\\x00\":\n\t\treturn msgUnknown0900\n\tcase \"\\x0a\\x08\":\n\t\treturn msgUnknown0a08\n\t}\n\treturn msgUnknown\n}\n\nfunc (s *Session25) msgAck0020(msg28 []byte) []byte {\n\tconst cmdDataSize = 36\n\n\tmsg := s.Msg(msgHhrSize + cmdHdrSize25 + cmdDataSize)\n\n\tcmd := msg[msgHhrSize:]\n\tcopy(cmd, \"\\x00\\x21\\x0b\\x00\")\n\tcmd[16] = cmdDataSize\n\tcopy(cmd[20:], msg28[20:24]) // request id (random)\n\n\t// 0  00000000\n\t// 4  00010001\n\t// 8  01000000\n\t// 12 04000000\n\t// 16 fb071f00\n\t// 20 00000000\n\t// 24 00000000\n\t// 28 00000300\n\t// 32 01000000\n\tdata := cmd[cmdHdrSize25:]\n\tdata[5] = 1\n\tdata[7] = 1\n\tdata[8] = 1\n\tdata[12] = 4\n\tcopy(data[16:], \"\\xfb\\x07\\x1f\\x00\")\n\tdata[30] = 3\n\tdata[32] = 1\n\treturn msg\n}\n\nfunc (s *Session25) msgAck0A08(msg28 []byte) []byte {\n\t// <- 0a080b005b0000000b51590002000000\n\t// -> 0b000b00000001000b5103000300000000000000\n\tmsg := s.Msg(msgHhrSize + 20)\n\tcmd := msg[msgHhrSize:]\n\tcopy(cmd, \"\\x0b\\x00\\x0b\\x00\")\n\tcopy(cmd[8:], msg28[8:10])\n\treturn msg\n}\n\n// ReorderBuffer used for UDP incoming data. Because the order of the packets may be mixed up.\ntype ReorderBuffer struct {\n\tbuf  map[uint16][]byte\n\tseq  uint16\n\tsize int\n}\n\nfunc NewReorderBuffer(size int) *ReorderBuffer {\n\treturn &ReorderBuffer{buf: make(map[uint16][]byte), size: size}\n}\n\n// Check return OK if this is the seq we are waiting for.\nfunc (r *ReorderBuffer) Check(seq uint16) (ok bool) {\n\treturn seq == r.seq\n}\n\nfunc (r *ReorderBuffer) Next() {\n\tr.seq++\n}\n\n// Available return how much free slots is in the buffer.\nfunc (r *ReorderBuffer) Available() int {\n\treturn r.size - len(r.buf)\n}\n\n// Push new item to buffer. Important! There is no buffer full check here.\nfunc (r *ReorderBuffer) Push(seq uint16, data []byte) {\n\t//log.Printf(\"push seq=%d wait=%d\", seq, r.seq)\n\tr.buf[seq] = bytes.Clone(data)\n}\n\n// Pop latest item from buffer. OK - if items wasn't dropped.\nfunc (r *ReorderBuffer) Pop() []byte {\n\tfor {\n\t\tif data := r.buf[r.seq]; data != nil {\n\t\t\tdelete(r.buf, r.seq)\n\t\t\tr.Next()\n\t\t\t//log.Printf(\"pop seq=%d\", r.seq)\n\t\t\treturn data\n\t\t}\n\t\tif r.Available() > 0 {\n\t\t\treturn nil\n\t\t}\n\t\t//log.Printf(\"drop seq=%d\", r.seq)\n\t\tr.Next() // drop item\n\t}\n}\n"
  },
  {
    "path": "pkg/tuya/README.md",
    "content": "## Useful links\n\n- https://developer.tuya.com/en/docs/iot/webrtc?id=Kacsd4x2hl0se\n- https://github.com/tuya/webrtc-demo-go\n- https://github.com/bacco007/HomeAssistantConfig/blob/master/custom_components/xtend_tuya/multi_manager/tuya_iot/ipc/webrtc/xt_tuya_iot_webrtc_manager.py\n- https://github.com/tuya/tuya-device-sharing-sdk\n- https://github.com/make-all/tuya-local/blob/main/custom_components/tuya_local/cloud.py\n- https://ipc-us.ismartlife.me/\n- https://protect-us.ismartlife.me/"
  },
  {
    "path": "pkg/tuya/client.go",
    "content": "package tuya\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/webrtc\"\n\t\"github.com/pion/rtp\"\n\tpion \"github.com/pion/webrtc/v4\"\n)\n\ntype Client struct {\n\tapi       TuyaAPI\n\tconn      *webrtc.Conn\n\tpc        *pion.PeerConnection\n\tconnected core.Waiter\n\tclosed    bool\n\n\t// HEVC only:\n\tdc         *pion.DataChannel\n\tvideoSSRC  *uint32\n\taudioSSRC  *uint32\n\tstreamType int\n\tisHEVC     bool\n\thandlersMu sync.RWMutex\n\thandlers   map[uint32]func(*rtp.Packet)\n}\n\ntype DataChannelMessage struct {\n\tType string `json:\"type\"` // \"codec\", \"start\", \"recv\", \"complete\"\n\tMsg  string `json:\"msg\"`\n}\n\n// RecvMessage contains SSRC values for video/audio streams\ntype RecvMessage struct {\n\tVideo struct {\n\t\tSSRC uint32 `json:\"ssrc\"`\n\t} `json:\"video\"`\n\tAudio struct {\n\t\tSSRC uint32 `json:\"ssrc\"`\n\t} `json:\"audio\"`\n}\n\nfunc Dial(rawURL string) (core.Producer, error) {\n\tescapedURL := strings.ReplaceAll(rawURL, \"#\", \"%23\")\n\tu, err := url.Parse(escapedURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tquery := u.Query()\n\n\t// Tuya Smart API\n\temail := query.Get(\"email\")\n\tpassword := query.Get(\"password\")\n\n\t// Tuya Cloud API\n\tuid := query.Get(\"uid\")\n\tclientId := query.Get(\"client_id\")\n\tclientSecret := query.Get(\"client_secret\")\n\n\t// Shared params\n\tdeviceId := query.Get(\"device_id\")\n\n\t// Stream params\n\tstreamResolution := query.Get(\"resolution\")\n\n\tuseSmartApi := deviceId != \"\" && email != \"\" && password != \"\"\n\tuseCloudApi := deviceId != \"\" && uid != \"\" && clientId != \"\" && clientSecret != \"\"\n\n\tif streamResolution == \"\" || (streamResolution != \"hd\" && streamResolution != \"sd\") {\n\t\tstreamResolution = \"hd\"\n\t}\n\n\tif !useSmartApi && !useCloudApi {\n\t\treturn nil, errors.New(\"tuya: wrong query params\")\n\t}\n\n\tclient := &Client{\n\t\thandlers: make(map[uint32]func(*rtp.Packet)),\n\t}\n\n\tif useSmartApi {\n\t\tif client.api, err = NewTuyaSmartApiClient(nil, u.Hostname(), email, password, deviceId); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"tuya: %w\", err)\n\t\t}\n\t} else {\n\t\tif client.api, err = NewTuyaCloudApiClient(u.Hostname(), uid, deviceId, clientId, clientSecret); err != nil {\n\t\t\treturn nil, fmt.Errorf(\"tuya: %w\", err)\n\t\t}\n\t}\n\n\tif err := client.api.Init(); err != nil {\n\t\treturn nil, fmt.Errorf(\"tuya: %w\", err)\n\t}\n\n\tclient.streamType = client.api.GetStreamType(streamResolution)\n\tclient.isHEVC = client.api.IsHEVC(client.streamType)\n\n\t// Create a new PeerConnection\n\tconf := pion.Configuration{\n\t\tICEServers:         client.api.GetICEServers(),\n\t\tICETransportPolicy: pion.ICETransportPolicyAll,\n\t\tBundlePolicy:       pion.BundlePolicyMaxBundle,\n\t}\n\n\tapi, err := webrtc.NewAPI()\n\tif err != nil {\n\t\tclient.Close(err)\n\t\treturn nil, err\n\t}\n\n\tclient.pc, err = api.NewPeerConnection(conf)\n\tif err != nil {\n\t\tclient.Close(err)\n\t\treturn nil, err\n\t}\n\n\t// protect from sending ICE candidate before Offer\n\tvar sendOffer core.Waiter\n\n\t// protect from blocking on errors\n\tdefer sendOffer.Done(nil)\n\n\t// Create new WebRTC connection\n\tclient.conn = webrtc.NewConn(client.pc)\n\tclient.conn.FormatName = \"tuya/webrtc\"\n\tclient.conn.Mode = core.ModeActiveProducer\n\tclient.conn.Protocol = \"mqtt\"\n\n\tmqttClient := client.api.GetMqtt()\n\tif mqttClient == nil {\n\t\terr = errors.New(\"tuya: no mqtt client\")\n\t\tclient.Close(err)\n\t\treturn nil, err\n\t}\n\n\t// Set up MQTT handlers\n\tmqttClient.handleAnswer = func(answer AnswerFrame) {\n\t\t// fmt.Printf(\"tuya: answer: %s\\n\", answer.Sdp)\n\n\t\tdesc := pion.SessionDescription{\n\t\t\tType: pion.SDPTypePranswer,\n\t\t\tSDP:  answer.Sdp,\n\t\t}\n\n\t\tif err = client.pc.SetRemoteDescription(desc); err != nil {\n\t\t\tclient.Close(err)\n\t\t\treturn\n\t\t}\n\n\t\tif err = client.conn.SetAnswer(answer.Sdp); err != nil {\n\t\t\tclient.Close(err)\n\t\t\treturn\n\t\t}\n\n\t\tif client.isHEVC {\n\t\t\t// Tuya responds with H264/90000 even for HEVC streams\n\t\t\t// So we need to replace video codecs with HEVC ones from API\n\t\t\tfor _, media := range client.conn.Medias {\n\t\t\t\tif media.Kind == core.KindVideo {\n\t\t\t\t\tcodecs := client.api.GetVideoCodecs()\n\t\t\t\t\tif codecs != nil {\n\t\t\t\t\t\tmedia.Codecs = codecs\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Audio codecs from API as well\n\t\t\t// Tuya responds with multiple audio codecs (PCMU, PCMA)\n\t\t\t// But the quality is bad if we use PCMU and skill only has PCMA\n\t\t\tfor _, media := range client.conn.Medias {\n\t\t\t\tif media.Kind == core.KindAudio {\n\t\t\t\t\tcodecs := client.api.GetAudioCodecs()\n\t\t\t\t\tif codecs != nil {\n\t\t\t\t\t\tmedia.Codecs = codecs\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\n\tmqttClient.handleCandidate = func(candidate CandidateFrame) {\n\t\t// fmt.Printf(\"tuya: candidate: %s\\n\", candidate.Candidate)\n\n\t\tif candidate.Candidate != \"\" {\n\t\t\tclient.conn.AddCandidate(candidate.Candidate)\n\t\t\tif err != nil {\n\t\t\t\tclient.Close(err)\n\t\t\t}\n\t\t}\n\t}\n\n\tmqttClient.handleDisconnect = func() {\n\t\t// fmt.Println(\"tuya: disconnect\")\n\t\tclient.Close(errors.New(\"mqtt: disconnect\"))\n\t}\n\n\tmqttClient.handleError = func(err error) {\n\t\t// fmt.Printf(\"tuya: error: %s\\n\", err.Error())\n\t\tclient.Close(err)\n\t}\n\n\tif client.isHEVC {\n\t\tmaxRetransmits := uint16(5)\n\t\tordered := true\n\t\tclient.dc, err = client.pc.CreateDataChannel(\"fmp4Stream\", &pion.DataChannelInit{\n\t\t\tMaxRetransmits: &maxRetransmits,\n\t\t\tOrdered:        &ordered,\n\t\t})\n\n\t\t// DataChannel receives two types of messages:\n\t\t// 1. String messages: Control messages (codec, recv)\n\t\t// 2. Binary messages: RTP packets with video/audio\n\t\tclient.dc.OnMessage(func(msg pion.DataChannelMessage) {\n\t\t\tif msg.IsString {\n\t\t\t\t// Handle control messages (codec, recv, etc.)\n\t\t\t\tif connected, err := client.probe(msg); err != nil {\n\t\t\t\t\tclient.Close(err)\n\t\t\t\t} else if connected {\n\t\t\t\t\tclient.connected.Done(nil)\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// Handle RTP packets - Route by SSRC retrieved from \"recv\" message\n\t\t\t\tpacket := &rtp.Packet{}\n\t\t\t\tif err := packet.Unmarshal(msg.Data); err != nil {\n\t\t\t\t\t// Skip invalid packets\n\t\t\t\t\treturn\n\t\t\t\t}\n\n\t\t\t\tif handler, ok := client.getHandler(packet.SSRC); ok {\n\t\t\t\t\thandler(packet)\n\t\t\t\t}\n\t\t\t}\n\t\t})\n\n\t\tclient.dc.OnError(func(err error) {\n\t\t\t// fmt.Printf(\"tuya: datachannel error: %s\\n\", err.Error())\n\t\t\tclient.Close(err)\n\t\t})\n\n\t\tclient.dc.OnClose(func() {\n\t\t\t// fmt.Println(\"tuya: datachannel closed\")\n\t\t\tclient.Close(errors.New(\"datachannel: closed\"))\n\t\t})\n\n\t\tclient.dc.OnOpen(func() {\n\t\t\t// fmt.Println(\"tuya: datachannel opened\")\n\n\t\t\tcodecRequest, _ := json.Marshal(DataChannelMessage{\n\t\t\t\tType: \"codec\",\n\t\t\t\tMsg:  \"\",\n\t\t\t})\n\n\t\t\tif err := client.sendMessageToDataChannel(codecRequest); err != nil {\n\t\t\t\tclient.Close(fmt.Errorf(\"failed to send codec request: %w\", err))\n\t\t\t}\n\t\t})\n\t}\n\n\t// Set up pc handler\n\tclient.conn.Listen(func(msg any) {\n\t\tswitch msg := msg.(type) {\n\t\tcase *pion.ICECandidate:\n\t\t\t_ = sendOffer.Wait()\n\t\t\tif err := mqttClient.SendCandidate(\"a=\" + msg.ToJSON().Candidate); err != nil {\n\t\t\t\tclient.Close(err)\n\t\t\t}\n\n\t\tcase pion.PeerConnectionState:\n\t\t\tswitch msg {\n\t\t\tcase pion.PeerConnectionStateNew:\n\t\t\t\tbreak\n\t\t\tcase pion.PeerConnectionStateConnecting:\n\t\t\t\tbreak\n\t\t\tcase pion.PeerConnectionStateConnected:\n\t\t\t\t// On HEVC, wait for DataChannel to be opened and camera to send codec info\n\t\t\t\tif !client.isHEVC {\n\t\t\t\t\tif streamResolution == \"hd\" {\n\t\t\t\t\t\t_ = mqttClient.SendResolution(0)\n\t\t\t\t\t}\n\t\t\t\t\tclient.connected.Done(nil)\n\t\t\t\t}\n\t\t\tcase pion.PeerConnectionStateClosed:\n\t\t\t\tclient.Close(errors.New(\"webrtc: \" + msg.String()))\n\t\t\tdefault:\n\t\t\t\t// client.Close(errors.New(\"webrtc: \" + msg.String()))\n\t\t\t}\n\t\t}\n\t})\n\n\t// Audio first, otherwise tuya will send corrupt sdp\n\tmedias := []*core.Media{\n\t\t{Kind: core.KindAudio, Direction: core.DirectionSendRecv},\n\t\t{Kind: core.KindVideo, Direction: core.DirectionRecvonly},\n\t}\n\n\t// Create offer\n\toffer, err := client.conn.CreateOffer(medias)\n\tif err != nil {\n\t\tclient.Close(err)\n\t\treturn nil, err\n\t}\n\n\t// horter sdp, remove a=extmap... line, device ONLY allow 8KB json payload\n\t// https://github.com/tuya/webrtc-demo-go/blob/04575054f18ccccb6bc9d82939dd46d449544e20/static/js/main.js#L224\n\tre := regexp.MustCompile(`\\r\\na=extmap[^\\r\\n]*`)\n\toffer = re.ReplaceAllString(offer, \"\")\n\n\t// Send offer\n\tif err := mqttClient.SendOffer(offer, streamResolution, client.streamType, client.isHEVC); err != nil {\n\t\terr = fmt.Errorf(\"tuya: %w\", err)\n\t\tclient.Close(err)\n\t\treturn nil, err\n\t}\n\n\tsendOffer.Done(nil)\n\n\t// Wait for connection\n\tif err = client.connected.Wait(); err != nil {\n\t\terr = fmt.Errorf(\"tuya: %w\", err)\n\t\tclient.Close(err)\n\t\treturn nil, err\n\t}\n\n\treturn client, nil\n}\n\nfunc (c *Client) GetMedias() []*core.Media {\n\treturn c.conn.GetMedias()\n}\n\nfunc (c *Client) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {\n\treturn c.conn.GetTrack(media, codec)\n}\n\nfunc (c *Client) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {\n\tlocalTrack := c.conn.GetSenderTrack(media.ID)\n\tif localTrack == nil {\n\t\treturn errors.New(\"webrtc: can't get track\")\n\t}\n\n\t// DISABLED: Speaker Protocol 312 command\n\t// JavaScript client doesn't send this on first call either\n\t// Only subsequent calls (when speakerChloron is set) send Protocol 312\n\t// mqttClient := c.api.GetMqtt()\n\t// if mqttClient != nil {\n\t// \t_ = mqttClient.SendSpeaker(1)\n\t// }\n\n\tpayloadType := codec.PayloadType\n\n\tsender := core.NewSender(media, codec)\n\n\tswitch track.Codec.Name {\n\tcase core.CodecPCMA, core.CodecPCMU, core.CodecPCM, core.CodecPCML:\n\t\t// Frame size affects audio delay with Tuya cameras:\n\t\t// Browser sends standard 20ms frames (160 bytes for G.711), but this causes\n\t\t// up to 4s delay on some Tuya cameras. Increasing to 240 bytes (30ms) reduces\n\t\t// delay to ~2s. Higher values (320+ bytes) don't work and cause issues.\n\t\t// Using 240 bytes (30ms) as optimal balance between latency and stability.\n\t\tframeSize := 240\n\n\t\tvar buf []byte\n\t\tvar seq uint16\n\t\tvar ts uint32\n\n\t\tsender.Handler = func(packet *rtp.Packet) {\n\t\t\tbuf = append(buf, packet.Payload...)\n\n\t\t\tfor len(buf) >= frameSize {\n\t\t\t\tpayload := buf[:frameSize]\n\n\t\t\t\tpkt := &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:    payloadType,\n\t\t\t\t\t\tSequenceNumber: seq,\n\t\t\t\t\t\tTimestamp:      ts,\n\t\t\t\t\t\tSSRC:           packet.SSRC,\n\t\t\t\t\t},\n\t\t\t\t\tPayload: payload,\n\t\t\t\t}\n\n\t\t\t\tseq++\n\t\t\t\tts += uint32(frameSize)\n\t\t\t\tbuf = buf[frameSize:]\n\n\t\t\t\tc.conn.Send += pkt.MarshalSize()\n\t\t\t\t_ = localTrack.WriteRTP(payloadType, pkt)\n\t\t\t}\n\t\t}\n\n\tdefault:\n\t\tsender.Handler = func(packet *rtp.Packet) {\n\t\t\tc.conn.Send += packet.MarshalSize()\n\t\t\t_ = localTrack.WriteRTP(payloadType, packet)\n\t\t}\n\t}\n\n\tsender.HandleRTP(track)\n\tc.conn.Senders = append(c.conn.Senders, sender)\n\n\treturn nil\n}\n\nfunc (c *Client) Start() error {\n\tif len(c.conn.Receivers) == 0 {\n\t\treturn errors.New(\"tuya: no receivers\")\n\t}\n\n\tvar video, audio *core.Receiver\n\tfor _, receiver := range c.conn.Receivers {\n\t\tif receiver.Codec.IsVideo() {\n\t\t\tvideo = receiver\n\t\t} else if receiver.Codec.IsAudio() {\n\t\t\taudio = receiver\n\t\t}\n\t}\n\n\tif c.videoSSRC != nil {\n\t\tc.setHandler(*c.videoSSRC, func(packet *rtp.Packet) {\n\t\t\tif video != nil {\n\t\t\t\tvideo.WriteRTP(packet)\n\t\t\t}\n\t\t})\n\t}\n\n\tif c.audioSSRC != nil {\n\t\tc.setHandler(*c.audioSSRC, func(packet *rtp.Packet) {\n\t\t\tif audio != nil {\n\t\t\t\taudio.WriteRTP(packet)\n\t\t\t}\n\t\t})\n\t}\n\n\treturn c.conn.Start()\n}\n\nfunc (c *Client) Stop() error {\n\tif c.closed {\n\t\treturn nil\n\t}\n\n\tc.closed = true\n\n\tc.clearHandlers()\n\n\tif c.conn != nil {\n\t\t_ = c.conn.Stop()\n\t}\n\n\tif c.api != nil {\n\t\tc.api.Close()\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) Close(err error) error {\n\tc.connected.Done(err)\n\treturn c.Stop()\n}\n\nfunc (c *Client) MarshalJSON() ([]byte, error) {\n\treturn c.conn.MarshalJSON()\n}\n\nfunc (c *Client) setHandler(ssrc uint32, handler func(*rtp.Packet)) {\n\tc.handlersMu.Lock()\n\tdefer c.handlersMu.Unlock()\n\tc.handlers[ssrc] = handler\n}\n\nfunc (c *Client) getHandler(ssrc uint32) (func(*rtp.Packet), bool) {\n\tc.handlersMu.RLock()\n\tdefer c.handlersMu.RUnlock()\n\thandler, ok := c.handlers[ssrc]\n\treturn handler, ok\n}\n\nfunc (c *Client) clearHandlers() {\n\tc.handlersMu.Lock()\n\tdefer c.handlersMu.Unlock()\n\tfor ssrc := range c.handlers {\n\t\tdelete(c.handlers, ssrc)\n\t}\n}\n\nfunc (c *Client) probe(msg pion.DataChannelMessage) (bool, error) {\n\t// fmt.Printf(\"[tuya] Received string message: %s\\n\", string(msg.Data))\n\n\tvar message DataChannelMessage\n\tif err := json.Unmarshal([]byte(msg.Data), &message); err != nil {\n\t\treturn false, err\n\t}\n\n\tswitch message.Type {\n\tcase \"codec\":\n\t\t// Camera responded to our codec request - now request frame start\n\t\tframeRequest, _ := json.Marshal(DataChannelMessage{\n\t\t\tType: \"start\",\n\t\t\tMsg:  \"frame\",\n\t\t})\n\n\t\terr := c.sendMessageToDataChannel(frameRequest)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\tcase \"recv\":\n\t\t// Camera sends SSRC values for video/audio streams\n\t\t// We need these to route incoming RTP packets correctly\n\t\tvar recvMessage RecvMessage\n\t\tif err := json.Unmarshal([]byte(message.Msg), &recvMessage); err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t\tvideoSSRC := recvMessage.Video.SSRC\n\t\taudioSSRC := recvMessage.Audio.SSRC\n\t\tc.videoSSRC = &videoSSRC\n\t\tc.audioSSRC = &audioSSRC\n\n\t\t// Send \"complete\" to tell camera we're ready to receive RTP packets\n\t\tcompleteMsg, _ := json.Marshal(DataChannelMessage{\n\t\t\tType: \"complete\",\n\t\t\tMsg:  \"\",\n\t\t})\n\n\t\terr := c.sendMessageToDataChannel(completeMsg)\n\t\tif err != nil {\n\t\t\treturn false, err\n\t\t}\n\n\t\treturn true, nil\n\t}\n\n\treturn false, nil\n}\n\nfunc (c *Client) sendMessageToDataChannel(message []byte) error {\n\tif c.dc != nil {\n\t\t// fmt.Printf(\"[tuya] sending message to data channel: %s\\n\", message)\n\t\treturn c.dc.Send(message)\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/tuya/cloud_api.go",
    "content": "package tuya\n\nimport (\n\t\"bytes\"\n\t\"crypto/md5\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/webrtc\"\n\t\"github.com/google/uuid\"\n)\n\ntype Token struct {\n\tUID          string `json:\"uid\"`\n\tAccessToken  string `json:\"access_token\"`\n\tRefreshToken string `json:\"refresh_token\"`\n\tExpireTime   int64  `json:\"expire_time\"`\n}\n\ntype WebRTCConfigResponse struct {\n\tTimestamp int64        `json:\"t\"`\n\tSuccess   bool         `json:\"success\"`\n\tResult    WebRTCConfig `json:\"result\"`\n\tMsg       string       `json:\"msg,omitempty\"`\n\tCode      int          `json:\"code,omitempty\"`\n}\n\ntype TokenResponse struct {\n\tTimestamp int64  `json:\"t\"`\n\tSuccess   bool   `json:\"success\"`\n\tResult    Token  `json:\"result\"`\n\tMsg       string `json:\"msg,omitempty\"`\n\tCode      int    `json:\"code,omitempty\"`\n}\n\ntype OpenIoTHubConfigRequest struct {\n\tUID      string `json:\"uid\"`\n\tUniqueID string `json:\"unique_id\"`\n\tLinkType string `json:\"link_type\"`\n\tTopics   string `json:\"topics\"`\n}\n\ntype OpenIoTHubConfig struct {\n\tUrl       string `json:\"url\"`\n\tClientID  string `json:\"client_id\"`\n\tUsername  string `json:\"username\"`\n\tPassword  string `json:\"password\"`\n\tSinkTopic struct {\n\t\tIPC string `json:\"ipc\"`\n\t} `json:\"sink_topic\"`\n\tSourceSink struct {\n\t\tIPC string `json:\"ipc\"`\n\t} `json:\"source_topic\"`\n\tExpireTime int `json:\"expire_time\"`\n}\n\ntype OpenIoTHubConfigResponse struct {\n\tTimestamp int              `json:\"t\"`\n\tSuccess   bool             `json:\"success\"`\n\tResult    OpenIoTHubConfig `json:\"result\"`\n\tMsg       string           `json:\"msg,omitempty\"`\n\tCode      int              `json:\"code,omitempty\"`\n}\n\ntype TuyaCloudApiClient struct {\n\tTuyaClient\n\tuid             string\n\tclientId        string\n\tclientSecret    string\n\taccessToken     string\n\trefreshToken    string\n\trefreshingToken bool\n}\n\nfunc NewTuyaCloudApiClient(baseUrl, uid, deviceId, clientId, clientSecret string) (*TuyaCloudApiClient, error) {\n\tmqttClient := NewTuyaMqttClient(deviceId)\n\n\tclient := &TuyaCloudApiClient{\n\t\tTuyaClient: TuyaClient{\n\t\t\thttpClient: &http.Client{Timeout: 15 * time.Second},\n\t\t\tmqtt:       mqttClient,\n\t\t\tdeviceId:   deviceId,\n\t\t\texpireTime: 0,\n\t\t\tbaseUrl:    baseUrl,\n\t\t},\n\t\tuid:             uid,\n\t\tclientId:        clientId,\n\t\tclientSecret:    clientSecret,\n\t\trefreshingToken: false,\n\t}\n\n\treturn client, nil\n}\n\n// WebRTC Flow\nfunc (c *TuyaCloudApiClient) Init() error {\n\tif err := c.initToken(); err != nil {\n\t\treturn fmt.Errorf(\"failed to initialize token: %w\", err)\n\t}\n\n\twebrtcConfig, err := c.loadWebrtcConfig()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to load webrtc config: %w\", err)\n\t}\n\n\thubConfig, err := c.loadHubConfig()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to load hub config: %w\", err)\n\t}\n\n\tif err := c.mqtt.Start(hubConfig, webrtcConfig, c.skill.WebRTC); err != nil {\n\t\treturn fmt.Errorf(\"failed to start MQTT: %w\", err)\n\t}\n\n\tif c.skill.LowPower > 0 {\n\t\t_ = c.mqtt.WakeUp(c.localKey)\n\t}\n\n\treturn nil\n}\n\nfunc (c *TuyaCloudApiClient) GetStreamUrl(streamType string) (streamUrl string, err error) {\n\tif err := c.initToken(); err != nil {\n\t\treturn \"\", fmt.Errorf(\"failed to initialize token: %w\", err)\n\t}\n\n\turl := fmt.Sprintf(\"https://%s/v1.0/devices/%s/stream/actions/allocate\", c.baseUrl, c.deviceId)\n\n\trequest := &AllocateRequest{\n\t\tType: streamType,\n\t}\n\n\tbody, err := c.request(\"POST\", url, request)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tvar allocResponse AllocateResponse\n\terr = json.Unmarshal(body, &allocResponse)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tif !allocResponse.Success {\n\t\treturn \"\", errors.New(allocResponse.Msg)\n\t}\n\n\treturn allocResponse.Result.URL, nil\n}\n\nfunc (c *TuyaCloudApiClient) initToken() (err error) {\n\tif c.refreshingToken {\n\t\treturn nil\n\t}\n\n\tnow := time.Now().Unix()\n\tif (c.expireTime - 60) > now {\n\t\treturn nil\n\t}\n\n\tc.refreshingToken = true\n\n\turl := fmt.Sprintf(\"https://%s/v1.0/token?grant_type=1\", c.baseUrl)\n\n\tc.accessToken = \"\"\n\tc.refreshToken = \"\"\n\n\tbody, err := c.request(\"GET\", url, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar tokenResponse TokenResponse\n\terr = json.Unmarshal(body, &tokenResponse)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !tokenResponse.Success {\n\t\treturn errors.New(tokenResponse.Msg)\n\t}\n\n\tc.accessToken = tokenResponse.Result.AccessToken\n\tc.refreshToken = tokenResponse.Result.RefreshToken\n\tc.expireTime = tokenResponse.Timestamp + tokenResponse.Result.ExpireTime\n\tc.refreshingToken = false\n\n\treturn nil\n}\n\nfunc (c *TuyaCloudApiClient) loadWebrtcConfig() (*WebRTCConfig, error) {\n\turl := fmt.Sprintf(\"https://%s/v1.0/users/%s/devices/%s/webrtc-configs\", c.baseUrl, c.uid, c.deviceId)\n\n\tbody, err := c.request(\"GET\", url, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar webRTCConfigResponse WebRTCConfigResponse\n\terr = json.Unmarshal(body, &webRTCConfigResponse)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif !webRTCConfigResponse.Success {\n\t\treturn nil, fmt.Errorf(webRTCConfigResponse.Msg)\n\t}\n\n\terr = json.Unmarshal([]byte(webRTCConfigResponse.Result.Skill), &c.skill)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Store LocalKey (not sure if cloud api provides this, but we need it for low power cameras)\n\tc.localKey = webRTCConfigResponse.Result.LocalKey\n\n\ticeServers, err := json.Marshal(&webRTCConfigResponse.Result.P2PConfig.Ices)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tc.iceServers, err = webrtc.UnmarshalICEServers(iceServers)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &webRTCConfigResponse.Result, nil\n}\n\nfunc (c *TuyaCloudApiClient) loadHubConfig() (config *MQTTConfig, err error) {\n\turl := fmt.Sprintf(\"https://%s/v2.0/open-iot-hub/access/config\", c.baseUrl)\n\n\trequest := &OpenIoTHubConfigRequest{\n\t\tUID:      c.uid,\n\t\tUniqueID: uuid.New().String(),\n\t\tLinkType: \"mqtt\",\n\t\tTopics:   \"ipc\",\n\t}\n\n\tbody, err := c.request(\"POST\", url, request)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar openIoTHubConfigResponse OpenIoTHubConfigResponse\n\terr = json.Unmarshal(body, &openIoTHubConfigResponse)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif !openIoTHubConfigResponse.Success {\n\t\treturn nil, fmt.Errorf(openIoTHubConfigResponse.Msg)\n\t}\n\n\treturn &MQTTConfig{\n\t\tUrl:            openIoTHubConfigResponse.Result.Url,\n\t\tUsername:       openIoTHubConfigResponse.Result.Username,\n\t\tPassword:       openIoTHubConfigResponse.Result.Password,\n\t\tClientID:       openIoTHubConfigResponse.Result.ClientID,\n\t\tPublishTopic:   openIoTHubConfigResponse.Result.SinkTopic.IPC,\n\t\tSubscribeTopic: openIoTHubConfigResponse.Result.SourceSink.IPC,\n\t}, nil\n}\n\nfunc (c *TuyaCloudApiClient) request(method string, url string, body any) ([]byte, error) {\n\tvar bodyReader io.Reader\n\tif body != nil {\n\t\tjsonBody, err := json.Marshal(body)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tbodyReader = bytes.NewReader(jsonBody)\n\t}\n\n\treq, err := http.NewRequest(method, url, bodyReader)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tts := time.Now().UnixNano() / 1000000\n\tsign := c.calBusinessSign(ts)\n\n\treq.Header.Set(\"Accept\", \"*\")\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Access-Control-Allow-Origin\", \"*\")\n\treq.Header.Set(\"Access-Control-Allow-Methods\", \"*\")\n\treq.Header.Set(\"Access-Control-Allow-Headers\", \"*\")\n\treq.Header.Set(\"mode\", \"no-cors\")\n\treq.Header.Set(\"client_id\", c.clientId)\n\treq.Header.Set(\"access_token\", c.accessToken)\n\treq.Header.Set(\"sign\", sign)\n\treq.Header.Set(\"t\", strconv.FormatInt(ts, 10))\n\n\tresponse, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer response.Body.Close()\n\n\tres, err := io.ReadAll(response.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif response.StatusCode != http.StatusOK {\n\t\treturn nil, err\n\t}\n\n\treturn res, nil\n}\n\nfunc (c *TuyaCloudApiClient) calBusinessSign(ts int64) string {\n\tdata := fmt.Sprintf(\"%s%s%s%d\", c.clientId, c.accessToken, c.clientSecret, ts)\n\tval := md5.Sum([]byte(data))\n\tres := fmt.Sprintf(\"%X\", val)\n\treturn res\n}\n"
  },
  {
    "path": "pkg/tuya/helper.go",
    "content": "package tuya\n\nimport (\n\t\"crypto/md5\"\n\tcryptoRand \"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/x509\"\n\t\"encoding/hex\"\n\t\"encoding/pem\"\n\t\"errors\"\n\t\"net/http\"\n\t\"net/http/cookiejar\"\n\t\"regexp\"\n\t\"time\"\n\n\t\"golang.org/x/net/publicsuffix\"\n)\n\nfunc EncryptPassword(password, pbKey string) (string, error) {\n\t// Hash password with MD5\n\thasher := md5.New()\n\thasher.Write([]byte(password))\n\thashedPassword := hex.EncodeToString(hasher.Sum(nil))\n\n\t// Decode PEM public key\n\tblock, _ := pem.Decode([]byte(\"-----BEGIN PUBLIC KEY-----\\n\" + pbKey + \"\\n-----END PUBLIC KEY-----\"))\n\tif block == nil {\n\t\treturn \"\", errors.New(\"failed to decode PEM block\")\n\t}\n\n\tpubKey, err := x509.ParsePKIXPublicKey(block.Bytes)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\trsaPubKey, ok := pubKey.(*rsa.PublicKey)\n\tif !ok {\n\t\treturn \"\", errors.New(\"not an RSA public key\")\n\t}\n\n\t// Encrypt with RSA\n\tencrypted, err := rsa.EncryptPKCS1v15(cryptoRand.Reader, rsaPubKey, []byte(hashedPassword))\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// Convert to hex string\n\treturn hex.EncodeToString(encrypted), nil\n}\n\nfunc IsEmailAddress(input string) bool {\n\temailRegex := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$`)\n\treturn emailRegex.MatchString(input)\n}\n\nfunc CreateHTTPClientWithSession() *http.Client {\n\tjar, err := cookiejar.New(&cookiejar.Options{\n\t\tPublicSuffixList: publicsuffix.List,\n\t})\n\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\treturn &http.Client{\n\t\tTimeout: 30 * time.Second,\n\t\tJar:     jar,\n\t}\n}\n"
  },
  {
    "path": "pkg/tuya/interface.go",
    "content": "package tuya\n\nimport (\n\t\"net/http\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\tpionWebrtc \"github.com/pion/webrtc/v4\"\n)\n\ntype TuyaAPI interface {\n\tGetMqtt() *TuyaMqttClient\n\n\tGetStreamType(streamResolution string) int\n\tIsHEVC(streamType int) bool\n\n\tGetVideoCodecs() []*core.Codec\n\tGetAudioCodecs() []*core.Codec\n\n\tGetStreamUrl(streamUrl string) (string, error)\n\tGetICEServers() []pionWebrtc.ICEServer\n\n\tInit() error\n\tClose()\n}\n\ntype TuyaClient struct {\n\tTuyaAPI\n\n\thttpClient *http.Client\n\tmqtt       *TuyaMqttClient\n\tbaseUrl    string\n\texpireTime int64\n\tdeviceId   string\n\tlocalKey   string\n\tskill      *Skill\n\ticeServers []pionWebrtc.ICEServer\n}\n\ntype AudioAttributes struct {\n\tCallMode           []int `json:\"call_mode\"`           // 1 = one way, 2 = two way\n\tHardwareCapability []int `json:\"hardware_capability\"` // 1 = mic, 2 = speaker\n}\n\ntype ICEServer struct {\n\tUrls       string `json:\"urls\"`\n\tUsername   string `json:\"username,omitempty\"`\n\tCredential string `json:\"credential,omitempty\"`\n\tTTL        int    `json:\"ttl,omitempty\"`\n}\n\ntype WebICE struct {\n\tUrls       string `json:\"urls\"`\n\tUsername   string `json:\"username,omitempty\"`\n\tCredential string `json:\"credential,omitempty\"`\n}\n\ntype P2PConfig struct {\n\tIces []ICEServer `json:\"ices\"`\n}\n\ntype AudioSkill struct {\n\tChannels   int `json:\"channels\"`\n\tDataBit    int `json:\"dataBit\"`\n\tCodecType  int `json:\"codecType\"`\n\tSampleRate int `json:\"sampleRate\"`\n}\n\ntype VideoSkill struct {\n\tStreamType int    `json:\"streamType\"` // 2 = main stream (HD), 4 = sub stream (SD)\n\tCodecType  int    `json:\"codecType\"`  // 2 = H264, 4 = H265 (HEVC)\n\tWidth      int    `json:\"width\"`\n\tHeight     int    `json:\"height\"`\n\tSampleRate int    `json:\"sampleRate\"`\n\tProfileId  string `json:\"profileId,omitempty\"`\n}\n\ntype Skill struct {\n\tWebRTC   int          `json:\"webrtc\"`             // Bit flags: bit 4=speaker, bit 5=clarity, bit 6=record\n\tLowPower int          `json:\"lowPower,omitempty\"` // 1 = battery-powered camera\n\tAudios   []AudioSkill `json:\"audios\"`\n\tVideos   []VideoSkill `json:\"videos\"`\n}\n\ntype WebRTCConfig struct {\n\tAudioAttributes      AudioAttributes `json:\"audio_attributes\"`\n\tAuth                 string          `json:\"auth\"`\n\tID                   string          `json:\"id\"`\n\tLocalKey             string          `json:\"local_key,omitempty\"`\n\tMotoID               string          `json:\"moto_id\"`\n\tP2PConfig            P2PConfig       `json:\"p2p_config\"`\n\tProtocolVersion      string          `json:\"protocol_version\"`\n\tSkill                string          `json:\"skill\"`\n\tSupportsWebRTCRecord bool            `json:\"supports_webrtc_record\"`\n\tSupportsWebRTC       bool            `json:\"supports_webrtc\"`\n\tVedioClaritiy        int             `json:\"vedio_clarity\"`\n\tVideoClaritiy        int             `json:\"video_clarity\"`\n\tVideoClarities       []int           `json:\"video_clarities\"`\n}\n\ntype MQTTConfig struct {\n\tUrl            string `json:\"url\"`\n\tPublishTopic   string `json:\"publish_topic\"`\n\tSubscribeTopic string `json:\"subscribe_topic\"`\n\tClientID       string `json:\"client_id\"`\n\tUsername       string `json:\"username\"`\n\tPassword       string `json:\"password\"`\n}\n\ntype Allocate struct {\n\tURL string `json:\"url\"`\n}\n\ntype AllocateRequest struct {\n\tType string `json:\"type\"`\n}\n\ntype AllocateResponse struct {\n\tSuccess bool     `json:\"success\"`\n\tResult  Allocate `json:\"result\"`\n\tMsg     string   `json:\"msg,omitempty\"`\n}\n\nfunc (c *TuyaClient) GetICEServers() []pionWebrtc.ICEServer {\n\treturn c.iceServers\n}\n\nfunc (c *TuyaClient) GetMqtt() *TuyaMqttClient {\n\treturn c.mqtt\n}\n\n// GetStreamType returns the Skill StreamType for the requested resolution\n// Returns Skill values (2 or 4), not MQTT values (0 or 1)\n// - \"hd\" → highest resolution streamType (usually 2 = mainStream)\n// - \"sd\" → lowest resolution streamType (usually 4 = substream)\n//\n// These values must be mapped before sending to MQTT:\n// - streamType 2 → MQTT stream_type 0\n// - streamType 4 → MQTT stream_type 1\nfunc (c *TuyaClient) GetStreamType(streamResolution string) int {\n\t// Default streamType if nothing is found\n\tdefaultStreamType := 1\n\n\tif c.skill == nil || len(c.skill.Videos) == 0 {\n\t\treturn defaultStreamType\n\t}\n\n\t// Find the highest and lowest resolution based on pixel count\n\tvar highestResType = defaultStreamType\n\tvar highestRes = 0\n\tvar lowestResType = defaultStreamType\n\tvar lowestRes = 0\n\n\tfor _, video := range c.skill.Videos {\n\t\tres := video.Width * video.Height\n\n\t\t// Highest Resolution\n\t\tif res > highestRes {\n\t\t\thighestRes = res\n\t\t\thighestResType = video.StreamType\n\t\t}\n\n\t\t// Lower Resolution (or first if not set yet)\n\t\tif lowestRes == 0 || res < lowestRes {\n\t\t\tlowestRes = res\n\t\t\tlowestResType = video.StreamType\n\t\t}\n\t}\n\n\t// Return the streamType based on the selection\n\tswitch streamResolution {\n\tcase \"hd\":\n\t\treturn highestResType\n\tcase \"sd\":\n\t\treturn lowestResType\n\tdefault:\n\t\treturn defaultStreamType\n\t}\n}\n\n// IsHEVC checks if the given streamType uses H265 (HEVC) codec\n// HEVC cameras use DataChannel, H264 cameras use RTP tracks\n// - codecType 4 = H265 (HEVC) → DataChannel mode\n// - codecType 2 = H264 → Normal RTP mode\nfunc (c *TuyaClient) IsHEVC(streamType int) bool {\n\tfor _, video := range c.skill.Videos {\n\t\tif video.StreamType == streamType {\n\t\t\treturn video.CodecType == 4 // 4 = H265/HEVC\n\t\t}\n\t}\n\n\treturn false\n}\n\nfunc (c *TuyaClient) GetVideoCodecs() []*core.Codec {\n\tif len(c.skill.Videos) > 0 {\n\t\tcodecs := make([]*core.Codec, 0)\n\n\t\tfor _, video := range c.skill.Videos {\n\t\t\tname := core.CodecH264\n\t\t\tif c.IsHEVC(video.StreamType) {\n\t\t\t\tname = core.CodecH265\n\t\t\t}\n\n\t\t\tcodec := &core.Codec{\n\t\t\t\tName:      name,\n\t\t\t\tClockRate: uint32(video.SampleRate),\n\t\t\t}\n\n\t\t\tcodecs = append(codecs, codec)\n\t\t}\n\n\t\tif len(codecs) > 0 {\n\t\t\treturn codecs\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (c *TuyaClient) GetAudioCodecs() []*core.Codec {\n\tif len(c.skill.Audios) > 0 {\n\t\tcodecs := make([]*core.Codec, 0)\n\n\t\tfor _, audio := range c.skill.Audios {\n\t\t\tname := getAudioCodecName(&audio)\n\n\t\t\tcodec := &core.Codec{\n\t\t\t\tName:      name,\n\t\t\t\tClockRate: uint32(audio.SampleRate),\n\t\t\t\tChannels:  uint8(audio.Channels),\n\t\t\t}\n\t\t\tcodecs = append(codecs, codec)\n\t\t}\n\n\t\tif len(codecs) > 0 {\n\t\t\treturn codecs\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (c *TuyaClient) Close() {\n\tc.mqtt.Stop()\n\tc.httpClient.CloseIdleConnections()\n}\n\n// https://protect-us.ismartlife.me/\nfunc getAudioCodecName(audioSkill *AudioSkill) string {\n\tswitch audioSkill.CodecType {\n\t// case 100:\n\t// \treturn \"ADPCM\"\n\tcase 101:\n\t\treturn core.CodecPCML\n\tcase 102, 103, 104:\n\t\treturn core.CodecAAC\n\tcase 105:\n\t\treturn core.CodecPCMU\n\tcase 106:\n\t\treturn core.CodecPCMA\n\t// case 107:\n\t// \treturn \"G726-32\"\n\t// case 108:\n\t// \treturn \"SPEEX\"\n\tcase 109:\n\t\treturn core.CodecMP3\n\tdefault:\n\t\treturn core.CodecPCML\n\t}\n}\n"
  },
  {
    "path": "pkg/tuya/mqtt.go",
    "content": "package tuya\n\nimport (\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"hash/crc32\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\tmqtt \"github.com/eclipse/paho.mqtt.golang\"\n)\n\ntype TuyaMqttClient struct {\n\tclient           mqtt.Client\n\twaiter           core.Waiter\n\twakeupWaiter     core.Waiter\n\tspeakerWaiter    core.Waiter\n\tpublishTopic     string\n\tsubscribeTopic   string\n\tauth             string\n\ticeServers       []ICEServer\n\tuid              string\n\tmotoId           string\n\tdeviceId         string\n\tsessionId        string\n\tclosed           bool\n\twebrtcVersion    int\n\thandleAnswer     func(answer AnswerFrame)\n\thandleCandidate  func(candidate CandidateFrame)\n\thandleDisconnect func()\n\thandleError      func(err error)\n}\n\ntype MqttFrameHeader struct {\n\tType          string `json:\"type\"`\n\tFrom          string `json:\"from\"`\n\tTo            string `json:\"to\"`\n\tSubDevID      string `json:\"sub_dev_id\"`\n\tSessionID     string `json:\"sessionid\"`\n\tMotoID        string `json:\"moto_id\"`\n\tTransactionID string `json:\"tid\"`\n}\n\ntype MqttFrame struct {\n\tHeader  MqttFrameHeader `json:\"header\"`\n\tMessage json.RawMessage `json:\"msg\"`\n}\n\ntype OfferFrame struct {\n\tMode              string      `json:\"mode\"`\n\tSdp               string      `json:\"sdp\"`\n\tStreamType        int         `json:\"stream_type\"` // 0: mainStream(HD), 1: substream(SD)\n\tAuth              string      `json:\"auth\"`\n\tDatachannelEnable bool        `json:\"datachannel_enable\"` // true for HEVC, false for H264\n\tToken             []ICEServer `json:\"token\"`\n}\n\ntype AnswerFrame struct {\n\tMode string `json:\"mode\"`\n\tSdp  string `json:\"sdp\"`\n}\n\ntype CandidateFrame struct {\n\tMode      string `json:\"mode\"`\n\tCandidate string `json:\"candidate\"`\n}\n\ntype ResolutionFrame struct {\n\tMode  string `json:\"mode\"`\n\tValue int    `json:\"cmdValue\"` // 0: HD, 1: SD\n}\n\ntype SpeakerFrame struct {\n\tMode  string `json:\"mode\"`\n\tValue int    `json:\"cmdValue\"` // 0: off, 1: on\n}\n\ntype DisconnectFrame struct {\n\tMode string `json:\"mode\"`\n}\n\ntype MqttLowPowerMessage struct {\n\tProtocol int    `json:\"protocol\"`\n\tT        int    `json:\"t\"`\n\tS        int    `json:\"s,omitempty\"`\n\tType     string `json:\"type,omitempty\"`\n\tData     struct {\n\t\tDevID                string                 `json:\"devId,omitempty\"`\n\t\tOnline               bool                   `json:\"online,omitempty\"`\n\t\tLastOnlineChangeTime int64                  `json:\"lastOnlineChangeTime,omitempty\"`\n\t\tGwID                 string                 `json:\"gwId,omitempty\"`\n\t\tCmd                  string                 `json:\"cmd,omitempty\"`\n\t\tDps                  map[string]interface{} `json:\"dps,omitempty\"`\n\t} `json:\"data\"`\n}\n\ntype MqttMessage struct {\n\tProtocol int       `json:\"protocol\"`\n\tPv       string    `json:\"pv\"`\n\tT        int64     `json:\"t\"`\n\tData     MqttFrame `json:\"data\"`\n}\n\nfunc NewTuyaMqttClient(deviceId string) *TuyaMqttClient {\n\treturn &TuyaMqttClient{\n\t\tdeviceId:     deviceId,\n\t\tsessionId:    core.RandString(6, 62),\n\t\twaiter:       core.Waiter{},\n\t\twakeupWaiter: core.Waiter{},\n\t}\n}\n\nfunc (c *TuyaMqttClient) Start(hubConfig *MQTTConfig, webrtcConfig *WebRTCConfig, webrtcVersion int) error {\n\tc.webrtcVersion = webrtcVersion\n\tc.motoId = webrtcConfig.MotoID\n\tc.auth = webrtcConfig.Auth\n\tc.iceServers = webrtcConfig.P2PConfig.Ices\n\n\tc.publishTopic = hubConfig.PublishTopic\n\tc.subscribeTopic = hubConfig.SubscribeTopic\n\n\tc.publishTopic = strings.Replace(c.publishTopic, \"moto_id\", c.motoId, 1)\n\tc.publishTopic = strings.Replace(c.publishTopic, \"{device_id}\", c.deviceId, 1)\n\n\tparts := strings.Split(c.subscribeTopic, \"/\")\n\tc.uid = parts[3]\n\n\topts := mqtt.NewClientOptions().AddBroker(hubConfig.Url).\n\t\tSetClientID(hubConfig.ClientID).\n\t\tSetUsername(hubConfig.Username).\n\t\tSetPassword(hubConfig.Password).\n\t\tSetOnConnectHandler(c.onConnect).\n\t\tSetAutoReconnect(true).\n\t\tSetMaxReconnectInterval(30 * time.Second).\n\t\tSetConnectTimeout(30 * time.Second).\n\t\tSetKeepAlive(60 * time.Second).\n\t\tSetPingTimeout(20 * time.Second)\n\n\tc.client = mqtt.NewClient(opts)\n\n\tif token := c.client.Connect(); token.Wait() && token.Error() != nil {\n\t\treturn token.Error()\n\t}\n\n\tif err := c.waiter.Wait(); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nfunc (c *TuyaMqttClient) Stop() {\n\tc.waiter.Done(errors.New(\"mqtt: stopped\"))\n\tc.wakeupWaiter.Done(errors.New(\"mqtt: stopped\"))\n\tc.speakerWaiter.Done(errors.New(\"mqtt: stopped\"))\n\n\tif c.client != nil {\n\t\t_ = c.SendDisconnect()\n\t\tc.client.Disconnect(100)\n\t}\n\n\tc.closed = true\n}\n\n// WakeUp sends a wake-up signal to battery-powered cameras (LowPower mode).\n// The camera wakes up and starts responding immediately - we don't wait for dps[149].\n// Note: LowPower cameras sleep after ~3 minutes of inactivity.\nfunc (c *TuyaMqttClient) WakeUp(localKey string) error {\n\t// Calculate CRC32 of localKey as wake-up payload\n\tcrc := crc32.ChecksumIEEE([]byte(localKey))\n\n\t// Convert to hex string\n\thexStr := fmt.Sprintf(\"%08x\", crc)\n\n\t// Convert hex string to byte array (2 chars at a time)\n\tpayload := make([]byte, len(hexStr)/2)\n\tfor i := 0; i < len(hexStr); i += 2 {\n\t\tb, err := hex.DecodeString(hexStr[i : i+2])\n\t\tif err != nil {\n\t\t\treturn fmt.Errorf(\"failed to decode hex: %w\", err)\n\t\t}\n\t\tpayload[i/2] = b[0]\n\t}\n\n\t// Publish to wake-up topic: m/w/{deviceId}\n\twakeUpTopic := fmt.Sprintf(\"m/w/%s\", c.deviceId)\n\ttoken := c.client.Publish(wakeUpTopic, 1, false, payload)\n\tif token.Wait() && token.Error() != nil {\n\t\treturn fmt.Errorf(\"failed to publish wake-up message: %w\", token.Error())\n\t}\n\n\t// Subscribe to lowPower topic to receive dps[149] status updates\n\t// (we don't wait for this signal - camera responds immediately)\n\tlowPowerTopic := fmt.Sprintf(\"smart/decrypt/in/%s\", c.deviceId)\n\tif token := c.client.Subscribe(lowPowerTopic, 1, c.onLowPowerMessage); token.Wait() && token.Error() != nil {\n\t\treturn fmt.Errorf(\"failed to subscribe to lowPower topic: %w\", token.Error())\n\t}\n\n\treturn nil\n}\n\nfunc (c *TuyaMqttClient) SendOffer(sdp string, streamResolution string, streamType int, isHEVC bool) error {\n\t// Map Skill StreamType to MQTT stream_type values\n\t// streamType comes from GetStreamType() and uses Skill StreamType values:\n\t// - mainStream = 2 (HD)\n\t// - substream = 4 (SD)\n\t//\n\t// But MQTT expects mapped stream_type values:\n\t// - mainStream (2) → stream_type: 0\n\t// - substream (4) → stream_type: 1\n\n\tmqttStreamType := streamType\n\tswitch streamType {\n\tcase 2:\n\t\tmqttStreamType = 0 // mainStream (HD)\n\tcase 4:\n\t\tmqttStreamType = 1 // substream (SD)\n\t}\n\n\treturn c.sendMqttMessage(\"offer\", 302, \"\", OfferFrame{\n\t\tMode:              \"webrtc\",\n\t\tSdp:               sdp,\n\t\tStreamType:        mqttStreamType,\n\t\tAuth:              c.auth,\n\t\tDatachannelEnable: isHEVC, // must be true for HEVC\n\t\tToken:             c.iceServers,\n\t})\n}\n\nfunc (c *TuyaMqttClient) SendCandidate(candidate string) error {\n\treturn c.sendMqttMessage(\"candidate\", 302, \"\", CandidateFrame{\n\t\tMode:      \"webrtc\",\n\t\tCandidate: candidate,\n\t})\n}\n\nfunc (c *TuyaMqttClient) SendResolution(resolution int) error {\n\t// Check if camera supports clarity switching\n\tisClaritySupported := (c.webrtcVersion & (1 << 5)) != 0\n\tif !isClaritySupported {\n\t\treturn nil\n\t}\n\n\treturn c.sendMqttMessage(\"resolution\", 312, \"\", ResolutionFrame{\n\t\tMode:  \"webrtc\",\n\t\tValue: resolution, // 0: HD, 1: SD\n\t})\n}\n\nfunc (c *TuyaMqttClient) SendSpeaker(speaker int) error {\n\tif err := c.sendMqttMessage(\"speaker\", 312, \"\", SpeakerFrame{\n\t\tMode:  \"webrtc\",\n\t\tValue: speaker, // 0: off, 1: on\n\t}); err != nil {\n\t\treturn err\n\t}\n\n\t// Wait for camera response\n\tif err := c.speakerWaiter.Wait(); err != nil {\n\t\treturn fmt.Errorf(\"speaker wait failed: %w\", err)\n\t}\n\n\treturn nil\n}\n\nfunc (c *TuyaMqttClient) SendDisconnect() error {\n\treturn c.sendMqttMessage(\"disconnect\", 302, \"\", DisconnectFrame{\n\t\tMode: \"webrtc\",\n\t})\n}\n\nfunc (c *TuyaMqttClient) onConnect(client mqtt.Client) {\n\tif token := client.Subscribe(c.subscribeTopic, 1, c.onMessage); token.Wait() && token.Error() != nil {\n\t\tc.waiter.Done(token.Error())\n\t\treturn\n\t}\n\n\tc.waiter.Done(nil)\n}\n\nfunc (c *TuyaMqttClient) onMessage(client mqtt.Client, msg mqtt.Message) {\n\tvar rmqtt MqttMessage\n\tif err := json.Unmarshal(msg.Payload(), &rmqtt); err != nil {\n\t\tc.onError(err)\n\t\treturn\n\t}\n\n\t// Filter by session ID to prevent processing messages from other sessions\n\tif rmqtt.Data.Header.SessionID != c.sessionId {\n\t\treturn\n\t}\n\n\tswitch rmqtt.Data.Header.Type {\n\tcase \"answer\":\n\t\tc.onMqttAnswer(&rmqtt)\n\tcase \"candidate\":\n\t\tc.onMqttCandidate(&rmqtt)\n\tcase \"disconnect\":\n\t\tc.onMqttDisconnect()\n\tcase \"speaker\":\n\t\tc.onMqttSpeaker(&rmqtt)\n\t}\n}\n\nfunc (c *TuyaMqttClient) onLowPowerMessage(client mqtt.Client, msg mqtt.Message) {\n\tvar message MqttLowPowerMessage\n\tif err := json.Unmarshal(msg.Payload(), &message); err != nil {\n\t\treturn\n\t}\n\n\t// Check if protocol is 4 and dps[149] is true\n\t// https://developer.tuya.com/en/docs/iot-device-dev/doorbell_solution?id=Kayamyivh15ox#title-2-Battery\n\tif message.Protocol == 4 {\n\t\tif val, ok := message.Data.Dps[\"149\"]; ok {\n\t\t\tif ready, ok := val.(bool); ok && ready {\n\t\t\t\t// Camera is now ready after wake-up (dps[149]:true received).\n\t\t\t\t// However, we don't wait for this signal (like ismartlife.me doesn't either).\n\t\t\t\t// The camera starts responding immediately after WakeUp() is called,\n\t\t\t\t// so we proceed with the connection without blocking.\n\t\t\t\t// This waiter is kept for potential future use.\n\t\t\t\tc.wakeupWaiter.Done(nil)\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (c *TuyaMqttClient) onMqttAnswer(msg *MqttMessage) {\n\tvar answerFrame AnswerFrame\n\tif err := json.Unmarshal(msg.Data.Message, &answerFrame); err != nil {\n\t\tc.onError(err)\n\t\treturn\n\t}\n\n\tc.onAnswer(answerFrame)\n}\n\nfunc (c *TuyaMqttClient) onMqttCandidate(msg *MqttMessage) {\n\tvar candidateFrame CandidateFrame\n\tif err := json.Unmarshal(msg.Data.Message, &candidateFrame); err != nil {\n\t\tc.onError(err)\n\t\treturn\n\t}\n\n\t// fix candidates\n\tcandidateFrame.Candidate = strings.TrimPrefix(candidateFrame.Candidate, \"a=\")\n\tcandidateFrame.Candidate = strings.TrimSuffix(candidateFrame.Candidate, \"\\r\\n\")\n\n\tc.onCandidate(candidateFrame)\n}\n\nfunc (c *TuyaMqttClient) onMqttDisconnect() {\n\tc.closed = true\n\tc.onDisconnect()\n}\n\nfunc (c *TuyaMqttClient) onMqttSpeaker(msg *MqttMessage) {\n\tvar speakerResponse struct {\n\t\tResCode int `json:\"resCode\"`\n\t}\n\n\tif err := json.Unmarshal(msg.Data.Message, &speakerResponse); err == nil {\n\t\tif speakerResponse.ResCode != 0 {\n\t\t\tc.speakerWaiter.Done(fmt.Errorf(\"speaker failed with resCode: %d\", speakerResponse.ResCode))\n\t\t\treturn\n\t\t}\n\t}\n\n\tc.speakerWaiter.Done(nil)\n}\n\nfunc (c *TuyaMqttClient) onAnswer(answer AnswerFrame) {\n\tif c.handleAnswer != nil {\n\t\tc.handleAnswer(answer)\n\t}\n}\n\nfunc (c *TuyaMqttClient) onCandidate(candidate CandidateFrame) {\n\tif c.handleCandidate != nil {\n\t\tc.handleCandidate(candidate)\n\t}\n}\n\nfunc (c *TuyaMqttClient) onDisconnect() {\n\tif c.handleDisconnect != nil {\n\t\tc.handleDisconnect()\n\t}\n}\n\nfunc (c *TuyaMqttClient) onError(err error) {\n\tif c.handleError != nil {\n\t\tc.handleError(err)\n\t}\n}\n\nfunc (c *TuyaMqttClient) sendMqttMessage(messageType string, protocol int, transactionID string, data interface{}) error {\n\tif c.closed {\n\t\treturn fmt.Errorf(\"mqtt client is closed, send mqtt message fail\")\n\t}\n\n\tjsonMessage, err := json.Marshal(data)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tmsg := &MqttMessage{\n\t\tProtocol: protocol,\n\t\tPv:       \"2.2\",\n\t\tT:        time.Now().Unix(),\n\t\tData: MqttFrame{\n\t\t\tHeader: MqttFrameHeader{\n\t\t\t\tType:          messageType,\n\t\t\t\tFrom:          c.uid,\n\t\t\t\tTo:            c.deviceId,\n\t\t\t\tSessionID:     c.sessionId,\n\t\t\t\tMotoID:        c.motoId,\n\t\t\t\tTransactionID: transactionID,\n\t\t\t},\n\t\t\tMessage: jsonMessage,\n\t\t},\n\t}\n\n\tpayload, err := json.Marshal(msg)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ttoken := c.client.Publish(c.publishTopic, 1, false, payload)\n\tif token.Wait() && token.Error() != nil {\n\t\treturn token.Error()\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/tuya/smart_api.go",
    "content": "package tuya\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"math/rand\"\n\t\"net/http\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/webrtc\"\n)\n\ntype LoginTokenRequest struct {\n\tCountryCode string `json:\"countryCode\"`\n\tUsername    string `json:\"username\"`\n\tIsUid       bool   `json:\"isUid\"`\n}\n\ntype LoginTokenResponse struct {\n\tResult  LoginToken `json:\"result\"`\n\tSuccess bool       `json:\"success\"`\n\tMsg     string     `json:\"errorMsg,omitempty\"`\n}\n\ntype LoginToken struct {\n\tToken     string `json:\"token\"`\n\tExponent  string `json:\"exponent\"`\n\tPublicKey string `json:\"publicKey\"`\n\tPbKey     string `json:\"pbKey\"`\n}\n\ntype PasswordLoginRequest struct {\n\tCountryCode string `json:\"countryCode\"`\n\tEmail       string `json:\"email,omitempty\"`\n\tMobile      string `json:\"mobile,omitempty\"`\n\tPasswd      string `json:\"passwd\"`\n\tToken       string `json:\"token\"`\n\tIfEncrypt   int    `json:\"ifencrypt\"`\n\tOptions     string `json:\"options\"`\n}\n\ntype PasswordLoginResponse struct {\n\tResult   LoginResult `json:\"result\"`\n\tSuccess  bool        `json:\"success\"`\n\tStatus   string      `json:\"status\"`\n\tErrorMsg string      `json:\"errorMsg,omitempty\"`\n}\n\ntype LoginResult struct {\n\tAttribute          int    `json:\"attribute\"`\n\tClientId           string `json:\"clientId\"`\n\tDataVersion        int    `json:\"dataVersion\"`\n\tDomain             Domain `json:\"domain\"`\n\tEcode              string `json:\"ecode\"`\n\tEmail              string `json:\"email\"`\n\tExtras             Extras `json:\"extras\"`\n\tHeadPic            string `json:\"headPic\"`\n\tImproveCompanyInfo bool   `json:\"improveCompanyInfo\"`\n\tNickname           string `json:\"nickname\"`\n\tPartnerIdentity    string `json:\"partnerIdentity\"`\n\tPhoneCode          string `json:\"phoneCode\"`\n\tReceiver           string `json:\"receiver\"`\n\tRegFrom            int    `json:\"regFrom\"`\n\tSid                string `json:\"sid\"`\n\tSnsNickname        string `json:\"snsNickname\"`\n\tTempUnit           int    `json:\"tempUnit\"`\n\tTimezone           string `json:\"timezone\"`\n\tTimezoneId         string `json:\"timezoneId\"`\n\tUid                string `json:\"uid\"`\n\tUserType           int    `json:\"userType\"`\n\tUsername           string `json:\"username\"`\n}\n\ntype Domain struct {\n\tAispeechHttpsUrl    string `json:\"aispeechHttpsUrl\"`\n\tAispeechQuicUrl     string `json:\"aispeechQuicUrl\"`\n\tDeviceHttpUrl       string `json:\"deviceHttpUrl\"`\n\tDeviceHttpsPskUrl   string `json:\"deviceHttpsPskUrl\"`\n\tDeviceHttpsUrl      string `json:\"deviceHttpsUrl\"`\n\tDeviceMediaMqttUrl  string `json:\"deviceMediaMqttUrl\"`\n\tDeviceMediaMqttsUrl string `json:\"deviceMediaMqttsUrl\"`\n\tDeviceMqttsPskUrl   string `json:\"deviceMqttsPskUrl\"`\n\tDeviceMqttsUrl      string `json:\"deviceMqttsUrl\"`\n\tGwApiUrl            string `json:\"gwApiUrl\"`\n\tGwMqttUrl           string `json:\"gwMqttUrl\"`\n\tHttpPort            int    `json:\"httpPort\"`\n\tHttpsPort           int    `json:\"httpsPort\"`\n\tHttpsPskPort        int    `json:\"httpsPskPort\"`\n\tMobileApiUrl        string `json:\"mobileApiUrl\"`\n\tMobileMediaMqttUrl  string `json:\"mobileMediaMqttUrl\"`\n\tMobileMqttUrl       string `json:\"mobileMqttUrl\"`\n\tMobileMqttsUrl      string `json:\"mobileMqttsUrl\"`\n\tMobileQuicUrl       string `json:\"mobileQuicUrl\"`\n\tMqttPort            int    `json:\"mqttPort\"`\n\tMqttQuicUrl         string `json:\"mqttQuicUrl\"`\n\tMqttsPort           int    `json:\"mqttsPort\"`\n\tMqttsPskPort        int    `json:\"mqttsPskPort\"`\n\tRegionCode          string `json:\"regionCode\"`\n}\n\ntype Extras struct {\n\tHomeId    string `json:\"homeId\"`\n\tSceneType string `json:\"sceneType\"`\n}\n\ntype AppInfoResponse struct {\n\tResult  AppInfo `json:\"result\"`\n\tT       int64   `json:\"t\"`\n\tSuccess bool    `json:\"success\"`\n\tMsg     string  `json:\"errorMsg,omitempty\"`\n}\n\ntype AppInfo struct {\n\tAppId    int    `json:\"appId\"`\n\tAppName  string `json:\"appName\"`\n\tClientId string `json:\"clientId\"`\n\tIcon     string `json:\"icon\"`\n}\n\ntype MQTTConfigResponse struct {\n\tResult  SmartApiMQTTConfig `json:\"result\"`\n\tSuccess bool               `json:\"success\"`\n\tMsg     string             `json:\"errorMsg,omitempty\"`\n}\n\ntype SmartApiMQTTConfig struct {\n\tMsid     string `json:\"msid\"`\n\tPassword string `json:\"password\"`\n}\n\ntype HomeListResponse struct {\n\tResult  []Home `json:\"result\"`\n\tT       int64  `json:\"t\"`\n\tSuccess bool   `json:\"success\"`\n\tMsg     string `json:\"errorMsg,omitempty\"`\n}\n\ntype SharedHomeListResponse struct {\n\tResult  SharedHome `json:\"result\"`\n\tT       int64      `json:\"t\"`\n\tSuccess bool       `json:\"success\"`\n\tMsg     string     `json:\"errorMsg,omitempty\"`\n}\n\ntype SharedHome struct {\n\tSecurityWebCShareInfoList []struct {\n\t\tDeviceInfoList []Device `json:\"deviceInfoList\"`\n\t\tNickname       string   `json:\"nickname\"`\n\t\tUsername       string   `json:\"username\"`\n\t} `json:\"securityWebCShareInfoList\"`\n}\n\ntype Home struct {\n\tAdmin            bool    `json:\"admin\"`\n\tBackground       string  `json:\"background\"`\n\tDealStatus       int     `json:\"dealStatus\"`\n\tDisplayOrder     int     `json:\"displayOrder\"`\n\tGeoName          string  `json:\"geoName\"`\n\tGid              int     `json:\"gid\"`\n\tGmtCreate        int64   `json:\"gmtCreate\"`\n\tGmtModified      int64   `json:\"gmtModified\"`\n\tGroupId          int     `json:\"groupId\"`\n\tGroupUserId      int     `json:\"groupUserId\"`\n\tId               int     `json:\"id\"`\n\tLat              float64 `json:\"lat\"`\n\tLon              float64 `json:\"lon\"`\n\tManagementStatus bool    `json:\"managementStatus\"`\n\tName             string  `json:\"name\"`\n\tOwnerId          string  `json:\"ownerId\"`\n\tRole             int     `json:\"role\"`\n\tStatus           bool    `json:\"status\"`\n\tUid              string  `json:\"uid\"`\n}\n\ntype RoomListRequest struct {\n\tHomeId string `json:\"homeId\"`\n}\n\ntype RoomListResponse struct {\n\tResult  []Room `json:\"result\"`\n\tT       int64  `json:\"t\"`\n\tSuccess bool   `json:\"success\"`\n\tMsg     string `json:\"errorMsg,omitempty\"`\n}\n\ntype Room struct {\n\tDeviceCount int      `json:\"deviceCount\"`\n\tDeviceList  []Device `json:\"deviceList\"`\n\tRoomId      string   `json:\"roomId\"`\n\tRoomName    string   `json:\"roomName\"`\n}\n\ntype Device struct {\n\tCategory            string `json:\"category\"`\n\tDeviceId            string `json:\"deviceId\"`\n\tDeviceName          string `json:\"deviceName\"`\n\tP2pType             int    `json:\"p2pType\"`\n\tProductId           string `json:\"productId\"`\n\tSupportCloudStorage bool   `json:\"supportCloudStorage\"`\n\tUuid                string `json:\"uuid\"`\n}\n\ntype SmartApiWebRTCConfigRequest struct {\n\tDevId         string `json:\"devId\"`\n\tClientTraceId string `json:\"clientTraceId\"`\n}\n\ntype SmartApiWebRTCConfigResponse struct {\n\tResult  SmartApiWebRTCConfig `json:\"result\"`\n\tSuccess bool                 `json:\"success\"`\n\tMsg     string               `json:\"errorMsg,omitempty\"`\n}\n\ntype SmartApiWebRTCConfig struct {\n\tAudioAttributes     AudioAttributes `json:\"audioAttributes\"`\n\tAuth                string          `json:\"auth\"`\n\tGatewayId           string          `json:\"gatewayId\"`\n\tId                  string          `json:\"id\"`\n\tLocalKey            string          `json:\"localKey\"`\n\tMotoId              string          `json:\"motoId\"`\n\tNodeId              string          `json:\"nodeId\"`\n\tP2PConfig           P2PConfig       `json:\"p2pConfig\"`\n\tProtocolVersion     string          `json:\"protocolVersion\"`\n\tSkill               string          `json:\"skill\"`\n\tSub                 bool            `json:\"sub\"`\n\tSupportWebrtcRecord bool            `json:\"supportWebrtcRecord\"`\n\tSupportsPtz         bool            `json:\"supportsPtz\"`\n\tSupportsWebrtc      bool            `json:\"supportsWebrtc\"`\n\tVedioClarity        int             `json:\"vedioClarity\"`\n\tVedioClaritys       []int           `json:\"vedioClaritys\"`\n\tVideoClarity        int             `json:\"videoClarity\"`\n}\n\ntype TuyaSmartApiClient struct {\n\tTuyaClient\n\n\temail       string\n\tpassword    string\n\tcountryCode string\n\tmqttsUrl    string\n}\n\ntype Region struct {\n\tName        string `json:\"name\"`\n\tHost        string `json:\"host\"`\n\tDescription string `json:\"description\"`\n\tContinent   string `json:\"continent\"`\n}\n\nvar AvailableRegions = []Region{\n\t{\"eu-central\", \"protect-eu.ismartlife.me\", \"Central Europe\", \"EU\"},\n\t{\"eu-east\", \"protect-we.ismartlife.me\", \"East Europe\", \"EU\"},\n\t{\"us-west\", \"protect-us.ismartlife.me\", \"West America\", \"AZ\"},\n\t{\"us-east\", \"protect-ue.ismartlife.me\", \"East America\", \"AZ\"},\n\t{\"china\", \"protect.ismartlife.me\", \"China\", \"AY\"},\n\t{\"india\", \"protect-in.ismartlife.me\", \"India\", \"IN\"},\n}\n\nfunc NewTuyaSmartApiClient(httpClient *http.Client, baseUrl, email, password, deviceId string) (*TuyaSmartApiClient, error) {\n\tvar region *Region\n\tfor _, r := range AvailableRegions {\n\t\tif r.Host == baseUrl {\n\t\t\tregion = &r\n\t\t\tbreak\n\t\t}\n\t}\n\n\tif region == nil {\n\t\treturn nil, fmt.Errorf(\"invalid region: %s\", baseUrl)\n\t}\n\n\tif httpClient == nil {\n\t\thttpClient = CreateHTTPClientWithSession()\n\t}\n\n\tmqttClient := NewTuyaMqttClient(deviceId)\n\n\tclient := &TuyaSmartApiClient{\n\t\tTuyaClient: TuyaClient{\n\t\t\thttpClient: httpClient,\n\t\t\tmqtt:       mqttClient,\n\t\t\tdeviceId:   deviceId,\n\t\t\texpireTime: 0,\n\t\t\tbaseUrl:    baseUrl,\n\t\t},\n\t\temail:       email,\n\t\tpassword:    password,\n\t\tcountryCode: region.Continent,\n\t}\n\n\treturn client, nil\n}\n\n// WebRTC Flow\nfunc (c *TuyaSmartApiClient) Init() error {\n\tif err := c.initToken(); err != nil {\n\t\treturn fmt.Errorf(\"failed to initialize token: %w\", err)\n\t}\n\n\twebrtcConfig, err := c.loadWebrtcConfig()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to load webrtc config: %w\", err)\n\t}\n\n\thubConfig, err := c.loadHubConfig()\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to load hub config: %w\", err)\n\t}\n\n\tif err := c.mqtt.Start(hubConfig, webrtcConfig, c.skill.WebRTC); err != nil {\n\t\treturn fmt.Errorf(\"failed to start MQTT: %w\", err)\n\t}\n\n\tif c.skill.LowPower > 0 {\n\t\t_ = c.mqtt.WakeUp(c.localKey)\n\t}\n\n\treturn nil\n}\n\nfunc (c *TuyaSmartApiClient) GetStreamUrl(streamType string) (streamUrl string, err error) {\n\treturn \"\", errors.New(\"not supported\")\n}\n\nfunc (c *TuyaSmartApiClient) GetAppInfo() (*AppInfoResponse, error) {\n\turl := fmt.Sprintf(\"https://%s/api/customized/web/app/info\", c.baseUrl)\n\n\tbody, err := c.request(\"POST\", url, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar appInfoResponse AppInfoResponse\n\tif err := json.Unmarshal(body, &appInfoResponse); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif !appInfoResponse.Success {\n\t\treturn nil, errors.New(appInfoResponse.Msg)\n\t}\n\n\treturn &appInfoResponse, nil\n}\n\nfunc (c *TuyaSmartApiClient) GetHomeList() (*HomeListResponse, error) {\n\turl := fmt.Sprintf(\"https://%s/api/new/common/homeList\", c.baseUrl)\n\n\tbody, err := c.request(\"POST\", url, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar homeListResponse HomeListResponse\n\tif err := json.Unmarshal(body, &homeListResponse); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif !homeListResponse.Success {\n\t\treturn nil, errors.New(homeListResponse.Msg)\n\t}\n\n\treturn &homeListResponse, nil\n}\n\nfunc (c *TuyaSmartApiClient) GetSharedHomeList() (*SharedHomeListResponse, error) {\n\turl := fmt.Sprintf(\"https://%s/api/new/playback/shareList\", c.baseUrl)\n\n\tbody, err := c.request(\"POST\", url, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar sharedHomeListResponse SharedHomeListResponse\n\tif err := json.Unmarshal(body, &sharedHomeListResponse); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif !sharedHomeListResponse.Success {\n\t\treturn nil, errors.New(sharedHomeListResponse.Msg)\n\t}\n\n\treturn &sharedHomeListResponse, nil\n}\n\nfunc (c *TuyaSmartApiClient) GetRoomList(homeId string) (*RoomListResponse, error) {\n\turl := fmt.Sprintf(\"https://%s/api/new/common/roomList\", c.baseUrl)\n\n\tdata := RoomListRequest{\n\t\tHomeId: homeId,\n\t}\n\n\tbody, err := c.request(\"POST\", url, data)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar roomListResponse RoomListResponse\n\tif err := json.Unmarshal(body, &roomListResponse); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif !roomListResponse.Success {\n\t\treturn nil, errors.New(roomListResponse.Msg)\n\t}\n\n\treturn &roomListResponse, nil\n}\n\nfunc (c *TuyaSmartApiClient) initToken() error {\n\ttokenUrl := fmt.Sprintf(\"https://%s/api/login/token\", c.baseUrl)\n\n\ttokenReq := LoginTokenRequest{\n\t\tCountryCode: c.countryCode,\n\t\tUsername:    c.email,\n\t\tIsUid:       false,\n\t}\n\n\tbody, err := c.request(\"POST\", tokenUrl, tokenReq)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar tokenResp LoginTokenResponse\n\tif err := json.Unmarshal(body, &tokenResp); err != nil {\n\t\treturn err\n\t}\n\n\tif !tokenResp.Success {\n\t\treturn errors.New(tokenResp.Msg)\n\t}\n\n\tencryptedPassword, err := EncryptPassword(c.password, tokenResp.Result.PbKey)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"failed to encrypt password: %v\", err)\n\t}\n\tvar loginUrl string\n\n\tloginReq := PasswordLoginRequest{\n\t\tCountryCode: c.countryCode,\n\t\tPasswd:      encryptedPassword,\n\t\tToken:       tokenResp.Result.Token,\n\t\tIfEncrypt:   1,\n\t\tOptions:     `{\"group\":1}`,\n\t}\n\n\tif IsEmailAddress(c.email) {\n\t\tloginUrl = fmt.Sprintf(\"https://%s/api/private/email/login\", c.baseUrl)\n\t\tloginReq.Email = c.email\n\t} else {\n\t\tloginUrl = fmt.Sprintf(\"https://%s/api/private/phone/login\", c.baseUrl)\n\t\tloginReq.Mobile = c.email\n\t}\n\n\tbody, err = c.request(\"POST\", loginUrl, loginReq)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar loginResp *PasswordLoginResponse\n\tif err := json.Unmarshal(body, &loginResp); err != nil {\n\t\treturn err\n\t}\n\n\tif !loginResp.Success {\n\t\treturn errors.New(loginResp.ErrorMsg)\n\t}\n\n\tc.mqttsUrl = fmt.Sprintf(\"ssl://%s:%d\", loginResp.Result.Domain.MobileMqttsUrl, loginResp.Result.Domain.MqttsPort)\n\tc.expireTime = time.Now().Unix() + 2*24*60*60 // 2 days in seconds\n\n\treturn nil\n}\n\nfunc (c *TuyaSmartApiClient) loadWebrtcConfig() (*WebRTCConfig, error) {\n\turl := fmt.Sprintf(\"https://%s/api/jarvis/config\", c.baseUrl)\n\n\tdata := SmartApiWebRTCConfigRequest{\n\t\tDevId:         c.deviceId,\n\t\tClientTraceId: fmt.Sprintf(\"%x\", rand.Int63()),\n\t}\n\n\tbody, err := c.request(\"POST\", url, data)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar webRTCConfigResponse SmartApiWebRTCConfigResponse\n\terr = json.Unmarshal(body, &webRTCConfigResponse)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif !webRTCConfigResponse.Success {\n\t\treturn nil, errors.New(webRTCConfigResponse.Msg)\n\t}\n\n\terr = json.Unmarshal([]byte(webRTCConfigResponse.Result.Skill), &c.skill)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// Store LocalKey\n\tc.localKey = webRTCConfigResponse.Result.LocalKey\n\n\ticeServers, err := json.Marshal(&webRTCConfigResponse.Result.P2PConfig.Ices)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tc.iceServers, err = webrtc.UnmarshalICEServers(iceServers)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &WebRTCConfig{\n\t\tAudioAttributes:      webRTCConfigResponse.Result.AudioAttributes,\n\t\tAuth:                 webRTCConfigResponse.Result.Auth,\n\t\tID:                   webRTCConfigResponse.Result.Id,\n\t\tMotoID:               webRTCConfigResponse.Result.MotoId,\n\t\tP2PConfig:            webRTCConfigResponse.Result.P2PConfig,\n\t\tProtocolVersion:      webRTCConfigResponse.Result.ProtocolVersion,\n\t\tSkill:                webRTCConfigResponse.Result.Skill,\n\t\tSupportsWebRTCRecord: webRTCConfigResponse.Result.SupportWebrtcRecord,\n\t\tSupportsWebRTC:       webRTCConfigResponse.Result.SupportsWebrtc,\n\t\tVedioClaritiy:        webRTCConfigResponse.Result.VedioClarity,\n\t\tVideoClaritiy:        webRTCConfigResponse.Result.VideoClarity,\n\t\tVideoClarities:       webRTCConfigResponse.Result.VedioClaritys,\n\t}, nil\n}\n\nfunc (c *TuyaSmartApiClient) loadHubConfig() (config *MQTTConfig, err error) {\n\tmqttUrl := fmt.Sprintf(\"https://%s/api/jarvis/mqtt\", c.baseUrl)\n\n\tmqttBody, err := c.request(\"POST\", mqttUrl, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar mqttConfigResponse MQTTConfigResponse\n\terr = json.Unmarshal(mqttBody, &mqttConfigResponse)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif !mqttConfigResponse.Success {\n\t\treturn nil, errors.New(mqttConfigResponse.Msg)\n\t}\n\n\treturn &MQTTConfig{\n\t\tUrl:            c.mqttsUrl,\n\t\tClientID:       fmt.Sprintf(\"web_%s\", mqttConfigResponse.Result.Msid),\n\t\tUsername:       fmt.Sprintf(\"web_%s\", mqttConfigResponse.Result.Msid),\n\t\tPassword:       mqttConfigResponse.Result.Password,\n\t\tPublishTopic:   \"/av/moto/moto_id/u/{device_id}\",\n\t\tSubscribeTopic: fmt.Sprintf(\"/av/u/%s\", mqttConfigResponse.Result.Msid),\n\t}, nil\n}\n\nfunc (c *TuyaSmartApiClient) request(method string, url string, body any) ([]byte, error) {\n\tvar bodyReader io.Reader\n\tif body != nil {\n\t\tjsonBody, err := json.Marshal(body)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tbodyReader = bytes.NewReader(jsonBody)\n\t}\n\n\treq, err := http.NewRequest(method, url, bodyReader)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json; charset=utf-8\")\n\treq.Header.Set(\"Accept\", \"*/*\")\n\treq.Header.Set(\"Origin\", fmt.Sprintf(\"https://%s\", c.baseUrl))\n\n\tresponse, err := c.httpClient.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer response.Body.Close()\n\n\tres, err := io.ReadAll(response.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif response.StatusCode != http.StatusOK {\n\t\treturn nil, err\n\t}\n\n\treturn res, nil\n}\n"
  },
  {
    "path": "pkg/v4l2/device/README.md",
    "content": "# Video For Linux Two\n\nBuild on Ubuntu\n\n```bash\nsudo apt install gcc-x86-64-linux-gnu\nsudo apt install gcc-i686-linux-gnu\nsudo apt install gcc-aarch64-linux-gnu binutils\nsudo apt install gcc-arm-linux-gnueabihf\nsudo apt install gcc-mipsel-linux-gnu\n\nx86_64-linux-gnu-gcc -w -static videodev2_arch.c -o videodev2_x86_64\ni686-linux-gnu-gcc -w -static videodev2_arch.c -o videodev2_i686\naarch64-linux-gnu-gcc -w -static videodev2_arch.c -o videodev2_aarch64\narm-linux-gnueabihf-gcc -w -static videodev2_arch.c -o videodev2_armhf\nmipsel-linux-gnu-gcc -w -static videodev2_arch.c -o videodev2_mipsel -D_TIME_BITS=32\n```\n\n## Useful links\n\n- https://github.com/torvalds/linux/blob/master/include/uapi/linux/videodev2.h\n"
  },
  {
    "path": "pkg/v4l2/device/device.go",
    "content": "//go:build linux\n\npackage device\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\t\"fmt\"\n\t\"syscall\"\n\t\"unsafe\"\n)\n\ntype Device struct {\n\tfd     int\n\tbufs   [][]byte\n\tpixFmt uint32\n}\n\nfunc Open(path string) (*Device, error) {\n\tfd, err := syscall.Open(path, syscall.O_RDWR|syscall.O_CLOEXEC, 0)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &Device{fd: fd}, nil\n}\n\nconst buffersCount = 2\n\ntype Capability struct {\n\tDriver  string\n\tCard    string\n\tBusInfo string\n\tVersion string\n}\n\nfunc (d *Device) Capability() (*Capability, error) {\n\tc := v4l2_capability{}\n\tif err := ioctl(d.fd, VIDIOC_QUERYCAP, unsafe.Pointer(&c)); err != nil {\n\t\treturn nil, err\n\t}\n\treturn &Capability{\n\t\tDriver:  str(c.driver[:]),\n\t\tCard:    str(c.card[:]),\n\t\tBusInfo: str(c.bus_info[:]),\n\t\tVersion: fmt.Sprintf(\"%d.%d.%d\", byte(c.version>>16), byte(c.version>>8), byte(c.version)),\n\t}, nil\n}\n\nfunc (d *Device) ListFormats() ([]uint32, error) {\n\tvar items []uint32\n\n\tfor i := uint32(0); ; i++ {\n\t\tfd := v4l2_fmtdesc{\n\t\t\tindex: i,\n\t\t\ttyp:   V4L2_BUF_TYPE_VIDEO_CAPTURE,\n\t\t}\n\t\tif err := ioctl(d.fd, VIDIOC_ENUM_FMT, unsafe.Pointer(&fd)); err != nil {\n\t\t\tif !errors.Is(err, syscall.EINVAL) {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\n\t\titems = append(items, fd.pixelformat)\n\t}\n\n\treturn items, nil\n}\n\nfunc (d *Device) ListSizes(pixFmt uint32) ([][2]uint32, error) {\n\tvar items [][2]uint32\n\n\tfor i := uint32(0); ; i++ {\n\t\tfs := v4l2_frmsizeenum{\n\t\t\tindex:        i,\n\t\t\tpixel_format: pixFmt,\n\t\t}\n\t\tif err := ioctl(d.fd, VIDIOC_ENUM_FRAMESIZES, unsafe.Pointer(&fs)); err != nil {\n\t\t\tif !errors.Is(err, syscall.EINVAL) {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\n\t\tif fs.typ != V4L2_FRMSIZE_TYPE_DISCRETE {\n\t\t\tcontinue\n\t\t}\n\n\t\titems = append(items, [2]uint32{fs.discrete.width, fs.discrete.height})\n\t}\n\n\treturn items, nil\n}\n\nfunc (d *Device) ListFrameRates(pixFmt, width, height uint32) ([]uint32, error) {\n\tvar items []uint32\n\n\tfor i := uint32(0); ; i++ {\n\t\tfi := v4l2_frmivalenum{\n\t\t\tindex:        i,\n\t\t\tpixel_format: pixFmt,\n\t\t\twidth:        width,\n\t\t\theight:       height,\n\t\t}\n\t\tif err := ioctl(d.fd, VIDIOC_ENUM_FRAMEINTERVALS, unsafe.Pointer(&fi)); err != nil {\n\t\t\tif !errors.Is(err, syscall.EINVAL) {\n\t\t\t\treturn nil, err\n\t\t\t}\n\t\t\tbreak\n\t\t}\n\n\t\tif fi.typ != V4L2_FRMIVAL_TYPE_DISCRETE || fi.discrete.numerator != 1 {\n\t\t\tcontinue\n\t\t}\n\n\t\titems = append(items, fi.discrete.denominator)\n\t}\n\n\treturn items, nil\n}\n\nfunc (d *Device) SetFormat(width, height, pixFmt uint32) error {\n\td.pixFmt = pixFmt\n\n\tf := v4l2_format{\n\t\ttyp: V4L2_BUF_TYPE_VIDEO_CAPTURE,\n\t\tpix: v4l2_pix_format{\n\t\t\twidth:       width,\n\t\t\theight:      height,\n\t\t\tpixelformat: pixFmt,\n\t\t\tfield:       V4L2_FIELD_NONE,\n\t\t\tcolorspace:  V4L2_COLORSPACE_DEFAULT,\n\t\t},\n\t}\n\treturn ioctl(d.fd, VIDIOC_S_FMT, unsafe.Pointer(&f))\n}\n\nfunc (d *Device) SetParam(fps uint32) error {\n\tp := v4l2_streamparm{\n\t\ttyp: V4L2_BUF_TYPE_VIDEO_CAPTURE,\n\t\tcapture: v4l2_captureparm{\n\t\t\ttimeperframe: v4l2_fract{numerator: 1, denominator: fps},\n\t\t},\n\t}\n\treturn ioctl(d.fd, VIDIOC_S_PARM, unsafe.Pointer(&p))\n}\n\nfunc (d *Device) StreamOn() (err error) {\n\trb := v4l2_requestbuffers{\n\t\tcount:  buffersCount,\n\t\ttyp:    V4L2_BUF_TYPE_VIDEO_CAPTURE,\n\t\tmemory: V4L2_MEMORY_MMAP,\n\t}\n\tif err = ioctl(d.fd, VIDIOC_REQBUFS, unsafe.Pointer(&rb)); err != nil {\n\t\treturn err\n\t}\n\n\td.bufs = make([][]byte, buffersCount)\n\tfor i := uint32(0); i < buffersCount; i++ {\n\t\tqb := v4l2_buffer{\n\t\t\tindex:  i,\n\t\t\ttyp:    V4L2_BUF_TYPE_VIDEO_CAPTURE,\n\t\t\tmemory: V4L2_MEMORY_MMAP,\n\t\t}\n\t\tif err = ioctl(d.fd, VIDIOC_QUERYBUF, unsafe.Pointer(&qb)); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif d.bufs[i], err = syscall.Mmap(\n\t\t\td.fd, int64(qb.offset), int(qb.length), syscall.PROT_READ, syscall.MAP_SHARED,\n\t\t); nil != err {\n\t\t\treturn err\n\t\t}\n\n\t\tif err = ioctl(d.fd, VIDIOC_QBUF, unsafe.Pointer(&qb)); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\ttyp := uint32(V4L2_BUF_TYPE_VIDEO_CAPTURE)\n\treturn ioctl(d.fd, VIDIOC_STREAMON, unsafe.Pointer(&typ))\n}\n\nfunc (d *Device) StreamOff() (err error) {\n\ttyp := uint32(V4L2_BUF_TYPE_VIDEO_CAPTURE)\n\tif err = ioctl(d.fd, VIDIOC_STREAMOFF, unsafe.Pointer(&typ)); err != nil {\n\t\treturn err\n\t}\n\n\tfor i := range d.bufs {\n\t\t_ = syscall.Munmap(d.bufs[i])\n\t}\n\n\trb := v4l2_requestbuffers{\n\t\tcount:  0,\n\t\ttyp:    V4L2_BUF_TYPE_VIDEO_CAPTURE,\n\t\tmemory: V4L2_MEMORY_MMAP,\n\t}\n\treturn ioctl(d.fd, VIDIOC_REQBUFS, unsafe.Pointer(&rb))\n}\n\nfunc (d *Device) Capture() ([]byte, error) {\n\tdec := v4l2_buffer{\n\t\ttyp:    V4L2_BUF_TYPE_VIDEO_CAPTURE,\n\t\tmemory: V4L2_MEMORY_MMAP,\n\t}\n\tif err := ioctl(d.fd, VIDIOC_DQBUF, unsafe.Pointer(&dec)); err != nil {\n\t\treturn nil, err\n\t}\n\n\tsrc := d.bufs[dec.index][:dec.bytesused]\n\tdst := make([]byte, dec.bytesused)\n\n\tswitch d.pixFmt {\n\tcase V4L2_PIX_FMT_YUYV:\n\t\tYUYVtoYUV(dst, src)\n\tcase V4L2_PIX_FMT_NV12:\n\t\tNV12toYUV(dst, src)\n\tdefault:\n\t\tcopy(dst, d.bufs[dec.index][:dec.bytesused])\n\t}\n\n\tenc := v4l2_buffer{\n\t\ttyp:    V4L2_BUF_TYPE_VIDEO_CAPTURE,\n\t\tmemory: V4L2_MEMORY_MMAP,\n\t\tindex:  dec.index,\n\t}\n\tif err := ioctl(d.fd, VIDIOC_QBUF, unsafe.Pointer(&enc)); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn dst, nil\n}\n\nfunc (d *Device) Close() error {\n\treturn syscall.Close(d.fd)\n}\n\nfunc ioctl(fd int, req uint, arg unsafe.Pointer) error {\n\t_, _, err := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), uintptr(req), uintptr(arg))\n\tif err != 0 {\n\t\treturn err\n\t}\n\treturn nil\n}\n\nfunc str(b []byte) string {\n\tif i := bytes.IndexByte(b, 0); i >= 0 {\n\t\treturn string(b[:i])\n\t}\n\treturn string(b)\n}\n"
  },
  {
    "path": "pkg/v4l2/device/formats.go",
    "content": "package device\n\nconst (\n\tV4L2_PIX_FMT_YUYV  = 'Y' | 'U'<<8 | 'Y'<<16 | 'V'<<24\n\tV4L2_PIX_FMT_NV12  = 'N' | 'V'<<8 | '1'<<16 | '2'<<24\n\tV4L2_PIX_FMT_MJPEG = 'M' | 'J'<<8 | 'P'<<16 | 'G'<<24\n\tV4L2_PIX_FMT_H264  = 'H' | '2'<<8 | '6'<<16 | '4'<<24\n\tV4L2_PIX_FMT_HEVC  = 'H' | 'E'<<8 | 'V'<<16 | 'C'<<24\n)\n\ntype Format struct {\n\tFourCC uint32\n\tName   string\n\tFFmpeg string\n}\n\nvar Formats = []Format{\n\t{V4L2_PIX_FMT_YUYV, \"YUV 4:2:2\", \"yuyv422\"},\n\t{V4L2_PIX_FMT_NV12, \"Y/UV 4:2:0\", \"nv12\"},\n\t{V4L2_PIX_FMT_MJPEG, \"Motion-JPEG\", \"mjpeg\"},\n\t{V4L2_PIX_FMT_H264, \"H.264\", \"h264\"},\n\t{V4L2_PIX_FMT_HEVC, \"HEVC\", \"hevc\"},\n}\n\nfunc YUYVtoYUV(dst, src []byte) {\n\tn := len(src)\n\ti0 := 0\n\tiy := 0\n\tiu := n / 2\n\tiv := n / 4 * 3\n\tfor i0 < n {\n\t\tdst[iy] = src[i0]\n\t\ti0++\n\t\tiy++\n\t\tdst[iu] = src[i0]\n\t\ti0++\n\t\tiu++\n\t\tdst[iy] = src[i0]\n\t\ti0++\n\t\tiy++\n\t\tdst[iv] = src[i0]\n\t\ti0++\n\t\tiv++\n\t}\n}\n\nfunc NV12toYUV(dst, src []byte) {\n\tn := len(src)\n\tk := n / 6\n\ti0 := k * 4\n\tiu := i0\n\tiv := i0 + k\n\tcopy(dst, src[:i0]) // copy Y\n\tfor i0 < n {\n\t\tdst[iu] = src[i0]\n\t\ti0++\n\t\tiu++\n\t\tdst[iv] = src[i0]\n\t\ti0++\n\t\tiv++\n\t}\n}\n"
  },
  {
    "path": "pkg/v4l2/device/videodev2_386.go",
    "content": "package device\n\nconst (\n\tVIDIOC_QUERYCAP = 0x80685600\n\tVIDIOC_ENUM_FMT = 0xc0405602\n\tVIDIOC_G_FMT    = 0xc0cc5604\n\tVIDIOC_S_FMT    = 0xc0cc5605\n\tVIDIOC_REQBUFS  = 0xc0145608\n\tVIDIOC_QUERYBUF = 0xc0445609\n\n\tVIDIOC_QBUF      = 0xc044560f\n\tVIDIOC_DQBUF     = 0xc0445611\n\tVIDIOC_STREAMON  = 0x40045612\n\tVIDIOC_STREAMOFF = 0x40045613\n\tVIDIOC_G_PARM    = 0xc0cc5615\n\tVIDIOC_S_PARM    = 0xc0cc5616\n\n\tVIDIOC_ENUM_FRAMESIZES     = 0xc02c564a\n\tVIDIOC_ENUM_FRAMEINTERVALS = 0xc034564b\n)\n\nconst (\n\tV4L2_BUF_TYPE_VIDEO_CAPTURE = 1\n\tV4L2_COLORSPACE_DEFAULT     = 0\n\tV4L2_FIELD_NONE             = 1\n\tV4L2_FRMIVAL_TYPE_DISCRETE  = 1\n\tV4L2_FRMSIZE_TYPE_DISCRETE  = 1\n\tV4L2_MEMORY_MMAP            = 1\n)\n\ntype v4l2_capability struct { // size 104\n\tdriver       [16]byte  // offset 0, size 16\n\tcard         [32]byte  // offset 16, size 32\n\tbus_info     [32]byte  // offset 48, size 32\n\tversion      uint32    // offset 80, size 4\n\tcapabilities uint32    // offset 84, size 4\n\tdevice_caps  uint32    // offset 88, size 4\n\treserved     [3]uint32 // offset 92, size 12\n}\n\ntype v4l2_format struct { // size 204\n\ttyp uint32          // offset 0, size 4\n\t_   [0]byte         // align\n\tpix v4l2_pix_format // offset 4, size 48\n\t_   [152]byte       // filler\n}\n\ntype v4l2_pix_format struct { // size 48\n\twidth        uint32 // offset 0, size 4\n\theight       uint32 // offset 4, size 4\n\tpixelformat  uint32 // offset 8, size 4\n\tfield        uint32 // offset 12, size 4\n\tbytesperline uint32 // offset 16, size 4\n\tsizeimage    uint32 // offset 20, size 4\n\tcolorspace   uint32 // offset 24, size 4\n\tpriv         uint32 // offset 28, size 4\n\tflags        uint32 // offset 32, size 4\n\tycbcr_enc    uint32 // offset 36, size 4\n\tquantization uint32 // offset 40, size 4\n\txfer_func    uint32 // offset 44, size 4\n}\n\ntype v4l2_streamparm struct { // size 204\n\ttyp     uint32           // offset 0, size 4\n\tcapture v4l2_captureparm // offset 4, size 40\n\t_       [160]byte        // filler\n}\n\ntype v4l2_captureparm struct { // size 40\n\tcapability   uint32     // offset 0, size 4\n\tcapturemode  uint32     // offset 4, size 4\n\ttimeperframe v4l2_fract // offset 8, size 8\n\textendedmode uint32     // offset 16, size 4\n\treadbuffers  uint32     // offset 20, size 4\n\treserved     [4]uint32  // offset 24, size 16\n}\n\ntype v4l2_fract struct { // size 8\n\tnumerator   uint32 // offset 0, size 4\n\tdenominator uint32 // offset 4, size 4\n}\n\ntype v4l2_requestbuffers struct { // size 20\n\tcount        uint32   // offset 0, size 4\n\ttyp          uint32   // offset 4, size 4\n\tmemory       uint32   // offset 8, size 4\n\tcapabilities uint32   // offset 12, size 4\n\tflags        uint8    // offset 16, size 1\n\treserved     [3]uint8 // offset 17, size 3\n}\n\ntype v4l2_buffer struct { // size 68\n\tindex     uint32        // offset 0, size 4\n\ttyp       uint32        // offset 4, size 4\n\tbytesused uint32        // offset 8, size 4\n\tflags     uint32        // offset 12, size 4\n\tfield     uint32        // offset 16, size 4\n\t_         [8]byte       // align\n\ttimecode  v4l2_timecode // offset 28, size 16\n\tsequence  uint32        // offset 44, size 4\n\tmemory    uint32        // offset 48, size 4\n\toffset    uint32        // offset 52, size 4\n\t_         [0]byte       // align\n\tlength    uint32        // offset 56, size 4\n\t_         [8]byte       // filler\n}\n\ntype v4l2_timecode struct { // size 16\n\ttyp      uint32   // offset 0, size 4\n\tflags    uint32   // offset 4, size 4\n\tframes   uint8    // offset 8, size 1\n\tseconds  uint8    // offset 9, size 1\n\tminutes  uint8    // offset 10, size 1\n\thours    uint8    // offset 11, size 1\n\tuserbits [4]uint8 // offset 12, size 4\n}\n\ntype v4l2_fmtdesc struct { // size 64\n\tindex       uint32    // offset 0, size 4\n\ttyp         uint32    // offset 4, size 4\n\tflags       uint32    // offset 8, size 4\n\tdescription [32]byte  // offset 12, size 32\n\tpixelformat uint32    // offset 44, size 4\n\tmbus_code   uint32    // offset 48, size 4\n\treserved    [3]uint32 // offset 52, size 12\n}\n\ntype v4l2_frmsizeenum struct { // size 44\n\tindex        uint32                // offset 0, size 4\n\tpixel_format uint32                // offset 4, size 4\n\ttyp          uint32                // offset 8, size 4\n\tdiscrete     v4l2_frmsize_discrete // offset 12, size 8\n\t_            [24]byte              // filler\n}\n\ntype v4l2_frmsize_discrete struct { // size 8\n\twidth  uint32 // offset 0, size 4\n\theight uint32 // offset 4, size 4\n}\n\ntype v4l2_frmivalenum struct { // size 52\n\tindex        uint32     // offset 0, size 4\n\tpixel_format uint32     // offset 4, size 4\n\twidth        uint32     // offset 8, size 4\n\theight       uint32     // offset 12, size 4\n\ttyp          uint32     // offset 16, size 4\n\tdiscrete     v4l2_fract // offset 20, size 8\n\t_            [24]byte   // filler\n}\n"
  },
  {
    "path": "pkg/v4l2/device/videodev2_arch.c",
    "content": "//go:build ignore\n#include <stdio.h>\n#include <stddef.h>\n#include <linux/videodev2.h>\n\n#define printconst1(con) printf(\"\\t%s = 0x%08lx\\n\", #con, con)\n#define printconst2(con) printf(\"\\t%s = %d\\n\", #con, con)\n#define printstruct(str) printf(\"type %s struct { // size %lu\\n\", #str, sizeof(struct str))\n#define printmember(str, mem, typ) printf(\"\\t%s %s // offset %lu, size %lu\\n\", #mem == \"type\" ? \"typ\" : #mem, typ, offsetof(struct str, mem), sizeof((struct str){0}.mem))\n#define printunimem(str, uni, mem, typ) printf(\"\\t%s %s // offset %lu, size %lu\\n\", #mem, typ, offsetof(struct str, uni.mem), sizeof((struct str){0}.uni.mem))\n#define printalign1(str, mem2, mem1) printf(\"\\t_ [%lu]byte // align\\n\", offsetof(struct str, mem2) - offsetof(struct str, mem1) - sizeof((struct str){0}.mem1))\n#define printfiller(str, mem) printf(\"\\t_ [%lu]byte // filler\\n\", sizeof(struct str) - offsetof(struct str, mem) - sizeof((struct str){0}.mem))\n\nint main() {\n\tprintf(\"const (\\n\");\n\tprintconst1(VIDIOC_QUERYCAP);\n\tprintconst1(VIDIOC_ENUM_FMT);\n\tprintconst1(VIDIOC_G_FMT);\n\tprintconst1(VIDIOC_S_FMT);\n\tprintconst1(VIDIOC_REQBUFS);\n\tprintconst1(VIDIOC_QUERYBUF);\n\tprintf(\"\\n\");\n\tprintconst1(VIDIOC_QBUF);\n\tprintconst1(VIDIOC_DQBUF);\n\tprintconst1(VIDIOC_STREAMON);\n\tprintconst1(VIDIOC_STREAMOFF);\n\tprintconst1(VIDIOC_G_PARM);\n\tprintconst1(VIDIOC_S_PARM);\n\tprintf(\"\\n\");\n\tprintconst1(VIDIOC_ENUM_FRAMESIZES);\n\tprintconst1(VIDIOC_ENUM_FRAMEINTERVALS);\n\tprintf(\")\\n\\n\");\n\n\tprintf(\"const (\\n\");\n\tprintconst2(V4L2_BUF_TYPE_VIDEO_CAPTURE);\n\tprintconst2(V4L2_COLORSPACE_DEFAULT);\n\tprintconst2(V4L2_FIELD_NONE);\n\tprintconst2(V4L2_FRMIVAL_TYPE_DISCRETE);\n\tprintconst2(V4L2_FRMSIZE_TYPE_DISCRETE);\n\tprintconst2(V4L2_MEMORY_MMAP);\n\tprintf(\")\\n\\n\");\n\n\tprintstruct(v4l2_capability);\n\tprintmember(v4l2_capability, driver, \"[16]byte\");\n\tprintmember(v4l2_capability, card, \"[32]byte\");\n\tprintmember(v4l2_capability, bus_info, \"[32]byte\");\n\tprintmember(v4l2_capability, version, \"uint32\");\n\tprintmember(v4l2_capability, capabilities, \"uint32\");\n\tprintmember(v4l2_capability, device_caps, \"uint32\");\n\tprintmember(v4l2_capability, reserved, \"[3]uint32\");\n\tprintf(\"}\\n\\n\");\n\n\tprintstruct(v4l2_format);\n\tprintmember(v4l2_format, type, \"uint32\");\n\tprintalign1(v4l2_format, fmt, type);\n\tprintunimem(v4l2_format, fmt, pix, \"v4l2_pix_format\");\n\tprintfiller(v4l2_format, fmt.pix);\n\tprintf(\"}\\n\\n\");\n\n\tprintstruct(v4l2_pix_format);\n\tprintmember(v4l2_pix_format, width, \"uint32\");\n\tprintmember(v4l2_pix_format, height, \"uint32\");\n\tprintmember(v4l2_pix_format, pixelformat, \"uint32\");\n\tprintmember(v4l2_pix_format, field, \"uint32\");\n\tprintmember(v4l2_pix_format, bytesperline, \"uint32\");\n\tprintmember(v4l2_pix_format, sizeimage, \"uint32\");\n\tprintmember(v4l2_pix_format, colorspace, \"uint32\");\n\tprintmember(v4l2_pix_format, priv, \"uint32\");\n\tprintmember(v4l2_pix_format, flags, \"uint32\");\n\tprintmember(v4l2_pix_format, ycbcr_enc, \"uint32\");\n\tprintmember(v4l2_pix_format, quantization, \"uint32\");\n\tprintmember(v4l2_pix_format, xfer_func, \"uint32\");\n\tprintf(\"}\\n\\n\");\n\n\tprintstruct(v4l2_streamparm);\n\tprintmember(v4l2_streamparm, type, \"uint32\");\n\tprintunimem(v4l2_streamparm, parm, capture, \"v4l2_captureparm\");\n\tprintfiller(v4l2_streamparm, parm.capture);\n\tprintf(\"}\\n\\n\");\n\n\tprintstruct(v4l2_captureparm);\n\tprintmember(v4l2_captureparm, capability, \"uint32\");\n\tprintmember(v4l2_captureparm, capturemode, \"uint32\");\n\tprintmember(v4l2_captureparm, timeperframe, \"v4l2_fract\");\n\tprintmember(v4l2_captureparm, extendedmode, \"uint32\");\n\tprintmember(v4l2_captureparm, readbuffers, \"uint32\");\n\tprintmember(v4l2_captureparm, reserved, \"[4]uint32\");\n\tprintf(\"}\\n\\n\");\n\n\tprintstruct(v4l2_fract);\n\tprintmember(v4l2_fract, numerator, \"uint32\");\n\tprintmember(v4l2_fract, denominator, \"uint32\");\n\tprintf(\"}\\n\\n\");\n\n\tprintstruct(v4l2_requestbuffers);\n\tprintmember(v4l2_requestbuffers, count, \"uint32\");\n\tprintmember(v4l2_requestbuffers, type, \"uint32\");\n\tprintmember(v4l2_requestbuffers, memory, \"uint32\");\n\tprintmember(v4l2_requestbuffers, capabilities, \"uint32\");\n\tprintmember(v4l2_requestbuffers, flags, \"uint8\");\n\tprintmember(v4l2_requestbuffers, reserved, \"[3]uint8\");\n\tprintf(\"}\\n\\n\");\n\n\tprintstruct(v4l2_buffer);\n\tprintmember(v4l2_buffer, index, \"uint32\");\n\tprintmember(v4l2_buffer, type, \"uint32\");\n\tprintmember(v4l2_buffer, bytesused, \"uint32\");\n\tprintmember(v4l2_buffer, flags, \"uint32\");\n\tprintmember(v4l2_buffer, field, \"uint32\");\n\tprintalign1(v4l2_buffer, timecode, field);\n\tprintmember(v4l2_buffer, timecode, \"v4l2_timecode\");\n\tprintmember(v4l2_buffer, sequence, \"uint32\");\n\tprintmember(v4l2_buffer, memory, \"uint32\");\n\tprintunimem(v4l2_buffer, m, offset, \"uint32\");\n\tprintalign1(v4l2_buffer, length, m.offset);\n\tprintmember(v4l2_buffer, length, \"uint32\");\n\tprintfiller(v4l2_buffer, length);\n\tprintf(\"}\\n\\n\");\n\n\tprintstruct(v4l2_timecode);\n\tprintmember(v4l2_timecode, type, \"uint32\");\n\tprintmember(v4l2_timecode, flags, \"uint32\");\n\tprintmember(v4l2_timecode, frames, \"uint8\");\n\tprintmember(v4l2_timecode, seconds, \"uint8\");\n\tprintmember(v4l2_timecode, minutes, \"uint8\");\n\tprintmember(v4l2_timecode, hours, \"uint8\");\n\tprintmember(v4l2_timecode, userbits, \"[4]uint8\");\n\tprintf(\"}\\n\\n\");\n\n\tprintstruct(v4l2_fmtdesc);\n\tprintmember(v4l2_fmtdesc, index, \"uint32\");\n\tprintmember(v4l2_fmtdesc, type, \"uint32\");\n\tprintmember(v4l2_fmtdesc, flags, \"uint32\");\n\tprintmember(v4l2_fmtdesc, description, \"[32]byte\");\n\tprintmember(v4l2_fmtdesc, pixelformat, \"uint32\");\n\tprintmember(v4l2_fmtdesc, mbus_code, \"uint32\");\n\tprintmember(v4l2_fmtdesc, reserved, \"[3]uint32\");\n\tprintf(\"}\\n\\n\");\n\n\tprintstruct(v4l2_frmsizeenum);\n\tprintmember(v4l2_frmsizeenum, index, \"uint32\");\n\tprintmember(v4l2_frmsizeenum, pixel_format, \"uint32\");\n\tprintmember(v4l2_frmsizeenum, type, \"uint32\");\n\tprintmember(v4l2_frmsizeenum, discrete, \"v4l2_frmsize_discrete\");\n\tprintfiller(v4l2_frmsizeenum, discrete);\n\tprintf(\"}\\n\\n\");\n\n\tprintstruct(v4l2_frmsize_discrete);\n\tprintmember(v4l2_frmsize_discrete, width, \"uint32\");\n\tprintmember(v4l2_frmsize_discrete, height, \"uint32\");\n\tprintf(\"}\\n\\n\");\n\n\tprintstruct(v4l2_frmivalenum);\n\tprintmember(v4l2_frmivalenum, index, \"uint32\");\n\tprintmember(v4l2_frmivalenum, pixel_format, \"uint32\");\n\tprintmember(v4l2_frmivalenum, width, \"uint32\");\n\tprintmember(v4l2_frmivalenum, height, \"uint32\");\n\tprintmember(v4l2_frmivalenum, type, \"uint32\");\n\tprintmember(v4l2_frmivalenum, discrete, \"v4l2_fract\");\n\tprintfiller(v4l2_frmivalenum, discrete);\n\tprintf(\"}\\n\\n\");\n\n\treturn 0;\n}"
  },
  {
    "path": "pkg/v4l2/device/videodev2_arm.go",
    "content": "package device\n\nconst (\n\tVIDIOC_QUERYCAP = 0x80685600\n\tVIDIOC_ENUM_FMT = 0xc0405602\n\tVIDIOC_G_FMT    = 0xc0cc5604\n\tVIDIOC_S_FMT    = 0xc0cc5605\n\tVIDIOC_REQBUFS  = 0xc0145608\n\tVIDIOC_QUERYBUF = 0xc0505609\n\n\tVIDIOC_QBUF      = 0xc050560f\n\tVIDIOC_DQBUF     = 0xc0505611\n\tVIDIOC_STREAMON  = 0x40045612\n\tVIDIOC_STREAMOFF = 0x40045613\n\tVIDIOC_G_PARM    = 0xc0cc5615\n\tVIDIOC_S_PARM    = 0xc0cc5616\n\n\tVIDIOC_ENUM_FRAMESIZES     = 0xc02c564a\n\tVIDIOC_ENUM_FRAMEINTERVALS = 0xc034564b\n)\n\nconst (\n\tV4L2_BUF_TYPE_VIDEO_CAPTURE = 1\n\tV4L2_COLORSPACE_DEFAULT     = 0\n\tV4L2_FIELD_NONE             = 1\n\tV4L2_FRMIVAL_TYPE_DISCRETE  = 1\n\tV4L2_FRMSIZE_TYPE_DISCRETE  = 1\n\tV4L2_MEMORY_MMAP            = 1\n)\n\ntype v4l2_capability struct { // size 104\n\tdriver       [16]byte  // offset 0, size 16\n\tcard         [32]byte  // offset 16, size 32\n\tbus_info     [32]byte  // offset 48, size 32\n\tversion      uint32    // offset 80, size 4\n\tcapabilities uint32    // offset 84, size 4\n\tdevice_caps  uint32    // offset 88, size 4\n\treserved     [3]uint32 // offset 92, size 12\n}\n\ntype v4l2_format struct { // size 204\n\ttyp uint32          // offset 0, size 4\n\t_   [0]byte         // align\n\tpix v4l2_pix_format // offset 4, size 48\n\t_   [152]byte       // filler\n}\n\ntype v4l2_pix_format struct { // size 48\n\twidth        uint32 // offset 0, size 4\n\theight       uint32 // offset 4, size 4\n\tpixelformat  uint32 // offset 8, size 4\n\tfield        uint32 // offset 12, size 4\n\tbytesperline uint32 // offset 16, size 4\n\tsizeimage    uint32 // offset 20, size 4\n\tcolorspace   uint32 // offset 24, size 4\n\tpriv         uint32 // offset 28, size 4\n\tflags        uint32 // offset 32, size 4\n\tycbcr_enc    uint32 // offset 36, size 4\n\tquantization uint32 // offset 40, size 4\n\txfer_func    uint32 // offset 44, size 4\n}\n\ntype v4l2_streamparm struct { // size 204\n\ttyp     uint32           // offset 0, size 4\n\tcapture v4l2_captureparm // offset 4, size 40\n\t_       [160]byte        // filler\n}\n\ntype v4l2_captureparm struct { // size 40\n\tcapability   uint32     // offset 0, size 4\n\tcapturemode  uint32     // offset 4, size 4\n\ttimeperframe v4l2_fract // offset 8, size 8\n\textendedmode uint32     // offset 16, size 4\n\treadbuffers  uint32     // offset 20, size 4\n\treserved     [4]uint32  // offset 24, size 16\n}\n\ntype v4l2_fract struct { // size 8\n\tnumerator   uint32 // offset 0, size 4\n\tdenominator uint32 // offset 4, size 4\n}\n\ntype v4l2_requestbuffers struct { // size 20\n\tcount        uint32   // offset 0, size 4\n\ttyp          uint32   // offset 4, size 4\n\tmemory       uint32   // offset 8, size 4\n\tcapabilities uint32   // offset 12, size 4\n\tflags        uint8    // offset 16, size 1\n\treserved     [3]uint8 // offset 17, size 3\n}\n\ntype v4l2_buffer struct { // size 80\n\tindex     uint32        // offset 0, size 4\n\ttyp       uint32        // offset 4, size 4\n\tbytesused uint32        // offset 8, size 4\n\tflags     uint32        // offset 12, size 4\n\tfield     uint32        // offset 16, size 4\n\t_         [20]byte      // align\n\ttimecode  v4l2_timecode // offset 40, size 16\n\tsequence  uint32        // offset 56, size 4\n\tmemory    uint32        // offset 60, size 4\n\toffset    uint32        // offset 64, size 4\n\t_         [0]byte       // align\n\tlength    uint32        // offset 68, size 4\n\t_         [8]byte       // filler\n}\n\ntype v4l2_timecode struct { // size 16\n\ttyp      uint32   // offset 0, size 4\n\tflags    uint32   // offset 4, size 4\n\tframes   uint8    // offset 8, size 1\n\tseconds  uint8    // offset 9, size 1\n\tminutes  uint8    // offset 10, size 1\n\thours    uint8    // offset 11, size 1\n\tuserbits [4]uint8 // offset 12, size 4\n}\n\ntype v4l2_fmtdesc struct { // size 64\n\tindex       uint32    // offset 0, size 4\n\ttyp         uint32    // offset 4, size 4\n\tflags       uint32    // offset 8, size 4\n\tdescription [32]byte  // offset 12, size 32\n\tpixelformat uint32    // offset 44, size 4\n\tmbus_code   uint32    // offset 48, size 4\n\treserved    [3]uint32 // offset 52, size 12\n}\n\ntype v4l2_frmsizeenum struct { // size 44\n\tindex        uint32                // offset 0, size 4\n\tpixel_format uint32                // offset 4, size 4\n\ttyp          uint32                // offset 8, size 4\n\tdiscrete     v4l2_frmsize_discrete // offset 12, size 8\n\t_            [24]byte              // filler\n}\n\ntype v4l2_frmsize_discrete struct { // size 8\n\twidth  uint32 // offset 0, size 4\n\theight uint32 // offset 4, size 4\n}\n\ntype v4l2_frmivalenum struct { // size 52\n\tindex        uint32     // offset 0, size 4\n\tpixel_format uint32     // offset 4, size 4\n\twidth        uint32     // offset 8, size 4\n\theight       uint32     // offset 12, size 4\n\ttyp          uint32     // offset 16, size 4\n\tdiscrete     v4l2_fract // offset 20, size 8\n\t_            [24]byte   // filler\n}\n"
  },
  {
    "path": "pkg/v4l2/device/videodev2_mipsle.go",
    "content": "package device\n\nconst (\n\tVIDIOC_QUERYCAP = 0x40685600\n\tVIDIOC_ENUM_FMT = 0xc0405602\n\tVIDIOC_G_FMT    = 0xc0cc5604\n\tVIDIOC_S_FMT    = 0xc0cc5605\n\tVIDIOC_REQBUFS  = 0xc0145608\n\tVIDIOC_QUERYBUF = 0xc0445609\n\n\tVIDIOC_QBUF      = 0xc044560f\n\tVIDIOC_DQBUF     = 0xc0445611\n\tVIDIOC_STREAMON  = 0x80045612\n\tVIDIOC_STREAMOFF = 0x80045613\n\tVIDIOC_G_PARM    = 0xc0cc5615\n\tVIDIOC_S_PARM    = 0xc0cc5616\n\n\tVIDIOC_ENUM_FRAMESIZES     = 0xc02c564a\n\tVIDIOC_ENUM_FRAMEINTERVALS = 0xc034564b\n)\n\nconst (\n\tV4L2_BUF_TYPE_VIDEO_CAPTURE = 1\n\tV4L2_COLORSPACE_DEFAULT     = 0\n\tV4L2_FIELD_NONE             = 1\n\tV4L2_FRMIVAL_TYPE_DISCRETE  = 1\n\tV4L2_FRMSIZE_TYPE_DISCRETE  = 1\n\tV4L2_MEMORY_MMAP            = 1\n)\n\ntype v4l2_capability struct { // size 104\n\tdriver       [16]byte  // offset 0, size 16\n\tcard         [32]byte  // offset 16, size 32\n\tbus_info     [32]byte  // offset 48, size 32\n\tversion      uint32    // offset 80, size 4\n\tcapabilities uint32    // offset 84, size 4\n\tdevice_caps  uint32    // offset 88, size 4\n\treserved     [3]uint32 // offset 92, size 12\n}\n\ntype v4l2_format struct { // size 204\n\ttyp uint32          // offset 0, size 4\n\t_   [0]byte         // align\n\tpix v4l2_pix_format // offset 4, size 48\n\t_   [152]byte       // filler\n}\n\ntype v4l2_pix_format struct { // size 48\n\twidth        uint32 // offset 0, size 4\n\theight       uint32 // offset 4, size 4\n\tpixelformat  uint32 // offset 8, size 4\n\tfield        uint32 // offset 12, size 4\n\tbytesperline uint32 // offset 16, size 4\n\tsizeimage    uint32 // offset 20, size 4\n\tcolorspace   uint32 // offset 24, size 4\n\tpriv         uint32 // offset 28, size 4\n\tflags        uint32 // offset 32, size 4\n\tycbcr_enc    uint32 // offset 36, size 4\n\tquantization uint32 // offset 40, size 4\n\txfer_func    uint32 // offset 44, size 4\n}\n\ntype v4l2_streamparm struct { // size 204\n\ttyp     uint32           // offset 0, size 4\n\tcapture v4l2_captureparm // offset 4, size 40\n\t_       [160]byte        // filler\n}\n\ntype v4l2_captureparm struct { // size 40\n\tcapability   uint32     // offset 0, size 4\n\tcapturemode  uint32     // offset 4, size 4\n\ttimeperframe v4l2_fract // offset 8, size 8\n\textendedmode uint32     // offset 16, size 4\n\treadbuffers  uint32     // offset 20, size 4\n\treserved     [4]uint32  // offset 24, size 16\n}\n\ntype v4l2_fract struct { // size 8\n\tnumerator   uint32 // offset 0, size 4\n\tdenominator uint32 // offset 4, size 4\n}\n\ntype v4l2_requestbuffers struct { // size 20\n\tcount        uint32   // offset 0, size 4\n\ttyp          uint32   // offset 4, size 4\n\tmemory       uint32   // offset 8, size 4\n\tcapabilities uint32   // offset 12, size 4\n\tflags        uint8    // offset 16, size 1\n\treserved     [3]uint8 // offset 17, size 3\n}\n\ntype v4l2_buffer struct { // size 68\n\tindex     uint32        // offset 0, size 4\n\ttyp       uint32        // offset 4, size 4\n\tbytesused uint32        // offset 8, size 4\n\tflags     uint32        // offset 12, size 4\n\tfield     uint32        // offset 16, size 4\n\t_         [8]byte       // align\n\ttimecode  v4l2_timecode // offset 28, size 16\n\tsequence  uint32        // offset 44, size 4\n\tmemory    uint32        // offset 48, size 4\n\toffset    uint32        // offset 52, size 4\n\t_         [0]byte       // align\n\tlength    uint32        // offset 56, size 4\n\t_         [8]byte       // filler\n}\n\ntype v4l2_timecode struct { // size 16\n\ttyp      uint32   // offset 0, size 4\n\tflags    uint32   // offset 4, size 4\n\tframes   uint8    // offset 8, size 1\n\tseconds  uint8    // offset 9, size 1\n\tminutes  uint8    // offset 10, size 1\n\thours    uint8    // offset 11, size 1\n\tuserbits [4]uint8 // offset 12, size 4\n}\n\ntype v4l2_fmtdesc struct { // size 64\n\tindex       uint32    // offset 0, size 4\n\ttyp         uint32    // offset 4, size 4\n\tflags       uint32    // offset 8, size 4\n\tdescription [32]byte  // offset 12, size 32\n\tpixelformat uint32    // offset 44, size 4\n\tmbus_code   uint32    // offset 48, size 4\n\treserved    [3]uint32 // offset 52, size 12\n}\n\ntype v4l2_frmsizeenum struct { // size 44\n\tindex        uint32                // offset 0, size 4\n\tpixel_format uint32                // offset 4, size 4\n\ttyp          uint32                // offset 8, size 4\n\tdiscrete     v4l2_frmsize_discrete // offset 12, size 8\n\t_            [24]byte              // filler\n}\n\ntype v4l2_frmsize_discrete struct { // size 8\n\twidth  uint32 // offset 0, size 4\n\theight uint32 // offset 4, size 4\n}\n\ntype v4l2_frmivalenum struct { // size 52\n\tindex        uint32     // offset 0, size 4\n\tpixel_format uint32     // offset 4, size 4\n\twidth        uint32     // offset 8, size 4\n\theight       uint32     // offset 12, size 4\n\ttyp          uint32     // offset 16, size 4\n\tdiscrete     v4l2_fract // offset 20, size 8\n\t_            [24]byte   // filler\n}\n"
  },
  {
    "path": "pkg/v4l2/device/videodev2_x64.go",
    "content": "//go:build amd64 || arm64\n\npackage device\n\nconst (\n\tVIDIOC_QUERYCAP = 0x80685600\n\tVIDIOC_ENUM_FMT = 0xc0405602\n\tVIDIOC_G_FMT    = 0xc0d05604\n\tVIDIOC_S_FMT    = 0xc0d05605\n\tVIDIOC_REQBUFS  = 0xc0145608\n\tVIDIOC_QUERYBUF = 0xc0585609\n\n\tVIDIOC_QBUF      = 0xc058560f\n\tVIDIOC_DQBUF     = 0xc0585611\n\tVIDIOC_STREAMON  = 0x40045612\n\tVIDIOC_STREAMOFF = 0x40045613\n\tVIDIOC_G_PARM    = 0xc0cc5615\n\tVIDIOC_S_PARM    = 0xc0cc5616\n\n\tVIDIOC_ENUM_FRAMESIZES     = 0xc02c564a\n\tVIDIOC_ENUM_FRAMEINTERVALS = 0xc034564b\n)\n\nconst (\n\tV4L2_BUF_TYPE_VIDEO_CAPTURE = 1\n\tV4L2_COLORSPACE_DEFAULT     = 0\n\tV4L2_FIELD_NONE             = 1\n\tV4L2_FRMIVAL_TYPE_DISCRETE  = 1\n\tV4L2_FRMSIZE_TYPE_DISCRETE  = 1\n\tV4L2_MEMORY_MMAP            = 1\n)\n\ntype v4l2_capability struct { // size 104\n\tdriver       [16]byte  // offset 0, size 16\n\tcard         [32]byte  // offset 16, size 32\n\tbus_info     [32]byte  // offset 48, size 32\n\tversion      uint32    // offset 80, size 4\n\tcapabilities uint32    // offset 84, size 4\n\tdevice_caps  uint32    // offset 88, size 4\n\treserved     [3]uint32 // offset 92, size 12\n}\n\ntype v4l2_format struct { // size 208\n\ttyp uint32          // offset 0, size 4\n\t_   [4]byte         // align\n\tpix v4l2_pix_format // offset 8, size 48\n\t_   [152]byte       // filler\n}\n\ntype v4l2_pix_format struct { // size 48\n\twidth        uint32 // offset 0, size 4\n\theight       uint32 // offset 4, size 4\n\tpixelformat  uint32 // offset 8, size 4\n\tfield        uint32 // offset 12, size 4\n\tbytesperline uint32 // offset 16, size 4\n\tsizeimage    uint32 // offset 20, size 4\n\tcolorspace   uint32 // offset 24, size 4\n\tpriv         uint32 // offset 28, size 4\n\tflags        uint32 // offset 32, size 4\n\tycbcr_enc    uint32 // offset 36, size 4\n\tquantization uint32 // offset 40, size 4\n\txfer_func    uint32 // offset 44, size 4\n}\n\ntype v4l2_streamparm struct { // size 204\n\ttyp     uint32           // offset 0, size 4\n\tcapture v4l2_captureparm // offset 4, size 40\n\t_       [160]byte        // filler\n}\n\ntype v4l2_captureparm struct { // size 40\n\tcapability   uint32     // offset 0, size 4\n\tcapturemode  uint32     // offset 4, size 4\n\ttimeperframe v4l2_fract // offset 8, size 8\n\textendedmode uint32     // offset 16, size 4\n\treadbuffers  uint32     // offset 20, size 4\n\treserved     [4]uint32  // offset 24, size 16\n}\n\ntype v4l2_fract struct { // size 8\n\tnumerator   uint32 // offset 0, size 4\n\tdenominator uint32 // offset 4, size 4\n}\n\ntype v4l2_requestbuffers struct { // size 20\n\tcount        uint32   // offset 0, size 4\n\ttyp          uint32   // offset 4, size 4\n\tmemory       uint32   // offset 8, size 4\n\tcapabilities uint32   // offset 12, size 4\n\tflags        uint8    // offset 16, size 1\n\treserved     [3]uint8 // offset 17, size 3\n}\n\ntype v4l2_buffer struct { // size 88\n\tindex     uint32        // offset 0, size 4\n\ttyp       uint32        // offset 4, size 4\n\tbytesused uint32        // offset 8, size 4\n\tflags     uint32        // offset 12, size 4\n\tfield     uint32        // offset 16, size 4\n\t_         [20]byte      // align\n\ttimecode  v4l2_timecode // offset 40, size 16\n\tsequence  uint32        // offset 56, size 4\n\tmemory    uint32        // offset 60, size 4\n\toffset    uint32        // offset 64, size 4\n\t_         [4]byte       // align\n\tlength    uint32        // offset 72, size 4\n\t_         [12]byte      // filler\n}\n\ntype v4l2_timecode struct { // size 16\n\ttyp      uint32   // offset 0, size 4\n\tflags    uint32   // offset 4, size 4\n\tframes   uint8    // offset 8, size 1\n\tseconds  uint8    // offset 9, size 1\n\tminutes  uint8    // offset 10, size 1\n\thours    uint8    // offset 11, size 1\n\tuserbits [4]uint8 // offset 12, size 4\n}\n\ntype v4l2_fmtdesc struct { // size 64\n\tindex       uint32    // offset 0, size 4\n\ttyp         uint32    // offset 4, size 4\n\tflags       uint32    // offset 8, size 4\n\tdescription [32]byte  // offset 12, size 32\n\tpixelformat uint32    // offset 44, size 4\n\tmbus_code   uint32    // offset 48, size 4\n\treserved    [3]uint32 // offset 52, size 12\n}\n\ntype v4l2_frmsizeenum struct { // size 44\n\tindex        uint32                // offset 0, size 4\n\tpixel_format uint32                // offset 4, size 4\n\ttyp          uint32                // offset 8, size 4\n\tdiscrete     v4l2_frmsize_discrete // offset 12, size 8\n\t_            [24]byte              // filler\n}\n\ntype v4l2_frmsize_discrete struct { // size 8\n\twidth  uint32 // offset 0, size 4\n\theight uint32 // offset 4, size 4\n}\n\ntype v4l2_frmivalenum struct { // size 52\n\tindex        uint32     // offset 0, size 4\n\tpixel_format uint32     // offset 4, size 4\n\twidth        uint32     // offset 8, size 4\n\theight       uint32     // offset 12, size 4\n\ttyp          uint32     // offset 16, size 4\n\tdiscrete     v4l2_fract // offset 20, size 8\n\t_            [24]byte   // filler\n}\n"
  },
  {
    "path": "pkg/v4l2/producer.go",
    "content": "//go:build linux\n\npackage v4l2\n\nimport (\n\t\"errors\"\n\t\"net/url\"\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264/annexb\"\n\t\"github.com/AlexxIT/go2rtc/pkg/v4l2/device\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Producer struct {\n\tcore.Connection\n\tdev *device.Device\n}\n\nfunc Open(rawURL string) (*Producer, error) {\n\t// Example (ffmpeg source compatible):\n\t// v4l2:device?video=/dev/video0&input_format=mjpeg&video_size=1280x720\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tquery := u.Query()\n\n\tdev, err := device.Open(query.Get(\"video\"))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tcodec := &core.Codec{\n\t\tClockRate:   90000,\n\t\tPayloadType: core.PayloadTypeRAW,\n\t}\n\n\tvar width, height, pixFmt uint32\n\n\tif wh := strings.Split(query.Get(\"video_size\"), \"x\"); len(wh) == 2 {\n\t\tcodec.FmtpLine = \"width=\" + wh[0] + \";height=\" + wh[1]\n\t\twidth = uint32(core.Atoi(wh[0]))\n\t\theight = uint32(core.Atoi(wh[1]))\n\t}\n\n\tswitch query.Get(\"input_format\") {\n\tcase \"yuyv422\":\n\t\tif codec.FmtpLine == \"\" {\n\t\t\treturn nil, errors.New(\"v4l2: invalid video_size\")\n\t\t}\n\t\tcodec.Name = core.CodecRAW\n\t\tcodec.FmtpLine += \";colorspace=422\"\n\t\tpixFmt = device.V4L2_PIX_FMT_YUYV\n\tcase \"nv12\":\n\t\tif codec.FmtpLine == \"\" {\n\t\t\treturn nil, errors.New(\"v4l2: invalid video_size\")\n\t\t}\n\t\tcodec.Name = core.CodecRAW\n\t\tcodec.FmtpLine += \";colorspace=420mpeg2\" // maybe 420jpeg\n\t\tpixFmt = device.V4L2_PIX_FMT_NV12\n\tcase \"mjpeg\":\n\t\tcodec.Name = core.CodecJPEG\n\t\tpixFmt = device.V4L2_PIX_FMT_MJPEG\n\tcase \"h264\":\n\t\tcodec.Name = core.CodecH264\n\t\tpixFmt = device.V4L2_PIX_FMT_H264\n\tcase \"hevc\":\n\t\tcodec.Name = core.CodecH265\n\t\tpixFmt = device.V4L2_PIX_FMT_HEVC\n\tdefault:\n\t\treturn nil, errors.New(\"v4l2: invalid input_format\")\n\t}\n\n\tif err = dev.SetFormat(width, height, pixFmt); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif fps := core.Atoi(query.Get(\"framerate\")); fps > 0 {\n\t\tif err = dev.SetParam(uint32(fps)); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tmedias := []*core.Media{\n\t\t{\n\t\t\tKind:      core.KindVideo,\n\t\t\tDirection: core.DirectionRecvonly,\n\t\t\tCodecs:    []*core.Codec{codec},\n\t\t},\n\t}\n\treturn &Producer{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"v4l2\",\n\t\t\tMedias:     medias,\n\t\t},\n\t\tdev: dev,\n\t}, nil\n}\n\nfunc (c *Producer) Start() error {\n\tif err := c.dev.StreamOn(); err != nil {\n\t\treturn err\n\t}\n\n\tvar bitstream bool\n\tswitch c.Medias[0].Codecs[0].Name {\n\tcase core.CodecH264, core.CodecH265:\n\t\tbitstream = true\n\t}\n\n\tfor {\n\t\tbuf, err := c.dev.Capture()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tc.Recv += len(buf)\n\n\t\tif len(c.Receivers) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tif bitstream {\n\t\t\tbuf = annexb.EncodeToAVCC(buf)\n\t\t}\n\n\t\tpkt := &rtp.Packet{\n\t\t\tHeader:  rtp.Header{Timestamp: core.Now90000()},\n\t\t\tPayload: buf,\n\t\t}\n\t\tc.Receivers[0].WriteRTP(pkt)\n\t}\n}\n\nfunc (c *Producer) Stop() error {\n\t_ = c.Connection.Stop()\n\treturn errors.Join(c.dev.StreamOff(), c.dev.Close())\n}\n"
  },
  {
    "path": "pkg/wav/backchannel.go",
    "content": "package wav\n\nimport (\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/shell\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Backchannel struct {\n\tcore.Connection\n\tcmd *shell.Command\n}\n\nfunc NewBackchannel(cmd *shell.Command) (core.Producer, error) {\n\tmedias := []*core.Media{\n\t\t{\n\t\t\tKind:      core.KindAudio,\n\t\t\tDirection: core.DirectionSendonly,\n\t\t\tCodecs: []*core.Codec{\n\t\t\t\t//{Name: core.CodecPCML},\n\t\t\t\t{Name: core.CodecPCMA},\n\t\t\t\t{Name: core.CodecPCMU},\n\t\t\t},\n\t\t},\n\t}\n\n\treturn &Backchannel{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"wav\",\n\t\t\tProtocol:   \"pipe\",\n\t\t\tMedias:     medias,\n\t\t\tTransport:  cmd,\n\t\t},\n\t\tcmd: cmd,\n\t}, nil\n}\n\nfunc (c *Backchannel) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {\n\treturn nil, core.ErrCantGetTrack\n}\n\nfunc (c *Backchannel) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {\n\twr, err := c.cmd.StdinPipe()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tb := Header(track.Codec)\n\tif _, err = wr.Write(b); err != nil {\n\t\treturn err\n\t}\n\n\tsender := core.NewSender(media, track.Codec)\n\tsender.Handler = func(packet *rtp.Packet) {\n\t\tif n, err := wr.Write(packet.Payload); err != nil {\n\t\t\tc.Send += n\n\t\t}\n\t}\n\tsender.HandleRTP(track)\n\tc.Senders = append(c.Senders, sender)\n\treturn nil\n}\n\nfunc (c *Backchannel) Start() error {\n\treturn c.cmd.Run()\n}\n"
  },
  {
    "path": "pkg/wav/producer.go",
    "content": "package wav\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"io\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/pion/rtp\"\n)\n\nconst FourCC = \"RIFF\"\n\nfunc Open(r io.Reader) (*Producer, error) {\n\t// https://en.wikipedia.org/wiki/WAV\n\t// https://www.mmsp.ece.mcgill.ca/Documents/AudioFormats/WAVE/WAVE.html\n\trd := bufio.NewReaderSize(r, core.BufferSize)\n\n\tcodec, err := ReadHeader(r)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif codec.Name == \"\" {\n\t\treturn nil, errors.New(\"waw: unsupported codec\")\n\t}\n\n\tmedias := []*core.Media{\n\t\t{\n\t\t\tKind:      core.KindAudio,\n\t\t\tDirection: core.DirectionRecvonly,\n\t\t\tCodecs:    []*core.Codec{codec},\n\t\t},\n\t}\n\treturn &Producer{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"wav\",\n\t\t\tMedias:     medias,\n\t\t\tTransport:  r,\n\t\t},\n\t\trd: rd,\n\t}, nil\n}\n\ntype Producer struct {\n\tcore.Connection\n\trd *bufio.Reader\n}\n\nfunc (c *Producer) Start() error {\n\tvar seq uint16\n\tvar ts uint32\n\n\tconst PacketSize = 0.040 * 8000 // 40ms\n\n\tfor {\n\t\tpayload := make([]byte, PacketSize)\n\t\tif _, err := io.ReadFull(c.rd, payload); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tc.Recv += PacketSize\n\n\t\tif len(c.Receivers) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tpkt := &rtp.Packet{\n\t\t\tHeader: rtp.Header{\n\t\t\t\tVersion:        2,\n\t\t\t\tMarker:         true,\n\t\t\t\tSequenceNumber: seq,\n\t\t\t\tTimestamp:      ts,\n\t\t\t},\n\t\t\tPayload: payload,\n\t\t}\n\t\tc.Receivers[0].WriteRTP(pkt)\n\n\t\tseq++\n\t\tts += PacketSize\n\t}\n}\n"
  },
  {
    "path": "pkg/wav/wav.go",
    "content": "package wav\n\nimport (\n\t\"encoding/binary\"\n\t\"io\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\nfunc Header(codec *core.Codec) []byte {\n\tvar fmt, size, extra byte\n\n\tswitch codec.Name {\n\tcase core.CodecPCML:\n\t\tfmt = 1\n\t\tsize = 2\n\tcase core.CodecPCMA:\n\t\tfmt = 6\n\t\tsize = 1\n\t\textra = 2\n\tcase core.CodecPCMU:\n\t\tfmt = 7\n\t\tsize = 1\n\t\textra = 2\n\tdefault:\n\t\treturn nil\n\t}\n\n\tchannels := byte(codec.Channels)\n\tif channels == 0 {\n\t\tchannels = 1\n\t}\n\n\tb := make([]byte, 0, 46) // cap with extra\n\tb = append(b, \"RIFF\\xFF\\xFF\\xFF\\xFFWAVEfmt \"...)\n\n\tb = append(b, 0x10+extra, 0, 0, 0)\n\tb = append(b, fmt, 0)\n\tb = append(b, channels, 0)\n\tb = binary.LittleEndian.AppendUint32(b, codec.ClockRate)\n\tb = binary.LittleEndian.AppendUint32(b, uint32(size*channels)*codec.ClockRate)\n\tb = append(b, size*channels, 0)\n\tb = append(b, size*8, 0)\n\tif extra > 0 {\n\t\tb = append(b, 0, 0) // ExtraParamSize (if PCM, then doesn't exist)\n\t}\n\n\tb = append(b, \"data\\xFF\\xFF\\xFF\\xFF\"...)\n\n\treturn b\n}\n\nfunc ReadHeader(r io.Reader) (*core.Codec, error) {\n\t// skip Master RIFF chunk\n\tif _, err := io.ReadFull(r, make([]byte, 12)); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar codec core.Codec\n\n\tfor {\n\t\tchunkID, data, err := readChunk(r)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif chunkID == \"data\" {\n\t\t\tbreak\n\t\t}\n\n\t\tif chunkID == \"fmt \" {\n\t\t\t// https://audiocoding.cc/articles/2008-05-22-wav-file-structure/wav_formats.txt\n\t\t\tswitch data[0] {\n\t\t\tcase 1:\n\t\t\t\tcodec.Name = core.CodecPCML\n\t\t\tcase 6:\n\t\t\t\tcodec.Name = core.CodecPCMA\n\t\t\tcase 7:\n\t\t\t\tcodec.Name = core.CodecPCMU\n\t\t\t}\n\n\t\t\tcodec.Channels = data[2]\n\t\t\tcodec.ClockRate = binary.LittleEndian.Uint32(data[4:])\n\t\t}\n\t}\n\n\treturn &codec, nil\n}\n\nfunc readChunk(r io.Reader) (chunkID string, data []byte, err error) {\n\tb := make([]byte, 8)\n\tif _, err = io.ReadFull(r, b); err != nil {\n\t\treturn\n\t}\n\n\tif chunkID = string(b[:4]); chunkID != \"data\" {\n\t\tsize := binary.LittleEndian.Uint32(b[4:])\n\t\tdata = make([]byte, size)\n\t\t_, err = io.ReadFull(r, data)\n\t}\n\n\treturn\n}\n"
  },
  {
    "path": "pkg/webrtc/README.md",
    "content": "## StateChange\n\n1. offer = pc.CreateOffer()\n2. pc.SetLocalDescription(offer)\n3. OnICEGatheringStateChange: gathering\n4. OnSignalingStateChange: have-local-offer\n*. OnICEGatheringStateChange: complete\n5. pc.SetRemoteDescription(answer)\n6. OnSignalingStateChange: stable\n7. OnICEConnectionStateChange: checking\n8. OnICEConnectionStateChange: connected\n"
  },
  {
    "path": "pkg/webrtc/api.go",
    "content": "package webrtc\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"slices\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/xnet\"\n\t\"github.com/pion/ice/v4\"\n\t\"github.com/pion/interceptor\"\n\t\"github.com/pion/webrtc/v4\"\n)\n\n// ReceiveMTU = Ethernet MTU (1500) - IP Header (20) - UDP Header (8)\n// https://ffmpeg.org/ffmpeg-all.html#Muxer\nconst ReceiveMTU = 1472\n\nfunc NewAPI() (*webrtc.API, error) {\n\treturn NewServerAPI(\"\", \"\", nil)\n}\n\ntype Filters struct {\n\tCandidates []string `yaml:\"candidates\"`\n\tLoopback   bool     `yaml:\"loopback\"`\n\tInterfaces []string `yaml:\"interfaces\"`\n\tIPs        []string `yaml:\"ips\"`\n\tNetworks   []string `yaml:\"networks\"`\n\tUDPPorts   []uint16 `yaml:\"udp_ports\"`\n}\n\nfunc (f *Filters) Network(protocol string) string {\n\tif f == nil || f.Networks == nil {\n\t\treturn protocol\n\t}\n\tv4 := slices.Contains(f.Networks, protocol+\"4\")\n\tv6 := slices.Contains(f.Networks, protocol+\"6\")\n\tif v4 && v6 {\n\t\treturn protocol\n\t} else if v4 {\n\t\treturn protocol + \"4\"\n\t} else if v6 {\n\t\treturn protocol + \"6\"\n\t}\n\treturn \"\"\n}\n\nfunc (f *Filters) NetIPs() (ips []net.IP) {\n\titfs, _ := net.Interfaces()\n\tfor _, itf := range itfs {\n\t\tif itf.Flags&net.FlagUp == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tif !f.IncludeLoopback() && itf.Flags&net.FlagLoopback != 0 {\n\t\t\tcontinue\n\t\t}\n\t\tif !f.InterfaceFilter(itf.Name) {\n\t\t\tcontinue\n\t\t}\n\n\t\taddrs, _ := itf.Addrs()\n\t\tfor _, addr := range addrs {\n\t\t\tip := parseNetAddr(addr)\n\t\t\tif ip == nil || !f.IPFilter(ip) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tips = append(ips, ip)\n\t\t}\n\t}\n\treturn\n}\n\nfunc parseNetAddr(addr net.Addr) net.IP {\n\tswitch addr := addr.(type) {\n\tcase *net.IPNet:\n\t\treturn addr.IP\n\tcase *net.IPAddr:\n\t\treturn addr.IP\n\t}\n\treturn nil\n}\n\nfunc (f *Filters) IncludeLoopback() bool {\n\treturn f != nil && f.Loopback\n}\n\nfunc (f *Filters) InterfaceFilter(name string) bool {\n\treturn f == nil || f.Interfaces == nil || slices.Contains(f.Interfaces, name)\n}\n\nfunc (f *Filters) IPFilter(ip net.IP) bool {\n\treturn f == nil || f.IPs == nil || core.Contains(f.IPs, ip.String())\n}\n\nfunc NewServerAPI(network, address string, filters *Filters) (*webrtc.API, error) {\n\t// for debug logs add to env: `PION_LOG_DEBUG=all`\n\tm := &webrtc.MediaEngine{}\n\t//if err := m.RegisterDefaultCodecs(); err != nil {\n\t//\treturn nil, err\n\t//}\n\tif err := RegisterDefaultCodecs(m); err != nil {\n\t\treturn nil, err\n\t}\n\n\ti := &interceptor.Registry{}\n\tif err := webrtc.RegisterDefaultInterceptors(m, i); err != nil {\n\t\treturn nil, err\n\t}\n\n\ts := webrtc.SettingEngine{}\n\n\t// fix https://github.com/pion/webrtc/pull/2407\n\ts.SetDTLSInsecureSkipHelloVerify(true)\n\n\tif filters != nil && filters.Loopback {\n\t\ts.SetIncludeLoopbackCandidate(true)\n\t}\n\n\tvar interfaceFilter func(name string) bool\n\tif filters != nil && filters.Interfaces != nil {\n\t\tinterfaceFilter = func(name string) bool {\n\t\t\treturn core.Contains(filters.Interfaces, name)\n\t\t}\n\t} else {\n\t\t// default interfaces - all, except loopback\n\t}\n\ts.SetInterfaceFilter(interfaceFilter)\n\n\tvar ipFilter func(ip net.IP) bool\n\tif filters != nil && filters.IPs != nil {\n\t\tipFilter = func(ip net.IP) bool {\n\t\t\treturn core.Contains(filters.IPs, ip.String())\n\t\t}\n\t} else {\n\t\t// try filter all Docker-like interfaces\n\t\tipFilter = func(ip net.IP) bool {\n\t\t\treturn !xnet.Docker.Contains(ip)\n\t\t}\n\t\t// if there are no such interfaces - disable the filter\n\t\t// the user will need to enable port forwarding\n\t\tif nets, _ := xnet.IPNets(ipFilter); len(nets) == 0 {\n\t\t\tipFilter = nil\n\t\t}\n\t}\n\ts.SetIPFilter(ipFilter)\n\n\tvar networkTypes []webrtc.NetworkType\n\tif filters != nil && filters.Networks != nil {\n\t\tfor _, s := range filters.Networks {\n\t\t\tif networkType, err := webrtc.NewNetworkType(s); err == nil {\n\t\t\t\tnetworkTypes = append(networkTypes, networkType)\n\t\t\t}\n\t\t}\n\t} else {\n\t\t// default network types - all\n\t\tnetworkTypes = []webrtc.NetworkType{\n\t\t\twebrtc.NetworkTypeUDP4, webrtc.NetworkTypeUDP6,\n\t\t\twebrtc.NetworkTypeTCP4, webrtc.NetworkTypeTCP6,\n\t\t}\n\t}\n\ts.SetNetworkTypes(networkTypes)\n\n\tif filters != nil && len(filters.UDPPorts) == 2 {\n\t\t_ = s.SetEphemeralUDPPortRange(filters.UDPPorts[0], filters.UDPPorts[1])\n\t}\n\n\t// If you don't specify an address, this won't cause an error.\n\t// Connections can still be established using random UDP addresses.\n\tif address != \"\" {\n\t\t// Both newMux functions respect filters and do not raise an error\n\t\t// if the port cannot be listened on.\n\t\tif network == \"\" || network == \"tcp\" {\n\t\t\ttcpMux := newTCPMux(address, filters)\n\t\t\ts.SetICETCPMux(tcpMux)\n\t\t}\n\t\tif network == \"\" || network == \"udp\" {\n\t\t\tudpMux := newUDPMux(address, filters)\n\t\t\ts.SetICEUDPMux(udpMux)\n\t\t}\n\t}\n\n\treturn webrtc.NewAPI(\n\t\twebrtc.WithMediaEngine(m),\n\t\twebrtc.WithInterceptorRegistry(i),\n\t\twebrtc.WithSettingEngine(s),\n\t), nil\n}\n\n// OnNewListener temporary ugly solution for log\nvar OnNewListener = func(ln any) {}\n\nfunc newTCPMux(address string, filters *Filters) ice.TCPMux {\n\tnetworkTCP := filters.Network(\"tcp\") // tcp or tcp4 or tcp6\n\tif ln, _ := net.Listen(networkTCP, address); ln != nil {\n\t\tOnNewListener(ln)\n\t\treturn webrtc.NewICETCPMux(nil, ln, 8)\n\t}\n\treturn nil\n}\n\nfunc newUDPMux(address string, filters *Filters) ice.UDPMux {\n\thost, port, err := net.SplitHostPort(address)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\t// UDPMux should not listening on unspecified address.\n\t// So we will create a listener on all available interfaces.\n\t// We can't use ice.NewMultiUDPMuxFromPort, because it sometimes crashes with an error:\n\t//     listen udp [***]:8555: bind: cannot assign requested address\n\tvar addrs []string\n\tif host == \"\" {\n\t\tfor _, ip := range filters.NetIPs() {\n\t\t\taddrs = append(addrs, fmt.Sprintf(\"%s:%s\", ip, port))\n\t\t}\n\t} else {\n\t\taddrs = []string{address}\n\t}\n\n\tnetworkUDP := filters.Network(\"udp\") // udp or udp4 or udp6\n\n\tvar muxes []ice.UDPMux\n\tfor _, addr := range addrs {\n\t\tif ln, _ := net.ListenPacket(networkUDP, addr); ln != nil {\n\t\t\tOnNewListener(ln)\n\t\t\tmux := ice.NewUDPMuxDefault(ice.UDPMuxParams{UDPConn: ln})\n\t\t\tmuxes = append(muxes, mux)\n\t\t}\n\t}\n\n\tswitch len(muxes) {\n\tcase 0:\n\t\treturn nil\n\tcase 1:\n\t\treturn muxes[0]\n\t}\n\treturn ice.NewMultiUDPMuxDefault(muxes...)\n}\n\nfunc RegisterDefaultCodecs(m *webrtc.MediaEngine) error {\n\tfor _, codec := range []webrtc.RTPCodecParameters{\n\t\t{\n\t\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\t\tMimeType: webrtc.MimeTypeOpus, ClockRate: 48000, Channels: 2, SDPFmtpLine: \"minptime=10;useinbandfec=1\",\n\t\t\t},\n\t\t\tPayloadType: 101, //111,\n\t\t},\n\t\t{\n\t\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\t\tMimeType: webrtc.MimeTypePCMU, ClockRate: 8000,\n\t\t\t},\n\t\t\tPayloadType: 0,\n\t\t},\n\t\t{\n\t\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\t\tMimeType: webrtc.MimeTypePCMA, ClockRate: 8000,\n\t\t\t},\n\t\t\tPayloadType: 8,\n\t\t},\n\t} {\n\t\tif err := m.RegisterCodec(codec, webrtc.RTPCodecTypeAudio); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\tvideoRTCPFeedback := []webrtc.RTCPFeedback{\n\t\t{\"goog-remb\", \"\"},\n\t\t{\"ccm\", \"fir\"},\n\t\t{\"nack\", \"\"},\n\t\t{\"nack\", \"pli\"},\n\t}\n\tfor _, codec := range []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: videoRTCPFeedback,\n\t\t\t},\n\t\t\tPayloadType: 96, // Chrome v110 - PayloadType: 102\n\t\t},\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=42e01f\",\n\t\t\t\tRTCPFeedback: videoRTCPFeedback,\n\t\t\t},\n\t\t\tPayloadType: 97, // Chrome v110 - PayloadType: 106\n\t\t},\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=640032\",\n\t\t\t\tRTCPFeedback: videoRTCPFeedback,\n\t\t\t},\n\t\t\tPayloadType: 98, // Chrome v110 - PayloadType: 112\n\t\t},\n\t\t// macOS Safari 15.1\n\t\t{\n\t\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\t\tMimeType:     webrtc.MimeTypeH265,\n\t\t\t\tClockRate:    90000,\n\t\t\t\tRTCPFeedback: videoRTCPFeedback,\n\t\t\t},\n\t\t\tPayloadType: 100,\n\t\t},\n\t} {\n\t\tif err := m.RegisterCodec(codec, webrtc.RTPCodecTypeVideo); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/webrtc/client.go",
    "content": "package webrtc\n\nimport (\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/pion/sdp/v3\"\n\t\"github.com/pion/webrtc/v4\"\n)\n\nfunc (c *Conn) CreateOffer(medias []*core.Media) (string, error) {\n\t// 1. Create transeivers with proper kind and direction\n\tfor _, media := range medias {\n\t\tvar err error\n\t\tswitch media.Direction {\n\t\tcase core.DirectionRecvonly:\n\t\t\t_, err = c.pc.AddTransceiverFromKind(\n\t\t\t\twebrtc.NewRTPCodecType(media.Kind),\n\t\t\t\twebrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionRecvonly},\n\t\t\t)\n\t\tcase core.DirectionSendonly:\n\t\t\t_, err = c.pc.AddTransceiverFromTrack(\n\t\t\t\tNewTrack(media.Kind),\n\t\t\t\twebrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendonly},\n\t\t\t)\n\t\tcase core.DirectionSendRecv:\n\t\t\t// default transceiver is sendrecv\n\t\t\t_, err = c.pc.AddTransceiverFromTrack(NewTrack(media.Kind))\n\t\tdefault:\n\t\t\t// Nest cameras require data channel\n\t\t\t_, err = c.pc.CreateDataChannel(media.Kind, nil)\n\t\t}\n\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t}\n\n\t// 2. Create local offer\n\tdesc, err := c.pc.CreateOffer(nil)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// 3. Start gathering phase\n\tif err = c.pc.SetLocalDescription(desc); err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn c.pc.LocalDescription().SDP, nil\n}\n\nfunc (c *Conn) CreateCompleteOffer(medias []*core.Media) (string, error) {\n\tif _, err := c.CreateOffer(medias); err != nil {\n\t\treturn \"\", err\n\t}\n\n\t<-webrtc.GatheringCompletePromise(c.pc)\n\treturn c.pc.LocalDescription().SDP, nil\n}\n\nfunc (c *Conn) SetAnswer(answer string) (err error) {\n\tdesc := webrtc.SessionDescription{\n\t\tType: webrtc.SDPTypeAnswer,\n\t\tSDP:  fakeFormatsInAnswer(c.pc.LocalDescription().SDP, answer),\n\t}\n\tif err = c.pc.SetRemoteDescription(desc); err != nil {\n\t\treturn err\n\t}\n\n\tsd := &sdp.SessionDescription{}\n\tif err = sd.Unmarshal([]byte(answer)); err != nil {\n\t\treturn err\n\t}\n\n\tc.Medias = UnmarshalMedias(sd.MediaDescriptions)\n\n\treturn nil\n}\n\n// fakeFormatsInAnswer - fix pion bug with remote SDP parsing:\n// pion will process formats only from first media of each kind\n// so we add all formats from first offer media to the first answer media\nfunc fakeFormatsInAnswer(offer, answer string) string {\n\tsd2 := &sdp.SessionDescription{}\n\tif err := sd2.Unmarshal([]byte(answer)); err != nil {\n\t\treturn answer\n\t}\n\n\t// check if answer has recvonly audio\n\tvar ok bool\n\tfor _, md2 := range sd2.MediaDescriptions {\n\t\tif md2.MediaName.Media == \"audio\" {\n\t\t\tif _, ok = md2.Attribute(\"recvonly\"); ok {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\tif !ok {\n\t\treturn answer\n\t}\n\n\tsd1 := &sdp.SessionDescription{}\n\tif err := sd1.Unmarshal([]byte(offer)); err != nil {\n\t\treturn answer\n\t}\n\n\tvar formats []string\n\tvar attrs []sdp.Attribute\n\n\tfor _, md1 := range sd1.MediaDescriptions {\n\t\tif md1.MediaName.Media == \"audio\" {\n\t\t\tfor _, attr := range md1.Attributes {\n\t\t\t\tswitch attr.Key {\n\t\t\t\tcase \"rtpmap\", \"fmtp\", \"rtcp-fb\", \"extmap\":\n\t\t\t\t\tattrs = append(attrs, attr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tformats = md1.MediaName.Formats\n\t\t\tbreak\n\t\t}\n\t}\n\n\tfor _, md2 := range sd2.MediaDescriptions {\n\t\tif md2.MediaName.Media == \"audio\" {\n\t\t\tfor _, attr := range md2.Attributes {\n\t\t\t\tswitch attr.Key {\n\t\t\t\tcase \"rtpmap\", \"fmtp\", \"rtcp-fb\", \"extmap\":\n\t\t\t\tdefault:\n\t\t\t\t\tattrs = append(attrs, attr)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tmd2.MediaName.Formats = formats\n\t\t\tmd2.Attributes = attrs\n\t\t\tbreak\n\t\t}\n\t}\n\n\tb, err := sd2.Marshal()\n\tif err != nil {\n\t\treturn answer\n\t}\n\n\treturn string(b)\n}\n"
  },
  {
    "path": "pkg/webrtc/client_test.go",
    "content": "package webrtc\n\nimport (\n\t\"testing\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/pion/webrtc/v4\"\n\t\"github.com/stretchr/testify/assert\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestClient(t *testing.T) {\n\tapi, err := NewAPI()\n\trequire.Nil(t, err)\n\n\tpc, err := api.NewPeerConnection(webrtc.Configuration{})\n\trequire.Nil(t, err)\n\n\tprod := NewConn(pc)\n\n\tmedias := []*core.Media{\n\t\t{Kind: core.KindVideo, Direction: core.DirectionRecvonly},\n\t\t{Kind: core.KindAudio, Direction: core.DirectionRecvonly},\n\t\t{Kind: core.KindAudio, Direction: core.DirectionSendonly},\n\t}\n\n\toffer, err := prod.CreateOffer(medias)\n\trequire.Nil(t, err)\n\tassert.NotEmpty(t, offer)\n\n\trequire.Len(t, prod.pc.GetReceivers(), 2)\n\trequire.Len(t, prod.pc.GetSenders(), 1)\n\n\tanswer := `v=0\no=- 1934370540648269799 1678277622 IN IP4 0.0.0.0\ns=-\nt=0 0\na=fingerprint:sha-256 77:8C:9A:62:51:81:69:EA:4E:BE:93:6B:4E:DF:51:D2:2F:E3:DF:E7:F4:8A:18:1A:C0:74:FA:AE:B8:98:29:9B\na=extmap-allow-mixed\na=group:BUNDLE 0 1 2\nm=video 9 UDP/TLS/RTP/SAVPF 97\nc=IN IP4 0.0.0.0\na=setup:active\na=mid:0\na=ice-ufrag:xxx\na=ice-pwd:xxx\na=rtcp-mux\na=rtcp-rsize\na=rtpmap:97 H264/90000\na=fmtp:97 packetization-mode=1;profile-level-id=42e01f\na=extmap:1 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\na=ssrc:2815449682 cname:go2rtc\na=ssrc:2815449682 msid:go2rtc video\na=ssrc:2815449682 mslabel:go2rtc\na=ssrc:2815449682 label:video\na=msid:go2rtc video\na=sendonly\nm=audio 9 UDP/TLS/RTP/SAVPF 8\nc=IN IP4 0.0.0.0\na=setup:active\na=mid:1\na=ice-ufrag:xxx\na=ice-pwd:xxx\na=rtcp-mux\na=rtcp-rsize\na=rtpmap:8 PCMA/8000\na=extmap:1 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\na=ssrc:1392166302 cname:go2rtc\na=ssrc:1392166302 msid:go2rtc audio\na=ssrc:1392166302 mslabel:go2rtc\na=ssrc:1392166302 label:audio\na=msid:go2rtc audio\na=sendonly\nm=audio 9 UDP/TLS/RTP/SAVPF 0\nc=IN IP4 0.0.0.0\na=setup:active\na=mid:2\na=ice-ufrag:xxx\na=ice-pwd:xxx\na=rtcp-mux\na=rtcp-rsize\na=rtpmap:0 PCMU/8000\na=extmap:1 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\na=recvonly\n`\n\n\terr = prod.SetAnswer(answer)\n\trequire.Nil(t, err)\n\n\tsender := prod.pc.GetSenders()[0]\n\n\tcaps := webrtc.RTPCodecCapability{\n\t\tMimeType:  webrtc.MimeTypePCMU,\n\t\tClockRate: 8000,\n\t\tChannels:  0,\n\t}\n\ttrack := sender.Track()\n\ttrack, err = webrtc.NewTrackLocalStaticRTP(caps, track.ID(), track.StreamID())\n\trequire.Nil(t, err)\n\n\terr = sender.ReplaceTrack(track)\n\trequire.Nil(t, err)\n}\n\nfunc TestUnmarshalICEServers(t *testing.T) {\n\ts := `[{\"credential\":\"xxx\",\"urls\":\"xxx\",\"username\":\"xxx\"},{\"credential\":null,\"urls\":\"xxx\",\"username\":null}]`\n\tservers, err := UnmarshalICEServers([]byte(s))\n\trequire.Nil(t, err)\n\trequire.Len(t, servers, 2)\n\trequire.Equal(t, []string{\"xxx\"}, servers[0].URLs)\n\n\ts = `[{\"urls\":\"xxx\"},{\"urls\":[\"yyy\",\"zzz\"]}]`\n\tservers, err = UnmarshalICEServers([]byte(s))\n\trequire.Nil(t, err)\n\trequire.Len(t, servers, 2)\n\trequire.Equal(t, []string{\"xxx\"}, servers[0].URLs)\n\trequire.Equal(t, []string{\"yyy\", \"zzz\"}, servers[1].URLs)\n}\n"
  },
  {
    "path": "pkg/webrtc/conn.go",
    "content": "package webrtc\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/pion/rtcp\"\n\t\"github.com/pion/rtp\"\n\t\"github.com/pion/webrtc/v4\"\n)\n\ntype Conn struct {\n\tcore.Connection\n\tcore.Listener\n\n\tMode core.Mode `json:\"mode\"`\n\n\tpc *webrtc.PeerConnection\n\n\toffer  string\n\tclosed core.Waiter\n}\n\nfunc NewConn(pc *webrtc.PeerConnection) *Conn {\n\tc := &Conn{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"webrtc\",\n\t\t\tTransport:  pc,\n\t\t},\n\t\tpc: pc,\n\t}\n\n\tpc.OnICECandidate(func(candidate *webrtc.ICECandidate) {\n\t\t// last candidate will be empty\n\t\tif candidate != nil {\n\t\t\tc.Fire(candidate)\n\t\t}\n\t})\n\n\tpc.OnDataChannel(func(channel *webrtc.DataChannel) {\n\t\tc.Fire(channel)\n\t})\n\n\tpc.OnICEConnectionStateChange(func(state webrtc.ICEConnectionState) {\n\t\tif state != webrtc.ICEConnectionStateChecking {\n\t\t\treturn\n\t\t}\n\t\tpc.SCTP().Transport().ICETransport().OnSelectedCandidatePairChange(\n\t\t\tfunc(pair *webrtc.ICECandidatePair) {\n\t\t\t\t// fix situation when candidate pair changes multiple times\n\t\t\t\tif i := strings.IndexByte(c.Protocol, '+'); i > 0 {\n\t\t\t\t\tc.Protocol = c.Protocol[:i]\n\t\t\t\t}\n\t\t\t\tc.Protocol += \"+\" + pair.Remote.Protocol.String()\n\t\t\t\tc.RemoteAddr = fmt.Sprintf(\n\t\t\t\t\t\"%s:%d %s\", sanitizeIP6(pair.Remote.Address), pair.Remote.Port, pair.Remote.Typ,\n\t\t\t\t)\n\t\t\t\tif pair.Remote.RelatedAddress != \"\" {\n\t\t\t\t\tc.RemoteAddr += fmt.Sprintf(\n\t\t\t\t\t\t\" %s:%d\", sanitizeIP6(pair.Remote.RelatedAddress), pair.Remote.RelatedPort,\n\t\t\t\t\t)\n\t\t\t\t}\n\t\t\t},\n\t\t)\n\t})\n\n\tpc.OnTrack(func(remote *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) {\n\t\tmedia, codec := c.getMediaCodec(remote)\n\t\tif media == nil {\n\t\t\treturn\n\t\t}\n\n\t\ttrack, err := c.GetTrack(media, codec)\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\n\t\tswitch c.Mode {\n\t\tcase core.ModePassiveProducer, core.ModeActiveProducer:\n\t\t\t// replace the theoretical list of codecs with the actual list of codecs\n\t\t\tif len(media.Codecs) > 1 {\n\t\t\t\tmedia.Codecs = []*core.Codec{codec}\n\t\t\t}\n\t\t}\n\n\t\tif c.Mode == core.ModePassiveProducer && remote.Kind() == webrtc.RTPCodecTypeVideo {\n\t\t\tgo func() {\n\t\t\t\tpkts := []rtcp.Packet{&rtcp.PictureLossIndication{MediaSSRC: uint32(remote.SSRC())}}\n\t\t\t\tfor range time.NewTicker(time.Second * 2).C {\n\t\t\t\t\tif err := pc.WriteRTCP(pkts); err != nil {\n\t\t\t\t\t\treturn\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}()\n\t\t}\n\n\t\tfor {\n\t\t\tb := make([]byte, ReceiveMTU)\n\t\t\tn, _, err := remote.Read(b)\n\t\t\tif err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tc.Recv += n\n\n\t\t\tpacket := &rtp.Packet{}\n\t\t\tif err := packet.Unmarshal(b[:n]); err != nil {\n\t\t\t\treturn\n\t\t\t}\n\n\t\t\tif len(packet.Payload) == 0 {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\ttrack.WriteRTP(packet)\n\t\t}\n\t})\n\n\t// OK connection:\n\t// 15:01:46 ICE connection state changed: checking\n\t// 15:01:46 peer connection state changed: connected\n\t// 15:01:54 peer connection state changed: disconnected\n\t// 15:02:20 peer connection state changed: failed\n\t//\n\t// Fail connection:\n\t// 14:53:08 ICE connection state changed: checking\n\t// 14:53:39 peer connection state changed: failed\n\tpc.OnConnectionStateChange(func(state webrtc.PeerConnectionState) {\n\t\tc.Fire(state)\n\n\t\tswitch state {\n\t\tcase webrtc.PeerConnectionStateConnected:\n\t\t\tfor _, sender := range c.Senders {\n\t\t\t\tsender.Start()\n\t\t\t}\n\t\tcase webrtc.PeerConnectionStateDisconnected, webrtc.PeerConnectionStateFailed, webrtc.PeerConnectionStateClosed:\n\t\t\t// disconnect event comes earlier, than failed\n\t\t\t// but it comes only for success connections\n\t\t\t_ = c.Close()\n\t\t}\n\t})\n\n\treturn c\n}\n\nfunc (c *Conn) MarshalJSON() ([]byte, error) {\n\treturn json.Marshal(c.Connection)\n}\n\nfunc (c *Conn) Close() error {\n\tc.closed.Done(nil)\n\treturn c.pc.Close()\n}\n\nfunc (c *Conn) AddCandidate(candidate string) error {\n\t// pion uses only candidate value from json/object candidate struct\n\treturn c.pc.AddICECandidate(webrtc.ICECandidateInit{Candidate: candidate})\n}\n\nfunc (c *Conn) GetSenderTrack(mid string) *Track {\n\tif tr := c.getTranseiver(mid); tr != nil {\n\t\tif s := tr.Sender(); s != nil {\n\t\t\tif t := s.Track().(*Track); t != nil {\n\t\t\t\treturn t\n\t\t\t}\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (c *Conn) getTranseiver(mid string) *webrtc.RTPTransceiver {\n\tfor _, tr := range c.pc.GetTransceivers() {\n\t\tif tr.Mid() == mid {\n\t\t\treturn tr\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc (c *Conn) getMediaCodec(remote *webrtc.TrackRemote) (*core.Media, *core.Codec) {\n\tfor _, tr := range c.pc.GetTransceivers() {\n\t\t// search Transeiver for this TrackRemote\n\t\tif tr.Receiver() == nil || tr.Receiver().Track() != remote {\n\t\t\tcontinue\n\t\t}\n\n\t\t// search Media for this MID\n\t\tfor _, media := range c.Medias {\n\t\t\tif media.ID != tr.Mid() || media.Direction != core.DirectionRecvonly {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// search codec for this PayloadType\n\t\t\tfor _, codec := range media.Codecs {\n\t\t\t\tif codec.PayloadType != uint8(remote.PayloadType()) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\treturn media, codec\n\t\t\t}\n\t\t}\n\t}\n\n\t// fix moment when core.ModePassiveProducer or core.ModeActiveProducer\n\t// sends new codec with new payload type to same media\n\t// check GetTrack\n\tpanic(core.Caller())\n\n\treturn nil, nil\n}\n\nfunc sanitizeIP6(host string) string {\n\tif strings.IndexByte(host, ':') > 0 {\n\t\treturn \"[\" + host + \"]\"\n\t}\n\treturn host\n}\n"
  },
  {
    "path": "pkg/webrtc/consumer.go",
    "content": "package webrtc\n\nimport (\n\t\"errors\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h265\"\n\t\"github.com/AlexxIT/go2rtc/pkg/pcm\"\n\t\"github.com/pion/rtp\"\n)\n\nfunc (c *Conn) GetMedias() []*core.Media {\n\treturn WithResampling(c.Medias)\n}\n\nfunc (c *Conn) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {\n\tcore.Assert(media.Direction == core.DirectionSendonly)\n\n\tfor _, sender := range c.Senders {\n\t\tif sender.Codec == codec {\n\t\t\tsender.Bind(track)\n\t\t\treturn nil\n\t\t}\n\t}\n\n\tswitch c.Mode {\n\tcase core.ModePassiveConsumer: // video/audio for browser\n\tcase core.ModeActiveProducer: // go2rtc as WebRTC client (backchannel)\n\tcase core.ModePassiveProducer: // WebRTC/WHIP\n\tdefault:\n\t\tpanic(core.Caller())\n\t}\n\n\tlocalTrack := c.GetSenderTrack(media.ID)\n\tif localTrack == nil {\n\t\treturn errors.New(\"webrtc: can't get track\")\n\t}\n\n\tpayloadType := codec.PayloadType\n\n\tsender := core.NewSender(media, codec)\n\tsender.Handler = func(packet *rtp.Packet) {\n\t\tc.Send += packet.MarshalSize()\n\t\t//important to send with remote PayloadType\n\t\t_ = localTrack.WriteRTP(payloadType, packet)\n\t}\n\n\tswitch track.Codec.Name {\n\tcase core.CodecH264:\n\t\tsender.Handler = h264.RTPPay(1200, sender.Handler)\n\t\tif track.Codec.IsRTP() {\n\t\t\tsender.Handler = h264.RTPDepay(track.Codec, sender.Handler)\n\t\t} else {\n\t\t\tsender.Handler = h264.RepairAVCC(track.Codec, sender.Handler)\n\t\t}\n\n\tcase core.CodecH265:\n\t\tsender.Handler = h265.RTPPay(1200, sender.Handler)\n\t\tif track.Codec.IsRTP() {\n\t\t\tsender.Handler = h265.RTPDepay(track.Codec, sender.Handler)\n\t\t} else {\n\t\t\tsender.Handler = h265.RepairAVCC(track.Codec, sender.Handler)\n\t\t}\n\n\tcase core.CodecPCMA, core.CodecPCMU, core.CodecPCM, core.CodecPCML:\n\t\t// Fix audio quality https://github.com/AlexxIT/WebRTC/issues/500\n\t\t// should be before ResampleToG711, because it will be called last\n\t\tsender.Handler = pcm.RepackG711(false, sender.Handler)\n\n\t\tif codec.ClockRate == 0 {\n\t\t\tif codec.Name == core.CodecPCM || codec.Name == core.CodecPCML {\n\t\t\t\tcodec.Name = core.CodecPCMA\n\t\t\t}\n\t\t\tcodec.ClockRate = 8000\n\t\t\tsender.Handler = pcm.TranscodeHandler(codec, track.Codec, sender.Handler)\n\t\t}\n\t}\n\n\t// TODO: rewrite this dirty logic\n\t// maybe not best solution, but ActiveProducer connected before AddTrack\n\tif c.Mode != core.ModeActiveProducer {\n\t\tsender.Bind(track)\n\t} else {\n\t\tsender.HandleRTP(track)\n\t}\n\n\tc.Senders = append(c.Senders, sender)\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/webrtc/helpers.go",
    "content": "package webrtc\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"hash/crc32\"\n\t\"net\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/pion/ice/v4\"\n\t\"github.com/pion/sdp/v3\"\n\t\"github.com/pion/stun/v3\"\n\t\"github.com/pion/webrtc/v4\"\n)\n\nfunc UnmarshalMedias(descriptions []*sdp.MediaDescription) (medias []*core.Media) {\n\t// 1. Sort medias, so video will always be before audio\n\t// 2. Ignore application media from Hass default lovelace card\n\t// 3. Ignore media without direction (inactive media)\n\t// 4. Inverse media direction (because it is remote peer medias list)\n\tfor _, kind := range []string{core.KindVideo, core.KindAudio} {\n\t\tfor _, md := range descriptions {\n\t\t\tif md.MediaName.Media != kind {\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tmedia := core.UnmarshalMedia(md)\n\t\t\tswitch media.Direction {\n\t\t\tcase core.DirectionSendRecv:\n\t\t\t\tmedia.Direction = core.DirectionRecvonly\n\t\t\t\tmedias = append(medias, media)\n\n\t\t\t\tmedia = media.Clone()\n\t\t\t\tmedia.Direction = core.DirectionSendonly\n\n\t\t\tcase core.DirectionRecvonly:\n\t\t\t\tmedia.Direction = core.DirectionSendonly\n\n\t\t\tcase core.DirectionSendonly:\n\t\t\t\tmedia.Direction = core.DirectionRecvonly\n\n\t\t\tcase \"\":\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\t// skip non-media codecs to avoid confusing users in info and logs\n\t\t\tmedia.Codecs = SkipNonMediaCodecs(media.Codecs)\n\n\t\t\tmedias = append(medias, media)\n\t\t}\n\t}\n\n\treturn\n}\n\nfunc SkipNonMediaCodecs(input []*core.Codec) (output []*core.Codec) {\n\tfor _, codec := range input {\n\t\tswitch codec.Name {\n\t\tcase \"RTX\", \"RED\", \"ULPFEC\", \"FLEXFEC-03\":\n\t\t\tcontinue\n\t\tcase \"CN\", \"TELEPHONE-EVENT\":\n\t\t\tcontinue // https://datatracker.ietf.org/doc/html/rfc7874\n\t\t}\n\t\t// VP8, VP9, H264, H265, AV1\n\t\t// OPUS, G722, PCMU, PCMA\n\t\toutput = append(output, codec)\n\t}\n\treturn\n}\n\n// WithResampling - will add for consumer: PCMA/0, PCMU/0, PCM/0, PCML/0\n// so it can add resampling for PCMA/PCMU and repack for PCM/PCML\nfunc WithResampling(medias []*core.Media) []*core.Media {\n\tfor _, media := range medias {\n\t\tif media.Kind != core.KindAudio || media.Direction != core.DirectionSendonly {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar pcma, pcmu, pcm, pcml *core.Codec\n\n\t\tfor _, codec := range media.Codecs {\n\t\t\tswitch codec.Name {\n\t\t\tcase core.CodecPCMA:\n\t\t\t\tif codec.ClockRate != 0 {\n\t\t\t\t\tpcma = codec\n\t\t\t\t} else {\n\t\t\t\t\tpcma = nil\n\t\t\t\t}\n\t\t\tcase core.CodecPCMU:\n\t\t\t\tif codec.ClockRate != 0 {\n\t\t\t\t\tpcmu = codec\n\t\t\t\t} else {\n\t\t\t\t\tpcmu = nil\n\t\t\t\t}\n\t\t\tcase core.CodecPCM:\n\t\t\t\tpcm = codec\n\t\t\tcase core.CodecPCML:\n\t\t\t\tpcml = codec\n\t\t\t}\n\t\t}\n\n\t\tif pcma != nil {\n\t\t\tpcma = pcma.Clone()\n\t\t\tpcma.ClockRate = 0 // reset clock rate so will match any\n\t\t\tmedia.Codecs = append(media.Codecs, pcma)\n\t\t}\n\t\tif pcmu != nil {\n\t\t\tpcmu = pcmu.Clone()\n\t\t\tpcmu.ClockRate = 0\n\t\t\tmedia.Codecs = append(media.Codecs, pcmu)\n\t\t}\n\t\tif pcma != nil && pcm == nil {\n\t\t\tpcm = pcma.Clone()\n\t\t\tpcm.Name = core.CodecPCM\n\t\t\tmedia.Codecs = append(media.Codecs, pcm)\n\t\t}\n\t\tif pcma != nil && pcml == nil {\n\t\t\tpcml = pcma.Clone()\n\t\t\tpcml.Name = core.CodecPCML\n\t\t\tmedia.Codecs = append(media.Codecs, pcml)\n\t\t}\n\t}\n\n\treturn medias\n}\n\nfunc NewCandidate(network, address string) (string, error) {\n\ti := strings.LastIndexByte(address, ':')\n\tif i < 0 {\n\t\treturn \"\", errors.New(\"wrong candidate: \" + address)\n\t}\n\thost, port := address[:i], address[i+1:]\n\n\ti, err := strconv.Atoi(port)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tconfig := &ice.CandidateHostConfig{\n\t\tNetwork:   network,\n\t\tAddress:   host,\n\t\tPort:      i,\n\t\tComponent: ice.ComponentRTP,\n\t}\n\n\tif network == \"tcp\" {\n\t\tconfig.TCPType = ice.TCPTypePassive\n\t}\n\n\tcand, err := ice.NewCandidateHost(config)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn \"candidate:\" + cand.Marshal(), nil\n}\n\nfunc LookupIP(address string) (string, error) {\n\tif strings.HasPrefix(address, \"stun:\") {\n\t\tip, err := GetCachedPublicIP()\n\t\tif err != nil {\n\t\t\treturn \"\", err\n\t\t}\n\t\treturn ip.String() + address[4:], nil\n\t}\n\n\tif IsIP(address) {\n\t\treturn address, nil\n\t}\n\n\ti := strings.IndexByte(address, ':')\n\tips, err := net.LookupIP(address[:i])\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tif len(ips) == 0 {\n\t\treturn \"\", fmt.Errorf(\"can't resolve: %s\", address)\n\t}\n\n\treturn ips[0].String() + address[i:], nil\n}\n\n// GetPublicIP example from https://github.com/pion/stun\nfunc GetPublicIP(address string) (net.IP, error) {\n\tconn, err := net.Dial(\"udp\", address)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tc, err := stun.NewClient(conn)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif err = conn.SetDeadline(time.Now().Add(time.Second * 3)); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar res stun.Event\n\n\tmessage := stun.MustBuild(stun.TransactionID, stun.BindingRequest)\n\tif err = c.Do(message, func(e stun.Event) { res = e }); err != nil {\n\t\treturn nil, err\n\t}\n\tif err = c.Close(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif res.Error != nil {\n\t\treturn nil, res.Error\n\t}\n\n\tvar xorAddr stun.XORMappedAddress\n\tif err = xorAddr.GetFrom(res.Message); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn xorAddr.IP, nil\n}\n\nvar cachedIP net.IP\nvar cachedTS time.Time\n\nfunc GetCachedPublicIP(stuns ...string) (net.IP, error) {\n\tif now := time.Now(); now.After(cachedTS) {\n\t\tfor _, addr := range stuns {\n\t\t\tif ip, _ := GetPublicIP(addr); ip != nil {\n\t\t\t\tcachedIP = ip\n\t\t\t\tcachedTS = now.Add(time.Minute * 5)\n\t\t\t\treturn ip, nil\n\t\t\t}\n\t\t}\n\t}\n\tif cachedIP == nil {\n\t\treturn nil, errors.New(\"webrtc: can't get public IP\")\n\t}\n\treturn cachedIP, nil\n}\n\nfunc IsIP(host string) bool {\n\tfor _, i := range host {\n\t\tif i >= 'A' {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n\nfunc MimeType(codec *core.Codec) string {\n\tswitch codec.Name {\n\tcase core.CodecH264:\n\t\treturn webrtc.MimeTypeH264\n\tcase core.CodecH265:\n\t\treturn webrtc.MimeTypeH265\n\tcase core.CodecVP8:\n\t\treturn webrtc.MimeTypeVP8\n\tcase core.CodecVP9:\n\t\treturn webrtc.MimeTypeVP9\n\tcase core.CodecAV1:\n\t\treturn webrtc.MimeTypeAV1\n\tcase core.CodecPCMU:\n\t\treturn webrtc.MimeTypePCMU\n\tcase core.CodecPCMA:\n\t\treturn webrtc.MimeTypePCMA\n\tcase core.CodecOpus:\n\t\treturn webrtc.MimeTypeOpus\n\tcase core.CodecG722:\n\t\treturn webrtc.MimeTypeG722\n\t}\n\tpanic(\"not implemented\")\n}\n\nfunc CandidateICE(network, host, port string, priority uint32) string {\n\t// 1. Foundation\n\t// 2. Component, always 1 because RTP\n\t// 3. \"udp\" or \"tcp\"\n\t// 4. Priority\n\t// 5. Host - IP4 or IP6 or domain name\n\t// 6. Port\n\t// 7. \"typ host\"\n\tfoundation := crc32.ChecksumIEEE([]byte(\"host\" + host + network + \"4\"))\n\ts := fmt.Sprintf(\"candidate:%d 1 %s %d %s %s typ host\", foundation, network, priority, host, port)\n\tif network == \"tcp\" {\n\t\treturn s + \" tcptype passive\"\n\t}\n\treturn s\n}\n\n// Priority = type << 24 + local << 8 + component\n// https://www.rfc-editor.org/rfc/rfc8445#section-5.1.2.1\n\nconst PriorityHostUDP uint32 = 0x001F_FFFF |\n\t126<<24 | // udp host\n\t7<<21 // udp\nconst PriorityHostTCPPassive uint32 = 0x001F_FFFF |\n\t99<<24 | // tcp host\n\t4<<21 // tcp passive\n\n// CandidateHostPriority (lower indexes has a higher priority)\nfunc CandidateHostPriority(network string, index int) uint32 {\n\tswitch network {\n\tcase \"udp\":\n\t\treturn PriorityHostUDP - uint32(index)\n\tcase \"tcp\":\n\t\treturn PriorityHostTCPPassive - uint32(index)\n\t}\n\treturn 0\n}\n\nfunc UnmarshalICEServers(b []byte) ([]webrtc.ICEServer, error) {\n\ttype ICEServer struct {\n\t\tURLs       any    `json:\"urls\"`\n\t\tUsername   string `json:\"username,omitempty\"`\n\t\tCredential string `json:\"credential,omitempty\"`\n\t}\n\n\tvar src []ICEServer\n\tif err := json.Unmarshal(b, &src); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar dst []webrtc.ICEServer\n\tfor i := range src {\n\t\tsrv := webrtc.ICEServer{\n\t\t\tUsername:   src[i].Username,\n\t\t\tCredential: src[i].Credential,\n\t\t}\n\n\t\tswitch v := src[i].URLs.(type) {\n\t\tcase []any:\n\t\t\tfor _, u := range v {\n\t\t\t\tif s, ok := u.(string); ok {\n\t\t\t\t\tsrv.URLs = append(srv.URLs, s)\n\t\t\t\t}\n\t\t\t}\n\t\tcase string:\n\t\t\tsrv.URLs = []string{v}\n\t\t}\n\n\t\tdst = append(dst, srv)\n\t}\n\n\treturn dst, nil\n}\n"
  },
  {
    "path": "pkg/webrtc/producer.go",
    "content": "package webrtc\n\nimport (\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/pion/webrtc/v4\"\n)\n\nfunc (c *Conn) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {\n\tcore.Assert(media.Direction == core.DirectionRecvonly)\n\n\tfor _, track := range c.Receivers {\n\t\tif track.Codec == codec {\n\t\t\treturn track, nil\n\t\t}\n\t}\n\n\tswitch c.Mode {\n\tcase core.ModePassiveConsumer: // backchannel from browser\n\t\t// set codec for consumer recv track so remote peer should send media with this codec\n\t\tparams := webrtc.RTPCodecParameters{\n\t\t\tRTPCodecCapability: webrtc.RTPCodecCapability{\n\t\t\t\tMimeType:  MimeType(codec),\n\t\t\t\tClockRate: codec.ClockRate,\n\t\t\t\tChannels:  uint16(codec.Channels),\n\t\t\t},\n\t\t\tPayloadType: 0, // don't know if this necessary\n\t\t}\n\n\t\ttr := c.getTranseiver(media.ID)\n\n\t\t_ = tr.SetCodecPreferences([]webrtc.RTPCodecParameters{params})\n\n\tcase core.ModePassiveProducer, core.ModeActiveProducer:\n\t\t// Passive producers: OBS Studio via WHIP or Browser\n\t\t// Active producers: go2rtc as WebRTC client or WebTorrent\n\n\tdefault:\n\t\tpanic(core.Caller())\n\t}\n\n\ttrack := core.NewReceiver(media, codec)\n\tc.Receivers = append(c.Receivers, track)\n\treturn track, nil\n}\n\nfunc (c *Conn) Start() error {\n\tc.closed.Wait()\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/webrtc/server.go",
    "content": "package webrtc\n\nimport (\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/pion/sdp/v3\"\n\t\"github.com/pion/webrtc/v4\"\n)\n\nfunc (c *Conn) SetOffer(offer string) (err error) {\n\tc.offer = offer\n\n\tsd := &sdp.SessionDescription{}\n\tif err = sd.Unmarshal([]byte(offer)); err != nil {\n\t\treturn\n\t}\n\n\t// create transceivers with opposite direction\n\tfor _, md := range sd.MediaDescriptions {\n\t\tvar mid string\n\t\tvar tr *webrtc.RTPTransceiver\n\t\tfor _, attr := range md.Attributes {\n\t\t\tswitch attr.Key {\n\t\t\tcase core.DirectionSendRecv:\n\t\t\t\ttr, _ = c.pc.AddTransceiverFromTrack(NewTrack(md.MediaName.Media))\n\t\t\tcase core.DirectionSendonly:\n\t\t\t\ttr, _ = c.pc.AddTransceiverFromKind(\n\t\t\t\t\twebrtc.NewRTPCodecType(md.MediaName.Media),\n\t\t\t\t\twebrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionRecvonly},\n\t\t\t\t)\n\t\t\tcase core.DirectionRecvonly:\n\t\t\t\ttr, _ = c.pc.AddTransceiverFromTrack(\n\t\t\t\t\tNewTrack(md.MediaName.Media),\n\t\t\t\t\twebrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendonly},\n\t\t\t\t)\n\t\t\tcase \"mid\":\n\t\t\t\tmid = attr.Value\n\t\t\t}\n\t\t}\n\n\t\tif mid != \"\" && tr != nil {\n\t\t\t_ = tr.SetMid(mid)\n\t\t}\n\t}\n\n\tc.Medias = UnmarshalMedias(sd.MediaDescriptions)\n\n\treturn\n}\n\nfunc (c *Conn) GetAnswer() (answer string, err error) {\n\t// we need to process remote offer after we create transeivers\n\tdesc := webrtc.SessionDescription{Type: webrtc.SDPTypeOffer, SDP: c.offer}\n\tif err = c.pc.SetRemoteDescription(desc); err != nil {\n\t\treturn \"\", err\n\t}\n\n\t// disable transceivers if we don't have track, make direction=inactive\ntranseivers:\n\tfor _, tr := range c.pc.GetTransceivers() {\n\t\tfor _, sender := range c.Senders {\n\t\t\tif sender.Media.ID == tr.Mid() {\n\t\t\t\tcontinue transeivers\n\t\t\t}\n\t\t}\n\n\t\tswitch tr.Direction() {\n\t\tcase webrtc.RTPTransceiverDirectionSendrecv:\n\t\t\t_ = tr.Sender().Stop()             // don't know if necessary\n\t\t\t_ = tr.SetSender(tr.Sender(), nil) // set direction to recvonly\n\t\tcase webrtc.RTPTransceiverDirectionSendonly:\n\t\t\t_ = tr.Stop()\n\t\t}\n\t}\n\n\tif desc, err = c.pc.CreateAnswer(nil); err != nil {\n\t\treturn\n\t}\n\tif err = c.pc.SetLocalDescription(desc); err != nil {\n\t\treturn\n\t}\n\n\treturn c.pc.LocalDescription().SDP, nil\n}\n\n// GetCompleteAnswer - get SDP answer with candidates inside\nfunc (c *Conn) GetCompleteAnswer(candidates []string, filter func(*webrtc.ICECandidate) bool) (string, error) {\n\tvar done = make(chan struct{})\n\n\tc.pc.OnICECandidate(func(candidate *webrtc.ICECandidate) {\n\t\tif candidate != nil {\n\t\t\tif filter == nil || filter(candidate) {\n\t\t\t\tcandidates = append(candidates, candidate.ToJSON().Candidate)\n\t\t\t}\n\t\t} else {\n\t\t\tdone <- struct{}{}\n\t\t}\n\t})\n\n\tanswer, err := c.GetAnswer()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\t<-done\n\n\tsd := &sdp.SessionDescription{}\n\tif err = sd.Unmarshal([]byte(answer)); err != nil {\n\t\treturn \"\", err\n\t}\n\n\tmd := sd.MediaDescriptions[0]\n\n\tfor _, candidate := range candidates {\n\t\tmd.WithPropertyAttribute(candidate)\n\t}\n\n\tb, err := sd.Marshal()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\treturn string(b), nil\n}\n"
  },
  {
    "path": "pkg/webrtc/track.go",
    "content": "package webrtc\n\nimport (\n\t\"sync\"\n\n\t\"github.com/pion/rtp\"\n\t\"github.com/pion/webrtc/v4\"\n)\n\ntype Track struct {\n\tkind     string\n\tid       string\n\tstreamID string\n\tsequence uint16\n\tssrc     uint32\n\twriter   webrtc.TrackLocalWriter\n\tmu       sync.Mutex\n}\n\nfunc NewTrack(kind string) *Track {\n\treturn &Track{\n\t\tkind:     kind,\n\t\tid:       \"go2rtc-\" + kind,\n\t\tstreamID: \"go2rtc\",\n\t}\n}\n\nfunc (t *Track) Bind(context webrtc.TrackLocalContext) (webrtc.RTPCodecParameters, error) {\n\tt.mu.Lock()\n\tt.ssrc = uint32(context.SSRC())\n\tt.writer = context.WriteStream()\n\tt.mu.Unlock()\n\n\tfor _, parameters := range context.CodecParameters() {\n\t\t// return first parameters\n\t\treturn parameters, nil\n\t}\n\n\treturn webrtc.RTPCodecParameters{}, nil\n}\n\nfunc (t *Track) Unbind(context webrtc.TrackLocalContext) error {\n\tt.mu.Lock()\n\tt.writer = nil\n\tt.mu.Unlock()\n\treturn nil\n}\n\nfunc (t *Track) ID() string {\n\treturn t.id\n}\n\nfunc (t *Track) RID() string {\n\treturn \"\" // don't know what it is\n}\n\nfunc (t *Track) StreamID() string {\n\treturn t.streamID\n}\n\nfunc (t *Track) Kind() webrtc.RTPCodecType {\n\treturn webrtc.NewRTPCodecType(t.kind)\n}\n\nfunc (t *Track) WriteRTP(payloadType uint8, packet *rtp.Packet) (err error) {\n\t// using mutex because Unbind https://github.com/AlexxIT/go2rtc/issues/994\n\tt.mu.Lock()\n\n\t// in case when we start WriteRTP before Track.Bind\n\tif t.writer != nil {\n\t\t// important to have internal counter if input packets from different sources\n\t\tt.sequence++\n\n\t\theader := packet.Header\n\t\theader.SSRC = t.ssrc\n\t\theader.PayloadType = payloadType\n\t\theader.SequenceNumber = t.sequence\n\t\t_, err = t.writer.WriteRTP(&header, packet.Payload)\n\t}\n\n\tt.mu.Unlock()\n\treturn\n}\n"
  },
  {
    "path": "pkg/webrtc/webrtc_test.go",
    "content": "package webrtc\n\nimport (\n\t\"testing\"\n\n\t\"github.com/pion/webrtc/v4\"\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestAlexa(t *testing.T) {\n\t// from https://github.com/AlexxIT/go2rtc/issues/825\n\toffer := `v=0\no=- 3911343731 3911343731 IN IP4 0.0.0.0\ns=a 2 z\nc=IN IP4 0.0.0.0\nt=0 0\na=group:BUNDLE audio0 video0\nm=audio 1 UDP/TLS/RTP/SAVPF 96 0 8\na=candidate:1 1 UDP 2013266431 52.90.193.210 60128 typ host\na=candidate:2 1 TCP 1015021823 52.90.193.210 9 typ host tcptype active\na=candidate:3 1 TCP 1010827519 52.90.193.210 45962 typ host tcptype passive\na=candidate:1 2 UDP 2013266430 52.90.193.210 46109 typ host\na=candidate:2 2 TCP 1015021822 52.90.193.210 9 typ host tcptype active\na=candidate:3 2 TCP 1010827518 52.90.193.210 53795 typ host tcptype passive\na=setup:actpass\na=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\na=rtpmap:96 opus/48000/2\na=rtpmap:0 PCMU/8000\na=rtpmap:8 PCMA/8000\na=rtcp:9 IN IP4 0.0.0.0\na=rtcp-mux\na=sendrecv\na=mid:audio0\na=ssrc:3573704076 cname:user3856789923@host-9dd1dd33\na=ice-ufrag:gxfV\na=ice-pwd:KepKrlQ1+LD+RGTAFaqVck\na=fingerprint:sha-256 A2:93:53:50:E4:2F:C5:4E:DF:7C:70:99:5A:A7:39:50:1A:63:E5:B2:CA:73:70:7A:C5:F4:01:BF:BD:99:57:FC\nm=video 1 UDP/TLS/RTP/SAVPF 99\na=candidate:1 1 UDP 2013266431 52.90.193.210 60128 typ host\na=candidate:1 2 UDP 2013266430 52.90.193.210 46109 typ host\na=candidate:2 1 TCP 1015021823 52.90.193.210 9 typ host tcptype active\na=candidate:3 1 TCP 1010827519 52.90.193.210 45962 typ host tcptype passive\na=candidate:3 2 TCP 1010827518 52.90.193.210 53795 typ host tcptype passive\na=candidate:2 2 TCP 1015021822 52.90.193.210 9 typ host tcptype active\nb=AS:2500\na=setup:actpass\na=extmap:3 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\na=rtpmap:99 H264/90000\na=rtcp:9 IN IP4 0.0.0.0\na=rtcp-mux\na=sendrecv\na=mid:video0\na=rtcp-fb:99 nack\na=rtcp-fb:99 nack pli\na=rtcp-fb:99 ccm fir\na=ssrc:3778078295 cname:user3856789923@host-9dd1dd33\na=ice-ufrag:gxfV\na=ice-pwd:KepKrlQ1+LD+RGTAFaqVck\na=fingerprint:sha-256 A2:93:53:50:E4:2F:C5:4E:DF:7C:70:99:5A:A7:39:50:1A:63:E5:B2:CA:73:70:7A:C5:F4:01:BF:BD:99:57:FC\n`\n\n\tpc, err := webrtc.NewPeerConnection(webrtc.Configuration{})\n\trequire.Nil(t, err)\n\n\tconn := NewConn(pc)\n\terr = conn.SetOffer(offer)\n\trequire.Nil(t, err)\n\n\t_, err = conn.GetAnswer()\n\trequire.Nil(t, err)\n}\n"
  },
  {
    "path": "pkg/webtorrent/client.go",
    "content": "package webtorrent\n\nimport (\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/webrtc\"\n\t\"github.com/gorilla/websocket\"\n\tpion \"github.com/pion/webrtc/v4\"\n)\n\nfunc NewClient(tracker, share, pwd string, pc *pion.PeerConnection) (*webrtc.Conn, error) {\n\t// 1. Create WebRTC producer\n\tprod := webrtc.NewConn(pc)\n\tprod.FormatName = \"webtorrent\"\n\tprod.Mode = core.ModeActiveProducer\n\tprod.Protocol = \"ws\"\n\n\tmedias := []*core.Media{\n\t\t{Kind: core.KindVideo, Direction: core.DirectionRecvonly},\n\t\t{Kind: core.KindAudio, Direction: core.DirectionRecvonly},\n\t}\n\n\t// 2. Create offer\n\toffer, err := prod.CreateCompleteOffer(medias)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 3. Encrypt offer\n\tnonce := strconv.FormatInt(time.Now().UnixNano(), 36)\n\n\tcipher, err := NewCipher(share, pwd, nonce)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tenc := cipher.Encrypt([]byte(offer))\n\n\t// 4. Connect to Tracker\n\tws, _, err := websocket.DefaultDialer.Dial(tracker, nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdefer ws.Close()\n\n\t// 5. Send offer\n\tmsg := fmt.Sprintf(\n\t\t`{\"action\":\"announce\",\"info_hash\":\"%s\",\"peer_id\":\"%s\",\"offers\":[{\"offer_id\":\"%s\",\"offer\":{\"type\":\"offer\",\"sdp\":\"%s\"}}],\"numwant\":1}`,\n\t\tInfoHash(share), core.RandString(16, 36), nonce, base64.StdEncoding.EncodeToString(enc),\n\t)\n\tif err = ws.WriteMessage(websocket.TextMessage, []byte(msg)); err != nil {\n\t\treturn nil, err\n\t}\n\n\t// wait 30 seconds until full answer\n\tif err = ws.SetReadDeadline(time.Now().Add(time.Second * 30)); err != nil {\n\t\treturn nil, err\n\t}\n\n\tfor {\n\t\t// 6. Read answer\n\t\tvar v Message\n\t\tif err = ws.ReadJSON(&v); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif v.Answer == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\t// 7. Decrypt answer\n\t\tenc, err = base64.StdEncoding.DecodeString(v.Answer.SDP)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tanswer, err := cipher.Decrypt(enc)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\t// 8. Set answer\n\t\tif err = prod.SetAnswer(string(answer)); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\treturn prod, nil\n\t}\n}\n"
  },
  {
    "path": "pkg/webtorrent/crypto.go",
    "content": "package webtorrent\n\nimport (\n\t\"crypto/aes\"\n\t\"crypto/cipher\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"time\"\n)\n\ntype Cipher struct {\n\tgcm   cipher.AEAD\n\tiv    []byte\n\tnonce []byte\n}\n\nfunc NewCipher(share, pwd, nonce string) (*Cipher, error) {\n\ttimestamp, err := strconv.ParseInt(nonce, 36, 64)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdelta := time.Duration(time.Now().UnixNano() - timestamp)\n\tif delta < 0 {\n\t\tdelta = -delta\n\t}\n\n\t// protect from replay attack, but respect wrong timezone on server\n\tif delta > 12*time.Hour {\n\t\treturn nil, fmt.Errorf(\"wrong timedelta %s\", delta)\n\t}\n\n\tc := &Cipher{}\n\n\thash := sha256.New()\n\thash.Write([]byte(nonce + \":\" + pwd))\n\tkey := hash.Sum(nil)\n\n\thash.Reset()\n\thash.Write([]byte(share + \":\" + nonce))\n\tc.iv = hash.Sum(nil)[:12]\n\n\tblock, err := aes.NewCipher(key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tc.gcm, err = cipher.NewGCM(block)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tc.nonce = []byte(nonce)\n\n\treturn c, nil\n}\n\nfunc (c *Cipher) Decrypt(ciphertext []byte) ([]byte, error) {\n\treturn c.gcm.Open(nil, c.iv, ciphertext, c.nonce)\n}\n\nfunc (c *Cipher) Encrypt(plaintext []byte) []byte {\n\treturn c.gcm.Seal(nil, c.iv, plaintext, c.nonce)\n}\n\nfunc InfoHash(share string) string {\n\thash := sha256.New()\n\thash.Write([]byte(share))\n\tsum := hash.Sum(nil)\n\treturn base64.StdEncoding.EncodeToString(sum)\n}\n"
  },
  {
    "path": "pkg/webtorrent/server.go",
    "content": "package webtorrent\n\nimport (\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/gorilla/websocket\"\n)\n\ntype Server struct {\n\tcore.Listener\n\n\tURL      string\n\tExchange func(src, offer string) (answer string, err error)\n\n\tshares   map[string]*Share\n\tmu       sync.Mutex\n\tannounce *core.Worker\n}\n\ntype Share struct {\n\tname string\n\tpwd  string\n\tsrc  string\n}\n\nfunc (s *Server) AddShare(name, pwd, src string) {\n\ts.mu.Lock()\n\n\tif s.shares == nil {\n\t\ts.shares = map[string]*Share{}\n\t}\n\n\tif len(s.shares) == 0 {\n\t\tgo s.Serve()\n\t}\n\n\thash := InfoHash(name)\n\ts.shares[hash] = &Share{\n\t\tname: name,\n\t\tpwd:  pwd,\n\t\tsrc:  src,\n\t}\n\n\ts.announce.Do()\n\n\ts.mu.Unlock()\n}\n\nfunc (s *Server) GetSharePwd(name string) (pwd string) {\n\thash := InfoHash(name)\n\ts.mu.Lock()\n\tif share, ok := s.shares[hash]; ok {\n\t\tpwd = share.pwd\n\t}\n\ts.mu.Unlock()\n\treturn\n}\n\nfunc (s *Server) RemoveShare(name string) {\n\thash := InfoHash(name)\n\ts.mu.Lock()\n\tif _, ok := s.shares[hash]; ok {\n\t\tdelete(s.shares, hash)\n\t}\n\ts.mu.Unlock()\n}\n\n// Serve - run reconnection loop, will exit on??\nfunc (s *Server) Serve() error {\n\tfor s.HasShares() {\n\t\ts.Fire(\"connect to tracker: \" + s.URL)\n\n\t\tws, _, err := websocket.DefaultDialer.Dial(s.URL, nil)\n\t\tif err != nil {\n\t\t\ts.Fire(err)\n\t\t\ttime.Sleep(time.Minute)\n\t\t\tcontinue\n\t\t}\n\n\t\tpeerID := core.RandString(16, 36)\n\n\t\t// instant run announce worker\n\t\ts.announce = core.NewWorker(0, func() time.Duration {\n\t\t\tif err = s.writer(ws, peerID); err != nil {\n\t\t\t\treturn 0\n\t\t\t}\n\t\t\treturn time.Minute\n\t\t})\n\n\t\t// run reader forewer\n\t\tfor {\n\t\t\tif err = s.reader(ws, peerID); err != nil {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\n\t\t// stop announcing worker\n\t\ts.announce.Stop()\n\n\t\t// ensure ws is stopped\n\t\t_ = ws.Close()\n\n\t\ttime.Sleep(time.Minute)\n\t}\n\n\ts.Fire(\"disconnect\")\n\n\treturn nil\n}\n\n// reader - receive offers in the loop, will exit on ws.Close\nfunc (s *Server) reader(ws *websocket.Conn, peerID string) error {\n\tvar v Message\n\tif err := ws.ReadJSON(&v); err != nil {\n\t\treturn err\n\t}\n\n\ts.Fire(&v)\n\n\ts.mu.Lock()\n\tshare, ok := s.shares[v.InfoHash]\n\ts.mu.Unlock()\n\n\t// skip any unknown shares\n\tif !ok || v.OfferId == \"\" || v.Offer == nil {\n\t\treturn nil\n\t}\n\n\ts.Fire(\"new offer: \" + v.OfferId)\n\n\tcipher, err := NewCipher(share.name, share.pwd, v.OfferId)\n\tif err != nil {\n\t\ts.Fire(err)\n\t\treturn nil\n\t}\n\n\tenc, err := base64.StdEncoding.DecodeString(v.Offer.SDP)\n\tif err != nil {\n\t\ts.Fire(err)\n\t\treturn nil\n\t}\n\n\toffer, err := cipher.Decrypt(enc)\n\tif err != nil {\n\t\ts.Fire(err)\n\t\treturn nil\n\t}\n\n\tanswer, err := s.Exchange(share.src, string(offer))\n\tif err != nil {\n\t\ts.Fire(err)\n\t\treturn nil\n\t}\n\n\tenc = cipher.Encrypt([]byte(answer))\n\n\traw := fmt.Sprintf(\n\t\t`{\"action\":\"announce\",\"info_hash\":\"%s\",\"peer_id\":\"%s\",\"offer_id\":\"%s\",\"answer\":{\"type\":\"answer\",\"sdp\":\"%s\"},\"to_peer_id\":\"%s\"}`,\n\t\tv.InfoHash, peerID, v.OfferId, base64.StdEncoding.EncodeToString(enc), v.PeerId,\n\t)\n\treturn ws.WriteMessage(websocket.TextMessage, []byte(raw))\n}\n\nfunc (s *Server) writer(ws *websocket.Conn, peerID string) error {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\n\tif len(s.shares) == 0 {\n\t\treturn ws.Close()\n\t}\n\n\tfor hash := range s.shares {\n\t\tmsg := fmt.Sprintf(\n\t\t\t`{\"action\":\"announce\",\"info_hash\":\"%s\",\"peer_id\":\"%s\",\"offers\":[],\"numwant\":10}`,\n\t\t\thash, peerID,\n\t\t)\n\t\tif err := ws.WriteMessage(websocket.TextMessage, []byte(msg)); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n\n\treturn nil\n}\n\nfunc (s *Server) HasShares() bool {\n\ts.mu.Lock()\n\tdefer s.mu.Unlock()\n\treturn len(s.shares) > 0\n}\n\ntype Message struct {\n\tAction   string `json:\"action\"`\n\tInfoHash string `json:\"info_hash\"`\n\n\t// Announce msg\n\tNumwant int    `json:\"numwant,omitempty\"`\n\tPeerId  string `json:\"peer_id,omitempty\"`\n\tOffers  []struct {\n\t\tOfferId string `json:\"offer_id\"`\n\t\tOffer   struct {\n\t\t\tType string `json:\"type\"`\n\t\t\tSDP  string `json:\"sdp\"`\n\t\t} `json:\"offer\"`\n\t} `json:\"offers,omitempty\"`\n\n\t// Interval msg\n\tInterval   int `json:\"interval,omitempty\"`\n\tComplete   int `json:\"complete,omitempty\"`\n\tIncomplete int `json:\"incomplete,omitempty\"`\n\n\t// Offer msg\n\tOfferId string `json:\"offer_id,omitempty\"`\n\tOffer   *struct {\n\t\tType string `json:\"type\"`\n\t\tSDP  string `json:\"sdp\"`\n\t} `json:\"offer,omitempty\"`\n\n\t// Answer msg\n\tToPeerId string `json:\"to_peer_id,omitempty\"`\n\tAnswer   *struct {\n\t\tType string `json:\"type\"`\n\t\tSDP  string `json:\"sdp\"`\n\t} `json:\"answer,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/wyoming/README.md",
    "content": "##  Default wake words\n\n- alexa_v0.1\n- hey_jarvis_v0.1\n- hey_mycroft_v0.1\n- hey_rhasspy_v0.1\n- ok_nabu_v0.1\n\n## Useful Links\n\n- https://github.com/rhasspy/wyoming-satellite\n- https://github.com/rhasspy/wyoming-openwakeword\n- https://github.com/fwartner/home-assistant-wakewords-collection\n- https://github.com/esphome/micro-wake-word-models/tree/main?tab=readme-ov-file\n"
  },
  {
    "path": "pkg/wyoming/api.go",
    "content": "package wyoming\n\nimport (\n\t\"bufio\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\ntype API struct {\n\tconn net.Conn\n\trd   *bufio.Reader\n}\n\nfunc DialAPI(address string) (*API, error) {\n\tconn, err := net.DialTimeout(\"tcp\", address, core.ConnDialTimeout)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn NewAPI(conn), nil\n}\n\nconst Version = \"1.5.4\"\n\nfunc NewAPI(conn net.Conn) *API {\n\treturn &API{conn: conn, rd: bufio.NewReader(conn)}\n}\n\nfunc (w *API) WriteEvent(evt *Event) (err error) {\n\thdr := EventHeader{\n\t\tType:          evt.Type,\n\t\tVersion:       Version,\n\t\tDataLength:    len(evt.Data),\n\t\tPayloadLength: len(evt.Payload),\n\t}\n\n\tbuf, err := json.Marshal(hdr)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tbuf = append(buf, '\\n')\n\tbuf = append(buf, evt.Data...)\n\tbuf = append(buf, evt.Payload...)\n\n\t_, err = w.conn.Write(buf)\n\treturn err\n}\n\nfunc (w *API) ReadEvent() (*Event, error) {\n\tdata, err := w.rd.ReadBytes('\\n')\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar hdr EventHeader\n\tif err = json.Unmarshal(data, &hdr); err != nil {\n\t\treturn nil, err\n\t}\n\n\tevt := Event{Type: hdr.Type}\n\n\tif hdr.DataLength > 0 {\n\t\tdata = make([]byte, hdr.DataLength)\n\t\tif _, err = io.ReadFull(w.rd, data); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tevt.Data = string(data)\n\t}\n\n\tif hdr.PayloadLength > 0 {\n\t\tevt.Payload = make([]byte, hdr.PayloadLength)\n\t\tif _, err = io.ReadFull(w.rd, evt.Payload); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\treturn &evt, nil\n}\n\nfunc (w *API) Close() error {\n\treturn w.conn.Close()\n}\n\ntype Event struct {\n\tType    string\n\tData    string\n\tPayload []byte\n}\n\ntype EventHeader struct {\n\tType          string `json:\"type\"`\n\tVersion       string `json:\"version\"`\n\tDataLength    int    `json:\"data_length,omitempty\"`\n\tPayloadLength int    `json:\"payload_length,omitempty\"`\n}\n"
  },
  {
    "path": "pkg/wyoming/backchannel.go",
    "content": "package wyoming\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Backchannel struct {\n\tcore.Connection\n\tapi *API\n}\n\nfunc newBackchannel(conn net.Conn) *Backchannel {\n\treturn &Backchannel{\n\t\tcore.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"wyoming\",\n\t\t\tMedias: []*core.Media{\n\t\t\t\t{\n\t\t\t\t\tKind:      core.KindAudio,\n\t\t\t\t\tDirection: core.DirectionSendonly,\n\t\t\t\t\tCodecs: []*core.Codec{\n\t\t\t\t\t\t{Name: core.CodecPCML, ClockRate: 22050},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tTransport: conn,\n\t\t},\n\t\tNewAPI(conn),\n\t}\n}\n\nfunc (b *Backchannel) GetTrack(media *core.Media, codec *core.Codec) (*core.Receiver, error) {\n\treturn nil, core.ErrCantGetTrack\n}\n\nfunc (b *Backchannel) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {\n\tsender := core.NewSender(media, codec)\n\tsender.Handler = func(pkt *rtp.Packet) {\n\t\tts := time.Now().Nanosecond()\n\t\tevt := &Event{\n\t\t\tType:    \"audio-chunk\",\n\t\t\tData:    fmt.Sprintf(`{\"rate\":22050,\"width\":2,\"channels\":1,\"timestamp\":%d}`, ts),\n\t\t\tPayload: pkt.Payload,\n\t\t}\n\t\t_ = b.api.WriteEvent(evt)\n\t}\n\tsender.HandleRTP(track)\n\tb.Senders = append(b.Senders, sender)\n\treturn nil\n}\n\nfunc (b *Backchannel) Start() error {\n\tfor {\n\t\tif _, err := b.api.ReadEvent(); err != nil {\n\t\t\treturn err\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/wyoming/expr.go",
    "content": "package wyoming\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"os\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/expr\"\n\t\"github.com/AlexxIT/go2rtc/pkg/wav\"\n)\n\ntype env struct {\n\t*satellite\n\tType string\n\tData string\n}\n\nfunc (s *satellite) handleEvent(evt *Event) {\n\tswitch evt.Type {\n\tcase \"describe\":\n\t\t// {\"asr\": [], \"tts\": [], \"handle\": [], \"intent\": [], \"wake\": [], \"satellite\": {\"name\": \"my satellite\", \"attribution\": {\"name\": \"\", \"url\": \"\"}, \"installed\": true, \"description\": \"my satellite\", \"version\": \"1.4.1\", \"area\": null, \"snd_format\": null}}\n\t\tdata := fmt.Sprintf(`{\"satellite\":{\"name\":%q,\"attribution\":{\"name\":\"go2rtc\",\"url\":\"https://github.com/AlexxIT/go2rtc\"},\"installed\":true}}`, s.srv.Name)\n\t\ts.WriteEvent(\"info\", data)\n\tcase \"run-satellite\":\n\t\ts.Detect()\n\tcase \"pause-satellite\":\n\t\ts.Stop()\n\tcase \"detect\": // WAKE_WORD_START {\"names\": null}\n\tcase \"detection\": // WAKE_WORD_END {\"name\": \"ok_nabu_v0.1\", \"timestamp\": 17580, \"speaker\": null}\n\tcase \"transcribe\": // STT_START {\"language\": \"en\"}\n\tcase \"voice-started\": // STT_VAD_START {\"timestamp\": 1160}\n\tcase \"voice-stopped\": // STT_VAD_END {\"timestamp\": 2470}\n\t\ts.Pause()\n\tcase \"transcript\": // STT_END {\"text\": \"how are you\"}\n\tcase \"synthesize\": // TTS_START {\"text\": \"Sorry, I couldn't understand that\", \"voice\": {\"language\": \"en\"}}\n\tcase \"audio-start\": // TTS_END {\"rate\": 22050, \"width\": 2, \"channels\": 1, \"timestamp\": 0}\n\tcase \"audio-chunk\": // {\"rate\": 22050, \"width\": 2, \"channels\": 1, \"timestamp\": 0}\n\tcase \"audio-stop\": // {\"timestamp\": 2.880000000000002}\n\t\t// run async because PlayAudio takes some time\n\t\tgo func() {\n\t\t\ts.PlayAudio()\n\t\t\ts.WriteEvent(\"played\")\n\t\t\ts.Detect()\n\t\t}()\n\tcase \"error\":\n\t\ts.Detect()\n\tcase \"internal-run\":\n\t\ts.WriteEvent(\"run-pipeline\", `{\"start_stage\":\"wake\",\"end_stage\":\"tts\"}`)\n\t\ts.Stream()\n\tcase \"internal-detection\":\n\t\ts.WriteEvent(\"run-pipeline\", `{\"start_stage\":\"asr\",\"end_stage\":\"tts\"}`)\n\t\ts.Stream()\n\t}\n}\n\nfunc (s *satellite) handleScript(evt *Event) {\n\tvar script string\n\tif s.srv.Event != nil {\n\t\tscript = s.srv.Event[evt.Type]\n\t}\n\n\ts.srv.Trace(\"event=%s data=%s payload size=%d\", evt.Type, evt.Data, len(evt.Payload))\n\n\tif script == \"\" {\n\t\ts.handleEvent(evt)\n\t\treturn\n\t}\n\n\t// run async because script can have sleeps\n\tgo func() {\n\t\te := &env{satellite: s, Type: evt.Type, Data: evt.Data}\n\t\tif res, err := expr.Eval(script, e); err != nil {\n\t\t\ts.srv.Trace(\"event=%s expr error=%s\", evt.Type, err)\n\t\t\ts.handleEvent(evt)\n\t\t} else {\n\t\t\ts.srv.Trace(\"event=%s expr result=%v\", evt.Type, res)\n\t\t}\n\t}()\n}\n\nfunc (s *satellite) Detect() bool {\n\treturn s.setMicState(stateWaitVAD)\n}\n\nfunc (s *satellite) Stream() bool {\n\treturn s.setMicState(stateActive)\n}\n\nfunc (s *satellite) Pause() bool {\n\treturn s.setMicState(stateIdle)\n}\n\nfunc (s *satellite) Stop() bool {\n\ts.micStop()\n\treturn true\n}\n\nfunc (s *satellite) WriteEvent(args ...string) bool {\n\tif len(args) == 0 {\n\t\treturn false\n\t}\n\tevt := &Event{Type: args[0]}\n\tif len(args) > 1 {\n\t\tevt.Data = args[1]\n\t}\n\tif err := s.api.WriteEvent(evt); err != nil {\n\t\treturn false\n\t}\n\treturn true\n}\n\nfunc (s *satellite) PlayAudio() bool {\n\treturn s.playAudio(sndCodec, bytes.NewReader(s.sndAudio))\n}\n\nfunc (s *satellite) PlayFile(path string) bool {\n\tf, err := os.Open(path)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\tcodec, err := wav.ReadHeader(f)\n\tif err != nil {\n\t\treturn false\n\t}\n\n\treturn s.playAudio(codec, f)\n}\n\nfunc (e *env) Sleep(s string) bool {\n\td, err := time.ParseDuration(s)\n\tif err != nil {\n\t\treturn false\n\t}\n\ttime.Sleep(d)\n\treturn true\n}\n"
  },
  {
    "path": "pkg/wyoming/mic.go",
    "content": "package wyoming\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\nfunc (s *Server) HandleMic(conn net.Conn) {\n\tdefer conn.Close()\n\n\tvar closed core.Waiter\n\tvar timestamp int\n\n\tapi := NewAPI(conn)\n\tmic := newMicConsumer(func(chunk []byte) {\n\t\tdata := fmt.Sprintf(`{\"rate\":16000,\"width\":2,\"channels\":1,\"timestamp\":%d}`, timestamp)\n\t\tevt := &Event{Type: \"audio-chunk\", Data: data, Payload: chunk}\n\t\tif err := api.WriteEvent(evt); err != nil {\n\t\t\tclosed.Done(nil)\n\t\t}\n\n\t\ttimestamp += len(chunk) / 2\n\t})\n\tmic.RemoteAddr = api.conn.RemoteAddr().String()\n\n\tif err := s.MicHandler(mic); err != nil {\n\t\ts.Error(\"mic error: %s\", err)\n\t\treturn\n\t}\n\n\t_ = closed.Wait()\n\t_ = mic.Stop()\n}\n"
  },
  {
    "path": "pkg/wyoming/producer.go",
    "content": "package wyoming\n\nimport (\n\t\"net\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Producer struct {\n\tcore.Connection\n\tapi *API\n}\n\nfunc newProducer(conn net.Conn) *Producer {\n\treturn &Producer{\n\t\tcore.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"wyoming\",\n\t\t\tMedias: []*core.Media{\n\t\t\t\t{\n\t\t\t\t\tKind:      core.KindAudio,\n\t\t\t\t\tDirection: core.DirectionRecvonly,\n\t\t\t\t\tCodecs: []*core.Codec{\n\t\t\t\t\t\t{Name: core.CodecPCML, ClockRate: 16000},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t\tTransport: conn,\n\t\t},\n\t\tNewAPI(conn),\n\t}\n}\n\nfunc (p *Producer) Start() error {\n\tvar seq uint16\n\tvar ts uint32\n\n\tfor {\n\t\tevt, err := p.api.ReadEvent()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tif evt.Type != \"audio-chunk\" {\n\t\t\tcontinue\n\t\t}\n\n\t\tp.Recv += len(evt.Payload)\n\n\t\tpkt := &core.Packet{\n\t\t\tHeader: rtp.Header{\n\t\t\t\tVersion:        2,\n\t\t\t\tMarker:         true,\n\t\t\t\tSequenceNumber: seq,\n\t\t\t\tTimestamp:      ts,\n\t\t\t},\n\t\t\tPayload: evt.Payload,\n\t\t}\n\t\tp.Receivers[0].WriteRTP(pkt)\n\n\t\tseq++\n\t\tts += uint32(len(evt.Payload) / 2)\n\t}\n}\n"
  },
  {
    "path": "pkg/wyoming/satellite.go",
    "content": "package wyoming\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"sync\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/pcm\"\n\t\"github.com/AlexxIT/go2rtc/pkg/pcm/s16le\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Server struct {\n\tName  string\n\tEvent map[string]string\n\n\tVADThreshold int16\n\tWakeURI      string\n\n\tMicHandler func(cons core.Consumer) error\n\tSndHandler func(prod core.Producer) error\n\n\tTrace func(format string, v ...any)\n\tError func(format string, v ...any)\n}\n\nfunc (s *Server) Serve(l net.Listener) error {\n\tfor {\n\t\tconn, err := l.Accept()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tgo s.Handle(conn)\n\t}\n}\n\nfunc (s *Server) Handle(conn net.Conn) {\n\tapi := NewAPI(conn)\n\tsat := newSatellite(api, s)\n\tdefer sat.Close()\n\n\tfor {\n\t\tevt, err := api.ReadEvent()\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\n\t\tswitch evt.Type {\n\t\tcase \"ping\": // {\"text\": null}\n\t\t\t_ = api.WriteEvent(&Event{Type: \"pong\", Data: evt.Data})\n\t\tcase \"audio-start\": // TTS_END {\"rate\": 22050, \"width\": 2, \"channels\": 1, \"timestamp\": 0}\n\t\t\tsat.sndAudio = sat.sndAudio[:0]\n\t\tcase \"audio-chunk\": // {\"rate\": 22050, \"width\": 2, \"channels\": 1, \"timestamp\": 0}\n\t\t\tsat.sndAudio = append(sat.sndAudio, evt.Payload...)\n\t\tdefault:\n\t\t\tsat.handleScript(evt)\n\t\t}\n\t}\n}\n\n// states like http.ConnState\nconst (\n\tstateError        = -2\n\tstateClosed       = -1\n\tstateNew          = 0\n\tstateIdle         = 1\n\tstateWaitVAD      = 2 // aka wait VAD\n\tstateWaitWakeWord = 3\n\tstateActive       = 4\n)\n\ntype satellite struct {\n\tapi *API\n\tsrv *Server\n\n\tmicState int8\n\tmicTS    int\n\tmicMu    sync.Mutex\n\tsndAudio []byte\n\n\tmic  *micConsumer\n\twake *WakeWord\n}\n\nfunc newSatellite(api *API, srv *Server) *satellite {\n\tsat := &satellite{api: api, srv: srv}\n\treturn sat\n}\n\nfunc (s *satellite) Close() error {\n\ts.Stop()\n\treturn s.api.Close()\n}\n\nconst wakeTimeout = 5 * 2 * 16000 // 5 seconds\n\nfunc (s *satellite) setMicState(state int8) bool {\n\ts.micMu.Lock()\n\tdefer s.micMu.Unlock()\n\n\tif s.micState == stateNew {\n\t\ts.mic = newMicConsumer(s.onMicChunk)\n\t\ts.mic.RemoteAddr = s.api.conn.RemoteAddr().String()\n\t\tif err := s.srv.MicHandler(s.mic); err != nil {\n\t\t\ts.micState = stateError\n\t\t\ts.srv.Error(\"can't get mic: %w\", err)\n\t\t\t_ = s.api.Close()\n\t\t} else {\n\t\t\ts.micState = stateIdle\n\t\t}\n\t}\n\n\tif s.micState < stateIdle {\n\t\treturn false\n\t}\n\n\ts.micState = state\n\ts.micTS = 0\n\treturn true\n}\n\nfunc (s *satellite) micStop() {\n\ts.micMu.Lock()\n\n\ts.micState = stateClosed\n\tif s.mic != nil {\n\t\t_ = s.mic.Stop()\n\t\ts.mic = nil\n\t}\n\tif s.wake != nil {\n\t\t_ = s.wake.Close()\n\t\ts.wake = nil\n\t}\n\n\ts.micMu.Unlock()\n}\n\nfunc (s *satellite) onMicChunk(chunk []byte) {\n\ts.micMu.Lock()\n\tdefer s.micMu.Unlock()\n\n\tif s.micState == stateIdle {\n\t\treturn\n\t}\n\n\tif s.micState == stateWaitVAD {\n\t\t// tests show that values over 1000 are most likely speech\n\t\tif s.srv.VADThreshold == 0 || s16le.PeaksRMS(chunk) > s.srv.VADThreshold {\n\t\t\tif s.wake == nil && s.srv.WakeURI != \"\" {\n\t\t\t\ts.wake, _ = DialWakeWord(s.srv.WakeURI)\n\t\t\t}\n\t\t\tif s.wake == nil {\n\t\t\t\t// some problems with wake word - redirect to HA\n\t\t\t\ts.micState = stateIdle\n\t\t\t\tgo s.handleScript(&Event{Type: \"internal-run\"})\n\t\t\t} else {\n\t\t\t\ts.micState = stateWaitWakeWord\n\t\t\t}\n\t\t\ts.micTS = 0\n\t\t}\n\t}\n\n\tif s.micState == stateWaitWakeWord {\n\t\tif s.wake.Detection != \"\" {\n\t\t\t// check if wake word detected\n\t\t\ts.micState = stateIdle\n\t\t\tgo s.handleScript(&Event{Type: \"internal-detection\", Data: `{\"name\":\"` + s.wake.Detection + `\"}`})\n\t\t} else if err := s.wake.WriteChunk(chunk); err != nil {\n\t\t\t// wake word service failed\n\t\t\ts.micState = stateWaitVAD\n\t\t\t_ = s.wake.Close()\n\t\t\ts.wake = nil\n\t\t} else if s.micTS > wakeTimeout {\n\t\t\t// wake word detection timeout\n\t\t\ts.micState = stateWaitVAD\n\t\t}\n\t} else if s.wake != nil {\n\t\t_ = s.wake.Close()\n\t\ts.wake = nil\n\t}\n\n\tif s.micState == stateActive {\n\t\tdata := fmt.Sprintf(`{\"rate\":16000,\"width\":2,\"channels\":1,\"timestamp\":%d}`, s.micTS)\n\t\tevt := &Event{Type: \"audio-chunk\", Data: data, Payload: chunk}\n\t\t_ = s.api.WriteEvent(evt)\n\t}\n\n\ts.micTS += len(chunk) / 2\n}\n\nfunc (s *satellite) playAudio(codec *core.Codec, rd io.Reader) bool {\n\tctx, cancel := context.WithCancel(context.Background())\n\tdefer cancel()\n\n\tprod := pcm.OpenSync(codec, rd)\n\tprod.OnClose(cancel)\n\n\tif err := s.srv.SndHandler(prod); err != nil {\n\t\treturn false\n\t} else {\n\t\t<-ctx.Done()\n\t\treturn true\n\t}\n}\n\ntype micConsumer struct {\n\tcore.Connection\n\tonData  func(chunk []byte)\n\tonClose func()\n}\n\nfunc newMicConsumer(onData func(chunk []byte)) *micConsumer {\n\tmedias := []*core.Media{\n\t\t{\n\t\t\tKind:      core.KindAudio,\n\t\t\tDirection: core.DirectionSendonly,\n\t\t\tCodecs:    pcm.ConsumerCodecs(),\n\t\t},\n\t}\n\n\treturn &micConsumer{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"wyoming\",\n\t\t\tProtocol:   \"tcp\",\n\t\t\tMedias:     medias,\n\t\t},\n\t\tonData: onData,\n\t}\n}\n\nfunc (c *micConsumer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {\n\tsrc := track.Codec\n\tdst := &core.Codec{\n\t\tName:      core.CodecPCML,\n\t\tClockRate: 16000,\n\t\tChannels:  1,\n\t}\n\tsender := core.NewSender(media, dst)\n\tsender.Handler = pcm.TranscodeHandler(dst, src,\n\t\trepack(func(packet *core.Packet) {\n\t\t\tc.onData(packet.Payload)\n\t\t}),\n\t)\n\tsender.HandleRTP(track)\n\tc.Senders = append(c.Senders, sender)\n\treturn nil\n}\n\nfunc (c *micConsumer) Stop() error {\n\tif c.onClose != nil {\n\t\tc.onClose()\n\t}\n\treturn c.Connection.Stop()\n}\n\nfunc repack(handler core.HandlerFunc) core.HandlerFunc {\n\tconst PacketSize = 2 * 16000 / 50 // 20ms\n\n\tvar buf []byte\n\n\treturn func(pkt *rtp.Packet) {\n\t\tbuf = append(buf, pkt.Payload...)\n\n\t\tfor len(buf) >= PacketSize {\n\t\t\tpkt = &core.Packet{Payload: buf[:PacketSize]}\n\t\t\tbuf = buf[PacketSize:]\n\t\t\thandler(pkt)\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/wyoming/snd.go",
    "content": "package wyoming\n\nimport (\n\t\"bytes\"\n\t\"net\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/pcm\"\n)\n\nfunc (s *Server) HandleSnd(conn net.Conn) {\n\tdefer conn.Close()\n\n\tvar snd []byte\n\n\tapi := NewAPI(conn)\n\tfor {\n\t\tevt, err := api.ReadEvent()\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\n\t\ts.Trace(\"event: %s data: %s payload: %d\", evt.Type, evt.Data, len(evt.Payload))\n\n\t\tswitch evt.Type {\n\t\tcase \"audio-start\":\n\t\t\tsnd = snd[:0]\n\t\tcase \"audio-chunk\":\n\t\t\tsnd = append(snd, evt.Payload...)\n\t\tcase \"audio-stop\":\n\t\t\tprod := pcm.OpenSync(sndCodec, bytes.NewReader(snd))\n\t\t\tif err = s.SndHandler(prod); err != nil {\n\t\t\t\ts.Error(\"snd error: %s\", err)\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n}\n\nvar sndCodec = &core.Codec{Name: core.CodecPCML, ClockRate: 22050}\n"
  },
  {
    "path": "pkg/wyoming/wakeword.go",
    "content": "package wyoming\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/url\"\n)\n\ntype WakeWord struct {\n\t*API\n\tnames []string\n\tsend  int\n\n\tDetection string\n}\n\nfunc DialWakeWord(rawURL string) (*WakeWord, error) {\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tapi, err := DialAPI(u.Host)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tnames := u.Query()[\"name\"]\n\tif len(names) == 0 {\n\t\tnames = []string{\"ok_nabu_v0.1\"}\n\t}\n\n\twake := &WakeWord{API: api, names: names}\n\tif err = wake.Start(); err != nil {\n\t\t_ = wake.Close()\n\t\treturn nil, err\n\t}\n\n\tgo wake.handle()\n\treturn wake, nil\n}\n\nfunc (w *WakeWord) handle() {\n\tdefer w.Close()\n\n\tfor {\n\t\tevt, err := w.ReadEvent()\n\t\tif err != nil {\n\t\t\treturn\n\t\t}\n\n\t\tif evt.Type == \"detection\" {\n\t\t\tvar data struct {\n\t\t\t\tName string `json:\"name\"`\n\t\t\t}\n\t\t\tif err = json.Unmarshal([]byte(evt.Data), &data); err != nil {\n\t\t\t\treturn\n\t\t\t}\n\t\t\tw.Detection = data.Name\n\t\t}\n\t}\n}\n\n//func (w *WakeWord) Describe() error {\n//\tif err := w.WriteEvent(&Event{Type: \"describe\"}); err != nil {\n//\t\treturn err\n//\t}\n//\n//\tevt, err := w.ReadEvent()\n//\tif err != nil {\n//\t\treturn err\n//\t}\n//\n//\tvar info struct {\n//\t\tWake []struct {\n//\t\t\tModels []struct {\n//\t\t\t\tName string `json:\"name\"`\n//\t\t\t} `json:\"models\"`\n//\t\t} `json:\"wake\"`\n//\t}\n//\tif err = json.Unmarshal(evt.Data, &info); err != nil {\n//\t\treturn err\n//\t}\n//\n//\treturn nil\n//}\n\nfunc (w *WakeWord) Start() error {\n\tmsg := struct {\n\t\tNames []string `json:\"names\"`\n\t}{\n\t\tNames: w.names,\n\t}\n\tdata, err := json.Marshal(msg)\n\tif err != nil {\n\t\treturn err\n\t}\n\tevt := &Event{Type: \"detect\", Data: string(data)}\n\tif err := w.WriteEvent(evt); err != nil {\n\t\treturn err\n\t}\n\n\tevt = &Event{Type: \"audio-start\", Data: audioData(0)}\n\treturn w.WriteEvent(evt)\n}\n\nfunc (w *WakeWord) Close() error {\n\treturn w.conn.Close()\n}\n\nfunc (w *WakeWord) WriteChunk(payload []byte) error {\n\tevt := &Event{Type: \"audio-chunk\", Data: audioData(w.send), Payload: payload}\n\tw.send += len(payload)\n\treturn w.WriteEvent(evt)\n}\n\nfunc audioData(send int) string {\n\t// timestamp in ms = send / 2 * 1000 / 16000 = send / 32\n\treturn fmt.Sprintf(`{\"rate\":16000,\"width\":2,\"channels\":1,\"timestamp\":%d}`, send/32)\n}\n"
  },
  {
    "path": "pkg/wyoming/wyoming.go",
    "content": "package wyoming\n\nimport (\n\t\"net\"\n\t\"net/url\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\nfunc Dial(rawURL string) (core.Producer, error) {\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tconn, err := net.DialTimeout(\"tcp\", u.Host, core.ConnDialTimeout)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif u.Query().Get(\"backchannel\") != \"1\" {\n\t\treturn newProducer(conn), nil\n\t} else {\n\t\treturn newBackchannel(conn), nil\n\t}\n}\n"
  },
  {
    "path": "pkg/wyze/backchannel.go",
    "content": "package wyze\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/aac\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/tutk\"\n\t\"github.com/pion/rtp\"\n)\n\nfunc (p *Producer) AddTrack(media *core.Media, codec *core.Codec, track *core.Receiver) error {\n\tif err := p.client.StartIntercom(); err != nil {\n\t\treturn fmt.Errorf(\"wyze: failed to enable intercom: %w\", err)\n\t}\n\n\t// Get the camera's audio codec info (what it sent us = what it accepts)\n\ttutkCodec, sampleRate, channels := p.client.GetBackchannelCodec()\n\tif tutkCodec == 0 {\n\t\treturn fmt.Errorf(\"wyze: no audio codec detected from camera\")\n\t}\n\n\tif p.client.verbose {\n\t\tfmt.Printf(\"[Wyze] Intercom enabled, using codec=0x%04x rate=%d ch=%d\\n\", tutkCodec, sampleRate, channels)\n\t}\n\n\tsender := core.NewSender(media, track.Codec)\n\n\t// Track our own timestamp - camera expects timestamps starting from 0\n\t// and incrementing by frame duration in microseconds\n\tvar timestamp uint32 = 0\n\tsamplesPerFrame := tutk.GetSamplesPerFrame(tutkCodec)\n\tframeDurationUS := samplesPerFrame * 1000000 / sampleRate\n\n\tsender.Handler = func(pkt *rtp.Packet) {\n\t\tif err := p.client.WriteAudio(tutkCodec, pkt.Payload, timestamp, sampleRate, channels); err == nil {\n\t\t\tp.Send += len(pkt.Payload)\n\t\t}\n\t\ttimestamp += frameDurationUS\n\t}\n\n\tswitch track.Codec.Name {\n\tcase core.CodecAAC:\n\t\tif track.Codec.IsRTP() {\n\t\t\tsender.Handler = aac.RTPToADTS(codec, sender.Handler)\n\t\t} else {\n\t\t\tsender.Handler = aac.EncodeToADTS(codec, sender.Handler)\n\t\t}\n\t}\n\n\tsender.HandleRTP(track)\n\tp.Senders = append(p.Senders, sender)\n\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/wyze/client.go",
    "content": "package wyze\n\nimport (\n\t\"crypto/rand\"\n\t\"encoding/binary\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/tutk\"\n\t\"github.com/AlexxIT/go2rtc/pkg/tutk/dtls\"\n)\n\nconst (\n\tFrameSize1080P      = 0\n\tFrameSize360P       = 1\n\tFrameSize720P       = 2\n\tFrameSize2K         = 3\n\tFrameSizeFloodlight = 4\n)\n\nconst (\n\tBitrateMax uint16 = 0xF0\n\tBitrateSD  uint16 = 0x3C\n)\n\nconst (\n\tMediaTypeVideo       = 1\n\tMediaTypeAudio       = 2\n\tMediaTypeReturnAudio = 3\n\tMediaTypeRDT         = 4\n)\n\nconst (\n\tKCmdAuth               = 10000\n\tKCmdChallenge          = 10001\n\tKCmdChallengeResp      = 10002\n\tKCmdAuthResult         = 10003\n\tKCmdControlChannel     = 10010\n\tKCmdControlChannelResp = 10011\n\tKCmdSetResolutionDB    = 10052\n\tKCmdSetResolutionDBRes = 10053\n\tKCmdSetResolution      = 10056\n\tKCmdSetResolutionResp  = 10057\n)\n\ntype Client struct {\n\tconn *dtls.DTLSConn\n\n\thost  string\n\tuid   string\n\tenr   string\n\tmac   string\n\tmodel string\n\n\tauthKey string\n\tverbose bool\n\n\tclosed  bool\n\tcloseMu sync.Mutex\n\n\thasAudio    bool\n\thasIntercom bool\n\n\taudioCodecID    byte\n\taudioSampleRate uint32\n\taudioChannels   uint8\n}\n\ntype AuthResponse struct {\n\tConnectionRes string         `json:\"connectionRes\"`\n\tCameraInfo    map[string]any `json:\"cameraInfo\"`\n}\n\nfunc Dial(rawURL string) (*Client, error) {\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"wyze: invalid URL: %w\", err)\n\t}\n\n\tquery := u.Query()\n\n\tif query.Get(\"dtls\") != \"true\" {\n\t\treturn nil, fmt.Errorf(\"wyze: only DTLS cameras are supported\")\n\t}\n\n\tc := &Client{\n\t\thost:    u.Host,\n\t\tuid:     query.Get(\"uid\"),\n\t\tenr:     query.Get(\"enr\"),\n\t\tmac:     query.Get(\"mac\"),\n\t\tmodel:   query.Get(\"model\"),\n\t\tverbose: query.Get(\"verbose\") == \"true\",\n\t}\n\n\tc.authKey = string(dtls.CalculateAuthKey(c.enr, c.mac))\n\n\tif c.verbose {\n\t\tfmt.Printf(\"[Wyze] Connecting to %s (UID: %s)\\n\", c.host, c.uid)\n\t}\n\n\tif err := c.connect(); err != nil {\n\t\tc.Close()\n\t\treturn nil, err\n\t}\n\n\tif err := c.doAVLogin(); err != nil {\n\t\tc.Close()\n\t\treturn nil, err\n\t}\n\n\tif err := c.doKAuth(); err != nil {\n\t\tc.Close()\n\t\treturn nil, err\n\t}\n\n\tif c.verbose {\n\t\tfmt.Printf(\"[Wyze] Connection established\\n\")\n\t}\n\n\treturn c, nil\n}\n\nfunc (c *Client) SupportsAudio() bool {\n\treturn c.hasAudio\n}\n\nfunc (c *Client) SupportsIntercom() bool {\n\treturn c.hasIntercom\n}\n\nfunc (c *Client) SetBackchannelCodec(codecID byte, sampleRate uint32, channels uint8) {\n\tc.audioCodecID = codecID\n\tc.audioSampleRate = sampleRate\n\tc.audioChannels = channels\n}\n\nfunc (c *Client) GetBackchannelCodec() (codecID byte, sampleRate uint32, channels uint8) {\n\treturn c.audioCodecID, c.audioSampleRate, c.audioChannels\n}\n\nfunc (c *Client) SetResolution(quality byte) error {\n\tvar frameSize uint8\n\tvar bitrate uint16\n\n\tswitch quality {\n\tcase 0: // Auto/HD - use model's best\n\t\tframeSize = c.hdFrameSize()\n\t\tbitrate = BitrateMax\n\tcase FrameSize360P: // 1 = SD/360P\n\t\tframeSize = FrameSize360P\n\t\tbitrate = BitrateSD\n\tcase FrameSize720P: // 2 = 720P\n\t\tframeSize = FrameSize720P\n\t\tbitrate = BitrateMax\n\tcase FrameSize2K: // 3 = 2K\n\t\tif c.is2K() {\n\t\t\tframeSize = FrameSize2K\n\t\t} else {\n\t\t\tframeSize = c.hdFrameSize()\n\t\t}\n\t\tbitrate = BitrateMax\n\tcase FrameSizeFloodlight: // 4 = Floodlight\n\t\tframeSize = c.hdFrameSize()\n\t\tbitrate = BitrateMax\n\tdefault:\n\t\tframeSize = quality\n\t\tbitrate = BitrateMax\n\t}\n\n\tif c.verbose {\n\t\tfmt.Printf(\"[Wyze] SetResolution: quality=%d frameSize=%d bitrate=%d model=%s\\n\", quality, frameSize, bitrate, c.model)\n\t}\n\n\t// Use K10052 (doorbell format) for certain models\n\tif c.useDoorbellResolution() {\n\t\tk10052 := c.buildK10052(frameSize, bitrate)\n\t\t_, err := c.conn.WriteAndWaitIOCtrl(k10052, c.matchHL(KCmdSetResolutionDBRes), 5*time.Second)\n\t\treturn err\n\t}\n\n\tk10056 := c.buildK10056(frameSize, bitrate)\n\t_, err := c.conn.WriteAndWaitIOCtrl(k10056, c.matchHL(KCmdSetResolutionResp), 5*time.Second)\n\treturn err\n}\n\nfunc (c *Client) StartVideo() error {\n\tk10010 := c.buildK10010(MediaTypeVideo, true)\n\t_, err := c.conn.WriteAndWaitIOCtrl(k10010, c.matchHL(KCmdControlChannelResp), 5*time.Second)\n\treturn err\n}\n\nfunc (c *Client) StartAudio() error {\n\tk10010 := c.buildK10010(MediaTypeAudio, true)\n\t_, err := c.conn.WriteAndWaitIOCtrl(k10010, c.matchHL(KCmdControlChannelResp), 5*time.Second)\n\treturn err\n}\n\nfunc (c *Client) StartIntercom() error {\n\tif c.conn == nil {\n\t\treturn fmt.Errorf(\"connection is nil\")\n\t}\n\n\tif c.conn.IsBackchannelReady() {\n\t\treturn nil\n\t}\n\n\tk10010 := c.buildK10010(MediaTypeReturnAudio, true)\n\tif _, err := c.conn.WriteAndWaitIOCtrl(k10010, c.matchHL(KCmdControlChannelResp), 5*time.Second); err != nil {\n\t\treturn fmt.Errorf(\"enable return audio: %w\", err)\n\t}\n\n\tif c.verbose {\n\t\tfmt.Printf(\"[Wyze] Speaker channel enabled, waiting for readiness...\\n\")\n\t}\n\n\treturn c.conn.AVServStart()\n}\n\nfunc (c *Client) StopIntercom() error {\n\tif c.conn == nil || !c.conn.IsBackchannelReady() {\n\t\treturn nil\n\t}\n\n\tk10010 := c.buildK10010(MediaTypeReturnAudio, false)\n\tc.conn.WriteIOCtrl(k10010)\n\n\treturn c.conn.AVServStop()\n}\n\nfunc (c *Client) ReadPacket() (*tutk.Packet, error) {\n\treturn c.conn.AVRecvFrameData()\n}\n\nfunc (c *Client) WriteAudio(codec byte, payload []byte, timestamp uint32, sampleRate uint32, channels uint8) error {\n\tif !c.conn.IsBackchannelReady() {\n\t\treturn fmt.Errorf(\"speaker channel not connected\")\n\t}\n\n\tif c.verbose {\n\t\tfmt.Printf(\"[Wyze] WriteAudio: codec=0x%02x, payload=%d bytes, rate=%d, ch=%d\\n\", codec, len(payload), sampleRate, channels)\n\t}\n\n\treturn c.conn.AVSendAudioData(codec, payload, timestamp, sampleRate, channels)\n}\n\nfunc (c *Client) SetDeadline(t time.Time) error {\n\tif c.conn != nil {\n\t\treturn c.conn.SetDeadline(t)\n\t}\n\treturn nil\n}\n\nfunc (c *Client) Protocol() string {\n\treturn \"wyze/dtls\"\n}\n\nfunc (c *Client) RemoteAddr() net.Addr {\n\tif c.conn != nil {\n\t\treturn c.conn.RemoteAddr()\n\t}\n\treturn nil\n}\n\nfunc (c *Client) Close() error {\n\tc.closeMu.Lock()\n\tif c.closed {\n\t\tc.closeMu.Unlock()\n\t\treturn nil\n\t}\n\tc.closed = true\n\tc.closeMu.Unlock()\n\n\tif c.verbose {\n\t\tfmt.Printf(\"[Wyze] Closing connection\\n\")\n\t}\n\n\tc.StopIntercom()\n\n\tif c.conn != nil {\n\t\tc.conn.Close()\n\t}\n\n\tif c.verbose {\n\t\tfmt.Printf(\"[Wyze] Connection closed\\n\")\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) connect() error {\n\thost := c.host\n\tport := 0\n\n\tif idx := strings.Index(host, \":\"); idx > 0 {\n\t\tif p, err := strconv.Atoi(host[idx+1:]); err == nil {\n\t\t\tport = p\n\t\t}\n\t\thost = host[:idx]\n\t}\n\n\tconn, err := dtls.DialDTLS(host, port, c.uid, c.authKey, c.enr, c.verbose)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"wyze: connect failed: %w\", err)\n\t}\n\n\tc.conn = conn\n\tif c.verbose {\n\t\tfmt.Printf(\"[Wyze] Connected to %s (IOTC + DTLS)\\n\", conn.RemoteAddr())\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) doAVLogin() error {\n\tif c.verbose {\n\t\tfmt.Printf(\"[Wyze] Sending AV Login\\n\")\n\t}\n\n\tif err := c.conn.AVClientStart(5 * time.Second); err != nil {\n\t\treturn fmt.Errorf(\"wyze: av login failed: %w\", err)\n\t}\n\n\tif c.verbose {\n\t\tfmt.Printf(\"[Wyze] AV Login response received\\n\")\n\t}\n\treturn nil\n}\n\nfunc (c *Client) doKAuth() error {\n\t// Step 1: K10000 -> K10001 (Challenge)\n\tdata, err := c.conn.WriteAndWaitIOCtrl(c.buildK10000(), c.matchHL(KCmdChallenge), 5*time.Second)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"wyze: K10001 failed: %w\", err)\n\t}\n\n\thlData := c.extractHL(data)\n\tchallenge, status, err := c.parseK10001(hlData)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"wyze: K10001 parse failed: %w\", err)\n\t}\n\n\tif c.verbose {\n\t\tfmt.Printf(\"[Wyze] K10001 challenge received, status=%d\\n\", status)\n\t}\n\n\t// Step 2: K10002 -> K10003 (Auth)\n\tdata, err = c.conn.WriteAndWaitIOCtrl(c.buildK10002(challenge, status), c.matchHL(KCmdAuthResult), 5*time.Second)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"wyze: K10002 failed: %w\", err)\n\t}\n\thlData = c.extractHL(data)\n\n\t// Parse K10003 response\n\tauthResp, err := c.parseK10003(hlData)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"wyze: K10003 parse failed: %w\", err)\n\t}\n\n\tif c.verbose && authResp != nil {\n\t\tif jsonBytes, err := json.MarshalIndent(authResp, \"\", \"  \"); err == nil {\n\t\t\tfmt.Printf(\"[Wyze] K10003 response:\\n%s\\n\", jsonBytes)\n\t\t}\n\t}\n\n\t// Extract audio capability from cameraInfo\n\tif authResp != nil && authResp.CameraInfo != nil {\n\t\tif channelResult, ok := authResp.CameraInfo[\"channelRequestResult\"].(map[string]any); ok {\n\t\t\tif audio, ok := channelResult[\"audio\"].(string); ok {\n\t\t\t\tc.hasAudio = audio == \"1\"\n\t\t\t} else {\n\t\t\t\tc.hasAudio = true\n\t\t\t}\n\t\t} else {\n\t\t\tc.hasAudio = true\n\t\t}\n\t} else {\n\t\tc.hasAudio = true\n\t}\n\n\tif c.verbose {\n\t\tfmt.Printf(\"[Wyze] K10003 auth success\\n\")\n\t}\n\n\tc.hasIntercom = c.conn.HasTwoWayStreaming()\n\n\tif c.verbose {\n\t\tfmt.Printf(\"[Wyze] K-auth complete\\n\")\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) buildK10000() []byte {\n\tjson := []byte(`{\"cameraInfo\":{\"audioEncoderList\":[137,138,140]}}`) // 137=PCMU, 138=PCMA, 140=PCM\n\tb := make([]byte, 16+len(json))\n\tcopy(b, \"HL\")                                           // magic\n\tb[2] = 5                                                // version\n\tbinary.LittleEndian.PutUint16(b[4:], KCmdAuth)          // 10000\n\tbinary.LittleEndian.PutUint16(b[6:], uint16(len(json))) // payload len\n\tcopy(b[16:], json)\n\treturn b\n}\n\nfunc (c *Client) buildK10002(challenge []byte, status byte) []byte {\n\tresp := generateChallengeResponse(challenge, c.enr, status)\n\tsessionID := make([]byte, 4)\n\trand.Read(sessionID)\n\tb := make([]byte, 38)\n\tcopy(b, \"HL\")                                           // magic\n\tb[2] = 5                                                // version\n\tbinary.LittleEndian.PutUint16(b[4:], KCmdChallengeResp) // 10002\n\tb[6] = 22                                               // payload len\n\tcopy(b[16:], resp[:16])                                 // challenge response\n\tcopy(b[32:], sessionID)                                 // random session ID\n\tb[36] = 1                                               // video enabled/disabled\n\tb[37] = 1                                               // audio enabled/disabled\n\treturn b\n}\n\nfunc (c *Client) buildK10010(mediaType byte, enabled bool) []byte {\n\tb := make([]byte, 18)\n\tcopy(b, \"HL\")                                            // magic\n\tb[2] = 5                                                 // version\n\tbinary.LittleEndian.PutUint16(b[4:], KCmdControlChannel) // 10010\n\tbinary.LittleEndian.PutUint16(b[6:], 2)                  // payload len\n\tb[16] = mediaType                                        // 1=video, 2=audio, 3=return audio\n\tb[17] = 1                                                // 1=enable, 2=disable\n\tif !enabled {\n\t\tb[17] = 2\n\t}\n\treturn b\n}\n\nfunc (c *Client) buildK10052(frameSize uint8, bitrate uint16) []byte {\n\tb := make([]byte, 22)\n\tcopy(b, \"HL\")                                             // magic\n\tb[2] = 5                                                  // version\n\tbinary.LittleEndian.PutUint16(b[4:], KCmdSetResolutionDB) // 10052\n\tbinary.LittleEndian.PutUint16(b[6:], 6)                   // payload len\n\tbinary.LittleEndian.PutUint16(b[16:], bitrate)            // bitrate (2 bytes)\n\tb[18] = frameSize + 1                                     // frame size (1 byte)\n\t// b[19] = fps, b[20:22] = zeros\n\treturn b\n}\n\nfunc (c *Client) buildK10056(frameSize uint8, bitrate uint16) []byte {\n\tb := make([]byte, 21)\n\tcopy(b, \"HL\")                                           // magic\n\tb[2] = 5                                                // version\n\tbinary.LittleEndian.PutUint16(b[4:], KCmdSetResolution) // 10056\n\tbinary.LittleEndian.PutUint16(b[6:], 5)                 // payload len\n\tb[16] = frameSize + 1                                   // frame size\n\tbinary.LittleEndian.PutUint16(b[17:], bitrate)          // bitrate\n\t// b[19:21] = FPS (0 = auto)\n\treturn b\n}\n\nfunc (c *Client) parseK10001(data []byte) (challenge []byte, status byte, err error) {\n\tif c.verbose {\n\t\tfmt.Printf(\"[Wyze] parseK10001: received %d bytes\\n\", len(data))\n\t}\n\n\tif len(data) < 33 {\n\t\treturn nil, 0, fmt.Errorf(\"data too short: %d bytes\", len(data))\n\t}\n\n\tif data[0] != 'H' || data[1] != 'L' {\n\t\treturn nil, 0, fmt.Errorf(\"invalid HL magic: %x %x\", data[0], data[1])\n\t}\n\n\tcmdID := binary.LittleEndian.Uint16(data[4:])\n\tif cmdID != KCmdChallenge {\n\t\treturn nil, 0, fmt.Errorf(\"expected cmdID 10001, got %d\", cmdID)\n\t}\n\n\tstatus = data[16]\n\tchallenge = make([]byte, 16)\n\tcopy(challenge, data[17:33])\n\n\treturn challenge, status, nil\n}\n\nfunc (c *Client) parseK10003(data []byte) (*AuthResponse, error) {\n\tif c.verbose {\n\t\tfmt.Printf(\"[Wyze] parseK10003: received %d bytes\\n\", len(data))\n\t}\n\n\tif len(data) < 16 {\n\t\treturn &AuthResponse{}, nil\n\t}\n\n\tif data[0] != 'H' || data[1] != 'L' {\n\t\treturn &AuthResponse{}, nil\n\t}\n\n\tcmdID := binary.LittleEndian.Uint16(data[4:])\n\ttextLen := binary.LittleEndian.Uint16(data[6:])\n\n\tif cmdID != KCmdAuthResult {\n\t\treturn &AuthResponse{}, nil\n\t}\n\n\tif len(data) > 16 && textLen > 0 {\n\t\tjsonData := data[16:]\n\t\tfor i := range jsonData {\n\t\t\tif jsonData[i] == '{' {\n\t\t\t\tvar resp AuthResponse\n\t\t\t\tif err := json.Unmarshal(jsonData[i:], &resp); err == nil {\n\t\t\t\t\tif c.verbose {\n\t\t\t\t\t\tfmt.Printf(\"[Wyze] parseK10003: parsed JSON\\n\")\n\t\t\t\t\t}\n\t\t\t\t\treturn &resp, nil\n\t\t\t\t}\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n\n\treturn &AuthResponse{}, nil\n}\n\nfunc (c *Client) useDoorbellResolution() bool {\n\tswitch c.model {\n\tcase \"WYZEDB3\", \"WVOD1\", \"HL_WCO2\", \"WYZEC1\":\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc (c *Client) hdFrameSize() uint8 {\n\tif c.isFloodlight() {\n\t\treturn FrameSizeFloodlight\n\t}\n\tif c.is2K() {\n\t\treturn FrameSize2K\n\t}\n\treturn FrameSize1080P\n}\n\nfunc (c *Client) is2K() bool {\n\tswitch c.model {\n\tcase \"HL_CAM3P\", \"HL_PANP\", \"HL_CAM4\", \"HL_DB2\", \"HL_CFL2\":\n\t\treturn true\n\t}\n\treturn false\n}\n\nfunc (c *Client) isFloodlight() bool {\n\treturn c.model == \"HL_CFL2\"\n}\n\nfunc (c *Client) matchHL(expectCmd uint16) func([]byte) bool {\n\treturn func(data []byte) bool {\n\t\thlData := c.extractHL(data)\n\t\tif hlData == nil {\n\t\t\treturn false\n\t\t}\n\t\tcmd, _, ok := tutk.ParseHL(hlData)\n\t\treturn ok && cmd == expectCmd\n\t}\n}\n\nfunc (c *Client) extractHL(data []byte) []byte {\n\t// Try offset 32 (magicIOCtrl, protoVersion)\n\tif hlData := tutk.FindHL(data, 32); hlData != nil {\n\t\treturn hlData\n\t}\n\t// Try offset 36 (magicChannelMsg)\n\tif len(data) >= 36 && data[16] == 0x00 {\n\t\treturn tutk.FindHL(data, 36)\n\t}\n\treturn nil\n}\n\nconst (\n\tstatusDefault byte = 1\n\tstatusENR16   byte = 3\n\tstatusENR32   byte = 6\n)\n\nfunc generateChallengeResponse(challengeBytes []byte, enr string, status byte) []byte {\n\tvar secretKey []byte\n\n\tswitch status {\n\tcase statusDefault:\n\t\tsecretKey = []byte(\"FFFFFFFFFFFFFFFF\")\n\tcase statusENR16:\n\t\tif len(enr) >= 16 {\n\t\t\tsecretKey = []byte(enr[:16])\n\t\t} else {\n\t\t\tsecretKey = make([]byte, 16)\n\t\t\tcopy(secretKey, enr)\n\t\t}\n\tcase statusENR32:\n\t\tif len(enr) >= 16 {\n\t\t\tfirstKey := []byte(enr[:16])\n\t\t\tchallengeBytes = tutk.XXTEADecryptVar(challengeBytes, firstKey)\n\t\t}\n\t\tif len(enr) >= 32 {\n\t\t\tsecretKey = []byte(enr[16:32])\n\t\t} else if len(enr) > 16 {\n\t\t\tsecretKey = make([]byte, 16)\n\t\t\tcopy(secretKey, []byte(enr[16:]))\n\t\t} else {\n\t\t\tsecretKey = []byte(\"FFFFFFFFFFFFFFFF\")\n\t\t}\n\tdefault:\n\t\tsecretKey = []byte(\"FFFFFFFFFFFFFFFF\")\n\t}\n\n\treturn tutk.XXTEADecryptVar(challengeBytes, secretKey)\n}\n"
  },
  {
    "path": "pkg/wyze/cloud.go",
    "content": "package wyze\n\nimport (\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\nconst (\n\tbaseURLAuth = \"https://auth-prod.api.wyze.com\"\n\tbaseURLAPI  = \"https://api.wyzecam.com\"\n\tappName     = \"com.hualai.WyzeCam\"\n\tappVersion  = \"2.50.0\"\n)\n\ntype Cloud struct {\n\tclient      *http.Client\n\tapiKey      string\n\tkeyID       string\n\taccessToken string\n\tphoneID     string\n\tcameras     []*Camera\n}\n\ntype Camera struct {\n\tMAC          string `json:\"mac\"`\n\tP2PID        string `json:\"p2p_id\"`\n\tENR          string `json:\"enr\"`\n\tIP           string `json:\"ip\"`\n\tNickname     string `json:\"nickname\"`\n\tProductModel string `json:\"product_model\"`\n\tProductType  string `json:\"product_type\"`\n\tDTLS         int    `json:\"dtls\"`\n\tFirmwareVer  string `json:\"firmware_ver\"`\n\tIsOnline     bool   `json:\"is_online\"`\n}\n\ntype deviceListResponse struct {\n\tCode string `json:\"code\"`\n\tMsg  string `json:\"msg\"`\n\tData struct {\n\t\tDeviceList []deviceInfo `json:\"device_list\"`\n\t} `json:\"data\"`\n}\n\ntype deviceInfo struct {\n\tMAC          string       `json:\"mac\"`\n\tENR          string       `json:\"enr\"`\n\tNickname     string       `json:\"nickname\"`\n\tProductModel string       `json:\"product_model\"`\n\tProductType  string       `json:\"product_type\"`\n\tFirmwareVer  string       `json:\"firmware_ver\"`\n\tConnState    int          `json:\"conn_state\"`\n\tDeviceParams deviceParams `json:\"device_params\"`\n}\n\ntype deviceParams struct {\n\tP2PID   string `json:\"p2p_id\"`\n\tP2PType int    `json:\"p2p_type\"`\n\tIP      string `json:\"ip\"`\n\tDTLS    int    `json:\"dtls\"`\n}\n\ntype p2pInfoResponse struct {\n\tCode string         `json:\"code\"`\n\tMsg  string         `json:\"msg\"`\n\tData map[string]any `json:\"data\"`\n}\n\ntype loginResponse struct {\n\tAccessToken    string   `json:\"access_token\"`\n\tRefreshToken   string   `json:\"refresh_token\"`\n\tUserID         string   `json:\"user_id\"`\n\tMFAOptions     []string `json:\"mfa_options\"`\n\tSMSSessionID   string   `json:\"sms_session_id\"`\n\tEmailSessionID string   `json:\"email_session_id\"`\n}\n\nfunc NewCloud(apiKey, keyID string) *Cloud {\n\treturn &Cloud{\n\t\tclient:  &http.Client{Timeout: 30 * time.Second},\n\t\tphoneID: generatePhoneID(),\n\t\tapiKey:  apiKey,\n\t\tkeyID:   keyID,\n\t}\n}\n\nfunc (c *Cloud) Login(email, password string) error {\n\tpayload := map[string]string{\n\t\t\"email\":    strings.TrimSpace(email),\n\t\t\"password\": hashPassword(password),\n\t}\n\n\tjsonData, _ := json.Marshal(payload)\n\n\treq, err := http.NewRequest(\"POST\", baseURLAuth+\"/api/user/login\", strings.NewReader(string(jsonData)))\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\treq.Header.Set(\"Apikey\", c.apiKey)\n\treq.Header.Set(\"Keyid\", c.keyID)\n\treq.Header.Set(\"User-Agent\", \"go2rtc\")\n\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar errResp apiError\n\t_ = json.Unmarshal(body, &errResp)\n\tif errResp.hasError() {\n\t\treturn fmt.Errorf(\"wyze: login failed (code %s): %s\", errResp.code(), errResp.message())\n\t}\n\n\tvar result loginResponse\n\tif err := json.Unmarshal(body, &result); err != nil {\n\t\treturn fmt.Errorf(\"wyze: failed to parse login response: %w\", err)\n\t}\n\n\tif len(result.MFAOptions) > 0 {\n\t\treturn &AuthError{\n\t\t\tMessage:  \"MFA required\",\n\t\t\tNeedsMFA: true,\n\t\t\tMFAType:  strings.Join(result.MFAOptions, \",\"),\n\t\t}\n\t}\n\n\tif result.AccessToken == \"\" {\n\t\treturn errors.New(\"wyze: no access token in response\")\n\t}\n\n\tc.accessToken = result.AccessToken\n\n\treturn nil\n}\n\nfunc (c *Cloud) GetCameraList() ([]*Camera, error) {\n\tpayload := map[string]any{\n\t\t\"access_token\":      c.accessToken,\n\t\t\"phone_id\":          c.phoneID,\n\t\t\"app_name\":          appName,\n\t\t\"app_ver\":           appName + \"___\" + appVersion,\n\t\t\"app_version\":       appVersion,\n\t\t\"phone_system_type\": 1,\n\t\t\"sc\":                \"9f275790cab94a72bd206c8876429f3c\",\n\t\t\"sv\":                \"9d74946e652647e9b6c9d59326aef104\",\n\t\t\"ts\":                time.Now().UnixMilli(),\n\t}\n\n\tjsonData, _ := json.Marshal(payload)\n\n\treq, err := http.NewRequest(\"POST\", baseURLAPI+\"/app/v2/home_page/get_object_list\", strings.NewReader(string(jsonData)))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result deviceListResponse\n\tif err := json.Unmarshal(body, &result); err != nil {\n\t\treturn nil, fmt.Errorf(\"wyze: failed to parse device list: %w\", err)\n\t}\n\n\tif result.Code != \"1\" {\n\t\treturn nil, fmt.Errorf(\"wyze: API error: %s - %s\", result.Code, result.Msg)\n\t}\n\n\tc.cameras = nil\n\tfor _, dev := range result.Data.DeviceList {\n\t\tif dev.ProductType != \"Camera\" {\n\t\t\tcontinue\n\t\t}\n\t\tif dev.DeviceParams.IP == \"\" {\n\t\t\tcontinue // skip cameras without IP (gwell protocol)\n\t\t}\n\n\t\tc.cameras = append(c.cameras, &Camera{\n\t\t\tMAC:          dev.MAC,\n\t\t\tP2PID:        dev.DeviceParams.P2PID,\n\t\t\tENR:          dev.ENR,\n\t\t\tIP:           dev.DeviceParams.IP,\n\t\t\tNickname:     dev.Nickname,\n\t\t\tProductModel: dev.ProductModel,\n\t\t\tProductType:  dev.ProductType,\n\t\t\tDTLS:         dev.DeviceParams.DTLS,\n\t\t\tFirmwareVer:  dev.FirmwareVer,\n\t\t\tIsOnline:     dev.ConnState == 1,\n\t\t})\n\t}\n\n\treturn c.cameras, nil\n}\n\nfunc (c *Cloud) GetCamera(id string) (*Camera, error) {\n\tif c.cameras == nil {\n\t\tif _, err := c.GetCameraList(); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tid = strings.ToUpper(id)\n\tfor _, cam := range c.cameras {\n\t\tif strings.ToUpper(cam.MAC) == id || strings.EqualFold(cam.Nickname, id) {\n\t\t\treturn cam, nil\n\t\t}\n\t}\n\n\treturn nil, fmt.Errorf(\"wyze: camera not found: %s\", id)\n}\n\nfunc (c *Cloud) GetP2PInfo(mac string) (map[string]any, error) {\n\tpayload := map[string]any{\n\t\t\"access_token\":      c.accessToken,\n\t\t\"phone_id\":          c.phoneID,\n\t\t\"device_mac\":        mac,\n\t\t\"app_name\":          appName,\n\t\t\"app_ver\":           appName + \"___\" + appVersion,\n\t\t\"app_version\":       appVersion,\n\t\t\"phone_system_type\": 1,\n\t\t\"sc\":                \"9f275790cab94a72bd206c8876429f3c\",\n\t\t\"sv\":                \"9d74946e652647e9b6c9d59326aef104\",\n\t\t\"ts\":                time.Now().UnixMilli(),\n\t}\n\n\tjsonData, _ := json.Marshal(payload)\n\n\treq, err := http.NewRequest(\"POST\", baseURLAPI+\"/app/v2/device/get_iotc_info\", strings.NewReader(string(jsonData)))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treq.Header.Set(\"Content-Type\", \"application/json\")\n\n\tresp, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tdefer resp.Body.Close()\n\n\tbody, err := io.ReadAll(resp.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar result p2pInfoResponse\n\tif err := json.Unmarshal(body, &result); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif result.Code != \"1\" {\n\t\treturn nil, fmt.Errorf(\"wyze: API error: %s - %s\", result.Code, result.Msg)\n\t}\n\n\treturn result.Data, nil\n}\n\ntype apiError struct {\n\tCode        string `json:\"code\"`\n\tErrorCode   int    `json:\"errorCode\"`\n\tMsg         string `json:\"msg\"`\n\tDescription string `json:\"description\"`\n}\n\nfunc (e *apiError) hasError() bool {\n\tif e.Code == \"1\" || e.Code == \"0\" {\n\t\treturn false\n\t}\n\tif e.Code == \"\" && e.ErrorCode == 0 {\n\t\treturn false\n\t}\n\treturn e.Code != \"\" || e.ErrorCode != 0\n}\n\nfunc (e *apiError) message() string {\n\tif e.Msg != \"\" {\n\t\treturn e.Msg\n\t}\n\treturn e.Description\n}\n\nfunc (e *apiError) code() string {\n\tif e.Code != \"\" {\n\t\treturn e.Code\n\t}\n\treturn fmt.Sprintf(\"%d\", e.ErrorCode)\n}\n\ntype AuthError struct {\n\tMessage  string `json:\"message\"`\n\tNeedsMFA bool   `json:\"needs_mfa,omitempty\"`\n\tMFAType  string `json:\"mfa_type,omitempty\"`\n}\n\nfunc (e *AuthError) Error() string {\n\treturn e.Message\n}\n\nfunc generatePhoneID() string {\n\treturn core.RandString(16, 16) // 16 hex chars\n}\n\nfunc hashPassword(password string) string {\n\tencoded := strings.TrimSpace(password)\n\tif strings.HasPrefix(strings.ToLower(encoded), \"md5:\") {\n\t\treturn encoded[4:]\n\t}\n\tfor range 3 {\n\t\thash := md5.Sum([]byte(encoded))\n\t\tencoded = hex.EncodeToString(hash[:])\n\t}\n\treturn encoded\n}\n"
  },
  {
    "path": "pkg/wyze/producer.go",
    "content": "package wyze\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/aac\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264/annexb\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h265\"\n\t\"github.com/AlexxIT/go2rtc/pkg/tutk\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Producer struct {\n\tcore.Connection\n\tclient *Client\n\tmodel  string\n}\n\nfunc NewProducer(rawURL string) (*Producer, error) {\n\tclient, err := Dial(rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tu, _ := url.Parse(rawURL)\n\tquery := u.Query()\n\n\t// 0 = HD (default), 1 = SD/360P, 2 = 720P, 3 = 2K, 4 = Floodlight\n\tvar quality byte\n\tswitch s := query.Get(\"subtype\"); s {\n\tcase \"\", \"hd\":\n\t\tquality = 0\n\tcase \"sd\":\n\t\tquality = FrameSize360P\n\tdefault:\n\t\tquality = core.ParseByte(s)\n\t}\n\n\tmedias, err := probe(client, quality)\n\tif err != nil {\n\t\t_ = client.Close()\n\t\treturn nil, err\n\t}\n\n\tprod := &Producer{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"wyze\",\n\t\t\tProtocol:   client.Protocol(),\n\t\t\tRemoteAddr: client.RemoteAddr().String(),\n\t\t\tSource:     rawURL,\n\t\t\tMedias:     medias,\n\t\t\tTransport:  client,\n\t\t},\n\t\tclient: client,\n\t\tmodel:  query.Get(\"model\"),\n\t}\n\n\treturn prod, nil\n}\n\nfunc (p *Producer) Start() error {\n\tfor {\n\t\tif p.client.verbose {\n\t\t\tfmt.Println(\"[Wyze] Reading packet...\")\n\t\t}\n\n\t\t_ = p.client.SetDeadline(time.Now().Add(core.ConnDeadline))\n\t\tpkt, err := p.client.ReadPacket()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\t\tif pkt == nil {\n\t\t\tcontinue\n\t\t}\n\n\t\tvar name string\n\t\tvar pkt2 *core.Packet\n\n\t\tswitch codecID := pkt.Codec; codecID {\n\t\tcase tutk.CodecH264:\n\t\t\tname = core.CodecH264\n\t\t\tpkt2 = &core.Packet{\n\t\t\t\tHeader:  rtp.Header{SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},\n\t\t\t\tPayload: annexb.EncodeToAVCC(pkt.Payload),\n\t\t\t}\n\n\t\tcase tutk.CodecH265:\n\t\t\tname = core.CodecH265\n\t\t\tpkt2 = &core.Packet{\n\t\t\t\tHeader:  rtp.Header{SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},\n\t\t\t\tPayload: annexb.EncodeToAVCC(pkt.Payload),\n\t\t\t}\n\n\t\tcase tutk.CodecPCMU:\n\t\t\tname = core.CodecPCMU\n\t\t\tpkt2 = &core.Packet{\n\t\t\t\tHeader:  rtp.Header{Version: 2, Marker: true, SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},\n\t\t\t\tPayload: pkt.Payload,\n\t\t\t}\n\n\t\tcase tutk.CodecPCMA:\n\t\t\tname = core.CodecPCMA\n\t\t\tpkt2 = &core.Packet{\n\t\t\t\tHeader:  rtp.Header{Version: 2, Marker: true, SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},\n\t\t\t\tPayload: pkt.Payload,\n\t\t\t}\n\n\t\tcase tutk.CodecAACADTS, tutk.CodecAACAlt, tutk.CodecAACRaw, tutk.CodecAACLATM:\n\t\t\tname = core.CodecAAC\n\t\t\tpayload := pkt.Payload\n\t\t\tif aac.IsADTS(payload) {\n\t\t\t\tpayload = payload[aac.ADTSHeaderLen(payload):]\n\t\t\t}\n\t\t\tpkt2 = &core.Packet{\n\t\t\t\tHeader:  rtp.Header{Version: aac.RTPPacketVersionAAC, Marker: true, SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},\n\t\t\t\tPayload: payload,\n\t\t\t}\n\n\t\tcase tutk.CodecOpus:\n\t\t\tname = core.CodecOpus\n\t\t\tpkt2 = &core.Packet{\n\t\t\t\tHeader:  rtp.Header{Version: 2, Marker: true, SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},\n\t\t\t\tPayload: pkt.Payload,\n\t\t\t}\n\n\t\tcase tutk.CodecPCML:\n\t\t\tname = core.CodecPCML\n\t\t\tpkt2 = &core.Packet{\n\t\t\t\tHeader:  rtp.Header{Version: 2, Marker: true, SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},\n\t\t\t\tPayload: pkt.Payload,\n\t\t\t}\n\n\t\tcase tutk.CodecMP3:\n\t\t\tname = core.CodecMP3\n\t\t\tpkt2 = &core.Packet{\n\t\t\t\tHeader:  rtp.Header{Version: 2, Marker: true, SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},\n\t\t\t\tPayload: pkt.Payload,\n\t\t\t}\n\n\t\tcase tutk.CodecMJPEG:\n\t\t\tname = core.CodecJPEG\n\t\t\tpkt2 = &core.Packet{\n\t\t\t\tHeader:  rtp.Header{SequenceNumber: uint16(pkt.FrameNo), Timestamp: pkt.Timestamp},\n\t\t\t\tPayload: pkt.Payload,\n\t\t\t}\n\n\t\tdefault:\n\t\t\tcontinue\n\t\t}\n\n\t\tfor _, recv := range p.Receivers {\n\t\t\tif recv.Codec.Name == name {\n\t\t\t\trecv.WriteRTP(pkt2)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc probe(client *Client, quality byte) ([]*core.Media, error) {\n\tclient.SetResolution(quality)\n\tclient.SetDeadline(time.Now().Add(core.ProbeTimeout))\n\n\tvar vcodec, acodec *core.Codec\n\tvar tutkAudioCodec byte\n\n\tfor {\n\t\tif client.verbose {\n\t\t\tfmt.Println(\"[Wyze] Probing for codecs...\")\n\t\t}\n\n\t\tpkt, err := client.ReadPacket()\n\t\tif err != nil {\n\t\t\treturn nil, fmt.Errorf(\"wyze: probe: %w\", err)\n\t\t}\n\t\tif pkt == nil || len(pkt.Payload) < 5 {\n\t\t\tcontinue\n\t\t}\n\n\t\tswitch pkt.Codec {\n\t\tcase tutk.CodecH264:\n\t\t\tif vcodec == nil {\n\t\t\t\tbuf := annexb.EncodeToAVCC(pkt.Payload)\n\t\t\t\tif len(buf) >= 5 && h264.NALUType(buf) == h264.NALUTypeSPS {\n\t\t\t\t\tvcodec = h264.AVCCToCodec(buf)\n\t\t\t\t}\n\t\t\t}\n\t\tcase tutk.CodecH265:\n\t\t\tif vcodec == nil {\n\t\t\t\tbuf := annexb.EncodeToAVCC(pkt.Payload)\n\t\t\t\tif len(buf) >= 5 && h265.NALUType(buf) == h265.NALUTypeVPS {\n\t\t\t\t\tvcodec = h265.AVCCToCodec(buf)\n\t\t\t\t}\n\t\t\t}\n\t\tcase tutk.CodecPCMU:\n\t\t\tif acodec == nil {\n\t\t\t\tacodec = &core.Codec{Name: core.CodecPCMU, ClockRate: pkt.SampleRate, Channels: pkt.Channels}\n\t\t\t\ttutkAudioCodec = pkt.Codec\n\t\t\t}\n\t\tcase tutk.CodecPCMA:\n\t\t\tif acodec == nil {\n\t\t\t\tacodec = &core.Codec{Name: core.CodecPCMA, ClockRate: pkt.SampleRate, Channels: pkt.Channels}\n\t\t\t\ttutkAudioCodec = pkt.Codec\n\t\t\t}\n\t\tcase tutk.CodecAACAlt, tutk.CodecAACADTS, tutk.CodecAACRaw, tutk.CodecAACLATM:\n\t\t\tif acodec == nil {\n\t\t\t\tconfig := aac.EncodeConfig(aac.TypeAACLC, pkt.SampleRate, pkt.Channels, false)\n\t\t\t\tacodec = aac.ConfigToCodec(config)\n\t\t\t\ttutkAudioCodec = pkt.Codec\n\t\t\t}\n\t\tcase tutk.CodecOpus:\n\t\t\tif acodec == nil {\n\t\t\t\tacodec = &core.Codec{Name: core.CodecOpus, ClockRate: 48000, Channels: 2}\n\t\t\t\ttutkAudioCodec = pkt.Codec\n\t\t\t}\n\t\tcase tutk.CodecPCML:\n\t\t\tif acodec == nil {\n\t\t\t\tacodec = &core.Codec{Name: core.CodecPCML, ClockRate: pkt.SampleRate, Channels: pkt.Channels}\n\t\t\t\ttutkAudioCodec = pkt.Codec\n\t\t\t}\n\t\tcase tutk.CodecMP3:\n\t\t\tif acodec == nil {\n\t\t\t\tacodec = &core.Codec{Name: core.CodecMP3, ClockRate: pkt.SampleRate, Channels: pkt.Channels}\n\t\t\t\ttutkAudioCodec = pkt.Codec\n\t\t\t}\n\t\tcase tutk.CodecMJPEG:\n\t\t\tif vcodec == nil {\n\t\t\t\tvcodec = &core.Codec{Name: core.CodecJPEG, ClockRate: 90000, PayloadType: core.PayloadTypeRAW}\n\t\t\t}\n\t\t}\n\n\t\tif vcodec != nil && (acodec != nil || !client.SupportsAudio()) {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t_ = client.SetDeadline(time.Time{})\n\n\tmedias := []*core.Media{\n\t\t{\n\t\t\tKind:      core.KindVideo,\n\t\t\tDirection: core.DirectionRecvonly,\n\t\t\tCodecs:    []*core.Codec{vcodec},\n\t\t},\n\t}\n\n\tif acodec != nil {\n\t\tmedias = append(medias, &core.Media{\n\t\t\tKind:      core.KindAudio,\n\t\t\tDirection: core.DirectionRecvonly,\n\t\t\tCodecs:    []*core.Codec{acodec},\n\t\t})\n\n\t\tif client.SupportsIntercom() {\n\t\t\tclient.SetBackchannelCodec(tutkAudioCodec, acodec.ClockRate, uint8(acodec.Channels))\n\t\t\tmedias = append(medias, &core.Media{\n\t\t\t\tKind:      core.KindAudio,\n\t\t\t\tDirection: core.DirectionSendonly,\n\t\t\t\tCodecs:    []*core.Codec{acodec.Clone()},\n\t\t\t})\n\t\t}\n\t}\n\n\tif client.verbose {\n\t\tfmt.Printf(\"[Wyze] Probed codecs: video=%s audio=%s\\n\", vcodec.Name, acodec.Name)\n\t\tif client.SupportsIntercom() {\n\t\t\tfmt.Printf(\"[Wyze] Intercom supported, audio send codec=%s\\n\", acodec.Name)\n\t\t}\n\t}\n\n\treturn medias, nil\n}\n"
  },
  {
    "path": "pkg/xiaomi/cloud.go",
    "content": "package xiaomi\n\nimport (\n\t\"bytes\"\n\t\"crypto/md5\"\n\t\"crypto/rand\"\n\t\"crypto/rc4\"\n\t\"crypto/sha1\"\n\t\"crypto/sha256\"\n\t\"encoding/base64\"\n\t\"encoding/binary\"\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\ntype Cloud struct {\n\tclient *http.Client\n\n\tsid       string\n\tcookies   string // for auth\n\tssecurity []byte // for encryption\n\n\tuserID    string\n\tpassToken string\n\n\tauth map[string]string\n}\n\nfunc NewCloud(sid string) *Cloud {\n\treturn &Cloud{\n\t\tclient: &http.Client{Timeout: 15 * time.Second},\n\t\tsid:    sid,\n\t}\n}\n\nfunc (c *Cloud) Login(username, password string) error {\n\tres, err := c.client.Get(\"https://account.xiaomi.com/pass/serviceLogin?_json=true&sid=\" + c.sid)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar v1 struct {\n\t\tQs       string `json:\"qs\"`\n\t\tSign     string `json:\"_sign\"`\n\t\tSid      string `json:\"sid\"`\n\t\tCallback string `json:\"callback\"`\n\t}\n\tif _, err = readLoginResponse(res.Body, &v1); err != nil {\n\t\treturn err\n\t}\n\n\thash := fmt.Sprintf(\"%X\", md5.Sum([]byte(password)))\n\n\tform := url.Values{\n\t\t\"_json\":    {\"true\"},\n\t\t\"hash\":     {hash},\n\t\t\"sid\":      {v1.Sid},\n\t\t\"callback\": {v1.Callback},\n\t\t\"_sign\":    {v1.Sign},\n\t\t\"qs\":       {v1.Qs},\n\t\t\"user\":     {username},\n\t}\n\tcookies := \"deviceId=\" + core.RandString(16, 62)\n\n\t// login after captcha\n\tif c.auth != nil && c.auth[\"captcha_code\"] != \"\" {\n\t\tform.Set(\"captCode\", c.auth[\"captcha_code\"])\n\t\tcookies += \"; ick=\" + c.auth[\"ick\"]\n\t}\n\n\treq := Request{\n\t\tMethod:     \"POST\",\n\t\tURL:        \"https://account.xiaomi.com/pass/serviceLoginAuth2\",\n\t\tBody:       form,\n\t\tRawCookies: cookies,\n\t}.Encode()\n\n\tres, err = c.client.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar v2 struct {\n\t\tSsecurity []byte `json:\"ssecurity\"`\n\t\tPassToken string `json:\"passToken\"`\n\t\tLocation  string `json:\"location\"`\n\n\t\tCaptchaURL      string `json:\"captchaURL\"`\n\t\tNotificationURL string `json:\"notificationUrl\"`\n\t}\n\tbody, err := readLoginResponse(res.Body, &v2)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\t// save auth for two-step verification\n\tc.auth = map[string]string{\n\t\t\"username\": username,\n\t\t\"password\": password,\n\t}\n\n\tif v2.CaptchaURL != \"\" {\n\t\treturn c.getCaptcha(v2.CaptchaURL)\n\t}\n\n\tif v2.NotificationURL != \"\" {\n\t\treturn c.authStart(v2.NotificationURL)\n\t}\n\n\tif v2.Location == \"\" {\n\t\treturn fmt.Errorf(\"xiaomi: %s\", body)\n\t}\n\n\tc.auth = nil\n\tc.ssecurity = v2.Ssecurity\n\tc.passToken = v2.PassToken\n\n\treturn c.finishAuth(v2.Location)\n}\n\nfunc (c *Cloud) LoginWithCaptcha(captcha string) error {\n\tif c.auth == nil || c.auth[\"ick\"] == \"\" {\n\t\tpanic(\"wrong login step\")\n\t}\n\n\tc.auth[\"captcha_code\"] = captcha\n\n\t// check if captcha after verify\n\tif c.auth[\"flag\"] != \"\" {\n\t\treturn c.sendTicket()\n\t}\n\n\treturn c.Login(c.auth[\"username\"], c.auth[\"password\"])\n}\n\nfunc (c *Cloud) LoginWithVerify(ticket string) error {\n\tif c.auth == nil || c.auth[\"flag\"] == \"\" {\n\t\tpanic(\"wrong login step\")\n\t}\n\n\treq := Request{\n\t\tMethod:     \"POST\",\n\t\tURL:        \"https://account.xiaomi.com/identity/auth/verify\" + c.verifyName(),\n\t\tRawParams:  \"_flag\" + c.auth[\"flag\"] + \"&ticket=\" + ticket + \"&trust=false&_json=true\",\n\t\tRawCookies: \"identity_session=\" + c.auth[\"identity_session\"],\n\t}.Encode()\n\n\tres, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar v1 struct {\n\t\tLocation string `json:\"location\"`\n\t}\n\tbody, err := readLoginResponse(res.Body, &v1)\n\tif err != nil {\n\t\treturn err\n\t}\n\tif v1.Location == \"\" {\n\t\treturn fmt.Errorf(\"xiaomi: %s\", body)\n\t}\n\n\treturn c.finishAuth(v1.Location)\n}\n\nfunc (c *Cloud) getCaptcha(captchaURL string) error {\n\tres, err := c.client.Get(\"https://account.xiaomi.com\" + captchaURL)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer res.Body.Close()\n\n\tbody, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tc.auth[\"ick\"] = findCookie(res, \"ick\")\n\n\treturn &LoginError{\n\t\tCaptcha: body,\n\t}\n}\n\nfunc (c *Cloud) authStart(notificationURL string) error {\n\trawURL := strings.Replace(notificationURL, \"/fe/service/identity/authStart\", \"/identity/list\", 1)\n\tres, err := c.client.Get(rawURL)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar v1 struct {\n\t\tCode int `json:\"code\"`\n\t\tFlag int `json:\"flag\"`\n\t}\n\tif _, err = readLoginResponse(res.Body, &v1); err != nil {\n\t\treturn err\n\t}\n\n\tc.auth[\"flag\"] = strconv.Itoa(v1.Flag)\n\tc.auth[\"identity_session\"] = findCookie(res, \"identity_session\")\n\n\treturn c.sendTicket()\n}\n\nfunc findCookie(res *http.Response, name string) string {\n\tfor _, cookie := range res.Cookies() {\n\t\tif cookie.Name == name {\n\t\t\treturn cookie.Value\n\t\t}\n\t}\n\treturn \"\"\n}\n\nfunc (c *Cloud) verifyName() string {\n\tswitch c.auth[\"flag\"] {\n\tcase \"4\":\n\t\treturn \"Phone\"\n\tcase \"8\":\n\t\treturn \"Email\"\n\t}\n\treturn \"\"\n}\n\nfunc (c *Cloud) sendTicket() error {\n\tname := c.verifyName()\n\tcookies := \"identity_session=\" + c.auth[\"identity_session\"]\n\n\treq := Request{\n\t\tURL:        \"https://account.xiaomi.com/identity/auth/verify\" + name,\n\t\tRawParams:  \"_flag=\" + c.auth[\"flag\"] + \"&_json=true\",\n\t\tRawCookies: cookies,\n\t}.Encode()\n\n\tres, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar v1 struct {\n\t\tCode        int    `json:\"code\"`\n\t\tMaskedPhone string `json:\"maskedPhone\"`\n\t\tMaskedEmail string `json:\"maskedEmail\"`\n\t}\n\tif _, err = readLoginResponse(res.Body, &v1); err != nil {\n\t\treturn err\n\t}\n\n\t// verify after captcha\n\tcaptCode := c.auth[\"captcha_code\"]\n\tif captCode != \"\" {\n\t\tcookies += \"; ick=\" + c.auth[\"ick\"]\n\t}\n\n\tform := url.Values{\n\t\t\"_json\": {\"true\"},\n\t\t\"icode\": {captCode},\n\t\t\"retry\": {\"0\"},\n\t}\n\n\treq = Request{\n\t\tMethod:     \"POST\",\n\t\tURL:        \"https://account.xiaomi.com/identity/auth/send\" + name + \"Ticket\",\n\t\tBody:       form,\n\t\tRawCookies: cookies,\n\t}.Encode()\n\n\tres, err = c.client.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar v2 struct {\n\t\tCode       int    `json:\"code\"`\n\t\tCaptchaURL string `json:\"captchaURL\"`\n\t}\n\tbody, err := readLoginResponse(res.Body, &v2)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif v2.CaptchaURL != \"\" {\n\t\treturn c.getCaptcha(v2.CaptchaURL)\n\t}\n\n\tif v2.Code != 0 {\n\t\treturn fmt.Errorf(\"xiaomi: %s\", body)\n\t}\n\n\treturn &LoginError{\n\t\tVerifyPhone: v1.MaskedPhone,\n\t\tVerifyEmail: v1.MaskedEmail,\n\t}\n}\n\ntype LoginError struct {\n\tCaptcha     []byte `json:\"captcha,omitempty\"`\n\tVerifyPhone string `json:\"verify_phone,omitempty\"`\n\tVerifyEmail string `json:\"verify_email,omitempty\"`\n}\n\nfunc (l *LoginError) Error() string {\n\treturn \"\"\n}\n\nfunc (c *Cloud) finishAuth(location string) error {\n\tres, err := c.client.Get(location)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer res.Body.Close()\n\n\t// LoginWithVerify\n\t//   - userId, cUserId, serviceToken from cookies\n\t//   - passToken from redirect cookies\n\t//   - ssecurity from extra header\n\t// LoginWithToken\n\t//   - userId, cUserId, serviceToken from cookies\n\tvar cUserId, serviceToken string\n\n\tfor res != nil {\n\t\tfor _, cookie := range res.Cookies() {\n\t\t\tswitch cookie.Name {\n\t\t\tcase \"userId\":\n\t\t\t\tc.userID = cookie.Value\n\t\t\tcase \"cUserId\":\n\t\t\t\tcUserId = cookie.Value\n\t\t\tcase \"serviceToken\":\n\t\t\t\tserviceToken = cookie.Value\n\t\t\tcase \"passToken\":\n\t\t\t\tc.passToken = cookie.Value\n\t\t\t}\n\t\t}\n\n\t\tif s := res.Header.Get(\"Extension-Pragma\"); s != \"\" {\n\t\t\tvar v1 struct {\n\t\t\t\tSsecurity []byte `json:\"ssecurity\"`\n\t\t\t}\n\t\t\tif err = json.Unmarshal([]byte(s), &v1); err != nil {\n\t\t\t\treturn err\n\t\t\t}\n\t\t\tc.ssecurity = v1.Ssecurity\n\t\t}\n\n\t\tres = res.Request.Response\n\t}\n\n\tc.cookies = fmt.Sprintf(\"userId=%s; cUserId=%s; serviceToken=%s\", c.userID, cUserId, serviceToken)\n\n\treturn nil\n}\n\nfunc (c *Cloud) LoginWithToken(userID, passToken string) error {\n\treq, err := http.NewRequest(\"GET\", \"https://account.xiaomi.com/pass/serviceLogin?_json=true&sid=\"+c.sid, nil)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treq.Header.Set(\"Cookie\", fmt.Sprintf(\"userId=%s; passToken=%s\", userID, passToken))\n\n\tres, err := c.client.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar v1 struct {\n\t\tSsecurity []byte `json:\"ssecurity\"`\n\t\tPassToken string `json:\"passToken\"`\n\t\tLocation  string `json:\"location\"`\n\t}\n\tif _, err = readLoginResponse(res.Body, &v1); err != nil {\n\t\treturn err\n\t}\n\n\tc.ssecurity = v1.Ssecurity\n\tc.passToken = v1.PassToken\n\n\treturn c.finishAuth(v1.Location)\n}\n\nfunc (c *Cloud) UserToken() (string, string) {\n\treturn c.userID, c.passToken\n}\n\nfunc (c *Cloud) Request(baseURL, apiURL, params string, headers map[string]string) ([]byte, error) {\n\tform := url.Values{\"data\": {params}}\n\n\tnonce := genNonce()\n\tsignedNonce := genSignedNonce(c.ssecurity, nonce)\n\n\t// 1. gen hash for data param\n\tform.Set(\"rc4_hash__\", genSignature64(\"POST\", apiURL, form, signedNonce))\n\n\t// 2. encrypt data and hash params\n\tfor _, v := range form {\n\t\tciphertext, err := crypt(signedNonce, []byte(v[0]))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tv[0] = base64.StdEncoding.EncodeToString(ciphertext)\n\t}\n\n\t// 3. add signature for encrypted data and hash params\n\tform.Set(\"signature\", genSignature64(\"POST\", apiURL, form, signedNonce))\n\n\t// 4. add nonce\n\tform.Set(\"_nonce\", base64.StdEncoding.EncodeToString(nonce))\n\n\treq, err := http.NewRequest(\"POST\", baseURL+apiURL, strings.NewReader(form.Encode()))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq.Header.Set(\"Cookie\", c.cookies)\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\n\tfor k, v := range headers {\n\t\treq.Header.Set(k, v)\n\t}\n\n\tres, err := c.client.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 {\n\t\treturn nil, errors.New(res.Status)\n\t}\n\n\tbody, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tciphertext, err := base64.StdEncoding.DecodeString(string(body))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tplaintext, err := crypt(signedNonce, ciphertext)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar res1 struct {\n\t\tCode    int             `json:\"code\"`\n\t\tMessage string          `json:\"message\"`\n\t\tResult  json.RawMessage `json:\"result\"`\n\t}\n\tif err = json.Unmarshal(plaintext, &res1); err != nil {\n\t\treturn nil, err\n\t}\n\n\tif res1.Code != 0 {\n\t\treturn nil, errors.New(\"xiaomi: \" + res1.Message)\n\t}\n\n\treturn res1.Result, nil\n}\n\nfunc readLoginResponse(rc io.ReadCloser, v any) ([]byte, error) {\n\tdefer rc.Close()\n\n\tbody, err := io.ReadAll(rc)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tbody, ok := bytes.CutPrefix(body, []byte(\"&&&START&&&\"))\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"xiaomi: %s\", body)\n\t}\n\n\treturn body, json.Unmarshal(body, &v)\n}\n\nfunc genNonce() []byte {\n\tts := time.Now().Unix() / 60\n\n\tnonce := make([]byte, 12)\n\t_, _ = rand.Read(nonce[:8])\n\tbinary.BigEndian.PutUint32(nonce[8:], uint32(ts))\n\treturn nonce\n}\n\nfunc genSignedNonce(ssecurity, nonce []byte) []byte {\n\thasher := sha256.New()\n\thasher.Write(ssecurity)\n\thasher.Write(nonce)\n\treturn hasher.Sum(nil)\n}\n\nfunc crypt(key, plaintext []byte) ([]byte, error) {\n\tcipher, err := rc4.NewCipher(key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttmp := make([]byte, 1024)\n\tcipher.XORKeyStream(tmp, tmp)\n\n\tciphertext := make([]byte, len(plaintext))\n\tcipher.XORKeyStream(ciphertext, plaintext)\n\n\treturn ciphertext, nil\n}\n\nfunc genSignature64(method, path string, values url.Values, signedNonce []byte) string {\n\ts := method + \"&\" + path + \"&data=\" + values.Get(\"data\")\n\tif values.Has(\"rc4_hash__\") {\n\t\ts += \"&rc4_hash__=\" + values.Get(\"rc4_hash__\")\n\t}\n\ts += \"&\" + base64.StdEncoding.EncodeToString(signedNonce)\n\n\thasher := sha1.New()\n\thasher.Write([]byte(s))\n\tsignature := hasher.Sum(nil)\n\n\treturn base64.StdEncoding.EncodeToString(signature)\n}\n\ntype Request struct {\n\tMethod     string\n\tURL        string\n\tRawParams  string\n\tBody       url.Values\n\tHeaders    url.Values\n\tRawCookies string\n}\n\nfunc (r Request) Encode() *http.Request {\n\tif r.RawParams != \"\" {\n\t\tr.URL += \"?\" + r.RawParams\n\t}\n\n\tvar body io.Reader\n\tif r.Body != nil {\n\t\tbody = strings.NewReader(r.Body.Encode())\n\t}\n\n\treq, err := http.NewRequest(r.Method, r.URL, body)\n\tif err != nil {\n\t\treturn nil\n\t}\n\n\tif r.Headers != nil {\n\t\treq.Header = http.Header(r.Headers)\n\t}\n\tif r.Body != nil {\n\t\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\t}\n\tif r.RawCookies != \"\" {\n\t\treq.Header.Set(\"Cookie\", r.RawCookies)\n\t}\n\n\treturn req\n}\n"
  },
  {
    "path": "pkg/xiaomi/crypto/crypto.go",
    "content": "package crypto\n\nimport (\n\t\"crypto/rand\"\n\t\"encoding/hex\"\n\n\t\"golang.org/x/crypto/chacha20\"\n\t\"golang.org/x/crypto/nacl/box\"\n)\n\nfunc GenerateKey() ([]byte, []byte, error) {\n\tpublic, private, err := box.GenerateKey(rand.Reader)\n\tif err != nil {\n\t\treturn nil, nil, err\n\t}\n\treturn public[:], private[:], err\n}\n\nfunc CalcSharedKey(devicePublicB64, clientPrivateB64 string) ([]byte, error) {\n\tvar sharedKey, publicKey, privateKey [32]byte\n\tif _, err := hex.Decode(publicKey[:], []byte(devicePublicB64)); err != nil {\n\t\treturn nil, err\n\t}\n\tif _, err := hex.Decode(privateKey[:], []byte(clientPrivateB64)); err != nil {\n\t\treturn nil, err\n\t}\n\tbox.Precompute(&sharedKey, &publicKey, &privateKey)\n\treturn sharedKey[:], nil\n}\n\nfunc Encode(src, key32 []byte) ([]byte, error) {\n\tdst := make([]byte, len(src)+8)\n\n\tif _, err := rand.Read(dst[:8]); err != nil {\n\t\treturn nil, err\n\t}\n\n\tnonce12 := make([]byte, 12)\n\tcopy(nonce12[4:], dst[:8])\n\n\tc, err := chacha20.NewUnauthenticatedCipher(key32, nonce12)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tc.XORKeyStream(dst[8:], src)\n\n\treturn dst, nil\n}\n\nfunc Decode(src, key32 []byte) ([]byte, error) {\n\treturn DecodeNonce(src[8:], src[:8], key32)\n}\n\nfunc DecodeNonce(src, nonce8, key32 []byte) ([]byte, error) {\n\tnonce12 := make([]byte, 12)\n\tcopy(nonce12[4:], nonce8)\n\n\tc, err := chacha20.NewUnauthenticatedCipher(key32, nonce12)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tdst := make([]byte, len(src))\n\tc.XORKeyStream(dst, src)\n\n\treturn dst, nil\n}\n"
  },
  {
    "path": "pkg/xiaomi/legacy/client.go",
    "content": "package legacy\n\nimport (\n\t\"encoding/binary\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net/url\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/tutk\"\n\t\"github.com/AlexxIT/go2rtc/pkg/xiaomi/crypto\"\n)\n\nfunc NewClient(rawURL string) (*Client, error) {\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tquery := u.Query()\n\tmodel := query.Get(\"model\")\n\n\tvar username, password string\n\tvar key []byte\n\n\tif query.Has(\"sign\") {\n\t\t// Legacy with encryption\n\t\tkey, err = crypto.CalcSharedKey(query.Get(\"device_public\"), query.Get(\"client_private\"))\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tusername = fmt.Sprintf(\n\t\t\t`{\"public_key\":\"%s\",\"sign\":\"%s\",\"account\":\"admin\"}`,\n\t\t\tquery.Get(\"client_public\"), query.Get(\"sign\"),\n\t\t)\n\t} else if model == ModelMijia || model == ModelXiaobai {\n\t\tusername = \"admin\"\n\t\tpassword = query.Get(\"password\")\n\t} else if model == ModelDafang || model == ModelXiaofang {\n\t\tusername = \"admin\"\n\t} else {\n\t\treturn nil, fmt.Errorf(\"xiaomi: unsupported model: %s\", model)\n\t}\n\n\tconn, err := tutk.Dial(u.Host, query.Get(\"uid\"), username, password)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tif model == ModelDafang || model == ModelXiaofang {\n\t\terr = xiaofangLogin(conn, query.Get(\"password\"))\n\t\tif err != nil {\n\t\t\t_ = conn.Close()\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tc := &Client{\n\t\tConn:  conn,\n\t\tkey:   key,\n\t\tmodel: model,\n\t}\n\n\treturn c, nil\n}\n\nfunc xiaofangLogin(conn *tutk.Conn, password string) error {\n\tdata := tutk.ICAM(0x0400be) // ask login\n\tif err := conn.WriteCommand(0x0100, data); err != nil {\n\t\treturn err\n\t}\n\n\t_, data, err := conn.ReadCommand() // login request\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tenc := data[24:] // data[23] == 3\n\ttutk.XXTEADecrypt(enc, enc, []byte(password))\n\n\tenc = append(enc, 0, 0, 0, 0, 1, 1, 1)\n\tdata = tutk.ICAM(0x0400c0, enc...) // login response\n\tif err = conn.WriteCommand(0x0100, data); err != nil {\n\t\treturn err\n\t}\n\n\t_, data, err = conn.ReadCommand()\n\treturn err\n}\n\ntype Client struct {\n\t*tutk.Conn\n\tkey   []byte\n\tmodel string\n}\n\nfunc (c *Client) Version() string {\n\treturn fmt.Sprintf(\"%s (%s)\", c.Conn.Version(), c.model)\n}\n\nfunc (c *Client) ReadPacket() (hdr, payload []byte, err error) {\n\thdr, payload, err = c.Conn.ReadPacket()\n\tif err != nil {\n\t\treturn\n\t}\n\tif c.key != nil {\n\t\tif c.model == ModelAqaraG2 && hdr[0] == tutk.CodecH265 {\n\t\t\tpayload, err = DecodeVideo(payload, c.key)\n\t\t} else {\n\t\t\t// ModelAqaraG2: audio AAC\n\t\t\t// ModelIMILABA1: video HEVC, audio PCMA\n\t\t\tpayload, err = crypto.Decode(payload, c.key)\n\t\t}\n\t}\n\treturn\n}\n\nconst (\n\tcmdVideoStart    = 0x01ff\n\tcmdVideoStop     = 0x02ff\n\tcmdAudioStart    = 0x0300\n\tcmdAudioStop     = 0x0301\n\tcmdStreamCtrlReq = 0x0320\n)\n\nfunc (c *Client) WriteCommandJSON(ctrlType uint32, format string, a ...any) error {\n\tif len(a) > 0 {\n\t\tformat = fmt.Sprintf(format, a...)\n\t}\n\treturn c.WriteCommand(ctrlType, []byte(format))\n}\n\nfunc (c *Client) StartMedia(video, audio string) error {\n\tswitch c.model {\n\tcase ModelAqaraG2:\n\t\t// 0 - 1920x1080, 1 - 1280x720, 2 - ?\n\t\tswitch video {\n\t\tcase \"\", \"fhd\":\n\t\t\tvideo = \"0\"\n\t\tcase \"hd\":\n\t\t\tvideo = \"1\"\n\t\tcase \"sd\":\n\t\t\tvideo = \"2\"\n\t\t}\n\n\t\treturn errors.Join(\n\t\t\tc.WriteCommandJSON(cmdVideoStart, `{}`),\n\t\t\tc.WriteCommandJSON(0x0605, `{\"channel\":%s}`, video),\n\t\t\tc.WriteCommandJSON(0x0704, `{}`), // don't know why\n\t\t)\n\n\tcase ModelIMILABA1, ModelMijia:\n\t\t// 0 - auto, 1 - low, 3 - hd\n\t\tswitch video {\n\t\tcase \"\", \"hd\":\n\t\t\tvideo = \"3\"\n\t\tcase \"sd\":\n\t\t\tvideo = \"1\" // 2 is also low quality\n\t\tcase \"auto\":\n\t\t\tvideo = \"0\"\n\t\t}\n\n\t\t// quality after start\n\t\treturn errors.Join(\n\t\t\tc.WriteCommandJSON(cmdAudioStart, `{}`),\n\t\t\tc.WriteCommandJSON(cmdVideoStart, `{}`),\n\t\t\tc.WriteCommandJSON(cmdStreamCtrlReq, `{\"videoquality\":%s}`, video),\n\t\t)\n\n\tcase ModelXiaobai:\n\t\t// 00030000 7b7d  audio on\n\t\t// 01030000 7b7d  audio off\n\t\t// 20030000 0000000001000000  fhd (1920x1080)\n\t\t// 20030000 0000000002000000  hd (1280x720)\n\t\t// 20030000 0000000004000000  low (640x360)\n\t\t// 20030000 00000000ff000000  auto (1920x1080)\n\t\t// ff010000 7b7d  video tart\n\t\t// ff020000 7b7d  video stop\n\n\t\tvar b byte\n\t\tswitch video {\n\t\tcase \"\", \"fhd\":\n\t\t\tb = 1\n\t\tcase \"hd\":\n\t\t\tb = 2\n\t\tcase \"sd\":\n\t\t\tb = 4\n\t\tcase \"auto\":\n\t\t\tb = 0xff\n\t\t}\n\n\t\t// quality before start\n\t\treturn errors.Join(\n\t\t\tc.WriteCommandJSON(cmdAudioStart, `{}`),\n\t\t\tc.WriteCommand(cmdStreamCtrlReq, []byte{0, 0, 0, 0, b, 0, 0, 0}),\n\t\t\tc.WriteCommandJSON(cmdVideoStart, `{}`),\n\t\t)\n\n\tcase ModelDafang, ModelXiaofang:\n\t\t// 00010000 4943414d 95010400000000000000000600000000000000d20400005a07 - 90k bitrate\n\t\t// 00010000 4943414d 95010400000000000000000600000000000000d20400001e07 - 30k bitrate\n\t\t//var b byte\n\t\t//switch video {\n\t\t//case \"\", \"hd\":\n\t\t//\tb = 0x5a // bitrate 90k\n\t\t//case \"sd\":\n\t\t//\tb = 0x1e // bitrate 30k\n\t\t//}\n\t\t//data := tutk.ICAM(0x040195, 0xd2, 4, 0, 0, b, 7)\n\t\t//if err := c.WriteCommand(0x100, data); err != nil {\n\t\t//\treturn err\n\t\t//}\n\t\treturn nil\n\t}\n\n\treturn fmt.Errorf(\"xiaomi: unsupported model: %s\", c.model)\n}\n\nfunc (c *Client) StopMedia() error {\n\treturn errors.Join(\n\t\tc.WriteCommandJSON(cmdVideoStop, `{}`),\n\t\tc.WriteCommand(cmdVideoStop, make([]byte, 8)),\n\t)\n}\n\nfunc DecodeVideo(data, key []byte) ([]byte, error) {\n\tif string(data[:4]) == \"\\x00\\x00\\x00\\x01\" || data[8] == 0 {\n\t\treturn data, nil\n\t}\n\n\tif data[8] != 1 {\n\t\t// Support could be added, but I haven't seen such cameras.\n\t\treturn nil, fmt.Errorf(\"xiaomi: unsupported encryption\")\n\t}\n\n\tnonce8 := data[:8]\n\ti1 := binary.LittleEndian.Uint32(data[9:])\n\ti2 := binary.LittleEndian.Uint32(data[13:])\n\tdata = data[17:]\n\tsrc := data[i1 : i1+i2]\n\n\tfor i := 32; i+16 < len(src); i += 160 {\n\t\tdst, err := crypto.DecodeNonce(src[i:i+16], nonce8, key)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t\tcopy(src[i:], dst) // copy result in same place\n\t}\n\n\treturn data, nil\n}\n\nconst (\n\tModelAqaraG2  = \"lumi.camera.gwagl01\"\n\tModelIMILABA1 = \"chuangmi.camera.ipc019e\"\n\tModelLoockV1  = \"loock.cateye.v01\"\n\tModelXiaobai  = \"chuangmi.camera.xiaobai\"\n\tModelXiaofang = \"isa.camera.isc5\"\n\t// ModelMijia support miss format for new fw and legacy format for old fw\n\tModelMijia = \"chuangmi.camera.v2\"\n\t// ModelDafang support miss format for new fw and legacy format for old fw\n\tModelDafang = \"isa.camera.df3\"\n)\n\nfunc Supported(model string) bool {\n\tswitch model {\n\tcase ModelAqaraG2, ModelIMILABA1, ModelLoockV1, ModelXiaobai, ModelXiaofang:\n\t\treturn true\n\t}\n\treturn false\n}\n"
  },
  {
    "path": "pkg/xiaomi/legacy/producer.go",
    "content": "package legacy\n\nimport (\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/aac\"\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264/annexb\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h265\"\n\t\"github.com/AlexxIT/go2rtc/pkg/tutk\"\n\t\"github.com/pion/rtp\"\n)\n\nfunc Dial(rawURL string) (*Producer, error) {\n\tclient, err := NewClient(rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tu, _ := url.Parse(rawURL)\n\tquery := u.Query()\n\n\terr = client.StartMedia(query.Get(\"subtype\"), \"\")\n\tif err != nil {\n\t\t_ = client.Close()\n\t\treturn nil, err\n\t}\n\n\tmedias, err := probe(client)\n\tif err != nil {\n\t\t_ = client.Close()\n\t\treturn nil, err\n\t}\n\n\tc := &Producer{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"xiaomi/legacy\",\n\t\t\tProtocol:   \"tutk+udp\",\n\t\t\tRemoteAddr: client.RemoteAddr().String(),\n\t\t\tUserAgent:  client.Version(),\n\t\t\tMedias:     medias,\n\t\t\tTransport:  client,\n\t\t},\n\t\tclient: client,\n\t}\n\treturn c, nil\n}\n\ntype Producer struct {\n\tcore.Connection\n\tclient *Client\n}\n\nconst codecXiaobaiPCMA = 1 // chuangmi.camera.xiaobai\n\nfunc probe(client *Client) ([]*core.Media, error) {\n\t_ = client.SetDeadline(time.Now().Add(15 * time.Second))\n\n\tvar vcodec, acodec *core.Codec\n\n\tfor {\n\t\t// 0   5000      codec\n\t\t// 2   0000      codec params\n\t\t// 4   01        active clients\n\t\t// 5   34        unknown const\n\t\t// 6   0600      unknown seq(s)\n\t\t// 8   80026801  unknown fixed\n\t\t// 12  ed8d5c69  time in sec\n\t\t// 16  4c03      time in 1/1000\n\t\t// 18  0000\n\t\thdr, payload, err := client.ReadPacket()\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tswitch codec := hdr[0]; codec {\n\t\tcase tutk.CodecH264, tutk.CodecH265:\n\t\t\tif vcodec == nil {\n\t\t\t\tavcc := annexb.EncodeToAVCC(payload)\n\t\t\t\tif codec == tutk.CodecH264 {\n\t\t\t\t\tif h264.NALUType(avcc) == h264.NALUTypeSPS {\n\t\t\t\t\t\tvcodec = h264.AVCCToCodec(avcc)\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tif h265.NALUType(avcc) == h265.NALUTypeVPS {\n\t\t\t\t\t\tvcodec = h265.AVCCToCodec(avcc)\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\tcase tutk.CodecPCMA, codecXiaobaiPCMA:\n\t\t\tif acodec == nil {\n\t\t\t\tacodec = &core.Codec{Name: core.CodecPCMA, ClockRate: 8000}\n\t\t\t}\n\t\tcase tutk.CodecPCML:\n\t\t\tif acodec == nil {\n\t\t\t\tacodec = &core.Codec{Name: core.CodecPCML, ClockRate: 8000}\n\t\t\t}\n\t\tcase tutk.CodecAACLATM:\n\t\t\tif acodec == nil {\n\t\t\t\tacodec = aac.ADTSToCodec(payload)\n\t\t\t\tif acodec != nil {\n\t\t\t\t\tacodec.PayloadType = core.PayloadTypeRAW\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\n\t\tif vcodec != nil && acodec != nil {\n\t\t\tbreak\n\t\t}\n\t}\n\n\tmedias := []*core.Media{\n\t\t{\n\t\t\tKind:      core.KindVideo,\n\t\t\tDirection: core.DirectionRecvonly,\n\t\t\tCodecs:    []*core.Codec{vcodec},\n\t\t},\n\t\t{\n\t\t\tKind:      core.KindAudio,\n\t\t\tDirection: core.DirectionRecvonly,\n\t\t\tCodecs:    []*core.Codec{acodec},\n\t\t},\n\t}\n\treturn medias, nil\n}\n\nfunc (c *Producer) Protocol() string {\n\treturn \"tutk+udp\"\n}\n\nfunc (c *Producer) Start() error {\n\tvar audioTS uint32\n\tvar videoSeq, audioSeq uint16\n\n\tfor {\n\t\t_ = c.client.SetDeadline(time.Now().Add(5 * time.Second))\n\t\thdr, payload, err := c.client.ReadPacket()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tn := len(payload)\n\t\tc.Recv += n\n\n\t\t// TODO: rewrite this\n\t\tvar name string\n\t\tvar pkt *core.Packet\n\n\t\tswitch codec := hdr[0]; codec {\n\t\tcase tutk.CodecH264, tutk.CodecH265:\n\t\t\tpkt = &core.Packet{\n\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\tSequenceNumber: videoSeq,\n\t\t\t\t\tTimestamp:      core.Now90000(),\n\t\t\t\t},\n\t\t\t\tPayload: annexb.EncodeToAVCC(payload),\n\t\t\t}\n\t\t\tvideoSeq++\n\n\t\t\tif codec == tutk.CodecH264 {\n\t\t\t\tname = core.CodecH264\n\t\t\t} else {\n\t\t\t\tname = core.CodecH265\n\t\t\t}\n\n\t\tcase tutk.CodecPCMA, tutk.CodecPCML, codecXiaobaiPCMA:\n\t\t\tpkt = &core.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\tSequenceNumber: audioSeq,\n\t\t\t\t\tTimestamp:      audioTS,\n\t\t\t\t},\n\t\t\t\tPayload: payload,\n\t\t\t}\n\t\t\taudioSeq++\n\n\t\t\tswitch codec {\n\t\t\tcase tutk.CodecPCMA, codecXiaobaiPCMA:\n\t\t\t\tname = core.CodecPCMA\n\t\t\t\taudioTS += uint32(n)\n\t\t\tcase tutk.CodecPCML:\n\t\t\t\tname = core.CodecPCML\n\t\t\t\taudioTS += uint32(n / 2) // because 16bit\n\t\t\t}\n\n\t\tcase tutk.CodecAACLATM:\n\t\t\tpkt = &core.Packet{\n\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\tSequenceNumber: audioSeq,\n\t\t\t\t\tTimestamp:      audioTS,\n\t\t\t\t},\n\t\t\t\tPayload: payload,\n\t\t\t}\n\t\t\taudioSeq++\n\n\t\t\tname = core.CodecAAC\n\t\t\taudioTS += 1024\n\t\t}\n\n\t\tfor _, recv := range c.Receivers {\n\t\t\tif recv.Codec.Name == name {\n\t\t\t\trecv.WriteRTP(pkt)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (c *Producer) Stop() error {\n\t_ = c.client.StopMedia()\n\treturn c.Connection.Stop()\n}\n"
  },
  {
    "path": "pkg/xiaomi/miss/backchannel.go",
    "content": "package miss\n\nimport (\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/opus\"\n\t\"github.com/AlexxIT/go2rtc/pkg/pcm\"\n\t\"github.com/pion/rtp\"\n)\n\nfunc (p *Producer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {\n\tif err := p.client.StartSpeaker(); err != nil {\n\t\treturn err\n\t}\n\t// TODO: check this!!!\n\ttime.Sleep(time.Second)\n\n\tsender := core.NewSender(media, track.Codec)\n\n\tswitch track.Codec.Name {\n\tcase core.CodecPCMA:\n\t\tvar buf []byte\n\n\t\tif p.client.SpeakerCodec() == codecPCM {\n\t\t\tdst := &core.Codec{Name: core.CodecPCML, ClockRate: 8000}\n\t\t\ttranscode := pcm.Transcode(dst, track.Codec)\n\n\t\t\tsender.Handler = func(pkt *rtp.Packet) {\n\t\t\t\tbuf = append(buf, transcode(pkt.Payload)...)\n\t\t\t\tconst size = 2 * 8000 * 0.040 // 16bit 40ms\n\t\t\t\tfor len(buf) >= size {\n\t\t\t\t\tp.Send += size\n\t\t\t\t\t_ = p.client.WriteAudio(codecPCM, buf[:size])\n\t\t\t\t\tbuf = buf[size:]\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tsender.Handler = func(pkt *rtp.Packet) {\n\t\t\t\tbuf = append(buf, pkt.Payload...)\n\t\t\t\tconst size = 8000 * 0.040 // 8bit 40 ms\n\t\t\t\tfor len(buf) >= size {\n\t\t\t\t\tp.Send += size\n\t\t\t\t\t_ = p.client.WriteAudio(codecPCMA, buf[:size])\n\t\t\t\t\tbuf = buf[size:]\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\tcase core.CodecOpus:\n\t\tif p.client.SpeakerCodec() == codecOPUS {\n\t\t\tvar buf []byte\n\t\t\tsender.Handler = func(pkt *rtp.Packet) {\n\t\t\t\tif buf == nil {\n\t\t\t\t\tbuf = pkt.Payload\n\t\t\t\t} else {\n\t\t\t\t\t// convert two 20ms to one 40ms\n\t\t\t\t\tbuf = opus.JoinFrames(buf, pkt.Payload)\n\t\t\t\t\tp.Send += len(buf)\n\t\t\t\t\t_ = p.client.WriteAudio(codecOPUS, buf)\n\t\t\t\t\tbuf = nil\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tsender.Handler = func(pkt *rtp.Packet) {\n\t\t\t\tp.Send += len(pkt.Payload)\n\t\t\t\t_ = p.client.WriteAudio(codecOPUS, pkt.Payload)\n\t\t\t}\n\t\t}\n\t}\n\n\tsender.HandleRTP(track)\n\tp.Senders = append(p.Senders, sender)\n\treturn nil\n}\n"
  },
  {
    "path": "pkg/xiaomi/miss/client.go",
    "content": "package miss\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"encoding/hex\"\n\t\"errors\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/tutk\"\n\t\"github.com/AlexxIT/go2rtc/pkg/xiaomi/crypto\"\n\t\"github.com/AlexxIT/go2rtc/pkg/xiaomi/miss/cs2\"\n)\n\nconst (\n\tcodecH264 = 4\n\tcodecH265 = 5\n\tcodecPCM  = 1024\n\tcodecPCMU = 1026\n\tcodecPCMA = 1027\n\tcodecOPUS = 1032\n)\n\ntype Conn interface {\n\tProtocol() string\n\tVersion() string\n\tReadCommand() (cmd uint32, data []byte, err error)\n\tWriteCommand(cmd uint32, data []byte) error\n\tReadPacket() (hdr, payload []byte, err error)\n\tWritePacket(hdr, payload []byte) error\n\tRemoteAddr() net.Addr\n\tSetDeadline(t time.Time) error\n\tClose() error\n}\n\nfunc NewClient(rawURL string) (*Client, error) {\n\tu, err := url.Parse(rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 1. Check if we can create shared key.\n\tquery := u.Query()\n\tkey, err := crypto.CalcSharedKey(query.Get(\"device_public\"), query.Get(\"client_private\"))\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tmodel := query.Get(\"model\")\n\n\t// 2. Check if this vendor supported.\n\tvar conn Conn\n\tswitch s := query.Get(\"vendor\"); s {\n\tcase \"cs2\":\n\t\tconn, err = cs2.Dial(u.Host, query.Get(\"transport\"))\n\tcase \"tutk\":\n\t\tconn, err = tutk.Dial(u.Host, query.Get(\"uid\"), \"Miss\", \"client\")\n\tdefault:\n\t\terr = fmt.Errorf(\"miss: unsupported vendor %s\", s)\n\t}\n\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\terr = login(conn, query.Get(\"client_public\"), query.Get(\"sign\"))\n\tif err != nil {\n\t\t_ = conn.Close()\n\t\treturn nil, err\n\t}\n\n\treturn &Client{Conn: conn, key: key, model: model}, nil\n}\n\ntype Client struct {\n\tConn\n\tkey   []byte\n\tmodel string\n}\n\nconst (\n\tcmdAuthReq           = 0x100\n\tcmdAuthRes           = 0x101\n\tcmdVideoStart        = 0x102\n\tcmdVideoStop         = 0x103\n\tcmdAudioStart        = 0x104\n\tcmdAudioStop         = 0x105\n\tcmdSpeakerStartReq   = 0x106\n\tcmdSpeakerStartRes   = 0x107\n\tcmdSpeakerStop       = 0x108\n\tcmdStreamCtrlReq     = 0x109\n\tcmdStreamCtrlRes     = 0x10A\n\tcmdGetAudioFormatReq = 0x10B\n\tcmdGetAudioFormatRes = 0x10C\n\tcmdPlaybackReq       = 0x10D\n\tcmdPlaybackRes       = 0x10E\n\tcmdDevInfoReq        = 0x110\n\tcmdDevInfoRes        = 0x111\n\tcmdMotorReq          = 0x112\n\tcmdMotorRes          = 0x113\n\tcmdEncoded           = 0x1001\n)\n\nfunc login(conn Conn, clientPublic, sign string) error {\n\ts := fmt.Sprintf(`{\"public_key\":\"%s\",\"sign\":\"%s\",\"uuid\":\"\",\"support_encrypt\":0}`, clientPublic, sign)\n\tif err := conn.WriteCommand(cmdAuthReq, []byte(s)); err != nil {\n\t\treturn err\n\t}\n\n\t_, data, err := conn.ReadCommand()\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif !bytes.Contains(data, []byte(`\"result\":\"success\"`)) {\n\t\treturn fmt.Errorf(\"miss: auth: %s\", data)\n\t}\n\n\treturn nil\n}\n\nfunc (c *Client) Version() string {\n\treturn fmt.Sprintf(\"%s (%s)\", c.Conn.Version(), c.model)\n}\n\nfunc (c *Client) WriteCommand(data []byte) error {\n\tdata, err := crypto.Encode(data, c.key)\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn c.Conn.WriteCommand(cmdEncoded, data)\n}\n\nconst (\n\tModelDafang  = \"isa.camera.df3\"\n\tModelLoockV2 = \"loock.cateye.v02\"\n\tModelC200    = \"chuangmi.camera.046c04\"\n\tModelC300    = \"chuangmi.camera.72ac1\"\n\t// ModelXiaofang looks like it has the same firmware as the ModelDafang.\n\t// There is also an older model \"isa.camera.isc5\" that only works with the legacy protocol.\n\tModelXiaofang = \"isa.camera.isc5c1\"\n)\n\nfunc (c *Client) StartMedia(channel, quality, audio string) error {\n\tswitch c.model {\n\tcase ModelDafang, ModelXiaofang:\n\t\tvar q, a byte\n\t\tif quality == \"sd\" {\n\t\t\tq = 1 // 0 - hd, 1 - sd, default - hd\n\t\t}\n\t\tif audio != \"0\" {\n\t\t\ta = 1 // 0 - off, 1 - on, default - on\n\t\t}\n\n\t\treturn errors.Join(\n\t\t\tc.WriteCommand(dafangVideoQuality(q)),\n\t\t\tc.WriteCommand(dafangVideoStart(1, a)),\n\t\t)\n\t}\n\n\t// 0 - auto, 1 - sd, 2 - hd, default - hd\n\tswitch quality {\n\tcase \"\", \"hd\":\n\t\t// Some models have broken codec settings in quality 3.\n\t\t// Some models have low quality in quality 2.\n\t\t// Different models require different default quality settings.\n\t\tswitch c.model {\n\t\tcase ModelC200, ModelC300:\n\t\t\tquality = \"3\"\n\t\tdefault:\n\t\t\tquality = \"2\"\n\t\t}\n\tcase \"sd\":\n\t\tquality = \"1\"\n\tcase \"auto\":\n\t\tquality = \"0\"\n\t}\n\n\tif audio == \"\" {\n\t\taudio = \"1\"\n\t}\n\n\tdata := binary.BigEndian.AppendUint32(nil, cmdVideoStart)\n\tswitch channel {\n\tcase \"\", \"0\":\n\t\tdata = fmt.Appendf(data, `{\"videoquality\":%s,\"enableaudio\":%s}`, quality, audio)\n\tdefault:\n\t\tdata = fmt.Appendf(data, `{\"videoquality\":-1,\"videoquality2\":%s,\"enableaudio\":%s}`, quality, audio)\n\t}\n\treturn c.WriteCommand(data)\n}\n\nfunc (c *Client) StopMedia() error {\n\tdata := binary.BigEndian.AppendUint32(nil, cmdVideoStop)\n\treturn c.WriteCommand(data)\n}\n\nfunc (c *Client) StartAudio() error {\n\tdata := binary.BigEndian.AppendUint32(nil, cmdAudioStart)\n\treturn c.WriteCommand(data)\n}\n\nfunc (c *Client) StartSpeaker() error {\n\tdata := binary.BigEndian.AppendUint32(nil, cmdSpeakerStartReq)\n\treturn c.WriteCommand(data)\n}\n\n// SpeakerCodec if the camera model has a non-standard two-way codec.\nfunc (c *Client) SpeakerCodec() uint32 {\n\tswitch c.model {\n\tcase ModelDafang, ModelXiaofang, \"isa.camera.hlc6\":\n\t\treturn codecPCM\n\tcase \"chuangmi.camera.72ac1\":\n\t\treturn codecOPUS\n\t}\n\treturn 0\n}\n\nconst hdrSize = 32\n\nfunc (c *Client) ReadPacket() (*Packet, error) {\n\thdr, payload, err := c.Conn.ReadPacket()\n\tif err != nil {\n\t\treturn nil, fmt.Errorf(\"miss: read media: %w\", err)\n\t}\n\n\tif len(hdr) < hdrSize {\n\t\treturn nil, fmt.Errorf(\"miss: packet header too small\")\n\t}\n\n\tpayload, err = crypto.Decode(payload, c.key)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tpkt := &Packet{\n\t\tCodecID:  binary.LittleEndian.Uint32(hdr[4:]),\n\t\tSequence: binary.LittleEndian.Uint32(hdr[8:]),\n\t\tFlags:    binary.LittleEndian.Uint32(hdr[12:]),\n\t\tPayload:  payload,\n\t}\n\n\tswitch c.model {\n\tcase ModelDafang, ModelXiaofang, ModelLoockV2:\n\t\t// Dafang has ts in sec\n\t\t// LoockV2 has ts in msec for video, but zero ts for audio\n\t\tpkt.Timestamp = uint64(time.Now().UnixMilli())\n\tdefault:\n\t\tpkt.Timestamp = binary.LittleEndian.Uint64(hdr[16:])\n\t}\n\n\treturn pkt, nil\n}\n\nfunc (c *Client) WriteAudio(codecID uint32, payload []byte) error {\n\tpayload, err := crypto.Encode(payload, c.key) // new payload will have new size!\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tn := uint32(len(payload))\n\n\theader := make([]byte, hdrSize)\n\tbinary.LittleEndian.PutUint32(header, n)\n\tbinary.LittleEndian.PutUint32(header[4:], codecID)\n\tbinary.LittleEndian.PutUint64(header[16:], uint64(time.Now().UnixMilli())) // not really necessary\n\treturn c.Conn.WritePacket(header, payload)\n}\n\ntype Packet struct {\n\t//Length    uint32\n\tCodecID   uint32\n\tSequence  uint32\n\tFlags     uint32\n\tTimestamp uint64 // msec\n\t//TimestampS uint32\n\t//Reserved uint32\n\tPayload []byte\n}\n\nfunc (p *Packet) SampleRate() uint32 {\n\t// flag:         1 0011 000 - sample rate 16000\n\t// flag: 100 00 01 0000 000 - sample rate  8000\n\tv := (p.Flags >> 3) & 0b1111\n\tif v != 0 {\n\t\treturn 16000\n\t}\n\treturn 8000\n}\n\n//func (p *Packet) AudioUnknown1() byte {\n//\treturn byte((p.Flags >> 7) & 0b11)\n//}\n//\n//func (p *Packet) AudioUnknown2() byte {\n//\treturn byte((p.Flags >> 9) & 0b11)\n//}\n\nfunc dafangRaw(cmd uint32, args ...byte) []byte {\n\tpayload := tutk.ICAM(cmd, args...)\n\n\tdata := make([]byte, 4+len(payload)*2)\n\tcopy(data, \"\\x7f\\xff\\xff\\xff\")\n\thex.Encode(data[4:], payload)\n\treturn data\n}\n\n// DafangVideoQuality 0 - hd, 1 - sd\nfunc dafangVideoQuality(quality uint8) []byte {\n\treturn dafangRaw(0xff07d5, quality)\n}\n\nfunc dafangVideoStart(video, audio uint8) []byte {\n\treturn dafangRaw(0xff07d8, video, audio)\n}\n\n//func dafangLeft() []byte {\n//\treturn dafangRaw(0xff2404, 2, 0, 5)\n//}\n//\n//func dafangRight() []byte {\n//\treturn dafangRaw(0xff2404, 1, 0, 5)\n//}\n//\n//func dafangUp() []byte {\n//\treturn dafangRaw(0xff2404, 0, 2, 5)\n//}\n//\n//func dafangDown() []byte {\n//\treturn dafangRaw(0xff2404, 0, 1, 5)\n//}\n//\n//func dafangStop() []byte {\n//\treturn dafangRaw(0xff2404, 0, 0, 5)\n//}\n"
  },
  {
    "path": "pkg/xiaomi/miss/cs2/conn.go",
    "content": "package cs2\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"fmt\"\n\t\"io\"\n\t\"net\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n)\n\nfunc Dial(host, transport string) (*Conn, error) {\n\tconn, err := handshake(host, transport)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t_, isTCP := conn.(*tcpConn)\n\n\tc := &Conn{\n\t\tConn:  conn,\n\t\tisTCP: isTCP,\n\t\tchannels: [4]*dataChannel{\n\t\t\tnewDataChannel(0, 10), nil, newDataChannel(250, 100), nil,\n\t\t},\n\t}\n\tgo c.worker()\n\treturn c, nil\n}\n\ntype Conn struct {\n\tnet.Conn\n\tisTCP bool\n\n\terr    error\n\tseqCh0 uint16\n\tseqCh3 uint16\n\n\tchannels [4]*dataChannel\n\n\tcmdMu  sync.Mutex\n\tcmdAck func()\n}\n\nconst (\n\tmagic        = 0xF1\n\tmagicDrw     = 0xD1\n\tmagicTCP     = 0x68\n\tmsgLanSearch = 0x30\n\tmsgPunchPkt  = 0x41\n\tmsgP2PRdyUDP = 0x42\n\tmsgP2PRdyTCP = 0x43\n\tmsgDrw       = 0xD0\n\tmsgDrwAck    = 0xD1\n\tmsgPing      = 0xE0\n\tmsgPong      = 0xE1\n\tmsgClose     = 0xF0\n\tmsgCloseAck  = 0xF1\n)\n\nfunc handshake(host, transport string) (net.Conn, error) {\n\tconn, err := newUDPConn(host, 32108)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t_ = conn.SetDeadline(time.Now().Add(5 * time.Second))\n\n\treq := []byte{magic, msgLanSearch, 0, 0}\n\tres, err := conn.(*udpConn).WriteUntil(req, func(res []byte) bool {\n\t\treturn res[1] == msgPunchPkt\n\t})\n\tif err != nil {\n\t\t_ = conn.Close()\n\t\treturn nil, err\n\t}\n\n\tvar msgUDP, msgTCP byte\n\n\tif transport == \"\" || transport == \"udp\" {\n\t\tmsgUDP = msgP2PRdyUDP\n\t}\n\tif transport == \"\" || transport == \"tcp\" {\n\t\tmsgTCP = msgP2PRdyTCP\n\t}\n\n\tres, err = conn.(*udpConn).WriteUntil(res, func(res []byte) bool {\n\t\treturn res[1] == msgUDP || res[1] == msgTCP\n\t})\n\tif err != nil {\n\t\t_ = conn.Close()\n\t\treturn nil, err\n\t}\n\n\t_ = conn.SetDeadline(time.Time{})\n\n\tif res[1] == msgTCP {\n\t\t_ = conn.Close()\n\t\t//host := fmt.Sprintf(\"%d.%d.%d.%d:%d\", b[31], b[30], b[29], b[28], uint16(b[27])<<8|uint16(b[26]))\n\t\treturn newTCPConn(conn.RemoteAddr().String())\n\t}\n\n\treturn conn, nil\n}\n\nfunc (c *Conn) worker() {\n\tdefer func() {\n\t\tc.channels[0].Close()\n\t\tc.channels[2].Close()\n\t}()\n\n\tvar keepaliveTS time.Time // only for TCP\n\n\tbuf := make([]byte, 1200)\n\n\tfor {\n\t\tn, err := c.Conn.Read(buf)\n\t\tif err != nil {\n\t\t\tc.err = fmt.Errorf(\"%s: %w\", \"cs2\", err)\n\t\t\treturn\n\t\t}\n\n\t\t// 0  f1d0  magic\n\t\t// 2  005d  size = total size + 4\n\t\t// 4  d1    magic\n\t\t// 5  00    channel\n\t\t// 6  0000  seq\n\t\tswitch buf[1] {\n\t\tcase msgDrw:\n\t\t\tch := buf[5]\n\t\t\tchannel := c.channels[ch]\n\n\t\t\tif c.isTCP {\n\t\t\t\t// For TCP we should send ping every second to keep connection alive.\n\t\t\t\t// Based on PCAP analysis: official Mi Home app sends PING every ~1s.\n\t\t\t\tif now := time.Now(); now.After(keepaliveTS) {\n\t\t\t\t\t_, _ = c.Conn.Write([]byte{magic, msgPing, 0, 0})\n\t\t\t\t\tkeepaliveTS = now.Add(time.Second)\n\t\t\t\t}\n\n\t\t\t\terr = channel.Push(buf[8:n])\n\t\t\t} else {\n\t\t\t\tvar pushed int\n\n\t\t\t\tseqHI, seqLO := buf[6], buf[7]\n\t\t\t\tseq := uint16(seqHI)<<8 | uint16(seqLO)\n\t\t\t\tpushed, err = channel.PushSeq(seq, buf[8:n])\n\n\t\t\t\tif pushed >= 0 {\n\t\t\t\t\t// For UDP we should send ACK.\n\t\t\t\t\tack := []byte{magic, msgDrwAck, 0, 6, magicDrw, ch, 0, 1, seqHI, seqLO}\n\t\t\t\t\t_, _ = c.Conn.Write(ack)\n\t\t\t\t}\n\t\t\t}\n\n\t\t\tif err != nil {\n\t\t\t\tc.err = fmt.Errorf(\"%s: %w\", \"cs2\", err)\n\t\t\t\treturn\n\t\t\t}\n\n\t\tcase msgPing:\n\t\t\t_, _ = c.Conn.Write([]byte{magic, msgPong, 0, 0})\n\t\tcase msgPong, msgP2PRdyUDP, msgP2PRdyTCP, msgClose, msgCloseAck: // skip it\n\t\tcase msgDrwAck: // only for UDP\n\t\t\tif c.cmdAck != nil {\n\t\t\t\tc.cmdAck()\n\t\t\t}\n\t\tdefault:\n\t\t\tfmt.Printf(\"%s: unknown msg: %x\\n\", \"cs2\", buf[:n])\n\t\t}\n\t}\n}\n\nfunc (c *Conn) Protocol() string {\n\tif c.isTCP {\n\t\treturn \"cs2+tcp\"\n\t}\n\treturn \"cs2+udp\"\n}\n\nfunc (c *Conn) Version() string {\n\treturn \"CS2\"\n}\n\nfunc (c *Conn) Error() error {\n\tif c.err != nil {\n\t\treturn c.err\n\t}\n\treturn io.EOF\n}\n\nfunc (c *Conn) ReadCommand() (cmd uint32, data []byte, err error) {\n\tbuf, ok := c.channels[0].Pop()\n\tif !ok {\n\t\treturn 0, nil, c.Error()\n\t}\n\tcmd = binary.LittleEndian.Uint32(buf)\n\tdata = buf[4:]\n\treturn\n}\n\nfunc (c *Conn) WriteCommand(cmd uint32, data []byte) error {\n\tc.cmdMu.Lock()\n\tdefer c.cmdMu.Unlock()\n\n\treq := marshalCmd(0, c.seqCh0, cmd, data)\n\tc.seqCh0++\n\n\tif c.isTCP {\n\t\t_, err := c.Conn.Write(req)\n\t\treturn err\n\t}\n\n\tvar repeat atomic.Int32\n\trepeat.Store(5)\n\n\ttimeout := time.NewTicker(time.Second)\n\tdefer timeout.Stop()\n\n\tc.cmdAck = func() {\n\t\trepeat.Store(0)\n\t\ttimeout.Reset(1)\n\t}\n\n\tfor {\n\t\tif _, err := c.Conn.Write(req); err != nil {\n\t\t\treturn err\n\t\t}\n\t\t<-timeout.C\n\t\tr := repeat.Add(-1)\n\t\tif r < 0 {\n\t\t\treturn nil\n\t\t}\n\t\tif r == 0 {\n\t\t\treturn fmt.Errorf(\"%s: can't send command %d\", \"cs2\", cmd)\n\t\t}\n\t}\n}\n\nconst hdrSize = 32\n\nfunc (c *Conn) ReadPacket() (hdr, payload []byte, err error) {\n\tdata, ok := c.channels[2].Pop()\n\tif !ok {\n\t\treturn nil, nil, c.Error()\n\t}\n\treturn data[:hdrSize], data[hdrSize:], nil\n}\n\nfunc (c *Conn) WritePacket(hdr, payload []byte) error {\n\tconst offset = 12\n\n\tn := hdrSize + uint32(len(payload))\n\treq := make([]byte, n+offset)\n\treq[0] = magic\n\treq[1] = msgDrw\n\tbinary.BigEndian.PutUint16(req[2:], uint16(n+8))\n\n\treq[4] = magicDrw\n\treq[5] = 3 // channel\n\tbinary.BigEndian.PutUint16(req[6:], c.seqCh3)\n\tc.seqCh3++\n\tbinary.BigEndian.PutUint32(req[8:], n)\n\tcopy(req[offset:], hdr)\n\tcopy(req[offset+hdrSize:], hdr)\n\n\t_, err := c.Conn.Write(req)\n\treturn err\n}\n\nfunc marshalCmd(channel byte, seq uint16, cmd uint32, payload []byte) []byte {\n\tsize := len(payload)\n\treq := make([]byte, 4+4+4+4+size)\n\n\t// 1. message header (4 bytes)\n\treq[0] = magic\n\treq[1] = msgDrw\n\tbinary.BigEndian.PutUint16(req[2:], uint16(4+4+4+size))\n\n\t// 2. drw? header (4 bytes)\n\treq[4] = magicDrw\n\treq[5] = channel\n\tbinary.BigEndian.PutUint16(req[6:], seq)\n\n\t// 3. payload size (4 bytes)\n\tbinary.BigEndian.PutUint32(req[8:], uint32(4+size))\n\n\t// 4. payload command (4 bytes)\n\tbinary.BigEndian.PutUint32(req[12:], cmd)\n\n\t// 5. payload\n\tcopy(req[16:], payload)\n\n\treturn req\n}\n\nfunc newUDPConn(host string, port int) (net.Conn, error) {\n\t// We using raw net.UDPConn, because RemoteAddr should be changed during handshake.\n\tconn, err := net.ListenUDP(\"udp\", nil)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\taddr, err := net.ResolveUDPAddr(\"udp\", host)\n\tif err != nil {\n\t\taddr = &net.UDPAddr{IP: net.ParseIP(host), Port: port}\n\t}\n\n\treturn &udpConn{UDPConn: conn, addr: addr}, nil\n}\n\ntype udpConn struct {\n\t*net.UDPConn\n\taddr *net.UDPAddr\n}\n\nfunc (c *udpConn) Read(b []byte) (n int, err error) {\n\tvar addr *net.UDPAddr\n\tfor {\n\t\tn, addr, err = c.UDPConn.ReadFromUDP(b)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\n\t\tif string(addr.IP) == string(c.addr.IP) || n >= 8 {\n\t\t\t//log.Printf(\"<- %x\", b[:n])\n\t\t\treturn\n\t\t}\n\t}\n}\n\nfunc (c *udpConn) Write(b []byte) (n int, err error) {\n\t//log.Printf(\"-> %x\", b)\n\treturn c.UDPConn.WriteToUDP(b, c.addr)\n}\n\nfunc (c *udpConn) RemoteAddr() net.Addr {\n\treturn c.addr\n}\n\nfunc (c *udpConn) WriteUntil(req []byte, ok func(res []byte) bool) ([]byte, error) {\n\tvar t *time.Timer\n\tt = time.AfterFunc(1, func() {\n\t\tif _, err := c.Write(req); err == nil && t != nil {\n\t\t\tt.Reset(time.Second)\n\t\t}\n\t})\n\tdefer t.Stop()\n\n\tbuf := make([]byte, 1200)\n\n\tfor {\n\t\tn, addr, err := c.UDPConn.ReadFromUDP(buf)\n\t\tif err != nil {\n\t\t\treturn nil, err\n\t\t}\n\n\t\tif string(addr.IP) != string(c.addr.IP) || n < 16 {\n\t\t\tcontinue // skip messages from another IP\n\t\t}\n\n\t\tif ok(buf[:n]) {\n\t\t\tc.addr.Port = addr.Port\n\t\t\treturn buf[:n], nil\n\t\t}\n\t}\n}\n\nfunc newTCPConn(addr string) (net.Conn, error) {\n\tconn, err := net.DialTimeout(\"tcp\", addr, 3*time.Second)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\treturn &tcpConn{conn.(*net.TCPConn), bufio.NewReader(conn)}, nil\n}\n\ntype tcpConn struct {\n\t*net.TCPConn\n\trd *bufio.Reader\n}\n\nfunc (c *tcpConn) Read(p []byte) (n int, err error) {\n\ttmp := make([]byte, 8)\n\tif _, err = io.ReadFull(c.rd, tmp); err != nil {\n\t\treturn\n\t}\n\tn = int(binary.BigEndian.Uint16(tmp))\n\tif len(p) < n {\n\t\treturn 0, fmt.Errorf(\"tcp: buffer too small\")\n\t}\n\t_, err = io.ReadFull(c.rd, p[:n])\n\t//log.Printf(\"<- %x%x\", tmp, p[:n])\n\treturn\n}\n\nfunc (c *tcpConn) Write(req []byte) (n int, err error) {\n\tn = len(req)\n\tbuf := make([]byte, 8+n)\n\tbinary.BigEndian.PutUint16(buf, uint16(n))\n\tbuf[2] = magicTCP\n\tcopy(buf[8:], req)\n\t//log.Printf(\"-> %x\", buf)\n\t_, err = c.TCPConn.Write(buf)\n\treturn\n}\n\nfunc newDataChannel(pushSize, popSize int) *dataChannel {\n\tc := &dataChannel{}\n\tif pushSize > 0 {\n\t\tc.pushBuf = make(map[uint16][]byte, pushSize)\n\t\tc.pushSize = pushSize\n\t}\n\tif popSize >= 0 {\n\t\tc.popBuf = make(chan []byte, popSize)\n\t}\n\treturn c\n}\n\ntype dataChannel struct {\n\twaitSeq  uint16\n\tpushBuf  map[uint16][]byte\n\tpushSize int\n\n\twaitData []byte\n\twaitSize int\n\tpopBuf   chan []byte\n}\n\nfunc (c *dataChannel) Push(b []byte) error {\n\tc.waitData = append(c.waitData, b...)\n\n\tfor len(c.waitData) > 4 {\n\t\t// Every new data starts with size. There can be several data inside one packet.\n\t\tif c.waitSize == 0 {\n\t\t\tc.waitSize = int(binary.BigEndian.Uint32(c.waitData))\n\t\t\tc.waitData = c.waitData[4:]\n\t\t}\n\t\tif c.waitSize > len(c.waitData) {\n\t\t\tbreak\n\t\t}\n\n\t\tselect {\n\t\tcase c.popBuf <- c.waitData[:c.waitSize]:\n\t\tdefault:\n\t\t\treturn fmt.Errorf(\"pop buffer is full\")\n\t\t}\n\n\t\tc.waitData = c.waitData[c.waitSize:]\n\t\tc.waitSize = 0\n\t}\n\treturn nil\n}\n\nfunc (c *dataChannel) Pop() ([]byte, bool) {\n\tdata, ok := <-c.popBuf\n\treturn data, ok\n}\n\nfunc (c *dataChannel) Close() {\n\tclose(c.popBuf)\n}\n\n// PushSeq returns how many seq were processed.\n// Returns 0 if seq was saved or processed earlier.\n// Returns -1 if seq could not be saved (buffer full or disabled).\nfunc (c *dataChannel) PushSeq(seq uint16, data []byte) (int, error) {\n\tdiff := int16(seq - c.waitSeq)\n\t// Check if this is seq from the future.\n\tif diff > 0 {\n\t\t// Support disabled buffer.\n\t\tif c.pushSize == 0 {\n\t\t\treturn -1, nil // couldn't save seq\n\t\t}\n\t\t// Check if we don't have this seq in the buffer.\n\t\tif c.pushBuf[seq] == nil {\n\t\t\t// Check if there is enough space in the buffer.\n\t\t\tif len(c.pushBuf) == c.pushSize {\n\t\t\t\treturn -1, nil // couldn't save seq\n\t\t\t}\n\t\t\tc.pushBuf[seq] = bytes.Clone(data)\n\t\t\t//log.Printf(\"push buf wait=%d seq=%d len=%d\", c.waitSeq, seq, len(c.pushBuf))\n\t\t}\n\t\treturn 0, nil\n\t}\n\n\t// Check if this is seq from the past.\n\tif diff < 0 {\n\t\treturn 0, nil\n\t}\n\n\tfor i := 1; ; i++ {\n\t\tif err := c.Push(data); err != nil {\n\t\t\treturn i, err\n\t\t}\n\t\tc.waitSeq++\n\t\t// Check if we have next seq in the buffer.\n\t\tif data = c.pushBuf[c.waitSeq]; data != nil {\n\t\t\tdelete(c.pushBuf, c.waitSeq)\n\t\t} else {\n\t\t\treturn i, nil\n\t\t}\n\t}\n}\n"
  },
  {
    "path": "pkg/xiaomi/miss/producer.go",
    "content": "package miss\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h264/annexb\"\n\t\"github.com/AlexxIT/go2rtc/pkg/h265\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Producer struct {\n\tcore.Connection\n\tclient *Client\n}\n\nfunc Dial(rawURL string) (core.Producer, error) {\n\tclient, err := NewClient(rawURL)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tu, _ := url.Parse(rawURL)\n\tquery := u.Query()\n\n\terr = client.StartMedia(query.Get(\"channel\"), query.Get(\"subtype\"), query.Get(\"audio\"))\n\tif err != nil {\n\t\t_ = client.Close()\n\t\treturn nil, err\n\t}\n\n\tmedias, err := probe(client, query.Get(\"audio\") != \"0\")\n\tif err != nil {\n\t\t_ = client.Close()\n\t\treturn nil, err\n\t}\n\n\treturn &Producer{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"xiaomi/miss\",\n\t\t\tProtocol:   client.Protocol(),\n\t\t\tRemoteAddr: client.RemoteAddr().String(),\n\t\t\tUserAgent:  client.Version(),\n\t\t\tMedias:     medias,\n\t\t\tTransport:  client,\n\t\t},\n\t\tclient: client,\n\t}, nil\n}\n\nfunc probe(client *Client, audio bool) ([]*core.Media, error) {\n\t_ = client.SetDeadline(time.Now().Add(15 * time.Second))\n\n\tvar vcodec, acodec *core.Codec\n\n\tfor {\n\t\tpkt, err := client.ReadPacket()\n\t\tif err != nil {\n\t\t\tif vcodec != nil {\n\t\t\t\terr = fmt.Errorf(\"no audio\")\n\t\t\t} else if acodec != nil {\n\t\t\t\terr = fmt.Errorf(\"no video\")\n\t\t\t}\n\t\t\treturn nil, fmt.Errorf(\"xiaomi: probe: %w\", err)\n\t\t}\n\n\t\tswitch pkt.CodecID {\n\t\tcase codecH264:\n\t\t\tif vcodec == nil {\n\t\t\t\tbuf := annexb.EncodeToAVCC(pkt.Payload)\n\t\t\t\tif h264.NALUType(buf) == h264.NALUTypeSPS {\n\t\t\t\t\tvcodec = h264.AVCCToCodec(buf)\n\t\t\t\t}\n\t\t\t}\n\t\tcase codecH265:\n\t\t\tif vcodec == nil {\n\t\t\t\tbuf := annexb.EncodeToAVCC(pkt.Payload)\n\t\t\t\tif h265.NALUType(buf) == h265.NALUTypeVPS {\n\t\t\t\t\tvcodec = h265.AVCCToCodec(buf)\n\t\t\t\t}\n\t\t\t}\n\t\tcase codecPCMA:\n\t\t\tif acodec == nil {\n\t\t\t\tacodec = &core.Codec{Name: core.CodecPCMA, ClockRate: pkt.SampleRate()}\n\t\t\t}\n\t\tcase codecOPUS:\n\t\t\tif acodec == nil {\n\t\t\t\tacodec = &core.Codec{Name: core.CodecOpus, ClockRate: 48000, Channels: 2}\n\t\t\t}\n\t\t}\n\n\t\tif vcodec != nil && (acodec != nil || !audio) {\n\t\t\tbreak\n\t\t}\n\t}\n\n\t_ = client.SetDeadline(time.Time{})\n\n\tmedias := []*core.Media{\n\t\t{\n\t\t\tKind:      core.KindVideo,\n\t\t\tDirection: core.DirectionRecvonly,\n\t\t\tCodecs:    []*core.Codec{vcodec},\n\t\t},\n\t}\n\n\tif acodec != nil {\n\t\tmedias = append(medias, &core.Media{\n\t\t\tKind:      core.KindAudio,\n\t\t\tDirection: core.DirectionRecvonly,\n\t\t\tCodecs:    []*core.Codec{acodec},\n\t\t})\n\n\t\tmedias = append(medias, &core.Media{\n\t\t\tKind:      core.KindAudio,\n\t\t\tDirection: core.DirectionSendonly,\n\t\t\tCodecs:    []*core.Codec{acodec.Clone()},\n\t\t})\n\t}\n\n\treturn medias, nil\n}\n\nconst timestamp40ms = 48000 * 0.040\n\nfunc (p *Producer) Start() error {\n\tvar audioTS uint32\n\n\tfor {\n\t\t_ = p.client.SetDeadline(time.Now().Add(10 * time.Second))\n\t\tpkt, err := p.client.ReadPacket()\n\t\tif err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tp.Recv += len(pkt.Payload)\n\n\t\t// TODO: rewrite this\n\t\tvar name string\n\t\tvar pkt2 *core.Packet\n\n\t\tswitch pkt.CodecID {\n\t\tcase codecH264, codecH265:\n\t\t\tpkt2 = &core.Packet{\n\t\t\t\tHeader: rtp.Header{\n\t\t\t\t\tSequenceNumber: uint16(pkt.Sequence),\n\t\t\t\t\tTimestamp:      TimeToRTP(pkt.Timestamp, 90000),\n\t\t\t\t},\n\t\t\t\tPayload: annexb.EncodeToAVCC(pkt.Payload),\n\t\t\t}\n\t\t\tif pkt.CodecID == codecH264 {\n\t\t\t\tname = core.CodecH264\n\t\t\t} else {\n\t\t\t\tname = core.CodecH265\n\t\t\t}\n\t\tcase codecPCMA:\n\t\t\tname = core.CodecPCMA\n\t\t\tpkt2 = &core.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\tSequenceNumber: uint16(pkt.Sequence),\n\t\t\t\t\tTimestamp:      audioTS,\n\t\t\t\t},\n\t\t\t\tPayload: pkt.Payload,\n\t\t\t}\n\t\t\taudioTS += uint32(len(pkt.Payload))\n\t\tcase codecOPUS:\n\t\t\tname = core.CodecOpus\n\t\t\tpkt2 = &core.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\tSequenceNumber: uint16(pkt.Sequence),\n\t\t\t\t\tTimestamp:      audioTS,\n\t\t\t\t},\n\t\t\t\tPayload: pkt.Payload,\n\t\t\t}\n\t\t\t// known cameras sends packets with 40ms long\n\t\t\taudioTS += timestamp40ms\n\t\t}\n\n\t\tfor _, recv := range p.Receivers {\n\t\t\tif recv.Codec.Name == name {\n\t\t\t\trecv.WriteRTP(pkt2)\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t}\n}\n\nfunc (p *Producer) Stop() error {\n\t_ = p.client.StopMedia()\n\treturn p.Connection.Stop()\n}\n\n// TimeToRTP convert time in milliseconds to RTP time\nfunc TimeToRTP(timeMS, clockRate uint64) uint32 {\n\treturn uint32(timeMS * clockRate / 1000)\n}\n"
  },
  {
    "path": "pkg/xiaomi/producer.go",
    "content": "package xiaomi\n\nimport (\n\t\"strings\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/AlexxIT/go2rtc/pkg/xiaomi/legacy\"\n\t\"github.com/AlexxIT/go2rtc/pkg/xiaomi/miss\"\n)\n\nfunc Dial(rawURL string) (core.Producer, error) {\n\t// Format: xiaomi/miss\n\tif strings.Contains(rawURL, \"vendor\") {\n\t\treturn miss.Dial(rawURL)\n\t}\n\n\t// Format: xiaomi/legacy\n\treturn legacy.Dial(rawURL)\n}\n\nfunc IsLegacy(model string) bool {\n\treturn legacy.Supported(model)\n}\n"
  },
  {
    "path": "pkg/xnet/net.go",
    "content": "package xnet\n\nimport (\n\t\"net\"\n\t\"strconv\"\n)\n\n// Docker has common docker addresses (class B):\n// https://en.wikipedia.org/wiki/Private_network\n// - docker0 172.17.0.1/16\n// - br-xxxx 172.18.0.1/16\n// - hassio  172.30.32.1/23\nvar Docker = net.IPNet{\n\tIP:   []byte{172, 16, 0, 0},\n\tMask: []byte{255, 240, 0, 0},\n}\n\n// ParseUnspecifiedPort will return port if address is unspecified\n// ex. \":8555\" or \"0.0.0.0:8555\"\nfunc ParseUnspecifiedPort(address string) int {\n\thost, port, err := net.SplitHostPort(address)\n\tif err != nil {\n\t\treturn 0\n\t}\n\n\tif host != \"\" && host != \"0.0.0.0\" && host != \"[::]\" {\n\t\treturn 0\n\t}\n\n\ti, _ := strconv.Atoi(port)\n\treturn i\n}\n\nfunc IPNets(ipFilter func(ip net.IP) bool) ([]*net.IPNet, error) {\n\tifaces, err := net.Interfaces()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar nets []*net.IPNet\n\n\tfor _, iface := range ifaces {\n\t\tif iface.Flags&net.FlagUp == 0 || iface.Flags&net.FlagLoopback != 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\taddrs, _ := iface.Addrs() // range on nil slice is OK\n\t\tfor _, addr := range addrs {\n\t\t\tswitch v := addr.(type) {\n\t\t\tcase *net.IPNet:\n\t\t\t\tip := v.IP.To4()\n\t\t\t\tif ip == nil {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tif ipFilter != nil && !ipFilter(ip) {\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\t\tnets = append(nets, v)\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nets, nil\n}\n"
  },
  {
    "path": "pkg/xnet/tls/tls.go",
    "content": "package tls\n\nimport (\n\t\"crypto/rand\"\n\t\"crypto/rsa\"\n\t\"crypto/tls\"\n\t\"crypto/x509\"\n\t\"crypto/x509/pkix\"\n\t\"encoding/pem\"\n\t\"math/big\"\n\t\"net\"\n\t\"time\"\n)\n\nfunc CreateCertificate() (*tls.Certificate, error) {\n\t// 1. Generate an RSA private key\n\tprivateKey, err := rsa.GenerateKey(rand.Reader, 2048)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// 2. Define the certificate template\n\tserialNumberLimit := new(big.Int).Lsh(big.NewInt(1), 128)\n\tserialNumber, err := rand.Int(rand.Reader, serialNumberLimit)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\ttemplate := x509.Certificate{\n\t\tSerialNumber: serialNumber,\n\t\tSubject: pkix.Name{\n\t\t\tOrganization: []string{\"home\"},\n\t\t\tCommonName:   \"localhost\",\n\t\t},\n\t\tNotBefore: time.Now(),\n\t\tNotAfter:  time.Now().Add(365 * 24 * time.Hour), // Valid for 1 year\n\n\t\tKeyUsage:              x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,\n\t\tExtKeyUsage:           []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},\n\t\tBasicConstraintsValid: true,\n\n\t\t// Add localhost as a valid IP and DNS name\n\t\tIPAddresses: []net.IP{[]byte{127, 0, 0, 1}},\n\t\tDNSNames:    []string{\"localhost\"},\n\t}\n\n\t// 3. Create a self-signed certificate\n\t// The parent is the template itself, and we use the generated public and private keys.\n\tderBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, &privateKey.PublicKey, privateKey)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tderBytes = pem.EncodeToMemory(&pem.Block{Type: \"CERTIFICATE\", Bytes: derBytes})\n\tkeyBytes := pem.EncodeToMemory(&pem.Block{Type: \"RSA PRIVATE KEY\", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)})\n\n\tcert, err := tls.X509KeyPair(derBytes, keyBytes)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &cert, nil\n}\n"
  },
  {
    "path": "pkg/y4m/README.md",
    "content": "## Planar YUV formats\n\nPacked YUV - yuyv422 - YUYV 4:2:2\nSemi-Planar - nv12 - Y/CbCr 4:2:0\nPlanar YUV - yuv420p - Planar YUV 4:2:0 - aka. [cosited](https://manned.org/yuv4mpeg.5)\n\n```\n[video4linux2,v4l2 @ 0x55fddc42a940] Raw       :     yuyv422 :           YUYV 4:2:2 : 1920x1080\n[video4linux2,v4l2 @ 0x55fddc42a940] Raw       :        nv12 :         Y/CbCr 4:2:0 : 1920x1080\n[video4linux2,v4l2 @ 0x55fddc42a940] Raw       :     yuv420p :     Planar YUV 4:2:0 : 1920x1080\n```\n\n## Useful links\n\n- https://learn.microsoft.com/en-us/windows/win32/medfound/recommended-8-bit-yuv-formats-for-video-rendering\n- https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Video_concepts\n- https://fourcc.org/yuv.php#YV12\n- https://docs.kernel.org/userspace-api/media/v4l/pixfmt-yuv-planar.html\n- https://gist.github.com/Jim-Bar/3cbba684a71d1a9d468a6711a6eddbeb\n"
  },
  {
    "path": "pkg/y4m/consumer.go",
    "content": "package y4m\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/pion/rtp\"\n)\n\ntype Consumer struct {\n\tcore.Connection\n\twr *core.WriteBuffer\n}\n\nfunc NewConsumer() *Consumer {\n\twr := core.NewWriteBuffer(nil)\n\treturn &Consumer{\n\t\tcore.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tTransport:  wr,\n\t\t\tFormatName: \"yuv4mpegpipe\",\n\t\t\tMedias: []*core.Media{\n\t\t\t\t{\n\t\t\t\t\tKind:      core.KindVideo,\n\t\t\t\t\tDirection: core.DirectionSendonly,\n\t\t\t\t\tCodecs: []*core.Codec{\n\t\t\t\t\t\t{Name: core.CodecRAW},\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t\twr,\n\t}\n}\n\nfunc (c *Consumer) AddTrack(media *core.Media, _ *core.Codec, track *core.Receiver) error {\n\tsender := core.NewSender(media, track.Codec)\n\tsender.Handler = func(packet *rtp.Packet) {\n\t\tif n, err := c.wr.Write([]byte(frameHdr)); err == nil {\n\t\t\tc.Send += n\n\t\t}\n\t\tif n, err := c.wr.Write(packet.Payload); err == nil {\n\t\t\tc.Send += n\n\t\t}\n\t}\n\n\thdr := fmt.Sprintf(\n\t\t\"YUV4MPEG2 W%s H%s C%s\\n\",\n\t\tcore.Between(track.Codec.FmtpLine, \"width=\", \";\"),\n\t\tcore.Between(track.Codec.FmtpLine, \"height=\", \";\"),\n\t\tcore.Between(track.Codec.FmtpLine, \"colorspace=\", \";\"),\n\t)\n\tif _, err := c.wr.Write([]byte(hdr)); err != nil {\n\t\treturn err\n\t}\n\n\tsender.HandleRTP(track)\n\tc.Senders = append(c.Senders, sender)\n\treturn nil\n}\n\nfunc (c *Consumer) WriteTo(wr io.Writer) (int64, error) {\n\treturn c.wr.WriteTo(wr)\n}\n"
  },
  {
    "path": "pkg/y4m/producer.go",
    "content": "package y4m\n\nimport (\n\t\"bufio\"\n\t\"errors\"\n\t\"io\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n\t\"github.com/pion/rtp\"\n)\n\nfunc Open(r io.Reader) (*Producer, error) {\n\trd := bufio.NewReaderSize(r, core.BufferSize)\n\tb, err := rd.ReadBytes('\\n')\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tb = b[:len(b)-1] // remove \\n\n\n\tfmtp := ParseHeader(b)\n\n\tif GetSize(fmtp) == 0 {\n\t\treturn nil, errors.New(\"y4m: unsupported format: \" + string(b))\n\t}\n\n\tmedias := []*core.Media{\n\t\t{\n\t\t\tKind:      core.KindVideo,\n\t\t\tDirection: core.DirectionRecvonly,\n\t\t\tCodecs: []*core.Codec{\n\t\t\t\t{\n\t\t\t\t\tName:        core.CodecRAW,\n\t\t\t\t\tClockRate:   90000,\n\t\t\t\t\tFmtpLine:    fmtp,\n\t\t\t\t\tPayloadType: core.PayloadTypeRAW,\n\t\t\t\t},\n\t\t\t},\n\t\t},\n\t}\n\treturn &Producer{\n\t\tConnection: core.Connection{\n\t\t\tID:         core.NewID(),\n\t\t\tFormatName: \"yuv4mpegpipe\",\n\t\t\tMedias:     medias,\n\t\t\tSDP:        string(b),\n\t\t\tTransport:  r,\n\t\t},\n\t\trd: rd,\n\t}, nil\n}\n\ntype Producer struct {\n\tcore.Connection\n\trd *bufio.Reader\n}\n\nfunc (c *Producer) Start() error {\n\tsize := GetSize(c.Medias[0].Codecs[0].FmtpLine)\n\n\tfor {\n\t\tif _, err := c.rd.Discard(len(frameHdr)); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tframe := make([]byte, size)\n\t\tif _, err := io.ReadFull(c.rd, frame); err != nil {\n\t\t\treturn err\n\t\t}\n\n\t\tc.Recv += size\n\n\t\tif len(c.Receivers) == 0 {\n\t\t\tcontinue\n\t\t}\n\n\t\tpkt := &rtp.Packet{\n\t\t\tHeader:  rtp.Header{Timestamp: core.Now90000()},\n\t\t\tPayload: frame,\n\t\t}\n\t\tc.Receivers[0].WriteRTP(pkt)\n\t}\n}\n"
  },
  {
    "path": "pkg/y4m/y4m.go",
    "content": "package y4m\n\nimport (\n\t\"bytes\"\n\t\"image\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\nconst FourCC = \"YUV4\"\n\nconst frameHdr = \"FRAME\\n\"\n\nfunc ParseHeader(b []byte) (fmtp string) {\n\tfor b != nil {\n\t\t// YUV4MPEG2 W1280 H720 F24:1 Ip A1:1 C420mpeg2 XYSCSS=420MPEG2\n\t\t// https://manned.org/yuv4mpeg.5\n\t\t// https://github.com/FFmpeg/FFmpeg/blob/master/libavformat/yuv4mpegenc.c\n\t\tkey := b[0]\n\n\t\tvar value string\n\t\tif i := bytes.IndexByte(b, ' '); i > 0 {\n\t\t\tvalue = string(b[1:i])\n\t\t\tb = b[i+1:]\n\t\t} else {\n\t\t\tvalue = string(b[1:])\n\t\t\tb = nil\n\t\t}\n\n\t\tswitch key {\n\t\tcase 'W':\n\t\t\tfmtp = \"width=\" + value\n\t\tcase 'H':\n\t\t\tfmtp += \";height=\" + value\n\t\tcase 'C':\n\t\t\tfmtp += \";colorspace=\" + value\n\t\t}\n\t}\n\treturn\n}\n\nfunc GetSize(fmtp string) int {\n\tw := core.Atoi(core.Between(fmtp, \"width=\", \";\"))\n\th := core.Atoi(core.Between(fmtp, \"height=\", \";\"))\n\n\tswitch core.Between(fmtp, \"colorspace=\", \";\") {\n\tcase \"mono\":\n\t\treturn w * h\n\tcase \"420mpeg2\", \"420jpeg\":\n\t\treturn w * h * 3 / 2\n\tcase \"422\":\n\t\treturn w * h * 2\n\tcase \"444\":\n\t\treturn w * h * 3\n\t}\n\n\treturn 0\n}\n\nfunc NewImage(fmtp string) func(frame []byte) image.Image {\n\tw := core.Atoi(core.Between(fmtp, \"width=\", \";\"))\n\th := core.Atoi(core.Between(fmtp, \"height=\", \";\"))\n\trect := image.Rect(0, 0, w, h)\n\n\tswitch core.Between(fmtp, \"colorspace=\", \";\") {\n\tcase \"mono\":\n\t\treturn func(frame []byte) image.Image {\n\t\t\treturn &image.Gray{\n\t\t\t\tPix:    frame,\n\t\t\t\tStride: w,\n\t\t\t\tRect:   rect,\n\t\t\t}\n\t\t}\n\tcase \"420mpeg2\", \"420jpeg\":\n\t\ti1 := w * h\n\t\ti2 := i1 + i1/4\n\t\ti3 := i2 + i1/4\n\n\t\treturn func(frame []byte) image.Image {\n\t\t\treturn &image.YCbCr{\n\t\t\t\tY:              frame[:i1],\n\t\t\t\tCb:             frame[i1:i2],\n\t\t\t\tCr:             frame[i2:i3],\n\t\t\t\tYStride:        w,\n\t\t\t\tCStride:        w / 2,\n\t\t\t\tSubsampleRatio: image.YCbCrSubsampleRatio420,\n\t\t\t\tRect:           rect,\n\t\t\t}\n\t\t}\n\tcase \"422\":\n\t\ti1 := w * h\n\t\ti2 := i1 + i1/2\n\t\ti3 := i2 + i1/2\n\n\t\treturn func(frame []byte) image.Image {\n\t\t\treturn &image.YCbCr{\n\t\t\t\tY:              frame[:i1],\n\t\t\t\tCb:             frame[i1:i2],\n\t\t\t\tCr:             frame[i2:i3],\n\t\t\t\tYStride:        w,\n\t\t\t\tCStride:        w / 2,\n\t\t\t\tSubsampleRatio: image.YCbCrSubsampleRatio422,\n\t\t\t\tRect:           rect,\n\t\t\t}\n\t\t}\n\tcase \"444\":\n\t\ti1 := w * h\n\t\ti2 := i1 + i1\n\t\ti3 := i2 + i1\n\n\t\treturn func(frame []byte) image.Image {\n\t\t\treturn &image.YCbCr{\n\t\t\t\tY:              frame[:i1],\n\t\t\t\tCb:             frame[i1:i2],\n\t\t\t\tCr:             frame[i2:i3],\n\t\t\t\tYStride:        w,\n\t\t\t\tCStride:        w,\n\t\t\t\tSubsampleRatio: image.YCbCrSubsampleRatio444,\n\t\t\t\tRect:           rect,\n\t\t\t}\n\t\t}\n\t}\n\n\treturn nil\n}\n\n// HasSameColor checks if all pixels has same color\nfunc HasSameColor(img image.Image) bool {\n\tvar pix []byte\n\n\tswitch img := img.(type) {\n\tcase *image.Gray:\n\t\tpix = img.Pix\n\tcase *image.YCbCr:\n\t\tpix = img.Y\n\t}\n\n\tif len(pix) == 0 {\n\t\treturn false\n\t}\n\n\ti0 := pix[0]\n\tfor _, i := range pix {\n\t\tif i != i0 {\n\t\t\treturn false\n\t\t}\n\t}\n\treturn true\n}\n"
  },
  {
    "path": "pkg/yaml/yaml.go",
    "content": "package yaml\n\nimport (\n\t\"bytes\"\n\t\"errors\"\n\n\t\"gopkg.in/yaml.v3\"\n)\n\nfunc Unmarshal(in []byte, out interface{}) (err error) {\n\treturn yaml.Unmarshal(in, out)\n}\n\nfunc Encode(v any, indent int) ([]byte, error) {\n\tb := bytes.NewBuffer(nil)\n\te := yaml.NewEncoder(b)\n\te.SetIndent(indent)\n\n\tif err := e.Encode(v); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn b.Bytes(), nil\n}\n\nfunc Patch(in []byte, path []string, value any) ([]byte, error) {\n\tout, err := patch(in, path, value)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\t// validate\n\tif err = yaml.Unmarshal(out, map[string]any{}); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn out, nil\n}\n\nfunc patch(in []byte, path []string, value any) ([]byte, error) {\n\tvar root yaml.Node\n\tif err := yaml.Unmarshal(in, &root); err != nil {\n\t\t// invalid yaml\n\t\treturn nil, err\n\t}\n\n\t// empty in\n\tif len(root.Content) != 1 {\n\t\treturn addToEnd(in, path, value)\n\t}\n\n\t// yaml is not dict\n\tif root.Content[0].Kind != yaml.MappingNode {\n\t\treturn nil, errors.New(\"yaml: can't patch\")\n\t}\n\n\t// dict items list\n\tnodes := root.Content[0].Content\n\n\tn := len(path) - 1\n\n\t// parent node key/value\n\tpKey, pVal := findNode(nodes, path[:n])\n\tif pKey == nil {\n\t\t// no parent node\n\t\treturn addToEnd(in, path, value)\n\t}\n\n\tvar paste []byte\n\n\tif value != nil {\n\t\t// nil value means delete key\n\t\tvar err error\n\t\tv := map[string]any{path[n]: value}\n\t\tif paste, err = Encode(v, 2); err != nil {\n\t\t\treturn nil, err\n\t\t}\n\t}\n\n\tiKey, _ := findNode(pVal.Content, path[n:])\n\tif iKey != nil {\n\t\t// key item not nil (replace value)\n\t\tpaste = addIndent(paste, iKey.Column-1)\n\n\t\ti0, i1 := nodeBounds(in, iKey)\n\t\treturn join(in[:i0], paste, in[i1:]), nil\n\t}\n\n\tif pVal.Content != nil {\n\t\t// parent value not nil (use first child indent)\n\t\tpaste = addIndent(paste, pVal.Column-1)\n\t} else {\n\t\t// parent value is nil (use parent indent + 2)\n\t\tpaste = addIndent(paste, pKey.Column+1)\n\t}\n\n\t_, i1 := nodeBounds(in, pKey)\n\treturn join(in[:i1], paste, in[i1:]), nil\n}\n\nfunc findNode(nodes []*yaml.Node, keys []string) (key, value *yaml.Node) {\n\tfor i, name := range keys {\n\t\tfor j := 0; j < len(nodes); j += 2 {\n\t\t\tif nodes[j].Value == name {\n\t\t\t\tif i < len(keys)-1 {\n\t\t\t\t\tnodes = nodes[j+1].Content\n\t\t\t\t\tbreak\n\t\t\t\t}\n\t\t\t\treturn nodes[j], nodes[j+1]\n\t\t\t}\n\t\t}\n\t}\n\treturn nil, nil\n}\n\nfunc nodeBounds(in []byte, node *yaml.Node) (offset0, offset1 int) {\n\t// start from next line after node\n\toffset0 = lineOffset(in, node.Line)\n\toffset1 = lineOffset(in, node.Line+1)\n\n\tif offset1 < 0 {\n\t\treturn offset0, len(in)\n\t}\n\n\tfor i := offset1; i < len(in); {\n\t\tindent, length := parseLine(in[i:])\n\t\tif indent+1 != length {\n\t\t\tif node.Column < indent+1 {\n\t\t\t\toffset1 = i + length\n\t\t\t} else {\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\ti += length\n\t}\n\n\treturn\n}\n\nfunc addToEnd(in []byte, path []string, value any) ([]byte, error) {\n\tif len(path) != 2 || value == nil {\n\t\treturn nil, errors.New(\"yaml: path not exist\")\n\t}\n\n\tv := map[string]map[string]any{\n\t\tpath[0]: {path[1]: value},\n\t}\n\tpaste, err := Encode(v, 2)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn join(in, paste), nil\n}\n\nfunc join(items ...[]byte) []byte {\n\tn := len(items) - 1\n\tfor _, b := range items {\n\t\tn += len(b)\n\t}\n\n\tbuf := make([]byte, 0, n)\n\tfor _, b := range items {\n\t\tif len(b) == 0 {\n\t\t\tcontinue\n\t\t}\n\t\tif n = len(buf); n > 0 && buf[n-1] != '\\n' {\n\t\t\tbuf = append(buf, '\\n')\n\t\t}\n\t\tbuf = append(buf, b...)\n\t}\n\n\treturn buf\n}\n\nfunc addPrefix(src, pre []byte) (dst []byte) {\n\tfor len(src) > 0 {\n\t\tdst = append(dst, pre...)\n\t\ti := bytes.IndexByte(src, '\\n') + 1\n\t\tif i == 0 {\n\t\t\tdst = append(dst, src...)\n\t\t\tbreak\n\t\t}\n\t\tdst = append(dst, src[:i]...)\n\t\tsrc = src[i:]\n\t}\n\n\treturn\n}\n\nfunc addIndent(in []byte, indent int) (dst []byte) {\n\tpre := make([]byte, indent)\n\tfor i := 0; i < indent; i++ {\n\t\tpre[i] = ' '\n\t}\n\treturn addPrefix(in, pre)\n}\n\nfunc lineOffset(in []byte, line int) (offset int) {\n\tfor l := 1; ; l++ {\n\t\tif l == line {\n\t\t\treturn offset\n\t\t}\n\n\t\ti := bytes.IndexByte(in[offset:], '\\n') + 1\n\t\tif i == 0 {\n\t\t\tbreak\n\t\t}\n\t\toffset += i\n\t}\n\treturn -1\n}\n\nfunc parseLine(b []byte) (indent int, length int) {\n\tprefix := true\n\tfor ; length < len(b); length++ {\n\t\tswitch b[length] {\n\t\tcase ' ':\n\t\t\tif prefix {\n\t\t\t\tindent++\n\t\t\t}\n\t\tcase '\\n':\n\t\t\tlength++\n\t\t\treturn\n\t\tdefault:\n\t\t\tprefix = false\n\t\t}\n\t}\n\treturn\n}\n"
  },
  {
    "path": "pkg/yaml/yaml_test.go",
    "content": "package yaml\n\nimport (\n\t\"testing\"\n\n\t\"github.com/stretchr/testify/require\"\n)\n\nfunc TestPatch(t *testing.T) {\n\ttests := []struct {\n\t\tname   string\n\t\tsrc    string\n\t\tpath   []string\n\t\tvalue  any\n\t\texpect string\n\t}{\n\t\t{\n\t\t\tname:   \"empty config\",\n\t\t\tsrc:    \"\",\n\t\t\tpath:   []string{\"streams\", \"camera1\"},\n\t\t\tvalue:  \"val1\",\n\t\t\texpect: \"streams:\\n  camera1: val1\\n\",\n\t\t},\n\t\t{\n\t\t\tname:   \"empty main key\",\n\t\t\tsrc:    \"#dummy\",\n\t\t\tpath:   []string{\"streams\", \"camera1\"},\n\t\t\tvalue:  \"val1\",\n\t\t\texpect: \"#dummy\\nstreams:\\n  camera1: val1\\n\",\n\t\t},\n\t\t{\n\t\t\tname:   \"single line value\",\n\t\t\tsrc:    \"streams:\\n  camera1: url1\\n  camera2: url2\",\n\t\t\tpath:   []string{\"streams\", \"camera1\"},\n\t\t\tvalue:  \"val1\",\n\t\t\texpect: \"streams:\\n  camera1: val1\\n  camera2: url2\",\n\t\t},\n\t\t{\n\t\t\tname:   \"next line value\",\n\t\t\tsrc:    \"streams:\\n  camera1:\\n    url1\\n  camera2: url2\",\n\t\t\tpath:   []string{\"streams\", \"camera1\"},\n\t\t\tvalue:  \"val1\",\n\t\t\texpect: \"streams:\\n  camera1: val1\\n  camera2: url2\",\n\t\t},\n\t\t{\n\t\t\tname:   \"two lines value\",\n\t\t\tsrc:    \"streams:\\n  camera1: url1\\n    url2\\n  camera2: url2\",\n\t\t\tpath:   []string{\"streams\", \"camera1\"},\n\t\t\tvalue:  \"val1\",\n\t\t\texpect: \"streams:\\n  camera1: val1\\n  camera2: url2\",\n\t\t},\n\t\t{\n\t\t\tname:   \"next two lines value\",\n\t\t\tsrc:    \"streams:\\n  camera1:\\n    url1\\n    url2\\n  camera2: url2\",\n\t\t\tpath:   []string{\"streams\", \"camera1\"},\n\t\t\tvalue:  \"val1\",\n\t\t\texpect: \"streams:\\n  camera1: val1\\n  camera2: url2\",\n\t\t},\n\t\t{\n\t\t\tname:   \"add array\",\n\t\t\tsrc:    \"\",\n\t\t\tpath:   []string{\"streams\", \"camera1\"},\n\t\t\tvalue:  []string{\"val1\", \"val2\"},\n\t\t\texpect: \"streams:\\n  camera1:\\n    - val1\\n    - val2\\n\",\n\t\t},\n\t\t{\n\t\t\tname:   \"remove value\",\n\t\t\tsrc:    \"streams:\\n  camera1: url1\\n  camera2: url2\",\n\t\t\tpath:   []string{\"streams\", \"camera1\"},\n\t\t\tvalue:  nil,\n\t\t\texpect: \"streams:\\n  camera2: url2\",\n\t\t},\n\t\t{\n\t\t\tname:   \"add pairings\",\n\t\t\tsrc:    \"homekit:\\n  camera1:\\nstreams:\\n  camera1: url1\",\n\t\t\tpath:   []string{\"homekit\", \"camera1\", \"pairings\"},\n\t\t\tvalue:  []string{\"val1\"},\n\t\t\texpect: \"homekit:\\n  camera1:\\n    pairings:\\n      - val1\\nstreams:\\n  camera1: url1\",\n\t\t},\n\t\t{\n\t\t\tname:   \"remove pairings\",\n\t\t\tsrc:    \"homekit:\\n  camera1:\\n    pairings:\\n      - val1\\nstreams:\\n  camera1: url1\",\n\t\t\tpath:   []string{\"homekit\", \"camera1\", \"pairings\"},\n\t\t\tvalue:  nil,\n\t\t\texpect: \"homekit:\\n  camera1:\\nstreams:\\n  camera1: url1\",\n\t\t},\n\t\t{\n\t\t\tname:   \"no new line\",\n\t\t\tsrc:    \"streams:\\n  camera1: url1\",\n\t\t\tpath:   []string{\"streams\", \"camera1\"},\n\t\t\tvalue:  \"val1\",\n\t\t\texpect: \"streams:\\n  camera1: val1\\n\",\n\t\t},\n\t\t{\n\t\t\tname:   \"no new line\",\n\t\t\tsrc:    \"streams:\\n  camera1: url1\\nhomekit:\\n  camera1:\\n    name: dummy\",\n\t\t\tpath:   []string{\"homekit\", \"camera1\", \"pairings\"},\n\t\t\tvalue:  []string{\"val1\"},\n\t\t\texpect: \"streams:\\n  camera1: url1\\nhomekit:\\n  camera1:\\n    name: dummy\\n    pairings:\\n      - val1\\n\",\n\t\t},\n\t}\n\tfor _, tt := range tests {\n\t\tt.Run(tt.name, func(t *testing.T) {\n\t\t\tb, err := Patch([]byte(tt.src), tt.path, tt.value)\n\t\t\trequire.NoError(t, err)\n\t\t\trequire.Equal(t, tt.expect, string(b))\n\t\t})\n\t}\n}\n"
  },
  {
    "path": "pkg/yandex/session.go",
    "content": "package yandex\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/cookiejar\"\n\t\"strings\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/AlexxIT/go2rtc/pkg/core\"\n)\n\ntype Session struct {\n\ttoken  string\n\tclient *http.Client\n}\n\nvar sessions = map[string]*Session{}\nvar sessionsMu sync.Mutex\n\nfunc GetSession(token string) (*Session, error) {\n\tsessionsMu.Lock()\n\tdefer sessionsMu.Unlock()\n\n\tif session, ok := sessions[token]; ok {\n\t\treturn session, nil\n\t}\n\n\tsession := &Session{token: token}\n\tif err := session.Login(); err != nil {\n\t\treturn nil, err\n\t}\n\n\tsessions[token] = session\n\n\treturn session, nil\n}\n\nfunc (s *Session) Login() error {\n\treq, err := http.NewRequest(\n\t\t\"POST\", \"https://mobileproxy.passport.yandex.net/1/bundle/auth/x_token/\",\n\t\tstrings.NewReader(\"type=x-token&retpath=https%3A%2F%2Fwww.yandex.ru\"),\n\t)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\treq.Header.Set(\"Content-Type\", \"application/x-www-form-urlencoded\")\n\treq.Header.Set(\"Ya-Consumer-Authorization\", \"OAuth \"+s.token)\n\n\tres, err := http.DefaultClient.Do(req)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar auth struct {\n\t\tPassportHost string `json:\"passport_host\"`\n\t\tStatus       string `json:\"status\"`\n\t\tTrackId      string `json:\"track_id\"`\n\t}\n\tif err = json.NewDecoder(res.Body).Decode(&auth); err != nil {\n\t\treturn err\n\t}\n\n\tif auth.Status != \"ok\" {\n\t\treturn errors.New(\"yandex: login error: \" + auth.Status)\n\t}\n\n\ts.client = &http.Client{Timeout: 15 * time.Second}\n\ts.client.CheckRedirect = func(req *http.Request, via []*http.Request) error {\n\t\treturn http.ErrUseLastResponse\n\t}\n\ts.client.Jar, _ = cookiejar.New(nil)\n\n\tres, err = s.client.Get(auth.PassportHost + \"/auth/session/?track_id=\" + auth.TrackId)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\ts.client.CheckRedirect = nil\n\n\treturn nil\n}\n\nfunc (s *Session) Get(url string) (*http.Response, error) {\n\treturn s.client.Get(url)\n}\n\nfunc (s *Session) GetCSRF() (string, error) {\n\tres, err := s.Get(\"https://yandex.ru/quasar\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tbody, err := io.ReadAll(res.Body)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\ttoken := core.Between(string(body), `\"csrfToken2\":\"`, `\"`)\n\treturn token, nil\n}\n\nfunc (s *Session) GetCookieString(url string) string {\n\treq, err := http.NewRequest(\"GET\", url, nil)\n\tif err != nil {\n\t\treturn \"\"\n\t}\n\tfor _, cookie := range s.client.Jar.Cookies(req.URL) {\n\t\treq.AddCookie(cookie)\n\t}\n\treturn req.Header.Get(\"Cookie\")\n}\n\nfunc (s *Session) GetDevices() ([]Device, error) {\n\tres, err := s.Get(\"https://iot.quasar.yandex.ru/m/v3/user/devices\")\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar data struct {\n\t\tHouseholds []struct {\n\t\t\tAll []Device `json:\"all\"`\n\t\t} `json:\"households\"`\n\t}\n\n\tif err = json.NewDecoder(res.Body).Decode(&data); err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar devices []Device\n\tfor _, household := range data.Households {\n\t\tdevices = append(devices, household.All...)\n\t}\n\treturn devices, nil\n}\n\nfunc (s *Session) GetSnapshotURL(deviceID string) (string, error) {\n\tdevices, err := s.GetDevices()\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\n\tfor _, device := range devices {\n\t\tif device.Id == deviceID {\n\t\t\treturn device.Parameters.SnapshotUrl, nil\n\t\t}\n\t}\n\n\treturn \"\", errors.New(\"yandex: can't get snapshot url for device: \" + deviceID)\n}\n\nfunc (s *Session) WebrtcCreateRoom(deviceID string) (*Room, error) {\n\tcsrf, err := s.GetCSRF()\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq, err := http.NewRequest(\n\t\t\"POST\", \"https://iot.quasar.yandex.ru/m/v3/user/devices/\"+deviceID+\"/webrtc/create-room\",\n\t\tstrings.NewReader(`{\"protocol\":\"whip\"}`),\n\t)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\treq.Header.Add(\"Content-Type\", \"application/json\")\n\treq.Header.Add(\"X-CSRF-Token\", csrf)\n\n\tres, err := s.client.Do(req)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\n\tvar data struct {\n\t\tResult Room `json:\"result\"`\n\t}\n\tif err = json.NewDecoder(res.Body).Decode(&data); err != nil {\n\t\treturn nil, err\n\t}\n\n\treturn &data.Result, nil\n}\n\ntype Device struct {\n\tId         string `json:\"id\"`\n\tName       string `json:\"name\"`\n\tType       string `json:\"type\"`\n\tParameters struct {\n\t\tSnapshotUrl string `json:\"snapshot_url,omitempty\"`\n\t} `json:\"parameters\"`\n}\n\ntype Room struct {\n\tServiceUrl    string `json:\"service_url\"`\n\tServiceName   string `json:\"service_name\"`\n\tRoomId        string `json:\"room_id\"`\n\tParticipantId string `json:\"participant_id\"`\n\tCredentials   string `json:\"jwt\"`\n}\n"
  },
  {
    "path": "scripts/README.md",
    "content": "# Scripts\n\nThis folder contains a script for building binaries for all platforms.\n\nThe project has no `CGO` dependencies, so building is as simple as possible using the `go build` command.\n\nThe project has to use the latest versions of go due to dependencies on third-party go libraries. Such as `pion/webrtc` or `golang.org/x`. Unfortunately, this breaks compatibility with older versions of operating systems.\n\nThe project uses [UPX](https://github.com/upx/upx) to compress binaries for Linux. The project does not use compression for Windows due to false antivirus alarms. The project does not use compression for macOS due to broken result.\n\n## Useful commands\n\n```\ngo get -u\ngo mod tidy\ngo mod why github.com/pion/rtcp\ngo list -deps .\\cmd\\go2rtc_rtsp\\\n./goweight\n```\n\n## Dependencies\n\n```\n- gopkg.in/yaml.v3\n  - github.com/kr/pretty\n- github.com/AlexxIT/go2rtc/pkg/hap\n  - github.com/tadglines/go-pkgs\n  - golang.org/x/crypto\n- github.com/AlexxIT/go2rtc/pkg/mdns\n  - github.com/miekg/dns\n- github.com/AlexxIT/go2rtc/pkg/pcm\n  - github.com/sigurn/crc16\n  - github.com/sigurn/crc8\n- github.com/pion/ice/v2\n  - github.com/google/uuid\n  - github.com/wlynxg/anet\n- github.com/rs/zerolog\n  - github.com/mattn/go-colorable\n  - github.com/mattn/go-isatty\n- github.com/stretchr/testify\n  - github.com/davecgh/go-spew\n  - github.com/pmezard/go-difflib\n- ???\n  - golang.org/x/mod\n  - golang.org/x/net\n  - golang.org/x/sys\n  - golang.org/x/tools\n```\n\n## Licenses\n\n- github.com/asticode/go-astits - MIT\n- github.com/eclipse/paho.mqtt.golang - EPL-2.0\n- github.com/expr-lang/expr - MIT\n- github.com/gorilla/websocket - BSD-2\n- github.com/mattn/go-isatty - MIT\n- github.com/miekg/dns - BSD-3\n- github.com/pion/dtls - MIT\n- github.com/pion/ice - MIT\n- github.com/pion/interceptor - MIT\n- github.com/pion/rtcp - MIT\n- github.com/pion/rtp - MIT\n- github.com/pion/sdp - MIT\n- github.com/pion/srtp - MIT\n- github.com/pion/stun - MIT\n- github.com/pion/webrtc - MIT\n- github.com/rs/zerolog - MIT\n- github.com/sigurn/crc16 - MIT\n- github.com/sigurn/crc8 - MIT\n- github.com/stretchr/testify - MIT\n- github.com/tadglines/go-pkgs - Apache\n- golang.org/x/crypto - BSD-3\n- gopkg.in/yaml.v3 - MIT and Apache\n- github.com/asticode/go-astikit - MIT\n- github.com/davecgh/go-spew - ISC (BSD/MIT like)\n- github.com/google/uuid - BSD-3\n- github.com/kr/pretty - MIT\n- github.com/mattn/go-colorable - MIT\n- github.com/pion/datachannel - MIT\n- github.com/pion/logging - MIT\n- github.com/pion/mdns - MIT\n- github.com/pion/randutil - MIT\n- github.com/pion/sctp - MIT\n- github.com/pmezard/go-difflib - ???\n- github.com/wlynxg/anet - BSD-3\n- golang.org/x/mod - BSD-3\n- golang.org/x/net - BSD-3\n- golang.org/x/sync - BSD-3\n- golang.org/x/sys - BSD-3\n- golang.org/x/tools - BSD-3\n\n## Virus\n\n- https://go.dev/doc/faq#virus\n- https://groups.google.com/g/golang-nuts/c/lPwiWYaApSU\n\n## Useful links\n\n- https://github.com/golang-standards/project-layout\n- https://github.com/micro/micro\n- https://github.com/golang/go/wiki/GoArm\n- https://gist.github.com/asukakenji/f15ba7e588ac42795f421b48b8aede63\n- https://en.wikipedia.org/wiki/AArch64\n- https://stackoverflow.com/questions/22267189/what-does-the-w-flag-mean-when-passed-in-via-the-ldflags-option-to-the-go-comman\n"
  },
  {
    "path": "scripts/build.cmd",
    "content": "@ECHO OFF\n\n@SET GOOS=windows\n@SET GOARCH=amd64\n@SET FILENAME=go2rtc_win64.zip\ngo build -ldflags \"-s -w\" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc.exe\n\n@SET GOOS=windows\n@SET GOARCH=386\n@SET FILENAME=go2rtc_win32.zip\ngo build -ldflags \"-s -w\" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc.exe\n\n@SET GOOS=windows\n@SET GOARCH=arm64\n@SET FILENAME=go2rtc_win_arm64.zip\ngo build -ldflags \"-s -w\" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc.exe\n\n@SET GOOS=linux\n@SET GOARCH=amd64\n@SET FILENAME=go2rtc_linux_amd64\ngo build -ldflags \"-s -w\" -trimpath -o %FILENAME% && upx --best --lzma %FILENAME%\n\n@SET GOOS=linux\n@SET GOARCH=386\n@SET FILENAME=go2rtc_linux_i386\ngo build -ldflags \"-s -w\" -trimpath -o %FILENAME% && upx --best --lzma %FILENAME%\n\n@SET GOOS=linux\n@SET GOARCH=arm64\n@SET FILENAME=go2rtc_linux_arm64\ngo build -ldflags \"-s -w\" -trimpath -o %FILENAME% && upx --best --lzma %FILENAME%\n\n@SET GOOS=linux\n@SET GOARCH=arm\n@SET GOARM=7\n@SET FILENAME=go2rtc_linux_arm\ngo build -ldflags \"-s -w\" -trimpath -o %FILENAME% && upx --best --lzma %FILENAME%\n\n@SET GOOS=linux\n@SET GOARCH=arm\n@SET GOARM=6\n@SET FILENAME=go2rtc_linux_armv6\ngo build -ldflags \"-s -w\" -trimpath -o %FILENAME% && upx --best --lzma %FILENAME%\n\n@SET GOOS=linux\n@SET GOARCH=mipsle\n@SET FILENAME=go2rtc_linux_mipsel\ngo build -ldflags \"-s -w\" -trimpath -o %FILENAME% && upx --best --lzma %FILENAME%\n\n@SET GOOS=darwin\n@SET GOARCH=amd64\n@SET FILENAME=go2rtc_mac_amd64.zip\ngo build -ldflags \"-s -w\" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc\n\n@SET GOOS=darwin\n@SET GOARCH=arm64\n@SET FILENAME=go2rtc_mac_arm64.zip\ngo build -ldflags \"-s -w\" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc\n\n@SET GOOS=freebsd\n@SET GOARCH=amd64\n@SET FILENAME=go2rtc_freebsd_amd64.zip\ngo build -ldflags \"-s -w\" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc\n\n@SET GOOS=freebsd\n@SET GOARCH=arm64\n@SET FILENAME=go2rtc_freebsd_arm64.zip\ngo build -ldflags \"-s -w\" -trimpath && 7z a -mx9 -sdel %FILENAME% go2rtc\n"
  },
  {
    "path": "scripts/build.sh",
    "content": "#!/bin/sh\n\nset -e  # Exit immediately if a command exits with a non-zero status.\nset -u  # Treat unset variables as an error when substituting.\n\ncheck_command() {\n    if ! command -v \"$1\" >/dev/null\n    then\n        echo \"Error: $1 could not be found. Please install it.\" >&2\n        return 1\n    fi\n}\n\nbuild_zip() {\n  go build -ldflags \"-s -w\" -trimpath -o $2\n  7z a -mx9 -sdel $1 $2\n}\n\nbuild_upx() {\n  go build -ldflags \"-s -w\" -trimpath -o $1\n  upx --best --lzma $1\n}\n\ncheck_command go\ncheck_command 7z\ncheck_command upx\n\nexport CGO_ENABLED=0\n\nset -x  # Print commands and their arguments as they are executed.\n\nGOOS=windows  GOARCH=amd64        build_zip go2rtc_win64.zip     go2rtc.exe\nGOOS=windows  GOARCH=386          build_zip go2rtc_win32.zip     go2rtc.exe\nGOOS=windows  GOARCH=arm64        build_zip go2rtc_win_arm64.zip go2rtc.exe\n\nGOOS=linux    GOARCH=amd64        build_upx go2rtc_linux_amd64\nGOOS=linux    GOARCH=386          build_upx go2rtc_linux_i386\nGOOS=linux    GOARCH=arm64        build_upx go2rtc_linux_arm64\nGOOS=linux    GOARCH=mipsle       build_upx go2rtc_linux_mipsel\nGOOS=linux    GOARCH=arm GOARM=7  build_upx go2rtc_linux_arm\nGOOS=linux    GOARCH=arm GOARM=6  build_upx go2rtc_linux_armv6\n\nGOOS=darwin   GOARCH=amd64        build_zip go2rtc_mac_amd64.zip go2rtc\nGOOS=darwin   GOARCH=arm64        build_zip go2rtc_mac_arm64.zip go2rtc\n\nGOOS=freebsd  GOARCH=amd64        build_zip go2rtc_freebsd_amd64.zip go2rtc\nGOOS=freebsd  GOARCH=arm64        build_zip go2rtc_freebsd_arm64.zip go2rtc\n"
  },
  {
    "path": "website/.vitepress/config.js",
    "content": "import {defineConfig} from 'vitepress';\n\nfunction replace_link(md) {\n    md.core.ruler.after('inline', 'replace-link', function (state) {\n        for (const block of state.tokens) {\n            if (block.type === 'inline' && block.children) {\n                for (const token of block.children) {\n                    const href = token.attrGet('href');\n                    if (href && href.indexOf('README.md') >= 0) {\n                        // token.attrJoin('style', 'color:red;');\n                        token.attrSet('href', href.replace('README.md', 'index.md'));\n                    }\n                }\n            }\n        }\n        return true;\n    });\n}\n\nexport default defineConfig({\n    title: 'go2rtc',\n    description: 'Ultimate camera streaming application',\n    head: [\n        // first line (green bold) of Telegram card, autodetect from hostname\n        ['meta', { property: 'og:site_name', content: 'go2rtc.org' }],\n        // second line of Telegram card (black bold), autodetect from site description\n        ['meta', { property: 'og:title', content: 'go2rtc - Ultimate camera streaming application' }],\n        // third line of Telegram card, autodetect from site description\n        ['meta', { property: 'og:description', content: 'Support alsa, doorbird, dvrip, eseecloud, ffmpeg, gopro, hass, hls, homekit, mjpeg, mp4, mpegts, nest, onvif, ring, roborock, rtmp, rtsp, tapo, vigi, tuya, v4l2, webrtc, wyze, xiaomi.' }],\n        ['meta', { property: 'og:url', content: 'https://go2rtc.org/' }],\n        ['meta', { property: 'og:image', content: 'https://go2rtc.org/images/logo.png' }],\n        // important for Telegram - the image will be at the bottom and large\n        ['meta', { property: 'twitter:card', content: 'summary_large_image' }],\n    ],\n    sitemap: {hostname: 'https://go2rtc.org'},\n\n    themeConfig: {\n        nav: [\n            {text: 'Home', link: '/'},\n        ],\n        sidebar: [\n            {\n                items: [\n                    {text: 'Installation', link: '/#installation'},\n                    {text: 'Configuration', link: '/#configuration'},\n                    {text: 'Security', link: '/#security'},\n                ],\n            },\n            {\n                text: 'Features',\n                items: [\n                    {text: 'Streaming input', link: '/#streaming-input'},\n                    {text: 'Streaming output', link: '/#streaming-output'},\n                    {text: 'Streaming ingest', link: '/#streaming-ingest'},\n                    {text: 'Two-way audio', link: '/#two-way-audio'},\n                    {text: 'Stream to camera', link: '/#stream-to-camera'},\n                    {text: 'Publish stream', link: '/#publish-stream'},\n                    {text: 'Preload stream', link: '/#preload-stream'},\n                    {text: 'Streaming stats', link: '/#streaming-stats'},\n                ],\n                collapsed: false,\n            },\n            {\n                text: 'Codecs',\n                items: [\n                    {text: 'Codecs filters', link: '/#codecs-filters'},\n                    {text: 'Codecs madness', link: '/#codecs-madness'},\n                    {text: 'Built-in transcoding', link: '/#built-in-transcoding'},\n                    {text: 'Codecs negotiation', link: '/#codecs-negotiation'},\n                ],\n                collapsed: true,\n            },\n            {\n                text: 'Other',\n                items: [\n                    {text: 'Projects using go2rtc', link: '/#projects-using-go2rtc'},\n                    {text: 'Camera experience', link: '/#camera-experience'},\n                    {text: 'Tips', link: '/#tips'},\n                ],\n                collapsed: true,\n            },\n            {\n                text: 'Core modules',\n                items: [\n                    {text: 'app', link: '/internal/app/'},\n                    {text: 'api', link: '/internal/api/'},\n                    {text: 'streams', link: '/internal/streams/'},\n                ],\n                collapsed: false,\n            },\n            {\n                text: 'Main modules',\n                items: [\n                    {text: 'http', link: '/internal/http/'},\n                    {text: 'mjpeg', link: '/internal/mjpeg/'},\n                    {text: 'mp4', link: '/internal/mp4/'},\n                    {text: 'rtsp', link: '/internal/rtsp/'},\n                    {text: 'webrtc', link: '/internal/webrtc/'},\n                ],\n                collapsed: false,\n            },\n            {\n                text: 'Other modules',\n                items: [\n                    {text: 'hls', link: '/internal/hls/'},\n                    {text: 'homekit', link: '/internal/homekit/'},\n                    {text: 'onvif', link: '/internal/onvif/'},\n                    {text: 'rtmp', link: '/internal/rtmp/'},\n                    {text: 'webtorrent', link: '/internal/webtorrent/'},\n                    {text: 'wyoming', link: '/internal/wyoming/'},\n                ],\n                collapsed: false,\n            },\n            {\n                text: 'Script sources',\n                items: [\n                    {text: 'echo', link: '/internal/echo/'},\n                    {text: 'exec', link: '/internal/exec/'},\n                    {text: 'expr', link: '/internal/expr/'},\n                    {text: 'ffmpeg', link: '/internal/ffmpeg/'},\n                ],\n                collapsed: false,\n            },\n            {\n                text: 'Other sources',\n                items: [\n                    {text: 'alsa', link: '/internal/alsa/'},\n                    {text: 'bubble', link: '/internal/bubble/'},\n                    {text: 'doorbird', link: '/internal/doorbird/'},\n                    {text: 'dvrip', link: '/internal/dvrip/'},\n                    {text: 'eseecloud', link: '/internal/eseecloud/'},\n                    {text: 'flussonic', link: '/internal/flussonic/'},\n                    {text: 'gopro', link: '/internal/gopro/'},\n                    {text: 'hass', link: '/internal/hass/'},\n                    {text: 'isapi', link: '/internal/isapi/'},\n                    {text: 'ivideon', link: '/internal/ivideon/'},\n                    {text: 'kasa', link: '/internal/kasa/'},\n                    {text: 'mpeg', link: '/internal/mpeg/'},\n                    {text: 'multitrans', link: '/internal/multitrans/'},\n                    {text: 'nest', link: '/internal/nest/'},\n                    {text: 'ring', link: '/internal/ring/'},\n                    {text: 'roborock', link: '/internal/roborock/'},\n                    {text: 'tapo', link: '/internal/tapo/'},\n                    {text: 'tuya', link: '/internal/tuya/'},\n                    {text: 'v4l2', link: '/internal/v4l2/'},\n                    {text: 'wyze', link: '/internal/wyze/'},\n                    {text: 'xiaomi', link: '/internal/xiaomi/'},\n                    {text: 'yandex', link: '/internal/yandex/'},\n                ],\n                collapsed: false,\n            },\n            {\n                text: 'Helper modules',\n                items: [\n                    {text: 'debug', link: '/internal/debug/'},\n                    {text: 'ngrok', link: '/internal/ngrok/'},\n                    {text: 'pinggy', link: '/internal/pinggy/'},\n                    {text: 'srtp', link: '/internal/srtp/'},\n                ],\n                collapsed: false,\n            },\n\n        ],\n        socialLinks: [\n            {icon: 'github', link: 'https://github.com/AlexxIT/go2rtc'}\n        ],\n        outline: [2, 3],\n        search: {provider: 'local'},\n    },\n\n    rewrites(id) {\n        // change file names\n        return id.replace('README.md', 'index.md');\n    },\n\n    markdown: {\n        config: (md) => {\n            // change markdown links\n            md.use(replace_link);\n        }\n    },\n\n    srcDir: '..',\n    srcExclude: ['examples/', 'pkg/'],\n\n    // cleanUrls: true,\n    ignoreDeadLinks: true,\n});\n"
  },
  {
    "path": "website/README.md",
    "content": "# WebSite\n\nThese are the sources of the [go2rtc.org](https://go2rtc.org/) website. It's content published on GitHub Pages and is a mirror of [alexxit.github.io/go2rtc/](http://alexxit.github.io/go2rtc/).\n\nThe site contains:\n\n- Project's documentation, which is compiled via [vitepress](https://github.com/vuejs/vitepress) from `README.md` files located in the root of the repository, as well as in the `internal` folder.\n- Project's API in OpenAPI format, and the [Redoc](https://github.com/Redocly/redoc) viewer\n- Project's assets (logo).\n"
  },
  {
    "path": "website/api/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <title>API | go2rtc</title>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <link href=\"https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700\" rel=\"stylesheet\">\n    <style>\n        body {\n            margin: 0;\n            padding: 0;\n        }\n    </style>\n</head>\n<body>\n<redoc spec-url=\"openapi.yaml\"></redoc>\n<script src=\"https://cdn.redoc.ly/redoc/latest/bundles/redoc.standalone.js\"></script>\n</body>\n</html>"
  },
  {
    "path": "website/api/openapi.yaml",
    "content": "openapi: 3.1.0\n\ninfo:\n  title: go2rtc\n  version: 1.9.13\n  license: { name: MIT,url: https://opensource.org/licenses/MIT }\n  contact: { url: https://github.com/AlexxIT/go2rtc }\n  description: |\n    Ultimate camera streaming application with support RTSP, RTMP, HTTP-FLV, WebRTC, MSE, HLS, MP4, MJPEG, HomeKit, FFmpeg, etc.\n\nservers:\n  - url: http://localhost:1984\n\ntags:\n  - name: Application\n    description: \"[Module: API](https://github.com/AlexxIT/go2rtc#module-api)\"\n  - name: Config\n    description: \"[Configuration](https://github.com/AlexxIT/go2rtc#configuration)\"\n  - name: Streams list\n    description: \"[Module: Streams](https://github.com/AlexxIT/go2rtc#module-streams)\"\n  - name: Consume stream\n  - name: HLS\n  - name: Snapshot\n  - name: Produce stream\n  - name: WebSocket\n    description: \"WebSocket API endpoint: `/api/ws` (see `api/README.md`)\"\n  - name: Discovery\n  - name: HomeKit\n  - name: ONVIF\n  - name: RTSPtoWebRTC\n  - name: WebTorrent\n    description: \"[Module: WebTorrent](https://github.com/AlexxIT/go2rtc#module-webtorrent)\"\n  - name: FFmpeg\n  - name: Debug\n\ncomponents:\n  parameters:\n    stream_src_path:\n      name: src\n      in: path\n      description: Source stream name\n      required: true\n      schema: { type: string }\n      example: camera1\n\n    stream_dst_path:\n      name: dst\n      in: path\n      description: Destination stream name\n      required: true\n      schema: { type: string }\n      example: camera1\n\n    stream_src_query:\n      name: src\n      in: query\n      description: Source stream name\n      required: true\n      schema: { type: string }\n      example: camera1\n\n    hls_session_id_path:\n      name: id\n      in: path\n      description: HLS session ID (passed as query param `id`)\n      required: true\n      schema: { type: string }\n      example: DvmHdd9w\n\n    mp4_filter:\n      name: mp4\n      in: query\n      description: MP4 codecs filter\n      required: false\n      schema:\n        type: string\n        enum: [ \"\", flac, all ]\n      example: flac\n\n    video_filter:\n      name: video\n      in: query\n      description: Video codecs filter\n      schema:\n        type: string\n        enum: [ \"\", all, h264, h265, mjpeg ]\n      example: h264,h265\n\n    audio_filter:\n      name: audio\n      in: query\n      description: Audio codecs filter\n      schema:\n        type: string\n        enum: [ \"\", all, aac, opus, pcm, pcmu, pcma ]\n      example: aac\n\n  responses:\n    discovery:\n      description: \"\"\n      content:\n        application/json:\n          example: { streams: [ { \"name\": \"Camera 1\",\"url\": \"...\" } ] }\n\n    webtorrent:\n      description: \"\"\n      content:\n        application/json:\n          example: { share: AKDypPy4zz, pwd: H0Km1HLTTP }\n\npaths:\n  /api:\n    get:\n      summary: Get application info\n      tags: [ Application ]\n      responses:\n        \"200\":\n          description: \"\"\n          content:\n            application/json:\n              schema:\n                type: object\n                properties:\n                  config_path: { type: string, example: \"/config/go2rtc.yaml\" }\n                  host: { type: string, example: \"192.168.1.123:1984\" }\n                  rtsp:\n                    type: object\n                    properties:\n                      listen: { type: string, example: \":8554\" }\n                      default_query: { type: string, example: \"video&audio\" }\n                  version: { type: string, example: \"1.9.12\" }\n\n  /api/exit:\n    post:\n      summary: Close application\n      tags: [ Application ]\n      parameters:\n        - name: code\n          in: query\n          description: Application exit code\n          required: false\n          schema: { type: integer }\n          example: 100\n      responses:\n        default:\n          description: \"\"\n\n  /api/restart:\n    post:\n      summary: Restart daemon\n      description: Restarts the daemon.\n      tags: [ Application ]\n      responses:\n        default:\n          description: \"\"\n\n  /api/log:\n    get:\n      summary: Get in-memory logs buffer\n      description: |\n        Returns current log output from the in-memory circular buffer.\n      tags: [ Application ]\n      responses:\n        \"200\":\n          description: OK\n          content:\n            application/jsonlines:\n              example: |\n                {\"level\":\"info\",\"version\":\"1.9.13\",\"platform\":\"linux/amd64\",\"revision\":\"dfe4755\",\"time\":1766841087331,\"message\":\"go2rtc\"}\n    delete:\n      summary: Clear in-memory logs buffer\n      tags: [ Application ]\n      responses:\n        \"200\":\n          description: \"\"\n          content:\n            text/plain: { example: \"\" }\n\n  /api/config:\n    get:\n      summary: Get main config file content\n      tags: [ Config ]\n      responses:\n        \"200\":\n          description: \"\"\n          content:\n            application/yaml: { example: \"streams:...\" }\n        \"404\":\n          description: Config file not found\n    post:\n      summary: Rewrite main config file\n      tags: [ Config ]\n      requestBody:\n        content:\n          \"*/*\": { example: \"streams:...\" }\n      responses:\n        default:\n          description: \"\"\n    patch:\n      summary: Merge changes to main config file\n      tags: [ Config ]\n      requestBody:\n        content:\n          \"*/*\": { example: \"streams:...\" }\n      responses:\n        default:\n          description: \"\"\n\n\n\n  /api/streams:\n    get:\n      summary: Get all streams info\n      tags: [ Streams list ]\n      responses:\n        \"200\":\n          description: \"\"\n          content:\n            application/json:\n              schema:\n                type: object\n                additionalProperties:\n                  type: object\n                  properties:\n                    producers:\n                      type: array\n                    consumers:\n                      type: array\n    put:\n      summary: Create new stream\n      tags: [ Streams list ]\n      parameters:\n        - name: src\n          in: query\n          description: Stream source (URI)\n          required: true\n          schema: { type: string }\n          example: \"rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0\"\n        - name: name\n          in: query\n          description: Stream name\n          required: false\n          schema: { type: string }\n          example: camera1\n      responses:\n        default:\n          description: \"\"\n    patch:\n      summary: Update stream source\n      tags: [ Streams list ]\n      parameters:\n        - name: src\n          in: query\n          description: Stream source (URI)\n          required: true\n          schema: { type: string }\n          example: \"rtsp://rtsp:12345678@192.168.1.123/av_stream/ch0\"\n        - name: name\n          in: query\n          description: Stream name\n          required: true\n          schema: { type: string }\n          example: camera1\n      responses:\n        default:\n          description: \"\"\n    delete:\n      summary: Delete stream\n      tags: [ Streams list ]\n      parameters:\n        - name: src\n          in: query\n          description: Stream name\n          required: true\n          schema: { type: string }\n          example: camera1\n      responses:\n        default:\n          description: \"\"\n    post:\n      summary: Send stream from source to destination\n      description: \"[Stream to camera](https://github.com/AlexxIT/go2rtc#stream-to-camera)\"\n      tags: [ Streams list ]\n      parameters:\n        - name: src\n          in: query\n          description: Stream source (URI)\n          required: true\n          schema: { type: string }\n          example: \"ffmpeg:http://example.com/song.mp3#audio=pcma#input=file\"\n        - name: dst\n          in: query\n          description: Destination stream name\n          required: true\n          schema: { type: string }\n          example: camera1\n      responses:\n        default:\n          description: \"\"\n\n  /api/streams.dot:\n    get:\n      summary: Get streams graph in Graphviz DOT format\n      tags: [ Streams list ]\n      parameters:\n        - name: src\n          in: query\n          description: Stream name filter. Repeat `src` to include multiple streams.\n          required: false\n          schema: { type: string }\n          example: camera1\n      responses:\n        \"200\":\n          description: OK\n          content:\n            text/vnd.graphviz:\n              example: \"digraph { ... }\"\n\n  /api/preload:\n    get:\n      summary: Get all preloaded streams\n      tags: [ Streams list ]\n      responses:\n        \"200\":\n          description: \"\"\n          content:\n            application/json:\n              schema:\n                type: object\n                additionalProperties:\n                  type: object\n                  properties:\n                    consumer:\n                      type: object\n                    query:\n                      type: string\n                      example: \"video&audio\"\n    put:\n      summary: Preload new stream\n      tags: [ Streams list ]\n      parameters:\n        - name: src\n          in: query\n          description: Stream source (name)\n          required: true\n          schema: { type: string }\n          example: \"camera1\"\n        - name: video\n          in: query\n          description: Video codecs filter\n          required: false\n          schema: { type: string }\n          example: all,h264,h265,...\n        - name: audio\n          in: query\n          description: Audio codecs filter\n          required: false\n          schema: { type: string }\n          example: all,aac,opus,...\n        - name: microphone\n          in: query\n          description: Microphone codecs filter\n          required: false\n          schema: { type: string }\n          example: all,aac,opus,...\n      responses:\n        default:\n          description: \"\"\n    delete:\n      summary: Delete preloaded stream\n      tags: [ Streams list ]\n      parameters:\n        - name: src\n          in: query\n          description: Stream source (name)\n          required: true\n          schema: { type: string }\n          example: \"camera1\"\n      responses:\n        default:\n          description: \"\"\n\n  /api/schemes:\n    get:\n      summary: Get supported source URL schemes\n      tags: [ Streams list ]\n      responses:\n        \"200\":\n          description: OK\n          content:\n            application/json:\n              schema:\n                type: array\n                items: { type: string }\n              example: [ rtsp, rtmp, webrtc, ffmpeg, hass ]\n\n\n  /api/streams?src={src}:\n    get:\n      summary: Get stream info in JSON format\n      tags: [ Consume stream ]\n      parameters:\n        - $ref: \"#/components/parameters/stream_src_path\"\n      responses:\n        \"200\":\n          description: \"\"\n          content:\n            application/json:\n              schema:\n                type: object\n                additionalProperties:\n                  type: object\n                  properties:\n                    producers:\n                      type: array\n                      items: { type: object }\n                    consumers:\n                      type: array\n                      items: { type: object }\n\n  /api/webrtc?src={src}:\n    post:\n      summary: Get stream in WebRTC format (WHEP)\n      description: \"[Module: WebRTC](https://github.com/AlexxIT/go2rtc#module-webrtc)\"\n      tags: [ Consume stream ]\n      parameters:\n        - $ref: \"#/components/parameters/stream_src_path\"\n      requestBody:\n        description: |\n          Support:\n          - JSON format (`Content-Type: application/json`)\n          - WHEP standard (`Content-Type: application/sdp`)\n          - raw SDP (`Content-Type: anything`)\n        required: true\n        content:\n          application/json: { example: { type: offer, sdp: \"v=0...\" } }\n          \"application/sdp\": { example: \"v=0...\" }\n          \"*/*\": { example: \"v=0...\" }\n      responses:\n        \"200\":\n          description: \"Response on JSON or raw SDP\"\n          content:\n            application/json: { example: { type: answer, sdp: \"v=0...\" } }\n            application/sdp: { example: \"v=0...\" }\n        \"201\":\n          description: \"Response on `Content-Type: application/sdp`\"\n          content:\n            application/sdp: { example: \"v=0...\" }\n\n  /api/stream.mp4?src={src}:\n    get:\n      summary: Get stream in MP4 format (HTTP progressive)\n      description: \"[Module: MP4](https://github.com/AlexxIT/go2rtc#module-mp4)\"\n      tags: [ Consume stream ]\n      parameters:\n        - $ref: \"#/components/parameters/stream_src_path\"\n        - name: duration\n          in: query\n          description: Limit the length of the stream in seconds\n          required: false\n          schema: { type: string }\n          example: 15\n        - name: filename\n          in: query\n          description: Download as a file with this name\n          required: false\n          schema: { type: string }\n          example: camera1.mp4\n        - name: rotate\n          in: query\n          description: \"Rotate video (degrees). Supported values: 90, 180, 270.\"\n          required: false\n          schema: { type: integer, enum: [ 90, 180, 270 ] }\n        - name: scale\n          in: query\n          description: Scale video in format `width:height`\n          required: false\n          schema: { type: string, example: \"1280:720\" }\n        - $ref: \"#/components/parameters/mp4_filter\"\n        - $ref: \"#/components/parameters/video_filter\"\n        - $ref: \"#/components/parameters/audio_filter\"\n      responses:\n        200:\n          description: \"\"\n          content: { video/mp4: { example: \"\" } }\n\n  /api/stream.m3u8?src={src}:\n    get:\n      summary: Get stream in HLS format\n      description: \"[Module: HLS](https://github.com/AlexxIT/go2rtc#module-hls)\"\n      tags: [ Consume stream, HLS ]\n      parameters:\n        - $ref: \"#/components/parameters/stream_src_path\"\n        - $ref: \"#/components/parameters/mp4_filter\"\n        - $ref: \"#/components/parameters/video_filter\"\n        - $ref: \"#/components/parameters/audio_filter\"\n      responses:\n        200:\n          description: \"\"\n          content: { application/vnd.apple.mpegurl: { example: \"\" } }\n\n  /api/hls/playlist.m3u8?id={id}:\n    get:\n      summary: Get HLS media playlist for an active session\n      tags: [ HLS ]\n      parameters:\n        - $ref: \"#/components/parameters/hls_session_id_path\"\n      responses:\n        \"200\":\n          description: OK\n          content:\n            application/vnd.apple.mpegurl: { example: \"\" }\n        \"404\":\n          description: Session not found\n\n  /api/hls/segment.ts?id={id}:\n    get:\n      summary: Get HLS MPEG-TS segment for an active session\n      tags: [ HLS ]\n      parameters:\n        - $ref: \"#/components/parameters/hls_session_id_path\"\n      responses:\n        \"200\":\n          description: OK\n          content:\n            video/mp2t: { example: \"\" }\n        \"404\":\n          description: Segment or session not found\n\n  /api/hls/init.mp4?id={id}:\n    get:\n      summary: Get HLS fMP4 init segment for an active session\n      tags: [ HLS ]\n      parameters:\n        - $ref: \"#/components/parameters/hls_session_id_path\"\n      responses:\n        \"200\":\n          description: OK\n          content:\n            video/mp4: { example: \"\" }\n        \"404\":\n          description: Segment or session not found\n\n  /api/hls/segment.m4s?id={id}:\n    get:\n      summary: Get HLS fMP4 media segment for an active session\n      tags: [ HLS ]\n      parameters:\n        - $ref: \"#/components/parameters/hls_session_id_path\"\n      responses:\n        \"200\":\n          description: OK\n          content:\n            video/iso.segment: { example: \"\" }\n        \"404\":\n          description: Segment or session not found\n\n  /api/stream.mjpeg?src={src}:\n    get:\n      summary: Get stream in MJPEG format\n      description: \"[Module: MJPEG](https://github.com/AlexxIT/go2rtc#module-mjpeg)\"\n      tags: [ Consume stream ]\n      parameters:\n        - $ref: \"#/components/parameters/stream_src_path\"\n      responses:\n        200:\n          description: \"\"\n          content: { multipart/x-mixed-replace: { example: \"\" } }\n\n  /api/stream.ascii?src={src}:\n    get:\n      summary: Get stream in ASCII-art format (ANSI escape codes)\n      description: \"[Module: MJPEG](https://github.com/AlexxIT/go2rtc#module-mjpeg)\"\n      tags: [ Consume stream ]\n      parameters:\n        - $ref: \"#/components/parameters/stream_src_path\"\n        - name: color\n          in: query\n          description: Foreground mode (`8`, `256`, `rgb` or ANSI SGR code)\n          required: false\n          schema: { type: string }\n        - name: back\n          in: query\n          description: Background mode (`8`, `256`, `rgb` or ANSI SGR code)\n          required: false\n          schema: { type: string }\n        - name: text\n          in: query\n          description: Charset preset (empty/default, `block`) or custom characters\n          required: false\n          schema: { type: string }\n      responses:\n        \"200\":\n          description: OK\n          content:\n            text/plain: { example: \"\" }\n        \"404\":\n          description: Stream not found\n\n  /api/stream.y4m?src={src}:\n    get:\n      summary: Get stream in YUV4MPEG2 format (y4m)\n      tags: [ Consume stream ]\n      parameters:\n        - $ref: \"#/components/parameters/stream_src_path\"\n      responses:\n        \"200\":\n          description: OK\n          content:\n            application/octet-stream: { example: \"\" }\n        \"404\":\n          description: Stream not found\n\n  /api/stream.ts?src={src}:\n    get:\n      summary: Get stream in MPEG-TS format\n      tags: [ Consume stream ]\n      parameters:\n        - $ref: \"#/components/parameters/stream_src_path\"\n      responses:\n        \"200\":\n          description: OK\n          content:\n            video/mp2t: { example: \"\" }\n        \"404\":\n          description: Stream not found\n\n  /api/stream.aac?src={src}:\n    get:\n      summary: Get stream audio in AAC (ADTS) format\n      tags: [ Consume stream ]\n      parameters:\n        - $ref: \"#/components/parameters/stream_src_path\"\n      responses:\n        \"200\":\n          description: OK\n          content:\n            audio/aac: { example: \"\" }\n        \"404\":\n          description: Stream not found\n\n  /api/stream.flv?src={src}:\n    get:\n      summary: Get stream in FLV format\n      tags: [ Consume stream ]\n      parameters:\n        - $ref: \"#/components/parameters/stream_src_path\"\n      responses:\n        \"200\":\n          description: OK\n          content:\n            video/x-flv: { example: \"\" }\n        \"404\":\n          description: Stream not found\n\n  /api/frame.jpeg?src={src}:\n    get:\n      summary: Get snapshot in JPEG format\n      description: \"[Module: MJPEG](https://github.com/AlexxIT/go2rtc#module-mjpeg)\"\n      tags: [ Snapshot ]\n      parameters:\n        - $ref: \"#/components/parameters/stream_src_path\"\n        - name: name\n          in: query\n          description: Optional stream name to create/update if `src` is a URL\n          required: false\n          schema: { type: string }\n        - name: width\n          in: query\n          description: \"Scale output width (alias: `w`)\"\n          required: false\n          schema: { type: integer, minimum: 1 }\n        - name: height\n          in: query\n          description: \"Scale output height (alias: `h`)\"\n          required: false\n          schema: { type: integer, minimum: 1 }\n        - name: rotate\n          in: query\n          description: \"Rotate output (degrees). Supported values: 90, 180, 270.\"\n          required: false\n          schema: { type: integer, enum: [ 90, 180, 270 ] }\n        - name: hardware\n          in: query\n          description: \"Hardware acceleration engine for FFmpeg snapshot transcoding (alias: `hw`)\"\n          required: false\n          schema: { type: string }\n      responses:\n        \"200\":\n          description: \"\"\n          content:\n            image/jpeg: { example: \"\" }\n\n  /api/frame.mp4?src={src}:\n    get:\n      summary: Get snapshot in MP4 format\n      description: \"[Module: MP4](https://github.com/AlexxIT/go2rtc#module-mp4)\"\n      tags: [ Snapshot ]\n      parameters:\n        - $ref: \"#/components/parameters/stream_src_path\"\n        - name: filename\n          in: query\n          description: Download as a file with this name\n          required: false\n          schema: { type: string }\n          example: camera1.mp4\n      responses:\n        200:\n          description: \"\"\n          content:\n            video/mp4: { example: \"\" }\n\n\n\n  /api/webrtc?dst={dst}:\n    post:\n      summary: Post stream in WebRTC format (WHIP)\n      description: \"[Incoming: WebRTC/WHIP](https://github.com/AlexxIT/go2rtc#incoming-webrtcwhip)\"\n      tags: [ Produce stream ]\n      parameters:\n        - $ref: \"#/components/parameters/stream_dst_path\"\n      responses:\n        \"201\":\n          description: Created\n          headers:\n            Location:\n              description: Resource URL for session\n              schema: { type: string }\n          content:\n            application/sdp: { example: \"v=0...\" }\n        \"404\":\n          description: Stream not found\n\n  /api/stream?dst={dst}:\n    post:\n      summary: Post stream in auto-detected format\n      description: |\n        Incoming source with automatic format detection. Use for pushing a stream into an existing `dst` stream.\n      tags: [ Produce stream ]\n      parameters:\n        - $ref: \"#/components/parameters/stream_dst_path\"\n      responses:\n        default:\n          description: \"\"\n\n  /api/stream.flv?dst={dst}:\n    post:\n      summary: Post stream in FLV format\n      description: \"[Incoming sources](https://github.com/AlexxIT/go2rtc#incoming-sources)\"\n      tags: [ Produce stream ]\n      parameters:\n        - $ref: \"#/components/parameters/stream_dst_path\"\n      responses:\n        default:\n          description: \"\"\n\n  /api/stream.ts?dst={dst}:\n    post:\n      summary: Post stream in MPEG-TS format\n      description: \"[Incoming sources](https://github.com/AlexxIT/go2rtc#incoming-sources)\"\n      tags: [ Produce stream ]\n      parameters:\n        - $ref: \"#/components/parameters/stream_dst_path\"\n      responses:\n        default:\n          description: \"\"\n\n  /api/stream.mjpeg?dst={dst}:\n    post:\n      summary: Post stream in MJPEG format\n      description: \"[Incoming sources](https://github.com/AlexxIT/go2rtc#incoming-sources)\"\n      tags: [ Produce stream ]\n      parameters:\n        - $ref: \"#/components/parameters/stream_dst_path\"\n      responses:\n        default:\n          description: \"\"\n\n\n  /api/ffmpeg:\n    post:\n      summary: Play file/live/TTS into a stream via FFmpeg\n      description: |\n        Helper endpoint for \"stream to camera\" scenarios.\n        Exactly one of `file`, `live`, `text` should be provided.\n      tags: [ FFmpeg ]\n      parameters:\n        - name: dst\n          in: query\n          description: Destination stream name\n          required: true\n          schema: { type: string }\n          example: camera1\n        - name: file\n          in: query\n          description: Input URL to treat as file (`#input=file`)\n          required: false\n          schema: { type: string }\n          example: \"http://example.com/song.mp3\"\n        - name: live\n          in: query\n          description: Live input URL\n          required: false\n          schema: { type: string }\n          example: \"http://example.com/live.mp3\"\n        - name: text\n          in: query\n          description: Text-to-speech phrase\n          required: false\n          schema: { type: string }\n          example: \"Hello\"\n        - name: voice\n          in: query\n          description: Optional TTS voice (engine-dependent)\n          required: false\n          schema: { type: string }\n      responses:\n        \"200\":\n          description: OK\n        \"400\":\n          description: Invalid parameters\n        \"404\":\n          description: Stream not found\n\n\n  /api/dvrip:\n    get:\n      summary: DVRIP cameras discovery\n      description: \"[Source: DVRIP](https://github.com/AlexxIT/go2rtc#source-dvrip)\"\n      tags: [ Discovery ]\n      responses:\n        \"200\": { $ref: \"#/components/responses/discovery\" }\n\n  /api/ffmpeg/devices:\n    get:\n      summary: FFmpeg USB devices discovery\n      description: \"[Source: FFmpeg Device](https://github.com/AlexxIT/go2rtc#source-ffmpeg-device)\"\n      tags: [ Discovery ]\n      responses:\n        \"200\": { $ref: \"#/components/responses/discovery\" }\n  \n  /api/ffmpeg/hardware:\n    get:\n      summary: FFmpeg hardware transcoding discovery\n      description: \"[Hardware acceleration](https://github.com/AlexxIT/go2rtc/wiki/Hardware-acceleration)\"\n      tags: [ Discovery ]\n      responses:\n        \"200\": { $ref: \"#/components/responses/discovery\" }\n  \n  /api/v4l2:\n    get:\n      summary: V4L2 video devices discovery (Linux)\n      tags: [ Discovery ]\n      responses:\n        \"200\": { $ref: \"#/components/responses/discovery\" }\n  \n  /api/alsa:\n    get:\n      summary: ALSA audio devices discovery (Linux)\n      tags: [ Discovery ]\n      responses:\n        \"200\": { $ref: \"#/components/responses/discovery\" }\n  \n  /api/gopro:\n    get:\n      summary: GoPro cameras discovery\n      tags: [ Discovery ]\n      responses:\n        \"200\": { $ref: \"#/components/responses/discovery\" }\n\n  /api/ring:\n    get:\n      summary: Ring cameras discovery\n      description: |\n        Provide either `email`/`password` (and optional `code` for 2FA) or `refresh_token`.\n        If 2FA is required, returns a JSON prompt instead of sources.\n      tags: [ Discovery ]\n      parameters:\n        - name: email\n          in: query\n          required: false\n          schema: { type: string }\n        - name: password\n          in: query\n          required: false\n          schema: { type: string }\n        - name: code\n          in: query\n          required: false\n          schema: { type: string }\n        - name: refresh_token\n          in: query\n          required: false\n          schema: { type: string }\n      responses:\n        \"200\":\n          description: OK\n          content:\n            application/json: { example: \"\" }\n\n  /api/tuya:\n    get:\n      summary: Tuya cameras discovery\n      tags: [ Discovery ]\n      parameters:\n        - name: region\n          in: query\n          description: Tuya API host (region)\n          required: true\n          schema: { type: string }\n          example: \"openapi.tuyaus.com\"\n        - name: email\n          in: query\n          required: true\n          schema: { type: string }\n        - name: password\n          in: query\n          required: true\n          schema: { type: string }\n      responses:\n        \"200\": { $ref: \"#/components/responses/discovery\" }\n        \"400\":\n          description: Invalid parameters\n        \"404\":\n          description: No cameras found\n  \n  /api/hass:\n    get:\n      summary: Home Assistant cameras discovery\n      description: \"[Source: Hass](https://github.com/AlexxIT/go2rtc#source-hass)\"\n      tags: [ Discovery ]\n      responses:\n        \"200\": { $ref: \"#/components/responses/discovery\" }\n        \"404\": { description: No Hass config }\n\n  /api/discovery/homekit:\n    get:\n      summary: HomeKit cameras discovery\n      description: \"[Source: HomeKit](https://github.com/AlexxIT/go2rtc#source-homekit)\"\n      tags: [ Discovery ]\n      responses:\n        \"200\": { $ref: \"#/components/responses/discovery\" }\n\n  /api/nest:\n    get:\n      summary: Nest cameras discovery\n      tags: [ Discovery ]\n      parameters:\n        - name: client_id\n          in: query\n          required: true\n          schema: { type: string }\n        - name: client_secret\n          in: query\n          required: true\n          schema: { type: string }\n        - name: refresh_token\n          in: query\n          required: true\n          schema: { type: string }\n        - name: project_id\n          in: query\n          required: true\n          schema: { type: string }\n      responses:\n        \"200\": { $ref: \"#/components/responses/discovery\" }\n\n  /api/onvif:\n    get:\n      summary: ONVIF cameras discovery\n      description: \"[Source: ONVIF](https://github.com/AlexxIT/go2rtc#source-onvif)\"\n      tags: [ Discovery ]\n      parameters:\n        - name: src\n          in: query\n          description: Optional ONVIF device URL to enumerate profiles\n          required: false\n          schema: { type: string }\n          example: \"onvif://user:pass@192.168.1.50:80\"\n      responses:\n        \"200\": { $ref: \"#/components/responses/discovery\" }\n\n  /api/roborock:\n    get:\n      summary: Roborock vacuums discovery (requires prior auth)\n      description: \"[Source: Roborock](https://github.com/AlexxIT/go2rtc#source-roborock)\"\n      tags: [ Discovery ]\n      responses:\n        \"200\": { $ref: \"#/components/responses/discovery\" }\n        \"404\":\n          description: No auth\n    post:\n      summary: Roborock login and discovery\n      tags: [ Discovery ]\n      requestBody:\n        required: true\n        content:\n          multipart/form-data:\n            schema:\n              type: object\n              properties:\n                username: { type: string }\n                password: { type: string }\n              required: [ username, password ]\n      responses:\n        \"200\": { $ref: \"#/components/responses/discovery\" }\n\n  /api/homekit:\n    get:\n      summary: Get HomeKit servers state\n      tags: [ HomeKit ]\n      parameters:\n        - name: id\n          in: query\n          description: Optional stream name (server ID)\n          required: false\n          schema: { type: string }\n      responses:\n        \"200\":\n          description: OK\n          content:\n            application/json: { example: \"\" }\n        \"404\":\n          description: Server not found\n    post:\n      summary: Pair HomeKit camera and create/update stream\n      tags: [ HomeKit ]\n      parameters:\n        - name: id\n          in: query\n          description: Stream name to create/update\n          required: true\n          schema: { type: string }\n        - name: src\n          in: query\n          description: HomeKit URL (without pin)\n          required: true\n          schema: { type: string }\n        - name: pin\n          in: query\n          description: HomeKit PIN\n          required: true\n          schema: { type: string }\n      responses:\n        \"200\":\n          description: OK\n    delete:\n      summary: Unpair HomeKit camera and delete stream\n      tags: [ HomeKit ]\n      parameters:\n        - name: id\n          in: query\n          description: Stream name / server ID\n          required: true\n          schema: { type: string }\n      responses:\n        \"200\":\n          description: OK\n        \"404\":\n          description: Stream not found\n\n  /api/homekit/accessories:\n    get:\n      summary: Get HomeKit accessories JSON for a stream\n      tags: [ HomeKit ]\n      parameters:\n        - name: id\n          in: query\n          description: Stream name\n          required: true\n          schema: { type: string }\n      responses:\n        \"200\":\n          description: OK\n          content:\n            application/json: { example: { } }\n        \"404\":\n          description: Stream not found\n\n  /pair-setup:\n    post:\n      summary: HomeKit Pair Setup (HAP)\n      description: HomeKit Accessory Protocol endpoint (TLV8).\n      tags: [ HomeKit ]\n      requestBody:\n        required: true\n        content:\n          application/pairing+tlv8: { example: \"\" }\n      responses:\n        \"200\":\n          description: OK\n          content:\n            application/pairing+tlv8: { example: \"\" }\n\n  /pair-verify:\n    post:\n      summary: HomeKit Pair Verify (HAP)\n      description: HomeKit Accessory Protocol endpoint (TLV8).\n      tags: [ HomeKit ]\n      requestBody:\n        required: true\n        content:\n          application/pairing+tlv8: { example: \"\" }\n      responses:\n        \"200\":\n          description: OK\n          content:\n            application/pairing+tlv8: { example: \"\" }\n\n\n  /onvif/:\n    get:\n      summary: ONVIF server implementation\n      description: Simple realisation of the ONVIF protocol. Accepts any suburl requests\n      tags: [ ONVIF ]\n      responses:\n        default:\n          description: \"\"\n\n\n\n  /stream/:\n    get:\n      summary: RTSPtoWebRTC server implementation\n      description: Simple API for support [RTSPtoWebRTC](https://www.home-assistant.io/integrations/rtsp_to_webrtc/) integration\n      tags: [ RTSPtoWebRTC ]\n      responses:\n        default:\n          description: \"\"\n\n\n  /api/ws:\n    get:\n      summary: WebSocket endpoint\n      description: |\n        Upgrade to WebSocket and exchange JSON messages:\n        - Request: `{ \"type\": \"...\", \"value\": ... }`\n        - Response: `{ \"type\": \"...\", \"value\": ... }`\n        \n        Supported message types depend on enabled modules (see `api/README.md`).\n      tags: [ WebSocket ]\n      parameters:\n        - name: src\n          in: query\n          description: Stream name (consumer)\n          required: false\n          schema: { type: string }\n        - name: dst\n          in: query\n          description: Stream name (producer)\n          required: false\n          schema: { type: string }\n      responses:\n        \"101\": { description: Switching Protocols }\n\n\n\n  /api/webtorrent?src={src}:\n    get:\n      summary: Get WebTorrent share info\n      tags: [ WebTorrent ]\n      parameters:\n        - $ref: \"#/components/parameters/stream_src_path\"\n      responses:\n        200: { $ref: \"#/components/responses/webtorrent\" }\n    post:\n      summary: Add WebTorrent share\n      tags: [ WebTorrent ]\n      parameters:\n        - $ref: \"#/components/parameters/stream_src_path\"\n      responses:\n        200: { $ref: \"#/components/responses/webtorrent\" }\n    delete:\n      summary: Delete WebTorrent share\n      tags: [ WebTorrent ]\n      parameters:\n        - $ref: \"#/components/parameters/stream_src_path\"\n      responses:\n        default: { description: \"\" }\n\n  /api/webtorrent:\n    get:\n      summary: Get all WebTorrent shares info\n      tags: [ WebTorrent ]\n      responses:\n        200: { $ref: \"#/components/responses/discovery\" }\n\n\n\n  /api/stack:\n    get:\n      summary: Show list unknown goroutines\n      tags: [ Debug ]\n      responses:\n        200:\n          description: \"\"\n          content: { text/plain: { example: \"\" } }\n"
  },
  {
    "path": "website/manifest.json",
    "content": "{\n  \"name\": \"go2rtc\",\n  \"icons\": [\n    {\n      \"src\": \"https://go2rtc.org/icons/android-chrome-192x192.png\",\n      \"sizes\": \"192x192\",\n      \"type\": \"image/png\"\n    },\n    {\n      \"src\": \"https://go2rtc.org/icons/android-chrome-512x512.png\",\n      \"sizes\": \"512x512\",\n      \"type\": \"image/png\"\n    }\n  ],\n  \"display\": \"standalone\",\n  \"theme_color\": \"#000000\",\n  \"background_color\": \"#000000\"\n}\n"
  },
  {
    "path": "website/webtorrent/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <title>webtorrent - go2rtc</title>\n    <style>\n        body {\n            background-color: black;\n            margin: 0;\n            padding: 0;\n        }\n\n        html, body, video {\n            height: 100%;\n            width: 100%;\n        }\n\n        div {\n            position: absolute;\n            top: 50%;\n            left: 50%;\n            display: flex;\n            flex-direction: column;\n            transform: translateX(-50%) translateY(-50%);\n        }\n    </style>\n</head>\n<body>\n<video id=\"video\" autoplay controls playsinline muted></video>\n<div id=\"login\">\n    <input id=\"share\" type=\"text\" placeholder=\"share\">\n    <input id=\"pwd\" type=\"text\" placeholder=\"password\">\n    <button id=\"connect\">connect</button>\n</div>\n<script>\n    async function PeerConnection(media) {\n        const pc = new RTCPeerConnection({\n            iceServers: [{urls: 'stun:stun.l.google.com:19302'}]\n        });\n\n        const localTracks = [];\n\n        if (/camera|microphone/.test(media)) {\n            const tracks = await getMediaTracks('user', {\n                video: media.indexOf('camera') >= 0,\n                audio: media.indexOf('microphone') >= 0,\n            });\n            tracks.forEach(track => {\n                pc.addTransceiver(track, {direction: 'sendonly'});\n                if (track.kind === 'video') localTracks.push(track);\n            });\n        }\n\n        if (media.indexOf('display') >= 0) {\n            const tracks = await getMediaTracks('display', {\n                video: true,\n                audio: media.indexOf('speaker') >= 0,\n            });\n            tracks.forEach(track => {\n                pc.addTransceiver(track, {direction: 'sendonly'});\n                if (track.kind === 'video') localTracks.push(track);\n            });\n        }\n\n        if (/video|audio/.test(media)) {\n            const tracks = ['video', 'audio']\n                .filter(kind => media.indexOf(kind) >= 0)\n                .map(kind => pc.addTransceiver(kind, {direction: 'recvonly'}).receiver.track);\n            localTracks.push(...tracks);\n        }\n\n        document.getElementById('video').srcObject = new MediaStream(localTracks);\n\n        return pc;\n    }\n\n    async function getMediaTracks(media, constraints) {\n        try {\n            const stream = media === 'user'\n                ? await navigator.mediaDevices.getUserMedia(constraints)\n                : await navigator.mediaDevices.getDisplayMedia(constraints);\n            return stream.getTracks();\n        } catch (e) {\n            console.warn(e);\n            return [];\n        }\n    }\n\n    function getOffer(pc, timeout) {\n        return new Promise((resolve, reject) => {\n            pc.addEventListener('icegatheringstatechange', () => {\n                if (pc.iceGatheringState === 'complete') resolve(pc.localDescription.sdp);\n            });\n\n            pc.createOffer().then(offer => pc.setLocalDescription(offer));\n\n            setTimeout(() => resolve(pc.localDescription.sdp), timeout || 5000);\n        });\n    }\n</script>\n<script>\n    function decode(buffer) {\n        return String.fromCharCode(...new Uint8Array(buffer));\n    }\n\n    function encode(string) {\n        return Uint8Array.from(string, c => c.charCodeAt(0));\n    }\n\n    async function cipher(share, pwd) {\n        const hash = await crypto.subtle.digest('SHA-256', encode(share));\n        const nonce = (Date.now() * 1000000).toString(36);\n\n        const ivData = await crypto.subtle.digest('SHA-256', encode(share + ':' + nonce));\n        const keyData = await crypto.subtle.digest('SHA-256', encode(nonce + ':' + pwd));\n        const key = await crypto.subtle.importKey(\n            'raw', keyData, {name: 'AES-GCM'}, false, ['encrypt', 'decrypt'],\n        );\n\n        return {\n            hash: btoa(decode(hash)),\n            nonce: nonce,\n            encrypt: async function (plaintext) {\n                const cryptotext = await crypto.subtle.encrypt(\n                    {name: 'AES-GCM', iv: ivData.slice(0, 12), additionalData: encode(nonce)},\n                    key, encode(plaintext),\n                );\n                return btoa(decode(cryptotext));\n            },\n            decrypt: async function (cryptotext) {\n                const plaintext = await crypto.subtle.decrypt(\n                    {name: 'AES-GCM', iv: ivData.slice(0, 12), additionalData: encode(nonce)},\n                    key, encode(atob(cryptotext)),\n                );\n                return decode(plaintext);\n            }\n        };\n    }\n</script>\n<script>\n    async function connect(share, pwd, media, tracker) {\n        const crypto = await cipher(share, pwd);\n        const pc = await PeerConnection(media || 'video+audio');\n        const offer = await crypto.encrypt(await getOffer(pc));\n\n        const ws = new WebSocket(tracker || 'wss://tracker.openwebtorrent.com/');\n        ws.addEventListener('open', () => {\n            ws.send(JSON.stringify({\n                action: 'announce',\n                info_hash: crypto.hash,\n                peer_id: Math.random().toString(36).substring(2),\n                offers: [{\n                    offer_id: crypto.nonce,\n                    offer: {type: 'offer', sdp: offer},\n                }],\n                numwant: 1,\n            }));\n        });\n\n        ws.addEventListener('message', async (ev) => {\n            const msg = JSON.parse(ev.data);\n            if (!msg.answer) return;\n\n            const answer = await crypto.decrypt(msg.answer.sdp);\n            await pc.setRemoteDescription({type: 'answer', sdp: answer});\n\n            ws.close();\n        });\n    }\n\n    document.getElementById('connect').addEventListener('click', () => {\n        const share = document.getElementById('share').value;\n        const pwd = document.getElementById('pwd').value;\n        connect(share, pwd);\n        document.getElementById('login').style.display = 'none';\n    });\n\n    if (location.hash) {\n        const params = new URLSearchParams(location.hash.substring(1));\n        const share = params.get('share');\n        const pwd = params.get('pwd');\n        const media = params.get('media');\n        const tracker = params.get('tr');\n        connect(share, pwd, media, tracker);\n        document.getElementById('login').style.display = 'none';\n    }\n</script>\n</body>\n</html>"
  },
  {
    "path": "www/README.md",
    "content": "# www\n\nThis folder contains static HTTP and JS content that is embedded into the application during build. An external developer can use it as a basis for integrating go2rtc into their project or for developing a custom web interface for go2rtc.\n\n## HTTP API\n\n`www/stream.html` - universal viewer with support params in URL:\n\n- multiple streams on page `src=camera1&src=camera2...`\n- stream technology autoselection `mode=webrtc,webrtc/tcp,mse,hls,mp4,mjpeg`\n- stream technology comparison `src=camera1&mode=webrtc&mode=mse&mode=mp4`\n- player width setting in pixels `width=320px` or percents `width=50%`\n\n`www/webrtc.html` - WebRTC viewer with support two way audio and params in URL:\n\n- `media=video+audio` - simple viewer\n- `media=video+audio+microphone` - two way audio from camera\n- `media=camera+microphone` - stream from browser\n- `media=display+speaker` - stream from desktop\n\n## JavaScript API\n\n- You can write your viewer from the scratch\n- You can extend the built-in viewer - `www/video-rtc.js`\n- Check example - `www/video-stream.js`\n- Check example - https://github.com/AlexxIT/WebRTC\n\n`video-rtc.js` features:\n\n- support technologies:\n    - WebRTC over UDP or TCP\n    - MSE or HLS or MP4 or MJPEG over WebSocket\n- automatic selection best technology according on:\n    - codecs inside your stream\n    - current browser capabilities\n    - current network configuration\n- automatic stop stream while browser or page not active\n- automatic stop stream while player not inside page viewport\n- automatic reconnection\n\nTechnology selection based on priorities:\n\n1. Video and Audio better than just Video\n2. H265 better than H264\n3. WebRTC better than MSE, than HLS, than MJPEG\n\n## Browser support\n\n[ECMAScript 2019 (ES10)](https://caniuse.com/?search=es10) supported by [iOS 12](https://en.wikipedia.org/wiki/IOS_12) (iPhone 5S, iPad Air, iPad Mini 2, etc.).\n\nBut [ECMAScript 2017 (ES8)](https://caniuse.com/?search=es8) almost fine (`es6 + async`) and recommended for [React+TypeScript](https://github.com/typescript-cheatsheets/react).\n\n## Known problems\n\n- Autoplay doesn't work for WebRTC in Safari [read more](https://developer.apple.com/documentation/webkit/delivering_video_content_for_safari/).\n\n## Useful links\n\n- https://www.webrtc-experiment.com/DetectRTC/\n- https://divtable.com/table-styler/\n- https://www.chromium.org/audio-video/\n- https://web.dev/i18n/en/fast-playback-with-preload/#manual_buffering\n- https://developer.mozilla.org/en-US/docs/Web/API/Media_Source_Extensions_API\n- https://chromium.googlesource.com/external/w3c/web-platform-tests/+/refs/heads/master/media-source/mediasource-is-type-supported.html\n- https://googlechrome.github.io/samples/media/sourcebuffer-changetype.html\n- https://chromestatus.com/feature/5100845653819392\n- https://developer.apple.com/documentation/webkit/delivering_video_content_for_safari\n- https://dirask.com/posts/JavaScript-supported-Audio-Video-MIME-Types-by-MediaRecorder-Chrome-and-Firefox-jERn81\n- https://privacycheck.sec.lrz.de/active/fp_cpt/fp_can_play_type.html\n"
  },
  {
    "path": "www/add.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>add - go2rtc</title>\n    <style>\n        main > button {\n            background-color: #444;\n            color: white;\n            cursor: pointer;\n            padding: 14px;\n            width: 100%;\n            border: none;\n            text-align: left;\n            font-size: 16px;\n            font-weight: bold;\n        }\n\n        main > div {\n            display: none;\n            gap: 10px;\n        }\n    </style>\n</head>\n<body>\n\n<script src=\"main.js\"></script>\n\n<script>\n    function drawTable(table, data) {\n        const cols = ['id', 'name', 'info', 'url', 'location'];\n        const th = (row) => cols.reduce((html, k) => k in row ? `${html}<th>${k}</th>` : html, '<tr>') + '</tr>';\n        const td = (row) => cols.reduce((html, k) => k in row ? `${html}<td style=\"word-break: break-word; white-space: normal;\">${row[k]}</td>` : html, '<tr>') + '</tr>';\n\n        const thead = th(data.sources[0]);\n        const tbody = data.sources.reduce((html, source) => `${html}${td(source)}`, '');\n\n        table.innerHTML = `<thead>${thead}</thead><tbody>${tbody}</tbody>`;\n    }\n\n    async function getSources(tableID, url) {\n        const table = document.getElementById(tableID);\n        table.innerText = 'loading...';\n\n        const r = typeof url === 'string' ? await fetch(url, {cache: 'no-cache'}) : url;\n        if (!r.ok) {\n            table.innerText = await r.text();\n            return;\n        }\n\n        drawTable(table, await r.json());\n    }\n</script>\n\n<main>\n    <button id=\"stream\">Temporary stream</button>\n    <div>\n        <form id=\"stream-form\">\n            <input type=\"text\" name=\"name\" placeholder=\"name\">\n            <input type=\"text\" name=\"src\" placeholder=\"url\" required size=\"30\">\n            <button type=\"submit\">add</button>\n        </form>\n    </div>\n    <script>\n        document.getElementById('stream').addEventListener('click', async ev => {\n            ev.target.nextElementSibling.style.display = 'grid';\n        });\n\n        document.getElementById('stream-form').addEventListener('submit', async ev => {\n            ev.preventDefault();\n\n            const url = new URL('api/streams', location.href);\n            url.searchParams.set('name', ev.target.elements['name'].value);\n            url.searchParams.set('src', ev.target.elements['src'].value);\n\n            const r = await fetch(url, {method: 'PUT'});\n            alert(r.ok ? 'OK' : 'ERROR: ' + await r.text());\n        });\n    </script>\n\n\n    <button id=\"alsa\">ALSA (Linux audio)</button>\n    <div>\n        <table id=\"alsa-table\"></table>\n    </div>\n    <script>\n        document.getElementById('alsa').addEventListener('click', async ev => {\n            ev.target.nextElementSibling.style.display = 'grid';\n            await getSources('alsa-table', 'api/alsa');\n        });\n    </script>\n\n\n    <button id=\"homekit\">Apple HomeKit</button>\n    <div>\n        <form id=\"homekit-pair\">\n            <input type=\"text\" name=\"id\" placeholder=\"stream id\" required>\n            <input type=\"text\" name=\"src\" placeholder=\"src\" required size=\"30\">\n            <input type=\"text\" name=\"pin\" placeholder=\"pin\" required size=\"10\">\n            <button type=\"submit\">pair</button>\n        </form>\n        <form id=\"homekit-unpair\">\n            <input type=\"text\" name=\"id\" placeholder=\"stream id\" required>\n            <button type=\"submit\">unpair</button>\n        </form>\n        <table id=\"homekit-table\"></table>\n    </div>\n    <script>\n        async function reloadHomeKit() {\n            await getSources('homekit-table', 'api/discovery/homekit');\n\n            const rows = document.querySelectorAll('#homekit-table tr');\n            rows.forEach((row, i) => {\n                let commands = '';\n                if (row.children[2].innerText.indexOf('status=1') > 0) {\n                    commands += '<a href=\"#\">pair</a>';\n                } else if (i > 0 && row.children[3].innerText) {\n                    commands += '<a href=\"#\">unpair</a>';\n                }\n                row.innerHTML += i > 0 ? `<td>${commands}</td>` : '<th>commands</th>';\n            });\n        }\n\n        document.getElementById('homekit').addEventListener('click', async ev => {\n            ev.target.nextElementSibling.style.display = 'grid';\n            await reloadHomeKit();\n        });\n\n        document.getElementById('homekit-table').addEventListener('click', ev => {\n            if (ev.target.innerText === 'pair') {\n                const form = document.querySelector('#homekit-pair');\n                const row = ev.target.closest('tr');\n                form.children[0].value = row.children[0].innerText;\n                form.children[1].value = row.children[2].innerText;\n            } else if (ev.target.innerText === 'unpair') {\n                const form = document.querySelector('#homekit-unpair');\n                const row = ev.target.closest('tr');\n                form.children[0].value = row.children[3].innerText;\n            }\n        });\n\n        document.getElementById('homekit-pair').addEventListener('submit', async ev => {\n            ev.preventDefault();\n\n            const params = new URLSearchParams(new FormData(ev.target));\n            const r = await fetch('api/homekit', {method: 'POST', body: params});\n            alert(r.ok ? 'OK' : 'ERROR: ' + await r.text());\n\n            await reloadHomeKit();\n        });\n\n        document.getElementById('homekit-unpair').addEventListener('submit', async ev => {\n            ev.preventDefault();\n\n            const params = new URLSearchParams(new FormData(ev.target));\n            const r = await fetch('api/homekit?' + params.toString(), {method: 'DELETE'});\n            alert(r.ok ? 'OK' : 'ERROR: ' + await r.text());\n\n            await reloadHomeKit();\n        });\n    </script>\n\n\n    <button id=\"dvrip\">DVRIP</button>\n    <div>\n        <table id=\"dvrip-table\"></table>\n    </div>\n    <script>\n        document.getElementById('dvrip').addEventListener('click', async ev => {\n            ev.target.nextElementSibling.style.display = 'grid';\n            await getSources('dvrip-table', 'api/dvrip');\n        });\n    </script>\n\n\n    <button id=\"devices\">FFmpeg Devices (USB)</button>\n    <div>\n        <table id=\"devices-table\"></table>\n    </div>\n    <script>\n        document.getElementById('devices').addEventListener('click', async ev => {\n            ev.target.nextElementSibling.style.display = 'grid';\n            await getSources('devices-table', 'api/ffmpeg/devices');\n        });\n    </script>\n\n\n    <button id=\"hardware\">FFmpeg Hardware</button>\n    <div>\n        <table id=\"hardware-table\"></table>\n    </div>\n    <script>\n        document.getElementById('hardware').addEventListener('click', async ev => {\n            ev.target.nextElementSibling.style.display = 'grid';\n            await getSources('hardware-table', 'api/ffmpeg/hardware');\n        });\n    </script>\n\n\n    <button id=\"nest\">Google Nest</button>\n    <div>\n        <form id=\"nest-form\">\n            <input type=\"text\" name=\"client_id\" placeholder=\"client_id\" required>\n            <input type=\"text\" name=\"client_secret\" placeholder=\"client_secret\" required>\n            <input type=\"text\" name=\"refresh_token\" placeholder=\"refresh_token\" required>\n            <input type=\"text\" name=\"project_id\" placeholder=\"project_id\" required>\n            <button type=\"submit\">login</button>\n        </form>\n        <table id=\"nest-table\"></table>\n    </div>\n    <script>\n        document.getElementById('nest').addEventListener('click', async ev => {\n            ev.target.nextElementSibling.style.display = 'grid';\n        });\n\n        document.getElementById('nest-form').addEventListener('submit', async ev => {\n            ev.preventDefault();\n\n            const query = new URLSearchParams(new FormData(ev.target));\n            const url = new URL('api/nest?' + query.toString(), location.href);\n\n            const r = await fetch(url, {cache: 'no-cache'});\n            await getSources('nest-table', r);\n        });\n    </script>\n\n    <button id=\"gopro\">GoPro</button>\n    <div>\n        <table id=\"gopro-table\"></table>\n    </div>\n    <script>\n        document.getElementById('gopro').addEventListener('click', async ev => {\n            ev.target.nextElementSibling.style.display = 'grid';\n            await getSources('gopro-table', 'api/gopro');\n        });\n    </script>\n\n\n    <button id=\"hass\">Home Assistant</button>\n    <div>\n        <table id=\"hass-table\"></table>\n    </div>\n    <script>\n        document.getElementById('hass').addEventListener('click', async ev => {\n            ev.target.nextElementSibling.style.display = 'grid';\n            await getSources('hass-table', 'api/hass');\n        });\n    </script>\n\n\n    <button id=\"onvif\">ONVIF</button>\n    <div>\n        <form id=\"onvif-form\">\n            <input type=\"text\" name=\"src\" placeholder=\"onvif://user:pass@192.168.1.123:80\" required size=\"30\">\n            <button type=\"submit\">test</button>\n        </form>\n        <table id=\"onvif-table\"></table>\n    </div>\n    <script>\n        document.getElementById('onvif').addEventListener('click', async ev => {\n            ev.target.nextElementSibling.style.display = 'grid';\n            await getSources('onvif-table', 'api/onvif');\n        });\n\n        document.getElementById('onvif-form').addEventListener('submit', async ev => {\n            ev.preventDefault();\n\n            const url = new URL('api/onvif', location.href);\n            url.searchParams.set('src', ev.target.elements['src'].value);\n\n            await getSources('onvif-table', url.toString());\n        });\n    </script>\n\n\n    <button id=\"ring\">Ring</button>\n    <div>\n        <form id=\"ring-credentials-form\">\n            <input type=\"email\" name=\"email\" placeholder=\"email\" required>\n            <input type=\"password\" name=\"password\" placeholder=\"password\" required>\n            <div id=\"tfa-field\" style=\"display: none\">\n                <input type=\"text\" name=\"code\" placeholder=\"2FA code\">\n                <div id=\"tfa-prompt\"></div>\n            </div>\n            <button type=\"submit\">login</button>\n        </form>\n        <form id=\"ring-token-form\">\n            <input type=\"text\" name=\"refresh_token\" placeholder=\"refresh_token\" required>\n            <button type=\"submit\">login</button>\n        </form>\n        <table id=\"ring-table\"></table>\n    </div>\n    <script>\n        document.getElementById('ring').addEventListener('click', async ev => {\n            ev.target.nextElementSibling.style.display = 'grid';\n        });\n\n        async function handleRingAuth(ev) {\n            ev.preventDefault();\n\n            const table = document.getElementById('ring-table');\n            table.innerText = 'loading...';\n\n            const query = new URLSearchParams(new FormData(ev.target));\n            const url = new URL('api/ring?' + query.toString(), location.href);\n\n            const r = await fetch(url, {cache: 'no-cache'});\n\n            if (!r.ok) {\n                table.innerText = (await r.text()) || 'Unknown error';\n                return;\n            }\n\n            const data = await r.json();\n\n            table.innerText = '';\n\n            if (data.needs_2fa) {\n                document.getElementById('tfa-field').style.display = 'block';\n                document.getElementById('tfa-prompt').textContent = data.prompt || 'Enter 2FA code';\n                return;\n            }\n\n            drawTable(table, data);\n        }\n\n        document.getElementById('ring-credentials-form').addEventListener('submit', handleRingAuth);\n        document.getElementById('ring-token-form').addEventListener('submit', handleRingAuth);\n    </script>\n\n\n    <button id=\"tuya\">Tuya</button>\n    <div>\n        <form id=\"tuya-credentials-form\">\n            <select name=\"region\" required>\n                <option value=\"protect-eu.ismartlife.me\">EU Central</option>\n                <option value=\"protect-we.ismartlife.me\">EU East</option>\n                <option value=\"protect-us.ismartlife.me\">US West</option>\n                <option value=\"protect-ue.ismartlife.me\">US East</option>\n                <option value=\"protect.ismartlife.me\">China</option>\n                <option value=\"protect-in.ismartlife.me\">India</option>\n            </select>\n            <input type=\"email\" name=\"email\" placeholder=\"email\" required>\n            <input type=\"password\" name=\"password\" placeholder=\"password\" required>\n            <button type=\"submit\">login</button>\n        </form>\n        <table id=\"tuya-table\"></table>\n    </div>\n    <script>\n        document.getElementById('tuya').addEventListener('click', async ev => {\n            ev.target.nextElementSibling.style.display = 'grid';\n        });\n\n        document.getElementById('tuya-credentials-form').addEventListener('submit', async ev => {\n            ev.preventDefault();\n\n            const table = document.getElementById('tuya-table');\n            table.innerText = 'loading...';\n\n            const query = new URLSearchParams(new FormData(ev.target));\n            const url = new URL('api/tuya?' + query.toString(), location.href);\n\n            const r = await fetch(url, {cache: 'no-cache'});\n\n            if (!r.ok) {\n                table.innerText = (await r.text()) || 'Unknown error';\n                return;\n            }\n\n            const data = await r.json();\n\n            table.innerText = '';\n\n            drawTable(table, data);\n        });\n    </script>\n\n\n    <button id=\"roborock\">Roborock</button>\n    <div>\n        <form id=\"roborock-form\">\n            <input type=\"text\" name=\"username\" placeholder=\"username\" required>\n            <input type=\"password\" name=\"password\" placeholder=\"password\" required>\n            <button type=\"submit\">login</button>\n        </form>\n        <table id=\"roborock-table\">\n        </table>\n    </div>\n    <script>\n        document.getElementById('roborock').addEventListener('click', async ev => {\n            ev.target.nextElementSibling.style.display = 'grid';\n            await getSources('roborock-table', 'api/roborock');\n        });\n\n        document.getElementById('roborock-form').addEventListener('submit', async ev => {\n            ev.preventDefault();\n            const r = await fetch('api/roborock', {method: 'POST', body: new FormData(ev.target)});\n            await getSources('roborock-table', r);\n        });\n    </script>\n\n\n    <button id=\"v4l2\">V4L2 (Linux video)</button>\n    <div>\n        <table id=\"v4l2-table\"></table>\n    </div>\n    <script>\n        document.getElementById('v4l2').addEventListener('click', async ev => {\n            ev.target.nextElementSibling.style.display = 'grid';\n            await getSources('v4l2-table', 'api/v4l2');\n        });\n    </script>\n\n\n    <button id=\"wyze\">Wyze</button>\n    <div>\n        <p style=\"margin: 5px 0; font-size: 12px; color: #888;\">\n            API Key required: <a href=\"https://support.wyze.com/hc/en-us/articles/16129834216731\" target=\"_blank\">Get your API Key</a>\n        </p>\n        <form id=\"wyze-login-form\">\n            <input type=\"text\" name=\"api_id\" placeholder=\"API ID\" required size=\"20\">\n            <input type=\"text\" name=\"api_key\" placeholder=\"API Key\" required size=\"36\">\n            <input type=\"email\" name=\"email\" placeholder=\"email\" required>\n            <input type=\"password\" name=\"password\" placeholder=\"password\" required>\n            <button type=\"submit\">login</button>\n        </form>\n        <form id=\"wyze-devices-form\">\n            <select id=\"wyze-id\" name=\"id\" required></select>\n            <button type=\"submit\">load devices</button>\n        </form>\n        <table id=\"wyze-table\"></table>\n    </div>\n    <script>\n        async function wyzeReload(ev) {\n            if (ev) ev.target.nextElementSibling.style.display = 'grid';\n\n            const r = await fetch('api/wyze', {'cache': 'no-cache'});\n            const data = await r.json();\n            const users = document.getElementById('wyze-id');\n            users.innerHTML = data.map(item => `<option value=\"${item}\">${item}</option>`).join('');\n        }\n\n        document.getElementById('wyze').addEventListener('click', wyzeReload);\n\n        document.getElementById('wyze-login-form').addEventListener('submit', async ev => {\n            ev.preventDefault();\n\n            const table = document.getElementById('wyze-table');\n            table.innerText = 'loading...';\n\n            const params = new URLSearchParams(new FormData(ev.target));\n            const r = await fetch('api/wyze', {method: 'POST', body: params});\n\n            if (!r.ok) {\n                table.innerText = (await r.text()) || 'Unknown error';\n                return;\n            }\n\n            const data = await r.json();\n            table.innerText = '';\n            drawTable(table, data);\n            wyzeReload();\n        });\n\n        document.getElementById('wyze-devices-form').addEventListener('submit', async ev => {\n            ev.preventDefault();\n            const params = new URLSearchParams(new FormData(ev.target));\n            await getSources('wyze-table', 'api/wyze?' + params.toString());\n        });\n    </script>\n\n\n    <button id=\"xiaomi\">Xiaomi</button>\n    <div>\n        <form id=\"xiaomi-login-form\">\n            <input type=\"text\" name=\"username\" placeholder=\"username\" required>\n            <input type=\"password\" name=\"password\" placeholder=\"password\" required>\n            <button type=\"submit\">login</button>\n        </form>\n        <form id=\"xiaomi-captcha-form\">\n            <img id=\"xiaomi-captcha\">\n            <input type=\"text\" name=\"captcha\" placeholder=\"captcha\" required size=\"10\">\n            <button type=\"submit\">send</button>\n        </form>\n        <form id=\"xiaomi-verify-form\">\n            <label id=\"xiaomi-verify\"></label>\n            <input type=\"text\" name=\"verify\" placeholder=\"verify\" required size=\"10\">\n            <button type=\"submit\">send</button>\n        </form>\n        <form id=\"xiaomi-devices-form\">\n            <select id=\"xiaomi-id\" name=\"id\" required></select>\n            <select name=\"region\" required>\n                <option value=\"cn\">China</option>\n                <option value=\"de\">Europe</option>\n                <option value=\"i2\">India</option>\n                <option value=\"ru\">Russia</option>\n                <option value=\"sg\">Singapore</option>\n                <option value=\"us\">United States</option>\n            </select>\n            <button type=\"submit\">load devices</button>\n        </form>\n        <table id=\"xiaomi-table\"></table>\n    </div>\n    <script>\n        async function xiaomiReload(ev) {\n            if (ev) ev.target.nextElementSibling.style.display = 'grid';\n\n            document.getElementById('xiaomi-login-form').style.display = 'flex';\n            document.getElementById('xiaomi-captcha-form').style.display = 'none';\n            document.getElementById('xiaomi-verify-form').style.display = 'none';\n\n            const r = await fetch('api/xiaomi', {'cache': 'no-cache'});\n            const data = await r.json();\n            const users = document.getElementById('xiaomi-id');\n            users.innerHTML = data.map(item => `<option value=\"${item}\">${item}</option>`).join('');\n        }\n\n        document.getElementById('xiaomi').addEventListener('click', xiaomiReload);\n\n        async function xiaomiLogin(ev) {\n            ev.preventDefault();\n            const params = new URLSearchParams(new FormData(ev.target));\n            const r = await fetch('api/xiaomi', {method: 'POST', body: params});\n            if (r.status === 401) {\n                /** @type {{captcha: string, verify_email: string, verify_phone: string}} */\n                const data = await r.json();\n                document.getElementById('xiaomi-login-form').style.display = 'none';\n                if (data.captcha) {\n                    document.getElementById('xiaomi-captcha-form').style.display = 'flex';\n                    document.getElementById('xiaomi-captcha').src = 'data:image/jpeg;base64,' + data.captcha;\n                } else {\n                    document.getElementById('xiaomi-verify-form').style.display = 'flex';\n                    document.getElementById('xiaomi-verify').innerText = data.verify_email || data.verify_phone;\n                }\n            } else if (r.ok) {\n                alert('OK');\n                xiaomiReload();\n            } else {\n                alert('ERROR: ' + await r.text());\n            }\n        }\n\n        document.getElementById('xiaomi-login-form').addEventListener('submit', xiaomiLogin);\n        document.getElementById('xiaomi-captcha-form').addEventListener('submit', xiaomiLogin);\n        document.getElementById('xiaomi-verify-form').addEventListener('submit', xiaomiLogin);\n\n        document.getElementById('xiaomi-devices-form').addEventListener('submit', async ev => {\n            ev.preventDefault();\n            const params = new URLSearchParams(new FormData(ev.target));\n            await getSources('xiaomi-table', 'api/xiaomi?' + params.toString());\n        });\n    </script>\n\n\n    <button id=\"webtorrent\">WebTorrent Shares</button>\n    <div>\n        <table id=\"webtorrent-table\"></table>\n    </div>\n    <script>\n        document.getElementById('webtorrent').addEventListener('click', async ev => {\n            ev.target.nextElementSibling.style.display = 'grid';\n            await getSources('webtorrent-table', 'api/webtorrent');\n        });\n    </script>\n</main>\n\n</body>\n</html>"
  },
  {
    "path": "www/config.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>config - go2rtc</title>\n    <style>\n        html, body {\n            height: 100%;\n        }\n\n        #config {\n            flex: 1 1 auto;\n            border-top: 1px solid #ccc;\n            min-height: 300px;\n        }\n    </style>\n</head>\n<body>\n\n<script src=\"main.js\"></script>\n\n<main>\n    <div>\n        <button id=\"save\">Save & Restart</button>\n        <button id=\"suggest\" title=\"ctrl + space\">Suggest</button>\n    </div>\n</main>\n<div id=\"config\"></div>\n\n<script src=\"https://cdn.jsdelivr.net/npm/monaco-editor@0.55.1/min/vs/loader.js\"></script>\n<script src=\"https://cdn.jsdelivr.net/npm/js-yaml@4.1.0/dist/js-yaml.min.js\"></script>\n<script>\n    /* global require, monaco */\n    const monacoRoot = 'https://cdn.jsdelivr.net/npm/monaco-editor@0.55.1/min';\n\n    window.MonacoEnvironment = {\n        getWorkerUrl: function () {\n            return `data:text/javascript;charset=utf-8,${encodeURIComponent(`\n                self.MonacoEnvironment = { baseUrl: '${monacoRoot}/' };\n                importScripts('${monacoRoot}/vs/base/worker/workerMain.js');\n            `)}`;\n        }\n    };\n\n    require.config({paths: {vs: `${monacoRoot}/vs`}});\n\n    require(['vs/editor/editor.main'], () => {\n        const container = document.getElementById('config');\n        container.textContent = '';\n\n        const ensureYamlLanguage = () => {\n            const languages =\n                (window.monaco &&\n                    monaco.languages &&\n                    typeof monaco.languages.getLanguages === 'function' &&\n                    monaco.languages.getLanguages()) ||\n                [];\n            const hasYaml = languages.some((l) => l.id === 'yaml');\n            if (hasYaml) return;\n\n            monaco.languages.register({\n                id: 'yaml',\n                extensions: ['.yaml', '.yml'],\n                aliases: ['YAML', 'yaml'],\n                mimetypes: ['application/x-yaml', 'text/yaml'],\n            });\n\n            monaco.languages.setLanguageConfiguration('yaml', {\n                comments: {lineComment: '#'},\n                brackets: [['{', '}'], ['[', ']'], ['(', ')']],\n                autoClosingPairs: [\n                    {open: '{', close: '}'},\n                    {open: '[', close: ']'},\n                    {open: '(', close: ')'},\n                    {open: '\"', close: '\"'},\n                    {open: '\\'', close: '\\''},\n                ],\n                surroundingPairs: [\n                    {open: '{', close: '}'},\n                    {open: '[', close: ']'},\n                    {open: '(', close: ')'},\n                    {open: '\"', close: '\"'},\n                    {open: '\\'', close: '\\''},\n                ],\n            });\n\n            monaco.languages.setMonarchTokensProvider('yaml', {\n                tokenizer: {\n                    root: [\n                        [/^\\s*(---|\\.\\.\\.)\\s*$/, 'delimiter'],\n                        [/#.*$/, 'comment'],\n                        [/^\\s*-\\s+/, 'delimiter'],\n                        [/[A-Za-z0-9_-]+(?=\\s*:)/, 'key'],\n                        [/:/, 'delimiter'],\n                        [/[{}\\[\\](),]/, 'delimiter'],\n                        [/\\b(true|false|null|~)\\b/, 'keyword'],\n                        [/-?\\d+(\\.\\d+)?\\b/, 'number'],\n                        [/\"/, 'string', '@string_double'],\n                        [/'/, 'string', '@string_single'],\n                        [/[^#\\s{}\\[\\](),]+/, 'string'],\n                        [/\\s+/, ''],\n                    ],\n                    string_double: [\n                        [/[^\\\\\"]+/, 'string'],\n                        [/\\\\./, 'string.escape'],\n                        [/\"/, 'string', '@pop'],\n                    ],\n                    string_single: [\n                        [/[^']+/, 'string'],\n                        [/'/, 'string', '@pop'],\n                    ],\n                },\n            });\n        };\n\n        ensureYamlLanguage();\n\n        const prefersDark = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches;\n        monaco.editor.setTheme(prefersDark ? 'vs-dark' : 'vs');\n\n        const editor = monaco.editor.create(container, {\n            language: 'yaml',\n            minimap: {enabled: false},\n            automaticLayout: true,\n            tabSize: 2,\n            insertSpaces: true,\n            quickSuggestions: {other: true, comments: false, strings: true},\n            suggestOnTriggerCharacters: true,\n            wordBasedSuggestions: false,\n            suggest: {showWords: false},\n            scrollBeyondLastLine: false,\n        });\n\n        const stripInlineComment = (line) => {\n            let inSingle = false;\n            let inDouble = false;\n            for (let i = 0; i < line.length; i++) {\n                const ch = line[i];\n                if (ch === '\\'' && !inDouble) {\n                    inSingle = !inSingle;\n                    continue;\n                }\n                if (ch === '\"' && !inSingle) {\n                    inDouble = !inDouble;\n                    continue;\n                }\n                if (ch === '#' && !inSingle && !inDouble) {\n                    if (i === 0 || /\\s/.test(line[i - 1])) return line.slice(0, i);\n                }\n            }\n            return line;\n        };\n\n        const countIndent = (line) => {\n            let indent = 0;\n            for (let i = 0; i < line.length; i++) {\n                if (line[i] === ' ') {\n                    indent++;\n                } else if (line[i] === '\\t') {\n                    indent += 2;\n                } else {\n                    break;\n                }\n            }\n            return indent;\n        };\n\n        const parseListItem = (line) => {\n            const m = line.match(/^([ \\t]*)-/);\n            if (!m) return null;\n\n            const dashIndex = m[1].length;\n            if (dashIndex + 1 < line.length && !/\\s/.test(line[dashIndex + 1])) return null;\n\n            let afterDashIndex = dashIndex + 1;\n            while (afterDashIndex < line.length && /\\s/.test(line[afterDashIndex])) afterDashIndex++;\n\n            const spacesAfterDash = Math.max(1, afterDashIndex - (dashIndex + 1));\n\n            return {\n                indent: countIndent(m[1]),\n                dashIndex,\n                afterDashIndex,\n                rest: line.slice(afterDashIndex),\n                contentIndent: countIndent(m[1]) + 1 + spacesAfterDash,\n            };\n        };\n\n        const parseKey = (line) => {\n            const m = line.match(/^([ \\t]*)/);\n            const indentStr = m ? m[0] : '';\n            const indentIndex = indentStr.length;\n            const indent = countIndent(indentStr);\n\n            if (indentIndex >= line.length) return null;\n\n            const i = indentIndex;\n            let key = '';\n            let rawKey = '';\n            let isQuoted = false;\n            let keyStartIndex = i;\n            let keyEndIndex = i;\n            let colonIndex = -1;\n\n            const parseQuotedKey = (quoteChar) => {\n                isQuoted = true;\n                let j = i + 1;\n                if (quoteChar === '\"') {\n                    while (j < line.length) {\n                        if (line[j] === '\\\\') {\n                            j += 2;\n                            continue;\n                        }\n                        if (line[j] === '\"') break;\n                        j++;\n                    }\n                } else {\n                    while (j < line.length) {\n                        if (line[j] === '\\'') {\n                            if (line[j + 1] === '\\'') {\n                                j += 2;\n                                continue;\n                            }\n                            break;\n                        }\n                        j++;\n                    }\n                }\n\n                if (j >= line.length) return null;\n                rawKey = line.slice(i, j + 1);\n                if (quoteChar === '\"') {\n                    key = line.slice(i + 1, j).replace(/\\\\\"/g, '\"').replace(/\\\\\\\\/g, '\\\\');\n                } else {\n                    key = line.slice(i + 1, j).replace(/''/g, '\\'');\n                }\n                keyStartIndex = i;\n                keyEndIndex = j + 1;\n\n                let k = j + 1;\n                while (k < line.length && /\\s/.test(line[k])) k++;\n                if (k >= line.length || line[k] !== ':') return null;\n                colonIndex = k;\n                return colonIndex;\n            };\n\n            if (line[i] === '\"' || line[i] === '\\'') {\n                if (parseQuotedKey(line[i]) === null) return null;\n            } else {\n                let j = i;\n                while (j < line.length) {\n                    if (line[j] === ':') {\n                        if (j + 1 >= line.length || /\\s/.test(line[j + 1])) {\n                            colonIndex = j;\n                            break;\n                        }\n                    }\n                    j++;\n                }\n                if (colonIndex === -1) return null;\n                rawKey = line.slice(i, colonIndex).replace(/\\s+$/, '');\n                if (!rawKey) return null;\n                key = rawKey;\n                keyStartIndex = i;\n                keyEndIndex = i + rawKey.length;\n            }\n\n            const after = line.slice(colonIndex + 1);\n            const isContainer = after.trim() === '' || after.trim().startsWith('#');\n            const valueStartIndex = colonIndex + 1;\n\n            return {\n                indent,\n                key,\n                rawKey,\n                isQuoted,\n                isContainer,\n                after,\n                keyStartIndex,\n                keyEndIndex,\n                colonIndex,\n                valueStartIndex\n            };\n        };\n\n        const unique = (arr) => [...new Set(arr)];\n\n        const toYamlScalar = (v) => {\n            if (v === '') return '\\'\\'';\n            if (typeof v === 'string') return v;\n            if (typeof v === 'number') return String(v);\n            if (typeof v === 'boolean') return v ? 'true' : 'false';\n            return JSON.stringify(v);\n        };\n\n        const createSchemaTools = (schemaRoot) => {\n            const resolveRef = (schema, seen = new Set()) => {\n                if (!schema || typeof schema !== 'object') return schema;\n                if (typeof schema.$ref === 'string') {\n                    const ref = schema.$ref;\n                    if (ref.startsWith('#/definitions/')) {\n                        if (seen.has(ref)) return schema;\n                        seen.add(ref);\n                        const name = ref.slice('#/definitions/'.length);\n                        const def = schemaRoot.definitions && schemaRoot.definitions[name];\n                        if (!def) return schema;\n                        const resolved = resolveRef(def, seen);\n                        const rest = Object.assign({}, schema);\n                        delete rest.$ref;\n                        return Object.assign({}, resolved, rest);\n                    }\n                }\n                return schema;\n            };\n\n            const mergeProps = (schemas) => {\n                const props = {};\n                for (const s of schemas) {\n                    const schema = resolveRef(s);\n                    if (schema && schema.properties && typeof schema.properties === 'object') {\n                        Object.assign(props, schema.properties);\n                    }\n                }\n                return props;\n            };\n\n            const getObjectProperties = (schema) => {\n                schema = resolveRef(schema);\n                if (!schema) return {};\n                if (schema.properties && typeof schema.properties === 'object') return schema.properties;\n                if (Array.isArray(schema.anyOf)) return mergeProps(schema.anyOf);\n                return {};\n            };\n\n            const getPropertySchema = (schema, key) => {\n                schema = resolveRef(schema);\n                if (!schema) return null;\n\n                if (schema.properties && schema.properties[key]) return resolveRef(schema.properties[key]);\n\n                if (schema.additionalProperties && typeof schema.additionalProperties === 'object') {\n                    return resolveRef(schema.additionalProperties);\n                }\n\n                if (Array.isArray(schema.anyOf)) {\n                    for (const alt of schema.anyOf) {\n                        const res = getPropertySchema(alt, key);\n                        if (res) return res;\n                    }\n                }\n\n                return null;\n            };\n\n            const getValueSuggestions = (schema) => {\n                schema = resolveRef(schema);\n                if (!schema) return [];\n\n                const values = [];\n\n                const addFrom = (s) => {\n                    s = resolveRef(s);\n                    if (!s) return;\n                    if (Array.isArray(s.enum)) values.push(...s.enum);\n                    if ('const' in s) values.push(s.const);\n                    if (Array.isArray(s.examples)) values.push(...s.examples);\n                    if ('default' in s) values.push(s.default);\n                };\n\n                if (Array.isArray(schema.anyOf)) {\n                    for (const alt of schema.anyOf) addFrom(alt);\n                } else {\n                    addFrom(schema);\n                }\n\n                return unique(values);\n            };\n\n            const getSchemaTypes = (schema, seen = new Set()) => {\n                schema = resolveRef(schema);\n                if (!schema || typeof schema !== 'object') return new Set();\n\n                if (Array.isArray(schema.anyOf)) {\n                    const types = new Set();\n                    for (const alt of schema.anyOf) {\n                        for (const t of getSchemaTypes(alt, seen)) types.add(t);\n                    }\n                    return types;\n                }\n\n                if (Array.isArray(schema.oneOf)) {\n                    const types = new Set();\n                    for (const alt of schema.oneOf) {\n                        for (const t of getSchemaTypes(alt, seen)) types.add(t);\n                    }\n                    return types;\n                }\n\n                if (Array.isArray(schema.type)) return new Set(schema.type);\n                if (typeof schema.type === 'string') return new Set([schema.type]);\n                if (schema.properties || schema.additionalProperties) return new Set(['object']);\n                if (schema.items) return new Set(['array']);\n                return new Set();\n            };\n\n            const schemaAllowsType = (schema, actualType) => {\n                const types = getSchemaTypes(schema);\n                if (actualType === 'integer' && types.has('number')) return true;\n                return types.has(actualType);\n            };\n\n            const schemaTypesLabel = (schema) => {\n                const types = Array.from(getSchemaTypes(schema));\n                if (types.length === 0) return 'any';\n                return types.sort().join(' | ');\n            };\n\n            const getArrayItemSchema = (schema) => {\n                schema = resolveRef(schema);\n                if (!schema) return null;\n                if (schema.type === 'array' && schema.items) return resolveRef(schema.items);\n                if (Array.isArray(schema.anyOf)) {\n                    for (const alt of schema.anyOf) {\n                        const item = getArrayItemSchema(alt);\n                        if (item) return item;\n                    }\n                }\n                return null;\n            };\n\n            return {\n                schemaRoot,\n                resolveRef,\n                getObjectProperties,\n                getPropertySchema,\n                getValueSuggestions,\n                getSchemaTypes,\n                schemaAllowsType,\n                schemaTypesLabel,\n                getArrayItemSchema,\n            };\n        };\n\n        const isIntLike = (s) => /^[+-]?\\d+$/.test(s);\n        const isNumberLike = (s) => (\n            /^[+-]?(?:\\d*\\.\\d+|\\d+\\.\\d*)(?:[eE][+-]?\\d+)?$/.test(s) ||\n            /^[+-]?\\d+(?:[eE][+-]?\\d+)$/.test(s) ||\n            isIntLike(s)\n        );\n\n        const classifyYamlScalar = (raw) => {\n            const v = raw.trim();\n            if (!v) return {type: 'null'};\n            if (/^\\$\\{[^}{]+\\}$/.test(v)) return {type: 'dynamic'};\n            if (v.startsWith('[')) return {type: 'array'};\n            if (v.startsWith('{')) return {type: 'object'};\n            if (v.startsWith('\"') || v.startsWith('\\'')) return {type: 'string'};\n            if (v === 'true' || v === 'false') return {type: 'boolean'};\n            if (v === 'null' || v === '~') return {type: 'null'};\n            if (isIntLike(v)) return {type: 'integer'};\n            if (isNumberLike(v)) return {type: 'number'};\n            return {type: 'string'};\n        };\n\n        const parseYamlValue = (raw) => {\n            const trimmed = raw.trim();\n            if (!trimmed) return {ok: false};\n            if (/^\\$\\{[^}{]+\\}$/.test(trimmed)) return {ok: false, dynamic: true};\n            if (trimmed.startsWith('|') || trimmed.startsWith('>')) return {ok: false, block: true};\n            if (window.jsyaml && window.jsyaml.load) {\n                try {\n                    return {ok: true, value: window.jsyaml.load(trimmed)};\n                } catch (e) {\n                    // nothing\n                }\n            }\n            if (trimmed.startsWith('\"') && trimmed.endsWith('\"') && trimmed.length >= 2) {\n                const inner = trimmed.slice(1, -1);\n                return {ok: true, value: inner.replace(/\\\\\"/g, '\"').replace(/\\\\\\\\/g, '\\\\')};\n            }\n            if (trimmed.startsWith('\\'') && trimmed.endsWith('\\'') && trimmed.length >= 2) {\n                const inner = trimmed.slice(1, -1);\n                return {ok: true, value: inner.replace(/''/g, '\\'')};\n            }\n            if (trimmed === 'true' || trimmed === 'false') return {ok: true, value: trimmed === 'true'};\n            if (trimmed === 'null' || trimmed === '~') return {ok: true, value: null};\n            if (isIntLike(trimmed)) return {ok: true, value: parseInt(trimmed, 10)};\n            if (isNumberLike(trimmed)) return {ok: true, value: Number(trimmed)};\n            return {ok: true, value: trimmed};\n        };\n\n        const lintYamlModel = (model, schemaTools) => {\n            const markers = [];\n            const markedLines = new Set();\n            let blockScalarParentIndent = null;\n\n            const isBlockScalarHeader = (text) => {\n                const rawText = (text == null) ? '' : text;\n                const t = rawText.trimStart ? rawText.trimStart() : rawText.replace(/^\\s+/, '');\n                return t.startsWith('|') || t.startsWith('>');\n            };\n\n            const checkChildIndent = (ctx, childIndent, lineNumber) => {\n                if (!ctx) return;\n                if (ctx.childIndent == null) {\n                    ctx.childIndent = childIndent;\n                    return;\n                }\n                if (childIndent !== ctx.childIndent) {\n                    markLineError(lineNumber, `YAML: inconsistent indentation (expected ${ctx.childIndent} spaces)`, 1);\n                }\n            };\n\n            const markLineError = (lineNumber, message, startColumn = 1) => {\n                if (markedLines.has(`${lineNumber}:${message}`)) return;\n                markedLines.add(`${lineNumber}:${message}`);\n                const lineText = model.getLineContent(lineNumber);\n                markers.push({\n                    severity: monaco.MarkerSeverity.Error,\n                    message,\n                    startLineNumber: lineNumber,\n                    startColumn,\n                    endLineNumber: lineNumber,\n                    endColumn: Math.max(startColumn + 1, lineText.length + 1),\n                });\n            };\n\n            if (window.jsyaml && window.jsyaml.load) {\n                try {\n                    window.jsyaml.load(model.getValue());\n                } catch (e) {\n                    const mark = (e && e.mark) || {};\n                    const line = typeof mark.line === 'number' ? mark.line + 1 : 1;\n                    const column = typeof mark.column === 'number' ? mark.column + 1 : 1;\n                    const reason = e && e.reason;\n                    const messageText = e && e.message;\n                    markLineError(line, reason ? `YAML: ${reason}` : `YAML: ${messageText || 'Invalid YAML'}`, column);\n                }\n            }\n\n            const pushMarker = (m) => markers.push(m);\n\n            const getExpectedContainerType = (schema) => {\n                if (!schemaTools) return null;\n                const types = schemaTools.getSchemaTypes(schema);\n                const wantsObject = types.has('object');\n                const wantsArray = types.has('array');\n                if (wantsObject && !wantsArray) return 'object';\n                if (wantsArray && !wantsObject) return 'array';\n                return null;\n            };\n\n            const stack = [{\n                indent: -1,\n                schema: (schemaTools && schemaTools.schemaRoot) || null,\n                expected: 'object',\n                actual: null,\n                keys: new Map(),\n                childIndent: null,\n                origin: null,\n                reportedTypeMismatch: false,\n            }];\n\n            let hasTopLevelKey = false;\n            let hasTopLevelList = false;\n            const lineCount = model.getLineCount();\n            for (let lineNumber = 1; lineNumber <= lineCount; lineNumber++) {\n                let line = model.getLineContent(lineNumber);\n                if (!line.trim()) continue;\n                line = stripInlineComment(line).trimEnd();\n                if (!line.trim()) continue;\n                const kv = parseKey(line);\n                if (kv && kv.indent === 0) hasTopLevelKey = true;\n                const li = parseListItem(line);\n                if (li && li.indent === 0) hasTopLevelList = true;\n            }\n\n            const setActualType = (ctx, actual, fallbackLineNumber) => {\n                if (ctx.actual !== null) return;\n                ctx.actual = actual;\n                if (ctx.origin && ctx.expected && ctx.expected !== actual && !ctx.reportedTypeMismatch) {\n                    pushMarker({\n                        severity: monaco.MarkerSeverity.Error,\n                        message: `Type mismatch: expected ${ctx.expected}, got ${actual}`,\n                        startLineNumber: ctx.origin.lineNumber,\n                        startColumn: ctx.origin.startColumn,\n                        endLineNumber: ctx.origin.lineNumber,\n                        endColumn: ctx.origin.endColumn,\n                    });\n                    ctx.reportedTypeMismatch = true;\n                } else if (!ctx.origin && ctx.expected && ctx.expected !== actual && fallbackLineNumber) {\n                    pushMarker({\n                        severity: monaco.MarkerSeverity.Error,\n                        message: `Type mismatch: expected ${ctx.expected}, got ${actual}`,\n                        startLineNumber: fallbackLineNumber,\n                        startColumn: 1,\n                        endLineNumber: fallbackLineNumber,\n                        endColumn: 2,\n                    });\n                    ctx.reportedTypeMismatch = true;\n                }\n            };\n\n            const checkValueType = (schema, actualType, lineNumber, startColumn, endColumn, keyName) => {\n                if (!schemaTools || !schema) return;\n                if (actualType === 'dynamic') return;\n                if (schemaTools.schemaAllowsType(schema, actualType)) return;\n                pushMarker({\n                    severity: monaco.MarkerSeverity.Error,\n                    message: `Type mismatch for ${keyName ? `\"${keyName}\"` : 'value'}: expected ${schemaTools.schemaTypesLabel(schema)}, got ${actualType}`,\n                    startLineNumber: lineNumber,\n                    startColumn,\n                    endLineNumber: lineNumber,\n                    endColumn: Math.max(startColumn + 1, endColumn),\n                });\n            };\n\n            const valueEquals = (a, b) => {\n                if (a === b) return true;\n                if (typeof a !== typeof b) return false;\n                if (a && b && typeof a === 'object') {\n                    const aIsArray = Array.isArray(a);\n                    const bIsArray = Array.isArray(b);\n                    if (aIsArray !== bIsArray) return false;\n                    if (aIsArray) {\n                        if (a.length !== b.length) return false;\n                        for (let i = 0; i < a.length; i++) {\n                            if (!valueEquals(a[i], b[i])) return false;\n                        }\n                        return true;\n                    }\n                    const aKeys = Object.keys(a);\n                    const bKeys = Object.keys(b);\n                    if (aKeys.length !== bKeys.length) return false;\n                    for (const key of aKeys) {\n                        if (!Object.prototype.hasOwnProperty.call(b, key)) return false;\n                        if (!valueEquals(a[key], b[key])) return false;\n                    }\n                    return true;\n                }\n                return false;\n            };\n\n            const schemaAllowsTypeLoose = (schema, actualType) => {\n                if (!schemaTools || !schema) return true;\n                const types = schemaTools.getSchemaTypes(schema);\n                if (types.size === 0) return true;\n                if (actualType === 'integer' && types.has('number')) return true;\n                return types.has(actualType);\n            };\n\n            const collectConstraintSchemas = (schema, actualType) => {\n                if (!schemaTools || !schema) return [];\n                schema = schemaTools.resolveRef(schema);\n                if (!schema) return [];\n                if (Array.isArray(schema.anyOf)) {\n                    const res = [];\n                    for (const alt of schema.anyOf) res.push(...collectConstraintSchemas(alt, actualType));\n                    return res;\n                }\n                if (Array.isArray(schema.oneOf)) {\n                    const res = [];\n                    for (const alt of schema.oneOf) res.push(...collectConstraintSchemas(alt, actualType));\n                    return res;\n                }\n                if (!schemaAllowsTypeLoose(schema, actualType)) return [];\n                return [schema];\n            };\n\n            const getSchemaEnumValues = (schema) => {\n                const values = [];\n                if (Array.isArray(schema.enum)) values.push(...schema.enum);\n                if (Object.prototype.hasOwnProperty.call(schema, 'const')) values.push(schema.const);\n                return values;\n            };\n\n            const checkValueConstraints = (schema, actualType, rawValue, lineNumber, startColumn, endColumn, keyName) => {\n                if (!schemaTools || !schema) return;\n                if (actualType === 'dynamic') return;\n                const parsed = parseYamlValue(rawValue);\n                if (!parsed.ok) return;\n\n                const candidates = collectConstraintSchemas(schema, actualType);\n                if (candidates.length === 0) return;\n\n                const hasConstraints = candidates.some((s) => (\n                    (Array.isArray(s.enum) || Object.prototype.hasOwnProperty.call(s, 'const')) ||\n                    (actualType === 'string' && typeof s.pattern === 'string')\n                ));\n                if (!hasConstraints) return;\n\n                const hasUnconstrained = candidates.some((s) => (\n                    !(Array.isArray(s.enum) || Object.prototype.hasOwnProperty.call(s, 'const')) &&\n                    !(actualType === 'string' && typeof s.pattern === 'string')\n                ));\n                if (hasUnconstrained) return;\n\n                const value = parsed.value;\n                const matchesAny = candidates.some((s) => {\n                    const enums = getSchemaEnumValues(s);\n                    if (enums.length > 0 && !enums.some((v) => valueEquals(v, value))) return false;\n                    if (actualType === 'string' && typeof s.pattern === 'string') {\n                        try {\n                            const re = new RegExp(s.pattern);\n                            if (!re.test(String(value))) return false;\n                        } catch (e) {\n                            return true;\n                        }\n                    }\n                    return true;\n                });\n                if (matchesAny) return;\n\n                const enumValues = [];\n                const patterns = [];\n                for (const s of candidates) {\n                    enumValues.push(...getSchemaEnumValues(s));\n                    if (actualType === 'string' && typeof s.pattern === 'string') patterns.push(s.pattern);\n                }\n                const enumLabel = unique(enumValues).map((v) => toYamlScalar(v)).join(', ');\n                const patternLabel = unique(patterns).join(' | ');\n\n                let message;\n                const label = keyName ? `\"${keyName}\"` : 'value';\n                if (enumValues.length && patterns.length) {\n                    message = `Value for ${label} must be one of: ${enumLabel}; or match pattern: ${patternLabel}`;\n                } else if (enumValues.length) {\n                    message = `Value for ${label} must be one of: ${enumLabel}`;\n                } else if (patterns.length) {\n                    message = `Value for ${label} must match pattern: ${patternLabel}`;\n                } else {\n                    return;\n                }\n\n                pushMarker({\n                    severity: monaco.MarkerSeverity.Error,\n                    message,\n                    startLineNumber: lineNumber,\n                    startColumn,\n                    endLineNumber: lineNumber,\n                    endColumn: Math.max(startColumn + 1, endColumn),\n                });\n            };\n\n            for (let lineNumber = 1; lineNumber <= lineCount; lineNumber++) {\n                let line = model.getLineContent(lineNumber);\n                if (!line.trim()) continue;\n\n                line = stripInlineComment(line).trimEnd();\n                if (!line.trim()) continue;\n\n                const indent = countIndent(line);\n                if (blockScalarParentIndent !== null) {\n                    if (indent <= blockScalarParentIndent) {\n                        blockScalarParentIndent = null;\n                    } else {\n                        continue; // treat as block scalar content\n                    }\n                }\n\n                if (indent === 0 && (hasTopLevelKey || hasTopLevelList)) {\n                    const trimmed = line.trim();\n                    if (trimmed !== '---' && trimmed !== '...' && !trimmed.startsWith('#')) {\n                        const listItem0 = parseListItem(line);\n                        const kv0 = parseKey(line);\n                        const flow0 = trimmed.startsWith('{') || trimmed.startsWith('[');\n                        if (!flow0 && !listItem0 && !kv0) {\n                            markLineError(lineNumber, 'YAML: unexpected content at document root');\n                            continue;\n                        }\n                    }\n                }\n\n                const listItem = parseListItem(line);\n                if (listItem) {\n                    if (listItem.indent > 0 && (hasTopLevelKey || hasTopLevelList) && stack.length === 1) {\n                        markLineError(lineNumber, 'YAML: unexpected indentation at document root', 1);\n                    }\n                    while (stack.length > 1 && listItem.indent <= stack[stack.length - 1].indent) stack.pop();\n\n                    const parent = stack[stack.length - 1];\n                    checkChildIndent(parent, listItem.indent, lineNumber);\n                    setActualType(parent, 'array', lineNumber);\n\n                    const itemSchema = schemaTools ? schemaTools.getArrayItemSchema(parent.schema) : null;\n                    const itemExpected = getExpectedContainerType(itemSchema);\n\n                    const itemCtx = {\n                        indent: listItem.indent,\n                        schema: itemSchema,\n                        expected: itemExpected,\n                        actual: null,\n                        keys: new Map(),\n                        childIndent: null,\n                        origin: {lineNumber, startColumn: listItem.dashIndex + 1, endColumn: listItem.dashIndex + 2},\n                        reportedTypeMismatch: false,\n                    };\n                    stack.push(itemCtx);\n\n                    if (!listItem.rest) continue;\n                    if (isBlockScalarHeader(listItem.rest)) {\n                        blockScalarParentIndent = listItem.indent;\n                        checkValueType(itemSchema, 'string', lineNumber, listItem.afterDashIndex + 1, line.length + 1, null);\n                        continue;\n                    }\n\n                    const inline = parseKey(' '.repeat(listItem.afterDashIndex) + listItem.rest);\n                    if (inline) {\n                        // handle inline mapping in the same line: \"- key: value\"\n                        const kv = inline;\n                        while (stack.length > 1 && kv.indent <= stack[stack.length - 1].indent) stack.pop();\n                        const ctx = stack[stack.length - 1];\n                        checkChildIndent(ctx, kv.indent, lineNumber);\n                        setActualType(ctx, 'object', lineNumber);\n\n                        const prev = ctx.keys.get(kv.key);\n                        if (prev) {\n                            pushMarker({\n                                severity: monaco.MarkerSeverity.Warning,\n                                message: `Duplicate key \"${kv.key}\" (previous at line ${prev.lineNumber})`,\n                                startLineNumber: lineNumber,\n                                startColumn: kv.keyStartIndex + 1,\n                                endLineNumber: lineNumber,\n                                endColumn: kv.keyEndIndex + 1,\n                            });\n                        } else {\n                            ctx.keys.set(kv.key, {lineNumber});\n                        }\n\n                        if (!kv.isQuoted && isNumberLike(kv.rawKey.trim())) {\n                            pushMarker({\n                                severity: monaco.MarkerSeverity.Error,\n                                message: `Map keys must be strings. Quote numeric key as '${kv.rawKey}':`,\n                                startLineNumber: lineNumber,\n                                startColumn: kv.keyStartIndex + 1,\n                                endLineNumber: lineNumber,\n                                endColumn: kv.keyEndIndex + 1,\n                            });\n                        }\n\n                        const propSchema = schemaTools ? schemaTools.getPropertySchema(ctx.schema, kv.key) : null;\n                        if (isBlockScalarHeader(kv.after)) {\n                            blockScalarParentIndent = kv.indent;\n                        }\n                        if (kv.isContainer) {\n                            stack.push({\n                                indent: kv.indent,\n                                schema: propSchema,\n                                expected: getExpectedContainerType(propSchema),\n                                actual: null,\n                                keys: new Map(),\n                                childIndent: null,\n                                origin: {lineNumber, startColumn: kv.keyStartIndex + 1, endColumn: kv.keyEndIndex + 1},\n                                reportedTypeMismatch: false,\n                            });\n                        } else if (propSchema) {\n                            const valueText = kv.after.trim();\n                            const actual = classifyYamlScalar(valueText).type;\n                            const valueStartColumn = kv.valueStartIndex + 1 + (kv.after.length - kv.after.trimStart().length);\n                            checkValueType(propSchema, actual, lineNumber, valueStartColumn, line.length + 1, kv.key);\n                            checkValueConstraints(propSchema, actual, valueText, lineNumber, valueStartColumn, line.length + 1, kv.key);\n                        }\n                        continue;\n                    }\n\n                    const scalar = classifyYamlScalar(listItem.rest).type;\n                    checkValueType(itemSchema, scalar, lineNumber, listItem.afterDashIndex + 1, line.length + 1, null);\n                    checkValueConstraints(itemSchema, scalar, listItem.rest, lineNumber, listItem.afterDashIndex + 1, line.length + 1, null);\n                    continue;\n                }\n\n                const kv = parseKey(line);\n                if (!kv) {\n                    markLineError(lineNumber, 'YAML: expected a map key (key:) or list item (-)', indent + 1);\n                    continue;\n                }\n                if (kv.indent > 0 && (hasTopLevelKey || hasTopLevelList) && stack.length === 1) {\n                    markLineError(lineNumber, 'YAML: unexpected indentation at document root', 1);\n                }\n\n                while (stack.length > 1 && kv.indent <= stack[stack.length - 1].indent) stack.pop();\n                const ctx = stack[stack.length - 1];\n                checkChildIndent(ctx, kv.indent, lineNumber);\n                setActualType(ctx, 'object', lineNumber);\n\n                const prev = ctx.keys.get(kv.key);\n                if (prev) {\n                    pushMarker({\n                        severity: monaco.MarkerSeverity.Warning,\n                        message: `Duplicate key \"${kv.key}\" (previous at line ${prev.lineNumber})`,\n                        startLineNumber: lineNumber,\n                        startColumn: kv.keyStartIndex + 1,\n                        endLineNumber: lineNumber,\n                        endColumn: kv.keyEndIndex + 1,\n                    });\n                } else {\n                    ctx.keys.set(kv.key, {lineNumber});\n                }\n\n                if (!kv.isQuoted && isNumberLike(kv.rawKey.trim())) {\n                    pushMarker({\n                        severity: monaco.MarkerSeverity.Error,\n                        message: `Map keys must be strings. Quote numeric key as '${kv.rawKey}':`,\n                        startLineNumber: lineNumber,\n                        startColumn: kv.keyStartIndex + 1,\n                        endLineNumber: lineNumber,\n                        endColumn: kv.keyEndIndex + 1,\n                    });\n                }\n\n                const propSchema = schemaTools ? schemaTools.getPropertySchema(ctx.schema, kv.key) : null;\n                if (kv.isContainer) {\n                    stack.push({\n                        indent: kv.indent,\n                        schema: propSchema,\n                        expected: getExpectedContainerType(propSchema),\n                        actual: null,\n                        keys: new Map(),\n                        childIndent: null,\n                        origin: {lineNumber, startColumn: kv.keyStartIndex + 1, endColumn: kv.keyEndIndex + 1},\n                        reportedTypeMismatch: false,\n                    });\n                    continue;\n                }\n\n                if (isBlockScalarHeader(kv.after)) {\n                    blockScalarParentIndent = kv.indent;\n                }\n\n                if (!propSchema) continue;\n\n                const actual = classifyYamlScalar(kv.after).type;\n                const valueStartColumn = kv.valueStartIndex + 1 + (kv.after.length - kv.after.trimStart().length);\n                checkValueType(propSchema, actual, lineNumber, valueStartColumn, line.length + 1, kv.key);\n                checkValueConstraints(propSchema, actual, kv.after, lineNumber, valueStartColumn, line.length + 1, kv.key);\n            }\n\n            return markers;\n        };\n\n        let schemaTools = null;\n        let completionProvider = null;\n        let hoverProvider = null;\n\n        const scheduleLint = (() => {\n            let handle = null;\n            return () => {\n                if (handle) clearTimeout(handle);\n                handle = setTimeout(() => {\n                    const model = editor.getModel();\n                    if (!model) return;\n                    monaco.editor.setModelMarkers(model, 'yaml-lint', lintYamlModel(model, schemaTools));\n                }, 250);\n            };\n        })();\n\n        editor.onDidChangeModelContent(() => scheduleLint());\n\n        const setupYamlHints = (schemaRoot) => {\n            schemaTools = createSchemaTools(schemaRoot);\n            scheduleLint();\n\n            const {\n                resolveRef,\n                getObjectProperties,\n                getPropertySchema,\n                getValueSuggestions,\n            } = schemaTools;\n\n            const buildContextStack = (model, upToLineNumber) => {\n                const stack = [{indent: -1, schema: schemaRoot}];\n\n                for (let lineNumber = 1; lineNumber <= upToLineNumber; lineNumber++) {\n                    let line = model.getLineContent(lineNumber);\n                    if (!line.trim()) continue;\n\n                    line = stripInlineComment(line).trimEnd();\n                    if (!line.trim()) continue;\n\n                    const listItem = parseListItem(line);\n                    if (listItem) {\n                        while (stack.length > 1 && listItem.indent <= stack[stack.length - 1].indent) stack.pop();\n                        const parent = resolveRef(stack[stack.length - 1].schema);\n                        if (parent && parent.type === 'array' && parent.items) {\n                            stack.push({indent: listItem.indent, schema: resolveRef(parent.items)});\n                        } else {\n                            stack.push({indent: listItem.indent, schema: null});\n                        }\n\n                        const inline = listItem.rest ? parseKey(' '.repeat(listItem.contentIndent) + listItem.rest) : null;\n                        if (inline && inline.isContainer) {\n                            while (stack.length > 1 && inline.indent <= stack[stack.length - 1].indent) stack.pop();\n                            const ctx = resolveRef(stack[stack.length - 1].schema);\n                            const next = ctx ? getPropertySchema(ctx, inline.key) : null;\n                            stack.push({indent: inline.indent, schema: next});\n                        }\n                        continue;\n                    }\n\n                    const kv = parseKey(line);\n                    if (!kv) continue;\n                    while (stack.length > 1 && kv.indent <= stack[stack.length - 1].indent) stack.pop();\n                    if (!kv.isContainer) continue;\n\n                    const ctx = resolveRef(stack[stack.length - 1].schema);\n                    const next = ctx ? getPropertySchema(ctx, kv.key) : null;\n                    stack.push({indent: kv.indent, schema: next});\n                }\n\n                return stack;\n            };\n\n            if (completionProvider) completionProvider.dispose();\n            completionProvider = monaco.languages.registerCompletionItemProvider('yaml', {\n                triggerCharacters: [':', ' '],\n                provideCompletionItems: (model, position) => {\n                    const line = model.getLineContent(position.lineNumber);\n                    const lineNoComment = stripInlineComment(line);\n                    const lineNoCommentTrimmedEnd = lineNoComment.trimEnd();\n                    const listItem = parseListItem(lineNoCommentTrimmedEnd);\n\n                    const wordUntil = model.getWordUntilPosition(position);\n                    const range = new monaco.Range(position.lineNumber, wordUntil.startColumn, position.lineNumber, wordUntil.endColumn);\n\n                    const cursorIndex = position.column - 1;\n                    let contentStartIndex = 0;\n                    if (listItem) {\n                        contentStartIndex = listItem.afterDashIndex;\n                    } else {\n                        contentStartIndex = countIndent(lineNoComment);\n                    }\n\n                    if (cursorIndex < contentStartIndex) return {suggestions: []};\n\n                    const text = lineNoCommentTrimmedEnd.slice(contentStartIndex);\n                    const cursorInText = cursorIndex - contentStartIndex;\n                    const colonIndex = text.indexOf(':');\n                    const isValueContext = colonIndex >= 0 && cursorInText > colonIndex;\n\n                    const stack = buildContextStack(model, position.lineNumber - 1);\n\n                    const effectiveIndent = listItem ? listItem.contentIndent : countIndent(lineNoComment);\n                    while (stack.length > 1 && effectiveIndent <= stack[stack.length - 1].indent) stack.pop();\n\n                    let contextSchema = resolveRef(stack[stack.length - 1].schema);\n\n                    if (listItem && cursorIndex >= listItem.afterDashIndex && contextSchema && contextSchema.type === 'array') {\n                        contextSchema = resolveRef(contextSchema.items);\n                    }\n\n                    if (!contextSchema) return {suggestions: []};\n\n                    // Scalar array item (e.g. \"- tcp4\") - suggest values (enum/examples/default)\n                    if (listItem && colonIndex === -1 && !isValueContext) {\n                        const props = getObjectProperties(contextSchema);\n                        if (!props || Object.keys(props).length === 0) {\n                            const values = getValueSuggestions(contextSchema);\n                            const suggestions = values.map((v) => ({\n                                label: toYamlScalar(v),\n                                kind: monaco.languages.CompletionItemKind.Value,\n                                insertText: toYamlScalar(v),\n                                range,\n                            }));\n                            return {suggestions};\n                        }\n                    }\n\n                    if (!isValueContext) {\n                        const props = getObjectProperties(contextSchema);\n                        const suggestions = Object.keys(props).map((key) => {\n                            const s = resolveRef(props[key]);\n                            const wantsArray = s && s.type === 'array';\n                            const wantsBlock = s && (s.type === 'object' || wantsArray || s.properties);\n                            const indent = listItem ? ' '.repeat(listItem.contentIndent) : ' '.repeat(countIndent(lineNoComment));\n                            const innerIndent = indent + '  ';\n\n                            const insertText = wantsArray ? `${key}:\\n${indent}` : (wantsBlock ? `${key}:\\n${innerIndent}` : `${key}: `);\n                            const hasValueSuggestions = !wantsBlock && getValueSuggestions(s).length > 0;\n\n                            return {\n                                label: key,\n                                kind: monaco.languages.CompletionItemKind.Property,\n                                insertText,\n                                insertTextRules: monaco.languages.CompletionItemInsertTextRule.InsertAsSnippet,\n                                command: (wantsBlock || hasValueSuggestions) ? {id: 'editor.action.triggerSuggest'} : undefined,\n                                documentation: s && s.description,\n                                range,\n                            };\n                        });\n\n                        return {suggestions};\n                    }\n\n                    const keyName = text.slice(0, colonIndex).trim();\n                    const keySchema = getPropertySchema(contextSchema, keyName);\n                    if (!keySchema) return {suggestions: []};\n\n                    const values = getValueSuggestions(keySchema);\n                    const suggestions = values.map((v) => ({\n                        label: toYamlScalar(v),\n                        kind: monaco.languages.CompletionItemKind.Value,\n                        insertText: toYamlScalar(v),\n                        range,\n                    }));\n\n                    return {suggestions};\n                }\n            });\n\n            if (hoverProvider) hoverProvider.dispose();\n            hoverProvider = monaco.languages.registerHoverProvider('yaml', {\n                provideHover: (model, position) => {\n                    if (!schemaTools) return null;\n\n                    const line = model.getLineContent(position.lineNumber);\n                    const lineNoComment = stripInlineComment(line).trimEnd();\n                    const listItem = parseListItem(lineNoComment);\n\n                    const cursorIndex = position.column - 1;\n                    if (listItem && cursorIndex < listItem.afterDashIndex) return null;\n\n                    let kv;\n                    let effectiveIndent;\n                    let keyStartIndex;\n                    let keyEndIndex;\n\n                    if (listItem) {\n                        if (!listItem.rest) return null;\n                        const synthetic = ' '.repeat(listItem.contentIndent) + listItem.rest;\n                        kv = parseKey(synthetic);\n                        if (!kv) return null;\n                        effectiveIndent = listItem.contentIndent;\n                        keyStartIndex = listItem.afterDashIndex + (kv.keyStartIndex - listItem.contentIndent);\n                        keyEndIndex = listItem.afterDashIndex + (kv.keyEndIndex - listItem.contentIndent);\n                    } else {\n                        kv = parseKey(lineNoComment);\n                        if (!kv) return null;\n                        effectiveIndent = countIndent(lineNoComment);\n                        keyStartIndex = kv.keyStartIndex;\n                        keyEndIndex = kv.keyEndIndex;\n                    }\n\n                    if (cursorIndex < keyStartIndex || cursorIndex >= keyEndIndex) return null;\n\n                    const stack = buildContextStack(model, position.lineNumber - 1);\n                    while (stack.length > 1 && effectiveIndent <= stack[stack.length - 1].indent) stack.pop();\n\n                    let contextSchema = resolveRef(stack[stack.length - 1].schema);\n                    if (listItem && cursorIndex >= listItem.afterDashIndex && contextSchema && contextSchema.type === 'array') {\n                        contextSchema = resolveRef(contextSchema.items);\n                    }\n\n                    if (!contextSchema) return null;\n                    const propSchema = getPropertySchema(contextSchema, kv.key);\n                    if (!propSchema) return null;\n\n                    const resolved = resolveRef(propSchema);\n                    const description = resolved && resolved.description;\n                    if (!description) return null;\n\n                    return {\n                        range: new monaco.Range(\n                            position.lineNumber,\n                            keyStartIndex + 1,\n                            position.lineNumber,\n                            keyEndIndex + 1\n                        ),\n                        contents: [{value: description}],\n                    };\n                }\n            });\n        };\n\n        const layout = () => {\n            const top = container.getBoundingClientRect().top;\n            container.style.height = `${Math.max(200, window.innerHeight - top)}px`;\n            editor.layout();\n        };\n        window.addEventListener('resize', layout);\n        layout();\n\n        let dump;\n\n        document.getElementById('save').addEventListener('click', async () => {\n            let r = await fetch('api/config', {cache: 'no-cache'});\n            if (r.ok && dump !== await r.text()) {\n                alert('Config was changed from another place. Refresh the page and make changes again');\n                return;\n            }\n\n            r = await fetch('api/config', {method: 'POST', body: editor.getValue()});\n            if (r.ok) {\n                alert('OK');\n                dump = editor.getValue();\n                await fetch('api/restart', {method: 'POST'});\n            } else {\n                alert(await r.text());\n            }\n        });\n\n        document.getElementById('suggest').addEventListener('click', () => {\n            editor.trigger('source', 'editor.action.triggerSuggest', {});\n        });\n\n        (async () => {\n            try {\n                const r = await fetch('schema.json', {cache: 'no-cache'});\n                if (r.ok) setupYamlHints(await r.json());\n            } catch (e) {\n                // ignore schema load errors\n            }\n\n            const r = await fetch('api/config', {cache: 'no-cache'});\n            if (r.status === 410) {\n                alert('Config file is not set');\n            } else if (r.status === 404) {\n                editor.setValue(''); // config file not exist\n            } else if (r.ok) {\n                dump = await r.text();\n                editor.setValue(dump);\n            } else {\n                alert(`Unknown error: ${r.statusText} (${r.status})`);\n            }\n        })();\n    });\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "www/hls.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n<head>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>hls - go2rtc</title>\n    <style>\n        body {\n            background-color: black;\n            margin: 0;\n            padding: 0;\n        }\n\n        html, body, video {\n            height: 100%;\n            width: 100%;\n        }\n    </style>\n</head>\n<body>\n<script src=\"https://cdn.jsdelivr.net/npm/hls.js@1\"></script>\n<video id=\"video\" autoplay controls playsinline muted></video>\n<script>\n    // http://192.168.1.123:1984/hls.html?src=demo&mp4\n    const url = new URL('api/stream.m3u8' + location.search, location.href);\n\n    const video = document.getElementById('video');\n    /* global Hls */\n    if (Hls.isSupported()) {\n        const hls = new Hls();\n        hls.loadSource(url.toString());\n        hls.attachMedia(video);\n    } else if (video.canPlayType('application/vnd.apple.mpegurl')) {\n        video.src = url.toString();\n    }\n</script>\n</body>\n</html>"
  },
  {
    "path": "www/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>go2rtc</title>\n    <style>\n        .controls {\n            display: flex;\n            flex-wrap: wrap;\n            gap: 10px;\n            align-items: center;\n        }\n\n        .info {\n            color: #888;\n        }\n    </style>\n</head>\n<body>\n\n<script src=\"main.js\"></script>\n\n<main>\n    <div class=\"controls\">\n        <button>stream</button>\n        modes\n        <label><input type=\"checkbox\" name=\"webrtc\" checked>webrtc</label>\n        <label><input type=\"checkbox\" name=\"mse\" checked>mse</label>\n        <label><input type=\"checkbox\" name=\"hls\" checked>hls</label>\n        <label><input type=\"checkbox\" name=\"mjpeg\" checked>mjpeg</label>\n    </div>\n    <table>\n        <thead>\n        <tr>\n            <th><label><input id=\"selectall\" type=\"checkbox\">name</label></th>\n            <th>online</th>\n            <th>commands</th>\n        </tr>\n        </thead>\n        <tbody id=\"streams\">\n        </tbody>\n    </table>\n    <div class=\"info\"></div>\n</main>\n\n<script>\n    const templates = [\n        '<a href=\"stream.html?src={name}\">stream</a>',\n        '<a href=\"links.html?src={name}\">links</a>',\n        '<a href=\"#\" data-name=\"{name}\">delete</a>',\n    ];\n\n    document.querySelector('.controls > button')\n        .addEventListener('click', () => {\n            const url = new URL('stream.html', location.href);\n\n            const streams = document.querySelectorAll('#streams input');\n            streams.forEach(i => {\n                if (i.checked) url.searchParams.append('src', i.name);\n            });\n\n            if (!url.searchParams.has('src')) return;\n\n            let mode = document.querySelectorAll('.controls input');\n            mode = Array.from(mode).filter(i => i.checked).map(i => i.name).join(',');\n\n            window.location.href = `${url}&mode=${mode}`;\n        });\n\n    const tbody = document.getElementById('streams');\n    tbody.addEventListener('click', async ev => {\n        if (ev.target.innerText !== 'delete') return;\n\n        ev.preventDefault();\n\n        const src = decodeURIComponent(ev.target.dataset.name);\n\n        const message = `Please type the name of the stream \"${src}\" to confirm its deletion from the configuration. This action is irreversible.`;\n        if (prompt(message) !== src) {\n            alert('Stream name does not match. Deletion cancelled.');\n            return;\n        }\n\n        const url = new URL('api/streams', location.href);\n        url.searchParams.set('src', src);\n\n        try {\n            await fetch(url, {method: 'DELETE'});\n            reload();\n        } catch (error) {\n            console.error('Failed to delete the stream:', error);\n        }\n    });\n\n    document.getElementById('selectall').addEventListener('change', ev => {\n        document.querySelectorAll('#streams input').forEach(el => {\n            el.checked = ev.target.checked;\n        });\n    });\n\n    function reload() {\n        const url = new URL('api/streams', location.href);\n        const checkboxStates = {};\n        tbody.querySelectorAll('input[type=\"checkbox\"][name]').forEach(checkbox => {\n            checkboxStates[checkbox.name] = checkbox.checked;\n        });\n        fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => {\n            const existingIds = Array.from(tbody.querySelectorAll('tr')).map(tr => tr.dataset['id']);\n            const fetchedIds = [];\n\n            for (const [key, value] of Object.entries(data)) {\n                const name = key.replace(/[<\">]/g, ''); // sanitize\n                fetchedIds.push(name);\n\n                let tr = tbody.querySelector(`tr[data-id=\"${name}\"]`);\n                const online = value && value.consumers ? value.consumers.length : 0;\n                const src = encodeURIComponent(name);\n                const links = templates.map(link => link.replace('{name}', src)).join(' ');\n\n                if (!tr) {\n                    tr = document.createElement('tr');\n                    tr.dataset['id'] = name;\n                    tbody.appendChild(tr);\n                }\n\n                const isChecked = checkboxStates[name] ? 'checked' : '';\n                tr.innerHTML =\n                    `<td><label><input type=\"checkbox\" name=\"${name}\" ${isChecked}>${name}</label></td>` +\n                    `<td><a href=\"api/streams?src=${src}\">${online} / info</a> / <a href=\"api/streams?src=${src}&video=all&audio=all&microphone\">probe</a> / <a href=\"net.html?src=${src}\">net</a></td>` +\n                    `<td>${links}</td>`;\n            }\n\n            // Remove old rows\n            existingIds.forEach(id => {\n                if (!fetchedIds.includes(id)) {\n                    const trToRemove = tbody.querySelector(`tr[data-id=\"${id}\"]`);\n                    tbody.removeChild(trToRemove);\n                }\n            });\n        });\n    }\n\n    // Auto-reload\n    setInterval(reload, 1000);\n\n    const url = new URL('api', location.href);\n    fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => {\n        const info = document.querySelector('.info');\n        info.innerText = `version: ${data.version} / config: ${data.config_path}`;\n    });\n\n    reload();\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "www/links.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>links - go2rtc</title>\n    <style>\n        div > li {\n            list-style-type: none;\n            padding-left: 10px;\n            position: relative;\n        }\n\n        div > li:before {\n            content: \"-\";\n            position: absolute;\n            left: 0;\n        }\n    </style>\n</head>\n<body>\n\n<script src=\"main.js\"></script>\n\n<main>\n    <div id=\"links\"></div>\n    <script>\n        const src = new URLSearchParams(location.search).get('src').replace(/[<\">]/g, ''); // sanitize\n\n        const links = document.getElementById('links');\n\n        links.innerHTML = `\n        <h2>Any codec in source</h2>\n        <li><a href=\"stream.html?src=${src}\">stream.html</a> with auto-select mode / browsers: all / codecs: H264, H265*, MJPEG, JPEG, AAC, PCMU, PCMA, OPUS</li>\n        <li><a href=\"api/streams?src=${src}\">info.json</a> page with active connections</li>\n    `;\n\n        const url = new URL('api', location.href);\n        fetch(url, {cache: 'no-cache'}).then(r => r.json()).then(data => {\n            let rtsp = location.host + ':8554';\n            try {\n                const host = data.host.match(/^[^:]+/)[0];\n                const port = data.rtsp.listen.match(/[0-9]+$/)[0];\n                rtsp = `${host}:${port}`;\n            } catch (e) {\n            }\n\n            links.innerHTML += `\n            <li><a href=\"rtsp://${rtsp}/${src}\">rtsp</a> with only one video and one audio / codecs: any</li>\n            <li><a href=\"rtsp://${rtsp}/${src}?mp4\">rtsp</a> for MP4 recording (Hass or Frigate) / codecs: H264, H265, AAC</li>\n            <li><a href=\"rtsp://${rtsp}/${src}?video=all&audio=all\">rtsp</a> with all tracks / codecs: any</li>\n\n            <pre>ffplay -fflags nobuffer -flags low_delay -rtsp_transport tcp \"rtsp://${rtsp}/${src}\"</pre>\n\n            <h2>H264/H265 source</h2>\n            <li><a href=\"stream.html?src=${src}&mode=webrtc\">stream.html</a> WebRTC stream / browsers: all / codecs: H264, PCMU, PCMA, OPUS / +H265 in Safari</li>\n            <li><a href=\"stream.html?src=${src}&mode=mse\">stream.html</a> MSE stream / browsers: Chrome, Firefox, Safari Mac/iPad / codecs: H264, H265*, AAC, PCMA*, PCMU*, PCM* / +OPUS in Chrome and Firefox</li>\n            <li><a href=\"api/stream.mp4?src=${src}\">stream.mp4</a> legacy MP4 stream with AAC audio / browsers: Chrome, Firefox / codecs: H264, H265*, AAC</li>\n            <li><a href=\"api/stream.mp4?src=${src}&mp4=flac\">stream.mp4</a> modern MP4 stream with common audio / browsers: Chrome, Firefox / codecs: H264, H265*, AAC, FLAC (PCMA, PCMU, PCM)</li>\n            <li><a href=\"api/stream.mp4?src=${src}&mp4=all\">stream.mp4</a> MP4 stream with any audio / browsers: Chrome / codecs: H264, H265*, AAC, OPUS, MP3, FLAC (PCMA, PCMU, PCM)</li>\n            <li><a href=\"api/frame.mp4?src=${src}\">frame.mp4</a> snapshot in MP4-format / browsers: all / codecs: H264, H265*</li>\n            <li><a href=\"api/stream.m3u8?src=${src}\">stream.m3u8</a> legacy HLS/TS / browsers: Safari all, Chrome Android / codecs: H264</li>\n            <li><a href=\"api/stream.m3u8?src=${src}&mp4\">stream.m3u8</a> legacy HLS/fMP4 / browsers: Safari all, Chrome Android / codecs: H264, H265*, AAC</li>\n            <li><a href=\"api/stream.m3u8?src=${src}&mp4=flac\">stream.m3u8</a> modern HLS/fMP4 / browsers: Safari all, Chrome Android / codecs: H264, H265*, AAC, FLAC (PCMA, PCMU, PCM)</li>\n\n            <h2>MJPEG source</h2>\n            <li><a href=\"stream.html?src=${src}&mode=mjpeg\">stream.html</a> with MJPEG mode / browsers: all / codecs: MJPEG, JPEG</li>\n            <li><a href=\"api/stream.mjpeg?src=${src}\">stream.mjpeg</a> MJPEG stream / browsers: all / codecs: MJPEG, JPEG</li>\n            <li><a href=\"api/frame.jpeg?src=${src}\">frame.jpeg</a> snapshot in JPEG-format / browsers: all / codecs: MJPEG, JPEG</li>\n        `;\n        });\n    </script>\n\n    <div id=\"homekit\" style=\"display: none\">\n        <h2>HomeKit server</h2>\n    </div>\n    <script>\n        fetch(`api/homekit?id=${src}`, {cache: 'no-cache'}).then(async (r) => {\n            if (!r.ok) return;\n\n            const div = document.querySelector('#homekit');\n            div.innerHTML += `<div><a href=\"${r.url}\">info.json</a> page with active connections</div>`;\n            div.style = '';\n\n            /** @type {{name: string, category_id: string, setup_code: string, setup_id: string, setup_uri: string}} */\n            const data = await r.json();\n            if (data.setup_code === undefined) return;\n\n            const script = document.createElement('script');\n            script.src = 'https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js';\n            script.async = true;\n            script.onload = () => {\n                /* global BigInt */\n                const categoryID = BigInt(data.category_id);\n                const pin = BigInt(data.setup_code.replaceAll('-', ''));\n                const payload = categoryID << BigInt(31) | BigInt(2 << 27) | pin;\n                const setupURI = `X-HM://${payload.toString(36).toUpperCase().padStart(9, '0')}${data.setup_id}`;\n\n                div.innerHTML += `<pre>Setup Name: ${data.name}\nSetup Code: ${data.setup_code}</pre>\n<div id=\"homekit-qrcode\"></div>`;\n\n                /* global QRCode */\n                new QRCode('homekit-qrcode', {text: setupURI, width: 128, height: 128});\n            };\n            document.head.appendChild(script);\n        });\n    </script>\n\n    <div>\n        <h2>Play audio</h2>\n        <label><input type=\"radio\" name=\"play\" value=\"file\" checked>\n            file - play remote (https://example.com/song.mp3) or local (/media/song.mp3) file\n        </label>\n        <label><input type=\"radio\" name=\"play\" value=\"live\">\n            live - play remote live stream (radio, etc.)\n        </label>\n        <label><input type=\"radio\" name=\"play\" value=\"text\">\n            text - play Text To Speech (if your FFmpeg support this)\n        </label>\n        <br>\n        <input id=\"play-url\" type=\"text\" placeholder=\"path / url / text\">\n        <button id=\"play-send\">send</button>\n        / cameras with two way audio support\n    </div>\n    <script>\n        document.getElementById('play-send').addEventListener('click', ev => {\n            ev.preventDefault();\n            // action - file / live / text\n            const action = document.querySelector('input[name=\"play\"]:checked').value;\n            const url = new URL('api/ffmpeg', location.href);\n            url.searchParams.set('dst', src);\n            url.searchParams.set(action, document.getElementById('play-url').value);\n            fetch(url, {method: 'POST'});\n        });\n    </script>\n\n    <div>\n        <h2>Publish stream</h2>\n        <pre>YouTube:  rtmps://xxx.rtmp.youtube.com/live2/xxxx-xxxx-xxxx-xxxx-xxxx\nTelegram: rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx</pre>\n        <input id=\"pub-url\" type=\"text\" placeholder=\"url\">\n        <button id=\"pub-send\">send</button>\n        / Telegram RTMPS server\n    </div>\n    <script>\n        document.getElementById('pub-send').addEventListener('click', ev => {\n            ev.preventDefault();\n            const url = new URL('api/streams', location.href);\n            url.searchParams.set('src', src);\n            url.searchParams.set('dst', document.getElementById('pub-url').value);\n            fetch(url, {method: 'POST'});\n        });\n    </script>\n\n    <div id=\"webrtc\">\n        <h2>WebRTC Magic</h2>\n        <label><input type=\"radio\" name=\"webrtc\" value=\"video+audio\" checked>\n            video+audio = simple viewer\n        </label>\n        <label><input type=\"radio\" name=\"webrtc\" value=\"video+audio+microphone\">\n            video+audio+microphone = two way audio from camera\n        </label>\n        <label><input type=\"radio\" name=\"webrtc\" value=\"camera+microphone\">\n            camera+microphone = stream from browser\n        </label>\n        <label><input type=\"radio\" name=\"webrtc\" value=\"display+speaker\">\n            display+speaker = broadcast software\n        </label>\n\n        <br>\n        <li><a id=\"local\" href=\"webrtc.html?src=\">webrtc.html</a> local WebRTC viewer</li>\n\n        <li>\n            <a id=\"shareadd\" href=\"#\">share link</a>\n            <a id=\"shareget\" href=\"#\">copy link</a>\n            <a id=\"sharedel\" href=\"#\">delete</a>\n            external WebRTC viewer\n        </li>\n    </div>\n    <script>\n        function webrtcLinksUpdate() {\n            const media = document.querySelector('input[name=\"webrtc\"]:checked').value;\n\n            const direction = media.indexOf('video') >= 0 || media === 'audio' ? 'src' : 'dst';\n            document.getElementById('local').href = `webrtc.html?${direction}=${src}&media=${media}`;\n\n            const share = document.getElementById('shareget');\n            share.href = `https://go2rtc.org/webtorrent/#${share.dataset.auth}&media=${media}`;\n        }\n\n        function share(method) {\n            const url = new URL('api/webtorrent', location.href);\n            url.searchParams.set('src', src);\n            return fetch(url, {method: method, cache: 'no-cache'});\n        }\n\n        function onshareadd(r) {\n            document.getElementById('shareget').dataset['auth'] = `share=${r.share}&pwd=${r.pwd}`;\n\n            document.getElementById('shareadd').style.display = 'none';\n            document.getElementById('shareget').style.display = '';\n            document.getElementById('sharedel').style.display = '';\n\n            webrtcLinksUpdate();\n        }\n\n        function onsharedel() {\n            document.getElementById('shareadd').style.display = '';\n            document.getElementById('shareget').style.display = 'none';\n            document.getElementById('sharedel').style.display = 'none';\n        }\n\n        function copyTextToClipboard(text) {\n            // https://web.dev/patterns/clipboard/copy-text\n            if (navigator.clipboard && window.isSecureContext) {\n                navigator.clipboard.writeText(text).catch(err => {\n                    console.error(err.name, err.message);\n                });\n            } else {\n                const textarea = document.createElement('textarea');\n                textarea.value = text;\n                textarea.style.opacity = '0';\n                document.body.appendChild(textarea);\n\n                textarea.focus();\n                textarea.select();\n\n                try {\n                    document.execCommand('copy');\n                } catch (err) {\n                    console.error(err.name, err.message);\n                }\n\n                document.body.removeChild(textarea);\n            }\n        }\n\n        document.getElementById('shareadd').addEventListener('click', ev => {\n            ev.preventDefault();\n            share('POST').then(r => r.json()).then(r => onshareadd(r));\n        });\n\n        document.getElementById('shareget').addEventListener('click', ev => {\n            ev.preventDefault();\n            copyTextToClipboard(ev.target.href);\n        });\n\n        document.getElementById('sharedel').addEventListener('click', ev => {\n            ev.preventDefault();\n            share('DELETE').then(() => onsharedel());\n        });\n\n        document.getElementById('webrtc').addEventListener('click', ev => {\n            if (ev.target.tagName === 'INPUT') webrtcLinksUpdate();\n        });\n\n        share('GET').then(r => {\n            if (r.ok) r.json().then(r => onshareadd(r));\n            else onsharedel();\n        });\n\n        webrtcLinksUpdate();\n    </script>\n</main>\n\n</body>\n</html>\n"
  },
  {
    "path": "www/log.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>log - go2rtc</title>\n    <style>\n        main > div {\n            display: flex;\n            flex-wrap: wrap;\n            gap: 10px;\n        }\n\n        table tbody {\n            font-size: 13px;\n        }\n\n        .trace {\n            color: #585858 !important;\n        }\n\n        .debug {\n            color: #808080 !important;\n        }\n\n        .info {\n            color: #0174DF !important;\n        }\n\n        .warn {\n            color: #FF9966 !important;\n        }\n\n        .error {\n            color: #DF0101 !important;\n        }\n    </style>\n</head>\n<body>\n\n<script src=\"main.js\"></script>\n\n<main>\n    <div>\n        <button id=\"clean\">Clean</button>\n        <button id=\"update\">Auto Update: ON</button>\n        <button id=\"reverse\">Reverse Log Order: OFF</button>\n    </div>\n    <table>\n        <thead>\n        <tr>\n            <th style=\"width: 100px\">Time</th>\n            <th style=\"width: 40px\">Level</th>\n            <th>Message</th>\n        </tr>\n        </thead>\n        <tbody id=\"log\">\n        </tbody>\n    </table>\n</main>\n\n<script>\n    document.getElementById('clean').addEventListener('click', async () => {\n        const r = await fetch('api/log', {method: 'DELETE'});\n        if (r.ok) reload();\n        alert(await r.text());\n    });\n\n    // Sanitizes the input text to prevent XSS when inserting into the DOM\n    function escapeHTML(text) {\n        return text\n            .replace(/&/g, '&amp;')\n            .replace(/</g, '&lt;')\n            .replace(/>/g, '&gt;')\n            .replace(/\"/g, '&quot;')\n            .replace(/'/g, '&#039;')\n            .replace(/\\n/g, '<br>');\n    }\n\n    const reverseBtn = document.getElementById('reverse');\n    const update = document.getElementById('update');\n\n    let reverseOrder = false;\n    let autoUpdateEnabled = true;\n\n    reverseBtn.textContent = `Reverse Log Order: ${reverseOrder ? 'ON' : 'OFF'}`;\n    update.textContent = `Auto Update: ${autoUpdateEnabled ? 'ON' : 'OFF'}`;\n\n    function applyLogStyling(jsonlines) {\n        const KEYS = ['time', 'level', 'message'];\n        let lines = JSON.parse('[' + jsonlines.trimEnd().replaceAll('\\n', ',') + ']');\n        if (reverseOrder) {\n            lines = lines.reverse();\n        }\n        return lines.map(line => {\n            const ts = new Date(line['time']).toLocaleString(undefined, {\n                hour: 'numeric',\n                minute: 'numeric',\n                second: 'numeric',\n                fractionalSecondDigits: 3\n            });\n            const msg = Object.keys(line).reduce((msg, key) => {\n                return KEYS.indexOf(key) < 0 ? `${msg} ${key}=${line[key]}` : msg;\n            }, line['message']);\n            return `<tr class=\"${line['level']}\"><td>${ts}</td><td>${line['level']}</td><td>${escapeHTML(msg)}</td></tr>`;\n        }).join('');\n    }\n\n    function reload() {\n        const url = new URL('api/log', location.href);\n        fetch(url, {cache: 'no-cache'})\n            .then(response => response.text())\n            .then(data => {\n                // Apply styling to the log data\n                document.getElementById('log').innerHTML = applyLogStyling(data);\n            })\n            .catch(error => {\n                console.error('An error occurred:', error);\n            });\n    }\n\n    reload();\n\n    update.textContent = `Auto Update: ${autoUpdateEnabled ? 'ON' : 'OFF'}`;\n    update.addEventListener('click', () => {\n        autoUpdateEnabled = !autoUpdateEnabled;\n        update.textContent = `Auto Update: ${autoUpdateEnabled ? 'ON' : 'OFF'}`;\n    });\n\n    // Toggle log order\n    reverseBtn.textContent = `Reverse Log Order: ${reverseOrder ? 'ON' : 'OFF'}`;\n    reverseBtn.addEventListener('click', () => {\n        reverseOrder = !reverseOrder;\n        reverseBtn.textContent = `Reverse Log Order: ${reverseOrder ? 'ON' : 'OFF'}`;\n        reload(); // Reload logs to apply the new order\n    });\n\n    // Reload the logs every 5 seconds\n    setInterval(() => {\n        if (autoUpdateEnabled) reload();\n    }, 5000);\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "www/main.js",
    "content": "document.head.innerHTML += `\n<style>\n    body {\n        background-color: white;  /* fix Hass black theme */\n        display: flex;\n        flex-direction: column;\n        font-family: Arial, sans-serif;\n        margin: 0;\n    }\n\n    /* navigation block */\n    nav {\n        background-color: #333;\n        overflow: hidden;\n    }\n\n    nav a {\n        float: left;\n        display: block;\n        color: #f2f2f2;\n        text-align: center;\n        padding: 14px 16px;\n        text-decoration: none;\n        font-size: 17px;\n    }\n\n    nav a:hover {\n        background-color: #ddd;\n        color: black;\n    }\n\n    /* main block */\n    main {\n        padding: 10px;\n        display: flex;\n        flex-direction: column;\n        gap: 10px;\n    }\n\n    /* checkbox */\n    label {\n        display: flex;\n        gap: 5px;\n        align-items: center;\n        cursor: pointer;\n    }\n\n    input[type=\"checkbox\"] {\n        width: 18px;\n        height: 18px;\n        cursor: pointer;\n    }\n\n    /* form */\n    form {\n        display: flex;\n        flex-wrap: wrap;\n        gap: 10px;\n    }\n\n    input[type=\"text\"], input[type=\"email\"], input[type=\"password\"], select {\n        padding: 10px;\n        border: 1px solid #ccc;\n        border-radius: 4px;\n        font-size: 16px;\n    }\n\n    button {\n        padding: 10px 20px;\n        border: 1px solid #ccc;\n        border-radius: 4px;\n        cursor: pointer;\n        font-size: 16px;\n    }\n\n    /* table */\n    table {\n        width: 100%;\n        background-color: white;\n        border-collapse: collapse;\n        margin: 0 auto;\n        overflow: hidden;\n    }\n\n    th, td {\n        padding: 12px 15px;\n        text-align: left;\n        border-bottom: 1px solid #e0e0e0;\n    }\n\n    th {\n        background-color: #444;\n        color: white;\n    }\n\n    tr:nth-child(even) {\n        background-color: #fafafa;\n    }\n\n    tr:hover {\n        background-color: #edf7ff;\n        transition: background-color 0.3s ease;\n    }\n\n    /* table on mobile */\n    @media (max-width: 480px) {\n        table, thead, tbody, th, td, tr {\n            display: block;\n        }\n\n        th, td {\n            box-sizing: border-box;\n            width: 100% !important;\n            border: none;\n        }\n\n        tr {\n            margin-bottom: 10px;\n            border-radius: 4px;\n        }\n    }\n</style>\n`;\n\ndocument.body.innerHTML = `\n<header>\n    <nav>\n        <a href=\"index.html\"><b>go2rtc</b></a>\n        <a href=\"add.html\">add</a>\n        <a href=\"config.html\">config</a>\n        <a href=\"log.html\">log</a>\n        <a href=\"net.html\">net</a>\n    </nav>\n</header>\n` + document.body.innerHTML;\n"
  },
  {
    "path": "www/net.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta charset=\"UTF-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\">\n    <title>net - go2rtc</title>\n    <script src=\"https://cdn.jsdelivr.net/npm/vis-network@10.0.2/standalone/umd/vis-network.min.js\"></script>\n    <style>\n        html, body, #network {\n            height: 100%;\n        }\n    </style>\n</head>\n<body>\n\n<script src=\"main.js\"></script>\n\n<div id=\"network\"></div>\n\n<script>\n    /* global vis */\n    window.addEventListener('load', () => {\n        const url = new URL('api/streams.dot' + location.search, location.href);\n\n        const container = document.getElementById('network');\n        const options = {\n            edges: {\n                font: {align: 'middle'},\n                smooth: false,\n            },\n            nodes: {shape: 'box'},\n            physics: false,\n        };\n\n        let network;\n\n        async function update() {\n            try {\n                const response = await fetch(url, {cache: 'no-cache'});\n                const dotData = await response.text();\n                const data = vis.parseDOTNetwork(dotData);\n\n                if (!network) {\n                    network = new vis.Network(container, data, options);\n                    network.storePositions();\n                } else {\n                    const positions = network.getPositions();\n                    const viewPosition = network.getViewPosition();\n                    const scale = network.getScale();\n                    const selectedNodes = network.getSelectedNodes();\n\n                    network.setData(data);\n\n                    for (const nodeId in positions) {\n                        network.moveNode(nodeId, positions[nodeId].x, positions[nodeId].y);\n                    }\n\n                    network.moveTo({position: viewPosition, scale: scale});\n\n                    network.selectNodes(selectedNodes);\n                }\n            } catch (error) {\n                console.error('Error fetching or updating network data:', error);\n            }\n\n            setTimeout(update, 5000);\n        }\n\n        update();\n    });\n</script>\n\n</body>\n</html>\n"
  },
  {
    "path": "www/schema.json",
    "content": "{\n  \"$schema\": \"http://json-schema.org/draft-07/schema#\",\n  \"title\": \"go2rtc\",\n  \"type\": \"object\",\n  \"additionalProperties\": false,\n  \"definitions\": {\n    \"listen\": {\n      \"type\": \"string\",\n      \"anyOf\": [\n        {\n          \"type\": \"string\",\n          \"pattern\": \":[0-9]{1,5}$\"\n        },\n        {\n          \"type\": \"string\",\n          \"const\": \"\"\n        }\n      ]\n    },\n    \"log_level\": {\n      \"type\": \"string\",\n      \"enum\": [\n        \"trace\",\n        \"debug\",\n        \"info\",\n        \"warn\",\n        \"error\",\n        \"fatal\",\n        \"panic\",\n        \"disabled\"\n      ]\n    },\n    \"source\": {\n      \"type\": \"string\",\n      \"examples\": [\n        \"rtsp://username:password@192.168.1.123/cam/realmonitor?channel=1&subtype=0&unicast=true&proto=Onvif\",\n        \"rtsp://username:password@192.168.1.123/stream1\",\n        \"rtsp://username:password@192.168.1.123/h264Preview_01_main\",\n        \"rtmp://192.168.1.123/bcs/channel0_main.bcs?channel=0&stream=0&user=username&password=password\",\n        \"http://192.168.1.123/flv?port=1935&app=bcs&stream=channel0_main.bcs&user=username&password=password\",\n        \"http://username:password@192.168.1.123/cgi-bin/snapshot.cgi?channel=1\",\n        \"ffmpeg:media.mp4#video=h264#hardware#width=1920#height=1080#rotate=180#audio=copy\",\n        \"ffmpeg:virtual?video=testsrc&size=4K#video=h264#hardware#bitrate=50M\",\n        \"exec:ffmpeg -re -i media.mp4 -c copy -rtsp_transport tcp -f rtsp {output}\",\n        \"onvif://username:password@192.168.1.123:80?subtype=0\",\n        \"tapo://password@192.168.1.123:8800?channel=0&subtype=0\"\n      ]\n    }\n  },\n  \"properties\": {\n    \"api\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"listen\": {\n          \"type\": \"string\",\n          \"default\": \":1984\",\n          \"examples\": [\n            \"127.0.0.1:1984\"\n          ]\n        },\n        \"username\": {\n          \"description\": \"Basic auth for WebUI\",\n          \"type\": \"string\",\n          \"examples\": [\n            \"admin\"\n          ]\n        },\n        \"password\": {\n          \"type\": \"string\"\n        },\n        \"local_auth\": {\n          \"description\": \"Enable auth check for localhost requests\",\n          \"type\": \"boolean\",\n          \"default\": false\n        },\n        \"base_path\": {\n          \"description\": \"API prefix for serving on suburl (/api => /rtc/api)\",\n          \"type\": \"string\",\n          \"examples\": [\n            \"/rtc\"\n          ]\n        },\n        \"static_dir\": {\n          \"description\": \"Folder for static files (custom web interface)\",\n          \"type\": \"string\",\n          \"examples\": [\n            \"www\"\n          ]\n        },\n        \"origin\": {\n          \"description\": \"Allow CORS requests (only * supported)\",\n          \"type\": \"string\",\n          \"enum\": [\n            \"*\",\n            \"\"\n          ]\n        },\n        \"tls_listen\": {\n          \"type\": \"string\"\n        },\n        \"tls_cert\": {\n          \"type\": \"string\",\n          \"examples\": [\n            \"-----BEGIN CERTIFICATE-----\",\n            \"/ssl/fullchain.pem\"\n          ]\n        },\n        \"tls_key\": {\n          \"type\": \"string\",\n          \"examples\": [\n            \"-----BEGIN PRIVATE KEY-----\",\n            \"/ssl/privkey.pem\"\n          ]\n        },\n        \"unix_listen\": {\n          \"type\": \"string\",\n          \"examples\": [\n            \"/tmp/go2rtc.sock\"\n          ]\n        },\n        \"allow_paths\": {\n          \"description\": \"Allow only these HTTP paths (full paths, including base_path)\",\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"examples\": [\n            [\n              \"/api\",\n              \"/api/streams\",\n              \"/api/webrtc\"\n            ]\n          ]\n        }\n      }\n    },\n    \"app\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"modules\": {\n          \"description\": \"Enable only these modules (empty / omitted means all)\",\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\",\n            \"enum\": [\n              \"api\",\n              \"ws\",\n              \"http\",\n              \"rtsp\",\n              \"webrtc\",\n              \"mp4\",\n              \"hls\",\n              \"mjpeg\",\n              \"hass\",\n              \"homekit\",\n              \"onvif\",\n              \"rtmp\",\n              \"webtorrent\",\n              \"wyoming\",\n              \"echo\",\n              \"exec\",\n              \"expr\",\n              \"ffmpeg\",\n              \"alsa\",\n              \"v4l2\",\n              \"bubble\",\n              \"doorbird\",\n              \"dvrip\",\n              \"eseecloud\",\n              \"flussonic\",\n              \"gopro\",\n              \"isapi\",\n              \"ivideon\",\n              \"kasa\",\n              \"mpeg\",\n              \"nest\",\n              \"ring\",\n              \"roborock\",\n              \"tapo\",\n              \"tuya\",\n              \"xiaomi\",\n              \"yandex\",\n              \"debug\",\n              \"ngrok\",\n              \"pinggy\",\n              \"srtp\"\n            ]\n          }\n        }\n      }\n    },\n    \"env\": {\n      \"description\": \"Config variables that can be referenced as ${NAME} / ${NAME:default}\",\n      \"type\": \"object\",\n      \"additionalProperties\": {\n        \"type\": \"string\"\n      }\n    },\n    \"echo\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"allow_paths\": {\n          \"description\": \"Allow only these binaries for echo: URLs (exact cmd name/path)\",\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        }\n      }\n    },\n    \"exec\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"allow_paths\": {\n          \"description\": \"Allow only these binaries for exec: URLs (exact cmd name/path)\",\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          },\n          \"examples\": [\n            [\n              \"ffmpeg\",\n              \"/usr/bin/ffmpeg\"\n            ]\n          ]\n        }\n      }\n    },\n    \"ffmpeg\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"bin\": {\n          \"type\": \"string\",\n          \"default\": \"ffmpeg\"\n        },\n        \"global\": {\n          \"type\": \"string\",\n          \"default\": \"-hide_banner\"\n        },\n        \"file\": {\n          \"type\": \"string\",\n          \"default\": \"-re -i {input}\"\n        },\n        \"http\": {\n          \"type\": \"string\",\n          \"default\": \"-fflags nobuffer -flags low_delay -i {input}\"\n        },\n        \"rtsp\": {\n          \"type\": \"string\",\n          \"default\": \"-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -rtsp_flags prefer_tcp -i {input}\"\n        },\n        \"rtsp/udp\": {\n          \"type\": \"string\",\n          \"default\": \"-fflags nobuffer -flags low_delay -timeout 5000000 -user_agent go2rtc/ffmpeg -i {input}\"\n        }\n      },\n      \"additionalProperties\": {\n        \"description\": \"FFmpeg template\",\n        \"type\": \"string\"\n      }\n    },\n    \"hass\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"config\": {\n          \"description\": \"Home Assistant config directory path\",\n          \"type\": \"string\",\n          \"examples\": [\n            \"/config\"\n          ]\n        }\n      }\n    },\n    \"homekit\": {\n      \"type\": \"object\",\n      \"additionalProperties\": {\n        \"type\": [\n          \"object\",\n          \"null\"\n        ],\n        \"properties\": {\n          \"pin\": {\n            \"description\": \"HomeKit pairing PIN\",\n            \"type\": \"string\",\n            \"default\": \"19550224\",\n            \"anyOf\": [\n              {\n                \"type\": \"string\",\n                \"pattern\": \"^[0-9]{8}$\"\n              },\n              {\n                \"type\": \"string\",\n                \"pattern\": \"^[0-9]{3}-[0-9]{2}-[0-9]{3}$\"\n              }\n            ]\n          },\n          \"name\": {\n            \"type\": \"string\"\n          },\n          \"device_id\": {\n            \"type\": \"string\"\n          },\n          \"device_private\": {\n            \"type\": \"string\"\n          },\n          \"category_id\": {\n            \"description\": \"Accessory category: `bridge`, `doorbell` or numeric ID\",\n            \"type\": \"string\",\n            \"default\": \"camera\",\n            \"anyOf\": [\n              {\n                \"type\": \"string\",\n                \"enum\": [\n                  \"bridge\",\n                  \"camera\",\n                  \"doorbell\"\n                ]\n              },\n              {\n                \"type\": \"string\",\n                \"pattern\": \"^[0-9]+$\"\n              },\n              {\n                \"type\": \"string\",\n                \"const\": \"\"\n              }\n            ]\n          },\n          \"pairings\": {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          }\n        }\n      }\n    },\n    \"log\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"format\": {\n          \"description\": \"Log format: color/json/text or empty for autodetect\",\n          \"type\": \"string\",\n          \"default\": \"color\",\n          \"enum\": [\n            \"\",\n            \"color\",\n            \"json\",\n            \"text\"\n          ]\n        },\n        \"level\": {\n          \"description\": \"Defaul log level\",\n          \"default\": \"info\",\n          \"$ref\": \"#/definitions/log_level\"\n        },\n        \"output\": {\n          \"description\": \"Log output: stdout/stderr/file[:path] or empty (memory only)\",\n          \"type\": \"string\",\n          \"default\": \"stdout\",\n          \"anyOf\": [\n            {\n              \"type\": \"string\",\n              \"enum\": [\n                \"\",\n                \"stdout\",\n                \"stderr\"\n              ]\n            },\n            {\n              \"type\": \"string\",\n              \"pattern\": \"^file(:.+)?$\",\n              \"examples\": [\n                \"file\",\n                \"file:go2rtc.log\"\n              ]\n            }\n          ]\n        },\n        \"time\": {\n          \"type\": \"string\",\n          \"default\": \"UNIXMS\",\n          \"anyOf\": [\n            {\n              \"type\": \"string\",\n              \"enum\": [\n                \"\",\n                \"UNIXMS\",\n                \"UNIXMICRO\",\n                \"UNIXNANO\",\n                \"2006-01-02T15:04:05Z07:00\",\n                \"2006-01-02T15:04:05.999999999Z07:00\"\n              ]\n            },\n            {\n              \"type\": \"string\"\n            }\n          ]\n        },\n        \"api\": {\n          \"$ref\": \"#/definitions/log_level\"\n        },\n        \"echo\": {\n          \"$ref\": \"#/definitions/log_level\"\n        },\n        \"exec\": {\n          \"description\": \"Value `exec: debug` will print stderr\",\n          \"$ref\": \"#/definitions/log_level\"\n        },\n        \"expr\": {\n          \"$ref\": \"#/definitions/log_level\"\n        },\n        \"ffmpeg\": {\n          \"description\": \"Will only be displayed with `exec: debug` setting\",\n          \"default\": \"error\",\n          \"$ref\": \"#/definitions/log_level\"\n        },\n        \"hass\": {\n          \"$ref\": \"#/definitions/log_level\"\n        },\n        \"hls\": {\n          \"$ref\": \"#/definitions/log_level\"\n        },\n        \"homekit\": {\n          \"$ref\": \"#/definitions/log_level\"\n        },\n        \"mjpeg\": {\n          \"$ref\": \"#/definitions/log_level\"\n        },\n        \"mp4\": {\n          \"$ref\": \"#/definitions/log_level\"\n        },\n        \"ngrok\": {\n          \"$ref\": \"#/definitions/log_level\"\n        },\n        \"onvif\": {\n          \"$ref\": \"#/definitions/log_level\"\n        },\n        \"rtmp\": {\n          \"$ref\": \"#/definitions/log_level\"\n        },\n        \"rtsp\": {\n          \"$ref\": \"#/definitions/log_level\"\n        },\n        \"streams\": {\n          \"$ref\": \"#/definitions/log_level\"\n        },\n        \"webrtc\": {\n          \"$ref\": \"#/definitions/log_level\"\n        },\n        \"webtorrent\": {\n          \"$ref\": \"#/definitions/log_level\"\n        },\n        \"wyoming\": {\n          \"$ref\": \"#/definitions/log_level\"\n        }\n      }\n    },\n    \"ngrok\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"command\": {\n          \"type\": \"string\",\n          \"examples\": [\n            \"ngrok tcp 8555 --authtoken xxx\",\n            \"ngrok start --all --config ngrok.yaml\"\n          ]\n        }\n      }\n    },\n    \"pinggy\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"tunnel\": {\n          \"description\": \"Expose local address via Pinggy\",\n          \"type\": \"string\",\n          \"examples\": [\n            \"http://127.0.0.1:1984\",\n            \"tcp://192.168.1.123:554\"\n          ]\n        }\n      }\n    },\n    \"preload\": {\n      \"description\": \"Preload streams on startup (map stream name => probe query, default `video&audio`)\",\n      \"type\": \"object\",\n      \"additionalProperties\": {\n        \"type\": \"string\",\n        \"examples\": [\n          \"video&audio\",\n          \"video\"\n        ]\n      }\n    },\n    \"publish\": {\n      \"type\": \"object\",\n      \"additionalProperties\": {\n        \"anyOf\": [\n          {\n            \"type\": \"string\",\n            \"examples\": [\n              \"rtmp://xxx.rtmp.youtube.com/live2/xxxx-xxxx-xxxx-xxxx-xxxx\",\n              \"rtmps://xxx-x.rtmp.t.me/s/xxxxxxxxxx:xxxxxxxxxxxxxxxxxxxxxx\"\n            ]\n          },\n          {\n            \"type\": \"array\",\n            \"items\": {\n              \"type\": \"string\"\n            }\n          }\n        ]\n      }\n    },\n    \"rtmp\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"listen\": {\n          \"type\": \"string\",\n          \"examples\": [\n            \":1935\"\n          ]\n        }\n      }\n    },\n    \"rtsp\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"listen\": {\n          \"type\": \"string\",\n          \"default\": \":8554\"\n        },\n        \"username\": {\n          \"type\": \"string\",\n          \"examples\": [\n            \"admin\"\n          ]\n        },\n        \"password\": {\n          \"type\": \"string\"\n        },\n        \"default_query\": {\n          \"type\": \"string\",\n          \"default\": \"video&audio\"\n        },\n        \"pkt_size\": {\n          \"type\": \"integer\"\n        }\n      }\n    },\n    \"srtp\": {\n      \"description\": \"SRTP server for HomeKit\",\n      \"type\": \"object\",\n      \"properties\": {\n        \"listen\": {\n          \"type\": \"string\",\n          \"default\": \":8443\"\n        }\n      }\n    },\n    \"streams\": {\n      \"type\": \"object\",\n      \"additionalProperties\": {\n        \"anyOf\": [\n          {\n            \"$ref\": \"#/definitions/source\"\n          },\n          {\n            \"type\": \"array\",\n            \"items\": {\n              \"$ref\": \"#/definitions/source\"\n            }\n          },\n          {\n            \"type\": \"null\"\n          }\n        ]\n      }\n    },\n    \"xiaomi\": {\n      \"type\": \"object\",\n      \"additionalProperties\": {\n        \"type\": \"string\"\n      }\n    },\n    \"webrtc\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"listen\": {\n          \"type\": \"string\",\n          \"default\": \":8555\",\n          \"examples\": [\n            \":8555/udp\"\n          ]\n        },\n        \"candidates\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\",\n            \"examples\": [\n              \"216.58.210.174:8555\",\n              \"stun:8555\",\n              \"home.duckdns.org:8555\"\n            ]\n          }\n        },\n        \"ice_servers\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"object\",\n            \"properties\": {\n              \"urls\": {\n                \"type\": \"array\",\n                \"items\": {\n                  \"type\": \"string\",\n                  \"examples\": [\n                    \"stun:stun.l.google.com:19302\",\n                    \"turn:123.123.123.123:3478\"\n                  ]\n                }\n              },\n              \"username\": {\n                \"type\": \"string\"\n              },\n              \"credential\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        },\n        \"filters\": {\n          \"type\": \"object\",\n          \"properties\": {\n            \"candidates\": {\n              \"description\": \"Keep only these candidates\",\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            },\n            \"interfaces\": {\n              \"description\": \"Keep only these interfaces\",\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            },\n            \"ips\": {\n              \"description\": \"Keep only these IP-addresses\",\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\"\n              }\n            },\n            \"networks\": {\n              \"description\": \"Use only these network types\",\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"string\",\n                \"enum\": [\n                  \"tcp4\",\n                  \"tcp6\",\n                  \"udp4\",\n                  \"udp6\"\n                ]\n              }\n            },\n            \"udp_ports\": {\n              \"description\": \"Use only these UDP ports range [min, max]\",\n              \"type\": \"array\",\n              \"items\": {\n                \"type\": \"integer\"\n              },\n              \"maxItems\": 2,\n              \"minItems\": 2\n            }\n          }\n        }\n      }\n    },\n    \"webtorrent\": {\n      \"type\": \"object\",\n      \"properties\": {\n        \"trackers\": {\n          \"type\": \"array\",\n          \"items\": {\n            \"type\": \"string\"\n          }\n        },\n        \"shares\": {\n          \"additionalProperties\": {\n            \"type\": \"object\",\n            \"properties\": {\n              \"pwd\": {\n                \"type\": \"string\",\n                \"minLength\": 4\n              },\n              \"src\": {\n                \"type\": \"string\"\n              }\n            }\n          }\n        }\n      }\n    },\n    \"wyoming\": {\n      \"type\": \"object\",\n      \"additionalProperties\": {\n        \"type\": \"object\",\n        \"properties\": {\n          \"listen\": {\n            \"description\": \"Listen address for Wyoming server\",\n            \"type\": \"string\"\n          },\n          \"name\": {\n            \"description\": \"Optional satellite name (default: stream name)\",\n            \"type\": \"string\"\n          },\n          \"mode\": {\n            \"description\": \"Optional mode: mic / snd / default\",\n            \"type\": \"string\",\n            \"enum\": [\n              \"\",\n              \"mic\",\n              \"snd\"\n            ]\n          },\n          \"event\": {\n            \"description\": \"Event handlers (map event type => expr script)\",\n            \"type\": \"object\",\n            \"additionalProperties\": {\n              \"type\": \"string\"\n            }\n          },\n          \"wake_uri\": {\n            \"description\": \"Optional WAKE service URI (ex. tcp://host:port?name=...)\",\n            \"type\": \"string\",\n            \"examples\": [\n              \"tcp://192.168.1.23:10400\"\n            ]\n          },\n          \"vad_threshold\": {\n            \"description\": \"Optional VAD threshold (0.1..3.5 typical)\",\n            \"type\": \"number\"\n          }\n        }\n      }\n    }\n  }\n}\n"
  },
  {
    "path": "www/static.go",
    "content": "package www\n\nimport \"embed\"\n\n//go:embed *.html\n//go:embed *.js\n//go:embed *.json\nvar Static embed.FS\n"
  },
  {
    "path": "www/stream.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <link rel=\"apple-touch-icon\" href=\"https://go2rtc.org/icons/apple-touch-icon-180x180.png\" sizes=\"180x180\">\n    <link rel=\"icon\" href=\"https://go2rtc.org/icons/favicon.ico\">\n    <link rel=\"manifest\" href=\"https://go2rtc.org/manifest.json\">\n    <title>stream - go2rtc</title>\n    <style>\n        body {\n            background: black;\n            margin: 0;\n            padding: 0;\n            display: flex;\n            font-family: Arial, Helvetica, sans-serif;\n        }\n\n        html, body {\n            height: 100%;\n            width: 100%;\n        }\n\n        .flex {\n            flex-wrap: wrap;\n            align-content: flex-start;\n            align-items: flex-start;\n        }\n    </style>\n</head>\n<body>\n<script type=\"module\" src=\"./video-stream.js\"></script>\n<script type=\"module\">\n    const params = new URLSearchParams(location.search);\n\n    // support multiple streams and multiple modes\n    const streams = params.getAll('src');\n    const modes = params.getAll('mode');\n    if (modes.length === 0) modes.push('');\n\n    while (modes.length > streams.length) {\n        streams.push(streams[0]);\n    }\n    while (streams.length > modes.length) {\n        modes.push(modes[0]);\n    }\n\n    if (streams.length > 1) {\n        document.body.className = 'flex';\n    }\n\n    const background = params.get('background') !== 'false';\n    const width = '1 0 ' + (params.get('width') || '320px');\n\n    for (let i = 0; i < streams.length; i++) {\n        /** @type {VideoStream} */\n        const video = document.createElement('video-stream');\n        video.background = background;\n        video.mode = modes[i] || video.mode;\n        video.style.flex = width;\n        video.src = new URL('api/ws?src=' + encodeURIComponent(streams[i]), location.href);\n        document.body.appendChild(video);\n    }\n\n    document.title = streams.join('/') + ' - go2rtc';\n</script>\n</body>\n</html>\n"
  },
  {
    "path": "www/video-rtc.js",
    "content": "/**\n * VideoRTC v1.6.0 - Video player for go2rtc streaming application.\n *\n * All modern web technologies are supported in almost any browser except Apple Safari.\n *\n * Support:\n * - ECMAScript 2017 (ES8) = ES6 + async\n * - RTCPeerConnection for Safari iOS 11.0+\n * - IntersectionObserver for Safari iOS 12.2+\n * - ManagedMediaSource for Safari 17+\n *\n * Doesn't support:\n * - MediaSource for Safari iOS\n * - Customized built-in elements (extends HTMLVideoElement) because Safari\n * - Autoplay for WebRTC in Safari\n */\nexport class VideoRTC extends HTMLElement {\n    constructor() {\n        super();\n\n        this.DISCONNECT_TIMEOUT = 5000;\n        this.RECONNECT_TIMEOUT = 15000;\n\n        this.CODECS = [\n            'avc1.640029',      // H.264 high 4.1 (Chromecast 1st and 2nd Gen)\n            'avc1.64002A',      // H.264 high 4.2 (Chromecast 3rd Gen)\n            'avc1.640033',      // H.264 high 5.1 (Chromecast with Google TV)\n            'hvc1.1.6.L153.B0', // H.265 main 5.1 (Chromecast Ultra)\n            'mp4a.40.2',        // AAC LC\n            'mp4a.40.5',        // AAC HE\n            'flac',             // FLAC (PCM compatible)\n            'opus',             // OPUS Chrome, Firefox\n        ];\n\n        /**\n         * [config] Supported modes (webrtc, webrtc/tcp, mse, hls, mp4, mjpeg).\n         * @type {string}\n         */\n        this.mode = 'webrtc,mse,hls,mjpeg';\n\n        /**\n         * [Config] Requested medias (video, audio, microphone).\n         * @type {string}\n         */\n        this.media = 'video,audio';\n\n        /**\n         * [config] Run stream when not displayed on the screen. Default `false`.\n         * @type {boolean}\n         */\n        this.background = false;\n\n        /**\n         * [config] Run stream only when player in the viewport. Stop when user scroll out player.\n         * Value is percentage of visibility from `0` (not visible) to `1` (full visible).\n         * Default `0` - disable;\n         * @type {number}\n         */\n        this.visibilityThreshold = 0;\n\n        /**\n         * [config] Run stream only when browser page on the screen. Stop when user change browser\n         * tab or minimise browser windows.\n         * @type {boolean}\n         */\n        this.visibilityCheck = true;\n\n        /**\n         * [config] WebRTC configuration\n         * @type {RTCConfiguration}\n         */\n        this.pcConfig = {\n            bundlePolicy: 'max-bundle',\n            iceServers: [{urls: ['stun:stun.cloudflare.com:3478', 'stun:stun.l.google.com:19302']}],\n            sdpSemantics: 'unified-plan',  // important for Chromecast 1\n        };\n\n        /**\n         * [info] WebSocket connection state. Values: CONNECTING, OPEN, CLOSED\n         * @type {number}\n         */\n        this.wsState = WebSocket.CLOSED;\n\n        /**\n         * [info] WebRTC connection state.\n         * @type {number}\n         */\n        this.pcState = WebSocket.CLOSED;\n\n        /**\n         * @type {HTMLVideoElement}\n         */\n        this.video = null;\n\n        /**\n         * @type {WebSocket}\n         */\n        this.ws = null;\n\n        /**\n         * @type {string|URL}\n         */\n        this.wsURL = '';\n\n        /**\n         * @type {RTCPeerConnection}\n         */\n        this.pc = null;\n\n        /**\n         * @type {number}\n         */\n        this.connectTS = 0;\n\n        /**\n         * @type {string}\n         */\n        this.mseCodecs = '';\n\n        /**\n         * [internal] Disconnect TimeoutID.\n         * @type {number}\n         */\n        this.disconnectTID = 0;\n\n        /**\n         * [internal] Reconnect TimeoutID.\n         * @type {number}\n         */\n        this.reconnectTID = 0;\n\n        /**\n         * [internal] Handler for receiving Binary from WebSocket.\n         * @type {Function}\n         */\n        this.ondata = null;\n\n        /**\n         * [internal] Handlers list for receiving JSON from WebSocket.\n         * @type {Object.<string,Function>}\n         */\n        this.onmessage = null;\n    }\n\n    /**\n     * Set video source (WebSocket URL). Support relative path.\n     * @param {string|URL} value\n     */\n    set src(value) {\n        if (typeof value !== 'string') value = value.toString();\n        if (value.startsWith('http')) {\n            value = 'ws' + value.substring(4);\n        } else if (value.startsWith('/')) {\n            value = 'ws' + location.origin.substring(4) + value;\n        }\n\n        this.wsURL = value;\n\n        this.onconnect();\n    }\n\n    /**\n     * Play video. Support automute when autoplay blocked.\n     * https://developer.chrome.com/blog/autoplay/\n     */\n    play() {\n        this.video.play().catch(() => {\n            if (!this.video.muted) {\n                this.video.muted = true;\n                this.video.play().catch(er => {\n                    console.warn(er);\n                });\n            }\n        });\n    }\n\n    /**\n     * Send message to server via WebSocket\n     * @param {Object} value\n     */\n    send(value) {\n        if (this.ws) this.ws.send(JSON.stringify(value));\n    }\n\n    /** @param {Function} isSupported */\n    codecs(isSupported) {\n        return this.CODECS\n            .filter(codec => this.media.includes(codec.includes('vc1') ? 'video' : 'audio'))\n            .filter(codec => isSupported(`video/mp4; codecs=\"${codec}\"`)).join();\n    }\n\n    /**\n     * `CustomElement`. Invoked each time the custom element is appended into a\n     * document-connected element.\n     */\n    connectedCallback() {\n        if (this.disconnectTID) {\n            clearTimeout(this.disconnectTID);\n            this.disconnectTID = 0;\n        }\n\n        // because video autopause on disconnected from DOM\n        if (this.video) {\n            const seek = this.video.seekable;\n            if (seek.length > 0) {\n                this.video.currentTime = seek.end(seek.length - 1);\n            }\n            this.play();\n        } else {\n            this.oninit();\n        }\n\n        this.onconnect();\n    }\n\n    /**\n     * `CustomElement`. Invoked each time the custom element is disconnected from the\n     * document's DOM.\n     */\n    disconnectedCallback() {\n        if (this.background || this.disconnectTID) return;\n        if (this.wsState === WebSocket.CLOSED && this.pcState === WebSocket.CLOSED) return;\n\n        this.disconnectTID = setTimeout(() => {\n            if (this.reconnectTID) {\n                clearTimeout(this.reconnectTID);\n                this.reconnectTID = 0;\n            }\n\n            this.disconnectTID = 0;\n\n            this.ondisconnect();\n        }, this.DISCONNECT_TIMEOUT);\n    }\n\n    /**\n     * Creates child DOM elements. Called automatically once on `connectedCallback`.\n     */\n    oninit() {\n        this.video = document.createElement('video');\n        this.video.controls = true;\n        this.video.playsInline = true;\n        this.video.preload = 'auto';\n\n        this.video.style.display = 'block'; // fix bottom margin 4px\n        this.video.style.width = '100%';\n        this.video.style.height = '100%';\n\n        this.appendChild(this.video);\n\n        this.video.addEventListener('error', ev => {\n            const err = this.video.error;\n            // https://developer.mozilla.org/en-US/docs/Web/API/MediaError/code\n            const MEDIA_ERRORS = {\n                1: 'MEDIA_ERR_ABORTED',\n                2: 'MEDIA_ERR_NETWORK',\n                3: 'MEDIA_ERR_DECODE',\n                4: 'MEDIA_ERR_SRC_NOT_SUPPORTED'\n            };\n            console.error('[VideoRTC] Video error:', {\n                error: err ? MEDIA_ERRORS[err.code] : 'unknown',\n                message: err ? err.message : 'unknown',\n                codecs: this.mseCodecs || 'not set',\n                readyState: this.video.readyState,\n                networkState: this.video.networkState,\n                currentTime: this.video.currentTime\n            });\n            if (this.ws) this.ws.close(); // run reconnect for broken MSE stream\n        });\n\n        // all Safari lies about supported audio codecs\n        const m = window.navigator.userAgent.match(/Version\\/(\\d+).+Safari/);\n        if (m) {\n            // AAC from v13, FLAC from v14, OPUS - unsupported\n            const skip = m[1] < '13' ? 'mp4a.40.2' : m[1] < '14' ? 'flac' : 'opus';\n            this.CODECS.splice(this.CODECS.indexOf(skip));\n        }\n\n        if (this.background) return;\n\n        if ('hidden' in document && this.visibilityCheck) {\n            document.addEventListener('visibilitychange', () => {\n                if (document.hidden) {\n                    this.disconnectedCallback();\n                } else if (this.isConnected) {\n                    this.connectedCallback();\n                }\n            });\n        }\n\n        if ('IntersectionObserver' in window && this.visibilityThreshold) {\n            const observer = new IntersectionObserver(entries => {\n                entries.forEach(entry => {\n                    if (!entry.isIntersecting) {\n                        this.disconnectedCallback();\n                    } else if (this.isConnected) {\n                        this.connectedCallback();\n                    }\n                });\n            }, {threshold: this.visibilityThreshold});\n            observer.observe(this);\n        }\n    }\n\n    /**\n     * Connect to WebSocket. Called automatically on `connectedCallback`.\n     * @return {boolean} true if the connection has started.\n     */\n    onconnect() {\n        if (!this.isConnected || !this.wsURL || this.ws || this.pc) return false;\n\n        // CLOSED or CONNECTING => CONNECTING\n        this.wsState = WebSocket.CONNECTING;\n\n        this.connectTS = Date.now();\n\n        this.ws = new WebSocket(this.wsURL);\n        this.ws.binaryType = 'arraybuffer';\n        this.ws.addEventListener('open', () => this.onopen());\n        this.ws.addEventListener('close', () => this.onclose());\n\n        return true;\n    }\n\n    ondisconnect() {\n        this.wsState = WebSocket.CLOSED;\n        if (this.ws) {\n            this.ws.close();\n            this.ws = null;\n        }\n\n        this.pcState = WebSocket.CLOSED;\n        if (this.pc) {\n            this.pc.getSenders().forEach(sender => {\n                if (sender.track) sender.track.stop();\n            });\n            this.pc.close();\n            this.pc = null;\n        }\n\n        this.video.src = '';\n        this.video.srcObject = null;\n    }\n\n    /**\n     * @returns {Array.<string>} of modes (mse, webrtc, etc.)\n     */\n    onopen() {\n        // CONNECTING => OPEN\n        this.wsState = WebSocket.OPEN;\n\n        this.ws.addEventListener('message', ev => {\n            if (typeof ev.data === 'string') {\n                const msg = JSON.parse(ev.data);\n                for (const mode in this.onmessage) {\n                    this.onmessage[mode](msg);\n                }\n            } else {\n                this.ondata(ev.data);\n            }\n        });\n\n        this.ondata = null;\n        this.onmessage = {};\n\n        const modes = [];\n\n        if (this.mode.includes('mse') && ('MediaSource' in window || 'ManagedMediaSource' in window)) {\n            modes.push('mse');\n            this.onmse();\n        } else if (this.mode.includes('hls') && this.video.canPlayType('application/vnd.apple.mpegurl')) {\n            modes.push('hls');\n            this.onhls();\n        } else if (this.mode.includes('mp4')) {\n            modes.push('mp4');\n            this.onmp4();\n        }\n\n        if (this.mode.includes('webrtc') && 'RTCPeerConnection' in window) {\n            modes.push('webrtc');\n            this.onwebrtc();\n        }\n\n        if (this.mode.includes('mjpeg')) {\n            if (modes.length) {\n                this.onmessage['mjpeg'] = msg => {\n                    if (msg.type !== 'error' || msg.value.indexOf(modes[0]) !== 0) return;\n                    this.onmjpeg();\n                };\n            } else {\n                modes.push('mjpeg');\n                this.onmjpeg();\n            }\n        }\n\n        return modes;\n    }\n\n    /**\n     * @return {boolean} true if reconnection has started.\n     */\n    onclose() {\n        if (this.wsState === WebSocket.CLOSED) return false;\n\n        // CONNECTING, OPEN => CONNECTING\n        this.wsState = WebSocket.CONNECTING;\n        this.ws = null;\n\n        // reconnect no more than once every X seconds\n        const delay = Math.max(this.RECONNECT_TIMEOUT - (Date.now() - this.connectTS), 0);\n\n        this.reconnectTID = setTimeout(() => {\n            this.reconnectTID = 0;\n            this.onconnect();\n        }, delay);\n\n        return true;\n    }\n\n    onmse() {\n        /** @type {MediaSource} */\n        let ms;\n\n        if ('ManagedMediaSource' in window) {\n            const MediaSource = window.ManagedMediaSource;\n\n            ms = new MediaSource();\n            ms.addEventListener('sourceopen', () => {\n                this.send({type: 'mse', value: this.codecs(MediaSource.isTypeSupported)});\n            }, {once: true});\n\n            this.video.disableRemotePlayback = true;\n            this.video.srcObject = ms;\n        } else {\n            ms = new MediaSource();\n            ms.addEventListener('sourceopen', () => {\n                URL.revokeObjectURL(this.video.src);\n                this.send({type: 'mse', value: this.codecs(MediaSource.isTypeSupported)});\n            }, {once: true});\n\n            this.video.src = URL.createObjectURL(ms);\n            this.video.srcObject = null;\n        }\n\n        this.play();\n\n        this.mseCodecs = '';\n\n        this.onmessage['mse'] = msg => {\n            if (msg.type !== 'mse') return;\n\n            this.mseCodecs = msg.value;\n\n            const sb = ms.addSourceBuffer(msg.value);\n            sb.mode = 'segments'; // segments or sequence\n            sb.addEventListener('updateend', () => {\n                if (!sb.updating && bufLen > 0) {\n                    try {\n                        const data = buf.slice(0, bufLen);\n                        sb.appendBuffer(data);\n                        bufLen = 0;\n                    } catch (e) {\n                        // console.debug(e);\n                    }\n                }\n\n                if (!sb.updating && sb.buffered && sb.buffered.length) {\n                    const end = sb.buffered.end(sb.buffered.length - 1);\n                    const start = end - 5;\n                    const start0 = sb.buffered.start(0);\n                    if (start > start0) {\n                        sb.remove(start0, start);\n                        ms.setLiveSeekableRange(start, end);\n                    }\n                    if (this.video.currentTime < start) {\n                        this.video.currentTime = start;\n                    }\n                    const gap = end - this.video.currentTime;\n                    this.video.playbackRate = gap > 0.1 ? gap : 0.1;\n                    // console.debug('VideoRTC.buffered', gap, this.video.playbackRate, this.video.readyState);\n                }\n            });\n\n            const buf = new Uint8Array(2 * 1024 * 1024);\n            let bufLen = 0;\n\n            this.ondata = data => {\n                if (sb.updating || bufLen > 0) {\n                    const b = new Uint8Array(data);\n                    buf.set(b, bufLen);\n                    bufLen += b.byteLength;\n                    // console.debug('VideoRTC.buffer', b.byteLength, bufLen);\n                } else {\n                    try {\n                        sb.appendBuffer(data);\n                    } catch (e) {\n                        // console.debug(e);\n                    }\n                }\n            };\n        };\n    }\n\n    onwebrtc() {\n        const pc = new RTCPeerConnection(this.pcConfig);\n\n        pc.addEventListener('icecandidate', ev => {\n            if (ev.candidate && this.mode.includes('webrtc/tcp') && ev.candidate.protocol === 'udp') return;\n\n            const candidate = ev.candidate ? ev.candidate.toJSON().candidate : '';\n            this.send({type: 'webrtc/candidate', value: candidate});\n        });\n\n        pc.addEventListener('connectionstatechange', () => {\n            if (pc.connectionState === 'connected') {\n                const tracks = pc.getTransceivers()\n                    .filter(tr => tr.currentDirection === 'recvonly') // skip inactive\n                    .map(tr => tr.receiver.track);\n                /** @type {HTMLVideoElement} */\n                const video2 = document.createElement('video');\n                video2.addEventListener('loadeddata', () => this.onpcvideo(video2), {once: true});\n                video2.srcObject = new MediaStream(tracks);\n            } else if (pc.connectionState === 'failed' || pc.connectionState === 'disconnected') {\n                pc.close(); // stop next events\n\n                this.pcState = WebSocket.CLOSED;\n                this.pc = null;\n\n                this.onconnect();\n            }\n        });\n\n        this.onmessage['webrtc'] = msg => {\n            switch (msg.type) {\n                case 'webrtc/candidate':\n                    if (this.mode.includes('webrtc/tcp') && msg.value.includes(' udp ')) return;\n\n                    pc.addIceCandidate({candidate: msg.value, sdpMid: '0'}).catch(er => {\n                        console.warn(er);\n                    });\n                    break;\n                case 'webrtc/answer':\n                    pc.setRemoteDescription({type: 'answer', sdp: msg.value}).catch(er => {\n                        console.warn(er);\n                    });\n                    break;\n                case 'error':\n                    if (!msg.value.includes('webrtc/offer')) return;\n                    pc.close();\n            }\n        };\n\n        this.createOffer(pc).then(offer => {\n            this.send({type: 'webrtc/offer', value: offer.sdp});\n        });\n\n        this.pcState = WebSocket.CONNECTING;\n        this.pc = pc;\n    }\n\n    /**\n     * @param pc {RTCPeerConnection}\n     * @return {Promise<RTCSessionDescriptionInit>}\n     */\n    async createOffer(pc) {\n        try {\n            if (this.media.includes('microphone')) {\n                const media = await navigator.mediaDevices.getUserMedia({audio: true});\n                media.getTracks().forEach(track => {\n                    pc.addTransceiver(track, {direction: 'sendonly'});\n                });\n            }\n        } catch (e) {\n            console.warn(e);\n        }\n\n        for (const kind of ['video', 'audio']) {\n            if (this.media.includes(kind)) {\n                pc.addTransceiver(kind, {direction: 'recvonly'});\n            }\n        }\n\n        const offer = await pc.createOffer();\n        await pc.setLocalDescription(offer);\n        return offer;\n    }\n\n    /**\n     * @param video2 {HTMLVideoElement}\n     */\n    onpcvideo(video2) {\n        if (this.pc) {\n            // Video+Audio > Video, H265 > H264, Video > Audio, WebRTC > MSE\n            let rtcPriority = 0, msePriority = 0;\n\n            /** @type {MediaStream} */\n            const stream = video2.srcObject;\n            if (stream.getVideoTracks().length > 0) {\n                // not the best, but a pretty simple way to check a codec\n                const isH265Supported =  this.pc.remoteDescription.sdp.includes('H265/90000');\n                rtcPriority += isH265Supported ? 0x240 : 0x220;\n            }\n            if (stream.getAudioTracks().length > 0) rtcPriority += 0x102;\n\n            if (this.mseCodecs.includes('hvc1.')) msePriority += 0x230;\n            if (this.mseCodecs.includes('avc1.')) msePriority += 0x210;\n            if (this.mseCodecs.includes('mp4a.')) msePriority += 0x101;\n\n            if (rtcPriority >= msePriority) {\n                this.video.srcObject = stream;\n                this.play();\n\n                this.pcState = WebSocket.OPEN;\n\n                this.wsState = WebSocket.CLOSED;\n                if (this.ws) {\n                    this.ws.close();\n                    this.ws = null;\n                }\n            } else {\n                this.pcState = WebSocket.CLOSED;\n                if (this.pc) {\n                    this.pc.close();\n                    this.pc = null;\n                }\n            }\n        }\n\n        video2.srcObject = null;\n    }\n\n    onmjpeg() {\n        this.ondata = data => {\n            this.video.controls = false;\n            this.video.poster = 'data:image/jpeg;base64,' + VideoRTC.btoa(data);\n        };\n\n        this.send({type: 'mjpeg'});\n    }\n\n    onhls() {\n        this.onmessage['hls'] = msg => {\n            if (msg.type !== 'hls') return;\n\n            const url = 'http' + this.wsURL.substring(2, this.wsURL.indexOf('/ws')) + '/hls/';\n            const playlist = msg.value.replace('hls/', url);\n            this.video.src = 'data:application/vnd.apple.mpegurl;base64,' + btoa(playlist);\n            this.play();\n        };\n\n        this.send({type: 'hls', value: this.codecs(type => this.video.canPlayType(type))});\n    }\n\n    onmp4() {\n        /** @type {HTMLCanvasElement} **/\n        const canvas = document.createElement('canvas');\n        /** @type {CanvasRenderingContext2D} */\n        let context;\n\n        /** @type {HTMLVideoElement} */\n        const video2 = document.createElement('video');\n        video2.autoplay = true;\n        video2.playsInline = true;\n        video2.muted = true;\n\n        video2.addEventListener('loadeddata', () => {\n            if (!context) {\n                canvas.width = video2.videoWidth;\n                canvas.height = video2.videoHeight;\n                context = canvas.getContext('2d');\n            }\n\n            context.drawImage(video2, 0, 0, canvas.width, canvas.height);\n\n            this.video.controls = false;\n            this.video.poster = canvas.toDataURL('image/jpeg');\n        });\n\n        this.ondata = data => {\n            video2.src = 'data:video/mp4;base64,' + VideoRTC.btoa(data);\n        };\n\n        this.send({type: 'mp4', value: this.codecs(this.video.canPlayType)});\n    }\n\n    static btoa(buffer) {\n        const bytes = new Uint8Array(buffer);\n        const len = bytes.byteLength;\n        let binary = '';\n        for (let i = 0; i < len; i++) {\n            binary += String.fromCharCode(bytes[i]);\n        }\n        return window.btoa(binary);\n    }\n}\n"
  },
  {
    "path": "www/video-stream.js",
    "content": "import {VideoRTC} from './video-rtc.js';\n\n/**\n * This is example, how you can extend VideoRTC player for your app.\n * Also you can check this example: https://github.com/AlexxIT/WebRTC\n */\nclass VideoStream extends VideoRTC {\n    set divMode(value) {\n        this.querySelector('.mode').innerText = value;\n        this.querySelector('.status').innerText = '';\n    }\n\n    set divError(value) {\n        const state = this.querySelector('.mode').innerText;\n        if (state !== 'loading') return;\n        this.querySelector('.mode').innerText = 'error';\n        this.querySelector('.status').innerText = value;\n    }\n\n    /**\n     * Custom GUI\n     */\n    oninit() {\n        console.debug('stream.oninit');\n        super.oninit();\n\n        this.innerHTML = `\n        <style>\n        video-stream {\n            position: relative;\n        }\n        .info {\n            position: absolute;\n            top: 0;\n            left: 0;\n            right: 0;\n            padding: 12px;\n            color: white;\n            display: flex;\n            justify-content: space-between;\n            pointer-events: none;\n        }\n        </style>\n        <div class=\"info\">\n            <div class=\"status\"></div>\n            <div class=\"mode\"></div>\n        </div>\n        `;\n\n        const info = this.querySelector('.info');\n        this.insertBefore(this.video, info);\n    }\n\n    onconnect() {\n        console.debug('stream.onconnect');\n        const result = super.onconnect();\n        if (result) this.divMode = 'loading';\n        return result;\n    }\n\n    ondisconnect() {\n        console.debug('stream.ondisconnect');\n        super.ondisconnect();\n    }\n\n    onopen() {\n        console.debug('stream.onopen');\n        const result = super.onopen();\n\n        this.onmessage['stream'] = msg => {\n            console.debug('stream.onmessge', msg);\n            switch (msg.type) {\n                case 'error':\n                    this.divError = msg.value;\n                    break;\n                case 'mse':\n                case 'hls':\n                case 'mp4':\n                case 'mjpeg':\n                    this.divMode = msg.type.toUpperCase();\n                    break;\n            }\n        };\n\n        return result;\n    }\n\n    onclose() {\n        console.debug('stream.onclose');\n        return super.onclose();\n    }\n\n    onpcvideo(ev) {\n        console.debug('stream.onpcvideo');\n        super.onpcvideo(ev);\n\n        if (this.pcState !== WebSocket.CLOSED) {\n            this.divMode = 'RTC';\n        }\n    }\n}\n\ncustomElements.define('video-stream', VideoStream);\n"
  },
  {
    "path": "www/webrtc-sync.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <title>webrtc - go2rtc</title>\n    <style>\n        body {\n            background-color: black;\n            margin: 0;\n            padding: 0;\n        }\n\n        html, body, video {\n            height: 100%;\n            width: 100%;\n        }\n    </style>\n</head>\n<body>\n<video id=\"video\" autoplay controls playsinline muted></video>\n<script>\n    async function PeerConnection(media) {\n        const pc = new RTCPeerConnection({\n            iceServers: [{urls: ['stun:stun.cloudflare.com:3478', 'stun:stun.l.google.com:19302']}]\n        });\n\n        document.getElementById('video').srcObject = new MediaStream([\n            pc.addTransceiver('audio', {direction: 'sendrecv'}).receiver.track,\n            pc.addTransceiver('video', {direction: 'sendrecv'}).receiver.track,\n        ]);\n\n        const tracks = await navigator.mediaDevices.getUserMedia({\n            video: media.indexOf('camera') >= 0,\n            audio: media.indexOf('microphone') >= 0,\n        });\n        tracks.getTracks().forEach(track => {\n            pc.addTrack(track);\n        });\n\n        return pc;\n    }\n\n    function getCompleteOffer(pc, timeout) {\n        return new Promise((resolve, reject) => {\n            pc.addEventListener('icegatheringstatechange', () => {\n                if (pc.iceGatheringState === 'complete') resolve(pc.localDescription.sdp);\n            });\n\n            pc.createOffer().then(offer => pc.setLocalDescription(offer));\n\n            setTimeout(() => resolve(pc.localDescription.sdp), timeout || 3000);\n        });\n    }\n\n    async function connect() {\n        const media = new URLSearchParams(location.search).get('media');\n        const pc = await PeerConnection(media);\n        const url = new URL('api/webrtc' + location.search, location.href);\n        const r = await fetch(url, {method: 'POST', body: await getCompleteOffer(pc)});\n        await pc.setRemoteDescription({type: 'answer', sdp: await r.text()});\n    }\n\n    connect();\n</script>\n</body>\n</html>"
  },
  {
    "path": "www/webrtc.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <title>webrtc - go2rtc</title>\n    <style>\n        body {\n            background-color: black;\n            margin: 0;\n            padding: 0;\n        }\n\n        html, body, video {\n            height: 100%;\n            width: 100%;\n        }\n    </style>\n</head>\n<body>\n<video id=\"video\" autoplay controls playsinline muted></video>\n<script>\n    async function PeerConnection(media) {\n        const pc = new RTCPeerConnection({\n            iceServers: [{urls: ['stun:stun.cloudflare.com:3478', 'stun:stun.l.google.com:19302']}]\n        });\n\n        const localTracks = [];\n\n        if (/camera|microphone/.test(media)) {\n            const tracks = await getMediaTracks('user', {\n                video: media.indexOf('camera') >= 0,\n                audio: media.indexOf('microphone') >= 0,\n            });\n            tracks.forEach(track => {\n                pc.addTransceiver(track, {direction: 'sendonly'});\n                if (track.kind === 'video') localTracks.push(track);\n            });\n        }\n\n        if (media.indexOf('display') >= 0) {\n            const tracks = await getMediaTracks('display', {\n                video: true,\n                audio: media.indexOf('speaker') >= 0,\n            });\n            tracks.forEach(track => {\n                pc.addTransceiver(track, {direction: 'sendonly'});\n                if (track.kind === 'video') localTracks.push(track);\n            });\n        }\n\n        if (/video|audio/.test(media)) {\n            const tracks = ['video', 'audio']\n                .filter(kind => media.indexOf(kind) >= 0)\n                .map(kind => pc.addTransceiver(kind, {direction: 'recvonly'}).receiver.track);\n            localTracks.push(...tracks);\n        }\n\n        document.getElementById('video').srcObject = new MediaStream(localTracks);\n\n        return pc;\n    }\n\n    async function getMediaTracks(media, constraints) {\n        try {\n            const stream = media === 'user'\n                ? await navigator.mediaDevices.getUserMedia(constraints)\n                : await navigator.mediaDevices.getDisplayMedia(constraints);\n            return stream.getTracks();\n        } catch (e) {\n            console.warn(e);\n            return [];\n        }\n    }\n\n    async function connect(media) {\n        const pc = await PeerConnection(media);\n        const url = new URL('api/ws' + location.search, location.href);\n        const ws = new WebSocket('ws' + url.toString().substring(4));\n\n        ws.addEventListener('open', () => {\n            pc.addEventListener('icecandidate', ev => {\n                if (!ev.candidate) return;\n                const msg = {type: 'webrtc/candidate', value: ev.candidate.candidate};\n                ws.send(JSON.stringify(msg));\n            });\n\n            pc.createOffer().then(offer => pc.setLocalDescription(offer)).then(() => {\n                const msg = {type: 'webrtc/offer', value: pc.localDescription.sdp};\n                ws.send(JSON.stringify(msg));\n            });\n        });\n\n        ws.addEventListener('message', ev => {\n            const msg = JSON.parse(ev.data);\n            if (msg.type === 'webrtc/candidate') {\n                pc.addIceCandidate({candidate: msg.value, sdpMid: '0'});\n            } else if (msg.type === 'webrtc/answer') {\n                pc.setRemoteDescription({type: 'answer', sdp: msg.value});\n            }\n        });\n    }\n\n    const media = new URLSearchParams(location.search).get('media');\n    connect(media || 'video+audio');\n</script>\n</body>\n</html>"
  }
]