├── .do ├── app.yaml └── deploy.template.yaml ├── .gitattributes ├── .github └── workflows │ ├── linter-release.yml │ └── release.yml ├── .gitignore ├── .gitmodules ├── .vscode └── launch.json ├── Caddyfile ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── assets └── create.idle.screen.h264.sh ├── cmd └── main.go ├── docs ├── CNAME ├── LICENSE.txt ├── README.md ├── README │ └── index.html ├── assets │ ├── css │ │ └── autumn.css │ ├── fuckingstyle.css │ └── fuckingstyle.css.map ├── brutal-cam.gemspec ├── codeblocks.md ├── codeblocks │ └── index.html └── index.html ├── embed.go ├── ftlserver └── ftlserver.go ├── go.mod ├── go.sum ├── h264.sdp ├── html ├── deadsfu.css ├── deadsfu.js ├── favicon.svg ├── help │ ├── index.html │ └── readme.md ├── index.html └── send │ └── index.html ├── internal ├── disrupt │ ├── disrupt.go │ ├── disrupt_test.go │ ├── norace.go │ └── race.go ├── newpeerconn │ ├── ducksoup.go │ ├── ion-mediaengine.go │ └── pion.go └── sfu │ ├── basic_test.go │ ├── broker_test.go │ ├── ddnsutil.go │ ├── dnslego.go │ ├── flags.go │ ├── galene.go │ ├── https.go │ ├── sfu.go │ └── xbroker.go ├── logotitle-text.svg ├── logotitle.svg ├── modd.conf ├── notes ├── design-diary.md ├── dns challenge decision logic.pseudo └── url-guide.md ├── scripts └── gen.go ├── test ├── alloc_test.go └── perf_test.go └── website ├── .gitignore ├── Gemfile ├── INDEX.md ├── LICENSE.txt ├── README.md ├── _config.yml ├── _layouts └── default.html ├── assets ├── css │ └── autumn.css └── fuckingstyle.scss ├── brutal-cam.gemspec └── codeblocks.md /.do/app.yaml: -------------------------------------------------------------------------------- 1 | alerts: 2 | - rule: DEPLOYMENT_FAILED 3 | - rule: DOMAIN_FAILED 4 | name: deadsfu 5 | region: nyc 6 | services: 7 | - http_port: 8080 8 | image: 9 | registry: x186k 10 | registry_type: DOCKER_HUB 11 | repository: deadsfu 12 | tag: latest 13 | instance_count: 1 14 | instance_size_slug: basic-xs 15 | name: x-186-k-deadsfu 16 | routes: 17 | - path: / 18 | source_dir: / 19 | -------------------------------------------------------------------------------- /.do/deploy.template.yaml: -------------------------------------------------------------------------------- 1 | spec: 2 | name: deadsfu 3 | services: 4 | - name: deadsfu 5 | git: 6 | branch: main 7 | repo_clone_url: https://github.com/x186k/deadsfu.git -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/x186k/deadsfu/2d9e20f43fc58a6dc6cb449fe8f2560868e93933/.gitattributes -------------------------------------------------------------------------------- /.github/workflows/linter-release.yml: -------------------------------------------------------------------------------- 1 | name: Linter Release 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | linting: 9 | name: Run the linter 10 | # The type of runner that the job will run on 11 | runs-on: ubuntu-20.04 12 | 13 | # Steps represent a sequence of tasks that will be executed as part of the job 14 | steps: 15 | # Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it 16 | - uses: actions/checkout@v2 17 | # dont need lfs assets for linting 18 | # with: 19 | # lfs: true 20 | - uses: actions/setup-go@v2 21 | with: 22 | go-version: "^1.16.5" # The Go version to download (if necessary) and use. 23 | - run: go version 24 | 25 | - name: golangci-lint 26 | uses: golangci/golangci-lint-action@v2 27 | with: 28 | # version: v2.5.2 29 | skip-go-installation: true 30 | args: --skip-files pion.go 31 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: deadsfu 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-20.04 10 | steps: 11 | - name: Login to DockerHub so makefile push will work 12 | if: github.event_name != 'pull_request' 13 | uses: docker/login-action@v1.8.0 14 | with: 15 | username: ${{ secrets.DOCKERHUB_USERNAME }} 16 | password: ${{ secrets.DOCKERHUB_TOKEN }} 17 | - name: checkout repo 18 | uses: actions/checkout@v2 19 | with: 20 | submodules: 'true' 21 | - name: upgrade go 22 | uses: actions/setup-go@v2 23 | with: 24 | go-version: '^1.17.1' # The Go version to download (if necessary) and use. 25 | - name: build application 26 | run: echo '${{ secrets.GITHUB_TOKEN }}' | gh auth login -h github.com --with-token 27 | - name: build application 28 | run: make release VER=${{ github.event.release.tag_name }} 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | core 2 | workspace.code-workspace 3 | deadsfu 4 | embed 5 | 6 | # do not track intermediate files used for making idle screens 7 | *.y4m 8 | moz.log 9 | rtp.pcap 10 | website/site 11 | cover.out 12 | 13 | 14 | deadsfu-out.pcap 15 | *.pprof 16 | dist/ 17 | memprofile.out 18 | profile.out 19 | deadsfu.test 20 | 21 | # keep .vscode/settings.json ignored, 22 | # but include launch.json for debugging purposes 23 | .vscode/* 24 | !.vscode/launch.json 25 | 26 | .idea/* 27 | __debug_bin 28 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deadsfu-binaries"] 2 | path = deadsfu-binaries 3 | url = https://github.com/x186k/deadsfu-binaries 4 | [submodule "html/getstats-shipper"] 5 | path = html/getstats-shipper 6 | url = https://github.com/x186k/getstats-shipper 7 | [submodule "html/whip-whap-js"] 8 | path = html/whip-whap-js 9 | url = https://github.com/x186k/whip-whap-js 10 | [submodule "html/dead-down"] 11 | path = html/dead-down 12 | url = http://github.com/cameronelliott/dead-down 13 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | 8 | { 9 | "name": "debug it", 10 | "type": "go", 11 | "request": "launch", 12 | "mode": "debug", 13 | "program": "${workspaceFolder}/cmd/main.go", 14 | "args": ["--http",":80","--html","../html"], 15 | "env": {}, 16 | "showLog": true, 17 | } 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /Caddyfile: -------------------------------------------------------------------------------- 1 | https://hjarta.duckdns.org:443 2 | 3 | tls { 4 | dns duckdns ${DUCKDNS} 5 | } 6 | 7 | #reverse_proxy /pub localhost:8080 8 | reverse_proxy * localhost:8080 9 | 10 | #file_server 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | FROM golang:1.17.1 as builder 3 | WORKDIR /app 4 | COPY . . 5 | ARG TARGETOS 6 | ARG TARGETARCH 7 | ARG VERSION 8 | RUN CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} go build -a -o main -ldflags "-X main.Version=${VERSION}" . 9 | 10 | 11 | FROM alpine:3.14.0 12 | RUN apk --no-cache add ca-certificates 13 | COPY --from=0 /app/main /app/main 14 | EXPOSE 8080 8084 15 | ENTRYPOINT ["/app/main"] 16 | 17 | 18 | # https://docs.docker.com/develop/develop-images/multistage-build/ 19 | # syntax=docker/dockerfile:1 20 | # FROM golang:1.16 21 | # WORKDIR /go/src/github.com/alexellis/href-counter/ 22 | # RUN go get -d -v golang.org/x/net/html 23 | # COPY app.go ./ 24 | # RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . 25 | 26 | # FROM alpine:latest 27 | # RUN apk --no-cache add ca-certificates 28 | # WORKDIR /root/ 29 | # COPY --from=0 /go/src/github.com/alexellis/href-counter/app ./ 30 | # CMD ["./app"] -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 x186k developers 4 | Copyright (c) 2021 Cameron Elliott http://cameronelliott.com 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | # VER := $(shell git describe --abbrev=0 --tags) 5 | 6 | # lets just skip building a proper dependency tree 7 | # thus, always build 8 | 9 | 10 | 11 | 12 | # tag examples 13 | # git tag -a v1.4 -m "my version 1.4" 14 | 15 | all: 16 | 17 | 18 | release: dockerbuild binbuild 19 | 20 | 21 | # implies push 22 | dockerbuild: 23 | test "$$(git describe --tags)" = "$(VER)" 24 | # if no tag specified, it defaults to latest:, SO DONT ADD :$(VER) 25 | docker build -t deadsfu --build-arg VERSION=$(VER) . 26 | docker image tag deadsfu x186k/deadsfu:latest 27 | docker image tag deadsfu x186k/deadsfu:$(VER) 28 | docker image push --all-tags x186k/deadsfu 29 | 30 | 31 | 32 | PLATFORMS := linux/amd64 windows/amd64 darwin/amd64 darwin/arm64 linux/arm64 33 | 34 | temp = $(subst /, ,$@) 35 | os = $(word 1, $(temp)) 36 | arch = $(word 2, $(temp)) 37 | ext = $(if $(findstring windows,$(os)),.exe) 38 | 39 | binname = deadsfu$(ext) 40 | bintarname = dist/deadsfu-$(os)-$(arch).tar.gz 41 | goflags = -ldflags "-X main.Version=$(VER)" 42 | 43 | binbuild: cleardist $(PLATFORMS) 44 | test "$$(git describe --tags)" = "$(VER)" 45 | gh release upload $(VER) --clobber ./dist/* 46 | 47 | cleardist: 48 | rm -rf dist 49 | mkdir dist 50 | 51 | $(PLATFORMS): 52 | GOOS=$(os) GOARCH=$(arch) go build -o $(binname) $(goflags) . 53 | tar -czf $(bintarname) $(binname) 54 | rm $(binname) 55 | openssl md5 -r $(bintarname) | sed 's/ .*//g' >$(bintarname).md5 56 | 57 | .PHONY: release $(PLATFORMS) 58 | 59 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | ## Dead-simple Scalable WebRTC Broadcasting 9 | 10 | 11 | **DeadSFU**: dead-simple broadcasting and video transmission. 12 | 13 | 14 | 15 | 16 | HTML tutorial 17 | 18 | 19 | 20 | 21 | ## Quick Links 22 | 23 | - [Feature List](#feature-list) 24 | - [Quick Install](#quick-install) 25 | - [Quick Start: OBS / FTL ingress](#quick-start-obs--ftl-ingress) 26 | - [Quick Start: Browser Ingress](#quick-start-browser-ingress) 27 | - [Getting Support](#getting-support) 28 | - [Email Newsletter](#email-newsletter) 29 | - [Contributing](#contributing) 30 | - [Getting Latest Updates](#getting-latest-updates) 31 | - [Compile From Source](#compile-from-source) 32 | - [DeadSFU Thanks](#deadsfu-thanks) 33 | 34 | 41 | 42 | [WHIP]: https://www.ietf.org/archive/id/draft-ietf-wish-whip-00.html 43 | 44 | ## Feature List 45 | 46 | - **Dead-Simple Usage:** Meticulously crafted for ease-of-use, scalability, and performance. 47 | - **Large Scale WebRTC Broadcasting:** SFUs can be easily cascaded to create clusters of hundreds of servers. 48 | - **Cloud Native Docker:** Ready to go Docker images for cloud-native broadcasting clusters. 49 | - **Auto Scaling Compatible:** HTTP signalling is compatible with most cluster-autoscaling methods. 50 | - **OBS Broadcasting:** Send from OBS to DeadSFU for doing WebRTC broadcasting. 51 | - **Browser Viewer:** Browser viewer enables watching broadcasts. 52 | - **Simple Ingress HTTPS Signalling:** [WHIP][WHIP] compatible: Send an Offer-SDP, get an Answer-SDP, and you're publishing! 53 | - **Simple Egress HTTPS Signalling:** WHIP-like: Send an Offer-SDP, get an Answer-SDP, and you're receiving! 54 | - **Designed For Fault Tolerance:** Single-peer-ingress design for practical large-scale fault-tolerant containerized broadcasting. 55 | - **Kubernetes/Docker capable:** Designed for Kubernetes or Swarm broadcasting clusters. 56 | - **HTTP load balancer compatible:** Designed standard HTTP load balancer compatibility on egress. 57 | - **Dead-simple Install:** Use a one-liner curl & untar command to prepare to broadcast. 58 | - **No Runtime Dependencies:** DeadSFU is a single binary that you can run locally or in production with a single command. 59 | 60 | **Don't see a feature on this list?** Check the issue track to see if your feature is there, if not open a new issue. We need your input to make our roadmap, and we'd love to hear from you. 61 | 62 | 63 | ## Quick Install 64 | 65 | Linux Intel/AMD64 66 | ```bash 67 | curl -sL https://github.com/x186k/deadsfu/releases/latest/download/deadsfu-linux-amd64.tar.gz | tar xvz 68 | ``` 69 | Linux ARM64 70 | ```bash 71 | curl -sL https://github.com/x186k/deadsfu/releases/latest/download/deadsfu-linux-arm64.tar.gz | tar xvz 72 | ``` 73 | macOS Intel CPU 74 | ```bash 75 | curl -sL https://github.com/x186k/deadsfu/releases/latest/download/deadsfu-darwin-amd64.tar.gz | tar xvz 76 | ``` 77 | macOS Apple CPU 78 | ```bash 79 | curl -sL https://github.com/x186k/deadsfu/releases/latest/download/deadsfu-darwin-arm64.tar.gz | tar xvz 80 | ``` 81 | Docker Pull 82 | ```bash 83 | docker pull x186k/deadsfu 84 | ``` 85 | Windows 86 | ```bash 87 | curl https://github.com/x186k/deadsfu/releases/latest/download/deadsfu-windows-amd64.zip -sLo tmp && tar -xvf tmp && del tmp 88 | ``` 89 | 90 | ## Quick Start: OBS / FTL ingress 91 | 92 | Remove the --ftl-key args if you are not using OBS/FTL. 93 | 94 | Linux/macOS 95 | ```bash 96 | ./deadsfu --http :8080 --html internal --ftl-key 123-abc 97 | ``` 98 | Windows 99 | ``` 100 | .\\deadsfu --http :8080 --html internal --ftl-key 123-abc 101 | ``` 102 | 103 | Docker Host Networking (recommended) Only Works on Linux 104 | ```bash 105 | docker run --name deadsfu --pull always --network host x186k/deadsfu --http :8080 --html internal --ftl-key 123-abc 106 | ``` 107 | Host networking can ease WebRTC connecting in difficult environments. The SFU can share the true host IP addresses. 108 | 109 | Docker Forwarded Ports (Mac,Win,Linux) 110 | ```bash 111 | docker run --name deadsfu --pull always -p 8080:8080 -p 8084:8084/udp -p 8084:8084/tcp x186k/deadsfu --http :8080 --html internal --ftl-key 123-abc 112 | ``` 113 | 114 | 115 | 116 | 117 | 118 | ## Quick Start: Browser Ingress 119 | 120 | #### Not Yet, file an issue for help 121 | 122 | 125 | 126 | 127 | ## Getting Support 128 | 129 | Author's email is `cameron@cameronelliott.com` 130 | 131 | Slack link: [Slack Invite Link](https://join.slack.com/t/deadsfu/shared_invite/zt-sv23oa10-XFFYoJHPty8BtuCmBthH_A) 132 | 133 | ## Email Newsletter 134 | 135 | [Get the email newletter.](https://docs.google.com/forms/d/e/1FAIpQLSd8rzXabvn73YC_GPRtXZb1zlKPeOEQuHDdVi4m9umJqEaJsA/viewform) 136 | 137 | ## Contributing 138 | 139 | If you have an idea to share, please post it on the [Github discussions](https://github.com/x186k/deadsfu/discussions/categories/ideas) board. 140 | If you have found a bug, please file an issue on [Github issues](https://github.com/x186k/deadsfu/issues) 141 | If you have suggestions or ideas, please submit an issue or create a discussion. Your ideas are wanted! 142 | 143 | ## Getting Latest Updates 144 | 145 | You can update by simply re-running the `curl` and `tar` commands again as in the install directions. 146 | 147 | For Docker, simply re-pull the latest image. 148 | 149 | ## Compile From Source 150 | 151 | The deadsfu repo includes git submodules. 152 | 153 | So, use the `--recursive` flag when checking out. [Git submodules](https://git-scm.com/book/en/v2/Git-Tools-Submodules). 154 | 155 | You need a version of Go greater than 1.16, we recommend 1.17 or later. 156 | 157 | Clone the main repo: 158 | ```bash 159 | git clone --recursive https://github.com/x186k/deadsfu 160 | ``` 161 | Change dir: 162 | ```bash 163 | cd deadsfu 164 | ``` 165 | 166 | Build with Go: 167 | ```bash 168 | go build . 169 | ``` 170 | 171 | ## DeadSFU Thanks 172 | 173 | - [Sean Dubois](https://github.com/Sean-Der) Creator of Pion 174 | - [Luis Orlando](https://github.com/OrlandoCo) Pion help code 175 | - [Juliusz Chroboczek](https://github.com/jech) Pion help and code 176 | - [Matt Holt](https://github.com/mholt) Creator of Caddy 177 | - [Francis Lavoie](https://github.com/francislavoie) Caddy maintainer 178 | - [Alex Williams](https://github.com/llspalex) Louper founder, inspiration. 179 | - [Sayan Bhattacharya](https://github.com/Thunder80) Louper developer. 180 | - [Charles Surett](https://github.com/scj643) Early user. 181 | - [Alex Peder](https://artiflix.com/) Project supporter. 182 | - [Akash](https://github.com/discist) Early user. 183 | 184 | 185 | 186 | 187 | 188 | 189 | -------------------------------------------------------------------------------- /assets/create.idle.screen.h264.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # steps : 4 | # 1. create y4m from png 5 | # 2. start chrome, which you use the y4m as a fake camera with 6 | # 3. start the sfu, and use 'log-packets' which gives text2pcap compatible output 7 | # 4. capture the moz.log file to a pcap 8 | # 5. hand edit the pcap to isolate the IDR including SPS and PPS 9 | # 6. save it inside of x186k-sfu-assets 10 | # 7. commit it 11 | # 8. cd x186k-sfu 12 | # 9. go generate 13 | # 10. go build 14 | 15 | ffmpeg -y -i x186k.idle.screen.1080.png -pix_fmt yuv420p x186k.idle.screen.1080.y4m 16 | 17 | CWD=$(pwd) 18 | 19 | open -a "Google Chrome" --args \ 20 | --disable-gpu \ 21 | --use-fake-device-for-media-stream \ 22 | --use-file-for-fake-video-capture="$CWD/x186k.idle.screen.1080.y4m" 23 | 24 | # then 25 | # run the sfu 26 | # go run . -debug -https-hostname foo.deadsfu.com -log-packets > moz.log 27 | # egrep '(RTP_PACKET)' moz.log | text2pcap -D -n -l 1 -i 17 -u 1234,1235 -t '%H:%M:%S.' - rtp.pcap 28 | # chop away on rtp.pcap with wireshark 29 | # using the wireshark display filter: 'h264.seq_parameter_set_id' can be helpful 30 | # 31 | 32 | 33 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import "github.com/x186k/deadsfu/internal/sfu" 4 | 5 | func init() { 6 | sfu.Init() 7 | } 8 | 9 | func main() { 10 | sfu.Main() 11 | } 12 | -------------------------------------------------------------------------------- /docs/CNAME: -------------------------------------------------------------------------------- 1 | deadsfu.com -------------------------------------------------------------------------------- /docs/LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Brutal Cam Theme 2 | 3 | I'm afraid is what happens when you let a back-end developer design a theme. 4 | 5 | A brutalist, minimalist theme. (I think) 6 | 7 | ## Installation 8 | 9 | You can fork or clone the repo. 10 | If you do a github GUI fork, then clone your fork of the repo, 11 | you can get updates (if any) to this theme by using the Github GUI. 12 | 13 | Basic way to clone and view site: 14 | 15 | git clone https://github.com/cameronelliott/brutal-cam 16 | 17 | cd brutal-cam 18 | 19 | 20 | Server for working with live-reload, open your browser to http://127.0.0.1:4000 21 | ```bash 22 | docker run --rm -it -v $PWD:/srv/jekyll -p 4000:4000 -p 35729:35729 jekyll/builder:latest jekyll serve --livereload 23 | ``` 24 | 25 | Build to _site 26 | ```bash 27 | docker run --rm -it -v $PWD:/srv/jekyll -p 127.0.0.1:4000:4000 jekyll/builder:latest jekyll build 28 | ``` -------------------------------------------------------------------------------- /docs/README/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | DeadSFU 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |

Brutal Cam Theme

20 | 21 |

I’m afraid is what happens when you let a back-end developer design a theme.

22 | 23 |

A brutalist, minimalist theme. (I think)

24 | 25 |

Installation

26 | 27 |

You can fork or clone the repo. 28 | If you do a github GUI fork, then clone your fork of the repo, 29 | you can get updates (if any) to this theme by using the Github GUI.

30 | 31 |

Basic way to clone and view site:

32 | 33 |

git clone https://github.com/cameronelliott/brutal-cam

34 | 35 |

cd brutal-cam

36 | 37 |

Server for working with live-reload, open your browser to http://127.0.0.1:4000

38 |
docker run --rm -it -v $PWD:/srv/jekyll -p 4000:4000 -p 35729:35729 jekyll/builder:latest jekyll serve --livereload
39 | 
40 | 41 |

Build to _site

