Compare commits
55 Commits
Author | SHA1 | Date |
---|---|---|
stryan | 5d413e3806 | |
Tyler Stiene | 7010fd277a | |
Tyler Stiene | 1e46cbbc6e | |
Tyler Stiene | 52c3132424 | |
Tyler Stiene | 4b941100fc | |
Tyler Stiene | 23a4b6fa66 | |
Tyler Stiene | 07ee7b36c5 | |
Tyler Stiene | f520fc67c7 | |
Tyler Stiene | c0ed696543 | |
Tyler Stiene | c28a9cd5f8 | |
Tyler Stiene | c410c49095 | |
Tyler Stiene | 08b4eb3771 | |
Tyler Stiene | 3d24e1dbc4 | |
Tyler Stiene | 3e96b27564 | |
Tyler Stiene | df2a477147 | |
Christoffer Tibell | 682887b936 | |
Tyler Stiene | 7dea05bdb4 | |
Tyler Stiene | 404af876db | |
Tyler Stiene | fd4884cdb6 | |
Tyler Stiene | c94362581a | |
Tyler Stiene | 8ca66fb500 | |
Tyler Stiene | 0f57c5d33a | |
Tyler Stiene | 52ea304bd1 | |
stryan | fb5940bfb8 | |
Tyler Stiene | 649fe9f33e | |
Tyler Stiene | 5c5ccb72c9 | |
Tyler Stiene | f7f492670c | |
Tyler Stiene | 6658c8534c | |
Tyler Stiene | cb4d2349c5 | |
Tyler Stiene | 6a188336b9 | |
Tyler Stiene | dbe46c87fc | |
Tyler Stiene | 78aab791d2 | |
Tyler Stiene | 2888928c51 | |
Tyler Stiene | 72fe283778 | |
Tyler Stiene | 27f8e5ba65 | |
Tyler Stiene | a963d2caa1 | |
Tyler Stiene | 5bc118c97c | |
Tyler Stiene | 9246563a35 | |
Tyler Stiene | 235dc44f41 | |
Tyler Stiene | a1f6a60b89 | |
Tyler Stiene | c7f79ba01d | |
Tyler Stiene | 29ff1d797f | |
Tyler Stiene | c959774c23 | |
Tyler Stiene | d18691d0f0 | |
Tyler Stiene | cc387ba3b9 | |
Tyler Stiene | bad460e57c | |
Tyler Stiene | b4a1a793a7 | |
Tyler Stiene | 9fb5dc5af5 | |
Tyler Stiene | 87f1e1a87e | |
Tyler Stiene | a18df28213 | |
Tyler Stiene | 026cdff797 | |
Tyler Stiene | dcc3fd48b4 | |
Tyler Stiene | 910eaa5954 | |
Tyler Stiene | 480fa533a2 | |
Tyler Stiene | 177553f3a4 |
|
@ -0,0 +1,33 @@
|
|||
name: 'Docker'
|
||||
on:
|
||||
create:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
jobs:
|
||||
docker-build-push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v1
|
||||
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
username: ${{ secrets.DOCKER_USERNAME }}
|
||||
password: ${{ secrets.DOCKER_PASSWORD }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@v1
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: stieneee/mumble-discord-bridge:latest,stieneee/mumble-discord-bridge:${{ github.ref_name }},ghcr.io/stieneee/mumble-discord-bridge:latest,ghcr.io/stieneee/mumble-discord-bridge:${{ github.ref_name }}
|
|
@ -1,29 +1,54 @@
|
|||
name: 'CI'
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*'
|
||||
branches:
|
||||
- main
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
goreleaser:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
-
|
||||
name: Set up Go
|
||||
|
||||
- name: Install libopus-dev zip
|
||||
run: sudo apt update && sudo apt-get -y install libopus-dev zip
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.15
|
||||
-
|
||||
name: Run GoReleaser
|
||||
go-version: 1.17
|
||||
|
||||
- name: go-license install
|
||||
run: go install github.com/google/go-licenses@latest
|
||||
|
||||
- name: go-license save
|
||||
run: go-licenses save ./cmd/mumble-discord-bridge --force --save_path="./LICENSES" && zip -r -9 LICENSES.zip ./LICENSES
|
||||
|
||||
- name: Run GoReleaser Build
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
with:
|
||||
version: latest
|
||||
args: build --rm-dist --skip-validate
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Run GoReleaser Release
|
||||
uses: goreleaser/goreleaser-action@v2
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
with:
|
||||
version: latest
|
||||
args: release --rm-dist
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload assets
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: mdb
|
||||
path: dist/*
|
|
@ -1,7 +1,12 @@
|
|||
.env
|
||||
main
|
||||
mumble-discord-bridge
|
||||
dist
|
||||
bridge
|
||||
.prof
|
||||
cert.pem
|
||||
*.prof
|
||||
*.out
|
||||
*.test
|
||||
cert.pem
|
||||
*.gob
|
||||
docker-compose.yml
|
||||
mdb-local
|
||||
LICENSES
|
||||
LICENSES.zip
|
|
@ -7,7 +7,8 @@ before:
|
|||
# you may remove this if you don't need go generate
|
||||
# - go generate ./...
|
||||
builds:
|
||||
- ldflags: '-s -w -linkmode external -extldflags "-static" -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser'
|
||||
- main: ./cmd/mumble-discord-bridge
|
||||
ldflags: '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}} -X main.date={{.Date}} -X main.builtBy=goreleaser'
|
||||
env:
|
||||
- CGO_ENABLED=1
|
||||
flags:
|
||||
|
@ -18,12 +19,37 @@ builds:
|
|||
# - darwin
|
||||
goarch:
|
||||
- amd64
|
||||
# dockers:
|
||||
# - goos: linux
|
||||
# goarch: amd64
|
||||
# image_templates:
|
||||
# - "ghcr.io/stieneee/mumble-discord-bridge:latest"
|
||||
# - "ghcr.io/stieneee/mumble-discord-bridge:{{ .Tag }}"
|
||||
# - "stieneee/mumble-discord-bridge:latest"
|
||||
# - "stieneee/mumble-discord-bridge:{{ .Tag }}"
|
||||
# skip_push: "false"
|
||||
# dockerfile: Dockerfile
|
||||
# use: docker
|
||||
# build_flag_templates:
|
||||
# - "--pull"
|
||||
# - "--label=org.opencontainers.image.created={{.Date}}"
|
||||
# - "--label=org.opencontainers.image.title={{.ProjectName}}"
|
||||
# - "--label=org.opencontainers.image.revision={{.FullCommit}}"
|
||||
# - "--label=org.opencontainers.image.version={{.Version}}"
|
||||
# - "--platform=linux/amd64"
|
||||
# push_flags:
|
||||
# - --tls-verify=false
|
||||
# extra_files:
|
||||
# - config.yml
|
||||
|
||||
archives:
|
||||
# archives:
|
||||
checksum:
|
||||
name_template: 'checksums.txt'
|
||||
snapshot:
|
||||
name_template: "{{ .Tag }}-next"
|
||||
release:
|
||||
extra_files:
|
||||
- glob: "./LICENSES.zip"
|
||||
changelog:
|
||||
sort: asc
|
||||
filters:
|
||||
|
|
18
Dockerfile
18
Dockerfile
|
@ -2,18 +2,28 @@
|
|||
|
||||
# Stage 1
|
||||
|
||||
FROM golang:1.15 as builder
|
||||
FROM golang:1.17 as builder
|
||||
WORKDIR /go/src/app
|
||||
COPY . .
|
||||
RUN curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | sh
|
||||
RUN ./bin/goreleaser build --skip-validate
|
||||
RUN apt update && apt install -y libopus-dev
|
||||
RUN go install github.com/goreleaser/goreleaser@latest
|
||||
RUN go install github.com/google/go-licenses@latest
|
||||
RUN goreleaser build --skip-validate
|
||||
RUN go-licenses save ./cmd/mumble-discord-bridge --force --save_path="./dist/LICENSES"
|
||||
|
||||
# Stage 2
|
||||
|
||||
FROM alpine:latest as static
|
||||
FROM alpine:latest as final
|
||||
WORKDIR /opt/
|
||||
RUN apk add opus
|
||||
RUN mkdir /lib64 && ln -s /lib/libc.musl-x86_64.so.1 /lib64/ld-linux-x86-64.so.2
|
||||
COPY --from=builder /go/src/app/dist/LICENSES .
|
||||
COPY --from=builder /go/src/app/dist/mumble-discord-bridge_linux_amd64/mumble-discord-bridge .
|
||||
|
||||
# FROM ubuntu:latest as final
|
||||
# WORKDIR /opt/
|
||||
# RUN apt update && apt install -y libopus0 ca-certificates && apt clean
|
||||
# COPY --from=builder /go/src/app/dist/mumble-discord-bridge_linux_amd64/mumble-discord-bridge .
|
||||
|
||||
# Entry Point
|
||||
CMD ["/opt/mumble-discord-bridge"]
|
||||
|
|
49
Makefile
49
Makefile
|
@ -1,22 +1,49 @@
|
|||
GOFILES=main.go mumble.go discord.go bridge.go config.go mumble-handlers.go discord-handlers.go
|
||||
GOFILES=$(shell find ./ -type f -name '*.go')
|
||||
LATEST_TAG=$(shell git describe --tags `git rev-list --tags --max-count=1`)
|
||||
|
||||
mumble-discord-bridge: $(GOFILES)
|
||||
goreleaser build --skip-validate --rm-dist
|
||||
mumble-discord-bridge: $(GOFILES) .goreleaser.yml
|
||||
goreleaser build --skip-validate --rm-dist --single-target --auto-snapshot
|
||||
|
||||
dev: $(GOFILES)
|
||||
goreleaser build --skip-validate --rm-dist && sudo ./dist/mumble-discord-bridge_linux_amd64/mumble-discord-bridge
|
||||
release:
|
||||
rm -rf LICENSES.zip LICENSES
|
||||
go-licenses save ./cmd/mumble-discord-bridge --save_path="./LICENSES"
|
||||
zip -r -9 LICENSES.zip ./LICENSES
|
||||
goreleaser release --rm-dist
|
||||
|
||||
dev-profile: $(GOFILES)
|
||||
goreleaser build --skip-validate --rm-dist && sudo ./dist/mumble-discord-bridge_linux_amd64/mumble-discord-bridge -cpuprofile cpu.prof
|
||||
dev: $(GOFILES) .goreleaser.yml
|
||||
goreleaser build --skip-validate --rm-dist --single-target --snapshot && sudo ./dist/mumble-discord-bridge_linux_amd64/mumble-discord-bridge
|
||||
|
||||
dev-race: $(GOFILES) .goreleaser.yml
|
||||
go run -race ./cmd/mumble-discord-bridge
|
||||
|
||||
dev-profile: $(GOFILES) .goreleaser.yml
|
||||
goreleaser build --skip-validate --rm-dist --single-target --snapshot && sudo ./dist/mumble-discord-bridge_linux_amd64/mumble-discord-bridge -cpuprofile cpu.prof
|
||||
|
||||
test-chart: SHELL:=/bin/bash
|
||||
test-chart:
|
||||
go test ./test &
|
||||
until pidof test.test; do continue; done;
|
||||
psrecord --plot docs/test-cpu-memory.png $$(pidof mumble-discord-bridge.test)
|
||||
|
||||
docker-latest:
|
||||
docker build -t stieneee/mumble-discord-bridge:latest .
|
||||
docker build -t stieneee/mumble-discord-bridge:latest -t stieneee/mumble-discord-bridge:$(LATEST_TAG) -t ghcr.io/stieneee/mumble-discord-bridge:latest -t ghcr.io/stieneee/mumble-discord-bridge:$(LATEST_TAG) .
|
||||
|
||||
docker-latest-run:
|
||||
docker run --env-file .env -it stieneee/mumble-discord-bridge:latest
|
||||
|
||||
docker-release:
|
||||
docker push stieneee/mumble-discord-bridge:latest
|
||||
docker push stieneee/mumble-discord-bridge:$(LATEST_TAG)
|
||||
docker push ghcr.io/stieneee/mumble-discord-bridge:latest
|
||||
docker push ghcr.io/stieneee/mumble-discord-bridge:$(LATEST_TAG)
|
||||
|
||||
docker-next:
|
||||
docker build -t stieneee/mumble-discord-bridge:next .
|
||||
docker build -t stieneee/mumble-discord-bridge:next -t ghcr.io/stieneee/mumble-discord-bridge:next .
|
||||
docker push stieneee/mumble-discord-bridge:next
|
||||
docker push ghcr.io/stieneee/mumble-discord-bridge:next
|
||||
|
||||
clean:
|
||||
rm -f mumble-discord-bridge
|
||||
rm -rf dist
|
||||
rm -rf LICENSES.zip LICENSES
|
||||
|
||||
.PHONY: docker-latest docker-latest-push clean
|
||||
.PHONY: release dev dev-profile dev-race test-chart docker-latest docker-latest-release docker-release docker-next clean
|
127
README.md
127
README.md
|
@ -4,71 +4,52 @@ Mumble Discord Bridge is an open source Go application to bridge the audio betwe
|
|||
|
||||
It was built with the hope that people can continue to use the voice application of their choice.
|
||||
|
||||
## PatchCord.io
|
||||
|
||||
Mumble Discord Bridge can be hosted on any server or computer and ships in a Docker container for convenience.
|
||||
|
||||
If hosting this application yourself seem like an difficult task please consider [PatchCord.io](https://patchcord.io).
|
||||
The site even offers a free tier for those who want to try out Mumble Discord Bridge.
|
||||
|
||||
## Usage
|
||||
|
||||
Several configuration variables must be set for the binary to function correctly.
|
||||
All variables can be set using flags or in the environment.
|
||||
The binary will also attempt to load .env file located in the working directory.
|
||||
See the help documentation for all the options
|
||||
|
||||
```bash
|
||||
Usage of ./mumble-discord-bridge:
|
||||
-discord-cid string
|
||||
DISCORD_CID, discord cid, required
|
||||
-discord-command string
|
||||
DISCORD_COMMAND, Discord command string, env alt DISCORD_COMMAND, optional, (defaults mumble-discord) (default "mumble-discord")
|
||||
-discord-disable-text
|
||||
DISCORD_DISABLE_TEXT, disable sending direct messages to discord, (default false)
|
||||
-discord-gid string
|
||||
DISCORD_GID, discord gid, required
|
||||
-discord-token string
|
||||
DISCORD_TOKEN, discord bot token, required
|
||||
-mode string
|
||||
MODE, [constant, manual, auto] determine which mode the bridge starts in, (default constant) (default "constant")
|
||||
-mumble-address string
|
||||
MUMBLE_ADDRESS, mumble server address, example example.com, required
|
||||
-mumble-channel string
|
||||
MUMBLE_CHANNEL, mumble channel to start in, using '/' to separate nested channels, optional
|
||||
-mumble-disable-text
|
||||
MUMBLE_DISABLE_TEXT, disable sending text to mumble, (default false)
|
||||
-mumble-insecure
|
||||
MUMBLE_INSECURE, mumble insecure, optional
|
||||
-mumble-certificate
|
||||
MUMBLE_CERTIFICATE, mumble client certificate, optional
|
||||
-mumble-password string
|
||||
MUMBLE_PASSWORD, mumble password, optional
|
||||
-mumble-port int
|
||||
MUMBLE_PORT, mumble port, (default 64738) (default 64738)
|
||||
-mumble-username string
|
||||
MUMBLE_USERNAME, mumble username, (default: discord) (default "Discord")
|
||||
-nice
|
||||
NICE, whether the bridge should automatically try to 'nice' itself, (default false)
|
||||
-debug
|
||||
DEBUG_LEVEL, DISCORD debug level, optional (default: 1)
|
||||
mumble-discord-bridge --help
|
||||
```
|
||||
|
||||
The bridge can be run with the follow modes:
|
||||
```bash
|
||||
|
||||
```text
|
||||
auto
|
||||
The bridge starts up but does not connect immediately. It can be either manually linked (see below) or will join the voice channels when there's at least one person on each side.
|
||||
The bridge will leave both voice channels once there is no one on either end
|
||||
manual
|
||||
The bridge starts up but does not connect immediately. It will join the voice channels when issued the link command and will leave with the unlink command
|
||||
constant
|
||||
constant (default)
|
||||
The bridge starts up and immediately connects to both Discord and Mumble voice channels. It can not be controlled in this mode and quits when the program is stopped
|
||||
```
|
||||
|
||||
In "auto" or "manual" modes, the bridge can be controlled in Discord with the following commands:
|
||||
|
||||
```bash
|
||||
```text
|
||||
!DISCORD_COMMAND link
|
||||
Commands the bridge to join the Discord channel the user is in and the Mumble server
|
||||
Commands the bridge to join the Discord channel the user is in and the Mumble server
|
||||
|
||||
!DISCORD_COMMAND unlink
|
||||
Commands the bridge to leave the Discord channel the user is in and the Mumble server
|
||||
Commands the bridge to leave the Discord channel the user is in and the Mumble server
|
||||
|
||||
!DISCORD_COMMAND refresh
|
||||
Commands the bridge to unlink, then link again.
|
||||
Commands the bridge to unlink, then link again.
|
||||
|
||||
!DISCORD_COMMAND auto
|
||||
Toggle between manual and auto mode
|
||||
Toggle between manual and auto mode
|
||||
```
|
||||
|
||||
## Setup
|
||||
|
||||
### Creating a Discord Bot
|
||||
|
@ -80,6 +61,7 @@ The guide below provides information on how to setup a Discord bot.
|
|||
|
||||
Individual Discord servers need to invite the bot before it can connect.
|
||||
The bot requires the following permissions:
|
||||
|
||||
* View Channels
|
||||
* See Messages
|
||||
* Read Message History
|
||||
|
@ -87,6 +69,8 @@ The bot requires the following permissions:
|
|||
* Voice Channel Speak
|
||||
* Voice Channel Use Voice Activity
|
||||
|
||||
Permission integer 36768768.
|
||||
|
||||
### Finding Discord CID and GID
|
||||
|
||||
Discord GID is a unique ID linked to one Discord Server, also called Guild. CID is similarly a unique ID for a Discord Channel. To find these you need to set Discord into developer Mode.
|
||||
|
@ -95,7 +79,7 @@ Discord GID is a unique ID linked to one Discord Server, also called Guild. CID
|
|||
|
||||
Then you can get the GID by right-clicking your server and selecting Copy-ID. Similarly the CID can be found right clicking the voice channel and selecting Copy ID.
|
||||
|
||||
### Generating Mumble Client (Optional)
|
||||
### Generating Mumble Client (Optional)
|
||||
|
||||
Optionally you can specify a client certificate for mumble [Mumble Certificates](https://wiki.mumble.info/wiki/Mumble_Certificates)
|
||||
If you don't have a client certificate, you can generate one with this command:
|
||||
|
@ -138,6 +122,33 @@ docker docker run -e MUMBLE_ADDRESS=example.com -e MUMBLE_PASSWORD=optional -e D
|
|||
docker stop mumble-discord-bridge && docker rm mumble-discord-bridge
|
||||
```
|
||||
|
||||
### MDB Bridge Options
|
||||
|
||||
The following options can be set using environment variables or with command line options.
|
||||
|
||||
| Environment Option | Flag | Type | Default | Description |
|
||||
|----------------------------|-----------------------------|---------|------------------|--------------------------------------------------------------------------------------------------------------------------------|
|
||||
| DEBUG_LEVEL | -debug-level | int | 1 | discord debug level |
|
||||
| DISCORD_CID | -discord-cid | string | "" | discord cid, required |
|
||||
| DISCORD_COMMAND | -discord-command | string | "mumble-discord" | discord command string, env alt DISCORD_COMMAND, optional |
|
||||
| DISCORD_DISABLE_BOT_STATUS | -discord-disable-bot-status | boolean | false | disable updating bot status |
|
||||
| DISCORD_DISABLE_TEXT | -discord-disable-text | boolean | false | disable sending direct messages to discord |
|
||||
| DISCORD_GID | -discord-gid | string | "" | discord gid, required |
|
||||
| DISCORD_TOKEN | -discord-token | string | "" | discord bot token, required |
|
||||
| MODE | -mode | string | "constant" | [constant, manual, auto] determine which mode the bridge starts in |
|
||||
| MUMBLE_ADDRESS | -mumble-address | string | "" | mumble server address, example example.com, required |
|
||||
| MUMBLE_CERTIFICATE | -mumble-certificate | string | "" | client certificate to use when connecting to the Mumble server |
|
||||
| MUMBLE_CHANNEL | -mumble-channel | string | "" | mumble channel to start in, using '/' to separate nested channels, optional |
|
||||
| MUMBLE_DISABLE_TEXT | -mumble-disable-text | boolean | false | disable sending text to mumble |
|
||||
| MUMBLE_INSECURE | -mumble-insecure | boolean | false | mumble insecure, ignore ssl certificates issues |
|
||||
| MUMBLE_PASSWORD | -mumble-password | string | "" | mumble password |
|
||||
| MUMBLE_PORT | -mumble-port | int | 64738 | mumble port |
|
||||
| MUMBLE_USERNAME | -mumble-username | string | "Discord" | mumble username |
|
||||
| PROMETHEUS_ENABLE | -prometheus-enable | boolean | false | enable prometheus metrics |
|
||||
| PROMETHEUS_PORT | -prometheus-port | int | 9559 | prometheus metrics port |
|
||||
| TO_DISCORD_BUFFER | -to-discord-buffer | int | 50 | jitter buffer from Mumble to Discord to absorb timing issues related to network, OS and hardware quality. (Increments of 10ms) |
|
||||
| TO_MUMBLE_BUFFER | -to-mumble-buffer | int | 50 | jitter buffer from Discord to Mumble to absorb timing issues related to network, OS and hardware quality. (Increments of 10ms) |****
|
||||
|
||||
### Mumbler Server Setting
|
||||
|
||||
To ensure compatibility please edit your murmur configuration file with the following
|
||||
|
@ -155,11 +166,32 @@ A simple go build command is all that is needed.
|
|||
Ensure the opus library is installed.
|
||||
|
||||
```bash
|
||||
go build -o mumble-discord-bridge *.go
|
||||
#or
|
||||
make mumble-discord-bridge
|
||||
go install github.com/goreleaser/goreleaser@latest
|
||||
goreleaser build --skip-validate --rm-dist --single-target --auto-snapshot
|
||||
```
|
||||
|
||||
### OpenBSD Users
|
||||
|
||||
OpenBSD users should consider compiling a custom kernel to use 1000 ticks for the best possible performance.
|
||||
See [issue 20](https://github.com/Stieneee/mumble-discord-bridge/issues/20) for the latest discussion about this topic.
|
||||
|
||||
## Jitter Buffer
|
||||
|
||||
The bridge implements simple jitter buffers that attempt to compensate for network, OS and hardware related jitter.
|
||||
These jitter buffers are configurable in both directions.
|
||||
A jitter buffer will slightly the delay the transmission of audio in order to have audio packets buffered for the next time step.
|
||||
The Mumble client itself includes a jitter buffer for similar reasons.
|
||||
A default jitter of 50ms should be adequate for most scenarios.
|
||||
A warning will be logged if short burst or audio are seen.
|
||||
A single warning can be ignored multiple warnings in short time spans would suggest the need for a larger jitter buffer.
|
||||
|
||||
## Monitoring the Bridge (Optional)
|
||||
|
||||
The bridge can be started with a Prometheus metrics endpoint enabled.
|
||||
The example folder contains the a docker-compose file that will spawn the bridge, Prometheus and Grafana configured to serve a single a pre-configured dashboard.
|
||||
|
||||
![Mumble Discord Bridge Grafana Dashboard](example/grafana-dashboard.png "Grafana Dashboard")
|
||||
|
||||
## Known Issues
|
||||
|
||||
Currently there is an issue opening the discord voice channel.
|
||||
|
@ -169,6 +201,9 @@ Audio leveling from Discord needs to be improved.
|
|||
|
||||
Delays in connecting to Mumble (such as from external authentication plugins) may result in extra error messages on initial connection.
|
||||
|
||||
There is an issue seen with Mumble-Server (murmur) 1.3.0 in which the bridge will loose the ability to send messages client after prolonged periods of connectivity.
|
||||
This issue has been appears to be resolved by murmur 1.3.4.
|
||||
|
||||
## License
|
||||
|
||||
Distributed under the MIT License. See LICENSE for more information.
|
||||
|
@ -182,5 +217,5 @@ Please consider opening an issue to discuss features and ideas.
|
|||
|
||||
The project would not have been possible without:
|
||||
|
||||
- [gumble](https://github.com/layeh/gumble)
|
||||
- [discordgo](https://github.com/bwmarrin/discordgo)
|
||||
* [gumble](https://github.com/layeh/gumble)
|
||||
* [discordgo](https://github.com/bwmarrin/discordgo)
|
||||
|
|
|
@ -6,36 +6,15 @@ import (
|
|||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"layeh.com/gumble/gumble"
|
||||
)
|
||||
|
||||
type bridgeMode int
|
||||
|
||||
const (
|
||||
bridgeModeAuto bridgeMode = iota
|
||||
bridgeModeManual
|
||||
bridgeModeConstant
|
||||
"strings"
|
||||
)
|
||||
|
||||
//BridgeConfig holds configuration information set at startup
|
||||
//It should not change during runtime
|
||||
type BridgeConfig struct {
|
||||
MumbleConfig *gumble.Config
|
||||
MumbleAddr string
|
||||
MumbleInsecure bool
|
||||
MumbleCertificate string
|
||||
MumbleChannel []string
|
||||
MumbleDisableText bool
|
||||
Command string
|
||||
GID string
|
||||
CID string
|
||||
DiscordDisableText bool
|
||||
}
|
||||
|
||||
func lookupEnvOrString(key string, defaultVal string) string {
|
||||
if val, ok := os.LookupEnv(key); ok {
|
||||
return val
|
||||
return strings.TrimSpace(val)
|
||||
}
|
||||
return defaultVal
|
||||
}
|
|
@ -4,6 +4,7 @@ import (
|
|||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"math"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime/pprof"
|
||||
|
@ -14,9 +15,9 @@ import (
|
|||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"github.com/joho/godotenv"
|
||||
"layeh.com/gumble/gumble"
|
||||
"layeh.com/gumble/gumbleutil"
|
||||
_ "layeh.com/gumble/opus"
|
||||
"github.com/stieneee/gumble/gumble"
|
||||
"github.com/stieneee/gumble/gumbleutil"
|
||||
"github.com/stieneee/mumble-discord-bridge/internal/bridge"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -40,16 +41,21 @@ func main() {
|
|||
mumblePassword := flag.String("mumble-password", lookupEnvOrString("MUMBLE_PASSWORD", ""), "MUMBLE_PASSWORD, mumble password, optional")
|
||||
mumbleInsecure := flag.Bool("mumble-insecure", lookupEnvOrBool("MUMBLE_INSECURE", false), " MUMBLE_INSECURE, mumble insecure, optional")
|
||||
mumbleCertificate := flag.String("mumble-certificate", lookupEnvOrString("MUMBLE_CERTIFICATE", ""), "MUMBLE_CERTIFICATE, client certificate to use when connecting to the Mumble server")
|
||||
mumbleChannel := flag.String("mumble-channel", lookupEnvOrString("MUMBLE_CHANNEL", ""), "MUMBLE_CHANNEL, mumble channel to start in, using '/' to seperate nested channels, optional")
|
||||
mumbleChannel := flag.String("mumble-channel", lookupEnvOrString("MUMBLE_CHANNEL", ""), "MUMBLE_CHANNEL, mumble channel to start in, using '/' to separate nested channels, optional")
|
||||
mumbleSendBuffer := flag.Int("to-mumble-buffer", lookupEnvOrInt("TO_MUMBLE_BUFFER", 50), "TO_MUMBLE_BUFFER, Jitter buffer from Discord to Mumble to absorb timing issues related to network, OS and hardware quality. (Increments of 10ms)")
|
||||
mumbleDisableText := flag.Bool("mumble-disable-text", lookupEnvOrBool("MUMBLE_DISABLE_TEXT", false), "MUMBLE_DISABLE_TEXT, disable sending text to mumble, (default false)")
|
||||
discordToken := flag.String("discord-token", lookupEnvOrString("DISCORD_TOKEN", ""), "DISCORD_TOKEN, discord bot token, required")
|
||||
discordGID := flag.String("discord-gid", lookupEnvOrString("DISCORD_GID", ""), "DISCORD_GID, discord gid, required")
|
||||
discordCID := flag.String("discord-cid", lookupEnvOrString("DISCORD_CID", ""), "DISCORD_CID, discord cid, required")
|
||||
discordSendBuffer := flag.Int("to-discord-buffer", lookupEnvOrInt("TO_DISCORD_BUFFER", 50), "TO_DISCORD_BUFFER, Jitter buffer from Mumble to Discord to absorb timing issues related to network, OS and hardware quality. (Increments of 10ms)")
|
||||
discordCommand := flag.String("discord-command", lookupEnvOrString("DISCORD_COMMAND", "mumble-discord"), "DISCORD_COMMAND, Discord command string, env alt DISCORD_COMMAND, optional, (defaults mumble-discord)")
|
||||
discordDisableText := flag.Bool("discord-disable-text", lookupEnvOrBool("DISCORD_DISABLE_TEXT", false), "DISCORD_DISABLE_TEXT, disable sending direct messages to discord, (default false)")
|
||||
discordDisableBotStatus := flag.Bool("discord-disable-bot-status", lookupEnvOrBool("DISCORD_DISABLE_BOT_STATUS", false), "DISCORD_DISABLE_BOT_STATUS, disable updating bot status, (default false)")
|
||||
mode := flag.String("mode", lookupEnvOrString("MODE", "constant"), "MODE, [constant, manual, auto] determine which mode the bridge starts in, (default constant)")
|
||||
nice := flag.Bool("nice", lookupEnvOrBool("NICE", false), "NICE, whether the bridge should automatically try to 'nice' itself, (default false)")
|
||||
debug := flag.Int("debug-level", lookupEnvOrInt("DEBUG", 1), "DEBUG_LEVEL, Discord debug level, optional, (default 1)")
|
||||
promEnable := flag.Bool("prometheus-enable", lookupEnvOrBool("PROMETHEUS_ENABLE", false), "PROMETHEUS_ENABLE, Enable prometheus metrics")
|
||||
promPort := flag.Int("prometheus-port", lookupEnvOrInt("PROMETHEUS_PORT", 9559), "PROMETHEUS_PORT, Prometheus metrics port, optional, (default 9559)")
|
||||
|
||||
cpuprofile := flag.String("cpuprofile", "", "write cpu profile to `file`")
|
||||
|
||||
|
@ -82,6 +88,10 @@ func main() {
|
|||
}
|
||||
}
|
||||
|
||||
if *promEnable {
|
||||
go bridge.StartPromServer(*promPort)
|
||||
}
|
||||
|
||||
// Optional CPU Profiling
|
||||
if *cpuprofile != "" {
|
||||
f, err := os.Create(*cpuprofile)
|
||||
|
@ -95,39 +105,61 @@ func main() {
|
|||
defer pprof.StopCPUProfile()
|
||||
}
|
||||
|
||||
// Buffer Math
|
||||
if *discordSendBuffer < 10 {
|
||||
*discordSendBuffer = 10
|
||||
}
|
||||
|
||||
if *mumbleSendBuffer < 10 {
|
||||
*mumbleSendBuffer = 10
|
||||
}
|
||||
|
||||
var discordStartStreamingCount int = int(math.Round(float64(*discordSendBuffer) / 10.0))
|
||||
log.Println("To Discord Jitter Buffer: ", discordStartStreamingCount*10, " ms")
|
||||
|
||||
var mumbleStartStreamCount int = int(math.Round(float64(*mumbleSendBuffer) / 10.0))
|
||||
log.Println("To Mumble Jitter Buffer: ", mumbleStartStreamCount*10, " ms")
|
||||
|
||||
// BRIDGE SETUP
|
||||
|
||||
Bridge := &BridgeState{
|
||||
BridgeConfig: &BridgeConfig{
|
||||
Bridge := &bridge.BridgeState{
|
||||
BridgeConfig: &bridge.BridgeConfig{
|
||||
// MumbleConfig: config,
|
||||
MumbleAddr: *mumbleAddr + ":" + strconv.Itoa(*mumblePort),
|
||||
MumbleInsecure: *mumbleInsecure,
|
||||
MumbleCertificate: *mumbleCertificate,
|
||||
MumbleChannel: strings.Split(*mumbleChannel, "/"),
|
||||
MumbleDisableText: *mumbleDisableText,
|
||||
Command: *discordCommand,
|
||||
GID: *discordGID,
|
||||
CID: *discordCID,
|
||||
DiscordDisableText: *discordDisableText,
|
||||
MumbleAddr: *mumbleAddr + ":" + strconv.Itoa(*mumblePort),
|
||||
MumbleInsecure: *mumbleInsecure,
|
||||
MumbleCertificate: *mumbleCertificate,
|
||||
MumbleChannel: strings.Split(*mumbleChannel, "/"),
|
||||
MumbleStartStreamCount: mumbleStartStreamCount,
|
||||
MumbleDisableText: *mumbleDisableText,
|
||||
Command: *discordCommand,
|
||||
GID: *discordGID,
|
||||
CID: *discordCID,
|
||||
DiscordStartStreamingCount: discordStartStreamingCount,
|
||||
DiscordDisableText: *discordDisableText,
|
||||
DiscordDisableBotStatus: *discordDisableBotStatus,
|
||||
Version: version,
|
||||
},
|
||||
Connected: false,
|
||||
DiscordUsers: make(map[string]discordUser),
|
||||
DiscordUsers: make(map[string]bridge.DiscordUser),
|
||||
MumbleUsers: make(map[string]bool),
|
||||
}
|
||||
|
||||
bridge.PromApplicationStartTime.SetToCurrentTime()
|
||||
|
||||
// MUMBLE SETUP
|
||||
Bridge.BridgeConfig.MumbleConfig = gumble.NewConfig()
|
||||
Bridge.BridgeConfig.MumbleConfig.Username = *mumbleUsername
|
||||
Bridge.BridgeConfig.MumbleConfig.Password = *mumblePassword
|
||||
Bridge.BridgeConfig.MumbleConfig.AudioInterval = time.Millisecond * 10
|
||||
|
||||
Bridge.MumbleListener = &MumbleListener{
|
||||
Bridge.MumbleListener = &bridge.MumbleListener{
|
||||
Bridge: Bridge,
|
||||
}
|
||||
|
||||
Bridge.BridgeConfig.MumbleConfig.Attach(gumbleutil.Listener{
|
||||
Connect: Bridge.MumbleListener.mumbleConnect,
|
||||
UserChange: Bridge.MumbleListener.mumbleUserChange,
|
||||
Connect: Bridge.MumbleListener.MumbleConnect,
|
||||
UserChange: Bridge.MumbleListener.MumbleUserChange,
|
||||
// ChannelChange: Bridge.MumbleListener.MumbleChannelChange,
|
||||
})
|
||||
|
||||
// DISCORD SETUP
|
||||
|
@ -144,12 +176,12 @@ func main() {
|
|||
Bridge.DiscordSession.Identify.Intents = discordgo.MakeIntent(discordgo.IntentsAllWithoutPrivileged)
|
||||
Bridge.DiscordSession.ShouldReconnectOnError = true
|
||||
// register handlers
|
||||
Bridge.DiscordListener = &DiscordListener{
|
||||
Bridge.DiscordListener = &bridge.DiscordListener{
|
||||
Bridge: Bridge,
|
||||
}
|
||||
Bridge.DiscordSession.AddHandler(Bridge.DiscordListener.messageCreate)
|
||||
Bridge.DiscordSession.AddHandler(Bridge.DiscordListener.guildCreate)
|
||||
Bridge.DiscordSession.AddHandler(Bridge.DiscordListener.voiceUpdate)
|
||||
Bridge.DiscordSession.AddHandler(Bridge.DiscordListener.MessageCreate)
|
||||
Bridge.DiscordSession.AddHandler(Bridge.DiscordListener.GuildCreate)
|
||||
Bridge.DiscordSession.AddHandler(Bridge.DiscordListener.VoiceUpdate)
|
||||
|
||||
// Open Discord websocket
|
||||
err = Bridge.DiscordSession.Open()
|
||||
|
@ -166,20 +198,22 @@ func main() {
|
|||
case "auto":
|
||||
log.Println("bridge starting in automatic mode")
|
||||
Bridge.AutoChanDie = make(chan bool)
|
||||
Bridge.Mode = bridgeModeAuto
|
||||
Bridge.Mode = bridge.BridgeModeAuto
|
||||
Bridge.DiscordChannelID = Bridge.BridgeConfig.CID
|
||||
go Bridge.AutoBridge()
|
||||
case "manual":
|
||||
log.Println("bridge starting in manual mode")
|
||||
Bridge.Mode = bridgeModeManual
|
||||
Bridge.Mode = bridge.BridgeModeManual
|
||||
case "constant":
|
||||
log.Println("bridge starting in constant mode")
|
||||
Bridge.Mode = bridgeModeConstant
|
||||
Bridge.Mode = bridge.BridgeModeConstant
|
||||
Bridge.DiscordChannelID = Bridge.BridgeConfig.CID
|
||||
go func() {
|
||||
for {
|
||||
Bridge.startBridge()
|
||||
log.Println("Bridge died. Restarting")
|
||||
Bridge.StartBridge()
|
||||
log.Println("Bridge died")
|
||||
time.Sleep(5 * time.Second)
|
||||
log.Println("Restarting")
|
||||
}
|
||||
}()
|
||||
default:
|
||||
|
@ -187,19 +221,25 @@ func main() {
|
|||
log.Fatalln("invalid bridge mode set")
|
||||
}
|
||||
|
||||
go Bridge.discordStatusUpdate()
|
||||
go Bridge.DiscordStatusUpdate()
|
||||
|
||||
// Shutdown on OS signal
|
||||
sc := make(chan os.Signal, 1)
|
||||
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt, os.Kill)
|
||||
signal.Notify(sc, syscall.SIGINT, syscall.SIGTERM, os.Interrupt)
|
||||
<-sc
|
||||
|
||||
log.Println("OS Signal. Bot shutting down")
|
||||
|
||||
time.AfterFunc(30*time.Second, func() {
|
||||
os.Exit(99)
|
||||
})
|
||||
|
||||
// Wait or the bridge to exit cleanly
|
||||
Bridge.BridgeMutex.Lock()
|
||||
if Bridge.Connected {
|
||||
//TODO BridgeDie occasionally panics on send to closed channel
|
||||
Bridge.BridgeDie <- true
|
||||
Bridge.WaitExit.Wait()
|
||||
}
|
||||
Bridge.BridgeMutex.Unlock()
|
||||
}
|
261
discord.go
261
discord.go
|
@ -1,261 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"layeh.com/gopus"
|
||||
"layeh.com/gumble/gumble"
|
||||
_ "layeh.com/gumble/opus"
|
||||
)
|
||||
|
||||
type fromDiscord struct {
|
||||
decoder *gopus.Decoder
|
||||
pcm chan []int16
|
||||
streaming bool
|
||||
}
|
||||
|
||||
// DiscordDuplex Handle discord voice stream
|
||||
type DiscordDuplex struct {
|
||||
Bridge *BridgeState
|
||||
|
||||
discordMutex sync.Mutex
|
||||
discordMixerMutex sync.Mutex
|
||||
fromDiscordMap map[uint32]fromDiscord
|
||||
}
|
||||
|
||||
// OnError gets called by dgvoice when an error is encountered.
|
||||
// By default logs to STDERR
|
||||
var OnError = func(str string, err error) {
|
||||
prefix := "dgVoice: " + str
|
||||
|
||||
if err != nil {
|
||||
log.Println(prefix + ": " + err.Error())
|
||||
} else {
|
||||
log.Println(prefix)
|
||||
}
|
||||
}
|
||||
|
||||
// SendPCM will receive on the provied channel encode
|
||||
// received PCM data into Opus then send that to Discordgo
|
||||
func (dd *DiscordDuplex) discordSendPCM(ctx context.Context, wg *sync.WaitGroup, cancel context.CancelFunc, pcm <-chan []int16) {
|
||||
const channels int = 1
|
||||
const frameRate int = 48000 // audio sampling rate
|
||||
const frameSize int = 960 // uint16 size of each audio frame
|
||||
const maxBytes int = (frameSize * 2) * 2 // max size of opus data
|
||||
|
||||
streaming := false
|
||||
|
||||
opusEncoder, err := gopus.NewEncoder(frameRate, channels, gopus.Audio)
|
||||
if err != nil {
|
||||
OnError("NewEncoder Error", err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
ticker := time.NewTicker(20 * time.Millisecond)
|
||||
|
||||
lastReady := true
|
||||
var readyTimeout *time.Timer
|
||||
|
||||
wg.Add(1)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
wg.Done()
|
||||
return
|
||||
default:
|
||||
}
|
||||
<-ticker.C
|
||||
if len(pcm) > 1 {
|
||||
if !streaming {
|
||||
dd.Bridge.DiscordVoice.Speaking(true)
|
||||
streaming = true
|
||||
}
|
||||
|
||||
r1 := <-pcm
|
||||
r2 := <-pcm
|
||||
|
||||
// try encoding pcm frame with Opus
|
||||
opus, err := opusEncoder.Encode(append(r1, r2...), frameSize, maxBytes)
|
||||
if err != nil {
|
||||
OnError("Encoding Error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if dd.Bridge.DiscordVoice.Ready == false || dd.Bridge.DiscordVoice.OpusSend == nil {
|
||||
if lastReady == true {
|
||||
OnError(fmt.Sprintf("Discordgo not ready for opus packets. %+v : %+v", dd.Bridge.DiscordVoice.Ready, dd.Bridge.DiscordVoice.OpusSend), nil)
|
||||
readyTimeout = time.AfterFunc(30*time.Second, func() {
|
||||
log.Println("set ready timeout")
|
||||
cancel()
|
||||
})
|
||||
lastReady = false
|
||||
}
|
||||
continue
|
||||
} else if lastReady == false {
|
||||
fmt.Println("Discordgo ready to send opus packets")
|
||||
lastReady = true
|
||||
readyTimeout.Stop()
|
||||
}
|
||||
dd.Bridge.DiscordVoice.OpusSend <- opus
|
||||
} else {
|
||||
if streaming {
|
||||
dd.Bridge.DiscordVoice.Speaking(false)
|
||||
streaming = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ReceivePCM will receive on the the Discordgo OpusRecv channel and decode
|
||||
// the opus audio into PCM then send it on the provided channel.
|
||||
func (dd *DiscordDuplex) discordReceivePCM(ctx context.Context, wg *sync.WaitGroup, cancel context.CancelFunc) {
|
||||
var err error
|
||||
|
||||
lastReady := true
|
||||
var readyTimeout *time.Timer
|
||||
|
||||
wg.Add(1)
|
||||
|
||||
for {
|
||||
if dd.Bridge.DiscordVoice.Ready == false || dd.Bridge.DiscordVoice.OpusRecv == nil {
|
||||
if lastReady == true {
|
||||
OnError(fmt.Sprintf("Discordgo not to receive opus packets. %+v : %+v", dd.Bridge.DiscordVoice.Ready, dd.Bridge.DiscordVoice.OpusSend), nil)
|
||||
readyTimeout = time.AfterFunc(30*time.Second, func() {
|
||||
log.Println("set ready timeout")
|
||||
cancel()
|
||||
})
|
||||
lastReady = false
|
||||
}
|
||||
continue
|
||||
} else if lastReady == false {
|
||||
fmt.Println("Discordgo ready to receive packets")
|
||||
lastReady = true
|
||||
readyTimeout.Stop()
|
||||
}
|
||||
|
||||
var ok bool
|
||||
var p *discordgo.Packet
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
wg.Done()
|
||||
return
|
||||
case p, ok = <-dd.Bridge.DiscordVoice.OpusRecv:
|
||||
}
|
||||
|
||||
if !ok {
|
||||
log.Println("Opus not ok")
|
||||
continue
|
||||
}
|
||||
|
||||
dd.discordMutex.Lock()
|
||||
_, ok = dd.fromDiscordMap[p.SSRC]
|
||||
dd.discordMutex.Unlock()
|
||||
if !ok {
|
||||
newStream := fromDiscord{}
|
||||
newStream.pcm = make(chan []int16, 100)
|
||||
newStream.streaming = false
|
||||
newStream.decoder, err = gopus.NewDecoder(48000, 1)
|
||||
if err != nil {
|
||||
OnError("error creating opus decoder", err)
|
||||
continue
|
||||
}
|
||||
dd.discordMutex.Lock()
|
||||
dd.fromDiscordMap[p.SSRC] = newStream
|
||||
dd.discordMutex.Unlock()
|
||||
}
|
||||
|
||||
dd.discordMutex.Lock()
|
||||
p.PCM, err = dd.fromDiscordMap[p.SSRC].decoder.Decode(p.Opus, 960, false)
|
||||
dd.discordMutex.Unlock()
|
||||
if err != nil {
|
||||
OnError("Error decoding opus data", err)
|
||||
continue
|
||||
}
|
||||
if len(p.PCM) != 960 {
|
||||
log.Println("Opus size error")
|
||||
continue
|
||||
}
|
||||
|
||||
dd.discordMutex.Lock()
|
||||
select {
|
||||
case dd.fromDiscordMap[p.SSRC].pcm <- p.PCM[0:480]:
|
||||
default:
|
||||
log.Println("fromDiscordMap buffer full. Dropping packet")
|
||||
dd.discordMutex.Unlock()
|
||||
continue
|
||||
}
|
||||
select {
|
||||
case dd.fromDiscordMap[p.SSRC].pcm <- p.PCM[480:960]:
|
||||
default:
|
||||
log.Println("fromDiscordMap buffer full. Dropping packet")
|
||||
}
|
||||
dd.discordMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (dd *DiscordDuplex) fromDiscordMixer(ctx context.Context, wg *sync.WaitGroup, toMumble chan<- gumble.AudioBuffer) {
|
||||
ticker := time.NewTicker(10 * time.Millisecond)
|
||||
sendAudio := false
|
||||
wg.Add(1)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
wg.Done()
|
||||
return
|
||||
case <-ticker.C:
|
||||
}
|
||||
|
||||
dd.discordMutex.Lock()
|
||||
|
||||
sendAudio = false
|
||||
internalMixerArr := make([][]int16, 0)
|
||||
|
||||
// Work through each channel
|
||||
for i := range dd.fromDiscordMap {
|
||||
if len(dd.fromDiscordMap[i].pcm) > 0 {
|
||||
sendAudio = true
|
||||
if dd.fromDiscordMap[i].streaming == false {
|
||||
x := dd.fromDiscordMap[i]
|
||||
x.streaming = true
|
||||
dd.fromDiscordMap[i] = x
|
||||
}
|
||||
|
||||
x1 := (<-dd.fromDiscordMap[i].pcm)
|
||||
internalMixerArr = append(internalMixerArr, x1)
|
||||
} else {
|
||||
if dd.fromDiscordMap[i].streaming == true {
|
||||
x := dd.fromDiscordMap[i]
|
||||
x.streaming = false
|
||||
dd.fromDiscordMap[i] = x
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
dd.discordMutex.Unlock()
|
||||
|
||||
outBuf := make([]int16, 480)
|
||||
|
||||
for i := 0; i < len(outBuf); i++ {
|
||||
for j := 0; j < len(internalMixerArr); j++ {
|
||||
outBuf[i] += (internalMixerArr[j])[i]
|
||||
}
|
||||
}
|
||||
|
||||
if sendAudio {
|
||||
select {
|
||||
case toMumble <- outBuf:
|
||||
default:
|
||||
log.Println("toMumble buffer full. Dropping packet")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 123 KiB |
Binary file not shown.
After Width: | Height: | Size: 27 KiB |
|
@ -1,3 +1,6 @@
|
|||
# This a basic docker-compose file to run an instance an instance of Mumble-Discord-Bridge
|
||||
# docker-compose -f ./docker-compose.yml up -d
|
||||
|
||||
version: "3"
|
||||
|
||||
services:
|
|
@ -0,0 +1,69 @@
|
|||
# This docker compose file contians an exmaple of staring Mumble-Discord-Bridge with Prometheus and Grafana
|
||||
# The monitoring folder is need to provide the nesscary default configs for Promethus and Grafana
|
||||
# Prometheus port 9090
|
||||
# Grafana port 3030
|
||||
|
||||
version: '3.8'
|
||||
|
||||
volumes:
|
||||
prometheus_data: {}
|
||||
grafana_data: {}
|
||||
|
||||
services:
|
||||
|
||||
services:
|
||||
mumble-discord-bridge:
|
||||
image: stieneee/mumble-discord-bridge
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- mdb
|
||||
environment:
|
||||
- MUMBLE_ADDRESS=example.com"
|
||||
- MUMBLE_USERNAME=discord-bridge
|
||||
- MUMBLE_PASSWORD=password
|
||||
- DISCORD_TOKEN=token
|
||||
- DISCORD_GID=gid
|
||||
- DISCORD_CID=cid
|
||||
|
||||
prometheus:
|
||||
image: prom/prometheus:latest
|
||||
container_name: prometheus
|
||||
volumes:
|
||||
- ./prometheus:/etc/prometheus
|
||||
- prometheus_data:/prometheus
|
||||
command:
|
||||
- '--config.file=/etc/prometheus/prometheus.yml'
|
||||
- '--storage.tsdb.path=/prometheus'
|
||||
- '--web.console.libraries=/etc/prometheus/console_libraries'
|
||||
- '--web.console.templates=/etc/prometheus/consoles'
|
||||
- '--storage.tsdb.retention.time=200h'
|
||||
- '--web.enable-lifecycle'
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- 9090
|
||||
ports:
|
||||
- 9090:9090
|
||||
depends_on:
|
||||
- mumble-discord-bridge
|
||||
|
||||
grafana:
|
||||
image: grafana/grafana:latest
|
||||
container_name: grafana
|
||||
volumes:
|
||||
# - grafana_data:/var/lib/grafana
|
||||
- ./grafana/provisioning/dashboards:/etc/grafana/provisioning/dashboards
|
||||
- ./grafana/provisioning/datasources:/etc/grafana/provisioning/datasources
|
||||
environment:
|
||||
# - GF_SECURITY_ADMIN_USER=${ADMIN_USER:-admin}
|
||||
# - GF_SECURITY_ADMIN_PASSWORD=${ADMIN_PASSWORD:-admin}
|
||||
- GF_USERS_ALLOW_SIGN_UP=false
|
||||
- GF_AUTH_ANONYMOUS_ENABLED=true
|
||||
- GF_AUTH_ANONYMOUS_ORG_NAME=Main Org.
|
||||
- GF_AUTH_ANONYMOUS_ORG_ROLE=Editor
|
||||
restart: unless-stopped
|
||||
expose:
|
||||
- 3000
|
||||
ports:
|
||||
- 3030:3000
|
||||
depends_on:
|
||||
- prometheus
|
Binary file not shown.
After Width: | Height: | Size: 156 KiB |
|
@ -0,0 +1,24 @@
|
|||
apiVersion: 1
|
||||
|
||||
providers:
|
||||
# <string> an unique provider name. Required
|
||||
- name: 'MDB'
|
||||
# <int> Org id. Default to 1
|
||||
orgId: 1
|
||||
# <string> name of the dashboard folder.
|
||||
folder: ''
|
||||
# <string> folder UID. will be automatically generated if not specified
|
||||
folderUid: ''
|
||||
# <string> provider type. Default to 'file'
|
||||
type: file
|
||||
# <bool> disable dashboard deletion
|
||||
disableDeletion: false
|
||||
# <int> how often Grafana will scan for changed dashboards
|
||||
updateIntervalSeconds: 30
|
||||
# <bool> allow updating provisioned dashboards from the UI
|
||||
allowUiUpdates: true
|
||||
options:
|
||||
# <string, required> path to dashboard files on disk. Required when using the 'file' type
|
||||
path: /etc/grafana/provisioning/dashboards
|
||||
# <bool> use folder names from filesystem to create folders in Grafana
|
||||
foldersFromFilesStructure: false
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,11 @@
|
|||
apiVersion: 1
|
||||
|
||||
datasources:
|
||||
- name: Prometheus
|
||||
type: prometheus
|
||||
access: proxy
|
||||
orgId: 1
|
||||
url: http://prometheus:9090
|
||||
basicAuth: false
|
||||
isDefault: true
|
||||
editable: true
|
|
@ -0,0 +1,12 @@
|
|||
global:
|
||||
scrape_interval: 15s
|
||||
evaluation_interval: 15s
|
||||
|
||||
# A scrape configuration containing exactly one endpoint to scrape.
|
||||
scrape_configs:
|
||||
- job_name: 'mdb'
|
||||
scrape_interval: 1s
|
||||
static_configs:
|
||||
- targets: [
|
||||
'mumble-discord-bridge:9559',
|
||||
]
|
12
go.mod
12
go.mod
|
@ -3,13 +3,11 @@ module github.com/stieneee/mumble-discord-bridge
|
|||
go 1.15
|
||||
|
||||
require (
|
||||
github.com/bwmarrin/discordgo v0.23.2
|
||||
github.com/golang/protobuf v1.4.3 // indirect
|
||||
github.com/gorilla/websocket v1.4.2 // indirect
|
||||
github.com/bwmarrin/discordgo v0.23.3-0.20210512035133-7d7206b01bb5
|
||||
github.com/joho/godotenv v1.3.0
|
||||
github.com/prometheus/client_golang v1.11.0
|
||||
github.com/stieneee/gopus v0.0.0-20210424193312-6d10f6090335
|
||||
github.com/stieneee/gumble v0.0.0-20210424210604-732f48b5e0de
|
||||
github.com/stieneee/tickerct v0.0.0-20210420020607-d1b092aa40e9
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad // indirect
|
||||
golang.org/x/sys v0.0.0-20210108172913-0df2131ae363 // indirect
|
||||
google.golang.org/protobuf v1.25.0 // indirect
|
||||
layeh.com/gopus v0.0.0-20161224163843-0ebf989153aa
|
||||
layeh.com/gumble v0.0.0-20200818122324-146f9205029b
|
||||
)
|
||||
|
|
202
go.sum
202
go.sum
|
@ -1,27 +1,29 @@
|
|||
cloud.google.com/go v0.26.0 h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/bwmarrin/discordgo v0.22.0 h1:uBxY1HmlVCsW1IuaPjpCGT6A2DBwRn0nvOguQIxDdFM=
|
||||
github.com/bwmarrin/discordgo v0.22.0/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M=
|
||||
github.com/bwmarrin/discordgo v0.23.2 h1:BzrtTktixGHIu9Tt7dEE6diysEF9HWnXeHuoJEt2fH4=
|
||||
github.com/bwmarrin/discordgo v0.23.2/go.mod h1:c1WtWUGN6nREDmzIpyTp/iD3VYt4Fpx+bVyfBG7JE+M=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1 h1:glEXhBS5PSLLv4IXzLA5yPRVX4bilULVyxxbrfOtDAk=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/client9/misspell v0.3.4 h1:ta993UF76GwbvJcIo3Y68y/M3WxlpEHPWIGDkJYwzJI=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/dchote/go-openal v0.0.0-20171116030048-f4a9a141d372 h1:tz3KnXWtRZR0RWOfcMNOw+HHezWLQa7vfSOWTtKjchI=
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
|
||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
||||
github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bwmarrin/discordgo v0.23.3-0.20210512035133-7d7206b01bb5 h1:VtiZMSjY2N6XpM1luSchBVX76QURpS0HA7BffVuHOCo=
|
||||
github.com/bwmarrin/discordgo v0.23.3-0.20210512035133-7d7206b01bb5/go.mod h1:OMKxbTmkKofBjBi4/yidO3ItxbJ6PUfEUkjchM4En8c=
|
||||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dchote/go-openal v0.0.0-20171116030048-f4a9a141d372/go.mod h1:74z+CYu2/mx4N+mcIS/rsvfAxBPBV9uv8zRAnwyFkdI=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473 h1:4cmBvAEBNJaGARUEs3/suWRyfyBfhf7I60WBZq+bv2w=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0 h1:EQciDnbrYxy13PgWoY8AqoxGiPrpgBZ1R8UNe3ddc+A=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1 h1:G5FRp8JnTd7RQH5kemVNlMeyXQAztQ3mOWV95KxsXH8=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY=
|
||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
|
@ -29,89 +31,129 @@ github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:x
|
|||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8=
|
||||
github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.0 h1:/QaMHBdZ26BB3SSst0Iwl10Epc+xhTquomWX0oZEB6w=
|
||||
github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
|
||||
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc=
|
||||
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
|
||||
github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4 h1:gQz4mCbXsO+nc9n1hCxHcGA3Zx3Eo+UHZoInFGUIXNM=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA=
|
||||
github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
|
||||
github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
||||
github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M=
|
||||
github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ=
|
||||
github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0=
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
|
||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
||||
github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo=
|
||||
github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ=
|
||||
github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc=
|
||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
||||
github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4=
|
||||
github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
|
||||
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
|
||||
github.com/stieneee/gopus v0.0.0-20210424193312-6d10f6090335 h1:yzwz6AqGKysli5du4CrQ48BMGUCSkrl7V7Kbo9VaG8w=
|
||||
github.com/stieneee/gopus v0.0.0-20210424193312-6d10f6090335/go.mod h1:tAKYr3fSBJGold7c9DMPlhupn9oy8hTgl3cZ0hoyRQs=
|
||||
github.com/stieneee/gumble v0.0.0-20210424210604-732f48b5e0de h1:4dWOeXRnba4jHVa3KuWf7i/GOIAlBMR3euVTUXOey2I=
|
||||
github.com/stieneee/gumble v0.0.0-20210424210604-732f48b5e0de/go.mod h1:hVIsmrlrudlx2HJbsDkIZI4crkv6NHSau0ldEWbQI/Y=
|
||||
github.com/stieneee/tickerct v0.0.0-20210420020607-d1b092aa40e9 h1:0l2H6Oj6JHMmkqm9xaBMQA5MOGhPT+Nn/thlTUcD9Iw=
|
||||
github.com/stieneee/tickerct v0.0.0-20210420020607-d1b092aa40e9/go.mod h1:54+oZlabriEpT52rPAjAeEWUFgYqv325LrS3hNxHGFE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad h1:DN0cp81fZ3njFcrLCytUHRSUkqBjfTo4Tx9RJTWs0EY=
|
||||
golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4 h1:c2HOrn5iMezYjSlGPncknSEr/8x5LELb/ilJbXi9DEA=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3 h1:XQyxROzUlZH+WIQwySDgnISgOivlhjIEwaQaJEJrrN0=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037 h1:YyJpGZS1sBuBCzLAR1VEpK193GlqGZbnPFnPV/5Rsb4=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210108172913-0df2131ae363 h1:wHn06sgWHMO1VsQ8F+KzDJx/JzqfsNLnc+oEi07qD7s=
|
||||
golang.org/x/sys v0.0.0-20210108172913-0df2131ae363/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221 h1:/ZHdbVpdR/jk3g30/d4yUL0JU9kksj8+F/bnQUVLGDM=
|
||||
golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 h1:JWgyZ1qgdTaF3N3oxC+MdTV7qvEEgHo3otj+HB5CM7Q=
|
||||
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135 h1:5Beo0mZN8dRzgrMMkDp0jc8YXQKx9DiJ2k1dkvGsn5A=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013 h1:+kGHl1aib/qcwaRi1CbqBZ1rk19r85MNUf8HaBghugY=
|
||||
google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg=
|
||||
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.0 h1:4MY060fB1DLGMB/7MBTLnwQUY6+F09GEiz6SsrNqyzM=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.25.0 h1:Ejskq+SyPohKW+1uil0JJMtmHCgJPJ/qWTxr8qp+R4c=
|
||||
google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc h1:/hemPrYIhOhy8zYrNj+069zDB68us2sMGsfkFJO0iZs=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
layeh.com/gopus v0.0.0-20161224163843-0ebf989153aa h1:WNU4LYsgD2UHxgKgB36mL6iMAMOvr127alafSlgBbiA=
|
||||
layeh.com/gopus v0.0.0-20161224163843-0ebf989153aa/go.mod h1:AOef7vHz0+v4sWwJnr0jSyHiX/1NgsMoaxl+rEPz/I0=
|
||||
layeh.com/gumble v0.0.0-20200818122324-146f9205029b h1:Kne6wkHqbqrygRsqs5XUNhSs84DFG5TYMeCkCbM56sY=
|
||||
layeh.com/gumble v0.0.0-20200818122324-146f9205029b/go.mod h1:tWPVA9ZAfImNwabjcd9uDE+Mtz0Hfs7a7G3vxrnrwyc=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0 h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
@ -12,15 +12,40 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"layeh.com/gumble/gumble"
|
||||
"github.com/stieneee/gumble/gumble"
|
||||
)
|
||||
|
||||
type discordUser struct {
|
||||
type DiscordUser struct {
|
||||
username string
|
||||
seen bool
|
||||
dm *discordgo.Channel
|
||||
}
|
||||
|
||||
type BridgeMode int
|
||||
|
||||
const (
|
||||
BridgeModeAuto BridgeMode = iota
|
||||
BridgeModeManual
|
||||
BridgeModeConstant
|
||||
)
|
||||
|
||||
type BridgeConfig struct {
|
||||
MumbleConfig *gumble.Config
|
||||
MumbleAddr string
|
||||
MumbleInsecure bool
|
||||
MumbleCertificate string
|
||||
MumbleChannel []string
|
||||
MumbleStartStreamCount int
|
||||
MumbleDisableText bool
|
||||
Command string
|
||||
GID string
|
||||
CID string
|
||||
DiscordStartStreamingCount int
|
||||
DiscordDisableText bool
|
||||
DiscordDisableBotStatus bool
|
||||
Version string
|
||||
}
|
||||
|
||||
//BridgeState manages dynamic information about the bridge during runtime
|
||||
type BridgeState struct {
|
||||
// The configuration data for this bridge
|
||||
|
@ -35,11 +60,14 @@ type BridgeState struct {
|
|||
// Wait for bridge to exit cleanly
|
||||
WaitExit *sync.WaitGroup
|
||||
|
||||
// Bridge State Mutex
|
||||
BridgeMutex sync.Mutex
|
||||
|
||||
// Bridge connection
|
||||
Connected bool
|
||||
|
||||
// The bridge mode constant, auto, manual. Default is constant.
|
||||
Mode bridgeMode
|
||||
Mode BridgeMode
|
||||
|
||||
// Discord session. This is created and outside the bridge state
|
||||
DiscordSession *discordgo.Session
|
||||
|
@ -51,7 +79,7 @@ type BridgeState struct {
|
|||
MumbleClient *gumble.Client
|
||||
|
||||
// Map of Discord users tracked by this bridge.
|
||||
DiscordUsers map[string]discordUser
|
||||
DiscordUsers map[string]DiscordUser
|
||||
DiscordUsersMutex sync.Mutex
|
||||
|
||||
// Map of Mumble users tracked by this bridge
|
||||
|
@ -77,7 +105,7 @@ type BridgeState struct {
|
|||
}
|
||||
|
||||
// startBridge established the voice connection
|
||||
func (b *BridgeState) startBridge() {
|
||||
func (b *BridgeState) StartBridge() {
|
||||
b.lock.Lock()
|
||||
defer b.lock.Unlock()
|
||||
|
||||
|
@ -92,6 +120,9 @@ func (b *BridgeState) startBridge() {
|
|||
|
||||
var err error
|
||||
|
||||
promBridgeStarts.Inc()
|
||||
promBridgeStartTime.SetToCurrentTime()
|
||||
|
||||
// DISCORD Connect Voice
|
||||
log.Println("Attempting to join Discord voice channel")
|
||||
if b.DiscordChannelID == "" {
|
||||
|
@ -111,7 +142,7 @@ func (b *BridgeState) startBridge() {
|
|||
|
||||
// MUMBLE Connect
|
||||
|
||||
b.MumbleStream = &MumbleDuplex{}
|
||||
b.MumbleStream = NewMumbleDuplex()
|
||||
det := b.BridgeConfig.MumbleConfig.AudioListeners.Attach(b.MumbleStream)
|
||||
defer det.Detach()
|
||||
|
||||
|
@ -150,26 +181,40 @@ func (b *BridgeState) startBridge() {
|
|||
defer close(toDiscord)
|
||||
defer close(toMumble)
|
||||
|
||||
// From Discord
|
||||
b.DiscordStream = NewDiscordDuplex(b)
|
||||
|
||||
// Start Passing Between
|
||||
|
||||
// From Mumble
|
||||
go b.MumbleStream.fromMumbleMixer(ctx, &wg, toDiscord)
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
b.MumbleStream.fromMumbleMixer(ctx, cancel, toDiscord)
|
||||
}()
|
||||
|
||||
// From Discord
|
||||
b.DiscordStream = &DiscordDuplex{
|
||||
Bridge: b,
|
||||
fromDiscordMap: make(map[uint32]fromDiscord),
|
||||
}
|
||||
|
||||
go b.DiscordStream.discordReceivePCM(ctx, &wg, cancel)
|
||||
go b.DiscordStream.fromDiscordMixer(ctx, &wg, toMumble)
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
b.DiscordStream.discordReceivePCM(ctx, cancel)
|
||||
}()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
b.DiscordStream.fromDiscordMixer(ctx, toMumble)
|
||||
}()
|
||||
|
||||
// To Discord
|
||||
go b.DiscordStream.discordSendPCM(ctx, &wg, cancel, toDiscord)
|
||||
|
||||
// Monitor Mumble
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
wg.Add(1)
|
||||
defer wg.Done()
|
||||
b.DiscordStream.discordSendPCM(ctx, cancel, toDiscord)
|
||||
}()
|
||||
|
||||
// Monitor
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
ticker := time.NewTicker(500 * time.Millisecond)
|
||||
for {
|
||||
select {
|
||||
|
@ -183,13 +228,14 @@ func (b *BridgeState) startBridge() {
|
|||
cancel()
|
||||
}
|
||||
case <-ctx.Done():
|
||||
wg.Done()
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
b.BridgeMutex.Lock()
|
||||
b.Connected = true
|
||||
b.BridgeMutex.Unlock()
|
||||
|
||||
// Hold until cancelled or external die request
|
||||
select {
|
||||
|
@ -200,16 +246,19 @@ func (b *BridgeState) startBridge() {
|
|||
cancel()
|
||||
}
|
||||
|
||||
b.BridgeMutex.Lock()
|
||||
b.Connected = false
|
||||
b.BridgeMutex.Unlock()
|
||||
|
||||
wg.Wait()
|
||||
log.Println("Terminating Bridge")
|
||||
b.MumbleUsersMutex.Lock()
|
||||
b.MumbleUsers = make(map[string]bool)
|
||||
b.MumbleUsersMutex.Unlock()
|
||||
b.DiscordUsers = make(map[string]discordUser)
|
||||
b.DiscordUsers = make(map[string]DiscordUser)
|
||||
}
|
||||
|
||||
func (b *BridgeState) discordStatusUpdate() {
|
||||
func (b *BridgeState) DiscordStatusUpdate() {
|
||||
m, _ := time.ParseDuration("30s")
|
||||
for {
|
||||
time.Sleep(3 * time.Second)
|
||||
|
@ -220,13 +269,17 @@ func (b *BridgeState) discordStatusUpdate() {
|
|||
log.Printf("error pinging mumble server %v\n", err)
|
||||
b.DiscordSession.UpdateListeningStatus("an error pinging mumble")
|
||||
} else {
|
||||
|
||||
promMumblePing.Set(float64(resp.Ping.Milliseconds()))
|
||||
|
||||
b.MumbleUsersMutex.Lock()
|
||||
b.BridgeMutex.Lock()
|
||||
b.MumbleUserCount = resp.ConnectedUsers
|
||||
if b.Connected {
|
||||
b.MumbleUserCount = b.MumbleUserCount - 1
|
||||
}
|
||||
if b.MumbleUserCount == 0 {
|
||||
status = "No users in Mumble"
|
||||
status = "no users in Mumble"
|
||||
} else {
|
||||
if len(b.MumbleUsers) > 0 {
|
||||
status = fmt.Sprintf("%v/%v users in Mumble\n", len(b.MumbleUsers), b.MumbleUserCount)
|
||||
|
@ -234,9 +287,18 @@ func (b *BridgeState) discordStatusUpdate() {
|
|||
status = fmt.Sprintf("%v users in Mumble\n", b.MumbleUserCount)
|
||||
}
|
||||
}
|
||||
b.BridgeMutex.Unlock()
|
||||
b.MumbleUsersMutex.Unlock()
|
||||
b.DiscordSession.UpdateListeningStatus(status)
|
||||
if !b.BridgeConfig.DiscordDisableBotStatus {
|
||||
b.DiscordSession.UpdateListeningStatus(status)
|
||||
}
|
||||
}
|
||||
|
||||
discordHeartBeat := b.DiscordSession.LastHeartbeatAck.Sub(b.DiscordSession.LastHeartbeatSent).Milliseconds()
|
||||
if discordHeartBeat > 0 {
|
||||
promDiscordHeartBeat.Set(float64(discordHeartBeat))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -244,29 +306,31 @@ func (b *BridgeState) discordStatusUpdate() {
|
|||
// when there is at least one user on both, starts up the bridge
|
||||
// when there are no users on either side, kills the bridge
|
||||
func (b *BridgeState) AutoBridge() {
|
||||
log.Println("beginning auto mode")
|
||||
log.Println("Beginning auto mode")
|
||||
ticker := time.NewTicker(3 * time.Second)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
case <-b.AutoChanDie:
|
||||
log.Println("ending automode")
|
||||
log.Println("Ending automode")
|
||||
return
|
||||
}
|
||||
|
||||
b.MumbleUsersMutex.Lock()
|
||||
b.DiscordUsersMutex.Lock()
|
||||
b.BridgeMutex.Lock()
|
||||
|
||||
if !b.Connected && b.MumbleUserCount > 0 && len(b.DiscordUsers) > 0 {
|
||||
log.Println("users detected in mumble and discord, bridging")
|
||||
go b.startBridge()
|
||||
log.Println("Users detected in mumble and discord, bridging")
|
||||
go b.StartBridge()
|
||||
}
|
||||
if b.Connected && b.MumbleUserCount == 0 && len(b.DiscordUsers) <= 1 {
|
||||
log.Println("no one online, killing bridge")
|
||||
log.Println("No one online, killing bridge")
|
||||
b.BridgeDie <- true
|
||||
}
|
||||
|
||||
b.BridgeMutex.Unlock()
|
||||
b.MumbleUsersMutex.Unlock()
|
||||
b.DiscordUsersMutex.Unlock()
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package main
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
@ -15,11 +15,11 @@ type DiscordListener struct {
|
|||
Bridge *BridgeState
|
||||
}
|
||||
|
||||
func (l *DiscordListener) guildCreate(s *discordgo.Session, event *discordgo.GuildCreate) {
|
||||
func (l *DiscordListener) GuildCreate(s *discordgo.Session, event *discordgo.GuildCreate) {
|
||||
log.Println("CREATE event registered")
|
||||
|
||||
if event.ID != l.Bridge.BridgeConfig.GID {
|
||||
log.Println("received GuildCreate from a guild not in config")
|
||||
log.Println("Received GuildCreate from a guild not in config")
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -32,7 +32,7 @@ func (l *DiscordListener) guildCreate(s *discordgo.Session, event *discordgo.Gui
|
|||
|
||||
u, err := s.User(vs.UserID)
|
||||
if err != nil {
|
||||
log.Println("error looking up username")
|
||||
log.Println("Error looking up username")
|
||||
}
|
||||
|
||||
dm, err := s.UserChannelCreate(u.ID)
|
||||
|
@ -41,7 +41,7 @@ func (l *DiscordListener) guildCreate(s *discordgo.Session, event *discordgo.Gui
|
|||
}
|
||||
|
||||
l.Bridge.DiscordUsersMutex.Lock()
|
||||
l.Bridge.DiscordUsers[vs.UserID] = discordUser{
|
||||
l.Bridge.DiscordUsers[vs.UserID] = DiscordUser{
|
||||
username: u.Username,
|
||||
seen: true,
|
||||
dm: dm,
|
||||
|
@ -49,17 +49,19 @@ func (l *DiscordListener) guildCreate(s *discordgo.Session, event *discordgo.Gui
|
|||
l.Bridge.DiscordUsersMutex.Unlock()
|
||||
|
||||
// If connected to mumble inform users of Discord users
|
||||
l.Bridge.BridgeMutex.Lock()
|
||||
if l.Bridge.Connected && !l.Bridge.BridgeConfig.MumbleDisableText {
|
||||
l.Bridge.MumbleClient.Do(func() {
|
||||
l.Bridge.MumbleClient.Self.Channel.Send(fmt.Sprintf("%v has joined Discord\n", u.Username), false)
|
||||
})
|
||||
}
|
||||
l.Bridge.BridgeMutex.Unlock()
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *DiscordListener) messageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
|
||||
func (l *DiscordListener) MessageCreate(s *discordgo.Session, m *discordgo.MessageCreate) {
|
||||
|
||||
// Ignore all messages created by the bot itself
|
||||
if m.Author.ID == s.State.User.ID {
|
||||
|
@ -80,22 +82,26 @@ func (l *DiscordListener) messageCreate(s *discordgo.Session, m *discordgo.Messa
|
|||
}
|
||||
prefix := "!" + l.Bridge.BridgeConfig.Command
|
||||
|
||||
if l.Bridge.Mode == bridgeModeConstant && strings.HasPrefix(m.Content, prefix) {
|
||||
if l.Bridge.Mode == BridgeModeConstant && strings.HasPrefix(m.Content, prefix) {
|
||||
l.Bridge.DiscordSession.ChannelMessageSend(m.ChannelID, "Constant mode enabled, manual commands can not be entered")
|
||||
return
|
||||
}
|
||||
|
||||
l.Bridge.BridgeMutex.Lock()
|
||||
bridgeConnected := l.Bridge.Connected
|
||||
l.Bridge.BridgeMutex.Unlock()
|
||||
|
||||
if strings.HasPrefix(m.Content, prefix+" link") {
|
||||
// Look for the message sender in that guild's current voice states.
|
||||
for _, vs := range g.VoiceStates {
|
||||
if l.Bridge.Connected {
|
||||
if bridgeConnected {
|
||||
l.Bridge.DiscordSession.ChannelMessageSend(m.ChannelID, "Bridge already running, unlink first")
|
||||
return
|
||||
}
|
||||
if vs.UserID == m.Author.ID {
|
||||
log.Printf("Trying to join GID %v and VID %v\n", g.ID, vs.ChannelID)
|
||||
l.Bridge.DiscordChannelID = vs.ChannelID
|
||||
go l.Bridge.startBridge()
|
||||
go l.Bridge.StartBridge()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -103,7 +109,7 @@ func (l *DiscordListener) messageCreate(s *discordgo.Session, m *discordgo.Messa
|
|||
|
||||
if strings.HasPrefix(m.Content, prefix+" unlink") {
|
||||
// Look for the message sender in that guild's current voice states.
|
||||
if !l.Bridge.Connected {
|
||||
if !bridgeConnected {
|
||||
l.Bridge.DiscordSession.ChannelMessageSend(m.ChannelID, "Bridge is not currently running")
|
||||
return
|
||||
}
|
||||
|
@ -118,7 +124,7 @@ func (l *DiscordListener) messageCreate(s *discordgo.Session, m *discordgo.Messa
|
|||
|
||||
if strings.HasPrefix(m.Content, prefix+" refresh") {
|
||||
// Look for the message sender in that guild's current voice states.
|
||||
if !l.Bridge.Connected {
|
||||
if !bridgeConnected {
|
||||
l.Bridge.DiscordSession.ChannelMessageSend(m.ChannelID, "Bridge is not currently running")
|
||||
return
|
||||
}
|
||||
|
@ -129,29 +135,28 @@ func (l *DiscordListener) messageCreate(s *discordgo.Session, m *discordgo.Messa
|
|||
|
||||
time.Sleep(5 * time.Second)
|
||||
|
||||
go l.Bridge.startBridge()
|
||||
go l.Bridge.StartBridge()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if strings.HasPrefix(m.Content, prefix+" auto") {
|
||||
if l.Bridge.Mode != bridgeModeAuto {
|
||||
if l.Bridge.Mode != BridgeModeAuto {
|
||||
l.Bridge.DiscordSession.ChannelMessageSend(m.ChannelID, "Auto mode enabled")
|
||||
l.Bridge.Mode = bridgeModeAuto
|
||||
l.Bridge.Mode = BridgeModeAuto
|
||||
l.Bridge.DiscordChannelID = l.Bridge.BridgeConfig.CID
|
||||
l.Bridge.AutoChanDie = make(chan bool)
|
||||
go l.Bridge.AutoBridge()
|
||||
} else {
|
||||
l.Bridge.DiscordSession.ChannelMessageSend(m.ChannelID, "Auto mode disabled")
|
||||
l.Bridge.DiscordChannelID = ""
|
||||
l.Bridge.AutoChanDie <- true
|
||||
l.Bridge.Mode = bridgeModeManual
|
||||
l.Bridge.Mode = BridgeModeManual
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (l *DiscordListener) voiceUpdate(s *discordgo.Session, event *discordgo.VoiceStateUpdate) {
|
||||
func (l *DiscordListener) VoiceUpdate(s *discordgo.Session, event *discordgo.VoiceStateUpdate) {
|
||||
l.Bridge.DiscordUsersMutex.Lock()
|
||||
defer l.Bridge.DiscordUsersMutex.Unlock()
|
||||
|
||||
|
@ -159,7 +164,7 @@ func (l *DiscordListener) voiceUpdate(s *discordgo.Session, event *discordgo.Voi
|
|||
|
||||
g, err := s.State.Guild(l.Bridge.BridgeConfig.GID)
|
||||
if err != nil {
|
||||
log.Println("error finding guild")
|
||||
log.Println("Error finding guild")
|
||||
panic(err)
|
||||
}
|
||||
|
||||
|
@ -181,7 +186,7 @@ func (l *DiscordListener) voiceUpdate(s *discordgo.Session, event *discordgo.Voi
|
|||
|
||||
u, err := s.User(vs.UserID)
|
||||
if err != nil {
|
||||
log.Println("error looking up username")
|
||||
log.Println("Error looking up username")
|
||||
continue
|
||||
}
|
||||
|
||||
|
@ -190,16 +195,18 @@ func (l *DiscordListener) voiceUpdate(s *discordgo.Session, event *discordgo.Voi
|
|||
if err != nil {
|
||||
log.Println("Error creating private channel for", u.Username)
|
||||
}
|
||||
l.Bridge.DiscordUsers[vs.UserID] = discordUser{
|
||||
l.Bridge.DiscordUsers[vs.UserID] = DiscordUser{
|
||||
username: u.Username,
|
||||
seen: true,
|
||||
dm: dm,
|
||||
}
|
||||
l.Bridge.BridgeMutex.Lock()
|
||||
if l.Bridge.Connected && !l.Bridge.BridgeConfig.MumbleDisableText {
|
||||
l.Bridge.MumbleClient.Do(func() {
|
||||
l.Bridge.MumbleClient.Self.Channel.Send(fmt.Sprintf("%v has joined Discord\n", u.Username), false)
|
||||
})
|
||||
}
|
||||
l.Bridge.BridgeMutex.Unlock()
|
||||
} else {
|
||||
du := l.Bridge.DiscordUsers[vs.UserID]
|
||||
du.seen = true
|
||||
|
@ -211,15 +218,21 @@ func (l *DiscordListener) voiceUpdate(s *discordgo.Session, event *discordgo.Voi
|
|||
|
||||
// Remove users that are no longer connected
|
||||
for id := range l.Bridge.DiscordUsers {
|
||||
if l.Bridge.DiscordUsers[id].seen == false {
|
||||
if !l.Bridge.DiscordUsers[id].seen {
|
||||
log.Println("User left Discord channel " + l.Bridge.DiscordUsers[id].username)
|
||||
l.Bridge.BridgeMutex.Lock()
|
||||
if l.Bridge.Connected && !l.Bridge.BridgeConfig.MumbleDisableText {
|
||||
l.Bridge.MumbleClient.Do(func() {
|
||||
l.Bridge.MumbleClient.Self.Channel.Send(fmt.Sprintf("%v has left Discord channel\n", l.Bridge.DiscordUsers[id].username), false)
|
||||
})
|
||||
}
|
||||
delete(l.Bridge.DiscordUsers, id)
|
||||
l.Bridge.BridgeMutex.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
l.Bridge.BridgeMutex.Lock()
|
||||
promDiscordUsers.Set(float64(len(l.Bridge.DiscordUsers)))
|
||||
l.Bridge.BridgeMutex.Unlock()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,421 @@
|
|||
package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bwmarrin/discordgo"
|
||||
"github.com/stieneee/gopus"
|
||||
"github.com/stieneee/gumble/gumble"
|
||||
"github.com/stieneee/mumble-discord-bridge/pkg/sleepct"
|
||||
)
|
||||
|
||||
type fromDiscord struct {
|
||||
decoder *gopus.Decoder
|
||||
pcm chan []int16
|
||||
receiving bool // is used to to track the assumption that we are streaming a continuos stream form discord
|
||||
streaming bool // The buffer streaming is streaming out
|
||||
lastSequence uint16
|
||||
lastTimeStamp uint32
|
||||
}
|
||||
|
||||
// DiscordDuplex Handle discord voice stream
|
||||
type DiscordDuplex struct {
|
||||
Bridge *BridgeState
|
||||
|
||||
discordMutex sync.Mutex
|
||||
fromDiscordMap map[uint32]fromDiscord
|
||||
discordSendSleepTick sleepct.SleepCT
|
||||
discordReceiveSleepTick sleepct.SleepCT
|
||||
}
|
||||
|
||||
func NewDiscordDuplex(b *BridgeState) *DiscordDuplex {
|
||||
return &DiscordDuplex{
|
||||
Bridge: b,
|
||||
fromDiscordMap: make(map[uint32]fromDiscord),
|
||||
discordSendSleepTick: sleepct.SleepCT{},
|
||||
discordReceiveSleepTick: sleepct.SleepCT{},
|
||||
}
|
||||
}
|
||||
|
||||
// OnError gets called by dgvoice when an error is encountered.
|
||||
// By default logs to STDERR
|
||||
var OnError = func(str string, err error) {
|
||||
prefix := "dgVoice: " + str
|
||||
|
||||
if err != nil {
|
||||
log.Println(prefix + ": " + err.Error())
|
||||
} else {
|
||||
log.Println(prefix)
|
||||
}
|
||||
}
|
||||
|
||||
// SendPCM will receive on the provied channel encode
|
||||
// received PCM data with Opus then send that to Discordgo
|
||||
func (dd *DiscordDuplex) discordSendPCM(ctx context.Context, cancel context.CancelFunc, pcm <-chan []int16) {
|
||||
const channels int = 1
|
||||
const frameRate int = 48000 // audio sampling rate
|
||||
const frameSize int = 960 // uint16 size of each audio frame
|
||||
const maxBytes int = (frameSize * 2) * 2 // max size of opus data
|
||||
|
||||
streaming := false
|
||||
|
||||
opusEncoder, err := gopus.NewEncoder(frameRate, channels, gopus.Audio)
|
||||
if err != nil {
|
||||
OnError("NewEncoder Error", err)
|
||||
panic(err)
|
||||
}
|
||||
|
||||
// Generate Opus Silence Frame
|
||||
opusSilence := []byte{0xf8, 0xff, 0xfe}
|
||||
|
||||
dd.discordSendSleepTick.Start(20 * time.Millisecond)
|
||||
|
||||
lastReady := true
|
||||
var readyTimeout *time.Timer
|
||||
var speakingStart time.Time
|
||||
|
||||
// Spy on the PCM channel to notify
|
||||
// TODO determine a method to notify a paused sleepct
|
||||
// pcm := make(chan []int16, 10)
|
||||
// go func() {
|
||||
// for {
|
||||
// t, ok := <-pcmIn
|
||||
// if !ok {
|
||||
// close(pcm)
|
||||
// return
|
||||
// } else {
|
||||
// dd.discordSendSleepTick.Notify()
|
||||
// pcm <- t
|
||||
// }
|
||||
// }
|
||||
// }()
|
||||
|
||||
internalSend := func(opus []byte) {
|
||||
dd.Bridge.DiscordVoice.RWMutex.RLock()
|
||||
if !dd.Bridge.DiscordVoice.Ready || dd.Bridge.DiscordVoice.OpusSend == nil {
|
||||
if lastReady {
|
||||
OnError(fmt.Sprintf("Discordgo not ready for opus packets. %+v : %+v", dd.Bridge.DiscordVoice.Ready, dd.Bridge.DiscordVoice.OpusSend), nil)
|
||||
readyTimeout = time.AfterFunc(30*time.Second, func() {
|
||||
log.Println("Debug: Set ready timeout")
|
||||
cancel()
|
||||
})
|
||||
lastReady = false
|
||||
}
|
||||
} else if !lastReady {
|
||||
fmt.Println("Discordgo ready to send opus packets")
|
||||
lastReady = true
|
||||
readyTimeout.Stop()
|
||||
} else {
|
||||
select {
|
||||
case dd.Bridge.DiscordVoice.OpusSend <- opus:
|
||||
case <-ctx.Done():
|
||||
}
|
||||
|
||||
promDiscordSentPackets.Inc()
|
||||
}
|
||||
dd.Bridge.DiscordVoice.RWMutex.RUnlock()
|
||||
}
|
||||
|
||||
defer log.Println("Stopping Discord send PCM")
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// if we are not streaming try to pause
|
||||
// promTimerDiscordSend.Observe(float64(dd.discordSendSleepTick.SleepNextTarget(ctx, !streaming)))
|
||||
promTimerDiscordSend.Observe(float64(dd.discordSendSleepTick.SleepNextTarget(ctx, false)))
|
||||
|
||||
if (len(pcm) > 1 && streaming) || (len(pcm) > dd.Bridge.BridgeConfig.DiscordStartStreamingCount && !streaming) {
|
||||
if !streaming {
|
||||
speakingStart = time.Now()
|
||||
done := make(chan bool, 1)
|
||||
go func() {
|
||||
// This call will prevent discordSendPCM from exiting if the discord connection is lost
|
||||
dd.Bridge.DiscordVoice.Speaking(true)
|
||||
done <- true
|
||||
}()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(5 * time.Second):
|
||||
fmt.Println("Discord speaking timeout :(")
|
||||
cancel()
|
||||
return
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
streaming = true
|
||||
}
|
||||
|
||||
r1 := <-pcm
|
||||
r2 := <-pcm
|
||||
|
||||
// try encoding pcm frame with Opus
|
||||
opus, err := opusEncoder.Encode(append(r1, r2...), frameSize, maxBytes)
|
||||
if err != nil {
|
||||
OnError("Encoding Error", err)
|
||||
continue
|
||||
}
|
||||
|
||||
internalSend(opus)
|
||||
|
||||
} else {
|
||||
if streaming {
|
||||
// Check to see if there is a short speaking cycle.
|
||||
// It is possible that short speaking cycle is the result of a short input to mumble (Not a problem). ie a quick tap of push to talk button.
|
||||
// Or when timing delays are introduced via network, hardware or kernel delays (Problem).
|
||||
// The problem delays result in choppy or stuttering sounds, especially when the silence frames are introduced into the opus frames below.
|
||||
// Multiple short cycle delays can result in a discord rate limiter being trigger due to of multiple JSON speaking/not-speaking state changes
|
||||
if time.Since(speakingStart).Milliseconds() < 50 {
|
||||
log.Println("Warning: Short Mumble to Discord speaking cycle. Consider increaseing the size of the to Discord jitter buffer.")
|
||||
}
|
||||
|
||||
// Send silence as suggested by Discord Documentation.
|
||||
// We want to do this after alerting the user of possible short speaking cycles
|
||||
for i := 0; i < 5; i++ {
|
||||
internalSend(opusSilence)
|
||||
// promTimerDiscordSend.Observe(float64(dd.discordSendSleepTick.SleepNextTarget(ctx, true)))
|
||||
promTimerDiscordSend.Observe(float64(dd.discordSendSleepTick.SleepNextTarget(ctx, false)))
|
||||
|
||||
}
|
||||
|
||||
dd.Bridge.DiscordVoice.Speaking(false)
|
||||
streaming = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ReceivePCM will receive on the the Discordgo OpusRecv channel and decode
|
||||
// the opus audio into PCM then send it on the provided channel.
|
||||
func (dd *DiscordDuplex) discordReceivePCM(ctx context.Context, cancel context.CancelFunc) {
|
||||
var err error
|
||||
|
||||
lastReady := true
|
||||
var readyTimeout *time.Timer
|
||||
|
||||
var zeros [480]int16
|
||||
for i := 0; i < 480; i++ {
|
||||
zeros[i] = 0
|
||||
}
|
||||
|
||||
for {
|
||||
dd.Bridge.DiscordVoice.RWMutex.RLock()
|
||||
if !dd.Bridge.DiscordVoice.Ready || dd.Bridge.DiscordVoice.OpusRecv == nil {
|
||||
if lastReady {
|
||||
OnError(fmt.Sprintf("Discordgo not to receive opus packets. %+v : %+v", dd.Bridge.DiscordVoice.Ready, dd.Bridge.DiscordVoice.OpusSend), nil)
|
||||
readyTimeout = time.AfterFunc(30*time.Second, func() {
|
||||
log.Println("Debug: Set ready timeout")
|
||||
cancel()
|
||||
})
|
||||
lastReady = false
|
||||
}
|
||||
continue
|
||||
} else if !lastReady {
|
||||
fmt.Println("Discordgo ready to receive packets")
|
||||
lastReady = true
|
||||
readyTimeout.Stop()
|
||||
}
|
||||
dd.Bridge.DiscordVoice.RWMutex.RUnlock()
|
||||
|
||||
var ok bool
|
||||
var p *discordgo.Packet
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("Stopping Discord receive PCM")
|
||||
return
|
||||
case p, ok = <-dd.Bridge.DiscordVoice.OpusRecv:
|
||||
}
|
||||
|
||||
if !ok {
|
||||
log.Println("Opus not ok")
|
||||
continue
|
||||
}
|
||||
|
||||
dd.discordMutex.Lock()
|
||||
|
||||
_, ok = dd.fromDiscordMap[p.SSRC]
|
||||
if !ok {
|
||||
newStream := fromDiscord{}
|
||||
newStream.pcm = make(chan []int16, 100)
|
||||
newStream.receiving = false
|
||||
newStream.streaming = false
|
||||
newStream.decoder, err = gopus.NewDecoder(48000, 1) // Decode into mono
|
||||
if err != nil {
|
||||
OnError("error creating opus decoder", err)
|
||||
dd.discordMutex.Unlock()
|
||||
continue
|
||||
}
|
||||
|
||||
dd.fromDiscordMap[p.SSRC] = newStream
|
||||
}
|
||||
|
||||
s := dd.fromDiscordMap[p.SSRC]
|
||||
|
||||
deltaT := int(p.Timestamp - s.lastTimeStamp)
|
||||
if p.Sequence-s.lastSequence != 1 {
|
||||
s.decoder.ResetState()
|
||||
}
|
||||
|
||||
// oldReceiving := s.receiving
|
||||
|
||||
if !s.receiving || deltaT < 1 || deltaT > 960*10 {
|
||||
// First packet assume deltaT
|
||||
// fmt.Println("replacing", deltaT, 960)
|
||||
deltaT = 960
|
||||
s.receiving = true
|
||||
}
|
||||
|
||||
s.lastTimeStamp = p.Timestamp
|
||||
s.lastSequence = p.Sequence
|
||||
|
||||
dd.fromDiscordMap[p.SSRC] = s
|
||||
dd.discordMutex.Unlock()
|
||||
|
||||
p.PCM, err = s.decoder.Decode(p.Opus, deltaT, false)
|
||||
if err != nil {
|
||||
OnError("Error decoding opus data", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// fmt.Println(p.SSRC, p.Type, deltaT, p.Sequence, p.Sequence-s.lastSequence, oldReceiving, s.streaming, len(p.Opus), len(p.PCM))
|
||||
|
||||
promDiscordReceivedPackets.Inc()
|
||||
|
||||
// Push data into pcm channel in 10ms chunks of mono pcm data
|
||||
dd.discordMutex.Lock()
|
||||
for l := 0; l < len(p.PCM); l = l + 480 {
|
||||
var next []int16
|
||||
u := l + 480
|
||||
|
||||
next = p.PCM[l:u]
|
||||
|
||||
select {
|
||||
case dd.fromDiscordMap[p.SSRC].pcm <- next:
|
||||
default:
|
||||
log.Println("From Discord buffer full. Dropping packet")
|
||||
}
|
||||
}
|
||||
dd.discordMutex.Unlock()
|
||||
|
||||
dd.discordReceiveSleepTick.Notify()
|
||||
}
|
||||
}
|
||||
|
||||
func (dd *DiscordDuplex) fromDiscordMixer(ctx context.Context, toMumble chan<- gumble.AudioBuffer) {
|
||||
mumbleSilence := gumble.AudioBuffer{}
|
||||
for i := 3; i < 480; i++ {
|
||||
mumbleSilence = append(mumbleSilence, 0x00)
|
||||
}
|
||||
var speakingStart time.Time
|
||||
|
||||
dd.discordReceiveSleepTick.Start(10 * time.Millisecond)
|
||||
|
||||
sendAudio := false
|
||||
toMumbleStreaming := false
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("Stopping from Discord mixer")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
// if didn't send audio try to pause
|
||||
// promTimerDiscordMixer.Observe(float64(dd.discordReceiveSleepTick.SleepNextTarget(ctx, !sendAudio)))
|
||||
// TODO Additional pause testing
|
||||
promTimerDiscordMixer.Observe(float64(dd.discordReceiveSleepTick.SleepNextTarget(ctx, false)))
|
||||
|
||||
dd.discordMutex.Lock()
|
||||
|
||||
sendAudio = false
|
||||
internalMixerArr := make([][]int16, 0)
|
||||
streamingCount := 0
|
||||
|
||||
// Work through each channel
|
||||
for i := range dd.fromDiscordMap {
|
||||
bufferLength := len(dd.fromDiscordMap[i].pcm)
|
||||
isStreaming := dd.fromDiscordMap[i].streaming
|
||||
if (bufferLength > 0 && isStreaming) || (bufferLength > dd.Bridge.BridgeConfig.MumbleStartStreamCount && !isStreaming) {
|
||||
if !toMumbleStreaming {
|
||||
speakingStart = time.Now()
|
||||
toMumbleStreaming = true
|
||||
}
|
||||
sendAudio = true
|
||||
|
||||
if !isStreaming {
|
||||
x := dd.fromDiscordMap[i]
|
||||
x.streaming = true
|
||||
dd.fromDiscordMap[i] = x
|
||||
}
|
||||
|
||||
streamingCount++
|
||||
x1 := (<-dd.fromDiscordMap[i].pcm)
|
||||
internalMixerArr = append(internalMixerArr, x1)
|
||||
} else {
|
||||
if dd.fromDiscordMap[i].streaming {
|
||||
x := dd.fromDiscordMap[i]
|
||||
x.streaming = false
|
||||
x.receiving = false // toggle this here is not optimal but there is no better location atm.
|
||||
dd.fromDiscordMap[i] = x
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
promDiscordArraySize.Set(float64(len(dd.fromDiscordMap)))
|
||||
promDiscordStreaming.Set(float64(streamingCount))
|
||||
|
||||
dd.discordMutex.Unlock()
|
||||
|
||||
mumbleTimeoutSend := func(outBuf []int16) {
|
||||
timeout := make(chan bool, 1)
|
||||
go func() {
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
timeout <- true
|
||||
}()
|
||||
|
||||
select {
|
||||
case toMumble <- outBuf:
|
||||
promSentMumblePackets.Inc()
|
||||
case <-timeout:
|
||||
log.Println("To Mumble timeout. Dropping packet")
|
||||
promToMumbleDropped.Inc()
|
||||
}
|
||||
}
|
||||
|
||||
if sendAudio {
|
||||
// Regular send mixed audio
|
||||
outBuf := make([]int16, 480)
|
||||
|
||||
for j := 0; j < len(internalMixerArr); j++ {
|
||||
for i := 0; i < len(internalMixerArr[j]); i++ {
|
||||
outBuf[i] += (internalMixerArr[j])[i]
|
||||
}
|
||||
}
|
||||
|
||||
mumbleTimeoutSend(outBuf)
|
||||
} else if !sendAudio && toMumbleStreaming {
|
||||
// Send opus silence to mumble
|
||||
// See note above about jitter buffer warning
|
||||
if time.Since(speakingStart).Milliseconds() < 50 {
|
||||
log.Println("Warning: Short Discord to Mumble speaking cycle. Consider increaseing the size of the to Mumble jitter buffer.", time.Since(speakingStart).Milliseconds())
|
||||
}
|
||||
|
||||
for i := 0; i < 5; i++ {
|
||||
mumbleTimeoutSend(mumbleSilence)
|
||||
promTimerDiscordMixer.Observe(float64(dd.discordReceiveSleepTick.SleepNextTarget(ctx, false)))
|
||||
}
|
||||
|
||||
toMumbleStreaming = false
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +1,12 @@
|
|||
package main
|
||||
package bridge
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"layeh.com/gumble/gumble"
|
||||
"github.com/stieneee/gumble/gumble"
|
||||
)
|
||||
|
||||
// MumbleListener Handle mumble events
|
||||
|
@ -12,53 +14,70 @@ type MumbleListener struct {
|
|||
Bridge *BridgeState
|
||||
}
|
||||
|
||||
func (l *MumbleListener) mumbleConnect(e *gumble.ConnectEvent) {
|
||||
func (l *MumbleListener) updateUsers() {
|
||||
l.Bridge.MumbleUsersMutex.Lock()
|
||||
l.Bridge.MumbleUsers = make(map[string]bool)
|
||||
for _, user := range l.Bridge.MumbleClient.Self.Channel.Users {
|
||||
//note, this might be too slow for really really big channels?
|
||||
//event listeners block while processing
|
||||
//also probably bad to rebuild the set every user change.
|
||||
if user.Name != l.Bridge.MumbleClient.Self.Name {
|
||||
l.Bridge.MumbleUsers[user.Name] = true
|
||||
}
|
||||
}
|
||||
promMumbleUsers.Set(float64(len(l.Bridge.MumbleUsers)))
|
||||
l.Bridge.MumbleUsersMutex.Unlock()
|
||||
|
||||
}
|
||||
|
||||
func (l *MumbleListener) MumbleConnect(e *gumble.ConnectEvent) {
|
||||
//join specified channel
|
||||
startingChannel := e.Client.Channels.Find(l.Bridge.BridgeConfig.MumbleChannel...)
|
||||
if startingChannel != nil {
|
||||
e.Client.Self.Move(startingChannel)
|
||||
}
|
||||
|
||||
// l.updateUsers() // patch below
|
||||
|
||||
// This is an ugly patch Mumble Client state is slow to update
|
||||
time.AfterFunc(5*time.Second, func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
fmt.Printf("Failed to mumble user list %v \n", r)
|
||||
}
|
||||
}()
|
||||
l.updateUsers()
|
||||
})
|
||||
}
|
||||
|
||||
func (l *MumbleListener) mumbleUserChange(e *gumble.UserChangeEvent) {
|
||||
l.Bridge.MumbleUsersMutex.Lock()
|
||||
if e.Type.Has(gumble.UserChangeConnected) || e.Type.Has(gumble.UserChangeChannel) || e.Type.Has(gumble.UserChangeDisconnected) {
|
||||
l.Bridge.MumbleUsers = make(map[string]bool)
|
||||
for _, user := range l.Bridge.MumbleClient.Self.Channel.Users {
|
||||
//note, this might be too slow for really really big channels?
|
||||
//event listeners block while processing
|
||||
//also probably bad to rebuild the set every user change.
|
||||
if user.Name != l.Bridge.MumbleClient.Self.Name {
|
||||
l.Bridge.MumbleUsers[user.Name] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
l.Bridge.MumbleUsersMutex.Unlock()
|
||||
func (l *MumbleListener) MumbleUserChange(e *gumble.UserChangeEvent) {
|
||||
l.updateUsers()
|
||||
|
||||
if e.Type.Has(gumble.UserChangeConnected) {
|
||||
|
||||
log.Println("User connected to mumble " + e.User.Name)
|
||||
|
||||
if !l.Bridge.BridgeConfig.MumbleDisableText {
|
||||
e.User.Send("Mumble-Discord-Bridge v" + version)
|
||||
e.User.Send("Mumble-Discord-Bridge v" + l.Bridge.BridgeConfig.Version)
|
||||
|
||||
// Tell the user who is connected to discord
|
||||
l.Bridge.DiscordUsersMutex.Lock()
|
||||
if len(l.Bridge.DiscordUsers) == 0 {
|
||||
e.User.Send("No users connected to Discord")
|
||||
} else {
|
||||
s := "Connected to Discord: "
|
||||
|
||||
arr := []string{}
|
||||
l.Bridge.DiscordUsersMutex.Lock()
|
||||
for u := range l.Bridge.DiscordUsers {
|
||||
arr = append(arr, l.Bridge.DiscordUsers[u].username)
|
||||
}
|
||||
|
||||
s = s + strings.Join(arr[:], ",")
|
||||
|
||||
l.Bridge.DiscordUsersMutex.Unlock()
|
||||
e.User.Send(s)
|
||||
}
|
||||
l.Bridge.DiscordUsersMutex.Unlock()
|
||||
|
||||
}
|
||||
|
||||
// Send discord a notice
|
|
@ -0,0 +1,143 @@
|
|||
package bridge
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/stieneee/gumble/gumble"
|
||||
_ "github.com/stieneee/gumble/opus"
|
||||
"github.com/stieneee/mumble-discord-bridge/pkg/sleepct"
|
||||
)
|
||||
|
||||
// MumbleDuplex - listener and outgoing
|
||||
type MumbleDuplex struct {
|
||||
mutex sync.Mutex
|
||||
fromMumbleArr []chan gumble.AudioBuffer
|
||||
mumbleStreamingArr []bool
|
||||
mumbleSleepTick sleepct.SleepCT
|
||||
}
|
||||
|
||||
func NewMumbleDuplex() *MumbleDuplex {
|
||||
return &MumbleDuplex{
|
||||
fromMumbleArr: make([]chan gumble.AudioBuffer, 0),
|
||||
mumbleStreamingArr: make([]bool, 0),
|
||||
mumbleSleepTick: sleepct.SleepCT{},
|
||||
}
|
||||
}
|
||||
|
||||
// OnAudioStream - Spawn routines to handle incoming packets
|
||||
func (m *MumbleDuplex) OnAudioStream(e *gumble.AudioStreamEvent) {
|
||||
|
||||
// hold a reference ot the channel in the closure
|
||||
streamChan := make(chan gumble.AudioBuffer, 100)
|
||||
|
||||
m.mutex.Lock()
|
||||
m.fromMumbleArr = append(m.fromMumbleArr, streamChan)
|
||||
m.mumbleStreamingArr = append(m.mumbleStreamingArr, false)
|
||||
m.mutex.Unlock()
|
||||
|
||||
promMumbleArraySize.Set(float64(len(m.fromMumbleArr)))
|
||||
|
||||
go func() {
|
||||
name := e.User.Name
|
||||
log.Println("New mumble audio stream", name)
|
||||
for p := range e.C {
|
||||
// log.Println("audio packet", p.Sender.Name, len(p.AudioBuffer))
|
||||
|
||||
// 480 per 10ms
|
||||
for i := 0; i < len(p.AudioBuffer)/480; i++ {
|
||||
streamChan <- p.AudioBuffer[480*i : 480*(i+1)]
|
||||
}
|
||||
promReceivedMumblePackets.Inc()
|
||||
m.mumbleSleepTick.Notify()
|
||||
}
|
||||
log.Println("Mumble audio stream ended", name)
|
||||
}()
|
||||
}
|
||||
|
||||
func (m *MumbleDuplex) fromMumbleMixer(ctx context.Context, cancel context.CancelFunc, toDiscord chan []int16) {
|
||||
m.mumbleSleepTick.Start(10 * time.Millisecond)
|
||||
|
||||
sendAudio := false
|
||||
|
||||
droppingPackets := false
|
||||
droppingPacketCount := 0
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
log.Println("Stopping From Mumble Mixer")
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
promTimerMumbleMixer.Observe(float64(m.mumbleSleepTick.SleepNextTarget(ctx, false)))
|
||||
|
||||
m.mutex.Lock()
|
||||
|
||||
sendAudio = false
|
||||
internalMixerArr := make([]gumble.AudioBuffer, 0)
|
||||
streamingCount := 0
|
||||
|
||||
// Work through each channel
|
||||
for i := 0; i < len(m.fromMumbleArr); i++ {
|
||||
if len(m.fromMumbleArr[i]) > 0 {
|
||||
sendAudio = true
|
||||
if !m.mumbleStreamingArr[i] {
|
||||
m.mumbleStreamingArr[i] = true
|
||||
streamingCount++
|
||||
// log.Println("Mumble starting", i)
|
||||
}
|
||||
|
||||
x1 := (<-m.fromMumbleArr[i])
|
||||
internalMixerArr = append(internalMixerArr, x1)
|
||||
} else {
|
||||
if m.mumbleStreamingArr[i] {
|
||||
m.mumbleStreamingArr[i] = false
|
||||
// log.Println("Mumble stopping", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m.mutex.Unlock()
|
||||
|
||||
promMumbleStreaming.Set(float64(streamingCount))
|
||||
|
||||
if sendAudio {
|
||||
|
||||
outBuf := make([]int16, 480)
|
||||
|
||||
for i := 0; i < len(outBuf); i++ {
|
||||
for j := 0; j < len(internalMixerArr); j++ {
|
||||
outBuf[i] += (internalMixerArr[j])[i]
|
||||
}
|
||||
}
|
||||
|
||||
promToDiscordBufferSize.Set(float64(len(toDiscord)))
|
||||
select {
|
||||
case toDiscord <- outBuf:
|
||||
{
|
||||
if droppingPackets {
|
||||
log.Println("Discord buffer ok, total packets dropped " + strconv.Itoa(droppingPacketCount))
|
||||
droppingPackets = false
|
||||
}
|
||||
}
|
||||
default:
|
||||
if !droppingPackets {
|
||||
log.Println("Error: toDiscord buffer full. Dropping packets")
|
||||
droppingPackets = true
|
||||
droppingPacketCount = 0
|
||||
}
|
||||
droppingPacketCount++
|
||||
promToDiscordDropped.Inc()
|
||||
if droppingPacketCount > 250 {
|
||||
log.Println("Discord Timeout")
|
||||
cancel()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,141 @@
|
|||
package bridge
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
"github.com/prometheus/client_golang/prometheus/promauto"
|
||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||
)
|
||||
|
||||
var (
|
||||
// Bridge General
|
||||
|
||||
PromApplicationStartTime = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "mdb_bridge_start_time",
|
||||
Help: "The time the application started",
|
||||
})
|
||||
|
||||
promBridgeStarts = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "mdb_bridge_starts_count",
|
||||
Help: "The number of times the bridge start routine has been called",
|
||||
})
|
||||
|
||||
promBridgeStartTime = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "mdb_bridge_starts_time",
|
||||
Help: "The time the current bridge instance started",
|
||||
})
|
||||
|
||||
// MUMBLE
|
||||
promMumblePing = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "mdb_mumble_ping",
|
||||
Help: "Mumble ping",
|
||||
})
|
||||
|
||||
promMumbleUsers = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "mdb_mumble_users_gauge",
|
||||
Help: "The number of connected Mumble users",
|
||||
})
|
||||
|
||||
promReceivedMumblePackets = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "mdb_mumble_received_count",
|
||||
Help: "The count of Mumble audio packets received",
|
||||
})
|
||||
|
||||
promSentMumblePackets = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "mdb_mumble_sent_count",
|
||||
Help: "The count of audio packets sent to mumble",
|
||||
})
|
||||
|
||||
// promToMumbleBufferSize = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
// Name: "mdb_to_mumble_buffer_gauge",
|
||||
// Help: "",
|
||||
// })
|
||||
|
||||
promToMumbleDropped = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "mdb_to_mumble_dropped",
|
||||
Help: "The number of packets timeouts to mumble",
|
||||
})
|
||||
|
||||
promMumbleArraySize = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "mdb_to_mumble_array_size_gauge",
|
||||
Help: "The array size of mumble streams",
|
||||
})
|
||||
|
||||
promMumbleStreaming = promauto.NewGauge(prometheus.GaugeOpts{ //SUMMARY?
|
||||
Name: "mdb_mumble_streaming_gauge",
|
||||
Help: "The number of active audio streams streaming audio from mumble",
|
||||
})
|
||||
|
||||
// DISCORD
|
||||
|
||||
// TODO Discrod Ping
|
||||
|
||||
promDiscordHeartBeat = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "mdb_discord_latency",
|
||||
Help: "Discord heartbeat latency",
|
||||
})
|
||||
|
||||
promDiscordUsers = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "mdb_discord_users_gauge",
|
||||
Help: "The number of Connected Discord users",
|
||||
})
|
||||
|
||||
promDiscordReceivedPackets = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "mdb_discord_received_count",
|
||||
Help: "The number of received packets from Discord",
|
||||
})
|
||||
|
||||
promDiscordSentPackets = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "mdb_discord_sent_count",
|
||||
Help: "The number of packets sent to Discord",
|
||||
})
|
||||
|
||||
promToDiscordBufferSize = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "mdb_discord_buffer_gauge",
|
||||
Help: "The buffer size for packets to Discord",
|
||||
})
|
||||
|
||||
promToDiscordDropped = promauto.NewCounter(prometheus.CounterOpts{
|
||||
Name: "mdb_to_discord_dropped",
|
||||
Help: "The count of packets dropped to discord",
|
||||
})
|
||||
|
||||
promDiscordArraySize = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "mdb_discord_array_size_gauge",
|
||||
Help: "The discord receiving array size",
|
||||
})
|
||||
|
||||
promDiscordStreaming = promauto.NewGauge(prometheus.GaugeOpts{
|
||||
Name: "mdb_discord_streaming_gauge",
|
||||
Help: "The number of active audio streams streaming from discord",
|
||||
})
|
||||
|
||||
// Sleep Timer Performance
|
||||
|
||||
promTimerDiscordSend = promauto.NewHistogram(prometheus.HistogramOpts{
|
||||
Name: "mdb_timer_discord_send",
|
||||
Help: "Timer performance for Discord send",
|
||||
Buckets: []float64{1000, 2000, 5000, 10000, 20000},
|
||||
})
|
||||
|
||||
promTimerDiscordMixer = promauto.NewHistogram(prometheus.HistogramOpts{
|
||||
Name: "mdb_timer_discord_mixer",
|
||||
Help: "Timer performance for the Discord mixer",
|
||||
Buckets: []float64{1000, 2000, 5000, 10000, 20000},
|
||||
})
|
||||
|
||||
promTimerMumbleMixer = promauto.NewHistogram(prometheus.HistogramOpts{
|
||||
Name: "mdb_timer_mumble_mixer",
|
||||
Help: "Timer performance for the Mumble mixer",
|
||||
Buckets: []float64{1000, 2000, 5000, 10000, 20000},
|
||||
})
|
||||
)
|
||||
|
||||
func StartPromServer(port int) {
|
||||
log.Println("Starting Metrics Server")
|
||||
http.Handle("/metrics", promhttp.Handler())
|
||||
http.ListenAndServe(":"+strconv.Itoa(port), nil)
|
||||
}
|
106
mumble.go
106
mumble.go
|
@ -1,106 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"layeh.com/gumble/gumble"
|
||||
_ "layeh.com/gumble/opus"
|
||||
)
|
||||
|
||||
var mutex sync.Mutex
|
||||
var fromMumbleArr []chan gumble.AudioBuffer
|
||||
var mumbleStreamingArr []bool
|
||||
|
||||
// MumbleDuplex - listenera and outgoing
|
||||
type MumbleDuplex struct{}
|
||||
|
||||
// OnAudioStream - Spawn routines to handle incoming packets
|
||||
func (m MumbleDuplex) OnAudioStream(e *gumble.AudioStreamEvent) {
|
||||
|
||||
// hold a reference ot the channel in the closure
|
||||
localMumbleArray := make(chan gumble.AudioBuffer, 100)
|
||||
|
||||
mutex.Lock()
|
||||
fromMumbleArr = append(fromMumbleArr, localMumbleArray)
|
||||
mumbleStreamingArr = append(mumbleStreamingArr, false)
|
||||
mutex.Unlock()
|
||||
|
||||
go func() {
|
||||
// TODO kill go routine on cleanup
|
||||
log.Println("new mumble audio stream", e.User.Name)
|
||||
for {
|
||||
select {
|
||||
case p := <-e.C:
|
||||
// log.Println("audio packet", p.Sender.Name, len(p.AudioBuffer))
|
||||
|
||||
// 480 per 10ms
|
||||
for i := 0; i < len(p.AudioBuffer)/480; i++ {
|
||||
localMumbleArray <- p.AudioBuffer[480*i : 480*(i+1)]
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
return
|
||||
}
|
||||
|
||||
func (m MumbleDuplex) fromMumbleMixer(ctx context.Context, wg *sync.WaitGroup, toDiscord chan []int16) {
|
||||
ticker := time.NewTicker(10 * time.Millisecond)
|
||||
sendAudio := false
|
||||
wg.Add(1)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
wg.Done()
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
<-ticker.C
|
||||
|
||||
mutex.Lock()
|
||||
|
||||
sendAudio = false
|
||||
internalMixerArr := make([]gumble.AudioBuffer, 0)
|
||||
|
||||
// Work through each channel
|
||||
for i := 0; i < len(fromMumbleArr); i++ {
|
||||
if len(fromMumbleArr[i]) > 0 {
|
||||
sendAudio = true
|
||||
if mumbleStreamingArr[i] == false {
|
||||
mumbleStreamingArr[i] = true
|
||||
// log.Println("mumble starting", i)
|
||||
}
|
||||
|
||||
x1 := (<-fromMumbleArr[i])
|
||||
internalMixerArr = append(internalMixerArr, x1)
|
||||
} else {
|
||||
if mumbleStreamingArr[i] == true {
|
||||
mumbleStreamingArr[i] = false
|
||||
// log.Println("mumble stopping", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mutex.Unlock()
|
||||
|
||||
outBuf := make([]int16, 480)
|
||||
|
||||
for i := 0; i < len(outBuf); i++ {
|
||||
for j := 0; j < len(internalMixerArr); j++ {
|
||||
outBuf[i] += (internalMixerArr[j])[i]
|
||||
}
|
||||
}
|
||||
|
||||
if sendAudio {
|
||||
select {
|
||||
case toDiscord <- outBuf:
|
||||
default:
|
||||
log.Println("toDiscord buffer full. Dropping packet")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,92 @@
|
|||
package sleepct
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
)
|
||||
|
||||
// SleepCT - Sleep constant time step crates a sleep based ticker.
|
||||
// designed to maintain a consistent sleep/tick interval.
|
||||
// The sleeper can be paused waiting to be signaled from another go routine.
|
||||
// This allows for the pausing of loops that do not have work to complete
|
||||
type SleepCT struct {
|
||||
d time.Duration // desired duration between targets
|
||||
t time.Time // last time target
|
||||
resume chan bool
|
||||
wake time.Time // last wake time
|
||||
drift int64 // last wake drift microseconds
|
||||
}
|
||||
|
||||
func (s *SleepCT) Start(d time.Duration) {
|
||||
s.resume = make(chan bool, 2)
|
||||
if s.t.IsZero() {
|
||||
s.d = d
|
||||
s.t = time.Now()
|
||||
} else {
|
||||
panic("SleepCT already started")
|
||||
}
|
||||
}
|
||||
|
||||
// Sleep to the next target duration.
|
||||
// If pause it set to true will sleep the duration and wait to be notified.
|
||||
// The notification channel will be cleared when the thread wakes.
|
||||
// SleepNextTarget should not be call more than once concurrently.
|
||||
func (s *SleepCT) SleepNextTarget(ctx context.Context, pause bool) int64 {
|
||||
|
||||
now := time.Now()
|
||||
|
||||
// if target is zero safety net
|
||||
if s.t.IsZero() {
|
||||
fmt.Println("SleepCT reset")
|
||||
s.t = now.Add(-s.d)
|
||||
}
|
||||
|
||||
// Sleep to Next Target
|
||||
s.t = s.t.Add(s.d)
|
||||
|
||||
// Compute the desired sleep time to reach the target
|
||||
d := time.Until(s.t)
|
||||
|
||||
// Sleep
|
||||
time.Sleep(d)
|
||||
|
||||
// record the wake time
|
||||
s.wake = time.Now()
|
||||
s.drift = s.wake.Sub(s.t).Microseconds()
|
||||
|
||||
// fmt.Println(s.t.UnixMilli(), d.Milliseconds(), wake.UnixMilli(), drift, pause, len(s.resume))
|
||||
|
||||
// external pause control
|
||||
if pause {
|
||||
// don't pause if the notification channel has something
|
||||
if len(s.resume) == 0 {
|
||||
// fmt.Println("pause")
|
||||
select {
|
||||
case <-s.resume:
|
||||
case <-ctx.Done():
|
||||
// fmt.Println("sleepct ctx exit")
|
||||
}
|
||||
// if we did pause set the last sleep target to now
|
||||
s.t = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
// Drain the resume channel
|
||||
select {
|
||||
case <-s.resume:
|
||||
default:
|
||||
}
|
||||
|
||||
// return the drift for monitoring purposes
|
||||
return s.drift
|
||||
}
|
||||
|
||||
// Notify attempts to resume a paused sleeper.
|
||||
// It is safe to call notify from other processes and as often as desired.
|
||||
func (s *SleepCT) Notify() {
|
||||
select {
|
||||
case s.resume <- true:
|
||||
default:
|
||||
}
|
||||
}
|
|
@ -0,0 +1,198 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand"
|
||||
"sort"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stieneee/mumble-discord-bridge/pkg/sleepct"
|
||||
"github.com/stieneee/tickerct"
|
||||
)
|
||||
|
||||
const testCount int64 = 10000
|
||||
const maxSleepInterval time.Duration = 15 * time.Millisecond
|
||||
const tickerInterval time.Duration = 10 * time.Millisecond
|
||||
const testDuration time.Duration = time.Duration(testCount * 10 * int64(time.Millisecond))
|
||||
|
||||
func testTickerBaseCase(wg *sync.WaitGroup, test *testing.T) {
|
||||
wg.Add(1)
|
||||
go func(interval time.Duration) {
|
||||
now := time.Now()
|
||||
start := now
|
||||
// start the ticker
|
||||
t := time.NewTicker(interval)
|
||||
var i int64
|
||||
for i = 0; i < testCount; i++ {
|
||||
now = <-t.C
|
||||
// fmt.Println(now)
|
||||
}
|
||||
t.Stop()
|
||||
fmt.Println("Ticker (unloaded) after", testDuration, "drifts", time.Since(start)-testDuration)
|
||||
wg.Done()
|
||||
}(tickerInterval)
|
||||
}
|
||||
|
||||
func TestTickerBaseCase(t *testing.T) {
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
testTickerBaseCase(&wg, t)
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func testTickerLoaded(wg *sync.WaitGroup) {
|
||||
wg.Add(1)
|
||||
go func(interval time.Duration) {
|
||||
now := time.Now()
|
||||
start := now
|
||||
// start the ticker
|
||||
t := time.NewTicker(interval)
|
||||
var i int64
|
||||
for i = 0; i < testCount; i++ {
|
||||
if i+1 < testCount {
|
||||
time.Sleep(time.Duration(float64(maxSleepInterval) * rand.Float64()))
|
||||
}
|
||||
now = <-t.C
|
||||
// fmt.Println(now)
|
||||
}
|
||||
t.Stop()
|
||||
fmt.Println("Ticker (loaded) after", testDuration, "drifts", time.Since(start)-testDuration)
|
||||
wg.Done()
|
||||
}(tickerInterval)
|
||||
}
|
||||
|
||||
func TestTicker(t *testing.T) {
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
testTickerLoaded(&wg)
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func testTickerCT(wg *sync.WaitGroup) {
|
||||
wg.Add(1)
|
||||
go func(interval time.Duration) {
|
||||
now := time.Now()
|
||||
start := now
|
||||
// start the ticker
|
||||
t := tickerct.NewTickerCT(interval)
|
||||
var i int64
|
||||
for i = 0; i < testCount; i++ {
|
||||
if i+1 < testCount {
|
||||
time.Sleep(time.Duration(float64(maxSleepInterval) * rand.Float64()))
|
||||
}
|
||||
now = <-t.C
|
||||
// fmt.Println(now)
|
||||
}
|
||||
t.Stop()
|
||||
fmt.Println("TickerCT (loaded) after", testDuration, "drifts", time.Since(start)-testDuration)
|
||||
wg.Done()
|
||||
}(tickerInterval)
|
||||
}
|
||||
|
||||
func TestTickerCT(t *testing.T) {
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
testTickerCT(&wg)
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func testSleepCT(wg *sync.WaitGroup) {
|
||||
wg.Add(1)
|
||||
go func(interval time.Duration) {
|
||||
now := time.Now()
|
||||
start := now
|
||||
// start the ticker
|
||||
s := sleepct.SleepCT{}
|
||||
s.Start(interval)
|
||||
var i int64
|
||||
for i = 0; i < testCount; i++ {
|
||||
if i+1 < testCount {
|
||||
time.Sleep(time.Duration(float64(maxSleepInterval) * rand.Float64()))
|
||||
}
|
||||
s.SleepNextTarget(context.TODO(), false)
|
||||
}
|
||||
fmt.Println("SleepCT (loaded) after", testDuration, "drifts", time.Since(start)-testDuration)
|
||||
wg.Done()
|
||||
}(tickerInterval)
|
||||
}
|
||||
|
||||
func TestSleepCT(t *testing.T) {
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
testSleepCT(&wg)
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func testSleepCTPause(wg *sync.WaitGroup) {
|
||||
wg.Add(1)
|
||||
go func(interval time.Duration) {
|
||||
now := time.Now()
|
||||
start := now
|
||||
// start the ticker
|
||||
s := sleepct.SleepCT{}
|
||||
s.Start(interval)
|
||||
var i int64
|
||||
for i = 0; i < testCount; i++ {
|
||||
if i+1 < testCount {
|
||||
time.Sleep(time.Duration(float64(maxSleepInterval) * rand.Float64()))
|
||||
}
|
||||
s.Notify()
|
||||
s.SleepNextTarget(context.TODO(), true)
|
||||
}
|
||||
fmt.Println("SleepCT Pause (loaded) after", testDuration, "drifts", time.Since(start)-testDuration)
|
||||
wg.Done()
|
||||
}(tickerInterval)
|
||||
}
|
||||
|
||||
func TestSleepCTPause(t *testing.T) {
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
testSleepCTPause(&wg)
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestIdleJitter(t *testing.T) {
|
||||
wg := sync.WaitGroup{}
|
||||
|
||||
const testSize = 100000
|
||||
const sleepTarget = time.Millisecond
|
||||
|
||||
res := make([]time.Duration, testSize)
|
||||
|
||||
for i := 0; i < testSize; i++ {
|
||||
start := time.Now()
|
||||
target := start.Add(sleepTarget)
|
||||
|
||||
time.Sleep(sleepTarget)
|
||||
|
||||
res[i] = time.Since(target)
|
||||
}
|
||||
|
||||
sort.Slice(res, func(i, j int) bool {
|
||||
return res[i] < res[j]
|
||||
})
|
||||
|
||||
var total float64 = 0
|
||||
for i := 0; i < testSize; i++ {
|
||||
total += float64(res[i])
|
||||
}
|
||||
avg := time.Duration(total / testSize)
|
||||
|
||||
nineFive := int64(math.Round(testSize * 0.95))
|
||||
nineNine := int64(math.Round(testSize * 0.99))
|
||||
nineNineNine := int64(math.Round(testSize * 0.999))
|
||||
|
||||
fmt.Println("IdleJitter test", testSize, sleepTarget)
|
||||
fmt.Println("IdleJitter results min/avg/95/99/99.9/max", res[0], avg, res[nineFive], res[nineNine], res[nineNineNine], res[testSize-1])
|
||||
|
||||
wg.Wait()
|
||||
}
|
Loading…
Reference in New Issue