large refactor, fixes #2
This commit is contained in:
parent
2729c66ccc
commit
7aa034f3ba
@ -9,6 +9,6 @@ npm install && npm run build
|
||||
cd ../
|
||||
sudo docker build -f Dockerfile -t nurdism/neko .
|
||||
|
||||
# sudo docker run -p 8080:8080 --shm-size=2gb nurdism/neko:latest
|
||||
# sudo docker run --network host --shm-size=2gb nurdism/neko:latest
|
||||
# sudo docker run --network host --shm-size=2gb -it nurdism/neko:latest /bin/bash
|
||||
# sudo docker run -p 8080:8080 --shm-size=1gb nurdism/neko:latest
|
||||
# sudo docker run --network host --shm-size=1gb nurdism/neko:latest
|
||||
# sudo docker run --network host --shm-size=1gb -it nurdism/neko:latest /bin/bash
|
20
.docker/test
20
.docker/test
@ -2,6 +2,16 @@
|
||||
|
||||
# usefull debugging tools pavucontrol htop x11vnc
|
||||
|
||||
# if [ ! -f ../server/bin/neko ]; then
|
||||
# echo "build server before testing"
|
||||
# exit 1
|
||||
# fi
|
||||
|
||||
# if [ ! -d ../client/dist/ ]; then
|
||||
# echo "build client before testing"
|
||||
# exit 1
|
||||
# fi
|
||||
|
||||
sudo mkdir -p /var/run/dbus /etc/neko
|
||||
sudo /etc/init.d/dbus start
|
||||
|
||||
@ -19,16 +29,6 @@ if [ ! -f /usr/lib/firefox-esr/distribution/extensions/uBlock0@raymondhill.net.x
|
||||
sudo curl -o /usr/lib/firefox-esr/distribution/extensions/uBlock0@raymondhill.net.xpi https://addons.mozilla.org/firefox/downloads/latest/ublock-origin/addon-607454-latest.xpi
|
||||
fi
|
||||
|
||||
if [ ! -f ../server/bin/neko ]; then
|
||||
echo "build server before testing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -d ../client/dist/ ]; then
|
||||
echo "build client before testing"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
sudo cp ../server/bin/neko /usr/bin/neko
|
||||
sudo cp -R ../client/dist /var/www/
|
||||
|
||||
|
45
README.md
45
README.md
@ -6,7 +6,7 @@
|
||||
<img src="https://github.com/nurdism/neko/raw/master/.github/demo.gif" width="650" height="auto"/>
|
||||
</div>
|
||||
|
||||
# n.eko
|
||||
# **n**.eko
|
||||
This is a proof of concept project I threw together over the last few days, it's ugly, it's not perfect, but it looks nice. This uses web rtc to stream a desktop inside of a docker container, I made this because [rabb.it](https://en.wikipedia.org/wiki/Rabb.it) went under and my internet can't handle streaming and discord keeps crashing. I just want to watch anime with my friends ლ(ಠ益ಠლ) so I started digging throughout the net and found a few *kinda* clones, but non of them had the virtual browser, then I found [Turtus](https://github.com/Khauri/Turtus) and I was able to figure out the rest.
|
||||
|
||||
This is by no means a fully featured clone of rabbit. The client has no concept of other peers. It has bugs, but for the most part it works. I'm not sure what the future holds for this. If I continue to use it and like it, I'll probably keep pushing updates to it. I'd be happy to accept PRs for any improvements.
|
||||
@ -15,15 +15,23 @@ This is by no means a fully featured clone of rabbit. The client has no concept
|
||||
I like cats (Neko is the Japanese word for cat), I'm a weeb/nerd, I own the domain [n.eko.moe](https://n.eko.moe/) and I love the logo /shrug
|
||||
|
||||
### Super easy mode setup
|
||||
1. Head on to [Digital Ocean](https://digitalocean.com/) and create an account
|
||||
2. Go [here](https://marketplace.digitalocean.com/apps/docker) and click on "Create Docker Droplet"
|
||||
3. Configure the droplet:
|
||||
* **576p** [$15/mo] Not Recommended
|
||||
* **720p** [$40/mo] Good Performance
|
||||
* **720p** [$80/mo] Recommended
|
||||
* **720p+** [$160/mo] Best Performance
|
||||
4. [Login to the droplet over ssh](https://www.digitalocean.com/docs/droplets/how-to/connect-with-ssh/)
|
||||
5. Run these commands:
|
||||
1. Deploy a server
|
||||
|
||||
*Recomended specs:*
|
||||
| Resolution | Cores | Ram | Recommendation |
|
||||
|------------|-------|-----|------------------|
|
||||
| **576p** | 2 | 2gb | Not Recommended |
|
||||
| **720p** | 4 | 4gb | Good Performance |
|
||||
| **720p** | 6 | 6gb | Recommended |
|
||||
| **720p+** | 8 | 8gb | Best Performance |
|
||||
|
||||
2. [SSH into your VPS](https://www.digitalocean.com/docs/droplets/how-to/connect-with-ssh/)
|
||||
|
||||
3. Install Docker
|
||||
```
|
||||
curl -sSL https://get.docker.com/ | CHANNEL=stable bash
|
||||
```
|
||||
4. Run these commands:
|
||||
```
|
||||
ufw allow 80/tcp
|
||||
wget https://raw.githubusercontent.com/nurdism/neko/master/docker-compose.yaml
|
||||
@ -33,14 +41,12 @@ I like cats (Neko is the Japanese word for cat), I'm a weeb/nerd, I own the doma
|
||||
|
||||
> *Protip*: Run `nano docker-compose.yaml` to edit the settings, then press *ctrl+x* to exit and save the file.
|
||||
|
||||
Heres the cool part, this will only cost you a little bit (maybe a few cents), *as long as you remember to delete the droplet after you are done!* Droplets are charged per hour, so when you want to share, just create a new droplet and start sharing.
|
||||
|
||||
### Running the container:
|
||||
```
|
||||
sudo docker run -p 8080:8080 -e NEKO_PASSWORD='secret' --shm-size=2gb nurdism/neko:latest
|
||||
sudo docker run -p 8080:8080 -e NEKO_PASSWORD='secret' --shm-size=1gb nurdism/neko:latest
|
||||
```
|
||||
|
||||
*Note:* `--shm-size=2gb` is required, firefox-esr tabs will crash (not sure if 2gb is *really* needed)
|
||||
*Note:* `--shm-size=1gb` is required, firefox-esr tabs will crash
|
||||
|
||||
### Config
|
||||
```
|
||||
@ -57,14 +63,3 @@ NEKO_CERT= // (SSL)Cert
|
||||
### Development
|
||||
*Highly* recommend you use a [dev container](https://code.visualstudio.com/docs/remote/containers) for [vscode](https://code.visualstudio.com/), I've included the `.devcontainer` I've used to develop this app. To build neko run:
|
||||
`cd .docker && ./build`
|
||||
|
||||
### Goals
|
||||
* Remove need for gstreamer, handle encoding/capturing with go
|
||||
* Make firefox work with client (wrtc issues)
|
||||
* Remove the need for supervisor and handle starting/stopping the applications
|
||||
* Find ouw witch audio/video codec is the cheapest as far as data and performance goes (with minimal quality loss)
|
||||
* Slim down the [robotgo](github.com/go-vgo/robotgo) package and *just* handle the key io
|
||||
* Add user names for the client
|
||||
* Add (text) chat for the client
|
||||
* Add that cool emoji thing rabb.it had
|
||||
* Slim down the docker container (use alpine)
|
||||
|
@ -11,6 +11,7 @@
|
||||
"no-case-declarations": "off",
|
||||
"no-dupe-class-members": "off",
|
||||
"no-console": "off",
|
||||
"no-empty": "off"
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -379,13 +379,11 @@
|
||||
<script lang="ts">
|
||||
import { Component, Ref, Watch, Vue } from 'vue-property-decorator'
|
||||
|
||||
const MOUSE_MOVE = 0x01
|
||||
const MOUSE_UP = 0x02
|
||||
const MOUSE_DOWN = 0x03
|
||||
const MOUSE_CLK = 0x04
|
||||
const KEY_DOWN = 0x05
|
||||
const KEY_UP = 0x06
|
||||
const KEY_CLK = 0x07
|
||||
const OP_MOVE = 0x01
|
||||
const OP_SCROLL = 0x02
|
||||
const OP_KEY_DOWN = 0x03
|
||||
const OP_KEY_UP = 0x04
|
||||
// const OP_KEY_CLK = 0x05
|
||||
|
||||
@Component({ name: 'stream-video' })
|
||||
export default class extends Vue {
|
||||
@ -429,6 +427,7 @@
|
||||
|
||||
beforeDestroy() {
|
||||
window.removeEventListener('resize', this.onResise)
|
||||
this.onClose()
|
||||
}
|
||||
|
||||
toggleControl() {
|
||||
@ -443,6 +442,7 @@
|
||||
}
|
||||
|
||||
toggleMedia() {
|
||||
console.log(`[NEKO] toggleMedia`, this.playing)
|
||||
if (!this.playing) {
|
||||
this._player
|
||||
.play()
|
||||
@ -479,8 +479,8 @@
|
||||
this.ws.onmessage = this.onMessage.bind(this)
|
||||
this.ws.onerror = event => console.error((event as ErrorEvent).error)
|
||||
this.ws.onclose = event => this.onClose.bind(this)
|
||||
this.onConnecting()
|
||||
this.timeout = setTimeout(this.onTimeout.bind(this), 5000)
|
||||
this.onConnecting()
|
||||
}
|
||||
|
||||
createPeer() {
|
||||
@ -488,7 +488,10 @@
|
||||
return
|
||||
}
|
||||
|
||||
this.peer = new RTCPeerConnection({ iceServers: [{ urls: 'stun:stun.l.google.com:19302' }] })
|
||||
this.peer = new RTCPeerConnection({
|
||||
iceServers: [{ urls: 'stun:stun.l.google.com:19302' }, { urls: 'stun:stun.services.mozilla.com' }],
|
||||
})
|
||||
|
||||
this.peer.onicecandidate = event => {
|
||||
if (event.candidate === null && this.peer!.localDescription) {
|
||||
this.ws!.send(
|
||||
@ -512,6 +515,7 @@
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
this.peer.ontrack = this.onTrack.bind(this)
|
||||
this.peer.addTransceiver('audio', { direction: 'recvonly' })
|
||||
this.peer.addTransceiver('video', { direction: 'recvonly' })
|
||||
@ -538,52 +542,33 @@
|
||||
case 'mousemove':
|
||||
buffer = new ArrayBuffer(7)
|
||||
payload = new DataView(buffer)
|
||||
payload.setUint8(0, MOUSE_MOVE)
|
||||
payload.setUint8(0, OP_MOVE)
|
||||
payload.setUint16(1, 4, true)
|
||||
payload.setUint16(3, Math.round((this.width / data.rect.width) * (data.x - data.rect.left)), true)
|
||||
payload.setUint16(5, Math.round((this.height / data.rect.height) * (data.y - data.rect.top)), true)
|
||||
break
|
||||
case 'wheel':
|
||||
buffer = new ArrayBuffer(4)
|
||||
buffer = new ArrayBuffer(7)
|
||||
payload = new DataView(buffer)
|
||||
payload.setUint8(0, MOUSE_CLK)
|
||||
payload.setUint16(1, 1, true)
|
||||
|
||||
const ydir = Math.sign(data.y)
|
||||
const xdir = Math.sign(data.x)
|
||||
|
||||
if ((!xdir && !ydir) || (xdir && ydir)) return
|
||||
if (ydir && ydir < 0) payload.setUint8(3, 4)
|
||||
if (ydir && ydir > 0) payload.setUint8(3, 5)
|
||||
if (xdir && xdir < 0) payload.setUint8(3, 6)
|
||||
if (xdir && xdir > 0) payload.setUint8(3, 7)
|
||||
break
|
||||
case 'mousedown':
|
||||
buffer = new ArrayBuffer(4)
|
||||
payload = new DataView(buffer)
|
||||
payload.setUint8(0, MOUSE_DOWN)
|
||||
payload.setUint16(1, 1, true)
|
||||
payload.setUint8(3, data.key)
|
||||
break
|
||||
case 'mouseup':
|
||||
buffer = new ArrayBuffer(4)
|
||||
payload = new DataView(buffer)
|
||||
payload.setUint8(0, MOUSE_UP)
|
||||
payload.setUint16(1, 1, true)
|
||||
payload.setUint8(3, data.key)
|
||||
payload.setUint8(0, OP_SCROLL)
|
||||
payload.setUint16(1, 4, true)
|
||||
payload.setInt16(3, (data.x * -1) / 10, true)
|
||||
payload.setInt16(5, (data.y * -1) / 10, true)
|
||||
break
|
||||
case 'keydown':
|
||||
case 'mousedown':
|
||||
buffer = new ArrayBuffer(5)
|
||||
payload = new DataView(buffer)
|
||||
payload.setUint8(0, KEY_DOWN)
|
||||
payload.setUint16(1, 2, true)
|
||||
payload.setUint8(0, OP_KEY_DOWN)
|
||||
payload.setUint16(1, 1, true)
|
||||
payload.setUint16(3, data.key, true)
|
||||
break
|
||||
case 'keyup':
|
||||
case 'mouseup':
|
||||
buffer = new ArrayBuffer(5)
|
||||
payload = new DataView(buffer)
|
||||
payload.setUint8(0, KEY_UP)
|
||||
payload.setUint16(1, 2, true)
|
||||
payload.setUint8(0, OP_KEY_UP)
|
||||
payload.setUint16(1, 1, true)
|
||||
payload.setUint16(3, data.key, true)
|
||||
break
|
||||
}
|
||||
@ -645,16 +630,19 @@
|
||||
onWheel(e: WheelEvent) {
|
||||
this.onMousePos(e)
|
||||
this.updateControles('wheel', { x: e.deltaX, y: e.deltaY })
|
||||
console.log('wheel', { x: e.deltaX, y: e.deltaY })
|
||||
}
|
||||
|
||||
onMouseDown(e: MouseEvent) {
|
||||
this.onMousePos(e)
|
||||
this.updateControles('mousedown', { key: e.button })
|
||||
console.log('mousedown', { key: e.button })
|
||||
}
|
||||
|
||||
onMouseUp(e: MouseEvent) {
|
||||
this.onMousePos(e)
|
||||
this.updateControles('mouseup', { key: e.button })
|
||||
console.log('mouseup', { key: e.button })
|
||||
}
|
||||
|
||||
onMouseMove(e: MouseEvent) {
|
||||
@ -675,6 +663,7 @@
|
||||
return
|
||||
}
|
||||
this.updateControles('keydown', { key: e.keyCode })
|
||||
console.log('keydown', { key: e.keyCode })
|
||||
}
|
||||
|
||||
onKeyUp(e: KeyboardEvent) {
|
||||
@ -682,6 +671,7 @@
|
||||
return
|
||||
}
|
||||
this.updateControles('keyup', { key: e.keyCode })
|
||||
console.log('keyup', { key: e.keyCode })
|
||||
}
|
||||
|
||||
onResise() {
|
||||
@ -769,7 +759,7 @@
|
||||
})
|
||||
break
|
||||
default:
|
||||
console.warn(`[NEKO] Unknown message event ${event}`)
|
||||
console.warn(`[NEKO] unknown message event ${event}`)
|
||||
}
|
||||
}
|
||||
|
||||
@ -778,6 +768,8 @@
|
||||
return
|
||||
}
|
||||
|
||||
console.log(`[NEKO] track recieved`, event)
|
||||
|
||||
this.stream = event.streams[0]
|
||||
if (!this.stream) {
|
||||
return
|
||||
@ -830,11 +822,25 @@
|
||||
this.controlling = false
|
||||
this.connected = false
|
||||
this.connecting = false
|
||||
this.ws = undefined
|
||||
this.peer = undefined
|
||||
|
||||
if (this.ws) {
|
||||
try {
|
||||
this.ws.close()
|
||||
} catch (err) {}
|
||||
this.ws = undefined
|
||||
}
|
||||
|
||||
if (this.peer) {
|
||||
try {
|
||||
this.peer.close()
|
||||
} catch (err) {}
|
||||
this.peer = undefined
|
||||
}
|
||||
|
||||
if (this.playing) {
|
||||
this.toggleMedia()
|
||||
}
|
||||
|
||||
this.$notify({
|
||||
group: 'neko',
|
||||
type: 'error',
|
||||
|
@ -42,6 +42,11 @@
|
||||
"gruntfuggly.todo-tree",
|
||||
"swyphcosmo.spellchecker",
|
||||
"eamodio.gitlens"
|
||||
]
|
||||
],
|
||||
"files.associations": {
|
||||
"iostream": "cpp",
|
||||
"xtest.h": "c",
|
||||
"xlib.h": "c"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,2 +1,3 @@
|
||||
DISPLAY=:0
|
||||
PULSE_SERVER=unix:/tmp/pulseaudio.socket
|
||||
GST_PLUGIN_PATH=/usr/lib/x86_64-linux-gnu/gstreamer-1.0/
|
||||
|
2
server/.vscode/launch.json
vendored
2
server/.vscode/launch.json
vendored
@ -10,7 +10,7 @@
|
||||
"envFile": "${workspaceFolder}/.env.development",
|
||||
"output": "${workspaceFolder}/bin/debug/neko",
|
||||
"cwd": "${workspaceFolder}/",
|
||||
"args": ["serve", "-d", "--bind", ":3000", "--static", "../client/dist", "--password", "123"]
|
||||
"args": ["serve", "-d", "--bind", ":3000", "--static", "../client/dist", "--password", "neko", "--admin", "admin"]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -6,4 +6,4 @@ GIT_DIRTY=`git diff-index --quiet HEAD -- || echo "✗-"`
|
||||
LDFLAGS=-ldflags "-s -X version.buildTime=${BUILD_TIME} -X version.gitRevision=${GIT_DIRTY}${GIT_REVISION} -X version.gitBranch=${GIT_BRANCH}"
|
||||
|
||||
build:
|
||||
go build -o bin/neko ${LDFLAGS} -i cmd/neko/main.go
|
||||
go build -o bin/neko ${LDFLAGS} -i cmd/neko/main.go
|
||||
|
@ -3,16 +3,16 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"n.eko.moe/neko"
|
||||
"n.eko.moe/neko/cmd"
|
||||
"n.eko.moe/neko/internal/utils"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
func main() {
|
||||
fmt.Print(utils.Colorf(utils.Header, "server", neko.Service.Version))
|
||||
fmt.Print(utils.Colorf(neko.Header, "server", neko.Service.Version))
|
||||
if err := cmd.Execute(); err != nil {
|
||||
log.Panic().Err(err).Msg("Failed to execute command")
|
||||
log.Panic().Err(err).Msg("failed to execute command")
|
||||
}
|
||||
}
|
||||
|
@ -3,10 +3,11 @@ package cmd
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"n.eko.moe/neko"
|
||||
"n.eko.moe/neko/internal/preflight"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func Execute() error {
|
||||
@ -15,8 +16,8 @@ func Execute() error {
|
||||
|
||||
var root = &cobra.Command{
|
||||
Use: "neko",
|
||||
Short: "",
|
||||
Long: ``,
|
||||
Short: "neko streaming server",
|
||||
Long: `neko streaming server`,
|
||||
Version: neko.Service.Version.String(),
|
||||
}
|
||||
|
||||
@ -28,8 +29,8 @@ func init() {
|
||||
})
|
||||
|
||||
if err := neko.Service.Root.Init(root); err != nil {
|
||||
neko.Service.Logger.Panic().Err(err).Msg("Unable to run command")
|
||||
log.Panic().Err(err).Msg("unable to run root command")
|
||||
}
|
||||
|
||||
root.SetVersionTemplate(fmt.Sprintf("Version: %s\n", neko.Service.Version))
|
||||
root.SetVersionTemplate(fmt.Sprintf("version: %s\n", neko.Service.Version))
|
||||
}
|
||||
|
@ -11,13 +11,15 @@ import (
|
||||
func init() {
|
||||
command := &cobra.Command{
|
||||
Use: "serve",
|
||||
Short: "",
|
||||
Long: ``,
|
||||
Short: "serve neko streaming server",
|
||||
Long: `serve neko streaming server`,
|
||||
Run: neko.Service.ServeCommand,
|
||||
}
|
||||
|
||||
configs := []config.Config{
|
||||
neko.Service.Serve,
|
||||
neko.Service.Server,
|
||||
neko.Service.WebRTC,
|
||||
neko.Service.WebSocket,
|
||||
}
|
||||
|
||||
cobra.OnInitialize(func() {
|
||||
@ -29,7 +31,7 @@ func init() {
|
||||
|
||||
for _, cfg := range configs {
|
||||
if err := cfg.Init(command); err != nil {
|
||||
log.Panic().Err(err).Msg("Unable to run command")
|
||||
log.Panic().Err(err).Msg("unable to run serve command")
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,9 +4,10 @@ go 1.13
|
||||
|
||||
require (
|
||||
github.com/go-chi/chi v4.0.3+incompatible
|
||||
github.com/go-vgo/robotgo v0.0.0-20200111145433-6e6028a14d57
|
||||
github.com/gorilla/websocket v1.4.1
|
||||
github.com/kataras/go-events v0.0.2
|
||||
github.com/matoous/go-nanoid v1.1.0
|
||||
github.com/pion/logging v0.2.2
|
||||
github.com/pion/webrtc/v2 v2.1.18
|
||||
github.com/pkg/errors v0.8.1
|
||||
github.com/rs/zerolog v1.17.2
|
||||
|
@ -1,13 +1,7 @@
|
||||
bou.ke/monkey v1.0.1/go.mod h1:FgHuK96Rv2Nlf+0u1OOVDpCMdsWyOFmeeketDHE7LIg=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ=
|
||||
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0=
|
||||
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
|
||||
github.com/StackExchange/wmi v0.0.0-20180116203802-5d049714c4a6/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d h1:G0m3OIz70MZUWq3EgK3CesDbo8upS2Vm9/P3FtgI+Jk=
|
||||
github.com/StackExchange/wmi v0.0.0-20190523213315-cbe66965904d/go.mod h1:3eOhrUMpNV+6aFIbp5/iudMxNCF27Vw2OZgy4xEx0Fg=
|
||||
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
|
||||
github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
@ -38,12 +32,7 @@ github.com/go-chi/chi v4.0.3+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxm
|
||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
||||
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-ole/go-ole v1.2.1/go.mod h1:7FAglXiTm7HKlQRDeOQ6ZNUHidzCWXuZWq/1dTyBNF8=
|
||||
github.com/go-ole/go-ole v1.2.4 h1:nNBDSCOigTSiarFpYE9J/KtEA1IOW4CNeqT9TQDqCxI=
|
||||
github.com/go-ole/go-ole v1.2.4/go.mod h1:XCwSNxSkXRo4vlyPy93sltvi/qJq0jqQhjqQNIwKuxM=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-vgo/robotgo v0.0.0-20200111145433-6e6028a14d57 h1:0BtenaNSwWghFTsHDQGTdurEdCMm7lsNmWfMoBS2JiY=
|
||||
github.com/go-vgo/robotgo v0.0.0-20200111145433-6e6028a14d57/go.mod h1:P6/F9lmSF2Z/74P/m80qEm6ApjE5HmB+rSzfBCNqIPo=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
@ -74,6 +63,8 @@ github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22
|
||||
github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kataras/go-events v0.0.2 h1:fhyUPXvUbrjIPmH4vRdrAAGoNzdcwJPQmjhg47m1nMU=
|
||||
github.com/kataras/go-events v0.0.2/go.mod h1:6IxMW59VJdEIqj3bjFGJvGLRdb0WHtrlxPZy9qXctcg=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
@ -85,8 +76,6 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9 h1:tbuodUh2vuhOVZAdW3NEUvosFHUMJwUNl7jk/VSEiwc=
|
||||
github.com/lucas-clemente/quic-go v0.7.1-0.20190401152353-907071221cf9/go.mod h1:PpMmPfPKO9nKJ/psF49ESTAGQSdfXxlg1otPbEB2nOw=
|
||||
github.com/lxn/win v0.0.0-20191024121223-cc00c7492fe1 h1:h0wbuSK8xUNmMwDdCxZx2OLdkVck6Bb31zj4CxCN5I4=
|
||||
github.com/lxn/win v0.0.0-20191024121223-cc00c7492fe1/go.mod h1:ouWl4wViUNh8tPSIwxTVMuS014WakR1hqvBc2I0bMoA=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
@ -106,11 +95,6 @@ github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE=
|
||||
github.com/otiai10/curr v0.0.0-20190513014714-f5a3d24e5776/go.mod h1:3HNVkVOU7vZeFXocWuvtcS0XSFLcf2XUSDHkq9t1jU4=
|
||||
github.com/otiai10/gosseract v2.2.1+incompatible h1:Ry5ltVdpdp4LAa2bMjsSJH34XHVOV7XMi41HtzL8X2I=
|
||||
github.com/otiai10/gosseract v2.2.1+incompatible/go.mod h1:XrzWItCzCpFRZ35n3YtVTgq5bLAhFIkascoRo8G32QE=
|
||||
github.com/otiai10/mint v1.2.4/go.mod h1:d+b7n/0R3tdyUYYylALXpWQ/kTN+QobSq/4SRGBkR3M=
|
||||
github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/pion/datachannel v1.4.13 h1:ezTn3AtUtXvKemRRjRdUgao/T8bH4ZJwrpOqU8Iz3Ss=
|
||||
@ -165,21 +149,11 @@ github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084 h1:sofwID9zm4tzrgykg80hfFph1mryUeLRsUfoocVVmRY=
|
||||
github.com/prometheus/procfs v0.0.0-20190507164030-5867b95ac084/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
||||
github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU=
|
||||
github.com/robotn/gohook v0.0.0-20191208195706-98eb507a75d9 h1:7vKLsPiS3XTrejZHaoKDS3/26K3t10rAtuf1+fRfIVA=
|
||||
github.com/robotn/gohook v0.0.0-20191208195706-98eb507a75d9/go.mod h1:n1o8s7fg6QGcgIsN9AmWQnBi6KrcbEUX0kFVUwTP53g=
|
||||
github.com/robotn/xgb v0.0.0-20190912153532-2cb92d044934 h1:2lhSR8N3T6I30q096DT7/5AKEIcf1vvnnWAmS0wfnNY=
|
||||
github.com/robotn/xgb v0.0.0-20190912153532-2cb92d044934/go.mod h1:SxQhJskUJ4rleVU44YvnrdvxQr0tKy5SRSigBrCgyyQ=
|
||||
github.com/robotn/xgbutil v0.0.0-20190912154524-c861d6f87770 h1:2uX8QRLkkxn2EpAQ6I3KhA79BkdRZfvugJUzJadiJwk=
|
||||
github.com/robotn/xgbutil v0.0.0-20190912154524-c861d6f87770/go.mod h1:svkDXUDQjUiWzLrA0OZgHc4lbOts3C+uRfP6/yjwYnU=
|
||||
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.17.2 h1:RMRHFw2+wF7LO0QqtELQwo8hqSmqISyCJeFeAAuWcRo=
|
||||
github.com/rs/zerolog v1.17.2/go.mod h1:9nvC1axdVrAHcu/s9taAVfBuIdTZLVQmKQyvrUjF5+I=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/shirou/gopsutil v2.19.6+incompatible h1:49/Gru26Lne9Cl3IoAVDZVM09hvkSrUodgIIsCVRwbs=
|
||||
github.com/shirou/gopsutil v2.19.6+incompatible/go.mod h1:WWnYX4lzhCH5h/3YBfyVA3VbLYjlMZZAQcW9ojMexNc=
|
||||
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4 h1:udFKJ0aHUL60LboW/A+DfgoHVedieIzIXE8uylPue0U=
|
||||
github.com/shirou/w32 v0.0.0-20160930032740-bb4de0191aa4/go.mod h1:qsXQc7+bwAM3Q1u/4XEfrquwF8Lw7D7y5cD8CuHnfIc=
|
||||
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d h1:zE9ykElWQ6/NYmHa3jpm/yHnI4xSofP+UP6SpjHcSeM=
|
||||
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||
@ -211,12 +185,6 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69
|
||||
github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U=
|
||||
github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/vcaesar/gops v0.0.0-20190910162627-58ac09d12a53 h1:tYb/9KQi8dTCSDia2NwbuhUbKlaurqC/S7MFQo96nLk=
|
||||
github.com/vcaesar/gops v0.0.0-20190910162627-58ac09d12a53/go.mod h1:5txYrXKrQG6ZJYdGIiMVVxiOhbdACnwBcHzIwGQ7Nkw=
|
||||
github.com/vcaesar/imgo v0.0.0-20191008162304-a83ea7753bc8 h1:9Y+hoKBYa+UtzGqkODfs8c0Q6gp2UfniVNsHQWghPi0=
|
||||
github.com/vcaesar/imgo v0.0.0-20191008162304-a83ea7753bc8/go.mod h1:52+3yYrTNjWKh+CkQozNRCLWCE/X666yAWPGbYC3DZI=
|
||||
github.com/vcaesar/tt v0.0.0-20191103173835-6896a351024b h1:psGhQitWSo4KBpLghvJPlhHxTJ8LQl1y0ekjSreqvu4=
|
||||
github.com/vcaesar/tt v0.0.0-20191103173835-6896a351024b/go.mod h1:GHPxQYhn+7OgKakRusH7KJ0M5MhywoeLb8Fcffs/Gtg=
|
||||
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
|
||||
@ -231,9 +199,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
|
||||
golang.org/x/crypto v0.0.0-20191029031824-8986dd9e96cf/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413 h1:ULYEB3JvPRE/IfO+9uO7vKV/xzVTO7XPAwm8xbf4w2g=
|
||||
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8 h1:hVwzHzIUGRjiF7EcUjqNxk3NCfkPxbDKRdnNE1Rpg0U=
|
||||
golang.org/x/image v0.0.0-20191009234506-e7c1f5e7dbb8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@ -244,7 +209,6 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191126235420-ef20fe5d7933/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553 h1:efeOvDhwQ29Dj3SdAV/MJf8oukgn+8D8WgaCaRMchF8=
|
||||
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
@ -262,9 +226,6 @@ golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5h
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190228124157-a34e9553db1e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190910064555-bbd175535a8b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191029155521-f43be2a4598c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab h1:FvshnhkKW+LO3HWHodML8kuVX8rnJTxKm9dFPuI68UM=
|
||||
golang.org/x/sys v0.0.0-20191206220618-eeba5f6aabab/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
|
@ -5,4 +5,4 @@ import "github.com/spf13/cobra"
|
||||
type Config interface {
|
||||
Init(cmd *cobra.Command) error
|
||||
Set()
|
||||
}
|
||||
}
|
||||
|
@ -5,16 +5,14 @@ import (
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type Serve struct {
|
||||
Cert string
|
||||
Key string
|
||||
Bind string
|
||||
Password string
|
||||
type Server struct {
|
||||
Cert string
|
||||
Key string
|
||||
Bind string
|
||||
Static string
|
||||
}
|
||||
|
||||
func (Serve) Init(cmd *cobra.Command) error {
|
||||
|
||||
func (Server) Init(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().String("bind", "127.0.0.1:8080", "Address/port/socket to serve neko")
|
||||
if err := viper.BindPFlag("bind", cmd.PersistentFlags().Lookup("bind")); err != nil {
|
||||
return err
|
||||
@ -30,12 +28,7 @@ func (Serve) Init(cmd *cobra.Command) error {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("password", "neko", "Password for connecting to stream")
|
||||
if err := viper.BindPFlag("password", cmd.PersistentFlags().Lookup("password")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("static", "./www", "Static files to serve")
|
||||
cmd.PersistentFlags().String("static", "./www", "Neko client files to serve")
|
||||
if err := viper.BindPFlag("static", cmd.PersistentFlags().Lookup("static")); err != nil {
|
||||
return err
|
||||
}
|
||||
@ -43,10 +36,9 @@ func (Serve) Init(cmd *cobra.Command) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Serve) Set() {
|
||||
func (s *Server) Set() {
|
||||
s.Cert = viper.GetString("cert")
|
||||
s.Key = viper.GetString("key")
|
||||
s.Bind = viper.GetString("bind")
|
||||
s.Password = viper.GetString("password")
|
||||
s.Static = viper.GetString("static")
|
||||
}
|
60
server/internal/config/webrtc.go
Normal file
60
server/internal/config/webrtc.go
Normal file
@ -0,0 +1,60 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type WebRTC struct {
|
||||
Device string
|
||||
AudioCodec string
|
||||
AudioParams string
|
||||
Display string
|
||||
VideoCodec string
|
||||
VideoParams string
|
||||
}
|
||||
|
||||
func (WebRTC) Init(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().String("device", "auto_null.monitor", "Audio device to capture")
|
||||
if err := viper.BindPFlag("device", cmd.PersistentFlags().Lookup("device")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("ac", "opus", "Audio codec to use for streaming")
|
||||
if err := viper.BindPFlag("acodec", cmd.PersistentFlags().Lookup("ac")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("ap", "", "Audio codec parameters to use for streaming")
|
||||
if err := viper.BindPFlag("aparams", cmd.PersistentFlags().Lookup("ap")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("display", ":0.0", "XDisplay to capture")
|
||||
if err := viper.BindPFlag("display", cmd.PersistentFlags().Lookup("display")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("vc", "vp8", "Video codec to use for streaming")
|
||||
if err := viper.BindPFlag("vcodec", cmd.PersistentFlags().Lookup("vc")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("vp", "", "Video codec parameters to use for streaming")
|
||||
if err := viper.BindPFlag("vparams", cmd.PersistentFlags().Lookup("vp")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *WebRTC) Set() {
|
||||
s.Device = strings.ToLower(viper.GetString("device"))
|
||||
s.AudioCodec = strings.ToLower(viper.GetString("acodec"))
|
||||
s.AudioParams = strings.ToLower(viper.GetString("aparams"))
|
||||
s.Display = strings.ToLower(viper.GetString("display"))
|
||||
s.VideoCodec = strings.ToLower(viper.GetString("vcodec"))
|
||||
s.VideoParams = strings.ToLower(viper.GetString("vparams"))
|
||||
}
|
30
server/internal/config/websocket.go
Normal file
30
server/internal/config/websocket.go
Normal file
@ -0,0 +1,30 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
type WebSocket struct {
|
||||
Password string
|
||||
AdminPassword string
|
||||
}
|
||||
|
||||
func (WebSocket) Init(cmd *cobra.Command) error {
|
||||
cmd.PersistentFlags().String("password", "neko", "Password for connecting to stream")
|
||||
if err := viper.BindPFlag("password", cmd.PersistentFlags().Lookup("password")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cmd.PersistentFlags().String("admin", "admin", "Admin password for connecting to stream")
|
||||
if err := viper.BindPFlag("admin", cmd.PersistentFlags().Lookup("admin")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *WebSocket) Set() {
|
||||
s.Password = viper.GetString("password")
|
||||
s.AdminPassword = viper.GetString("admin")
|
||||
}
|
15
server/internal/event/events.go
Normal file
15
server/internal/event/events.go
Normal file
@ -0,0 +1,15 @@
|
||||
package event
|
||||
|
||||
const SDP_REPLY = "sdp/reply"
|
||||
const SDP_PROVIDE = "sdp/provide"
|
||||
|
||||
const CONTROL_RELEASE = "control/release"
|
||||
const CONTROL_RELEASED = "control/released"
|
||||
const CONTROL_REQUEST = "control/request"
|
||||
const CONTROL_GIVE = "control/give"
|
||||
const CONTROL_GIVEN = "control/given"
|
||||
const CONTROL_LOCKED = "control/locked"
|
||||
const CONTROL_REQUESTING = "control/requesting"
|
||||
|
||||
const IDENTITY_PROVIDE = "identity/provide"
|
||||
const IDENTITY_NAME = "identity/name"
|
@ -6,6 +6,10 @@ typedef struct SampleHandlerUserData {
|
||||
int pipelineId;
|
||||
} SampleHandlerUserData;
|
||||
|
||||
void gstreamer_init(void) {
|
||||
gst_init(NULL, NULL);
|
||||
}
|
||||
|
||||
GMainLoop *gstreamer_send_main_loop = NULL;
|
||||
void gstreamer_send_start_mainloop(void) {
|
||||
gstreamer_send_main_loop = g_main_loop_new(NULL, FALSE);
|
||||
@ -60,7 +64,6 @@ GstFlowReturn gstreamer_send_new_sample_handler(GstElement *object, gpointer use
|
||||
}
|
||||
|
||||
GstElement *gstreamer_send_create_pipeline(char *pipeline) {
|
||||
gst_init(NULL, NULL);
|
||||
GError *error = NULL;
|
||||
return gst_parse_launch(pipeline, &error);
|
||||
}
|
||||
|
@ -17,9 +17,20 @@ import (
|
||||
"github.com/pion/webrtc/v2/pkg/media"
|
||||
)
|
||||
|
||||
func init() {
|
||||
go C.gstreamer_send_start_mainloop()
|
||||
}
|
||||
/*
|
||||
apt-get install \
|
||||
libgstreamer1.0-0 \
|
||||
gstreamer1.0-plugins-base \
|
||||
gstreamer1.0-plugins-good \
|
||||
gstreamer1.0-plugins-bad \
|
||||
gstreamer1.0-plugins-ugly\
|
||||
gstreamer1.0-libav \
|
||||
gstreamer1.0-doc \
|
||||
gstreamer1.0-tools \
|
||||
gstreamer1.0-x \
|
||||
gstreamer1.0-alsa \
|
||||
gstreamer1.0-pulseaudio
|
||||
*/
|
||||
|
||||
// Pipeline is a wrapper for a GStreamer Pipeline
|
||||
type Pipeline struct {
|
||||
@ -32,6 +43,7 @@ type Pipeline struct {
|
||||
|
||||
var pipelines = make(map[int]*Pipeline)
|
||||
var pipelinesLock sync.Mutex
|
||||
var registry *C.GstRegistry
|
||||
|
||||
const (
|
||||
videoClockRate = 90000
|
||||
@ -39,6 +51,11 @@ const (
|
||||
pcmClockRate = 8000
|
||||
)
|
||||
|
||||
func init() {
|
||||
C.gstreamer_init()
|
||||
registry = C.gst_registry_get()
|
||||
}
|
||||
|
||||
// CreatePipeline creates a GStreamer Pipeline
|
||||
func CreatePipeline(codecName string, tracks []*webrtc.Track, pipelineSrc string) *Pipeline {
|
||||
pipelineStr := "appsink name=appsink"
|
||||
@ -46,33 +63,97 @@ func CreatePipeline(codecName string, tracks []*webrtc.Track, pipelineSrc string
|
||||
|
||||
switch codecName {
|
||||
case webrtc.VP8:
|
||||
// https://gstreamer.freedesktop.org/documentation/vpx/vp8enc.html?gi-language=c
|
||||
// gstreamer1.0-plugins-good
|
||||
// vp8enc error-resilient=partitions keyframe-max-dist=10 auto-alt-ref=true cpu-used=5 deadline=1
|
||||
pipelineStr = pipelineSrc + " ! vp8enc error-resilient=partitions keyframe-max-dist=10 auto-alt-ref=true cpu-used=5 deadline=1 ! " + pipelineStr
|
||||
clockRate = videoClockRate
|
||||
|
||||
if err := CheckPlugins([]string{"ximagesrc", "vpx"}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
case webrtc.VP9:
|
||||
// https://gstreamer.freedesktop.org/documentation/vpx/vp9enc.html?gi-language=c
|
||||
// gstreamer1.0-plugins-good
|
||||
// vp9enc
|
||||
|
||||
// Causes panic!
|
||||
pipelineStr = pipelineSrc + " ! vp9enc ! " + pipelineStr
|
||||
clockRate = videoClockRate
|
||||
|
||||
if err := CheckPlugins([]string{"ximagesrc", "vpx"}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
case webrtc.H264:
|
||||
pipelineStr = pipelineSrc + " ! video/x-raw,format=I420 ! x264enc bframes=0 speed-preset=veryfast key-int-max=60 ! video/x-h264,stream-format=byte-stream ! " + pipelineStr
|
||||
// https://gstreamer.freedesktop.org/documentation/x264/index.html?gi-language=c
|
||||
// gstreamer1.0-plugins-ugly
|
||||
// video/x-raw,format=I420 ! x264enc bframes=0 key-int-max=60 byte-stream=true tune=zerolatency speed-preset=veryfast ! video/x-h264,stream-format=byte-stream
|
||||
|
||||
// https://gstreamer.freedesktop.org/documentation/openh264/openh264enc.html?gi-language=c#openh264enc
|
||||
// gstreamer1.0-plugins-bad
|
||||
// openh264enc multi-thread=4 complexity=high bitrate=3072000 max-bitrate=4096000
|
||||
pipelineStr = pipelineSrc + " ! openh264enc multi-thread=4 complexity=high bitrate=3072000 max-bitrate=4096000 ! video/x-h264,stream-format=byte-stream ! " + pipelineStr
|
||||
clockRate = videoClockRate
|
||||
|
||||
if err := CheckPlugins([]string{"ximagesrc"}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if err := CheckPlugins([]string{"openh264"}); err != nil {
|
||||
pipelineStr = pipelineSrc + " ! video/x-raw,format=I420 ! x264enc bframes=0 key-int-max=60 byte-stream=true tune=zerolatency speed-preset=veryfast ! video/x-h264,stream-format=byte-stream ! " + pipelineStr
|
||||
|
||||
if err := CheckPlugins([]string{"x264"}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
case webrtc.Opus:
|
||||
// https://gstreamer.freedesktop.org/documentation/opus/opusenc.html
|
||||
// gstreamer1.0-plugins-base
|
||||
// opusenc
|
||||
pipelineStr = pipelineSrc + " ! opusenc ! " + pipelineStr
|
||||
clockRate = audioClockRate
|
||||
|
||||
if err := CheckPlugins([]string{"pulseaudio", "opus"}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
case webrtc.G722:
|
||||
// https://gstreamer.freedesktop.org/documentation/libav/avenc_g722.html?gi-language=c
|
||||
// gstreamer1.0-libav
|
||||
// avenc_g722
|
||||
pipelineStr = pipelineSrc + " ! avenc_g722 ! " + pipelineStr
|
||||
clockRate = audioClockRate
|
||||
|
||||
if err := CheckPlugins([]string{"pulseaudio", "libav"}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
case webrtc.PCMU:
|
||||
// https://gstreamer.freedesktop.org/documentation/mulaw/mulawenc.html?gi-language=c
|
||||
// gstreamer1.0-plugins-good
|
||||
// audio/x-raw, rate=8000 ! mulawenc
|
||||
|
||||
pipelineStr = pipelineSrc + " ! audio/x-raw, rate=8000 ! mulawenc ! " + pipelineStr
|
||||
clockRate = pcmClockRate
|
||||
|
||||
if err := CheckPlugins([]string{"pulseaudio", "mulaw"}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
case webrtc.PCMA:
|
||||
// https://gstreamer.freedesktop.org/documentation/alaw/alawenc.html?gi-language=c
|
||||
// gstreamer1.0-plugins-good
|
||||
// audio/x-raw, rate=8000 ! alawenc
|
||||
pipelineStr = pipelineSrc + " ! audio/x-raw, rate=8000 ! alawenc ! " + pipelineStr
|
||||
clockRate = pcmClockRate
|
||||
|
||||
if err := CheckPlugins([]string{"pulseaudio", "alaw"}); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
default:
|
||||
panic("Unhandled codec " + codecName)
|
||||
}
|
||||
@ -105,6 +186,21 @@ func (p *Pipeline) Stop() {
|
||||
C.gstreamer_send_stop_pipeline(p.Pipeline)
|
||||
}
|
||||
|
||||
func CheckPlugins(plugins []string) error {
|
||||
var plugin *C.GstPlugin
|
||||
for _, pluginstr := range plugins {
|
||||
plugincstr := C.CString(pluginstr)
|
||||
plugin = C.gst_registry_find_plugin(registry, plugincstr)
|
||||
C.free(unsafe.Pointer(plugincstr))
|
||||
if plugin == nil {
|
||||
return fmt.Errorf("Required gstreamer plugin %s not found", pluginstr)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
//export goHandlePipelineBuffer
|
||||
func goHandlePipelineBuffer(buffer unsafe.Pointer, bufferLen C.int, duration C.int, pipelineID C.int) {
|
||||
pipelinesLock.Lock()
|
||||
|
@ -9,8 +9,10 @@
|
||||
extern void goHandlePipelineBuffer(void *buffer, int bufferLen, int samples, int pipelineId);
|
||||
|
||||
GstElement *gstreamer_send_create_pipeline(char *pipeline);
|
||||
|
||||
void gstreamer_send_start_pipeline(GstElement *pipeline, int pipelineId);
|
||||
void gstreamer_send_stop_pipeline(GstElement *pipeline);
|
||||
void gstreamer_send_start_mainloop(void);
|
||||
void gstreamer_init(void);
|
||||
|
||||
#endif
|
||||
|
94
server/internal/hid/hid.c
Normal file
94
server/internal/hid/hid.c
Normal file
@ -0,0 +1,94 @@
|
||||
#include "hid.h"
|
||||
|
||||
static Display *display = NULL;
|
||||
static char *name = ":0.0";
|
||||
static int registered = 0;
|
||||
static int dirty = 0;
|
||||
|
||||
Display *getXDisplay(void) {
|
||||
/* Close the display if displayName has changed */
|
||||
if (dirty) {
|
||||
closeXDisplay();
|
||||
dirty = 0;
|
||||
}
|
||||
|
||||
if (display == NULL) {
|
||||
/* First try the user set displayName */
|
||||
display = XOpenDisplay(name);
|
||||
|
||||
/* Then try using environment variable DISPLAY */
|
||||
if (display == NULL) {
|
||||
display = XOpenDisplay(NULL);
|
||||
}
|
||||
|
||||
if (display == NULL) {
|
||||
fputs("Could not open main display\n", stderr);
|
||||
} else if (!registered) {
|
||||
atexit(&closeXDisplay);
|
||||
registered = 1;
|
||||
}
|
||||
}
|
||||
|
||||
return display;
|
||||
}
|
||||
|
||||
void closeXDisplay(void) {
|
||||
if (display != NULL) {
|
||||
XCloseDisplay(display);
|
||||
display = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
void setXDisplay(char *input) {
|
||||
name = strdup(input);
|
||||
dirty = 1;
|
||||
}
|
||||
|
||||
void mouseMove(int x, int y) {
|
||||
Display *display = getXDisplay();
|
||||
XWarpPointer(display, None, DefaultRootWindow(display), 0, 0, 0, 0, x, y);
|
||||
XSync(display, 0);
|
||||
}
|
||||
|
||||
void mouseScroll(int x, int y) {
|
||||
int ydir = 4; /* Button 4 is up, 5 is down. */
|
||||
int xdir = 6;
|
||||
|
||||
Display *display = getXDisplay();
|
||||
|
||||
if (y < 0) {
|
||||
ydir = 5;
|
||||
}
|
||||
|
||||
if (x < 0) {
|
||||
xdir = 7;
|
||||
}
|
||||
|
||||
int xi;
|
||||
int yi;
|
||||
|
||||
for (xi = 0; xi < abs(x); xi++) {
|
||||
XTestFakeButtonEvent(display, xdir, 1, CurrentTime);
|
||||
XTestFakeButtonEvent(display, xdir, 0, CurrentTime);
|
||||
}
|
||||
|
||||
for (yi = 0; yi < abs(y); yi++) {
|
||||
XTestFakeButtonEvent(display, ydir, 1, CurrentTime);
|
||||
XTestFakeButtonEvent(display, ydir, 0, CurrentTime);
|
||||
}
|
||||
|
||||
XSync(display, 0);
|
||||
}
|
||||
|
||||
void mouseEvent(unsigned int button, int down) {
|
||||
Display *display = getXDisplay();
|
||||
XTestFakeButtonEvent(display, button, down, CurrentTime);
|
||||
XSync(display, 0);
|
||||
}
|
||||
|
||||
void keyEvent(unsigned long key, int down) {
|
||||
Display *display = getXDisplay();
|
||||
KeyCode code = XKeysymToKeycode(display, key);
|
||||
XTestFakeKeyEvent(display, code, down, CurrentTime);
|
||||
XSync(display, 0);
|
||||
}
|
236
server/internal/hid/hid.go
Normal file
236
server/internal/hid/hid.go
Normal file
@ -0,0 +1,236 @@
|
||||
package hid
|
||||
|
||||
/*
|
||||
#cgo linux CFLAGS: -I/usr/src
|
||||
#cgo linux LDFLAGS: -L/usr/src -lX11 -lXtst
|
||||
|
||||
#include "hid.h"
|
||||
*/
|
||||
import "C"
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"n.eko.moe/neko/internal/hid/keycode"
|
||||
)
|
||||
|
||||
var debounce = make(map[int]time.Time)
|
||||
var buttons = make(map[int]keycode.Button)
|
||||
var keys = make(map[int]keycode.Key)
|
||||
|
||||
func init() {
|
||||
keys[keycode.BACKSPACE.Code] = keycode.BACKSPACE
|
||||
keys[keycode.TAB.Code] = keycode.TAB
|
||||
keys[keycode.CLEAR.Code] = keycode.CLEAR
|
||||
keys[keycode.ENTER.Code] = keycode.ENTER
|
||||
keys[keycode.SHIFT.Code] = keycode.SHIFT
|
||||
keys[keycode.CTRL.Code] = keycode.CTRL
|
||||
keys[keycode.ALT.Code] = keycode.ALT
|
||||
keys[keycode.PAUSE.Code] = keycode.PAUSE
|
||||
keys[keycode.CAPS_LOCK.Code] = keycode.CAPS_LOCK
|
||||
keys[keycode.ESCAPE.Code] = keycode.ESCAPE
|
||||
keys[keycode.SPACE.Code] = keycode.SPACE
|
||||
keys[keycode.PAGE_UP.Code] = keycode.PAGE_UP
|
||||
keys[keycode.PAGE_DOWN.Code] = keycode.PAGE_DOWN
|
||||
keys[keycode.END.Code] = keycode.END
|
||||
keys[keycode.HOME.Code] = keycode.HOME
|
||||
keys[keycode.LEFT_ARROW.Code] = keycode.LEFT_ARROW
|
||||
keys[keycode.UP_ARROW.Code] = keycode.UP_ARROW
|
||||
keys[keycode.RIGHT_ARROW.Code] = keycode.RIGHT_ARROW
|
||||
keys[keycode.DOWN_ARROW.Code] = keycode.DOWN_ARROW
|
||||
keys[keycode.INSERT.Code] = keycode.INSERT
|
||||
keys[keycode.DELETE.Code] = keycode.DELETE
|
||||
keys[keycode.KEY_0.Code] = keycode.KEY_0
|
||||
keys[keycode.KEY_1.Code] = keycode.KEY_1
|
||||
keys[keycode.KEY_2.Code] = keycode.KEY_2
|
||||
keys[keycode.KEY_3.Code] = keycode.KEY_3
|
||||
keys[keycode.KEY_4.Code] = keycode.KEY_4
|
||||
keys[keycode.KEY_5.Code] = keycode.KEY_5
|
||||
keys[keycode.KEY_6.Code] = keycode.KEY_6
|
||||
keys[keycode.KEY_7.Code] = keycode.KEY_7
|
||||
keys[keycode.KEY_8.Code] = keycode.KEY_8
|
||||
keys[keycode.KEY_9.Code] = keycode.KEY_9
|
||||
keys[keycode.KEY_A.Code] = keycode.KEY_A
|
||||
keys[keycode.KEY_B.Code] = keycode.KEY_B
|
||||
keys[keycode.KEY_C.Code] = keycode.KEY_C
|
||||
keys[keycode.KEY_D.Code] = keycode.KEY_D
|
||||
keys[keycode.KEY_E.Code] = keycode.KEY_E
|
||||
keys[keycode.KEY_F.Code] = keycode.KEY_F
|
||||
keys[keycode.KEY_G.Code] = keycode.KEY_G
|
||||
keys[keycode.KEY_H.Code] = keycode.KEY_H
|
||||
keys[keycode.KEY_I.Code] = keycode.KEY_I
|
||||
keys[keycode.KEY_J.Code] = keycode.KEY_J
|
||||
keys[keycode.KEY_K.Code] = keycode.KEY_K
|
||||
keys[keycode.KEY_L.Code] = keycode.KEY_L
|
||||
keys[keycode.KEY_M.Code] = keycode.KEY_M
|
||||
keys[keycode.KEY_N.Code] = keycode.KEY_N
|
||||
keys[keycode.KEY_O.Code] = keycode.KEY_O
|
||||
keys[keycode.KEY_P.Code] = keycode.KEY_P
|
||||
keys[keycode.KEY_Q.Code] = keycode.KEY_Q
|
||||
keys[keycode.KEY_R.Code] = keycode.KEY_R
|
||||
keys[keycode.KEY_S.Code] = keycode.KEY_S
|
||||
keys[keycode.KEY_T.Code] = keycode.KEY_T
|
||||
keys[keycode.KEY_U.Code] = keycode.KEY_U
|
||||
keys[keycode.KEY_V.Code] = keycode.KEY_V
|
||||
keys[keycode.KEY_W.Code] = keycode.KEY_W
|
||||
keys[keycode.KEY_X.Code] = keycode.KEY_X
|
||||
keys[keycode.KEY_Y.Code] = keycode.KEY_Y
|
||||
keys[keycode.KEY_Z.Code] = keycode.KEY_Z
|
||||
keys[keycode.WIN_LEFT.Code] = keycode.WIN_LEFT
|
||||
keys[keycode.WIN_RIGHT.Code] = keycode.WIN_RIGHT
|
||||
keys[keycode.PAD_0.Code] = keycode.PAD_0
|
||||
keys[keycode.PAD_1.Code] = keycode.PAD_1
|
||||
keys[keycode.PAD_2.Code] = keycode.PAD_2
|
||||
keys[keycode.PAD_3.Code] = keycode.PAD_3
|
||||
keys[keycode.PAD_4.Code] = keycode.PAD_4
|
||||
keys[keycode.PAD_5.Code] = keycode.PAD_5
|
||||
keys[keycode.PAD_6.Code] = keycode.PAD_6
|
||||
keys[keycode.PAD_7.Code] = keycode.PAD_7
|
||||
keys[keycode.PAD_8.Code] = keycode.PAD_8
|
||||
keys[keycode.PAD_9.Code] = keycode.PAD_9
|
||||
keys[keycode.MULTIPLY.Code] = keycode.MULTIPLY
|
||||
keys[keycode.ADD.Code] = keycode.ADD
|
||||
keys[keycode.SUBTRACT.Code] = keycode.SUBTRACT
|
||||
keys[keycode.DECIMAL.Code] = keycode.DECIMAL
|
||||
keys[keycode.DIVIDE.Code] = keycode.DIVIDE
|
||||
keys[keycode.KEY_F1.Code] = keycode.KEY_F1
|
||||
keys[keycode.KEY_F2.Code] = keycode.KEY_F2
|
||||
keys[keycode.KEY_F3.Code] = keycode.KEY_F3
|
||||
keys[keycode.KEY_F4.Code] = keycode.KEY_F4
|
||||
keys[keycode.KEY_F5.Code] = keycode.KEY_F5
|
||||
keys[keycode.KEY_F6.Code] = keycode.KEY_F6
|
||||
keys[keycode.KEY_F7.Code] = keycode.KEY_F7
|
||||
keys[keycode.KEY_F8.Code] = keycode.KEY_F8
|
||||
keys[keycode.KEY_F9.Code] = keycode.KEY_F9
|
||||
keys[keycode.KEY_F10.Code] = keycode.KEY_F10
|
||||
keys[keycode.KEY_F11.Code] = keycode.KEY_F11
|
||||
keys[keycode.KEY_F12.Code] = keycode.KEY_F12
|
||||
keys[keycode.NUM_LOCK.Code] = keycode.NUM_LOCK
|
||||
keys[keycode.SCROLL_LOCK.Code] = keycode.SCROLL_LOCK
|
||||
keys[keycode.SEMI_COLON.Code] = keycode.SEMI_COLON
|
||||
keys[keycode.EQUAL.Code] = keycode.EQUAL
|
||||
keys[keycode.COMMA.Code] = keycode.COMMA
|
||||
keys[keycode.DASH.Code] = keycode.DASH
|
||||
keys[keycode.PERIOD.Code] = keycode.PERIOD
|
||||
keys[keycode.FORWARD_SLASH.Code] = keycode.FORWARD_SLASH
|
||||
keys[keycode.GRAVE.Code] = keycode.GRAVE
|
||||
keys[keycode.OPEN_BRACKET.Code] = keycode.OPEN_BRACKET
|
||||
keys[keycode.BACK_SLASH.Code] = keycode.BACK_SLASH
|
||||
keys[keycode.CLOSE_BRAKET.Code] = keycode.CLOSE_BRAKET
|
||||
keys[keycode.SINGLE_QUOTE.Code] = keycode.SINGLE_QUOTE
|
||||
|
||||
buttons[keycode.LEFT_BUTTON.Code] = keycode.LEFT_BUTTON
|
||||
buttons[keycode.CENTER_BUTTON.Code] = keycode.CENTER_BUTTON
|
||||
buttons[keycode.RIGHT_BUTTON.Code] = keycode.RIGHT_BUTTON
|
||||
buttons[keycode.SCROLL_UP_BUTTON.Code] = keycode.SCROLL_UP_BUTTON
|
||||
buttons[keycode.SCROLL_DOWN_BUTTON.Code] = keycode.SCROLL_DOWN_BUTTON
|
||||
buttons[keycode.SCROLL_LEFT_BUTTON.Code] = keycode.SCROLL_LEFT_BUTTON
|
||||
buttons[keycode.SCROLL_RIGHT_BUTTON.Code] = keycode.SCROLL_RIGHT_BUTTON
|
||||
}
|
||||
|
||||
func Display(display string) {
|
||||
C.setXDisplay(C.CString(display))
|
||||
}
|
||||
|
||||
func Move(x, y int) {
|
||||
C.mouseMove(C.int(x), C.int(y))
|
||||
}
|
||||
|
||||
func Scroll(x, y int) {
|
||||
C.mouseScroll(C.int(x), C.int(y))
|
||||
}
|
||||
|
||||
func ButtonDown(code int) (*keycode.Button, error) {
|
||||
button, ok := buttons[code]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid button %v", code)
|
||||
}
|
||||
|
||||
if _, ok := debounce[code]; ok {
|
||||
return nil, fmt.Errorf("debounced button %v(%v)", button.Name, code)
|
||||
}
|
||||
|
||||
debounce[code] = time.Now()
|
||||
|
||||
C.mouseEvent(C.uint(button.Keysym), C.int(1))
|
||||
return &button, nil
|
||||
}
|
||||
|
||||
func KeyDown(code int) (*keycode.Key, error) {
|
||||
key, ok := keys[code]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid key %v", code)
|
||||
}
|
||||
|
||||
if _, ok := debounce[code]; ok {
|
||||
return nil, fmt.Errorf("debounced key %v(%v)", key.Name, code)
|
||||
}
|
||||
|
||||
debounce[code] = time.Now()
|
||||
|
||||
C.keyEvent(C.ulong(key.Keysym), C.int(1))
|
||||
return &key, nil
|
||||
}
|
||||
|
||||
|
||||
func ButtonUp(code int) (*keycode.Button, error) {
|
||||
button, ok := buttons[code]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid button %v", code)
|
||||
}
|
||||
|
||||
if _, ok := debounce[code]; !ok {
|
||||
return nil, fmt.Errorf("debounced button %v(%v)", button.Name, code)
|
||||
}
|
||||
|
||||
delete(debounce, code)
|
||||
|
||||
C.mouseEvent(C.uint(button.Keysym), C.int(0))
|
||||
return &button, nil
|
||||
}
|
||||
|
||||
func KeyUp(code int) (*keycode.Key, error) {
|
||||
key, ok := keys[code]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("invalid key %v", code)
|
||||
}
|
||||
|
||||
if _, ok := debounce[code]; !ok {
|
||||
return nil, fmt.Errorf("debounced key %v(%v)", key.Name, code)
|
||||
}
|
||||
|
||||
delete(debounce, code)
|
||||
|
||||
C.keyEvent(C.ulong(key.Keysym), C.int(0))
|
||||
return &key, nil
|
||||
}
|
||||
|
||||
func Reset() {
|
||||
for key := range debounce {
|
||||
if (key < 8) {
|
||||
ButtonUp(key)
|
||||
} else {
|
||||
KeyUp(key)
|
||||
}
|
||||
|
||||
delete(debounce, key)
|
||||
}
|
||||
}
|
||||
|
||||
func Check(duration time.Duration) {
|
||||
t := time.Now()
|
||||
for key, start := range debounce {
|
||||
if t.Sub(start) < duration {
|
||||
continue
|
||||
}
|
||||
|
||||
if (key < 8) {
|
||||
ButtonUp(key)
|
||||
} else {
|
||||
KeyUp(key)
|
||||
}
|
||||
|
||||
delete(debounce, key)
|
||||
}
|
||||
}
|
34
server/internal/hid/hid.h
Normal file
34
server/internal/hid/hid.h
Normal file
@ -0,0 +1,34 @@
|
||||
#pragma once
|
||||
|
||||
#ifndef XDISPLAY_H
|
||||
#define XDISPLAY_H
|
||||
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/extensions/XTest.h>
|
||||
#include <stdint.h>
|
||||
#include <stdlib.h>
|
||||
#include <stdio.h> /* For fputs() */
|
||||
#include <string.h> /* For strdup() */
|
||||
|
||||
/* Returns the main display, closed either on exit or when closeMainDisplay()
|
||||
* is invoked. This removes a bit of the overhead of calling XOpenDisplay() &
|
||||
* XCloseDisplay() everytime the main display needs to be used.
|
||||
*
|
||||
* Note that this is almost certainly not thread safe. */
|
||||
Display *getXDisplay(void);
|
||||
void closeXDisplay(void);
|
||||
void mouseMove(int x, int y);
|
||||
void mouseScroll(int x, int y);
|
||||
void mouseEvent(unsigned int button, int down);
|
||||
void keyEvent(unsigned long key, int down);
|
||||
|
||||
#ifdef __cplusplus
|
||||
extern "C"
|
||||
{
|
||||
#endif
|
||||
void setXDisplay(char *input);
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
#endif
|
||||
|
49
server/internal/hid/keycode/button.go
Normal file
49
server/internal/hid/keycode/button.go
Normal file
@ -0,0 +1,49 @@
|
||||
package keycode
|
||||
|
||||
type Button struct {
|
||||
Name string
|
||||
Code int
|
||||
Keysym int
|
||||
}
|
||||
|
||||
var LEFT_BUTTON = Button{
|
||||
Name: "LEFT",
|
||||
Code: 0,
|
||||
Keysym: 1,
|
||||
}
|
||||
|
||||
var CENTER_BUTTON = Button{
|
||||
Name: "CENTER",
|
||||
Code: 1,
|
||||
Keysym: 2,
|
||||
}
|
||||
|
||||
var RIGHT_BUTTON = Button{
|
||||
Name: "RIGHT",
|
||||
Code: 2,
|
||||
Keysym: 3,
|
||||
}
|
||||
|
||||
var SCROLL_UP_BUTTON = Button{
|
||||
Name: "SCROLL_UP",
|
||||
Code: 3,
|
||||
Keysym: 4,
|
||||
}
|
||||
|
||||
var SCROLL_DOWN_BUTTON = Button{
|
||||
Name: "SCROLL_DOWN",
|
||||
Code: 4,
|
||||
Keysym: 5,
|
||||
}
|
||||
|
||||
var SCROLL_LEFT_BUTTON = Button{
|
||||
Name: "SCROLL_LEFT",
|
||||
Code: 5,
|
||||
Keysym: 6,
|
||||
}
|
||||
|
||||
var SCROLL_RIGHT_BUTTON = Button{
|
||||
Name: "SCROLL_RIGHT",
|
||||
Code: 6,
|
||||
Keysym: 7,
|
||||
}
|
701
server/internal/hid/keycode/keys.go
Normal file
701
server/internal/hid/keycode/keys.go
Normal file
@ -0,0 +1,701 @@
|
||||
package keycode
|
||||
|
||||
type Key struct {
|
||||
Name string
|
||||
Value string
|
||||
Code int
|
||||
Keysym int
|
||||
}
|
||||
|
||||
var BACKSPACE = Key{
|
||||
Name: "BACKSPACE",
|
||||
Value: "BackSpace",
|
||||
Code: 8,
|
||||
Keysym: int(0xff08),
|
||||
}
|
||||
|
||||
var TAB = Key{
|
||||
Name: "TAB",
|
||||
Value: "Tab",
|
||||
Code: 9,
|
||||
Keysym: int(0xFF09),
|
||||
}
|
||||
|
||||
var CLEAR = Key{
|
||||
Name: "CLEAR",
|
||||
Value: "Clear",
|
||||
Code: 12,
|
||||
Keysym: int(0xFF0B),
|
||||
}
|
||||
|
||||
var ENTER = Key{
|
||||
Name: "ENTER",
|
||||
Value: "Enter",
|
||||
Code: 13,
|
||||
Keysym: int(0xFF0D),
|
||||
}
|
||||
|
||||
var SHIFT = Key{
|
||||
Name: "SHIFT",
|
||||
Value: "Shift",
|
||||
Code: 16,
|
||||
Keysym: int(0xFFE1),
|
||||
}
|
||||
|
||||
var CTRL = Key{
|
||||
Name: "CTRL",
|
||||
Value: "Ctrl",
|
||||
Code: 17,
|
||||
Keysym: int(0xFFE3),
|
||||
}
|
||||
|
||||
var ALT = Key{
|
||||
Name: "ALT",
|
||||
Value: "Alt",
|
||||
Code: 18,
|
||||
Keysym: int(0xFFE9),
|
||||
}
|
||||
|
||||
var PAUSE = Key{
|
||||
Name: "PAUSE",
|
||||
Value: "Pause",
|
||||
Code: 19,
|
||||
Keysym: int(0xFF13),
|
||||
}
|
||||
|
||||
var CAPS_LOCK = Key{
|
||||
Name: "CAPS_LOCK",
|
||||
Value: "Caps Lock",
|
||||
Code: 20,
|
||||
Keysym: int(0xFFE5),
|
||||
}
|
||||
|
||||
var ESCAPE = Key{
|
||||
Name: "ESCAPE",
|
||||
Value: "Escape",
|
||||
Code: 27,
|
||||
Keysym: int(0xFF1B),
|
||||
}
|
||||
|
||||
var SPACE = Key{
|
||||
Name: "SPACE",
|
||||
Value: " ",
|
||||
Code: 32,
|
||||
Keysym: int(0x0020),
|
||||
}
|
||||
|
||||
var PAGE_UP = Key{
|
||||
Name: "PAGE_UP",
|
||||
Value: "Page Up",
|
||||
Code: 33,
|
||||
Keysym: int(0xFF55),
|
||||
}
|
||||
|
||||
var PAGE_DOWN = Key{
|
||||
Name: "PAGE_DOWN",
|
||||
Value: "Page Down",
|
||||
Code: 34,
|
||||
Keysym: int(0xFF56),
|
||||
}
|
||||
|
||||
var END = Key{
|
||||
Name: "END",
|
||||
Value: "End",
|
||||
Code: 35,
|
||||
Keysym: int(0xFF57),
|
||||
}
|
||||
|
||||
var HOME = Key{
|
||||
Name: "HOME",
|
||||
Value: "Home",
|
||||
Code: 36,
|
||||
Keysym: int(0xFF50),
|
||||
}
|
||||
|
||||
var LEFT_ARROW = Key{
|
||||
Name: "LEFT_ARROW",
|
||||
Value: "Left Arrow",
|
||||
Code: 37,
|
||||
Keysym: int(0xFF51),
|
||||
}
|
||||
|
||||
var UP_ARROW = Key{
|
||||
Name: "UP_ARROW",
|
||||
Value: "Up Arrow",
|
||||
Code: 38,
|
||||
Keysym: int(0xFF52),
|
||||
}
|
||||
|
||||
var RIGHT_ARROW = Key{
|
||||
Name: "RIGHT_ARROW",
|
||||
Value: "Right Arrow",
|
||||
Code: 39,
|
||||
Keysym: int(0xFF53),
|
||||
}
|
||||
|
||||
var DOWN_ARROW = Key{
|
||||
Name: "DOWN_ARROW",
|
||||
Value: "Down Arrow",
|
||||
Code: 40,
|
||||
Keysym: int(0xFF54),
|
||||
}
|
||||
|
||||
var INSERT = Key{
|
||||
Name: "INSERT",
|
||||
Value: "Insert",
|
||||
Code: 45,
|
||||
Keysym: int(0xFF63),
|
||||
}
|
||||
|
||||
var DELETE = Key{
|
||||
Name: "DELETE",
|
||||
Value: "Delete",
|
||||
Code: 46,
|
||||
Keysym: int(0xFFFF),
|
||||
}
|
||||
|
||||
var KEY_0 = Key{
|
||||
Name: "KEY_0",
|
||||
Value: "0",
|
||||
Code: 48,
|
||||
Keysym: int(0x0030),
|
||||
}
|
||||
|
||||
var KEY_1 = Key{
|
||||
Name: "KEY_1",
|
||||
Value: "1",
|
||||
Code: 49,
|
||||
Keysym: int(0x0031),
|
||||
}
|
||||
|
||||
var KEY_2 = Key{
|
||||
Name: "KEY_2",
|
||||
Value: "2",
|
||||
Code: 50,
|
||||
Keysym: int(0x0032),
|
||||
}
|
||||
|
||||
var KEY_3 = Key{
|
||||
Name: "KEY_3",
|
||||
Value: "3",
|
||||
Code: 51,
|
||||
Keysym: int(0x0033),
|
||||
}
|
||||
|
||||
var KEY_4 = Key{
|
||||
Name: "KEY_4",
|
||||
Value: "4",
|
||||
Code: 52,
|
||||
Keysym: int(0x0034),
|
||||
}
|
||||
|
||||
var KEY_5 = Key{
|
||||
Name: "KEY_5",
|
||||
Value: "5",
|
||||
Code: 53,
|
||||
Keysym: int(0x0035),
|
||||
}
|
||||
|
||||
var KEY_6 = Key{
|
||||
Name: "KEY_6",
|
||||
Value: "6",
|
||||
Code: 54,
|
||||
Keysym: int(0x0036),
|
||||
}
|
||||
|
||||
var KEY_7 = Key{
|
||||
Name: "KEY_7",
|
||||
Value: "7",
|
||||
Code: 55,
|
||||
Keysym: int(0x0037),
|
||||
}
|
||||
|
||||
var KEY_8 = Key{
|
||||
Name: "KEY_8",
|
||||
Value: "8",
|
||||
Code: 56,
|
||||
Keysym: int(0x0038),
|
||||
}
|
||||
|
||||
var KEY_9 = Key{
|
||||
Name: "KEY_9",
|
||||
Value: "9",
|
||||
Code: 57,
|
||||
Keysym: int(0x0039),
|
||||
}
|
||||
|
||||
var KEY_A = Key{
|
||||
Name: "KEY_A",
|
||||
Value: "a",
|
||||
Code: 65,
|
||||
Keysym: int(0x0061),
|
||||
}
|
||||
|
||||
var KEY_B = Key{
|
||||
Name: "KEY_B",
|
||||
Value: "b",
|
||||
Code: 66,
|
||||
Keysym: int(0x0062),
|
||||
}
|
||||
|
||||
var KEY_C = Key{
|
||||
Name: "KEY_C",
|
||||
Value: "c",
|
||||
Code: 67,
|
||||
Keysym: int(0x0063),
|
||||
}
|
||||
|
||||
var KEY_D = Key{
|
||||
Name: "KEY_D",
|
||||
Value: "d",
|
||||
Code: 68,
|
||||
Keysym: int(0x0064),
|
||||
}
|
||||
|
||||
var KEY_E = Key{
|
||||
Name: "KEY_E",
|
||||
Value: "e",
|
||||
Code: 69,
|
||||
Keysym: int(0x0065),
|
||||
}
|
||||
|
||||
var KEY_F = Key{
|
||||
Name: "KEY_F",
|
||||
Value: "f",
|
||||
Code: 70,
|
||||
Keysym: int(0x0066),
|
||||
}
|
||||
|
||||
var KEY_G = Key{
|
||||
Name: "KEY_G",
|
||||
Value: "g",
|
||||
Code: 71,
|
||||
Keysym: int(0x0067),
|
||||
}
|
||||
|
||||
var KEY_H = Key{
|
||||
Name: "KEY_H",
|
||||
Value: "h",
|
||||
Code: 72,
|
||||
Keysym: int(0x0068),
|
||||
}
|
||||
|
||||
var KEY_I = Key{
|
||||
Name: "KEY_I",
|
||||
Value: "i",
|
||||
Code: 73,
|
||||
Keysym: int(0x0069),
|
||||
}
|
||||
|
||||
var KEY_J = Key{
|
||||
Name: "KEY_J",
|
||||
Value: "j",
|
||||
Code: 74,
|
||||
Keysym: int(0x006a),
|
||||
}
|
||||
|
||||
var KEY_K = Key{
|
||||
Name: "KEY_K",
|
||||
Value: "k",
|
||||
Code: 75,
|
||||
Keysym: int(0x006b),
|
||||
}
|
||||
|
||||
var KEY_L = Key{
|
||||
Name: "KEY_L",
|
||||
Value: "l",
|
||||
Code: 76,
|
||||
Keysym: int(0x006c),
|
||||
}
|
||||
|
||||
var KEY_M = Key{
|
||||
Name: "KEY_M",
|
||||
Value: "m",
|
||||
Code: 77,
|
||||
Keysym: int(0x006d),
|
||||
}
|
||||
|
||||
var KEY_N = Key{
|
||||
Name: "KEY_N",
|
||||
Value: "n",
|
||||
Code: 78,
|
||||
Keysym: int(0x006e),
|
||||
}
|
||||
|
||||
var KEY_O = Key{
|
||||
Name: "KEY_O",
|
||||
Value: "o",
|
||||
Code: 79,
|
||||
Keysym: int(0x006f),
|
||||
}
|
||||
|
||||
var KEY_P = Key{
|
||||
Name: "KEY_P",
|
||||
Value: "p",
|
||||
Code: 80,
|
||||
Keysym: int(0x0070),
|
||||
}
|
||||
|
||||
var KEY_Q = Key{
|
||||
Name: "KEY_Q",
|
||||
Value: "q",
|
||||
Code: 81,
|
||||
Keysym: int(0x0071),
|
||||
}
|
||||
|
||||
var KEY_R = Key{
|
||||
Name: "KEY_R",
|
||||
Value: "r",
|
||||
Code: 82,
|
||||
Keysym: int(0x0072),
|
||||
}
|
||||
|
||||
var KEY_S = Key{
|
||||
Name: "KEY_S",
|
||||
Value: "s",
|
||||
Code: 83,
|
||||
Keysym: int(0x0073),
|
||||
}
|
||||
|
||||
var KEY_T = Key{
|
||||
Name: "KEY_T",
|
||||
Value: "t",
|
||||
Code: 84,
|
||||
Keysym: int(0x0074),
|
||||
}
|
||||
|
||||
var KEY_U = Key{
|
||||
Name: "KEY_U",
|
||||
Value: "u",
|
||||
Code: 85,
|
||||
Keysym: int(0x0075),
|
||||
}
|
||||
|
||||
var KEY_V = Key{
|
||||
Name: "KEY_V",
|
||||
Value: "v",
|
||||
Code: 86,
|
||||
Keysym: int(0x0076),
|
||||
}
|
||||
|
||||
var KEY_W = Key{
|
||||
Name: "KEY_W",
|
||||
Value: "w",
|
||||
Code: 87,
|
||||
Keysym: int(0x0077),
|
||||
}
|
||||
|
||||
var KEY_X = Key{
|
||||
Name: "KEY_X",
|
||||
Value: "x",
|
||||
Code: 88,
|
||||
Keysym: int(0x0078),
|
||||
}
|
||||
|
||||
var KEY_Y = Key{
|
||||
Name: "KEY_Y",
|
||||
Value: "y",
|
||||
Code: 89,
|
||||
Keysym: int(0x0079),
|
||||
}
|
||||
|
||||
var KEY_Z = Key{
|
||||
Name: "KEY_Z",
|
||||
Value: "z",
|
||||
Code: 90,
|
||||
Keysym: int(0x007a),
|
||||
}
|
||||
|
||||
var WIN_LEFT = Key{
|
||||
Name: "WIN_LEFT",
|
||||
Value: "Win Left",
|
||||
Code: 91,
|
||||
Keysym: int(0xFFEB),
|
||||
}
|
||||
|
||||
var WIN_RIGHT = Key{
|
||||
Name: "WIN_RIGHT",
|
||||
Value: "Win Right",
|
||||
Code: 92,
|
||||
Keysym: int(0xFF67),
|
||||
}
|
||||
|
||||
var PAD_0 = Key{
|
||||
Name: "PAD_0",
|
||||
Value: "Num Pad 0",
|
||||
Code: 96,
|
||||
Keysym: int(0xFFB0),
|
||||
}
|
||||
|
||||
var PAD_1 = Key{
|
||||
Name: "PAD_1",
|
||||
Value: "Num Pad 1",
|
||||
Code: 97,
|
||||
Keysym: int(0xFFB1),
|
||||
}
|
||||
|
||||
var PAD_2 = Key{
|
||||
Name: "PAD_2",
|
||||
Value: "Num Pad 2",
|
||||
Code: 98,
|
||||
Keysym: int(0xFFB2),
|
||||
}
|
||||
|
||||
var PAD_3 = Key{
|
||||
Name: "PAD_3",
|
||||
Value: "Num Pad 3",
|
||||
Code: 99,
|
||||
Keysym: int(0xFFB3),
|
||||
}
|
||||
|
||||
var PAD_4 = Key{
|
||||
Name: "PAD_4",
|
||||
Value: "Num Pad 4",
|
||||
Code: 100,
|
||||
Keysym: int(0xFFB4),
|
||||
}
|
||||
|
||||
var PAD_5 = Key{
|
||||
Name: "PAD_5",
|
||||
Value: "Num Pad 5",
|
||||
Code: 101,
|
||||
Keysym: int(0xFFB5),
|
||||
}
|
||||
|
||||
var PAD_6 = Key{
|
||||
Name: "PAD_6",
|
||||
Value: "Num Pad 6",
|
||||
Code: 102,
|
||||
Keysym: int(0xFFB6),
|
||||
}
|
||||
|
||||
var PAD_7 = Key{
|
||||
Name: "PAD_7",
|
||||
Value: "Num Pad 7",
|
||||
Code: 103,
|
||||
Keysym: int(0xFFB7),
|
||||
}
|
||||
|
||||
var PAD_8 = Key{
|
||||
Name: "PAD_8",
|
||||
Value: "Num Pad 8",
|
||||
Code: 104,
|
||||
Keysym: int(0xFFB8),
|
||||
}
|
||||
|
||||
var PAD_9 = Key{
|
||||
Name: "PAD_9",
|
||||
Value: "Num Pad 9",
|
||||
Code: 105,
|
||||
Keysym: int(0xFFB9),
|
||||
}
|
||||
|
||||
var MULTIPLY = Key{
|
||||
Name: "MULTIPLY",
|
||||
Value: "*",
|
||||
Code: 106,
|
||||
Keysym: int(0xFFAA),
|
||||
}
|
||||
|
||||
var ADD = Key{
|
||||
Name: "ADD",
|
||||
Value: "+",
|
||||
Code: 107,
|
||||
Keysym: int(0xFFAB),
|
||||
}
|
||||
|
||||
var SUBTRACT = Key{
|
||||
Name: "SUBTRACT",
|
||||
Value: "-",
|
||||
Code: 109,
|
||||
Keysym: int(0xFFAD),
|
||||
}
|
||||
|
||||
var DECIMAL = Key{
|
||||
Name: "DECIMAL",
|
||||
Value: ".",
|
||||
Code: 110,
|
||||
Keysym: int(0xFFAE),
|
||||
}
|
||||
|
||||
var DIVIDE = Key{
|
||||
Name: "DIVIDE",
|
||||
Value: "/",
|
||||
Code: 111,
|
||||
Keysym: int(0xFFAF),
|
||||
}
|
||||
|
||||
var KEY_F1 = Key{
|
||||
Name: "KEY_F1",
|
||||
Value: "f1",
|
||||
Code: 112,
|
||||
Keysym: int(0xFFBE),
|
||||
}
|
||||
|
||||
var KEY_F2 = Key{
|
||||
Name: "KEY_F2",
|
||||
Value: "f2",
|
||||
Code: 113,
|
||||
Keysym: int(0xFFBF),
|
||||
}
|
||||
|
||||
var KEY_F3 = Key{
|
||||
Name: "KEY_F3",
|
||||
Value: "f3",
|
||||
Code: 114,
|
||||
Keysym: int(0xFFC0),
|
||||
}
|
||||
|
||||
var KEY_F4 = Key{
|
||||
Name: "KEY_F4",
|
||||
Value: "f4",
|
||||
Code: 115,
|
||||
Keysym: int(0xFFC1),
|
||||
}
|
||||
|
||||
var KEY_F5 = Key{
|
||||
Name: "KEY_F5",
|
||||
Value: "f5",
|
||||
Code: 116,
|
||||
Keysym: int(0xFFC2),
|
||||
}
|
||||
|
||||
var KEY_F6 = Key{
|
||||
Name: "KEY_F6",
|
||||
Value: "f6",
|
||||
Code: 117,
|
||||
Keysym: int(0xFFC3),
|
||||
}
|
||||
|
||||
var KEY_F7 = Key{
|
||||
Name: "KEY_F7",
|
||||
Value: "f7",
|
||||
Code: 118,
|
||||
Keysym: int(0xFFC4),
|
||||
}
|
||||
|
||||
var KEY_F8 = Key{
|
||||
Name: "KEY_F8",
|
||||
Value: "f8",
|
||||
Code: 119,
|
||||
Keysym: int(0xFFC5),
|
||||
}
|
||||
|
||||
var KEY_F9 = Key{
|
||||
Name: "KEY_F9",
|
||||
Value: "f9",
|
||||
Code: 120,
|
||||
Keysym: int(0xFFC6),
|
||||
}
|
||||
|
||||
var KEY_F10 = Key{
|
||||
Name: "KEY_F10",
|
||||
Value: "f10",
|
||||
Code: 121,
|
||||
Keysym: int(0xFFC7),
|
||||
}
|
||||
|
||||
var KEY_F11 = Key{
|
||||
Name: "KEY_F11",
|
||||
Value: "f11",
|
||||
Code: 122,
|
||||
Keysym: int(0xFFC8),
|
||||
}
|
||||
|
||||
var KEY_F12 = Key{
|
||||
Name: "KEY_F12",
|
||||
Value: "f12",
|
||||
Code: 123,
|
||||
Keysym: int(0xFFC9),
|
||||
}
|
||||
|
||||
var NUM_LOCK = Key{
|
||||
Name: "NUM_LOCK",
|
||||
Value: "Num Lock",
|
||||
Code: 144,
|
||||
Keysym: int(0xFF7F),
|
||||
}
|
||||
|
||||
var SCROLL_LOCK = Key{
|
||||
Name: "SCROLL_LOCK",
|
||||
Value: "Scroll Lock",
|
||||
Code: 145,
|
||||
Keysym: int(0xFF14),
|
||||
}
|
||||
|
||||
var SEMI_COLON = Key{
|
||||
Name: "SEMI_COLON",
|
||||
Value: ";",
|
||||
Code: 186,
|
||||
Keysym: int(0x003b),
|
||||
}
|
||||
|
||||
var EQUAL = Key{
|
||||
Name: "EQUAL",
|
||||
Value: "=",
|
||||
Code: 187,
|
||||
Keysym: int(0x003d),
|
||||
}
|
||||
|
||||
var COMMA = Key{
|
||||
Name: "COMMA",
|
||||
Value: ",",
|
||||
Code: 188,
|
||||
Keysym: int(0x002c),
|
||||
}
|
||||
|
||||
var DASH = Key{
|
||||
Name: "DASH",
|
||||
Value: "-",
|
||||
Code: 189,
|
||||
Keysym: int(0x002d),
|
||||
}
|
||||
|
||||
var PERIOD = Key{
|
||||
Name: "PERIOD",
|
||||
Value: ".",
|
||||
Code: 190,
|
||||
Keysym: int(0x002e),
|
||||
}
|
||||
|
||||
var FORWARD_SLASH = Key{
|
||||
Name: "FORWARD_SLASH",
|
||||
Value: "/",
|
||||
Code: 191,
|
||||
Keysym: int(0x002f),
|
||||
}
|
||||
|
||||
var GRAVE = Key{
|
||||
Name: "GRAVE",
|
||||
Value: "`",
|
||||
Code: 192,
|
||||
Keysym: int(0x0060),
|
||||
}
|
||||
|
||||
var OPEN_BRACKET = Key{
|
||||
Name: "OPEN_BRACKET",
|
||||
Value: "[",
|
||||
Code: 219,
|
||||
Keysym: int(0x005b),
|
||||
}
|
||||
|
||||
var BACK_SLASH = Key{
|
||||
Name: "BACK_SLASH",
|
||||
Value: "\\",
|
||||
Code: 220,
|
||||
Keysym: int(0x005c),
|
||||
}
|
||||
|
||||
var CLOSE_BRAKET = Key{
|
||||
Name: "CLOSE_BRAKET",
|
||||
Value: "]",
|
||||
Code: 221,
|
||||
Keysym: int(0x005d),
|
||||
}
|
||||
|
||||
var SINGLE_QUOTE = Key{
|
||||
Name: "SINGLE_QUOTE",
|
||||
Value: "'",
|
||||
Code: 222,
|
||||
Keysym: int(0x0022),
|
||||
}
|
@ -1,102 +1,102 @@
|
||||
package endpoint
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"runtime/debug"
|
||||
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/go-chi/chi/middleware"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type (
|
||||
Endpoint func(http.ResponseWriter, *http.Request) error
|
||||
Endpoint func(http.ResponseWriter, *http.Request) error
|
||||
|
||||
ErrResponse struct {
|
||||
Status int `json:"status,omitempty"`
|
||||
Err string `json:"error,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Details string `json:"details,omitempty"`
|
||||
Code string `json:"code,omitempty"`
|
||||
RequestID string `json:"request,omitempty"`
|
||||
}
|
||||
ErrResponse struct {
|
||||
Status int `json:"status,omitempty"`
|
||||
Err string `json:"error,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
Details string `json:"details,omitempty"`
|
||||
Code string `json:"code,omitempty"`
|
||||
RequestID string `json:"request,omitempty"`
|
||||
}
|
||||
)
|
||||
|
||||
func Handle(handler Endpoint) http.HandlerFunc {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := handler(w, r); err != nil {
|
||||
WriteError(w, r, err)
|
||||
}
|
||||
}
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := handler(w, r); err != nil {
|
||||
WriteError(w, r, err)
|
||||
}
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
|
||||
var nonErrorsCodes = map[int]bool{
|
||||
404: true,
|
||||
404: true,
|
||||
}
|
||||
|
||||
func errResponse(input interface{}) *ErrResponse {
|
||||
var res *ErrResponse
|
||||
var err interface{}
|
||||
var res *ErrResponse
|
||||
var err interface{}
|
||||
|
||||
switch input.(type) {
|
||||
case *HandlerError:
|
||||
e := input.(*HandlerError)
|
||||
res = &ErrResponse{
|
||||
Status: e.Status,
|
||||
Err: http.StatusText(e.Status),
|
||||
Message: e.Message,
|
||||
}
|
||||
err = e.Err
|
||||
default:
|
||||
res = &ErrResponse{
|
||||
Status: http.StatusInternalServerError,
|
||||
Err: http.StatusText(http.StatusInternalServerError),
|
||||
}
|
||||
err = input
|
||||
}
|
||||
switch input.(type) {
|
||||
case *HandlerError:
|
||||
e := input.(*HandlerError)
|
||||
res = &ErrResponse{
|
||||
Status: e.Status,
|
||||
Err: http.StatusText(e.Status),
|
||||
Message: e.Message,
|
||||
}
|
||||
err = e.Err
|
||||
default:
|
||||
res = &ErrResponse{
|
||||
Status: http.StatusInternalServerError,
|
||||
Err: http.StatusText(http.StatusInternalServerError),
|
||||
}
|
||||
err = input
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case *error:
|
||||
e := err.(error)
|
||||
res.Details = e.Error()
|
||||
break
|
||||
default:
|
||||
res.Details = fmt.Sprintf("%+v", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
switch err.(type) {
|
||||
case *error:
|
||||
e := err.(error)
|
||||
res.Details = e.Error()
|
||||
break
|
||||
default:
|
||||
res.Details = fmt.Sprintf("%+v", err)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return res
|
||||
return res
|
||||
}
|
||||
|
||||
func WriteError(w http.ResponseWriter, r *http.Request, err interface{}) {
|
||||
hlog := log.With().
|
||||
Str("module", "http").
|
||||
Logger()
|
||||
hlog := log.With().
|
||||
Str("module", "http").
|
||||
Logger()
|
||||
|
||||
res := errResponse(err)
|
||||
res := errResponse(err)
|
||||
|
||||
if reqID := middleware.GetReqID(r.Context()); reqID != "" {
|
||||
res.RequestID = reqID
|
||||
}
|
||||
if reqID := middleware.GetReqID(r.Context()); reqID != "" {
|
||||
res.RequestID = reqID
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(res.Status)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(res.Status)
|
||||
|
||||
if err := json.NewEncoder(w).Encode(res); err != nil {
|
||||
hlog.Warn().Err(err).Msg("Failed writing json error response")
|
||||
}
|
||||
if err := json.NewEncoder(w).Encode(res); err != nil {
|
||||
hlog.Warn().Err(err).Msg("Failed writing json error response")
|
||||
}
|
||||
|
||||
if !nonErrorsCodes[res.Status] {
|
||||
logEntry := middleware.GetLogEntry(r)
|
||||
if logEntry != nil {
|
||||
logEntry.Panic(err, debug.Stack())
|
||||
} else {
|
||||
hlog.Error().Str("stack", string(debug.Stack())).Msgf("%+v", err)
|
||||
}
|
||||
}
|
||||
if !nonErrorsCodes[res.Status] {
|
||||
logEntry := middleware.GetLogEntry(r)
|
||||
if logEntry != nil {
|
||||
logEntry.Panic(err, debug.Stack())
|
||||
} else {
|
||||
hlog.Error().Str("stack", string(debug.Stack())).Msgf("%+v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,15 +3,15 @@ package endpoint
|
||||
import "fmt"
|
||||
|
||||
type HandlerError struct {
|
||||
Status int
|
||||
Message string
|
||||
Err error
|
||||
Status int
|
||||
Message string
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *HandlerError) Error() string {
|
||||
if e.Err != nil {
|
||||
return fmt.Sprintf("%s: %s", e.Message, e.Err.Error())
|
||||
}
|
||||
if e.Err != nil {
|
||||
return fmt.Sprintf("%s: %s", e.Message, e.Err.Error())
|
||||
}
|
||||
|
||||
return e.Message
|
||||
return e.Message
|
||||
}
|
||||
|
87
server/internal/http/http.go
Normal file
87
server/internal/http/http.go
Normal file
@ -0,0 +1,87 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"n.eko.moe/neko/internal/config"
|
||||
"n.eko.moe/neko/internal/http/endpoint"
|
||||
"n.eko.moe/neko/internal/http/middleware"
|
||||
"n.eko.moe/neko/internal/websocket"
|
||||
)
|
||||
|
||||
type Server struct {
|
||||
logger zerolog.Logger
|
||||
router *chi.Mux
|
||||
http *http.Server
|
||||
conf *config.Server
|
||||
}
|
||||
|
||||
func New(conf *config.Server, webSocketHandler *websocket.WebSocketHandler) *Server {
|
||||
logger := log.With().Str("module", "webrtc").Logger()
|
||||
|
||||
router := chi.NewRouter()
|
||||
// router.Use(middleware.Recoverer) // Recover from panics without crashing server
|
||||
router.Use(middleware.RequestID) // Create a request ID for each request
|
||||
router.Use(middleware.Logger) // Log API request calls
|
||||
|
||||
router.Get("/ws", func(w http.ResponseWriter, r *http.Request) {
|
||||
webSocketHandler.Upgrade(w, r)
|
||||
})
|
||||
|
||||
fs := http.FileServer(http.Dir(conf.Static))
|
||||
router.Get("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := os.Stat(conf.Static + r.RequestURI); os.IsNotExist(err) {
|
||||
http.StripPrefix(r.RequestURI, fs).ServeHTTP(w, r)
|
||||
} else {
|
||||
fs.ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
|
||||
router.NotFound(endpoint.Handle(func(w http.ResponseWriter, r *http.Request) error {
|
||||
return &endpoint.HandlerError{
|
||||
Status: http.StatusNotFound,
|
||||
Message: fmt.Sprintf("file '%s' is not found", r.RequestURI),
|
||||
}
|
||||
}))
|
||||
|
||||
server := &http.Server{
|
||||
Addr: conf.Bind,
|
||||
Handler: router,
|
||||
}
|
||||
|
||||
return &Server{
|
||||
logger: logger,
|
||||
router: router,
|
||||
http: server,
|
||||
conf: conf,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Start() {
|
||||
if s.conf.Cert != "" && s.conf.Key != "" {
|
||||
go func() {
|
||||
if err := s.http.ListenAndServeTLS(s.conf.Cert, s.conf.Key); err != http.ErrServerClosed {
|
||||
s.logger.Panic().Err(err).Msg("unable to start https server")
|
||||
}
|
||||
}()
|
||||
s.logger.Info().Msgf("https listening on %s", s.http.Addr)
|
||||
} else {
|
||||
go func() {
|
||||
if err := s.http.ListenAndServe(); err != http.ErrServerClosed {
|
||||
s.logger.Panic().Err(err).Msg("unable to start http server")
|
||||
}
|
||||
}()
|
||||
s.logger.Warn().Msgf("http listening on %s", s.http.Addr)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Shutdown() error {
|
||||
return s.http.Shutdown(context.Background())
|
||||
}
|
@ -61,13 +61,13 @@ func (e *entry) Write(status, bytes int, elapsed time.Duration) {
|
||||
res["elapsed"] = float64(elapsed.Nanoseconds()) / 1000000.0
|
||||
|
||||
e.fields["res"] = res
|
||||
e.fields["module"] = "api"
|
||||
e.fields["module"] = "http"
|
||||
|
||||
if len(e.errors) > 0 {
|
||||
e.fields["errors"] = e.errors
|
||||
log.Error().Fields(e.fields).Msgf("Request failed (%d)", status)
|
||||
log.Error().Fields(e.fields).Msgf("request failed (%d)", status)
|
||||
} else {
|
||||
log.Debug().Fields(e.fields).Msgf("Request complete (%d)", status)
|
||||
log.Debug().Fields(e.fields).Msgf("request complete (%d)", status)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,9 +4,9 @@ package middleware
|
||||
// a pointer so it fits in an interface{} without allocation. This technique
|
||||
// for defining context keys was copied from Go 1.7's new use of context in net/http.
|
||||
type ctxKey struct {
|
||||
name string
|
||||
name string
|
||||
}
|
||||
|
||||
func (k *ctxKey) String() string {
|
||||
return "neko/ctx/" + k.name
|
||||
return "neko/ctx/" + k.name
|
||||
}
|
||||
|
@ -4,21 +4,21 @@ package middleware
|
||||
// https://github.com/zenazn/goji/tree/master/web/middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http"
|
||||
|
||||
"n.eko.moe/neko/internal/http/endpoint"
|
||||
"n.eko.moe/neko/internal/http/endpoint"
|
||||
)
|
||||
|
||||
func Recoverer(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if rvr := recover(); rvr != nil {
|
||||
endpoint.WriteError(w, r, rvr)
|
||||
}
|
||||
}()
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if rvr := recover(); rvr != nil {
|
||||
endpoint.WriteError(w, r, rvr)
|
||||
}
|
||||
}()
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
}
|
||||
|
||||
return http.HandlerFunc(fn)
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
|
@ -1,14 +1,14 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
// Key to use when setting the request ID.
|
||||
@ -37,19 +37,19 @@ var reqid uint64
|
||||
// than a millionth of a percent chance of generating two colliding IDs.
|
||||
|
||||
func init() {
|
||||
hostname, err := os.Hostname()
|
||||
if hostname == "" || err != nil {
|
||||
hostname = "localhost"
|
||||
}
|
||||
var buf [12]byte
|
||||
var b64 string
|
||||
for len(b64) < 10 {
|
||||
rand.Read(buf[:])
|
||||
b64 = base64.StdEncoding.EncodeToString(buf[:])
|
||||
b64 = strings.NewReplacer("+", "", "/", "").Replace(b64)
|
||||
}
|
||||
hostname, err := os.Hostname()
|
||||
if hostname == "" || err != nil {
|
||||
hostname = "localhost"
|
||||
}
|
||||
var buf [12]byte
|
||||
var b64 string
|
||||
for len(b64) < 10 {
|
||||
rand.Read(buf[:])
|
||||
b64 = base64.StdEncoding.EncodeToString(buf[:])
|
||||
b64 = strings.NewReplacer("+", "", "/", "").Replace(b64)
|
||||
}
|
||||
|
||||
prefix = fmt.Sprintf("%s/%s", hostname, b64[0:10])
|
||||
prefix = fmt.Sprintf("%s/%s", hostname, b64[0:10])
|
||||
}
|
||||
|
||||
// RequestID is a middleware that injects a request ID into the context of each
|
||||
@ -58,32 +58,32 @@ func init() {
|
||||
// process, and where the last number is an atomically incremented request
|
||||
// counter.
|
||||
func RequestID(next http.Handler) http.Handler {
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
requestID := r.Header.Get("X-Request-Id")
|
||||
if requestID == "" {
|
||||
myid := atomic.AddUint64(&reqid, 1)
|
||||
requestID = fmt.Sprintf("%s-%06d", prefix, myid)
|
||||
}
|
||||
ctx = context.WithValue(ctx, RequestIDKey, requestID)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
fn := func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
requestID := r.Header.Get("X-Request-Id")
|
||||
if requestID == "" {
|
||||
myid := atomic.AddUint64(&reqid, 1)
|
||||
requestID = fmt.Sprintf("%s-%06d", prefix, myid)
|
||||
}
|
||||
ctx = context.WithValue(ctx, RequestIDKey, requestID)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
}
|
||||
return http.HandlerFunc(fn)
|
||||
}
|
||||
|
||||
// GetReqID returns a request ID from the given context if one is present.
|
||||
// Returns the empty string if a request ID cannot be found.
|
||||
func GetReqID(ctx context.Context) string {
|
||||
if ctx == nil {
|
||||
return ""
|
||||
}
|
||||
if reqID, ok := ctx.Value(RequestIDKey).(string); ok {
|
||||
return reqID
|
||||
}
|
||||
return ""
|
||||
if ctx == nil {
|
||||
return ""
|
||||
}
|
||||
if reqID, ok := ctx.Value(RequestIDKey).(string); ok {
|
||||
return reqID
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// NextRequestID generates the next request ID in the sequence.
|
||||
func NextRequestID() uint64 {
|
||||
return atomic.AddUint64(&reqid, 1)
|
||||
return atomic.AddUint64(&reqid, 1)
|
||||
}
|
||||
|
@ -17,7 +17,7 @@ func JSON(w http.ResponseWriter, data interface{}, status int) error {
|
||||
if err != nil {
|
||||
return &endpoint.HandlerError{
|
||||
Status: http.StatusInternalServerError,
|
||||
Message: "Unable to write JSON response",
|
||||
Message: "unable to write JSON response",
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
@ -1,203 +0,0 @@
|
||||
package keys
|
||||
|
||||
const KEY_0 = 48
|
||||
const KEY_1 = 49
|
||||
const KEY_2 = 50
|
||||
const KEY_3 = 51
|
||||
const KEY_4 = 52
|
||||
const KEY_5 = 53
|
||||
const KEY_6 = 54
|
||||
const KEY_7 = 55
|
||||
const KEY_8 = 56
|
||||
const KEY_9 = 57
|
||||
|
||||
const KEY_A = 65
|
||||
const KEY_B = 66
|
||||
const KEY_C = 67
|
||||
const KEY_D = 68
|
||||
const KEY_E = 69
|
||||
const KEY_F = 70
|
||||
const KEY_G = 71
|
||||
const KEY_H = 72
|
||||
const KEY_I = 73
|
||||
const KEY_J = 74
|
||||
const KEY_K = 75
|
||||
const KEY_L = 76
|
||||
const KEY_M = 77
|
||||
const KEY_N = 78
|
||||
const KEY_O = 79
|
||||
const KEY_P = 80
|
||||
const KEY_Q = 81
|
||||
const KEY_R = 82
|
||||
const KEY_S = 83
|
||||
const KEY_T = 84
|
||||
const KEY_U = 85
|
||||
const KEY_V = 86
|
||||
const KEY_W = 87
|
||||
const KEY_X = 88
|
||||
const KEY_Y = 89
|
||||
const KEY_Z = 90
|
||||
|
||||
const KEY_NUMPAD0 = 96
|
||||
const KEY_NUMPAD1 = 97
|
||||
const KEY_NUMPAD2 = 98
|
||||
const KEY_NUMPAD3 = 99
|
||||
const KEY_NUMPAD4 = 100
|
||||
const KEY_NUMPAD5 = 101
|
||||
const KEY_NUMPAD6 = 102
|
||||
const KEY_NUMPAD7 = 103
|
||||
const KEY_NUMPAD8 = 104
|
||||
const KEY_NUMPAD9 = 105
|
||||
|
||||
const KEY_F1 = 112
|
||||
const KEY_F2 = 113
|
||||
const KEY_F3 = 114
|
||||
const KEY_F4 = 115
|
||||
const KEY_F5 = 116
|
||||
const KEY_F6 = 117
|
||||
const KEY_F7 = 118
|
||||
const KEY_F8 = 119
|
||||
const KEY_F9 = 120
|
||||
const KEY_F10 = 121
|
||||
const KEY_F11 = 122
|
||||
const KEY_F12 = 123
|
||||
|
||||
const KEY_BACK_SPACE = 8
|
||||
const KEY_TAB = 9
|
||||
const KEY_ENTER = 13
|
||||
const KEY_ENTER_ALT = 14
|
||||
const KEY_SHIFT = 16
|
||||
const KEY_CONTROL = 17
|
||||
const KEY_ALT = 18
|
||||
const KEY_ESCAPE = 27
|
||||
const KEY_SPACE = 32
|
||||
const KEY_PAGE_UP = 33
|
||||
const KEY_PAGE_DOWN = 34
|
||||
const KEY_END = 35
|
||||
const KEY_LEFT = 37
|
||||
const KEY_UP = 38
|
||||
const KEY_RIGHT = 39
|
||||
const KEY_DOWN = 40
|
||||
const KEY_DELETE = 46
|
||||
const KEY_SEMICOLON = 59
|
||||
const KEY_SEMICOLON_ALT = 186
|
||||
const KEY_EQUALS = 61
|
||||
const KEY_EQUALS_ALT = 187
|
||||
const KEY_MULTIPLY = 106
|
||||
const KEY_ADD = 107
|
||||
const KEY_SEPARATOR = 108
|
||||
const KEY_SUBTRACT = 109
|
||||
const KEY_SUBTRACT_ALT = 189
|
||||
const KEY_DECIMAL = 110
|
||||
const KEY_DIVIDE = 111
|
||||
const KEY_COMMA = 188
|
||||
const KEY_PERIOD = 190
|
||||
const KEY_SLASH = 191
|
||||
const KEY_BACK_QUOTE = 192
|
||||
const KEY_BACK_SLASH = 220
|
||||
const KEY_OPEN_BRACKET = 219
|
||||
const KEY_CLOSE_BRACKET = 221
|
||||
const KEY_QUOTE = 222
|
||||
|
||||
var Keyboard = map[int]string{}
|
||||
|
||||
func init() {
|
||||
Keyboard[KEY_A] = "a"
|
||||
Keyboard[KEY_B] = "b"
|
||||
Keyboard[KEY_C] = "c"
|
||||
Keyboard[KEY_D] = "d"
|
||||
Keyboard[KEY_E] = "e"
|
||||
Keyboard[KEY_F] = "f"
|
||||
Keyboard[KEY_G] = "g"
|
||||
Keyboard[KEY_H] = "h"
|
||||
Keyboard[KEY_I] = "i"
|
||||
Keyboard[KEY_J] = "j"
|
||||
Keyboard[KEY_K] = "k"
|
||||
Keyboard[KEY_L] = "l"
|
||||
Keyboard[KEY_M] = "m"
|
||||
Keyboard[KEY_N] = "n"
|
||||
Keyboard[KEY_O] = "o"
|
||||
Keyboard[KEY_P] = "p"
|
||||
Keyboard[KEY_Q] = "q"
|
||||
Keyboard[KEY_R] = "r"
|
||||
Keyboard[KEY_S] = "s"
|
||||
Keyboard[KEY_T] = "t"
|
||||
Keyboard[KEY_U] = "u"
|
||||
Keyboard[KEY_V] = "v"
|
||||
Keyboard[KEY_W] = "w"
|
||||
Keyboard[KEY_X] = "x"
|
||||
Keyboard[KEY_Y] = "y"
|
||||
Keyboard[KEY_Z] = "z"
|
||||
|
||||
Keyboard[KEY_0] = "0"
|
||||
Keyboard[KEY_1] = "1"
|
||||
Keyboard[KEY_2] = "2"
|
||||
Keyboard[KEY_3] = "3"
|
||||
Keyboard[KEY_4] = "4"
|
||||
Keyboard[KEY_5] = "5"
|
||||
Keyboard[KEY_6] = "6"
|
||||
Keyboard[KEY_7] = "7"
|
||||
Keyboard[KEY_8] = "8"
|
||||
Keyboard[KEY_9] = "9"
|
||||
|
||||
Keyboard[KEY_NUMPAD0] = "0"
|
||||
Keyboard[KEY_NUMPAD1] = "1"
|
||||
Keyboard[KEY_NUMPAD2] = "2"
|
||||
Keyboard[KEY_NUMPAD3] = "3"
|
||||
Keyboard[KEY_NUMPAD4] = "4"
|
||||
Keyboard[KEY_NUMPAD5] = "5"
|
||||
Keyboard[KEY_NUMPAD6] = "6"
|
||||
Keyboard[KEY_NUMPAD7] = "7"
|
||||
Keyboard[KEY_NUMPAD8] = "8"
|
||||
Keyboard[KEY_NUMPAD9] = "9"
|
||||
|
||||
Keyboard[KEY_F1] = "f1"
|
||||
Keyboard[KEY_F2] = "f2"
|
||||
Keyboard[KEY_F3] = "f3"
|
||||
Keyboard[KEY_F4] = "f4"
|
||||
Keyboard[KEY_F5] = "f5"
|
||||
Keyboard[KEY_F6] = "f6"
|
||||
Keyboard[KEY_F7] = "f7"
|
||||
Keyboard[KEY_F8] = "f8"
|
||||
Keyboard[KEY_F9] = "f9"
|
||||
Keyboard[KEY_F10] = "f10"
|
||||
Keyboard[KEY_F11] = "f11"
|
||||
Keyboard[KEY_F12] = "f12"
|
||||
|
||||
Keyboard[KEY_QUOTE] = "'"
|
||||
Keyboard[KEY_COMMA] = ","
|
||||
Keyboard[KEY_PERIOD] = "."
|
||||
Keyboard[KEY_SEMICOLON] = ";"
|
||||
Keyboard[KEY_SEMICOLON_ALT] = ";"
|
||||
Keyboard[KEY_SLASH] = "/"
|
||||
Keyboard[KEY_BACK_SLASH] = "\\"
|
||||
Keyboard[KEY_BACK_QUOTE] = "`"
|
||||
Keyboard[KEY_OPEN_BRACKET] = "["
|
||||
Keyboard[KEY_CLOSE_BRACKET] = "]"
|
||||
Keyboard[KEY_EQUALS] = "="
|
||||
Keyboard[KEY_EQUALS_ALT] = "="
|
||||
Keyboard[KEY_MULTIPLY] = "*"
|
||||
Keyboard[KEY_ADD] = "+"
|
||||
Keyboard[KEY_SEPARATOR] = "."
|
||||
Keyboard[KEY_SUBTRACT] = "-"
|
||||
Keyboard[KEY_SUBTRACT_ALT] = "-"
|
||||
Keyboard[KEY_DECIMAL] = "."
|
||||
Keyboard[KEY_DIVIDE] = "/"
|
||||
Keyboard[KEY_BACK_SPACE] = "backspace"
|
||||
Keyboard[KEY_DELETE] = "delete"
|
||||
Keyboard[KEY_ENTER] = "enter"
|
||||
Keyboard[KEY_ENTER_ALT] = "enter"
|
||||
Keyboard[KEY_TAB] = "tab"
|
||||
Keyboard[KEY_ESCAPE] = "escape"
|
||||
Keyboard[KEY_UP] = "up"
|
||||
Keyboard[KEY_DOWN] = "down"
|
||||
Keyboard[KEY_RIGHT] = "right"
|
||||
Keyboard[KEY_LEFT] = "left"
|
||||
Keyboard[KEY_END] = "end"
|
||||
Keyboard[KEY_PAGE_UP] = "pageup"
|
||||
Keyboard[KEY_PAGE_DOWN] = "pagedown"
|
||||
Keyboard[KEY_ALT] = "alt"
|
||||
Keyboard[KEY_CONTROL] = "control"
|
||||
Keyboard[KEY_SHIFT] = "shift"
|
||||
Keyboard[KEY_SPACE] = "space"
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package keys
|
||||
|
||||
const MOUSE_LEFT = 0
|
||||
const MOUSE_MIDDLE = 1
|
||||
const MOUSE_RIGHT = 2
|
||||
const MOUSE_WHEEL_UP = 4
|
||||
const MOUSE_WHEEL_DOWN = 5
|
||||
const MOUSE_WHEEL_RIGH = 6
|
||||
const MOUSE_WHEEL_LEFT = 7
|
||||
|
||||
var Mouse = map[int]string{}
|
||||
|
||||
func init() {
|
||||
Mouse[MOUSE_LEFT] = "left"
|
||||
Mouse[MOUSE_MIDDLE] = "center"
|
||||
Mouse[MOUSE_RIGHT] = "right"
|
||||
Mouse[MOUSE_WHEEL_UP] = "wheelUp"
|
||||
Mouse[MOUSE_WHEEL_DOWN] = "wheelDown"
|
||||
Mouse[MOUSE_WHEEL_RIGH] = "wheelRight"
|
||||
Mouse[MOUSE_WHEEL_LEFT] = "wheelLeft"
|
||||
}
|
15
server/internal/message/messages.go
Normal file
15
server/internal/message/messages.go
Normal file
@ -0,0 +1,15 @@
|
||||
package message
|
||||
|
||||
type Message struct {
|
||||
Event string `json:"event"`
|
||||
}
|
||||
|
||||
type IdentityProvide struct {
|
||||
Message
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type SDP struct {
|
||||
Message
|
||||
SDP string `json:"sdp"`
|
||||
}
|
@ -1,71 +0,0 @@
|
||||
package nanoid
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
gonanoid "github.com/matoous/go-nanoid"
|
||||
)
|
||||
|
||||
var nano *NanoID
|
||||
|
||||
func init() {
|
||||
nano = &NanoID{
|
||||
alphabet: "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ",
|
||||
size: 16,
|
||||
}
|
||||
}
|
||||
|
||||
func New(alphabet string, size int) *NanoID {
|
||||
return &NanoID{
|
||||
alphabet: alphabet,
|
||||
size: size,
|
||||
}
|
||||
}
|
||||
|
||||
type NanoID struct {
|
||||
alphabet string
|
||||
size int
|
||||
}
|
||||
|
||||
func (n *NanoID) NewID() (string, error) {
|
||||
return gonanoid.Generate(n.alphabet, n.size)
|
||||
}
|
||||
|
||||
func (n *NanoID) NewIDSize(size int) (string, error) {
|
||||
return gonanoid.Generate(n.alphabet, size)
|
||||
}
|
||||
|
||||
func (n *NanoID) NewIDRang(max int, min int) (string, error) {
|
||||
rand.Seed(time.Now().Unix())
|
||||
return gonanoid.Generate(n.alphabet, rand.Intn(max-min)+min)
|
||||
}
|
||||
|
||||
func (n *NanoID) GenerateID(alphabet string, size int) (string, error) {
|
||||
return gonanoid.Generate(alphabet, size)
|
||||
}
|
||||
|
||||
func (n *NanoID) GenerateIDRange(alphabet string, max int, min int) (string, error) {
|
||||
rand.Seed(time.Now().Unix())
|
||||
return gonanoid.Generate(alphabet, rand.Intn(max-min)+min)
|
||||
}
|
||||
|
||||
func NewID() (string, error) {
|
||||
return nano.NewID()
|
||||
}
|
||||
|
||||
func NewIDSize(size int) (string, error) {
|
||||
return nano.NewIDSize(size)
|
||||
}
|
||||
|
||||
func NewIDRang(max int, min int) (string, error) {
|
||||
return nano.NewIDRang(max, min)
|
||||
}
|
||||
|
||||
func GenerateID(alphabet string, size int) (string, error) {
|
||||
return nano.GenerateID(alphabet, size)
|
||||
}
|
||||
|
||||
func GenerateIDRange(alphabet string, max int, min int) (string, error) {
|
||||
return nano.GenerateIDRange(alphabet, max, min)
|
||||
}
|
@ -41,8 +41,8 @@ func Config(name string) {
|
||||
Logger()
|
||||
|
||||
if file == "" {
|
||||
logger.Warn().Msg("Preflight complete without config file")
|
||||
logger.Warn().Msg("preflight complete without config file")
|
||||
} else {
|
||||
logger.Info().Msg("Preflight complete")
|
||||
logger.Info().Msg("preflight complete")
|
||||
}
|
||||
}
|
||||
|
@ -42,17 +42,17 @@ func Logs(name string) {
|
||||
if err == nil {
|
||||
err = os.Rename(latest, filepath.Join(logs, "neko."+time.Now().Format("2006-01-02T15-04-05Z07-00")+".log"))
|
||||
if err != nil {
|
||||
log.Panic().Err(err).Msg("Failed to rotate log file")
|
||||
log.Panic().Err(err).Msg("failed to rotate log file")
|
||||
}
|
||||
}
|
||||
|
||||
logf, err := os.OpenFile(latest, os.O_RDWR|os.O_CREATE, 0666)
|
||||
if err != nil {
|
||||
log.Panic().Err(err).Msg("Failed to create log file")
|
||||
log.Panic().Err(err).Msg("failed to create log file")
|
||||
}
|
||||
|
||||
logger := diode.NewWriter(logf, 1000, 10*time.Millisecond, func(missed int) {
|
||||
fmt.Printf("Logger Dropped %d messages", missed)
|
||||
fmt.Printf("logger dropped %d messages", missed)
|
||||
})
|
||||
|
||||
log.Logger = log.Output(io.MultiWriter(console, logger))
|
||||
|
172
server/internal/session/manager.go
Normal file
172
server/internal/session/manager.go
Normal file
@ -0,0 +1,172 @@
|
||||
package session
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/kataras/go-events"
|
||||
"github.com/pion/webrtc/v2"
|
||||
|
||||
"n.eko.moe/neko/internal/utils"
|
||||
)
|
||||
|
||||
func New() *SessionManager {
|
||||
return &SessionManager{
|
||||
host: "",
|
||||
members: make(map[string]*Session),
|
||||
emmiter: events.New(),
|
||||
}
|
||||
}
|
||||
|
||||
type SessionManager struct {
|
||||
host string
|
||||
members map[string]*Session
|
||||
emmiter events.EventEmmiter
|
||||
}
|
||||
|
||||
func (m *SessionManager) New(id string, admin bool, socket *websocket.Conn) *Session {
|
||||
session := &Session{
|
||||
ID: id,
|
||||
Admin: admin,
|
||||
socket: socket,
|
||||
}
|
||||
|
||||
m.members[id] = session
|
||||
m.emmiter.Emit("created", id, session)
|
||||
|
||||
return session
|
||||
}
|
||||
|
||||
func (m *SessionManager) IsHost(id string) bool {
|
||||
return m.host == id
|
||||
}
|
||||
|
||||
func (m *SessionManager) HasHost() bool {
|
||||
return m.host != ""
|
||||
}
|
||||
|
||||
func (m *SessionManager) SetHost(id string) error {
|
||||
_, ok := m.members[id]
|
||||
if ok {
|
||||
m.host = id
|
||||
m.emmiter.Emit("host", id)
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("invalid session id %s", id)
|
||||
}
|
||||
|
||||
func (m *SessionManager) GetHost() (*Session, bool) {
|
||||
host, ok := m.members[m.host]
|
||||
return host, ok
|
||||
}
|
||||
|
||||
func (m *SessionManager) ClearHost() {
|
||||
id := m.host
|
||||
m.host = ""
|
||||
m.emmiter.Emit("host_cleared", id)
|
||||
}
|
||||
|
||||
func (m *SessionManager) Has(id string) bool {
|
||||
_, ok := m.members[id]
|
||||
return ok
|
||||
}
|
||||
|
||||
func (m *SessionManager) Get(id string) (*Session, bool) {
|
||||
session, ok := m.members[id]
|
||||
return session, ok
|
||||
}
|
||||
|
||||
func (m *SessionManager) Set(id string, session *Session) {
|
||||
m.members[id] = session
|
||||
}
|
||||
|
||||
func (m *SessionManager) Destroy(id string) error {
|
||||
session, ok := m.members[id]
|
||||
if ok {
|
||||
err := session.destroy()
|
||||
delete(m.members, id)
|
||||
m.emmiter.Emit("destroyed", id)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *SessionManager) SetSocket(id string, socket *websocket.Conn) (bool, error) {
|
||||
session, ok := m.members[id]
|
||||
if ok {
|
||||
session.socket = socket
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("invalid session id %s", id)
|
||||
}
|
||||
|
||||
func (m *SessionManager) SetPeer(id string, peer *webrtc.PeerConnection) (bool, error) {
|
||||
session, ok := m.members[id]
|
||||
if ok {
|
||||
session.peer = peer
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("invalid session id %s", id)
|
||||
}
|
||||
|
||||
func (m *SessionManager) SetName(id string, name string) (bool, error) {
|
||||
session, ok := m.members[id]
|
||||
if ok {
|
||||
session.Name = name
|
||||
return true, nil
|
||||
}
|
||||
|
||||
return false, fmt.Errorf("invalid session id %s", id)
|
||||
}
|
||||
|
||||
func (m *SessionManager) Clear() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *SessionManager) Brodcast(v interface{}, exclude interface{}) error {
|
||||
if exclude != nil {
|
||||
for id, sess := range m.members {
|
||||
if in, _ := utils.ArrayIn(id, exclude); in {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := sess.Send(v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for _, sess := range m.members {
|
||||
if err := sess.Send(v); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *SessionManager) OnHost(listener func(id string)) {
|
||||
m.emmiter.On("host", func(payload ...interface{}) {
|
||||
listener(payload[0].(string))
|
||||
})
|
||||
}
|
||||
|
||||
func (m *SessionManager) OnHostCleared(listener func(id string)) {
|
||||
m.emmiter.On("host_cleared", func(payload ...interface{}) {
|
||||
listener(payload[0].(string))
|
||||
})
|
||||
}
|
||||
|
||||
func (m *SessionManager) OnCreated(listener func(id string, session *Session)) {
|
||||
m.emmiter.On("created", func(payload ...interface{}) {
|
||||
listener(payload[0].(string), payload[1].(*Session))
|
||||
})
|
||||
}
|
||||
|
||||
func (m *SessionManager) OnDestroy(listener func(id string)) {
|
||||
m.emmiter.On("destroyed", func(payload ...interface{}) {
|
||||
listener(payload[0].(string))
|
||||
})
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package webrtc
|
||||
package session
|
||||
|
||||
import (
|
||||
"sync"
|
||||
@ -7,14 +7,23 @@ import (
|
||||
"github.com/pion/webrtc/v2"
|
||||
)
|
||||
|
||||
type session struct {
|
||||
id string
|
||||
type Session struct {
|
||||
ID string
|
||||
Name string
|
||||
Admin bool
|
||||
socket *websocket.Conn
|
||||
peer *webrtc.PeerConnection
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
func (session *session) send(v interface{}) error {
|
||||
// TODO: write to peer data channel
|
||||
func (session *Session) Write(v interface{}) error {
|
||||
session.mu.Lock()
|
||||
defer session.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (session *Session) Send(v interface{}) error {
|
||||
session.mu.Lock()
|
||||
defer session.mu.Unlock()
|
||||
|
||||
@ -25,7 +34,7 @@ func (session *session) send(v interface{}) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (session *session) destroy() error {
|
||||
func (session *Session) destroy() error {
|
||||
if session.peer != nil && session.peer.ConnectionState() == webrtc.PeerConnectionStateConnected {
|
||||
if err := session.peer.Close(); err != nil {
|
||||
return err
|
||||
@ -37,5 +46,6 @@ func (session *session) destroy() error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
package structs
|
||||
|
||||
import "fmt"
|
||||
|
||||
type Version struct {
|
||||
Major string
|
||||
Minor string
|
||||
Patch string
|
||||
Version string
|
||||
GitVersion string
|
||||
GitCommit string
|
||||
GitTreeState string
|
||||
BuildDate string
|
||||
GoVersion string
|
||||
Compiler string
|
||||
Platform string
|
||||
}
|
||||
|
||||
func (i *Version) String() string {
|
||||
return fmt.Sprintf("%s.%s.%s", i.Major, i.Minor, i.Patch)
|
||||
}
|
24
server/internal/utils/array.go
Normal file
24
server/internal/utils/array.go
Normal file
@ -0,0 +1,24 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func ArrayIn(val interface{}, array interface{}) (exists bool, index int) {
|
||||
exists = false
|
||||
index = -1
|
||||
|
||||
switch reflect.TypeOf(array).Kind() {
|
||||
case reflect.Slice:
|
||||
s := reflect.ValueOf(array)
|
||||
for i := 0; i < s.Len(); i++ {
|
||||
if reflect.DeepEqual(val, s.Index(i).Interface()) == true {
|
||||
index = i
|
||||
exists = true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
package utils
|
||||
|
||||
const Header = `&34
|
||||
_ __ __
|
||||
/ | / /__ / /______ \ /\
|
||||
/ |/ / _ \/ //_/ __ \ ) ( ')
|
||||
/ /| / __/ ,< / /_/ / ( / )
|
||||
/_/ |_/\___/_/|_|\____/ \(__)|
|
||||
&1&37 nurdism/neko &33%s v%s&0
|
||||
`
|
10
server/internal/utils/json.go
Normal file
10
server/internal/utils/json.go
Normal file
@ -0,0 +1,10 @@
|
||||
package utils
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
func Unmarshal(in interface{}, raw []byte, callback func() error) error {
|
||||
if err := json.Unmarshal(raw, &in); err != nil {
|
||||
return err
|
||||
}
|
||||
return callback()
|
||||
}
|
@ -1,25 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type CountedSyncMap struct {
|
||||
sync.Map
|
||||
len uint64
|
||||
}
|
||||
|
||||
func (m *CountedSyncMap) CountedDelete(key interface{}) {
|
||||
m.Delete(key)
|
||||
atomic.AddUint64(&m.len, ^uint64(0))
|
||||
}
|
||||
|
||||
func (m *CountedSyncMap) CountedStore(key, value interface{}) {
|
||||
m.Store(key, value)
|
||||
atomic.AddUint64(&m.len, uint64(1))
|
||||
}
|
||||
|
||||
func (m *CountedSyncMap) CountedLen() uint64 {
|
||||
return atomic.LoadUint64(&m.len)
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
package webrtc
|
||||
|
||||
type dataHeader struct {
|
||||
Event uint8
|
||||
Length uint16
|
||||
}
|
||||
|
||||
type dataMouseMove struct {
|
||||
dataHeader
|
||||
X int16
|
||||
Y int16
|
||||
}
|
||||
|
||||
type dataMouseKey struct {
|
||||
dataHeader
|
||||
Key uint8
|
||||
}
|
||||
|
||||
type dataKeyboardKey struct {
|
||||
dataHeader
|
||||
Key uint16
|
||||
}
|
138
server/internal/webrtc/handle.go
Normal file
138
server/internal/webrtc/handle.go
Normal file
@ -0,0 +1,138 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"strconv"
|
||||
|
||||
"github.com/pion/webrtc/v2"
|
||||
"n.eko.moe/neko/internal/hid"
|
||||
)
|
||||
|
||||
const OP_MOVE = 0x01
|
||||
const OP_SCROLL = 0x02
|
||||
const OP_KEY_DOWN = 0x03
|
||||
const OP_KEY_UP = 0x04
|
||||
const OP_KEY_CLK = 0x05
|
||||
|
||||
type PayloadHeader struct {
|
||||
Event uint8
|
||||
Length uint16
|
||||
}
|
||||
|
||||
type PayloadMove struct {
|
||||
PayloadHeader
|
||||
X uint16
|
||||
Y uint16
|
||||
}
|
||||
|
||||
type PayloadScroll struct {
|
||||
PayloadHeader
|
||||
X int16
|
||||
Y int16
|
||||
}
|
||||
|
||||
type PayloadKey struct {
|
||||
PayloadHeader
|
||||
Key uint16
|
||||
}
|
||||
|
||||
func (m *WebRTCManager) handle(id string, msg webrtc.DataChannelMessage) error {
|
||||
if !m.sessions.IsHost(id) {
|
||||
return nil
|
||||
}
|
||||
|
||||
buffer := bytes.NewBuffer(msg.Data)
|
||||
header := &PayloadHeader{}
|
||||
hbytes := make([]byte, 3)
|
||||
|
||||
if _, err := buffer.Read(hbytes); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := binary.Read(bytes.NewBuffer(hbytes), binary.LittleEndian, header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buffer = bytes.NewBuffer(msg.Data)
|
||||
|
||||
switch header.Event {
|
||||
case OP_MOVE:
|
||||
payload := &PayloadMove{}
|
||||
if err := binary.Read(buffer, binary.LittleEndian, payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
hid.Move(int(payload.X), int(payload.Y))
|
||||
break
|
||||
case OP_SCROLL:
|
||||
payload := &PayloadScroll{}
|
||||
if err := binary.Read(buffer, binary.LittleEndian, payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.logger.
|
||||
Debug().
|
||||
Str("x", strconv.Itoa(int(payload.X))).
|
||||
Str("y", strconv.Itoa(int(payload.Y))).
|
||||
Msg("scroll")
|
||||
|
||||
hid.Scroll(int(payload.X), int(payload.Y))
|
||||
break
|
||||
case OP_KEY_DOWN:
|
||||
payload := &PayloadKey{}
|
||||
if err := binary.Read(buffer, binary.LittleEndian, payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if payload.Key < 8 {
|
||||
button, err := hid.ButtonDown(int(payload.Key))
|
||||
if err != nil {
|
||||
m.logger.Warn().Err(err).Msg("key down failed")
|
||||
return nil
|
||||
}
|
||||
|
||||
m.logger.Debug().Msgf("button down %s(%d)", button.Name, payload.Key)
|
||||
} else {
|
||||
key, err := hid.KeyDown(int(payload.Key))
|
||||
if err != nil {
|
||||
m.logger.Warn().Err(err).Msg("key down failed")
|
||||
return nil
|
||||
}
|
||||
|
||||
m.logger.Debug().Msgf("key down %s(%d)", key.Name, payload.Key)
|
||||
}
|
||||
|
||||
break
|
||||
case OP_KEY_UP:
|
||||
payload := &PayloadKey{}
|
||||
err := binary.Read(buffer, binary.LittleEndian, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if payload.Key < 8 {
|
||||
button, err := hid.ButtonUp(int(payload.Key))
|
||||
if err != nil {
|
||||
m.logger.Warn().Err(err).Msg("button up failed")
|
||||
return nil
|
||||
}
|
||||
|
||||
m.logger.Debug().Msgf("button up %s(%d)", button.Name, payload.Key)
|
||||
} else {
|
||||
key, err := hid.KeyUp(int(payload.Key))
|
||||
if err != nil {
|
||||
m.logger.Warn().Err(err).Msg("keyup failed")
|
||||
return nil
|
||||
}
|
||||
|
||||
m.logger.Debug().Msgf("key up %s(%d)", key.Name, payload.Key)
|
||||
}
|
||||
break
|
||||
case OP_KEY_CLK:
|
||||
// unused
|
||||
break
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
32
server/internal/webrtc/logger.go
Normal file
32
server/internal/webrtc/logger.go
Normal file
@ -0,0 +1,32 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"github.com/pion/logging"
|
||||
"github.com/rs/zerolog"
|
||||
)
|
||||
|
||||
type logger struct {
|
||||
logger zerolog.Logger
|
||||
}
|
||||
|
||||
func (l logger) Trace(msg string) { l.logger.Trace().Msg(msg) }
|
||||
func (l logger) Tracef(format string, args ...interface{}) { l.logger.Trace().Msgf(format, args...) }
|
||||
func (l logger) Debug(msg string) { l.logger.Debug().Msg(msg) }
|
||||
func (l logger) Debugf(format string, args ...interface{}) { l.logger.Debug().Msgf(format, args...) }
|
||||
func (l logger) Info(msg string) { l.logger.Info().Msg(msg) }
|
||||
func (l logger) Infof(format string, args ...interface{}) { l.logger.Info().Msgf(format, args...) }
|
||||
func (l logger) Warn(msg string) { l.logger.Warn().Msg(msg) }
|
||||
func (l logger) Warnf(format string, args ...interface{}) { l.logger.Warn().Msgf(format, args...) }
|
||||
func (l logger) Error(msg string) { l.logger.Error().Msg(msg) }
|
||||
func (l logger) Errorf(format string, args ...interface{}) { l.logger.Error().Msgf(format, args...) }
|
||||
|
||||
type loggerFactory struct {
|
||||
logger zerolog.Logger
|
||||
}
|
||||
|
||||
func (l loggerFactory) NewLogger(subsystem string) logging.LeveledLogger {
|
||||
l.logger.Debug().Msgf("creating logger for %s", subsystem)
|
||||
return logger{
|
||||
logger: l.logger.With().Str("subsystem", subsystem).Logger(),
|
||||
}
|
||||
}
|
@ -1,61 +1,42 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pion/webrtc/v2"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"n.eko.moe/neko/internal/config"
|
||||
"n.eko.moe/neko/internal/event"
|
||||
"n.eko.moe/neko/internal/gst"
|
||||
"n.eko.moe/neko/internal/hid"
|
||||
"n.eko.moe/neko/internal/message"
|
||||
"n.eko.moe/neko/internal/session"
|
||||
)
|
||||
|
||||
func NewManager(password string) (*WebRTCManager, error) {
|
||||
func New(sessions *session.SessionManager, conf *config.WebRTC) *WebRTCManager {
|
||||
logger := log.With().Str("module", "webrtc").Logger()
|
||||
engine := webrtc.MediaEngine{}
|
||||
engine.RegisterDefaultCodecs()
|
||||
|
||||
videoCodec := webrtc.NewRTPVP8Codec(webrtc.DefaultPayloadTypeVP8, 90000)
|
||||
video, err := webrtc.NewTrack(webrtc.DefaultPayloadTypeVP8, rand.Uint32(), "stream", "stream", videoCodec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
setings := webrtc.SettingEngine{
|
||||
LoggerFactory: loggerFactory{
|
||||
logger: logger,
|
||||
},
|
||||
}
|
||||
engine.RegisterCodec(videoCodec)
|
||||
|
||||
audioCodec := webrtc.NewRTPOpusCodec(webrtc.DefaultPayloadTypeOpus, 48000)
|
||||
audio, err := webrtc.NewTrack(webrtc.DefaultPayloadTypeOpus, rand.Uint32(), "stream", "stream", audioCodec)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
engine.RegisterCodec(audioCodec)
|
||||
|
||||
videoPipeline := gst.CreatePipeline(webrtc.VP8, []*webrtc.Track{video}, "ximagesrc show-pointer=true use-damage=false ! video/x-raw,framerate=30/1 ! videoconvert")
|
||||
// ximagesrc xid=0 show-pointer=true ! videoconvert ! queue | videotestsrc
|
||||
|
||||
audioPipeline := gst.CreatePipeline(webrtc.Opus, []*webrtc.Track{audio}, "pulsesrc device=auto_null.monitor ! audioconvert")
|
||||
// pulsesrc device=auto_null.monitor ! audioconvert | audiotestsrc
|
||||
// gst-launch-1.0 -v pulsesrc device=auto_null.monitor ! audioconvert ! vorbisenc ! oggmux ! filesink location=alsasrc.ogg
|
||||
|
||||
return &WebRTCManager{
|
||||
logger: log.With().Str("service", "webrtc").Logger(),
|
||||
engine: engine,
|
||||
api: webrtc.NewAPI(webrtc.WithMediaEngine(engine)),
|
||||
video: video,
|
||||
videoPipeline: videoPipeline,
|
||||
audio: audio,
|
||||
audioPipeline: audioPipeline,
|
||||
controller: "",
|
||||
password: password,
|
||||
sessions: make(map[string]*session),
|
||||
debounce: make(map[int]time.Time),
|
||||
cleanup: time.NewTicker(500 * time.Second),
|
||||
shutdown: make(chan bool),
|
||||
upgrader: websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
},
|
||||
logger: logger,
|
||||
engine: engine,
|
||||
setings: setings,
|
||||
api: webrtc.NewAPI(webrtc.WithMediaEngine(engine), webrtc.WithSettingEngine(setings)),
|
||||
cleanup: time.NewTicker(1 * time.Second),
|
||||
shutdown: make(chan bool),
|
||||
sessions: sessions,
|
||||
conf: conf,
|
||||
config: webrtc.Configuration{
|
||||
ICEServers: []webrtc.ICEServer{
|
||||
{
|
||||
@ -64,49 +45,182 @@ func NewManager(password string) (*WebRTCManager, error) {
|
||||
},
|
||||
SDPSemantics: webrtc.SDPSemanticsUnifiedPlanWithFallback,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
type WebRTCManager struct {
|
||||
logger zerolog.Logger
|
||||
upgrader websocket.Upgrader
|
||||
engine webrtc.MediaEngine
|
||||
api *webrtc.API
|
||||
setings webrtc.SettingEngine
|
||||
config webrtc.Configuration
|
||||
password string
|
||||
controller string
|
||||
sessions map[string]*session
|
||||
debounce map[int]time.Time
|
||||
shutdown chan bool
|
||||
cleanup *time.Ticker
|
||||
sessions *session.SessionManager
|
||||
api *webrtc.API
|
||||
video *webrtc.Track
|
||||
audio *webrtc.Track
|
||||
videoPipeline *gst.Pipeline
|
||||
audioPipeline *gst.Pipeline
|
||||
cleanup *time.Ticker
|
||||
conf *config.WebRTC
|
||||
shutdown chan bool
|
||||
}
|
||||
|
||||
func (manager *WebRTCManager) Start() error {
|
||||
manager.videoPipeline.Start()
|
||||
manager.audioPipeline.Start()
|
||||
func (m *WebRTCManager) Start() {
|
||||
|
||||
hid.Display(m.conf.Display)
|
||||
|
||||
switch m.conf.VideoCodec {
|
||||
case "vp8":
|
||||
if err := m.createVideoTrack(webrtc.DefaultPayloadTypeVP8); err != nil {
|
||||
m.logger.Panic().Err(err).Msg("unable to start webrtc manager")
|
||||
}
|
||||
case "vp9":
|
||||
if err := m.createVideoTrack(webrtc.DefaultPayloadTypeVP9); err != nil {
|
||||
m.logger.Panic().Err(err).Msg("unable to start webrtc manager")
|
||||
}
|
||||
case "h264":
|
||||
if err := m.createVideoTrack(webrtc.DefaultPayloadTypeH264); err != nil {
|
||||
m.logger.Panic().Err(err).Msg("unable to start webrtc manager")
|
||||
}
|
||||
default:
|
||||
m.logger.Panic().Err(errors.Errorf("unknown video codec %s", m.conf.AudioCodec)).Msg("unable to start webrtc manager")
|
||||
}
|
||||
|
||||
switch m.conf.AudioCodec {
|
||||
case "opus":
|
||||
if err := m.createAudioTrack(webrtc.DefaultPayloadTypeOpus); err != nil {
|
||||
m.logger.Panic().Err(err).Msg("unable to start webrtc manager")
|
||||
}
|
||||
case "g722":
|
||||
if err := m.createAudioTrack(webrtc.DefaultPayloadTypeG722); err != nil {
|
||||
m.logger.Panic().Err(err).Msg("unable to start webrtc manager")
|
||||
}
|
||||
case "pcmu":
|
||||
if err := m.createAudioTrack(webrtc.DefaultPayloadTypePCMU); err != nil {
|
||||
m.logger.Panic().Err(err).Msg("unable to start webrtc manager")
|
||||
}
|
||||
case "pcma":
|
||||
if err := m.createAudioTrack(webrtc.DefaultPayloadTypePCMA); err != nil {
|
||||
m.logger.Panic().Err(err).Msg("unable to start webrtc manager")
|
||||
}
|
||||
default:
|
||||
m.logger.Panic().Err(errors.Errorf("unknown audio codec %s", m.conf.AudioCodec)).Msg("unable to start webrtc manager")
|
||||
}
|
||||
|
||||
m.videoPipeline.Start()
|
||||
m.audioPipeline.Start()
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
m.logger.Info().Msg("shutdown")
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-manager.shutdown:
|
||||
case <-m.shutdown:
|
||||
return
|
||||
case <-manager.cleanup.C:
|
||||
manager.checkKeys()
|
||||
case <-m.cleanup.C:
|
||||
hid.Check(time.Second * 10)
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
m.sessions.OnHostCleared(func(id string) {
|
||||
hid.Reset()
|
||||
})
|
||||
|
||||
m.sessions.OnCreated(func(id string, session *session.Session) {
|
||||
m.logger.Debug().Str("id", id).Msg("session created")
|
||||
})
|
||||
|
||||
m.sessions.OnDestroy(func(id string) {
|
||||
m.logger.Debug().Str("id", id).Msg("session destroyed")
|
||||
})
|
||||
|
||||
// TODO: log resolution, bit rate and codec parameters
|
||||
m.logger.Info().
|
||||
Str("video_display", m.conf.Display).
|
||||
Str("video_codec", m.conf.VideoCodec).
|
||||
Str("audio_device", m.conf.Device).
|
||||
Str("audio_codec", m.conf.AudioCodec).
|
||||
Msgf("webrtc streaming")
|
||||
}
|
||||
|
||||
func (m *WebRTCManager) Shutdown() error {
|
||||
m.logger.Info().Msgf("webrtc shutting down")
|
||||
|
||||
m.cleanup.Stop()
|
||||
m.shutdown <- true
|
||||
m.videoPipeline.Stop()
|
||||
m.audioPipeline.Stop()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *WebRTCManager) Shutdown() error {
|
||||
manager.cleanup.Stop()
|
||||
manager.shutdown <- true
|
||||
manager.videoPipeline.Stop()
|
||||
manager.audioPipeline.Stop()
|
||||
func (m *WebRTCManager) CreatePeer(id string, sdp string) error {
|
||||
session, ok := m.sessions.Get(id)
|
||||
if !ok {
|
||||
return fmt.Errorf("invalid session id %s", id)
|
||||
}
|
||||
|
||||
peer, err := m.api.NewPeerConnection(m.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := peer.AddTransceiverFromTrack(m.video, webrtc.RtpTransceiverInit{
|
||||
Direction: webrtc.RTPTransceiverDirectionSendonly,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := peer.AddTransceiverFromTrack(m.audio, webrtc.RtpTransceiverInit{
|
||||
Direction: webrtc.RTPTransceiverDirectionSendonly,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
peer.SetRemoteDescription(webrtc.SessionDescription{
|
||||
SDP: sdp,
|
||||
Type: webrtc.SDPTypeOffer,
|
||||
})
|
||||
|
||||
answer, err := peer.CreateAnswer(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = peer.SetLocalDescription(answer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := session.Send(message.SDP{
|
||||
Message: message.Message{Event: event.SDP_REPLY},
|
||||
SDP: answer.SDP,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
peer.OnDataChannel(func(d *webrtc.DataChannel) {
|
||||
d.OnMessage(func(msg webrtc.DataChannelMessage) {
|
||||
if err = m.handle(id, msg); err != nil {
|
||||
m.logger.Warn().Err(err).Msg("data handle failed")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
peer.OnConnectionStateChange(func(connectionState webrtc.PeerConnectionState) {
|
||||
switch connectionState {
|
||||
case webrtc.PeerConnectionStateDisconnected:
|
||||
case webrtc.PeerConnectionStateFailed:
|
||||
m.logger.Info().Str("id", id).Msg("peer disconnected")
|
||||
m.sessions.Destroy(id)
|
||||
break
|
||||
case webrtc.PeerConnectionStateConnected:
|
||||
m.logger.Info().Str("id", id).Msg("peer connected")
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
m.sessions.SetPeer(id, peer)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -1,15 +0,0 @@
|
||||
package webrtc
|
||||
|
||||
type message struct {
|
||||
Event string `json:"event"`
|
||||
}
|
||||
|
||||
type messageIdentityProvide struct {
|
||||
message
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type messageSDP struct {
|
||||
message
|
||||
SDP string `json:"sdp"`
|
||||
}
|
@ -1,266 +0,0 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/go-vgo/robotgo"
|
||||
"github.com/pion/webrtc/v2"
|
||||
|
||||
"n.eko.moe/neko/internal/keys"
|
||||
)
|
||||
|
||||
func (manager *WebRTCManager) createPeer(session *session, raw []byte) error {
|
||||
payload := messageSDP{}
|
||||
if err := json.Unmarshal(raw, &payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
peer, err := manager.api.NewPeerConnection(manager.config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = peer.AddTrack(manager.video)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = peer.AddTrack(manager.audio)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
peer.SetRemoteDescription(webrtc.SessionDescription{
|
||||
SDP: payload.SDP,
|
||||
Type: webrtc.SDPTypeOffer,
|
||||
})
|
||||
|
||||
answer, err := peer.CreateAnswer(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = peer.SetLocalDescription(answer); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session.send(messageSDP{
|
||||
message{Event: "sdp/reply"},
|
||||
answer.SDP,
|
||||
})
|
||||
|
||||
session.peer = peer
|
||||
|
||||
peer.OnDataChannel(func(d *webrtc.DataChannel) {
|
||||
d.OnMessage(func(msg webrtc.DataChannelMessage) {
|
||||
if err = manager.onData(session, msg); err != nil {
|
||||
manager.logger.Warn().Err(err).Msg("onData failed")
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
peer.OnConnectionStateChange(func(connectionState webrtc.PeerConnectionState) {
|
||||
switch connectionState {
|
||||
case webrtc.PeerConnectionStateDisconnected:
|
||||
case webrtc.PeerConnectionStateFailed:
|
||||
manager.destroy(session)
|
||||
break
|
||||
case webrtc.PeerConnectionStateConnected:
|
||||
manager.logger.Info().Str("ID", session.id).Msg("Peer connected")
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *WebRTCManager) onData(session *session, msg webrtc.DataChannelMessage) error {
|
||||
if manager.controller != session.id {
|
||||
return nil
|
||||
}
|
||||
|
||||
header := &dataHeader{}
|
||||
buffer := bytes.NewBuffer(msg.Data)
|
||||
byt := make([]byte, 3)
|
||||
_, err := buffer.Read(byt)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = binary.Read(bytes.NewBuffer(byt), binary.LittleEndian, header)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
buffer = bytes.NewBuffer(msg.Data)
|
||||
|
||||
switch header.Event {
|
||||
case 0x01: // MOUSE_MOVE
|
||||
payload := &dataMouseMove{}
|
||||
if err := binary.Read(buffer, binary.LittleEndian, payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
robotgo.Move(int(payload.X), int(payload.Y))
|
||||
break
|
||||
case 0x02: // MOUSE_UP
|
||||
payload := &dataMouseKey{}
|
||||
if err := binary.Read(buffer, binary.LittleEndian, payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
code := int(payload.Key)
|
||||
if key, ok := keys.Mouse[code]; ok {
|
||||
if _, ok := manager.debounce[code]; !ok {
|
||||
return nil
|
||||
}
|
||||
delete(manager.debounce, code)
|
||||
robotgo.MouseToggle("up", key)
|
||||
manager.logger.Debug().Msgf("MOUSE_UP key: %v (%v)", code, key)
|
||||
} else {
|
||||
manager.logger.Warn().Msgf("Unknown MOUSE_UP key: %v", code)
|
||||
}
|
||||
break
|
||||
case 0x03: // MOUSE_DOWN
|
||||
payload := &dataMouseKey{}
|
||||
err := binary.Read(buffer, binary.LittleEndian, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
code := int(payload.Key)
|
||||
if key, ok := keys.Mouse[code]; ok {
|
||||
if _, ok := manager.debounce[code]; ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
manager.debounce[code] = time.Now()
|
||||
robotgo.MouseToggle("down", key)
|
||||
manager.logger.Debug().Msgf("MOUSE_DOWN key: %v (%v)", code, key)
|
||||
} else {
|
||||
manager.logger.Warn().Msgf("Unknown MOUSE_DOWN key: %v", code)
|
||||
}
|
||||
break
|
||||
case 0x04: // MOUSE_CLK
|
||||
payload := &dataMouseKey{}
|
||||
err := binary.Read(buffer, binary.LittleEndian, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
code := int(payload.Key)
|
||||
if key, ok := keys.Mouse[code]; ok {
|
||||
switch code {
|
||||
case keys.MOUSE_WHEEL_DOWN:
|
||||
robotgo.Scroll(0, -1)
|
||||
break
|
||||
case keys.MOUSE_WHEEL_UP:
|
||||
robotgo.Scroll(0, 1)
|
||||
break
|
||||
case keys.MOUSE_WHEEL_LEFT:
|
||||
robotgo.Scroll(-1, 0)
|
||||
break
|
||||
case keys.MOUSE_WHEEL_RIGH:
|
||||
robotgo.Scroll(1, 0)
|
||||
break
|
||||
default:
|
||||
robotgo.Click(key, false)
|
||||
}
|
||||
|
||||
manager.logger.Debug().Msgf("MOUSE_CLK key: %v (%v)", code, key)
|
||||
} else {
|
||||
manager.logger.Warn().Msgf("Unknown MOUSE_CLK key: %v", code)
|
||||
}
|
||||
break
|
||||
case 0x05: // KEY_DOWN
|
||||
payload := &dataKeyboardKey{}
|
||||
err := binary.Read(buffer, binary.LittleEndian, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
code := int(payload.Key)
|
||||
if key, ok := keys.Keyboard[code]; ok {
|
||||
if _, ok := manager.debounce[code]; ok {
|
||||
return nil
|
||||
}
|
||||
manager.debounce[code] = time.Now()
|
||||
robotgo.KeyToggle(key, "down")
|
||||
manager.logger.Debug().Msgf("KEY_DOWN key: %v (%v)", code, key)
|
||||
} else {
|
||||
manager.logger.Warn().Msgf("Unknown KEY_DOWN key: %v", code)
|
||||
}
|
||||
break
|
||||
case 0x06: // KEY_UP
|
||||
payload := &dataKeyboardKey{}
|
||||
if err := binary.Read(buffer, binary.LittleEndian, payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
code := int(payload.Key)
|
||||
if key, ok := keys.Keyboard[code]; ok {
|
||||
if _, ok := manager.debounce[code]; !ok {
|
||||
return nil
|
||||
}
|
||||
delete(manager.debounce, code)
|
||||
robotgo.KeyToggle(key, "up")
|
||||
manager.logger.Debug().Msgf("KEY_UP key: %v (%v)", code, key)
|
||||
} else {
|
||||
manager.logger.Warn().Msgf("Unknown KEY_UP key: %v", code)
|
||||
}
|
||||
break
|
||||
case 0x07: // KEY_CLK
|
||||
payload := &dataKeyboardKey{}
|
||||
err := binary.Read(buffer, binary.LittleEndian, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
code := int(payload.Key)
|
||||
if key, ok := keys.Keyboard[code]; ok {
|
||||
robotgo.KeyTap(key)
|
||||
manager.logger.Debug().Msgf("KEY_CLK key: %v (%v)", code, key)
|
||||
} else {
|
||||
manager.logger.Warn().Msgf("Unknown KEY_CLK key: %v", code)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *WebRTCManager) clearKeys() {
|
||||
for code := range manager.debounce {
|
||||
if key, ok := keys.Keyboard[code]; ok {
|
||||
robotgo.MouseToggle(key, "up")
|
||||
}
|
||||
|
||||
if key, ok := keys.Mouse[code]; ok {
|
||||
robotgo.KeyToggle(key, "up")
|
||||
}
|
||||
|
||||
delete(manager.debounce, code)
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *WebRTCManager) checkKeys() {
|
||||
t := time.Now()
|
||||
for code, start := range manager.debounce {
|
||||
if t.Sub(start) < (time.Second * 10) {
|
||||
continue
|
||||
}
|
||||
|
||||
if key, ok := keys.Keyboard[code]; ok {
|
||||
robotgo.MouseToggle(key, "up")
|
||||
}
|
||||
|
||||
if key, ok := keys.Mouse[code]; ok {
|
||||
robotgo.KeyToggle(key, "up")
|
||||
}
|
||||
|
||||
delete(manager.debounce, code)
|
||||
}
|
||||
}
|
99
server/internal/webrtc/tracks.go
Normal file
99
server/internal/webrtc/tracks.go
Normal file
@ -0,0 +1,99 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
|
||||
"github.com/pion/webrtc/v2"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"n.eko.moe/neko/internal/gst"
|
||||
)
|
||||
|
||||
func (m *WebRTCManager) createVideoTrack(payloadType uint8) error {
|
||||
|
||||
clockrate := uint32(90000)
|
||||
var codec *webrtc.RTPCodec
|
||||
switch payloadType {
|
||||
case webrtc.DefaultPayloadTypeVP8:
|
||||
codec = webrtc.NewRTPVP8Codec(payloadType, clockrate)
|
||||
break
|
||||
case webrtc.DefaultPayloadTypeVP9:
|
||||
codec = webrtc.NewRTPVP9Codec(payloadType, clockrate)
|
||||
break
|
||||
case webrtc.DefaultPayloadTypeH264:
|
||||
codec = webrtc.NewRTPH264Codec(payloadType, clockrate)
|
||||
break
|
||||
default:
|
||||
return errors.Errorf("unknown video codec %s", payloadType)
|
||||
}
|
||||
|
||||
track, err := webrtc.NewTrack(payloadType, rand.Uint32(), "stream", "stream", codec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var pipeline *gst.Pipeline
|
||||
src := fmt.Sprintf("ximagesrc xid=%s show-pointer=true use-damage=false ! video/x-raw,framerate=30/1 ! videoconvert ! queue", m.conf.Display)
|
||||
switch payloadType {
|
||||
case webrtc.DefaultPayloadTypeVP8:
|
||||
pipeline = gst.CreatePipeline(webrtc.VP8, []*webrtc.Track{track}, src)
|
||||
break
|
||||
case webrtc.DefaultPayloadTypeVP9:
|
||||
pipeline = gst.CreatePipeline(webrtc.VP9, []*webrtc.Track{track}, src)
|
||||
break
|
||||
case webrtc.DefaultPayloadTypeH264:
|
||||
pipeline = gst.CreatePipeline(webrtc.H264, []*webrtc.Track{track}, src)
|
||||
break
|
||||
}
|
||||
|
||||
m.video = track
|
||||
m.videoPipeline = pipeline
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *WebRTCManager) createAudioTrack(payloadType uint8) error {
|
||||
var codec *webrtc.RTPCodec
|
||||
switch payloadType {
|
||||
case webrtc.DefaultPayloadTypeOpus:
|
||||
codec = webrtc.NewRTPOpusCodec(payloadType, 48000)
|
||||
break
|
||||
case webrtc.DefaultPayloadTypeG722:
|
||||
codec = webrtc.NewRTPG722Codec(payloadType, 48000)
|
||||
break
|
||||
case webrtc.DefaultPayloadTypePCMU:
|
||||
codec = webrtc.NewRTPPCMUCodec(payloadType, 8000)
|
||||
break
|
||||
case webrtc.DefaultPayloadTypePCMA:
|
||||
codec = webrtc.NewRTPPCMACodec(payloadType, 8000)
|
||||
break
|
||||
default:
|
||||
return errors.Errorf("unknown audio codec %s", payloadType)
|
||||
}
|
||||
|
||||
track, err := webrtc.NewTrack(payloadType, rand.Uint32(), "stream", "stream", codec)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var pipeline *gst.Pipeline
|
||||
src := fmt.Sprintf("pulsesrc device=%s ! audioconvert", m.conf.Device)
|
||||
switch payloadType {
|
||||
case webrtc.DefaultPayloadTypeOpus:
|
||||
pipeline = gst.CreatePipeline(webrtc.Opus, []*webrtc.Track{track}, src)
|
||||
break
|
||||
case webrtc.DefaultPayloadTypeG722:
|
||||
pipeline = gst.CreatePipeline(webrtc.G722, []*webrtc.Track{track}, src)
|
||||
break
|
||||
case webrtc.DefaultPayloadTypePCMU:
|
||||
pipeline = gst.CreatePipeline(webrtc.PCMU, []*webrtc.Track{track}, src)
|
||||
break
|
||||
case webrtc.DefaultPayloadTypePCMA:
|
||||
pipeline = gst.CreatePipeline(webrtc.PCMA, []*webrtc.Track{track}, src)
|
||||
break
|
||||
}
|
||||
|
||||
m.audio = track
|
||||
m.audioPipeline = pipeline
|
||||
return nil
|
||||
}
|
@ -1,228 +0,0 @@
|
||||
package webrtc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"n.eko.moe/neko/internal/nanoid"
|
||||
)
|
||||
|
||||
const (
|
||||
// Send pings to peer with this period. Must be less than pongWait.
|
||||
pingPeriod = 60 * time.Second
|
||||
)
|
||||
|
||||
func (manager *WebRTCManager) Upgrade(w http.ResponseWriter, r *http.Request) error {
|
||||
manager.logger.
|
||||
Info().
|
||||
Msg("Attempting to upgrade ws")
|
||||
|
||||
socket, err := manager.upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
manager.logger.Error().Err(err).Msg("Failed to upgrade websocket!")
|
||||
return err
|
||||
}
|
||||
|
||||
sessionID, ok := manager.authenticate(r)
|
||||
if ok != true {
|
||||
manager.logger.Warn().Msg("Authenticatetion failed")
|
||||
if err = socket.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
session := &session{
|
||||
id: sessionID,
|
||||
socket: socket,
|
||||
mu: sync.Mutex{},
|
||||
}
|
||||
|
||||
manager.logger.
|
||||
Info().
|
||||
Str("ID", sessionID).
|
||||
Str("RemoteAddr", socket.RemoteAddr().String()).
|
||||
Msg("Created Session")
|
||||
|
||||
manager.sessions[sessionID] = session
|
||||
|
||||
defer func() {
|
||||
manager.destroy(session)
|
||||
}()
|
||||
|
||||
if err = manager.onConnected(session); err != nil {
|
||||
manager.logger.Error().Err(err).Msg("onConnected failed!")
|
||||
return nil
|
||||
}
|
||||
|
||||
manager.handleWS(session)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *WebRTCManager) authenticate(r *http.Request) (sessionID string, ok bool) {
|
||||
|
||||
passwords, ok := r.URL.Query()["password"]
|
||||
if !ok || len(passwords[0]) < 1 {
|
||||
return "", false
|
||||
}
|
||||
|
||||
if passwords[0] != manager.password {
|
||||
manager.logger.Warn().Str("Password", passwords[0]).Msg("Wrong password: ")
|
||||
return "", false
|
||||
}
|
||||
|
||||
id, err := nanoid.NewIDSize(32)
|
||||
if err != nil {
|
||||
return "", false
|
||||
}
|
||||
return id, true
|
||||
}
|
||||
|
||||
func (manager *WebRTCManager) onConnected(session *session) error {
|
||||
if err := session.send(messageIdentityProvide{
|
||||
message: message{Event: "identity/provide"},
|
||||
ID: session.id,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *WebRTCManager) onMessage(session *session, raw []byte) error {
|
||||
message := message{}
|
||||
if err := json.Unmarshal(raw, &message); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch message.Event {
|
||||
case "sdp/provide":
|
||||
return errors.Wrap(manager.createPeer(session, raw), "sdp/provide failed")
|
||||
case "control/release":
|
||||
return errors.Wrap(manager.controlRelease(session), "control/release failed")
|
||||
case "control/request":
|
||||
return errors.Wrap(manager.controlRequest(session), "control/request failed")
|
||||
default:
|
||||
manager.logger.Warn().Msgf("Unknown client method %s", message.Event)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *WebRTCManager) handleWS(session *session) {
|
||||
bytes := make(chan []byte)
|
||||
cancel := make(chan struct{})
|
||||
ticker := time.NewTicker(pingPeriod)
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
ticker.Stop()
|
||||
manager.logger.Info().Str("RemoteAddr", session.socket.RemoteAddr().String()).Msg("Handle WS ending")
|
||||
manager.destroy(session)
|
||||
}()
|
||||
|
||||
for {
|
||||
_, raw, err := session.socket.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
||||
manager.logger.Warn().Err(err).Msg("ReadMessage error")
|
||||
}
|
||||
break
|
||||
}
|
||||
bytes <- raw
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case raw := <-bytes:
|
||||
manager.logger.Info().
|
||||
Str("ID", session.id).
|
||||
Str("Message", string(raw)).
|
||||
Msg("Reading from Websocket")
|
||||
if err := manager.onMessage(session, raw); err != nil {
|
||||
manager.logger.Error().Err(err).Msg("onClientMessage has failed")
|
||||
return
|
||||
}
|
||||
case <-cancel:
|
||||
return
|
||||
case _ = <-ticker.C:
|
||||
if err := session.socket.WriteMessage(websocket.PingMessage, nil); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (manager *WebRTCManager) destroy(session *session) {
|
||||
if manager.controller == session.id {
|
||||
manager.controller = ""
|
||||
manager.clearKeys()
|
||||
for id, sess := range manager.sessions {
|
||||
if id != session.id {
|
||||
if err := sess.send(message{Event: "control/released"}); err != nil {
|
||||
manager.logger.Error().Err(err).Msg("session.send has failed")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := session.destroy(); err != nil {
|
||||
manager.logger.Error().Err(err).Msg("session.destroy has failed")
|
||||
}
|
||||
|
||||
delete(manager.sessions, session.id)
|
||||
}
|
||||
|
||||
func (manager *WebRTCManager) controlRelease(session *session) error {
|
||||
if manager.controller == session.id {
|
||||
manager.controller = ""
|
||||
manager.clearKeys()
|
||||
|
||||
if err := session.send(message{Event: "control/release"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for id, sess := range manager.sessions {
|
||||
if id != session.id {
|
||||
if err := sess.send(message{Event: "control/released"}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (manager *WebRTCManager) controlRequest(session *session) error {
|
||||
if manager.controller == "" {
|
||||
manager.controller = session.id
|
||||
|
||||
if err := session.send(message{Event: "control/give"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for id, sess := range manager.sessions {
|
||||
if id != session.id {
|
||||
if err := sess.send(message{Event: "control/given"}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := session.send(message{Event: "control/locked"}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
controller, ok := manager.sessions[manager.controller]
|
||||
if ok {
|
||||
controller.send(message{Event: "control/requesting"})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
205
server/internal/websocket/handler.go
Normal file
205
server/internal/websocket/handler.go
Normal file
@ -0,0 +1,205 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
gonanoid "github.com/matoous/go-nanoid"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"n.eko.moe/neko/internal/config"
|
||||
"n.eko.moe/neko/internal/session"
|
||||
"n.eko.moe/neko/internal/webrtc"
|
||||
)
|
||||
|
||||
func New(sessions *session.SessionManager, webrtc *webrtc.WebRTCManager, conf *config.WebSocket) *WebSocketHandler {
|
||||
logger := log.With().Str("module", "websocket").Logger()
|
||||
|
||||
return &WebSocketHandler{
|
||||
logger: logger,
|
||||
conf: conf,
|
||||
sessions: sessions,
|
||||
upgrader: websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
return true
|
||||
},
|
||||
},
|
||||
handler: &MessageHandler{
|
||||
logger: logger.With().Str("subsystem", "handler").Logger(),
|
||||
sessions: sessions,
|
||||
webrtc: webrtc,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Send pings to peer with this period. Must be less than pongWait.
|
||||
const pingPeriod = 60 * time.Second
|
||||
|
||||
const alphabet = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
|
||||
type WebSocketHandler struct {
|
||||
logger zerolog.Logger
|
||||
upgrader websocket.Upgrader
|
||||
handler *MessageHandler
|
||||
conf *config.WebSocket
|
||||
sessions *session.SessionManager
|
||||
shutdown chan bool
|
||||
}
|
||||
|
||||
func (ws *WebSocketHandler) Start() error {
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
ws.logger.Info().Msg("shutdown")
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ws.shutdown:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
ws.sessions.OnCreated(func(id string, session *session.Session) {
|
||||
if err := ws.handler.Created(id, session); err != nil {
|
||||
ws.logger.Warn().Str("id", id).Err(err).Msg("session created with and error")
|
||||
} else {
|
||||
ws.logger.Debug().Str("id", id).Msg("session created")
|
||||
}
|
||||
})
|
||||
|
||||
ws.sessions.OnDestroy(func(id string) {
|
||||
if err := ws.handler.Destroyed(id); err != nil {
|
||||
ws.logger.Warn().Str("id", id).Err(err).Msg("session destroyed with and error")
|
||||
} else {
|
||||
ws.logger.Debug().Str("id", id).Msg("session destroyed")
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ws *WebSocketHandler) Shutdown() error {
|
||||
ws.shutdown <- true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ws *WebSocketHandler) Upgrade(w http.ResponseWriter, r *http.Request) error {
|
||||
ws.logger.Debug().Msg("attempting to upgrade connection")
|
||||
|
||||
socket, err := ws.upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
ws.logger.Error().Err(err).Msg("failed to upgrade connection")
|
||||
return err
|
||||
}
|
||||
|
||||
id, admin, err := ws.authenticate(r)
|
||||
if err != nil {
|
||||
ws.logger.Warn().Err(err).Msg("authenticatetion failed")
|
||||
if err = socket.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
ws.sessions.New(id, admin, socket)
|
||||
|
||||
ws.logger.
|
||||
Debug().
|
||||
Str("session", id).
|
||||
Str("address", socket.RemoteAddr().String()).
|
||||
Msg("new connection created")
|
||||
|
||||
defer func() {
|
||||
ws.logger.
|
||||
Debug().
|
||||
Str("session", id).
|
||||
Str("address", socket.RemoteAddr().String()).
|
||||
Msg("session ended")
|
||||
}()
|
||||
|
||||
if err = ws.handler.Connected(id, socket); err != nil {
|
||||
ws.logger.Error().Err(err).Msg("connection failed")
|
||||
if err = socket.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
ws.handle(socket, id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ws *WebSocketHandler) authenticate(r *http.Request) (string, bool, error) {
|
||||
id, err := gonanoid.Generate(alphabet, 32)
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
passwords, ok := r.URL.Query()["password"]
|
||||
if !ok || len(passwords[0]) < 1 {
|
||||
return "", false, fmt.Errorf("no password provided")
|
||||
}
|
||||
|
||||
if passwords[0] == ws.conf.AdminPassword {
|
||||
return id, true, nil
|
||||
}
|
||||
|
||||
if passwords[0] == ws.conf.Password {
|
||||
return id, false, nil
|
||||
}
|
||||
|
||||
return "", false, fmt.Errorf("invalid password: %s", passwords[0])
|
||||
}
|
||||
|
||||
func (ws *WebSocketHandler) handle(socket *websocket.Conn, id string) {
|
||||
bytes := make(chan []byte)
|
||||
cancel := make(chan struct{})
|
||||
ticker := time.NewTicker(pingPeriod)
|
||||
|
||||
go func() {
|
||||
defer func() {
|
||||
ticker.Stop()
|
||||
ws.logger.Debug().Str("address", socket.RemoteAddr().String()).Msg("handle socket ending")
|
||||
ws.handler.Disconnected(id)
|
||||
}()
|
||||
|
||||
for {
|
||||
_, raw, err := socket.ReadMessage()
|
||||
if err != nil {
|
||||
if websocket.IsUnexpectedCloseError(err, websocket.CloseGoingAway, websocket.CloseAbnormalClosure) {
|
||||
ws.logger.Warn().Err(err).Msg("read message error")
|
||||
} else {
|
||||
ws.logger.Debug().Err(err).Msg("read message error")
|
||||
}
|
||||
close(cancel)
|
||||
break
|
||||
}
|
||||
bytes <- raw
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case raw := <-bytes:
|
||||
ws.logger.Debug().
|
||||
Str("session", id).
|
||||
Str("raw", string(raw)).
|
||||
Msg("recieved message from client")
|
||||
if err := ws.handler.Message(id, raw); err != nil {
|
||||
ws.logger.Error().Err(err).Msg("message handler has failed")
|
||||
return
|
||||
}
|
||||
case <-cancel:
|
||||
return
|
||||
case _ = <-ticker.C:
|
||||
if err := socket.WriteMessage(websocket.PingMessage, nil); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
131
server/internal/websocket/messages.go
Normal file
131
server/internal/websocket/messages.go
Normal file
@ -0,0 +1,131 @@
|
||||
package websocket
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/rs/zerolog"
|
||||
|
||||
"n.eko.moe/neko/internal/event"
|
||||
"n.eko.moe/neko/internal/message"
|
||||
"n.eko.moe/neko/internal/session"
|
||||
"n.eko.moe/neko/internal/utils"
|
||||
"n.eko.moe/neko/internal/webrtc"
|
||||
)
|
||||
|
||||
type MessageHandler struct {
|
||||
logger zerolog.Logger
|
||||
sessions *session.SessionManager
|
||||
webrtc *webrtc.WebRTCManager
|
||||
}
|
||||
|
||||
func (h *MessageHandler) Connected(id string, socket *websocket.Conn) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandler) Disconnected(id string) error {
|
||||
return h.sessions.Destroy(id)
|
||||
}
|
||||
|
||||
func (h *MessageHandler) Created(id string, session *session.Session) error {
|
||||
if err := session.Send(message.IdentityProvide{
|
||||
Message: message.Message{Event: event.IDENTITY_PROVIDE},
|
||||
ID: id,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandler) Destroyed(id string) error {
|
||||
if h.sessions.IsHost(id) {
|
||||
h.sessions.ClearHost()
|
||||
if err := h.sessions.Brodcast(message.Message{Event: event.CONTROL_RELEASED}, []string{id}); err != nil {
|
||||
h.logger.Warn().Err(err).Msgf("brodcasting event %s has failed", event.CONTROL_RELEASED)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandler) Message(id string, raw []byte) error {
|
||||
header := message.Message{}
|
||||
if err := json.Unmarshal(raw, &header); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session, ok := h.sessions.Get(id)
|
||||
if !ok {
|
||||
errors.Errorf("unknown session id %s", id)
|
||||
}
|
||||
|
||||
switch header.Event {
|
||||
case event.SDP_PROVIDE:
|
||||
payload := message.SDP{}
|
||||
return errors.Wrapf(utils.Unmarshal(&payload, raw, func() error { return h.webrtc.CreatePeer(id, payload.SDP) }), "%s failed", header.Event)
|
||||
case event.CONTROL_RELEASE:
|
||||
return errors.Wrapf(h.controlRelease(id, session), "%s failed", header.Event)
|
||||
case event.CONTROL_REQUEST:
|
||||
return errors.Wrapf(h.controlRequest(id, session), "%s failed", header.Event)
|
||||
default:
|
||||
return errors.Errorf("unknown message event %s", header.Event)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *MessageHandler) controlRelease(id string, session *session.Session) error {
|
||||
if !h.sessions.IsHost(id) {
|
||||
return nil
|
||||
}
|
||||
|
||||
h.logger.Debug().Str("id", id).Msgf("host called %s", event.CONTROL_RELEASED)
|
||||
h.sessions.ClearHost()
|
||||
|
||||
if err := session.Send(message.Message{Event: event.CONTROL_RELEASE}); err != nil {
|
||||
h.logger.Warn().Err(err).Str("id", id).Msgf("sending event %s has failed", event.CONTROL_RELEASE)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := h.sessions.Brodcast(message.Message{Event: event.CONTROL_RELEASED}, []string{session.ID}); err != nil {
|
||||
h.logger.Warn().Err(err).Msgf("brodcasting event %s has failed", event.CONTROL_RELEASED)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *MessageHandler) controlRequest(id string, session *session.Session) error {
|
||||
h.logger.Debug().Str("id", id).Msgf("user called %s", event.CONTROL_REQUEST)
|
||||
|
||||
if !h.sessions.HasHost() {
|
||||
h.sessions.SetHost(id)
|
||||
|
||||
if err := session.Send(message.Message{Event: event.CONTROL_GIVE}); err != nil {
|
||||
h.logger.Warn().Err(err).Str("id", id).Msgf("sending event %s has failed", event.CONTROL_GIVE)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := h.sessions.Brodcast(message.Message{Event: event.CONTROL_GIVEN}, []string{session.ID}); err != nil {
|
||||
h.logger.Warn().Err(err).Msgf("brodcasting event %s has failed", event.CONTROL_GIVEN)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := session.Send(message.Message{Event: event.CONTROL_LOCKED}); err != nil {
|
||||
h.logger.Warn().Err(err).Str("id", id).Msgf("sending event %s has failed", event.CONTROL_LOCKED)
|
||||
return err
|
||||
}
|
||||
|
||||
host, ok := h.sessions.GetHost()
|
||||
if ok {
|
||||
if err := host.Send(message.Message{Event: event.CONTROL_REQUESTING}); err != nil {
|
||||
h.logger.Warn().Err(err).Str("id", id).Msgf("sending event %s has failed", event.CONTROL_REQUESTING)
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
173
server/neko.go
173
server/neko.go
@ -1,25 +1,31 @@
|
||||
package neko
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
|
||||
"n.eko.moe/neko/internal/config"
|
||||
"n.eko.moe/neko/internal/http/endpoint"
|
||||
"n.eko.moe/neko/internal/http/middleware"
|
||||
"n.eko.moe/neko/internal/structs"
|
||||
"n.eko.moe/neko/internal/http"
|
||||
"n.eko.moe/neko/internal/session"
|
||||
"n.eko.moe/neko/internal/webrtc"
|
||||
"n.eko.moe/neko/internal/websocket"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
const Header = `&34
|
||||
_ __ __
|
||||
/ | / /__ / /______ \ /\
|
||||
/ |/ / _ \/ //_/ __ \ ) ( ')
|
||||
/ /| / __/ ,< / /_/ / ( / )
|
||||
/_/ |_/\___/_/|_|\____/ \(__)|
|
||||
&1&37 nurdism/neko &33%s v%s&0
|
||||
`
|
||||
|
||||
var (
|
||||
//
|
||||
buildDate = ""
|
||||
@ -41,7 +47,7 @@ var Service *Neko
|
||||
|
||||
func init() {
|
||||
Service = &Neko{
|
||||
Version: &structs.Version{
|
||||
Version: &Version{
|
||||
Major: major,
|
||||
Minor: minor,
|
||||
Patch: patch,
|
||||
@ -53,122 +59,97 @@ func init() {
|
||||
Compiler: runtime.Compiler,
|
||||
Platform: fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH),
|
||||
},
|
||||
Root: &config.Root{},
|
||||
Serve: &config.Serve{},
|
||||
Root: &config.Root{},
|
||||
Server: &config.Server{},
|
||||
WebRTC: &config.WebRTC{},
|
||||
WebSocket: &config.WebSocket{},
|
||||
}
|
||||
}
|
||||
|
||||
type Version struct {
|
||||
Major string
|
||||
Minor string
|
||||
Patch string
|
||||
Version string
|
||||
GitVersion string
|
||||
GitCommit string
|
||||
GitTreeState string
|
||||
BuildDate string
|
||||
GoVersion string
|
||||
Compiler string
|
||||
Platform string
|
||||
}
|
||||
|
||||
func (i *Version) String() string {
|
||||
return fmt.Sprintf("%s.%s.%s", i.Major, i.Minor, i.Patch)
|
||||
}
|
||||
|
||||
type Neko struct {
|
||||
Version *structs.Version
|
||||
Root *config.Root
|
||||
Serve *config.Serve
|
||||
Logger zerolog.Logger
|
||||
http *http.Server
|
||||
manager *webrtc.WebRTCManager
|
||||
Version *Version
|
||||
Root *config.Root
|
||||
Server *config.Server
|
||||
WebRTC *config.WebRTC
|
||||
WebSocket *config.WebSocket
|
||||
|
||||
logger zerolog.Logger
|
||||
server *http.Server
|
||||
sessions *session.SessionManager
|
||||
webRTCManager *webrtc.WebRTCManager
|
||||
webSocketHandler *websocket.WebSocketHandler
|
||||
}
|
||||
|
||||
func (neko *Neko) Preflight() {
|
||||
neko.Logger = log.With().Str("service", "neko").Logger()
|
||||
neko.logger = log.With().Str("service", "neko").Logger()
|
||||
}
|
||||
|
||||
func (neko *Neko) Start() {
|
||||
router := chi.NewRouter()
|
||||
sessions := session.New()
|
||||
|
||||
manager, err := webrtc.NewManager(neko.Serve.Password)
|
||||
if err != nil {
|
||||
neko.Logger.Panic().Err(err).Msg("Can not create webrtc manager")
|
||||
}
|
||||
webRTCManager := webrtc.New(sessions, neko.WebRTC)
|
||||
webRTCManager.Start()
|
||||
|
||||
if err := manager.Start(); err != nil {
|
||||
neko.Logger.Panic().Err(err).Msg("Can not start webrtc manager")
|
||||
}
|
||||
webSocketHandler := websocket.New(sessions, webRTCManager, neko.WebSocket)
|
||||
webSocketHandler.Start()
|
||||
|
||||
router.Use(middleware.Recoverer) // Recover from panics without crashing server
|
||||
router.Use(middleware.RequestID) // Create a request ID for each request
|
||||
router.Use(middleware.Logger) // Log API request calls
|
||||
server := http.New(neko.Server, webSocketHandler)
|
||||
server.Start()
|
||||
|
||||
router.Get("/ping", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("."))
|
||||
})
|
||||
|
||||
router.Get("/ws", func(w http.ResponseWriter, r *http.Request) {
|
||||
if err := manager.Upgrade(w, r); err != nil {
|
||||
neko.Logger.Error().Err(err).Msg("session.destroy has failed")
|
||||
}
|
||||
})
|
||||
|
||||
fs := http.FileServer(http.Dir(neko.Serve.Static))
|
||||
router.Get("/*", func(w http.ResponseWriter, r *http.Request) {
|
||||
if _, err := os.Stat(neko.Serve.Static + r.RequestURI); os.IsNotExist(err) {
|
||||
http.StripPrefix(r.RequestURI, fs).ServeHTTP(w, r)
|
||||
} else {
|
||||
fs.ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
|
||||
router.NotFound(endpoint.Handle(func(w http.ResponseWriter, r *http.Request) error {
|
||||
return &endpoint.HandlerError{
|
||||
Status: http.StatusNotFound,
|
||||
Message: fmt.Sprintf("Endpoint '%s' is not avalible", r.RequestURI),
|
||||
}
|
||||
}))
|
||||
|
||||
server := &http.Server{
|
||||
Addr: neko.Serve.Bind,
|
||||
Handler: router,
|
||||
}
|
||||
|
||||
if neko.Serve.Cert != "" && neko.Serve.Key != "" {
|
||||
go func() {
|
||||
if err := server.ListenAndServeTLS(neko.Serve.Cert, neko.Serve.Key); err != http.ErrServerClosed {
|
||||
neko.Logger.Panic().Err(err).Msg("Unable to start https server")
|
||||
}
|
||||
}()
|
||||
neko.Logger.Info().Msgf("HTTPS listening on %s", server.Addr)
|
||||
} else {
|
||||
go func() {
|
||||
if err := server.ListenAndServe(); err != http.ErrServerClosed {
|
||||
neko.Logger.Panic().Err(err).Msg("Unable to start http server")
|
||||
}
|
||||
}()
|
||||
neko.Logger.Warn().Msgf("HTTP listening on %s", server.Addr)
|
||||
}
|
||||
|
||||
neko.http = server
|
||||
neko.manager = manager
|
||||
neko.sessions = sessions
|
||||
neko.webRTCManager = webRTCManager
|
||||
neko.webSocketHandler = webSocketHandler
|
||||
neko.server = server
|
||||
}
|
||||
|
||||
func (neko *Neko) Shutdown() {
|
||||
if neko.manager != nil {
|
||||
if err := neko.manager.Shutdown(); err != nil {
|
||||
neko.Logger.Err(err).Msg("WebRTC manager shutdown with an error")
|
||||
} else {
|
||||
neko.Logger.Debug().Msg("WebRTC manager shutdown")
|
||||
}
|
||||
if err := neko.webRTCManager.Shutdown(); err != nil {
|
||||
neko.logger.Err(err).Msg("webrtc manager shutdown with an error")
|
||||
} else {
|
||||
neko.logger.Debug().Msg("webrtc manager shutdown")
|
||||
}
|
||||
if neko.http != nil {
|
||||
if err := neko.http.Shutdown(context.Background()); err != nil {
|
||||
neko.Logger.Err(err).Msg("HTTP server shutdown with an error")
|
||||
} else {
|
||||
neko.Logger.Debug().Msg("HTTP server shutdown")
|
||||
}
|
||||
|
||||
if err := neko.webSocketHandler.Shutdown(); err != nil {
|
||||
neko.logger.Err(err).Msg("websocket handler shutdown with an error")
|
||||
} else {
|
||||
neko.logger.Debug().Msg("websocket handler shutdown")
|
||||
}
|
||||
|
||||
if err := neko.server.Shutdown(); err != nil {
|
||||
neko.logger.Err(err).Msg("server shutdown with an error")
|
||||
} else {
|
||||
neko.logger.Debug().Msg("server shutdown")
|
||||
}
|
||||
}
|
||||
|
||||
func (neko *Neko) ServeCommand(cmd *cobra.Command, args []string) {
|
||||
neko.Logger.Info().Msg("Starting HTTP/S server")
|
||||
neko.logger.Info().Msg("starting neko server")
|
||||
neko.Start()
|
||||
|
||||
neko.Logger.Info().Msg("Service ready")
|
||||
neko.logger.Info().Msg("neko ready")
|
||||
|
||||
quit := make(chan os.Signal)
|
||||
signal.Notify(quit, os.Interrupt)
|
||||
sig := <-quit
|
||||
|
||||
neko.Logger.Warn().Msgf("Received %s, attempting graceful shutdown: \n", sig)
|
||||
|
||||
neko.logger.Warn().Msgf("received %s, attempting graceful shutdown: \n", sig)
|
||||
neko.Shutdown()
|
||||
neko.Logger.Info().Msg("Shutting down complete")
|
||||
neko.logger.Info().Msg("shutdown complete")
|
||||
}
|
||||
|
Reference in New Issue
Block a user