├── .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 |
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 |
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 |
36 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
error
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
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 |
--------------------------------------------------------------------------------