42 |
docker run --rm -it -v $PWD:/srv/jekyll -p 127.0.0.1:4000:4000 jekyll/builder:latest jekyll build
43 | 
44 | 45 | 46 | 47 | 48 | 49 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /docs/assets/css/autumn.css: -------------------------------------------------------------------------------- 1 | .highlight .hll { background-color: #ffffcc } 2 | .highlight .c { color: #aaaaaa; font-style: italic } /* Comment */ 3 | .highlight .err { color: #F00000; background-color: #F0A0A0 } /* Error */ 4 | .highlight .k { color: #0000aa } /* Keyword */ 5 | .highlight .cm { color: #aaaaaa; font-style: italic } /* Comment.Multiline */ 6 | .highlight .cp { color: #4c8317 } /* Comment.Preproc */ 7 | .highlight .c1 { color: #aaaaaa; font-style: italic } /* Comment.Single */ 8 | .highlight .cs { color: #0000aa; font-style: italic } /* Comment.Special */ 9 | .highlight .gd { color: #aa0000 } /* Generic.Deleted */ 10 | .highlight .ge { font-style: italic } /* Generic.Emph */ 11 | .highlight .gr { color: #aa0000 } /* Generic.Error */ 12 | .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 13 | .highlight .gi { color: #00aa00 } /* Generic.Inserted */ 14 | .highlight .go { color: #888888 } /* Generic.Output */ 15 | .highlight .gp { color: #555555 } /* Generic.Prompt */ 16 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 17 | .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 18 | .highlight .gt { color: #aa0000 } /* Generic.Traceback */ 19 | .highlight .kc { color: #0000aa } /* Keyword.Constant */ 20 | .highlight .kd { color: #0000aa } /* Keyword.Declaration */ 21 | .highlight .kn { color: #0000aa } /* Keyword.Namespace */ 22 | .highlight .kp { color: #0000aa } /* Keyword.Pseudo */ 23 | .highlight .kr { color: #0000aa } /* Keyword.Reserved */ 24 | .highlight .kt { color: #00aaaa } /* Keyword.Type */ 25 | .highlight .m { color: #009999 } /* Literal.Number */ 26 | .highlight .s { color: #aa5500 } /* Literal.String */ 27 | .highlight .na { color: #1e90ff } /* Name.Attribute */ 28 | .highlight .nb { color: #00aaaa } /* Name.Builtin */ 29 | .highlight .nc { color: #00aa00; text-decoration: underline } /* Name.Class */ 30 | .highlight .no { color: #aa0000 } /* Name.Constant */ 31 | .highlight .nd { color: #888888 } /* Name.Decorator */ 32 | .highlight .ni { color: #800000; font-weight: bold } /* Name.Entity */ 33 | .highlight .nf { color: #00aa00 } /* Name.Function */ 34 | .highlight .nn { color: #00aaaa; text-decoration: underline } /* Name.Namespace */ 35 | .highlight .nt { color: #1e90ff; font-weight: bold } /* Name.Tag */ 36 | .highlight .nv { color: #aa0000 } /* Name.Variable */ 37 | .highlight .ow { color: #0000aa } /* Operator.Word */ 38 | .highlight .w { color: #bbbbbb } /* Text.Whitespace */ 39 | .highlight .mf { color: #009999 } /* Literal.Number.Float */ 40 | .highlight .mh { color: #009999 } /* Literal.Number.Hex */ 41 | .highlight .mi { color: #009999 } /* Literal.Number.Integer */ 42 | .highlight .mo { color: #009999 } /* Literal.Number.Oct */ 43 | .highlight .sb { color: #aa5500 } /* Literal.String.Backtick */ 44 | .highlight .sc { color: #aa5500 } /* Literal.String.Char */ 45 | .highlight .sd { color: #aa5500 } /* Literal.String.Doc */ 46 | .highlight .s2 { color: #aa5500 } /* Literal.String.Double */ 47 | .highlight .se { color: #aa5500 } /* Literal.String.Escape */ 48 | .highlight .sh { color: #aa5500 } /* Literal.String.Heredoc */ 49 | .highlight .si { color: #aa5500 } /* Literal.String.Interpol */ 50 | .highlight .sx { color: #aa5500 } /* Literal.String.Other */ 51 | .highlight .sr { color: #009999 } /* Literal.String.Regex */ 52 | .highlight .s1 { color: #aa5500 } /* Literal.String.Single */ 53 | .highlight .ss { color: #0000aa } /* Literal.String.Symbol */ 54 | .highlight .bp { color: #00aaaa } /* Name.Builtin.Pseudo */ 55 | .highlight .vc { color: #aa0000 } /* Name.Variable.Class */ 56 | .highlight .vg { color: #aa0000 } /* Name.Variable.Global */ 57 | .highlight .vi { color: #aa0000 } /* Name.Variable.Instance */ 58 | .highlight .il { color: #009999 } /* Literal.Number.Integer.Long */ 59 | -------------------------------------------------------------------------------- /docs/assets/fuckingstyle.css: -------------------------------------------------------------------------------- 1 | @import url(/assets/css/autumn.css);body{max-width:650px;margin:40px auto;padding:0 10px;font:18px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";color:#444}h1,h2,h3{line-height:1.2}@media (prefers-color-scheme: dark){body{color:#ccc;background:black}a:link{color:#5bf}a:visited{color:#ccf}}a:link{color:blue}a:visited{color:blue}pre.highlight>button{opacity:0}pre.highlight:hover>button{opacity:1}pre.highlight>button:active,pre.highlight>button:focus{opacity:1}.copy-code-button{color:#272822;background-color:#FFF;border-color:#272822;border:2px solid;border-radius:3px 3px 0px 0px;display:block;margin-left:auto;margin-right:0;margin-bottom:-2px;padding:3px 8px;font-size:0.8em}.copy-code-button:hover{cursor:pointer;background-color:#F2F2F2}.copy-code-button:focus{background-color:#E6E6E6;outline:0}.copy-code-button:active{background-color:#D9D9D9}.highlight pre{margin:0}pre.highlight{background:#f0f0f0;overflow:auto} 2 | 3 | /*# sourceMappingURL=fuckingstyle.css.map */ -------------------------------------------------------------------------------- /docs/assets/fuckingstyle.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "file": "fuckingstyle.css", 4 | "sources": [ 5 | "fuckingstyle.scss" 6 | ], 7 | "sourcesContent": [ 8 | "body {\n max-width: 650px;\n margin: 40px auto;\n padding: 0 10px;\n font: 18px/1.5 -apple-system, BlinkMacSystemFont, \"Segoe UI\", Roboto, \"Helvetica Neue\", Arial, \"Noto Sans\", sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\", \"Noto Color Emoji\";\n color: #444;\n}\n\nh1,\nh2,\nh3 {\n line-height: 1.2;\n}\n\n@media (prefers-color-scheme: dark) {\n body {\n color: #ccc;\n background: black;\n }\n\n a:link {\n color: #5bf;\n }\n\n a:visited {\n color: #ccf;\n }\n}\n\n// cameron\na {\n\t&:link {\n\t\tcolor:blue;\n\t}\n\t&:visited {\n\t\tcolor:blue;\n\t}\n}\n\n//cameron\n/* _sass/_post.css */\n// https://www.dannyguo.com/blog/how-to-add-copy-to-clipboard-buttons-to-code-blocks-in-hugo/\npre.highlight > button {\n opacity: 0;\n}\n\npre.highlight:hover > button {\n opacity: 1;\n}\n\npre.highlight > button:active,\npre.highlight > button:focus {\n opacity: 1;\n}\n\n@import \"/assets/css/autumn.css\";\n\n\n.copy-code-button {\n color: #272822;\n background-color: #FFF;\n border-color: #272822;\n border: 2px solid;\n border-radius: 3px 3px 0px 0px;\n\n /* right-align */\n display: block;\n margin-left: auto;\n margin-right: 0;\n\n margin-bottom: -2px;\n padding: 3px 8px;\n font-size: 0.8em;\n}\n\n.copy-code-button:hover {\n cursor: pointer;\n background-color: #F2F2F2;\n}\n\n.copy-code-button:focus {\n /* Avoid an ugly focus outline on click in Chrome,\n but darken the button for accessibility.\n See https://stackoverflow.com/a/25298082/1481479 */\n background-color: #E6E6E6;\n outline: 0;\n}\n\n.copy-code-button:active {\n background-color: #D9D9D9;\n}\n\n.highlight pre {\n /* Avoid pushing up the copy buttons. */\n margin: 0;\n}\n\npre.highlight {\n background:#f0f0f0;\n overflow:auto;\n}\n\n" 9 | ], 10 | "names": [], 11 | "mappings": "AAuDA,OAAO,CAAP,2BAAO,CAvDP,AAAA,IAAI,AAAC,CACH,SAAS,CAAE,KAAK,CAChB,MAAM,CAAE,SAAS,CACjB,OAAO,CAAE,MAAM,CACf,IAAI,CAAE,8LAA8L,CACpM,KAAK,CAAE,IAAI,CACZ,AAED,AAAA,EAAE,CACF,EAAE,CACF,EAAE,AAAC,CACD,WAAW,CAAE,GAAG,CACjB,AAED,MAAM,6BACJ,CAAA,AAAA,IAAI,AAAC,CACH,KAAK,CAAE,IAAI,CACX,UAAU,CAAE,KAAK,CAClB,AAED,AAAA,CAAC,CAAC,IAAI,AAAC,CACL,KAAK,CAAE,IAAI,CACZ,AAED,AAAA,CAAC,CAAC,OAAO,AAAC,CACR,KAAK,CAAE,IAAI,CACZ,CARA,AAYH,AACC,CADA,CACE,IAAI,AAAC,CACN,KAAK,CAAC,IAAI,CACV,AAHF,AAIC,CAJA,CAIE,OAAO,AAAC,CACT,KAAK,CAAC,IAAI,CACV,AAMF,AAAA,GAAG,AAAA,UAAU,CAAG,MAAM,AAAC,CACrB,OAAO,CAAE,CAAC,CACX,AAED,AAAA,GAAG,AAAA,UAAU,CAAC,KAAK,CAAG,MAAM,AAAC,CAC3B,OAAO,CAAE,CAAC,CACX,AAED,AAAA,GAAG,AAAA,UAAU,CAAG,MAAM,CAAC,MAAM,CAC7B,GAAG,AAAA,UAAU,CAAG,MAAM,CAAC,KAAK,AAAC,CAC3B,OAAO,CAAE,CAAC,CACX,AAKD,AAAA,iBAAiB,AAAC,CAChB,KAAK,CAAE,OAAO,CACd,gBAAgB,CAAE,IAAI,CACtB,YAAY,CAAE,OAAO,CACrB,MAAM,CAAE,SAAS,CACjB,aAAa,CAAE,eAAe,CAG9B,OAAO,CAAE,KAAK,CACd,WAAW,CAAE,IAAI,CACjB,YAAY,CAAE,CAAC,CAEf,aAAa,CAAE,IAAI,CACnB,OAAO,CAAE,OAAO,CAChB,SAAS,CAAE,KAAK,CACjB,AAED,AAAA,iBAAiB,CAAC,KAAK,AAAC,CACtB,MAAM,CAAE,OAAO,CACf,gBAAgB,CAAE,OAAO,CAC1B,AAED,AAAA,iBAAiB,CAAC,KAAK,AAAC,CAItB,gBAAgB,CAAE,OAAO,CACzB,OAAO,CAAE,CAAC,CACX,AAED,AAAA,iBAAiB,CAAC,MAAM,AAAC,CACvB,gBAAgB,CAAE,OAAO,CAC1B,AAED,AAAA,UAAU,CAAC,GAAG,AAAC,CAEb,MAAM,CAAE,CAAC,CACV,AAED,AAAA,GAAG,AAAA,UAAU,AAAE,CACb,UAAU,CAAC,OAAO,CAClB,QAAQ,CAAC,IAAI,CACd" 12 | } -------------------------------------------------------------------------------- /docs/brutal-cam.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "brutal-cam" 5 | spec.version = "0.1.0" 6 | spec.authors = [""] 7 | spec.email = [""] 8 | 9 | spec.summary = "TODO: Write a short summary, because Rubygems requires one." 10 | spec.homepage = "TODO: Put your gem's website or public repo URL here." 11 | spec.license = "MIT" 12 | 13 | spec.files = `git ls-files -z`.split("\x0").select { |f| f.match(%r!^(assets|_layouts|_includes|_sass|LICENSE|README|_config\.yml)!i) } 14 | 15 | spec.add_runtime_dependency "jekyll", "~> 4.2" 16 | end 17 | -------------------------------------------------------------------------------- /docs/codeblocks.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | codeblock: 5 | ```bash 6 | curl -sL https://github.com/x186k/deadsfu/releases/latest/download/deadsfu-darwin-arm64.tar.gz | tar xvz 7 | ``` 8 | 9 | 10 | 11 | 12 | codeblock: 13 | ```bash 14 | ls -ald .. 15 | ``` 16 | 17 | 18 | -------------------------------------------------------------------------------- /docs/codeblocks/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | DeadSFU 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |

codeblock:

21 |
curl -sL https://github.com/x186k/deadsfu/releases/latest/download/deadsfu-darwin-arm64.tar.gz | tar xvz
22 | 
23 | 24 |

codeblock:

25 |
ls -ald ..
26 | 
27 | 28 | 29 | 30 | 31 | 32 | 33 | 69 | 70 | 71 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | DeadSFU 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |

DeadSFU

20 | 21 |

Please visit the Github repo to learn how you can use DeadSFU.

22 | 23 | 24 | 25 | 26 | 27 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /embed.go: -------------------------------------------------------------------------------- 1 | package deadsfu 2 | 3 | import "embed" 4 | 5 | //go:embed html 6 | var HtmlContent embed.FS 7 | 8 | //go:embed deadsfu-binaries/idle-clip.zip 9 | var IdleClipZipBytes []byte 10 | 11 | //go:embed deadsfu-binaries/favicon_io/favicon.ico 12 | var Favicon_ico []byte 13 | 14 | //go:embed deadsfu-binaries/deadsfu-camera-not-available.mp4 15 | var DeadsfuCameraNotAvailableMp4 []byte 16 | -------------------------------------------------------------------------------- /ftlserver/ftlserver.go: -------------------------------------------------------------------------------- 1 | package ftlserver 2 | 3 | import ( 4 | "bufio" 5 | "crypto/hmac" 6 | crand "crypto/rand" 7 | "crypto/sha512" 8 | "encoding/hex" 9 | 10 | "errors" 11 | "fmt" 12 | 13 | "log" 14 | "net" 15 | 16 | "os" 17 | "strings" 18 | "time" 19 | ) 20 | 21 | const ( 22 | // FTL_INGEST_RESP_UNKNOWN = 0 23 | // FTL_INGEST_RESP_OK = 200 24 | // FTL_INGEST_RESP_PING = 201 25 | // FTL_INGEST_RESP_BAD_REQUEST = 400 // The handshake was not formatted correctly 26 | FTL_INGEST_RESP_UNAUTHORIZED = 401 // This channel id is not authorized to stream 27 | // FTL_INGEST_RESP_OLD_VERSION = 402 // This ftl api version is no longer supported 28 | // FTL_INGEST_RESP_AUDIO_SSRC_COLLISION = 403 29 | // FTL_INGEST_RESP_VIDEO_SSRC_COLLISION = 404 30 | FTL_INGEST_RESP_INVALID_STREAM_KEY = 405 // The corresponding channel does not match this key 31 | // FTL_INGEST_RESP_CHANNEL_IN_USE = 406 // The channel ID successfully authenticated however it is already actively streaming 32 | // FTL_INGEST_RESP_REGION_UNSUPPORTED = 407 // Streaming from this country or region is not authorized by local governments 33 | // FTL_INGEST_RESP_NO_MEDIA_TIMEOUT = 408 34 | // FTL_INGEST_RESP_GAME_BLOCKED = 409 // The game the user account is set to can't be streamed. 35 | // FTL_INGEST_RESP_SERVER_TERMINATE = 410 // The sterver has terminated the stream. 36 | // FTL_INGEST_RESP_INTERNAL_SERVER_ERROR = 500 37 | // FTL_INGEST_RESP_INTERNAL_MEMORY_ERROR = 900 38 | // FTL_INGEST_RESP_INTERNAL_COMMAND_ERROR = 901 39 | // FTL_INGEST_RESP_INTERNAL_SOCKET_CLOSED = 902 40 | // FTL_INGEST_RESP_INTERNAL_SOCKET_TIMEOUT = 903 41 | ) 42 | 43 | type FtlServer interface { 44 | TakePacket(inf *log.Logger, dbg *log.Logger, packet []byte) bool 45 | } 46 | 47 | type FindServerFunc func(inf *log.Logger, dbg *log.Logger, chanid string) (server FtlServer, hmackey string) 48 | 49 | func NewTcpSession(inf *log.Logger, dbg *log.Logger, tcpconn *net.TCPConn, findsvr FindServerFunc, ftpUdpPort int) { 50 | var err error 51 | defer tcpconn.Close() //could be redundant 52 | 53 | err = tcpconn.SetKeepAlive(true) 54 | if err != nil { 55 | inf.Println("SetKeepAlive", err) //nil err okay 56 | return 57 | } 58 | 59 | err = tcpconn.SetKeepAlivePeriod(time.Second * 5) 60 | if err != nil { 61 | inf.Println("SetKeepAlivePeriod", err) //nil err okay 62 | return 63 | } 64 | 65 | err = tcpconn.SetReadDeadline(time.Now().Add(10 * time.Second)) //ping period is 5sec 66 | if err != nil { 67 | inf.Println("SetReadDeadline: ", err) //nil err okay 68 | return 69 | } 70 | 71 | inf.Println("OBS/FTL GOT TCP SOCKET CONNECTION") 72 | 73 | scanner := bufio.NewScanner(tcpconn) 74 | 75 | if !scanner.Scan() { 76 | dbg.Println("waiting hmac/register: error or eof", scanner.Err()) 77 | return 78 | } 79 | 80 | line := scanner.Text() 81 | tokens := strings.SplitN(line, " ", 2) 82 | command := tokens[0] 83 | 84 | if command != "HMAC" { 85 | inf.Println("unrecognized 1st token on socket:", command) 86 | return 87 | } 88 | 89 | var l string 90 | 91 | dbg.Println("ftl: got hmac") 92 | 93 | if !scanner.Scan() { 94 | dbg.Println("waiting blank: error or eof", scanner.Err()) 95 | return 96 | } 97 | if l = scanner.Text(); l != "" { 98 | inf.Println("ftl/no blank after hmac:", l) 99 | return 100 | } 101 | dbg.Println("ftl: got hmac blank") 102 | 103 | numrand := 128 104 | message := make([]byte, numrand) 105 | _, err = crand.Read(message) 106 | if err != nil { 107 | inf.Print(err) 108 | return 109 | } 110 | 111 | fmt.Fprintf(tcpconn, "200 %s\n", hex.EncodeToString(message)) 112 | 113 | if !scanner.Scan() { 114 | dbg.Println("waiting connect: error or eof", scanner.Err()) 115 | return 116 | } 117 | 118 | if l = scanner.Text(); !strings.HasPrefix(l, "CONNECT ") { 119 | inf.Println("ftl/no connect:", l) 120 | return 121 | } 122 | dbg.Println("ftl: got connect") 123 | 124 | connectsplit := strings.Split(l, " ") 125 | if len(connectsplit) < 3 { 126 | inf.Println("ftl: bad connect") 127 | return 128 | } 129 | 130 | userid := connectsplit[1] 131 | connectMsg := "CONNECT " + userid + " $" 132 | client_hash, err := hex.DecodeString(l[len(connectMsg):]) 133 | if err != nil { 134 | inf.Println(err) 135 | return 136 | } 137 | 138 | // endpointMapMutex.Lock() 139 | // sfuinfo, ok := endpointMap[userid] 140 | // endpointMapMutex.Unlock() 141 | server, key := findsvr(inf, dbg, userid) 142 | if server == nil { 143 | inf.Println("Non existent userid presented", userid) 144 | fmt.Fprintf(tcpconn, "%d\n", FTL_INGEST_RESP_UNAUTHORIZED) 145 | return 146 | } 147 | 148 | hmackey := []byte(key) 149 | 150 | good := validMAC(hmackey, message, client_hash) 151 | 152 | dbg.Println("ftl: auth is okay:", good) 153 | 154 | if !good { 155 | inf.Println("FTL authentication failed for", userid) 156 | fmt.Fprintf(tcpconn, "%d\n", FTL_INGEST_RESP_INVALID_STREAM_KEY) 157 | return 158 | } 159 | 160 | fmt.Fprintf(tcpconn, "200\n") 161 | 162 | kvmap := make(map[string]string) 163 | 164 | err = tcpconn.SetReadDeadline(time.Now().Add(10 * time.Second)) 165 | if err != nil { 166 | inf.Println(err) 167 | } 168 | for scanner.Scan() { 169 | l = scanner.Text() 170 | if l == "." { 171 | break 172 | } 173 | if l != "" { 174 | split := strings.SplitN(l, ": ", 2) 175 | if len(split) == 2 { 176 | kvmap[split[0]] = split[1] 177 | } else { 178 | inf.Println("ftl/bad format keyval section:", l) 179 | return 180 | } 181 | } 182 | } 183 | 184 | for k, v := range kvmap { 185 | dbg.Println("ftl: key/value", k, v) 186 | } 187 | 188 | keyvalsOK := true // todo 189 | //do a consistency check of the key vals 190 | if !keyvalsOK { 191 | inf.Println("ftl/issue with k/v pairs") 192 | return 193 | } 194 | 195 | // net.DialUDP("udp",nil,), not yet, cause we don't know remote port 196 | x := net.UDPAddr{IP: nil, Port: ftpUdpPort, Zone: ""} 197 | udprx, err := net.ListenUDP("udp", &x) 198 | if err != nil { 199 | inf.Println(err) 200 | return 201 | } 202 | defer udprx.Close() 203 | 204 | laddr := udprx.LocalAddr().(*net.UDPAddr) 205 | dbg.Println("bound inbound udp on", laddr) 206 | 207 | fmt.Fprintf(tcpconn, "200. Use UDP port %d\n", laddr.Port) 208 | 209 | // PING goroutine 210 | go func() { 211 | // when the ping goroutine exits, we want to shut everything down 212 | defer tcpconn.Close() 213 | defer udprx.Close() 214 | 215 | for { 216 | err = tcpconn.SetReadDeadline(time.Now().Add(8 * time.Second)) //ping period is 5sec 217 | if err != nil { 218 | inf.Println("ping GR done: ", scanner.Err()) //nil err okay 219 | return 220 | } 221 | 222 | ok := scanner.Scan() 223 | if !ok { 224 | dbg.Println("ping GR done: ", scanner.Err()) //nil err okay 225 | return 226 | } 227 | 228 | l := scanner.Text() 229 | 230 | if strings.HasPrefix(l, "PING ") { 231 | // XXX PING is sometimes followed by streamkey-id 232 | // but we don't validate it. 233 | // it is checked for Connect message 234 | dbg.Println("ftl: ping!") 235 | fmt.Fprintf(tcpconn, "201\n") 236 | } else if l == "" { 237 | //ignore blank 238 | } else if l == "DISCONNECT" { 239 | inf.Println("disconnect, ping GR done") 240 | return 241 | } else { 242 | inf.Println("ftl: unexpected msg:", l) 243 | } 244 | } 245 | }() 246 | 247 | buf := make([]byte, 2000) 248 | 249 | for { 250 | err = udprx.SetReadDeadline(time.Now().Add(time.Second)) 251 | if err != nil { 252 | inf.Println(err) 253 | return 254 | } 255 | 256 | n, _, err := udprx.ReadFromUDP(buf) 257 | if errors.Is(err, os.ErrDeadlineExceeded) { 258 | inf.Println("no FTL packets for a while, closing") 259 | return 260 | } else if err != nil { 261 | inf.Println(err) 262 | return 263 | } 264 | 265 | // not now: udprx, err = net.DialUDP("udp", laddr, readaddr) 266 | 267 | if n < 12 { 268 | continue 269 | } 270 | 271 | // if you don't make a copy here, 272 | //then every implementor of x.TakePacket() 273 | // needs to either: a) not touch the byte array, 274 | // or make their own copy 275 | // I think it is better to make the copy here 276 | // and remove that requirement upon implementors 277 | // if you use pion/rtp.Unmarshal, it's easy to encounter a bug 278 | // if you pass the original 279 | pktcopy := make([]byte, n) 280 | copy(pktcopy, buf[:n]) 281 | 282 | ok := server.TakePacket(inf, dbg, pktcopy) 283 | if !ok { 284 | dbg.Println("indication from ftl parent to close FTL chanid:", userid) 285 | return 286 | } 287 | 288 | // _, err = sfuinfo.udptx.Write(buf[:n]) 289 | // if err != nil { 290 | // if errors.Is(err, unix.ECONNREFUSED) { // or windows.WSAECONNRESET 291 | // nrefused++ 292 | // if nrefused > 10 { 293 | // xlog.Println("ending session: too many ECONNREFUSED") 294 | // return 295 | // } 296 | // } 297 | // } 298 | } 299 | 300 | } 301 | 302 | func validMAC(key, message, messageMAC []byte) bool { 303 | mac := hmac.New(sha512.New, key) 304 | mac.Write(message) 305 | expectedMAC := mac.Sum(nil) 306 | return hmac.Equal(messageMAC, expectedMAC) 307 | } 308 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/x186k/deadsfu 2 | 3 | go 1.18 4 | 5 | require ( 6 | github.com/caddyserver/certmagic v0.14.6-0.20210923203551-1bbe11e2914b 7 | github.com/google/uuid v1.3.0 8 | github.com/libdns/cloudflare v0.1.0 9 | github.com/libdns/duckdns v0.1.1 10 | github.com/libdns/libdns v0.2.1 11 | github.com/miekg/dns v1.1.42 12 | github.com/pion/interceptor v0.1.7 13 | github.com/pion/rtcp v1.2.9 14 | github.com/pion/rtp v1.7.4 15 | github.com/pion/sdp/v3 v3.0.4 16 | github.com/pion/webrtc/v3 v3.1.24 17 | github.com/pkg/profile v1.5.0 18 | github.com/spf13/pflag v1.0.5 19 | github.com/stretchr/testify v1.7.0 20 | github.com/x186k/ddns5libdns v0.0.0-20210712210115-f62ae7c09b3a 21 | go.uber.org/zap v1.17.0 22 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd 23 | ) 24 | 25 | require ( 26 | github.com/davecgh/go-spew v1.1.1 // indirect 27 | github.com/klauspost/cpuid/v2 v2.0.6 // indirect 28 | github.com/kr/text v0.2.0 // indirect 29 | github.com/mholt/acmez v1.0.0 // indirect 30 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect 31 | github.com/pion/datachannel v1.5.2 // indirect 32 | github.com/pion/dtls/v2 v2.1.3 // indirect 33 | github.com/pion/ice/v2 v2.2.1 // indirect 34 | github.com/pion/logging v0.2.2 // indirect 35 | github.com/pion/mdns v0.0.5 // indirect 36 | github.com/pion/randutil v0.1.0 // indirect 37 | github.com/pion/sctp v1.8.2 // indirect 38 | github.com/pion/srtp/v2 v2.0.5 // indirect 39 | github.com/pion/stun v0.3.5 // indirect 40 | github.com/pion/transport v0.13.0 // indirect 41 | github.com/pion/turn/v2 v2.0.8 // indirect 42 | github.com/pion/udp v0.1.1 // indirect 43 | github.com/pkg/errors v0.9.1 // indirect 44 | github.com/pmezard/go-difflib v1.0.0 // indirect 45 | go.uber.org/atomic v1.7.0 // indirect 46 | go.uber.org/multierr v1.6.0 // indirect 47 | golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 // indirect 48 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect 49 | golang.org/x/text v0.3.7 // indirect 50 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect 51 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect 52 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect 53 | ) 54 | 55 | // replace github.com/spf13/pflag => ../pflag 56 | 57 | // replace github.com/libdns/duckdns => ../duckdns 58 | //replace github.com/libdns/cloudflare => ../cloudflare 59 | 60 | // replace github.com/x186k/dynamicdns => ../dynamicdns 61 | //replace github.com/pion/webrtc/v3 => ../webrtc 62 | //replace github.com/pion/webrtc/v3 v3.0.4 => github.com/cameronelliott/webrtc/v3 v3.0.5 63 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= 2 | github.com/caddyserver/certmagic v0.14.6-0.20210923203551-1bbe11e2914b h1:lIVMaA+OyTC/871QwU65Oaa6Lz1dHgr/TbNDO4iB4pM= 3 | github.com/caddyserver/certmagic v0.14.6-0.20210923203551-1bbe11e2914b/go.mod h1:/0VQ5og2Jxa5yBQ8eT80wWS7fi/DgNy1uXeXRUJ1Wj0= 4 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 7 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 8 | github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= 9 | github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= 10 | github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0/go.mod h1:fyg7847qk6SyHyPtNmDHnmrv/HOrqktSC+C9fM+CJOE= 11 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 12 | github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= 13 | github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= 14 | github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= 15 | github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= 16 | github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= 17 | github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= 18 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 19 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 20 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 21 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 22 | github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 23 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 24 | github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= 25 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 26 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 27 | github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= 28 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 29 | github.com/klauspost/cpuid/v2 v2.0.6 h1:dQ5ueTiftKxp0gyjKSx5+8BtPWkyQbd95m8Gys/RarI= 30 | github.com/klauspost/cpuid/v2 v2.0.6/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 31 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 32 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 33 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 34 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 35 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 36 | github.com/libdns/cloudflare v0.1.0 h1:93WkJaGaiXCe353LHEP36kAWCUw0YjFqwhkBkU2/iic= 37 | github.com/libdns/cloudflare v0.1.0/go.mod h1:a44IP6J1YH6nvcNl1PverfJviADgXUnsozR3a7vBKN8= 38 | github.com/libdns/duckdns v0.1.1 h1:wkgu98DkwpjduH2fxC2YkCiNkNnfQiHaYskMkEyRZ28= 39 | github.com/libdns/duckdns v0.1.1/go.mod h1:jCQ/7+qvhLK39+28qXvKEYGBBvmHBCmIwNqdJTCUmVs= 40 | github.com/libdns/libdns v0.2.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40= 41 | github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis= 42 | github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40= 43 | github.com/mholt/acmez v1.0.0 h1:ZAdWrilnq41HTlUO0vMJ6C+z8ZvzQ9I2LR1/Bo+137U= 44 | github.com/mholt/acmez v1.0.0/go.mod h1:8qnn8QA/Ewx8E3ZSsmscqsIjhhpxuy9vqdgbX2ceceM= 45 | github.com/miekg/dns v1.1.40/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM= 46 | github.com/miekg/dns v1.1.42 h1:gWGe42RGaIqXQZ+r3WUGEKBEtvPHY2SXo4dqixDNxuY= 47 | github.com/miekg/dns v1.1.42/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= 48 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= 49 | github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= 50 | github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= 51 | github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= 52 | github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= 53 | github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= 54 | github.com/onsi/ginkgo v1.16.4/go.mod h1:dX+/inL/fNMqNlz0e9LfyB9TswhZpCVdJM/Z6Vvnwo0= 55 | github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= 56 | github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= 57 | github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= 58 | github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= 59 | github.com/pion/datachannel v1.5.2 h1:piB93s8LGmbECrpO84DnkIVWasRMk3IimbcXkTQLE6E= 60 | github.com/pion/datachannel v1.5.2/go.mod h1:FTGQWaHrdCwIJ1rw6xBIfZVkslikjShim5yr05XFuCQ= 61 | github.com/pion/dtls/v2 v2.1.2/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus= 62 | github.com/pion/dtls/v2 v2.1.3 h1:3UF7udADqous+M2R5Uo2q/YaP4EzUoWKdfX2oscCUio= 63 | github.com/pion/dtls/v2 v2.1.3/go.mod h1:o6+WvyLDAlXF7YiPB/RlskRoeK+/JtuaZa5emwQcWus= 64 | github.com/pion/ice/v2 v2.2.1 h1:R3MeuJZpU1ty3diPqpD5OxaxcZ15eprAc+EtUiSoFxg= 65 | github.com/pion/ice/v2 v2.2.1/go.mod h1:Op8jlPtjeiycsXh93Cs4jK82C9j/kh7vef6ztIOvtIQ= 66 | github.com/pion/interceptor v0.1.7 h1:HThW0tIIKT9RRoDWGURe8rlZVOx0fJHxBHpA0ej0+bo= 67 | github.com/pion/interceptor v0.1.7/go.mod h1:Lh3JSl/cbJ2wP8I3ccrjh1K/deRGRn3UlSPuOTiHb6U= 68 | github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= 69 | github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= 70 | github.com/pion/mdns v0.0.5 h1:Q2oj/JB3NqfzY9xGZ1fPzZzK7sDSD8rZPOvcIQ10BCw= 71 | github.com/pion/mdns v0.0.5/go.mod h1:UgssrvdD3mxpi8tMxAXbsppL3vJ4Jipw1mTCW+al01g= 72 | github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA= 73 | github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= 74 | github.com/pion/rtcp v1.2.6/go.mod h1:52rMNPWFsjr39z9B9MhnkqhPLoeHTv1aN63o/42bWE0= 75 | github.com/pion/rtcp v1.2.9 h1:1ujStwg++IOLIEoOiIQ2s+qBuJ1VN81KW+9pMPsif+U= 76 | github.com/pion/rtcp v1.2.9/go.mod h1:qVPhiCzAm4D/rxb6XzKeyZiQK69yJpbUDJSF7TgrqNo= 77 | github.com/pion/rtp v1.7.0/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= 78 | github.com/pion/rtp v1.7.4 h1:4dMbjb1SuynU5OpA3kz1zHK+u+eOCQjW3MAeVHf1ODA= 79 | github.com/pion/rtp v1.7.4/go.mod h1:bDb5n+BFZxXx0Ea7E5qe+klMuqiBrP+w8XSjiWtCUko= 80 | github.com/pion/sctp v1.8.0/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s= 81 | github.com/pion/sctp v1.8.2 h1:yBBCIrUMJ4yFICL3RIvR4eh/H2BTTvlligmSTy+3kiA= 82 | github.com/pion/sctp v1.8.2/go.mod h1:xFe9cLMZ5Vj6eOzpyiKjT9SwGM4KpK/8Jbw5//jc+0s= 83 | github.com/pion/sdp/v3 v3.0.4 h1:2Kf+dgrzJflNCSw3TV5v2VLeI0s/qkzy2r5jlR0wzf8= 84 | github.com/pion/sdp/v3 v3.0.4/go.mod h1:bNiSknmJE0HYBprTHXKPQ3+JjacTv5uap92ueJZKsRk= 85 | github.com/pion/srtp/v2 v2.0.5 h1:ks3wcTvIUE/GHndO3FAvROQ9opy0uLELpwHJaQ1yqhQ= 86 | github.com/pion/srtp/v2 v2.0.5/go.mod h1:8k6AJlal740mrZ6WYxc4Dg6qDqqhxoRG2GSjlUhDF0A= 87 | github.com/pion/stun v0.3.5 h1:uLUCBCkQby4S1cf6CGuR9QrVOKcvUwFeemaC865QHDg= 88 | github.com/pion/stun v0.3.5/go.mod h1:gDMim+47EeEtfWogA37n6qXZS88L5V6LqFcf+DZA2UA= 89 | github.com/pion/transport v0.12.2/go.mod h1:N3+vZQD9HlDP5GWkZ85LohxNsDcNgofQmyL6ojX5d8Q= 90 | github.com/pion/transport v0.12.3/go.mod h1:OViWW9SP2peE/HbwBvARicmAVnesphkNkCVZIWJ6q9A= 91 | github.com/pion/transport v0.13.0 h1:KWTA5ZrQogizzYwPEciGtHPLwpAjE91FgXnyu+Hv2uY= 92 | github.com/pion/transport v0.13.0/go.mod h1:yxm9uXpK9bpBBWkITk13cLo1y5/ur5VQpG22ny6EP7g= 93 | github.com/pion/turn/v2 v2.0.8 h1:KEstL92OUN3k5k8qxsXHpr7WWfrdp7iJZHx99ud8muw= 94 | github.com/pion/turn/v2 v2.0.8/go.mod h1:+y7xl719J8bAEVpSXBXvTxStjJv3hbz9YFflvkpcGPw= 95 | github.com/pion/udp v0.1.1 h1:8UAPvyqmsxK8oOjloDk4wUt63TzFe9WEJkg5lChlj7o= 96 | github.com/pion/udp v0.1.1/go.mod h1:6AFo+CMdKQm7UiA0eUPA8/eVCTx8jBIITLZHc9DWX5M= 97 | github.com/pion/webrtc/v3 v3.1.24 h1:s9PuwisrgHe1FTqfwK4p3T7rXtAHaUNhycbdMjADT28= 98 | github.com/pion/webrtc/v3 v3.1.24/go.mod h1:mO/yv7fBN3Lp7YNlnYcTj1jtpvNvssJG+7eh6itZ4xM= 99 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 100 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 101 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 102 | github.com/pkg/profile v1.5.0 h1:042Buzk+NhDI+DeSAA62RwJL8VAuZUMQZUjCsRz1Mug= 103 | github.com/pkg/profile v1.5.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= 104 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 105 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 106 | github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= 107 | github.com/sclevine/agouti v3.0.0+incompatible/go.mod h1:b4WX9W9L1sfQKXeJf1mUTLZKJ48R1S7H23Ji7oFO5Bw= 108 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 109 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 110 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 111 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 112 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 113 | github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= 114 | github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 115 | github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= 116 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 117 | github.com/x186k/ddns5libdns v0.0.0-20210712210115-f62ae7c09b3a h1:3MHOw+STEPfEb0cvsGzjjE9LrDVwb0iedtFFTnhYiaU= 118 | github.com/x186k/ddns5libdns v0.0.0-20210712210115-f62ae7c09b3a/go.mod h1:9FAoj8bfSg+cVEoDIyMfI+4GAtFllfDfLOW20S74JsM= 119 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 120 | go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= 121 | go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= 122 | go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= 123 | go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= 124 | go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= 125 | go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= 126 | go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= 127 | go.uber.org/zap v1.15.0/go.mod h1:Mb2vm2krFEG5DV0W9qcHBYFtp/Wku1cvYaqPsS/WYfc= 128 | go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= 129 | go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= 130 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 131 | golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 132 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 133 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 134 | golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= 135 | golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838 h1:71vQrMauZZhcTVK6KdYM+rklehEEwb3E+ZhaE5jrPrE= 136 | golang.org/x/crypto v0.0.0-20220131195533-30dcbda58838/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 137 | golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= 138 | golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= 139 | golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= 140 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 141 | golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 142 | golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 143 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 144 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 145 | golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 146 | golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= 147 | golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 148 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 149 | golang.org/x/net v0.0.0-20201201195509-5d6afe98e0b7/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 150 | golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 151 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 152 | golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= 153 | golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 154 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 155 | golang.org/x/net v0.0.0-20211201190559-0a0e4e1bb54c/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 156 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd h1:O7DYs+zxREGLKzKoMQrtrEacpb0ZVXA5rIwylE2Xchk= 157 | golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= 158 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 159 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 160 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 161 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c h1:5KslGYwFpkhGh+Q16bwMP3cOontH8FOep7tGV86Y7SQ= 162 | golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 163 | golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 164 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 165 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 166 | golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 167 | golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 168 | golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 169 | golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 170 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 171 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 172 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 173 | golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 174 | golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 175 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 176 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 177 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e h1:fLOSk5Q00efkSvAm+4xcoXD+RRmLmmulPn5I3Y9F2EM= 178 | golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 179 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 180 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 181 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 182 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 183 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 184 | golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= 185 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 186 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 187 | golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= 188 | golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= 189 | golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 190 | golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 191 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 192 | golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= 193 | golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 194 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 195 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 196 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 197 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= 198 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 199 | google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= 200 | google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= 201 | google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= 202 | google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= 203 | google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= 204 | google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= 205 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 206 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 207 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 208 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 209 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 210 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= 211 | gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 212 | gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= 213 | gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= 214 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= 215 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 216 | gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 217 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 218 | gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 219 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 220 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 221 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 222 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo= 223 | gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 224 | honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= 225 | -------------------------------------------------------------------------------- /h264.sdp: -------------------------------------------------------------------------------- 1 | c=IN IP4 127.0.0.1 2 | m=video 5000 RTP/AVP 96 3 | a=rtpmap:96 H264/90000 -------------------------------------------------------------------------------- /html/deadsfu.css: -------------------------------------------------------------------------------- 1 | /* .nav-item a:hover { cursor: pointer } */ 2 | /* .nav-link a:hover { cursor: crosshair;} */ 3 | /* this somehow changes the cursor over links in the navbar */ 4 | /* a.nav-link { 5 | cursor: default; 6 | } */ 7 | 8 | a { 9 | color: #0053fa; 10 | 11 | background-color: transparent; 12 | text-decoration: none; 13 | } 14 | 15 | /* override bootstrap button color */ 16 | .btn-primary, 17 | .btn-primary:hover, 18 | .btn-primary:active, 19 | .btn-primary:visited { 20 | background-color: #0053fa !important; 21 | } -------------------------------------------------------------------------------- /html/deadsfu.js: -------------------------------------------------------------------------------- 1 | //@ts-check 2 | //The @ts-check statement above enables jsdoc typechecking 3 | // https://stackoverflow.com/a/52076280/86375 4 | // http://demo.unified-streaming.com/players/dash.js-2.4.1/build/jsdoc/jsdoc_cheat-sheet.pdf 5 | 6 | 7 | import * as whipwhap from "./whip-whap-js/whip-whap-js.js" 8 | 9 | function uuidv4() { 10 | //@ts-ignore 11 | return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => 12 | (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) 13 | ) 14 | } 15 | 16 | 17 | window.onload = async function () { 18 | 19 | 20 | let pc = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] }) 21 | 22 | const xstate = document.getElementById('xstate') 23 | 24 | pc.addEventListener('downtime-msg', function (ev) { 25 | //@ts-ignore 26 | let nsec = ev.detail.numsec; let status = ev.detail.status 27 | let time = (new Date(nsec * 1000)).toISOString().substr(11, 8) 28 | xstate.innerText = `downtime: ${time} httpcode: ${status}` 29 | }) 30 | //firefox does not fire 'onconnectionstatechange' right now, so use ice... 31 | pc.addEventListener('iceconnectionstatechange', ev => xstate.innerText = pc.iceConnectionState) 32 | pc.addEventListener('iceconnectionstatechange', whipwhap.handleIceStateChange) 33 | 34 | 35 | let video1 = /** @type {HTMLVideoElement} */ (document.getElementById('video1')) 36 | let searchParams = new URLSearchParams(window.location.search) 37 | let bearerToken = searchParams.get('access_token') 38 | //let roomname = window.location.pathname 39 | let roomname = searchParams.get('room') 40 | let mp4 = searchParams.get('mp4') 41 | let nolongpoll = searchParams.get('nolongpoll') 42 | // no, DRY: the go code does this also 43 | // if (!roomname) { 44 | // roomname = "mainroom" 45 | // } 46 | if (!roomname) { 47 | roomname = "" 48 | } 49 | //console.log(555, roomname) 50 | 51 | let headers = new Headers() 52 | if (typeof bearerToken === 'string') { // may be null or undefined 53 | headers.set('Authorization', `Bearer ${bearerToken}`) 54 | } 55 | 56 | 57 | if (location.pathname == '/send/' || location.pathname == '/send') { 58 | let whipUrl = '/whip?room=' + roomname 59 | 60 | pc.addEventListener('negotiationneeded', ev => whipwhap.handleNegotiationNeeded(ev, whipUrl, headers)) 61 | 62 | /** @type {MediaStream} */ 63 | var mediaStream 64 | 65 | //no camera available 66 | //camera available on localhost in some instances, so https check not reliable 67 | //if (location.protocol !== 'https:') { 68 | if (mp4) { 69 | video1.src = mp4 70 | video1.loop = true 71 | video1.crossOrigin = 'anonymous' 72 | await video1.play() 73 | //@ts-ignore 74 | mediaStream = video1.captureStream() 75 | } else if (!navigator.mediaDevices) { 76 | video1.src = '/no-camera.mp4' 77 | video1.loop = true 78 | video1.crossOrigin = 'anonymous' 79 | await video1.play() 80 | //@ts-ignore 81 | mediaStream = video1.captureStream() 82 | } else { 83 | try { 84 | mediaStream = await navigator.mediaDevices.getUserMedia({ audio: true, video: true }) 85 | video1.srcObject = mediaStream 86 | video1.play() 87 | } catch (error) { 88 | alert('Camera setup failed:' + error) 89 | return 90 | } 91 | 92 | } 93 | 94 | 95 | 96 | pc.addTransceiver(mediaStream.getVideoTracks()[0], { 'direction': 'sendonly' }) 97 | pc.addTransceiver(mediaStream.getAudioTracks()[0], { 'direction': 'sendonly' }) 98 | 99 | document.title = "Sending" 100 | 101 | } else { 102 | const uuidRE = /^([0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[0-9a-d][0-9a-f]{3}-[0-9a-f]{12}|00000000-0000-0000-0000-000000000000)$/i 103 | let subuuid = searchParams.get('subuuid') 104 | 105 | if (!uuidRE.test(subuuid)) { 106 | subuuid = uuidv4() 107 | } 108 | window.location.hash = '?subuuid=' + subuuid 109 | startRoomListFetchLoop(headers, subuuid, nolongpoll) 110 | 111 | let whapUrl = '/whap?room=' + roomname 112 | 113 | headers.set('X-deadsfu-subuuid', subuuid) //sfu also accepts param &subuuid=..., but this is more secure 114 | 115 | // console.debug(newurl.searchParams.get('room')) // we just pass along 'room' 116 | 117 | pc.addEventListener('negotiationneeded', ev => whipwhap.handleNegotiationNeeded(ev, whapUrl, headers)) 118 | 119 | pc.addTransceiver('video', { 'direction': 'recvonly' }) // build sdp 120 | pc.addTransceiver('audio', { 'direction': 'recvonly' }) // build sdp 121 | 122 | pc.ontrack = ev => { 123 | video1.srcObject = ev.streams[0] 124 | // want to remove these someday 11.7.21 cam 125 | // video1.autoplay = true 126 | // video1.controls = true 127 | // return false 128 | } 129 | 130 | 131 | 132 | 133 | document.title = "Receiving" 134 | 135 | } 136 | 137 | // not needed! 138 | //pc.restartIce() // Start connecting! 139 | 140 | 141 | 142 | // @ts-ignore 143 | if (startGetStatsShipping) { 144 | // @ts-ignore 145 | startGetStatsShipping(pc) 146 | } else { 147 | console.debug('startGetStatsShipping() not invoked') 148 | } 149 | 150 | 151 | const rxtxSpan = document.getElementById('rxtx') 152 | 153 | // declare func 154 | async function rxtxTimeoutCallback() { 155 | 156 | let rates = await whipwhap.helperGetRxTxRate(pc) 157 | let qualityLim = '' 158 | if (rates.qualityLimitation == true) { 159 | qualityLim = 'QL' 160 | } 161 | rxtxSpan.textContent = `${rates.rxrate}/${rates.txrate} rx/tx kbps ${qualityLim}` 162 | 163 | if (pc.signalingState == 'closed') { 164 | //this two lines are a desparate way to handle the pixelbook 165 | //laptop lid close/reopen 166 | // after this happens, the pc will be closed, 167 | // and pc.restartIce() does nothing. 168 | // this leaves us two choices: 1. create a new PC, 2. reload the page. 169 | // we choose #2 at this time. 170 | location.hash = 'closereloadlaptoplid' 171 | location.reload() 172 | // pc.restartIce() this doesnt work after pixelbook lid closed/opened 173 | // , maybe pc.restartIce() never works from a 'closed' PC 174 | } 175 | 176 | 177 | setTimeout(rxtxTimeoutCallback, 3000) // milliseconds 178 | } 179 | 180 | // initiate timeout update loop 181 | rxtxTimeoutCallback() 182 | 183 | 184 | // enable full screen nav-bar button 185 | 186 | document.getElementById("gofullscreen").onclick = (ev) => fullScreen(video1) 187 | 188 | } 189 | 190 | /** 191 | * 192 | * @param {HTMLVideoElement} vidElement 193 | */ 194 | function fullScreen(vidElement) { 195 | 196 | if (vidElement.requestFullscreen) { 197 | vidElement.requestFullscreen() 198 | } else { 199 | // Toggle fullscreen in Safari for iPad 200 | // @ts-ignore 201 | if (vidElement.webkitEnterFullScreen) { 202 | // @ts-ignore 203 | vidElement.webkitEnterFullScreen() 204 | } 205 | } 206 | return false 207 | } 208 | 209 | 210 | /** 211 | * @param {Headers} hdrs could contain 'Authorization' 212 | * @param {string} roomname 213 | * @param {string} subuuid 214 | */ 215 | function switchRoom(hdrs, roomname, subuuid) { 216 | console.debug('** switchroom') 217 | let opt = { 218 | method: 'GET', 219 | headers: hdrs, 220 | /** @type RequestCache */ cache: "no-store" 221 | } 222 | 223 | var url = new URL('/switchRoom', location.origin) 224 | url.searchParams.set('room', roomname) 225 | url.searchParams.set('subuuid', subuuid) 226 | 227 | //console.log(8888,url.href) 228 | fetch(url.href) 229 | } 230 | //@ts-ignore 231 | window.switchRoom = switchRoom 232 | 233 | 234 | 235 | 236 | /** 237 | * @param {Headers} hdrs could contain 'Authorization' 238 | * @param {string} subuuid 239 | * @param {string} nolongpoll 240 | */ 241 | async function startRoomListFetchLoop(hdrs, subuuid, nolongpoll) { 242 | 243 | setTimeout(myCallback, 0, -10) //no delay 244 | 245 | /** 246 | * @param {string} serial this number is used to block by server 247 | */ 248 | async function myCallback(serial) { 249 | let url = '/getRoomList?serial=' + serial 250 | 251 | let opt = { 252 | method: 'GET', 253 | headers: hdrs, 254 | /** @type RequestCache */ cache: "no-store" 255 | } 256 | 257 | let resp = { status: -1 } 258 | try { // without try/catch, a thrown except from fetch exits our 'thread' 259 | //console.log(888, url) 260 | resp = await fetch(url, opt) 261 | } catch (error) { 262 | ; // not needed console.log(error) 263 | } 264 | //console.log(111, resp.status) 265 | if (resp.status == 200) { 266 | let json = await resp.text() 267 | let obj = JSON.parse(json) 268 | //console.debug('json', json) 269 | //console.debug('jsono', obj) 270 | updateSourcesNav(hdrs, obj.rooms, subuuid) 271 | 272 | let serial = obj.serial 273 | let sleep = 0 274 | if (nolongpoll) { 275 | // sfu will only block+long poll when serial's match 276 | // this prevents sfu from blocking/long-polling 277 | // but then we need to increase the sleep time 278 | serial = '-5' 279 | sleep = 3000 280 | } 281 | 282 | setTimeout(myCallback, sleep, serial) 283 | } else { 284 | // failure 285 | setTimeout(myCallback, 2000) // wait two sec before try again 286 | } 287 | } 288 | } 289 | 290 | /** 291 | * @param {Headers} hdrs could contain 'Authorization' 292 | * @param {Array} roomnames 293 | * @param {string} subuuid 294 | */ 295 | function updateSourcesNav(hdrs, roomnames, subuuid) { 296 | 297 | let p = document.getElementById('sources-proto') 298 | let div = document.getElementById("sources-div") 299 | 300 | while (div.firstChild) { 301 | div.removeChild(div.firstChild); 302 | } 303 | 304 | roomnames.forEach(v => { 305 | //console.log(333, v) 306 | let b = /** @type HTMLElement */ (p.cloneNode(true)) 307 | b.id = '' 308 | b.hidden = false 309 | let bb = /** @type HTMLAnchorElement */ (b.firstChild) 310 | 311 | bb.href = "javascript:void(0)" 312 | bb.onclick = function () { switchRoom(hdrs, v, subuuid) } 313 | bb.innerText = v 314 | 315 | //console.debug('**', b.outerHTML) 316 | //console.debug('**', bb.outerHTML) 317 | 318 | //bb.innerText = v 319 | //console.debug(99, bb.innerText) 320 | div.appendChild(b) 321 | 322 | }); 323 | 324 | 325 | 326 | } 327 | 328 | -------------------------------------------------------------------------------- /html/favicon.svg: -------------------------------------------------------------------------------- 1 | 5 | 6 | 22 | -------------------------------------------------------------------------------- /html/help/index.html: -------------------------------------------------------------------------------- 1 | ../dead-down/index.html -------------------------------------------------------------------------------- /html/help/readme.md: -------------------------------------------------------------------------------- 1 | 2 | # DeadSFU - Console Short Help Page 3 | 4 | 5 | ## Table of Contents 6 | 7 | - [Link for Full Help Site](#link-for-full-help-site) 8 | - [Browser Camera Video Sending](#browser-camera-video-sending) 9 | - [Receiving Video in Browser](#receiving-video-in-browser) 10 | - [Navigation Bar Explainer](#navigation-bar-explainer) 11 | 12 | DeadSFU is designed as a dead-simple SFU and video switch. 13 | [SFU defined here](#sfu-definition) 14 | 15 | ## Link for Full Help Site 16 | 17 | [DeadSFU.com] is where the full documentation can be found. 18 | This page is just the built-in short help. 19 | 20 | 21 | ## Browser Camera Video Sending 22 | 23 | Browser camera sending *requires* HTTPS access to the SFU. 24 | Without HTTPS access to the SFU, camera capture and send will *not* be possible. 25 | 26 | HTTPS access can be setup directly in DeadSFU using the `--https-*` flags, 27 | or by using an HTTPS terminating proxy. 28 | [Caddy], [nginx], and [Traefik] are a few examples of HTTPS terminating proxys. 29 | 30 | can be used to send to the room named 'main'. 31 | the default room is 'main', so this also sends to 'main'. 32 | 33 | ## Receiving Video in Browser 34 | 35 | can be used to view the room: 'main'. 36 | the default room is 'main', so this also views the room: 'main'. 37 | 38 | ## Navigation Bar Explainer 39 | 40 | 47 |
    48 |
  • 49 | 50 | 51 | 52 | 53 | - clickable button to go fullscreen 54 | 55 |
  • 56 |
  • 0/1500 rx/tx kbps - indicates you are sending at 1.5mbps
  • 57 |
  • 1000/0 rx/tx kbps - indicates you are receiving at 1mbps
  • 58 |
  • connected - indicates the connection state
  • 59 |
  • All 2nd line links are clickable room names
  • 60 |
61 | 62 | 63 | 64 | 65 | ## SFU Definition 66 | *SFU* - `Selective Forwarding Unit`. 67 | A WebRTC SFU is a basic building block of WebRTC systems. An SFU acts as an digital amplifier of sorts. 68 | An SFU will receive a media stream, and forward that stream to down-stream receivers. This forwarding/repeating operation may be duplicated and performed for many incoming streams. 69 | 70 | 71 | 72 | [DeadSFU]: https://deadsfu.com 73 | [DeadSFU.com]: https://deadsfu.com 74 | [Markdown]: http://daringfireball.net/projects/markdown/ 75 | [Caddy]: https://caddyserver.com/ 76 | [nginx]: https://www.nginx.com/ 77 | [Traefik]: https://traefik.io/ -------------------------------------------------------------------------------- /html/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 11 | 12 | Console 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 |
31 | 32 | 33 | 34 | 35 | 81 | 82 | 83 | 84 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 |
100 |
102 | 103 | 104 |
105 | 106 | 107 | 108 | 109 | 110 | 113 | 114 | 115 | 116 | 117 | 118 | -------------------------------------------------------------------------------- /html/send/index.html: -------------------------------------------------------------------------------- 1 | ../index.html -------------------------------------------------------------------------------- /internal/disrupt/disrupt.go: -------------------------------------------------------------------------------- 1 | package disrupt 2 | 3 | import ( 4 | "log" 5 | "sync" 6 | "sync/atomic" 7 | "unsafe" 8 | ) 9 | 10 | type nolock struct{} 11 | 12 | func (*nolock) Lock() {} 13 | func (*nolock) Unlock() {} 14 | 15 | const CacheLine = 64 16 | 17 | // SPMC 18 | type Disrupt[T any] struct { 19 | next int64 20 | _2 [CacheLine]byte 21 | len64 int64 22 | _3 [CacheLine]byte 23 | mask64 int64 24 | _4 [CacheLine]byte 25 | cond sync.Cond 26 | _5 [CacheLine]byte 27 | buf []T 28 | _1 [CacheLine]byte 29 | } 30 | 31 | func NewDisrupt[T any](n int) *Disrupt[T] { 32 | 33 | if n == 0 || (n&(n-1)) != 0 { 34 | log.Fatal("require positive power of two") 35 | } 36 | 37 | buf := make([]T, n) 38 | 39 | return &Disrupt[T]{ 40 | cond: sync.Cond{L: &nolock{}}, 41 | buf: buf, 42 | len64: int64(n), 43 | mask64: int64(n - 1), 44 | } 45 | } 46 | 47 | func (d *Disrupt[T]) Close() { 48 | 49 | i := atomic.LoadInt64(&d.next) 50 | atomic.StoreInt64(&d.next, -i) 51 | 52 | d.cond.Broadcast() 53 | //d.cond.Signal() //must uncomment signal below when using this 54 | } 55 | 56 | func (d *Disrupt[T]) Put(v T) { 57 | 58 | i := atomic.LoadInt64(&d.next) 59 | // ix := i % d.len64 60 | ix := i & d.mask64 61 | 62 | if i < 0 { 63 | log.Fatal("closed") 64 | } 65 | 66 | if RaceEnabled { 67 | RaceAcquire(unsafe.Pointer(d)) 68 | //RaceDisable() 69 | } 70 | 71 | d.buf[ix] = v 72 | 73 | if RaceEnabled { 74 | //RaceEnable() 75 | RaceRelease(unsafe.Pointer(d)) 76 | } 77 | 78 | i++ 79 | atomic.StoreInt64(&d.next, i) 80 | 81 | d.cond.Broadcast() 82 | //d.cond.Signal() //must uncomment signal below when using this 83 | 84 | } 85 | 86 | func (d *Disrupt[T]) Get(k int64) (value T, next int64, more bool) { 87 | 88 | //ix := k % d.len64 89 | 90 | again: 91 | 92 | ix := k & d.mask64 93 | 94 | // if k >= i { 95 | // log.Fatal("invalid index too high") 96 | // } 97 | 98 | for j := atomic.LoadInt64(&d.next); k >= j; j = atomic.LoadInt64(&d.next) { 99 | if j < 0 { //closed? 100 | if k >= -j { // no values left 101 | var zeroval T 102 | return zeroval, k, false 103 | } 104 | break 105 | } else { 106 | d.cond.Wait() 107 | //d.cond.Signal() // wake any other waiters, when not using broadcast in Put 108 | } 109 | } 110 | 111 | if RaceEnabled { 112 | RaceAcquire(unsafe.Pointer(d)) 113 | //RaceDisable() 114 | } 115 | 116 | val := d.buf[ix] 117 | 118 | if RaceEnabled { 119 | //RaceEnable() 120 | RaceRelease(unsafe.Pointer(d)) 121 | } 122 | 123 | // did we grab stale data? 124 | 125 | j := atomic.LoadInt64(&d.next) 126 | 127 | if j < 0 { // if closed, fix sign 128 | j = -j 129 | } 130 | if k <= (j - d.len64) { // we read possibly overwritten data 131 | 132 | k++ //discard bad data 133 | goto again 134 | 135 | // val = zeroval 136 | // k = j - 1 137 | } 138 | 139 | k++ 140 | 141 | return val, k, true 142 | 143 | } 144 | -------------------------------------------------------------------------------- /internal/disrupt/disrupt_test.go: -------------------------------------------------------------------------------- 1 | package disrupt 2 | 3 | import ( 4 | "math" 5 | "runtime" 6 | "sync" 7 | "sync/atomic" 8 | "testing" 9 | "time" 10 | 11 | "github.com/stretchr/testify/assert" 12 | "github.com/stretchr/testify/require" 13 | ) 14 | 15 | var A, B int64 16 | 17 | var X, Y uint64 18 | 19 | func BenchmarkLoadInt64(b *testing.B) { 20 | b.RunParallel(func(p *testing.PB) { 21 | k := int64(0) 22 | for p.Next() { 23 | k += atomic.LoadInt64(&A) 24 | } 25 | atomic.StoreInt64(&B, k) 26 | }) 27 | } 28 | 29 | func BenchmarkLoadUInt64(b *testing.B) { 30 | b.RunParallel(func(p *testing.PB) { 31 | k := uint64(0) 32 | for p.Next() { 33 | k += atomic.LoadUint64(&X) 34 | } 35 | atomic.StoreUint64(&Y, k) 36 | }) 37 | } 38 | 39 | func BenchmarkDisrupt(b *testing.B) { 40 | 41 | w := sync.WaitGroup{} 42 | 43 | w.Add(1) 44 | 45 | a := NewDisrupt[int64](int(math.Pow(2, 24))) 46 | 47 | go func() { 48 | 49 | runtime.LockOSThread() 50 | time.Sleep(time.Millisecond) 51 | 52 | for i := int64(0); i < int64(b.N); i++ { 53 | a.Put(i) 54 | } 55 | a.Close() 56 | 57 | w.Done() 58 | 59 | }() 60 | 61 | b.ResetTimer() 62 | 63 | runtime.LockOSThread() 64 | for ix := int64(0); ix < int64(b.N); ix++ { 65 | val, nextix, ok := a.Get(ix) 66 | if int64(val) != ix || nextix != ix+1 || !ok { 67 | b.Fatalf("bad vals ix:%v val:%v nextix:%v ok:%v", ix, val, nextix, ok) 68 | } 69 | } 70 | 71 | b.StopTimer() 72 | 73 | var zero int64 74 | v, ix, isopen := a.Get(int64(b.N)) 75 | require.Equal(b, zero, v) 76 | require.Equal(b, int64(b.N), ix) 77 | require.Equal(b, false, isopen) 78 | 79 | w.Wait() 80 | 81 | } 82 | 83 | var C, D [1500]byte 84 | 85 | func Benchmark1500Copy(b *testing.B) { 86 | for i := 0; i < b.N; i++ { 87 | C = D 88 | 89 | } 90 | } 91 | 92 | func TestDisrupt(t *testing.T) { 93 | 94 | w := sync.WaitGroup{} 95 | 96 | w.Add(1) 97 | 98 | N := int(1e7) 99 | 100 | a := NewDisrupt[int64](int(math.Pow(2, 12))) 101 | 102 | go func() { 103 | 104 | time.Sleep(time.Microsecond) 105 | 106 | for i := int64(0); i < int64(N); i++ { 107 | a.Put(i) 108 | runtime.Gosched() 109 | } 110 | 111 | a.Close() 112 | 113 | w.Done() 114 | 115 | }() 116 | 117 | for i := int64(0); i < int64(N); i++ { 118 | v, k, isopen := a.Get(i) 119 | if int64(v) != i || k != i+1 || !isopen { 120 | require.Equal(t, i, v) 121 | require.Equal(t, i+1, k) 122 | require.Equal(t, true, isopen) 123 | } 124 | } 125 | 126 | v, k, isopen := a.Get(int64(N)) 127 | 128 | var zero int64 129 | 130 | require.Equal(t, zero, v) 131 | require.Equal(t, int64(N), k) 132 | require.Equal(t, false, isopen) 133 | 134 | w.Wait() 135 | 136 | } 137 | 138 | func TestPutGet1(t *testing.T) { 139 | 140 | w := sync.WaitGroup{} 141 | 142 | w.Add(1) 143 | a := NewDisrupt[int](256) 144 | 145 | go func() { 146 | 147 | t0 := time.Now() 148 | 149 | v, i, ok := a.Get(0) 150 | 151 | assert.Equal(t, 99, v) 152 | assert.Equal(t, int64(1), i) 153 | assert.Equal(t, true, ok) 154 | dur := time.Since(t0) 155 | assert.GreaterOrEqual(t, dur, time.Millisecond, ok) 156 | 157 | w.Done() 158 | 159 | }() 160 | 161 | time.Sleep(time.Millisecond) 162 | a.Put(99) 163 | 164 | w.Wait() 165 | 166 | } 167 | -------------------------------------------------------------------------------- /internal/disrupt/norace.go: -------------------------------------------------------------------------------- 1 | //go:build !race 2 | // +build !race 3 | 4 | package disrupt 5 | 6 | import "unsafe" 7 | 8 | const RaceEnabled = false 9 | 10 | func RaceDisable() {} 11 | 12 | func RaceEnable() {} 13 | 14 | func RaceAcquire(p unsafe.Pointer) {} 15 | 16 | func RaceReleaseMerge(p unsafe.Pointer) {} 17 | 18 | func RaceRelease(p unsafe.Pointer) {} 19 | -------------------------------------------------------------------------------- /internal/disrupt/race.go: -------------------------------------------------------------------------------- 1 | //go:build race 2 | // +build race 3 | 4 | package disrupt 5 | 6 | import "unsafe" 7 | 8 | const RaceEnabled = true 9 | 10 | //go:linkname RaceDisable runtime.RaceDisable 11 | func RaceDisable() 12 | 13 | //go:linkname RaceEnable runtime.RaceEnable 14 | func RaceEnable() 15 | 16 | //go:linkname RaceAcquire runtime.RaceAcquire 17 | func RaceAcquire(p unsafe.Pointer) 18 | 19 | //go:linkname RaceReleaseMerge runtime.RaceReleaseMerge 20 | func RaceReleaseMerge(p unsafe.Pointer) 21 | 22 | //go:linkname RaceRelease runtime.RaceRelease 23 | func RaceRelease(p unsafe.Pointer) 24 | -------------------------------------------------------------------------------- /internal/newpeerconn/ducksoup.go: -------------------------------------------------------------------------------- 1 | // This file 2 | // MIT License 3 | // Copyright (c) 2021 CREAM Lab @ IRCAM & FEMTO-ST 4 | // Copyright (c) 2021 x186k developers 5 | 6 | package newpeerconn 7 | 8 | import ( 9 | //"github.com/pion/ice" 10 | //"github.com/pion/webrtc/v3" 11 | 12 | //"github.com/creamlab/ducksoup/helpers" 13 | "github.com/pion/interceptor" 14 | "github.com/pion/interceptor/pkg/nack" 15 | "github.com/pion/interceptor/pkg/report" 16 | "github.com/pion/interceptor/pkg/twcc" 17 | "github.com/pion/sdp/v3" 18 | "github.com/pion/webrtc/v3" 19 | ) 20 | 21 | var videoRTCPFeedback = []webrtc.RTCPFeedback{ 22 | {Type: "goog-remb", Parameter: ""}, 23 | {Type: "ccm", Parameter: "fir"}, 24 | {Type: "nack", Parameter: ""}, 25 | {Type: "nack", Parameter: "pli"}, 26 | {Type: "transport-cc", Parameter: ""}, 27 | } 28 | 29 | var OpusCodecs = []webrtc.RTPCodecParameters{ 30 | { 31 | RTPCodecCapability: webrtc.RTPCodecCapability{ 32 | MimeType: "audio/opus", 33 | ClockRate: 48000, 34 | Channels: 2, 35 | SDPFmtpLine: "minptime=10;useinbandfec=1;stereo=0", 36 | RTCPFeedback: nil}, 37 | PayloadType: 111, 38 | }, 39 | } 40 | var H264Codecs = []webrtc.RTPCodecParameters{ 41 | { 42 | RTPCodecCapability: webrtc.RTPCodecCapability{ 43 | MimeType: "video/H264", 44 | ClockRate: 90000, 45 | Channels: 0, 46 | SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f", 47 | RTCPFeedback: videoRTCPFeedback}, 48 | PayloadType: 102, 49 | }, 50 | { 51 | RTPCodecCapability: webrtc.RTPCodecCapability{ 52 | MimeType: "video/H264", 53 | ClockRate: 90000, 54 | Channels: 0, 55 | SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f", 56 | RTCPFeedback: videoRTCPFeedback}, 57 | PayloadType: 127, 58 | }, 59 | { 60 | RTPCodecCapability: webrtc.RTPCodecCapability{ 61 | MimeType: "video/H264", 62 | ClockRate: 90000, 63 | Channels: 0, 64 | SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", 65 | RTCPFeedback: videoRTCPFeedback}, 66 | PayloadType: 125, 67 | }, 68 | { 69 | RTPCodecCapability: webrtc.RTPCodecCapability{ 70 | MimeType: "video/H264", 71 | ClockRate: 90000, 72 | Channels: 0, 73 | SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f", 74 | RTCPFeedback: videoRTCPFeedback}, 75 | PayloadType: 108, 76 | }, 77 | { 78 | RTPCodecCapability: webrtc.RTPCodecCapability{ 79 | MimeType: "video/H264", 80 | ClockRate: 90000, 81 | Channels: 0, 82 | SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032", 83 | RTCPFeedback: videoRTCPFeedback}, 84 | PayloadType: 123, 85 | }, 86 | } 87 | 88 | func NewWebRTCAPI() (*webrtc.API, error) { 89 | s := webrtc.SettingEngine{} 90 | //s.SetSRTPReplayProtectionWindow(512) 91 | //s.SetICEMulticastDNSMode(ice.MulticastDNSModeDisabled) 92 | m := &webrtc.MediaEngine{} 93 | 94 | // always include opus 95 | for _, c := range OpusCodecs { 96 | if err := m.RegisterCodec(c, webrtc.RTPCodecTypeAudio); err != nil { 97 | return nil, err 98 | } 99 | } 100 | 101 | // select video codecs 102 | // for _, c := range VP8Codecs { 103 | // if err := m.RegisterCodec(c, webrtc.RTPCodecTypeVideo); err != nil { 104 | // return nil, err 105 | // } 106 | // } 107 | for _, c := range H264Codecs { 108 | if err := m.RegisterCodec(c, webrtc.RTPCodecTypeVideo); err != nil { 109 | return nil, err 110 | } 111 | } 112 | 113 | i := &interceptor.Registry{} 114 | 115 | if err := registerInterceptors(m, i); err != nil { 116 | return nil, err 117 | } 118 | 119 | return webrtc.NewAPI( 120 | webrtc.WithSettingEngine(s), 121 | webrtc.WithMediaEngine(m), 122 | webrtc.WithInterceptorRegistry(i), 123 | ), nil 124 | } 125 | 126 | // adapted from https://github.com/pion/webrtc/blob/v3.1.2/interceptor.go 127 | func registerInterceptors(mediaEngine *webrtc.MediaEngine, interceptorRegistry *interceptor.Registry) error { 128 | if err := configureNack(mediaEngine, interceptorRegistry); err != nil { 129 | return err 130 | } 131 | 132 | if err := configureRTCPReports(interceptorRegistry); err != nil { 133 | return err 134 | } 135 | 136 | if true { 137 | if err := configureTWCCHeaderExtension(mediaEngine, interceptorRegistry); err != nil { 138 | return err 139 | } 140 | 141 | if err := configureTWCCSender(mediaEngine, interceptorRegistry); err != nil { 142 | return err 143 | } 144 | } 145 | 146 | // if err := configureAbsSendTimeHeaderExtension(mediaEngine, interceptorRegistry); err != nil { 147 | // return err 148 | // } 149 | 150 | // if err := configureSDESHeaderExtension(mediaEngine, interceptorRegistry); err != nil { 151 | // return err 152 | // } 153 | 154 | return nil 155 | } 156 | 157 | // ConfigureRTCPReports will setup everything necessary for generating Sender and Receiver Reports 158 | func configureRTCPReports(interceptorRegistry *interceptor.Registry) error { 159 | receiver, err := report.NewReceiverInterceptor() 160 | if err != nil { 161 | return err 162 | } 163 | 164 | sender, err := report.NewSenderInterceptor() 165 | if err != nil { 166 | return err 167 | } 168 | 169 | interceptorRegistry.Add(receiver) 170 | interceptorRegistry.Add(sender) 171 | return nil 172 | } 173 | 174 | // ConfigureNack will setup everything necessary for handling generating/responding to nack messages. 175 | func configureNack(mediaEngine *webrtc.MediaEngine, interceptorRegistry *interceptor.Registry) error { 176 | generator, err := nack.NewGeneratorInterceptor() 177 | if err != nil { 178 | return err 179 | } 180 | 181 | responder, err := nack.NewResponderInterceptor() 182 | if err != nil { 183 | return err 184 | } 185 | 186 | mediaEngine.RegisterFeedback(webrtc.RTCPFeedback{Type: "nack"}, webrtc.RTPCodecTypeVideo) 187 | mediaEngine.RegisterFeedback(webrtc.RTCPFeedback{Type: "nack", Parameter: "pli"}, webrtc.RTPCodecTypeVideo) 188 | interceptorRegistry.Add(responder) 189 | interceptorRegistry.Add(generator) 190 | return nil 191 | } 192 | 193 | // ConfigureTWCCHeaderExtensionSender will setup everything necessary for adding 194 | // a TWCC header extension to outgoing RTP packets. This will allow the remote peer to generate TWCC reports. 195 | func configureTWCCHeaderExtension(mediaEngine *webrtc.MediaEngine, interceptorRegistry *interceptor.Registry) error { 196 | if err := mediaEngine.RegisterHeaderExtension( 197 | webrtc.RTPHeaderExtensionCapability{URI: sdp.TransportCCURI}, webrtc.RTPCodecTypeVideo, 198 | ); err != nil { 199 | return err 200 | } 201 | 202 | if err := mediaEngine.RegisterHeaderExtension( 203 | webrtc.RTPHeaderExtensionCapability{URI: sdp.TransportCCURI}, webrtc.RTPCodecTypeAudio, 204 | ); err != nil { 205 | return err 206 | } 207 | 208 | i, err := twcc.NewHeaderExtensionInterceptor() 209 | if err != nil { 210 | return err 211 | } 212 | 213 | interceptorRegistry.Add(i) 214 | return nil 215 | } 216 | 217 | // ConfigureTWCCSender will setup everything necessary for generating TWCC reports. 218 | func configureTWCCSender(mediaEngine *webrtc.MediaEngine, interceptorRegistry *interceptor.Registry) error { 219 | mediaEngine.RegisterFeedback(webrtc.RTCPFeedback{Type: webrtc.TypeRTCPFBTransportCC}, webrtc.RTPCodecTypeVideo) 220 | mediaEngine.RegisterFeedback(webrtc.RTCPFeedback{Type: webrtc.TypeRTCPFBTransportCC}, webrtc.RTPCodecTypeAudio) 221 | 222 | generator, err := twcc.NewSenderInterceptor() 223 | if err != nil { 224 | return err 225 | } 226 | 227 | interceptorRegistry.Add(generator) 228 | return nil 229 | } 230 | 231 | // For more accurante REMB reports 232 | func configureAbsSendTimeHeaderExtension(mediaEngine *webrtc.MediaEngine, interceptorRegistry *interceptor.Registry) error { 233 | 234 | if err := mediaEngine.RegisterHeaderExtension( 235 | webrtc.RTPHeaderExtensionCapability{URI: sdp.ABSSendTimeURI}, webrtc.RTPCodecTypeVideo, 236 | ); err != nil { 237 | return err 238 | } 239 | 240 | if err := mediaEngine.RegisterHeaderExtension( 241 | webrtc.RTPHeaderExtensionCapability{URI: sdp.ABSSendTimeURI}, webrtc.RTPCodecTypeAudio, 242 | ); err != nil { 243 | return err 244 | } 245 | 246 | return nil 247 | } 248 | 249 | func configureSDESHeaderExtension(mediaEngine *webrtc.MediaEngine, interceptorRegistry *interceptor.Registry) error { 250 | 251 | if err := mediaEngine.RegisterHeaderExtension( 252 | webrtc.RTPHeaderExtensionCapability{URI: sdp.SDESMidURI}, 253 | webrtc.RTPCodecTypeVideo, 254 | ); err != nil { 255 | return err 256 | } 257 | 258 | if err := mediaEngine.RegisterHeaderExtension( 259 | webrtc.RTPHeaderExtensionCapability{URI: sdp.SDESRTPStreamIDURI}, 260 | webrtc.RTPCodecTypeVideo, 261 | ); err != nil { 262 | return err 263 | } 264 | 265 | if err := mediaEngine.RegisterHeaderExtension( 266 | webrtc.RTPHeaderExtensionCapability{URI: sdp.SDESMidURI}, 267 | webrtc.RTPCodecTypeAudio, 268 | ); err != nil { 269 | return err 270 | } 271 | 272 | if err := mediaEngine.RegisterHeaderExtension( 273 | webrtc.RTPHeaderExtensionCapability{URI: sdp.SDESRTPStreamIDURI}, 274 | webrtc.RTPCodecTypeAudio, 275 | ); err != nil { 276 | return err 277 | } 278 | 279 | return nil 280 | } 281 | -------------------------------------------------------------------------------- /internal/newpeerconn/ion-mediaengine.go: -------------------------------------------------------------------------------- 1 | package newpeerconn 2 | 3 | //portions of this: MIT License, Copyright (c) 2019, the ion sfu project 4 | 5 | import ( 6 | "github.com/pion/sdp/v3" 7 | "github.com/pion/webrtc/v3" 8 | ) 9 | 10 | const ( 11 | mimeTypeH264 = "video/h264" 12 | mimeTypeOpus = "audio/opus" 13 | ) 14 | 15 | const frameMarking = "urn:ietf:params:rtp-hdrext:framemarking" 16 | 17 | var _ = getPublisherMediaEngine 18 | 19 | func getPublisherMediaEngine() (*webrtc.MediaEngine, error) { 20 | me := &webrtc.MediaEngine{} 21 | if err := me.RegisterCodec(webrtc.RTPCodecParameters{ 22 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: mimeTypeOpus, ClockRate: 48000, Channels: 2, SDPFmtpLine: "minptime=10;useinbandfec=1", RTCPFeedback: nil}, 23 | PayloadType: 111, 24 | }, webrtc.RTPCodecTypeAudio); err != nil { 25 | return nil, err 26 | } 27 | 28 | videoRTCPFeedback := []webrtc.RTCPFeedback{ 29 | {Type: "goog-remb", Parameter: ""}, 30 | {Type: "ccm", Parameter: "fir"}, 31 | {Type: "nack", Parameter: ""}, 32 | {Type: "nack", Parameter: "pli"}, 33 | {Type: "transport-cc", Parameter: ""}, 34 | } 35 | 36 | for _, codec := range []webrtc.RTPCodecParameters{ 37 | // { 38 | // RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: mimeTypeVP8, ClockRate: 90000, RTCPFeedback: videoRTCPFeedback}, 39 | // PayloadType: 96, 40 | // }, 41 | // { 42 | // RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: mimeTypeVP9, ClockRate: 90000, SDPFmtpLine: "profile-id=0", RTCPFeedback: videoRTCPFeedback}, 43 | // PayloadType: 98, 44 | // }, 45 | // { 46 | // RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: mimeTypeVP9, ClockRate: 90000, SDPFmtpLine: "profile-id=1", RTCPFeedback: videoRTCPFeedback}, 47 | // PayloadType: 100, 48 | // }, 49 | { 50 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: mimeTypeH264, ClockRate: 90000, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f", RTCPFeedback: videoRTCPFeedback}, 51 | PayloadType: 102, 52 | }, 53 | { 54 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: mimeTypeH264, ClockRate: 90000, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f", RTCPFeedback: videoRTCPFeedback}, 55 | PayloadType: 127, 56 | }, 57 | { 58 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: mimeTypeH264, ClockRate: 90000, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", RTCPFeedback: videoRTCPFeedback}, 59 | PayloadType: 125, 60 | }, 61 | { 62 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: mimeTypeH264, ClockRate: 90000, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f", RTCPFeedback: videoRTCPFeedback}, 63 | PayloadType: 108, 64 | }, 65 | { 66 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: mimeTypeH264, ClockRate: 90000, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032", RTCPFeedback: videoRTCPFeedback}, 67 | PayloadType: 123, 68 | }, 69 | } { 70 | if err := me.RegisterCodec(codec, webrtc.RTPCodecTypeVideo); err != nil { 71 | return nil, err 72 | } 73 | } 74 | 75 | for _, extension := range []string{ 76 | sdp.SDESMidURI, 77 | sdp.SDESRTPStreamIDURI, 78 | sdp.TransportCCURI, 79 | frameMarking, 80 | } { 81 | if err := me.RegisterHeaderExtension(webrtc.RTPHeaderExtensionCapability{URI: extension}, webrtc.RTPCodecTypeVideo); err != nil { 82 | return nil, err 83 | } 84 | } 85 | for _, extension := range []string{ 86 | sdp.SDESMidURI, 87 | sdp.SDESRTPStreamIDURI, 88 | sdp.AudioLevelURI, 89 | } { 90 | if err := me.RegisterHeaderExtension(webrtc.RTPHeaderExtensionCapability{URI: extension}, webrtc.RTPCodecTypeAudio); err != nil { 91 | return nil, err 92 | } 93 | } 94 | 95 | return me, nil 96 | } 97 | 98 | var _ = getSubscriberMediaEngine 99 | 100 | func getSubscriberMediaEngine() (*webrtc.MediaEngine, error) { 101 | me := &webrtc.MediaEngine{} 102 | return me, nil 103 | } 104 | -------------------------------------------------------------------------------- /internal/newpeerconn/pion.go: -------------------------------------------------------------------------------- 1 | package newpeerconn 2 | 3 | import ( 4 | _ "io" 5 | //"net/http/httputil" 6 | //"github.com/davecgh/go-spew/spew" 7 | //"github.com/digitalocean/godo" 8 | "github.com/pion/webrtc/v3" 9 | ) 10 | 11 | //how I fixed structs 12 | // RTPCodecCapability\{([^,]*),([^,]*),([^,]*),([^,]*),([^,]*)\} 13 | // RTPCodecCapability{MimeType:$1,ClockRate:$2,Channels:$3,SDPFmtpLine:$4,RTCPFeedback:$5} 14 | 15 | func RegisterH264AndOpusCodecs(m *webrtc.MediaEngine) error { 16 | // Default Pion Audio Codecs 17 | 18 | for _, codec := range []webrtc.RTPCodecParameters{ 19 | { 20 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeOpus, ClockRate: 48000, Channels: 2, SDPFmtpLine: "minptime=10;useinbandfec=1", RTCPFeedback: nil}, 21 | PayloadType: 111, 22 | }, 23 | // { 24 | // RTPCodecCapability: webrtc.RTPCodecCapability{MimeType:webrtc.MimeTypeG722,ClockRate: 8000,Channels: 0,SDPFmtpLine: "",RTCPFeedback: nil}, 25 | // PayloadType: 9, 26 | // }, 27 | // { 28 | // RTPCodecCapability: webrtc.RTPCodecCapability{MimeType:webrtc.MimeTypePCMU,ClockRate: 8000,Channels: 0,SDPFmtpLine: "",RTCPFeedback: nil}, 29 | // PayloadType: 0, 30 | // }, 31 | // { 32 | // RTPCodecCapability: webrtc.RTPCodecCapability{MimeType:webrtc.MimeTypePCMA,ClockRate: 8000,Channels: 0,SDPFmtpLine: "",RTCPFeedback: nil}, 33 | // PayloadType: 8, 34 | // }, 35 | } { 36 | if err := m.RegisterCodec(codec, webrtc.RTPCodecTypeAudio); err != nil { 37 | return err 38 | } 39 | } 40 | 41 | // Default Pion Audio Header Extensions 42 | for _, extension := range []string{ 43 | "urn:ietf:params:rtp-hdrext:sdes:mid", 44 | "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id", 45 | "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id", 46 | } { 47 | if err := m.RegisterHeaderExtension(webrtc.RTPHeaderExtensionCapability{URI: extension}, webrtc.RTPCodecTypeAudio); err != nil { 48 | return err 49 | } 50 | } 51 | 52 | videoRTCPFeedback := []webrtc.RTCPFeedback{{Type: "goog-remb", Parameter: ""}, {Type: "ccm", Parameter: "fir"}, {Type: "nack", Parameter: ""}, {Type: "nack", Parameter: "pli"}} 53 | for _, codec := range []webrtc.RTPCodecParameters{ 54 | // { 55 | // RTPCodecCapability: webrtc.RTPCodecCapability{MimeType:webrtc.MimeTypeVP8,ClockRate: 90000,Channels: 0,SDPFmtpLine: "",RTCPFeedback: videoRTCPFeedback}, 56 | // PayloadType: 96, 57 | // }, 58 | // { 59 | // RTPCodecCapability: webrtc.RTPCodecCapability{MimeType:"video/rtx",ClockRate: 90000,Channels: 0,SDPFmtpLine: "apt=96",RTCPFeedback: nil}, 60 | // PayloadType: 97, 61 | // }, 62 | 63 | // { 64 | // RTPCodecCapability: webrtc.RTPCodecCapability{MimeType:webrtc.MimeTypeVP9,ClockRate: 90000,Channels: 0,SDPFmtpLine: "profile-id=0",RTCPFeedback: videoRTCPFeedback}, 65 | // PayloadType: 98, 66 | // }, 67 | // { 68 | // RTPCodecCapability: webrtc.RTPCodecCapability{MimeType:"video/rtx",ClockRate: 90000,Channels: 0,SDPFmtpLine: "apt=98",RTCPFeedback: nil}, 69 | // PayloadType: 99, 70 | // }, 71 | 72 | // { 73 | // RTPCodecCapability: webrtc.RTPCodecCapability{MimeType:webrtc.MimeTypeVP9,ClockRate: 90000,Channels: 0,SDPFmtpLine: "profile-id=1",RTCPFeedback: videoRTCPFeedback}, 74 | // PayloadType: 100, 75 | // }, 76 | // { 77 | // RTPCodecCapability: webrtc.RTPCodecCapability{MimeType:"video/rtx",ClockRate: 90000,Channels: 0,SDPFmtpLine: "apt=100",RTCPFeedback: nil}, 78 | // PayloadType: 101, 79 | // }, 80 | 81 | { 82 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264, ClockRate: 90000, Channels: 0, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f", RTCPFeedback: videoRTCPFeedback}, 83 | PayloadType: 102, 84 | }, 85 | { 86 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: "video/rtx", ClockRate: 90000, Channels: 0, SDPFmtpLine: "apt=102", RTCPFeedback: nil}, 87 | PayloadType: 121, 88 | }, 89 | 90 | { 91 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264, ClockRate: 90000, Channels: 0, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f", RTCPFeedback: videoRTCPFeedback}, 92 | PayloadType: 127, 93 | }, 94 | { 95 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: "video/rtx", ClockRate: 90000, Channels: 0, SDPFmtpLine: "apt=127", RTCPFeedback: nil}, 96 | PayloadType: 120, 97 | }, 98 | 99 | { 100 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264, ClockRate: 90000, Channels: 0, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f", RTCPFeedback: videoRTCPFeedback}, 101 | PayloadType: 125, 102 | }, 103 | { 104 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: "video/rtx", ClockRate: 90000, Channels: 0, SDPFmtpLine: "apt=125", RTCPFeedback: nil}, 105 | PayloadType: 107, 106 | }, 107 | 108 | { 109 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264, ClockRate: 90000, Channels: 0, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f", RTCPFeedback: videoRTCPFeedback}, 110 | PayloadType: 108, 111 | }, 112 | { 113 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: "video/rtx", ClockRate: 90000, Channels: 0, SDPFmtpLine: "apt=108", RTCPFeedback: nil}, 114 | PayloadType: 109, 115 | }, 116 | 117 | { 118 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264, ClockRate: 90000, Channels: 0, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f", RTCPFeedback: videoRTCPFeedback}, 119 | PayloadType: 127, 120 | }, 121 | { 122 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: "video/rtx", ClockRate: 90000, Channels: 0, SDPFmtpLine: "apt=127", RTCPFeedback: nil}, 123 | PayloadType: 120, 124 | }, 125 | 126 | { 127 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264, ClockRate: 90000, Channels: 0, SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640032", RTCPFeedback: videoRTCPFeedback}, 128 | PayloadType: 123, 129 | }, 130 | { 131 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: "video/rtx", ClockRate: 90000, Channels: 0, SDPFmtpLine: "apt=123", RTCPFeedback: nil}, 132 | PayloadType: 118, 133 | }, 134 | 135 | { 136 | RTPCodecCapability: webrtc.RTPCodecCapability{MimeType: "video/ulpfec", ClockRate: 90000, Channels: 0, SDPFmtpLine: "", RTCPFeedback: nil}, 137 | PayloadType: 116, 138 | }, 139 | } { 140 | if err := m.RegisterCodec(codec, webrtc.RTPCodecTypeVideo); err != nil { 141 | return err 142 | } 143 | } 144 | 145 | // Default Pion Video Header Extensions 146 | for _, extension := range []string{ 147 | "urn:ietf:params:rtp-hdrext:sdes:mid", 148 | "urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id", 149 | "urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id", 150 | } { 151 | if err := m.RegisterHeaderExtension(webrtc.RTPHeaderExtensionCapability{URI: extension}, webrtc.RTPCodecTypeVideo); err != nil { 152 | return err 153 | } 154 | } 155 | 156 | return nil 157 | } 158 | -------------------------------------------------------------------------------- /internal/sfu/basic_test.go: -------------------------------------------------------------------------------- 1 | package sfu 2 | 3 | import ( 4 | "fmt" 5 | "hash/crc64" 6 | "io" 7 | "sync/atomic" 8 | 9 | "net/http/httptest" 10 | 11 | "strings" 12 | "testing" 13 | "time" 14 | 15 | "github.com/pion/webrtc/v3" 16 | "github.com/pion/webrtc/v3/pkg/media" 17 | "github.com/stretchr/testify/assert" 18 | "github.com/stretchr/testify/require" 19 | //this is kinda weird, but works, so we use it. 20 | "github.com/x186k/deadsfu" 21 | ) 22 | 23 | // func TestMain(m *testing.M) { 24 | 25 | // // call flag.Parse() here if TestMain uses flags 26 | 27 | // log.SetOutput(ioutil.Discard) 28 | // log.SetFlags(0) 29 | 30 | // os.Exit(m.Run()) 31 | // } 32 | 33 | var rtcconf = webrtc.Configuration{ 34 | ICEServers: []webrtc.ICEServer{ 35 | { 36 | URLs: []string{"stun:stun.l.google.com:19302"}, 37 | }, 38 | }, 39 | } 40 | 41 | var tab = crc64.MakeTable(crc64.ISO) 42 | 43 | func calccrc(p []byte) uint64 { 44 | a := uint64(0) 45 | // a = crc64.Update(0, tab, p[0:1]) 46 | // a = crc64.Update(a, tab, p[2:8]) 47 | a = crc64.Update(a, tab, p[12:]) 48 | return a 49 | } 50 | 51 | var pkthash map[uint64]int = make(map[uint64]int) 52 | 53 | func TestPubSub(t *testing.T) { 54 | //log.SetOutput(ioutil.Discard) 55 | //log.SetFlags(0) 56 | 57 | fake := append(make([]byte, 12), spspps...) 58 | 59 | pkthash[calccrc(fake)] = 2 60 | 61 | p := readRTPFromZip(deadsfu.IdleClipZipBytes) 62 | 63 | for _, v := range p { 64 | raw, err := v.Marshal() 65 | checkFatal(err) 66 | crc := calccrc(raw) 67 | 68 | pkthash[crc] = 1 69 | 70 | } 71 | 72 | //initStateAndGoroutines() 73 | 74 | pc, err := webrtc.NewPeerConnection(rtcconf) 75 | checkFatal(err) 76 | 77 | var numvid int32 = 0 78 | 79 | pc.OnTrack(func(track *webrtc.TrackRemote, receiver *webrtc.RTPReceiver) { 80 | _ = receiver 81 | //panic("--ontrack") 82 | 83 | mimetype := track.Codec().MimeType 84 | 85 | for { 86 | p, _, err := track.ReadRTP() 87 | if err == io.EOF { 88 | return 89 | } 90 | checkFatal(err) 91 | 92 | _ = fmt.Print 93 | _ = p 94 | raw, err := p.Marshal() 95 | checkFatal(err) 96 | found := pkthash[calccrc(raw)] 97 | fmt.Printf(" rx test-pc %v %v %v\n", mimetype, len(p.Payload), found) 98 | if mimetype == "video/H264" { 99 | atomic.AddInt32(&numvid, 1) 100 | } 101 | } 102 | }) 103 | 104 | ro := webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionRecvonly} 105 | // create transceivers for 1x audio, 3x video 106 | _, err = pc.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio, ro) 107 | checkFatal(err) 108 | _, err = pc.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo, ro) 109 | checkFatal(err) 110 | 111 | //vt, err := webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: "video/h264"}, "video", "pion") 112 | //checkFatal(err) 113 | 114 | // track, err := webrtc.NewTrackLocalStaticRTP(webrtc.RTPCodecCapability{MimeType: audioMimeType}, "audio", mediaStreamId) 115 | // checkFatal(err) 116 | // rtpSender, err := pc.AddTrack(vt) 117 | // checkFatal(err) 118 | // go processRTCP(rtpSender) 119 | 120 | pc.OnICEConnectionStateChange(func(s webrtc.ICEConnectionState) { println("ICEConnection", s.String()) }) 121 | pc.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { println("Connection", s.String()) }) 122 | 123 | offer, err := pc.CreateOffer(nil) 124 | checkFatal(err) 125 | 126 | //logSdpReport("dialupstream-offer", offer) 127 | gatherComplete := webrtc.GatheringCompletePromise(pc) 128 | err = pc.SetLocalDescription(offer) //start ICE 129 | checkFatal(err) 130 | <-gatherComplete 131 | 132 | req := httptest.NewRequest("POST", "http://ignored.com/sub", strings.NewReader(offer.SDP)) 133 | w := httptest.NewRecorder() 134 | req.Header.Set("Content-Type", "application/sdp") 135 | subHandler(w, req) // not super clean, but \_(ツ)_/¯ 136 | resp := w.Result() 137 | answerraw, _ := io.ReadAll(resp.Body) 138 | assert.Equal(t, 201, resp.StatusCode) 139 | assert.Equal(t, "application/sdp", resp.Header.Get("Content-Type")) 140 | ans := webrtc.SessionDescription{Type: webrtc.SDPTypeAnswer, SDP: string(answerraw)} 141 | assert.True(t, ValidateSDP(ans)) 142 | err = pc.SetRemoteDescription(ans) 143 | checkFatal(err) 144 | 145 | //t.FailNow() 146 | time.Sleep(time.Second * 2) 147 | require.True(t, atomic.LoadInt32(&numvid) > 0) 148 | 149 | println("ok, got idle video okay") 150 | 151 | go startMultiTrackPublisher(t) 152 | 153 | for { 154 | time.Sleep(time.Second) 155 | err := video1.WriteSample(media.Sample{Data: spspps, Duration: time.Second}) 156 | if err != nil && err != io.ErrClosedPipe { 157 | panic(err) 158 | } 159 | } 160 | //select {} 161 | } 162 | 163 | var spspps = []byte{ 164 | 0x78, 0x00, 0x10, 0x67, 0x42, 0xc0, 0x1f, 0x43, 165 | 0x23, 0x50, 0x14, 0x05, 0xef, 0x2c, 0x03, 0xc2, 166 | 0x21, 0x1a, 0x80, 0x00, 0x04, 0x68, 0x48, 0xe3, 167 | 0xc8, 168 | } 169 | 170 | var video1 *webrtc.TrackLocalStaticSample 171 | 172 | func startMultiTrackPublisher(t *testing.T) { 173 | 174 | pc, err := webrtc.NewPeerConnection(rtcconf) 175 | checkFatal(err) 176 | 177 | // so := webrtc.RTPTransceiverInit{Direction: webrtc.RTPTransceiverDirectionSendrecv} 178 | // // create transceivers for 1x audio, 3x video 179 | // _, err = pc.AddTransceiverFromKind(webrtc.RTPCodecTypeAudio, so) 180 | // checkFatal(err) 181 | // _, err = pc.AddTransceiverFromKind(webrtc.RTPCodecTypeVideo, so) 182 | // checkFatal(err) 183 | 184 | video1, err = webrtc.NewTrackLocalStaticSample(webrtc.RTPCodecCapability{MimeType: "video/h264", SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f"}, "1", "1") 185 | checkFatal(err) 186 | 187 | rtpSender, err := pc.AddTrack(video1) 188 | checkFatal(err) 189 | go processRTCP(rtpSender) 190 | 191 | go func() { 192 | 193 | }() 194 | 195 | pc.OnICEConnectionStateChange(func(s webrtc.ICEConnectionState) { println("pub-ICEConnection", s.String()) }) 196 | pc.OnConnectionStateChange(func(s webrtc.PeerConnectionState) { println("pub-Connection", s.String()) }) 197 | 198 | offer, err := pc.CreateOffer(nil) 199 | checkFatal(err) 200 | 201 | //logSdpReport("dialupstream-offer", offer) 202 | gatherComplete := webrtc.GatheringCompletePromise(pc) 203 | err = pc.SetLocalDescription(offer) //start ICE 204 | checkFatal(err) 205 | <-gatherComplete 206 | 207 | req := httptest.NewRequest("POST", "http://ignored.com/pub", strings.NewReader(offer.SDP)) 208 | w := httptest.NewRecorder() 209 | req.Header.Set("Content-Type", "application/sdp") 210 | //pubHandler(w, req) // not super clean, but \_(ツ)_/¯ 211 | resp := w.Result() 212 | answerraw, _ := io.ReadAll(resp.Body) 213 | assert.Equal(t, 201, resp.StatusCode) 214 | assert.Equal(t, "application/sdp", resp.Header.Get("Content-Type")) 215 | ans := webrtc.SessionDescription{Type: webrtc.SDPTypeAnswer, SDP: string(answerraw)} 216 | assert.True(t, ValidateSDP(ans)) 217 | err = pc.SetRemoteDescription(ans) 218 | checkFatal(err) 219 | 220 | // go func() { 221 | // tk := time.NewTicker(time.Second / 2) 222 | 223 | // for range tk.C { 224 | // //RACEY 225 | // //println(98, txtracks[0].pending, txtracks[0].rxid) 226 | // //println(99, txtracks[1].pending, txtracks[1].rxid) 227 | // } 228 | // }() 229 | 230 | } 231 | -------------------------------------------------------------------------------- /internal/sfu/broker_test.go: -------------------------------------------------------------------------------- 1 | package sfu 2 | 3 | import ( 4 | //"sync" 5 | //"sync" 6 | "log" 7 | "os" 8 | "reflect" 9 | "testing" 10 | "time" 11 | "unsafe" 12 | 13 | "github.com/pion/rtp" 14 | //"github.com/x186k/deadsfu" 15 | ) 16 | 17 | // c@macmini ~/D/deadsfu (main) [1]> go test -bench='^BenchmarkBrokerWithWriter' . -run='^$' . 18 | // goos: darwin 19 | // goarch: amd64 20 | // pkg: github.com/x186k/deadsfu 21 | // cpu: Intel(R) Core(TM) i5-8500B CPU @ 3.00GHz 22 | // BenchmarkBrokerWithWriter1PairsPool-6 4031882 302.8 ns/op 302.8 ns/write 33 B/op 0 allocs/op 23 | // BenchmarkBrokerWithWriter10PairsPool-6 3100899 422.4 ns/op 42.24 ns/write 33 B/op 0 allocs/op 24 | // BenchmarkBrokerWithWriter100PairsPool-6 693356 1987 ns/op 19.87 ns/write 32 B/op 0 allocs/op 25 | // BenchmarkBrokerWithWriter1000PairsPool-6 87928 20177 ns/op 20.18 ns/write 15 B/op 0 allocs/op 26 | // BenchmarkBrokerWithWriter1PairsNoPool-6 3432055 359.8 ns/op 359.8 ns/write 181 B/op 2 allocs/op 27 | // BenchmarkBrokerWithWriter10PairsNoPool-6 2623671 438.1 ns/op 43.81 ns/write 181 B/op 2 allocs/op 28 | // BenchmarkBrokerWithWriter100PairsNoPool-6 609979 1992 ns/op 19.92 ns/write 181 B/op 2 allocs/op 29 | // BenchmarkBrokerWithWriter1000PairsNoPool-6 56983 20324 ns/op 20.32 ns/write 182 B/op 2 allocs/op 30 | // PASS 31 | // ok github.com/x186k/deadsfu 12.716s 32 | 33 | var N int 34 | 35 | func TestMain(m *testing.M) { 36 | idleMediaPackets = idleMediaLoader() 37 | log.SetFlags(log.Lshortfile) 38 | os.Exit(m.Run()) 39 | } 40 | 41 | // var pool = sync.Pool{ 42 | // // New optionally specifies a function to generate 43 | // // a value when Get would otherwise return nil. 44 | // New: func() interface{} { return new(XPacket) }, 45 | // } 46 | 47 | func BenchmarkBrokerNoWrite(b *testing.B) { 48 | 49 | a := NewXBroker() 50 | go a.Start() 51 | 52 | c := a.Subscribe() 53 | 54 | d := make(chan struct{}) 55 | go func() { 56 | 57 | // for zz := range c { 58 | 59 | // //pp.Put(zz) 60 | // N += int(zz.Typ) 61 | // } 62 | close(d) 63 | }() 64 | 65 | //println(88,b.N) 66 | b.ReportAllocs() 67 | b.ResetTimer() 68 | 69 | for i := 0; i < b.N; i++ { 70 | //p := pp.Get().(*XPacket) 71 | p := XPacket{} 72 | p.Typ = Video 73 | if i%200 == 0 { 74 | p.Keyframe = true 75 | } else { 76 | p.Keyframe = false 77 | } 78 | a.Publish(&p) 79 | 80 | } 81 | b.StopTimer() 82 | a.RemoveClose(c) 83 | 84 | <-d 85 | } 86 | 87 | type DummyWriter struct { 88 | //nwrite int 89 | } 90 | 91 | func (z *DummyWriter) WriteRTP(p *rtp.Packet) error { 92 | //z.nwrite++ 93 | return nil 94 | } 95 | 96 | func BenchmarkBrokerWithWriter1PairsNoPool(b *testing.B) { 97 | benchmarkBrokerWithWriter(b, 1) 98 | } 99 | func BenchmarkBrokerWithWriter10PairsNoPool(b *testing.B) { 100 | benchmarkBrokerWithWriter(b, 10) 101 | } 102 | func BenchmarkBrokerWithWriter100PairsNoPool(b *testing.B) { 103 | benchmarkBrokerWithWriter(b, 100) 104 | } 105 | func BenchmarkBrokerWithWriter1000PairsNoPool(b *testing.B) { 106 | benchmarkBrokerWithWriter(b, 1000) 107 | } 108 | 109 | func benchmarkBrokerWithWriter(b *testing.B, numwrites int) { 110 | 111 | trks := NewTxTracks() 112 | 113 | for i := 0; i < numwrites; i++ { 114 | vidwriter := &DummyWriter{} 115 | pair := TxTrackPair{ 116 | aud: TxTrack{ 117 | track: vidwriter, 118 | splicer: RtpSplicer{}, 119 | clockrate: 48000, 120 | }, 121 | vid: TxTrack{ 122 | track: vidwriter, 123 | splicer: RtpSplicer{}, 124 | clockrate: 90000, 125 | }, 126 | } 127 | trks.Add(&pair) 128 | } 129 | 130 | a := NewXBroker() 131 | go a.Start() 132 | ch := a.Subscribe() 133 | go Writer(ch, trks, "test") 134 | 135 | b.ReportAllocs() 136 | b.ResetTimer() 137 | 138 | var p *XPacket 139 | p = &XPacket{ 140 | Arrival: 0, 141 | Pkt: rtp.Packet{}, 142 | Typ: 0, 143 | Keyframe: false, 144 | } 145 | 146 | // if usepool { 147 | // p = xpacketPool.Get().(*XPacket) 148 | // *p = XPacket{ 149 | // arrival: 0, 150 | // pkt: rtp.Packet{}, 151 | // typ: 0, 152 | // keyframe: false, 153 | // } 154 | // } 155 | 156 | for i := 0; i < b.N; i++ { 157 | 158 | p = &XPacket{ 159 | Arrival: 0, 160 | Pkt: rtp.Packet{}, 161 | Typ: Video, 162 | Keyframe: false, 163 | } 164 | 165 | p.Keyframe = i%200 == 0 166 | 167 | a.Publish(p) 168 | 169 | } 170 | b.StopTimer() 171 | a.RemoveClose(ch) 172 | 173 | time.Sleep(time.Millisecond * 5) // wait for drain 174 | 175 | durintf := GetUnexportedField(reflect.ValueOf(b).Elem().FieldByName("duration")) 176 | duration := durintf.(time.Duration) 177 | 178 | b.ReportMetric(float64(duration)/float64(b.N)/float64(numwrites), "ns/write") 179 | 180 | //data race 181 | // nn:=0 182 | // for k:=range trks.live{ 183 | // z:=k.vid.track.(*Z) 184 | // nn+=z.nwrite 185 | // } 186 | // log.Println("num writes", nn) 187 | } 188 | 189 | func GetUnexportedField(field reflect.Value) interface{} { 190 | return reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Interface() 191 | } 192 | -------------------------------------------------------------------------------- /internal/sfu/ddnsutil.go: -------------------------------------------------------------------------------- 1 | package sfu 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "net" 7 | "strings" 8 | "time" 9 | 10 | "github.com/libdns/libdns" 11 | "github.com/miekg/dns" 12 | ) 13 | 14 | // Copyright 2015 Matthew Holt 15 | // Copyright 2021 Cameron Elliott 16 | 17 | // 18 | // Licensed under the Apache License, Version 2.0 (the "License"); 19 | // you may not use this file except in compliance with the License. 20 | // You may obtain a copy of the License at 21 | // 22 | // http://www.apache.org/licenses/LICENSE-2.0 23 | // 24 | // Unless required by applicable law or agreed to in writing, software 25 | // distributed under the License is distributed on an "AS IS" BASIS, 26 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 27 | // See the License for the specific language governing permissions and 28 | // limitations under the License. 29 | 30 | // DDNSProvider defines the set of operations required for 31 | // ACME challenges. A DNS provider must be able to append and 32 | // delete records in order to solve ACME challenges. Find one 33 | // you can use at https://github.com/libdns. If your provider 34 | // isn't implemented yet, feel free to contribute! 35 | type DDNSProvider interface { 36 | //libdns.RecordAppender 37 | libdns.RecordDeleter 38 | libdns.RecordSetter 39 | } 40 | 41 | // ddnsWaitUntilSet blocks until the record created in Present() appears in 42 | // authoritative lookups, i.e. until it has propagated, or until 43 | // timeout, whichever is first. 44 | func ddnsWaitUntilSet(ctx context.Context, dnsName string, dnsVal string, dnstype uint16) error { 45 | // dnsName := challenge.DNS01TXTRecordName() 46 | // keyAuth := challenge.DNS01KeyAuthorization() 47 | 48 | dnsVal = NormalizeIP(dnsVal, dnstype) 49 | 50 | //yup yuck 51 | 52 | // you could change timeout here 53 | timeout := 2 * time.Minute 54 | 55 | const interval = 2 * time.Second 56 | 57 | // you can change the nameservers here 58 | resolvers := recursiveNameservers([]string{}) 59 | 60 | dbg.Ddns.Println("ddnsWaitUntilSet resolvers", resolvers) 61 | 62 | var err error 63 | start := time.Now() 64 | for time.Since(start) < timeout { 65 | select { 66 | case <-time.After(interval): 67 | case <-ctx.Done(): 68 | return ctx.Err() 69 | } 70 | var val string 71 | val, _ = checkDNSPropagation(dnsName, resolvers, dnstype) 72 | // if err != nil { 73 | // return fmt.Errorf("checking DNS propagation of %s: %w", dnsName, err) 74 | // } 75 | 76 | dbg.Ddns.Printf("wait() want:%v got:%v from checkDNSPropagation()", dnsVal, val) 77 | 78 | if val == dnsVal { 79 | return nil 80 | } 81 | } 82 | 83 | return fmt.Errorf("timed out waiting for record to fully propagate; verify DNS provider configuration is correct - last error: %v", err) 84 | } 85 | 86 | // splitFQDN("foo.bar.com",2) 87 | // will return ("foo","bar.com") 88 | func splitFQDN(fqdn string, suffixCount int) (prefix string, zone string) { 89 | split := dns.SplitDomainName(fqdn) 90 | ix := len(split) - suffixCount 91 | prefix = strings.Join(split[0:ix], ".") 92 | zone = strings.Join(split[ix:], ".") 93 | return 94 | } 95 | 96 | // ddnsSetRecord creates the DNS record for the given ACME challenge. 97 | func ddnsSetRecord(ctx context.Context, provider DDNSProvider, fqdn string, suffixCount int, dnsVal string, dnstype uint16) error { 98 | // dnsName := challenge.DNS01TXTRecordName() 99 | // keyAuth := challenge.DNS01KeyAuthorization() 100 | 101 | prefix, zone := splitFQDN(fqdn, suffixCount) 102 | 103 | // NO! 104 | // We can take TXT also 105 | // dnsVal = NormalizeIP(dnsVal, dnstype) 106 | 107 | recstr := dnstype2String(dnstype) 108 | 109 | rec := libdns.Record{ 110 | Type: recstr, 111 | Name: prefix, 112 | Value: dnsVal, 113 | TTL: time.Second * 0, 114 | } 115 | 116 | // zone, err := findZoneByFQDN(dnsName, recursiveNameservers([]string{})) 117 | // if err != nil { 118 | // return fmt.Errorf("could not determine zone for domain %q: %v", dnsName, err) 119 | // } 120 | 121 | results, err := provider.SetRecords(ctx, zone, []libdns.Record{rec}) 122 | if err != nil { 123 | return fmt.Errorf("adding temporary record for zone %s: %w", zone, err) 124 | } 125 | if len(results) != 1 { 126 | return fmt.Errorf("expected one record, got %d: %v", len(results), results) 127 | } 128 | 129 | return nil 130 | } 131 | 132 | func dnstype2String(dnstype uint16) string { 133 | 134 | recstr := "" 135 | switch dnstype { 136 | case dns.TypeA: 137 | recstr = "A" 138 | case dns.TypeAAAA: 139 | recstr = "AAAA" 140 | case dns.TypeTXT: 141 | recstr = "TXT" 142 | default: 143 | panic("unsupported record type, easy to fix") 144 | } 145 | return recstr 146 | } 147 | 148 | var _ = ddnsRemoveAddrs 149 | 150 | // ddnsRemoveAddrs deletes the DNS record created in Present(). 151 | func ddnsRemoveAddrs(ctx context.Context, provider DDNSProvider, fqdn string, suffixCount int, dnstype uint16) error { 152 | //dnsName := challenge.DNS01TXTRecordName() 153 | 154 | fqdn = strings.TrimSuffix(fqdn, ".") 155 | recstr := dnstype2String(dnstype) 156 | prefix, zone := splitFQDN(fqdn, suffixCount) 157 | 158 | rec := libdns.Record{ 159 | Type: recstr, 160 | Name: prefix, 161 | //Value: dnsVal, 162 | TTL: time.Second * 0, 163 | } 164 | 165 | // clean up the record 166 | _, err := provider.DeleteRecords(ctx, zone, []libdns.Record{rec}) 167 | if err != nil { 168 | return fmt.Errorf("deleting temporary record for zone %s: %w", zone, err) 169 | } 170 | 171 | return nil 172 | } 173 | 174 | // NormalizeIP may not have been needed! 175 | func NormalizeIP(ipstr string, dnstype uint16) string { 176 | 177 | switch dnstype { 178 | case dns.TypeA: 179 | ip := net.ParseIP(ipstr) 180 | return ip.String() 181 | case dns.TypeAAAA: 182 | ip := net.ParseIP(ipstr) 183 | return strings.ToLower(ip.String()) 184 | //return strings.ToLower(fmt.Sprintf("%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x", 185 | // ip[0], ip[1], ip[2], ip[3], ip[4], ip[5], ip[6], ip[7], ip[8], ip[9], ip[10], ip[11], ip[12], ip[13], ip[14], ip[15])) 186 | case dns.TypeTXT: 187 | return ipstr 188 | default: 189 | panic("A or AAAA only") 190 | } 191 | } 192 | -------------------------------------------------------------------------------- /internal/sfu/dnslego.go: -------------------------------------------------------------------------------- 1 | // Code in this file from "github.com/caddyserver/certmagic" 2 | // Which is under the Apache 2.0 license 3 | // But most of this code, appears to be taken from "github.com/go-acme/lego" 4 | // as explained below, which is under the MIT license. 5 | // So, the code below is mostly under the MIT license with a 6 | // sprinkling of Apache 2.0 for flavoring. -Cam 7 | // 8 | package sfu 9 | 10 | import ( 11 | "errors" 12 | "fmt" 13 | "net" 14 | "strings" 15 | "sync" 16 | "time" 17 | 18 | "github.com/miekg/dns" 19 | ) 20 | 21 | // Code in this file adapted from go-acme/lego, July 2020: 22 | // https://github.com/go-acme/lego 23 | // by Ludovic Fernandez and Dominik Menke 24 | // 25 | // It has been modified. 26 | 27 | // findZoneByFQDN determines the zone apex for the given fqdn by recursing 28 | // up the domain labels until the nameserver returns a SOA record in the 29 | // answer section. 30 | func findZoneByFQDN(fqdn string, nameservers []string) (string, error) { 31 | if !strings.HasSuffix(fqdn, ".") { 32 | fqdn += "." 33 | } 34 | soa, err := lookupSoaByFqdn(fqdn, nameservers) 35 | if err != nil { 36 | return "", err 37 | } 38 | return soa.zone, nil 39 | } 40 | 41 | func lookupSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error) { 42 | if !strings.HasSuffix(fqdn, ".") { 43 | fqdn += "." 44 | } 45 | 46 | fqdnSOACacheMu.Lock() 47 | defer fqdnSOACacheMu.Unlock() 48 | 49 | // prefer cached version if fresh 50 | if ent := fqdnSOACache[fqdn]; ent != nil && !ent.isExpired() { 51 | return ent, nil 52 | } 53 | 54 | ent, err := fetchSoaByFqdn(fqdn, nameservers) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | // save result to cache, but don't allow 60 | // the cache to grow out of control 61 | if len(fqdnSOACache) >= 1000 { 62 | for key := range fqdnSOACache { 63 | delete(fqdnSOACache, key) 64 | break 65 | } 66 | } 67 | fqdnSOACache[fqdn] = ent 68 | 69 | return ent, nil 70 | } 71 | 72 | func fetchSoaByFqdn(fqdn string, nameservers []string) (*soaCacheEntry, error) { 73 | var err error 74 | var in *dns.Msg 75 | 76 | labelIndexes := dns.Split(fqdn) 77 | for _, index := range labelIndexes { 78 | domain := fqdn[index:] 79 | 80 | in, err = dnsQuery(domain, dns.TypeSOA, nameservers, true) 81 | if err != nil { 82 | continue 83 | } 84 | if in == nil { 85 | continue 86 | } 87 | 88 | switch in.Rcode { 89 | case dns.RcodeSuccess: 90 | // Check if we got a SOA RR in the answer section 91 | if len(in.Answer) == 0 { 92 | continue 93 | } 94 | 95 | // CNAME records cannot/should not exist at the root of a zone. 96 | // So we skip a domain when a CNAME is found. 97 | if dnsMsgContainsCNAME(in) { 98 | continue 99 | } 100 | 101 | for _, ans := range in.Answer { 102 | if soa, ok := ans.(*dns.SOA); ok { 103 | return newSoaCacheEntry(soa), nil 104 | } 105 | } 106 | case dns.RcodeNameError: 107 | // NXDOMAIN 108 | default: 109 | // Any response code other than NOERROR and NXDOMAIN is treated as error 110 | return nil, fmt.Errorf("unexpected response code '%s' for %s", dns.RcodeToString[in.Rcode], domain) 111 | } 112 | } 113 | 114 | return nil, fmt.Errorf("could not find the start of authority for %s%s", fqdn, formatDNSError(in, err)) 115 | } 116 | 117 | // dnsMsgContainsCNAME checks for a CNAME answer in msg 118 | func dnsMsgContainsCNAME(msg *dns.Msg) bool { 119 | for _, ans := range msg.Answer { 120 | if _, ok := ans.(*dns.CNAME); ok { 121 | return true 122 | } 123 | } 124 | return false 125 | } 126 | 127 | func dnsQuery(fqdn string, rtype uint16, nameservers []string, recursive bool) (*dns.Msg, error) { 128 | dbg.Ddns.Printf("dnsQuery(%v,%v,%v,%v) entered", fqdn, dns.TypeToString[rtype], nameservers, recursive) 129 | 130 | m := createDNSMsg(fqdn, rtype, recursive) 131 | var in *dns.Msg 132 | var err error 133 | for _, ns := range nameservers { 134 | in, err = sendDNSQuery(m, ns) 135 | 136 | gotansr := err == nil && in != nil && len(in.Answer) > 0 137 | 138 | dbg.Ddns.Printf("sendDNSQuery(%v) retrn gotansr:%v nilerr:%v", ns, gotansr, err == nil) 139 | 140 | if gotansr { 141 | return in, nil 142 | } 143 | } 144 | //the error of the last query doesn't well represent the failure of this routine 145 | //return in,err 146 | return &dns.Msg{}, fmt.Errorf("dnsQuery() to %v of type %s failed", fqdn, dns.TypeToString[rtype]) 147 | } 148 | 149 | func createDNSMsg(fqdn string, rtype uint16, recursive bool) *dns.Msg { 150 | m := new(dns.Msg) 151 | m.SetQuestion(fqdn, rtype) 152 | m.SetEdns0(4096, false) 153 | if !recursive { 154 | m.RecursionDesired = false 155 | } 156 | return m 157 | } 158 | 159 | func sendDNSQuery(m *dns.Msg, ns string) (*dns.Msg, error) { 160 | udp := &dns.Client{Net: "udp", Timeout: dnsTimeout} 161 | in, _, err := udp.Exchange(m, ns) 162 | // two kinds of errors we can handle by retrying with TCP: 163 | // truncation and timeout; see https://github.com/caddyserver/caddy/issues/3639 164 | truncated := in != nil && in.Truncated 165 | timeoutErr := err != nil && strings.Contains(err.Error(), "timeout") 166 | dbg.Ddns.Printf("udp sendDNSQuery result ns:%v tout:%v trunc:%v err:%v", ns, timeoutErr, truncated, err) 167 | if truncated || timeoutErr { 168 | tcp := &dns.Client{Net: "tcp", Timeout: dnsTimeout} 169 | in, _, err = tcp.Exchange(m, ns) 170 | dbg.Ddns.Printf("tcp sendDNSQuery result ns:%v err:%v", ns, err) 171 | } 172 | return in, err 173 | } 174 | 175 | func formatDNSError(msg *dns.Msg, err error) string { 176 | var parts []string 177 | if msg != nil { 178 | parts = append(parts, dns.RcodeToString[msg.Rcode]) 179 | } 180 | if err != nil { 181 | parts = append(parts, err.Error()) 182 | } 183 | if len(parts) > 0 { 184 | return ": " + strings.Join(parts, " ") 185 | } 186 | return "" 187 | } 188 | 189 | // soaCacheEntry holds a cached SOA record (only selected fields) 190 | type soaCacheEntry struct { 191 | zone string // zone apex (a domain name) 192 | primaryNs string // primary nameserver for the zone apex 193 | expires time.Time // time when this cache entry should be evicted 194 | } 195 | 196 | func newSoaCacheEntry(soa *dns.SOA) *soaCacheEntry { 197 | return &soaCacheEntry{ 198 | zone: soa.Hdr.Name, 199 | primaryNs: soa.Ns, 200 | expires: time.Now().Add(time.Duration(soa.Refresh) * time.Second), 201 | } 202 | } 203 | 204 | // isExpired checks whether a cache entry should be considered expired. 205 | func (cache *soaCacheEntry) isExpired() bool { 206 | return time.Now().After(cache.expires) 207 | } 208 | 209 | // systemOrDefaultNameservers attempts to get system nameservers from the 210 | // resolv.conf file given by path before falling back to hard-coded defaults. 211 | func systemOrDefaultNameservers(path string, defaults []string) []string { 212 | config, err := dns.ClientConfigFromFile(path) 213 | if err != nil || len(config.Servers) == 0 { 214 | return defaults 215 | } 216 | return config.Servers 217 | } 218 | 219 | // populateNameserverPorts ensures that all nameservers have a port number. 220 | func populateNameserverPorts(servers []string) { 221 | for i := range servers { 222 | _, port, _ := net.SplitHostPort(servers[i]) 223 | if port == "" { 224 | servers[i] = net.JoinHostPort(servers[i], "53") 225 | } 226 | } 227 | } 228 | 229 | // checkDNSPropagation checks if the expected TXT record has been propagated to all authoritative nameservers. 230 | func checkDNSPropagation(fqdn string, resolvers []string, dnstype uint16) (string, error) { 231 | if !strings.HasSuffix(fqdn, ".") { 232 | fqdn += "." 233 | } 234 | dbg.Ddns.Printf("checkDNSPropagation(%v,%v,%v) entry", fqdn, resolvers, dns.TypeToString[dnstype]) 235 | 236 | // Initial attempt to resolve at the recursive NS 237 | r, err := dnsQuery(fqdn, dnstype, resolvers, true) 238 | 239 | dbg.Ddns.Printf("dnsQuery() ret: dns.RcodeSuccess:%v nilerr:%v", r.Rcode == dns.RcodeSuccess, err == nil) 240 | if err != nil { 241 | return "", err 242 | } 243 | 244 | // TODO: make this configurable, maybe 245 | // if !p.requireCompletePropagation { 246 | // return true, nil 247 | // } 248 | 249 | if r.Rcode == dns.RcodeSuccess { 250 | fqdn = updateDomainWithCName(r, fqdn) 251 | dbg.Ddns.Printf("dnsQuery() returns fqdn = %v", fqdn) 252 | } 253 | 254 | authoritativeNss, err := lookupNameservers(fqdn, resolvers) 255 | dbg.Ddns.Printf("lookupNameservers() ret authnss:%v nilerr:%v", authoritativeNss, err == nil) 256 | if err != nil { 257 | return "", err 258 | } 259 | 260 | xx, err := checkAuthoritativeNss(fqdn, authoritativeNss, dnstype) 261 | dbg.Ddns.Printf("checkAuthoritativeNss() ret val:%v nilerr:%v", xx, err == nil) 262 | 263 | return xx, err 264 | 265 | } 266 | 267 | // checkAuthoritativeNss queries each of the given nameservers for the expected TXT record. 268 | func checkAuthoritativeNss(fqdn string, nameservers []string, dnstype uint16) (string, error) { 269 | for _, ns := range nameservers { 270 | r, err := dnsQuery(fqdn, dnstype, []string{net.JoinHostPort(ns, "53")}, false) 271 | if err != nil { 272 | return "", err 273 | } 274 | 275 | if r.Rcode != dns.RcodeSuccess { 276 | if r.Rcode == dns.RcodeNameError { 277 | // if Present() succeeded, then it must show up eventually, or else 278 | // something is really broken in the DNS provider or their API; 279 | // no need for error here, simply have the caller try again 280 | return "", nil 281 | } 282 | return "", fmt.Errorf("NS %s returned %s for %s", ns, dns.RcodeToString[r.Rcode], fqdn) 283 | } 284 | 285 | for _, rr := range r.Answer { 286 | 287 | switch rr.Header().Rrtype { 288 | case dns.TypeA: 289 | if txt, ok := rr.(*dns.A); ok { 290 | return txt.A.String(), nil 291 | } 292 | case dns.TypeAAAA: 293 | if txt, ok := rr.(*dns.AAAA); ok { 294 | return txt.AAAA.String(), nil 295 | } 296 | case dns.TypeTXT: 297 | if txt, ok := rr.(*dns.TXT); ok { 298 | return strings.Join(txt.Txt, ""), nil 299 | } 300 | 301 | } 302 | 303 | } 304 | } 305 | 306 | return "", nil 307 | } 308 | 309 | // lookupNameservers returns the authoritative nameservers for the given fqdn. 310 | func lookupNameservers(fqdn string, resolvers []string) ([]string, error) { 311 | var authoritativeNss []string 312 | 313 | zone, err := findZoneByFQDN(fqdn, resolvers) 314 | if err != nil { 315 | return nil, fmt.Errorf("could not determine the zone: %w", err) 316 | } 317 | 318 | r, err := dnsQuery(zone, dns.TypeNS, resolvers, true) 319 | if err != nil { 320 | return nil, err 321 | } 322 | 323 | for _, rr := range r.Answer { 324 | if ns, ok := rr.(*dns.NS); ok { 325 | authoritativeNss = append(authoritativeNss, strings.ToLower(ns.Ns)) 326 | } 327 | } 328 | 329 | if len(authoritativeNss) > 0 { 330 | return authoritativeNss, nil 331 | } 332 | return nil, errors.New("could not determine authoritative nameservers") 333 | } 334 | 335 | // Update FQDN with CNAME if any 336 | func updateDomainWithCName(r *dns.Msg, fqdn string) string { 337 | for _, rr := range r.Answer { 338 | if cn, ok := rr.(*dns.CNAME); ok { 339 | if cn.Hdr.Name == fqdn { 340 | return cn.Target 341 | } 342 | } 343 | } 344 | return fqdn 345 | } 346 | 347 | // recursiveNameservers are used to pre-check DNS propagation. It 348 | // prepends user-configured nameservers (custom) to the defaults 349 | // obtained from resolv.conf and defaultNameservers and ensures 350 | // that all server addresses have a port value. 351 | func recursiveNameservers(custom []string) []string { 352 | servers := append(custom, systemOrDefaultNameservers(defaultResolvConf, defaultNameservers)...) 353 | populateNameserverPorts(servers) 354 | return servers 355 | } 356 | 357 | var defaultNameservers = []string{ 358 | "8.8.8.8:53", 359 | "8.8.4.4:53", 360 | "1.1.1.1:53", 361 | "1.0.0.1:53", 362 | } 363 | 364 | var dnsTimeout = 10 * time.Second 365 | 366 | var ( 367 | fqdnSOACache = map[string]*soaCacheEntry{} 368 | fqdnSOACacheMu sync.Mutex 369 | ) 370 | 371 | const defaultResolvConf = "/etc/resolv.conf" 372 | -------------------------------------------------------------------------------- /internal/sfu/flags.go: -------------------------------------------------------------------------------- 1 | package sfu 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | "log" 7 | "net" 8 | "net/http" 9 | "os" 10 | "reflect" 11 | "strings" 12 | 13 | "github.com/caddyserver/certmagic" 14 | "github.com/libdns/cloudflare" 15 | "github.com/libdns/duckdns" 16 | "github.com/spf13/pflag" 17 | "github.com/x186k/ddns5libdns" 18 | ) 19 | 20 | var httpFlag = pflag.String("http", "", "The addr:port at which http will bind/listen. addr may be empty, for example ':80' or ':8080' ") 21 | var httpsDomainFlag = pflag.StringP("https-domain", "q", "", "Domain name for https. Can add :port if needed. Uses port 443 when :port not provided") 22 | var httpsDnsProvider = pflag.StringP("https-dns-provider", "r", "", "One of ddns5, duckdns or cloudflare") 23 | var httpsDnsRegisterIp = pflag.BoolP("https-dns-register-ip", "s", false, "DNS-Register the IP of this box, at provider, for name: --https-domain. Uses interface addrs") 24 | var httpsDnsRegisterIpPublic = pflag.BoolP("https-dns-register-ip-public", "t", false, "DNS-Register the IP of this box, at provider, for name: --https-domain. Detects public addrs") 25 | var httpsUseDns01Challenge = pflag.BoolP("https-dns01-challenge", "u", false, "When registering at Let's Encrypt, use the DNS challenge, not HTTP/HTTPS. Recommended behind firewalls") 26 | 27 | var dialUpstreamUrlFlag = pflag.StringP("dial-upstream", "d", "", "Specify a URL for upstream SFU. No path, no params. Used for SFU chaining!. Upstream dial triggered on subscriber connection to room.") 28 | 29 | var iceCandidateHost = pflag.String("ice-candidate-host", "", "For forcing the ice host candidate IP address") 30 | var iceCandidateSrflx = pflag.String("ice-candidate-srflx", "", "For forcing the ice srflx candidate IP address") 31 | 32 | var ftlKey = pflag.String("ftl-key", "", "Set the ftl/obs Settings/Stream/Stream-key. LIKE A PASSWORD! CHANGE THIS FROM DEFAULT! ") 33 | var ftlUdpPort = pflag.Int("ftl-udp-port", 8084, "The UDP port to use for FTL UDP rx. Zero is valid. Zero for ephemeral port num") 34 | 35 | //var ffmpeg =pflag.StringToString("ffmpeg","","ffmpeg shortcut to spawn for RTP ingress into room" 36 | //var rtptx = pflag.String("rtp-tx", "", "addr:port to send rtp to. ie: '127.0.0.1:4444'") 37 | //var rtprx = pflag.StringArray("rtp-rx", nil, "use :port or addr:port. eg: '--rtp-rx :5004 --rtp-rx :5006' payload 96 for h264, 97 for opus") 38 | //var rtpWireshark = pflag.Bool("rtp-wireshark", false, "when on 127.0.0.1, also receive my sent packets") 39 | var stunServer = pflag.String("stun-server", "stun.l.google.com:19302", "hostname:port of STUN server") 40 | var htmlSource = pflag.String("html", "", "required. 'internal' suggested. HTML source: internal, none, , ") 41 | var cpuprofile = pflag.Bool("profile", false, "Enable Go runtime profiling. Developer tool. Press enter to start/stop on console") 42 | var pprofFlag = pflag.Bool("pprof", false, "enable pprof based profiling on :6060") 43 | 44 | //var idleExitDuration = pflag.Duration("idle-exit-duration", time.Duration(0), `If there is no input video for duration, exit process/container. eg: '1h' one hour, '30m': 30 minutes`) 45 | 46 | var idleClipServerURL = pflag.String("idle-clip-server-url", "http://localhost:8088/idle-clip", "what server to hit when using --idle-clip-server-input") 47 | var idleClipServerInput = pflag.String("idle-clip-server-input", "", "a .jpg, .png, .mov, etc to use for your Idle Clip") 48 | var idleClipZipfile = pflag.String("idle-clip-zipfile", "", "provide a zipfile for the Idle Clip") 49 | 50 | var getStatsLogging = pflag.String("getstats-url", "", "The url of a server for getStats() logging") 51 | 52 | var debugFlag = pflag.StringSlice("debug", nil, "use '--debug help' to see options. use comma to seperate multiple options") 53 | 54 | var helpShortFlag = pflag.BoolP("help", "h", false, "Print the short, getting-started help") 55 | var helpFullFlag = pflag.BoolP("help2", "2", false, "Print the full, long help") 56 | var helpHttpsFlag = pflag.BoolP("help3", "3", false, "Print the help on using HTTPS") 57 | 58 | // var logPackets = flag.Bool("z-log-packets", false, "log packets for later use with text2pcap") 59 | // var logSplicer = flag.Bool("z-log-splicer", false, "log RTP splicing debug info") 60 | // egrep '(RTP_PACKET|RTCP_PACKET)' moz.log | text2pcap -D -n -l 1 -i 17 -u 1234,1235 -t '%H:%M:%S.' - rtp.pcap 61 | 62 | const bearerHelp = ` 63 | Bearer Authentication. Like a password. Required on HTTP/S requests. 64 | Provide via URL: https://base.url?access_token= 65 | Provide via headers: 'Authorization: Bearer ' 66 | ` 67 | 68 | var bearerToken = pflag.StringP("bearer-token", "b", "", bearerHelp) 69 | 70 | func printShortHelp() { 71 | fmt.Println("Short flags list:") 72 | fmt.Println() 73 | x := pflag.NewFlagSet("xxx", pflag.ExitOnError) 74 | x.AddFlag(pflag.CommandLine.Lookup("http")) 75 | x.AddFlag(pflag.CommandLine.Lookup("html")) 76 | x.AddFlag(pflag.CommandLine.ShorthandLookup("h")) 77 | x.AddFlag(pflag.CommandLine.ShorthandLookup("2")) 78 | x.AddFlag(pflag.CommandLine.ShorthandLookup("3")) 79 | x.SortFlags = false 80 | x.PrintDefaults() 81 | fmt.Println(` 82 | 83 | Suggested new-user command: './deadsfu --http :8080 --html internal' 84 | (Next, open browser to http://localhost:8080/) 85 | 86 | Minimum required flags: --html is required, and either --http or --https-domain is required.`) 87 | fmt.Println() 88 | } 89 | 90 | func printHttpsHelp() { 91 | fmt.Println("Https flags list:") 92 | fmt.Println() 93 | 94 | x := pflag.NewFlagSet("xxx", pflag.ExitOnError) 95 | x.AddFlag(pflag.CommandLine.ShorthandLookup("q")) 96 | x.AddFlag(pflag.CommandLine.ShorthandLookup("r")) 97 | x.AddFlag(pflag.CommandLine.ShorthandLookup("s")) 98 | x.AddFlag(pflag.CommandLine.ShorthandLookup("t")) 99 | x.AddFlag(pflag.CommandLine.ShorthandLookup("u")) 100 | 101 | x.SortFlags = false 102 | x.PrintDefaults() 103 | fmt.Println(` 104 | https related flags help: 105 | 106 | -q or --https-domain 107 | Use this option the domain name, and optional port for https. 108 | Defaults to port 443 for the port. Use domain:port if you need something else. 109 | Port zero is valid, for auto-assign. 110 | With this flag, a certificate will be aquired from Let's Encrypt. 111 | BY USING THIS FLAG, you consent to agreeing to the Let's Encrypt's terms. 112 | 113 | -r or --https-dns-provider 114 | You can use: ddns5, duckdns, cloudflare 115 | This flag is required when using --https-domain, as a DNS TXT record must be set for Let's Encrypt 116 | ddns5: does not require a token! Domain must be: .ddns5.com 117 | duckdns: uses the environment variable DUCKDNS_TOKEN for the API token. Domain must be: .duckdns.org 118 | cloudflare: uses the environment variable CLOUDFLARE_TOKEN for the API token 119 | 120 | -s or --https-dns-register-ip 121 | Register the IP addresses of this system at the DNS provider. 122 | Looks at interfaces addresses. Sets DNS A/AAAA. 123 | 124 | -t or --https-dns-register-ip-public 125 | Register the IP addresses of this system at the DNS provider. 126 | Queries Internet for my public address. Sets DNS A/AAAA. 127 | Mutually exclusive with -3. 128 | 129 | -u or --https-acme-challenge-dns01 130 | Switch from the default ACME challenge of HTTP/HTTPS to DNS. 131 | Use this when Let's Encrypt can't reach your system behind a firewall. 132 | Great for corporate private-IP video transfer. ie: 192.168.* or 10.* 133 | 134 | Examples: 135 | $ ./deadsfu -1 foof.duckdns.org -2 duckdns 136 | $ DUCKDNS_TOKEN=xxxx ./deadsfu -1 cameron4321.ddns5.com -2 ddns5 137 | $ CLOUDFLARE_TOKEN=xxxx ./deadsfu -1 my.example.com -2 cloudflare`) 138 | fmt.Println() 139 | } 140 | 141 | var dbg = struct { 142 | Url FastLogger 143 | Pub FastLogger 144 | Sub FastLogger 145 | Dial FastLogger 146 | Media FastLogger 147 | Https FastLogger 148 | Ice FastLogger 149 | Main FastLogger 150 | Ftl FastLogger 151 | Ddns FastLogger 152 | PeerConn FastLogger 153 | Switching FastLogger 154 | Goroutine FastLogger 155 | ReceiverLostPackets FastLogger 156 | Rooms FastLogger 157 | Numgoroutine FastLogger 158 | }{ 159 | Url: FastLogger{}, 160 | Media: FastLogger{}, 161 | Https: FastLogger{}, 162 | Ice: FastLogger{}, 163 | Main: FastLogger{}, 164 | Ftl: FastLogger{}, 165 | Ddns: FastLogger{}, 166 | PeerConn: FastLogger{}, 167 | Switching: FastLogger{}, 168 | Goroutine: FastLogger{}, 169 | ReceiverLostPackets: FastLogger{}, 170 | Rooms: FastLogger{}, 171 | Numgoroutine: FastLogger{help: "periodically print goroutine count"}, 172 | } 173 | 174 | func getDbgMap() map[string]reflect.Value { 175 | 176 | loggers := make(map[string]reflect.Value) 177 | v := reflect.ValueOf(&dbg) 178 | typeOfS := v.Elem().Type() 179 | for i := 0; i < v.Elem().NumField(); i++ { 180 | name := typeOfS.Field(i).Name 181 | //pl(name,v.Elem().Field(i).CanSet()) 182 | 183 | name = strings.ToLower(name) 184 | 185 | loggers[name] = v.Elem().Field(i) 186 | } 187 | return loggers 188 | } 189 | 190 | func processDebugFlag() { 191 | 192 | for _, v := range *debugFlag { 193 | if v == "help" { 194 | printDebugFlagHelp() 195 | os.Exit(0) 196 | } 197 | } 198 | 199 | flags := make(map[string]struct{}) 200 | 201 | for _, name := range *debugFlag { 202 | flags[name] = struct{}{} 203 | } 204 | 205 | loggers := getDbgMap() 206 | 207 | for name := range flags { 208 | if _, ok := loggers[name]; !ok { 209 | checkFatal(fmt.Errorf("'%s' is not a valid debug option", name)) 210 | } 211 | } 212 | 213 | for name, l := range loggers { 214 | a := FastLogger{ 215 | Logger: log.New(io.Discard, name, 0), 216 | enabled: false, 217 | } 218 | l.Set(reflect.ValueOf(a)) 219 | } 220 | 221 | for name := range flags { 222 | if l, ok := loggers[name]; ok { 223 | a := FastLogger{ 224 | Logger: log.New(os.Stdout, "["+name+"] ", logFlags), 225 | enabled: true, 226 | } 227 | l.Set(reflect.ValueOf(a)) 228 | } 229 | } 230 | 231 | } 232 | 233 | func printDebugFlagHelp() { 234 | fmt.Println() 235 | fmt.Println("debug options may be comma seperated.") 236 | fmt.Println("debug options available:") 237 | for k, v := range getDbgMap() { 238 | fastlogger := v.Interface().(FastLogger) 239 | fmt.Println("--debug", fmt.Sprintf("%-25s", k), "#", fastlogger.help) 240 | } 241 | fmt.Println() 242 | fmt.Println(`Examples: 243 | $ ./deadsfu --debug help # show this help 244 | $ ./deadsfu --debug Url,Media # print url and media info 245 | $ ./deadsfu --debug Ice # print debug log on ice-candidates`) 246 | } 247 | 248 | func parseFlags() { 249 | 250 | pflag.Usage = printShortHelp 251 | 252 | pflag.Parse() 253 | if *helpShortFlag { 254 | printShortHelp() 255 | os.Exit(0) 256 | } else if *helpHttpsFlag { 257 | printHttpsHelp() 258 | os.Exit(0) 259 | } else if *helpFullFlag { 260 | fmt.Println("Full flags list:") 261 | fmt.Println() 262 | pflag.CommandLine.SortFlags = false 263 | pflag.PrintDefaults() 264 | os.Exit(0) 265 | } 266 | 267 | processDebugFlag() 268 | 269 | } 270 | func oneTimeFlagsActions() { 271 | 272 | if *pprofFlag { 273 | go func() { // pprof 274 | log.Fatal(http.ListenAndServe(":6060", nil)) 275 | }() 276 | } 277 | 278 | if *httpsDomainFlag != "" { 279 | 280 | _, _, err := net.SplitHostPort(*httpsDomainFlag) 281 | if err != nil && strings.Contains(err.Error(), "missing port") { 282 | foo := *httpsDomainFlag + ":443" 283 | *httpsDomainFlag = foo 284 | } 285 | host, _, err := net.SplitHostPort(*httpsDomainFlag) 286 | checkFatal(err) 287 | 288 | var provider DDNSUnion 289 | switch *httpsDnsProvider { 290 | case "": 291 | default: 292 | checkFatal(fmt.Errorf("Invalid DNS provider name, see help")) 293 | case "ddns5": 294 | provider = &ddns5libdns.Provider{} 295 | case "duckdns": 296 | token := os.Getenv("DUCKDNS_TOKEN") 297 | if token == "" { 298 | checkFatal(fmt.Errorf("env var DUCKDNS_TOKEN is not set")) 299 | } 300 | provider = &duckdns.Provider{APIToken: token} 301 | case "cloudflare": 302 | token := os.Getenv("CLOUDFLARE_TOKEN") 303 | if token == "" { 304 | checkFatal(fmt.Errorf("env var CLOUDFLARE_TOKEN is not set")) 305 | } 306 | provider = &cloudflare.Provider{APIToken: token} 307 | } 308 | 309 | if *httpsDnsRegisterIp { 310 | addrs, err := getDefaultRouteInterfaceAddresses() 311 | checkFatal(err) 312 | ddnsRegisterIPAddresses(provider, host, 2, addrs) 313 | } 314 | 315 | if *httpsDnsRegisterIpPublic { 316 | myipv4, err := getMyPublicIpV4() 317 | checkFatal(err) 318 | ddnsRegisterIPAddresses(provider, host, 2, []net.IP{myipv4}) 319 | } 320 | 321 | if *httpsUseDns01Challenge { 322 | certmagic.DefaultACME.DNS01Solver = &certmagic.DNS01Solver{ 323 | //DNSProvider: provider.(certmagic.ACMEDNSProvider), 324 | DNSProvider: provider, 325 | TTL: 0, 326 | PropagationTimeout: 0, 327 | Resolvers: []string{}, 328 | } 329 | } 330 | 331 | } 332 | 333 | } 334 | -------------------------------------------------------------------------------- /internal/sfu/galene.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2020 by Juliusz Chroboczek 2 | 3 | // Permission is hereby granted, free of charge, to any person obtaining a copy 4 | // of this software and associated documentation files (the "Software"), to deal 5 | // in the Software without restriction, including without limitation the rights 6 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | // copies of the Software, and to permit persons to whom the Software is 8 | // furnished to do so, subject to the following conditions: 9 | 10 | // The above copyright notice and this permission notice shall be included in 11 | // all copies or substantial portions of the Software. 12 | 13 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | // THE SOFTWARE. 20 | 21 | package sfu 22 | 23 | import ( 24 | _ "strings" 25 | 26 | _ "github.com/pion/rtp" 27 | _ "github.com/pion/rtp/codecs" 28 | ) 29 | 30 | // isKeyframe determines if packet is the start of a keyframe. 31 | // It returns (true, true) if that is the case, (false, true) if that is 32 | // definitely not the case, and (false, false) if the information cannot 33 | // be determined. 34 | // func isKeyframe(codec string, packet *rtp.Packet) (bool, bool) { 35 | // switch strings.ToLower(codec) { 36 | // case "video/vp8": 37 | // var vp8 codecs.VP8Packet 38 | // _, err := vp8.Unmarshal(packet.Payload) 39 | // if err != nil || len(vp8.Payload) < 1 { 40 | // return false, false 41 | // } 42 | 43 | // if vp8.S != 0 && vp8.PID == 0 && (vp8.Payload[0]&0x1) == 0 { 44 | // return true, true 45 | // } 46 | // return false, true 47 | // case "video/vp9": 48 | // var vp9 codecs.VP9Packet 49 | // _, err := vp9.Unmarshal(packet.Payload) 50 | // if err != nil || len(vp9.Payload) < 1 { 51 | // return false, false 52 | // } 53 | // if !vp9.B { 54 | // return false, true 55 | // } 56 | 57 | // if (vp9.Payload[0] & 0xc0) != 0x80 { 58 | // return false, false 59 | // } 60 | 61 | // profile := (vp9.Payload[0] >> 4) & 0x3 62 | // if profile != 3 { 63 | // return (vp9.Payload[0] & 0xC) == 0, true 64 | // } 65 | // return (vp9.Payload[0] & 0x6) == 0, true 66 | // case "video/h264": 67 | // if len(packet.Payload) < 1 { 68 | // return false, false 69 | // } 70 | // nalu := packet.Payload[0] & 0x1F 71 | // if nalu == 0 { 72 | // // reserved 73 | // return false, false 74 | // } else if nalu <= 23 { 75 | // // simple NALU 76 | // //cam 77 | // // you really need to go a little deeper and 78 | // // look at: first_mb_in_slice 79 | // // slice_header( ) { C Descriptor first_mb_in_slice 80 | // return nalu == 5, true 81 | // } else if nalu == 24 || nalu == 25 || nalu == 26 || nalu == 27 { 82 | // // STAP-A, STAP-B, MTAP16 or MTAP24 83 | // i := 1 84 | // if nalu == 25 || nalu == 26 || nalu == 27 { 85 | // // skip DON 86 | // i += 2 87 | // } 88 | // for i < len(packet.Payload) { 89 | // if i+2 > len(packet.Payload) { 90 | // return false, false 91 | // } 92 | // length := uint16(packet.Payload[i])<<8 | 93 | // uint16(packet.Payload[i+1]) 94 | // i += 2 95 | // if i+int(length) > len(packet.Payload) { 96 | // return false, false 97 | // } 98 | // offset := 0 99 | // if nalu == 26 { 100 | // offset = 3 101 | // } else if nalu == 27 { 102 | // offset = 4 103 | // } 104 | // if offset >= int(length) { 105 | // return false, false 106 | // } 107 | // n := packet.Payload[i + offset] & 0x1F 108 | // if n == 5 { 109 | // return true, true 110 | // } else if n >= 24 { 111 | // // is this legal? 112 | // return false, false 113 | // } 114 | // i += int(length) 115 | // } 116 | // if i == len(packet.Payload) { 117 | // return false, true 118 | // } 119 | // return false, false 120 | // } else if nalu == 28 || nalu == 29 { 121 | // // FU-A or FU-B 122 | // if len(packet.Payload) < 2 { 123 | // return false, false 124 | // } 125 | // if (packet.Payload[1] & 0x80) == 0 { 126 | // // not a starting fragment 127 | // return false, true 128 | // } 129 | // return (packet.Payload[1]&0x1F == 5), true 130 | // } 131 | // return false, false 132 | 133 | // default: 134 | // return false, false 135 | // } 136 | // } 137 | -------------------------------------------------------------------------------- /internal/sfu/https.go: -------------------------------------------------------------------------------- 1 | package sfu 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "errors" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "net" 11 | "net/http" 12 | "os" 13 | "time" 14 | 15 | "github.com/caddyserver/certmagic" 16 | "github.com/libdns/libdns" 17 | "github.com/miekg/dns" 18 | "go.uber.org/zap" 19 | "golang.org/x/net/proxy" 20 | ) 21 | 22 | type DDNSUnion interface { 23 | libdns.RecordAppender 24 | libdns.RecordDeleter 25 | libdns.RecordSetter 26 | } 27 | 28 | func startHttpsListener(ctx context.Context, hostport string, mux *http.ServeMux) { 29 | var err error 30 | 31 | host, port, err := net.SplitHostPort(hostport) 32 | checkFatal(err) 33 | 34 | httpsHasCertificate := make(chan bool) 35 | go reportHttpsReadyness(httpsHasCertificate) 36 | 37 | ca := certmagic.LetsEncryptProductionCA 38 | if false { 39 | ca = certmagic.LetsEncryptStagingCA 40 | } 41 | 42 | mgrTemplate := certmagic.ACMEManager{ 43 | CA: ca, 44 | Email: "", 45 | Agreed: true, 46 | DisableHTTPChallenge: false, 47 | DisableTLSALPNChallenge: false, 48 | } 49 | magic := certmagic.NewDefault() 50 | 51 | magic.OnEvent = func(s string, i interface{}) { 52 | _ = i 53 | switch s { 54 | // called at time of challenge passing 55 | case "cert_obtained": 56 | // log.Println("Let's Encrypt Certificate Aquired") 57 | // called every run where cert is found in cache including when the challenge passes 58 | // since the followed gets called for both obained and found in cache, we use that 59 | case "cached_managed_cert": 60 | close(httpsHasCertificate) 61 | log.Println("HTTPS READY: Certificate Acquired") 62 | case "tls_handshake_started": 63 | //silent 64 | case "tls_handshake_completed": 65 | //silent 66 | default: 67 | log.Println("certmagic event:", s) //, i) 68 | } 69 | } 70 | 71 | if dbg.Https.enabled { 72 | logger, err := zap.NewDevelopment() 73 | checkFatal(err) 74 | mgrTemplate.Logger = logger 75 | } 76 | 77 | myACME := certmagic.NewACMEManager(magic, mgrTemplate) 78 | magic.Issuers = []certmagic.Issuer{myACME} 79 | 80 | err = magic.ManageSync(context.Background(), []string{host}) 81 | checkFatal(err) 82 | tlsConfig := magic.TLSConfig() 83 | tlsConfig.NextProtos = append([]string{"h2", "http/1.1"}, tlsConfig.NextProtos...) 84 | 85 | go func() { // test tcp4 port 86 | time.Sleep(time.Second) 87 | reportOpenPort(hostport, "tcp4") 88 | }() 89 | go func() { // test tcp6 port 90 | time.Sleep(time.Second) 91 | reportOpenPort(hostport, "tcp6") 92 | }() 93 | //elog.Printf("%v IS READY", httpsUrl.String()) 94 | 95 | ln, err := net.Listen("tcp", ":"+port) 96 | checkFatal(err) 97 | 98 | httpsLn := tls.NewListener(ln, tlsConfig) 99 | checkFatal(err) 100 | 101 | log.Println("SFU HTTPS IS READY ON", ln.Addr()) 102 | 103 | // err = http.Serve(httpsLn, mux) 104 | // checkFatal(err) 105 | srv := &http.Server{Handler: mux} 106 | err = srv.Serve(httpsLn) 107 | checkFatal(err) 108 | } 109 | 110 | // ddnsRegisterIPAddresses will register IP addresses to hostnames 111 | // zone might be duckdns.org 112 | // subname might be server01 113 | func ddnsRegisterIPAddresses(provider DDNSProvider, fqdn string, suffixCount int, addrs []net.IP) { 114 | 115 | //timestr := strconv.FormatInt(time.Now().UnixNano(), 10) 116 | // ddnsHelper.Present(nil, *ddnsDomain, timestr, dns.TypeTXT) 117 | // ddnsHelper.Wait(nil, *ddnsDomain, timestr, dns.TypeTXT) 118 | for _, v := range addrs { 119 | 120 | var dnstype uint16 121 | var network string 122 | 123 | if v.To4() != nil { 124 | dnstype = dns.TypeA 125 | network = "ip4" 126 | } else { 127 | dnstype = dns.TypeAAAA 128 | network = "ip6" 129 | } 130 | 131 | normalip := NormalizeIP(v.String(), dnstype) 132 | 133 | pubpriv := "Public" 134 | if IsPrivate(v) { 135 | pubpriv = "Private" 136 | } 137 | dbg.Https.Printf("Registering DNS %v %v %v %v IP-addr", fqdn, dns.TypeToString[dnstype], normalip, pubpriv) 138 | 139 | //log.Println("DDNS setting", fqdn, suffixCount, normalip, dns.TypeToString[dnstype]) 140 | err := ddnsSetRecord(context.Background(), provider, fqdn, suffixCount, normalip, dnstype) 141 | checkFatal(err) 142 | 143 | dbg.Https.Println("DDNS waiting for propagation", fqdn, suffixCount, normalip, dns.TypeToString[dnstype]) 144 | err = ddnsWaitUntilSet(context.Background(), fqdn, normalip, dnstype) 145 | checkFatal(err) 146 | 147 | log.Printf("IPAddr %v DNS registered as %v", v, fqdn) 148 | 149 | localDNSIP, err := net.ResolveIPAddr(network, fqdn) 150 | checkFatal(err) 151 | 152 | dbg.Https.Println("net.ResolveIPAddr", network, fqdn, localDNSIP.String()) 153 | 154 | if !localDNSIP.IP.Equal(v) { 155 | checkFatal(fmt.Errorf("Inconsistent DNS, please use another name")) 156 | } 157 | 158 | //log.Println("DDNS propagation complete", fqdn, suffixCount, normalip) 159 | } 160 | } 161 | 162 | // remove with go 1.17 arrival 163 | func IsPrivate(ip net.IP) bool { 164 | if ip4 := ip.To4(); ip4 != nil { 165 | // Following RFC 4193, Section 3. Local IPv6 Unicast Addresses which says: 166 | // The Internet Assigned Numbers Authority (IANA) has reserved the 167 | // following three blocks of the IPv4 address space for private internets: 168 | // 10.0.0.0 - 10.255.255.255 (10/8 prefix) 169 | // 172.16.0.0 - 172.31.255.255 (172.16/12 prefix) 170 | // 192.168.0.0 - 192.168.255.255 (192.168/16 prefix) 171 | return ip4[0] == 10 || 172 | (ip4[0] == 172 && ip4[1]&0xf0 == 16) || 173 | (ip4[0] == 192 && ip4[1] == 168) 174 | } 175 | // Following RFC 4193, Section 3. Private Address Space which says: 176 | // The Internet Assigned Numbers Authority (IANA) has reserved the 177 | // following block of the IPv6 address space for local internets: 178 | // FC00:: - FDFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF:FFFF (FC00::/7 prefix) 179 | return len(ip) == net.IPv6len && ip[0]&0xfe == 0xfc 180 | } 181 | 182 | func getMyPublicIpV4() (net.IP, error) { 183 | var publicmyip []string = []string{"https://api.ipify.org", "http://checkip.amazonaws.com/"} 184 | 185 | client := http.Client{ 186 | Timeout: 3 * time.Second, 187 | } 188 | for _, v := range publicmyip { 189 | res, err := client.Get(v) 190 | if err != nil { 191 | return nil, err 192 | } 193 | ipraw, err := ioutil.ReadAll(res.Body) 194 | if err != nil { 195 | return nil, err 196 | } 197 | ip := net.ParseIP(string(ipraw)) 198 | if ip != nil { 199 | return ip, nil 200 | } 201 | } 202 | return nil, fmt.Errorf("Unable to query the internet for my public ipv4") 203 | } 204 | 205 | func getDefaultRouteInterfaceAddresses() ([]net.IP, error) { 206 | 207 | // we don't send a single packets to these hosts 208 | // but we use their addresses to discover our interface to get to the Internet 209 | // These addresses could be almost anything 210 | 211 | var ipaddrs []net.IP 212 | 213 | addr, err := getDefRouteIntfAddrIPv4() 214 | if err != nil { 215 | return nil, err 216 | } 217 | ipaddrs = append(ipaddrs, addr) 218 | 219 | addr, err = getDefRouteIntfAddrIPv6() 220 | if err != nil { 221 | return nil, err 222 | } 223 | ipaddrs = append(ipaddrs, addr) 224 | 225 | return ipaddrs, nil 226 | } 227 | 228 | func reportHttpsReadyness(ready chan bool) { 229 | t0 := time.Now() 230 | ticker := time.NewTicker(time.Second * 5).C 231 | for { 232 | select { 233 | case t1 := <-ticker: 234 | 235 | n := int(t1.Sub(t0).Seconds()) 236 | 237 | log.Printf("HTTPS NOT READY: Waited %d seconds.", n) 238 | 239 | if n >= 30 { 240 | log.Printf("No HTTPS certificate: Stopping status messages. Will update if aquired.") 241 | return 242 | } 243 | 244 | case <-ready: 245 | return 246 | } 247 | } 248 | } 249 | 250 | func reportOpenPort(hostport, network string) { 251 | 252 | tcpaddr, err := net.ResolveTCPAddr(network, hostport) 253 | if err != nil { 254 | // not fatal 255 | // if there is no ipv6 (or v4) address, continue on 256 | return 257 | } 258 | 259 | if IsPrivate(tcpaddr.IP) { 260 | log.Printf("IPAddr %v IS PRIVATE IP, not Internet reachable. RFC 1918, 4193", tcpaddr.IP.String()) 261 | return 262 | } 263 | 264 | // use default proxy addr 265 | proxyok, iamopen := canConnectThroughProxy("", tcpaddr, network) 266 | 267 | if !proxyok { 268 | //just be silent about proxy errors, Cameron didn't pay his bill 269 | return 270 | } 271 | 272 | if iamopen { 273 | log.Printf("IPAddr %v port:%v IS OPEN from Internet", tcpaddr.IP.String(), tcpaddr.Port) 274 | } else { 275 | log.Printf("IPAddr %v port:%v IS NOT OPEN from Internet", tcpaddr.IP.String(), tcpaddr.Port) 276 | } 277 | } 278 | 279 | // canConnectThroughProxy uses an internet proxy to see if my ports are open 280 | // network should be either tcp4 or tcp6, not tcp 281 | func canConnectThroughProxy(proxyaddr string, tcpaddr *net.TCPAddr, network string) (proxyOK bool, portOpen bool) { 282 | const ( 283 | baseDialerTimeout = 3 * time.Second 284 | proxyDialerTimeout = 3 * time.Second 285 | SOCKS5PROXY = "deadsfu.com:60000" 286 | ) 287 | if network != "tcp4" && network != "tcp6" { 288 | checkFatal(fmt.Errorf("network not okay")) 289 | } 290 | 291 | if proxyaddr == "" { 292 | proxyaddr = SOCKS5PROXY 293 | } 294 | 295 | baseDialer := &net.Dialer{ 296 | Timeout: baseDialerTimeout, 297 | //Deadline: time.Time{}, 298 | //FallbackDelay: -1, 299 | } 300 | 301 | // always get to proxy using ipv4, more reliable for this test 302 | dialer, err := proxy.SOCKS5(network, proxyaddr, nil, baseDialer) 303 | if err != nil { 304 | return 305 | } 306 | 307 | contextDialer, ok := dialer.(proxy.ContextDialer) 308 | if !ok { 309 | dbg.Https.Println("cannot deref dialer") 310 | //not fatal 311 | return 312 | } 313 | 314 | ctx, cancel := context.WithTimeout(context.Background(), proxyDialerTimeout) 315 | _ = cancel 316 | 317 | // we ask the proxy for the given network 318 | conn, err := contextDialer.DialContext(ctx, network, tcpaddr.String()) 319 | if err != nil { 320 | for xx := err; xx != nil; xx = errors.Unwrap(xx) { 321 | //fmt.Printf("%#v\n", xx) 322 | var operr *net.OpError 323 | if errors.As(xx, &operr) { 324 | readop := operr.Op == "read" 325 | dialop := operr.Op == "dial" 326 | iotimeout := operr.Err == os.ErrDeadlineExceeded //also errors.Is(xx, os.ErrDeadlineExceeded) 327 | 328 | if readop && iotimeout { 329 | proxyOK = true 330 | portOpen = false 331 | return 332 | } else if dialop && iotimeout { 333 | proxyOK = false 334 | portOpen = false 335 | return 336 | } 337 | } else { 338 | fmt.Println(err) 339 | } 340 | } 341 | // unexpected issue with proxy, but we stay silent 342 | // unless debugging is on 343 | // maybe Cam didn't pay proxy bill 344 | dbg.Https.Println("unexpected proxy behavior") 345 | for xx := err; xx != nil; xx = errors.Unwrap(xx) { 346 | dbg.Https.Printf("%#v\n", xx) 347 | } 348 | 349 | return 350 | } 351 | conn.Close() 352 | 353 | // if we got here, I got through the proxy and connected to myself (I think) 354 | 355 | proxyOK = true 356 | portOpen = true 357 | return 358 | } 359 | -------------------------------------------------------------------------------- /internal/sfu/xbroker.go: -------------------------------------------------------------------------------- 1 | package sfu 2 | 3 | import ( 4 | "log" 5 | "sync" 6 | "sync/atomic" 7 | "time" 8 | ) 9 | 10 | //credit for inspiration to https://stackoverflow.com/a/49877632/86375 11 | 12 | /* 13 | The XBroker does these things: 14 | - does broadcast fan-out of rtp packets to Go channels 15 | - does broadcast fan-out of rtp packets to Pion Tracks 16 | - records GOPs from keyframe, and shares the PGOP or GOP-so-far with Subscribers 17 | */ 18 | 19 | const ( 20 | XBrokerInputChannelDepth = 1000 // no benefit to being small 21 | XBrokerOutputChannelDepth = 1000 // same 22 | ) 23 | 24 | type XBroker struct { 25 | mu sync.Mutex 26 | subs map[chan *XPacket]struct{} 27 | inCh chan *XPacket 28 | gop []*XPacket 29 | droppedRx int32 // concurrent/atomic increments 30 | droppedTx int // non-current increments 31 | // capacityMaxRx int 32 | // capacityMaxTx int 33 | } 34 | 35 | var TimerXPacket XPacket 36 | 37 | func NewXBroker() *XBroker { 38 | return &XBroker{ 39 | mu: sync.Mutex{}, 40 | subs: make(map[chan *XPacket]struct{}), 41 | inCh: make(chan *XPacket, XBrokerInputChannelDepth), 42 | gop: make([]*XPacket, 0, 2000), 43 | droppedRx: 0, 44 | droppedTx: 0, 45 | } 46 | } 47 | 48 | func (b *XBroker) Start() { 49 | 50 | gotKF := false 51 | 52 | var lastVideo int64 = nanotime() 53 | var sendingIdleVid bool = true 54 | 55 | for m := range b.inCh { 56 | //b.capacityMaxRx = MaxInt(b.capacityMaxRx, cap(b.inCh)+1) 57 | 58 | switch m.Typ { 59 | case Audio: 60 | case Video: 61 | 62 | lastVideo = m.Arrival // faster than nanotime() 63 | 64 | if sendingIdleVid && m.Keyframe { 65 | sendingIdleVid = false 66 | //log.Println("SWITCHING TO INPUT, NOW INPUT VIDEO PRESENT") 67 | } 68 | 69 | if sendingIdleVid { 70 | continue 71 | } 72 | case IdleVideo: 73 | 74 | rxActive := m.Arrival-lastVideo <= int64(time.Second) // use arrival, not nanotime(), its cheaper 75 | 76 | if !rxActive && !sendingIdleVid && m.Keyframe { 77 | sendingIdleVid = true 78 | //log.Println("SWITCHING TO IDLE, NO INPUT VIDEO PRESENT") 79 | } 80 | 81 | if !sendingIdleVid { 82 | continue 83 | } 84 | 85 | m.Typ = Video //racy?? 86 | 87 | default: 88 | log.Fatal("invalid xpkt type") 89 | } 90 | 91 | // fast block 92 | b.mu.Lock() 93 | if m.Typ == Video { // save video GOPs 94 | tooLarge := len(b.gop) > 50000 95 | if m.Keyframe || tooLarge { 96 | for i := range b.gop { 97 | b.gop[i] = nil 98 | } 99 | b.gop = b.gop[0:0] 100 | gotKF = true 101 | } 102 | } 103 | if gotKF { 104 | b.gop = append(b.gop, m) // save audio and video for replay 105 | } 106 | 107 | // non-blocking send loop 108 | for ch := range b.subs { 109 | select { 110 | case ch <- m: 111 | //b.capacityMaxTx = MaxInt(b.capacityMaxTx, cap(ch)) 112 | default: 113 | b.droppedTx++ 114 | errlog.Println("xbroker TX drop count", b.droppedTx) 115 | } 116 | } 117 | b.mu.Unlock() 118 | } 119 | 120 | // NO NO NO! 121 | // we require/expect all subscribers 122 | // to terminate independantly from the close of the broker input chan 123 | // 124 | // so, we don't close the subscriber's input channels. 125 | // if we did this, we will try to close channels twice sometimes. 126 | // 127 | // Close all subscriber channeles 128 | // b.mu.Lock() 129 | // for k := range b.subs { 130 | // close(k) // close Writer() func 131 | // } 132 | // b.mu.Unlock() 133 | } 134 | 135 | func (b *XBroker) Stop() { 136 | //panic("this is done by idle generator") 137 | //racy 138 | close(b.inCh) 139 | } 140 | 141 | func (b *XBroker) Subscribe() chan *XPacket { 142 | 143 | c := make(chan *XPacket, XBrokerOutputChannelDepth) 144 | 145 | b.mu.Lock() 146 | defer b.mu.Unlock() 147 | 148 | b.subs[c] = struct{}{} 149 | 150 | return c 151 | } 152 | 153 | func (b *XBroker) SubscribeReplay() chan *XPacket { 154 | 155 | c := make(chan *XPacket, len(b.gop)+XBrokerOutputChannelDepth) 156 | for _, v := range b.gop { 157 | c <- v // pre load gop into channel 158 | } 159 | 160 | b.mu.Lock() 161 | defer b.mu.Unlock() 162 | 163 | b.subs[c] = struct{}{} 164 | 165 | return c 166 | } 167 | 168 | // RemoveClose 169 | // you may unsubscribe multiple times on the same channel 170 | // this functionality is needed for the Replay() func logic 171 | func (b *XBroker) RemoveClose(c chan *XPacket) { 172 | b.mu.Lock() 173 | defer b.mu.Unlock() 174 | 175 | if _, ok := b.subs[c]; ok { 176 | delete(b.subs, c) 177 | close(c) 178 | } 179 | } 180 | 181 | // non blocking 182 | // concurrent use 183 | func (b *XBroker) Publish(msg *XPacket) { 184 | 185 | select { 186 | case b.inCh <- msg: 187 | default: 188 | atomic.AddInt32(&b.droppedRx, 1) 189 | errlog.Println("xbroker RX drop count", b.droppedRx) 190 | } 191 | 192 | } 193 | 194 | func MaxInt(x, y int) int { 195 | if x < y { 196 | return y 197 | } 198 | return x 199 | } 200 | -------------------------------------------------------------------------------- /logotitle.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 20 | -------------------------------------------------------------------------------- /modd.conf: -------------------------------------------------------------------------------- 1 | **/*.go **/*.html modd.conf { 2 | #daemon: go run . --http :80 --ftl-key 123-abc --ftl-proxy-addr 192.168.86.23 -s foo --rtp-send 127.0.0.1:4444 --rtp-wireshark 3 | #daemon: go run . --http :80 --ftl-key 123-abc --rtp-send 127.0.0.1:4444 --rtp-wireshark 4 | 5 | daemon: go run --race ./cmd --http :80 --html ./html 6 | #daemon: go run . --http :80 --html ./html 7 | } 8 | -------------------------------------------------------------------------------- /notes/dns challenge decision logic.pseudo: -------------------------------------------------------------------------------- 1 | // editing using willumz.generic-pseudocode 2 | // How do we decide whether to do DNS ACME challenge? 3 | 4 | 5 | // Decision: do not do port open checking. just report on certifcage progress. 6 | // part of the reason for this, is the port 80 http challenge can work 7 | // even when we are not running an HTTP server that we instantiate. 8 | // and if cert cannot be obtained in 8 seconds, send message to user 9 | 10 | 11 | switch(httpsAutoFlag) do 12 | case "none": 13 | // no registerDDNS() 14 | case "public": 15 | // NO LONGER : 16 | // if httpport != 80 && httpsport != 443 do 17 | // info("Using DNS challenge due to port choices") 18 | // registerDDNS() 19 | // end 20 | // if you use -http-auto public, you must have 80 or 443 open 21 | 22 | case "local": 23 | registerDDNS() 24 | //case "auto": //no: the value isn't worth the new-user learning burden, for this option 25 | end 26 | -------------------------------------------------------------------------------- /notes/url-guide.md: -------------------------------------------------------------------------------- 1 | # URL Guide 2 | 3 | ## DeadSFU URL Summary 4 | 5 | | URL | Purpose | 6 | |--------------|-------------------------------------------------| 7 | | /whip | send video to SFU using WHIP, ?room required | 8 | | /whap | recv video from SFU using WHAP, ?room required | 9 | | /* | serve index.html in 'viewer'-mode | 10 | | /*?send | serve index.html in 'sender'-mode | 11 | | /favicon.ico | serve the DeadSFU favicon | 12 | 13 | ## DeadSFU URL Examples 14 | 15 | | URL | Purpose | 16 | |---------------------|-----------------------------------------------| 17 | | /whip?room=/foo/bar | send video to SFU using WHIP ?room required | 18 | | /whap?room=/foo/bar | recv video from SFU using WHAP ?room required | 19 | | /foo/bar | serve index.html in 'viewer'-mode | 20 | | /foo/bar?send | serve index.html in 'sender'-mode | 21 | | /favicon.ico | serve the DeadSFU favicon | 22 | 23 | ## Discussion should WHIP and WHAP be indicated by URL path or query params? (QP) 24 | 25 | If we use QP, not path, then 'rooms' then whip and whap would look like: 26 | xsfu.com/live/stream200?whip 27 | xsfu.com/live/stream200?whap 28 | and HTML access happens like: 29 | xsfu.com/index.html or xsfu.com/ 30 | 31 | If we use Path, not QP, then rooms are accessed like this: 32 | xsfu.com/whip?room=/live/stream200 33 | xsfu.com/whap?room=/live/stream200 34 | and HTML access happens like: 35 | xsfu.com/index.html or xsfu.com/ 36 | 37 | *In either case, we want the /index.html file to be served for all /.../ paths* 38 | This is so if a user goes to xsfu.com/live/stream200, they get a view-page, and maybe send-page link. 39 | 40 | 41 | ## Decision on Path vs Query Params for whip/whap indication 42 | 43 | *DECISION: use /whip and /whap and indicate room using query path: room=/foo/bar* 44 | *Decision this means we are not supporting /room200?whip* 45 | 46 | Reasons: 47 | - It may be a smidge conceptually easier for new devs to do /whip?room=feedxyz 48 | - It is certainly easier to write the Go muxer setup for /whip vs /feedxyz?whip 49 | 50 | After thinking about this twice. 51 | 52 | 53 | ## /whip and /whap explained 54 | 55 | /whip takes WHIP protocol, and uses 56 | 57 | 58 | -------------------------------------------------------------------------------- /scripts/gen.go: -------------------------------------------------------------------------------- 1 | // inspired 2 | // https://blog.carlmjohnson.net/post/2016-11-27-how-to-use-go-generate/ 3 | 4 | // +build ignore 5 | 6 | // go generate 7 | package main 8 | 9 | import ( 10 | //"fmt" 11 | "io/ioutil" 12 | //"strconv" 13 | 14 | //"fmt" 15 | 16 | //"io/ioutil" 17 | 18 | "net/http" 19 | ) 20 | 21 | func check(err error) { 22 | if err != nil { 23 | panic(err) 24 | } 25 | } 26 | 27 | const assetsBaseUrl= "https://github.com/x186k/x186k-sfu-assets/raw/main/" 28 | 29 | func main() { 30 | url:=assetsBaseUrl+"idle.screen.h264.pcapng" 31 | 32 | // a, err := ioutil.ReadFile("html/index.html") 33 | // die(err) 34 | // fmt.Println(len(a)) 35 | 36 | rsp, err := http.Get(url) 37 | check(err) 38 | defer rsp.Body.Close() 39 | 40 | 41 | // for k,v:=range rsp.Header{ 42 | // fmt.Println(k,v) 43 | // } 44 | // XXX really should check md5 and filelen, and avoid download if same 45 | 46 | // len,err:=strconv.Atoi(rsp.Header.Get("Content-Length")) 47 | // check(err) 48 | 49 | // if len!= 50 | 51 | raw, err := ioutil.ReadAll(rsp.Body) 52 | check(err) 53 | 54 | err = ioutil.WriteFile("embed/idle.screen.h264.pcapng", raw, 0777) 55 | check(err) 56 | 57 | } 58 | -------------------------------------------------------------------------------- /test/alloc_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "reflect" 5 | "sync" 6 | "testing" 7 | "time" 8 | "unsafe" 9 | 10 | "github.com/pion/rtp" 11 | //"github.com/x186k/deadsfu/internal/sfu" 12 | ) 13 | 14 | type XPacket1500 struct { 15 | Arrival int64 16 | Pkt rtp.Packet 17 | Typ int32 18 | Keyframe bool 19 | Buf [1500]byte 20 | } 21 | type XPacket9000 struct { 22 | Arrival int64 23 | Pkt rtp.Packet 24 | Typ int32 25 | Keyframe bool 26 | Buf [9000]byte 27 | } 28 | 29 | func BenchmarkAllocStack1500(b *testing.B) { 30 | for N := 0; N < b.N; N++ { 31 | // a := XXPacket{} 32 | // a.Buf = make([]byte, 1460) 33 | a := XPacket1500{} 34 | 35 | _ = a 36 | } 37 | } 38 | 39 | var Obj1500 XPacket1500 40 | var Obj9000 XPacket9000 41 | 42 | func BenchmarkAllocHeap1500(b *testing.B) { 43 | for N := 0; N < b.N; N++ { 44 | Obj1500 = XPacket1500{} 45 | _ = Obj1500 46 | } 47 | } 48 | 49 | func BenchmarkAllocHeap1500Parallel(b *testing.B) { 50 | b.RunParallel(func(pb *testing.PB) { 51 | for pb.Next() { 52 | Obj1500 = XPacket1500{} 53 | _ = Obj1500 54 | } 55 | }) 56 | b.StopTimer() // required to get duration 57 | reportCPUUtil(b, 1500) 58 | } 59 | 60 | func BenchmarkAllocHeap9000(b *testing.B) { 61 | for N := 0; N < b.N; N++ { 62 | Obj9000 = XPacket9000{} 63 | _ = Obj9000 64 | } 65 | } 66 | 67 | func BenchmarkAllocHeap9000Parallel(b *testing.B) { 68 | b.RunParallel(func(pb *testing.PB) { 69 | for pb.Next() { 70 | Obj9000 = XPacket9000{} 71 | _ = Obj9000 72 | } 73 | }) 74 | b.StopTimer() // required to get duration 75 | 76 | reportCPUUtil(b, 9000) 77 | } 78 | 79 | //for example 80 | // units -v '100megabit/sec * 1/1500bytes * 70ns' 81 | // units -v '100megabit/sec * 1byte/8bits * 1sec/1e9ns * 1/1500bytes * 70ns' 82 | // units -v '100megabit/sec * 1/1500bytes * 70ns' 83 | // Definition: 0.00058333333 84 | func reportCPUUtil(b *testing.B, mtu float64) { 85 | duration := GetDuration(b) 86 | nsPerOp := float64(duration) / float64(b.N) 87 | b.ReportMetric(nsPerOp, "ns/op2") 88 | 89 | averagePacketLen := mtu / 2 90 | 91 | cpuutil := 100e6 / 8 / 1e9 / averagePacketLen * nsPerOp 92 | b.ReportMetric(cpuutil, "100mbps-cpuFraction") 93 | b.ReportMetric(cpuutil*100, "100mbps-cpuPercent") 94 | } 95 | 96 | var bytePool1500 = sync.Pool{ 97 | New: func() interface{} { 98 | return new(XPacket1500) 99 | }, 100 | } 101 | var bytePool9000 = sync.Pool{ 102 | New: func() interface{} { 103 | return new(XPacket9000) 104 | }, 105 | } 106 | 107 | var Pub int 108 | 109 | func BenchmarkAllocPool1500(b *testing.B) { 110 | for N := 0; N < b.N; N++ { 111 | obj := bytePool1500.Get().(*XPacket1500) 112 | _ = obj 113 | Pub += int(obj.Arrival) 114 | bytePool1500.Put(obj) 115 | } 116 | } 117 | func BenchmarkAllocPool9000(b *testing.B) { 118 | for N := 0; N < b.N; N++ { 119 | obj := bytePool9000.Get().(*XPacket9000) 120 | _ = obj 121 | Pub += int(obj.Arrival) 122 | bytePool9000.Put(obj) 123 | } 124 | } 125 | func BenchmarkAllocPoolParallel9000(b *testing.B) { 126 | b.RunParallel(func(pb *testing.PB) { 127 | for pb.Next() { 128 | obj := bytePool9000.Get().(*XPacket9000) 129 | _ = obj 130 | Pub += int(obj.Arrival) 131 | bytePool9000.Put(obj) 132 | } 133 | }) 134 | 135 | } 136 | 137 | func BenchmarkXX(b *testing.B) { 138 | 139 | for i := 0; i < b.N; i++ { 140 | time.Sleep(time.Microsecond) 141 | } 142 | } 143 | 144 | func GetDuration(b *testing.B) time.Duration { 145 | 146 | durintf := GetUnexportedField(reflect.ValueOf(b).Elem().FieldByName("duration")) 147 | duration := durintf.(time.Duration) 148 | return duration 149 | } 150 | 151 | func GetUnexportedField(field reflect.Value) interface{} { 152 | return reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Interface() 153 | } 154 | -------------------------------------------------------------------------------- /test/perf_test.go: -------------------------------------------------------------------------------- 1 | package test 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "reflect" 7 | "testing" 8 | "time" 9 | "unsafe" 10 | 11 | "github.com/pion/rtp" 12 | ) 13 | 14 | type WriteRtpIntf interface { 15 | WriteRTP(p *rtp.Packet) error 16 | } 17 | 18 | type RtpSplicer struct { 19 | lastUnixnanosNow int64 20 | lastSSRC uint32 21 | lastTS uint32 22 | tsOffset uint32 23 | lastSN uint16 24 | snOffset uint16 25 | } 26 | 27 | func (s *RtpSplicer) SpliceWriteRTPPointer(trk WriteRtpIntf, p *rtp.Packet, unixnano int64, rtphz int64) { 28 | 29 | // credit to Orlando Co of ion-sfu 30 | // for helping me decide to go this route and keep it simple 31 | // code is modeled on code from ion-sfu 32 | // 12/30/21 maybe we should use something else other than SSRC transitions? 33 | if p.SSRC != s.lastSSRC { 34 | 35 | if unixnano == 0 { 36 | panic("unixnano cannot be zero.") 37 | } 38 | 39 | td1 := unixnano - s.lastUnixnanosNow // nanos 40 | if td1 < 0 { 41 | td1 = 0 // be positive or zero! (go monotonic clocks should mean this never happens) 42 | } 43 | //td2 := td1 * rtphz / int64(time.Second) //convert nanos -> 90khz or similar clockrate 44 | td2 := td1 / (int64(time.Second) / rtphz) //convert nanos -> 90khz or similar clockrate. speed not important 45 | if td2 == 0 { 46 | td2 = 1 47 | } 48 | s.tsOffset = p.Timestamp - (s.lastTS + uint32(td2)) 49 | s.snOffset = p.SequenceNumber - s.lastSN - 1 50 | 51 | } 52 | 53 | p.Timestamp -= s.tsOffset 54 | p.SequenceNumber -= s.snOffset 55 | 56 | // xdbg := true 57 | // if xdbg { 58 | // if p.SSRC != s.lastSSRC && rtphz == 90000 { 59 | // pl("last ts", s.lastTS, s.lastUnixnanosNow) 60 | // pl("new ts", p.Timestamp, unixnano) 61 | // } 62 | // } 63 | 64 | s.lastUnixnanosNow = unixnano 65 | s.lastTS = p.Timestamp 66 | s.lastSN = p.SequenceNumber 67 | s.lastSSRC = p.SSRC 68 | 69 | //I had believed it was possible to see: io.ErrClosedPipe { 70 | // but I no longer believe this to be true 71 | // if it turns out I can see those, we will need to adjust 72 | err := trk.WriteRTP(p) 73 | if err != nil { 74 | panic(err) 75 | } 76 | } 77 | 78 | func (s *RtpSplicer) SpliceWriteRTP(trk WriteRtpIntf, p rtp.Packet, unixnano int64, rtphz int64) { 79 | 80 | // credit to Orlando Co of ion-sfu 81 | // for helping me decide to go this route and keep it simple 82 | // code is modeled on code from ion-sfu 83 | // 12/30/21 maybe we should use something else other than SSRC transitions? 84 | if p.SSRC != s.lastSSRC { 85 | 86 | if unixnano == 0 { 87 | panic("unixnano cannot be zero.") 88 | } 89 | 90 | td1 := unixnano - s.lastUnixnanosNow // nanos 91 | if td1 < 0 { 92 | td1 = 0 // be positive or zero! (go monotonic clocks should mean this never happens) 93 | } 94 | //td2 := td1 * rtphz / int64(time.Second) //convert nanos -> 90khz or similar clockrate 95 | td2 := td1 / (int64(time.Second) / rtphz) //convert nanos -> 90khz or similar clockrate. speed not important 96 | if td2 == 0 { 97 | td2 = 1 98 | } 99 | s.tsOffset = p.Timestamp - (s.lastTS + uint32(td2)) 100 | s.snOffset = p.SequenceNumber - s.lastSN - 1 101 | 102 | } 103 | 104 | p.Timestamp -= s.tsOffset 105 | p.SequenceNumber -= s.snOffset 106 | 107 | // xdbg := true 108 | // if xdbg { 109 | // if p.SSRC != s.lastSSRC && rtphz == 90000 { 110 | // pl("last ts", s.lastTS, s.lastUnixnanosNow) 111 | // pl("new ts", p.Timestamp, unixnano) 112 | // } 113 | // } 114 | 115 | s.lastUnixnanosNow = unixnano 116 | s.lastTS = p.Timestamp 117 | s.lastSN = p.SequenceNumber 118 | s.lastSSRC = p.SSRC 119 | 120 | //I had believed it was possible to see: io.ErrClosedPipe { 121 | // but I no longer believe this to be true 122 | // if it turns out I can see those, we will need to adjust 123 | err := trk.WriteRTP(&p) 124 | if err != nil { 125 | panic(err) 126 | } 127 | } 128 | 129 | type Z struct{} 130 | 131 | func (z Z) WriteRTP(p *rtp.Packet) error { 132 | return nil 133 | } 134 | 135 | var z Z 136 | 137 | func BenchmarkSplicePointer(b *testing.B) { 138 | spl := &RtpSplicer{} 139 | p := &rtp.Packet{} 140 | for i := 0; i < b.N; i++ { 141 | spl.SpliceWriteRTPPointer(z, p, int64(i), 90000) 142 | } 143 | } 144 | func BenchmarkSpliceStack(b *testing.B) { 145 | spl := &RtpSplicer{} 146 | p := rtp.Packet{} 147 | for i := 0; i < b.N; i++ { 148 | spl.SpliceWriteRTP(z, p, int64(i), 90000) 149 | } 150 | } 151 | 152 | type Pptr *rtp.Packet 153 | 154 | func TestX(t *testing.T) { 155 | var i *rtp.Packet 156 | 157 | fmt.Fprintln(os.Stderr, 9999) 158 | fmt.Printf("Size of *rtp.packet (reflect.TypeOf.Size): %d\n", reflect.TypeOf(i).Size()) 159 | fmt.Printf("Size of *rtp.packet (unsafe.Sizeof): %d\n", unsafe.Sizeof(i)) 160 | 161 | var j rtp.Packet 162 | fmt.Printf("Size of rtp.packet (reflect.TypeOf.Size): %d\n", reflect.TypeOf(j).Size()) 163 | fmt.Printf("Size of rtp.packet (unsafe.Sizeof): %d\n", unsafe.Sizeof(j)) 164 | } 165 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | .bundle 3 | .jekyll-cache 4 | .sass-cache 5 | _site 6 | Gemfile.lock 7 | -------------------------------------------------------------------------------- /website/Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | 4 | 5 | 6 | gem "kramdown-parser-gfm" 7 | gem "jekyll-optional-front-matter", "0.3.2" 8 | gem "jekyll-relative-links", "0.6.1" 9 | #gem "jekyll-remote-theme", "0.4.3" 10 | 11 | 12 | -------------------------------------------------------------------------------- /website/INDEX.md: -------------------------------------------------------------------------------- 1 | --- 2 | permalink: index.html 3 | --- 4 | 5 | 6 | # DeadSFU 7 | 8 | Please visit the [Github repo](https://github.com/x186k/deadsfu) to learn how you can use DeadSFU. 9 | -------------------------------------------------------------------------------- /website/LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /website/README.md: -------------------------------------------------------------------------------- 1 | # Brutal Cam Theme 2 | 3 | I'm afraid is what happens when you let a back-end developer design a theme. 4 | 5 | A brutalist, minimalist theme. (I think) 6 | 7 | ## Installation 8 | 9 | You can fork or clone the repo. 10 | If you do a github GUI fork, then clone your fork of the repo, 11 | you can get updates (if any) to this theme by using the Github GUI. 12 | 13 | Basic way to clone and view site: 14 | 15 | git clone https://github.com/cameronelliott/brutal-cam 16 | 17 | cd brutal-cam 18 | 19 | 20 | Server for working with live-reload, open your browser to http://127.0.0.1:4000 21 | ```bash 22 | docker run --rm -it -v $PWD:/srv/jekyll -p 4000:4000 -p 35729:35729 jekyll/builder:latest jekyll serve --livereload 23 | ``` 24 | 25 | Build to _site 26 | ```bash 27 | docker run --rm -it -v $PWD:/srv/jekyll -p 127.0.0.1:4000:4000 jekyll/builder:latest jekyll build 28 | ``` -------------------------------------------------------------------------------- /website/_config.yml: -------------------------------------------------------------------------------- 1 | title: DeadSFU 2 | 3 | 4 | headlines: | 5 | 6 | 7 | 8 | 9 | 10 | 11 | include: 12 | - README.md 13 | 14 | # remote_theme: cameronelliott/brutal-cam 15 | 16 | permalink: pretty 17 | 18 | # relative_links: 19 | # enabled: true 20 | # collections: false 21 | # exclude: 22 | # - index.md 23 | 24 | 25 | sass: 26 | style: :compressed 27 | 28 | future: true 29 | 30 | # get help 31 | # https://github.com/benbalter/jekyll-relative-links 32 | # https://github.com/benbalter/jekyll-optional-front-matter 33 | 34 | plugins: 35 | - jekyll-relative-links 36 | - jekyll-optional-front-matter 37 | # - jekyll-remote-theme 38 | 39 | defaults: 40 | - 41 | scope: 42 | path: "" # an empty string here means all files in the project 43 | values: 44 | layout: "default" 45 | 46 | -------------------------------------------------------------------------------- /website/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ site.title | default: site.github.repository_name }} 7 | 8 | 9 | 10 | {{ site.headlines }} 11 | 12 | 13 | 14 | 15 | 16 | {{ content }} 17 | 18 | 19 | 20 | 21 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /website/assets/css/autumn.css: -------------------------------------------------------------------------------- 1 | .highlight .hll { background-color: #ffffcc } 2 | .highlight .c { color: #aaaaaa; font-style: italic } /* Comment */ 3 | .highlight .err { color: #F00000; background-color: #F0A0A0 } /* Error */ 4 | .highlight .k { color: #0000aa } /* Keyword */ 5 | .highlight .cm { color: #aaaaaa; font-style: italic } /* Comment.Multiline */ 6 | .highlight .cp { color: #4c8317 } /* Comment.Preproc */ 7 | .highlight .c1 { color: #aaaaaa; font-style: italic } /* Comment.Single */ 8 | .highlight .cs { color: #0000aa; font-style: italic } /* Comment.Special */ 9 | .highlight .gd { color: #aa0000 } /* Generic.Deleted */ 10 | .highlight .ge { font-style: italic } /* Generic.Emph */ 11 | .highlight .gr { color: #aa0000 } /* Generic.Error */ 12 | .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ 13 | .highlight .gi { color: #00aa00 } /* Generic.Inserted */ 14 | .highlight .go { color: #888888 } /* Generic.Output */ 15 | .highlight .gp { color: #555555 } /* Generic.Prompt */ 16 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 17 | .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ 18 | .highlight .gt { color: #aa0000 } /* Generic.Traceback */ 19 | .highlight .kc { color: #0000aa } /* Keyword.Constant */ 20 | .highlight .kd { color: #0000aa } /* Keyword.Declaration */ 21 | .highlight .kn { color: #0000aa } /* Keyword.Namespace */ 22 | .highlight .kp { color: #0000aa } /* Keyword.Pseudo */ 23 | .highlight .kr { color: #0000aa } /* Keyword.Reserved */ 24 | .highlight .kt { color: #00aaaa } /* Keyword.Type */ 25 | .highlight .m { color: #009999 } /* Literal.Number */ 26 | .highlight .s { color: #aa5500 } /* Literal.String */ 27 | .highlight .na { color: #1e90ff } /* Name.Attribute */ 28 | .highlight .nb { color: #00aaaa } /* Name.Builtin */ 29 | .highlight .nc { color: #00aa00; text-decoration: underline } /* Name.Class */ 30 | .highlight .no { color: #aa0000 } /* Name.Constant */ 31 | .highlight .nd { color: #888888 } /* Name.Decorator */ 32 | .highlight .ni { color: #800000; font-weight: bold } /* Name.Entity */ 33 | .highlight .nf { color: #00aa00 } /* Name.Function */ 34 | .highlight .nn { color: #00aaaa; text-decoration: underline } /* Name.Namespace */ 35 | .highlight .nt { color: #1e90ff; font-weight: bold } /* Name.Tag */ 36 | .highlight .nv { color: #aa0000 } /* Name.Variable */ 37 | .highlight .ow { color: #0000aa } /* Operator.Word */ 38 | .highlight .w { color: #bbbbbb } /* Text.Whitespace */ 39 | .highlight .mf { color: #009999 } /* Literal.Number.Float */ 40 | .highlight .mh { color: #009999 } /* Literal.Number.Hex */ 41 | .highlight .mi { color: #009999 } /* Literal.Number.Integer */ 42 | .highlight .mo { color: #009999 } /* Literal.Number.Oct */ 43 | .highlight .sb { color: #aa5500 } /* Literal.String.Backtick */ 44 | .highlight .sc { color: #aa5500 } /* Literal.String.Char */ 45 | .highlight .sd { color: #aa5500 } /* Literal.String.Doc */ 46 | .highlight .s2 { color: #aa5500 } /* Literal.String.Double */ 47 | .highlight .se { color: #aa5500 } /* Literal.String.Escape */ 48 | .highlight .sh { color: #aa5500 } /* Literal.String.Heredoc */ 49 | .highlight .si { color: #aa5500 } /* Literal.String.Interpol */ 50 | .highlight .sx { color: #aa5500 } /* Literal.String.Other */ 51 | .highlight .sr { color: #009999 } /* Literal.String.Regex */ 52 | .highlight .s1 { color: #aa5500 } /* Literal.String.Single */ 53 | .highlight .ss { color: #0000aa } /* Literal.String.Symbol */ 54 | .highlight .bp { color: #00aaaa } /* Name.Builtin.Pseudo */ 55 | .highlight .vc { color: #aa0000 } /* Name.Variable.Class */ 56 | .highlight .vg { color: #aa0000 } /* Name.Variable.Global */ 57 | .highlight .vi { color: #aa0000 } /* Name.Variable.Instance */ 58 | .highlight .il { color: #009999 } /* Literal.Number.Integer.Long */ 59 | -------------------------------------------------------------------------------- /website/assets/fuckingstyle.scss: -------------------------------------------------------------------------------- 1 | --- 2 | --- 3 | 4 | body { 5 | max-width: 650px; 6 | margin: 40px auto; 7 | padding: 0 10px; 8 | font: 18px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; 9 | color: #444; 10 | } 11 | 12 | h1, 13 | h2, 14 | h3 { 15 | line-height: 1.2; 16 | } 17 | 18 | @media (prefers-color-scheme: dark) { 19 | body { 20 | color: #ccc; 21 | background: black; 22 | } 23 | 24 | a:link { 25 | color: #5bf; 26 | } 27 | 28 | a:visited { 29 | color: #ccf; 30 | } 31 | } 32 | 33 | // cameron 34 | a { 35 | &:link { 36 | color:blue; 37 | } 38 | &:visited { 39 | color:blue; 40 | } 41 | } 42 | 43 | //cameron 44 | /* _sass/_post.css */ 45 | // https://www.dannyguo.com/blog/how-to-add-copy-to-clipboard-buttons-to-code-blocks-in-hugo/ 46 | pre.highlight > button { 47 | opacity: 0; 48 | } 49 | 50 | pre.highlight:hover > button { 51 | opacity: 1; 52 | } 53 | 54 | pre.highlight > button:active, 55 | pre.highlight > button:focus { 56 | opacity: 1; 57 | } 58 | 59 | @import "/assets/css/autumn.css"; 60 | 61 | 62 | .copy-code-button { 63 | color: #272822; 64 | background-color: #FFF; 65 | border-color: #272822; 66 | border: 2px solid; 67 | border-radius: 3px 3px 0px 0px; 68 | 69 | /* right-align */ 70 | display: block; 71 | margin-left: auto; 72 | margin-right: 0; 73 | 74 | margin-bottom: -2px; 75 | padding: 3px 8px; 76 | font-size: 0.8em; 77 | } 78 | 79 | .copy-code-button:hover { 80 | cursor: pointer; 81 | background-color: #F2F2F2; 82 | } 83 | 84 | .copy-code-button:focus { 85 | /* Avoid an ugly focus outline on click in Chrome, 86 | but darken the button for accessibility. 87 | See https://stackoverflow.com/a/25298082/1481479 */ 88 | background-color: #E6E6E6; 89 | outline: 0; 90 | } 91 | 92 | .copy-code-button:active { 93 | background-color: #D9D9D9; 94 | } 95 | 96 | .highlight pre { 97 | /* Avoid pushing up the copy buttons. */ 98 | margin: 0; 99 | } 100 | 101 | pre.highlight { 102 | background:#f0f0f0; 103 | overflow:auto; 104 | } 105 | 106 | -------------------------------------------------------------------------------- /website/brutal-cam.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | Gem::Specification.new do |spec| 4 | spec.name = "brutal-cam" 5 | spec.version = "0.1.0" 6 | spec.authors = [""] 7 | spec.email = [""] 8 | 9 | spec.summary = "TODO: Write a short summary, because Rubygems requires one." 10 | spec.homepage = "TODO: Put your gem's website or public repo URL here." 11 | spec.license = "MIT" 12 | 13 | spec.files = `git ls-files -z`.split("\x0").select { |f| f.match(%r!^(assets|_layouts|_includes|_sass|LICENSE|README|_config\.yml)!i) } 14 | 15 | spec.add_runtime_dependency "jekyll", "~> 4.2" 16 | end 17 | -------------------------------------------------------------------------------- /website/codeblocks.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | codeblock: 5 | ```bash 6 | curl -sL https://github.com/x186k/deadsfu/releases/latest/download/deadsfu-darwin-arm64.tar.gz | tar xvz 7 | ``` 8 | 9 | 10 | 11 | 12 | codeblock: 13 | ```bash 14 | ls -ald .. 15 | ``` 16 | 17 | 18 | --------------------------------------------------------------------------------